feat(themes): add Swiss Brutalist restaurant design system with chapter numbers and high-contrast grid
This commit is contained in:
@@ -155,14 +155,24 @@ export const restaurantsRoutes: FastifyPluginAsync = async (app) => {
|
||||
return reply.code(400).send({ message: "themeKey is required" });
|
||||
}
|
||||
|
||||
const THEME_NAMES: Record<string, string> = {
|
||||
swiss: "Swiss Brutalist",
|
||||
artisan: "Amber Artisan",
|
||||
elegant: "Elegant Gold",
|
||||
modern: "Modern Sapphire",
|
||||
dark: "Luxury Dark",
|
||||
minimal: "Nordic Minimal",
|
||||
classic: "Classic Bistro",
|
||||
};
|
||||
|
||||
let themeId = themeData?.id;
|
||||
if (!themeId) {
|
||||
const { data: newTheme } = await supabase
|
||||
.from("themes")
|
||||
.insert({
|
||||
key: themeKey,
|
||||
name: themeKey === "artisan" ? "Amber Artisan" : themeKey,
|
||||
config: { primaryColor: "#E5A855", background: "#121316", productLayout: "card", categoryLayout: "tabs" },
|
||||
name: THEME_NAMES[themeKey] || themeKey,
|
||||
config: { primaryColor: themeKey === "swiss" ? "#000000" : "#E5A855", background: themeKey === "swiss" ? "#FFFFFF" : "#121316", productLayout: "card", categoryLayout: "tabs" },
|
||||
})
|
||||
.select("id")
|
||||
.maybeSingle();
|
||||
|
||||
Binary file not shown.
@@ -31,6 +31,18 @@ export interface ThemeOption {
|
||||
}
|
||||
|
||||
export const THEME_OPTIONS: ThemeOption[] = [
|
||||
{
|
||||
key: "swiss",
|
||||
name: "Swiss Brutalist",
|
||||
category: "Modern Sanat & Konsept",
|
||||
color: "#000000",
|
||||
badgeBg: "#000000",
|
||||
badgeText: "#FFFFFF",
|
||||
bgPreview: "#FFFFFF",
|
||||
accent: "Monokrom Siyah & Kobalt Mavi",
|
||||
desc: "Brutalist İsviçre tipografisi, 8px kalın siyah kenarlıklar, bölüm numaraları (Ø1, Ø2) ve monokrom ızgara.",
|
||||
suitableFor: "Çağdaş restoranlar, sanat galerisi kafeleri, avangard mutfaklar, konsept bistrolar",
|
||||
},
|
||||
{
|
||||
key: "artisan",
|
||||
name: "Amber Artisan",
|
||||
|
||||
@@ -315,7 +315,7 @@ export function PublicMenuClient({
|
||||
<div className="bg-stone-900 text-white px-5 py-4 border-b border-stone-800 animate-slide-up">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<p className="text-xs font-bold uppercase tracking-wider text-stone-400">
|
||||
Canlı Tema Önizleme (6 Şablon)
|
||||
Canlı Tema Önizleme (7 Şablon)
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
@@ -325,7 +325,7 @@ export function PublicMenuClient({
|
||||
Kapat ✕
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 sm:grid-cols-6 gap-2">
|
||||
<div className="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-7 gap-2">
|
||||
{Object.values(THEME_PRESETS).map((preset) => {
|
||||
const isSelected = selectedThemeKey === preset.key;
|
||||
return (
|
||||
@@ -422,176 +422,252 @@ export function PublicMenuClient({
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
filteredCategories.map((category) => (
|
||||
<section
|
||||
key={category.id}
|
||||
id={`category-${category.id}`}
|
||||
className="scroll-mt-28 space-y-3.5"
|
||||
>
|
||||
{/* Sticky Category Heading Banner */}
|
||||
<div
|
||||
className="sticky top-[58px] z-20 flex items-center justify-between py-2.5 px-3 -mx-3 rounded-2xl backdrop-blur-xl border-b transition-all shadow-sm"
|
||||
style={{
|
||||
backgroundColor: `${theme.config.background}F2`,
|
||||
borderColor: theme.cardBorder,
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<h2
|
||||
className="text-base sm:text-lg font-extrabold tracking-tight"
|
||||
style={{ color: theme.config.primaryColor }}
|
||||
>
|
||||
{category.name}
|
||||
</h2>
|
||||
{category.description && (
|
||||
<p className="text-xs mt-0.5 line-clamp-1" style={{ color: theme.textSecondary }}>
|
||||
{category.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<span
|
||||
className="text-[11px] font-bold px-2.5 py-0.5 rounded-full shadow-sm"
|
||||
style={{ backgroundColor: theme.badgeBg, color: theme.badgeText }}
|
||||
>
|
||||
{category.menu_items.length} Ürün
|
||||
</span>
|
||||
</div>
|
||||
filteredCategories.map((category, catIndex) => {
|
||||
const chapterNum = String(catIndex + 1).padStart(2, "0");
|
||||
const isSwiss = theme.key === "swiss";
|
||||
|
||||
{/* Items Container: Grid / Card / List Layout based on Theme */}
|
||||
{/* Items Container: Grid / Card / List Layout based on Theme */}
|
||||
<div
|
||||
className={
|
||||
theme.key === "artisan"
|
||||
? "grid grid-cols-2 gap-3 sm:gap-4"
|
||||
: theme.config.productLayout === "list"
|
||||
? "space-y-3"
|
||||
: "grid grid-cols-1 gap-3.5"
|
||||
}
|
||||
return (
|
||||
<section
|
||||
key={category.id}
|
||||
id={`category-${category.id}`}
|
||||
className={isSwiss ? "scroll-mt-28 space-y-0" : "scroll-mt-28 space-y-3.5"}
|
||||
>
|
||||
{category.menu_items.map((item) => {
|
||||
if (theme.key === "artisan") {
|
||||
{/* Category Heading Banner */}
|
||||
{isSwiss ? (
|
||||
<div className="sticky top-[58px] z-20 bg-white border-t-8 border-b-4 border-black py-4 px-3 flex items-end justify-between transition-all">
|
||||
<div>
|
||||
<span className="text-[11px] font-black uppercase tracking-widest text-[#1D4ED8] block mb-1">
|
||||
CHAPTER {chapterNum} // SECTION
|
||||
</span>
|
||||
<h2 className="text-2xl sm:text-3xl font-black uppercase tracking-tighter text-black leading-none">
|
||||
{category.name}
|
||||
</h2>
|
||||
{category.description && (
|
||||
<p className="text-xs text-stone-600 mt-1 font-medium line-clamp-1">
|
||||
{category.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-4xl sm:text-5xl font-black italic text-[#1D4ED8] leading-none tracking-tighter select-none">
|
||||
Ø{catIndex + 1}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="sticky top-[58px] z-20 flex items-center justify-between py-2.5 px-3 -mx-3 rounded-2xl backdrop-blur-xl border-b transition-all shadow-sm"
|
||||
style={{
|
||||
backgroundColor: `${theme.config.background}F2`,
|
||||
borderColor: theme.cardBorder,
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<h2
|
||||
className="text-base sm:text-lg font-extrabold tracking-tight"
|
||||
style={{ color: theme.config.primaryColor }}
|
||||
>
|
||||
{category.name}
|
||||
</h2>
|
||||
{category.description && (
|
||||
<p className="text-xs mt-0.5 line-clamp-1" style={{ color: theme.textSecondary }}>
|
||||
{category.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<span
|
||||
className="text-[11px] font-bold px-2.5 py-0.5 rounded-full shadow-sm"
|
||||
style={{ backgroundColor: theme.badgeBg, color: theme.badgeText }}
|
||||
>
|
||||
{category.menu_items.length} Ürün
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Items Container: Grid / Card / List Layout based on Theme */}
|
||||
<div
|
||||
className={
|
||||
isSwiss
|
||||
? "divide-y-2 divide-black border-b-2 border-black"
|
||||
: theme.key === "artisan"
|
||||
? "grid grid-cols-2 gap-3 sm:gap-4"
|
||||
: theme.config.productLayout === "list"
|
||||
? "space-y-3"
|
||||
: "grid grid-cols-1 gap-3.5"
|
||||
}
|
||||
>
|
||||
{category.menu_items.map((item, itemIndex) => {
|
||||
if (isSwiss) {
|
||||
const itemCode = `${String.fromCharCode(65 + catIndex)}${String(itemIndex + 1).padStart(2, "0")}`;
|
||||
return (
|
||||
<article
|
||||
key={item.id}
|
||||
onClick={() => setSelectedItem(item)}
|
||||
className="group bg-white hover:bg-stone-50 cursor-pointer flex flex-col sm:flex-row transition-all duration-200"
|
||||
>
|
||||
<div className="flex-1 p-4 flex flex-col justify-between">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1.5">
|
||||
<span className="text-xs font-black uppercase tracking-widest text-[#1D4ED8]">
|
||||
[{itemCode}]
|
||||
</span>
|
||||
<h3 className="font-black text-lg sm:text-xl uppercase tracking-tighter text-black group-hover:text-[#1D4ED8] transition-colors leading-tight">
|
||||
{item.name}
|
||||
</h3>
|
||||
</div>
|
||||
{item.description && (
|
||||
<p className="text-xs text-stone-600 leading-relaxed font-normal line-clamp-2">
|
||||
{item.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 pt-2 flex items-baseline justify-between border-t border-stone-200">
|
||||
<span className="text-[10px] font-black uppercase tracking-widest text-stone-500">
|
||||
[ORGANIC] [HOUSE-MADE]
|
||||
</span>
|
||||
<span className="text-xl sm:text-2xl font-black text-black tracking-tight">
|
||||
₺{item.price.toFixed(0)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{item.image_url && (
|
||||
<div className="sm:w-36 sm:min-w-[9rem] aspect-square sm:aspect-auto border-t sm:border-t-0 sm:border-l-2 border-black bg-stone-100 overflow-hidden flex-shrink-0">
|
||||
<img
|
||||
src={item.image_url}
|
||||
alt={item.name}
|
||||
className="w-full h-full object-cover grayscale contrast-125 group-hover:scale-105 group-hover:grayscale-0 transition-all duration-300"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
if (theme.key === "artisan") {
|
||||
return (
|
||||
<article
|
||||
key={item.id}
|
||||
onClick={() => setSelectedItem(item)}
|
||||
className="group relative rounded-3xl p-3.5 transition-all duration-300 cursor-pointer border flex flex-col justify-between hover:scale-[1.02] active:scale-[0.98] shadow-md"
|
||||
style={{
|
||||
backgroundColor: theme.cardBg,
|
||||
borderColor: theme.cardBorder,
|
||||
}}
|
||||
>
|
||||
{/* Image Container with 3D Dish presentation */}
|
||||
<div className="relative w-full aspect-square rounded-2xl overflow-hidden bg-stone-900/60 mb-3 flex items-center justify-center border border-white/5">
|
||||
{item.image_url ? (
|
||||
<img
|
||||
src={item.image_url}
|
||||
alt={item.name}
|
||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
|
||||
loading="lazy"
|
||||
/>
|
||||
) : (
|
||||
<div className="text-3xl opacity-60">✨</div>
|
||||
)}
|
||||
<span
|
||||
className="absolute top-2 right-2 px-2 py-0.5 rounded-full text-[9px] font-extrabold uppercase tracking-wide shadow-md"
|
||||
style={{ backgroundColor: theme.config.primaryColor, color: "#121316" }}
|
||||
>
|
||||
ÖZEL
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Info */}
|
||||
<div className="flex-1 flex flex-col justify-between">
|
||||
<div>
|
||||
<h3 className="font-bold text-xs sm:text-sm text-white tracking-tight leading-snug line-clamp-1 group-hover:text-amber-400 transition-colors">
|
||||
{item.name}
|
||||
</h3>
|
||||
{item.description ? (
|
||||
<p className="text-[11px] text-stone-400 mt-1 line-clamp-2 leading-relaxed">
|
||||
{item.description}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="mt-3 pt-2.5 border-t border-white/5 flex items-center justify-between">
|
||||
<span
|
||||
className="text-sm sm:text-base font-black tracking-tight"
|
||||
style={{ color: theme.config.primaryColor }}
|
||||
>
|
||||
₺{item.price.toFixed(0)}
|
||||
</span>
|
||||
<span
|
||||
className="w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold text-stone-950 shadow-sm transition-transform group-hover:scale-110"
|
||||
style={{ backgroundColor: theme.config.primaryColor }}
|
||||
>
|
||||
+
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<article
|
||||
key={item.id}
|
||||
onClick={() => setSelectedItem(item)}
|
||||
className="group relative rounded-3xl p-3.5 transition-all duration-300 cursor-pointer border flex flex-col justify-between hover:scale-[1.02] active:scale-[0.98] shadow-md"
|
||||
className={`group relative rounded-2xl p-4 transition-all duration-200 cursor-pointer border hover:shadow-lg active:scale-[0.99] flex gap-3.5 items-center ${
|
||||
theme.config.productLayout === "list" ? "justify-between" : ""
|
||||
}`}
|
||||
style={{
|
||||
backgroundColor: theme.cardBg,
|
||||
borderColor: theme.cardBorder,
|
||||
}}
|
||||
>
|
||||
{/* Image Container with 3D Dish presentation */}
|
||||
<div className="relative w-full aspect-square rounded-2xl overflow-hidden bg-stone-900/60 mb-3 flex items-center justify-center border border-white/5">
|
||||
{item.image_url ? (
|
||||
<img
|
||||
src={item.image_url}
|
||||
alt={item.name}
|
||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
|
||||
loading="lazy"
|
||||
/>
|
||||
) : (
|
||||
<div className="text-3xl opacity-60">✨</div>
|
||||
)}
|
||||
<span
|
||||
className="absolute top-2 right-2 px-2 py-0.5 rounded-full text-[9px] font-extrabold uppercase tracking-wide shadow-md"
|
||||
style={{ backgroundColor: theme.config.primaryColor, color: "#121316" }}
|
||||
>
|
||||
ÖZEL
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Info */}
|
||||
<div className="flex-1 flex flex-col justify-between">
|
||||
<div>
|
||||
<h3 className="font-bold text-xs sm:text-sm text-white tracking-tight leading-snug line-clamp-1 group-hover:text-amber-400 transition-colors">
|
||||
{/* Item Details */}
|
||||
<div className="flex-1 min-w-0 pr-1">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<h3 className="font-bold text-sm tracking-tight leading-snug group-hover:text-amber-600 transition-colors">
|
||||
{item.name}
|
||||
</h3>
|
||||
{item.description ? (
|
||||
<p className="text-[11px] text-stone-400 mt-1 line-clamp-2 leading-relaxed">
|
||||
{item.description}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="mt-3 pt-2.5 border-t border-white/5 flex items-center justify-between">
|
||||
{item.description && (
|
||||
<p
|
||||
className="text-xs mt-1 leading-relaxed line-clamp-2"
|
||||
style={{ color: theme.textSecondary }}
|
||||
>
|
||||
{item.description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="mt-2.5 flex items-center justify-between">
|
||||
<span
|
||||
className="text-sm sm:text-base font-black tracking-tight"
|
||||
className="text-base font-extrabold tracking-tight"
|
||||
style={{ color: theme.config.primaryColor }}
|
||||
>
|
||||
₺{item.price.toFixed(0)}
|
||||
</span>
|
||||
<span
|
||||
className="w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold text-stone-950 shadow-sm transition-transform group-hover:scale-110"
|
||||
style={{ backgroundColor: theme.config.primaryColor }}
|
||||
>
|
||||
+
|
||||
|
||||
<span className="text-[10px] font-bold px-2 py-0.5 rounded-md bg-black/5 dark:bg-white/10 group-hover:bg-amber-100 dark:group-hover:bg-amber-900/40 transition-colors">
|
||||
İncele →
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Item Thumbnail Image */}
|
||||
{item.image_url && (
|
||||
<div className="relative w-20 h-20 rounded-xl overflow-hidden flex-shrink-0 bg-stone-100 dark:bg-stone-800 shadow-sm">
|
||||
<img
|
||||
src={item.image_url}
|
||||
alt={item.name}
|
||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<article
|
||||
key={item.id}
|
||||
onClick={() => setSelectedItem(item)}
|
||||
className={`group relative rounded-2xl p-4 transition-all duration-200 cursor-pointer border hover:shadow-lg active:scale-[0.99] flex gap-3.5 items-center ${
|
||||
theme.config.productLayout === "list" ? "justify-between" : ""
|
||||
}`}
|
||||
style={{
|
||||
backgroundColor: theme.cardBg,
|
||||
borderColor: theme.cardBorder,
|
||||
}}
|
||||
>
|
||||
{/* Item Details */}
|
||||
<div className="flex-1 min-w-0 pr-1">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<h3 className="font-bold text-sm tracking-tight leading-snug group-hover:text-amber-600 transition-colors">
|
||||
{item.name}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{item.description && (
|
||||
<p
|
||||
className="text-xs mt-1 leading-relaxed line-clamp-2"
|
||||
style={{ color: theme.textSecondary }}
|
||||
>
|
||||
{item.description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="mt-2.5 flex items-center justify-between">
|
||||
<span
|
||||
className="text-base font-extrabold tracking-tight"
|
||||
style={{ color: theme.config.primaryColor }}
|
||||
>
|
||||
₺{item.price.toFixed(0)}
|
||||
</span>
|
||||
|
||||
<span className="text-[10px] font-bold px-2 py-0.5 rounded-md bg-black/5 dark:bg-white/10 group-hover:bg-amber-100 dark:group-hover:bg-amber-900/40 transition-colors">
|
||||
İncele →
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Item Thumbnail Image */}
|
||||
{item.image_url && (
|
||||
<div className="relative w-20 h-20 rounded-xl overflow-hidden flex-shrink-0 bg-stone-100 dark:bg-stone-800 shadow-sm">
|
||||
<img
|
||||
src={item.image_url}
|
||||
alt={item.name}
|
||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
))
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</main>
|
||||
|
||||
|
||||
@@ -16,6 +16,25 @@ export interface ThemePreset {
|
||||
}
|
||||
|
||||
export const THEME_PRESETS: Record<string, ThemePreset> = {
|
||||
swiss: {
|
||||
key: "swiss",
|
||||
name: "Swiss Brutalist",
|
||||
fontFamily: "heading",
|
||||
config: {
|
||||
primaryColor: "#000000",
|
||||
background: "#FFFFFF",
|
||||
productLayout: "card",
|
||||
categoryLayout: "tabs",
|
||||
},
|
||||
cardBg: "#FFFFFF",
|
||||
cardBorder: "#000000",
|
||||
textPrimary: "#000000",
|
||||
textSecondary: "#666666",
|
||||
accentBg: "#F0F0F0",
|
||||
headerGradient: "linear-gradient(180deg, #000000 0%, #111111 100%)",
|
||||
badgeBg: "#000000",
|
||||
badgeText: "#FFFFFF",
|
||||
},
|
||||
artisan: {
|
||||
key: "artisan",
|
||||
name: "Amber Artisan",
|
||||
|
||||
Reference in New Issue
Block a user