first commit
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Alert,
|
||||
KeyboardAvoidingView,
|
||||
Modal,
|
||||
Platform,
|
||||
Pressable,
|
||||
Text,
|
||||
TextInput,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import * as Haptics from "expo-haptics";
|
||||
|
||||
interface AddCategoryModalProps {
|
||||
visible: boolean;
|
||||
initialName?: string;
|
||||
isEditing?: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (name: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export function AddCategoryModal({
|
||||
visible,
|
||||
initialName = "",
|
||||
isEditing = false,
|
||||
onClose,
|
||||
onSubmit,
|
||||
}: AddCategoryModalProps) {
|
||||
const [name, setName] = useState(initialName);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setName(initialName);
|
||||
}, [initialName, visible]);
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!name.trim()) {
|
||||
Alert.alert("Eksik Bilgi", "Lütfen kategori adını giriniz.");
|
||||
return;
|
||||
}
|
||||
|
||||
setSubmitting(true);
|
||||
try {
|
||||
await onSubmit(name.trim());
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
||||
setName("");
|
||||
onClose();
|
||||
} catch (err) {
|
||||
Alert.alert("Hata", err instanceof Error ? err.message : "İşlem başarısız oldu.");
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
animationType="fade"
|
||||
transparent
|
||||
onRequestClose={onClose}
|
||||
>
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === "ios" ? "padding" : undefined}
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: "rgba(0,0,0,0.5)",
|
||||
justifyContent: "center",
|
||||
padding: 24,
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: "#FFFFFF",
|
||||
borderRadius: 20,
|
||||
padding: 20,
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.15,
|
||||
shadowRadius: 12,
|
||||
elevation: 5,
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
<Text style={{ fontSize: 18, fontWeight: "700", color: "#1C1917" }}>
|
||||
{isEditing ? "Kategoriyi Yeniden Adlandır" : "Yeni Kategori Ekle"}
|
||||
</Text>
|
||||
<Pressable onPress={onClose} style={{ padding: 4 }}>
|
||||
<Ionicons name="close" size={22} color="#78716C" />
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
<TextInput
|
||||
value={name}
|
||||
onChangeText={setName}
|
||||
placeholder="Örn: Tatlılar, Başlangıçlar, Sıcak İçecekler"
|
||||
placeholderTextColor="#A8A29E"
|
||||
autoFocus
|
||||
style={{
|
||||
backgroundColor: "#FAFAFA",
|
||||
borderWidth: 1,
|
||||
borderColor: "#E7E5E4",
|
||||
borderRadius: 12,
|
||||
padding: 14,
|
||||
fontSize: 15,
|
||||
fontWeight: "600",
|
||||
color: "#1C1917",
|
||||
marginBottom: 18,
|
||||
}}
|
||||
/>
|
||||
|
||||
<View style={{ flexDirection: "row", gap: 10, justifyContent: "flex-end" }}>
|
||||
<Pressable
|
||||
onPress={onClose}
|
||||
style={{
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 16,
|
||||
borderRadius: 10,
|
||||
backgroundColor: "#F5F5F4",
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: "#78716C", fontWeight: "600", fontSize: 14 }}>Vazgeç</Text>
|
||||
</Pressable>
|
||||
|
||||
<Pressable
|
||||
onPress={handleSubmit}
|
||||
disabled={submitting || !name.trim()}
|
||||
style={({ pressed }) => ({
|
||||
backgroundColor: !name.trim() ? "#D6D3D1" : "#1C1917",
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 20,
|
||||
borderRadius: 10,
|
||||
opacity: pressed ? 0.85 : 1,
|
||||
})}
|
||||
>
|
||||
<Text style={{ color: "#FFFFFF", fontWeight: "700", fontSize: 14 }}>
|
||||
{submitting ? "Kaydediliyor..." : isEditing ? "Güncelle" : "Ekle"}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Alert,
|
||||
Image,
|
||||
KeyboardAvoidingView,
|
||||
Modal,
|
||||
Platform,
|
||||
Pressable,
|
||||
ScrollView,
|
||||
Text,
|
||||
TextInput,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import * as Haptics from "expo-haptics";
|
||||
import * as ImagePicker from "expo-image-picker";
|
||||
|
||||
interface AddItemModalProps {
|
||||
visible: boolean;
|
||||
categoryName: string;
|
||||
categoryId: string;
|
||||
onClose: () => void;
|
||||
onAdd: (categoryId: string, name: string, price: number, description?: string, imageUrl?: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export function AddItemModal({
|
||||
visible,
|
||||
categoryName,
|
||||
categoryId,
|
||||
onClose,
|
||||
onAdd,
|
||||
}: AddItemModalProps) {
|
||||
const [name, setName] = useState("");
|
||||
const [price, setPrice] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [imageUrl, setImageUrl] = useState<string | null>(null);
|
||||
const [adding, setAdding] = useState(false);
|
||||
|
||||
async function handlePickImage() {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light).catch(() => {});
|
||||
const permission = await ImagePicker.requestMediaLibraryPermissionsAsync();
|
||||
if (!permission.granted) {
|
||||
Alert.alert("İzin Gerekli", "Ürün görseli seçebilmek için fotoğraf galerisi erişim izni gereklidir.");
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await ImagePicker.launchImageLibraryAsync({
|
||||
mediaTypes: ["images"],
|
||||
allowsEditing: true,
|
||||
aspect: [4, 3],
|
||||
quality: 0.6,
|
||||
base64: true,
|
||||
});
|
||||
|
||||
if (!result.canceled && result.assets[0]) {
|
||||
const asset = result.assets[0];
|
||||
if (asset.base64) {
|
||||
setImageUrl(`data:image/jpeg;base64,${asset.base64}`);
|
||||
} else {
|
||||
setImageUrl(asset.uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAdd() {
|
||||
const parsedPrice = Number(price.replace(",", "."));
|
||||
if (!name.trim()) {
|
||||
Alert.alert("Eksik Bilgi", "Lütfen ürün adını giriniz.");
|
||||
return;
|
||||
}
|
||||
if (Number.isNaN(parsedPrice) || parsedPrice < 0) {
|
||||
Alert.alert("Geçersiz Fiyat", "Lütfen geçerli bir fiyat giriniz.");
|
||||
return;
|
||||
}
|
||||
|
||||
setAdding(true);
|
||||
try {
|
||||
await onAdd(
|
||||
categoryId,
|
||||
name.trim(),
|
||||
parsedPrice,
|
||||
description.trim() || undefined,
|
||||
imageUrl || undefined,
|
||||
);
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success).catch(() => {});
|
||||
setName("");
|
||||
setPrice("");
|
||||
setDescription("");
|
||||
setImageUrl(null);
|
||||
onClose();
|
||||
} catch (err) {
|
||||
Alert.alert("Hata", err instanceof Error ? err.message : "Ürün eklenemedi.");
|
||||
} finally {
|
||||
setAdding(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
animationType="slide"
|
||||
transparent
|
||||
onRequestClose={onClose}
|
||||
>
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === "ios" ? "padding" : undefined}
|
||||
style={{ flex: 1, backgroundColor: "rgba(0,0,0,0.5)", justifyContent: "flex-end" }}
|
||||
>
|
||||
<Pressable style={{ flex: 1 }} onPress={onClose} />
|
||||
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: "#FFFFFF",
|
||||
borderTopLeftRadius: 24,
|
||||
borderTopRightRadius: 24,
|
||||
maxHeight: "88%",
|
||||
paddingBottom: Platform.OS === "ios" ? 34 : 20,
|
||||
}}
|
||||
>
|
||||
<View style={{ alignItems: "center", paddingTop: 10, paddingBottom: 6 }}>
|
||||
<View
|
||||
style={{
|
||||
width: 36,
|
||||
height: 4,
|
||||
borderRadius: 2,
|
||||
backgroundColor: "#E7E5E4",
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 12,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: "#F5F5F4",
|
||||
}}
|
||||
>
|
||||
<View>
|
||||
<Text style={{ fontSize: 18, fontWeight: "700", color: "#1C1917" }}>
|
||||
Yeni Ürün Ekle
|
||||
</Text>
|
||||
<Text style={{ fontSize: 12, color: "#78716C", marginTop: 2 }}>
|
||||
Kategori: <Text style={{ fontWeight: "700", color: "#C8A96B" }}>{categoryName}</Text>
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Pressable
|
||||
onPress={onClose}
|
||||
style={{
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 18,
|
||||
backgroundColor: "#F5F5F4",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Ionicons name="close" size={20} color="#78716C" />
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
<ScrollView contentContainerStyle={{ padding: 20, gap: 16 }}>
|
||||
{/* Image Picker */}
|
||||
<View>
|
||||
<Text style={{ fontSize: 13, fontWeight: "700", color: "#44403C", marginBottom: 8 }}>
|
||||
Ürün Fotoğrafı (Opsiyonel)
|
||||
</Text>
|
||||
{imageUrl ? (
|
||||
<View style={{ position: "relative", width: "100%", height: 160, borderRadius: 14, overflow: "hidden", borderWidth: 1, borderColor: "#E7E5E4" }}>
|
||||
<Image source={{ uri: imageUrl }} style={{ width: "100%", height: 160 }} resizeMode="cover" />
|
||||
<Pressable
|
||||
onPress={() => setImageUrl(null)}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 10,
|
||||
right: 10,
|
||||
backgroundColor: "rgba(0,0,0,0.65)",
|
||||
borderRadius: 14,
|
||||
padding: 6,
|
||||
}}
|
||||
>
|
||||
<Ionicons name="trash-outline" size={18} color="#FFFFFF" />
|
||||
</Pressable>
|
||||
</View>
|
||||
) : (
|
||||
<Pressable
|
||||
onPress={handlePickImage}
|
||||
style={({ pressed }) => ({
|
||||
height: 100,
|
||||
borderRadius: 14,
|
||||
borderWidth: 1.5,
|
||||
borderStyle: "dashed",
|
||||
borderColor: "#D6D3D1",
|
||||
backgroundColor: "#FAF8F5",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 6,
|
||||
opacity: pressed ? 0.8 : 1,
|
||||
})}
|
||||
>
|
||||
<Ionicons name="camera-outline" size={26} color="#C8A96B" />
|
||||
<Text style={{ fontSize: 13, fontWeight: "600", color: "#78716C" }}>
|
||||
Galeriden Fotoğraf Seç
|
||||
</Text>
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View>
|
||||
<Text style={{ fontSize: 13, fontWeight: "700", color: "#44403C", marginBottom: 6 }}>
|
||||
Ürün Adı *
|
||||
</Text>
|
||||
<TextInput
|
||||
value={name}
|
||||
onChangeText={setName}
|
||||
placeholder="Örn: Mercimek Çorbası"
|
||||
placeholderTextColor="#A8A29E"
|
||||
style={{
|
||||
backgroundColor: "#FAFAFA",
|
||||
borderWidth: 1,
|
||||
borderColor: "#E7E5E4",
|
||||
borderRadius: 12,
|
||||
padding: 14,
|
||||
fontSize: 15,
|
||||
fontWeight: "600",
|
||||
color: "#1C1917",
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View>
|
||||
<Text style={{ fontSize: 13, fontWeight: "700", color: "#44403C", marginBottom: 6 }}>
|
||||
Fiyat (₺) *
|
||||
</Text>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
backgroundColor: "#FAFAFA",
|
||||
borderWidth: 1,
|
||||
borderColor: "#E7E5E4",
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 14,
|
||||
}}
|
||||
>
|
||||
<TextInput
|
||||
value={price}
|
||||
onChangeText={setPrice}
|
||||
placeholder="120"
|
||||
placeholderTextColor="#A8A29E"
|
||||
keyboardType="decimal-pad"
|
||||
style={{
|
||||
flex: 1,
|
||||
paddingVertical: 14,
|
||||
fontSize: 16,
|
||||
fontWeight: "700",
|
||||
color: "#C8A96B",
|
||||
}}
|
||||
/>
|
||||
<Text style={{ fontSize: 16, fontWeight: "700", color: "#1C1917" }}>₺</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View>
|
||||
<Text style={{ fontSize: 13, fontWeight: "700", color: "#44403C", marginBottom: 6 }}>
|
||||
Açıklama (Opsiyonel)
|
||||
</Text>
|
||||
<TextInput
|
||||
value={description}
|
||||
onChangeText={setDescription}
|
||||
placeholder="Kısa içerik veya porsiyon açıklaması..."
|
||||
placeholderTextColor="#A8A29E"
|
||||
multiline
|
||||
numberOfLines={3}
|
||||
style={{
|
||||
backgroundColor: "#FAFAFA",
|
||||
borderWidth: 1,
|
||||
borderColor: "#E7E5E4",
|
||||
borderRadius: 12,
|
||||
padding: 14,
|
||||
fontSize: 14,
|
||||
color: "#1C1917",
|
||||
minHeight: 70,
|
||||
textAlignVertical: "top",
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Pressable
|
||||
onPress={handleAdd}
|
||||
disabled={adding || !name.trim() || !price.trim()}
|
||||
style={({ pressed }) => ({
|
||||
backgroundColor: !name.trim() || !price.trim() ? "#D6D3D1" : "#1C1917",
|
||||
paddingVertical: 16,
|
||||
borderRadius: 14,
|
||||
alignItems: "center",
|
||||
opacity: pressed ? 0.85 : 1,
|
||||
marginTop: 8,
|
||||
})}
|
||||
>
|
||||
<Text style={{ color: "#FFFFFF", fontSize: 15, fontWeight: "700" }}>
|
||||
{adding ? "Ekleniyor..." : "Ürünü Menüye Ekle"}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</ScrollView>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import { useRef } from "react";
|
||||
import { Pressable, ScrollView, Text, View } from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import * as Haptics from "expo-haptics";
|
||||
|
||||
interface CategoryPill {
|
||||
id: string;
|
||||
name: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
interface CategoryPillBarProps {
|
||||
categories: CategoryPill[];
|
||||
activeCategoryId: string | null;
|
||||
onSelectCategory: (id: string) => void;
|
||||
onAddCategoryPress: () => void;
|
||||
}
|
||||
|
||||
export function CategoryPillBar({
|
||||
categories,
|
||||
activeCategoryId,
|
||||
onSelectCategory,
|
||||
onAddCategoryPress,
|
||||
}: CategoryPillBarProps) {
|
||||
const scrollRef = useRef<ScrollView>(null);
|
||||
|
||||
if (categories.length === 0) return null;
|
||||
|
||||
return (
|
||||
<View style={{ backgroundColor: "#FAF8F5", paddingVertical: 10, borderBottomWidth: 1, borderBottomColor: "#E7E5E4" }}>
|
||||
<ScrollView
|
||||
ref={scrollRef}
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={{ paddingHorizontal: 16, gap: 8, alignItems: "center" }}
|
||||
>
|
||||
{categories.map((cat) => {
|
||||
const isActive = cat.id === activeCategoryId;
|
||||
return (
|
||||
<Pressable
|
||||
key={cat.id}
|
||||
onPress={() => {
|
||||
Haptics.selectionAsync();
|
||||
onSelectCategory(cat.id);
|
||||
}}
|
||||
style={({ pressed }) => ({
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
backgroundColor: isActive ? "#1C1917" : "#FFFFFF",
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 14,
|
||||
borderRadius: 20,
|
||||
borderWidth: 1,
|
||||
borderColor: isActive ? "#1C1917" : "#E7E5E4",
|
||||
opacity: pressed ? 0.85 : 1,
|
||||
minHeight: 38,
|
||||
})}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 13,
|
||||
fontWeight: isActive ? "700" : "600",
|
||||
color: isActive ? "#FFFFFF" : "#44403C",
|
||||
}}
|
||||
>
|
||||
{cat.name}
|
||||
</Text>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: isActive ? "rgba(255, 255, 255, 0.2)" : "#F5F5F4",
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 2,
|
||||
borderRadius: 10,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 11,
|
||||
fontWeight: "700",
|
||||
color: isActive ? "#FFFFFF" : "#78716C",
|
||||
}}
|
||||
>
|
||||
{cat.count}
|
||||
</Text>
|
||||
</View>
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
onAddCategoryPress();
|
||||
}}
|
||||
style={({ pressed }) => ({
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 4,
|
||||
backgroundColor: "#F3EDE2",
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 12,
|
||||
borderRadius: 20,
|
||||
borderWidth: 1,
|
||||
borderColor: "rgba(200, 169, 107, 0.4)",
|
||||
borderStyle: "dashed",
|
||||
opacity: pressed ? 0.8 : 1,
|
||||
minHeight: 38,
|
||||
})}
|
||||
>
|
||||
<Ionicons name="add" size={16} color="#926E27" />
|
||||
<Text style={{ fontSize: 13, fontWeight: "700", color: "#926E27" }}>
|
||||
Kategori
|
||||
</Text>
|
||||
</Pressable>
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,433 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Alert,
|
||||
Image,
|
||||
KeyboardAvoidingView,
|
||||
Modal,
|
||||
Platform,
|
||||
Pressable,
|
||||
ScrollView,
|
||||
Switch,
|
||||
Text,
|
||||
TextInput,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import * as Haptics from "expo-haptics";
|
||||
import * as ImagePicker from "expo-image-picker";
|
||||
import type { MenuItemData } from "./MenuItemCard";
|
||||
|
||||
interface ItemDetailSheetProps {
|
||||
item: MenuItemData | null;
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (updatedItem: MenuItemData) => Promise<void>;
|
||||
onDelete: (itemId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export function ItemDetailSheet({
|
||||
item,
|
||||
visible,
|
||||
onClose,
|
||||
onSave,
|
||||
onDelete,
|
||||
}: ItemDetailSheetProps) {
|
||||
const [name, setName] = useState("");
|
||||
const [price, setPrice] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [imageUrl, setImageUrl] = useState<string | null>(null);
|
||||
const [isActive, setIsActive] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (item) {
|
||||
setName(item.name);
|
||||
setPrice(String(item.price));
|
||||
setDescription(item.description ?? "");
|
||||
setImageUrl(item.image_url ?? null);
|
||||
setIsActive(item.is_active);
|
||||
}
|
||||
}, [item]);
|
||||
|
||||
if (!item) return null;
|
||||
|
||||
async function handlePickImage() {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light).catch(() => {});
|
||||
const permission = await ImagePicker.requestMediaLibraryPermissionsAsync();
|
||||
if (!permission.granted) {
|
||||
Alert.alert("İzin Gerekli", "Ürün görseli seçebilmek için fotoğraf galerisi erişim izni gereklidir.");
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await ImagePicker.launchImageLibraryAsync({
|
||||
mediaTypes: ["images"],
|
||||
allowsEditing: true,
|
||||
aspect: [4, 3],
|
||||
quality: 0.6,
|
||||
base64: true,
|
||||
});
|
||||
|
||||
if (!result.canceled && result.assets[0]) {
|
||||
const asset = result.assets[0];
|
||||
if (asset.base64) {
|
||||
setImageUrl(`data:image/jpeg;base64,${asset.base64}`);
|
||||
} else {
|
||||
setImageUrl(asset.uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
const parsedPrice = Number(price.replace(",", "."));
|
||||
if (!name.trim()) {
|
||||
Alert.alert("Eksik Bilgi", "Lütfen ürün adını giriniz.");
|
||||
return;
|
||||
}
|
||||
if (Number.isNaN(parsedPrice) || parsedPrice < 0) {
|
||||
Alert.alert("Geçersiz Fiyat", "Lütfen geçerli bir fiyat giriniz.");
|
||||
return;
|
||||
}
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
await onSave({
|
||||
...item!,
|
||||
name: name.trim(),
|
||||
price: parsedPrice,
|
||||
description: description.trim() || null,
|
||||
image_url: imageUrl || null,
|
||||
is_active: isActive,
|
||||
});
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success).catch(() => {});
|
||||
onClose();
|
||||
} catch (err) {
|
||||
Alert.alert("Hata", err instanceof Error ? err.message : "Kaydedilemedi.");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
function confirmDelete() {
|
||||
Alert.alert(
|
||||
"Ürünü Sil",
|
||||
`"${item?.name}" menüden silinecek. Emin misiniz?`,
|
||||
[
|
||||
{ text: "Vazgeç", style: "cancel" },
|
||||
{
|
||||
text: "Sil",
|
||||
style: "destructive",
|
||||
onPress: async () => {
|
||||
setDeleting(true);
|
||||
try {
|
||||
await onDelete(item!.id);
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning).catch(() => {});
|
||||
onClose();
|
||||
} catch (err) {
|
||||
Alert.alert("Hata", err instanceof Error ? err.message : "Silinemedi.");
|
||||
} finally {
|
||||
setDeleting(false);
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
animationType="slide"
|
||||
transparent
|
||||
onRequestClose={onClose}
|
||||
>
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === "ios" ? "padding" : undefined}
|
||||
style={{ flex: 1, backgroundColor: "rgba(0,0,0,0.5)", justifyContent: "flex-end" }}
|
||||
>
|
||||
<Pressable style={{ flex: 1 }} onPress={onClose} />
|
||||
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: "#FFFFFF",
|
||||
borderTopLeftRadius: 24,
|
||||
borderTopRightRadius: 24,
|
||||
maxHeight: "90%",
|
||||
paddingBottom: Platform.OS === "ios" ? 34 : 20,
|
||||
}}
|
||||
>
|
||||
{/* Header Handle & Bar */}
|
||||
<View style={{ alignItems: "center", paddingTop: 10, paddingBottom: 6 }}>
|
||||
<View
|
||||
style={{
|
||||
width: 36,
|
||||
height: 4,
|
||||
borderRadius: 2,
|
||||
backgroundColor: "#E7E5E4",
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 12,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: "#F5F5F4",
|
||||
}}
|
||||
>
|
||||
<Text style={{ fontSize: 18, fontWeight: "700", color: "#1C1917" }}>
|
||||
Ürün Detayları
|
||||
</Text>
|
||||
<Pressable
|
||||
onPress={onClose}
|
||||
style={{
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 18,
|
||||
backgroundColor: "#F5F5F4",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Ionicons name="close" size={20} color="#78716C" />
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
<ScrollView contentContainerStyle={{ padding: 20, gap: 16 }}>
|
||||
{/* Availability Toggle Box */}
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
backgroundColor: isActive ? "#F0FDF4" : "#FEF2F2",
|
||||
borderWidth: 1,
|
||||
borderColor: isActive ? "#BBF7D0" : "#FECACA",
|
||||
borderRadius: 14,
|
||||
padding: 14,
|
||||
}}
|
||||
>
|
||||
<View>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 15,
|
||||
fontWeight: "700",
|
||||
color: isActive ? "#15803D" : "#B91C1C",
|
||||
}}
|
||||
>
|
||||
{isActive ? "Menüde Aktif & Siparişe Açık" : "Tükendi / Menüde Gizli"}
|
||||
</Text>
|
||||
<Text style={{ fontSize: 12, color: "#78716C", marginTop: 2 }}>
|
||||
{isActive
|
||||
? "Müşteriler bu ürünü QR menüde görebilir."
|
||||
: "Müşteriler ürünü Tükendi olarak görür."}
|
||||
</Text>
|
||||
</View>
|
||||
<Switch
|
||||
value={isActive}
|
||||
onValueChange={(val) => {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium).catch(() => {});
|
||||
setIsActive(val);
|
||||
}}
|
||||
trackColor={{ false: "#D6D3D1", true: "#059669" }}
|
||||
thumbColor="#FFFFFF"
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Image Picker / Preview */}
|
||||
<View>
|
||||
<Text style={{ fontSize: 13, fontWeight: "700", color: "#44403C", marginBottom: 8 }}>
|
||||
Ürün Fotoğrafı
|
||||
</Text>
|
||||
{imageUrl ? (
|
||||
<View style={{ position: "relative", width: "100%", height: 160, borderRadius: 14, overflow: "hidden", borderWidth: 1, borderColor: "#E7E5E4" }}>
|
||||
<Image source={{ uri: imageUrl }} style={{ width: "100%", height: 160 }} resizeMode="cover" />
|
||||
<Pressable
|
||||
onPress={() => setImageUrl(null)}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 10,
|
||||
right: 10,
|
||||
backgroundColor: "rgba(0,0,0,0.65)",
|
||||
borderRadius: 14,
|
||||
padding: 6,
|
||||
}}
|
||||
>
|
||||
<Ionicons name="trash-outline" size={18} color="#FFFFFF" />
|
||||
</Pressable>
|
||||
<Pressable
|
||||
onPress={handlePickImage}
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 10,
|
||||
right: 10,
|
||||
backgroundColor: "#1C1917",
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 6,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
<Ionicons name="camera-outline" size={14} color="#FFFFFF" />
|
||||
<Text style={{ color: "#FFFFFF", fontSize: 11, fontWeight: "700" }}>Değiştir</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
) : (
|
||||
<Pressable
|
||||
onPress={handlePickImage}
|
||||
style={({ pressed }) => ({
|
||||
height: 100,
|
||||
borderRadius: 14,
|
||||
borderWidth: 1.5,
|
||||
borderStyle: "dashed",
|
||||
borderColor: "#D6D3D1",
|
||||
backgroundColor: "#FAF8F5",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 6,
|
||||
opacity: pressed ? 0.8 : 1,
|
||||
})}
|
||||
>
|
||||
<Ionicons name="camera-outline" size={26} color="#C8A96B" />
|
||||
<Text style={{ fontSize: 13, fontWeight: "600", color: "#78716C" }}>
|
||||
Galeriden Fotoğraf Seç
|
||||
</Text>
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Name Input */}
|
||||
<View>
|
||||
<Text style={{ fontSize: 13, fontWeight: "700", color: "#44403C", marginBottom: 6 }}>
|
||||
Ürün Adı *
|
||||
</Text>
|
||||
<TextInput
|
||||
value={name}
|
||||
onChangeText={setName}
|
||||
placeholder="Örn: Izgara Levrek"
|
||||
placeholderTextColor="#A8A29E"
|
||||
style={{
|
||||
backgroundColor: "#FAFAFA",
|
||||
borderWidth: 1,
|
||||
borderColor: "#E7E5E4",
|
||||
borderRadius: 12,
|
||||
padding: 14,
|
||||
fontSize: 15,
|
||||
fontWeight: "600",
|
||||
color: "#1C1917",
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Price Input */}
|
||||
<View>
|
||||
<Text style={{ fontSize: 13, fontWeight: "700", color: "#44403C", marginBottom: 6 }}>
|
||||
Fiyat (₺) *
|
||||
</Text>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
backgroundColor: "#FAFAFA",
|
||||
borderWidth: 1,
|
||||
borderColor: "#E7E5E4",
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 14,
|
||||
}}
|
||||
>
|
||||
<TextInput
|
||||
value={price}
|
||||
onChangeText={setPrice}
|
||||
placeholder="0.00"
|
||||
placeholderTextColor="#A8A29E"
|
||||
keyboardType="decimal-pad"
|
||||
style={{
|
||||
flex: 1,
|
||||
paddingVertical: 14,
|
||||
fontSize: 16,
|
||||
fontWeight: "700",
|
||||
color: "#C8A96B",
|
||||
}}
|
||||
/>
|
||||
<Text style={{ fontSize: 16, fontWeight: "700", color: "#1C1917" }}>₺</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Description Input */}
|
||||
<View>
|
||||
<Text style={{ fontSize: 13, fontWeight: "700", color: "#44403C", marginBottom: 6 }}>
|
||||
Açıklama / Malzemeler
|
||||
</Text>
|
||||
<TextInput
|
||||
value={description}
|
||||
onChangeText={setDescription}
|
||||
placeholder="İçindekiler, porsiyon bilgisi, pişirme tarzı..."
|
||||
placeholderTextColor="#A8A29E"
|
||||
multiline
|
||||
numberOfLines={3}
|
||||
style={{
|
||||
backgroundColor: "#FAFAFA",
|
||||
borderWidth: 1,
|
||||
borderColor: "#E7E5E4",
|
||||
borderRadius: 12,
|
||||
padding: 14,
|
||||
fontSize: 14,
|
||||
color: "#1C1917",
|
||||
minHeight: 80,
|
||||
textAlignVertical: "top",
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<View style={{ gap: 10, marginTop: 10 }}>
|
||||
<Pressable
|
||||
onPress={handleSave}
|
||||
disabled={saving || deleting}
|
||||
style={({ pressed }) => ({
|
||||
backgroundColor: "#1C1917",
|
||||
paddingVertical: 16,
|
||||
borderRadius: 14,
|
||||
alignItems: "center",
|
||||
opacity: pressed || saving ? 0.85 : 1,
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 6,
|
||||
})}
|
||||
>
|
||||
<Text style={{ color: "#FFFFFF", fontSize: 15, fontWeight: "700" }}>
|
||||
{saving ? "Kaydediliyor..." : "Değişiklikleri Kaydet"}
|
||||
</Text>
|
||||
</Pressable>
|
||||
|
||||
<Pressable
|
||||
onPress={confirmDelete}
|
||||
disabled={saving || deleting}
|
||||
style={({ pressed }) => ({
|
||||
backgroundColor: "#FEF2F2",
|
||||
borderWidth: 1,
|
||||
borderColor: "#FEE2E2",
|
||||
paddingVertical: 14,
|
||||
borderRadius: 14,
|
||||
alignItems: "center",
|
||||
opacity: pressed || deleting ? 0.85 : 1,
|
||||
})}
|
||||
>
|
||||
<Text style={{ color: "#DC2626", fontSize: 14, fontWeight: "700" }}>
|
||||
{deleting ? "Siliniyor..." : "Bu Ürünü Menüden Sil"}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
import { Image, Pressable, Switch, Text, View } from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import * as Haptics from "expo-haptics";
|
||||
|
||||
export interface MenuItemData {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
price: number;
|
||||
is_active: boolean;
|
||||
image_url?: string | null;
|
||||
}
|
||||
|
||||
interface MenuItemCardProps {
|
||||
item: MenuItemData;
|
||||
onPress: () => void;
|
||||
onToggleActive: (active: boolean) => void;
|
||||
}
|
||||
|
||||
export function MenuItemCard({ item, onPress, onToggleActive }: MenuItemCardProps) {
|
||||
const isAvailable = item.is_active;
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light).catch(() => {});
|
||||
onPress();
|
||||
}}
|
||||
style={({ pressed }) => ({
|
||||
backgroundColor: isAvailable ? "#FFFFFF" : "#F5F5F4",
|
||||
borderRadius: 16,
|
||||
padding: 12,
|
||||
marginBottom: 10,
|
||||
borderWidth: 1,
|
||||
borderColor: isAvailable ? "#E7E5E4" : "#D6D3D1",
|
||||
opacity: pressed ? 0.88 : 1,
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: isAvailable ? 0.03 : 0,
|
||||
shadowRadius: 4,
|
||||
elevation: isAvailable ? 1 : 0,
|
||||
})}
|
||||
>
|
||||
<View style={{ flexDirection: "row", alignItems: "center" }}>
|
||||
{/* Item Image Thumbnail if available */}
|
||||
{item.image_url ? (
|
||||
<Image
|
||||
source={{ uri: item.image_url }}
|
||||
style={{
|
||||
width: 64,
|
||||
height: 64,
|
||||
borderRadius: 12,
|
||||
marginRight: 12,
|
||||
backgroundColor: "#F5F5F4",
|
||||
}}
|
||||
resizeMode="cover"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{/* Item Info */}
|
||||
<View style={{ flex: 1, marginRight: 10 }}>
|
||||
<View style={{ flexDirection: "row", alignItems: "center", gap: 6, flexWrap: "wrap" }}>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 15,
|
||||
fontWeight: "700",
|
||||
color: isAvailable ? "#1C1917" : "#78716C",
|
||||
textDecorationLine: isAvailable ? "none" : "line-through",
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
</Text>
|
||||
|
||||
{!isAvailable ? (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: "#FEE2E2",
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 2,
|
||||
borderRadius: 4,
|
||||
}}
|
||||
>
|
||||
<Text style={{ fontSize: 10, fontWeight: "700", color: "#DC2626" }}>
|
||||
Tükendi (86)
|
||||
</Text>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
{item.description ? (
|
||||
<Text
|
||||
numberOfLines={2}
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: isAvailable ? "#78716C" : "#A8A29E",
|
||||
marginTop: 2,
|
||||
lineHeight: 16,
|
||||
}}
|
||||
>
|
||||
{item.description}
|
||||
</Text>
|
||||
) : null}
|
||||
|
||||
{/* Price */}
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 15,
|
||||
fontWeight: "800",
|
||||
color: isAvailable ? "#C8A96B" : "#A8A29E",
|
||||
marginTop: 4,
|
||||
}}
|
||||
>
|
||||
{item.price} ₺
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Action Column: Stock Switch & Details Chevron */}
|
||||
<View style={{ alignItems: "flex-end", gap: 6 }}>
|
||||
<View style={{ flexDirection: "row", alignItems: "center", gap: 4 }}>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 11,
|
||||
fontWeight: "600",
|
||||
color: isAvailable ? "#059669" : "#78716C",
|
||||
}}
|
||||
>
|
||||
{isAvailable ? "Aktif" : "Kapalı"}
|
||||
</Text>
|
||||
<Switch
|
||||
value={isAvailable}
|
||||
onValueChange={(val) => {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium).catch(() => {});
|
||||
onToggleActive(val);
|
||||
}}
|
||||
trackColor={{ false: "#D6D3D1", true: "#059669" }}
|
||||
thumbColor="#FFFFFF"
|
||||
style={{ transform: [{ scaleX: 0.8 }, { scaleY: 0.8 }] }}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 2,
|
||||
paddingVertical: 2,
|
||||
paddingHorizontal: 4,
|
||||
}}
|
||||
>
|
||||
<Text style={{ fontSize: 11, color: "#A8A29E", fontWeight: "600" }}>Düzenle</Text>
|
||||
<Ionicons name="chevron-forward" size={13} color="#A8A29E" />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import { ActivityIndicator, Pressable, Text, View } from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import * as Haptics from "expo-haptics";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
|
||||
interface PublishActionBarProps {
|
||||
isPublished: boolean;
|
||||
totalItems: number;
|
||||
totalCategories: number;
|
||||
publishing: boolean;
|
||||
onPublish: () => void;
|
||||
onPreview: () => void;
|
||||
onShowQr: () => void;
|
||||
}
|
||||
|
||||
export function PublishActionBar({
|
||||
isPublished,
|
||||
totalItems,
|
||||
totalCategories,
|
||||
publishing,
|
||||
onPublish,
|
||||
onPreview,
|
||||
onShowQr,
|
||||
}: PublishActionBarProps) {
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
backgroundColor: "#FFFFFF",
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: "#E7E5E4",
|
||||
paddingTop: 12,
|
||||
paddingHorizontal: 16,
|
||||
paddingBottom: Math.max(insets.bottom, 14),
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: -3 },
|
||||
shadowOpacity: 0.06,
|
||||
shadowRadius: 8,
|
||||
elevation: 8,
|
||||
}}
|
||||
>
|
||||
<View style={{ flexDirection: "row", gap: 10, alignItems: "center" }}>
|
||||
{/* Permanent Preview Button */}
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light).catch(() => {});
|
||||
onPreview();
|
||||
}}
|
||||
style={({ pressed }) => ({
|
||||
flex: 1,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 6,
|
||||
backgroundColor: "#FAF8F5",
|
||||
borderWidth: 1.5,
|
||||
borderColor: "rgba(200, 169, 107, 0.4)",
|
||||
borderRadius: 14,
|
||||
paddingVertical: 14,
|
||||
opacity: pressed ? 0.85 : 1,
|
||||
minHeight: 50,
|
||||
})}
|
||||
>
|
||||
<Ionicons name="eye-outline" size={18} color="#C8A96B" />
|
||||
<Text style={{ fontSize: 14, fontWeight: "700", color: "#1C1917" }}>
|
||||
Önizle
|
||||
</Text>
|
||||
</Pressable>
|
||||
|
||||
{/* Publish Action Button */}
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium).catch(() => {});
|
||||
onPublish();
|
||||
}}
|
||||
disabled={publishing || totalItems === 0}
|
||||
style={({ pressed }) => ({
|
||||
flex: 2,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 8,
|
||||
backgroundColor: totalItems === 0 ? "#D6D3D1" : "#1C1917",
|
||||
borderRadius: 14,
|
||||
paddingVertical: 14,
|
||||
opacity: pressed || publishing ? 0.85 : 1,
|
||||
minHeight: 50,
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.12,
|
||||
shadowRadius: 6,
|
||||
})}
|
||||
>
|
||||
{publishing ? (
|
||||
<ActivityIndicator color="#FFFFFF" size="small" />
|
||||
) : (
|
||||
<>
|
||||
<Ionicons
|
||||
name={isPublished ? "sync" : "cloud-upload-outline"}
|
||||
size={18}
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
<Text style={{ color: "#FFFFFF", fontSize: 15, fontWeight: "700" }}>
|
||||
{isPublished ? "Değişiklikleri Yayınla" : "Menüyü Yayınla"}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,421 @@
|
||||
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: "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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user