"use client"; /* eslint-disable @next/next/no-img-element */ import { MediaKind } from "@prisma/client"; import { Check, FileText, ImageIcon, Search, Type, Upload } from "lucide-react"; import { useEffect, useId, useMemo, useRef, useState } from "react"; import { AppCard } from "@/components/ui/app-card"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table"; import type { AppLocale } from "@/lib/locale"; import type { MediaOption } from "@/lib/media"; import { cn } from "@/lib/utils"; import { PAGE_TITLE_TOKEN, SITE_NAME_TOKEN, type SiteSettings, type SiteSettingsMediaBindings, } from "@/lib/site-settings"; type SiteSettingsFormProps = { action: (formData: FormData) => Promise; initialSettings: SiteSettings; initialBindings: SiteSettingsMediaBindings; mediaOptions: MediaOption[]; }; type MediaFieldState = { mode: "upload" | "library"; assetId: string; url: string; label: string; kind: MediaKind; }; const localeFields: Array<{ key: AppLocale; label: string; suffix: "Ar" | "En" | "De"; sampleTitle: string; }> = [ { key: "ar", label: "Arabic", suffix: "Ar", sampleTitle: "Home", }, { key: "en", label: "English", suffix: "En", sampleTitle: "About", }, { key: "de", label: "Deutsch", suffix: "De", sampleTitle: "Portfolio", }, ]; function createImageFieldState( assetId: string | null | undefined, url: string | null | undefined, label: string, ): MediaFieldState { if (assetId) { return { mode: "library", assetId, url: "", label, kind: MediaKind.IMAGE, }; } return { mode: "upload", assetId: "", url: url ?? "", label, kind: MediaKind.IMAGE, }; } function getMediaPreviewUrl( value: MediaFieldState, options: MediaOption[], fallbackUrl: string | null | undefined, ) { if (value.mode === "library") { return options.find((option) => option.id === value.assetId)?.url ?? fallbackUrl ?? ""; } return fallbackUrl ?? ""; } function buildPreviewTitle( titleTemplate: string, sampleTitle: string, siteName: string, ) { return titleTemplate.includes(PAGE_TITLE_TOKEN) ? titleTemplate .replace(PAGE_TITLE_TOKEN, sampleTitle) .replaceAll(SITE_NAME_TOKEN, siteName) : `${sampleTitle} | ${siteName}`; } function MediaLibraryModal({ open, onOpenChange, options, selectedAssetId, onSelect, title, }: { open: boolean; onOpenChange: (open: boolean) => void; options: MediaOption[]; selectedAssetId: string; onSelect: (option: MediaOption) => void; title: string; }) { const searchId = useId(); const [query, setQuery] = useState(""); const visibleOptions = useMemo(() => { const normalizedQuery = query.trim().toLowerCase(); if (!normalizedQuery) { return options; } return options.filter((option) => option.label.toLowerCase().includes(normalizedQuery)); }, [options, query]); useEffect(() => { if (!open) { setQuery(""); } }, [open]); return ( {title} Bild aus der Media Library auswaehlen.
setQuery(event.target.value)} className="pl-9" placeholder="Search media" />
{visibleOptions.length > 0 ? ( visibleOptions.map((option) => { const isActive = option.id === selectedAssetId; return ( ); }) ) : (
No media found.
)}
); } function SiteSettingsMediaRow({ title, value, onChange, options, inputName, fileFieldName, accept, clearLabel, fallbackUrl, }: { title: string; value: MediaFieldState; onChange: (nextValue: MediaFieldState) => void; options: MediaOption[]; inputName: string; fileFieldName: string; accept: string; clearLabel: string; fallbackUrl?: string | null; }) { const [isLibraryOpen, setIsLibraryOpen] = useState(false); const hiddenInputRef = useRef(null); const fileInputId = useId(); const previewUrl = getMediaPreviewUrl(value, options, fallbackUrl); const serializedValue = JSON.stringify({ mode: value.mode, assetId: value.assetId, url: value.url, label: value.label, kind: value.kind, }); useEffect(() => { const hiddenInput = hiddenInputRef.current; if (!hiddenInput) { return; } hiddenInput.dispatchEvent(new Event("input", { bubbles: true })); hiddenInput.dispatchEvent(new Event("change", { bubbles: true })); }, [serializedValue]); return ( <>

{title}

{previewUrl ? ( {title} ) : ( )}
onChange({ ...value, label: event.target.value })} placeholder="Label" className="pl-8" />
{ const nextLabel = event.target.files?.[0]?.name.replace(/\.[^.]+$/, "") ?? value.label; onChange({ ...value, mode: "upload", assetId: "", label: value.label.trim() ? value.label : nextLabel, }); }} />
{value.mode === "library" && value.assetId ? "Library selected" : "Upload mode"} {value.assetId ? ( ) : null}
option.kind === MediaKind.IMAGE)} selectedAssetId={value.assetId} title={title} onSelect={(option) => onChange({ ...value, mode: "library", assetId: option.id, url: "", label: option.label, }) } /> ); } function BadgeLike({ children }: { children: string }) { return ( {children} ); } export function SiteSettingsForm({ action, initialSettings, initialBindings, mediaOptions, }: SiteSettingsFormProps) { const [settings, setSettings] = useState(initialSettings); const [siteLogoLight, setSiteLogoLight] = useState( createImageFieldState( initialBindings.siteLogoLight?.assetId, initialBindings.siteLogoLight?.url, "Site Logo Light", ), ); const [siteLogoDark, setSiteLogoDark] = useState( createImageFieldState( initialBindings.siteLogoDark?.assetId, initialBindings.siteLogoDark?.url, "Site Logo Dark", ), ); const [favicon, setFavicon] = useState( createImageFieldState( initialBindings.favicon?.assetId, initialBindings.favicon?.url, "Favicon", ), ); const [defaultOgImage, setDefaultOgImage] = useState( createImageFieldState( initialBindings.defaultOgImage?.assetId, initialBindings.defaultOgImage?.url, "Default OG Image", ), ); const siteLogoLightPreviewUrl = getMediaPreviewUrl( siteLogoLight, mediaOptions, initialBindings.siteLogoLight?.url, ); const siteLogoDarkPreviewUrl = getMediaPreviewUrl( siteLogoDark, mediaOptions, initialBindings.siteLogoDark?.url, ); const faviconPreviewUrl = getMediaPreviewUrl(favicon, mediaOptions, initialBindings.favicon?.url); const defaultOgImagePreviewUrl = getMediaPreviewUrl( defaultOgImage, mediaOptions, initialBindings.defaultOgImage?.url, ); return (

Localized Titles And Copy

Nutze {" "} {PAGE_TITLE_TOKEN} {" "} und {" "} {SITE_NAME_TOKEN} {" "} fuer die globalen Titelvorlagen pro Sprache.

{localeFields.map((locale) => (

{locale.label}

setSettings((current) => ({ ...current, locales: { ...current.locales, [locale.key]: { ...current.locales[locale.key], siteName: event.target.value, }, }, })) } placeholder="Site Name" className="pl-8" />
setSettings((current) => ({ ...current, locales: { ...current.locales, [locale.key]: { ...current.locales[locale.key], titleTemplate: event.target.value, }, }, })) } placeholder="Title Template" className="pl-8" />
setSettings((current) => ({ ...current, locales: { ...current.locales, [locale.key]: { ...current.locales[locale.key], siteDescription: event.target.value, }, }, })) } placeholder="Site Description" className="pl-8" />
setSettings((current) => ({ ...current, locales: { ...current.locales, [locale.key]: { ...current.locales[locale.key], subhead: event.target.value, }, }, })) } placeholder="Subhead" className="pl-8" />
))}

Brand And Preview Images

Preview

Search Result

{faviconPreviewUrl ? ( Favicon ) : (
)}

{buildPreviewTitle( settings.locales.de.titleTemplate, "About", settings.locales.de.siteName, )}

{settings.locales.de.siteDescription || "No description"}

Brand Assets

Light

{siteLogoLightPreviewUrl ? ( Site Logo Light ) : (
No logo selected
)}

Dark

{siteLogoDarkPreviewUrl ? ( Site Logo Dark ) : (
No logo selected
)}

Social Preview

{defaultOgImagePreviewUrl ? ( Default OG ) : (
No OG image selected
)}

{buildPreviewTitle( settings.locales.en.titleTemplate, "Portfolio", settings.locales.en.siteName, )}

{settings.locales.en.siteDescription || "No description"}

Locale Summary

{localeFields.map((locale) => ( {locale.label}

{buildPreviewTitle( settings.locales[locale.key].titleTemplate, locale.sampleTitle, settings.locales[locale.key].siteName, )}

{settings.locales[locale.key].subhead || "No subhead"}

))}
); }