diff --git a/app/apple-icon.png/route.ts b/app/apple-icon.png/route.ts index 06e8288..95cef18 100644 --- a/app/apple-icon.png/route.ts +++ b/app/apple-icon.png/route.ts @@ -1,6 +1,4 @@ -import { NextResponse } from "next/server"; - -import { buildFallbackIconResponse, getDynamicSiteIconUrls } from "@/lib/site-icons"; +import { buildSiteIconResponse, getDynamicSiteIconUrls } from "@/lib/site-icons"; export const dynamic = "force-dynamic"; export const revalidate = 0; @@ -8,13 +6,5 @@ export const revalidate = 0; export async function GET() { const iconUrls = await getDynamicSiteIconUrls(); - if (iconUrls.faviconAssetUrl) { - const response = NextResponse.redirect(iconUrls.faviconAssetUrl, 307); - - response.headers.set("Cache-Control", "no-store, max-age=0"); - - return response; - } - - return buildFallbackIconResponse(iconUrls.siteName, 180); + return buildSiteIconResponse(iconUrls.faviconAssetUrl); } diff --git a/app/favicon.ico/route.ts b/app/favicon.ico/route.ts index a833a32..95cef18 100644 --- a/app/favicon.ico/route.ts +++ b/app/favicon.ico/route.ts @@ -1,6 +1,4 @@ -import { NextResponse } from "next/server"; - -import { buildFallbackIconResponse, getDynamicSiteIconUrls } from "@/lib/site-icons"; +import { buildSiteIconResponse, getDynamicSiteIconUrls } from "@/lib/site-icons"; export const dynamic = "force-dynamic"; export const revalidate = 0; @@ -8,13 +6,5 @@ export const revalidate = 0; export async function GET() { const iconUrls = await getDynamicSiteIconUrls(); - if (iconUrls.faviconAssetUrl) { - const response = NextResponse.redirect(iconUrls.faviconAssetUrl, 307); - - response.headers.set("Cache-Control", "no-store, max-age=0"); - - return response; - } - - return buildFallbackIconResponse(iconUrls.siteName, 32); + return buildSiteIconResponse(iconUrls.faviconAssetUrl); } diff --git a/app/root/site-settings/page.tsx b/app/root/site-settings/page.tsx index 991a965..26ab7aa 100644 --- a/app/root/site-settings/page.tsx +++ b/app/root/site-settings/page.tsx @@ -64,6 +64,7 @@ export default async function RootSiteSettingsPage({ headerTitle={copy.title} headerDescription={copy.subtitle} saveFormId="site-settings-form" + reloadDocumentOnSave >
{searchParams?.success ? ( diff --git a/components/root/form-save-button.tsx b/components/root/form-save-button.tsx index a9b6683..a84e1df 100644 --- a/components/root/form-save-button.tsx +++ b/components/root/form-save-button.tsx @@ -12,6 +12,7 @@ type FormSaveButtonProps = { formSelector?: string; formSelectors?: string[]; label?: string; + reloadDocumentOnSuccess?: boolean; }; function serializeForm(form: HTMLFormElement) { @@ -29,6 +30,7 @@ export function FormSaveButton({ formSelector, formSelectors, label = "Speichern", + reloadDocumentOnSuccess = false, }: FormSaveButtonProps) { const pathname = usePathname(); const router = useRouter(); @@ -220,6 +222,11 @@ export function FormSaveButton({ } pendingSubmissionRef.current = null; + if (reloadDocumentOnSuccess) { + window.location.reload(); + return; + } + router.refresh(); if (!searchParams.has("__saved")) { @@ -233,7 +240,7 @@ export function FormSaveButton({ router.replace(nextQuery ? `${pathname}?${nextQuery}` : pathname, { scroll: false, }); - }, [currentUrl, pathname, router, searchParams]); + }, [currentUrl, pathname, reloadDocumentOnSuccess, router, searchParams]); return ( ))} + {canClear ? ( + + ) : null}
-
- - onChange({ ...value, label: event.target.value })} - placeholder="Homepage Hero" - /> -
+ {value.mode !== "library" ? ( +
+ + onChange({ ...value, label: event.target.value, isCleared: false })} + placeholder="Homepage Hero" + /> +
+ ) : null} {value.mode === "upload" ? (
@@ -116,27 +188,71 @@ export function MediaFieldPicker({ onChange({ ...value, url: event.target.value })} + onChange={(event) => onChange({ ...value, url: event.target.value, isCleared: false })} placeholder="https://example.com/image.jpg" />
) : null} {value.mode === "library" ? ( -
+
- +
+
+ + setLibraryQuery(event.target.value)} + className="pl-9" + placeholder="Search media" + /> +
+ +
+ {visibleLibraryOptions.length > 0 ? ( + visibleLibraryOptions.map((option) => { + const isActive = option.id === value.assetId; + + return ( + + ); + }) + ) : ( +
+ No media found. +
+ )} +
+
) : null} diff --git a/components/root/root-dashboard-shell.tsx b/components/root/root-dashboard-shell.tsx index 02c24d5..04bc9ab 100644 --- a/components/root/root-dashboard-shell.tsx +++ b/components/root/root-dashboard-shell.tsx @@ -48,6 +48,7 @@ type RootDashboardShellProps = { saveFormId?: string; saveFormSelector?: string; saveButtonLabel?: string; + reloadDocumentOnSave?: boolean; headerActions?: ReactNode; sidebarTopContent?: ReactNode; toolbar?: ReactNode; @@ -64,6 +65,7 @@ export async function RootDashboardShell({ saveFormId, saveFormSelector, saveButtonLabel, + reloadDocumentOnSave = false, headerActions, sidebarTopContent, toolbar, @@ -98,6 +100,7 @@ export async function RootDashboardShell({ formIds={saveFormId ? ["sidebar-maintenance-form", saveFormId] : ["sidebar-maintenance-form"]} formSelectors={saveFormSelector ? [saveFormSelector] : undefined} label={saveButtonLabel ?? "Speichern"} + reloadDocumentOnSuccess={reloadDocumentOnSave} /> diff --git a/components/root/site-settings-form.tsx b/components/root/site-settings-form.tsx index f2852fd..e106fe3 100644 --- a/components/root/site-settings-form.tsx +++ b/components/root/site-settings-form.tsx @@ -65,6 +65,7 @@ function createImageFieldState( url: "", label, kind: MediaKind.IMAGE, + isCleared: false, }; } @@ -75,6 +76,7 @@ function createImageFieldState( url, label, kind: MediaKind.IMAGE, + isCleared: false, }; } @@ -84,6 +86,7 @@ function createImageFieldState( url: "", label, kind: MediaKind.IMAGE, + isCleared: false, }; } @@ -92,6 +95,10 @@ function getMediaPreviewUrl( options: MediaOption[], fallbackUrl: string | null | undefined, ) { + if (value.isCleared) { + return ""; + } + if (value.mode === "external") { return value.url || fallbackUrl || ""; } @@ -280,12 +287,20 @@ export function SiteSettingsForm({ value={favicon} onChange={setFavicon} options={mediaOptions} + hasInitialValue={Boolean(initialBindings.favicon?.assetId || initialBindings.favicon?.url)} inputName="faviconMedia" fileFieldName="faviconFile" fileLabel="Upload Favicon" - externalLabel="Favicon URL" libraryLabel="Favicon Library" accept=".png,.svg,.ico,image/png,image/svg+xml,image/x-icon,image/vnd.microsoft.icon" + allowExternal={false} + allowClear + clearLabel="Remove Favicon" + emptyValue={{ + mode: "upload", + assetId: "", + url: "", + }} /> diff --git a/lib/app-config.ts b/lib/app-config.ts index 896bc1c..ce93961 100644 --- a/lib/app-config.ts +++ b/lib/app-config.ts @@ -98,7 +98,9 @@ export async function getSiteSettingsMediaBindings(): Promise { return buildSiteIconUrls({ siteName: defaultLocaleSettings.siteName, - faviconAssetId: bindings.favicon?.assetId, + faviconVersion: bindings.favicon?.version, faviconUrl: bindings.favicon?.url, }); } -export function buildFallbackIconResponse(siteName: string, size: number) { - const label = getFallbackLabel(siteName); +function getResolvedPathname(iconUrl: string) { + const resolvedUrl = new URL(iconUrl, "https://local.invalid"); - return new ImageResponse( - ( -
- {label} -
- ), - { - width: size, - height: size, - headers: { - "Cache-Control": "no-store, max-age=0", - }, + return resolvedUrl.pathname; +} + +async function createFileResponse(absolutePath: string) { + const fileBuffer = await readFile(absolutePath); + const contentType = ICON_CONTENT_TYPES[path.extname(absolutePath).toLowerCase()] ?? "application/octet-stream"; + + return new Response(fileBuffer, { + status: 200, + headers: { + "Content-Type": contentType, + "Cache-Control": "no-store, max-age=0", }, - ); + }); +} + +export async function buildSiteIconResponse(iconUrl: string | null) { + if (iconUrl) { + const pathname = getResolvedPathname(iconUrl); + + if (isManagedMediaFilePath(pathname)) { + return createFileResponse(resolveMediaUploadPath(pathname)); + } + } + + return new Response(Buffer.from(TRANSPARENT_PNG_BASE64, "base64"), { + status: 200, + headers: { + "Content-Type": "image/png", + "Cache-Control": "no-store, max-age=0", + }, + }); } diff --git a/lib/site-settings.ts b/lib/site-settings.ts index 121d6f6..379a609 100644 --- a/lib/site-settings.ts +++ b/lib/site-settings.ts @@ -26,6 +26,7 @@ export type SiteSettings = { export type SiteSettingsMediaBinding = { assetId: string; url: string; + version: string; }; export type SiteSettingsMediaBindings = {