Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions v5/messages/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
"searchAria": "البحث في المخزون",
"category": "الفئة:",
"training": "التدريب:",
"materials": "المواد:",
"location": "الموقع:",
"viewModeLabel": "وضع العرض",
"grid": "[ شبكة ]",
"table": "[ جدول ]",
Expand Down
2 changes: 2 additions & 0 deletions v5/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
"searchAria": "Search inventory",
"category": "CATEGORY:",
"training": "TRAINING:",
"materials": "MATERIALS:",
"location": "LOCATION:",
"viewModeLabel": "View mode",
"grid": "[ GRID ]",
"table": "[ TABLE ]",
Expand Down
2 changes: 2 additions & 0 deletions v5/messages/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 ]",
Expand Down
2 changes: 2 additions & 0 deletions v5/messages/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 ]",
Expand Down
2 changes: 2 additions & 0 deletions v5/messages/he.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
"searchAria": "חיפוש במלאי",
"category": "קטגוריה:",
"training": "הכשרה:",
"materials": "חומרים:",
"location": "מיקום:",
"viewModeLabel": "מצב תצוגה",
"grid": "[ רשת ]",
"table": "[ טבלה ]",
Expand Down
2 changes: 2 additions & 0 deletions v5/messages/hi.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
"searchAria": "इन्वेंटरी खोजें",
"category": "श्रेणी:",
"training": "प्रशिक्षण:",
"materials": "सामग्री:",
"location": "स्थान:",
"viewModeLabel": "व्यू मोड",
"grid": "[ ग्रिड ]",
"table": "[ तालिका ]",
Expand Down
2 changes: 2 additions & 0 deletions v5/messages/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
"searchAria": "在庫を検索",
"category": "カテゴリ:",
"training": "トレーニング:",
"materials": "素材:",
"location": "場所:",
"viewModeLabel": "表示モード",
"grid": "[ グリッド ]",
"table": "[ テーブル ]",
Expand Down
2 changes: 2 additions & 0 deletions v5/messages/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
"searchAria": "재고 검색",
"category": "카테고리:",
"training": "교육:",
"materials": "재료:",
"location": "위치:",
"viewModeLabel": "보기 모드",
"grid": "[ 그리드 ]",
"table": "[ 테이블 ]",
Expand Down
2 changes: 2 additions & 0 deletions v5/messages/pt-BR.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 ]",
Expand Down
2 changes: 2 additions & 0 deletions v5/messages/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
"searchAria": "Поиск по инвентарю",
"category": "КАТЕГОРИЯ:",
"training": "ОБУЧЕНИЕ:",
"materials": "МАТЕРИАЛЫ:",
"location": "РАСПОЛОЖЕНИЕ:",
"viewModeLabel": "Режим просмотра",
"grid": "[ СЕТКА ]",
"table": "[ ТАБЛИЦА ]",
Expand Down
2 changes: 2 additions & 0 deletions v5/messages/tr.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 ]",
Expand Down
2 changes: 2 additions & 0 deletions v5/messages/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
"searchAria": "搜索库存",
"category": "类别:",
"training": "培训:",
"materials": "材料:",
"location": "位置:",
"viewModeLabel": "视图模式",
"grid": "[ 网格 ]",
"table": "[ 表格 ]",
Expand Down
26 changes: 26 additions & 0 deletions v5/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions v5/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
107 changes: 86 additions & 21 deletions v5/src/components/GalleryShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -17,44 +18,70 @@ 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<keyof MakerLabTool> = [
"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<string | null>(null);
const [training, setTraining] = useState<string | null>(null);
const [material, setMaterial] = useState<string | null>(null);
const [location, setLocation] = useState<string | null>(null);
const [viewMode, setViewMode] = useState<"grid" | "table">("grid");

const categories = useMemo(
() => Array.from(new Set(tools.map((tool) => tool.category))).sort(),
[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 (
<main className="page-shell">
Expand Down Expand Up @@ -116,6 +143,44 @@ export function GalleryShell({ tools }: GalleryShellProps) {
</div>
</div>

<div className="filter-group">
<span>{t("materials")}</span>
<div className="chip-row">
{materials.map((materialName) => (
<button
className={material === materialName ? "chip chip-active" : "chip"}
key={materialName}
type="button"
aria-pressed={material === materialName}
onClick={() =>
setMaterial((selected) => (selected === materialName ? null : materialName))
}
>
{materialName}
</button>
))}
</div>
</div>

<div className="filter-group">
<span>{t("location")}</span>
<div className="chip-row">
{locations.map((locationName) => (
<button
className={location === locationName ? "chip chip-active" : "chip"}
key={locationName}
type="button"
aria-pressed={location === locationName}
onClick={() =>
setLocation((selected) => (selected === locationName ? null : locationName))
}
>
{locationName}
</button>
))}
</div>
</div>

<div className="view-toggle" aria-label={t("viewModeLabel")}>
<button
className={viewMode === "grid" ? "is-active" : ""}
Expand Down