diff --git a/apps/mobile/src/app/menu/qr.tsx b/apps/mobile/src/app/menu/qr.tsx index 09d9280..a0048b1 100644 --- a/apps/mobile/src/app/menu/qr.tsx +++ b/apps/mobile/src/app/menu/qr.tsx @@ -24,11 +24,22 @@ import { getActiveRestaurant, type ActiveRestaurant } from "@/lib/active-restaur import { COMPONENT_TEMPLATES, TEMPLATE_ASPECT_RATIOS, - FONT_OPTIONS, type QrCustomFields, - type TextItemConfig, } from "@/components/qr-templates"; +import { + QrCanvasStudio, + getDefaultLayersForTemplate, + type CanvasTextLayer, +} from "@/components/qr-canvas"; +const HIDE_ALL_TEMPLATE_FIELDS: QrCustomFields = { + restaurantName: { text: "", visible: false }, + subtitle: { text: "", visible: false }, + tableNo: { text: "", visible: false }, + ctaText: { text: "", visible: false }, + wifiText: { text: "", visible: false }, + socialText: { text: "", visible: false }, +}; interface QrResponse { id: string; @@ -52,64 +63,11 @@ export default function QrScreen() { ); const [exportingPng, setExportingPng] = useState(false); const [exportingPdf, setExportingPdf] = useState(false); - const [showCustomizePanel, setShowCustomizePanel] = useState(false); - const [fields, setFields] = useState({ - restaurantName: { - text: "Gourmet Bistro", - fontSize: 26, - fontId: "serif", - visible: true, - }, - subtitle: { - text: "RESTAURANT & BAR\nMODERN CUISINE", - fontSize: 11, - fontId: "sans", - visible: true, - }, - tableNo: { - text: "MASA NO: 01", - fontSize: 11, - fontId: "sans", - visible: true, - }, - ctaText: { - text: "MENÜYÜ GÖRMEK İÇİN OKUTUN", - fontSize: 13, - fontId: "serif", - visible: true, - }, - wifiText: { - text: "WiFi: Gourmet_Guest | Şifre: lezzetli01", - fontSize: 10, - fontId: "sans", - visible: true, - }, - socialText: { - text: "@gourmetbistro", - fontSize: 10, - fontId: "sans", - visible: true, - }, - }); - - const [selectedEditField, setSelectedEditField] = useState(null); - - const FIELD_DEFINITIONS: Array<{ - key: keyof QrCustomFields; - label: string; - placeholder: string; - min: number; - max: number; - icon: string; - }> = [ - { key: "restaurantName", label: "Restoran Başlığı", placeholder: "Restoran Adı", min: 14, max: 48, icon: "restaurant-outline" }, - { key: "subtitle", label: "Slogan / Alt Başlık", placeholder: "Slogan (Enter ile alt satıra geçebilir)", min: 8, max: 26, icon: "chatbubble-outline" }, - { key: "ctaText", label: "Yönlendirme (CTA) Metni", placeholder: "Örn: MENÜYÜ GÖRMEK İÇİN OKUTUN", min: 9, max: 28, icon: "scan-outline" }, - { key: "tableNo", label: "Masa Numarası", placeholder: "Örn: MASA NO: 01", min: 8, max: 22, icon: "grid-outline" }, - { key: "wifiText", label: "WiFi & Şifre Bilgisi", placeholder: "Örn: WiFi: Bistro | Şifre: 12345", min: 7, max: 20, icon: "wifi-outline" }, - { key: "socialText", label: "Sosyal Medya / İletişim", placeholder: "Örn: @menulio", min: 7, max: 20, icon: "share-social-outline" }, - ]; + // Instagram Story / Canva Tarzı Katmanlar + const [layers, setLayers] = useState(() => + getDefaultLayersForTemplate("qr-1", "Gourmet Bistro") + ); // eslint-disable-next-line @typescript-eslint/no-explicit-any const viewShotRef = useRef(null); @@ -124,10 +82,6 @@ export default function QrScreen() { const displayWidth = 300; const displayHeight = Math.round(displayWidth / aspectRatio); - - - - useEffect(() => { (async () => { const activeRest = await getActiveRestaurant(); @@ -143,13 +97,11 @@ export default function QrScreen() { const restDetail = await api.get<{ name?: string }>(`/restaurants/${activeRest.restaurantId}`); if (restDetail?.name) { setRestaurantName(restDetail.name); - setFields((prev) => ({ - ...prev, - restaurantName: { - ...prev.restaurantName, - text: restDetail.name || "Gourmet Bistro", - }, - })); + setLayers((prev) => + prev.map((l) => + l.id === "rName" ? { ...l, text: restDetail.name!.toUpperCase() } : l + ) + ); } } catch { // fallback @@ -164,6 +116,7 @@ export default function QrScreen() { + async function handleCopy() { if (!qr?.targetUrl) return; await Clipboard.setStringAsync(qr.targetUrl); @@ -331,14 +284,17 @@ export default function QrScreen() { return ( setSelectedKey(preset.key)} + onPress={() => { + setSelectedKey(preset.key); + setLayers(getDefaultLayersForTemplate(preset.key, restaurantName)); + }} style={{ + paddingHorizontal: 16, + paddingVertical: 10, + borderRadius: 14, backgroundColor: isSelected ? "#D4AF37" : "#1A1A1E", borderWidth: 1.5, borderColor: isSelected ? "#D4AF37" : "#2B2B32", - borderRadius: 14, - paddingHorizontal: 16, - paddingVertical: 10, flexDirection: "row", alignItems: "center", gap: 8, @@ -369,417 +325,38 @@ export default function QrScreen() { )} - {/* Live Stand Preview Wrapped in ViewShot */} + {/* İnteraktif Instagram / Canva Tarzı QR Stant Stüdyosu */} - {/* Component-based template — no XML parsing, pixel-perfect */} - - {TemplateComponent ? ( - - ) : null} - + + TemplateComponent ? ( + + ) : null + } + layers={layers} + onLayersChange={setLayers} + /> - {/* Hızlı Katman Seçici (Canlı Seçim) */} - - - - CANLI DÜZENLENECEK ALANI SEÇİN: - - {selectedEditField && ( - setSelectedEditField(null)} - style={{ - backgroundColor: "rgba(255,255,255,0.08)", - paddingHorizontal: 8, - paddingVertical: 3, - borderRadius: 6, - }} - > - Kapat - - )} - - - - {FIELD_DEFINITIONS.map(({ key, label, icon }) => { - const item = fields[key]; - const isSelected = selectedEditField === key; - return ( - setSelectedEditField(isSelected ? null : key)} - style={({ pressed }) => ({ - backgroundColor: isSelected ? "#D4AF37" : item.visible ? "#242429" : "#1A1A1E", - paddingHorizontal: 12, - paddingVertical: 9, - borderRadius: 12, - borderWidth: 1.5, - borderColor: isSelected ? "#D4AF37" : item.visible ? "rgba(212, 175, 55, 0.3)" : "#2B2B32", - flexDirection: "row", - alignItems: "center", - gap: 6, - opacity: pressed ? 0.8 : 1, - })} - > - - - {label} - - {item.offsetY ? ( - - - {item.offsetY > 0 ? `+${item.offsetY}` : item.offsetY}px - - - ) : null} - - ); - })} - - - - {/* CANLI DÜZENLEYİCİ KARTI (Sayfa İçinde Anında Görsel Geri Bildirim) */} - {selectedEditField && (() => { - const def = FIELD_DEFINITIONS.find((d) => d.key === selectedEditField)!; - const item = fields[selectedEditField]; - - return ( - - {/* Başlık & Görünürlük */} - - - - - - - {def.label} - - - - {/* Stantta Göster / Gizle */} - - - {item.visible ? "Görünür" : "Gizli"} - - - setFields((prev) => ({ - ...prev, - [selectedEditField]: { ...prev[selectedEditField], visible: v }, - })) - } - trackColor={{ false: "#27272A", true: "#D4AF37" }} - thumbColor="#FFFFFF" - ios_backgroundColor="#27272A" - /> - - - - {item.visible && ( - <> - {/* Metin Girişi (Enter ile alt satır destekli) */} - - - METİN İÇERİĞİ (ENTER İLE YENİ SATIRA GEÇEBİLİR): - - - setFields((prev) => ({ - ...prev, - [selectedEditField]: { ...prev[selectedEditField], text: t }, - })) - } - placeholder={def.placeholder} - placeholderTextColor="#71717A" - multiline - style={{ - backgroundColor: "#121215", - borderWidth: 1, - borderColor: "rgba(255,255,255,0.12)", - borderRadius: 12, - paddingHorizontal: 12, - paddingVertical: 10, - color: "#FFFFFF", - fontSize: 14, - minHeight: 46, - }} - /> - - - {/* DİKEY KONUM / YUKARI - AŞAĞI TAŞIMA (offsetY) */} - - - - DİKEY KONUM (YUKARI / AŞAĞI TAŞIMA) - - - {item.offsetY ? `${item.offsetY > 0 ? "+" : ""}${item.offsetY} px` : "Orijinal (0px)"} - - - - - setFields((prev) => ({ - ...prev, - [selectedEditField]: { - ...prev[selectedEditField], - offsetY: (prev[selectedEditField].offsetY || 0) - 3, - }, - })) - } - style={{ - flex: 1, - backgroundColor: "#242429", - paddingVertical: 8, - borderRadius: 8, - alignItems: "center", - flexDirection: "row", - justifyContent: "center", - gap: 4, - }} - > - - Yukarı (-3px) - - - - setFields((prev) => ({ - ...prev, - [selectedEditField]: { - ...prev[selectedEditField], - offsetY: (prev[selectedEditField].offsetY || 0) + 3, - }, - })) - } - style={{ - flex: 1, - backgroundColor: "#242429", - paddingVertical: 8, - borderRadius: 8, - alignItems: "center", - flexDirection: "row", - justifyContent: "center", - gap: 4, - }} - > - - Aşağı (+3px) - - - {item.offsetY ? ( - - setFields((prev) => ({ - ...prev, - [selectedEditField]: { - ...prev[selectedEditField], - offsetY: 0, - }, - })) - } - style={{ - backgroundColor: "#3A2A2A", - paddingHorizontal: 12, - paddingVertical: 8, - borderRadius: 8, - alignItems: "center", - justifyContent: "center", - }} - > - Sıfırla - - ) : null} - - - - {/* YAZI TİPİ (FONT) & BOYUT YAN YANA */} - - - YAZI TİPİ (TÜRKÇE DESTEKLİ FONT) - - - {FONT_OPTIONS.map((f) => { - const isSelected = item.fontId === f.id; - return ( - - setFields((prev) => ({ - ...prev, - [selectedEditField]: { ...prev[selectedEditField], fontId: f.id }, - })) - } - style={{ - backgroundColor: isSelected ? "#D4AF37" : "#242429", - paddingHorizontal: 12, - paddingVertical: 7, - borderRadius: 10, - borderWidth: 1.5, - borderColor: isSelected ? "#D4AF37" : "rgba(255,255,255,0.06)", - }} - > - - {f.label} - - - ); - })} - - - - {/* YAZI BOYUTU (FONT SIZE) */} - - - YAZI BOYUTU: - - - - setFields((prev) => ({ - ...prev, - [selectedEditField]: { - ...prev[selectedEditField], - fontSize: Math.max(def.min, (prev[selectedEditField].fontSize || 12) - 1), - }, - })) - } - style={{ - backgroundColor: "#242429", - paddingHorizontal: 14, - paddingVertical: 6, - borderRadius: 8, - }} - > - - - - - {item.fontSize || 12} px - - - - setFields((prev) => ({ - ...prev, - [selectedEditField]: { - ...prev[selectedEditField], - fontSize: Math.min(def.max, (prev[selectedEditField].fontSize || 12) + 1), - }, - })) - } - style={{ - backgroundColor: "#242429", - paddingHorizontal: 14, - paddingVertical: 6, - borderRadius: 8, - }} - > - - - - - - )} - - ); - })()} diff --git a/apps/mobile/src/components/qr-canvas/CanvasLayerItem.tsx b/apps/mobile/src/components/qr-canvas/CanvasLayerItem.tsx new file mode 100644 index 0000000..24d7565 --- /dev/null +++ b/apps/mobile/src/components/qr-canvas/CanvasLayerItem.tsx @@ -0,0 +1,119 @@ +import React, { useRef } from "react"; +import { + PanResponder, + Pressable, + Text, + View, +} from "react-native"; +import { Ionicons } from "@expo/vector-icons"; +import { type CanvasTextLayer, getCanvasFontFamily } from "./types"; + +interface CanvasLayerItemProps { + layer: CanvasTextLayer; + isSelected: boolean; + onSelect: (id: string) => void; + onUpdatePosition: (id: string, x: number, y: number) => void; + onDelete: (id: string) => void; + canvasWidth: number; + canvasHeight: number; +} + +export const CanvasLayerItem: React.FC = ({ + layer, + isSelected, + onSelect, + onUpdatePosition, + onDelete, + canvasWidth, + canvasHeight, +}) => { + const panStartRef = useRef({ x: layer.x, y: layer.y }); + + const panResponder = useRef( + PanResponder.create({ + onStartShouldSetPanResponder: () => true, + onMoveShouldSetPanResponder: () => true, + onPanResponderGrant: () => { + onSelect(layer.id); + panStartRef.current = { x: layer.x, y: layer.y }; + }, + onPanResponderMove: (_evt, gestureState) => { + const newX = Math.max(0, Math.min(canvasWidth - 40, panStartRef.current.x + gestureState.dx)); + const newY = Math.max(0, Math.min(canvasHeight - 30, panStartRef.current.y + gestureState.dy)); + onUpdatePosition(layer.id, Math.round(newX), Math.round(newY)); + }, + }) + ).current; + + const fontFamily = getCanvasFontFamily(layer.fontId); + + return ( + + + + {layer.text || "Metin"} + + + {/* Seçili İken Hızlı Silme Rozeti */} + {isSelected && ( + onDelete(layer.id)} + style={{ + position: "absolute", + top: -12, + right: -12, + backgroundColor: "#EF4444", + width: 22, + height: 22, + borderRadius: 11, + alignItems: "center", + justifyContent: "center", + borderWidth: 1.5, + borderColor: "#FFFFFF", + shadowColor: "#000", + shadowOpacity: 0.3, + shadowRadius: 3, + elevation: 4, + }} + > + + + )} + + + ); +}; diff --git a/apps/mobile/src/components/qr-canvas/QrCanvasStudio.tsx b/apps/mobile/src/components/qr-canvas/QrCanvasStudio.tsx new file mode 100644 index 0000000..0bcbb87 --- /dev/null +++ b/apps/mobile/src/components/qr-canvas/QrCanvasStudio.tsx @@ -0,0 +1,573 @@ +import React, { useState } from "react"; +import { + Pressable, + ScrollView, + Switch, + Text, + TextInput, + View, +} from "react-native"; +import { Ionicons } from "@expo/vector-icons"; +import { CanvasLayerItem } from "./CanvasLayerItem"; +import { + type CanvasTextLayer, + CANVAS_FONT_OPTIONS, + CANVAS_COLOR_PALETTE, + INSTAGRAM_BADGE_STYLES, + getDefaultLayersForTemplate, +} from "./types"; + +interface QrCanvasStudioProps { + templateKey: string; + restaurantName: string; + qrBase64?: string; + width: number; + height: number; + renderBackground: () => React.ReactNode; + layers: CanvasTextLayer[]; + onLayersChange: React.Dispatch>; +} + +export const QrCanvasStudio: React.FC = ({ + templateKey, + restaurantName, + width, + height, + renderBackground, + layers, + onLayersChange, +}) => { + const [selectedLayerId, setSelectedLayerId] = useState(layers[0]?.id || null); + + const selectedLayer = layers.find((l) => l.id === selectedLayerId); + + // Pozisyon Güncelleme + const handleUpdatePosition = (id: string, x: number, y: number) => { + onLayersChange((prev) => + prev.map((l) => (l.id === id ? { ...l, x, y } : l)) + ); + }; + + // Yeni Katman Ekle + const handleAddTextLayer = () => { + const newLayer: CanvasTextLayer = { + id: `layer_${Date.now()}`, + text: "Yeni Metin", + x: Math.round(width / 2 - 40), + y: Math.round(height / 2 - 20), + rotation: 0, + fontSize: 14, + fontId: "sans", + color: "#FFFFFF", + bgBoxColor: "rgba(18, 18, 21, 0.85)", + fontWeight: "800", + }; + onLayersChange((prev) => [...prev, newLayer]); + setSelectedLayerId(newLayer.id); + }; + + // Katman Sil + const handleDeleteLayer = (id: string) => { + onLayersChange((prev) => prev.filter((l) => l.id !== id)); + if (selectedLayerId === id) { + setSelectedLayerId(null); + } + }; + + // Katman Kopyala + const handleDuplicateLayer = (id: string) => { + const target = layers.find((l) => l.id === id); + if (!target) return; + const duplicated: CanvasTextLayer = { + ...target, + id: `layer_${Date.now()}`, + x: Math.min(width - 50, target.x + 15), + y: Math.min(height - 30, target.y + 15), + }; + onLayersChange((prev) => [...prev, duplicated]); + setSelectedLayerId(duplicated.id); + }; + + // Şablonu Sıfırla + const handleResetLayers = () => { + const defaults = getDefaultLayersForTemplate(templateKey, restaurantName); + onLayersChange(defaults); + setSelectedLayerId(defaults[0]?.id || null); + }; + + return ( + + {/* 1. İNTERAKTİF TUVAL ALANI */} + + {/* Arka Plan Şablonu & Sabit QR */} + + {renderBackground()} + + + {/* Serbest Sürüklenebilir Katmanlar */} + {layers.map((layer) => ( + + ))} + + + {/* 2. STÜDYO EYLEM BUTONLARI */} + + {/* Yeni Metin Ekle */} + ({ + flex: 1, + backgroundColor: "#D4AF37", + paddingVertical: 10, + paddingHorizontal: 12, + borderRadius: 12, + flexDirection: "row", + alignItems: "center", + justifyContent: "center", + gap: 6, + opacity: pressed ? 0.85 : 1, + })} + > + + + + Metin Ekle + + + + {/* Seçiliyi Kopyala */} + {selectedLayer && ( + handleDuplicateLayer(selectedLayer.id)} + style={({ pressed }) => ({ + backgroundColor: "#242429", + borderWidth: 1, + borderColor: "rgba(255,255,255,0.1)", + paddingVertical: 10, + paddingHorizontal: 12, + borderRadius: 12, + flexDirection: "row", + alignItems: "center", + gap: 6, + opacity: pressed ? 0.85 : 1, + })} + > + + + Çoğalt + + + )} + + {/* Sıfırla */} + ({ + backgroundColor: "#242429", + borderWidth: 1, + borderColor: "rgba(255,255,255,0.1)", + paddingVertical: 10, + paddingHorizontal: 12, + borderRadius: 12, + flexDirection: "row", + alignItems: "center", + gap: 6, + opacity: pressed ? 0.85 : 1, + })} + > + + + Sıfırla + + + + + {/* 3. SEÇİLİ METİN INSTAGRAM TARZI DÜZENLEYİCİ */} + {selectedLayer ? ( + + {/* Başlık & Kapat */} + + + + + + + Metin & Stil Stüdyosu + + + + setSelectedLayerId(null)} + style={{ + backgroundColor: "rgba(255,255,255,0.08)", + paddingHorizontal: 10, + paddingVertical: 4, + borderRadius: 8, + }} + > + Seçimi Kaldır + + + + {/* Metin İçeriği (Enter Destekli) */} + + + METİN İÇERİĞİ (ENTER İLE YENİ SATIR): + + + onLayersChange((prev) => + prev.map((l) => (l.id === selectedLayer.id ? { ...l, text: t } : l)) + ) + } + placeholder="Metninizi yazın..." + placeholderTextColor="#71717A" + multiline + style={{ + backgroundColor: "#121215", + borderWidth: 1, + borderColor: "rgba(255,255,255,0.12)", + borderRadius: 12, + paddingHorizontal: 12, + paddingVertical: 10, + color: "#FFFFFF", + fontSize: 14, + minHeight: 46, + }} + /> + + + {/* DÖNDÜRME & EĞME AÇISI (ROTATION) */} + + + + EĞİM & DÖNDÜRME AÇISI (ROTATION) + + + {selectedLayer.rotation || 0}° + + + + {[-15, -6, 0, 6, 15].map((deg) => { + const isActive = selectedLayer.rotation === deg; + return ( + + onLayersChange((prev) => + prev.map((l) => + l.id === selectedLayer.id ? { ...l, rotation: deg } : l + ) + ) + } + style={{ + flex: 1, + backgroundColor: isActive ? "#D4AF37" : "#242429", + paddingVertical: 8, + borderRadius: 8, + alignItems: "center", + }} + > + + {deg > 0 ? `+${deg}°` : `${deg}°`} + + + ); + })} + + + + {/* INSTAGRAM TARZI VURGU / KUTU STİLİ */} + + + INSTAGRAM KUTUCUK STİLİ (BADGE): + + + {INSTAGRAM_BADGE_STYLES.map((b) => { + const isActive = selectedLayer.bgBoxColor === b.bgBoxColor; + return ( + + onLayersChange((prev) => + prev.map((l) => + l.id === selectedLayer.id ? { ...l, bgBoxColor: b.bgBoxColor } : l + ) + ) + } + style={{ + backgroundColor: isActive ? "#D4AF37" : "#242429", + paddingHorizontal: 12, + paddingVertical: 7, + borderRadius: 10, + borderWidth: 1.5, + borderColor: isActive ? "#D4AF37" : "rgba(255,255,255,0.06)", + }} + > + + {b.label} + + + ); + })} + + + + {/* YAZI RENGİ PALETİ */} + + + YAZI RENGİ: + + + {CANVAS_COLOR_PALETTE.map((c) => { + const isActive = selectedLayer.color === c.color; + return ( + + onLayersChange((prev) => + prev.map((l) => + l.id === selectedLayer.id ? { ...l, color: c.color } : l + ) + ) + } + style={{ + width: 32, + height: 32, + borderRadius: 16, + backgroundColor: c.color, + borderWidth: isActive ? 3 : 1.5, + borderColor: isActive ? "#D4AF37" : "rgba(255,255,255,0.3)", + alignItems: "center", + justifyContent: "center", + }} + > + {isActive && ( + + )} + + ); + })} + + + + {/* YAZI TİPİ (FONT) */} + + + YAZI TİPİ (TÜRKÇE UYUMLU): + + + {CANVAS_FONT_OPTIONS.map((f) => { + const isActive = selectedLayer.fontId === f.id; + return ( + + onLayersChange((prev) => + prev.map((l) => + l.id === selectedLayer.id ? { ...l, fontId: f.id } : l + ) + ) + } + style={{ + backgroundColor: isActive ? "#D4AF37" : "#242429", + paddingHorizontal: 12, + paddingVertical: 7, + borderRadius: 10, + borderWidth: 1.5, + borderColor: isActive ? "#D4AF37" : "rgba(255,255,255,0.06)", + }} + > + + {f.label} + + + ); + })} + + + + {/* BOYUT & GÖLGE AYARI */} + + {/* Boyut */} + + + Boyut: + + + onLayersChange((prev) => + prev.map((l) => + l.id === selectedLayer.id + ? { ...l, fontSize: Math.max(8, l.fontSize - 1) } + : l + ) + ) + } + style={{ + backgroundColor: "#242429", + paddingHorizontal: 12, + paddingVertical: 6, + borderRadius: 8, + }} + > + + + + {selectedLayer.fontSize}px + + + onLayersChange((prev) => + prev.map((l) => + l.id === selectedLayer.id + ? { ...l, fontSize: Math.min(60, l.fontSize + 1) } + : l + ) + ) + } + style={{ + backgroundColor: "#242429", + paddingHorizontal: 12, + paddingVertical: 6, + borderRadius: 8, + }} + > + + + + + {/* Gölge Anahtarı */} + + + Gölge + + + onLayersChange((prev) => + prev.map((l) => + l.id === selectedLayer.id ? { ...l, hasShadow: v } : l + ) + ) + } + trackColor={{ false: "#27272A", true: "#D4AF37" }} + thumbColor="#FFFFFF" + ios_backgroundColor="#27272A" + /> + + + + ) : ( + + + + Stant üzerindeki herhangi bir metne dokunarak sürükleyin veya düzenleyin. + + + )} + + ); +}; diff --git a/apps/mobile/src/components/qr-canvas/index.ts b/apps/mobile/src/components/qr-canvas/index.ts new file mode 100644 index 0000000..bc3cc69 --- /dev/null +++ b/apps/mobile/src/components/qr-canvas/index.ts @@ -0,0 +1,3 @@ +export * from "./types"; +export * from "./CanvasLayerItem"; +export * from "./QrCanvasStudio"; diff --git a/apps/mobile/src/components/qr-canvas/types.ts b/apps/mobile/src/components/qr-canvas/types.ts new file mode 100644 index 0000000..846f6a8 --- /dev/null +++ b/apps/mobile/src/components/qr-canvas/types.ts @@ -0,0 +1,206 @@ +export interface CanvasTextLayer { + id: string; + text: string; + x: number; // px from canvas left + y: number; // px from canvas top + rotation: number; // degrees (-180 to 180) + fontSize: number; + fontId: string; + color: string; + bgBoxColor?: string; // Instagram badge kutusu (örn: rgba(0,0,0,0.8), #D4AF37 vb.) + hasShadow?: boolean; + letterSpacing?: number; + fontWeight?: "normal" | "600" | "700" | "800" | "900"; +} + +export const CANVAS_FONT_OPTIONS = [ + { + id: "sans", + label: "Modern Sans", + fontFamily: "System, -apple-system, Roboto, 'Helvetica Neue', Arial, sans-serif", + }, + { + id: "serif", + label: "Zarif Serif", + fontFamily: "Georgia, 'Times New Roman', serif", + }, + { + id: "times", + label: "Klasik Times", + fontFamily: "'Times New Roman', Times, Georgia, serif", + }, + { + id: "palatino", + label: "Lüks Palatino", + fontFamily: "Palatino, 'Palatino Linotype', Georgia, serif", + }, + { + id: "trebuchet", + label: "Dinamik", + fontFamily: "'Trebuchet MS', 'Segoe UI', Arial, sans-serif", + }, +]; + +export const CANVAS_COLOR_PALETTE = [ + { id: "gold", color: "#D4AF37", label: "Altın" }, + { id: "white", color: "#FFFFFF", label: "Beyaz" }, + { id: "black", color: "#121215", label: "Siyah" }, + { id: "cream", color: "#F6EFE3", label: "Krem" }, + { id: "emerald", color: "#0C4443", label: "Zümrüt" }, + { id: "crimson", color: "#A64423", label: "Kızıl" }, + { id: "blue", color: "#003554", label: "Lacivert" }, +]; + +export const INSTAGRAM_BADGE_STYLES = [ + { id: "none", label: "Düz", bgBoxColor: undefined }, + { id: "dark", label: "Siyah Kutu", bgBoxColor: "rgba(18, 18, 21, 0.88)" }, + { id: "gold", label: "Altın Kutu", bgBoxColor: "rgba(212, 175, 55, 0.92)" }, + { id: "white", label: "Beyaz Kutu", bgBoxColor: "rgba(255, 255, 255, 0.92)" }, +]; + +export function getCanvasFontFamily(fontId?: string): string { + const found = CANVAS_FONT_OPTIONS.find((f) => f.id === fontId); + return found ? found.fontFamily : "System, -apple-system, Roboto, sans-serif"; +} + +// Her şablon için başlangıç varsayılan katmanları (1:1 veya standart orana göre) +export function getDefaultLayersForTemplate(templateKey: string, restaurantName = "Gourmet Bistro"): CanvasTextLayer[] { + if (templateKey === "qr-1") { + return [ + { + id: "rName", + text: restaurantName.toUpperCase(), + x: 40, + y: 50, + rotation: 0, + fontSize: 18, + fontId: "serif", + color: "#D4AF37", + fontWeight: "800", + letterSpacing: 2, + }, + { + id: "sub", + text: "RESTAURANT & BAR", + x: 65, + y: 78, + rotation: 0, + fontSize: 9, + fontId: "sans", + color: "#E5C378", + fontWeight: "600", + letterSpacing: 1.5, + }, + { + id: "cta", + text: "MENÜYÜ GÖRMEK İÇİN OKUTUN", + x: 35, + y: 236, + rotation: 0, + fontSize: 10, + fontId: "serif", + color: "#D4AF37", + fontWeight: "700", + letterSpacing: 1, + }, + { + id: "table", + text: "MASA NO: 01 • WiFi: Bistro", + x: 45, + y: 265, + rotation: 0, + fontSize: 9, + fontId: "sans", + color: "#C5A059", + fontWeight: "600", + }, + ]; + } + + if (templateKey === "qr-8") { + return [ + { + id: "rName", + text: "EXPLORE OUR MENU", + x: 35, + y: 35, + rotation: 0, + fontSize: 16, + fontId: "sans", + color: "#FFFFFF", + fontWeight: "900", + letterSpacing: 1, + }, + { + id: "sub", + text: "THE DAILY SPECIALS", + x: 50, + y: 68, + rotation: -4, + fontSize: 13, + fontId: "sans", + color: "#D1A752", + fontWeight: "800", + }, + { + id: "cta", + text: "SCAN TO VIEW\nFULL MENU & SPECIALS", + x: 55, + y: 215, + rotation: 0, + fontSize: 9.5, + fontId: "sans", + color: "#FFFFFF", + fontWeight: "800", + }, + { + id: "wifi", + text: "SCAN TO ORDER & BROWSE", + x: 55, + y: 258, + rotation: 0, + fontSize: 9, + fontId: "sans", + color: "#D1A752", + fontWeight: "800", + }, + ]; + } + + // Varsayılan + return [ + { + id: "rName", + text: restaurantName, + x: 50, + y: 45, + rotation: 0, + fontSize: 17, + fontId: "serif", + color: "#D4AF37", + fontWeight: "800", + }, + { + id: "sub", + text: "SCAN TO ORDER", + x: 75, + y: 75, + rotation: 0, + fontSize: 10, + fontId: "sans", + color: "#FFFFFF", + fontWeight: "700", + }, + { + id: "cta", + text: "DİJİTAL MENÜ", + x: 75, + y: 245, + rotation: 0, + fontSize: 11, + fontId: "serif", + color: "#D4AF37", + fontWeight: "800", + }, + ]; +}