Standardize ZXDB UI; add SSR search/tables
Unify the look and feel of all /zxdb pages and minimize client pop-in. - Make all /zxdb pages full-width to match /explorer - Convert Languages, Genres, Machine Types, and Labels lists to Bootstrap tables with table-striped and table-hover inside table-responsive wrappers - Replace raw FK IDs with linked names via SSR repository joins - Add scoped search boxes on detail pages (labels, genres, languages, machine types) with SSR filtering and pagination that preserves q/tab - Keep explorer results consistent: show Machine/Language names with links, no client lookups required This improves consistency, readability, and first paint stability across the ZXDB section while keeping navigation fast and discoverable. Signed-off-by: Junie@lucy.xalior.com
This commit is contained in:
@@ -28,7 +28,9 @@ export interface SearchResultItem {
|
||||
title: string;
|
||||
isXrated: number;
|
||||
machinetypeId: number | null;
|
||||
machinetypeName?: string | null;
|
||||
languageId: string | null;
|
||||
languageName?: string | null;
|
||||
}
|
||||
|
||||
export interface PagedResult<T> {
|
||||
@@ -70,8 +72,18 @@ export async function searchEntries(params: SearchParams): Promise<PagedResult<S
|
||||
|
||||
const [items, countRows] = await Promise.all([
|
||||
db
|
||||
.select()
|
||||
.select({
|
||||
id: entries.id,
|
||||
title: entries.title,
|
||||
isXrated: entries.isXrated,
|
||||
machinetypeId: entries.machinetypeId,
|
||||
machinetypeName: machinetypes.name,
|
||||
languageId: entries.languageId,
|
||||
languageName: languages.name,
|
||||
})
|
||||
.from(entries)
|
||||
.leftJoin(machinetypes, eq(machinetypes.id, entries.machinetypeId as any))
|
||||
.leftJoin(languages, eq(languages.id, entries.languageId as any))
|
||||
.where(whereExpr as any)
|
||||
.orderBy(sort === "id_desc" ? desc(entries.id) : entries.title)
|
||||
.limit(pageSize)
|
||||
@@ -102,10 +114,14 @@ export async function searchEntries(params: SearchParams): Promise<PagedResult<S
|
||||
title: entries.title,
|
||||
isXrated: entries.isXrated,
|
||||
machinetypeId: entries.machinetypeId,
|
||||
machinetypeName: machinetypes.name,
|
||||
languageId: entries.languageId,
|
||||
languageName: languages.name,
|
||||
})
|
||||
.from(searchByTitles)
|
||||
.innerJoin(entries, eq(entries.id, searchByTitles.entryId))
|
||||
.leftJoin(machinetypes, eq(machinetypes.id, entries.machinetypeId as any))
|
||||
.leftJoin(languages, eq(languages.id, entries.languageId as any))
|
||||
.where(like(searchByTitles.entryTitle, pattern))
|
||||
.groupBy(entries.id)
|
||||
.orderBy(sort === "id_desc" ? desc(entries.id) : entries.title)
|
||||
@@ -130,6 +146,12 @@ export interface EntryDetail {
|
||||
genre: { id: number | null; name: string | null };
|
||||
authors: LabelSummary[];
|
||||
publishers: LabelSummary[];
|
||||
// Additional entry fields for richer details
|
||||
maxPlayers?: number;
|
||||
availabletypeId?: string | null;
|
||||
withoutLoadScreen?: number;
|
||||
withoutInlay?: number;
|
||||
issueId?: number | null;
|
||||
}
|
||||
|
||||
export async function getEntryById(id: number): Promise<EntryDetail | null> {
|
||||
@@ -146,6 +168,11 @@ export async function getEntryById(id: number): Promise<EntryDetail | null> {
|
||||
languageName: languages.name,
|
||||
genreId: entries.genretypeId,
|
||||
genreName: genretypes.name,
|
||||
maxPlayers: entries.maxPlayers,
|
||||
availabletypeId: entries.availabletypeId,
|
||||
withoutLoadScreen: entries.withoutLoadScreen,
|
||||
withoutInlay: entries.withoutInlay,
|
||||
issueId: entries.issueId,
|
||||
})
|
||||
.from(entries)
|
||||
.leftJoin(machinetypes, eq(machinetypes.id, entries.machinetypeId as any))
|
||||
@@ -178,6 +205,11 @@ export async function getEntryById(id: number): Promise<EntryDetail | null> {
|
||||
genre: { id: (base.genreId as any) ?? null, name: (base.genreName as any) ?? null },
|
||||
authors: authorRows as any,
|
||||
publishers: publisherRows as any,
|
||||
maxPlayers: (base.maxPlayers as any) ?? undefined,
|
||||
availabletypeId: (base.availabletypeId as any) ?? undefined,
|
||||
withoutLoadScreen: (base.withoutLoadScreen as any) ?? undefined,
|
||||
withoutInlay: (base.withoutInlay as any) ?? undefined,
|
||||
issueId: (base.issueId as any) ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -237,30 +269,67 @@ export async function getLabelById(id: number): Promise<LabelDetail | null> {
|
||||
export interface LabelContribsParams {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
q?: string; // optional title filter
|
||||
}
|
||||
|
||||
export async function getLabelAuthoredEntries(labelId: number, params: LabelContribsParams): Promise<PagedResult<SearchResultItem>> {
|
||||
const pageSize = Math.max(1, Math.min(params.pageSize ?? 20, 100));
|
||||
const page = Math.max(1, params.page ?? 1);
|
||||
const offset = (page - 1) * pageSize;
|
||||
const hasQ = !!(params.q && params.q.trim());
|
||||
|
||||
if (!hasQ) {
|
||||
const countRows = await db
|
||||
.select({ total: sql<number>`count(distinct ${authors.entryId})` })
|
||||
.from(authors)
|
||||
.where(eq(authors.labelId, labelId));
|
||||
const total = Number(countRows[0]?.total ?? 0);
|
||||
|
||||
const items = await db
|
||||
.select({
|
||||
id: entries.id,
|
||||
title: entries.title,
|
||||
isXrated: entries.isXrated,
|
||||
machinetypeId: entries.machinetypeId,
|
||||
machinetypeName: machinetypes.name,
|
||||
languageId: entries.languageId,
|
||||
languageName: languages.name,
|
||||
})
|
||||
.from(authors)
|
||||
.innerJoin(entries, eq(entries.id, authors.entryId))
|
||||
.leftJoin(machinetypes, eq(machinetypes.id, entries.machinetypeId as any))
|
||||
.leftJoin(languages, eq(languages.id, entries.languageId as any))
|
||||
.where(eq(authors.labelId, labelId))
|
||||
.groupBy(entries.id)
|
||||
.orderBy(entries.title)
|
||||
.limit(pageSize)
|
||||
.offset(offset);
|
||||
|
||||
return { items: items as any, page, pageSize, total };
|
||||
}
|
||||
|
||||
const pattern = `%${params.q!.toLowerCase().replace(/[^a-z0-9]+/g, "")}%`;
|
||||
const countRows = await db
|
||||
.select({ total: sql<number>`count(distinct ${authors.entryId})` })
|
||||
.select({ total: sql<number>`count(distinct ${entries.id})` })
|
||||
.from(authors)
|
||||
.where(eq(authors.labelId, labelId));
|
||||
const total = Number(countRows[0]?.total ?? 0);
|
||||
|
||||
.innerJoin(entries, eq(entries.id, authors.entryId))
|
||||
.where(and(eq(authors.labelId, labelId), sql`${entries.id} in (select entry_id from ${searchByTitles} where ${searchByTitles.entryTitle} like ${pattern})`) as any);
|
||||
const total = Number((countRows as any)[0]?.total ?? 0);
|
||||
const items = await db
|
||||
.select({
|
||||
id: entries.id,
|
||||
title: entries.title,
|
||||
isXrated: entries.isXrated,
|
||||
machinetypeId: entries.machinetypeId,
|
||||
machinetypeName: machinetypes.name,
|
||||
languageId: entries.languageId,
|
||||
languageName: languages.name,
|
||||
})
|
||||
.from(authors)
|
||||
.innerJoin(entries, eq(entries.id, authors.entryId))
|
||||
.where(eq(authors.labelId, labelId))
|
||||
.leftJoin(machinetypes, eq(machinetypes.id, entries.machinetypeId as any))
|
||||
.leftJoin(languages, eq(languages.id, entries.languageId as any))
|
||||
.where(and(eq(authors.labelId, labelId), sql`${entries.id} in (select entry_id from ${searchByTitles} where ${searchByTitles.entryTitle} like ${pattern})`) as any)
|
||||
.groupBy(entries.id)
|
||||
.orderBy(entries.title)
|
||||
.limit(pageSize)
|
||||
@@ -273,24 +342,60 @@ export async function getLabelPublishedEntries(labelId: number, params: LabelCon
|
||||
const pageSize = Math.max(1, Math.min(params.pageSize ?? 20, 100));
|
||||
const page = Math.max(1, params.page ?? 1);
|
||||
const offset = (page - 1) * pageSize;
|
||||
const hasQ = !!(params.q && params.q.trim());
|
||||
|
||||
if (!hasQ) {
|
||||
const countRows = await db
|
||||
.select({ total: sql<number>`count(distinct ${publishers.entryId})` })
|
||||
.from(publishers)
|
||||
.where(eq(publishers.labelId, labelId));
|
||||
const total = Number(countRows[0]?.total ?? 0);
|
||||
|
||||
const items = await db
|
||||
.select({
|
||||
id: entries.id,
|
||||
title: entries.title,
|
||||
isXrated: entries.isXrated,
|
||||
machinetypeId: entries.machinetypeId,
|
||||
machinetypeName: machinetypes.name,
|
||||
languageId: entries.languageId,
|
||||
languageName: languages.name,
|
||||
})
|
||||
.from(publishers)
|
||||
.innerJoin(entries, eq(entries.id, publishers.entryId))
|
||||
.leftJoin(machinetypes, eq(machinetypes.id, entries.machinetypeId as any))
|
||||
.leftJoin(languages, eq(languages.id, entries.languageId as any))
|
||||
.where(eq(publishers.labelId, labelId))
|
||||
.groupBy(entries.id)
|
||||
.orderBy(entries.title)
|
||||
.limit(pageSize)
|
||||
.offset(offset);
|
||||
|
||||
return { items: items as any, page, pageSize, total };
|
||||
}
|
||||
|
||||
const pattern = `%${params.q!.toLowerCase().replace(/[^a-z0-9]+/g, "")}%`;
|
||||
const countRows = await db
|
||||
.select({ total: sql<number>`count(distinct ${publishers.entryId})` })
|
||||
.select({ total: sql<number>`count(distinct ${entries.id})` })
|
||||
.from(publishers)
|
||||
.where(eq(publishers.labelId, labelId));
|
||||
const total = Number(countRows[0]?.total ?? 0);
|
||||
|
||||
.innerJoin(entries, eq(entries.id, publishers.entryId))
|
||||
.where(and(eq(publishers.labelId, labelId), sql`${entries.id} in (select entry_id from ${searchByTitles} where ${searchByTitles.entryTitle} like ${pattern})`) as any);
|
||||
const total = Number((countRows as any)[0]?.total ?? 0);
|
||||
const items = await db
|
||||
.select({
|
||||
id: entries.id,
|
||||
title: entries.title,
|
||||
isXrated: entries.isXrated,
|
||||
machinetypeId: entries.machinetypeId,
|
||||
machinetypeName: machinetypes.name,
|
||||
languageId: entries.languageId,
|
||||
languageName: languages.name,
|
||||
})
|
||||
.from(publishers)
|
||||
.innerJoin(entries, eq(entries.id, publishers.entryId))
|
||||
.where(eq(publishers.labelId, labelId))
|
||||
.leftJoin(machinetypes, eq(machinetypes.id, entries.machinetypeId as any))
|
||||
.leftJoin(languages, eq(languages.id, entries.languageId as any))
|
||||
.where(and(eq(publishers.labelId, labelId), sql`${entries.id} in (select entry_id from ${searchByTitles} where ${searchByTitles.entryTitle} like ${pattern})`) as any)
|
||||
.groupBy(entries.id)
|
||||
.orderBy(entries.title)
|
||||
.limit(pageSize)
|
||||
@@ -408,52 +513,187 @@ export async function searchMachinetypes(params: SimpleSearchParams) {
|
||||
return { items: items as any, page, pageSize, total };
|
||||
}
|
||||
|
||||
export async function entriesByGenre(genreId: number, page: number, pageSize: number): Promise<PagedResult<SearchResultItem>> {
|
||||
export async function entriesByGenre(
|
||||
genreId: number,
|
||||
page: number,
|
||||
pageSize: number,
|
||||
q?: string
|
||||
): Promise<PagedResult<SearchResultItem>> {
|
||||
const offset = (page - 1) * pageSize;
|
||||
const countRows = (await db
|
||||
.select({ total: sql<number>`count(*)` })
|
||||
const hasQ = !!(q && q.trim());
|
||||
|
||||
if (!hasQ) {
|
||||
const countRows = (await db
|
||||
.select({ total: sql<number>`count(*)` })
|
||||
.from(entries)
|
||||
.where(eq(entries.genretypeId, genreId as any))) as unknown as { total: number }[];
|
||||
const items = await db
|
||||
.select({
|
||||
id: entries.id,
|
||||
title: entries.title,
|
||||
isXrated: entries.isXrated,
|
||||
machinetypeId: entries.machinetypeId,
|
||||
machinetypeName: machinetypes.name,
|
||||
languageId: entries.languageId,
|
||||
languageName: languages.name,
|
||||
})
|
||||
.from(entries)
|
||||
.leftJoin(machinetypes, eq(machinetypes.id, entries.machinetypeId as any))
|
||||
.leftJoin(languages, eq(languages.id, entries.languageId as any))
|
||||
.where(eq(entries.genretypeId, genreId as any))
|
||||
.orderBy(entries.title)
|
||||
.limit(pageSize)
|
||||
.offset(offset);
|
||||
return { items: items as any, page, pageSize, total: Number(countRows?.[0]?.total ?? 0) };
|
||||
}
|
||||
|
||||
const pattern = `%${q!.toLowerCase().replace(/[^a-z0-9]+/g, "")}%`;
|
||||
const countRows = await db
|
||||
.select({ total: sql<number>`count(distinct ${entries.id})` })
|
||||
.from(entries)
|
||||
.where(eq(entries.genretypeId, genreId as any))) as unknown as { total: number }[];
|
||||
.where(and(eq(entries.genretypeId, genreId as any), sql`id in (select entry_id from ${searchByTitles} where ${searchByTitles.entryTitle} like ${pattern})`) as any);
|
||||
const total = Number((countRows as any)[0]?.total ?? 0);
|
||||
const items = await db
|
||||
.select({ id: entries.id, title: entries.title, isXrated: entries.isXrated, machinetypeId: entries.machinetypeId, languageId: entries.languageId })
|
||||
.select({
|
||||
id: entries.id,
|
||||
title: entries.title,
|
||||
isXrated: entries.isXrated,
|
||||
machinetypeId: entries.machinetypeId,
|
||||
machinetypeName: machinetypes.name,
|
||||
languageId: entries.languageId,
|
||||
languageName: languages.name,
|
||||
})
|
||||
.from(entries)
|
||||
.where(eq(entries.genretypeId, genreId as any))
|
||||
.leftJoin(machinetypes, eq(machinetypes.id, entries.machinetypeId as any))
|
||||
.leftJoin(languages, eq(languages.id, entries.languageId as any))
|
||||
.where(and(eq(entries.genretypeId, genreId as any), sql`id in (select entry_id from ${searchByTitles} where ${searchByTitles.entryTitle} like ${pattern})`) as any)
|
||||
.groupBy(entries.id)
|
||||
.orderBy(entries.title)
|
||||
.limit(pageSize)
|
||||
.offset(offset);
|
||||
return { items: items as any, page, pageSize, total: Number(countRows?.[0]?.total ?? 0) };
|
||||
return { items: items as any, page, pageSize, total };
|
||||
}
|
||||
|
||||
export async function entriesByLanguage(langId: string, page: number, pageSize: number): Promise<PagedResult<SearchResultItem>> {
|
||||
export async function entriesByLanguage(
|
||||
langId: string,
|
||||
page: number,
|
||||
pageSize: number,
|
||||
q?: string
|
||||
): Promise<PagedResult<SearchResultItem>> {
|
||||
const offset = (page - 1) * pageSize;
|
||||
const countRows = (await db
|
||||
.select({ total: sql<number>`count(*)` })
|
||||
const hasQ = !!(q && q.trim());
|
||||
|
||||
if (!hasQ) {
|
||||
const countRows = (await db
|
||||
.select({ total: sql<number>`count(*)` })
|
||||
.from(entries)
|
||||
.where(eq(entries.languageId, langId as any))) as unknown as { total: number }[];
|
||||
const items = await db
|
||||
.select({
|
||||
id: entries.id,
|
||||
title: entries.title,
|
||||
isXrated: entries.isXrated,
|
||||
machinetypeId: entries.machinetypeId,
|
||||
machinetypeName: machinetypes.name,
|
||||
languageId: entries.languageId,
|
||||
languageName: languages.name,
|
||||
})
|
||||
.from(entries)
|
||||
.leftJoin(machinetypes, eq(machinetypes.id, entries.machinetypeId as any))
|
||||
.leftJoin(languages, eq(languages.id, entries.languageId as any))
|
||||
.where(eq(entries.languageId, langId as any))
|
||||
.orderBy(entries.title)
|
||||
.limit(pageSize)
|
||||
.offset(offset);
|
||||
return { items: items as any, page, pageSize, total: Number(countRows?.[0]?.total ?? 0) };
|
||||
}
|
||||
|
||||
const pattern = `%${q!.toLowerCase().replace(/[^a-z0-9]+/g, "")}%`;
|
||||
const countRows = await db
|
||||
.select({ total: sql<number>`count(distinct ${entries.id})` })
|
||||
.from(entries)
|
||||
.where(eq(entries.languageId, langId as any))) as unknown as { total: number }[];
|
||||
.where(and(eq(entries.languageId, langId as any), sql`id in (select entry_id from ${searchByTitles} where ${searchByTitles.entryTitle} like ${pattern})`) as any);
|
||||
const total = Number((countRows as any)[0]?.total ?? 0);
|
||||
const items = await db
|
||||
.select({ id: entries.id, title: entries.title, isXrated: entries.isXrated, machinetypeId: entries.machinetypeId, languageId: entries.languageId })
|
||||
.select({
|
||||
id: entries.id,
|
||||
title: entries.title,
|
||||
isXrated: entries.isXrated,
|
||||
machinetypeId: entries.machinetypeId,
|
||||
machinetypeName: machinetypes.name,
|
||||
languageId: entries.languageId,
|
||||
languageName: languages.name,
|
||||
})
|
||||
.from(entries)
|
||||
.where(eq(entries.languageId, langId as any))
|
||||
.leftJoin(machinetypes, eq(machinetypes.id, entries.machinetypeId as any))
|
||||
.leftJoin(languages, eq(languages.id, entries.languageId as any))
|
||||
.where(and(eq(entries.languageId, langId as any), sql`id in (select entry_id from ${searchByTitles} where ${searchByTitles.entryTitle} like ${pattern})`) as any)
|
||||
.groupBy(entries.id)
|
||||
.orderBy(entries.title)
|
||||
.limit(pageSize)
|
||||
.offset(offset);
|
||||
return { items: items as any, page, pageSize, total: Number(countRows?.[0]?.total ?? 0) };
|
||||
return { items: items as any, page, pageSize, total };
|
||||
}
|
||||
|
||||
export async function entriesByMachinetype(mtId: number, page: number, pageSize: number): Promise<PagedResult<SearchResultItem>> {
|
||||
export async function entriesByMachinetype(
|
||||
mtId: number,
|
||||
page: number,
|
||||
pageSize: number,
|
||||
q?: string
|
||||
): Promise<PagedResult<SearchResultItem>> {
|
||||
const offset = (page - 1) * pageSize;
|
||||
const countRows = (await db
|
||||
.select({ total: sql<number>`count(*)` })
|
||||
const hasQ = !!(q && q.trim());
|
||||
|
||||
if (!hasQ) {
|
||||
const countRows = (await db
|
||||
.select({ total: sql<number>`count(*)` })
|
||||
.from(entries)
|
||||
.where(eq(entries.machinetypeId, mtId as any))) as unknown as { total: number }[];
|
||||
const items = await db
|
||||
.select({
|
||||
id: entries.id,
|
||||
title: entries.title,
|
||||
isXrated: entries.isXrated,
|
||||
machinetypeId: entries.machinetypeId,
|
||||
machinetypeName: machinetypes.name,
|
||||
languageId: entries.languageId,
|
||||
languageName: languages.name,
|
||||
})
|
||||
.from(entries)
|
||||
.leftJoin(machinetypes, eq(machinetypes.id, entries.machinetypeId as any))
|
||||
.leftJoin(languages, eq(languages.id, entries.languageId as any))
|
||||
.where(eq(entries.machinetypeId, mtId as any))
|
||||
.orderBy(entries.title)
|
||||
.limit(pageSize)
|
||||
.offset(offset);
|
||||
return { items: items as any, page, pageSize, total: Number(countRows?.[0]?.total ?? 0) };
|
||||
}
|
||||
|
||||
const pattern = `%${q!.toLowerCase().replace(/[^a-z0-9]+/g, "")}%`;
|
||||
const countRows = await db
|
||||
.select({ total: sql<number>`count(distinct ${entries.id})` })
|
||||
.from(entries)
|
||||
.where(eq(entries.machinetypeId, mtId as any))) as unknown as { total: number }[];
|
||||
.where(and(eq(entries.machinetypeId, mtId as any), sql`id in (select entry_id from ${searchByTitles} where ${searchByTitles.entryTitle} like ${pattern})`) as any);
|
||||
const total = Number((countRows as any)[0]?.total ?? 0);
|
||||
const items = await db
|
||||
.select({ id: entries.id, title: entries.title, isXrated: entries.isXrated, machinetypeId: entries.machinetypeId, languageId: entries.languageId })
|
||||
.select({
|
||||
id: entries.id,
|
||||
title: entries.title,
|
||||
isXrated: entries.isXrated,
|
||||
machinetypeId: entries.machinetypeId,
|
||||
machinetypeName: machinetypes.name,
|
||||
languageId: entries.languageId,
|
||||
languageName: languages.name,
|
||||
})
|
||||
.from(entries)
|
||||
.where(eq(entries.machinetypeId, mtId as any))
|
||||
.leftJoin(machinetypes, eq(machinetypes.id, entries.machinetypeId as any))
|
||||
.leftJoin(languages, eq(languages.id, entries.languageId as any))
|
||||
.where(and(eq(entries.machinetypeId, mtId as any), sql`id in (select entry_id from ${searchByTitles} where ${searchByTitles.entryTitle} like ${pattern})`) as any)
|
||||
.groupBy(entries.id)
|
||||
.orderBy(entries.title)
|
||||
.limit(pageSize)
|
||||
.offset(offset);
|
||||
return { items: items as any, page, pageSize, total: Number(countRows?.[0]?.total ?? 0) };
|
||||
return { items: items as any, page, pageSize, total };
|
||||
}
|
||||
|
||||
// ----- Facets for search -----
|
||||
|
||||
Reference in New Issue
Block a user