Adding the first stubs of the magazine browser
This commit is contained in:
79
src/app/zxdb/issues/[id]/page.tsx
Normal file
79
src/app/zxdb/issues/[id]/page.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getIssue } from "@/server/repo/zxdb";
|
||||
import EntryLink from "@/app/zxdb/components/EntryLink";
|
||||
|
||||
export const metadata = { title: "ZXDB Issue" };
|
||||
export const revalidate = 3600;
|
||||
|
||||
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
const issueId = Number(id);
|
||||
if (!Number.isFinite(issueId) || issueId <= 0) return notFound();
|
||||
|
||||
const issue = await getIssue(issueId);
|
||||
if (!issue) return notFound();
|
||||
|
||||
const ym = [issue.dateYear ?? "", issue.dateMonth ? String(issue.dateMonth).padStart(2, "0") : ""].filter(Boolean).join("/");
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-3 d-flex gap-2 flex-wrap">
|
||||
<Link className="btn btn-outline-secondary btn-sm" href={`/zxdb/magazines/${issue.magazine.id}`}>← Back to magazine</Link>
|
||||
<Link className="btn btn-outline-secondary btn-sm" href="/zxdb/magazines">All magazines</Link>
|
||||
{issue.linkMask && (
|
||||
<a className="btn btn-outline-secondary btn-sm" href={issue.linkMask} target="_blank" rel="noreferrer">Issue link</a>
|
||||
)}
|
||||
{issue.archiveMask && (
|
||||
<a className="btn btn-outline-secondary btn-sm" href={issue.archiveMask} target="_blank" rel="noreferrer">Archive</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h1 className="mb-1">{issue.magazine.title}</h1>
|
||||
<div className="text-secondary mb-3">
|
||||
Issue: {ym || issue.id}{issue.volume != null ? ` · Vol ${issue.volume}` : ""}{issue.number != null ? ` · No ${issue.number}` : ""}
|
||||
</div>
|
||||
|
||||
{(issue.special || issue.supplement) && (
|
||||
<div className="mb-3">
|
||||
{issue.special && <div><strong>Special:</strong> {issue.special}</div>}
|
||||
{issue.supplement && <div><strong>Supplement:</strong> {issue.supplement}</div>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<h2 className="h5 mt-4">References</h2>
|
||||
{issue.refs.length === 0 ? (
|
||||
<div className="text-secondary">No references recorded.</div>
|
||||
) : (
|
||||
<div className="table-responsive">
|
||||
<table className="table table-sm align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ width: 80 }}>Page</th>
|
||||
<th style={{ width: 140 }}>Type</th>
|
||||
<th>Reference</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{issue.refs.map((r) => (
|
||||
<tr key={r.id}>
|
||||
<td>{r.page}</td>
|
||||
<td>{r.typeName}</td>
|
||||
<td>
|
||||
{r.entryId ? (
|
||||
<EntryLink id={r.entryId} title={r.entryTitle ?? undefined} />
|
||||
) : r.labelId ? (
|
||||
<Link href={`/zxdb/labels/${r.labelId}`}>{r.labelName ?? r.labelId}</Link>
|
||||
) : (
|
||||
<span className="text-secondary">—</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
85
src/app/zxdb/magazines/[id]/page.tsx
Normal file
85
src/app/zxdb/magazines/[id]/page.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getMagazine } from "@/server/repo/zxdb";
|
||||
|
||||
export const metadata = { title: "ZXDB Magazine" };
|
||||
export const revalidate = 3600;
|
||||
|
||||
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
const magazineId = Number(id);
|
||||
if (!Number.isFinite(magazineId) || magazineId <= 0) return notFound();
|
||||
|
||||
const mag = await getMagazine(magazineId);
|
||||
if (!mag) return notFound();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="mb-1">{mag.title}</h1>
|
||||
<div className="text-secondary mb-3">Language: {mag.languageId}</div>
|
||||
|
||||
<div className="mb-3 d-flex gap-2 flex-wrap">
|
||||
<Link className="btn btn-outline-secondary btn-sm" href="/zxdb/magazines">← Back to list</Link>
|
||||
{mag.linkSite && (
|
||||
<a className="btn btn-outline-secondary btn-sm" href={mag.linkSite} target="_blank" rel="noreferrer">
|
||||
Official site
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h2 className="h5 mt-4">Issues</h2>
|
||||
{mag.issues.length === 0 ? (
|
||||
<div className="text-secondary">No issues found.</div>
|
||||
) : (
|
||||
<div className="table-responsive">
|
||||
<table className="table table-sm align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ width: 200 }}>Issue</th>
|
||||
<th style={{ width: 100 }}>Volume</th>
|
||||
<th style={{ width: 100 }}>Number</th>
|
||||
<th>Special</th>
|
||||
<th>Supplement</th>
|
||||
<th style={{ width: 100 }}>Links</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{mag.issues.map((i) => (
|
||||
<tr key={i.id}>
|
||||
<td>
|
||||
<Link href={`/zxdb/issues/${i.id}`} className="link-underline link-underline-opacity-0">
|
||||
{i.dateYear ?? ""}
|
||||
{i.dateMonth ? `/${String(i.dateMonth).padStart(2, "0")}` : ""}
|
||||
{" "}
|
||||
<span className="text-secondary">(open issue)</span>
|
||||
</Link>
|
||||
</td>
|
||||
<td>{i.volume ?? ""}</td>
|
||||
<td>{i.number ?? ""}</td>
|
||||
<td>{i.special ?? ""}</td>
|
||||
<td>{i.supplement ?? ""}</td>
|
||||
<td>
|
||||
<div className="d-flex gap-2">
|
||||
{i.linkMask && (
|
||||
<a className="btn btn-outline-secondary btn-sm" href={i.linkMask} target="_blank" rel="noreferrer" title="Link">
|
||||
<span className="bi bi-link-45deg" aria-hidden />
|
||||
<span className="visually-hidden">Link</span>
|
||||
</a>
|
||||
)}
|
||||
{i.archiveMask && (
|
||||
<a className="btn btn-outline-secondary btn-sm" href={i.archiveMask} target="_blank" rel="noreferrer" title="Archive">
|
||||
<span className="bi bi-archive" aria-hidden />
|
||||
<span className="visually-hidden">Archive</span>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
81
src/app/zxdb/magazines/page.tsx
Normal file
81
src/app/zxdb/magazines/page.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import Link from "next/link";
|
||||
import { listMagazines } from "@/server/repo/zxdb";
|
||||
|
||||
export const metadata = { title: "ZXDB Magazines" };
|
||||
|
||||
// Depends on searchParams (?q=, ?page=). Force dynamic so each request renders correctly.
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function Page({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
|
||||
}) {
|
||||
const sp = await searchParams;
|
||||
const q = (Array.isArray(sp.q) ? sp.q[0] : sp.q) ?? "";
|
||||
const page = Math.max(1, Number(Array.isArray(sp.page) ? sp.page[0] : sp.page) || 1);
|
||||
|
||||
const data = await listMagazines({ q, page, pageSize: 20 });
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="mb-3">Magazines</h1>
|
||||
|
||||
<form className="mb-3" action="/zxdb/magazines" method="get">
|
||||
<div className="input-group">
|
||||
<input type="text" className="form-control" name="q" placeholder="Search magazines..." defaultValue={q} />
|
||||
<button className="btn btn-outline-secondary" type="submit">
|
||||
<span className="bi bi-search" aria-hidden />
|
||||
<span className="visually-hidden">Search</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="list-group">
|
||||
{data.items.map((m) => (
|
||||
<Link key={m.id} className="list-group-item list-group-item-action d-flex justify-content-between align-items-center" href={`/zxdb/magazines/${m.id}`}>
|
||||
<span>
|
||||
{m.title}
|
||||
<span className="text-secondary ms-2">({m.languageId})</span>
|
||||
</span>
|
||||
<span className="badge bg-secondary rounded-pill" title="Issues">
|
||||
{m.issueCount}
|
||||
</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Pagination page={data.page} pageSize={data.pageSize} total={data.total} q={q} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Pagination({ page, pageSize, total, q }: { page: number; pageSize: number; total: number; q: string }) {
|
||||
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
||||
if (totalPages <= 1) return null;
|
||||
const makeHref = (p: number) => {
|
||||
const params = new URLSearchParams();
|
||||
if (q) params.set("q", q);
|
||||
params.set("page", String(p));
|
||||
return `/zxdb/magazines?${params.toString()}`;
|
||||
};
|
||||
return (
|
||||
<nav className="mt-3" aria-label="Pagination">
|
||||
<ul className="pagination">
|
||||
<li className={`page-item ${page <= 1 ? "disabled" : ""}`}>
|
||||
<Link className="page-link" href={makeHref(Math.max(1, page - 1))}>
|
||||
Previous
|
||||
</Link>
|
||||
</li>
|
||||
<li className="page-item disabled">
|
||||
<span className="page-link">Page {page} of {totalPages}</span>
|
||||
</li>
|
||||
<li className={`page-item ${page >= totalPages ? "disabled" : ""}`}>
|
||||
<Link className="page-link" href={makeHref(Math.min(totalPages, page + 1))}>
|
||||
Next
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -44,6 +44,22 @@ export default async function Page() {
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="col-sm-6 col-lg-4">
|
||||
<Link href="/zxdb/magazines" className="text-decoration-none">
|
||||
<div className="card h-100 shadow-sm">
|
||||
<div className="card-body d-flex align-items-center">
|
||||
<div className="me-3" aria-hidden>
|
||||
<span className="bi bi-journal-text" style={{ fontSize: 28 }} />
|
||||
</div>
|
||||
<div>
|
||||
<h5 className="card-title mb-1">Magazines</h5>
|
||||
<div className="card-text text-secondary">Browse magazines and their issues</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
|
||||
Reference in New Issue
Block a user