feat(studio): localize catalog studio UI and polish responsive layout
Route all Catalog Studio strings through the admin.studio namespace (workspace, filters, review, inspector, quality, sync, scale32, organize imports and page titles/subtitles) with English fallback, and add shared StudioPage chrome plus a loading skeleton. Tighten mobile padding on the header and sub-bars.
This commit is contained in:
1 parent
d7d6382b60
commit
56676effc3
28 files changed
+1293
-634
No files matched your search
@@ -1,4 +1,5 @@
|
||||
"use client";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
compareFurniture,
|
||||
@@ -9,6 +10,21 @@ import { FurnitureCompleteness } from "./furniture-completeness";
|
||||
import type { FurniItem } from "./studio-types";
|
||||
import { useFurnitureInspection } from "./use-furniture-inspection";
|
||||
|
||||
const HEALTH_KEYS = {
|
||||
complete: "healthComplete",
|
||||
incomplete: "healthIncomplete",
|
||||
new: "healthNew",
|
||||
conflict: "healthConflict",
|
||||
unknown: "healthUnknown",
|
||||
} as const;
|
||||
|
||||
const FIELD_KEYS = {
|
||||
Name: "fieldName",
|
||||
Description: "fieldDescription",
|
||||
Revision: "fieldRevision",
|
||||
Type: "fieldType",
|
||||
} as const;
|
||||
|
||||
export function FurnitureComparison({
|
||||
item,
|
||||
local,
|
||||
@@ -16,69 +32,81 @@ export function FurnitureComparison({
|
||||
item: FurniItem;
|
||||
local: FurnitureInspection;
|
||||
}) {
|
||||
const t = useTranslations("admin.studio.inspector");
|
||||
const comparison = compareFurniture(item, local);
|
||||
return (
|
||||
<div className="space-y-3 text-sm">
|
||||
<FurnitureCompleteness local={local} />
|
||||
{!local.furnidataReadable && (
|
||||
<p className="text-[var(--admin-warning)]">
|
||||
Local furnidata could not be read. Its values are not verified.
|
||||
{t("furnidataUnreadable")}
|
||||
</p>
|
||||
)}
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="p-2 text-left">Field</th>
|
||||
<th className="p-2 text-left">Local now</th>
|
||||
<th className="p-2 text-left">Source</th>
|
||||
<th className="p-2 text-left">{t("field")}</th>
|
||||
<th className="p-2 text-left">{t("localNow")}</th>
|
||||
<th className="p-2 text-left">{t("source")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{comparison.fields.map((field) => (
|
||||
<tr
|
||||
key={field.label}
|
||||
className={field.changed ? "bg-[var(--admin-warning)]/10" : ""}
|
||||
>
|
||||
<th className="p-2 text-left">
|
||||
{field.label}
|
||||
{field.changed ? " · Changed" : ""}
|
||||
</th>
|
||||
<td className="max-w-48 break-words p-2">
|
||||
{field.before ?? "—"}
|
||||
</td>
|
||||
<td className="max-w-48 break-words p-2">
|
||||
{field.after ?? "Not supplied"}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{comparison.fields.map((field) => {
|
||||
const labelKey =
|
||||
FIELD_KEYS[field.label as keyof typeof FIELD_KEYS];
|
||||
return (
|
||||
<tr
|
||||
key={field.label}
|
||||
className={
|
||||
field.changed ? "bg-[var(--admin-warning)]/10" : ""
|
||||
}
|
||||
>
|
||||
<th className="p-2 text-left">
|
||||
{labelKey ? t(labelKey) : field.label}
|
||||
{field.changed ? ` · ${t("changed")}` : ""}
|
||||
</th>
|
||||
<td className="max-w-48 break-words p-2">
|
||||
{field.before ?? "—"}
|
||||
</td>
|
||||
<td className="max-w-48 break-words p-2">
|
||||
{field.after ?? t("notSupplied")}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p className="text-xs text-[var(--admin-text-muted)]">
|
||||
Source values are compared with local data. Import may remap IDs and
|
||||
translate names. Existing furniture keeps its local IDs and SQL
|
||||
settings; new furniture receives a free local ID when the source ID is
|
||||
occupied.
|
||||
{t("compareNote")}
|
||||
</p>
|
||||
<details>
|
||||
<summary className="cursor-pointer font-medium">
|
||||
SQL records and catalog offers
|
||||
{t("sqlRecords")}
|
||||
</summary>
|
||||
<div className="mt-2 space-y-2 text-xs">
|
||||
{local.sql.length ? (
|
||||
local.sql.map((row) => (
|
||||
<p key={row.id}>
|
||||
Item #{row.id} · Sprite #{row.spriteId} · {row.name}
|
||||
{t("itemRow", {
|
||||
id: row.id,
|
||||
sprite: row.spriteId,
|
||||
name: row.name,
|
||||
})}
|
||||
</p>
|
||||
))
|
||||
) : (
|
||||
<p>No local SQL item.</p>
|
||||
<p>{t("noSqlItem")}</p>
|
||||
)}
|
||||
{local.catalog.map((row) => (
|
||||
<p key={row.id}>
|
||||
Offer #{row.id} · Page #{row.pageId} · {row.credits} credits +{" "}
|
||||
{row.points} points
|
||||
{t("offerRow", {
|
||||
id: row.id,
|
||||
page: row.pageId,
|
||||
credits: row.credits,
|
||||
points: row.points,
|
||||
})}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
@@ -95,11 +123,12 @@ export function FurnitureInspector({
|
||||
onRepair?: () => Promise<unknown>;
|
||||
busy?: boolean;
|
||||
}) {
|
||||
const t = useTranslations("admin.studio.inspector");
|
||||
const inspection = useFurnitureInspection([item.classname]);
|
||||
if (inspection.loading)
|
||||
return (
|
||||
<p role="status" className="py-3 text-sm">
|
||||
Checking local data and assets…
|
||||
{t("checking")}
|
||||
</p>
|
||||
);
|
||||
if (inspection.error)
|
||||
@@ -107,7 +136,7 @@ export function FurnitureInspector({
|
||||
<div role="alert">
|
||||
<p>{inspection.error}</p>
|
||||
<Button variant="outline" onClick={inspection.retry}>
|
||||
Retry inspection
|
||||
{t("retry")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
@@ -118,51 +147,35 @@ export function FurnitureInspector({
|
||||
return local && health ? (
|
||||
<div className="space-y-3">
|
||||
<section className="rounded-md border border-[var(--admin-border)] p-3 space-y-2">
|
||||
<strong>
|
||||
{
|
||||
{
|
||||
complete: "Complete",
|
||||
incomplete: "Needs repair",
|
||||
new: "Not imported",
|
||||
conflict: "Local conflict",
|
||||
unknown: "Not verified",
|
||||
}[health.state]
|
||||
}
|
||||
</strong>
|
||||
<strong>{t(HEALTH_KEYS[health.state])}</strong>
|
||||
{health.missing.length > 0 && (
|
||||
<p className="text-sm">Missing: {health.missing.join(", ")}</p>
|
||||
<p className="text-sm">
|
||||
{t("missing", { parts: health.missing.join(", ") })}
|
||||
</p>
|
||||
)}
|
||||
{health.state === "conflict" && (
|
||||
<p className="text-sm">
|
||||
Duplicate records or mismatched IDs/types require review before
|
||||
repair.
|
||||
</p>
|
||||
<p className="text-sm">{t("conflictNote")}</p>
|
||||
)}
|
||||
{health.state === "unknown" && (
|
||||
<p className="text-sm">
|
||||
Some local data could not be checked. Retry verification before
|
||||
repair.
|
||||
</p>
|
||||
<p className="text-sm">{t("unknownNote")}</p>
|
||||
)}
|
||||
{health.state === "incomplete" && onRepair && (
|
||||
<>
|
||||
<p className="text-sm text-[var(--admin-text-muted)]">
|
||||
Repair adds missing parts and preserves existing IDs, settings,
|
||||
furnidata and catalog offers. Missing offers use the automatic
|
||||
category and price shown below.
|
||||
{t("repairNote")}
|
||||
</p>
|
||||
<Button disabled={busy} onClick={() => void onRepair()}>
|
||||
{busy ? "Repair queued or running…" : "Repair furni"}
|
||||
{busy ? t("repairQueued") : t("repair")}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<Button variant="outline" disabled={busy} onClick={inspection.retry}>
|
||||
Recheck local state
|
||||
{t("recheck")}
|
||||
</Button>
|
||||
</section>
|
||||
<FurnitureComparison item={item} local={local} />
|
||||
</div>
|
||||
) : (
|
||||
<p>Local inspection unavailable.</p>
|
||||
<p>{t("unavailable")}</p>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user