diff --git a/apps/mobile/src/app/menu/qr.tsx b/apps/mobile/src/app/menu/qr.tsx index 3decb99..cf6f67f 100644 --- a/apps/mobile/src/app/menu/qr.tsx +++ b/apps/mobile/src/app/menu/qr.tsx @@ -6,7 +6,9 @@ import { Pressable, ScrollView, Share, + Switch, Text, + TextInput, View, } from "react-native"; import { Ionicons } from "@expo/vector-icons"; @@ -17,7 +19,13 @@ import * as Print from "expo-print"; import { QR_STAND_PRESETS, type QrStandPreset } from "@menulio/shared"; import { api } from "@/lib/api"; import { getActiveRestaurant, type ActiveRestaurant } from "@/lib/active-restaurant"; -import { COMPONENT_TEMPLATES, TEMPLATE_ASPECT_RATIOS } from "@/components/qr-templates"; +import { + COMPONENT_TEMPLATES, + TEMPLATE_ASPECT_RATIOS, + FONT_OPTIONS, + type QrCustomTextConfig, +} from "@/components/qr-templates"; + interface QrResponse { id: string; @@ -41,6 +49,21 @@ export default function QrScreen() { ); const [exportingPng, setExportingPng] = useState(false); const [exportingPdf, setExportingPdf] = useState(false); + const [showCustomizePanel, setShowCustomizePanel] = useState(false); + const [selectedFontId, setSelectedFontId] = useState("didot"); + const [customText, setCustomText] = useState({ + restaurantName: "Gourmet Bistro", + subtitle: "RESTAURANT & BAR | MODERN CUISINE", + tableNo: "MASA NO: 01", + ctaText: "MENÜYÜ GÖRMEK İÇİN OKUTUN", + wifiText: "WiFi: Gourmet_Guest | Şifre: lezzetli01", + socialText: "@gourmetbistro", + showTableNo: true, + showSubtitle: true, + showCta: true, + showWifi: true, + showSocial: true, + }); // eslint-disable-next-line @typescript-eslint/no-explicit-any const viewShotRef = useRef(null); @@ -48,6 +71,10 @@ export default function QrScreen() { const activePreset: QrStandPreset = QR_STAND_PRESETS[selectedKey] || availablePresets[0] || Object.values(QR_STAND_PRESETS)[0]!; + const activeFont = + FONT_OPTIONS.find((f) => f.id === selectedFontId)?.fontFamily || + "'Playfair Display', Georgia, serif"; + // Component-based template sistemi const TemplateComponent = COMPONENT_TEMPLATES[selectedKey] ?? Object.values(COMPONENT_TEMPLATES)[0]; @@ -56,6 +83,7 @@ export default function QrScreen() { const displayHeight = Math.round(displayWidth / aspectRatio); + useEffect(() => { (async () => { const activeRest = await getActiveRestaurant(); @@ -71,6 +99,10 @@ export default function QrScreen() { const restDetail = await api.get<{ name?: string }>(`/restaurants/${activeRest.restaurantId}`); if (restDetail?.name) { setRestaurantName(restDetail.name); + setCustomText((prev) => ({ + ...prev, + restaurantName: restDetail.name, + })); } } catch { // fallback @@ -83,6 +115,7 @@ export default function QrScreen() { })(); }, []); + async function handleCopy() { if (!qr?.targetUrl) return; await Clipboard.setStringAsync(qr.targetUrl); @@ -322,12 +355,332 @@ export default function QrScreen() { qrBase64={qr.pngBase64} width={displayWidth} height={displayHeight} + restaurantName={customText.restaurantName} + subtitle={customText.subtitle} + tableNo={customText.tableNo} + ctaText={customText.ctaText} + wifiText={customText.wifiText} + socialText={customText.socialText} + fontFamily={activeFont} + showTableNo={customText.showTableNo} + showSubtitle={customText.showSubtitle} + showCta={customText.showCta} + showWifi={customText.showWifi} + showSocial={customText.showSocial} /> ) : null} + {/* Metin & Font Özelleştirme Kartı (Açılır/Kapanır) */} + + setShowCustomizePanel(!showCustomizePanel)} + style={{ + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + }} + > + + + + + + + Metin ve Yazı Tipi Özelleştir + + + İstediğiniz alanları düzenleyin veya kaldırın + + + + + + + {showCustomizePanel && ( + + {/* Yazı Tipi (Font) Seçici */} + + + YAZI TİPİ (FONT) + + + {FONT_OPTIONS.map((f) => { + const isSelected = f.id === selectedFontId; + return ( + setSelectedFontId(f.id)} + style={{ + backgroundColor: isSelected ? "#D4AF37" : "#242429", + paddingHorizontal: 14, + paddingVertical: 8, + borderRadius: 10, + borderWidth: 1, + borderColor: isSelected ? "#D4AF37" : "rgba(255,255,255,0.08)", + }} + > + + {f.label} + + + ); + })} + + + + {/* Alan 1: Restoran Başlığı */} + + + RESTORAN BAŞLIĞI + + setCustomText((p) => ({ ...p, restaurantName: t }))} + placeholder="Restoran Adı" + placeholderTextColor="#71717A" + style={{ + backgroundColor: "#121215", + borderWidth: 1, + borderColor: "rgba(255,255,255,0.1)", + borderRadius: 12, + paddingHorizontal: 14, + paddingVertical: 10, + color: "#FFFFFF", + fontSize: 14, + }} + /> + + + {/* Alan 2: Slogan / Alt Başlık */} + + + + SLOGAN / ALT BAŞLIK + + + + {customText.showSubtitle ? "Göster" : "Gizle"} + + setCustomText((p) => ({ ...p, showSubtitle: v }))} + trackColor={{ false: "#27272A", true: "#D4AF37" }} + thumbColor="#FFFFFF" + ios_backgroundColor="#27272A" + /> + + + {customText.showSubtitle && ( + setCustomText((p) => ({ ...p, subtitle: t }))} + placeholder="Slogan / Açıklama" + placeholderTextColor="#71717A" + style={{ + backgroundColor: "#121215", + borderWidth: 1, + borderColor: "rgba(255,255,255,0.1)", + borderRadius: 12, + paddingHorizontal: 14, + paddingVertical: 10, + color: "#FFFFFF", + fontSize: 14, + }} + /> + )} + + + {/* Alan 3: Masa Numarası */} + + + + MASA NUMARASI + + + + {customText.showTableNo ? "Göster" : "Gizle"} + + setCustomText((p) => ({ ...p, showTableNo: v }))} + trackColor={{ false: "#27272A", true: "#D4AF37" }} + thumbColor="#FFFFFF" + ios_backgroundColor="#27272A" + /> + + + {customText.showTableNo && ( + setCustomText((p) => ({ ...p, tableNo: t }))} + placeholder="Örn: MASA NO: 01" + placeholderTextColor="#71717A" + style={{ + backgroundColor: "#121215", + borderWidth: 1, + borderColor: "rgba(255,255,255,0.1)", + borderRadius: 12, + paddingHorizontal: 14, + paddingVertical: 10, + color: "#FFFFFF", + fontSize: 14, + }} + /> + )} + + + {/* Alan 4: Yönlendirme (CTA) */} + + + + YÖNLENDİRME (CTA) METNİ + + + + {customText.showCta ? "Göster" : "Gizle"} + + setCustomText((p) => ({ ...p, showCta: v }))} + trackColor={{ false: "#27272A", true: "#D4AF37" }} + thumbColor="#FFFFFF" + ios_backgroundColor="#27272A" + /> + + + {customText.showCta && ( + setCustomText((p) => ({ ...p, ctaText: t }))} + placeholder="Örn: MENÜYÜ GÖRMEK İÇİN OKUTUN" + placeholderTextColor="#71717A" + style={{ + backgroundColor: "#121215", + borderWidth: 1, + borderColor: "rgba(255,255,255,0.1)", + borderRadius: 12, + paddingHorizontal: 14, + paddingVertical: 10, + color: "#FFFFFF", + fontSize: 14, + }} + /> + )} + + + {/* Alan 5: WiFi Bilgileri */} + + + + WIFI BİLGİSİ + + + + {customText.showWifi ? "Göster" : "Gizle"} + + setCustomText((p) => ({ ...p, showWifi: v }))} + trackColor={{ false: "#27272A", true: "#D4AF37" }} + thumbColor="#FFFFFF" + ios_backgroundColor="#27272A" + /> + + + {customText.showWifi && ( + setCustomText((p) => ({ ...p, wifiText: t }))} + placeholder="Örn: WiFi: Bistro | Şifre: 12345" + placeholderTextColor="#71717A" + style={{ + backgroundColor: "#121215", + borderWidth: 1, + borderColor: "rgba(255,255,255,0.1)", + borderRadius: 12, + paddingHorizontal: 14, + paddingVertical: 10, + color: "#FFFFFF", + fontSize: 14, + }} + /> + )} + + + {/* Alan 6: Sosyal Medya / İletişim */} + + + + SOSYAL MEDYA / İLETİŞİM + + + + {customText.showSocial ? "Göster" : "Gizle"} + + setCustomText((p) => ({ ...p, showSocial: v }))} + trackColor={{ false: "#27272A", true: "#D4AF37" }} + thumbColor="#FFFFFF" + ios_backgroundColor="#27272A" + /> + + + {customText.showSocial && ( + setCustomText((p) => ({ ...p, socialText: t }))} + placeholder="Örn: @menulio" + placeholderTextColor="#71717A" + style={{ + backgroundColor: "#121215", + borderWidth: 1, + borderColor: "rgba(255,255,255,0.1)", + borderRadius: 12, + paddingHorizontal: 14, + paddingVertical: 10, + color: "#FFFFFF", + fontSize: 14, + }} + /> + )} + + + )} + + + {/* Output Action Buttons */} {/* PDF Download Button */} diff --git a/apps/mobile/src/components/qr-templates/QrTemplate2.tsx b/apps/mobile/src/components/qr-templates/QrTemplate2.tsx index dd955f8..59d8ca9 100644 --- a/apps/mobile/src/components/qr-templates/QrTemplate2.tsx +++ b/apps/mobile/src/components/qr-templates/QrTemplate2.tsx @@ -1,6 +1,5 @@ import * as React from "react"; import Svg, { - SvgProps, Path, Rect, Circle, @@ -9,12 +8,24 @@ import Svg, { Text, Image, } from "react-native-svg"; +import type { QrTemplateProps } from "./index"; -interface QrTemplate2Props extends SvgProps { - qrBase64?: string; -} - -const QrTemplate2 = ({ qrBase64, ...props }: QrTemplate2Props) => ( +const QrTemplate2 = ({ + qrBase64, + restaurantName = "The Daily Grind", + subtitle = "COFFEE & KITCHEN", + tableNo = "MASA NO: 04", + ctaText = "MENÜYÜ GÖRMEK İÇİN OKUTUN", + wifiText = "WiFi: DailyGrind | Şifre: coffee2026", + socialText = "@dailygrind", + fontFamily = "'Playfair Display', Georgia, serif", + showTableNo = true, + showSubtitle = true, + showCta = true, + showWifi = true, + showSocial = true, + ...props +}: QrTemplateProps) => ( ( /> - {/* Kahve Çekirdekleri (arka plan süsü) */} + {/* Kahve Çekirdekleri */} @@ -196,11 +207,12 @@ const QrTemplate2 = ({ qrBase64, ...props }: QrTemplate2Props) => ( {/* Metinler */} + {/* Üst Tag / Takı */} ( > {"THE"} - - {"The Daily Grind"} - - - {"COFFEE & KITCHEN"} - - - {"SCAN TO VIEW MENU"} - - - {"DAILY GRIND CAFE | WIFI: DAILY_GRIND_GUEST"} - - - {"\u015E\u0130FRE: coffee2026 \u2022 MASA NO: 04"} - + + {/* Restoran Adı */} + {restaurantName ? ( + + {restaurantName} + + ) : null} + + {/* Slogan */} + {showSubtitle && subtitle ? ( + + {subtitle} + + ) : null} + + {/* CTA */} + {showCta && ctaText ? ( + + {ctaText} + + ) : null} + + {/* WiFi */} + {showWifi && wifiText ? ( + + {wifiText} + + ) : null} + + {/* Masa No & Sosyal */} + {showTableNo || showSocial ? ( + + {[ + showTableNo && tableNo ? tableNo : "", + showSocial && socialText ? socialText : "", + ] + .filter(Boolean) + .join(" • ")} + + ) : null} + - {"EST. 2018"} + {"EST. 2026"} diff --git a/apps/mobile/src/components/qr-templates/QrTemplate3.tsx b/apps/mobile/src/components/qr-templates/QrTemplate3.tsx index 914d5e0..70cfdd8 100644 --- a/apps/mobile/src/components/qr-templates/QrTemplate3.tsx +++ b/apps/mobile/src/components/qr-templates/QrTemplate3.tsx @@ -1,6 +1,5 @@ import * as React from "react"; import Svg, { - SvgProps, Defs, LinearGradient, Stop, @@ -11,15 +10,26 @@ import Svg, { Ellipse, Circle, Text, - TSpan, Image, } from "react-native-svg"; +import type { QrTemplateProps } from "./index"; -interface QrTemplate3Props extends SvgProps { - qrBase64?: string; -} - -const QrTemplate3 = ({ qrBase64, ...props }: QrTemplate3Props) => ( +const QrTemplate3 = ({ + qrBase64, + restaurantName = "BELLA NAPOLI", + subtitle = "AUTENTICA CUCINA ITALIANA", + tableNo = "MASA NO: 01", + ctaText = "MENÜYÜ GÖRMEK İÇİN OKUTUN", + wifiText = "WiFi: GourmetBistro | Şifre: lezzetli01", + socialText = "@gourmetbistro", + fontFamily = "'Playfair Display', Georgia, serif", + showTableNo = true, + showSubtitle = true, + showCta = true, + showWifi = true, + showSocial = true, + ...props +}: QrTemplateProps) => ( ( strokeWidth={0.8} rx={15} /> - + {showTableNo && tableNo ? ( + + ) : null} {/* İkonlar */} @@ -243,102 +255,113 @@ const QrTemplate3 = ({ qrBase64, ...props }: QrTemplate3Props) => ( {/* Metinler */} - - {"MASA NO: 01"} - + {/* Masa No */} + {showTableNo && tableNo ? ( + + {tableNo} + + ) : null} + {"PIZZERIA"} - - {"BELLA NAPOLI"} - - - {"AUTENTICA CUCINA ITALIANA"} - - - {"MENÜYÜ GÖRMEK"} - - - {"İÇİN OKUTUN"} - - - {"WiFi: GourmetBistro | Şifre: lezzetli01"} - - - {"BİZİ TAKİP EDİN | @gourmetbistro"} - + + {/* Restoran Adı */} + {restaurantName ? ( + + {restaurantName} + + ) : null} + + {/* Slogan */} + {showSubtitle && subtitle ? ( + + {subtitle} + + ) : null} + + {/* CTA */} + {showCta && ctaText ? ( + + {ctaText} + + ) : null} + + {/* WiFi */} + {showWifi && wifiText ? ( + + {wifiText} + + ) : null} + + {/* Sosyal Medya */} + {showSocial && socialText ? ( + + {socialText} + + ) : null} - ); export default QrTemplate3; diff --git a/apps/mobile/src/components/qr-templates/QrTemplateLumiere.tsx b/apps/mobile/src/components/qr-templates/QrTemplateLumiere.tsx index 86c09d4..3930dbf 100644 --- a/apps/mobile/src/components/qr-templates/QrTemplateLumiere.tsx +++ b/apps/mobile/src/components/qr-templates/QrTemplateLumiere.tsx @@ -1,6 +1,5 @@ import * as React from "react"; import Svg, { - SvgProps, Path, Rect, Circle, @@ -8,12 +7,24 @@ import Svg, { Text, Image, } from "react-native-svg"; +import type { QrTemplateProps } from "./index"; -interface QrTemplateLumiereProps extends SvgProps { - qrBase64?: string; -} - -const QrTemplate1 = ({ qrBase64, ...props }: QrTemplateLumiereProps) => ( +const QrTemplateLumiere = ({ + qrBase64, + restaurantName = "LUMIÈRE", + subtitle = "RESTAURANT & BAR | MODERN CUISINE", + tableNo = "MASA NO: 01", + ctaText = "MENÜYÜ GÖRMEK İÇİN OKUTUN", + wifiText = "WiFi: Lumiere_Guest | Şifre: lumiere2026", + socialText = "Rezervasyon: 0212 555 0199", + fontFamily = "'Playfair Display', Georgia, serif", + showTableNo = true, + showSubtitle = true, + showCta = true, + showWifi = true, + showSocial = true, + ...props +}: QrTemplateProps) => ( ( d="M560 532a18 18 0 0 0-28 28" /> + {/* Merkez amblem */} ( /> + {/* Yatay çizgi */} ( strokeWidth={0.8} d="M120 198h360" /> + {/* QR Placeholder / Gerçek QR */} ( )} + {/* Metinler */} - - {"LUMI\xC8RE"} - - - {"RESTAURANT & BAR | MODERN CUISINE"} - - - {"SCAN WITH CAMERA"} - - - {"Reservations: LumiereDining.com | 240-555-0199"} - - - {"TABLE 01 \u2022 WIFI: Lumiere_Guest"} - + {/* Restoran Adı */} + {restaurantName ? ( + + {restaurantName} + + ) : null} + + {/* Slogan / Alt Başlık */} + {showSubtitle && subtitle ? ( + + {subtitle} + + ) : null} + + {/* CTA / Yönlendirme */} + {showCta && ctaText ? ( + + {ctaText} + + ) : null} + + {/* Sosyal / İletişim */} + {showSocial && socialText ? ( + + {socialText} + + ) : null} + + {/* Masa No & WiFi */} + {showTableNo || showWifi ? ( + + {[ + showTableNo && tableNo ? tableNo : "", + showWifi && wifiText ? wifiText : "", + ] + .filter(Boolean) + .join(" • ")} + + ) : null} ); -export default QrTemplate1; +export default QrTemplateLumiere; diff --git a/apps/mobile/src/components/qr-templates/index.ts b/apps/mobile/src/components/qr-templates/index.ts index 4017705..fd176e2 100644 --- a/apps/mobile/src/components/qr-templates/index.ts +++ b/apps/mobile/src/components/qr-templates/index.ts @@ -1,27 +1,52 @@ import type React from "react"; import type { SvgProps } from "react-native-svg"; -import QrTemplateLumiere from "./QrTemplateLumiere"; +import QrTemplate1 from "./QrTemplateLumiere"; import QrTemplate2 from "./QrTemplate2"; import QrTemplate3 from "./QrTemplate3"; -export interface QrTemplateProps extends SvgProps { +export interface QrCustomTextConfig { + restaurantName?: string; + subtitle?: string; + tableNo?: string; + ctaText?: string; + wifiText?: string; + socialText?: string; + fontFamily?: string; + // Görünürlük anahtarları + showTableNo?: boolean; + showSubtitle?: boolean; + showCta?: boolean; + showWifi?: boolean; + showSocial?: boolean; +} + +export interface QrTemplateProps extends SvgProps, QrCustomTextConfig { qrBase64?: string; } +export const FONT_OPTIONS = [ + { id: "sans", label: "Modern Sans", fontFamily: "System, -apple-system, Arial, sans-serif" }, + { id: "serif", label: "Zarif Serif", fontFamily: "Georgia, 'Times New Roman', serif" }, + { id: "didot", label: "Lüks Didot", fontFamily: "Didot, 'Playfair Display', Georgia, serif" }, + { id: "palatino", label: "Klasik Kitap", fontFamily: "Palatino, 'Palatino Linotype', serif" }, + { id: "trebuchet", label: "Dinamik", fontFamily: "'Trebuchet MS', Helvetica, sans-serif" }, +]; + // Yeni component eklemek için buraya import edip COMPONENT_TEMPLATES'e kaydet export const COMPONENT_TEMPLATES: Record< string, React.FC > = { - "qr-lumiere": QrTemplateLumiere, + "qr-1": QrTemplate1, "qr-2": QrTemplate2, "qr-3": QrTemplate3, }; // Her template'in aspect ratio'su (width/height) export const TEMPLATE_ASPECT_RATIOS: Record = { - "qr-lumiere": 1, // 600x600 = 1:1 + "qr-1": 1, // 600x600 = 1:1 "qr-2": 400 / 650, // 400x650 = standart kart "qr-3": 400 / 650, // 400x650 = standart kart }; +