434 lines
16 KiB
TypeScript
434 lines
16 KiB
TypeScript
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>
|
||
);
|
||
}
|