import { deleteMediaAsset, getMediaAssetById, updateMediaAsset } from "@cms/db" import { Button } from "@cms/ui/button" import Link from "next/link" import { redirect } from "next/navigation" import { AdminShell } from "@/components/admin-shell" import { deleteStoredMediaObject } from "@/lib/media/storage" import { requirePermissionForRoute } from "@/lib/route-guards" export const dynamic = "force-dynamic" type SearchParamsInput = Record type PageProps = { params: Promise<{ id: string }> searchParams: Promise } function readFirstValue(value: string | string[] | undefined): string | null { if (Array.isArray(value)) { return value[0] ?? null } return value ?? null } function readInputString(formData: FormData, field: string): string { const value = formData.get(field) return typeof value === "string" ? value.trim() : "" } function readNullableString(formData: FormData, field: string): string | null { const value = readInputString(formData, field) return value.length > 0 ? value : null } function readNullableInt(formData: FormData, field: string): number | null { const value = readInputString(formData, field) if (!value) { return null } const parsed = Number.parseInt(value, 10) if (!Number.isFinite(parsed)) { return 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") if (!raw) { return [] } return raw .split(",") .map((item) => item.trim()) .filter((item) => item.length > 0) } function redirectWithState(mediaAssetId: string, params: { notice?: string; error?: string }) { const query = new URLSearchParams() if (params.notice) { query.set("notice", params.notice) } if (params.error) { query.set("error", params.error) } const value = query.toString() redirect(value ? `/media/${mediaAssetId}?${value}` : `/media/${mediaAssetId}`) } function toLocalDateTimeInputValue(date: Date): string { const offset = date.getTimezoneOffset() * 60_000 return new Date(date.getTime() - offset).toISOString().slice(0, 16) } export default async function MediaAssetEditorPage({ params, searchParams }: PageProps) { const role = await requirePermissionForRoute({ nextPath: "/media", permission: "media:read", scope: "team", }) const resolvedParams = await params const mediaAssetId = resolvedParams.id const [resolvedSearchParams, asset] = await Promise.all([ searchParams, getMediaAssetById(mediaAssetId), ]) if (!asset) { redirect("/media?error=Media+asset+not+found") } const mediaAsset = asset const notice = readFirstValue(resolvedSearchParams.notice) const error = readFirstValue(resolvedSearchParams.error) const previewUrl = mediaAsset.storageKey ? `/api/media/file/${mediaAsset.id}` : null const isImage = Boolean(mediaAsset.mimeType?.startsWith("image/")) const isVideo = Boolean(mediaAsset.mimeType?.startsWith("video/")) async function updateMediaAssetAction(formData: FormData) { "use server" await requirePermissionForRoute({ nextPath: "/media", permission: "media:write", scope: "team", }) try { await updateMediaAsset({ id: mediaAssetId, title: readInputString(formData, "title"), type: readInputString(formData, "type"), description: readNullableString(formData, "description"), altText: readNullableString(formData, "altText"), 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"), height: readNullableInt(formData, "height"), sizeBytes: readNullableInt(formData, "sizeBytes"), isPublished: readInputString(formData, "isPublished") === "true", }) } catch { redirectWithState(mediaAssetId, { error: "Failed to update media asset. Validate values and try again.", }) } redirectWithState(mediaAssetId, { notice: "Media asset updated." }) } async function deleteMediaAssetAction() { "use server" await requirePermissionForRoute({ nextPath: "/media", permission: "media:write", scope: "team", }) try { if (mediaAsset.storageKey) { await deleteStoredMediaObject(mediaAsset.storageKey) } await deleteMediaAsset(mediaAssetId) } catch { redirectWithState(mediaAssetId, { error: "Failed to delete media asset and file from storage. Check storage config and links.", }) } redirect("/media?notice=Media+asset+deleted") } return ( {notice ? (
{notice}
) : null} {error ? (
{error}
) : null}

Preview

{mediaAsset.mimeType ? `MIME: ${mediaAsset.mimeType}` : "MIME: unknown"}

{!previewUrl ? (

No stored file is linked for this media asset.

) : isImage ? ( // biome-ignore lint/performance/noImgElement: Auth-protected media preview requires direct browser request with session cookies. {mediaAsset.altText ) : isVideo ? ( // biome-ignore lint/a11y/useMediaCaption: Preview uses source assets without guaranteed caption tracks.
{previewUrl ? ( Open raw media file ) : null}

{mediaAsset.title}

ID: {mediaAsset.id}

Back to media list