feat(media): add enrichment metadata fields across admin and public
This commit is contained in:
@@ -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<Response> {
|
||||
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,
|
||||
|
||||
@@ -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
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<label className="space-y-1">
|
||||
<span className="text-xs text-neutral-600">License type</span>
|
||||
<input
|
||||
name="licenseType"
|
||||
defaultValue={mediaAsset.licenseType ?? ""}
|
||||
className="w-full rounded border border-neutral-300 px-3 py-2 text-sm"
|
||||
/>
|
||||
</label>
|
||||
<label className="space-y-1">
|
||||
<span className="text-xs text-neutral-600">License URL</span>
|
||||
<input
|
||||
name="licenseUrl"
|
||||
defaultValue={mediaAsset.licenseUrl ?? ""}
|
||||
className="w-full rounded border border-neutral-300 px-3 py-2 text-sm"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<label className="space-y-1">
|
||||
<span className="text-xs text-neutral-600">Usage context</span>
|
||||
<input
|
||||
name="usageContext"
|
||||
defaultValue={mediaAsset.usageContext ?? ""}
|
||||
className="w-full rounded border border-neutral-300 px-3 py-2 text-sm"
|
||||
/>
|
||||
</label>
|
||||
<label className="space-y-1">
|
||||
<span className="text-xs text-neutral-600">Location</span>
|
||||
<input
|
||||
name="location"
|
||||
defaultValue={mediaAsset.location ?? ""}
|
||||
className="w-full rounded border border-neutral-300 px-3 py-2 text-sm"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label className="space-y-1">
|
||||
<span className="text-xs text-neutral-600">Captured at</span>
|
||||
<input
|
||||
name="capturedAt"
|
||||
type="datetime-local"
|
||||
defaultValue={
|
||||
mediaAsset.capturedAt ? toLocalDateTimeInputValue(mediaAsset.capturedAt) : ""
|
||||
}
|
||||
className="w-full rounded border border-neutral-300 px-3 py-2 text-sm"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className="grid gap-3 md:grid-cols-3">
|
||||
<label className="space-y-1">
|
||||
<span className="text-xs text-neutral-600">MIME type</span>
|
||||
|
||||
@@ -149,6 +149,52 @@ export function MediaUploadForm() {
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<label className="space-y-1">
|
||||
<span className="text-xs text-neutral-600">License type</span>
|
||||
<input
|
||||
name="licenseType"
|
||||
placeholder="e.g. CC BY-NC 4.0"
|
||||
className="w-full rounded border border-neutral-300 px-3 py-2 text-sm"
|
||||
/>
|
||||
</label>
|
||||
<label className="space-y-1">
|
||||
<span className="text-xs text-neutral-600">License URL</span>
|
||||
<input
|
||||
name="licenseUrl"
|
||||
className="w-full rounded border border-neutral-300 px-3 py-2 text-sm"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<label className="space-y-1">
|
||||
<span className="text-xs text-neutral-600">Usage context</span>
|
||||
<input
|
||||
name="usageContext"
|
||||
placeholder="e.g. homepage hero, social preview"
|
||||
className="w-full rounded border border-neutral-300 px-3 py-2 text-sm"
|
||||
/>
|
||||
</label>
|
||||
<label className="space-y-1">
|
||||
<span className="text-xs text-neutral-600">Location</span>
|
||||
<input
|
||||
name="location"
|
||||
placeholder="e.g. Berlin studio"
|
||||
className="w-full rounded border border-neutral-300 px-3 py-2 text-sm"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label className="space-y-1">
|
||||
<span className="text-xs text-neutral-600">Captured at</span>
|
||||
<input
|
||||
name="capturedAt"
|
||||
type="datetime-local"
|
||||
className="w-full rounded border border-neutral-300 px-3 py-2 text-sm"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="space-y-1">
|
||||
<span className="text-xs text-neutral-600">Tags (comma-separated)</span>
|
||||
<input name="tags" className="w-full rounded border border-neutral-300 px-3 py-2 text-sm" />
|
||||
|
||||
@@ -25,6 +25,8 @@ export default async function PublicArtworkPage({ params }: PublicArtworkPagePro
|
||||
notFound()
|
||||
}
|
||||
|
||||
const primaryMedia = artwork.renditions[0]?.mediaAsset ?? null
|
||||
|
||||
return (
|
||||
<section className="mx-auto w-full max-w-5xl space-y-6 px-6 py-16">
|
||||
<header className="space-y-2">
|
||||
@@ -94,6 +96,22 @@ export default async function PublicArtworkPage({ params }: PublicArtworkPagePro
|
||||
<strong>{t("fields.tags")}:</strong>{" "}
|
||||
{formatLabelList(artwork.tagLinks.map((entry) => entry.tag.name))}
|
||||
</p>
|
||||
<p>
|
||||
<strong>{t("fields.licenseType")}:</strong> {primaryMedia?.licenseType || "-"}
|
||||
</p>
|
||||
<p>
|
||||
<strong>{t("fields.licenseUrl")}:</strong> {primaryMedia?.licenseUrl || "-"}
|
||||
</p>
|
||||
<p>
|
||||
<strong>{t("fields.usageContext")}:</strong> {primaryMedia?.usageContext || "-"}
|
||||
</p>
|
||||
<p>
|
||||
<strong>{t("fields.location")}:</strong> {primaryMedia?.location || "-"}
|
||||
</p>
|
||||
<p>
|
||||
<strong>{t("fields.capturedAt")}:</strong>{" "}
|
||||
{primaryMedia?.capturedAt ? primaryMedia.capturedAt.toLocaleDateString("en-US") : "-"}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
@@ -87,7 +87,12 @@
|
||||
"galleries": "Galerien",
|
||||
"albums": "Alben",
|
||||
"categories": "Kategorien",
|
||||
"tags": "Tags"
|
||||
"tags": "Tags",
|
||||
"licenseType": "Lizenztyp",
|
||||
"licenseUrl": "Lizenz-URL",
|
||||
"usageContext": "Nutzungskontext",
|
||||
"location": "Ort",
|
||||
"capturedAt": "Aufgenommen am"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +87,12 @@
|
||||
"galleries": "Galleries",
|
||||
"albums": "Albums",
|
||||
"categories": "Categories",
|
||||
"tags": "Tags"
|
||||
"tags": "Tags",
|
||||
"licenseType": "License type",
|
||||
"licenseUrl": "License URL",
|
||||
"usageContext": "Usage context",
|
||||
"location": "Location",
|
||||
"capturedAt": "Captured at"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +87,12 @@
|
||||
"galleries": "Galerías",
|
||||
"albums": "Álbumes",
|
||||
"categories": "Categorías",
|
||||
"tags": "Etiquetas"
|
||||
"tags": "Etiquetas",
|
||||
"licenseType": "Tipo de licencia",
|
||||
"licenseUrl": "URL de licencia",
|
||||
"usageContext": "Contexto de uso",
|
||||
"location": "Ubicación",
|
||||
"capturedAt": "Capturado el"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +87,12 @@
|
||||
"galleries": "Galeries",
|
||||
"albums": "Albums",
|
||||
"categories": "Catégories",
|
||||
"tags": "Tags"
|
||||
"tags": "Tags",
|
||||
"licenseType": "Type de licence",
|
||||
"licenseUrl": "URL de licence",
|
||||
"usageContext": "Contexte d'utilisation",
|
||||
"location": "Lieu",
|
||||
"capturedAt": "Capturé le"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user