feat: add save as site logo button to logo generator

This commit is contained in:
openhands committed 2026-07-03 16:45:43 +02:00
1 parent 98506fea1e
commit af2d5b4943
2 files changed
+95 -1

No files matched your search

+39
View File
@@ -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" };
}
}
+56 -1
View File
@@ -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<HTMLAnchorElement | null>(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 (
<div className="card-grid md-2" style={{ alignItems: "start" }}>
{/* ── Controls ──────────────────────────────── */}
@@ -206,6 +253,14 @@ export default function LogoGenerator() {
<button type="button" className="btn btn-primary" onClick={handleDownload}>
⬇️ Download PNG
</button>
<button
type="button"
className="btn btn-primary"
onClick={handleSave}
disabled={saveStatus === "saving"}
>
{saveStatus === "saving" ? "⏳ Saving..." : saveStatus === "done" ? "✅ Saved!" : saveStatus === "error" ? "❌ Error" : "💾 Save as site logo"}
</button>
{/* Reused, hidden anchor used to trigger the file download. */}
<a ref={linkRef} style={{ display: "none" }} aria-hidden href="/">
download