Files
menulio/apps/web/src/components/QrStandGeneratorClient.tsx
T

361 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useRef, useState } from "react";
import Link from "next/link";
import { QRCodeCanvas } from "qrcode.react";
import { toPng } from "html-to-image";
import { jsPDF } from "jspdf";
import { QR_STAND_PRESETS, type QrStandPreset } from "@menulio/shared";
interface QrStandGeneratorClientProps {
restaurantName?: string;
logoUrl?: string | null;
slug?: string;
categoriesList?: string[];
}
export function QrStandGeneratorClient({
restaurantName = "THE URBAN BURGER",
logoUrl = null,
slug = "kebapci-sinan-3",
categoriesList = [],
}: QrStandGeneratorClientProps) {
const [selectedKey, setSelectedKey] = useState<string>("qr-4");
const [downloadingPng, setDownloadingPng] = useState(false);
const [downloadingPdf, setDownloadingPdf] = useState(false);
const standRef = useRef<HTMLDivElement>(null);
const activePreset: QrStandPreset = (QR_STAND_PRESETS[selectedKey] || Object.values(QR_STAND_PRESETS)[0])!;
const targetUrl = `https://menul.io/${slug}`;
const displayCategories =
categoriesList.length > 0 ? categoriesList.slice(0, 4) : activePreset.categories;
async function handleDownloadPng() {
if (!standRef.current) return;
setDownloadingPng(true);
try {
const dataUrl = await toPng(standRef.current, {
cacheBust: true,
pixelRatio: 3, // 300 DPI high resolution
});
const link = document.createElement("a");
link.download = `${slug}-masa-stant-qr.png`;
link.href = dataUrl;
link.click();
} catch (err) {
console.error("PNG indirme hatası:", err);
alert("PNG indirilemedi. Lütfen tekrar deneyin.");
} finally {
setDownloadingPng(false);
}
}
async function handleDownloadPdf() {
if (!standRef.current) return;
setDownloadingPdf(true);
try {
const dataUrl = await toPng(standRef.current, {
cacheBust: true,
pixelRatio: 3,
});
// A5 dimensions in mm: 148 x 210
const pdf = new jsPDF({
orientation: "portrait",
unit: "mm",
format: "a5",
});
const pdfWidth = pdf.internal.pageSize.getWidth();
const pdfHeight = pdf.internal.pageSize.getHeight();
pdf.addImage(dataUrl, "PNG", 0, 0, pdfWidth, pdfHeight);
pdf.save(`${slug}-masa-stant-a5.pdf`);
} catch (err) {
console.error("PDF indirme hatası:", err);
alert("PDF indirilemedi. Lütfen tekrar deneyin.");
} finally {
setDownloadingPdf(false);
}
}
return (
<div className="min-h-screen bg-stone-950 text-stone-100 flex flex-col font-sans select-none">
{/* Top Navbar */}
<header className="border-b border-stone-800/80 bg-stone-900/90 backdrop-blur-xl sticky top-0 z-50 px-4 sm:px-6 py-3.5">
<div className="max-w-7xl mx-auto flex items-center justify-between">
<div className="flex items-center gap-3">
<Link href="/" className="font-black text-xl tracking-tight text-amber-400">
MENULIO
</Link>
<span className="text-stone-600 hidden sm:inline">/</span>
<span className="text-xs font-semibold text-stone-400 hidden sm:inline">
Masa Stant QR Jeneratörü
</span>
</div>
<Link
href="/"
className="text-xs font-bold px-4 py-2 rounded-xl bg-amber-500 hover:bg-amber-400 text-stone-950 transition-all shadow-md shadow-amber-500/20"
>
Ana Sayfa
</Link>
</div>
</header>
{/* Main Content Area */}
<main className="flex-1 max-w-6xl w-full mx-auto p-4 sm:p-8 flex flex-col lg:flex-row gap-8 items-center lg:items-start justify-center">
{/* Left Panel: Preset Selectors & Download Controls */}
<div className="w-full lg:w-96 flex flex-col gap-6">
{Object.values(QR_STAND_PRESETS).length > 1 && (
<div className="bg-stone-900/80 border border-stone-800 p-6 rounded-3xl space-y-4 shadow-xl">
<div>
<span className="text-xs font-bold uppercase tracking-wider text-amber-400">
1. Stant Teması Seçin
</span>
<h1 className="text-xl sm:text-2xl font-extrabold text-white mt-1">
Masa Kartı Şablonları
</h1>
<p className="text-xs text-stone-400 mt-1 leading-relaxed">
Restoranınızın konseptine uygun masa stant tasarımını seçin.
</p>
</div>
{/* Presets List */}
<div className="space-y-2.5 pt-2">
{Object.values(QR_STAND_PRESETS).map((preset) => {
const isSelected = preset.key === selectedKey;
return (
<button
key={preset.key}
onClick={() => setSelectedKey(preset.key)}
className={`w-full p-3.5 rounded-2xl border text-left transition-all flex items-center justify-between ${
isSelected
? "bg-amber-500/10 border-amber-500 text-amber-400 shadow-md shadow-amber-500/5"
: "bg-stone-950/60 border-stone-800 text-stone-300 hover:border-stone-700"
}`}
>
<div className="flex items-center gap-3">
<div
className="w-4 h-4 rounded-full border border-white/20 shadow-sm"
style={{ background: preset.plaqueBg }}
/>
<div>
<div className="text-xs font-bold">{preset.name}</div>
<div className="text-[10px] text-stone-400">{preset.categoryName}</div>
</div>
</div>
{isSelected && <span className="text-xs font-bold"></span>}
</button>
);
})}
</div>
</div>
)}
{/* Action Download Buttons */}
<div className="bg-stone-900/80 border border-stone-800 p-6 rounded-3xl space-y-3.5 shadow-xl">
<span className="text-xs font-bold uppercase tracking-wider text-stone-400">
2. Baskı Çıktısı Alın
</span>
{/* PNG Download Button */}
<button
onClick={handleDownloadPng}
disabled={downloadingPng}
className="w-full py-3.5 px-4 rounded-2xl bg-amber-500 hover:bg-amber-400 text-stone-950 font-extrabold text-sm transition-all shadow-lg shadow-amber-500/20 flex items-center justify-center gap-2"
>
{downloadingPng ? (
<span>Hazırlanıyor...</span>
) : (
<>
<svg className="w-5 h-5 fill-current" viewBox="0 0 24 24">
<path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z" />
</svg>
<span>PNG Olarak İndir (300 DPI Ultra HD)</span>
</>
)}
</button>
{/* PDF Download Button */}
<button
onClick={handleDownloadPdf}
disabled={downloadingPdf}
className="w-full py-3.5 px-4 rounded-2xl bg-stone-800 hover:bg-stone-700 text-white font-extrabold text-sm border border-stone-700 transition-all flex items-center justify-center gap-2"
>
{downloadingPdf ? (
<span>Hazırlanıyor...</span>
) : (
<>
<svg className="w-5 h-5 fill-current" viewBox="0 0 24 24">
<path d="M20 2H8c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H8V4h12v12zM4 6H2v14c0 1.1.9 2 2 2h14v-2H4V6z" />
</svg>
<span>PDF Olarak İndir (A5 Matbaa Baskı)</span>
</>
)}
</button>
</div>
</div>
{/* Right Panel: Stand Live Visual Canvas Preview */}
<div className="flex-1 flex flex-col items-center justify-center w-full">
<div
className="p-6 sm:p-10 rounded-[3rem] shadow-2xl flex items-center justify-center border border-white/5"
style={{ background: activePreset.bgGradient }}
>
{/* Tabletop Plaque Container */}
{activePreset.svgTemplateUrl ? (
<div
ref={standRef}
className={`w-[360px] sm:w-[380px] rounded-[2.5rem] relative overflow-hidden shadow-[0_25px_60px_rgba(0,0,0,0.6)] border-2 border-white/10 ${
activePreset.aspectRatio === "672/900" ? "aspect-[672/900]" : "aspect-[400/650]"
}`}
>
{/* Exact SVG Template Vector Background Graphic */}
<img
src={activePreset.svgTemplateUrl}
alt={activePreset.name}
className="absolute inset-0 w-full h-full object-cover pointer-events-none"
/>
{/* Dynamic Restaurant Logo & Name Header Overlay */}
<div
className={`absolute left-0 right-0 flex flex-col items-center justify-center px-6 text-center z-10 ${
activePreset.key === "qr-1" ? "top-[13%]" : "top-[15.5%]"
}`}
>
{logoUrl ? (
<img
src={logoUrl}
alt={restaurantName}
className="h-8 w-auto max-w-[120px] object-contain mb-1"
/>
) : null}
<span
className="font-black text-xl sm:text-2xl tracking-wider uppercase font-['Montserrat'] leading-tight drop-shadow-md"
style={{ color: activePreset.accentText }}
>
{restaurantName}
</span>
</div>
{/* Dynamic QR Code Canvas Overlay */}
<div
className={`absolute left-1/2 -translate-x-1/2 rounded-[20px] p-3 shadow-2xl flex items-center justify-center bg-white z-10 border border-zinc-200 ${
activePreset.key === "qr-1"
? "top-[37%] w-[52%] aspect-square"
: "top-[33.1%] w-[55%] aspect-square"
}`}
>
<QRCodeCanvas
value={targetUrl}
size={200}
bgColor={activePreset.qrBg || "#ffffff"}
fgColor={activePreset.qrColor || "#111111"}
level="H"
style={{ width: "100%", height: "100%" }}
includeMargin={false}
/>
</div>
</div>
) : (
<div
ref={standRef}
className="w-[360px] sm:w-[380px] min-h-[660px] rounded-[2.5rem] p-7 flex flex-col items-center text-center relative shadow-[0_25px_60px_rgba(0,0,0,0.6)] border-2"
style={{
background: activePreset.plaqueBg,
borderColor: activePreset.borderAccent,
}}
>
{/* Inner Line Accent */}
<div
className="absolute inset-2.5 rounded-[2rem] border pointer-events-none"
style={{ borderColor: activePreset.borderAccent }}
/>
{/* 01. Brand Badge */}
<header className="flex flex-col items-center text-center mt-2 z-10">
<div
className="border-2 rounded-xl px-4 py-2 flex flex-col items-center mb-4 shadow-md"
style={{
borderColor: activePreset.logoBorder,
backgroundColor: activePreset.logoBg,
}}
>
{logoUrl ? (
<img
src={logoUrl}
alt={restaurantName}
className="h-10 w-auto max-w-[140px] object-contain mb-1"
/>
) : null}
<span
className="font-black text-xl tracking-wider uppercase font-['Montserrat'] leading-tight"
style={{ color: activePreset.accentText }}
>
{restaurantName}
</span>
</div>
{/* Scan Action Text */}
<h2
className="font-['Montserrat'] font-black text-2xl sm:text-3xl tracking-tight uppercase leading-[0.95] drop-shadow-sm"
style={{ color: activePreset.headerText }}
>
DİJİTAL MENÜ<br />
<span className="tracking-wide">KAMERANIZLA TARATIN</span>
</h2>
</header>
{/* 02. Centered Custom QR Code Box */}
<section
className="w-60 h-60 sm:w-64 sm:h-64 rounded-3xl p-5 shadow-2xl flex items-center justify-center my-6 relative border border-black/15 z-10"
style={{ backgroundColor: activePreset.qrBg }}
>
<QRCodeCanvas
value={targetUrl}
size={200}
bgColor={activePreset.qrBg}
fgColor={activePreset.qrColor}
level="H"
includeMargin={false}
/>
</section>
{/* 03. Category Highlights Footer */}
<footer className="w-full relative px-4 text-center z-10 pb-2 flex-1 flex flex-col justify-end">
<p
className="text-xs tracking-wide mb-3 font-semibold"
style={{ color: activePreset.subText }}
>
Telefonunuzun kamerasını doğrultun
</p>
{/* Category Text Stack */}
<div
className="flex flex-col gap-1.5 max-w-[210px] mx-auto font-['Montserrat'] font-extrabold uppercase tracking-widest text-sm sm:text-base"
style={{ color: activePreset.headerText }}
>
{displayCategories.map((cat, idx) => (
<div key={idx} className="w-full">
<div className="py-0.5">{cat}</div>
{idx < displayCategories.length - 1 && (
<div
className="w-full h-[1px] my-0.5"
style={{ backgroundColor: activePreset.dividerColor }}
/>
)}
</div>
))}
</div>
</footer>
</div>
)}
</div>
</div>
</main>
</div>
);
}