Add portfolio thingies

This commit is contained in:
2025-12-25 09:24:50 +01:00
parent e285e7b9af
commit 3bd555a17a
4 changed files with 462 additions and 0 deletions

View File

@ -0,0 +1,214 @@
"use server";
import { 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;
slug: string;
altText: string | null;
sortKey: number | null;
year: number | null;
fileKey: string;
thumbW: number;
thumbH: number;
dominantHex: string;
};
export type PortfolioFilters = {
year?: number | null;
galleryId?: string | null;
albumId?: string | null;
tagIds?: string[];
search?: string;
};
function coerceYear(year?: number | string | null) {
if (typeof year === "number") return Number.isFinite(year) ? year : null;
if (typeof year === "string") {
const t = year.trim();
if (!t) return null;
const n = Number(t);
return Number.isFinite(n) ? n : null;
}
return 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 }> {
const { take = 60, cursor = null, filters = {}, onlyPublished = true } = args;
const year = coerceYear(filters.year ?? null);
const baseWhere: Prisma.ArtworkWhereInput = {
...(onlyPublished ? { published: true } : {}),
...(year != null ? { year } : {}),
...(filters.galleryId ? { galleryId: filters.galleryId } : {}),
...(filters.albumId ? { albums: { some: { id: filters.albumId } } } : {}),
...(filters.tagIds?.length
? { tags: { some: { id: { in: filters.tagIds } } } }
: {}),
...(filters.search?.trim()
? {
OR: [
{ name: { contains: filters.search.trim(), mode: "insensitive" } },
{ slug: { contains: filters.search.trim(), mode: "insensitive" } },
{ altText: { contains: filters.search.trim(), mode: "insensitive" } },
],
}
: {}),
};
// Require thumbnail variant for grid placement
const baseSelect = {
id: true,
name: true,
slug: true,
altText: true,
year: true,
sortKey: true,
file: { select: { fileKey: true } },
variants: {
where: { type: { in: ["thumbnail"] } },
select: { type: true, width: true, height: true },
},
colors: {
where: { type: "Vibrant" },
select: { color: { select: { hex: true } } },
take: 1,
},
} satisfies Prisma.ArtworkSelect;
const mapRow = (r: any): PortfolioArtworkItem | null => {
const thumb = pickVariant(r.variants, "thumbnail");
if (!thumb?.width || !thumb?.height) return null;
return {
id: r.id,
name: r.name,
slug: r.slug,
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",
};
};
// --- Segment handling ---
// Segment A: sortKey != null orderBy sortKey asc, id asc
// Segment B: sortKey == null orderBy id asc
const inNullSegment = cursor?.afterSortKey === null;
// Query A (non-null segment) unless we're already in null segment
let items: PortfolioArtworkItem[] = [];
let nextCursor: Cursor = null;
if (!inNullSegment) {
const whereA: Prisma.ArtworkWhereInput = {
...baseWhere,
sortKey: { not: null },
variants: { some: { type: "thumbnail" } },
};
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: baseSelect,
});
items = rowsA.map(mapRow).filter((x): x is PortfolioArtworkItem => x !== null);
// If we fully satisfied take within segment A, return cursor in A
if (items.length >= take) {
const last = items[items.length - 1]!;
nextCursor = { afterSortKey: last.sortKey!, afterId: last.id };
return { items, nextCursor };
}
// Otherwise, we will transition into segment B and fill the remainder
const remaining = take - items.length;
const lastAId = items.length > 0 ? items[items.length - 1]!.id : null;
const whereB: Prisma.ArtworkWhereInput = {
...baseWhere,
sortKey: null,
variants: { some: { type: "thumbnail" } },
...(lastAId ? { id: { gt: lastAId } } : {}), // IMPORTANT: don't restart from beginning during transition
};
const rowsB = await prisma.artwork.findMany({
where: whereB,
orderBy: [{ id: "asc" }],
take: Math.min(remaining, 200),
select: baseSelect,
});
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 };
}
// Query B (null segment continuation)
const whereB: Prisma.ArtworkWhereInput = {
...baseWhere,
sortKey: null,
variants: { some: { type: "thumbnail" } },
...(cursor ? { id: { gt: cursor.afterId } } : {}),
};
const rowsB = await prisma.artwork.findMany({
where: whereB,
orderBy: [{ id: "asc" }],
take: Math.min(take, 200),
select: baseSelect,
});
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 };
}