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

557 lines
26 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 React, { useState } from "react";
import Link from "next/link";
import { THEME_PRESETS, ThemePreset } from "@/lib/theme";
import { DEMO_RESTAURANT, DEMO_CATEGORIES } from "@/lib/demo-data";
import { PublicMenuClient } from "@/components/PublicMenuClient";
export function MenuTemplatesAdminClient() {
const [themesMap, setThemesMap] = useState<Record<string, ThemePreset>>(THEME_PRESETS);
const [activeThemeKey, setActiveThemeKey] = useState<string>("coastal");
const [activeTab, setActiveTab] = useState<"gallery" | "customize">("gallery");
const [toastMsg, setToastMsg] = useState<string | null>(null);
const [jsonModalOpen, setJsonModalOpen] = useState(false);
const [importedJson, setImportedJson] = useState("");
const currentTheme = themesMap[activeThemeKey] || THEME_PRESETS.coastal!;
const showToast = (msg: string) => {
setToastMsg(msg);
setTimeout(() => setToastMsg(null), 3000);
};
const handleUpdateCurrentTheme = (updates: Partial<ThemePreset> | Partial<ThemePreset["config"]>) => {
setThemesMap((prev) => {
const existing = prev[activeThemeKey];
if (!existing) return prev;
let updatedConfig = { ...existing.config };
let updatedTopLevel = { ...existing };
if ("primaryColor" in updates || "productLayout" in updates || "categoryLayout" in updates || "headerLayout" in updates) {
updatedConfig = { ...updatedConfig, ...updates };
} else {
updatedTopLevel = { ...updatedTopLevel, ...updates };
}
return {
...prev,
[activeThemeKey]: {
...updatedTopLevel,
config: updatedConfig,
},
};
});
};
const handleSaveAll = () => {
showToast(`"${currentTheme.name}" şablon ayarları başarıyla kaydedildi!`);
};
const handleImportJsonSubmit = () => {
try {
const parsed = JSON.parse(importedJson);
if (!parsed.key || !parsed.name || !parsed.config) {
throw new Error("Geçersiz şablon JSON formatı. 'key', 'name' ve 'config' alanları zorunludur.");
}
setThemesMap((prev) => ({
...prev,
[parsed.key]: parsed,
}));
setActiveThemeKey(parsed.key);
setJsonModalOpen(false);
setImportedJson("");
showToast(`Yeni şablon "${parsed.name}" başarıyla içe aktarıldı!`);
} catch (err: any) {
alert("Hata: " + (err?.message || "Geçersiz JSON formatı"));
}
};
return (
<div className="max-w-7xl mx-auto space-y-6">
{/* Toast Bildirimi */}
{toastMsg && (
<div className="fixed top-20 right-6 z-50 bg-[#D4AF37] text-black font-extrabold px-5 py-3 rounded-2xl shadow-2xl shadow-[#D4AF37]/30 text-xs flex items-center gap-2 animate-bounce">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2.5" d="M5 13l4 4L19 7" />
</svg>
{toastMsg}
</div>
)}
{/* Üst Karşılama Barı */}
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4 bg-[#121215] border border-white/10 p-6 rounded-3xl">
<div>
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-[#D4AF37]/10 border border-[#D4AF37]/30 text-xs font-bold text-[#D4AF37] mb-2">
🎨 WEB DİNAMİK MENÜ TEMA STÜDYOSU
</div>
<h1 className="text-2xl md:text-3xl font-black text-white tracking-tight">
Web Menü Şablon Yöneticisi
</h1>
<p className="text-xs text-zinc-400 mt-1">
Restoran web menülerinin renk paletlerini, düzen yapılarını ve tipografisini canlı cihaz simülatöründe anında test edip yönetin.
</p>
</div>
<div className="flex items-center gap-3 flex-wrap">
<button
type="button"
onClick={() => {
setImportedJson(JSON.stringify(currentTheme, null, 2));
setJsonModalOpen(true);
}}
className="px-4 py-2.5 rounded-xl bg-white/5 border border-white/10 hover:bg-white/10 text-xs font-bold text-zinc-300 transition-all flex items-center gap-2 cursor-pointer"
>
<svg className="w-4 h-4 text-[#D4AF37]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
</svg>
JSON İçe/Dışa Aktar
</button>
<button
type="button"
onClick={handleSaveAll}
className="px-5 py-2.5 rounded-xl bg-gradient-to-r from-[#D4AF37] to-[#F59E0B] text-black font-extrabold text-xs shadow-lg shadow-[#D4AF37]/20 hover:opacity-95 transition-all flex items-center gap-2 cursor-pointer"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2.5" d="M5 13l4 4L19 7" />
</svg>
Değişiklikleri Yayınla & Kaydet
</button>
</div>
</div>
{/* Ana İçerik Çift Sütun Düzeni: Sol Kontrol / Sağ Canlı Simülatör */}
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 items-start">
{/* SOL PANEL: Şablon Galerisi ve Özelleştirici */}
<div className="lg:col-span-7 space-y-6">
{/* Sekme Butonları */}
<div className="flex items-center gap-2 p-1.5 rounded-2xl bg-[#121215] border border-white/10">
<button
type="button"
onClick={() => setActiveTab("gallery")}
className={`flex-1 py-2.5 px-4 rounded-xl font-extrabold text-xs transition-all flex items-center justify-center gap-2 cursor-pointer ${
activeTab === "gallery"
? "bg-[#D4AF37] text-black shadow-lg shadow-[#D4AF37]/20"
: "text-zinc-400 hover:text-white"
}`}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" />
</svg>
Hazır Şablonlar ({Object.keys(themesMap).length})
</button>
<button
type="button"
onClick={() => setActiveTab("customize")}
className={`flex-1 py-2.5 px-4 rounded-xl font-extrabold text-xs transition-all flex items-center justify-center gap-2 cursor-pointer ${
activeTab === "customize"
? "bg-[#D4AF37] text-black shadow-lg shadow-[#D4AF37]/20"
: "text-zinc-400 hover:text-white"
}`}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" />
</svg>
Seçili Temayı Özelleştir ({currentTheme.key})
</button>
</div>
{/* TAB 1: ŞABLON GALERİSİ KARTLARI */}
{activeTab === "gallery" && (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{Object.values(themesMap).map((theme) => {
const isSelected = theme.key === activeThemeKey;
return (
<div
key={theme.key}
onClick={() => setActiveThemeKey(theme.key)}
className={`p-5 rounded-2xl border transition-all cursor-pointer space-y-4 relative overflow-hidden group ${
isSelected
? "bg-[#16161A] border-[#D4AF37] ring-2 ring-[#D4AF37]/30 shadow-xl shadow-[#D4AF37]/10"
: "bg-[#121215] border-white/10 hover:border-white/20 hover:bg-white/[0.02]"
}`}
>
{/* Üst İkon & Seçim Rozeti */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span
className="w-4 h-4 rounded-full border border-black/20 shadow-sm"
style={{ backgroundColor: theme.config.primaryColor }}
/>
<span
className="w-4 h-4 rounded-full border border-black/20 shadow-sm"
style={{ backgroundColor: theme.cardBg }}
/>
</div>
{isSelected ? (
<span className="text-[10px] bg-[#D4AF37] text-black font-extrabold px-2 py-0.5 rounded-full flex items-center gap-1">
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="3" d="M5 13l4 4L19 7" />
</svg>
AKTİF
</span>
) : (
<span className="text-[10px] text-zinc-500 font-mono uppercase">
{theme.key}
</span>
)}
</div>
{/* Tema Adı & Açıklama */}
<div>
<h3 className="font-extrabold text-white text-base group-hover:text-[#D4AF37] transition-colors">
{theme.name}
</h3>
<div className="flex items-center gap-2 mt-2 text-[11px] text-zinc-400">
<span className="bg-white/5 border border-white/10 px-2 py-0.5 rounded-md capitalize font-mono">
{theme.fontFamily}
</span>
<span className="bg-white/5 border border-white/10 px-2 py-0.5 rounded-md capitalize font-mono">
{theme.config.productLayout}
</span>
<span className="bg-white/5 border border-white/10 px-2 py-0.5 rounded-md capitalize font-mono">
{theme.config.categoryLayout}
</span>
</div>
</div>
{/* Alt Butonlar */}
<div className="pt-3 border-t border-white/5 flex items-center justify-between">
<button
type="button"
onClick={(e) => {
e.stopPropagation();
setActiveThemeKey(theme.key);
setActiveTab("customize");
}}
className="text-xs font-bold text-[#D4AF37] hover:underline flex items-center gap-1"
>
Renk & Düzen Özelleştir &rarr;
</button>
</div>
</div>
);
})}
</div>
)}
{/* TAB 2: SEÇİLİ TEMAYI ÖZELLEŞTİR */}
{activeTab === "customize" && (
<div className="p-6 rounded-2xl bg-[#121215] border border-white/10 space-y-6">
<div className="flex items-center justify-between border-b border-white/10 pb-4">
<div>
<h3 className="text-lg font-bold text-white flex items-center gap-2">
<span
className="w-3.5 h-3.5 rounded-full inline-block"
style={{ backgroundColor: currentTheme.config.primaryColor }}
/>
{currentTheme.name}
</h3>
<p className="text-xs text-zinc-400 mt-0.5">
Ana renkleri, arka plan tonlarını ve düzen tiplerini anında özelleştirin.
</p>
</div>
<button
type="button"
onClick={() => setActiveTab("gallery")}
className="text-xs font-bold text-zinc-400 hover:text-white"
>
&larr; Galeriye Dön
</button>
</div>
{/* Renk Ayarları */}
<div className="space-y-4">
<h4 className="text-xs font-extrabold text-[#D4AF37] uppercase tracking-wider">
1. Renk Paleti & Tonlar
</h4>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{/* Primary Color */}
<div className="space-y-1.5 p-3 rounded-xl bg-white/[0.03] border border-white/5">
<label className="text-xs font-semibold text-zinc-300 block">
Ana Vurgu Rengi (Primary)
</label>
<div className="flex items-center gap-3">
<input
type="color"
value={currentTheme.config.primaryColor || "#D4AF37"}
onChange={(e) =>
handleUpdateCurrentTheme({ primaryColor: e.target.value })
}
className="w-10 h-10 rounded-lg cursor-pointer bg-transparent border-0"
/>
<input
type="text"
value={currentTheme.config.primaryColor || "#D4AF37"}
onChange={(e) =>
handleUpdateCurrentTheme({ primaryColor: e.target.value })
}
className="flex-1 bg-black/40 border border-white/10 rounded-lg px-3 py-1.5 text-xs text-white font-mono uppercase"
/>
</div>
</div>
{/* Background Color */}
<div className="space-y-1.5 p-3 rounded-xl bg-white/[0.03] border border-white/5">
<label className="text-xs font-semibold text-zinc-300 block">
Sayfa Arka Plan Rengi
</label>
<div className="flex items-center gap-3">
<input
type="color"
value={currentTheme.config.background || "#FAF8F5"}
onChange={(e) =>
handleUpdateCurrentTheme({ background: e.target.value })
}
className="w-10 h-10 rounded-lg cursor-pointer bg-transparent border-0"
/>
<input
type="text"
value={currentTheme.config.background || "#FAF8F5"}
onChange={(e) =>
handleUpdateCurrentTheme({ background: e.target.value })
}
className="flex-1 bg-black/40 border border-white/10 rounded-lg px-3 py-1.5 text-xs text-white font-mono uppercase"
/>
</div>
</div>
{/* Card Background */}
<div className="space-y-1.5 p-3 rounded-xl bg-white/[0.03] border border-white/5">
<label className="text-xs font-semibold text-zinc-300 block">
Ürün Kart Arka Planı
</label>
<div className="flex items-center gap-3">
<input
type="color"
value={currentTheme.cardBg.startsWith("#") ? currentTheme.cardBg : "#FFFFFF"}
onChange={(e) => handleUpdateCurrentTheme({ cardBg: e.target.value })}
className="w-10 h-10 rounded-lg cursor-pointer bg-transparent border-0"
/>
<input
type="text"
value={currentTheme.cardBg}
onChange={(e) => handleUpdateCurrentTheme({ cardBg: e.target.value })}
className="flex-1 bg-black/40 border border-white/10 rounded-lg px-3 py-1.5 text-xs text-white font-mono uppercase"
/>
</div>
</div>
{/* Text Primary */}
<div className="space-y-1.5 p-3 rounded-xl bg-white/[0.03] border border-white/5">
<label className="text-xs font-semibold text-zinc-300 block">
Ana Başlık & Fiyat Yazı Rengi
</label>
<div className="flex items-center gap-3">
<input
type="color"
value={currentTheme.textPrimary.startsWith("#") ? currentTheme.textPrimary : "#1C1917"}
onChange={(e) => handleUpdateCurrentTheme({ textPrimary: e.target.value })}
className="w-10 h-10 rounded-lg cursor-pointer bg-transparent border-0"
/>
<input
type="text"
value={currentTheme.textPrimary}
onChange={(e) => handleUpdateCurrentTheme({ textPrimary: e.target.value })}
className="flex-1 bg-black/40 border border-white/10 rounded-lg px-3 py-1.5 text-xs text-white font-mono uppercase"
/>
</div>
</div>
</div>
</div>
{/* Düzen & Yerleşim Ayarları */}
<div className="space-y-4 pt-4 border-t border-white/10">
<h4 className="text-xs font-extrabold text-[#D4AF37] uppercase tracking-wider">
2. Düzen & Yerleşim Mimarisi
</h4>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
{/* Product Layout */}
<div className="space-y-2">
<label className="text-xs font-semibold text-zinc-300 block">
Ürün Görünüm Tipi
</label>
<select
value={currentTheme.config.productLayout || "card"}
onChange={(e) =>
handleUpdateCurrentTheme({ productLayout: e.target.value as any })
}
className="w-full bg-black/40 border border-white/10 rounded-xl px-3 py-2 text-xs text-white focus:border-[#D4AF37] focus:outline-none"
>
<option value="card">Card (Kutu / Kart)</option>
<option value="list">List (Satır / Liste)</option>
</select>
</div>
{/* Category Layout */}
<div className="space-y-2">
<label className="text-xs font-semibold text-zinc-300 block">
Kategori Gezinme Tipi
</label>
<select
value={currentTheme.config.categoryLayout || "tabs"}
onChange={(e) =>
handleUpdateCurrentTheme({ categoryLayout: e.target.value as any })
}
className="w-full bg-black/40 border border-white/10 rounded-xl px-3 py-2 text-xs text-white focus:border-[#D4AF37] focus:outline-none"
>
<option value="tabs">Tabs (Yatay Sekmeler)</option>
<option value="accordion">Accordion (Açılır Kapanır)</option>
<option value="flat">Flat (Düz Sıralı)</option>
</select>
</div>
{/* Header Layout */}
<div className="space-y-2">
<label className="text-xs font-semibold text-zinc-300 block">
Header Hizalaması
</label>
<select
value={currentTheme.config.headerLayout || "centered"}
onChange={(e) =>
handleUpdateCurrentTheme({ headerLayout: e.target.value as any })
}
className="w-full bg-black/40 border border-white/10 rounded-xl px-3 py-2 text-xs text-white focus:border-[#D4AF37] focus:outline-none"
>
<option value="centered">Centered (Ortalanmış)</option>
<option value="left">Left (Sola Hizalı)</option>
</select>
</div>
</div>
</div>
{/* Tipografi */}
<div className="space-y-4 pt-4 border-t border-white/10">
<h4 className="text-xs font-extrabold text-[#D4AF37] uppercase tracking-wider">
3. Tipografi & Font Ailesi
</h4>
<div className="grid grid-cols-3 gap-3">
{[
{ id: "serif", label: "Zarif Serif", desc: "Playfair / Georgia" },
{ id: "sans", label: "Modern Sans", desc: "Inter / System" },
{ id: "heading", label: "Güçlü Display", desc: "Outfit / Montserrat" },
].map((font) => (
<button
key={font.id}
type="button"
onClick={() => handleUpdateCurrentTheme({ fontFamily: font.id as any })}
className={`p-3 rounded-xl border text-left transition-all cursor-pointer ${
currentTheme.fontFamily === font.id
? "bg-[#D4AF37]/15 border-[#D4AF37] text-white"
: "bg-white/[0.02] border-white/10 text-zinc-400 hover:text-white"
}`}
>
<div className="text-xs font-bold">{font.label}</div>
<div className="text-[10px] opacity-70 mt-0.5">{font.desc}</div>
</button>
))}
</div>
</div>
</div>
)}
</div>
{/* SAĞ PANEL: Canlı İnceleme Telefon Simülatörü */}
<div className="lg:col-span-5 sticky top-20">
<div className="bg-[#121215] border border-white/10 rounded-3xl p-5 space-y-4 shadow-2xl">
<div className="flex items-center justify-between border-b border-white/10 pb-3">
<div className="flex items-center gap-2">
<div className="w-2.5 h-2.5 rounded-full bg-emerald-400 animate-pulse" />
<span className="text-xs font-bold text-white">Canlı Telefon Simülatörü</span>
</div>
<span className="text-[10px] font-mono text-[#D4AF37] bg-[#D4AF37]/10 px-2 py-0.5 rounded-full font-bold">
{currentTheme.name}
</span>
</div>
{/* Mock iPhone Frame */}
<div className="w-full max-w-[340px] h-[640px] mx-auto rounded-[40px] border-[8px] border-zinc-800 bg-black shadow-2xl overflow-hidden relative flex flex-col">
{/* iPhone Notch Header */}
<div className="w-full h-6 bg-black shrink-0 flex items-center justify-center relative z-20">
<div className="w-24 h-4 bg-zinc-900 rounded-b-xl" />
</div>
{/* Dynamic Theme Render Component */}
<div className="flex-1 overflow-y-auto relative">
<PublicMenuClient
restaurant={DEMO_RESTAURANT}
categories={DEMO_CATEGORIES}
initialThemeKey={currentTheme.key}
customThemeConfig={currentTheme.config}
/>
</div>
{/* Home Bar Indicator */}
<div className="w-full h-4 bg-black shrink-0 flex items-center justify-center z-20">
<div className="w-28 h-1 bg-zinc-700 rounded-full" />
</div>
</div>
<div className="text-center text-[11px] text-zinc-500">
Sol taraftan yaptığınız renk ve düzen değişiklikleri anında burada simüle edilir.
</div>
</div>
</div>
</div>
{/* JSON İÇE / DIŞA AKTARMA MODAL */}
{jsonModalOpen && (
<div className="fixed inset-0 z-50 bg-black/80 backdrop-blur-sm flex items-center justify-center p-4">
<div className="w-full max-w-xl bg-[#121215] border border-white/15 rounded-3xl p-6 space-y-4 shadow-2xl">
<div className="flex items-center justify-between border-b border-white/10 pb-3">
<h3 className="font-extrabold text-white text-base">
Tema JSON Spesifikasyonu
</h3>
<button
type="button"
onClick={() => setJsonModalOpen(false)}
className="text-zinc-400 hover:text-white"
>
</button>
</div>
<p className="text-xs text-zinc-400">
Mevcut temanın JSON kodunu kopyalayabilir veya yeni bir tema JSON spesifikasyonunu buraya yapıştırıp içe aktarabilirsiniz:
</p>
<textarea
value={importedJson}
onChange={(e) => setImportedJson(e.target.value)}
rows={12}
className="w-full bg-black/60 border border-white/10 rounded-xl p-4 text-xs font-mono text-emerald-400 focus:outline-none focus:border-[#D4AF37]"
/>
<div className="flex items-center justify-end gap-3 pt-2">
<button
type="button"
onClick={() => {
navigator.clipboard.writeText(importedJson);
showToast("JSON panoya kopyalandı!");
}}
className="px-4 py-2 rounded-xl bg-white/5 border border-white/10 hover:bg-white/10 text-xs font-bold text-white cursor-pointer"
>
Panoya Kopyala
</button>
<button
type="button"
onClick={handleImportJsonSubmit}
className="px-4 py-2 rounded-xl bg-[#D4AF37] text-black font-extrabold text-xs cursor-pointer shadow-lg shadow-[#D4AF37]/20"
>
İçe Aktar & Uygula
</button>
</div>
</div>
</div>
)}
</div>
);
}