diff --git a/client/public/locales/en/common.json b/client/public/locales/en/common.json index cd42931..22c28ce 100644 --- a/client/public/locales/en/common.json +++ b/client/public/locales/en/common.json @@ -362,6 +362,14 @@ }, "buttons": { "add_spool": "Add Spool" + }, + "color_lookup": { + "button": "Find color from filamentcolors.xyz", + "title": "Find Color - filamentcolors.xyz", + "manufacturer": "Manufacturer", + "color_search": "Search color name...", + "results": "results", + "showing_page": "page {{page}}" } }, "vendor": { diff --git a/client/src/components/filamentColorLookup.tsx b/client/src/components/filamentColorLookup.tsx new file mode 100644 index 0000000..69fd086 --- /dev/null +++ b/client/src/components/filamentColorLookup.tsx @@ -0,0 +1,166 @@ +import { SearchOutlined } from "@ant-design/icons"; +import { useTranslate } from "@refinedev/core"; +import { useQuery } from "@tanstack/react-query"; +import { Button, Input, Modal, Select, Space, Spin, Tooltip } from "antd"; +import { useState } from "react"; +import { getAPIURL } from "../utils/url"; + +interface Swatch { + id: number; + color_name: string; + hex_color: string | null; + manufacturer_name: string; + filament_type: string; + image_front: string | null; +} + +interface Manufacturer { + id: number; + name: string; +} + +interface FilamentColorLookupProps { + onSelect: (hexColor: string) => void; +} + +export function FilamentColorLookup({ onSelect }: FilamentColorLookupProps) { + const t = useTranslate(); + const [open, setOpen] = useState(false); + const [manufacturer, setManufacturer] = useState(); + const [colorSearch, setColorSearch] = useState(""); + const [page, setPage] = useState(1); + + const { data: manufacturers, isLoading: mfrsLoading } = useQuery({ + queryKey: ["filamentcolors", "manufacturers"], + queryFn: async () => { + const res = await fetch(`${getAPIURL()}/filamentcolors/manufacturers`); + return res.json(); + }, + enabled: open, + }); + + const { data: searchResult, isLoading: searchLoading } = useQuery<{ count: number; swatches: Swatch[] }>({ + queryKey: ["filamentcolors", "search", manufacturer, colorSearch, page], + queryFn: async () => { + const params = new URLSearchParams(); + if (manufacturer) params.set("manufacturer", manufacturer); + if (colorSearch) params.set("color", colorSearch); + params.set("limit", "50"); + params.set("page", String(page)); + const res = await fetch(`${getAPIURL()}/filamentcolors/search?${params}`); + return res.json(); + }, + enabled: open, + }); + + const handleSelect = (hex: string) => { + onSelect(hex); + setOpen(false); + }; + + return ( + <> + + + + {page} / {Math.ceil(searchResult.count / 50)} + + + + )} + + )} + + + ); +} diff --git a/client/src/pages/filaments/create.tsx b/client/src/pages/filaments/create.tsx index 599c527..dc0188f 100644 --- a/client/src/pages/filaments/create.tsx +++ b/client/src/pages/filaments/create.tsx @@ -10,6 +10,7 @@ import { ExtraFieldFormItem, ParsedExtras, StringifiedExtras } from "../../compo import { FilamentImportModal } from "../../components/filamentImportModal"; import { VendorCreateModal } from "../../components/vendorCreateModal"; import { MultiColorPicker } from "../../components/multiColorPicker"; +import { FilamentColorLookup } from "../../components/filamentColorLookup"; import { DEFAULT_DENSITY, DEFAULT_DIAMETER, @@ -248,19 +249,22 @@ export const FilamentCreate: React.FC {colorType == "single" && ( - { - return e?.toHex(); - }} - > - - +
+ { + return e?.toHex(); + }} + > + + + form.setFieldValue("color_hex", hex)} /> +
)} {colorType == "multi" && ( = () => { {colorType == "single" && ( - { - return e?.toHex(); - }} - > - - +
+ { + return e?.toHex(); + }} + > + + + form.setFieldValue("color_hex", hex)} /> +
)} {colorType == "multi" && ( list[FilamentColorManufacturer]: + """Proxy manufacturer list from filamentcolors.xyz.""" + async with httpx.AsyncClient(timeout=10) as client: + resp = await client.get(f"{FILAMENTCOLORS_API}/manufacturer/", params={"format": "json", "limit": 500}) + resp.raise_for_status() + data = resp.json() + + return [ + FilamentColorManufacturer(id=m["id"], name=m["name"]) + for m in data.get("results", []) + ] + + +@router.get( + "/filamentcolors/search", + name="Search filamentcolors.xyz swatches", + description="Search color swatches from filamentcolors.xyz by manufacturer and/or color name.", + response_model=FilamentColorSearchResult, +) +async def search_filamentcolors( + *, + manufacturer: Annotated[ + Optional[str], + Query(description="Manufacturer name to filter by."), + ] = None, + color: Annotated[ + Optional[str], + Query(description="Color name to search for."), + ] = None, + filament_type: Annotated[ + Optional[str], + Query(description="Filament type to filter by (e.g., PLA, PETG)."), + ] = None, + limit: Annotated[int, Query(ge=1, le=100)] = 50, + page: Annotated[int, Query(ge=1)] = 1, +) -> FilamentColorSearchResult: + """Proxy swatch search from filamentcolors.xyz.""" + params: dict = {"format": "json", "limit": limit, "page": page} + if manufacturer: + params["manufacturer__name"] = manufacturer + if color: + params["color_name__icontains"] = color + if filament_type: + params["filament_type__name__icontains"] = filament_type + + async with httpx.AsyncClient(timeout=10) as client: + resp = await client.get(f"{FILAMENTCOLORS_API}/swatch/", params=params) + resp.raise_for_status() + data = resp.json() + + swatches = [ + FilamentColorSwatch( + id=s["id"], + color_name=s.get("color_name", ""), + hex_color=s.get("hex_color"), + manufacturer_name=s.get("manufacturer", {}).get("name", ""), + filament_type=s.get("filament_type", {}).get("name", ""), + image_front=s.get("image_front"), + ) + for s in data.get("results", []) + ] + + return FilamentColorSearchResult(count=data.get("count", 0), swatches=swatches)