refactor(catalog): separate Studio data hooks and presentation
CI / check (push) Successful in 1m40s
CI / deploy (push) Successful in 1m26s
CI / e2e (push) Successful in 24s

This commit is contained in:
Simo committed 2026-09-05 18:15:29 +02:00
1 parent 570f3dde44
commit 3ae4f21217
9 files changed
+348 -263

No files matched your search

+29 -118
View File
@@ -25,10 +25,7 @@ import {
} from "lucide-react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { toast } from "sonner";
import {
deleteImportedFurni,
setFurnidataTranslateEnabled,
} from "@/actions/import-furni";
import { deleteImportedFurni } from "@/actions/import-furni";
import { NitroEditorDialog } from "@/app/admin/import/furni/nitro-editor-dialog";
import { useConfirmDialog } from "@/components/admin/confirm-dialog";
import { Badge } from "@/components/ui/badge";
@@ -68,8 +65,9 @@ import { BatchProgress } from "./batch-progress";
import { CatalogRail } from "./catalog-rail";
import { CheckboxDot } from "./checkbox-dot";
import { FurnitureInspector } from "./furniture-inspector";
import { FurnitureJobHistory, useFurnitureJobs } from "./furniture-jobs";
import { FurnitureJobHistory } from "./furniture-jobs";
import { ImportReview } from "./import-review";
import { filterFurniture } from "./studio-filters";
import {
getFurniImageUrl,
handleImgError,
@@ -82,25 +80,8 @@ import type {
Meta,
VerificationSummary,
} from "./studio-types";
// Language codes that get a FurnitureData_<lang>.json file. Mirrors
// FURNIDATA_LANGUAGES in furni-data-i18n (kept client-safe here so we don't pull
// server-only fs modules into the browser bundle).
const TRANSLATE_LANGS = [
"nl",
"en",
"de",
"fr",
"es",
"tr",
"it",
"pt",
"fi",
"pl",
"ru",
"ar",
"ja",
] as const;
import { useFurnitureJobs } from "./use-furniture-jobs";
import { useStudioTranslations } from "./use-studio-translations";
export function StudioClient({
source,
@@ -206,106 +187,36 @@ export function StudioClient({
new Set(),
);
// Translation controls (per-import toggle + language selection, plus a global
// default that persists to the site setting and seeds the per-import toggle).
const ALL_LANGS = TRANSLATE_LANGS as readonly string[];
const [translate, setTranslate] = useState<boolean>(defaultTranslate);
const [translateLangs, setTranslateLangs] = useState<string[]>([
...ALL_LANGS,
]);
const [langPickerOpen, setLangPickerOpen] = useState(false);
const [buildingLanguages, setBuildingLanguages] = useState(false);
const [globalTranslate, setGlobalTranslate] =
useState<boolean>(defaultTranslate);
// Build every language for the whole catalog at once (used to translate
// everything after importing with translation disabled for speed).
const buildAllLanguages = async () => {
setBuildingLanguages(true);
try {
const res = await adminFetch(
"/api/admin/import/furni?action=build-languages",
);
const data = (await res.json()) as {
languages?: Array<{ lang: string; ok: boolean }>;
error?: string;
};
if (res.ok) {
const langs = data.languages ?? [];
const okCount = langs.filter((l) => l.ok).length;
toast.success(
`Languages translated (${okCount}/${langs.length} completed)`,
);
} else {
toast.error(data.error ?? "Translation failed");
}
} catch {
toast.error("Network error while translating languages");
} finally {
setBuildingLanguages(false);
}
};
const toggleLang = (lang: string) =>
setTranslateLangs((prev) =>
prev.includes(lang) ? prev.filter((l) => l !== lang) : [...prev, lang],
);
const onGlobalTranslateChange = async (next: boolean) => {
setGlobalTranslate(next);
setTranslate(next);
try {
await setFurnidataTranslateEnabled({ enabled: next });
} catch {
toast.error("Could not save the default translation setting");
}
};
const {
ALL_LANGS,
translate,
setTranslate,
translateLangs,
setTranslateLangs,
langPickerOpen,
setLangPickerOpen,
buildingLanguages,
globalTranslate,
buildAllLanguages,
toggleLang,
onGlobalTranslateChange,
} = useStudioTranslations(defaultTranslate);
const categories = useMemo(() => {
const cats = new Set(items.map((i) => i.category).filter(Boolean));
return ["all", ...Array.from(cats).sort()];
}, [items]);
const filteredItems = useMemo(() => {
let result = items;
if (typeFilter !== "all") {
result = result.filter((i) => i.type === typeFilter);
}
if (categoryFilter !== "all") {
result = result.filter((i) => i.category === categoryFilter);
}
if (statusFilter === "imported")
result = result.filter((i) => i.alreadyImported);
else if (statusFilter === "inCatalog")
result = result.filter((i) => i.alreadyImported && i.nitroExists);
else if (statusFilter === "missingNitro")
result = result.filter((i) => i.alreadyImported && !i.nitroExists);
else if (statusFilter === "missingFurnidata")
result = result.filter(
(i) => i.alreadyImported && i.hasFurnidata === false,
);
else if (statusFilter === "missingCatalog")
result = result.filter(
(i) => i.alreadyImported && i.hasCatalogEntry === false,
);
else if (statusFilter === "notImported")
result = result.filter((i) => !i.alreadyImported);
const sorted = [...result];
if (sortBy === "name") sorted.sort((a, b) => a.name.localeCompare(b.name));
else if (sortBy === "classname")
sorted.sort((a, b) => a.classname.localeCompare(b.classname));
else if (sortBy === "category")
sorted.sort((a, b) => (a.category || "").localeCompare(b.category || ""));
else if (sortBy === "imported-first")
sorted.sort(
(a, b) => (b.alreadyImported ? 1 : 0) - (a.alreadyImported ? 1 : 0),
);
else if (sortBy === "not-imported-first")
sorted.sort(
(a, b) => (a.alreadyImported ? 1 : 0) - (b.alreadyImported ? 1 : 0),
);
return sorted;
}, [items, typeFilter, categoryFilter, statusFilter, sortBy]);
const filteredItems = useMemo(
() =>
filterFurniture(items, {
typeFilter,
categoryFilter,
statusFilter,
sortBy,
}),
[items, typeFilter, categoryFilter, statusFilter, sortBy],
);
const allSelected = useMemo(
() =>