diff --git a/TODO.md b/TODO.md index c1c92c1..3c5f2eb 100644 --- a/TODO.md +++ b/TODO.md @@ -140,7 +140,7 @@ This file is the single source of truth for roadmap and delivery progress. - [ ] [P1] Page builder with reusable content blocks (hero, rich text, gallery, CTA, forms, price cards) - [~] [P1] Navigation management (menus, nested items, order, visibility) - [~] [P1] Media library (upload, browse, replace, delete) with media-type classification (artwork, banner, promo, generic, video/gif) -- [ ] [P1] Media enrichment metadata (alt text, copyright, author, source, tags, licensing, usage context) +- [x] [P1] Media enrichment metadata (alt text, copyright, author, source, tags, licensing, usage context) - [ ] [P1] Portfolio grouping primitives (galleries, albums, categories, tags) with ordering/visibility controls - [ ] [P1] Artwork refinement fields (medium, dimensions, year, framing, availability, price visibility) - [ ] [P1] Artwork rendition management (thumbnail, card, full, retina/custom sizes) @@ -343,6 +343,7 @@ This file is the single source of truth for roadmap and delivery progress. - [2026-02-12] Upload storage is now provider-based (`local` + `s3`) via `CMS_MEDIA_STORAGE_PROVIDER`; admin-side GUI toggle remains a later MVP item. - [2026-02-12] Media storage keys now use asset-centric layout (`tenant//asset///__.`) with DB-managed media taxonomy. - [2026-02-12] Admin media CRUD now includes list-to-detail flow (`/media/:id`) with metadata edit and delete actions. +- [2026-02-12] Media enrichment metadata baseline completed: `MediaAsset` now supports licensing/usage/location/captured-at fields across upload input, admin editor, and public artwork detail rendering. - [2026-02-12] MVP1 pages/navigation baseline started: `Page`, `NavigationMenu`, and `NavigationItem` models plus admin CRUD routes (`/pages`, `/pages/:id`, `/navigation`). - [2026-02-12] Public app now renders CMS-managed navigation (header) and CMS-managed pages by slug (including homepage when `home` page exists). - [2026-02-12] Commissions/customer baseline added: admin `/commissions` now supports customer creation, commission intake, status transitions, and a basic kanban board. diff --git a/apps/admin/src/app/api/media/upload/route.ts b/apps/admin/src/app/api/media/upload/route.ts index 7537e54..47e9f6f 100644 --- a/apps/admin/src/app/api/media/upload/route.ts +++ b/apps/admin/src/app/api/media/upload/route.ts @@ -58,6 +58,22 @@ function parseTags(formData: FormData): string[] { .filter((item) => item.length > 0) } +function parseOptionalDateField(formData: FormData, field: string): Date | undefined { + const value = parseTextField(formData, field) + + if (!value) { + return undefined + } + + const parsed = new Date(value) + + if (Number.isNaN(parsed.getTime())) { + return undefined + } + + return parsed +} + function deriveTitleFromFilename(fileName: string): string { const trimmed = fileName.trim() @@ -178,6 +194,11 @@ export async function POST(request: Request): Promise { source: parseOptionalField(formData, "source"), copyright: parseOptionalField(formData, "copyright"), author: parseOptionalField(formData, "author"), + licenseType: parseOptionalField(formData, "licenseType"), + licenseUrl: parseOptionalField(formData, "licenseUrl"), + usageContext: parseOptionalField(formData, "usageContext"), + location: parseOptionalField(formData, "location"), + capturedAt: parseOptionalDateField(formData, "capturedAt"), tags: parseTags(formData), storageKey: stored.storageKey, mimeType: fileEntry.type || undefined, diff --git a/apps/admin/src/app/media/[id]/page.tsx b/apps/admin/src/app/media/[id]/page.tsx index dd62a32..e856651 100644 --- a/apps/admin/src/app/media/[id]/page.tsx +++ b/apps/admin/src/app/media/[id]/page.tsx @@ -50,6 +50,22 @@ function readNullableInt(formData: FormData, field: string): number | null { return parsed } +function readNullableDate(formData: FormData, field: string): Date | null { + const value = readInputString(formData, field) + + if (!value) { + return null + } + + const parsed = new Date(value) + + if (Number.isNaN(parsed.getTime())) { + return null + } + + return parsed +} + function readTags(formData: FormData): string[] { const raw = readInputString(formData, "tags") @@ -127,6 +143,11 @@ export default async function MediaAssetEditorPage({ params, searchParams }: Pag source: readNullableString(formData, "source"), copyright: readNullableString(formData, "copyright"), author: readNullableString(formData, "author"), + licenseType: readNullableString(formData, "licenseType"), + licenseUrl: readNullableString(formData, "licenseUrl"), + usageContext: readNullableString(formData, "usageContext"), + location: readNullableString(formData, "location"), + capturedAt: readNullableDate(formData, "capturedAt"), tags: readTags(formData), mimeType: readNullableString(formData, "mimeType"), width: readNullableInt(formData, "width"), @@ -320,6 +341,56 @@ export default async function MediaAssetEditorPage({ params, searchParams }: Pag +
+ + +
+ +
+ + +
+ + +
+
+ + +
+ +
+ + +
+ + +