feat(media): add enrichment metadata fields across admin and public

This commit is contained in:
2026-02-12 22:42:08 +01:00
parent 6e9c0ad4e5
commit b9424c8a8b
14 changed files with 212 additions and 5 deletions

View File

@@ -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,

View File

@@ -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>

View File

@@ -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" />