import { attachArtworkRendition, createAlbum, createArtwork, createCategory, createGallery, createTag, deleteArtworkRendition, deleteGrouping, linkArtworkToGrouping, listArtworks, listMediaAssets, listMediaFoundationGroups, updateArtwork, updateGrouping, } from "@cms/db" import { Button } from "@cms/ui/button" import { revalidatePath } from "next/cache" import { redirect } from "next/navigation" import { AdminShell } from "@/components/admin-shell" import { requirePermissionForRoute } from "@/lib/route-guards" export const dynamic = "force-dynamic" type SearchParamsInput = Record type GroupType = "gallery" | "album" | "category" | "tag" function readField(formData: FormData, key: string): string { const value = formData.get(key) return typeof value === "string" ? value.trim() : "" } function readOptionalField(formData: FormData, key: string): string | undefined { const value = readField(formData, key) return value.length > 0 ? value : undefined } function readOptionalNullableField(formData: FormData, key: string): string | null { const value = readField(formData, key) return value.length > 0 ? value : null } function readNonNegativeInt(formData: FormData, key: string): number { const raw = readField(formData, key) const value = Number(raw) return Number.isFinite(value) && value >= 0 ? Math.floor(value) : 0 } function readOptionalNonNegativeInt(formData: FormData, key: string): number | undefined { const raw = readField(formData, key) if (!raw) { return undefined } const value = Number(raw) return Number.isFinite(value) && value >= 0 ? Math.floor(value) : undefined } function readBooleanField(formData: FormData, key: string): boolean { return formData.get(key) === "on" || readField(formData, key) === "true" } function readFirstValue(value: string | string[] | undefined): string | null { if (Array.isArray(value)) { return value[0] ?? null } return value ?? null } function slugify(input: string): string { return input .toLowerCase() .trim() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "") .slice(0, 180) } function redirectWithState(params: { notice?: string; error?: string }) { const query = new URLSearchParams() if (params.notice) { query.set("notice", params.notice) } if (params.error) { query.set("error", params.error) } const value = query.toString() redirect(value ? `/portfolio?${value}` : "/portfolio") } async function requireWritePermission() { await requirePermissionForRoute({ nextPath: "/portfolio", permission: "media:write", scope: "team", }) } async function createArtworkAction(formData: FormData) { "use server" await requireWritePermission() const title = readField(formData, "title") const slug = slugify(readField(formData, "slug") || title) try { await createArtwork({ title, slug, description: readOptionalField(formData, "description"), medium: readOptionalField(formData, "medium"), dimensions: readOptionalField(formData, "dimensions"), framing: readOptionalField(formData, "framing"), availability: readOptionalField(formData, "availability"), priceAmountCents: (() => { const raw = readField(formData, "priceAmount") return raw ? Math.round(Number(raw) * 100) : undefined })(), priceCurrency: (() => { const raw = readField(formData, "priceCurrency").toUpperCase() return raw.length === 3 ? raw : undefined })(), isPriceVisible: readBooleanField(formData, "isPriceVisible"), year: (() => { const raw = readField(formData, "year") return raw ? Number(raw) : undefined })(), }) } catch { redirectWithState({ error: "Failed to create artwork." }) } revalidatePath("/portfolio") redirectWithState({ notice: "Artwork created." }) } async function updateArtworkAction(formData: FormData) { "use server" await requireWritePermission() try { await updateArtwork({ id: readField(formData, "artworkId"), medium: readOptionalNullableField(formData, "medium"), dimensions: readOptionalNullableField(formData, "dimensions"), year: (() => { const raw = readField(formData, "year") return raw ? Number(raw) : null })(), framing: readOptionalNullableField(formData, "framing"), availability: readOptionalNullableField(formData, "availability"), priceAmountCents: (() => { const value = readOptionalNonNegativeInt(formData, "priceAmountCents") return value ?? null })(), priceCurrency: (() => { const raw = readField(formData, "priceCurrency").toUpperCase() return raw.length === 3 ? raw : null })(), isPriceVisible: readBooleanField(formData, "isPriceVisible"), isPublished: readBooleanField(formData, "isPublished"), }) } catch { redirectWithState({ error: "Failed to update artwork refinement fields." }) } revalidatePath("/portfolio") redirectWithState({ notice: "Artwork refinement updated." }) } async function createGroupAction(formData: FormData) { "use server" await requireWritePermission() const type = readField(formData, "groupType") as GroupType const name = readField(formData, "name") const slug = slugify(readField(formData, "slug") || name) try { if (type === "gallery") { await createGallery({ name, slug, description: readOptionalField(formData, "description"), sortOrder: readNonNegativeInt(formData, "sortOrder"), isVisible: readBooleanField(formData, "isVisible"), }) } else if (type === "album") { await createAlbum({ name, slug, description: readOptionalField(formData, "description"), sortOrder: readNonNegativeInt(formData, "sortOrder"), isVisible: readBooleanField(formData, "isVisible"), }) } else if (type === "category") { await createCategory({ name, slug, description: readOptionalField(formData, "description"), sortOrder: readNonNegativeInt(formData, "sortOrder"), isVisible: readBooleanField(formData, "isVisible"), }) } else { await createTag({ name, slug, description: readOptionalField(formData, "description"), sortOrder: readNonNegativeInt(formData, "sortOrder"), isVisible: readBooleanField(formData, "isVisible"), }) } } catch { redirectWithState({ error: "Failed to create grouping entity." }) } revalidatePath("/portfolio") redirectWithState({ notice: `${type} created.` }) } async function updateGroupAction(formData: FormData) { "use server" await requireWritePermission() try { await updateGrouping({ groupType: readField(formData, "groupType"), groupId: readField(formData, "groupId"), name: readField(formData, "name"), slug: slugify(readField(formData, "slug")), description: readOptionalNullableField(formData, "description"), sortOrder: readNonNegativeInt(formData, "sortOrder"), isVisible: readBooleanField(formData, "isVisible"), }) } catch { redirectWithState({ error: "Failed to update grouping entity." }) } revalidatePath("/portfolio") redirectWithState({ notice: "Grouping entity updated." }) } async function deleteGroupAction(formData: FormData) { "use server" await requireWritePermission() try { await deleteGrouping({ groupType: readField(formData, "groupType"), groupId: readField(formData, "groupId"), }) } catch { redirectWithState({ error: "Failed to delete grouping entity." }) } revalidatePath("/portfolio") redirectWithState({ notice: "Grouping entity deleted." }) } async function linkArtworkGroupAction(formData: FormData) { "use server" await requireWritePermission() const artworkId = readField(formData, "artworkId") const groupType = readField(formData, "groupType") as GroupType const groupId = readField(formData, "groupId") try { await linkArtworkToGrouping({ artworkId, groupType, groupId, }) } catch { redirectWithState({ error: "Failed to link artwork to grouping." }) } revalidatePath("/portfolio") redirectWithState({ notice: "Artwork linked to grouping." }) } async function attachRenditionAction(formData: FormData) { "use server" await requireWritePermission() try { await attachArtworkRendition({ artworkId: readField(formData, "artworkId"), mediaAssetId: readField(formData, "mediaAssetId"), slot: readField(formData, "slot"), width: (() => { const raw = readField(formData, "width") return raw ? Number(raw) : undefined })(), height: (() => { const raw = readField(formData, "height") return raw ? Number(raw) : undefined })(), isPrimary: readField(formData, "isPrimary") === "true", }) } catch { redirectWithState({ error: "Failed to attach artwork rendition." }) } revalidatePath("/portfolio") redirectWithState({ notice: "Rendition attached." }) } async function deleteRenditionAction(formData: FormData) { "use server" await requireWritePermission() try { await deleteArtworkRendition(readField(formData, "renditionId")) } catch { redirectWithState({ error: "Failed to delete rendition." }) } revalidatePath("/portfolio") redirectWithState({ notice: "Rendition deleted." }) } export default async function PortfolioPage({ searchParams, }: { searchParams: Promise }) { const role = await requirePermissionForRoute({ nextPath: "/portfolio", permission: "media:read", scope: "team", }) const [resolvedSearchParams, artworks, mediaAssets, groups] = await Promise.all([ searchParams, listArtworks(30), listMediaAssets(200), listMediaFoundationGroups(), ]) const notice = readFirstValue(resolvedSearchParams.notice) const error = readFirstValue(resolvedSearchParams.error) return ( {notice ? (
{notice}
) : null} {error ? (
{error}
) : null}

Create Artwork