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" />
|
||||
|
||||
Reference in New Issue
Block a user