Add entry ports and scores
Surface ports, remakes, scores, and notes on entry detail. Signed-off-by: codex@lucy.xalior.com
This commit is contained in:
@@ -36,6 +36,34 @@ export type EntryDetailData = {
|
|||||||
link: string | null;
|
link: string | null;
|
||||||
comments: string | null;
|
comments: string | null;
|
||||||
}[];
|
}[];
|
||||||
|
ports?: {
|
||||||
|
id: number;
|
||||||
|
title: string | null;
|
||||||
|
platform: { id: number; name: string | null };
|
||||||
|
isOfficial: boolean;
|
||||||
|
linkSystem: string | null;
|
||||||
|
}[];
|
||||||
|
remakes?: {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
fileLink: string;
|
||||||
|
fileDate: string | null;
|
||||||
|
fileSize: number | null;
|
||||||
|
authors: string | null;
|
||||||
|
platforms: string | null;
|
||||||
|
remakeYears: string | null;
|
||||||
|
remakeStatus: string | null;
|
||||||
|
}[];
|
||||||
|
scores?: {
|
||||||
|
website: { id: number; name: string | null };
|
||||||
|
score: number;
|
||||||
|
votes: number;
|
||||||
|
}[];
|
||||||
|
notes?: {
|
||||||
|
id: number;
|
||||||
|
type: { id: string; name: string | null };
|
||||||
|
text: string;
|
||||||
|
}[];
|
||||||
origins?: {
|
origins?: {
|
||||||
type: { id: string; name: string | null };
|
type: { id: string; name: string | null };
|
||||||
libraryTitle: string;
|
libraryTitle: string;
|
||||||
@@ -491,6 +519,138 @@ export default function EntryDetailClient({ data }: { data: EntryDetailData | nu
|
|||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h5>Ports</h5>
|
||||||
|
{(!data.ports || data.ports.length === 0) && <div className="text-secondary">No ports recorded</div>}
|
||||||
|
{data.ports && data.ports.length > 0 && (
|
||||||
|
<div className="table-responsive">
|
||||||
|
<table className="table table-sm table-striped align-middle">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Title</th>
|
||||||
|
<th style={{ width: 160 }}>Platform</th>
|
||||||
|
<th style={{ width: 120 }}>Official</th>
|
||||||
|
<th>Link</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{data.ports.map((p) => (
|
||||||
|
<tr key={p.id}>
|
||||||
|
<td>{p.title ?? <span className="text-secondary">-</span>}</td>
|
||||||
|
<td>{p.platform.name ?? `#${p.platform.id}`}</td>
|
||||||
|
<td>{p.isOfficial ? "Yes" : "No"}</td>
|
||||||
|
<td>
|
||||||
|
{p.linkSystem ? (
|
||||||
|
<a href={p.linkSystem} target="_blank" rel="noreferrer">Link</a>
|
||||||
|
) : (
|
||||||
|
<span className="text-secondary">-</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h5>Remakes</h5>
|
||||||
|
{(!data.remakes || data.remakes.length === 0) && <div className="text-secondary">No remakes recorded</div>}
|
||||||
|
{data.remakes && data.remakes.length > 0 && (
|
||||||
|
<div className="table-responsive">
|
||||||
|
<table className="table table-sm table-striped align-middle">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Title</th>
|
||||||
|
<th style={{ width: 160 }}>Platforms</th>
|
||||||
|
<th style={{ width: 140 }}>Years</th>
|
||||||
|
<th style={{ width: 140 }}>File</th>
|
||||||
|
<th>Notes</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{data.remakes.map((r) => (
|
||||||
|
<tr key={r.id}>
|
||||||
|
<td>{r.title}</td>
|
||||||
|
<td>{r.platforms ?? <span className="text-secondary">-</span>}</td>
|
||||||
|
<td>{r.remakeYears ?? <span className="text-secondary">-</span>}</td>
|
||||||
|
<td>
|
||||||
|
{r.fileLink ? (
|
||||||
|
<a href={r.fileLink} target="_blank" rel="noreferrer">File</a>
|
||||||
|
) : (
|
||||||
|
<span className="text-secondary">-</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td>{r.remakeStatus ?? r.authors ?? <span className="text-secondary">-</span>}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h5>Scores</h5>
|
||||||
|
{(!data.scores || data.scores.length === 0) && <div className="text-secondary">No scores recorded</div>}
|
||||||
|
{data.scores && data.scores.length > 0 && (
|
||||||
|
<div className="table-responsive">
|
||||||
|
<table className="table table-sm table-striped align-middle">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Website</th>
|
||||||
|
<th style={{ width: 120 }}>Score</th>
|
||||||
|
<th style={{ width: 120 }}>Votes</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{data.scores.map((s, idx) => (
|
||||||
|
<tr key={`${s.website.id}-${idx}`}>
|
||||||
|
<td>{s.website.name ?? `#${s.website.id}`}</td>
|
||||||
|
<td>{s.score}</td>
|
||||||
|
<td>{s.votes}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h5>Notes</h5>
|
||||||
|
{(!data.notes || data.notes.length === 0) && <div className="text-secondary">No notes recorded</div>}
|
||||||
|
{data.notes && data.notes.length > 0 && (
|
||||||
|
<div className="table-responsive">
|
||||||
|
<table className="table table-sm table-striped align-middle">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style={{ width: 140 }}>Type</th>
|
||||||
|
<th>Text</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{data.notes.map((n) => (
|
||||||
|
<tr key={n.id}>
|
||||||
|
<td>{n.type.name ?? n.type.id}</td>
|
||||||
|
<td>{n.text}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
{/* Aliases (alternative titles) */}
|
{/* Aliases (alternative titles) */}
|
||||||
<div>
|
<div>
|
||||||
<h5>Aliases</h5>
|
<h5>Aliases</h5>
|
||||||
|
|||||||
@@ -39,6 +39,12 @@ import {
|
|||||||
tags,
|
tags,
|
||||||
tagtypes,
|
tagtypes,
|
||||||
members,
|
members,
|
||||||
|
ports,
|
||||||
|
platforms,
|
||||||
|
remakes,
|
||||||
|
scores,
|
||||||
|
notes,
|
||||||
|
notetypes,
|
||||||
webrefs,
|
webrefs,
|
||||||
websites,
|
websites,
|
||||||
magazines,
|
magazines,
|
||||||
@@ -319,6 +325,34 @@ export interface EntryDetail {
|
|||||||
link: string | null;
|
link: string | null;
|
||||||
comments: string | null;
|
comments: string | null;
|
||||||
}[];
|
}[];
|
||||||
|
ports?: {
|
||||||
|
id: number;
|
||||||
|
title: string | null;
|
||||||
|
platform: { id: number; name: string | null };
|
||||||
|
isOfficial: boolean;
|
||||||
|
linkSystem: string | null;
|
||||||
|
}[];
|
||||||
|
remakes?: {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
fileLink: string;
|
||||||
|
fileDate: string | null;
|
||||||
|
fileSize: number | null;
|
||||||
|
authors: string | null;
|
||||||
|
platforms: string | null;
|
||||||
|
remakeYears: string | null;
|
||||||
|
remakeStatus: string | null;
|
||||||
|
}[];
|
||||||
|
scores?: {
|
||||||
|
website: { id: number; name: string | null };
|
||||||
|
score: number;
|
||||||
|
votes: number;
|
||||||
|
}[];
|
||||||
|
notes?: {
|
||||||
|
id: number;
|
||||||
|
type: { id: string; name: string | null };
|
||||||
|
text: string;
|
||||||
|
}[];
|
||||||
origins?: {
|
origins?: {
|
||||||
type: { id: string; name: string | null };
|
type: { id: string; name: string | null };
|
||||||
libraryTitle: string;
|
libraryTitle: string;
|
||||||
@@ -627,6 +661,37 @@ export async function getEntryById(id: number): Promise<EntryDetail | null> {
|
|||||||
link: string | null;
|
link: string | null;
|
||||||
comments: string | null;
|
comments: string | null;
|
||||||
}[] = [];
|
}[] = [];
|
||||||
|
let portRows: {
|
||||||
|
id: number | string;
|
||||||
|
title: string | null;
|
||||||
|
platformId: number | string;
|
||||||
|
platformName: string | null;
|
||||||
|
isOfficial: number | boolean;
|
||||||
|
linkSystem: string | null;
|
||||||
|
}[] = [];
|
||||||
|
let remakeRows: {
|
||||||
|
id: number | string;
|
||||||
|
title: string;
|
||||||
|
fileLink: string;
|
||||||
|
fileDate: string | null;
|
||||||
|
fileSize: number | string | null;
|
||||||
|
authors: string | null;
|
||||||
|
platforms: string | null;
|
||||||
|
remakeYears: string | null;
|
||||||
|
remakeStatus: string | null;
|
||||||
|
}[] = [];
|
||||||
|
let scoreRows: {
|
||||||
|
websiteId: number | string;
|
||||||
|
websiteName: string | null;
|
||||||
|
score: number | string;
|
||||||
|
votes: number | string;
|
||||||
|
}[] = [];
|
||||||
|
let noteRows: {
|
||||||
|
id: number | string;
|
||||||
|
notetypeId: string;
|
||||||
|
notetypeName: string | null;
|
||||||
|
text: string;
|
||||||
|
}[] = [];
|
||||||
try {
|
try {
|
||||||
aliasRows = await db
|
aliasRows = await db
|
||||||
.select({ releaseSeq: aliases.releaseSeq, languageId: aliases.languageId, title: aliases.title })
|
.select({ releaseSeq: aliases.releaseSeq, languageId: aliases.languageId, title: aliases.title })
|
||||||
@@ -741,6 +806,67 @@ export async function getEntryById(id: number): Promise<EntryDetail | null> {
|
|||||||
);
|
);
|
||||||
tagRows = rows as typeof tagRows;
|
tagRows = rows as typeof tagRows;
|
||||||
} catch {}
|
} catch {}
|
||||||
|
try {
|
||||||
|
const rows = await db
|
||||||
|
.select({
|
||||||
|
id: ports.id,
|
||||||
|
title: ports.title,
|
||||||
|
platformId: ports.platformId,
|
||||||
|
platformName: platforms.name,
|
||||||
|
isOfficial: ports.isOfficial,
|
||||||
|
linkSystem: ports.linkSystem,
|
||||||
|
})
|
||||||
|
.from(ports)
|
||||||
|
.leftJoin(platforms, eq(platforms.id, ports.platformId))
|
||||||
|
.where(eq(ports.entryId, id))
|
||||||
|
.orderBy(asc(ports.title));
|
||||||
|
portRows = rows as typeof portRows;
|
||||||
|
} catch {}
|
||||||
|
try {
|
||||||
|
const rows = await db
|
||||||
|
.select({
|
||||||
|
id: remakes.id,
|
||||||
|
title: remakes.title,
|
||||||
|
fileLink: remakes.fileLink,
|
||||||
|
fileDate: remakes.fileDate,
|
||||||
|
fileSize: remakes.fileSize,
|
||||||
|
authors: remakes.authors,
|
||||||
|
platforms: remakes.platforms,
|
||||||
|
remakeYears: remakes.remakeYears,
|
||||||
|
remakeStatus: remakes.remakeStatus,
|
||||||
|
})
|
||||||
|
.from(remakes)
|
||||||
|
.where(eq(remakes.entryId, id))
|
||||||
|
.orderBy(asc(remakes.title));
|
||||||
|
remakeRows = rows as typeof remakeRows;
|
||||||
|
} catch {}
|
||||||
|
try {
|
||||||
|
const rows = await db
|
||||||
|
.select({
|
||||||
|
websiteId: websites.id,
|
||||||
|
websiteName: websites.name,
|
||||||
|
score: scores.score,
|
||||||
|
votes: scores.votes,
|
||||||
|
})
|
||||||
|
.from(scores)
|
||||||
|
.innerJoin(websites, eq(websites.id, scores.websiteId))
|
||||||
|
.where(eq(scores.entryId, id));
|
||||||
|
scoreRows = rows as typeof scoreRows;
|
||||||
|
} catch {}
|
||||||
|
try {
|
||||||
|
const rows = await db
|
||||||
|
.select({
|
||||||
|
id: notes.id,
|
||||||
|
notetypeId: notes.notetypeId,
|
||||||
|
notetypeName: notetypes.name,
|
||||||
|
text: notes.text,
|
||||||
|
})
|
||||||
|
.from(notes)
|
||||||
|
.leftJoin(notetypes, eq(notetypes.id, notes.notetypeId))
|
||||||
|
.where(eq(notes.entryId, id))
|
||||||
|
.orderBy(asc(notes.id));
|
||||||
|
noteRows = rows as typeof noteRows;
|
||||||
|
} catch {}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: base.id,
|
id: base.id,
|
||||||
@@ -781,6 +907,34 @@ export async function getEntryById(id: number): Promise<EntryDetail | null> {
|
|||||||
link: t.link ?? null,
|
link: t.link ?? null,
|
||||||
comments: t.comments ?? null,
|
comments: t.comments ?? null,
|
||||||
})),
|
})),
|
||||||
|
ports: portRows.map((p) => ({
|
||||||
|
id: Number(p.id),
|
||||||
|
title: p.title ?? null,
|
||||||
|
platform: { id: Number(p.platformId), name: p.platformName ?? null },
|
||||||
|
isOfficial: !!p.isOfficial,
|
||||||
|
linkSystem: p.linkSystem ?? null,
|
||||||
|
})),
|
||||||
|
remakes: remakeRows.map((r) => ({
|
||||||
|
id: Number(r.id),
|
||||||
|
title: r.title,
|
||||||
|
fileLink: r.fileLink,
|
||||||
|
fileDate: r.fileDate ?? null,
|
||||||
|
fileSize: r.fileSize != null ? Number(r.fileSize) : null,
|
||||||
|
authors: r.authors ?? null,
|
||||||
|
platforms: r.platforms ?? null,
|
||||||
|
remakeYears: r.remakeYears ?? null,
|
||||||
|
remakeStatus: r.remakeStatus ?? null,
|
||||||
|
})),
|
||||||
|
scores: scoreRows.map((s) => ({
|
||||||
|
website: { id: Number(s.websiteId), name: s.websiteName ?? null },
|
||||||
|
score: Number(s.score),
|
||||||
|
votes: Number(s.votes),
|
||||||
|
})),
|
||||||
|
notes: noteRows.map((n) => ({
|
||||||
|
id: Number(n.id),
|
||||||
|
type: { id: n.notetypeId, name: n.notetypeName ?? null },
|
||||||
|
text: n.text,
|
||||||
|
})),
|
||||||
origins: originRows.map((o) => ({
|
origins: originRows.map((o) => ({
|
||||||
type: { id: o.origintypeId, name: o.origintypeName ?? null },
|
type: { id: o.origintypeId, name: o.origintypeName ?? null },
|
||||||
libraryTitle: o.libraryTitle,
|
libraryTitle: o.libraryTitle,
|
||||||
|
|||||||
Reference in New Issue
Block a user