From af2d5b4943303f93a88d39e5395fddf8bc19c8da Mon Sep 17 00:00:00 2001 From: openhands Date: Fri, 3 Jul 2026 16:45:43 +0200 Subject: [PATCH] feat: add save as site logo button to logo generator --- src/actions/save-logo.ts | 39 ++++++++++++++++ src/components/public/logo-generator.tsx | 57 +++++++++++++++++++++++- 2 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 src/actions/save-logo.ts diff --git a/src/actions/save-logo.ts b/src/actions/save-logo.ts new file mode 100644 index 00000000..d54673b0 --- /dev/null +++ b/src/actions/save-logo.ts @@ -0,0 +1,39 @@ +"use server"; + +import { revalidatePath } from "next/cache"; +import { writeFile } from "fs/promises"; +import path from "path"; +import { prisma } from "@/lib/prisma"; +import { siteSettings } from "@/lib/services/site-settings"; + +const MEDIA_DIR = "public/assets/images/media"; + +export async function saveLogo(formData: FormData): Promise<{ success: boolean; url?: string; error?: string }> { + try { + const file = formData.get("file") as File | null; + if (!file) return { success: false, error: "No file provided" }; + + const ext = "png"; + const filename = `logo-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.${ext}`; + const dir = path.join(process.cwd(), MEDIA_DIR); + const filePath = path.join(dir, filename); + + const buffer = Buffer.from(await file.arrayBuffer()); + await writeFile(filePath, buffer); + + const url = `/api/media/${filename}`; + + await prisma.websiteSetting.upsert({ + where: { key: "cms_logo" }, + update: { value: url }, + create: { key: "cms_logo", value: url, comment: "Logo (generator)" }, + }); + + siteSettings.reload(); + revalidatePath("/", "layout"); + + return { success: true, url }; + } catch (e) { + return { success: false, error: e instanceof Error ? e.message : "Unknown error" }; + } +} diff --git a/src/components/public/logo-generator.tsx b/src/components/public/logo-generator.tsx index 6c3ccb97..dcee6262 100644 --- a/src/components/public/logo-generator.tsx +++ b/src/components/public/logo-generator.tsx @@ -1,7 +1,8 @@ "use client"; -import { useCallback, useMemo, useRef, useState } from "react"; +import { useCallback, useMemo, useRef, useState, useTransition } from "react"; import { ContentCard } from "@/components/public/ui"; +import { saveLogo } from "@/actions/save-logo"; /** * Client-side logo generator. Lets the visitor type a hotel name, pick a fun @@ -38,6 +39,8 @@ export default function LogoGenerator() { const [bold, setBold] = useState(true); const linkRef = useRef(null); + const [saveStatus, setSaveStatus] = useState<"idle" | "saving" | "done" | "error">("idle"); + const [, startTransition] = useTransition(); const safeText = text.trim().length > 0 ? text : "Your hotel"; const fontWeight = bold ? "bold" : "normal"; @@ -109,6 +112,50 @@ export default function LogoGenerator() { }, "image/png"); }, [bgColor, bgEnabled, fontStack, fontWeight, safeText, size, textColor]); + const handleSave = useCallback(() => { + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + if (!ctx) return; + + const fontSpec = `${fontWeight} ${size}px ${fontStack}`; + ctx.font = fontSpec; + const metrics = ctx.measureText(safeText); + const padX = Math.round(size * 0.4); + const padY = Math.round(size * 0.45); + const textWidth = Math.max(1, Math.ceil(metrics.width)); + const width = textWidth + padX * 2; + const height = Math.ceil(size * 1.35) + padY * 2; + + const dpr = Math.min(typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1, 3); + canvas.width = Math.round(width * dpr); + canvas.height = Math.round(height * dpr); + ctx.scale(dpr, dpr); + + if (bgEnabled) { + ctx.fillStyle = bgColor; + ctx.fillRect(0, 0, width, height); + } + + ctx.font = fontSpec; + ctx.fillStyle = textColor; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillText(safeText, width / 2, height / 2); + + setSaveStatus("saving"); + canvas.toBlob(async (blob) => { + if (!blob) { setSaveStatus("error"); return; } + const file = new File([blob], "logo.png", { type: "image/png" }); + const fd = new FormData(); + fd.append("file", file); + startTransition(async () => { + const result = await saveLogo(fd); + setSaveStatus(result.success ? "done" : "error"); + if (result.success) setTimeout(() => setSaveStatus("idle"), 3000); + }); + }, "image/png"); + }, [bgColor, bgEnabled, fontStack, fontWeight, safeText, size, textColor, startTransition]); + return (
{/* ── Controls ──────────────────────────────── */} @@ -206,6 +253,14 @@ export default function LogoGenerator() { + {/* Reused, hidden anchor used to trigger the file download. */} download