diff --git a/v5/messages/ar.json b/v5/messages/ar.json index d170708..16c21e5 100644 --- a/v5/messages/ar.json +++ b/v5/messages/ar.json @@ -21,6 +21,8 @@ "searchAria": "البحث في المخزون", "category": "الفئة:", "training": "التدريب:", + "materials": "المواد:", + "location": "الموقع:", "viewModeLabel": "وضع العرض", "grid": "[ شبكة ]", "table": "[ جدول ]", diff --git a/v5/messages/en.json b/v5/messages/en.json index ff3be98..5f359df 100644 --- a/v5/messages/en.json +++ b/v5/messages/en.json @@ -21,6 +21,8 @@ "searchAria": "Search inventory", "category": "CATEGORY:", "training": "TRAINING:", + "materials": "MATERIALS:", + "location": "LOCATION:", "viewModeLabel": "View mode", "grid": "[ GRID ]", "table": "[ TABLE ]", diff --git a/v5/messages/es.json b/v5/messages/es.json index 3171a72..f9a7d58 100644 --- a/v5/messages/es.json +++ b/v5/messages/es.json @@ -21,6 +21,8 @@ "searchAria": "Buscar en el inventario", "category": "CATEGORÍA:", "training": "FORMACIÓN:", + "materials": "MATERIALES:", + "location": "UBICACIÓN:", "viewModeLabel": "Modo de vista", "grid": "[ CUADRÍCULA ]", "table": "[ TABLA ]", diff --git a/v5/messages/fr.json b/v5/messages/fr.json index 669b760..2568804 100644 --- a/v5/messages/fr.json +++ b/v5/messages/fr.json @@ -21,6 +21,8 @@ "searchAria": "Rechercher dans l'inventaire", "category": "CATÉGORIE :", "training": "FORMATION :", + "materials": "MATÉRIAUX :", + "location": "EMPLACEMENT :", "viewModeLabel": "Mode d'affichage", "grid": "[ GRILLE ]", "table": "[ TABLEAU ]", diff --git a/v5/messages/he.json b/v5/messages/he.json index 93198cb..0befa99 100644 --- a/v5/messages/he.json +++ b/v5/messages/he.json @@ -21,6 +21,8 @@ "searchAria": "חיפוש במלאי", "category": "קטגוריה:", "training": "הכשרה:", + "materials": "חומרים:", + "location": "מיקום:", "viewModeLabel": "מצב תצוגה", "grid": "[ רשת ]", "table": "[ טבלה ]", diff --git a/v5/messages/hi.json b/v5/messages/hi.json index 8a947bd..bb6a007 100644 --- a/v5/messages/hi.json +++ b/v5/messages/hi.json @@ -21,6 +21,8 @@ "searchAria": "इन्वेंटरी खोजें", "category": "श्रेणी:", "training": "प्रशिक्षण:", + "materials": "सामग्री:", + "location": "स्थान:", "viewModeLabel": "व्यू मोड", "grid": "[ ग्रिड ]", "table": "[ तालिका ]", diff --git a/v5/messages/ja.json b/v5/messages/ja.json index 3c0c0a7..38308e2 100644 --- a/v5/messages/ja.json +++ b/v5/messages/ja.json @@ -21,6 +21,8 @@ "searchAria": "在庫を検索", "category": "カテゴリ:", "training": "トレーニング:", + "materials": "素材:", + "location": "場所:", "viewModeLabel": "表示モード", "grid": "[ グリッド ]", "table": "[ テーブル ]", diff --git a/v5/messages/ko.json b/v5/messages/ko.json index 630567a..1950d6a 100644 --- a/v5/messages/ko.json +++ b/v5/messages/ko.json @@ -21,6 +21,8 @@ "searchAria": "재고 검색", "category": "카테고리:", "training": "교육:", + "materials": "재료:", + "location": "위치:", "viewModeLabel": "보기 모드", "grid": "[ 그리드 ]", "table": "[ 테이블 ]", diff --git a/v5/messages/pt-BR.json b/v5/messages/pt-BR.json index 8ad29ac..42be387 100644 --- a/v5/messages/pt-BR.json +++ b/v5/messages/pt-BR.json @@ -21,6 +21,8 @@ "searchAria": "Buscar no inventário", "category": "CATEGORIA:", "training": "TREINAMENTO:", + "materials": "MATERIAIS:", + "location": "LOCALIZAÇÃO:", "viewModeLabel": "Modo de visualização", "grid": "[ GRADE ]", "table": "[ TABELA ]", diff --git a/v5/messages/ru.json b/v5/messages/ru.json index 73fc270..38e4721 100644 --- a/v5/messages/ru.json +++ b/v5/messages/ru.json @@ -21,6 +21,8 @@ "searchAria": "Поиск по инвентарю", "category": "КАТЕГОРИЯ:", "training": "ОБУЧЕНИЕ:", + "materials": "МАТЕРИАЛЫ:", + "location": "РАСПОЛОЖЕНИЕ:", "viewModeLabel": "Режим просмотра", "grid": "[ СЕТКА ]", "table": "[ ТАБЛИЦА ]", diff --git a/v5/messages/tr.json b/v5/messages/tr.json index 1749086..633b91e 100644 --- a/v5/messages/tr.json +++ b/v5/messages/tr.json @@ -21,6 +21,8 @@ "searchAria": "Envanterde ara", "category": "KATEGORİ:", "training": "EĞİTİM:", + "materials": "MALZEMELER:", + "location": "KONUM:", "viewModeLabel": "Görünüm modu", "grid": "[ IZGARA ]", "table": "[ TABLO ]", diff --git a/v5/messages/zh-CN.json b/v5/messages/zh-CN.json index 53bbcc0..0a8768b 100644 --- a/v5/messages/zh-CN.json +++ b/v5/messages/zh-CN.json @@ -21,6 +21,8 @@ "searchAria": "搜索库存", "category": "类别:", "training": "培训:", + "materials": "材料:", + "location": "位置:", "viewModeLabel": "视图模式", "grid": "[ 网格 ]", "table": "[ 表格 ]", diff --git a/v5/package-lock.json b/v5/package-lock.json index ebd20d5..c5d395a 100644 --- a/v5/package-lock.json +++ b/v5/package-lock.json @@ -11,6 +11,7 @@ "@ai-sdk/anthropic": "^3.0.46", "@ai-sdk/react": "^3.0.97", "ai": "^6.0.95", + "match-sorter": "^8.3.0", "next": "16.1.6", "next-intl": "^4.13.0", "react": "19.2.3", @@ -315,6 +316,15 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", @@ -5893,6 +5903,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/match-sorter": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-8.3.0.tgz", + "integrity": "sha512-8Py1GbZi5zsclYSFcPAH4H5xfTbeD0bOREA7qP/t8bW4MbOSlPl8sbqHOedEV7O+Bxyvxm6xs/v6BXJGe+JDNA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.8", + "remove-accents": "0.5.0" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -7591,6 +7611,12 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remove-accents": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz", + "integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==", + "license": "MIT" + }, "node_modules/resolve": { "version": "2.0.0-next.6", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz", diff --git a/v5/package.json b/v5/package.json index 486f936..86c7f6d 100644 --- a/v5/package.json +++ b/v5/package.json @@ -16,6 +16,7 @@ "@ai-sdk/anthropic": "^3.0.46", "@ai-sdk/react": "^3.0.97", "ai": "^6.0.95", + "match-sorter": "^8.3.0", "next": "16.1.6", "next-intl": "^4.13.0", "react": "19.2.3", diff --git a/v5/src/components/GalleryShell.tsx b/v5/src/components/GalleryShell.tsx index 42bf02b..1aba9ad 100644 --- a/v5/src/components/GalleryShell.tsx +++ b/v5/src/components/GalleryShell.tsx @@ -3,6 +3,7 @@ import Image from "next/image"; import { useMemo, useState } from "react"; import { useTranslations } from "next-intl"; +import { matchSorter } from "match-sorter"; import type { MakerLabTool } from "./catalog-types"; import { TechnicalFrame } from "./TechnicalFrame"; import { ToolCard } from "./ToolCard"; @@ -17,11 +18,30 @@ const TRAINING_LEVELS = [ { value: "Advanced", key: "trainingAdvanced" }, ] as const; +// Ranked, typo-tolerant search keys. match-sorter ranks earlier keys above +// later ones when match quality ties, so key order doubles as relevance +// weight: name first, then the structured metadata (category / tags / +// materials), then the free-text description last. +const SEARCH_KEYS: ReadonlyArray = [ + "name", + "category", + "categorySub", + "tags", + "materials", + "location", + "zone", + "ppe", + "trainingLevel", + "description", +]; + export function GalleryShell({ tools }: GalleryShellProps) { const t = useTranslations("gallery"); const [query, setQuery] = useState(""); const [category, setCategory] = useState(null); const [training, setTraining] = useState(null); + const [material, setMaterial] = useState(null); + const [location, setLocation] = useState(null); const [viewMode, setViewMode] = useState<"grid" | "table">("grid"); const categories = useMemo( @@ -29,32 +49,39 @@ export function GalleryShell({ tools }: GalleryShellProps) { [tools] ); + // Facet options are derived from the loaded catalog, mirroring `categories`. + const materials = useMemo( + () => Array.from(new Set(tools.flatMap((tool) => tool.materials))).sort(), + [tools] + ); + + const locations = useMemo( + () => Array.from(new Set(tools.map((tool) => tool.location).filter(Boolean))).sort(), + [tools] + ); + const filteredTools = useMemo(() => { - const normalizedQuery = query.trim().toLowerCase(); - - return tools.filter((tool) => { - const searchable = [ - tool.name, - tool.category, - tool.categorySub, - tool.location, - tool.zone, - tool.trainingLevel, - tool.description, - ...tool.ppe, - ...tool.materials, - ...tool.tags, - ] - .join(" ") - .toLowerCase(); - - const matchesQuery = !normalizedQuery || searchable.includes(normalizedQuery); + // Apply facet filters first; each dimension is single-select and they + // combine with AND across dimensions. + const faceted = tools.filter((tool) => { const matchesCategory = !category || tool.category === category; const matchesTraining = !training || tool.trainingLevel === training; + const matchesMaterial = !material || tool.materials.includes(material); + const matchesLocation = !location || tool.location === location; - return matchesQuery && matchesCategory && matchesTraining; + return matchesCategory && matchesTraining && matchesMaterial && matchesLocation; }); - }, [category, query, tools, training]); + + const normalizedQuery = query.trim(); + + // Empty query preserves the catalog's existing order (show all). + if (!normalizedQuery) { + return faceted; + } + + // Fuzzy, ranked match across the weighted keys (typo tolerant). + return matchSorter(faceted, normalizedQuery, { keys: SEARCH_KEYS.slice() }); + }, [category, location, material, query, tools, training]); return (
@@ -116,6 +143,44 @@ export function GalleryShell({ tools }: GalleryShellProps) { +
+ {t("materials")} +
+ {materials.map((materialName) => ( + + ))} +
+
+ +
+ {t("location")} +
+ {locations.map((locationName) => ( + + ))} +
+
+