feat: add save as site logo button to logo generator
This commit is contained in:
1 parent
98506fea1e
commit
af2d5b4943
2 files changed
+95
-1
No files matched your search
@@ -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" };
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user