240 lines
6.0 KiB
TypeScript
240 lines
6.0 KiB
TypeScript
"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 };
|
|
}
|