"use server"; import type { Prisma } from "@/generated/prisma/browser"; import { prisma } from "@/lib/prisma"; export type Cursor = { afterSortKey: number | null; afterId: string; } | null; export type PortfolioArtworkItem = { id: string; name: string; altText: string | null; sortKey: number | null; year: number | null; fileKey: string; thumbW: number; thumbH: number; dominantHex: string; }; export type PortfolioFilters = { year?: number | "all" | null; albumId?: string | "all" | null; q?: string | null; }; function coerceYear(y: PortfolioFilters["year"]) { if (y === "all" || y == null) return null; if (typeof y === "number") return Number.isFinite(y) ? y : null; return null; } function normQ(q?: string | null) { const s = (q ?? "").trim(); return s.length ? s : null; } type VariantPick = { type: string; width: number; height: number }; function pickVariant(variants: VariantPick[], type: string) { return variants.find((v) => v.type === type) ?? null; } export async function getPortfolioArtworksPage(args: { take?: number; cursor?: Cursor; filters?: PortfolioFilters; onlyPublished?: boolean; }): Promise<{ items: PortfolioArtworkItem[]; nextCursor: Cursor; total: number; years: number[]; albums: Array<{ id: string; name: string }>; }> { const { take = 60, cursor = null, filters = {}, onlyPublished = true } = args; const year = coerceYear(filters.year ?? null); const q = normQ(filters.q); const albumId = filters.albumId && filters.albumId !== "all" ? filters.albumId : null; const baseWhere: Prisma.ArtworkWhereInput = { ...(onlyPublished ? { published: true } : {}), ...(year != null ? { year } : {}), ...(albumId ? { albums: { some: { id: albumId } } } : {}), variants: { some: { type: "thumbnail" } }, }; const where: Prisma.ArtworkWhereInput = q ? { AND: [ baseWhere, { OR: [ { name: { contains: q, mode: "insensitive" } }, { tags: { some: { name: { contains: q, mode: "insensitive" } } }, }, ], }, ], } : baseWhere; const [yearsRaw, albums, total] = await Promise.all([ prisma.artwork.findMany({ where: { ...(onlyPublished ? { published: true } : {}) }, distinct: ["year"], select: { year: true }, orderBy: [{ year: "desc" }], }), prisma.album.findMany({ orderBy: [{ sortIndex: "asc" }, { name: "asc" }], select: { id: true, name: true }, }), prisma.artwork.count({ where }), ]); const years = yearsRaw .map((r) => r.year) .filter((y): y is number => typeof y === "number") .sort((a, b) => b - a); const inNullSegment = cursor?.afterSortKey === null; const select = { id: true, name: true, altText: true, year: true, sortKey: true, file: { select: { fileKey: true } }, variants: { where: { type: "thumbnail" }, select: { type: true, width: true, height: true }, take: 1, }, colors: { where: { type: "Vibrant" }, select: { color: { select: { hex: true } } }, take: 1, }, } satisfies Prisma.ArtworkSelect; type ArtworkRow = Prisma.ArtworkGetPayload<{ select: typeof select }>; const mapRow = (r: ArtworkRow): PortfolioArtworkItem | null => { const thumb = pickVariant(r.variants, "thumbnail"); if (!thumb?.width || !thumb?.height) return null; return { id: r.id, name: r.name, altText: r.altText ?? null, sortKey: r.sortKey ?? null, year: r.year ?? null, fileKey: r.file.fileKey, thumbW: thumb.width, thumbH: thumb.height, dominantHex: r.colors[0]?.color?.hex ?? "#999999", }; }; let items: PortfolioArtworkItem[] = []; let nextCursor: Cursor = null; if (!inNullSegment) { const whereA: Prisma.ArtworkWhereInput = { AND: [where, { sortKey: { not: null } }], }; if (cursor?.afterSortKey != null) { const sk = Number(cursor.afterSortKey); whereA.OR = [ { sortKey: { gt: sk } }, { AND: [{ sortKey: sk }, { id: { gt: cursor.afterId } }] }, ]; } const rowsA = await prisma.artwork.findMany({ where: whereA, orderBy: [{ sortKey: "asc" }, { id: "asc" }], take: Math.min(take, 200), select, }); items = rowsA .map(mapRow) .filter((x): x is PortfolioArtworkItem => x !== null); if (items.length >= take) { const last = items.at(-1); if (!last) { return { items, nextCursor: null, total, years, albums }; } if (last.sortKey == null) { return { items, nextCursor: null, total, years, albums }; } nextCursor = { afterSortKey: last.sortKey, afterId: last.id }; return { items, nextCursor, total, years, albums }; } const remaining = take - items.length; const whereB: Prisma.ArtworkWhereInput = { AND: [where, { sortKey: null }], }; const rowsB = await prisma.artwork.findMany({ where: whereB, orderBy: [{ id: "asc" }], take: Math.min(remaining, 200), select, }); const more = rowsB .map(mapRow) .filter((x): x is PortfolioArtworkItem => x !== null); items = items.concat(more); const last = items[items.length - 1]; nextCursor = items.length < take || !last ? null : { afterSortKey: last.sortKey ?? null, afterId: last.id }; return { items, nextCursor, total, years, albums }; } const whereB: Prisma.ArtworkWhereInput = { AND: [where, { sortKey: null }], ...(cursor ? { id: { gt: cursor.afterId } } : {}), }; const rowsB = await prisma.artwork.findMany({ where: whereB, orderBy: [{ id: "asc" }], take: Math.min(take, 200), select, }); items = rowsB .map(mapRow) .filter((x): x is PortfolioArtworkItem => x !== null); const last = items[items.length - 1]; nextCursor = items.length < take || !last ? null : { afterSortKey: null, afterId: last.id }; return { items, nextCursor, total, years, albums }; }