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";
|
"use client";
|
||||||
|
|
||||||
import { useCallback, useMemo, useRef, useState } from "react";
|
import { useCallback, useMemo, useRef, useState, useTransition } from "react";
|
||||||
import { ContentCard } from "@/components/public/ui";
|
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
|
* 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 [bold, setBold] = useState(true);
|
||||||
|
|
||||||
const linkRef = useRef<HTMLAnchorElement | null>(null);
|
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 safeText = text.trim().length > 0 ? text : "Your hotel";
|
||||||
const fontWeight = bold ? "bold" : "normal";
|
const fontWeight = bold ? "bold" : "normal";
|
||||||
@@ -109,6 +112,50 @@ export default function LogoGenerator() {
|
|||||||
}, "image/png");
|
}, "image/png");
|
||||||
}, [bgColor, bgEnabled, fontStack, fontWeight, safeText, size, textColor]);
|
}, [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 (
|
return (
|
||||||
<div className="card-grid md-2" style={{ alignItems: "start" }}>
|
<div className="card-grid md-2" style={{ alignItems: "start" }}>
|
||||||
{/* ── Controls ──────────────────────────────── */}
|
{/* ── Controls ──────────────────────────────── */}
|
||||||
@@ -206,6 +253,14 @@ export default function LogoGenerator() {
|
|||||||
<button type="button" className="btn btn-primary" onClick={handleDownload}>
|
<button type="button" className="btn btn-primary" onClick={handleDownload}>
|
||||||
⬇️ Download PNG
|
⬇️ Download PNG
|
||||||
</button>
|
</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. */}
|
{/* Reused, hidden anchor used to trigger the file download. */}
|
||||||
<a ref={linkRef} style={{ display: "none" }} aria-hidden href="/">
|
<a ref={linkRef} style={{ display: "none" }} aria-hidden href="/">
|
||||||
download
|
download
|
||||||
|
|||||||
Reference in new issue
Block a user