Show entry origins data and display label type names in label detail view. Signed-off-by: codex@lucy.xalior.com
220 lines
9.1 KiB
TypeScript
220 lines
9.1 KiB
TypeScript
"use client";
|
|
|
|
import Link from "next/link";
|
|
import EntryLink from "../../components/EntryLink";
|
|
import { useMemo, useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
|
|
type Label = {
|
|
id: number;
|
|
name: string;
|
|
labeltypeId: string | null;
|
|
labeltypeName: string | null;
|
|
permissions: {
|
|
website: { id: number; name: string; link?: string | null };
|
|
type: { id: string; name: string | null };
|
|
text: string | null;
|
|
}[];
|
|
licenses: {
|
|
id: number;
|
|
name: string;
|
|
type: { id: string; name: string | null };
|
|
linkWikipedia?: string | null;
|
|
linkSite?: string | null;
|
|
comments?: string | null;
|
|
}[];
|
|
};
|
|
type Item = { id: number; title: string; isXrated: number; machinetypeId: number | null; machinetypeName?: string | null; languageId: string | null; languageName?: string | null };
|
|
type Paged<T> = { items: T[]; page: number; pageSize: number; total: number };
|
|
|
|
type Payload = { label: Label | null; authored: Paged<Item>; published: Paged<Item> };
|
|
|
|
export default function LabelDetailClient({ id, initial, initialTab, initialQ }: { id: number; initial: Payload; initialTab?: "authored" | "published"; initialQ?: string }) {
|
|
// Keep only interactive UI state (tab). Data should come directly from SSR props so it updates on navigation.
|
|
const [tab, setTab] = useState<"authored" | "published">(initialTab ?? "authored");
|
|
const [q, setQ] = useState(initialQ ?? "");
|
|
const router = useRouter();
|
|
// Names are now delivered by SSR payload to minimize pop-in.
|
|
|
|
// Hooks must be called unconditionally
|
|
const current = useMemo<Paged<Item> | null>(
|
|
() => (tab === "authored" ? initial?.authored : initial?.published) ?? null,
|
|
[initial, tab]
|
|
);
|
|
const totalPages = useMemo(() => (current ? Math.max(1, Math.ceil(current.total / current.pageSize)) : 1), [current]);
|
|
|
|
if (!initial || !initial.label) return <div className="alert alert-warning">Not found</div>;
|
|
|
|
return (
|
|
<div>
|
|
<div className="d-flex align-items-center justify-content-between flex-wrap gap-2">
|
|
<h1 className="mb-0">{initial.label.name}</h1>
|
|
<div>
|
|
<span className="badge text-bg-light">
|
|
{initial.label.labeltypeName
|
|
? `${initial.label.labeltypeName} (${initial.label.labeltypeId ?? "?"})`
|
|
: (initial.label.labeltypeId ?? "?")}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="row g-4 mt-1">
|
|
<div className="col-lg-6">
|
|
<h5>Permissions</h5>
|
|
{initial.label.permissions.length === 0 && <div className="text-secondary">No permissions recorded</div>}
|
|
{initial.label.permissions.length > 0 && (
|
|
<div className="table-responsive">
|
|
<table className="table table-sm table-striped align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th>Website</th>
|
|
<th style={{ width: 140 }}>Type</th>
|
|
<th>Notes</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{initial.label.permissions.map((p, idx) => (
|
|
<tr key={`${p.website.id}-${p.type.id}-${idx}`}>
|
|
<td>
|
|
{p.website.link ? (
|
|
<a href={p.website.link} target="_blank" rel="noreferrer">{p.website.name}</a>
|
|
) : (
|
|
<span>{p.website.name}</span>
|
|
)}
|
|
</td>
|
|
<td>{p.type.name ?? p.type.id}</td>
|
|
<td>{p.text ?? ""}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="col-lg-6">
|
|
<h5>Licenses</h5>
|
|
{initial.label.licenses.length === 0 && <div className="text-secondary">No licenses linked</div>}
|
|
{initial.label.licenses.length > 0 && (
|
|
<div className="table-responsive">
|
|
<table className="table table-sm table-striped align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th style={{ width: 140 }}>Type</th>
|
|
<th>Links</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{initial.label.licenses.map((l) => (
|
|
<tr key={l.id}>
|
|
<td>{l.name}</td>
|
|
<td>{l.type.name ?? l.type.id}</td>
|
|
<td>
|
|
<div className="d-flex gap-2 flex-wrap">
|
|
{l.linkWikipedia && (
|
|
<a href={l.linkWikipedia} target="_blank" rel="noreferrer">Wikipedia</a>
|
|
)}
|
|
{l.linkSite && (
|
|
<a href={l.linkSite} target="_blank" rel="noreferrer">Site</a>
|
|
)}
|
|
{!l.linkWikipedia && !l.linkSite && <span className="text-secondary">-</span>}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<ul className="nav nav-tabs mt-3">
|
|
<li className="nav-item">
|
|
<button className={`nav-link ${tab === "authored" ? "active" : ""}`} onClick={() => setTab("authored")}>Authored</button>
|
|
</li>
|
|
<li className="nav-item">
|
|
<button className={`nav-link ${tab === "published" ? "active" : ""}`} onClick={() => setTab("published")}>Published</button>
|
|
</li>
|
|
</ul>
|
|
|
|
<form className="row gy-2 gx-2 align-items-center mt-2" onSubmit={(e) => { e.preventDefault(); const p = new URLSearchParams(); p.set("tab", tab); if (q) p.set("q", q); p.set("page", "1"); router.push(`/zxdb/labels/${id}?${p.toString()}`); }}>
|
|
<div className="col-sm-8 col-md-6 col-lg-4">
|
|
<input className="form-control" placeholder={`Search within ${tab}…`} value={q} onChange={(e) => setQ(e.target.value)} />
|
|
</div>
|
|
<div className="col-auto">
|
|
<button className="btn btn-primary">Search</button>
|
|
</div>
|
|
</form>
|
|
|
|
<div className="mt-3">
|
|
{current && current.items.length === 0 && <div className="alert alert-warning">No entries.</div>}
|
|
{current && current.items.length > 0 && (
|
|
<div className="table-responsive">
|
|
<table className="table table-striped table-hover align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th style={{ width: 80 }}>ID</th>
|
|
<th>Title</th>
|
|
<th style={{ width: 160 }}>Machine</th>
|
|
<th style={{ width: 120 }}>Language</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{current.items.map((it) => (
|
|
<tr key={it.id}>
|
|
<td><EntryLink id={it.id} /></td>
|
|
<td><EntryLink id={it.id} title={it.title} /></td>
|
|
<td>
|
|
{it.machinetypeId != null ? (
|
|
it.machinetypeName ? (
|
|
<Link href={`/zxdb/machinetypes/${it.machinetypeId}`}>{it.machinetypeName}</Link>
|
|
) : (
|
|
<span>{it.machinetypeId}</span>
|
|
)
|
|
) : (
|
|
<span className="text-secondary">-</span>
|
|
)}
|
|
</td>
|
|
<td>
|
|
{it.languageId ? (
|
|
it.languageName ? (
|
|
<Link href={`/zxdb/languages/${it.languageId}`}>{it.languageName}</Link>
|
|
) : (
|
|
<span>{it.languageId}</span>
|
|
)
|
|
) : (
|
|
<span className="text-secondary">-</span>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="d-flex align-items-center gap-2 mt-2">
|
|
<span>Page {current ? current.page : 1} / {totalPages}</span>
|
|
<div className="ms-auto d-flex gap-2">
|
|
<Link
|
|
className={`btn btn-sm btn-outline-secondary ${current && current.page <= 1 ? "disabled" : ""}`}
|
|
aria-disabled={current ? current.page <= 1 : true}
|
|
href={`/zxdb/labels/${id}?${(() => { const p = new URLSearchParams(); p.set("tab", tab); if (q) p.set("q", q); p.set("page", String(Math.max(1, (current ? current.page : 1) - 1))); return p.toString(); })()}`}
|
|
>
|
|
Prev
|
|
</Link>
|
|
<Link
|
|
className={`btn btn-sm btn-outline-secondary ${current && current.page >= totalPages ? "disabled" : ""}`}
|
|
aria-disabled={current ? current.page >= totalPages : true}
|
|
href={`/zxdb/labels/${id}?${(() => { const p = new URLSearchParams(); p.set("tab", tab); if (q) p.set("q", q); p.set("page", String(Math.min(totalPages, (current ? current.page : 1) + 1))); return p.toString(); })()}`}
|
|
>
|
|
Next
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|