Add genre column to entries
Include genre data in entry search results and show it in the entries table layout. Signed-off-by: codex@lucy.xalior.com
This commit is contained in:
@@ -10,6 +10,8 @@ type Item = {
|
||||
id: number;
|
||||
title: string;
|
||||
isXrated: number;
|
||||
genreId: number | null;
|
||||
genreName?: string | null;
|
||||
machinetypeId: number | null;
|
||||
machinetypeName?: string | null;
|
||||
languageId: string | null;
|
||||
@@ -78,6 +80,25 @@ export default function EntriesExplorer({
|
||||
|
||||
const pageSize = 20;
|
||||
const totalPages = useMemo(() => (data ? Math.max(1, Math.ceil(data.total / data.pageSize)) : 1), [data]);
|
||||
const activeFilters = useMemo(() => {
|
||||
const chips: string[] = [];
|
||||
if (q) chips.push(`q: ${q}`);
|
||||
if (genreId !== "") {
|
||||
const name = genres.find((g) => g.id === Number(genreId))?.name ?? `#${genreId}`;
|
||||
chips.push(`genre: ${name}`);
|
||||
}
|
||||
if (languageId !== "") {
|
||||
const name = languages.find((l) => l.id === languageId)?.name ?? languageId;
|
||||
chips.push(`lang: ${name}`);
|
||||
}
|
||||
if (machinetypeId !== "") {
|
||||
const name = machines.find((m) => m.id === Number(machinetypeId))?.name ?? `#${machinetypeId}`;
|
||||
chips.push(`machine: ${name}`);
|
||||
}
|
||||
if (scope === "title_aliases") chips.push("scope: titles + aliases");
|
||||
if (scope === "title_aliases_origins") chips.push("scope: titles + aliases + origins");
|
||||
return chips;
|
||||
}, [q, genreId, languageId, machinetypeId, scope, genres, languages, machines]);
|
||||
|
||||
function updateUrl(nextPage = page) {
|
||||
const params = new URLSearchParams();
|
||||
@@ -176,6 +197,16 @@ export default function EntriesExplorer({
|
||||
fetchData(q, 1, true);
|
||||
}
|
||||
|
||||
function resetFilters() {
|
||||
setQ("");
|
||||
setGenreId("");
|
||||
setLanguageId("");
|
||||
setMachinetypeId("");
|
||||
setSort("id_desc");
|
||||
setScope("title");
|
||||
setPage(1);
|
||||
}
|
||||
|
||||
const prevHref = useMemo(() => {
|
||||
const params = new URLSearchParams();
|
||||
if (q) params.set("q", q);
|
||||
@@ -209,9 +240,32 @@ export default function EntriesExplorer({
|
||||
]}
|
||||
/>
|
||||
|
||||
<h1 className="mb-3">Entries</h1>
|
||||
<form className="row gy-2 gx-2 align-items-center" onSubmit={onSubmit}>
|
||||
<div className="col-sm-8 col-md-6 col-lg-4">
|
||||
<div className="d-flex align-items-center justify-content-between flex-wrap gap-2 mb-3">
|
||||
<div>
|
||||
<h1 className="mb-1">Entries</h1>
|
||||
<div className="text-secondary">
|
||||
{data ? `${data.total.toLocaleString()} results` : "Loading results..."}
|
||||
</div>
|
||||
</div>
|
||||
{activeFilters.length > 0 && (
|
||||
<div className="d-flex flex-wrap gap-2 align-items-center">
|
||||
{activeFilters.map((chip) => (
|
||||
<span key={chip} className="badge text-bg-light">{chip}</span>
|
||||
))}
|
||||
<button type="button" className="btn btn-sm btn-outline-secondary" onClick={resetFilters}>
|
||||
Clear filters
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="row g-3">
|
||||
<div className="col-lg-3">
|
||||
<div className="card shadow-sm">
|
||||
<div className="card-body">
|
||||
<form className="d-flex flex-column gap-2" onSubmit={onSubmit}>
|
||||
<div>
|
||||
<label className="form-label small text-secondary">Search</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
@@ -220,55 +274,55 @@ export default function EntriesExplorer({
|
||||
onChange={(e) => setQ(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-auto">
|
||||
<div className="d-grid">
|
||||
<button className="btn btn-primary" type="submit" disabled={loading}>Search</button>
|
||||
</div>
|
||||
<div className="col-auto">
|
||||
<div>
|
||||
<label className="form-label small text-secondary">Genre</label>
|
||||
<select className="form-select" value={genreId} onChange={(e) => { setGenreId(e.target.value === "" ? "" : Number(e.target.value)); setPage(1); }}>
|
||||
<option value="">Genre</option>
|
||||
<option value="">All genres</option>
|
||||
{genres.map((g) => (
|
||||
<option key={g.id} value={g.id}>{g.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-auto">
|
||||
<div>
|
||||
<label className="form-label small text-secondary">Language</label>
|
||||
<select className="form-select" value={languageId} onChange={(e) => { setLanguageId(e.target.value); setPage(1); }}>
|
||||
<option value="">Language</option>
|
||||
<option value="">All languages</option>
|
||||
{languages.map((l) => (
|
||||
<option key={l.id} value={l.id}>{l.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-auto">
|
||||
<div>
|
||||
<label className="form-label small text-secondary">Machine</label>
|
||||
<select className="form-select" value={machinetypeId} onChange={(e) => { setMachinetypeId(e.target.value === "" ? "" : Number(e.target.value)); setPage(1); }}>
|
||||
<option value="">Machine</option>
|
||||
<option value="">All machines</option>
|
||||
{machines.map((m) => (
|
||||
<option key={m.id} value={m.id}>{m.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-auto">
|
||||
<div>
|
||||
<label className="form-label small text-secondary">Sort</label>
|
||||
<select className="form-select" value={sort} onChange={(e) => { setSort(e.target.value as "title" | "id_desc"); setPage(1); }}>
|
||||
<option value="title">Sort: Title</option>
|
||||
<option value="id_desc">Sort: Newest</option>
|
||||
<option value="title">Title (A–Z)</option>
|
||||
<option value="id_desc">Newest</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-auto">
|
||||
<div>
|
||||
<label className="form-label small text-secondary">Search scope</label>
|
||||
<select className="form-select" value={scope} onChange={(e) => { setScope(e.target.value as SearchScope); setPage(1); }}>
|
||||
<option value="title">Search: Titles</option>
|
||||
<option value="title_aliases">Search: Titles + Aliases</option>
|
||||
<option value="title_aliases_origins">Search: Titles + Aliases + Origins</option>
|
||||
<option value="title">Titles</option>
|
||||
<option value="title_aliases">Titles + Aliases</option>
|
||||
<option value="title_aliases_origins">Titles + Aliases + Origins</option>
|
||||
</select>
|
||||
</div>
|
||||
{loading && (
|
||||
<div className="col-auto text-secondary">Loading...</div>
|
||||
)}
|
||||
</form>
|
||||
|
||||
{facets && (
|
||||
<div className="mt-3">
|
||||
<div className="d-flex flex-wrap gap-2 align-items-center">
|
||||
<span className="text-secondary small">Facets</span>
|
||||
<div>
|
||||
<div className="text-secondary small mb-1">Facets</div>
|
||||
<div className="d-flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className={`btn btn-sm ${scope === "title_aliases" ? "btn-primary" : "btn-outline-secondary"}`}
|
||||
@@ -290,8 +344,13 @@ export default function EntriesExplorer({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{loading && <div className="text-secondary small">Loading...</div>}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3">
|
||||
<div className="col-lg-9">
|
||||
{data && data.items.length === 0 && !loading && (
|
||||
<div className="alert alert-warning">No results.</div>
|
||||
)}
|
||||
@@ -300,18 +359,28 @@ export default function EntriesExplorer({
|
||||
<table className="table table-striped table-hover align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{width: 80}}>ID</th>
|
||||
<th style={{ width: 80 }}>ID</th>
|
||||
<th>Title</th>
|
||||
<th style={{width: 160}}>Machine</th>
|
||||
<th style={{width: 120}}>Language</th>
|
||||
<th style={{ width: 160 }}>Genre</th>
|
||||
<th style={{ width: 160 }}>Machine</th>
|
||||
<th style={{ width: 120 }}>Language</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.items.map((it) => (
|
||||
<tr key={it.id}>
|
||||
<td><EntryLink id={it.id} /></td>
|
||||
<td><EntryLink id={it.id} title={it.title} /></td>
|
||||
<td>
|
||||
<EntryLink id={it.id} title={it.title} />
|
||||
{it.genreId != null ? (
|
||||
it.genreName ? (
|
||||
<Link href={`/zxdb/genres/${it.genreId}`}>{it.genreName}</Link>
|
||||
) : (
|
||||
<span>{it.genreId}</span>
|
||||
)
|
||||
) : (
|
||||
<span className="text-secondary">-</span>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
{it.machinetypeId != null ? (
|
||||
@@ -342,11 +411,10 @@ export default function EntriesExplorer({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="d-flex align-items-center gap-2 mt-2">
|
||||
<span>
|
||||
Page {data?.page ?? 1} / {totalPages}
|
||||
</span>
|
||||
<div className="d-flex align-items-center gap-2 mt-4">
|
||||
<span>Page {data?.page ?? 1} / {totalPages}</span>
|
||||
<div className="ms-auto d-flex gap-2">
|
||||
<Link
|
||||
className={`btn btn-outline-secondary ${!data || (data.page <= 1) ? "disabled" : ""}`}
|
||||
|
||||
@@ -74,6 +74,8 @@ export interface SearchResultItem {
|
||||
id: number;
|
||||
title: string;
|
||||
isXrated: number;
|
||||
genreId: number | null;
|
||||
genreName?: string | null;
|
||||
machinetypeId: number | null;
|
||||
machinetypeName?: string | null;
|
||||
languageId: string | null;
|
||||
@@ -199,12 +201,15 @@ export async function searchEntries(params: SearchParams): Promise<PagedResult<S
|
||||
id: entries.id,
|
||||
title: entries.title,
|
||||
isXrated: entries.isXrated,
|
||||
genreId: entries.genretypeId,
|
||||
genreName: genretypes.name,
|
||||
machinetypeId: entries.machinetypeId,
|
||||
machinetypeName: machinetypes.name,
|
||||
languageId: entries.languageId,
|
||||
languageName: languages.name,
|
||||
})
|
||||
.from(entries)
|
||||
.leftJoin(genretypes, eq(genretypes.id, entries.genretypeId))
|
||||
.leftJoin(machinetypes, eq(machinetypes.id, entries.machinetypeId))
|
||||
.leftJoin(languages, eq(languages.id, entries.languageId))
|
||||
.where(whereExpr ?? sql`true`)
|
||||
@@ -239,12 +244,15 @@ export async function searchEntries(params: SearchParams): Promise<PagedResult<S
|
||||
id: entries.id,
|
||||
title: entries.title,
|
||||
isXrated: entries.isXrated,
|
||||
genreId: entries.genretypeId,
|
||||
genreName: genretypes.name,
|
||||
machinetypeId: entries.machinetypeId,
|
||||
machinetypeName: machinetypes.name,
|
||||
languageId: entries.languageId,
|
||||
languageName: languages.name,
|
||||
})
|
||||
.from(entries)
|
||||
.leftJoin(genretypes, eq(genretypes.id, entries.genretypeId))
|
||||
.leftJoin(machinetypes, eq(machinetypes.id, entries.machinetypeId))
|
||||
.leftJoin(languages, eq(languages.id, entries.languageId))
|
||||
.where(sql`${entries.id} in (select entry_id from (${union}) as matches)`)
|
||||
@@ -273,6 +281,8 @@ export async function searchEntries(params: SearchParams): Promise<PagedResult<S
|
||||
id: entries.id,
|
||||
title: entries.title,
|
||||
isXrated: entries.isXrated,
|
||||
genreId: entries.genretypeId,
|
||||
genreName: genretypes.name,
|
||||
machinetypeId: entries.machinetypeId,
|
||||
machinetypeName: machinetypes.name,
|
||||
languageId: entries.languageId,
|
||||
@@ -280,6 +290,7 @@ export async function searchEntries(params: SearchParams): Promise<PagedResult<S
|
||||
})
|
||||
.from(searchByTitles)
|
||||
.innerJoin(entries, eq(entries.id, searchByTitles.entryId))
|
||||
.leftJoin(genretypes, eq(genretypes.id, entries.genretypeId))
|
||||
.leftJoin(machinetypes, eq(machinetypes.id, entries.machinetypeId))
|
||||
.leftJoin(languages, eq(languages.id, entries.languageId))
|
||||
.where(like(searchByTitles.entryTitle, pattern))
|
||||
@@ -1170,6 +1181,8 @@ export async function getLabelAuthoredEntries(labelId: number, params: LabelCont
|
||||
id: entries.id,
|
||||
title: entries.title,
|
||||
isXrated: entries.isXrated,
|
||||
genreId: entries.genretypeId,
|
||||
genreName: genretypes.name,
|
||||
machinetypeId: entries.machinetypeId,
|
||||
machinetypeName: machinetypes.name,
|
||||
languageId: entries.languageId,
|
||||
@@ -1177,6 +1190,7 @@ export async function getLabelAuthoredEntries(labelId: number, params: LabelCont
|
||||
})
|
||||
.from(authors)
|
||||
.innerJoin(entries, eq(entries.id, authors.entryId))
|
||||
.leftJoin(genretypes, eq(genretypes.id, entries.genretypeId))
|
||||
.leftJoin(machinetypes, eq(machinetypes.id, entries.machinetypeId))
|
||||
.leftJoin(languages, eq(languages.id, entries.languageId))
|
||||
.where(eq(authors.labelId, labelId))
|
||||
@@ -1200,6 +1214,8 @@ export async function getLabelAuthoredEntries(labelId: number, params: LabelCont
|
||||
id: entries.id,
|
||||
title: entries.title,
|
||||
isXrated: entries.isXrated,
|
||||
genreId: entries.genretypeId,
|
||||
genreName: genretypes.name,
|
||||
machinetypeId: entries.machinetypeId,
|
||||
machinetypeName: machinetypes.name,
|
||||
languageId: entries.languageId,
|
||||
@@ -1207,6 +1223,7 @@ export async function getLabelAuthoredEntries(labelId: number, params: LabelCont
|
||||
})
|
||||
.from(authors)
|
||||
.innerJoin(entries, eq(entries.id, authors.entryId))
|
||||
.leftJoin(genretypes, eq(genretypes.id, entries.genretypeId))
|
||||
.leftJoin(machinetypes, eq(machinetypes.id, entries.machinetypeId))
|
||||
.leftJoin(languages, eq(languages.id, entries.languageId))
|
||||
.where(and(eq(authors.labelId, labelId), sql`${entries.id} in (select entry_id from ${searchByTitles} where ${searchByTitles.entryTitle} like ${pattern})`))
|
||||
@@ -1236,6 +1253,8 @@ export async function getLabelPublishedEntries(labelId: number, params: LabelCon
|
||||
id: entries.id,
|
||||
title: entries.title,
|
||||
isXrated: entries.isXrated,
|
||||
genreId: entries.genretypeId,
|
||||
genreName: genretypes.name,
|
||||
machinetypeId: entries.machinetypeId,
|
||||
machinetypeName: machinetypes.name,
|
||||
languageId: entries.languageId,
|
||||
@@ -1243,6 +1262,7 @@ export async function getLabelPublishedEntries(labelId: number, params: LabelCon
|
||||
})
|
||||
.from(publishers)
|
||||
.innerJoin(entries, eq(entries.id, publishers.entryId))
|
||||
.leftJoin(genretypes, eq(genretypes.id, entries.genretypeId))
|
||||
.leftJoin(machinetypes, eq(machinetypes.id, entries.machinetypeId))
|
||||
.leftJoin(languages, eq(languages.id, entries.languageId))
|
||||
.where(eq(publishers.labelId, labelId))
|
||||
@@ -1266,6 +1286,8 @@ export async function getLabelPublishedEntries(labelId: number, params: LabelCon
|
||||
id: entries.id,
|
||||
title: entries.title,
|
||||
isXrated: entries.isXrated,
|
||||
genreId: entries.genretypeId,
|
||||
genreName: genretypes.name,
|
||||
machinetypeId: entries.machinetypeId,
|
||||
machinetypeName: machinetypes.name,
|
||||
languageId: entries.languageId,
|
||||
@@ -1273,6 +1295,7 @@ export async function getLabelPublishedEntries(labelId: number, params: LabelCon
|
||||
})
|
||||
.from(publishers)
|
||||
.innerJoin(entries, eq(entries.id, publishers.entryId))
|
||||
.leftJoin(genretypes, eq(genretypes.id, entries.genretypeId))
|
||||
.leftJoin(machinetypes, eq(machinetypes.id, entries.machinetypeId))
|
||||
.leftJoin(languages, eq(languages.id, entries.languageId))
|
||||
.where(and(eq(publishers.labelId, labelId), sql`${entries.id} in (select entry_id from ${searchByTitles} where ${searchByTitles.entryTitle} like ${pattern})`))
|
||||
@@ -1409,12 +1432,15 @@ export async function entriesByGenre(
|
||||
id: entries.id,
|
||||
title: entries.title,
|
||||
isXrated: entries.isXrated,
|
||||
genreId: entries.genretypeId,
|
||||
genreName: genretypes.name,
|
||||
machinetypeId: entries.machinetypeId,
|
||||
machinetypeName: machinetypes.name,
|
||||
languageId: entries.languageId,
|
||||
languageName: languages.name,
|
||||
})
|
||||
.from(entries)
|
||||
.leftJoin(genretypes, eq(genretypes.id, entries.genretypeId))
|
||||
.leftJoin(machinetypes, eq(machinetypes.id, entries.machinetypeId))
|
||||
.leftJoin(languages, eq(languages.id, entries.languageId))
|
||||
.where(eq(entries.genretypeId, genreId))
|
||||
@@ -1435,12 +1461,15 @@ export async function entriesByGenre(
|
||||
id: entries.id,
|
||||
title: entries.title,
|
||||
isXrated: entries.isXrated,
|
||||
genreId: entries.genretypeId,
|
||||
genreName: genretypes.name,
|
||||
machinetypeId: entries.machinetypeId,
|
||||
machinetypeName: machinetypes.name,
|
||||
languageId: entries.languageId,
|
||||
languageName: languages.name,
|
||||
})
|
||||
.from(entries)
|
||||
.leftJoin(genretypes, eq(genretypes.id, entries.genretypeId))
|
||||
.leftJoin(machinetypes, eq(machinetypes.id, entries.machinetypeId))
|
||||
.leftJoin(languages, eq(languages.id, entries.languageId))
|
||||
.where(and(eq(entries.genretypeId, genreId), sql`${entries.id} in (select entry_id from ${searchByTitles} where ${searchByTitles.entryTitle} like ${pattern})`))
|
||||
@@ -1470,12 +1499,15 @@ export async function entriesByLanguage(
|
||||
id: entries.id,
|
||||
title: entries.title,
|
||||
isXrated: entries.isXrated,
|
||||
genreId: entries.genretypeId,
|
||||
genreName: genretypes.name,
|
||||
machinetypeId: entries.machinetypeId,
|
||||
machinetypeName: machinetypes.name,
|
||||
languageId: entries.languageId,
|
||||
languageName: languages.name,
|
||||
})
|
||||
.from(entries)
|
||||
.leftJoin(genretypes, eq(genretypes.id, entries.genretypeId))
|
||||
.leftJoin(machinetypes, eq(machinetypes.id, entries.machinetypeId))
|
||||
.leftJoin(languages, eq(languages.id, entries.languageId))
|
||||
.where(eq(entries.languageId, langId))
|
||||
@@ -1496,12 +1528,15 @@ export async function entriesByLanguage(
|
||||
id: entries.id,
|
||||
title: entries.title,
|
||||
isXrated: entries.isXrated,
|
||||
genreId: entries.genretypeId,
|
||||
genreName: genretypes.name,
|
||||
machinetypeId: entries.machinetypeId,
|
||||
machinetypeName: machinetypes.name,
|
||||
languageId: entries.languageId,
|
||||
languageName: languages.name,
|
||||
})
|
||||
.from(entries)
|
||||
.leftJoin(genretypes, eq(genretypes.id, entries.genretypeId))
|
||||
.leftJoin(machinetypes, eq(machinetypes.id, entries.machinetypeId))
|
||||
.leftJoin(languages, eq(languages.id, entries.languageId))
|
||||
.where(and(eq(entries.languageId, langId), sql`${entries.id} in (select entry_id from ${searchByTitles} where ${searchByTitles.entryTitle} like ${pattern})`))
|
||||
@@ -1531,12 +1566,15 @@ export async function entriesByMachinetype(
|
||||
id: entries.id,
|
||||
title: entries.title,
|
||||
isXrated: entries.isXrated,
|
||||
genreId: entries.genretypeId,
|
||||
genreName: genretypes.name,
|
||||
machinetypeId: entries.machinetypeId,
|
||||
machinetypeName: machinetypes.name,
|
||||
languageId: entries.languageId,
|
||||
languageName: languages.name,
|
||||
})
|
||||
.from(entries)
|
||||
.leftJoin(genretypes, eq(genretypes.id, entries.genretypeId))
|
||||
.leftJoin(machinetypes, eq(machinetypes.id, entries.machinetypeId))
|
||||
.leftJoin(languages, eq(languages.id, entries.languageId))
|
||||
.where(eq(entries.machinetypeId, mtId))
|
||||
@@ -1557,12 +1595,15 @@ export async function entriesByMachinetype(
|
||||
id: entries.id,
|
||||
title: entries.title,
|
||||
isXrated: entries.isXrated,
|
||||
genreId: entries.genretypeId,
|
||||
genreName: genretypes.name,
|
||||
machinetypeId: entries.machinetypeId,
|
||||
machinetypeName: machinetypes.name,
|
||||
languageId: entries.languageId,
|
||||
languageName: languages.name,
|
||||
})
|
||||
.from(entries)
|
||||
.leftJoin(genretypes, eq(genretypes.id, entries.genretypeId))
|
||||
.leftJoin(machinetypes, eq(machinetypes.id, entries.machinetypeId))
|
||||
.leftJoin(languages, eq(languages.id, entries.languageId))
|
||||
.where(and(eq(entries.machinetypeId, mtId), sql`${entries.id} in (select entry_id from ${searchByTitles} where ${searchByTitles.entryTitle} like ${pattern})`))
|
||||
|
||||
Reference in New Issue
Block a user