From 29f8a44f0f994236a95b1f24533e190c904fe3e7 Mon Sep 17 00:00:00 2001 From: openhands Date: Sun, 5 Jul 2026 23:32:24 +0200 Subject: [PATCH] Add favicon generator with color picker and text --- src/app/admin/favicon/favicon-form.tsx | 120 ++++++++------ src/app/admin/favicon/favicon-generator.tsx | 170 ++++++++++++++++++++ src/messages/en.json | 10 +- src/messages/nl.json | 10 +- 4 files changed, 258 insertions(+), 52 deletions(-) create mode 100644 src/app/admin/favicon/favicon-generator.tsx diff --git a/src/app/admin/favicon/favicon-form.tsx b/src/app/admin/favicon/favicon-form.tsx index 8fc8c7e8..09112031 100644 --- a/src/app/admin/favicon/favicon-form.tsx +++ b/src/app/admin/favicon/favicon-form.tsx @@ -3,6 +3,7 @@ import { useRef, useState } from "react"; import { useTranslations } from "next-intl"; import { saveFavicon, deleteFavicon } from "@/actions/save-favicon"; +import { FaviconGenerator } from "./favicon-generator"; export function FaviconForm({ currentUrl }: { currentUrl: string | null }) { const t = useTranslations("pages.admin.favicon"); @@ -42,62 +43,81 @@ export function FaviconForm({ currentUrl }: { currentUrl: string | null }) { } } + async function handleGenerated(blob: Blob) { + setUploading(true); + setMessage(null); + const file = new File([blob], "favicon.png", { type: "image/png" }); + const fd = new FormData(); + fd.append("file", file); + const result = await saveFavicon(fd); + setUploading(false); + + if (result.success && result.url) { + setPreview(result.url); + setMessage({ type: "success", text: t("saved") }); + } else { + setMessage({ type: "error", text: result.error ?? t("error") }); + } + } + return ( -
- {preview && ( -
- -
- Favicon preview -
{preview}
+
+
+

{t("uploadLabel")}

+ {preview && ( +
+ +
+ Favicon preview +
{preview}
+
-
- )} + )} -
-
- - -

{t("uploadHint")}

-
+ +
+ +

{t("uploadHint")}

+
-
- - {preview && ( - - )} -
-
+ {preview && ( + + )} +
+ - {message && ( -
- {message.text} -
- )} + {message && ( +
+ {message.text} +
+ )} +
+ +
); } diff --git a/src/app/admin/favicon/favicon-generator.tsx b/src/app/admin/favicon/favicon-generator.tsx new file mode 100644 index 00000000..9b930633 --- /dev/null +++ b/src/app/admin/favicon/favicon-generator.tsx @@ -0,0 +1,170 @@ +"use client"; + +import { useCallback, useEffect, useRef, useState } from "react"; +import { useTranslations } from "next-intl"; + +const SIZE = 64; +const HALF = SIZE / 2; + +interface Props { + onSave: (blob: Blob) => void; +} + +export function FaviconGenerator({ onSave }: Props) { + const t = useTranslations("pages.admin.favicon.generator"); + const [bgColor, setBgColor] = useState("#6366f1"); + const [textColor, setTextColor] = useState("#ffffff"); + const [text, setText] = useState("A"); + const [rounded, setRounded] = useState(true); + const canvasRef = useRef(null); + + const render = useCallback(() => { + const canvas = canvasRef.current; + if (!canvas) return; + const ctx = canvas.getContext("2d"); + if (!ctx) return; + + canvas.width = SIZE; + canvas.height = SIZE; + + // Background + ctx.clearRect(0, 0, SIZE, SIZE); + ctx.fillStyle = bgColor; + if (rounded) { + const r = 12; + ctx.beginPath(); + ctx.moveTo(r, 0); + ctx.lineTo(SIZE - r, 0); + ctx.quadraticCurveTo(SIZE, 0, SIZE, r); + ctx.lineTo(SIZE, SIZE - r); + ctx.quadraticCurveTo(SIZE, SIZE, SIZE - r, SIZE); + ctx.lineTo(r, SIZE); + ctx.quadraticCurveTo(0, SIZE, 0, SIZE - r); + ctx.lineTo(0, r); + ctx.quadraticCurveTo(0, 0, r, 0); + ctx.closePath(); + ctx.fill(); + } else { + ctx.fillRect(0, 0, SIZE, SIZE); + } + + // Text + const displayText = text.trim() || "?"; + const fontSize = displayText.length > 2 ? 28 : displayText.length > 1 ? 32 : 40; + ctx.fillStyle = textColor; + ctx.font = `bold ${fontSize}px Arial, sans-serif`; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillText(displayText, HALF, HALF + 1); + }, [bgColor, textColor, text, rounded]); + + useEffect(() => { + render(); + }, [render]); + + function handleSave() { + const canvas = canvasRef.current; + if (!canvas) return; + canvas.toBlob((blob) => { + if (blob) onSave(blob); + }, "image/png"); + } + + const swatches = [ + "#6366f1", "#3b82f6", "#06b6d4", "#10b981", + "#f59e0b", "#ef4444", "#ec4899", "#8b5cf6", + "#1e1b4b", "#1f2937", "#000000", "#ffffff", + ]; + + return ( +
+

{t("title")}

+ +
+ +
+ +
+
+ + setText(e.target.value.slice(0, 4))} + maxLength={4} + className="w-full" + placeholder="A" + /> +
+ +
+ +
+ setBgColor(e.target.value)} + className="w-10 h-10 rounded-lg cursor-pointer border border-[var(--border)]" + /> + {bgColor} +
+
+ {swatches.map((c) => ( +
+
+ +
+ +
+ setTextColor(e.target.value)} + className="w-10 h-10 rounded-lg cursor-pointer border border-[var(--border)]" + /> + {textColor} +
+
+ +
+ setRounded(e.target.checked)} + className="rounded" + /> + +
+ + +
+
+ ); +} diff --git a/src/messages/en.json b/src/messages/en.json index d94eddbe..4c3d78a1 100644 --- a/src/messages/en.json +++ b/src/messages/en.json @@ -1987,7 +1987,15 @@ "saved": "Favicon saved successfully!", "deleted": "Favicon removed successfully!", "error": "Something went wrong", - "confirmDelete": "Remove the favicon? The site will no longer show a custom favicon." + "confirmDelete": "Remove the favicon? The site will no longer show a custom favicon.", + "generator": { + "title": "Generate favicon", + "text": "Text", + "bgColor": "Background color", + "textColor": "Text color", + "rounded": "Rounded corners", + "save": "Save as favicon" + } }, "settings": { "title": "CMS Settings", diff --git a/src/messages/nl.json b/src/messages/nl.json index 28e587ed..9dadb861 100644 --- a/src/messages/nl.json +++ b/src/messages/nl.json @@ -1984,7 +1984,15 @@ "saved": "Favicon succesvol opgeslagen!", "deleted": "Favicon succesvol verwijderd!", "error": "Er is iets misgegaan", - "confirmDelete": "Favicon verwijderen? De site zal geen aangepaste favicon meer tonen." + "confirmDelete": "Favicon verwijderen? De site zal geen aangepaste favicon meer tonen.", + "generator": { + "title": "Favicon genereren", + "text": "Tekst", + "bgColor": "Achtergrondkleur", + "textColor": "Tekstkleur", + "rounded": "Afgeronde hoeken", + "save": "Opslaan als favicon" + } }, "settings": { "title": "CMS-instellingen",