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
@@ -41,6 +41,7 @@ export function ImportReview({
|
||||
onConfirm: (items: FurniItem[]) => void;
|
||||
}) {
|
||||
const preflightT = useTranslations("admin.studio.preflight");
|
||||
const reviewT = useTranslations("admin.studio.review");
|
||||
const [reviewFilter, setReviewFilter] = useState<
|
||||
"all" | "complete" | "conflict" | "verify"
|
||||
>("all");
|
||||
@@ -59,14 +60,16 @@ export function ImportReview({
|
||||
body: form,
|
||||
});
|
||||
const data = await response.json();
|
||||
if (!response.ok) throw Error(data.error || "Upload failed");
|
||||
if (!response.ok) throw Error(data.error || reviewT("uploadFailed"));
|
||||
setAttachments((current) => ({
|
||||
...current,
|
||||
[item.classname]: data.attachmentId,
|
||||
}));
|
||||
toast.success(diagnosticsT("attached"));
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : "Upload failed");
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : reviewT("uploadFailed"),
|
||||
);
|
||||
} finally {
|
||||
setUploading(null);
|
||||
}
|
||||
@@ -134,46 +137,44 @@ export function ImportReview({
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{step === "review"
|
||||
? "Review furniture import"
|
||||
: "Confirm furniture import"}
|
||||
? reviewT("titleReview")
|
||||
: reviewT("titleConfirm")}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
1. Select → 2. Review → 3. Import
|
||||
</DialogDescription>
|
||||
<DialogDescription>{reviewT("steps")}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="min-h-0 flex-1 space-y-4 overflow-y-auto">
|
||||
<div className="rounded-lg bg-[var(--admin-canvas)] p-3 text-sm">
|
||||
<p>
|
||||
<strong>Source:</strong> {source}
|
||||
<strong>{reviewT("source")}</strong> {source}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Destination:</strong> local catalog, furnidata, bundle and
|
||||
icons
|
||||
<strong>{reviewT("destination")}</strong>{" "}
|
||||
{reviewT("destinationValue")}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Translation:</strong> {translation}
|
||||
<strong>{reviewT("translation")}</strong> {translation}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Selection:</strong> {items.length} furniture items
|
||||
<strong>{reviewT("selection")}</strong>{" "}
|
||||
{reviewT("selectionCount", { count: items.length })}
|
||||
</p>
|
||||
</div>
|
||||
{assets.loading && !inspection.loading && (
|
||||
<p role="status">
|
||||
Checking source files… {assets.items.length} checked
|
||||
{reviewT("checkingSource", { count: assets.items.length })}
|
||||
</p>
|
||||
)}
|
||||
{assets.error && (
|
||||
<div role="alert">
|
||||
<p>{assets.error}</p>
|
||||
<Button variant="outline" onClick={assets.retry}>
|
||||
Retry source check
|
||||
{reviewT("retrySource")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{assets.items.some((item) => item.state === "unknown") && (
|
||||
<p className="text-sm text-[var(--admin-warning)]">
|
||||
Some sources could not be verified. The import will still try to
|
||||
download their files.
|
||||
{reviewT("unknownSources")}
|
||||
</p>
|
||||
)}
|
||||
{unavailable.map((check) => (
|
||||
@@ -182,7 +183,7 @@ export function ImportReview({
|
||||
className="rounded-lg border border-[var(--admin-border)] p-3 space-y-2"
|
||||
>
|
||||
<p className="text-sm font-medium">
|
||||
Source files not found by preliminary check: {check.classname}
|
||||
{reviewT("sourceNotFound", { name: check.classname })}
|
||||
</p>
|
||||
<p className="text-xs text-[var(--admin-text-muted)]">
|
||||
{progressT("recoveryHint")}
|
||||
@@ -190,24 +191,25 @@ export function ImportReview({
|
||||
</section>
|
||||
))}
|
||||
{inspection.loading ? (
|
||||
<p role="status">Checking local data and assets…</p>
|
||||
<p role="status">{reviewT("checkingLocal")}</p>
|
||||
) : inspection.error ? (
|
||||
<div role="alert">
|
||||
<p>{inspection.error}</p>
|
||||
<Button variant="outline" onClick={inspection.retry}>
|
||||
Retry inspection
|
||||
{reviewT("retryInspection")}
|
||||
</Button>
|
||||
</div>
|
||||
) : !complete ? (
|
||||
<p role="alert">
|
||||
Inspection is incomplete. Close and retry before importing.
|
||||
</p>
|
||||
<p role="alert">{reviewT("incomplete")}</p>
|
||||
) : (
|
||||
<>
|
||||
<p role="status" className="text-sm">
|
||||
{ready.length} ready to import · {existing} already in SQL
|
||||
(complete missing data) · {blocked} conflicts ·{" "}
|
||||
{unavailable.length} source file warnings
|
||||
{reviewT("summary", {
|
||||
ready: ready.length,
|
||||
existing,
|
||||
blocked,
|
||||
warnings: unavailable.length,
|
||||
})}
|
||||
</p>
|
||||
{blocked > 0 && (
|
||||
<div
|
||||
@@ -232,8 +234,7 @@ export function ImportReview({
|
||||
item.nitro.exists === null,
|
||||
) && (
|
||||
<p className="text-sm text-[var(--admin-warning)]">
|
||||
Some local files could not be verified. Review the item
|
||||
details before continuing.
|
||||
{reviewT("localUnverified")}
|
||||
</p>
|
||||
)}
|
||||
{step === "review" ? (
|
||||
@@ -329,14 +330,14 @@ export function ImportReview({
|
||||
</span>
|
||||
<span>
|
||||
{comparison.state === "new"
|
||||
? "New"
|
||||
? reviewT("statusNew")
|
||||
: comparison.state === "changed"
|
||||
? "Differences"
|
||||
? reviewT("statusChanged")
|
||||
: comparison.state === "conflict"
|
||||
? "Conflict"
|
||||
? reviewT("statusConflict")
|
||||
: comparison.state === "unknown"
|
||||
? "Not fully verified"
|
||||
: "Present"}
|
||||
? reviewT("statusUnknown")
|
||||
: reviewT("statusPresent")}
|
||||
</span>
|
||||
</button>
|
||||
{expanded === item.classname && (
|
||||
@@ -358,7 +359,9 @@ export function ImportReview({
|
||||
}}
|
||||
/>
|
||||
{uploading === item.classname && (
|
||||
<span role="status">Validating file…</span>
|
||||
<span role="status">
|
||||
{reviewT("validatingFile")}
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
)}
|
||||
@@ -381,16 +384,10 @@ export function ImportReview({
|
||||
) : (
|
||||
<>
|
||||
<p className="text-sm">
|
||||
Only the {ready.length} eligible items below will be
|
||||
submitted. Existing furniture keeps its local IDs, SQL
|
||||
settings and catalog offers. Missing assets and catalog
|
||||
entries are completed; furnidata is refreshed. Conflicts are
|
||||
excluded.
|
||||
{reviewT("confirmIntro", { count: ready.length })}
|
||||
</p>
|
||||
<p className="text-xs text-[var(--admin-text-muted)]">
|
||||
Automatic categories and prices below are estimates. Source
|
||||
rules may override them; final results appear in the import
|
||||
summary.
|
||||
{reviewT("confirmEstimates")}
|
||||
</p>
|
||||
<ul className="space-y-2">
|
||||
{pageItems.map((item) => {
|
||||
@@ -405,8 +402,11 @@ export function ImportReview({
|
||||
>
|
||||
<strong>{item.name}</strong>
|
||||
<p>
|
||||
{placement.categoryLabel} · {placement.credits}{" "}
|
||||
credits + {placement.points} points
|
||||
{reviewT("placement", {
|
||||
category: placement.categoryLabel,
|
||||
credits: placement.credits,
|
||||
points: placement.points,
|
||||
})}
|
||||
</p>
|
||||
</li>
|
||||
);
|
||||
@@ -424,17 +424,15 @@ export function ImportReview({
|
||||
disabled={page === 0}
|
||||
onClick={() => setPage((value) => value - 1)}
|
||||
>
|
||||
Previous items
|
||||
{reviewT("previous")}
|
||||
</Button>
|
||||
<span>
|
||||
Page {page + 1} / {pageCount}
|
||||
</span>
|
||||
<span>{reviewT("page", { page: page + 1, total: pageCount })}</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={page + 1 === pageCount}
|
||||
onClick={() => setPage((value) => value + 1)}
|
||||
>
|
||||
Next items
|
||||
{reviewT("next")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
@@ -451,7 +449,7 @@ export function ImportReview({
|
||||
: onCancel
|
||||
}
|
||||
>
|
||||
{step === "confirm" ? "Back to review" : "Cancel"}
|
||||
{step === "confirm" ? reviewT("backToReview") : reviewT("cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
style={{
|
||||
@@ -480,8 +478,8 @@ export function ImportReview({
|
||||
}}
|
||||
>
|
||||
{step === "review"
|
||||
? "Continue to confirmation"
|
||||
: `Start import (${ready.length})`}
|
||||
? reviewT("continue")
|
||||
: reviewT("startImport", { count: ready.length })}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
Reference in new issue
Block a user