Files
v2.admin.gaertan.art/src/components/artworks/single/ArtworkDetails.tsx

306 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Link from "next/link";
import * as React from "react";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table";
import { cn } from "@/lib/utils";
import type { ArtworkWithRelations } from "@/types/Artwork";
function fmtDate(value?: Date | string | null) {
if (!value) return "—";
const d = typeof value === "string" ? new Date(value) : value;
if (Number.isNaN(d.getTime())) return "—";
return new Intl.DateTimeFormat("de-DE", {
year: "numeric",
month: "short",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
}).format(d);
}
function fmtBool(value?: boolean | null) {
if (value === true) return "Yes";
if (value === false) return "No";
return "—";
}
function fmtNum(value?: number | null, digits = 0) {
if (value === null || value === undefined) return "—";
return new Intl.NumberFormat("de-DE", {
maximumFractionDigits: digits,
minimumFractionDigits: digits,
}).format(value);
}
function fmtBytes(bytes?: number | null) {
if (!bytes && bytes !== 0) return "—";
const units = ["B", "KB", "MB", "GB", "TB"];
let v = bytes;
let i = 0;
while (v >= 1024 && i < units.length - 1) {
v /= 1024;
i++;
}
return `${fmtNum(v, i === 0 ? 0 : 2)} ${units[i]}`;
}
function KVTable({ rows }: { rows: Array<{ k: string; v: React.ReactNode }> }) {
return (
<Table>
<TableBody>
{rows.map((r) => (
<TableRow key={r.k} className="hover:bg-transparent">
<TableCell className="w-[38%] align-top py-2 text-muted-foreground">
{r.k}
</TableCell>
<TableCell className="py-2">{r.v}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
}
function StatusPill({
label,
variant,
}: {
label: string;
variant?: "default" | "secondary" | "destructive" | "outline";
}) {
return (
<Badge variant={variant ?? "secondary"} className="whitespace-nowrap">
{label}
</Badge>
);
}
export default function ArtworkDetails({
artwork,
className,
}: {
artwork: ArtworkWithRelations;
className?: string;
}) {
const meta = artwork.metadata ?? null;
// Your schema: Artwork has `fileId` + relation `file: FileData`
// but depending on your `ArtworkWithRelations` type, `file` may be optional.
const file = (artwork as any).file ?? null;
const flags = [
artwork.published ? <StatusPill key="published" label="Published" /> : <StatusPill key="unpublished" label="Unpublished" variant="outline" />,
artwork.nsfw ? <StatusPill key="nsfw" label="NSFW" variant="destructive" /> : <StatusPill key="sfw" label="SFW" variant="secondary" />,
artwork.needsWork ? <StatusPill key="needs-work" label="Needs work" variant="outline" /> : <StatusPill key="ok" label="OK" variant="secondary" />,
artwork.setAsHeader ? <StatusPill key="header" label="Header" /> : null,
].filter(Boolean);
return (
<Card className={cn("overflow-hidden", className)}>
<CardHeader className="space-y-1">
<div className="flex items-start justify-between gap-3">
<div>
<CardTitle className="text-base">Artwork details</CardTitle>
<CardDescription className="text-sm">
Read-only technical information and metadata
</CardDescription>
</div>
<div className="flex flex-wrap items-center justify-end gap-2">{flags}</div>
</div>
</CardHeader>
<CardContent className="space-y-6">
{/* Core */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="text-sm font-medium">Core</div>
</div>
<KVTable
rows={[
{ k: "ID", v: <span className="font-mono text-xs break-all">{artwork.id}</span> },
{ k: "Slug", v: <span className="font-mono text-xs break-all">{artwork.slug}</span> },
{ k: "Sort index", v: fmtNum(artwork.sortIndex ?? 0) },
{ k: "Sort key", v: artwork.sortKey != null ? fmtNum(artwork.sortKey) : "—" },
{ k: "Created", v: fmtDate(artwork.createdAt as any) },
{ k: "Updated", v: fmtDate(artwork.updatedAt as any) },
{ k: "Creation date", v: fmtDate(artwork.creationDate as any) },
{
k: "Creation (month/year)",
v:
artwork.month || artwork.year
? `${artwork.month ? fmtNum(artwork.month) : "—"} / ${artwork.year ? fmtNum(artwork.year) : "—"}`
: "—",
},
{
k: "Color status",
v: (
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<Badge variant="outline">{artwork.colorStatus ?? "—"}</Badge>
{artwork.colorsGeneratedAt ? (
<span className="text-xs text-muted-foreground">
generated {fmtDate(artwork.colorsGeneratedAt as any)}
</span>
) : null}
</div>
{artwork.colorError ? (
<div className="text-xs text-destructive wrap-break-word">{artwork.colorError}</div>
) : null}
</div>
),
},
{
k: "OKLab",
v:
artwork.okLabL != null || artwork.okLabA != null || artwork.okLabB != null ? (
<div className="flex flex-wrap gap-2">
<Badge variant="secondary">L {fmtNum(artwork.okLabL, 3)}</Badge>
<Badge variant="secondary">a {fmtNum(artwork.okLabA, 3)}</Badge>
<Badge variant="secondary">b {fmtNum(artwork.okLabB, 3)}</Badge>
</div>
) : (
"—"
),
},
{
k: "Relations",
v: (
<div className="flex flex-wrap gap-2 text-xs">
<Badge variant="secondary">{(artwork.categories?.length ?? 0)} categories</Badge>
<Badge variant="secondary">{(artwork.tags?.length ?? 0)} tags</Badge>
<Badge variant="secondary">{(artwork.colors?.length ?? 0)} colors</Badge>
<Badge variant="secondary">{(artwork.variants?.length ?? 0)} variants</Badge>
</div>
),
},
]}
/>
</div>
<Separator />
{/* Metadata */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="text-sm font-medium">Artwork metadata</div>
{!meta ? <Badge variant="outline">None</Badge> : null}
</div>
{meta ? (
<KVTable
rows={[
{ k: "Format", v: meta.format ?? "—" },
{ k: "Space", v: meta.space ?? "—" },
{ k: "Depth", v: meta.depth ?? "—" },
{ k: "Channels", v: meta.channels != null ? fmtNum(meta.channels) : "—" },
{
k: "Dimensions",
v:
meta.width && meta.height
? `${fmtNum(meta.width)} × ${fmtNum(meta.height)} px`
: "—",
},
{
k: "Auto-orient",
v:
meta.autoOrientW || meta.autoOrientH
? `${meta.autoOrientW ?? "—"} × ${meta.autoOrientH ?? "—"}`
: "—",
},
{ k: "Bits per sample", v: meta.bitsPerSample != null ? fmtNum(meta.bitsPerSample) : "—" },
{ k: "Density", v: meta.density != null ? fmtNum(meta.density, 2) : "—" },
{ k: "Has alpha", v: fmtBool(meta.hasAlpha) },
{ k: "Has profile", v: fmtBool(meta.hasProfile) },
{ k: "Is palette", v: fmtBool(meta.isPalette) },
{ k: "Is progressive", v: fmtBool(meta.isProgressive) },
{ k: "Metadata ID", v: <span className="font-mono text-xs break-all">{meta.id}</span> },
]}
/>
) : (
<div className="text-sm text-muted-foreground">No metadata available for this artwork.</div>
)}
</div>
<Separator />
{/* File data */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="text-sm font-medium">File</div>
{!file ? <Badge variant="outline">Missing relation</Badge> : null}
</div>
{file ? (
<KVTable
rows={[
{ k: "File ID", v: <span className="font-mono text-xs break-all">{file.id}</span> },
{ k: "File key", v: <span className="font-mono text-xs break-all">{file.fileKey}</span> },
{ k: "Original name", v: <span className="break-all">{file.originalFile}</span> },
{ k: "Stored name", v: file.name ?? "—" },
{ k: "MIME type", v: file.fileType ?? "—" },
{ k: "Size", v: fmtBytes(file.fileSize) },
{ k: "Uploaded", v: fmtDate(file.uploadDate as any) },
]}
/>
) : (
<div className="text-sm text-muted-foreground">
This component expects the artwork query to include the <span className="font-mono">file</span> relation.
</div>
)}
</div>
{/* Variants (optional but helpful) */}
{artwork.variants?.length ? (
<>
<Separator />
<div className="space-y-3">
<div className="text-sm font-medium">Variants</div>
<div className="space-y-2">
{artwork.variants
.slice()
.sort((a: any, b: any) => (a.type ?? "").localeCompare(b.type ?? ""))
.map((v: any) => (
<div
key={v.id}
className="flex flex-wrap items-center justify-between gap-2 rounded-md border px-3 py-2"
>
<div className="flex flex-col">
<div className="flex items-center gap-2">
<Badge variant="secondary">{v.type ?? "variant"}</Badge>
<span className="text-xs text-muted-foreground">
{v.width && v.height ? `${fmtNum(v.width)}×${fmtNum(v.height)} px` : "—"}
</span>
</div>
<div className="text-xs text-muted-foreground break-all">
{v.mimeType ?? "—"} {v.fileExtension ? `(${v.fileExtension})` : ""}
</div>
</div>
<div className="flex items-center gap-2 text-xs">
<Badge variant="outline">{v.sizeBytes ? fmtBytes(v.sizeBytes) : "—"}</Badge>
{v.url ? (
<Link
href={v.url}
target="_blank"
rel="noreferrer"
className="text-muted-foreground underline underline-offset-4 hover:text-foreground"
>
Open
</Link>
) : null}
</div>
</div>
))}
</div>
</div>
</>
) : null}
</CardContent>
</Card>
);
}