Add Catalog Studio inspection and guided import review
This commit is contained in:
1 parent
2578bf6a09
commit
26f071117b
7 files changed
+961
-44
No files matched your search
@@ -0,0 +1,198 @@
|
||||
"use client";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { adminFetch } from "@/lib/admin-fetch";
|
||||
import {
|
||||
compareFurniture,
|
||||
type FurnitureInspection,
|
||||
} from "@/lib/furni/studio-inspection";
|
||||
import type { FurniItem } from "./studio-types";
|
||||
|
||||
export function useFurnitureInspection(classnames: string[]) {
|
||||
const [attempt, setAttempt] = useState(0);
|
||||
const key = JSON.stringify({ classnames, attempt });
|
||||
const [state, setState] = useState<{
|
||||
key: string;
|
||||
items: FurnitureInspection[];
|
||||
error: string;
|
||||
loading: boolean;
|
||||
}>({ key: "", items: [], error: "", loading: true });
|
||||
useEffect(() => {
|
||||
const abort = new AbortController();
|
||||
setState({ key, items: [], error: "", loading: true });
|
||||
void (async () => {
|
||||
try {
|
||||
const names = JSON.parse(key).classnames as string[];
|
||||
const inspected: FurnitureInspection[] = [];
|
||||
for (let offset = 0; offset < names.length; offset += 500) {
|
||||
const res = await adminFetch("/api/admin/studio/inspect", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
classnames: names.slice(offset, offset + 500),
|
||||
}),
|
||||
signal: abort.signal,
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok || !Array.isArray(data.items))
|
||||
throw new Error(data.error || "Could not inspect local furniture");
|
||||
inspected.push(...data.items);
|
||||
}
|
||||
if (!abort.signal.aborted)
|
||||
setState({ key, items: inspected, error: "", loading: false });
|
||||
} catch (error) {
|
||||
if (!abort.signal.aborted)
|
||||
setState({
|
||||
key,
|
||||
items: [],
|
||||
error: (error as Error).message,
|
||||
loading: false,
|
||||
});
|
||||
}
|
||||
})();
|
||||
return () => abort.abort();
|
||||
}, [key]);
|
||||
return {
|
||||
...state,
|
||||
loading: state.key !== key || state.loading,
|
||||
retry: () => setAttempt((value) => value + 1),
|
||||
};
|
||||
}
|
||||
const status = (value: boolean | null | undefined) =>
|
||||
value == null ? "Not verified" : value ? "Present" : "Missing";
|
||||
export function FurnitureComparison({
|
||||
item,
|
||||
local,
|
||||
}: {
|
||||
item: FurniItem;
|
||||
local: FurnitureInspection;
|
||||
}) {
|
||||
const comparison = compareFurniture(item, local);
|
||||
return (
|
||||
<div className="space-y-3 text-sm">
|
||||
<dl className="grid grid-cols-2 gap-2">
|
||||
{(
|
||||
[
|
||||
[".nitro", local.nitro.exists],
|
||||
["Local icon", local.icon.exists],
|
||||
[
|
||||
"Furnidata",
|
||||
local.furnidataReadable ? local.furnidata.length > 0 : null,
|
||||
],
|
||||
["SQL item", local.sql.length > 0],
|
||||
["Catalog offer", local.catalog.length > 0],
|
||||
] as const
|
||||
).map(([label, present]) => (
|
||||
<div
|
||||
key={label}
|
||||
className="rounded-md border border-[var(--admin-border)] p-2"
|
||||
>
|
||||
<dt className="text-xs text-[var(--admin-text-muted)]">{label}</dt>
|
||||
<dd
|
||||
className={
|
||||
present
|
||||
? "text-[var(--admin-success)]"
|
||||
: "text-[var(--admin-warning)]"
|
||||
}
|
||||
>
|
||||
{status(present)}
|
||||
</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
{comparison.state === "conflict" && (
|
||||
<p role="alert" className="text-[var(--admin-warning)]">
|
||||
Multiple local records use this classname. Review them in Catalog
|
||||
audit before importing.
|
||||
</p>
|
||||
)}
|
||||
{!local.furnidataReadable && (
|
||||
<p className="text-[var(--admin-warning)]">
|
||||
Local furnidata could not be read. Its values are not verified.
|
||||
</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>
|
||||
</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>
|
||||
))}
|
||||
</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 SQL items are skipped by this import flow.
|
||||
</p>
|
||||
<details>
|
||||
<summary className="cursor-pointer font-medium">
|
||||
SQL records and catalog offers
|
||||
</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}
|
||||
</p>
|
||||
))
|
||||
) : (
|
||||
<p>No local SQL item.</p>
|
||||
)}
|
||||
{local.catalog.map((row) => (
|
||||
<p key={row.id}>
|
||||
Offer #{row.id} · Page #{row.pageId} · {row.credits} credits +{" "}
|
||||
{row.points} points
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export function FurnitureInspector({ item }: { item: FurniItem }) {
|
||||
const inspection = useFurnitureInspection([item.classname]);
|
||||
if (inspection.loading)
|
||||
return (
|
||||
<p role="status" className="py-3 text-sm">
|
||||
Checking local data and assets…
|
||||
</p>
|
||||
);
|
||||
if (inspection.error)
|
||||
return (
|
||||
<div role="alert">
|
||||
<p>{inspection.error}</p>
|
||||
<Button variant="outline" onClick={inspection.retry}>
|
||||
Retry inspection
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
const local = inspection.items.find(
|
||||
(entry) => entry.classname === item.classname,
|
||||
);
|
||||
return local ? (
|
||||
<FurnitureComparison item={item} local={local} />
|
||||
) : (
|
||||
<p>Local inspection unavailable.</p>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user