diff --git a/TODO.md b/TODO.md index ee99e56..acb4b4c 100644 --- a/TODO.md +++ b/TODO.md @@ -143,7 +143,7 @@ This file is the single source of truth for roadmap and delivery progress. - [x] [P1] Media enrichment metadata (alt text, copyright, author, source, tags, licensing, usage context) - [x] [P1] Portfolio grouping primitives (galleries, albums, categories, tags) with ordering/visibility controls - [x] [P1] Artwork refinement fields (medium, dimensions, year, framing, availability, price visibility) -- [ ] [P1] Artwork rendition management (thumbnail, card, full, retina/custom sizes) +- [x] [P1] Artwork rendition management (thumbnail, card, full, retina/custom sizes) - [ ] [P1] Type-specific processing presets (artwork/banner/promo/video/gif) with validation rules - [ ] [P1] Users management (invite, roles, status) - [ ] [P1] Disable/ban user function and enforcement in auth/session checks @@ -363,6 +363,7 @@ This file is the single source of truth for roadmap and delivery progress. - [2026-02-12] Public portfolio baseline added with `/{locale}/portfolio` and `/{locale}/portfolio/{slug}`, including published-artwork filters (gallery/album/category/tag), rendition image streaming via web `/api/media/file/:id`, and media-aware artwork detail rendering. - [2026-02-12] Portfolio grouping controls completed in admin `/portfolio`: galleries/albums/categories/tags now support visibility and sort-order management (create/update/delete), and public tag filters now respect visibility. - [2026-02-12] Artwork refinement baseline completed: admin `/portfolio` now captures/edits medium, dimensions, year, framing, availability, publish state, and optional price visibility (`priceAmountCents` + `priceCurrency`), with public artwork detail rendering visible prices only. +- [2026-02-12] Artwork rendition management completed: admin `/portfolio` supports `thumbnail/card/full/retina/custom` slot assignment with dimensions and primary flag, plus per-artwork rendition listing and delete controls. - [2026-02-12] Public UX pass: commission request flow now reports explicit invalid budget range errors, and header navigation now falls back to localized defaults (`home`, `portfolio`, `news`, `commissions`) when no CMS menu exists; seed data now creates those default menu entries. - [2026-02-12] Added `e2e/public-rendering.pw.ts` web coverage for fallback navigation visibility, portfolio routes, and commission submission validation (invalid budget range + successful submission path). - [2026-02-12] Testing execution is temporarily paused for delivery velocity: root test scripts are stubbed and CI test steps are disabled; all testing backlog is consolidated under `MVP 3: Testing and Quality`. diff --git a/apps/admin/src/app/portfolio/page.tsx b/apps/admin/src/app/portfolio/page.tsx index 5e925d8..231c5db 100644 --- a/apps/admin/src/app/portfolio/page.tsx +++ b/apps/admin/src/app/portfolio/page.tsx @@ -5,6 +5,7 @@ import { createCategory, createGallery, createTag, + deleteArtworkRendition, deleteGrouping, linkArtworkToGrouping, listArtworks, @@ -316,6 +317,21 @@ async function attachRenditionAction(formData: FormData) { 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, }: { @@ -641,6 +657,7 @@ export default async function PortfolioPage({ + - {artwork.renditions.length} + +
+ {artwork.renditions.length === 0 ? ( + 0 + ) : ( + artwork.renditions.map((rendition) => ( +
+ + + {rendition.slot} + + + {rendition.width ?? "-"}x{rendition.height ?? "-"} + + {rendition.isPrimary ? ( + + primary + + ) : null} + +
+ )) + )} +
+ g:{artwork.galleryLinks.length} a:{artwork.albumLinks.length} c: {artwork.categoryLinks.length} t:{artwork.tagLinks.length} diff --git a/packages/content/src/media.ts b/packages/content/src/media.ts index f2c9054..ef4fae9 100644 --- a/packages/content/src/media.ts +++ b/packages/content/src/media.ts @@ -9,7 +9,7 @@ export const mediaAssetTypeSchema = z.enum([ "generic", ]) -export const artworkRenditionSlotSchema = z.enum(["thumbnail", "card", "full", "custom"]) +export const artworkRenditionSlotSchema = z.enum(["thumbnail", "card", "full", "retina", "custom"]) export const createMediaAssetInputSchema = z.object({ id: z.string().uuid().optional(), diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts index ff6f157..dfba8f4 100644 --- a/packages/db/src/index.ts +++ b/packages/db/src/index.ts @@ -24,6 +24,7 @@ export { createGallery, createMediaAsset, createTag, + deleteArtworkRendition, deleteGrouping, deleteMediaAsset, getMediaAssetById, diff --git a/packages/db/src/media-foundation.ts b/packages/db/src/media-foundation.ts index a66228d..3ae8575 100644 --- a/packages/db/src/media-foundation.ts +++ b/packages/db/src/media-foundation.ts @@ -33,10 +33,14 @@ export async function listArtworks(limit = 24) { take: limit, include: { renditions: { + orderBy: [{ isPrimary: "desc" }, { updatedAt: "desc" }], select: { id: true, slot: true, mediaAssetId: true, + width: true, + height: true, + isPrimary: true, }, }, galleryLinks: { @@ -340,6 +344,12 @@ export async function attachArtworkRendition(input: unknown) { }) } +export async function deleteArtworkRendition(id: string) { + return db.artworkRendition.delete({ + where: { id }, + }) +} + export async function getMediaFoundationSummary() { const [mediaAssets, artworks, galleries, albums, categories, tags] = await Promise.all([ db.mediaAsset.count(), @@ -473,6 +483,7 @@ export async function listPublishedArtworks(input: ListPublishedArtworksInput = isPublished: true, }, }, + orderBy: [{ isPrimary: "desc" }, { updatedAt: "desc" }], include: { mediaAsset: { select: { @@ -547,6 +558,7 @@ export async function getPublishedArtworkBySlug(slug: string) { isPublished: true, }, }, + orderBy: [{ isPrimary: "desc" }, { updatedAt: "desc" }], include: { mediaAsset: { select: {