Refactor porfolio page
This commit is contained in:
76
src/actions/portfolio/getPortfolioArtworksMeta.ts
Normal file
76
src/actions/portfolio/getPortfolioArtworksMeta.ts
Normal file
@ -0,0 +1,76 @@
|
||||
"use server";
|
||||
|
||||
import type { PortfolioFilters } from "@/actions/portfolio/getPortfolioArtworksPage";
|
||||
import { Prisma } from "@/generated/prisma/browser";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export async function getPortfolioArtworksMeta(args: {
|
||||
filters: PortfolioFilters;
|
||||
onlyPublished?: boolean;
|
||||
}): Promise<{
|
||||
total: number;
|
||||
years: number[];
|
||||
albums: Array<{ id: string; name: string }>;
|
||||
}> {
|
||||
const { filters, onlyPublished = true } = args;
|
||||
|
||||
const year = coerceYear(filters.year);
|
||||
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" } },
|
||||
{ slug: { contains: q, mode: "insensitive" } },
|
||||
{ altText: { contains: q, mode: "insensitive" } },
|
||||
{ tags: { some: { name: { contains: q, mode: "insensitive" } } } },
|
||||
{ albums: { some: { name: { contains: q, mode: "insensitive" } } } },
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
: baseWhere;
|
||||
|
||||
const [total, yearsRaw, albums] = await Promise.all([
|
||||
prisma.artwork.count({ where }),
|
||||
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 },
|
||||
}),
|
||||
]);
|
||||
|
||||
const years = yearsRaw
|
||||
.map((r) => r.year)
|
||||
.filter((y): y is number => typeof y === "number")
|
||||
.sort((a, b) => b - a);
|
||||
|
||||
return { total, years, albums };
|
||||
}
|
||||
@ -26,24 +26,22 @@ export type PortfolioArtworkItem = {
|
||||
};
|
||||
|
||||
export type PortfolioFilters = {
|
||||
year?: number | null;
|
||||
galleryId?: string | null;
|
||||
albumId?: string | null;
|
||||
tagIds?: string[];
|
||||
search?: string;
|
||||
year?: number | "all" | null;
|
||||
albumId?: string | "all" | null;
|
||||
q?: string | null;
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
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;
|
||||
@ -54,44 +52,76 @@ export async function getPortfolioArtworksPage(args: {
|
||||
cursor?: Cursor;
|
||||
filters?: PortfolioFilters;
|
||||
onlyPublished?: boolean;
|
||||
}): Promise<{ items: PortfolioArtworkItem[]; nextCursor: Cursor }> {
|
||||
}): 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 } : {}),
|
||||
...(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" } },
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
...(albumId ? { albums: { some: { id: albumId } } } : {}),
|
||||
variants: { some: { type: "thumbnail" } },
|
||||
};
|
||||
|
||||
// Require thumbnail variant for grid placement
|
||||
const baseSelect = {
|
||||
const where: Prisma.ArtworkWhereInput = q
|
||||
? {
|
||||
AND: [
|
||||
baseWhere,
|
||||
{
|
||||
OR: [
|
||||
{ name: { contains: q, mode: "insensitive" } },
|
||||
{ slug: { contains: q, mode: "insensitive" } },
|
||||
{ altText: { contains: q, mode: "insensitive" } },
|
||||
{ tags: { some: { name: { contains: q, mode: "insensitive" } } } },
|
||||
{ albums: { 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);
|
||||
|
||||
// Segment logic (sortKey != null first, then null)
|
||||
const inNullSegment = cursor?.afterSortKey === null;
|
||||
|
||||
const select = {
|
||||
id: true,
|
||||
name: true,
|
||||
slug: true,
|
||||
altText: true,
|
||||
year: true,
|
||||
sortKey: true,
|
||||
file: { select: { fileKey: true } },
|
||||
variants: {
|
||||
where: { type: { in: ["thumbnail"] } },
|
||||
where: { type: "thumbnail" },
|
||||
select: { type: true, width: true, height: true },
|
||||
take: 1,
|
||||
},
|
||||
colors: {
|
||||
where: { type: "Vibrant" },
|
||||
@ -118,20 +148,12 @@ export async function getPortfolioArtworksPage(args: {
|
||||
};
|
||||
};
|
||||
|
||||
// --- 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" } },
|
||||
AND: [where, { sortKey: { not: null } }],
|
||||
};
|
||||
|
||||
if (cursor?.afterSortKey != null) {
|
||||
@ -146,35 +168,30 @@ export async function getPortfolioArtworksPage(args: {
|
||||
where: whereA,
|
||||
orderBy: [{ sortKey: "asc" }, { id: "asc" }],
|
||||
take: Math.min(take, 200),
|
||||
select: baseSelect,
|
||||
select,
|
||||
});
|
||||
|
||||
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 };
|
||||
return { items, nextCursor, total, years, albums };
|
||||
}
|
||||
|
||||
// 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 lastAId = items.length ? 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
|
||||
AND: [where, { sortKey: null }],
|
||||
...(lastAId ? { id: { gt: lastAId } } : {}),
|
||||
};
|
||||
|
||||
const rowsB = await prisma.artwork.findMany({
|
||||
where: whereB,
|
||||
orderBy: [{ id: "asc" }],
|
||||
take: Math.min(remaining, 200),
|
||||
select: baseSelect,
|
||||
select,
|
||||
});
|
||||
|
||||
const more = rowsB.map(mapRow).filter((x): x is PortfolioArtworkItem => x !== null);
|
||||
@ -186,14 +203,11 @@ export async function getPortfolioArtworksPage(args: {
|
||||
? null
|
||||
: { afterSortKey: last.sortKey ?? null, afterId: last.id };
|
||||
|
||||
return { items, nextCursor };
|
||||
return { items, nextCursor, total, years, albums };
|
||||
}
|
||||
|
||||
// Query B (null segment continuation)
|
||||
const whereB: Prisma.ArtworkWhereInput = {
|
||||
...baseWhere,
|
||||
sortKey: null,
|
||||
variants: { some: { type: "thumbnail" } },
|
||||
AND: [where, { sortKey: null }],
|
||||
...(cursor ? { id: { gt: cursor.afterId } } : {}),
|
||||
};
|
||||
|
||||
@ -201,7 +215,7 @@ export async function getPortfolioArtworksPage(args: {
|
||||
where: whereB,
|
||||
orderBy: [{ id: "asc" }],
|
||||
take: Math.min(take, 200),
|
||||
select: baseSelect,
|
||||
select,
|
||||
});
|
||||
|
||||
items = rowsB.map(mapRow).filter((x): x is PortfolioArtworkItem => x !== null);
|
||||
@ -210,5 +224,5 @@ export async function getPortfolioArtworksPage(args: {
|
||||
nextCursor =
|
||||
items.length < take || !last ? null : { afterSortKey: null, afterId: last.id };
|
||||
|
||||
return { items, nextCursor };
|
||||
return { items, nextCursor, total, years, albums };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user