feat: admin auth, web menu template studio, live text editing & Ayris Tech footer backlink

This commit is contained in:
AyrisAI
2026-08-23 01:45:00 +03:00
parent e7ba6c7807
commit 1f8195ff67
48 changed files with 5588 additions and 402 deletions
@@ -0,0 +1,556 @@
"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>
);
}