Files
menulio/apps/mobile/src/components/menu/ThemeSelectorSheet.tsx
T

434 lines
16 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.
import { useEffect, useState } from "react";
import {
ActivityIndicator,
Alert,
Linking,
Modal,
Platform,
Pressable,
ScrollView,
Share,
Text,
View,
} from "react-native";
import { Ionicons } from "@expo/vector-icons";
import * as Haptics from "expo-haptics";
import * as Clipboard from "expo-clipboard";
import { api } from "@/lib/api";
import { getPublicMenuUrl } from "@/lib/urls";
export interface ThemeOption {
key: string;
name: string;
category: string;
color: string;
badgeBg: string;
badgeText: string;
bgPreview: string;
desc: string;
suitableFor: string;
accent: string;
}
export const THEME_OPTIONS: ThemeOption[] = [
{
key: "artisan",
name: "Amber Artisan",
category: "Gurme & Organik / Koyu",
color: "#E5A855",
badgeBg: "#282B34",
badgeText: "#E5A855",
bgPreview: "#121316",
accent: "Amber Bal & Mat Oniks",
desc: "Koyu antrasit mat zemin, kehribar vurgular, modern bento ızgarası ve yuvarlak ürün kartları.",
suitableFor: "Gurme şarküteri, kuruyemiş & organik market, fıçı bira evleri, artisan kahveciler",
},
{
key: "elegant",
name: "Elegant Gold",
category: "Fine Dining & Lüks",
color: "#C8A96B",
badgeBg: "#FDF8F0",
badgeText: "#926E27",
bgPreview: "#FAF8F5",
accent: "Altın & Fildişi",
desc: "Zarif altın ve ipeksi krem tonlarında, yüksek prestijli serif tipografi.",
suitableFor: "Fine dining, şarap evleri, gurme steakhouse",
},
{
key: "modern",
name: "Modern Sapphire",
category: "Kafe & Fast Casual",
color: "#2563EB",
badgeBg: "#EFF6FF",
badgeText: "#1D4ED8",
bgPreview: "#F8FAFC",
accent: "Kraliyet Safiri & Beyaz",
desc: "Canlı mavi safir ve beyaz tonlarında, hızlı gezinti odaklı modern ızgara mizanpajı.",
suitableFor: "Kafeler, burgerciler, yeni nesil bistrolar",
},
{
key: "dark",
name: "Luxury Dark",
category: "Gece Kulübü & Bar",
color: "#F59E0B",
badgeBg: "#27272A",
badgeText: "#FBBF24",
bgPreview: "#0F0F12",
accent: "Obsidian & Kehribar",
desc: "Koyu obsidian siyahı ve kehribar parıltılı, loş ortamlara özel şık gece modu.",
suitableFor: "Kokteyl barlar, lounge, gece kulüpleri",
},
{
key: "minimal",
name: "Nordic Minimal",
category: "Butik Fırın & Kahveci",
color: "#18181B",
badgeBg: "#F4F4F5",
badgeText: "#27272A",
bgPreview: "#FAFAFA",
accent: "Monokrom & Grafit",
desc: "Ferah negatif alanlar, sakin monokrom renkler ve sade liste düzeni.",
suitableFor: "3. nesil kahveciler, butik fırınlar, tatlıcılar",
},
{
key: "classic",
name: "Classic Bistro",
category: "Geleneksel & Rustik",
color: "#8B1E1E",
badgeBg: "#FEF2F2",
badgeText: "#991B1B",
bgPreview: "#FFF9F2",
accent: "Toskana Bordo & Sıcak Ahşap",
desc: "Toskana bordo ve sıcak rustik dokularla geleneksel menü panosu hissi.",
suitableFor: "Meyhaneler, geleneksel lokantalar, trattoria'lar",
},
];
interface ThemeSelectorSheetProps {
visible: boolean;
restaurantId: string;
restaurantSlug?: string;
onClose: () => void;
onThemeChanged?: (themeKey: string) => void;
}
export function ThemeSelectorSheet({
visible,
restaurantId,
restaurantSlug,
onClose,
onThemeChanged,
}: ThemeSelectorSheetProps) {
const [selectedKey, setSelectedKey] = useState("elegant");
const [loading, setLoading] = useState(true);
const [savingKey, setSavingKey] = useState<string | null>(null);
const WEB_BASE_URL = process.env.EXPO_PUBLIC_WEB_URL || "http://localhost:3000";
useEffect(() => {
if (!visible || !restaurantId) return;
(async () => {
setLoading(true);
try {
const res = await api.get<{ themeKey: string }>(`/restaurants/${restaurantId}/theme`);
if (res?.themeKey) {
setSelectedKey(res.themeKey);
}
} catch {
setSelectedKey("elegant");
} finally {
setLoading(false);
}
})();
}, [visible, restaurantId]);
async function handleApplyTheme(key: string) {
setSelectedKey(key);
setSavingKey(key);
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium).catch(() => {});
try {
await api.put(`/restaurants/${restaurantId}/theme`, { themeKey: key });
onThemeChanged?.(key);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success).catch(() => {});
Alert.alert("Başarılı 🎉", "Menü şablonunuz güncellendi! Müşterileriniz artık bu temayı görecek.");
} catch (err) {
Alert.alert("Hata", err instanceof Error ? err.message : "Şablon uygulanamadı.");
} finally {
setSavingKey(null);
}
}
function handleOpenDemo(key: string) {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light).catch(() => {});
const demoUrl = `${WEB_BASE_URL}/demo?theme=${key}`;
Linking.openURL(demoUrl).catch(() => {
Alert.alert("Demo Linki", demoUrl);
});
}
async function handleShareDemo(theme: ThemeOption) {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light).catch(() => {});
const demoUrl = `${WEB_BASE_URL}/demo?theme=${theme.key}`;
try {
await Share.share({
title: `${theme.name} — menul.io Canlı Demo`,
message: `menul.io "${theme.name}" restoran menü şablonunu canlı olarak inceleyin:\n${demoUrl}`,
});
} catch {
await Clipboard.setStringAsync(demoUrl);
Alert.alert("Kopyalandı", "Demo linki panoya kopyalandı.");
}
}
function handleOpenMyMenuPreview(key: string) {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light).catch(() => {});
if (restaurantSlug) {
const myMenuUrl = `${getPublicMenuUrl(restaurantSlug)}?theme=${key}`;
Linking.openURL(myMenuUrl).catch(() => {
Alert.alert("Önizleme Linki", myMenuUrl);
});
} else {
handleOpenDemo(key);
}
}
return (
<Modal visible={visible} animationType="slide" transparent onRequestClose={onClose}>
<View style={{ flex: 1, backgroundColor: "rgba(0,0,0,0.55)", justifyContent: "flex-end" }}>
<Pressable style={{ flex: 1 }} onPress={onClose} />
<View
style={{
backgroundColor: "#FFFFFF",
borderTopLeftRadius: 28,
borderTopRightRadius: 28,
maxHeight: "92%",
paddingBottom: Platform.OS === "ios" ? 34 : 20,
}}
>
{/* Grab Bar */}
<View style={{ alignItems: "center", paddingTop: 10, paddingBottom: 6 }}>
<View
style={{
width: 40,
height: 4,
borderRadius: 2,
backgroundColor: "#E7E5E4",
}}
/>
</View>
{/* Header */}
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: 20,
paddingVertical: 12,
borderBottomWidth: 1,
borderBottomColor: "#F5F5F4",
}}
>
<View style={{ flex: 1, marginRight: 10 }}>
<Text style={{ fontSize: 18, fontWeight: "800", color: "#1C1917" }}>
Menü Şablonları & Canlı Önizleme
</Text>
<Text style={{ fontSize: 12, color: "#78716C", marginTop: 2 }}>
5 lüks şablonu inceleyin, demolarını paylaşın veya menünüze uygulayın
</Text>
</View>
<Pressable onPress={onClose} style={{ padding: 4 }}>
<Ionicons name="close-circle-outline" size={26} color="#9CA3AF" />
</Pressable>
</View>
{loading ? (
<View style={{ padding: 50, alignItems: "center" }}>
<ActivityIndicator size="large" color="#C8A96B" />
<Text style={{ marginTop: 12, color: "#78716C", fontSize: 13, fontWeight: "600" }}>
Şablonlar yükleniyor...
</Text>
</View>
) : (
<ScrollView contentContainerStyle={{ padding: 20, gap: 16 }}>
{THEME_OPTIONS.map((theme) => {
const isSelected = selectedKey === theme.key;
const isApplying = savingKey === theme.key;
return (
<View
key={theme.key}
style={{
backgroundColor: isSelected ? "#FAF8F5" : "#FFFFFF",
borderRadius: 20,
padding: 16,
borderWidth: 2,
borderColor: isSelected ? theme.color : "#E7E5E4",
shadowColor: "#000",
shadowOffset: { width: 0, height: 3 },
shadowOpacity: isSelected ? 0.08 : 0.03,
shadowRadius: 10,
elevation: isSelected ? 3 : 1,
}}
>
{/* Top Row: Color Pip, Name, Category Badge & Active Check */}
<View style={{ flexDirection: "row", justifyContent: "space-between", alignItems: "center", marginBottom: 8 }}>
<View style={{ flexDirection: "row", alignItems: "center", gap: 10 }}>
<View
style={{
width: 18,
height: 18,
borderRadius: 9,
backgroundColor: theme.color,
borderWidth: 2,
borderColor: "#FFFFFF",
shadowColor: "#000",
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.2,
shadowRadius: 2,
elevation: 2,
}}
/>
<View>
<Text style={{ fontSize: 16, fontWeight: "800", color: "#1C1917" }}>
{theme.name}
</Text>
</View>
</View>
<View
style={{
backgroundColor: theme.badgeBg,
paddingHorizontal: 8,
paddingVertical: 3,
borderRadius: 6,
}}
>
<Text style={{ fontSize: 11, fontWeight: "700", color: theme.badgeText }}>
{theme.category}
</Text>
</View>
</View>
{/* Description */}
<Text style={{ fontSize: 13, color: "#57534E", lineHeight: 18, marginBottom: 8 }}>
{theme.desc}
</Text>
{/* Meta details */}
<View style={{ flexDirection: "row", alignItems: "center", gap: 6, marginBottom: 14 }}>
<Ionicons name="sparkles" size={13} color={theme.color} />
<Text style={{ fontSize: 11, color: "#78716C", fontWeight: "600" }}>
Renk: <Text style={{ color: "#1C1917", fontWeight: "700" }}>{theme.accent}</Text> {theme.suitableFor}
</Text>
</View>
{/* Action Bar: Canlı Demo, Paylaş, Uygula */}
<View style={{ flexDirection: "row", gap: 8, borderTopWidth: 1, borderTopColor: "#F5F5F4", paddingTop: 12 }}>
{/* Canlı Demo Aç */}
<Pressable
onPress={() => handleOpenDemo(theme.key)}
style={({ pressed }) => ({
flex: 1,
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
gap: 4,
backgroundColor: "#F5F5F4",
paddingVertical: 10,
borderRadius: 10,
opacity: pressed ? 0.8 : 1,
})}
>
<Ionicons name="eye-outline" size={15} color="#1C1917" />
<Text style={{ fontSize: 12, fontWeight: "700", color: "#1C1917" }}>
Demo Menü
</Text>
</Pressable>
{/* Demo Linkini Gönder / Paylaş */}
<Pressable
onPress={() => handleShareDemo(theme)}
style={({ pressed }) => ({
width: 40,
alignItems: "center",
justifyContent: "center",
backgroundColor: "#FAF8F5",
borderWidth: 1,
borderColor: "#E7E5E4",
paddingVertical: 10,
borderRadius: 10,
opacity: pressed ? 0.8 : 1,
})}
>
<Ionicons name="share-outline" size={16} color="#78716C" />
</Pressable>
{/* Bu Şablonu Uygula / Aktif Rozeti */}
<Pressable
onPress={() => handleApplyTheme(theme.key)}
disabled={isSelected || isApplying}
style={({ pressed }) => ({
flex: 1.3,
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
gap: 6,
backgroundColor: isSelected ? "#ECFDF5" : theme.color,
borderWidth: isSelected ? 1 : 0,
borderColor: "#A7F3D0",
paddingVertical: 10,
borderRadius: 10,
opacity: pressed ? 0.85 : 1,
})}
>
{isApplying ? (
<ActivityIndicator size="small" color="#FFFFFF" />
) : isSelected ? (
<>
<Ionicons name="checkmark-circle" size={16} color="#059669" />
<Text style={{ fontSize: 12, fontWeight: "800", color: "#059669" }}>
Aktif Şablon
</Text>
</>
) : (
<>
<Ionicons name="checkmark" size={15} color="#FFFFFF" />
<Text style={{ fontSize: 12, fontWeight: "800", color: "#FFFFFF" }}>
Şablonu Seç
</Text>
</>
)}
</Pressable>
</View>
</View>
);
})}
</ScrollView>
)}
{/* Footer Close */}
<View style={{ paddingHorizontal: 20, paddingTop: 10 }}>
<Pressable
onPress={onClose}
style={{
backgroundColor: "#1C1917",
borderRadius: 14,
paddingVertical: 14,
alignItems: "center",
justifyContent: "center",
}}
>
<Text style={{ color: "#FFFFFF", fontSize: 15, fontWeight: "700" }}>Kapat</Text>
</Pressable>
</View>
</View>
</View>
</Modal>
);
}