feat(web): object detail + two-pane page + app routing
Implements the navigable SPA shell: object detail pane showing inventory-minimum fields, flexible fields (via Record<string,unknown> cast) and visibility badge; ObjectsPage two-pane layout; BrowserRouter wired through RequireAuth+AppShell; QueryClient provided in main.tsx. Consolidates ObjectList NavLink to use isActive function form, removing manual useParams highlight. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useObject, useFieldDefinitions } from "../api/queries";
|
||||
import { VisibilityBadge } from "./visibility-badge";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
function Field({
|
||||
label,
|
||||
value,
|
||||
}: {
|
||||
label: string;
|
||||
value: string | number | null | undefined;
|
||||
}) {
|
||||
if (value === null || value === undefined || value === "") return null;
|
||||
|
||||
return (
|
||||
<div className="border-b py-2">
|
||||
<div className="text-xs uppercase tracking-wide text-neutral-400">{label}</div>
|
||||
<div className="text-sm text-neutral-900">{value}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ObjectDetail() {
|
||||
const { t } = useTranslation();
|
||||
const { id } = useParams();
|
||||
const { data: object, isLoading, isError } = useObject(id!);
|
||||
const { data: definitions } = useFieldDefinitions();
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="p-4">
|
||||
<Skeleton className="h-40 w-full" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError) return <p className="p-4 text-sm text-red-600">{t("objects.loadError")}</p>;
|
||||
|
||||
if (!object) return <p className="p-4 text-sm text-neutral-500">{t("objects.notFound")}</p>;
|
||||
|
||||
const labelFor = (key: string) =>
|
||||
definitions?.find((d) => d.key === key)?.labels.find((l) => l.lang === "en")?.label ?? key;
|
||||
|
||||
const flexible = Object.entries(object.fields as Record<string, unknown>);
|
||||
|
||||
return (
|
||||
<div className="overflow-auto p-4">
|
||||
<div className="mb-4 flex items-center gap-3">
|
||||
<h2 className="text-xl font-semibold">{object.object_name}</h2>
|
||||
<VisibilityBadge visibility={object.visibility} />
|
||||
</div>
|
||||
<Field label={t("fieldsLabels.objectNumber")} value={object.object_number} />
|
||||
<Field label={t("fieldsLabels.count")} value={object.number_of_objects} />
|
||||
<Field label={t("fieldsLabels.briefDescription")} value={object.brief_description} />
|
||||
<Field label={t("fieldsLabels.currentLocation")} value={object.current_location} />
|
||||
<Field label={t("fieldsLabels.currentOwner")} value={object.current_owner} />
|
||||
<Field label={t("fieldsLabels.recorder")} value={object.recorder} />
|
||||
<Field label={t("fieldsLabels.recordingDate")} value={object.recording_date} />
|
||||
{flexible.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<div className="mb-1 text-xs font-medium uppercase text-neutral-500">
|
||||
{t("fieldsLabels.flexible")}
|
||||
</div>
|
||||
{flexible.map(([key, value]) => (
|
||||
<Field
|
||||
key={key}
|
||||
label={labelFor(key)}
|
||||
value={typeof value === "object" ? JSON.stringify(value) : String(value)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user