Compare commits

..
36 Commits
Author SHA1 Message Date
AyrisAIandClaude Sonnet 5 d33a38de4e feat: restaurant cover photo upload
restaurants.cover_url existed in the schema and was already read by the
public menu page, but nothing could ever write it — the update endpoint
didn't accept it and there was no picker in the app. Adds coverUrl to
PATCH /restaurants/:id and a cover photo picker in the mobile account
screen, next to the existing logo picker.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-29 01:58:10 +03:00
AyrisAIandClaude Sonnet 5 df250c40e8 feat(web): public menu language pill selector; fix missing cover_url column
Public menu page now shows a TR|EN|... pill selector (styled after the
Menulio design reference) that switches category/item text to the AI
translations added via the mobile app, falling back to Turkish for
anything not yet translated.

Also adds a migration for restaurants.cover_url, which was referenced by
existing code but was missing from the live database — the public menu
page was silently forcing cover photos to null on every restaurant.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-29 01:55:26 +03:00
AyrisAIandClaude Sonnet 5 6ec4631e82 feat(mobile): language management UI in account settings
Restaurant owners can now enable/disable AI-translated menu languages
(EN/DE/AR/RU/FR) from the account screen, calling the new
/restaurants/:id/languages endpoints. Turkish stays as the fixed base
language; adding a language triggers a full AI translation pass, removing
one just hides it (translations are kept for a fast re-enable).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-29 01:53:45 +03:00
AyrisAIandClaude Sonnet 5 00195ae7d7 feat(api): AI-powered multi-language menu translation
Adds enabled_languages on restaurants and translations JSONB on menu
categories/items. New /restaurants/:id/languages endpoints bulk-translate
the full menu via Gemini/GPT-4o-mini (reusing the ai-scanner dual-provider
pattern) when a language is enabled. New categories/items are auto-
translated in the background into any already-enabled languages.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-29 01:46:19 +03:00
AyrisAI 739de7de5c feat(web): add missing qr-3/qr-8/qr-9 stand SVG templates
QR_STAND_PRESETS (packages/shared) references qr-1..qr-9 (skipping
qr-7), but apps/web/public only had qr-1,2,4,5,6.svg — qr-3, qr-8,
and qr-9 404'd wherever the raw preset list renders before/instead of
a DB-driven override. Built to match the existing qr-1.svg structure
(layer-background/layer-icons/qr-placeholder/layer-texts, same 400x650
canvas, QR box at translate(90,215) 220x220 — verified this aligns
with QrStandGeneratorClient's hardcoded 33.08%/55% overlay position),
using each preset's own color palette from qr-stands.ts.
2026-08-29 01:21:05 +03:00
AyrisAI 1f8195ff67 feat: admin auth, web menu template studio, live text editing & Ayris Tech footer backlink 2026-08-23 01:45:00 +03:00
AyrisAI e7ba6c7807 feat(qr): complete QR studio, dynamic google fonts, mouse canvas drag, and template sync 2026-08-22 16:00:30 +03:00
AyrisAI c1569c0879 chore: migrate monorepo configuration strictly to pnpm v11, clean up npm leftovers and update workspace config 2026-08-21 12:11:26 +03:00
AyrisAI ae2386eacf feat(mobile): integrate Instagram/Canva style interactive QR Canvas Studio with touch drag, rotate, badge boxes, shadows and dynamic layers 2026-08-21 01:24:17 +03:00
AyrisAI d00b45ce72 feat(mobile): integrate QrTemplate9 Vintage Table Menu with 9.jpeg background and live editing 2026-08-21 01:19:04 +03:00
AyrisAI 102a9a744b feat(mobile): integrate QrTemplate8 Emerald & Gold Special with 8.jpeg background and live editing 2026-08-21 01:10:40 +03:00
AyrisAI b0645c0ac3 feat(mobile): replace blocking modal with inline live inspector for instantaneous visual editing feedback 2026-08-21 01:04:03 +03:00
AyrisAI 2c3599839b feat(mobile): add QrTemplate6 Stonehearth Grill, modal text editor and vertical positioning (offsetY) controls 2026-08-21 01:00:29 +03:00
AyrisAI 2530ab4005 feat(mobile): add multiline text with Enter, per-text independent font selection and font size controls 2026-08-21 00:55:44 +03:00
AyrisAI b72846bd91 feat(mobile): integrate QrTemplate5 with 5.png background image and dynamic texts 2026-08-21 00:52:26 +03:00
AyrisAI d2846ed628 feat(mobile): add QrTemplate4 Crimson Petal, support full Turkish characters on all system fonts and templates 2026-08-21 00:41:16 +03:00
AyrisAI 4dbb68a73a feat(mobile): add full text editing, show/hide switches and font family selector for QR stands 2026-08-21 00:36:14 +03:00
AyrisAI 932c174b5f fix(mobile): resolve TSpan text overlapping in QrTemplate3 2026-08-21 00:31:17 +03:00
AyrisAI 34dd24eee2 feat(mobile): register QrTemplate2 Daily Grind and filter UI to active component templates 2026-08-21 00:30:14 +03:00
AyrisAI 8481a34e61 feat(mobile): add QrTemplate3 Pizzeria, fix PDF export for component system 2026-08-21 00:25:44 +03:00
AyrisAI ccd8c90267 fix(mobile): decode XML entities for react-native-svg text rendering 2026-08-21 00:07:43 +03:00
AyrisAI 3629f98855 fix(mobile): proper nested-g depth counting for qr-placeholder replacement 2026-08-21 00:06:03 +03:00
AyrisAI dd282f3987 fix(mobile): strip XML comments before SvgXml parse to fix closing tag error 2026-08-21 00:02:37 +03:00
AyrisAI 4236ddb5ee feat: re-integrate all SVGs from svg ornekleri/ fresh, add qr-6 2026-08-20 23:59:45 +03:00
AyrisAI c7e4832441 fix(mobile): do not mutate SVG attributes, only replace qr-placeholder group 2026-08-20 23:58:21 +03:00
AyrisAI fa50e73198 fix(mobile): inject QR directly into SVG XML for pixel-perfect placement 2026-08-20 23:56:37 +03:00
AyrisAI 808097377d feat(qr-stands): add qr-5 L'Atelier Bistro Fine Dining template 2026-08-20 23:50:30 +03:00
AyrisAI 6cde8dfb87 feat: sync qr presets and mobile rendering with svg/ directory 2026-08-20 23:43:58 +03:00
AyrisAI 01c9f5f195 feat(mobile): render exact SVG vector templates in mobile QR stand screen 2026-08-20 23:43:26 +03:00
AyrisAI f768b92873 feat(qr-stands): clean SVG template renderer with exact QR placement and no redundant overlays 2026-08-20 23:39:56 +03:00
AyrisAI eb0b5d1886 feat(qr-stands): add qr-2.svg preset from user code 2026-08-20 23:36:01 +03:00
AyrisAI 38fd0dd030 feat(qr-stands): activate user provided qr-1.svg and qr-4.svg 2026-08-20 23:34:57 +03:00
AyrisAI 18396f3b99 fix(qr-stands): restore exact user-provided qr-1.svg alongside qr-4.svg 2026-08-20 23:31:54 +03:00
AyrisAI edb7af9288 feat(qr-stands): add flat vector qr-1.svg urban preset 2026-08-20 23:30:04 +03:00
AyrisAI a0cb04b2c6 feat(qr-stands): set qr-4 as exclusive vector stand template 2026-08-20 23:25:19 +03:00
AyrisAI 4ce6505a4d feat(qr-stands): add custom-svg-3 preset with user provided SVG stand code 2026-08-20 19:03:18 +03:00
119 changed files with 18720 additions and 739 deletions
+8
View File
@@ -5,6 +5,10 @@ build/
.expo/
.turbo/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
*.tsbuildinfo
.env
.env.local
@@ -15,3 +19,7 @@ supabase/.temp/
coverage/
*.ipa
*.apk
# Lockfile protections (monorepo strictly uses pnpm-lock.yaml)
package-lock.json
yarn.lock
+4
View File
@@ -0,0 +1,4 @@
auto-install-peers=true
strict-peer-dependencies=false
enable-pre-post-scripts=true
only-built-dependencies=core-js,@swc/core,esbuild,sharp,@esbuild/darwin-arm64,@next/swc-darwin-arm64
+144
View File
@@ -0,0 +1,144 @@
# 🚀 Menulio — Cross-Platform SaaS & Dynamic QR Stand Studio Ecosystem
> **Modern Restoranlar İçin Dijital Menü Deneyimi & Anlık JSON Şablon Motoru Destekli Masa Kartı Tasarım Platformu**
![Menulio Banner](https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?w=1200&auto=format&fit=crop&q=80)
---
## 📌 Proje Özeti (Executive Summary)
**Menulio**, restoranların ve gastronomi işletmelerinin dijital menülerini ve fiziksel masa kartı QR stantlarını uçtan uca yönetmelerini sağlayan **Full-Stack Cross-Platform SaaS mimarisidir**.
Geleneksel QR menü sistemlerinden farklı olarak Menulio, **0 (Sıfır) App Store güncellemesi gerektiren "JSON-Driven Dynamic Template Engine" (V2)** teknolojisine sahiptir. Admin panelinden tasarlanan SVG/PNG arka planları, kavisli metin hesaplamaları (*curved text math*), 360° döndürme, renk paletleri ve özel QR yerleşimleri API üzerinden anında hem Web hem de Mobil (iOS/Android) platformlarında canlı yayınlanır.
---
## 🛠️ Öne Çıkan Mimari Özellikler (Key Highlights)
### 1. 🎨 JSON-Driven Dynamic QR Stand Engine (V2)
- **Sıfır Güncelleme Dağıtımı**: Mobil uygulamayı yeniden derlemeden Web Admin panelinden yeni masa kartı şablonu oluşturma ve anında yayınlama.
- **Kavisli Metin Matematiği (`curveDegree`)**: Radyal açı hesaplamaları ile dairesel ve yay biçiminde metin hizalama (Yay ◡ ve ◠ kavis derecesi ayarlama).
- **Figma & Instagram Story Tarzı Tuval Editörü**: Sürüklenebilir (*drag & drop*), ölçeklenebilir, 360° döndürülebilir metin ve QR katmanları.
- **Manyetik Kılavuz Çizgileri**: Akıllı dikey/yatay merkezleme ve hizalama çizgileri (*snap-to-grid*).
- **Matbaa Kalitesinde Baskı Çıktısı**: 300 DPI Ultra HD PNG ve A5 Matbaa standartlarında vektörel PDF çıktısı.
### 2. 📱 Cross-Platform Mobil Uygulama (Expo & React Native)
- **iOS & Android Desteği**: Expo SDK 52 ve React Native mimarisi üzerine kurulu yüksek performanslı arayüz.
- **Dokunsal Geri Bildirim (Haptic Feedback)**: İşlem anında `expo-haptics` ile kullanıcıya fiziksel titreşim uyarısı.
- **Yerel iOS Derleme & Paketleme**: Xcode Archive & Fastlane entegrasyonu ile tam imzalı `embedded.mobileprovision` içerikli `.ipa` dağıtımı.
### 3. 🌐 Web Uygulaması & 10+ Lüks Menü Teması (Next.js 15 App Router)
- **10 Özel Restoran Teması**: `Eleia Coastal`, `Neon Street Fuze`, `La Maison d'Élève`, `Swiss Brutalist`, `Amber Artisan`, `Elegant Gold`, `Modern Sapphire`, `Luxury Dark`, `Nordic Minimal`, `Classic Bistro`.
- **Wildcard Subdomain Yönlendirmesi**: `restoran-adi.menul.io` şeklinde her işletmeye özel subdomain ve özel alan adı (custom domain) desteği.
- **Canlı Cihaz Simülatörü**: Admin panelinde tema ve şablon değişikliklerini anında gösteren interaktif iPhone çerçevesi.
### 4. 🔐 Çift Katmanlı Yönetici Güvenliği (Dual-Layer Admin Auth)
- **Next.js Server Middleware**: Yetkisiz sunucu isteklerini anında `/admin/login` sayfasına yönlendiren çerez tabanlı doğrulama.
- **Client Auth Guard**: React tarafında yükleme animasyonu ve oturum doğrulama mekanizması.
- **Supabase Auth & Fallback Credential**: Supabase PostgreSQL veritabanı entegrasyonu ve güvenli token çerezi (`menulio_admin_token`).
---
## 💻 Teknolojik Mimari & Kullanılan Teknolojiler
| Katman | Kullanılan Teknolojiler |
| :--- | :--- |
| **Monorepo Mimarisi** | `pnpm Workspaces`, Shared Type Definitions (`@menulio/shared`) |
| **Web Uygulaması (Frontend)** | `Next.js 15 (App Router)`, `React 18`, `Tailwind CSS`, `Glassmorphism UI` |
| **Mobil Uygulama** | `React Native`, `Expo (SDK 52)`, `Expo Router`, `ViewShot`, `Expo Haptics` |
| **Veritabanı & Auth** | `Supabase (PostgreSQL, Auth, RLS)`, REST API, Local Disk Cache |
| **Görsel & Baskı Motoru** | `html-to-image`, `jspdf`, `qrcode.react`, Google Fonts API |
| **CI/CD & Derleme** | `Xcode 26`, `Fastlane`, `EAS Build (Expo Application Services)` |
---
## 📸 Ekran Görüntüleri & Görsel Bileşenler
```
+-----------------------------------------------------------------------+
| MENULIO ADMIN PANEL - DİNAMİK ŞABLON & TEMA STÜDYOSU |
+-----------------------------------------------------------------------+
| [ Sol Panel: Renk/Düzen Kontrolleri ] | [ Sağ: Canlı iPhone Simülatörü ]|
| - Primary Color: #D4AF37 | +-----------------------------+ |
| - Product Layout: Card / List | | OZZY KEBAP | |
| - Category Layout: Tabs / Accordion | | [ QR KOD BÖLGESİ ] | |
| - Curved Text Math: 35° Arc | | MENÜYÜ GÖRMEK İÇİN OKUTUN | |
| - Font Family: Playfair / Inter | +-----------------------------+ |
+-----------------------------------------------------------------------+
```
---
## 📐 Proje Dosya & Klasör Yapısı
```
menulio/
├── apps/
│ ├── mobile/ # React Native / Expo Mobil Uygulama
│ │ ├── src/
│ │ │ ├── app/ # Expo Router Dosya Tabanlı Yönlendirme
│ │ │ ├── components/
│ │ │ │ ├── qr-canvas/ # Tuval Editörü, Curved Text & Dokunmatik Katmanlar
│ │ │ │ └── qr-templates/ # SVG & Vektörel Şablon Render Bileşenleri
│ │ └── ios/ # Yerel Xcode Proje Klasörü & Podfile
│ │
│ ├── web/ # Next.js 15 Web & Admin Uygulaması
│ │ ├── src/
│ │ │ ├── app/
│ │ │ │ ├── admin/ # Yönetici Paneli & Auth Guard (/admin)
│ │ │ │ │ ├── templates/ # QR Stand Şablon Stüdyosu
│ │ │ │ │ └── menu-templates/ # Web Menü Temaları Stüdyosu
│ │ │ │ ├── qr/ # Müşteri QR Masa Kartı Jeneratörü
│ │ │ │ └── menu/[slug]/ # Canlı Restoran Web Menüsü
│ │ │ ├── components/ # Web Bileşenleri & Simülatörler
│ │ │ └── lib/ # Auth, Supabase & Tema Tanımları
│ │
│ └── api/ # Express / Node.js Backend REST Servisleri
└── packages/
└── shared/ # Tip Tanımları, Renk Paletleri & Ortak Sabitler
```
---
## ⚡ Kurulum ve Çalıştırma (Getting Started)
Proje `pnpm` monorepo yapısında kurgulanmıştır.
### 1. Bağımlılıkları Yükleyin
```bash
pnpm install
```
### 2. Geliştirme Sunucularını Başlatın
- **Tüm Uygulamaları Aynı Anda Başlatma**:
```bash
pnpm dev
```
- **Sadece Web Uygulamasını Başlatma**:
```bash
pnpm --filter web dev
```
- **Sadece Mobil Uygulamayı (Expo) Başlatma**:
```bash
pnpm --filter mobile start
```
---
## 👨‍💻 Mühendislik & Problem Çözme Vurguları (Portfolio Highlights)
1. **React Native Font Rendering Fix**:
- React Native'in iOS ve Android native font motorunda virgüllü CSS fallback dizgilerinin (`Georgia, 'Times New Roman', serif`) sessizce varsayılan sistem fontuna düşme problemi tespit edilerek, platforma özel tekil font çözümleme mekanizması geliştirildi.
2. **Kavisli Metin Radyal Geometri Matematiği**:
- Metin karakterlerinin dairesel bir yay etrafında doğru açı (`rotate`), `translateX` ve `translateY` offset hesaplamaları ile hem Web SVG/HTML hem de Native Text bileşenlerinde piksel mükemmelliğinde işlenmesi sağlandı.
3. **Senkronize JSON Şablon Yayınlama**:
- Web Admin panelinde yapılan herhangi bir tasarım veya metin düzenlemesinin, veritabanı ve REST API üzerinden anında mobil uygulama tuvalinde ve web jeneratöründe canlı olarak görünmesi sağlandı.
---
## 📄 Lisans
Bu proje kişisel portfolyo ve özel geliştirme amacıyla kurgulanmıştır. Tüm hakları saklıdır.
+203
View File
@@ -0,0 +1,203 @@
[
{
"key": "qr-1",
"name": "Lumière Restaurant (QR-1)",
"categoryName": "Özel Masa Kartı",
"svgTemplateUrl": "/qr-1.svg",
"aspectRatio": "1/1",
"bgType": "gradient",
"bgGradient": "radial-gradient(circle at 50% 50%, #132034 0%, #0b0d13 100%)",
"plaqueColor": "#132034",
"borderAccent": "#d4af37",
"accentText": "#d4af37",
"headerText": "#FFFFFF",
"subText": "#e5c378",
"dividerColor": "rgba(212, 175, 55, 0.3)",
"isActive": true,
"qrConfig": {
"xPercent": 50,
"yPercent": 38,
"size": 115,
"borderRadius": 14,
"borderColor": "#D4AF37",
"bgBoxColor": "#FFFFFF"
},
"layers": [
{
"id": "rName",
"text": "{{RESTAURANT_NAME}}",
"yPercent": 16,
"fontSize": 18,
"fontFamily": "Playfair Display, Georgia, serif",
"color": "#D4AF37",
"fontWeight": "800",
"letterSpacing": 2
},
{
"id": "sub",
"text": "RESTAURANT & BAR",
"yPercent": 26,
"fontSize": 10,
"fontFamily": "Plus Jakarta Sans, sans-serif",
"color": "#E5C378",
"fontWeight": "600",
"letterSpacing": 1.5
},
{
"id": "cta",
"text": "MENÜYÜ GÖRMEK İÇİN OKUTUN",
"yPercent": 80,
"fontSize": 11,
"fontFamily": "Playfair Display, Georgia, serif",
"color": "#FFFFFF",
"fontWeight": "700",
"letterSpacing": 1
},
{
"id": "table",
"text": "MASA NO: 01 • WIFI: Bistro",
"yPercent": 90,
"fontSize": 9,
"fontFamily": "Plus Jakarta Sans, sans-serif",
"color": "#A1A1AA",
"fontWeight": "600"
}
]
},
{
"key": "qr-2",
"name": "The Daily Grind Cafe (QR-2)",
"categoryName": "Özel Masa Kartı",
"svgTemplateUrl": "/qr-2.svg",
"aspectRatio": "400/650",
"bgType": "gradient",
"bgGradient": "radial-gradient(circle at 50% 50%, #252c36 0%, #171c22 100%)",
"plaqueColor": "#f6efe2",
"borderAccent": "#3a2312",
"accentText": "#8c6239",
"headerText": "#2c1a0e",
"subText": "#3a2312",
"dividerColor": "rgba(140, 98, 57, 0.3)",
"isActive": true,
"qrConfig": {
"xPercent": 50,
"yPercent": 36,
"size": 125,
"borderRadius": 14,
"borderColor": "#3a2312",
"bgBoxColor": "#FFFFFF"
},
"layers": [
{
"id": "rName",
"text": "{{RESTAURANT_NAME}}",
"yPercent": 14,
"fontSize": 18,
"fontFamily": "Playfair Display, Georgia, serif",
"color": "#8c6239",
"fontWeight": "800",
"letterSpacing": 2
},
{
"id": "cta",
"text": "MENÜYÜ GÖRMEK İÇİN OKUTUN",
"yPercent": 82,
"fontSize": 11,
"fontFamily": "Playfair Display, Georgia, serif",
"color": "#2c1a0e",
"fontWeight": "700",
"letterSpacing": 1
}
]
},
{
"key": "qr-3",
"name": "Pizzeria Bella Napoli (QR-3)",
"categoryName": "Özel Masa Kartı",
"svgTemplateUrl": "/qr-3.svg",
"aspectRatio": "400/650",
"bgType": "gradient",
"bgGradient": "radial-gradient(circle at 50% 50%, #faf4e6 0%, #f7eed8 100%)",
"plaqueColor": "#fdfbf5",
"borderAccent": "#b92b27",
"accentText": "#b92b27",
"headerText": "#18181b",
"subText": "#5d4037",
"dividerColor": "rgba(185, 43, 39, 0.3)",
"isActive": true,
"qrConfig": {
"xPercent": 50,
"yPercent": 36,
"size": 125,
"borderRadius": 14,
"borderColor": "#b92b27",
"bgBoxColor": "#FFFFFF"
},
"layers": [
{
"id": "rName",
"text": "{{RESTAURANT_NAME}}",
"yPercent": 14,
"fontSize": 18,
"fontFamily": "Playfair Display, Georgia, serif",
"color": "#b92b27",
"fontWeight": "800",
"letterSpacing": 2
},
{
"id": "cta",
"text": "MENÜYÜ GÖRMEK İÇİN OKUTUN",
"yPercent": 82,
"fontSize": 11,
"fontFamily": "Playfair Display, Georgia, serif",
"color": "#18181b",
"fontWeight": "700",
"letterSpacing": 1
}
]
},
{
"key": "qr-8",
"name": "Emerald & Gold Special (QR-8)",
"categoryName": "Özel Kare Masa Kartı",
"svgTemplateUrl": "/qr-8.svg",
"aspectRatio": "1/1",
"bgType": "gradient",
"bgGradient": "radial-gradient(circle at 50% 50%, #0c4443 0%, #062423 100%)",
"plaqueColor": "#f9f5eb",
"borderAccent": "#d1a752",
"accentText": "#d1a752",
"headerText": "#ffffff",
"subText": "#d1a752",
"dividerColor": "rgba(209, 167, 82, 0.4)",
"isActive": true,
"qrConfig": {
"xPercent": 50,
"yPercent": 38,
"size": 115,
"borderRadius": 14,
"borderColor": "#d1a752",
"bgBoxColor": "#FFFFFF"
},
"layers": [
{
"id": "rName",
"text": "{{RESTAURANT_NAME}}",
"yPercent": 16,
"fontSize": 18,
"fontFamily": "Playfair Display, Georgia, serif",
"color": "#d1a752",
"fontWeight": "800"
},
{
"id": "cta",
"text": "MENÜYÜ GÖRMEK İÇİN OKUTUN",
"yPercent": 80,
"fontSize": 11,
"fontFamily": "Playfair Display, Georgia, serif",
"color": "#ffffff",
"fontWeight": "700"
}
]
}
]
+4
View File
@@ -4,11 +4,13 @@ import { env } from "./env.js";
import { aiImportsRoutes } from "./routes/ai-imports.js";
import { analyticsRoutes } from "./routes/analytics.js";
import { domainsRoutes } from "./routes/domains.js";
import { languagesRoutes } from "./routes/languages.js";
import { menuCategoriesRoutes } from "./routes/menu-categories.js";
import { menuItemsRoutes } from "./routes/menu-items.js";
import { menusRoutes } from "./routes/menus.js";
import { meRoutes } from "./routes/me.js";
import { qrRoutes } from "./routes/qr.js";
import { qrTemplatesRoutes } from "./routes/qr-templates.js";
import { restaurantsRoutes } from "./routes/restaurants.js";
import { subscriptionRoutes } from "./routes/subscription.js";
@@ -42,8 +44,10 @@ await app.register(menusRoutes);
await app.register(menuCategoriesRoutes);
await app.register(menuItemsRoutes);
await app.register(qrRoutes);
await app.register(qrTemplatesRoutes);
await app.register(aiImportsRoutes);
await app.register(domainsRoutes);
await app.register(languagesRoutes);
await app.register(analyticsRoutes);
await app.register(subscriptionRoutes);
+148
View File
@@ -0,0 +1,148 @@
import { env } from "../env.js";
export interface TranslatableItem {
id: string;
name: string;
description: string | null;
}
export interface TranslatableCategory {
id: string;
name: string;
items: TranslatableItem[];
}
export interface TranslatedItem {
id: string;
name: string;
description: string | null;
}
export interface TranslatedCategory {
id: string;
name: string;
items: TranslatedItem[];
}
const LANGUAGE_NAMES: Record<string, string> = {
en: "English",
de: "German",
ar: "Arabic",
ru: "Russian",
fr: "French",
};
function buildPrompt(targetLanguageName: string): string {
return `Sen profesyonel bir restoran menüsü çevirmenisin. Sana Türkçe bir restoran menüsü (kategori adları, ürün adları ve açıklamaları) JSON olarak verilecek.
Bunları ${targetLanguageName} diline çevir. Yemek isimlerini o mutfağın doğal terimleriyle çevir (örn. "Adana Kebap" -> "Adana Kebab", tamamen literal çeviri yapma, restoran menüsü diline uygun doğal bir üslup kullan).
Her kategori ve ürünün "id" alanını AYNEN koru, sadece "name" ve "description" alanlarını çevir. description null ise null bırak.
SADECE aşağıdaki JSON formatında geçerli bir JSON yanıtı ver, başka hiçbir metin ekleme:
{
"categories": [
{ "id": "...", "name": "çevrilmiş kategori adı", "items": [ { "id": "...", "name": "çevrilmiş ürün adı", "description": "çevrilmiş açıklama veya null" } ] }
]
}`;
}
function cleanAndParse(text: string): { categories: TranslatedCategory[] } {
const cleaned = text
.replace(/^```json\s*/i, "")
.replace(/^```\s*/i, "")
.replace(/\s*```$/i, "")
.trim();
const parsed = JSON.parse(cleaned) as { categories: TranslatedCategory[] };
if (!parsed.categories || !Array.isArray(parsed.categories)) {
throw new Error("Invalid structure from translator: categories array missing");
}
return parsed;
}
async function translateWithGemini(
categories: TranslatableCategory[],
targetLanguageName: string,
apiKey: string,
): Promise<TranslatedCategory[]> {
const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${apiKey}`;
const response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
contents: [
{
parts: [
{ text: buildPrompt(targetLanguageName) },
{ text: JSON.stringify({ categories }) },
],
},
],
generationConfig: { response_mime_type: "application/json", temperature: 0.2 },
}),
});
if (!response.ok) {
throw new Error(`Gemini translate error (${response.status}): ${await response.text()}`);
}
const data = (await response.json()) as {
candidates?: { content?: { parts?: { text?: string }[] } }[];
};
const rawText = data.candidates?.[0]?.content?.parts?.[0]?.text;
if (!rawText) throw new Error("No text response from Gemini translate");
return cleanAndParse(rawText).categories;
}
async function translateWithOpenAI(
categories: TranslatableCategory[],
targetLanguageName: string,
apiKey: string,
): Promise<TranslatedCategory[]> {
const response = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
body: JSON.stringify({
model: "gpt-4o-mini",
response_format: { type: "json_object" },
messages: [
{ role: "system", content: buildPrompt(targetLanguageName) },
{ role: "user", content: JSON.stringify({ categories }) },
],
temperature: 0.2,
}),
});
if (!response.ok) {
throw new Error(`OpenAI translate error (${response.status}): ${await response.text()}`);
}
const data = (await response.json()) as { choices?: { message?: { content?: string } }[] };
const content = data.choices?.[0]?.message?.content;
if (!content) throw new Error("No message content from OpenAI translate");
return cleanAndParse(content).categories;
}
export function resolveLanguageName(code: string): string {
return LANGUAGE_NAMES[code] ?? code;
}
export async function translateMenu(
categories: TranslatableCategory[],
languageCode: string,
): Promise<TranslatedCategory[]> {
if (categories.length === 0) return [];
const targetLanguageName = resolveLanguageName(languageCode);
if (env.GEMINI_API_KEY) {
try {
return await translateWithGemini(categories, targetLanguageName, env.GEMINI_API_KEY);
} catch (err) {
console.warn("Gemini translate failed, falling back:", err);
}
}
if (env.OPENAI_API_KEY) {
return translateWithOpenAI(categories, targetLanguageName, env.OPENAI_API_KEY);
}
throw new Error("No translation provider configured (GEMINI_API_KEY / OPENAI_API_KEY missing)");
}
+147
View File
@@ -0,0 +1,147 @@
import type { FastifyPluginAsync } from "fastify";
import { z } from "zod";
import { requireAuth, requireRestaurantMember } from "../lib/auth.js";
import { supabase } from "../lib/supabase.js";
import { translateMenu, type TranslatableCategory } from "../lib/translator.js";
const SUPPORTED_LANGUAGES = ["en", "de", "ar", "ru", "fr"] as const;
const addLanguageSchema = z.object({
code: z.enum(SUPPORTED_LANGUAGES),
});
async function getRestaurantMenuCategories(restaurantId: string): Promise<TranslatableCategory[]> {
if (!supabase) return [];
const { data: locations } = await supabase.from("locations").select("id").eq("restaurant_id", restaurantId);
const locationIds = (locations ?? []).map((l) => l.id);
if (locationIds.length === 0) return [];
const { data: menus } = await supabase.from("menus").select("id").in("location_id", locationIds);
const menuIds = (menus ?? []).map((m) => m.id);
if (menuIds.length === 0) return [];
const { data: categories } = await supabase
.from("menu_categories")
.select("id, name, menu_items(id, name, description)")
.in("menu_id", menuIds);
return (categories ?? []).map((cat) => ({
id: cat.id,
name: cat.name,
items: (cat.menu_items ?? []).map((item: { id: string; name: string; description: string | null }) => ({
id: item.id,
name: item.name,
description: item.description,
})),
}));
}
export const languagesRoutes: FastifyPluginAsync = async (app) => {
// Add a language: translates all current menu content into it, then
// enables it. New items created afterwards are translated on save
// (see menu-categories.ts / menu-items.ts).
app.post("/restaurants/:id/languages", async (req, reply) => {
const userId = await requireAuth(req, reply);
if (!userId || !supabase) return;
const { id: restaurantId } = req.params as { id: string };
const isMember = await requireRestaurantMember(userId, restaurantId);
if (!isMember) return reply.code(403).send({ message: "forbidden" });
const parsed = addLanguageSchema.safeParse(req.body);
if (!parsed.success) {
return reply.code(400).send({ message: "invalid language code", issues: parsed.error.issues });
}
const { code } = parsed.data;
const { data: restaurant } = await supabase
.from("restaurants")
.select("enabled_languages")
.eq("id", restaurantId)
.single();
const current: string[] = restaurant?.enabled_languages ?? ["tr"];
if (current.includes(code)) {
return reply.code(409).send({ message: "Bu dil zaten aktif." });
}
const categories = await getRestaurantMenuCategories(restaurantId);
try {
const translated = await translateMenu(categories, code);
for (const cat of translated) {
const original = categories.find((c) => c.id === cat.id);
const { data: existingCatRow } = await supabase
.from("menu_categories")
.select("translations")
.eq("id", cat.id)
.single();
await supabase
.from("menu_categories")
.update({
translations: { ...(existingCatRow?.translations ?? {}), [code]: { name: cat.name } },
})
.eq("id", cat.id);
for (const item of cat.items) {
const { data: existingItemRow } = await supabase
.from("menu_items")
.select("translations")
.eq("id", item.id)
.single();
await supabase
.from("menu_items")
.update({
translations: {
...(existingItemRow?.translations ?? {}),
[code]: { name: item.name, description: item.description },
},
})
.eq("id", item.id);
}
void original;
}
} catch (err) {
req.log.error(err);
return reply.code(500).send({ message: "Menü çevirisi başarısız oldu", error: (err as Error).message });
}
const { data: updated } = await supabase
.from("restaurants")
.update({ enabled_languages: [...current, code] })
.eq("id", restaurantId)
.select("enabled_languages")
.single();
return reply.send({ enabledLanguages: updated?.enabled_languages ?? [...current, code] });
});
app.delete("/restaurants/:id/languages/:code", async (req, reply) => {
const userId = await requireAuth(req, reply);
if (!userId || !supabase) return;
const { id: restaurantId, code } = req.params as { id: string; code: string };
const isMember = await requireRestaurantMember(userId, restaurantId);
if (!isMember) return reply.code(403).send({ message: "forbidden" });
const { data: restaurant } = await supabase
.from("restaurants")
.select("enabled_languages")
.eq("id", restaurantId)
.single();
const current: string[] = restaurant?.enabled_languages ?? ["tr"];
const next = current.filter((c) => c !== code);
const { data: updated } = await supabase
.from("restaurants")
.update({ enabled_languages: next })
.eq("id", restaurantId)
.select("enabled_languages")
.single();
return reply.send({ enabledLanguages: updated?.enabled_languages ?? next });
});
};
+30
View File
@@ -3,6 +3,34 @@ import { z } from "zod";
import { requireAuth } from "../lib/auth.js";
import { getCategoryIfMember, getMenuIfMember } from "../lib/access.js";
import { supabase } from "../lib/supabase.js";
import { translateMenu } from "../lib/translator.js";
// New categories/items are translated into whatever languages the
// restaurant already has enabled beyond Turkish (bulk-translated once when
// a language is first added — see routes/languages.ts).
async function translateNewCategoryInBackground(restaurantId: string, categoryId: string, name: string) {
if (!supabase) return;
const { data: restaurant } = await supabase
.from("restaurants")
.select("enabled_languages")
.eq("id", restaurantId)
.single();
const extraLanguages = (restaurant?.enabled_languages ?? []).filter((l: string) => l !== "tr");
if (extraLanguages.length === 0) return;
const translations: Record<string, { name: string }> = {};
for (const lang of extraLanguages) {
try {
const [translated] = await translateMenu([{ id: categoryId, name, items: [] }], lang);
if (translated) translations[lang] = { name: translated.name };
} catch (err) {
console.warn(`Category translation to ${lang} failed:`, err);
}
}
if (Object.keys(translations).length > 0) {
await supabase.from("menu_categories").update({ translations }).eq("id", categoryId);
}
}
const createCategorySchema = z.object({
name: z.string().min(1).max(120),
@@ -47,6 +75,8 @@ export const menuCategoriesRoutes: FastifyPluginAsync = async (app) => {
return reply.code(500).send({ message: "failed to create category" });
}
void translateNewCategoryInBackground(access.restaurantId, data.id, data.name);
return reply.code(201).send(data);
});
+36
View File
@@ -3,6 +3,40 @@ import { z } from "zod";
import { requireAuth } from "../lib/auth.js";
import { getCategoryIfMember, getItemIfMember } from "../lib/access.js";
import { supabase } from "../lib/supabase.js";
import { translateMenu } from "../lib/translator.js";
async function translateNewItemInBackground(
restaurantId: string,
itemId: string,
name: string,
description: string | null,
) {
if (!supabase) return;
const { data: restaurant } = await supabase
.from("restaurants")
.select("enabled_languages")
.eq("id", restaurantId)
.single();
const extraLanguages = (restaurant?.enabled_languages ?? []).filter((l: string) => l !== "tr");
if (extraLanguages.length === 0) return;
const translations: Record<string, { name: string; description: string | null }> = {};
for (const lang of extraLanguages) {
try {
const [translatedCat] = await translateMenu(
[{ id: "single", name: "-", items: [{ id: itemId, name, description }] }],
lang,
);
const translatedItem = translatedCat?.items[0];
if (translatedItem) translations[lang] = { name: translatedItem.name, description: translatedItem.description };
} catch (err) {
console.warn(`Item translation to ${lang} failed:`, err);
}
}
if (Object.keys(translations).length > 0) {
await supabase.from("menu_items").update({ translations }).eq("id", itemId);
}
}
const createItemSchema = z.object({
name: z.string().min(1).max(160),
@@ -53,6 +87,8 @@ export const menuItemsRoutes: FastifyPluginAsync = async (app) => {
return reply.code(500).send({ message: "failed to create item" });
}
void translateNewItemInBackground(access.restaurantId, data.id, data.name, data.description);
return reply.code(201).send(data);
});
+156
View File
@@ -0,0 +1,156 @@
import type { FastifyInstance } from "fastify";
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { type QrStandPreset } from "@menulio/shared";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const DATA_FILE = path.resolve(__dirname, "../data/qr-templates.json");
export interface DynamicQrTemplate extends QrStandPreset {
id: string;
isActive: boolean;
bgType?: "gradient" | "image" | "svg";
bgImageUrl?: string;
createdAt: string;
updatedAt: string;
defaultQrConfig?: {
xPercent: number;
yPercent: number;
size: number;
borderRadius: number;
borderColor: string;
bgBoxColor: string;
};
defaultLayers?: Array<{
id: string;
text: string;
xPercent?: number;
yPercent: number;
fontSize: number;
fontFamily?: string;
color: string;
fontWeight?: string;
}>;
}
async function loadTemplatesFromDisk(): Promise<DynamicQrTemplate[]> {
try {
const content = await fs.readFile(DATA_FILE, "utf-8");
const parsed = JSON.parse(content);
return parsed.map((p: any) => ({
...p,
id: p.key,
isActive: p.isActive ?? true,
createdAt: p.createdAt || new Date().toISOString(),
updatedAt: p.updatedAt || new Date().toISOString(),
defaultQrConfig: p.defaultQrConfig || p.qrConfig,
defaultLayers: p.defaultLayers || p.layers,
}));
} catch {
return [];
}
}
async function saveTemplatesToDisk(templates: DynamicQrTemplate[]): Promise<void> {
try {
await fs.writeFile(DATA_FILE, JSON.stringify(templates, null, 2), "utf-8");
} catch (err) {
console.error("Failed to write qr-templates to disk:", err);
}
}
export async function qrTemplatesRoutes(app: FastifyInstance) {
// 1. Tüm Aktif Şablonları Listele (Mobil ve Web İçin)
app.get("/qr-templates", async () => {
const list = await loadTemplatesFromDisk();
return {
templates: list.filter((t) => t.isActive),
total: list.length,
};
});
// 2. Admin Şablon Listesi (Aktif + Taslak hepsi)
app.get("/admin/qr-templates", async () => {
const list = await loadTemplatesFromDisk();
return {
templates: list,
total: list.length,
};
});
// 3. Yeni Şablon Ekle veya Mevcut Olanı Güncelle
app.post<{ Body: Partial<DynamicQrTemplate> }>("/admin/qr-templates", async (req, reply) => {
const body = req.body;
if (!body.name) {
return reply.status(400).send({ error: "Şablon adı zorunludur." });
}
const key = body.key || `qr-${Date.now()}`;
const list = await loadTemplatesFromDisk();
const existingIndex = list.findIndex((t) => t.id === key || t.key === key);
const now = new Date().toISOString();
const newTemplate: DynamicQrTemplate = {
key,
id: key,
name: body.name,
categoryName: body.categoryName || "Özel Şablon",
svgTemplateUrl: body.svgTemplateUrl || `/templates/${key}.svg`,
aspectRatio: body.aspectRatio || "1/1",
bgType: body.bgType || "gradient",
bgImageUrl: body.bgImageUrl,
bgGradient: body.bgGradient || "radial-gradient(circle at 50% 50%, #132034 0%, #0b0d13 100%)",
plaqueColor: body.plaqueColor || "#132034",
borderAccent: body.borderAccent || "#D4AF37",
accentText: body.accentText || "#D4AF37",
headerText: body.headerText || "#FFFFFF",
subText: body.subText || "#E5C378",
dividerColor: body.dividerColor || "rgba(212, 175, 55, 0.3)",
isActive: body.isActive !== undefined ? body.isActive : true,
createdAt: existingIndex >= 0 ? list[existingIndex].createdAt : now,
updatedAt: now,
defaultQrConfig: body.defaultQrConfig || (body as any).qrConfig || {
xPercent: 50,
yPercent: 38,
size: 115,
borderRadius: 14,
borderColor: body.borderAccent || "#D4AF37",
bgBoxColor: "#FFFFFF",
},
defaultLayers: body.defaultLayers || (body as any).layers || [
{
id: "rName",
text: "{{RESTAURANT_NAME}}",
yPercent: 16,
fontSize: 18,
color: body.accentText || "#D4AF37",
fontWeight: "800",
},
],
};
if (existingIndex >= 0) {
list[existingIndex] = newTemplate;
} else {
list.unshift(newTemplate);
}
await saveTemplatesToDisk(list);
return { success: true, template: newTemplate };
});
// 4. Şablon Sil
app.delete<{ Params: { id: string } }>("/admin/qr-templates/:id", async (req, reply) => {
const { id } = req.params;
const list = await loadTemplatesFromDisk();
const filtered = list.filter((t) => t.id !== id && t.key !== id);
if (filtered.length === list.length) {
return reply.status(404).send({ error: "Şablon bulunamadı." });
}
await saveTemplatesToDisk(filtered);
return { success: true, deletedId: id };
});
}
+3 -1
View File
@@ -16,6 +16,7 @@ const createRestaurantSchema = z.object({
const updateRestaurantSchema = z.object({
name: z.string().min(1).max(120).optional(),
logoUrl: z.string().nullish(),
coverUrl: z.string().nullish(),
phone: z.string().max(30).nullish(),
address: z.string().max(500).nullish(),
slug: z.string().min(2).max(100).optional(),
@@ -108,7 +109,7 @@ export const restaurantsRoutes: FastifyPluginAsync = async (app) => {
return reply.code(400).send({ message: "invalid body", issues: parsed.error.issues });
}
const { name, logoUrl, phone, address, slug } = parsed.data;
const { name, logoUrl, coverUrl, phone, address, slug } = parsed.data;
let formattedSlug: string | undefined = undefined;
if (slug !== undefined) {
@@ -145,6 +146,7 @@ export const restaurantsRoutes: FastifyPluginAsync = async (app) => {
.update({
...(name !== undefined ? { name } : {}),
...(logoUrl !== undefined ? { logo_url: logoUrl } : {}),
...(coverUrl !== undefined ? { cover_url: coverUrl } : {}),
...(phone !== undefined ? { phone } : {}),
...(address !== undefined ? { address } : {}),
...(formattedSlug !== undefined ? { slug: formattedSlug } : {}),
+3 -1
View File
@@ -7,7 +7,8 @@
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "automatic",
"newArchEnabled": false,
"newArchEnabled": true,
"platforms": ["ios", "android"],
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
@@ -17,6 +18,7 @@
"ios": {
"supportsTablet": false,
"bundleIdentifier": "com.ayristech.menulio",
"buildNumber": "21",
"icon": "./assets/icon.png",
"infoPlist": {
"NSCameraUsageDescription": "Menünüzün fotoğrafını çekerek AI ile dijitalleştirmek için kamera izni gereklidir.",
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 4.5 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 357 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

+71
View File
@@ -0,0 +1,71 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 650" width="400" height="650">
<defs>
<!-- Define a linear gradient for gold effect -->
<linearGradient id="goldGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#D4AF37"/>
<stop offset="100%" stop-color="#FFD700"/>
</linearGradient>
</defs>
<!-- Background Canvas -->
<rect id="bg-canvas" fill="#451A03" width="400" height="650"/>
<!-- Main Menu Card Panel -->
<rect id="main-card" x="20" y="20" width="360" height="610" rx="24" fill="#000000" stroke="url(#goldGradient)" stroke-width="2"/>
<!-- Layer for Icons/Graphics -->
<g id="layer-icons">
<!-- Top decorative line -->
<line x1="80" y1="165" x2="320" y2="165" stroke="url(#goldGradient)" stroke-width="1" fill="#D97706"/>
<!-- Bottom decorative line -->
<line x1="80" y1="535" x2="320" y2="535" stroke="url(#goldGradient)" stroke-width="1" fill="#B45309"/>
<!-- Subtle corner accents (L-shapes) -->
<path d="M40 40 L40 70 M40 40 L70 40" stroke="url(#goldGradient)" stroke-width="1.5" stroke-linecap="round" fill="#D97706"/>
<path d="M360 40 L360 70 M360 40 L330 40" stroke="url(#goldGradient)" stroke-width="1.5" stroke-linecap="round" fill="#B45309"/>
<path d="M40 610 L40 580 M40 610 L70 610" stroke="url(#goldGradient)" stroke-width="1.5" stroke-linecap="round" fill="#D97706"/>
<path d="M360 610 L360 580 M360 610 L330 610" stroke="url(#goldGradient)" stroke-width="1.5" stroke-linecap="round" fill="#B45309"/>
<!-- Minimalist Fork and Knife Line Art -->
<g transform="translate(0, 15)">
<!-- Fork -->
<path d="M185 550 L185 560 M188 550 L188 560 M191 550 L191 560 M188 560 L188 565" stroke="url(#goldGradient)" stroke-width="1.5" stroke-linecap="round" fill="#D97706"/>
<!-- Knife -->
<path d="M205 550 L205 560 L208 560 L208 552 C208 550 206.5 549 205 549 Z M206.5 560 L206.5 565" stroke="url(#goldGradient)" stroke-width="1.5" stroke-linecap="round" fill="#B45309"/>
</g>
</g>
<!-- QR Placeholder -->
<g id="qr-placeholder" transform="translate(90, 215)">
<rect width="220" height="220" rx="20" fill="#FFFFFF" stroke="#E4E4E7" stroke-width="2"/>
<rect x="12" y="12" width="196" height="196" rx="14" fill="#FAFAFA" stroke="#E4E4E7" stroke-dasharray="4 3"/>
<text x="110" y="115" font-family="Inter, sans-serif" font-size="12" font-weight="600" fill="#71717A" text-anchor="middle">
QR KOD ALANI
</text>
</g>
<!-- Layer for Texts -->
<g id="layer-texts" fill="#c10101">
<!-- Table Info badge -->
<rect x="140" y="40" width="120" height="30" rx="15" fill="url(#goldGradient)"/>
<text x="200" y="60" font-family="Inter, sans-serif" font-size="14" font-weight="600" fill="#1A1A1D" text-anchor="middle">
MASA NO: 01
</text>
<!-- Restaurant Name -->
<text x="200" y="120" font-family="Inter, sans-serif" font-size="26" font-weight="700" fill="url(#goldGradient)" text-anchor="middle">
Gourmet Bistro
</text>
<!-- Tagline / Subtitle -->
<text x="200" y="145" font-family="Inter, sans-serif" font-size="12" font-weight="400" fill="#E4E4E7" text-anchor="middle">
Temassız Dijital Menü
</text>
<!-- "MENÜYÜ GÖRMEK İÇİN OKUTUN" -->
<text x="200" y="480" font-family="Inter, sans-serif" font-size="16" font-weight="700" fill="url(#goldGradient)" text-anchor="middle">
MENÜYÜ GÖRMEK İÇİN OKUTUN
</text>
<!-- Instruction note -->
<text x="200" y="505" font-family="Inter, sans-serif" font-size="11" font-weight="400" fill="#A1A1AA" text-anchor="middle">
Kameranızı QR Koda Odaklayın
</text>
<!-- WiFi & Social Media footer info -->
<text x="200" y="595" font-family="Inter, sans-serif" font-size="11" font-weight="400" fill="#A1A1AA" text-anchor="middle">
Ücretsiz Wi-Fi: GourmetBistro_Guest
</text>
<text x="200" y="610" font-family="Inter, sans-serif" font-size="11" font-weight="400" fill="#A1A1AA" text-anchor="middle">
Bizi Takip Edin: @GourmetBistro
</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

+285
View File
@@ -0,0 +1,285 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 400 650"
width="400"
height="650"
>
<g id="layer-background">
<rect
id="bg-canvas"
x="0"
y="0"
width="400"
height="650"
fill="#0B0E14"
/>
</g>
<g id="layer-card">
<!-- Ana Lüks Kart Arka Planı (Gece Yarısı Laciverti & Kömür) -->
<rect
id="main-card"
x="20"
y="20"
width="360"
height="610"
rx="24"
fill="#121721"
stroke="#D4AF37"
stroke-width="1.5"
/>
<!-- İç Çift Altın Bordür Hattı -->
<rect
id="inner-border"
x="28"
y="28"
width="344"
height="594"
rx="18"
fill="none"
stroke="#C5A059"
stroke-width="0.8"
stroke-opacity="0.4"
/>
<rect
x="32"
y="32"
width="336"
height="586"
rx="14"
fill="none"
stroke="#C5A059"
stroke-width="0.5"
stroke-opacity="0.2"
/>
</g>
<g id="layer-icons">
<!-- Üst Köşe Geometrik Art-Deco Motifleri -->
<path
d="M 38 48 L 48 38 M 38 38 L 52 38 M 38 38 L 38 52"
stroke="#D4AF37"
stroke-width="1"
stroke-linecap="round"
stroke-opacity="0.6"
fill="none"
/>
<path
d="M 362 48 L 352 38 M 362 38 L 348 38 M 362 38 L 362 52"
stroke="#D4AF37"
stroke-width="1"
stroke-linecap="round"
stroke-opacity="0.6"
fill="none"
/>
<!-- Üst Masa Numarası Kapsülü -->
<rect
x="148"
y="44"
width="104"
height="22"
rx="11"
fill="#1A2230"
stroke="#D4AF37"
stroke-width="0.8"
/>
<circle cx="158" cy="55" r="1.5" fill="#D4AF37" />
<circle cx="242" cy="55" r="1.5" fill="#D4AF37" />
<!-- Merkezi Gastronomi & Yıldız Amblemi -->
<g transform="translate(200, 84)">
<circle cx="0" cy="0" r="11" fill="none" stroke="#D4AF37" stroke-width="0.8" stroke-dasharray="2 2" />
<polygon points="0,-7 2,-2 7,0 2,2 0,7 -2,2 -7,0 -2,-2" fill="#D4AF37" />
</g>
<!-- Zarif Tipografik Ayırıcı -->
<line x1="75" y1="172" x2="175" y2="172" stroke="#D4AF37" stroke-width="0.75" stroke-opacity="0.5" />
<polygon points="200,169.5 202.5,172 200,174.5 197.5,172" fill="#D4AF37" />
<line x1="225" y1="172" x2="325" y2="172" stroke="#D4AF37" stroke-width="0.75" stroke-opacity="0.5" />
<!-- QR Alanı Hedef Odak Çerçeveleri -->
<path d="M 80 205 L 80 195 L 90 195" fill="none" stroke="#D4AF37" stroke-width="2" stroke-linecap="round" />
<path d="M 320 205 L 320 195 L 310 195" fill="none" stroke="#D4AF37" stroke-width="2" stroke-linecap="round" />
<path d="M 80 445 L 80 455 L 90 455" fill="none" stroke="#D4AF37" stroke-width="2" stroke-linecap="round" />
<path d="M 320 445 L 320 455 L 310 455" fill="none" stroke="#D4AF37" stroke-width="2" stroke-linecap="round" />
<!-- Alt Bilgi Kartı Paneli -->
<rect
x="36"
y="520"
width="328"
height="84"
rx="14"
fill="#171F2C"
stroke="#D4AF37"
stroke-width="0.8"
stroke-opacity="0.4"
/>
<line x1="200" y1="530" x2="200" y2="594" stroke="#D4AF37" stroke-width="0.5" stroke-opacity="0.25" />
<!-- Minimal WiFi Vektörü -->
<g transform="translate(62, 545)">
<path d="M 0 4 A 12 12 0 0 1 18 4" fill="none" stroke="#D4AF37" stroke-width="1.4" stroke-linecap="round" />
<path d="M 3.5 8 A 7 7 0 0 1 14.5 8" fill="none" stroke="#D4AF37" stroke-width="1.4" stroke-linecap="round" />
<circle cx="9" cy="12" r="1.2" fill="#D4AF37" />
</g>
<!-- Minimal Instagram / Web Vektörü -->
<g transform="translate(224, 545)">
<rect x="0" y="0" width="15" height="15" rx="4" fill="none" stroke="#D4AF37" stroke-width="1.4" />
<circle cx="7.5" cy="7.5" r="3.2" fill="none" stroke="#D4AF37" stroke-width="1.1" />
<circle cx="11.5" cy="3.5" r="0.75" fill="#D4AF37" />
</g>
</g>
<g id="qr-placeholder" transform="translate(90, 215)">
<rect
width="220"
height="220"
rx="20"
fill="#FFFFFF"
stroke="#E4E4E7"
stroke-width="2"
/>
<rect
x="12"
y="12"
width="196"
height="196"
rx="14"
fill="#FAFAFA"
stroke="#E4E4E7"
stroke-dasharray="4 3"
/>
<text
x="110"
y="115"
font-family="Inter, Arial, sans-serif"
font-size="12"
font-weight="600"
fill="#71717A"
text-anchor="middle"
>QR KOD ALANI</text>
</g>
<g id="layer-texts">
<!-- Masa Numarası -->
<text
x="200"
y="59"
font-family="Inter, Arial, sans-serif"
font-size="9"
font-weight="700"
letter-spacing="2.5"
fill="#E5C378"
text-anchor="middle"
>TABLE • 01</text>
<!-- Restoran Logosu & İsmi -->
<text
x="200"
y="126"
font-family="'Playfair Display', 'Cinzel', Georgia, serif"
font-size="28"
font-weight="700"
letter-spacing="1.5"
fill="#F9F6F0"
text-anchor="middle"
>LATELIER BISTRO</text>
<!-- Fransızca / Uluslararası Alt Başlık -->
<text
x="200"
y="152"
font-family="Inter, Arial, sans-serif"
font-size="8.5"
font-weight="600"
letter-spacing="4"
fill="#D4AF37"
text-anchor="middle"
>FINE DINING &amp; COCKTAILS</text>
<!-- Çağrı Metni / Talimat -->
<text
x="200"
y="480"
font-family="Inter, Arial, sans-serif"
font-size="11.5"
font-weight="700"
letter-spacing="2"
fill="#F9F6F0"
text-anchor="middle"
>DİJİTAL MENÜ İÇİN OKUTUN</text>
<text
x="200"
y="498"
font-family="'Playfair Display', Georgia, serif"
font-size="10.5"
font-style="italic"
font-weight="400"
letter-spacing="0.5"
fill="#94A3B8"
text-anchor="middle"
>Kameranızı QR koda yaklaştırınız</text>
<!-- Sol Sütun: Yüksek Hızlı WiFi Bilgileri -->
<text
x="126"
y="550"
font-family="Inter, Arial, sans-serif"
font-size="9"
font-weight="700"
letter-spacing="1.2"
fill="#E5C378"
text-anchor="middle"
>GUEST WIFI</text>
<text
x="126"
y="567"
font-family="Inter, Arial, sans-serif"
font-size="8.5"
font-weight="500"
fill="#CBD5E1"
text-anchor="middle"
>Ağ: Atelier-Guest</text>
<text
x="126"
y="582"
font-family="Inter, Arial, sans-serif"
font-size="8.5"
font-weight="500"
fill="#CBD5E1"
text-anchor="middle"
>Şifre: bistro2026</text>
<!-- Sağ Sütun: Sosyal Medya & Rezervasyon -->
<text
x="286"
y="550"
font-family="Inter, Arial, sans-serif"
font-size="8.5"
font-weight="700"
letter-spacing="1.2"
fill="#E5C378"
text-anchor="middle"
>SOSYAL MEDYA</text>
<text
x="286"
y="575"
font-family="Inter, Arial, sans-serif"
font-size="10.5"
font-weight="600"
letter-spacing="0.5"
fill="#F9F6F0"
text-anchor="middle"
>@latelier.bistro</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.1 KiB

+275
View File
@@ -0,0 +1,275 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 400 650"
width="400"
height="650"
>
<g id="layer-background">
<!-- Koyu Doku / Kontrast Arka Plan -->
<rect
id="bg-canvas"
x="0"
y="0"
width="400"
height="650"
fill="#252C36"
/>
</g>
<g id="layer-card">
<!-- Sıcak Parşömen / Vintage Krem Ana Kart -->
<rect
id="main-card"
x="20"
y="20"
width="360"
height="610"
rx="20"
fill="#F6EFE2"
stroke="#3A2312"
stroke-width="1.5"
/>
<!-- Çift İnce Vintage Çerçeve -->
<rect
x="28"
y="28"
width="344"
height="594"
rx="14"
fill="none"
stroke="#8C6239"
stroke-width="1"
stroke-opacity="0.4"
/>
<rect
x="32"
y="32"
width="336"
height="586"
rx="10"
fill="none"
stroke="#8C6239"
stroke-width="0.5"
stroke-opacity="0.25"
/>
</g>
<g id="layer-icons">
<!-- Sol Üst: Botanik Kahve / Çay Dalı -->
<g transform="translate(36, 36) scale(0.75)" stroke="#3A2312" stroke-width="1.4" fill="none" stroke-linecap="round">
<path d="M 0 0 C 15 15 25 35 30 65" />
<path d="M 10 12 C 5 2 20 -2 22 8 C 22 18 10 12 10 12 Z" fill="#EADCC7" />
<path d="M 18 27 C 12 20 28 14 32 23 C 32 32 18 27 18 27 Z" fill="#EADCC7" />
<path d="M 24 45 C 32 38 45 48 38 56 C 30 62 24 45 24 45 Z" fill="#EADCC7" />
</g>
<!-- Sağ Üst: Kuşbakışı Latte Art Fincanı -->
<g transform="translate(340, 52)">
<!-- Tabak -->
<circle cx="0" cy="0" r="32" fill="#EADCC7" stroke="#3A2312" stroke-width="1.2" />
<circle cx="0" cy="0" r="26" fill="#F6EFE2" stroke="#8C6239" stroke-width="0.8" stroke-dasharray="2 2" />
<!-- Fincan Kenarı & Kahve -->
<circle cx="0" cy="0" r="21" fill="#7A4B26" stroke="#3A2312" stroke-width="1.4" />
<!-- Latte Art / Rosetta Yaprakları -->
<path d="M 0 10 C -8 4 -4 -6 0 -12 C 4 -6 8 4 0 10 Z" fill="#F6EFE2" />
<path d="M 0 4 C -5 0 -2 -5 0 -8 C 2 -5 5 0 0 4 Z" fill="#7A4B26" />
<circle cx="0" cy="-14" r="1.8" fill="#F6EFE2" />
</g>
<!-- Sol Orta: Küçük Kahve Fincanı & Tabağı -->
<g transform="translate(48, 250)">
<ellipse cx="0" cy="0" rx="26" ry="26" fill="#EADCC7" stroke="#3A2312" stroke-width="1" />
<circle cx="0" cy="0" r="17" fill="#6A3B18" stroke="#3A2312" stroke-width="1.2" />
<!-- Basit Kalp Latte Deseni -->
<path d="M -5 -3 C -8 -7 0 -10 0 -4 C 0 -10 8 -7 5 -3 C 2 3 0 7 0 7 C 0 7 -2 3 -5 -3 Z" fill="#F6EFE2" />
<!-- Fincan Kulpu -->
<path d="M 17 -4 C 23 -4 23 4 17 4" fill="none" stroke="#3A2312" stroke-width="1.5" />
</g>
<!-- Sağ Orta: Dikey Yaprak İllüstrasyonu -->
<g transform="translate(345, 230) scale(0.7)" stroke="#3A2312" stroke-width="1.3" fill="none">
<path d="M 10 0 C 0 30 -5 70 5 110" />
<path d="M 6 25 C 18 15 28 28 14 36 C 6 36 6 25 6 25 Z" fill="#EADCC7" />
<path d="M 2 60 C -12 48 -20 62 -8 70 C 2 70 2 60 2 60 Z" fill="#EADCC7" />
<path d="M 5 88 C 18 78 24 92 12 100 C 5 100 5 88 5 88 Z" fill="#EADCC7" />
</g>
<!-- Üst Başlık Yan Süsleme Çizgileri -->
<path d="M 142 66 C 148 64 154 68 160 66" fill="none" stroke="#8C6239" stroke-width="1.2" stroke-linecap="round" />
<path d="M 240 66 C 246 68 252 64 258 66" fill="none" stroke="#8C6239" stroke-width="1.2" stroke-linecap="round" />
<!-- Sol Alt: Çekirdek ve Yaprak Grubu -->
<g transform="translate(42, 530) scale(0.65)" stroke="#3A2312" stroke-width="1.4" fill="none">
<path d="M 0 110 C 20 70 15 30 0 0" />
<path d="M 8 30 C 24 15 35 30 18 42 Z" fill="#EADCC7" />
<path d="M 12 70 C 30 55 40 72 20 84 Z" fill="#EADCC7" />
</g>
<!-- Kahve Çekirdekleri Motifleri -->
<!-- Sol Alt Çekirdek 1 -->
<g transform="translate(108, 595) rotate(-20) scale(0.8)">
<ellipse cx="0" cy="0" rx="9" ry="6.5" fill="#CBB296" stroke="#3A2312" stroke-width="1.2" />
<path d="M -7 0 Q 0 3 0 0 Q 0 -3 7 0" fill="none" stroke="#3A2312" stroke-width="1.2" />
</g>
<!-- Sol Alt Çekirdek 2 -->
<g transform="translate(128, 584) rotate(35) scale(0.8)">
<ellipse cx="0" cy="0" rx="9" ry="6.5" fill="#CBB296" stroke="#3A2312" stroke-width="1.2" />
<path d="M -7 0 Q 0 3 0 0 Q 0 -3 7 0" fill="none" stroke="#3A2312" stroke-width="1.2" />
</g>
<!-- Sağ Orta Çekirdek -->
<g transform="translate(336, 360) rotate(-45) scale(0.7)">
<ellipse cx="0" cy="0" rx="9" ry="6.5" fill="#CBB296" stroke="#3A2312" stroke-width="1.2" />
<path d="M -7 0 Q 0 3 0 0 Q 0 -3 7 0" fill="none" stroke="#3A2312" stroke-width="1.2" />
</g>
<!-- Sağ Alt: Sıcak Kahve Fincanı İllüstrasyonu -->
<g transform="translate(325, 590)">
<!-- Tabak -->
<ellipse cx="0" cy="18" rx="28" ry="7" fill="#EADCC7" stroke="#3A2312" stroke-width="1.2" />
<!-- Fincan Gövdesi -->
<path d="M -18 2 C -18 16 18 16 18 2 Z" fill="#F6EFE2" stroke="#3A2312" stroke-width="1.4" />
<ellipse cx="0" cy="2" rx="18" ry="4" fill="#6A3B18" stroke="#3A2312" stroke-width="1.2" />
<!-- Kulp -->
<path d="M 16 4 C 24 4 24 12 16 14" fill="none" stroke="#3A2312" stroke-width="1.3" />
<!-- Buhar Çizgileri -->
<path d="M -6 -4 C -8 -9 -4 -12 -6 -17" fill="none" stroke="#8C6239" stroke-width="1" stroke-linecap="round" />
<path d="M 2 -5 C 0 -10 4 -13 2 -19" fill="none" stroke="#8C6239" stroke-width="1" stroke-linecap="round" />
</g>
<!-- QR Alanı İnce Köşe Süsleri -->
<path d="M 80 205 C 80 195 80 195 90 195" fill="none" stroke="#3A2312" stroke-width="2" stroke-linecap="round" />
<path d="M 320 205 C 320 195 320 195 310 195" fill="none" stroke="#3A2312" stroke-width="2" stroke-linecap="round" />
<path d="M 80 445 C 80 455 80 455 90 455" fill="none" stroke="#3A2312" stroke-width="2" stroke-linecap="round" />
<path d="M 320 445 C 320 455 320 455 310 455" fill="none" stroke="#3A2312" stroke-width="2" stroke-linecap="round" />
<!-- Alt Est. Bölümü Çizgileri -->
<line x1="145" y1="608" x2="175" y2="608" stroke="#8C6239" stroke-width="0.8" />
<line x1="225" y1="608" x2="255" y2="608" stroke="#8C6239" stroke-width="0.8" />
</g>
<g id="qr-placeholder" transform="translate(90, 215)">
<!-- QR Kod Arka Planı (Hafif Sıcak Ton) -->
<rect
width="220"
height="220"
rx="20"
fill="#FBF8F2"
stroke="#D8C7B0"
stroke-width="1.5"
/>
<rect
x="10"
y="10"
width="200"
height="200"
rx="14"
fill="#F5EFE6"
stroke="#D8C7B0"
stroke-dasharray="4 3"
/>
<!-- QR Merkezine Kahve Çekirdeği İkonu Rozeti -->
<circle cx="110" cy="98" r="18" fill="#FBF8F2" stroke="#8C6239" stroke-width="1" />
<g transform="translate(110, 98) scale(0.65)">
<ellipse cx="-5" cy="2" rx="7" ry="5" fill="#3A2312" transform="rotate(-30)" />
<ellipse cx="5" cy="0" rx="7" ry="5" fill="#3A2312" transform="rotate(40)" />
</g>
<text
x="110"
y="132"
font-family="Inter, Arial, sans-serif"
font-size="11"
font-weight="700"
letter-spacing="1"
fill="#8C6239"
text-anchor="middle"
>QR KOD ALANI</text>
</g>
<g id="layer-texts">
<!-- Üst Başlık Takısı -->
<text
x="200"
y="70"
font-family="'Playfair Display', Georgia, serif"
font-size="14"
font-style="italic"
font-weight="700"
letter-spacing="4"
fill="#3A2312"
text-anchor="middle"
>THE</text>
<!-- Ana Tipografi: El Yazısı / Retro Tabela Havası -->
<text
x="200"
y="114"
font-family="'Playfair Display', 'Brush Script MT', 'Great Vibes', Georgia, cursive, serif"
font-size="34"
font-weight="800"
letter-spacing="0.5"
fill="#2C1A0E"
text-anchor="middle"
>The Daily Grind</text>
<!-- Alt Kategori Başlığı -->
<text
x="200"
y="146"
font-family="Inter, Arial, sans-serif"
font-size="13"
font-weight="800"
letter-spacing="3"
fill="#3A2312"
text-anchor="middle"
>COFFEE &amp; KITCHEN</text>
<!-- Büyük ve Belirgin Çağrı Metni -->
<text
x="200"
y="488"
font-family="Inter, Arial, sans-serif"
font-size="19"
font-weight="900"
letter-spacing="1.5"
fill="#2C1A0E"
text-anchor="middle"
>SCAN TO VIEW MENU</text>
<!-- Restoran & WiFi Bilgi Bandı -->
<text
x="200"
y="520"
font-family="Inter, Arial, sans-serif"
font-size="10.5"
font-weight="700"
letter-spacing="0.5"
fill="#3A2312"
text-anchor="middle"
>DAILY GRIND CAFE | WIFI: DAILY_GRIND_GUEST</text>
<!-- Şifre ve Masa Detayı -->
<text
x="200"
y="542"
font-family="Inter, Arial, sans-serif"
font-size="9"
font-weight="600"
letter-spacing="1"
fill="#8C6239"
text-anchor="middle"
>ŞİFRE: coffee2026 • MASA NO: 04</text>
<!-- Kuruluş / Est. Yılı -->
<text
x="200"
y="611"
font-family="'Playfair Display', Georgia, serif"
font-size="11"
font-style="italic"
font-weight="700"
letter-spacing="2"
fill="#3A2312"
text-anchor="middle"
>EST. 2018</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 666 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 516 KiB

+7
View File
@@ -15,6 +15,13 @@
"distribution": "internal",
"ios": {
"image": "latest"
},
"env": {
"EXPO_PUBLIC_SUPABASE_URL": "https://fwahdopsyvryvjaqande.supabase.co",
"EXPO_PUBLIC_SUPABASE_ANON_KEY": "sb_publishable_9YsK7YU7xfzxJQsz3x5iMA_LHCqUN3b",
"EXPO_PUBLIC_API_URL": "https://api.menul.io",
"EXPO_PUBLIC_WEB_URL": "https://menul.io",
"EXPO_PUBLIC_REVENUECAT_API_KEY": "appl_uQIvGUuvEthCNheZtcRfVixVhFy"
}
},
"production": {
+30
View File
@@ -0,0 +1,30 @@
# OSX
#
.DS_Store
# Xcode
#
build/
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata
*.xccheckout
*.moved-aside
DerivedData
*.hmap
*.ipa
*.xcuserstate
project.xcworkspace
.xcode.env.local
# Bundle artifacts
*.jsbundle
# CocoaPods
/Pods/
+11
View File
@@ -0,0 +1,11 @@
# This `.xcode.env` file is versioned and is used to source the environment
# used when running script phases inside Xcode.
# To customize your local environment, you can create an `.xcode.env.local`
# file that is not versioned.
# NODE_BINARY variable contains the PATH to the node executable.
#
# Customize the NODE_BINARY variable here.
# For example, to use nvm with brew, add the following line
# . "$(brew --prefix nvm)/nvm.sh" --no-use
export NODE_BINARY=$(command -v node)
@@ -0,0 +1,544 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 46;
objects = {
/* Begin PBXBuildFile section */
05BAC76EB2F44404BCDAFE0E /* noop-file.swift in Sources */ = {isa = PBXBuildFile; fileRef = 920486E7D8564009BA09A00A /* noop-file.swift */; };
13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.mm */; };
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; };
47FCCC6B41070FB7061A2F1B /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 4F738668B5B302410E775CFC /* PrivacyInfo.xcprivacy */; };
96905EF65AED1B983A6B3ABC /* libPods-Menulio.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 58EEBF8E8E6FB1BC6CAF49B5 /* libPods-Menulio.a */; };
B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */; };
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
13B07F961A680F5B00A75B9A /* Menulio.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Menulio.app; sourceTree = BUILT_PRODUCTS_DIR; };
13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = Menulio/AppDelegate.h; sourceTree = "<group>"; };
13B07FB01A68108700A75B9A /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = Menulio/AppDelegate.mm; sourceTree = "<group>"; };
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = Menulio/Images.xcassets; sourceTree = "<group>"; };
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Menulio/Info.plist; sourceTree = "<group>"; };
13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = Menulio/main.m; sourceTree = "<group>"; };
4F738668B5B302410E775CFC /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; name = PrivacyInfo.xcprivacy; path = Menulio/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
58EEBF8E8E6FB1BC6CAF49B5 /* libPods-Menulio.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Menulio.a"; sourceTree = BUILT_PRODUCTS_DIR; };
6C2E3173556A471DD304B334 /* Pods-Menulio.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Menulio.debug.xcconfig"; path = "Target Support Files/Pods-Menulio/Pods-Menulio.debug.xcconfig"; sourceTree = "<group>"; };
7A4D352CD337FB3A3BF06240 /* Pods-Menulio.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Menulio.release.xcconfig"; path = "Target Support Files/Pods-Menulio/Pods-Menulio.release.xcconfig"; sourceTree = "<group>"; };
920486E7D8564009BA09A00A /* noop-file.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = "noop-file.swift"; path = "Menulio/noop-file.swift"; sourceTree = "<group>"; };
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = Menulio/SplashScreen.storyboard; sourceTree = "<group>"; };
BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = "<group>"; };
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
F5CD4FEA57634883A4542F6A /* Menulio-Bridging-Header.h */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.h; name = "Menulio-Bridging-Header.h"; path = "Menulio/Menulio-Bridging-Header.h"; sourceTree = "<group>"; };
FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-Menulio/ExpoModulesProvider.swift"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
13B07F8C1A680F5B00A75B9A /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
96905EF65AED1B983A6B3ABC /* libPods-Menulio.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
13B07FAE1A68108700A75B9A /* Menulio */ = {
isa = PBXGroup;
children = (
BB2F792B24A3F905000567C9 /* Supporting */,
13B07FAF1A68108700A75B9A /* AppDelegate.h */,
13B07FB01A68108700A75B9A /* AppDelegate.mm */,
13B07FB51A68108700A75B9A /* Images.xcassets */,
13B07FB61A68108700A75B9A /* Info.plist */,
13B07FB71A68108700A75B9A /* main.m */,
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */,
920486E7D8564009BA09A00A /* noop-file.swift */,
F5CD4FEA57634883A4542F6A /* Menulio-Bridging-Header.h */,
4F738668B5B302410E775CFC /* PrivacyInfo.xcprivacy */,
);
name = Menulio;
sourceTree = "<group>";
};
2D16E6871FA4F8E400B85C8A /* Frameworks */ = {
isa = PBXGroup;
children = (
ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
58EEBF8E8E6FB1BC6CAF49B5 /* libPods-Menulio.a */,
);
name = Frameworks;
sourceTree = "<group>";
};
832341AE1AAA6A7D00B99B32 /* Libraries */ = {
isa = PBXGroup;
children = (
);
name = Libraries;
sourceTree = "<group>";
};
83CBB9F61A601CBA00E9B192 = {
isa = PBXGroup;
children = (
13B07FAE1A68108700A75B9A /* Menulio */,
832341AE1AAA6A7D00B99B32 /* Libraries */,
83CBBA001A601CBA00E9B192 /* Products */,
2D16E6871FA4F8E400B85C8A /* Frameworks */,
D65327D7A22EEC0BE12398D9 /* Pods */,
D7E4C46ADA2E9064B798F356 /* ExpoModulesProviders */,
);
indentWidth = 2;
sourceTree = "<group>";
tabWidth = 2;
usesTabs = 0;
};
83CBBA001A601CBA00E9B192 /* Products */ = {
isa = PBXGroup;
children = (
13B07F961A680F5B00A75B9A /* Menulio.app */,
);
name = Products;
sourceTree = "<group>";
};
92DBD88DE9BF7D494EA9DA96 /* Menulio */ = {
isa = PBXGroup;
children = (
FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */,
);
name = Menulio;
sourceTree = "<group>";
};
BB2F792B24A3F905000567C9 /* Supporting */ = {
isa = PBXGroup;
children = (
BB2F792C24A3F905000567C9 /* Expo.plist */,
);
name = Supporting;
path = Menulio/Supporting;
sourceTree = "<group>";
};
D65327D7A22EEC0BE12398D9 /* Pods */ = {
isa = PBXGroup;
children = (
6C2E3173556A471DD304B334 /* Pods-Menulio.debug.xcconfig */,
7A4D352CD337FB3A3BF06240 /* Pods-Menulio.release.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
};
D7E4C46ADA2E9064B798F356 /* ExpoModulesProviders */ = {
isa = PBXGroup;
children = (
92DBD88DE9BF7D494EA9DA96 /* Menulio */,
);
name = ExpoModulesProviders;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
13B07F861A680F5B00A75B9A /* Menulio */ = {
isa = PBXNativeTarget;
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Menulio" */;
buildPhases = (
08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */,
5C48BE09ECBB9F203B7F2ED2 /* [Expo] Configure project */,
13B07F871A680F5B00A75B9A /* Sources */,
13B07F8C1A680F5B00A75B9A /* Frameworks */,
13B07F8E1A680F5B00A75B9A /* Resources */,
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */,
EFB4C3DB6A514B0ADA511E60 /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = Menulio;
productName = Menulio;
productReference = 13B07F961A680F5B00A75B9A /* Menulio.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
83CBB9F71A601CBA00E9B192 /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 1130;
TargetAttributes = {
13B07F861A680F5B00A75B9A = {
LastSwiftMigration = 1250;
};
};
};
buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "Menulio" */;
compatibilityVersion = "Xcode 3.2";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 83CBB9F61A601CBA00E9B192;
productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
13B07F861A680F5B00A75B9A /* Menulio */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
13B07F8E1A680F5B00A75B9A /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */,
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */,
47FCCC6B41070FB7061A2F1B /* PrivacyInfo.xcprivacy in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Bundle React Native code and images";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "if [[ -f \"$PODS_ROOT/../.xcode.env\" ]]; then\n source \"$PODS_ROOT/../.xcode.env\"\nfi\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n# The project root by default is one level up from the ios directory\nexport PROJECT_ROOT=\"$PROJECT_DIR\"/..\n\nif [[ \"$CONFIGURATION\" = *Debug* ]]; then\n export SKIP_BUNDLING=1\nfi\nif [[ -z \"$ENTRY_FILE\" ]]; then\n # Set the entry JS file using the bundler's entry resolution.\n export ENTRY_FILE=\"$(\"$NODE_BINARY\" -e \"require('expo/scripts/resolveAppEntry')\" \"$PROJECT_ROOT\" ios absolute | tail -n 1)\"\nfi\n\nif [[ -z \"$CLI_PATH\" ]]; then\n # Use Expo CLI\n export CLI_PATH=\"$(\"$NODE_BINARY\" --print \"require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })\")\"\nfi\nif [[ -z \"$BUNDLE_COMMAND\" ]]; then\n # Default Expo CLI command for bundling\n export BUNDLE_COMMAND=\"export:embed\"\nfi\n\n# Source .xcode.env.updates if it exists to allow\n# SKIP_BUNDLING to be unset if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.updates\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.updates\"\nfi\n# Source local changes to allow overrides\n# if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n`\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'\"`\n\n";
};
08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Menulio-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
5C48BE09ECBB9F203B7F2ED2 /* [Expo] Configure project */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "[Expo] Configure project";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-Menulio/expo-configure-project.sh\"\n";
};
800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Menulio/Pods-Menulio-resources.sh",
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/PurchasesHybridCommon/PurchasesHybridCommon.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/RNCAsyncStorage/RNCAsyncStorage_resources.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/RevenueCat/RevenueCat.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/glog/glog_privacy.bundle",
);
name = "[CP] Copy Pods Resources";
outputPaths = (
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/PurchasesHybridCommon.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNCAsyncStorage_resources.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RevenueCat.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/boost_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/glog_privacy.bundle",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Menulio/Pods-Menulio-resources.sh\"\n";
showEnvVarsInLog = 0;
};
EFB4C3DB6A514B0ADA511E60 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Menulio/Pods-Menulio-frameworks.sh",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes",
);
name = "[CP] Embed Pods Frameworks";
outputPaths = (
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Menulio/Pods-Menulio-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
13B07F871A680F5B00A75B9A /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */,
13B07FC11A68108700A75B9A /* main.m in Sources */,
B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */,
05BAC76EB2F44404BCDAFE0E /* noop-file.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
13B07F941A680F5B00A75B9A /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 6C2E3173556A471DD304B334 /* Pods-Menulio.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Menulio/Menulio.entitlements;
CURRENT_PROJECT_VERSION = 21;
ENABLE_BITCODE = NO;
GCC_PREPROCESSOR_DEFINITIONS = (
"$(inherited)",
"FB_SONARKIT_ENABLED=1",
);
INFOPLIST_FILE = Menulio/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.0;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
"-lc++",
);
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
PRODUCT_BUNDLE_IDENTIFIER = com.ayristech.menulio;
PRODUCT_NAME = Menulio;
SWIFT_OBJC_BRIDGING_HEADER = "Menulio/Menulio-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
13B07F951A680F5B00A75B9A /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7A4D352CD337FB3A3BF06240 /* Pods-Menulio.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Menulio/Menulio.entitlements;
CURRENT_PROJECT_VERSION = 21;
INFOPLIST_FILE = Menulio/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.0;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
"-lc++",
);
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.ayristech.menulio;
PRODUCT_NAME = Menulio;
SWIFT_OBJC_BRIDGING_HEADER = "Menulio/Menulio-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
83CBBA201A601CBA00E9B192 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_CXX_LANGUAGE_STANDARD = "c++20";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_SYMBOLS_PRIVATE_EXTERN = NO;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)";
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
OTHER_LDFLAGS = (
"$(inherited)",
" ",
);
REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../node_modules/.pnpm/react-native@0.76.9_@babel+core@7.29.7_supports-color@8.1.1__@babel+preset-env@7.29.7_@_10f0e3ffd93936a63eed56e417b084b2/node_modules/react-native";
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
USE_HERMES = true;
};
name = Debug;
};
83CBBA211A601CBA00E9B192 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_CXX_LANGUAGE_STANDARD = "c++20";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = YES;
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)";
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
MTL_ENABLE_DEBUG_INFO = NO;
OTHER_LDFLAGS = (
"$(inherited)",
" ",
);
REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../node_modules/.pnpm/react-native@0.76.9_@babel+core@7.29.7_supports-color@8.1.1__@babel+preset-env@7.29.7_@_10f0e3ffd93936a63eed56e417b084b2/node_modules/react-native";
SDKROOT = iphoneos;
USE_HERMES = true;
VALIDATE_PRODUCT = YES;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Menulio" */ = {
isa = XCConfigurationList;
buildConfigurations = (
13B07F941A680F5B00A75B9A /* Debug */,
13B07F951A680F5B00A75B9A /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "Menulio" */ = {
isa = XCConfigurationList;
buildConfigurations = (
83CBBA201A601CBA00E9B192 /* Debug */,
83CBBA211A601CBA00E9B192 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */;
}
@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1130"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
BuildableName = "Menulio.app"
BlueprintName = "Menulio"
ReferencedContainer = "container:Menulio.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "00E356ED1AD99517003FC87E"
BuildableName = "MenulioTests.xctest"
BlueprintName = "MenulioTests"
ReferencedContainer = "container:Menulio.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
BuildableName = "Menulio.app"
BlueprintName = "Menulio"
ReferencedContainer = "container:Menulio.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
BuildableName = "Menulio.app"
BlueprintName = "Menulio"
ReferencedContainer = "container:Menulio.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Menulio.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>
+7
View File
@@ -0,0 +1,7 @@
#import <RCTAppDelegate.h>
#import <UIKit/UIKit.h>
#import <Expo/Expo.h>
@interface AppDelegate : EXAppDelegateWrapper
@end
+62
View File
@@ -0,0 +1,62 @@
#import "AppDelegate.h"
#import <React/RCTBundleURLProvider.h>
#import <React/RCTLinkingManager.h>
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.moduleName = @"main";
// You can add your custom initial props in the dictionary below.
// They will be passed down to the ViewController used by React Native.
self.initialProps = @{};
return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
{
return [self bundleURL];
}
- (NSURL *)bundleURL
{
#if DEBUG
return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@".expo/.virtual-metro-entry"];
#else
return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
#endif
}
// Linking API
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
return [super application:application openURL:url options:options] || [RCTLinkingManager application:application openURL:url options:options];
}
// Universal Links
- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler {
BOOL result = [RCTLinkingManager application:application continueUserActivity:userActivity restorationHandler:restorationHandler];
return [super application:application continueUserActivity:userActivity restorationHandler:restorationHandler] || result;
}
// Explicitly define remote notification delegates to ensure compatibility with some third-party libraries
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
{
return [super application:application didRegisterForRemoteNotificationsWithDeviceToken:deviceToken];
}
// Explicitly define remote notification delegates to ensure compatibility with some third-party libraries
- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error
{
return [super application:application didFailToRegisterForRemoteNotificationsWithError:error];
}
// Explicitly define remote notification delegates to ensure compatibility with some third-party libraries
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
return [super application:application didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler];
}
@end
Binary file not shown.

After

Width:  |  Height:  |  Size: 546 KiB

@@ -0,0 +1,14 @@
{
"images": [
{
"filename": "App-Icon-1024x1024@1x.png",
"idiom": "universal",
"platform": "ios",
"size": "1024x1024"
}
],
"info": {
"version": 1,
"author": "expo"
}
}
@@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "expo"
}
}
@@ -0,0 +1,20 @@
{
"colors": [
{
"color": {
"components": {
"alpha": "1.000",
"blue": "0.0666666666666667",
"green": "0.0509803921568627",
"red": "0.0352941176470588"
},
"color-space": "srgb"
},
"idiom": "universal"
}
],
"info": {
"version": 1,
"author": "expo"
}
}
@@ -0,0 +1,23 @@
{
"images": [
{
"idiom": "universal",
"filename": "image.png",
"scale": "1x"
},
{
"idiom": "universal",
"filename": "image@2x.png",
"scale": "2x"
},
{
"idiom": "universal",
"filename": "image@3x.png",
"scale": "3x"
}
],
"info": {
"version": 1,
"author": "expo"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 489 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 489 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 489 KiB

+87
View File
@@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Menulio</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>0.0.1</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>menulio</string>
<string>com.ayristech.menulio</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>21</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSMinimumSystemVersion</key>
<string>12.0</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<false/>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
<key>NSCameraUsageDescription</key>
<string>Menünüzün fotoğrafını çekerek AI ile dijitalleştirmek için kamera izni gereklidir.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to access your microphone</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Menü fotoğraflarınızı ve restoran logonuzu yüklemek için galeri izni gereklidir.</string>
<key>NSUserActivityTypes</key>
<array>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
</array>
<key>UILaunchStoryboardName</key>
<string>SplashScreen</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UIRequiresFullScreen</key>
<false/>
<key>UIStatusBarStyle</key>
<string>UIStatusBarStyleDefault</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIUserInterfaceStyle</key>
<string>Automatic</string>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</plist>
@@ -0,0 +1,3 @@
//
// Use this file to import your target's public headers that you would like to expose to Swift.
//
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>
@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>0A2A.1</string>
<string>3B52.1</string>
<string>C617.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>E174.1</string>
<string>85F4.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>35F9.1</string>
</array>
</dict>
</array>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
<key>NSPrivacyTracking</key>
<false/>
</dict>
</plist>
@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="EXPO-VIEWCONTROLLER-1">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22685"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<scene sceneID="EXPO-SCENE-1">
<objects>
<viewController storyboardIdentifier="SplashScreenViewController" id="EXPO-VIEWCONTROLLER-1" sceneMemberID="viewController">
<view key="view" userInteractionEnabled="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="EXPO-ContainerView" userLabel="ContainerView">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<imageView id="EXPO-SplashScreen" userLabel="SplashScreenLogo" image="SplashScreenLogo" contentMode="scaleAspectFit" clipsSubviews="true" userInteractionEnabled="false" translatesAutoresizingMaskIntoConstraints="false">
<rect key="frame" x="0" y="0" width="414" height="736"/>
</imageView>
</subviews>
<viewLayoutGuide key="safeArea" id="Rmq-lb-GrQ"/>
<constraints>
<constraint firstItem="EXPO-SplashScreen" firstAttribute="top" secondItem="EXPO-ContainerView" secondAttribute="top" id="83fcb9b545b870ba44c24f0feeb116490c499c52"/>
<constraint firstItem="EXPO-SplashScreen" firstAttribute="leading" secondItem="EXPO-ContainerView" secondAttribute="leading" id="61d16215e44b98e39d0a2c74fdbfaaa22601b12c"/>
<constraint firstItem="EXPO-SplashScreen" firstAttribute="trailing" secondItem="EXPO-ContainerView" secondAttribute="trailing" id="f934da460e9ab5acae3ad9987d5b676a108796c1"/>
<constraint firstItem="EXPO-SplashScreen" firstAttribute="bottom" secondItem="EXPO-ContainerView" secondAttribute="bottom" id="d6a0be88096b36fb132659aa90203d39139deda9"/>
</constraints>
<color key="backgroundColor" name="SplashScreenBackground"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="EXPO-PLACEHOLDER-1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="0.0" y="0.0"/>
</scene>
</scenes>
<resources>
<image name="SplashScreenLogo" width="414" height="736"/>
<namedColor name="SplashScreenBackground">
<color alpha="1.000" blue="0.0666666666666667" green="0.0509803921568627" red="0.0352941176470588" customColorSpace="sRGB" colorSpace="custom"/>
</namedColor>
</resources>
</document>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>EXUpdatesCheckOnLaunch</key>
<string>ALWAYS</string>
<key>EXUpdatesEnabled</key>
<false/>
<key>EXUpdatesLaunchWaitMs</key>
<integer>0</integer>
</dict>
</plist>
+10
View File
@@ -0,0 +1,10 @@
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
+4
View File
@@ -0,0 +1,4 @@
//
// @generated
// A blank Swift file must be created for native modules with Swift files to work correctly.
//
+66
View File
@@ -0,0 +1,66 @@
require File.join(File.dirname(`node --print "require.resolve('expo/package.json')"`), "scripts/autolinking")
require File.join(File.dirname(`node --print "require.resolve('react-native/package.json')"`), "scripts/react_native_pods")
require 'json'
podfile_properties = JSON.parse(File.read(File.join(__dir__, 'Podfile.properties.json'))) rescue {}
ENV['RCT_NEW_ARCH_ENABLED'] = podfile_properties['newArchEnabled'] == 'true' ? '1' : '0'
ENV['EX_DEV_CLIENT_NETWORK_INSPECTOR'] = podfile_properties['EX_DEV_CLIENT_NETWORK_INSPECTOR']
platform :ios, podfile_properties['ios.deploymentTarget'] || '15.1'
install! 'cocoapods',
:deterministic_uuids => false
prepare_react_native_project!
target 'Menulio' do
use_expo_modules!
if ENV['EXPO_USE_COMMUNITY_AUTOLINKING'] == '1'
config_command = ['node', '-e', "process.argv=['', '', 'config'];require('@react-native-community/cli').run()"];
else
config_command = [
'node',
'--no-warnings',
'--eval',
'require(require.resolve(\'expo-modules-autolinking\', { paths: [require.resolve(\'expo/package.json\')] }))(process.argv.slice(1))',
'react-native-config',
'--json',
'--platform',
'ios'
]
end
config = use_native_modules!(config_command)
use_frameworks! :linkage => podfile_properties['ios.useFrameworks'].to_sym if podfile_properties['ios.useFrameworks']
use_frameworks! :linkage => ENV['USE_FRAMEWORKS'].to_sym if ENV['USE_FRAMEWORKS']
use_react_native!(
:path => config[:reactNativePath],
:hermes_enabled => podfile_properties['expo.jsEngine'] == nil || podfile_properties['expo.jsEngine'] == 'hermes',
# An absolute path to your application root.
:app_path => "#{Pod::Config.instance.installation_root}/..",
:privacy_file_aggregation_enabled => podfile_properties['apple.privacyManifestAggregationEnabled'] != 'false',
)
post_install do |installer|
react_native_post_install(
installer,
config[:reactNativePath],
:mac_catalyst_enabled => false,
:ccache_enabled => podfile_properties['apple.ccacheEnabled'] == 'true',
)
# This is necessary for Xcode 14, because it signs resource bundles by default
# when building for devices.
installer.target_installation_results.pod_target_installation_results
.each do |pod_name, target_installation_result|
target_installation_result.resource_bundle_targets.each do |resource_bundle_target|
resource_bundle_target.build_configurations.each do |config|
config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO'
end
end
end
end
end
File diff suppressed because it is too large Load Diff
+5
View File
@@ -0,0 +1,5 @@
{
"expo.jsEngine": "hermes",
"EX_DEV_CLIENT_NETWORK_INSPECTOR": "true",
"newArchEnabled": "true"
}
+4 -3
View File
@@ -25,9 +25,9 @@
"expo-haptics": "^14.0.1",
"expo-image-picker": "~16.0.6",
"expo-linking": "~7.0.5",
"expo-print": "^57.0.1",
"expo-print": "~14.0.3",
"expo-router": "~4.0.0",
"expo-sharing": "^57.0.13",
"expo-sharing": "~13.0.1",
"expo-splash-screen": "~0.29.24",
"expo-status-bar": "~2.0.0",
"react": "18.3.1",
@@ -35,8 +35,9 @@
"react-native-purchases": "^8.2.0",
"react-native-safe-area-context": "4.12.0",
"react-native-screens": "~4.4.0",
"react-native-svg": "15.8.0",
"react-native-url-polyfill": "^2.0.0",
"react-native-view-shot": "^5.1.1"
"react-native-view-shot": "~4.0.3"
},
"devDependencies": {
"@babel/core": "^7.25.2",
+1 -1
View File
@@ -4,7 +4,7 @@ import { StatusBar } from "expo-status-bar";
export default function RootLayout() {
return (
<>
<StatusBar style="dark" />
<StatusBar style="light" />
<Stack screenOptions={{ headerShown: false }} />
</>
);
+224
View File
@@ -26,10 +26,20 @@ interface RestaurantDetail {
name: string;
slug: string;
logo_url: string | null;
cover_url: string | null;
phone: string | null;
address: string | null;
enabled_languages?: string[];
}
const SUPPORTED_LANGUAGES: { code: string; label: string; flag: string }[] = [
{ code: "en", label: "İngilizce", flag: "🇬🇧" },
{ code: "de", label: "Almanca", flag: "🇩🇪" },
{ code: "ar", label: "Arapça", flag: "🇸🇦" },
{ code: "ru", label: "Rusça", flag: "🇷🇺" },
{ code: "fr", label: "Fransızca", flag: "🇫🇷" },
];
export interface TxtRecord {
name: string;
value: string;
@@ -79,6 +89,7 @@ export default function AccountScreen() {
const [phone, setPhone] = useState("");
const [address, setAddress] = useState("");
const [logoUrl, setLogoUrl] = useState<string | null>(null);
const [coverUrl, setCoverUrl] = useState<string | null>(null);
const [slug, setSlug] = useState("");
const [savingRest, setSavingRest] = useState(false);
const [deletingRestaurant, setDeletingRestaurant] = useState(false);
@@ -87,6 +98,10 @@ export default function AccountScreen() {
const [selectedTheme, setSelectedTheme] = useState("elegant");
const [savingTheme, setSavingTheme] = useState(false);
// Language / AI Translation state
const [enabledLanguages, setEnabledLanguages] = useState<string[]>(["tr"]);
const [updatingLanguage, setUpdatingLanguage] = useState<string | null>(null);
// Custom Domain state
const [domains, setDomains] = useState<DomainRow[]>([]);
const [newDomainInput, setNewDomainInput] = useState("");
@@ -117,7 +132,9 @@ export default function AccountScreen() {
setPhone(rest.phone ?? "");
setAddress(rest.address ?? "");
setLogoUrl(rest.logo_url ?? null);
setCoverUrl(rest.cover_url ?? null);
setSlug(rest.slug);
setEnabledLanguages(rest.enabled_languages ?? ["tr"]);
// Fetch theme
const themeRes = await api.get<{ themeKey: string }>(`/restaurants/${cached.restaurantId}/theme`).catch(() => ({ themeKey: "elegant" }));
@@ -169,6 +186,29 @@ export default function AccountScreen() {
}
}
async function handlePickCover() {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light).catch(() => {});
const permission = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (!permission.granted) {
Alert.alert("İzin Gerekli", "Kapak fotoğrafı seçebilmek için fotoğraf galerisi izni gereklidir.");
return;
}
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ["images"],
allowsEditing: true,
aspect: [16, 9],
quality: 0.6,
base64: true,
});
if (!result.canceled && result.assets[0]) {
const asset = result.assets[0];
const newCover = asset.base64 ? `data:image/jpeg;base64,${asset.base64}` : asset.uri;
setCoverUrl(newCover);
}
}
async function handleSaveRestaurant() {
if (!active || !name.trim()) return;
setSavingRest(true);
@@ -179,6 +219,7 @@ export default function AccountScreen() {
phone: phone.trim() || null,
address: address.trim() || null,
logoUrl: logoUrl || null,
coverUrl: coverUrl || null,
slug: slug.trim() || undefined,
});
@@ -214,6 +255,54 @@ export default function AccountScreen() {
}
}
async function handleAddLanguage(code: string, label: string) {
if (!active || updatingLanguage) return;
setUpdatingLanguage(code);
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium).catch(() => {});
try {
const res = await api.post<{ enabledLanguages: string[] }>(
`/restaurants/${active.restaurantId}/languages`,
{ code },
);
setEnabledLanguages(res.enabledLanguages);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success).catch(() => {});
Alert.alert("Menü Çevrildi 🎉", `Menünüz yapay zeka ile ${label} diline çevrildi.`);
} catch (err) {
Alert.alert("Hata", err instanceof Error ? err.message : "Dil eklenemedi.");
} finally {
setUpdatingLanguage(null);
}
}
async function handleRemoveLanguage(code: string, label: string) {
if (!active || updatingLanguage) return;
Alert.alert(
`${label} Dilini Kaldır`,
"Bu dil menüde artık gösterilmeyecek. Çeviriler saklanacağı için dili tekrar eklerseniz yeniden çevrilmesi gerekmez.",
[
{ text: "Vazgeç", style: "cancel" },
{
text: "Kaldır",
style: "destructive",
onPress: async () => {
setUpdatingLanguage(code);
try {
const res = await api.delete<{ enabledLanguages: string[] }>(
`/restaurants/${active.restaurantId}/languages/${code}`,
);
setEnabledLanguages(res.enabledLanguages);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success).catch(() => {});
} catch (err) {
Alert.alert("Hata", err instanceof Error ? err.message : "Dil kaldırılamadı.");
} finally {
setUpdatingLanguage(null);
}
},
},
],
);
}
async function handleAddCustomDomain() {
if (!active || !newDomainInput.trim()) {
Alert.alert("Eksik Bilgi", "Lütfen bağlamak istediğiniz alan adını girin (Örn: menu.kebapciahmet.com)");
@@ -605,6 +694,94 @@ export default function AccountScreen() {
</View>
</View>
{/* Language / AI Translation Section */}
<View
style={{
backgroundColor: "#FFFFFF",
borderRadius: 20,
padding: 20,
borderWidth: 1,
borderColor: "#E7E5E4",
gap: 14,
}}
>
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
<Ionicons name="language-outline" size={20} color="#C8A96B" />
<Text style={{ fontSize: 16, fontWeight: "800", color: "#1C1917" }}>
Diller (Yapay Zeka Çevirisi)
</Text>
</View>
<Text style={{ fontSize: 12, color: "#78716C", lineHeight: 18 }}>
Menünüzü yapay zeka ile otomatik olarak çevirin. Müşterileriniz menüde istedikleri dili seçebilir.
</Text>
<View style={{ flexDirection: "row", flexWrap: "wrap", gap: 8 }}>
{/* Turkish — always on, base language */}
<View
style={{
flexDirection: "row",
alignItems: "center",
gap: 6,
backgroundColor: "#1C1917",
paddingHorizontal: 12,
paddingVertical: 8,
borderRadius: 10,
}}
>
<Text style={{ fontSize: 13 }}>🇹🇷</Text>
<Text style={{ fontSize: 12, fontWeight: "700", color: "#FFFFFF" }}>Türkçe</Text>
</View>
{SUPPORTED_LANGUAGES.map((lang) => {
const isEnabled = enabledLanguages.includes(lang.code);
const isUpdating = updatingLanguage === lang.code;
return (
<Pressable
key={lang.code}
onPress={() =>
isEnabled
? handleRemoveLanguage(lang.code, lang.label)
: handleAddLanguage(lang.code, lang.label)
}
disabled={isUpdating}
style={{
flexDirection: "row",
alignItems: "center",
gap: 6,
backgroundColor: isEnabled ? "#FDF8F0" : "#FAF8F5",
borderWidth: 1.5,
borderColor: isEnabled ? "#C8A96B" : "#E7E5E4",
paddingHorizontal: 12,
paddingVertical: 8,
borderRadius: 10,
opacity: isUpdating ? 0.6 : 1,
}}
>
{isUpdating ? (
<ActivityIndicator size="small" color="#C8A96B" />
) : (
<Text style={{ fontSize: 13 }}>{lang.flag}</Text>
)}
<Text
style={{
fontSize: 12,
fontWeight: "700",
color: isEnabled ? "#926E27" : "#57534E",
}}
>
{lang.label}
</Text>
{isEnabled ? (
<Ionicons name="close-circle" size={14} color="#C8A96B" />
) : (
<Ionicons name="add-circle-outline" size={14} color="#A8A29E" />
)}
</Pressable>
);
})}
</View>
</View>
{/* Custom Domain Section */}
<View
style={{
@@ -902,6 +1079,53 @@ export default function AccountScreen() {
</Text>
</View>
{/* Cover Photo Picker */}
<View>
<Text style={{ fontSize: 12, fontWeight: "700", color: "#78716C", marginBottom: 6 }}>
Kapak Fotoğrafı (Menü Üst Görseli)
</Text>
<Pressable
onPress={handlePickCover}
style={{
width: "100%",
aspectRatio: 16 / 9,
borderRadius: 14,
overflow: "hidden",
backgroundColor: "#1C1917",
borderWidth: 1.5,
borderColor: "#D6D3D1",
alignItems: "center",
justifyContent: "center",
}}
>
{coverUrl ? (
<Image source={{ uri: coverUrl }} style={{ width: "100%", height: "100%" }} resizeMode="cover" />
) : (
<View style={{ alignItems: "center", gap: 6 }}>
<Ionicons name="image-outline" size={28} color="#C8A96B" />
<Text style={{ color: "#C8A96B", fontSize: 12, fontWeight: "700" }}>
Kapak Fotoğrafı Ekle
</Text>
</View>
)}
<View
style={{
position: "absolute",
bottom: 0,
left: 0,
right: 0,
backgroundColor: "rgba(0,0,0,0.6)",
alignItems: "center",
paddingVertical: 6,
}}
>
<Text style={{ color: "#FFFFFF", fontSize: 11, fontWeight: "700" }}>
📷 {coverUrl ? "Değiştir" : "Fotoğraf Seç"}
</Text>
</View>
</Pressable>
</View>
{/* Restaurant Name */}
<View>
<Text style={{ fontSize: 12, fontWeight: "700", color: "#78716C", marginBottom: 6 }}>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,296 @@
import React, { useRef } from "react";
import {
PanResponder,
Text,
View,
} from "react-native";
import * as Haptics from "expo-haptics";
import { type CanvasTextLayer, getCanvasFontFamily } from "./types";
interface CanvasLayerItemProps {
layer: CanvasTextLayer;
isSelected: boolean;
onSelect: (id: string) => void;
onEdit: (layer: CanvasTextLayer) => void;
onUpdatePosition: (id: string, x: number, y: number) => void;
onDelete: (id: string) => void;
onDragStart?: () => void;
onDragMove?: (rawX: number, rawY: number, itemWidth: number, itemHeight: number) => { snappedX: number; snappedY: number };
onDragEnd?: (finalX: number, finalY: number, itemWidth: number, itemHeight: number) => void;
canvasWidth: number;
canvasHeight: number;
}
export const CurvedTextNative: React.FC<{
text: string;
fontSize: number;
color: string;
fontFamily: string;
fontWeight?: any;
letterSpacing?: number;
curveDegree: number;
hasShadow?: boolean;
}> = ({
text,
fontSize,
color,
fontFamily,
fontWeight = "800",
letterSpacing = 0.5,
curveDegree,
hasShadow,
}) => {
const isUp = curveDegree > 0;
const absDegree = Math.abs(curveDegree);
const radius = Math.max(40, 420 - absDegree * 3.5);
const charWidth = fontSize * 0.52 + (letterSpacing || 0);
const numChars = text.length;
const arcLength = numChars * charWidth;
const totalAngleRad = arcLength / radius;
const stepRad = numChars > 1 ? totalAngleRad / (numChars - 1) : 0;
const startAngleRad = -totalAngleRad / 2;
const maxOffsetY = Math.abs(-radius * (Math.cos(totalAngleRad / 2) - 1));
const containerHeight = fontSize + maxOffsetY + 12;
const containerWidth = Math.max(numChars * charWidth, radius * 2 * Math.sin(totalAngleRad / 2)) + 16;
return (
<View style={{ width: containerWidth, height: containerHeight, alignItems: "center", justifyContent: "center" }}>
{text.split("").map((ch, i) => {
const angleRad = startAngleRad + i * stepRad;
const angleDeg = angleRad * (180 / Math.PI);
const charX = radius * Math.sin(angleRad);
const charY = isUp
? -radius * (Math.cos(angleRad) - 1)
: radius * (Math.cos(angleRad) - 1);
const charRotate = isUp ? angleDeg : -angleDeg;
return (
<Text
key={i}
style={{
position: "absolute",
fontSize,
color,
fontFamily,
fontWeight,
letterSpacing,
textShadowColor: hasShadow ? "rgba(0,0,0,0.8)" : "transparent",
textShadowOffset: { width: 0, height: 1 },
textShadowRadius: 3,
transform: [
{ translateX: charX },
{ translateY: charY },
{ rotate: `${charRotate}deg` },
],
}}
>
{ch === " " ? "\u00A0" : ch}
</Text>
);
})}
</View>
);
};
export const CanvasLayerItem: React.FC<CanvasLayerItemProps> = ({
layer,
isSelected,
onSelect,
onEdit,
onUpdatePosition,
onDelete,
onDragStart,
onDragMove,
onDragEnd,
canvasWidth,
canvasHeight,
}) => {
const layerRef = useRef(layer);
layerRef.current = layer;
const canvasWidthRef = useRef(canvasWidth);
canvasWidthRef.current = canvasWidth;
const canvasHeightRef = useRef(canvasHeight);
canvasHeightRef.current = canvasHeight;
const panStartRef = useRef({ x: layer.x, y: layer.y });
const layoutSizeRef = useRef({ width: 70, height: 26 });
const currentPosRef = useRef({ x: layer.x, y: layer.y });
const lastTapRef = useRef<number>(0);
const isDraggingRef = useRef(false);
const panResponder = useRef(
PanResponder.create({
onStartShouldSetPanResponder: () => true,
onStartShouldSetPanResponderCapture: () => true,
onMoveShouldSetPanResponder: (_evt, gestureState) =>
Math.abs(gestureState.dx) > 1 || Math.abs(gestureState.dy) > 1,
onMoveShouldSetPanResponderCapture: (_evt, gestureState) =>
Math.abs(gestureState.dx) > 1 || Math.abs(gestureState.dy) > 1,
onPanResponderTerminationRequest: () => false,
onPanResponderGrant: () => {
const now = Date.now();
const DOUBLE_TAP_DELAY = 300;
const curLayer = layerRef.current;
if (now - lastTapRef.current < DOUBLE_TAP_DELAY) {
try {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
} catch {}
onEdit(curLayer);
} else {
onSelect(curLayer.id);
}
lastTapRef.current = now;
panStartRef.current = { x: curLayer.x, y: curLayer.y };
currentPosRef.current = { x: curLayer.x, y: curLayer.y };
isDraggingRef.current = false;
onDragStart?.();
},
onPanResponderMove: (_evt, gestureState) => {
if (!isDraggingRef.current && (Math.abs(gestureState.dx) > 2 || Math.abs(gestureState.dy) > 2)) {
isDraggingRef.current = true;
}
const curLayer = layerRef.current;
const itemW = layoutSizeRef.current.width;
const itemH = layoutSizeRef.current.height;
const cWidth = canvasWidthRef.current;
const cHeight = canvasHeightRef.current;
const maxX = Math.max(0, cWidth - itemW);
const maxY = Math.max(0, cHeight - itemH);
const rawX = Math.max(0, Math.min(maxX, panStartRef.current.x + gestureState.dx));
const rawY = Math.max(0, Math.min(maxY, panStartRef.current.y + gestureState.dy));
if (onDragMove) {
const { snappedX, snappedY } = onDragMove(rawX, rawY, itemW, itemH);
currentPosRef.current = { x: snappedX, y: snappedY };
onUpdatePosition(curLayer.id, snappedX, snappedY);
} else {
const sx = Math.round(rawX);
const sy = Math.round(rawY);
currentPosRef.current = { x: sx, y: sy };
onUpdatePosition(curLayer.id, sx, sy);
}
},
onPanResponderRelease: () => {
const itemW = layoutSizeRef.current.width;
const itemH = layoutSizeRef.current.height;
onDragEnd?.(currentPosRef.current.x, currentPosRef.current.y, itemW, itemH);
},
onPanResponderTerminate: () => {
const itemW = layoutSizeRef.current.width;
const itemH = layoutSizeRef.current.height;
onDragEnd?.(currentPosRef.current.x, currentPosRef.current.y, itemW, itemH);
},
})
).current;
// Instagram Badge arka plan stili
const renderBackgroundStyle = () => {
switch (layer.bgStyle) {
case "solid":
return {
backgroundColor: layer.bgBoxColor || "rgba(18, 18, 21, 0.92)",
paddingHorizontal: 12,
paddingVertical: 5,
borderRadius: 8,
};
case "soft":
return {
backgroundColor: "rgba(255, 255, 255, 0.18)",
paddingHorizontal: 12,
paddingVertical: 5,
borderRadius: 8,
borderWidth: 1,
borderColor: "rgba(255, 255, 255, 0.25)",
};
case "outline":
return {
backgroundColor: "transparent",
paddingHorizontal: 10,
paddingVertical: 4,
borderRadius: 8,
borderWidth: 1.5,
borderColor: layer.color || "#D4AF37",
};
case "none":
default:
return {
backgroundColor: "transparent",
paddingHorizontal: 4,
paddingVertical: 2,
};
}
};
return (
<View
{...panResponder.panHandlers}
accessible={true}
accessibilityRole="button"
accessibilityLabel={`Metin: ${layer.text}`}
accessibilityHint="Metni düzenlemek için çift dokunun, taşımak için sürükleyin"
accessibilityState={{ selected: isSelected }}
onLayout={(e) => {
const { width, height } = e.nativeEvent.layout;
layoutSizeRef.current = { width, height };
}}
style={{
position: "absolute",
left: layer.x,
top: layer.y,
transform: [{ rotate: `${layer.rotation || 0}deg` }],
zIndex: isSelected ? 90 : 20,
}}
>
<View
style={{
borderWidth: isSelected ? 1.5 : 0,
borderColor: isSelected ? "#D4AF37" : "transparent",
borderStyle: isSelected ? "dashed" : "solid",
borderRadius: 10,
padding: 2,
}}
>
<View style={renderBackgroundStyle()}>
{layer.curveDegree && layer.curveDegree !== 0 ? (
<CurvedTextNative
text={layer.text}
fontSize={layer.fontSize}
color={layer.color}
fontFamily={getCanvasFontFamily(layer.fontId)}
fontWeight={layer.fontWeight || "bold"}
letterSpacing={layer.letterSpacing ?? 0.5}
curveDegree={layer.curveDegree}
hasShadow={layer.hasShadow}
/>
) : (
<Text
style={{
fontSize: layer.fontSize,
color: layer.color,
fontFamily: getCanvasFontFamily(layer.fontId),
fontWeight: layer.fontWeight || "bold",
textAlign: layer.textAlign || "center",
letterSpacing: layer.letterSpacing ?? 0.5,
textShadowColor: layer.hasShadow ? "rgba(0,0,0,0.8)" : "transparent",
textShadowOffset: { width: 0, height: 1 },
textShadowRadius: 3,
}}
>
{layer.text}
</Text>
)}
</View>
</View>
</View>
);
};
@@ -0,0 +1,176 @@
import React, { useRef } from "react";
import {
Image,
PanResponder,
View,
} from "react-native";
import { Ionicons } from "@expo/vector-icons";
import { type CanvasQrConfig } from "./types";
interface CanvasQrItemProps {
qrConfig: CanvasQrConfig;
qrBase64?: string;
isSelected: boolean;
onSelect: () => void;
onUpdatePosition: (x: number, y: number) => void;
onDragStart?: () => void;
onDragMove?: (rawX: number, rawY: number, itemWidth: number, itemHeight: number) => { snappedX: number; snappedY: number };
onDragEnd?: (finalX: number, finalY: number, itemWidth: number, itemHeight: number) => void;
canvasWidth: number;
canvasHeight: number;
}
export const CanvasQrItem: React.FC<CanvasQrItemProps> = ({
qrConfig,
qrBase64,
isSelected,
onSelect,
onUpdatePosition,
onDragStart,
onDragMove,
onDragEnd,
canvasWidth,
canvasHeight,
}) => {
// Stale closure önlemi için ref'ler
const qrConfigRef = useRef(qrConfig);
qrConfigRef.current = qrConfig;
const canvasWidthRef = useRef(canvasWidth);
canvasWidthRef.current = canvasWidth;
const canvasHeightRef = useRef(canvasHeight);
canvasHeightRef.current = canvasHeight;
const panStartRef = useRef({ x: qrConfig.x, y: qrConfig.y });
const currentPosRef = useRef({ x: qrConfig.x, y: qrConfig.y });
const panResponder = useRef(
PanResponder.create({
onStartShouldSetPanResponder: () => true,
onStartShouldSetPanResponderCapture: () => true,
onMoveShouldSetPanResponder: (_evt, gestureState) =>
Math.abs(gestureState.dx) > 1 || Math.abs(gestureState.dy) > 1,
onMoveShouldSetPanResponderCapture: (_evt, gestureState) =>
Math.abs(gestureState.dx) > 1 || Math.abs(gestureState.dy) > 1,
onPanResponderTerminationRequest: () => false,
onPanResponderGrant: () => {
onSelect();
const cur = qrConfigRef.current;
panStartRef.current = { x: cur.x, y: cur.y };
currentPosRef.current = { x: cur.x, y: cur.y };
onDragStart?.();
},
onPanResponderMove: (_evt, gestureState) => {
const cur = qrConfigRef.current;
const cWidth = canvasWidthRef.current;
const cHeight = canvasHeightRef.current;
const maxX = Math.max(0, cWidth - cur.size);
const maxY = Math.max(0, cHeight - cur.size);
const rawX = Math.max(0, Math.min(maxX, panStartRef.current.x + gestureState.dx));
const rawY = Math.max(0, Math.min(maxY, panStartRef.current.y + gestureState.dy));
if (onDragMove) {
const { snappedX, snappedY } = onDragMove(rawX, rawY, cur.size, cur.size);
currentPosRef.current = { x: snappedX, y: snappedY };
onUpdatePosition(snappedX, snappedY);
} else {
const sx = Math.round(rawX);
const sy = Math.round(rawY);
currentPosRef.current = { x: sx, y: sy };
onUpdatePosition(sx, sy);
}
},
onPanResponderRelease: () => {
const cur = qrConfigRef.current;
onDragEnd?.(currentPosRef.current.x, currentPosRef.current.y, cur.size, cur.size);
},
onPanResponderTerminate: () => {
const cur = qrConfigRef.current;
onDragEnd?.(currentPosRef.current.x, currentPosRef.current.y, cur.size, cur.size);
},
})
).current;
return (
<View
{...panResponder.panHandlers}
accessible={true}
accessibilityRole="button"
accessibilityLabel="QR Kod Alanı"
accessibilityHint="QR kodunu seçmek, boyutunu ayarlamak veya stant üzerinde taşımak için dokunun"
accessibilityState={{ selected: isSelected }}
style={{
position: "absolute",
left: qrConfig.x,
top: qrConfig.y,
width: qrConfig.size,
height: qrConfig.size,
zIndex: isSelected ? 80 : 15,
}}
>
<View
style={{
width: "100%",
height: "100%",
backgroundColor: qrConfig.bgBoxColor || "#FFFFFF",
borderRadius: qrConfig.borderRadius,
borderWidth: isSelected ? 2 : qrConfig.borderWidth,
borderColor: isSelected ? "#D4AF37" : qrConfig.borderColor,
borderStyle: isSelected ? "dashed" : "solid",
padding: qrConfig.padding,
alignItems: "center",
justifyContent: "center",
shadowColor: "#000",
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.25,
shadowRadius: 6,
elevation: 4,
}}
>
{qrBase64 ? (
<Image
source={{ uri: `data:image/png;base64,${qrBase64}` }}
style={{
width: "100%",
height: "100%",
borderRadius: Math.max(0, qrConfig.borderRadius - 4),
}}
resizeMode="contain"
/>
) : (
<View style={{ alignItems: "center", justifyContent: "center", width: "100%", height: "100%" }}>
<Ionicons name="qr-code" size={Math.round(qrConfig.size * 0.7)} color="#0C0C0E" />
</View>
)}
{/* Seçili İken Taşıma Rozeti */}
{isSelected && (
<View
style={{
position: "absolute",
top: -8,
right: -8,
backgroundColor: "#D4AF37",
width: 22,
height: 22,
borderRadius: 11,
alignItems: "center",
justifyContent: "center",
borderWidth: 1.5,
borderColor: "#0C0C0E",
}}
>
<Ionicons name="move" size={12} color="#0C0C0E" />
</View>
)}
</View>
</View>
);
};
@@ -0,0 +1,709 @@
import React, { useEffect, useRef, useState } from "react";
import {
KeyboardAvoidingView,
Modal,
Platform,
Pressable,
ScrollView,
StyleSheet,
Text,
TextInput,
View,
} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Ionicons } from "@expo/vector-icons";
import * as Haptics from "expo-haptics";
import {
type CanvasTextLayer,
CANVAS_FONT_OPTIONS,
CANVAS_COLOR_PALETTE,
getCanvasFontFamily,
} from "./types";
interface InstagramTextEditorModalProps {
visible: boolean;
layer: CanvasTextLayer | null;
onSave: (updated: Partial<CanvasTextLayer>) => void;
onClose: () => void;
onDelete?: (id: string) => void;
}
export const InstagramTextEditorModal: React.FC<InstagramTextEditorModalProps> = ({
visible,
layer,
onSave,
onClose,
onDelete,
}) => {
const insets = useSafeAreaInsets();
const inputRef = useRef<TextInput>(null);
const [text, setText] = useState("");
const [fontId, setFontId] = useState("sans");
const [color, setColor] = useState("#FFFFFF");
const [fontSize, setFontSize] = useState(16);
const [textAlign, setTextAlign] = useState<"left" | "center" | "right">("center");
const [bgStyle, setBgStyle] = useState<"none" | "solid" | "soft" | "outline">("none");
const [bgBoxColor, setBgBoxColor] = useState<string | undefined>(undefined);
const [curveDegree, setCurveDegree] = useState<number>(0);
// Modal açıldığında layer verilerini yükle
useEffect(() => {
if (layer && visible) {
setText(layer.text || "");
setFontId(layer.fontId || "sans");
setColor(layer.color || "#FFFFFF");
setFontSize(layer.fontSize || 16);
setTextAlign(layer.textAlign || "center");
setCurveDegree(layer.curveDegree || 0);
if (layer.bgBoxColor) {
if (layer.bgBoxColor.includes("0.2") || layer.bgBoxColor.includes("0.3")) {
setBgStyle("soft");
} else {
setBgStyle("solid");
}
setBgBoxColor(layer.bgBoxColor);
} else {
setBgStyle("none");
setBgBoxColor(undefined);
}
// Auto focus text input
setTimeout(() => {
inputRef.current?.focus();
}, 150);
}
}, [layer, visible]);
// Kavis Modunu Değiştir (Düz (0) -> Yukarı Kavis (40) -> Ters Kavis (-40) -> Düz)
const handleToggleCurveDegree = () => {
try {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
} catch {}
setCurveDegree((prev) => {
if (prev === 0) return 40;
if (prev === 40) return 60;
if (prev === 60) return -40;
return 0;
});
};
// Arka plan pill stilini sırayla değiştir (Instagram "A" butonu mantığı)
const handleToggleBgStyle = () => {
try {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
} catch {}
if (bgStyle === "none") {
setBgStyle("solid");
// Solid: seçili rengi kutu yap, metin rengini zıt yap
if (color === "#FFFFFF" || color === "#F6EFE3") {
setBgBoxColor("rgba(18, 18, 21, 0.92)");
} else {
setBgBoxColor("rgba(212, 175, 55, 0.92)");
}
} else if (bgStyle === "solid") {
setBgStyle("soft");
setBgBoxColor("rgba(255, 255, 255, 0.22)");
} else if (bgStyle === "soft") {
setBgStyle("outline");
setBgBoxColor(undefined);
} else {
setBgStyle("none");
setBgBoxColor(undefined);
}
};
// Metin Hizalama Değiştir (Sol -> Orta -> Sağ)
const handleToggleAlign = () => {
try {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
} catch {}
setTextAlign((prev) => {
if (prev === "center") return "left";
if (prev === "left") return "right";
return "center";
});
};
// Renk Değiştir
const handleSelectColor = (selectedColor: string) => {
try {
Haptics.selectionAsync();
} catch {}
setColor(selectedColor);
if (bgStyle === "solid") {
if (selectedColor === "#FFFFFF" || selectedColor === "#F6EFE3") {
setBgBoxColor("rgba(18, 18, 21, 0.92)");
} else if (selectedColor === "#121215") {
setBgBoxColor("rgba(255, 255, 255, 0.92)");
} else {
setBgBoxColor(selectedColor);
}
}
};
// Font Değiştir
const handleSelectFont = (fId: string) => {
try {
Haptics.selectionAsync();
} catch {}
setFontId(fId);
};
// Bitti / Kaydet
const handleDone = () => {
try {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
} catch {}
let finalBg = bgBoxColor;
if (bgStyle === "none" || bgStyle === "outline") {
finalBg = undefined;
}
onSave({
text: text.trim() ? text : "Metin",
fontId,
color,
fontSize,
textAlign,
bgBoxColor: finalBg,
bgStyle,
curveDegree,
hasShadow: bgStyle === "outline" || (!finalBg && color !== "#121215"),
});
onClose();
};
const fontFamily = getCanvasFontFamily(fontId);
if (!visible || !layer) return null;
return (
<Modal
visible={visible}
transparent
animationType="fade"
statusBarTranslucent
onRequestClose={onClose}
>
<View style={styles.overlayBackdrop}>
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"}
style={[styles.container, { paddingTop: insets.top + 8, paddingBottom: insets.bottom + 12 }]}
>
{/* ÜST BAR - INSTAGRAM STORY KONTROLLERİ */}
<View style={styles.topBar}>
{/* Sol: İptal / Kapat */}
<Pressable
onPress={onClose}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
style={styles.iconButton}
>
<Ionicons name="close" size={24} color="#FFFFFF" />
</Pressable>
{/* Orta Araçlar: Hizalama & Arka Plan Rozeti */}
<View style={styles.centerTools}>
{/* Hizalama Butonu */}
<Pressable
onPress={handleToggleAlign}
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
style={styles.toolPill}
>
<Ionicons
name={
textAlign === "left"
? "reorder-three-outline"
: textAlign === "right"
? "reorder-three-outline"
: "reorder-two-outline"
}
size={20}
color="#FFFFFF"
/>
</Pressable>
{/* Instagram "A" Arka Plan Butonu */}
<Pressable
onPress={handleToggleBgStyle}
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
style={[
styles.toolPill,
bgStyle !== "none" && { backgroundColor: "#FFFFFF" },
]}
>
<Text
style={[
styles.bgPillText,
bgStyle !== "none" && { color: "#0C0C0E", fontWeight: "900" },
]}
>
A
</Text>
{bgStyle === "soft" && (
<View style={styles.softIndicator} />
)}
{bgStyle === "outline" && (
<View style={styles.outlineIndicator} />
)}
</Pressable>
{/* Kavis (Yay / Eğri Yazı) Toggle Butonu */}
<Pressable
onPress={handleToggleCurveDegree}
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
style={[
styles.toolPill,
curveDegree !== 0 && { backgroundColor: "#D4AF37" },
]}
>
<Text
style={[
styles.bgPillText,
{ fontSize: 14 },
curveDegree !== 0 ? { color: "#0C0C0E", fontWeight: "900" } : { color: "#FFFFFF" },
]}
>
{curveDegree > 0 ? "◠" : curveDegree < 0 ? "◡" : "⌒"}
</Text>
</Pressable>
{/* Katmanı Sil (Varsa) */}
{onDelete && (
<Pressable
onPress={() => {
try {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning);
} catch {}
onDelete(layer.id);
onClose();
}}
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
style={[styles.toolPill, { backgroundColor: "rgba(239, 68, 68, 0.2)" }]}
>
<Ionicons name="trash-outline" size={17} color="#EF4444" />
</Pressable>
)}
</View>
{/* Sağ: BİTTİ (Done) Butonu */}
<Pressable
onPress={handleDone}
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
style={styles.doneButton}
>
<Text style={styles.doneButtonText}>Bitti</Text>
</Pressable>
</View>
{/* ORTA BÖLÜM: SOL BOYUT SLIDER & CANLI METİN ALANI */}
<View style={styles.editorBody}>
{/* Sol: Dikey Boyut Stepper / Gösterge */}
<View style={styles.sizeControlCol}>
<Pressable
onPress={() => {
try { Haptics.selectionAsync(); } catch {}
setFontSize((s) => Math.min(48, s + 2));
}}
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
style={styles.sizeStepBtn}
>
<Ionicons name="add" size={16} color="#FFFFFF" />
</Pressable>
<View style={styles.sizeTrack}>
<View
style={[
styles.sizeTrackFill,
{ height: `${Math.min(100, Math.max(10, ((fontSize - 8) / 40) * 100))}%` },
]}
/>
</View>
<Pressable
onPress={() => {
try { Haptics.selectionAsync(); } catch {}
setFontSize((s) => Math.max(8, s - 2));
}}
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
style={styles.sizeStepBtn}
>
<Ionicons name="remove" size={16} color="#FFFFFF" />
</Pressable>
<Text style={styles.sizeLabel}>{fontSize}pt</Text>
</View>
{/* Orta: Canlı İnstagram Yazı Alanı */}
<View style={styles.textInputArea}>
<View
style={[
styles.textPillWrapper,
bgStyle === "solid" && {
backgroundColor: bgBoxColor || "#D4AF37",
borderRadius: 14,
paddingHorizontal: 16,
paddingVertical: 8,
},
bgStyle === "soft" && {
backgroundColor: "rgba(255, 255, 255, 0.22)",
borderRadius: 14,
paddingHorizontal: 16,
paddingVertical: 8,
borderWidth: 1,
borderColor: "rgba(255, 255, 255, 0.3)",
},
bgStyle === "outline" && {
borderWidth: 1.5,
borderColor: color,
borderRadius: 14,
paddingHorizontal: 14,
paddingVertical: 6,
},
]}
>
<TextInput
ref={inputRef}
value={text}
onChangeText={setText}
multiline
placeholder="Yazmaya başla..."
placeholderTextColor="rgba(255,255,255,0.4)"
textAlign={textAlign}
autoCapitalize="sentences"
style={[
styles.textInput,
{
fontFamily,
fontSize,
color:
bgStyle === "solid" && (bgBoxColor === "#FFFFFF" || bgBoxColor === "#F6EFE3")
? "#0C0C0E"
: color,
textAlign,
letterSpacing: fontId === "serif" ? 1.5 : 0.5,
textShadowColor: bgStyle === "outline" ? color : "rgba(0,0,0,0.7)",
textShadowOffset: { width: 0, height: 1 },
textShadowRadius: bgStyle === "outline" ? 8 : 4,
},
]}
/>
</View>
</View>
</View>
{/* ALT BÖLÜM 0: KAVİS HASSAS AYAR BARI */}
<View style={{ flexDirection: "row", alignItems: "center", justifyContent: "center", paddingHorizontal: 16, paddingVertical: 4, gap: 10 }}>
<Text style={{ color: "#A1A1AA", fontSize: 11, fontWeight: "700" }}>KAVİS:</Text>
<View style={{ flexDirection: "row", alignItems: "center", backgroundColor: "rgba(255,255,255,0.1)", borderRadius: 14, paddingHorizontal: 6, paddingVertical: 3, gap: 8 }}>
<Pressable
onPress={() => {
try { Haptics.selectionAsync(); } catch {}
setCurveDegree((deg) => Math.max(-100, deg - 5));
}}
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
style={{ width: 26, height: 26, borderRadius: 13, backgroundColor: "rgba(255,255,255,0.15)", alignItems: "center", justifyContent: "center" }}
>
<Ionicons name="remove" size={15} color="#FFFFFF" />
</Pressable>
<Text style={{ color: "#D4AF37", fontSize: 13, fontWeight: "900", minWidth: 36, textAlign: "center" }}>
{curveDegree}°
</Text>
<Pressable
onPress={() => {
try { Haptics.selectionAsync(); } catch {}
setCurveDegree((deg) => Math.min(100, deg + 5));
}}
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
style={{ width: 26, height: 26, borderRadius: 13, backgroundColor: "rgba(255,255,255,0.15)", alignItems: "center", justifyContent: "center" }}
>
<Ionicons name="add" size={15} color="#FFFFFF" />
</Pressable>
</View>
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ gap: 6 }}>
{[
{ label: "Düz (0°)", deg: 0 },
{ label: "25° ◠", deg: 25 },
{ label: "45° ◠", deg: 45 },
{ label: "65° ◠", deg: 65 },
{ label: "-35° ◡", deg: -35 },
].map((item) => {
const isActive = curveDegree === item.deg;
return (
<Pressable
key={item.deg}
onPress={() => {
try { Haptics.selectionAsync(); } catch {}
setCurveDegree(item.deg);
}}
style={{
paddingHorizontal: 10,
paddingVertical: 5,
borderRadius: 12,
backgroundColor: isActive ? "#D4AF37" : "rgba(255,255,255,0.1)",
}}
>
<Text style={{ color: isActive ? "#0C0C0E" : "#FFFFFF", fontSize: 11, fontWeight: isActive ? "800" : "600" }}>
{item.label}
</Text>
</Pressable>
);
})}
</ScrollView>
</View>
{/* ALT BÖLÜM 1: RENK SEÇİCİ BALONCUKLAR */}
<View style={styles.colorPickerBar}>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.colorScrollContent}
>
{CANVAS_COLOR_PALETTE.map((c) => {
const isSelected = color === c.color;
return (
<Pressable
key={c.id}
onPress={() => handleSelectColor(c.color)}
style={[
styles.colorCircleWrapper,
isSelected && { borderColor: "#FFFFFF", transform: [{ scale: 1.15 }] },
]}
>
<View
style={[
styles.colorCircle,
{ backgroundColor: c.color },
c.color === "#121215" && { borderWidth: 1, borderColor: "rgba(255,255,255,0.4)" },
]}
/>
</Pressable>
);
})}
</ScrollView>
</View>
{/* ALT BÖLÜM 2: INSTAGRAM FONT STİLLERİ CAROUSEL */}
<View style={styles.fontPickerBar}>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.fontScrollContent}
>
{CANVAS_FONT_OPTIONS.map((f) => {
const isSelected = fontId === f.id;
return (
<Pressable
key={f.id}
onPress={() => handleSelectFont(f.id)}
style={[
styles.fontPill,
isSelected && styles.fontPillActive,
]}
>
<Text
style={[
styles.fontPillText,
{ fontFamily: f.fontFamily },
isSelected && styles.fontPillTextActive,
]}
>
{f.label}
</Text>
</Pressable>
);
})}
</ScrollView>
</View>
</KeyboardAvoidingView>
</View>
</Modal>
);
};
const styles = StyleSheet.create({
overlayBackdrop: {
flex: 1,
backgroundColor: "rgba(10, 10, 14, 0.94)",
},
container: {
flex: 1,
justifyContent: "space-between",
},
topBar: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
paddingHorizontal: 16,
height: 52,
zIndex: 10,
},
iconButton: {
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: "rgba(255, 255, 255, 0.12)",
alignItems: "center",
justifyContent: "center",
},
centerTools: {
flexDirection: "row",
alignItems: "center",
gap: 10,
},
toolPill: {
width: 38,
height: 38,
borderRadius: 19,
backgroundColor: "rgba(255, 255, 255, 0.12)",
alignItems: "center",
justifyContent: "center",
position: "relative",
},
bgPillText: {
color: "#FFFFFF",
fontSize: 16,
fontWeight: "700",
},
softIndicator: {
position: "absolute",
bottom: 4,
width: 6,
height: 6,
borderRadius: 3,
backgroundColor: "#60A5FA",
},
outlineIndicator: {
position: "absolute",
bottom: 4,
width: 6,
height: 6,
borderRadius: 3,
backgroundColor: "#F59E0B",
},
doneButton: {
backgroundColor: "#FFFFFF",
paddingHorizontal: 18,
paddingVertical: 8,
borderRadius: 20,
alignItems: "center",
justifyContent: "center",
},
doneButtonText: {
color: "#0C0C0E",
fontSize: 14,
fontWeight: "800",
},
editorBody: {
flex: 1,
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 16,
},
sizeControlCol: {
width: 36,
alignItems: "center",
gap: 8,
},
sizeStepBtn: {
width: 30,
height: 30,
borderRadius: 15,
backgroundColor: "rgba(255, 255, 255, 0.15)",
alignItems: "center",
justifyContent: "center",
},
sizeTrack: {
width: 4,
height: 120,
backgroundColor: "rgba(255, 255, 255, 0.18)",
borderRadius: 2,
justifyContent: "flex-end",
overflow: "hidden",
},
sizeTrackFill: {
width: "100%",
backgroundColor: "#D4AF37",
borderRadius: 2,
},
sizeLabel: {
color: "rgba(255, 255, 255, 0.6)",
fontSize: 10,
fontWeight: "700",
},
textInputArea: {
flex: 1,
justifyContent: "center",
alignItems: "center",
paddingHorizontal: 12,
},
textPillWrapper: {
maxWidth: "100%",
alignItems: "center",
justifyContent: "center",
},
textInput: {
minWidth: 140,
maxWidth: "100%",
padding: 0,
margin: 0,
fontWeight: "800",
},
colorPickerBar: {
paddingVertical: 10,
},
colorScrollContent: {
paddingHorizontal: 20,
gap: 12,
alignItems: "center",
},
colorCircleWrapper: {
width: 32,
height: 32,
borderRadius: 16,
borderWidth: 2,
borderColor: "transparent",
alignItems: "center",
justifyContent: "center",
},
colorCircle: {
width: 24,
height: 24,
borderRadius: 12,
},
fontPickerBar: {
paddingVertical: 8,
},
fontScrollContent: {
paddingHorizontal: 16,
gap: 8,
alignItems: "center",
},
fontPill: {
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 18,
backgroundColor: "rgba(255, 255, 255, 0.08)",
borderWidth: 1,
borderColor: "rgba(255, 255, 255, 0.12)",
},
fontPillActive: {
backgroundColor: "#FFFFFF",
borderColor: "#FFFFFF",
},
fontPillText: {
color: "rgba(255, 255, 255, 0.8)",
fontSize: 12,
fontWeight: "700",
letterSpacing: 0.5,
},
fontPillTextActive: {
color: "#0C0C0E",
fontWeight: "900",
},
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,5 @@
export * from "./types";
export * from "./CanvasLayerItem";
export * from "./CanvasQrItem";
export * from "./InstagramTextEditorModal";
export * from "./QrCanvasStudio";
@@ -0,0 +1,309 @@
import { Platform } from "react-native";
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
bgStyle?: "none" | "solid" | "soft" | "outline";
textAlign?: "left" | "center" | "right";
hasShadow?: boolean;
letterSpacing?: number;
fontWeight?: "normal" | "600" | "700" | "800" | "900";
curveDegree?: number; // -100 (Aşağı Kavis ◡) ile +100 (Yukarı Kavis ◠) arası kavis derecesi
}
export interface CanvasQrConfig {
x: number;
y: number;
size: number;
borderRadius: number;
borderWidth: number;
borderColor: string;
padding: number;
bgBoxColor: string;
}
export const CANVAS_FONT_OPTIONS = [
{
id: "sans",
label: "MODERN",
fontFamily: Platform.OS === "ios" ? "System" : "sans-serif",
},
{
id: "serif",
label: "ZARİF",
fontFamily: Platform.OS === "ios" ? "Georgia" : "serif",
},
{
id: "times",
label: "KLASİK",
fontFamily: Platform.OS === "ios" ? "Times New Roman" : "serif",
},
{
id: "typewriter",
label: "DAKTİLO",
fontFamily: Platform.OS === "ios" ? "Courier" : "monospace",
},
{
id: "palatino",
label: "LÜKS",
fontFamily: Platform.OS === "ios" ? "Palatino" : "serif",
},
{
id: "trebuchet",
label: "DİNAMİK",
fontFamily: Platform.OS === "ios" ? "Trebuchet MS" : "sans-serif",
},
{
id: "heavy",
label: "GÜÇLÜ",
fontFamily: Platform.OS === "ios" ? "Impact" : "sans-serif-black",
},
];
export function getCanvasFontFamily(fontId?: string): string {
if (!fontId) return Platform.OS === "ios" ? "System" : "sans-serif";
const found = CANVAS_FONT_OPTIONS.find((f) => f.id === fontId);
if (found) {
return found.fontFamily;
}
if (fontId.includes(",")) {
const first = fontId.split(",")[0].trim().replace(/['"]/g, "");
return first || (Platform.OS === "ios" ? "System" : "sans-serif");
}
return fontId;
}
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: "#10B981", label: "Yeşil" },
{ id: "crimson", color: "#EF4444", label: "Kırmızı" },
{ id: "amber", color: "#F59E0B", label: "Amber" },
{ id: "rose", color: "#EC4899", label: "Pembe" },
{ id: "indigo", color: "#6366F1", label: "Lila" },
{ id: "cyan", color: "#06B6D4", label: "Turkuaz" },
];
export const INSTAGRAM_BADGE_STYLES = [
{ id: "none", label: "Düz", bgBoxColor: undefined },
{ id: "dark", label: "Koyu", bgBoxColor: "rgba(18, 18, 21, 0.88)" },
{ id: "gold", label: "Altın", bgBoxColor: "rgba(212, 175, 55, 0.92)" },
{ id: "white", label: "Beyaz", bgBoxColor: "rgba(255, 255, 255, 0.92)" },
{ id: "glass", label: "Cam", bgBoxColor: "rgba(255, 255, 255, 0.2)" },
];
export const QR_SIZE_PRESETS = [
{ label: "Kompakt", size: 85 },
{ label: "Standart", size: 115 },
{ label: "Büyük", size: 145 },
{ label: "Geniş", size: 175 },
];
export const QR_RADIUS_PRESETS = [
{ label: "Düz", radius: 0 },
{ label: "Hafif", radius: 10 },
{ label: "Yuvarlak", radius: 18 },
{ label: "Oval", radius: 28 },
];
export const QR_BORDER_COLORS = [
{ id: "gold", color: "#D4AF37", label: "Altın" },
{ id: "black", color: "#121215", label: "Siyah" },
{ id: "white", color: "#FFFFFF", label: "Beyaz" },
{ id: "none", color: "transparent", label: "Yok" },
];
export function getDefaultQrConfigForTemplate(
templateKey: string,
canvasWidth: number,
canvasHeight: number
): CanvasQrConfig {
if (templateKey === "qr-1" || templateKey === "qr-8" || templateKey === "qr-9") {
const size = 110;
return {
x: Math.round((canvasWidth - size) / 2),
y: 102,
size,
borderRadius: 14,
borderWidth: 1.5,
borderColor: "#D4AF37",
padding: 6,
bgBoxColor: "#FFFFFF",
};
}
// Standart Dikey Şablonlar (qr-2, qr-3, qr-4, qr-5, qr-6)
const size = 125;
return {
x: Math.round((canvasWidth - size) / 2),
y: Math.round(canvasHeight * 0.36),
size,
borderRadius: 14,
borderWidth: 1.5,
borderColor: "#D4AF37",
padding: 6,
bgBoxColor: "#FFFFFF",
};
}
// 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,
textAlign: "center",
},
{
id: "sub",
text: "RESTAURANT & BAR",
x: 65,
y: 78,
rotation: 0,
fontSize: 9,
fontId: "sans",
color: "#E5C378",
fontWeight: "600",
letterSpacing: 1.5,
textAlign: "center",
},
{
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,
textAlign: "center",
},
{
id: "table",
text: "MASA NO: 01 • WiFi: Bistro",
x: 45,
y: 265,
rotation: 0,
fontSize: 9,
fontId: "sans",
color: "#C5A059",
fontWeight: "600",
textAlign: "center",
},
];
}
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,
textAlign: "center",
},
{
id: "sub",
text: "THE DAILY SPECIALS",
x: 50,
y: 68,
rotation: -4,
fontSize: 13,
fontId: "sans",
color: "#D1A752",
fontWeight: "800",
textAlign: "center",
},
{
id: "cta",
text: "SCAN TO VIEW\nFULL MENU & SPECIALS",
x: 55,
y: 215,
rotation: 0,
fontSize: 9.5,
fontId: "sans",
color: "#FFFFFF",
fontWeight: "800",
textAlign: "center",
},
{
id: "wifi",
text: "SCAN TO ORDER & BROWSE",
x: 55,
y: 258,
rotation: 0,
fontSize: 9,
fontId: "sans",
color: "#D1A752",
fontWeight: "800",
textAlign: "center",
},
];
}
// Varsayılan
return [
{
id: "rName",
text: restaurantName,
x: 50,
y: 45,
rotation: 0,
fontSize: 17,
fontId: "serif",
color: "#D4AF37",
fontWeight: "800",
textAlign: "center",
},
{
id: "sub",
text: "SCAN TO ORDER",
x: 75,
y: 75,
rotation: 0,
fontSize: 10,
fontId: "sans",
color: "#FFFFFF",
fontWeight: "700",
textAlign: "center",
},
{
id: "cta",
text: "DİJİTAL MENÜ",
x: 75,
y: 245,
rotation: 0,
fontSize: 11,
fontId: "serif",
color: "#D4AF37",
fontWeight: "800",
textAlign: "center",
},
];
}
@@ -0,0 +1,316 @@
import * as React from "react";
import Svg, {
Path,
Rect,
Circle,
G,
Ellipse,
Text,
Image,
} from "react-native-svg";
import { type QrTemplateProps, getFontFamily, renderSvgLines } from "./common";
const QrTemplate2 = ({
qrBase64,
fields,
hideQr,
...props
}: QrTemplateProps) => {
const rName = fields?.restaurantName;
const sub = fields?.subtitle;
const table = fields?.tableNo;
const cta = fields?.ctaText;
const wifi = fields?.wifiText;
const social = fields?.socialText;
const rNameFont = getFontFamily(rName?.fontId, "Georgia, 'Times New Roman', serif");
const subFont = getFontFamily(sub?.fontId, "System, -apple-system, Roboto, sans-serif");
const ctaFont = getFontFamily(cta?.fontId, "Georgia, 'Times New Roman', serif");
const infoFont = getFontFamily(wifi?.fontId, "System, -apple-system, Roboto, sans-serif");
return (
<Svg viewBox="0 0 400 650" width={300} height={487.5} {...props}>
{/* Arka Plan */}
<Path fill="#252c36" d="M0 0h400v650H0z" />
{/* Ana Kart */}
<Rect
width={360}
height={610}
x={20}
y={20}
fill="#f6efe2"
stroke="#3a2312"
strokeWidth={1.5}
rx={20}
/>
<Rect
width={344}
height={594}
x={28}
y={28}
fill="none"
stroke="#8c6239"
strokeOpacity={0.4}
rx={14}
/>
<Rect
width={336}
height={586}
x={32}
y={32}
fill="none"
stroke="#8c6239"
strokeOpacity={0.25}
strokeWidth={0.5}
rx={10}
/>
{/* Sol Üst Botanik Dal */}
<G fill="none" stroke="#3a2312" strokeLinecap="round" strokeWidth={1.05}>
<Path d="M36 36c11.25 11.25 18.75 26.25 22.5 48.75" />
<Path
fill="#eadcc7"
d="M43.5 45c-3.75-7.5 7.5-10.5 9-3 0 7.5-9 3-9 3Zm6 11.25C45 51 57 46.5 60 53.25c0 6.75-10.5 3-10.5 3Zm4.5 13.5C60 64.5 69.75 72 64.5 78c-6 4.5-10.5-8.25-10.5-8.25Z"
/>
</G>
{/* Sağ Üst Kahve İkonu */}
<G transform="translate(340 52)">
<Circle r={32} fill="#eadcc7" stroke="#3a2312" strokeWidth={1.2} />
<Circle
r={26}
fill="#f6efe2"
stroke="#8c6239"
strokeDasharray="2 2"
strokeWidth={0.8}
/>
<Circle r={21} fill="#7a4b26" stroke="#3a2312" strokeWidth={1.4} />
<Path fill="#f6efe2" d="M0 10C-8 4-4-6 0-12 4-6 8 4 0 10" />
<Path fill="#7a4b26" d="M0 4C-5 0-2-5 0-8 2-5 5 0 0 4" />
<Circle cy={-14} r={1.8} fill="#f6efe2" />
</G>
{/* Sol Orta Kahve Bardağı */}
<G transform="translate(48 250)">
<Circle r={26} fill="#eadcc7" stroke="#3a2312" />
<Circle r={17} fill="#6a3b18" stroke="#3a2312" strokeWidth={1.2} />
<Path fill="#f6efe2" d="M-5-3c-3-4 5-7 5-1 0-6 8-3 5 1L0 7z" />
<Path fill="none" stroke="#3a2312" strokeWidth={1.5} d="M17-4c6 0 6 8 0 8" />
</G>
{/* Sağ Dal */}
<G fill="none" stroke="#3a2312" strokeWidth={0.91}>
<Path d="M352 230c-7 21-10.5 49-3.5 77" />
<Path
fill="#eadcc7"
d="M349.2 247.5c8.4-7 15.4 2.1 5.6 7.7-5.6 0-5.6-7.7-5.6-7.7Zm-2.8 24.5c-9.8-8.4-15.4 1.4-7 7 7 0 7-7 7-7Zm2.1 19.6c9.1-7 13.3 2.8 4.9 8.4-4.9 0-4.9-8.4-4.9-8.4Z"
/>
</G>
{/* Dekoratif Çizgi */}
<Path
fill="none"
stroke="#8c6239"
strokeLinecap="round"
strokeWidth={1.2}
d="M142 66c6-2 12 2 18 0m80 0c6 2 12-2 18 0"
/>
{/* Sol Alt Dal */}
<G fill="none" stroke="#3a2312" strokeWidth={0.91}>
<Path d="M42 601.5c13-26 9.75-52 0-71.5" />
<Path
fill="#eadcc7"
d="M47.2 549.5c10.4-9.75 17.55 0 6.5 7.8Zm2.6 26c11.7-9.75 18.2 1.3 5.2 9.1Z"
/>
</G>
{/* Kahve Çekirdekleri */}
<G stroke="#3a2312" strokeWidth={1.2} transform="rotate(-20 1741.206 -8.75) scale(.8)">
<Ellipse fill="#cbb296" rx={9} ry={6.5} />
<Path fill="none" d="M-7 0q7 3 7 0t7 0" />
</G>
<G stroke="#3a2312" strokeWidth={1.2} transform="rotate(35 -862.106 494.982) scale(.8)">
<Ellipse fill="#cbb296" rx={9} ry={6.5} />
<Path fill="none" d="M-7 0q7 3 7 0t7 0" />
</G>
<G stroke="#3a2312" strokeWidth={1.2} transform="rotate(-45 602.558 -225.588) scale(.7)">
<Ellipse fill="#cbb296" rx={9} ry={6.5} />
<Path fill="none" d="M-7 0q7 3 7 0t7 0" />
</G>
{/* Sağ Alt Fincan */}
<G transform="translate(325 590)">
<Ellipse cy={18} fill="#eadcc7" stroke="#3a2312" strokeWidth={1.2} rx={28} ry={7} />
<Path fill="#f6efe2" stroke="#3a2312" strokeWidth={1.4} d="M-18 2c0 14 36 14 36 0Z" />
<Ellipse cy={2} fill="#6a3b18" stroke="#3a2312" strokeWidth={1.2} rx={18} ry={4} />
<Path fill="none" stroke="#3a2312" strokeWidth={1.3} d="M16 4c8 0 8 8 0 10" />
<Path fill="none" stroke="#8c6239" strokeLinecap="round" d="M-6-4c-2-5 2-8 0-13M2-5c-2-5 2-8 0-14" />
</G>
{/* QR Köşe Çerçeveleri */}
<Path
fill="none"
stroke="#3a2312"
strokeLinecap="round"
strokeWidth={2}
d="M80 205c0-10 0-10 10-10m230 10c0-10 0-10-10-10M80 445c0 10 0 10 10 10m230-10c0 10 0 10-10 10"
/>
<Path stroke="#8c6239" strokeWidth={0.8} d="M145 608h30m50 0h30" />
{/* QR Placeholder / Gerçek QR */}
{!hideQr && (
<G transform="translate(90 215)">
<Rect width={220} height={220} fill="#fbf8f2" stroke="#d8c7b0" strokeWidth={1.5} rx={20} />
{qrBase64 ? (
<Image
href={`data:image/png;base64,${qrBase64}`}
x={10}
y={10}
width={200}
height={200}
/>
) : (
<>
<Rect
width={200}
height={200}
x={10}
y={10}
fill="#f5efe6"
stroke="#d8c7b0"
strokeDasharray="4 3"
rx={14}
/>
<Circle cx={110} cy={98} r={18} fill="#fbf8f2" stroke="#8c6239" />
<G fill="#3a2312" transform="matrix(.65 0 0 .65 110 98)">
<Ellipse cx={-5} cy={2} rx={7} ry={5} transform="rotate(-30)" />
<Ellipse cx={5} rx={7} ry={5} transform="rotate(40)" />
</G>
<Text
x={110}
y={132}
fill="#8c6239"
fontFamily="System, sans-serif"
fontSize={11}
fontWeight={700}
letterSpacing={1}
textAnchor="middle"
>
{"QR KOD ALANI"}
</Text>
</>
)}
</G>
)}
{/* Metinler (Çok satırlı destekli) */}
<G textAnchor="middle">
<Text
x={200}
y={68}
fill="#3a2312"
fontFamily={rNameFont}
fontSize={14}
fontStyle="italic"
fontWeight={700}
letterSpacing={4}
>
{"THE"}
</Text>
{/* Restoran Adı */}
{rName?.visible !== false &&
renderSvgLines({
text: rName?.text || "The Daily Grind",
x: 200,
y: 110,
offsetY: rName?.offsetY,
fill: "#2c1a0e",
fontFamily: rNameFont,
fontSize: rName?.fontSize || 28,
fontWeight: "bold",
letterSpacing: 0.5,
})}
{/* Slogan */}
{sub?.visible !== false &&
renderSvgLines({
text: sub?.text || "COFFEE & KITCHEN",
x: 200,
y: 144,
offsetY: sub?.offsetY,
fill: "#3a2312",
fontFamily: subFont,
fontSize: sub?.fontSize || 12,
fontWeight: 700,
letterSpacing: 2.5,
})}
{/* CTA */}
{cta?.visible !== false &&
renderSvgLines({
text: cta?.text || "MENÜYÜ GÖRMEK İÇİN OKUTUN",
x: 200,
y: 486,
offsetY: cta?.offsetY,
fill: "#2c1a0e",
fontFamily: ctaFont,
fontSize: cta?.fontSize || 15,
fontWeight: "bold",
letterSpacing: 1.2,
})}
{/* WiFi */}
{wifi?.visible !== false &&
renderSvgLines({
text: wifi?.text || "WiFi: DailyGrind | Şifre: coffee2026",
x: 200,
y: 520,
offsetY: wifi?.offsetY,
fill: "#3a2312",
fontFamily: infoFont,
fontSize: wifi?.fontSize || 10.5,
fontWeight: 700,
letterSpacing: 0.5,
})}
{/* Masa No & Sosyal */}
{(table?.visible !== false || social?.visible !== false) &&
renderSvgLines({
text: [
table?.visible !== false && table?.text ? table.text : "",
social?.visible !== false && social?.text ? social.text : "",
]
.filter(Boolean)
.join(" • "),
x: 200,
y: 544,
offsetY: table?.offsetY || social?.offsetY,
fill: "#8c6239",
fontFamily: infoFont,
fontSize: table?.fontSize || social?.fontSize || 10,
fontWeight: 600,
letterSpacing: 1,
})}
<Text
x={200}
y={611}
fill="#3a2312"
fontFamily={rNameFont}
fontSize={11}
fontStyle="italic"
fontWeight={700}
letterSpacing={2}
>
{"EST. 2026"}
</Text>
</G>
</Svg>
);
};
export default QrTemplate2;
@@ -0,0 +1,357 @@
import * as React from "react";
import Svg, {
Defs,
LinearGradient,
Stop,
Pattern,
Path,
G,
Rect,
Ellipse,
Circle,
Text,
Image,
} from "react-native-svg";
import { type QrTemplateProps, getFontFamily, renderSvgLines } from "./common";
const QrTemplate3 = ({
qrBase64,
fields,
hideQr,
...props
}: QrTemplateProps) => {
const rName = fields?.restaurantName;
const sub = fields?.subtitle;
const table = fields?.tableNo;
const cta = fields?.ctaText;
const wifi = fields?.wifiText;
const social = fields?.socialText;
const rNameFont = getFontFamily(rName?.fontId, "Georgia, 'Playfair Display', serif");
const subFont = getFontFamily(sub?.fontId, "System, -apple-system, Roboto, sans-serif");
const ctaFont = getFontFamily(cta?.fontId, "Georgia, 'Playfair Display', serif");
const infoFont = getFontFamily(wifi?.fontId, "System, -apple-system, Roboto, sans-serif");
return (
<Svg viewBox="0 0 400 650" width={300} height={487.5} {...props}>
<Defs>
<LinearGradient id="card-grad-3" x1="0%" x2="0%" y1="0%" y2="100%">
<Stop offset="0%" stopColor="#fdfbf5" />
<Stop offset="50%" stopColor="#faf4e6" />
<Stop offset="100%" stopColor="#f7eed8" />
</LinearGradient>
<LinearGradient id="oven-fire-3" x1="0%" x2="0%" y1="100%" y2="0%">
<Stop offset="0%" stopColor="#c0392b" />
<Stop offset="50%" stopColor="#e67e22" />
<Stop offset="100%" stopColor="#f1c40f" />
</LinearGradient>
<LinearGradient id="wine-grad-3" x1="0%" x2="0%" y1="0%" y2="100%">
<Stop offset="0%" stopColor="#7b1123" />
<Stop offset="100%" stopColor="#4a0512" />
</LinearGradient>
<Pattern
id="checkered-border-3"
width={20}
height={20}
patternUnits="userSpaceOnUse"
>
<Path fill="#b92b27" d="M0 0h20v20H0z" />
<Path fill="#fdf8ee" d="M0 0h10v10H0zm10 10h10v10H10z" />
</Pattern>
</Defs>
{/* Arka Plan */}
<Path fill="url(#checkered-border-3)" d="M0 0h400v650H0z" />
{/* Kart */}
<G id="layer-card">
<Rect
width={356}
height={606}
x={22}
y={22}
fill="url(#card-grad-3)"
stroke="#b92b27"
strokeWidth={2}
rx={20}
/>
<Rect
width={344}
height={594}
x={28}
y={28}
fill="none"
stroke="#8c6239"
strokeOpacity={0.5}
strokeWidth={0.8}
rx={15}
/>
{table?.visible !== false && table?.text ? (
<Rect
width={140}
height={24}
x={130}
y={34}
fill="#faf4e6"
stroke="#8c6239"
strokeWidth={1.2}
rx={12}
/>
) : null}
</G>
{/* İkonlar */}
<G id="layer-icons">
<Path
fill="none"
stroke="#c5a880"
strokeLinecap="round"
strokeWidth={1.5}
d="M68 88q12-13 28-2 6 5 10 2m226 0q-12-13-28-2-6 5-10 2"
/>
{/* Pizza Fırını */}
<G id="pizza-oven" transform="translate(200 192)">
<Rect
width={88}
height={6}
x={-44}
fill="#d7ccc8"
stroke="#5d4037"
rx={2}
/>
<Path
fill="#d7ccc8"
stroke="#5d4037"
strokeWidth={1.5}
d="M-40 0a40 38 0 0 1 80 0Z"
/>
<Path stroke="#8d6e63" d="m-30-25 7 6m6-15 4 8M0-38v9m17-5-4 8m17 1-7 6" />
<Path fill="#3e2723" stroke="#271c19" d="M-28 0a28 26 0 0 1 56 0Z" />
<Path fill="url(#oven-fire-3)" d="M-18 0q3-16 12-10 6-12 12-2 9-6 12 12Z" />
<Path fill="#fff9c4" d="M-10 0q5-10 10-6 5-6 10 6Z" />
</G>
{/* Sol Malzemeler */}
<G id="left-ingredients" transform="translate(108 185)">
<Ellipse
cx={-16}
cy={6}
fill="#d32f2f"
stroke="#8e0000"
strokeWidth={0.8}
rx={14}
ry={10}
transform="rotate(-25 -16 6)"
/>
<Ellipse
cx={-16}
cy={6}
fill="#e53935"
rx={10}
ry={6.5}
transform="rotate(-25 -16 6)"
/>
<Ellipse
cx={2}
cy={7}
fill="#c62828"
stroke="#8e0000"
strokeWidth={0.8}
rx={13}
ry={9}
transform="rotate(-10 2 7)"
/>
<Ellipse cx={2} cy={7} fill="#e53935" rx={9} ry={5.5} transform="rotate(-10 2 7)" />
<Circle cx={2} cy={7} r={1.5} fill="#fdd835" />
<Circle cx={16} cy={-2} r={14} fill="#e53935" stroke="#b71c1c" strokeWidth={0.8} />
<Ellipse cx={12} cy={-6} fill="#ff8a80" opacity={0.6} rx={4} ry={2} />
<Path
stroke="#2e7d32"
strokeLinecap="round"
strokeWidth={1.5}
d="M16-16v3m0 0-5 2m5-2 5 2m-5-2-3-2m3 2 3-2"
/>
</G>
{/* Sağ Malzemeler */}
<G id="right-ingredients" transform="translate(290 182)">
<Circle cx={-14} cy={2} r={15} fill="#e53935" stroke="#b71c1c" strokeWidth={0.8} />
<Ellipse cx={-18} cy={-3} fill="#ff8a80" opacity={0.6} rx={4} ry={2} />
<Path
stroke="#2e7d32"
strokeLinecap="round"
strokeWidth={1.5}
d="M-14-13v3m0 0-4 2m4-2 4 2"
/>
<Path fill="#2e7d32" stroke="#1b5e20" strokeWidth={0.8} d="M-2 0C8-16 26-12 24 2 22 14 6 10-2 0Z" />
<Path fill="none" stroke="#4caf50" strokeWidth={0.7} d="M-2 0q14-4 26 2" />
<Path fill="#388e3c" stroke="#1b5e20" strokeWidth={0.8} d="M8 6c10-6 24 4 18 16-8 6-18-4-18-16Z" />
<Path fill="none" stroke="#81c784" strokeWidth={0.7} d="M8 6q12 6 18 16" />
</G>
{/* Alt Pizza Dilimi */}
<G id="footer-pizza-slice" transform="rotate(-10 3278.565 44.969)">
<Path fill="#deac68" stroke="#8d6e63" strokeWidth={1.2} d="m0 0 46-18c4 8 6 32 0 40Z" />
<Path fill="#fbc02d" d="m2 0 40-15c3 7 5 25 0 33Z" />
<Circle cx={28} cy={-5} r={5} fill="#c62828" stroke="#8e0000" strokeWidth={0.8} />
<Circle cx={32} cy={8} r={4.5} fill="#c62828" stroke="#8e0000" strokeWidth={0.8} />
<Circle cx={16} r={3.5} fill="#c62828" stroke="#8e0000" strokeWidth={0.8} />
<Ellipse cx={23} cy={4} fill="#2e7d32" rx={2} ry={4} transform="rotate(30 23 4)" />
<Ellipse cx={34} cy={-11} fill="#2e7d32" rx={2} ry={3} transform="rotate(-40 34 -11)" />
</G>
{/* Şarap Kadehi */}
<G id="footer-wine-glass" transform="translate(345 562)">
<Path fill="none" stroke="#71717a" strokeWidth={1.2} d="M-14-24C-15-8-11 4 0 6 11 4 15-8 14-24Z" />
<Path fill="url(#wine-grad-3)" d="M-13-12C-11 0-7 4 0 5.5 7 4 11 0 13-12Z" />
<Ellipse cy={-12} fill="#991b1b" rx={13} ry={2} />
<Path stroke="#71717a" strokeWidth={1.4} d="M0 6v22" />
<Ellipse cy={28} fill="#e4e4e7" stroke="#71717a" strokeWidth={1.2} rx={11} ry={2} />
<Path fill="none" stroke="#fff" strokeLinecap="round" d="M-10-20c-2 8 1 18 6 23" opacity={0.6} />
</G>
{/* QR Köşe Aksanları */}
<G id="qr-corner-accents">
<Path fill="none" stroke="#2e7d32" strokeLinecap="round" strokeWidth={3} d="M78 227v-22h22" />
<Path fill="none" stroke="#b92b27" strokeLinecap="round" strokeWidth={3} d="M322 227v-22h-22M78 423v22h22" />
<Path fill="none" stroke="#2e7d32" strokeLinecap="round" strokeWidth={3} d="M322 423v22h-22" />
</G>
</G>
{/* QR Placeholder / Gerçek QR */}
{!hideQr && (
<G id="qr-placeholder" transform="translate(90 215)">
<Rect width={220} height={220} fill="#fff" stroke="#e4e4e7" strokeWidth={2} rx={20} />
{qrBase64 ? (
<Image
href={`data:image/png;base64,${qrBase64}`}
x={10}
y={10}
width={200}
height={200}
/>
) : (
<>
<Rect
width={196}
height={196}
x={12}
y={12}
fill="#fafafa"
stroke="#e4e4e7"
strokeDasharray="4 3"
rx={14}
/>
<Text
x={110}
y={115}
fill="#71717a"
fontFamily="System, sans-serif"
fontSize={12}
fontWeight={700}
textAnchor="middle"
>
{"QR KOD ALANI"}
</Text>
</>
)}
</G>
)}
{/* Metinler (Çok satırlı destekli) */}
<G id="layer-texts">
{/* Masa No */}
{table?.visible !== false &&
renderSvgLines({
text: table?.text || "MASA NO: 01",
x: 200,
y: 50,
offsetY: table?.offsetY,
fill: "#18181b",
fontFamily: infoFont,
fontSize: table?.fontSize || 11,
fontWeight: "bold",
letterSpacing: 1.2,
})}
<Text
x={200}
y={96}
fill="#3e2723"
fontFamily={rNameFont}
fontSize={24}
textAnchor="middle"
>
{"PIZZERIA"}
</Text>
{/* Restoran Adı */}
{rName?.visible !== false &&
renderSvgLines({
text: rName?.text || "BELLA NAPOLI",
x: 200,
y: 130,
offsetY: rName?.offsetY,
fill: "#18181b",
fontFamily: rNameFont,
fontSize: rName?.fontSize || 26,
fontWeight: "bold",
letterSpacing: 1,
})}
{/* Slogan (Enter ile çok satır olabilir) */}
{sub?.visible !== false &&
renderSvgLines({
text: sub?.text || "AUTENTICA CUCINA ITALIANA",
x: 200,
y: 222,
offsetY: sub?.offsetY,
fill: "#2e7d32",
fontFamily: subFont,
fontSize: sub?.fontSize || 11.5,
fontWeight: "bold",
letterSpacing: 1.8,
})}
{/* CTA */}
{cta?.visible !== false &&
renderSvgLines({
text: cta?.text || "MENÜYÜ GÖRMEK İÇİN OKUTUN",
x: 200,
y: 474,
offsetY: cta?.offsetY,
fill: "#b92b27",
fontFamily: ctaFont,
fontSize: cta?.fontSize || 13,
fontWeight: "bold",
letterSpacing: 1.2,
})}
{/* WiFi */}
{wifi?.visible !== false &&
renderSvgLines({
text: wifi?.text || "WiFi: GourmetBistro | Şifre: lezzetli01",
x: 200,
y: 546,
offsetY: wifi?.offsetY,
fill: "#3e2723",
fontFamily: infoFont,
fontSize: wifi?.fontSize || 10.5,
fontWeight: "bold",
})}
{/* Sosyal Medya */}
{social?.visible !== false &&
renderSvgLines({
text: social?.text || "BİZİ TAKİP EDİN | @gourmetbistro",
x: 200,
y: 570,
offsetY: social?.offsetY,
fill: "#b92b27",
fontFamily: infoFont,
fontSize: social?.fontSize || 10.5,
fontWeight: "bold",
letterSpacing: 0.5,
})}
</G>
</Svg>
);
};
export default QrTemplate3;
@@ -0,0 +1,280 @@
import * as React from "react";
import Svg, {
Defs,
G,
Path,
Circle,
Use,
Rect,
Text,
Image,
} from "react-native-svg";
import { type QrTemplateProps, getFontFamily, renderSvgLines } from "./common";
const QrTemplate4 = ({
qrBase64,
fields,
hideQr,
...props
}: QrTemplateProps) => {
const rName = fields?.restaurantName;
const sub = fields?.subtitle;
const table = fields?.tableNo;
const cta = fields?.ctaText;
const wifi = fields?.wifiText;
const social = fields?.socialText;
const rNameFont = getFontFamily(rName?.fontId, "Georgia, 'Times New Roman', serif");
const subFont = getFontFamily(sub?.fontId, "System, -apple-system, Roboto, sans-serif");
const ctaFont = getFontFamily(cta?.fontId, "Georgia, 'Times New Roman', serif");
const infoFont = getFontFamily(wifi?.fontId, "System, -apple-system, Roboto, sans-serif");
return (
<Svg width={300} height={435} viewBox="0 0 400 580" {...props}>
<Defs>
<G id="b4">
<Path
fill="none"
stroke="#c59b4b"
strokeWidth={1.3}
d="M0-22c22-8 36 6 38 22-2 16-16 30-38 22Z"
/>
<Path
fill="none"
stroke="#c59b4b"
strokeWidth={0.9}
d="M0-12c14-6 24 2 26 12-2 10-12 18-26 12Z"
/>
<Path stroke="#c59b4b" strokeWidth={0.8} d="M0 0h26" />
<Path
fill="none"
stroke="#c59b4b"
strokeWidth={1.2}
d="M0-8c16-28 34-32 46-18C36-12 20-4 0-8ZM0 8c16 28 34 32 46 18C36 12 20 4 0 8Z"
/>
<Circle cx={48} cy={-28} r={1.5} fill="#c59b4b" />
<Circle cx={48} cy={28} r={1.5} fill="#c59b4b" />
<Path
fill="none"
stroke="#c59b4b"
strokeWidth={1.1}
d="M0-35c24-21 52-15 56-1-14 6-32 12-56 1Zm0 70c24 21 52 15 56 1-14-6-32-12-56-1Z"
/>
<Path
fill="none"
stroke="#c59b4b"
d="M28-44c14-20 36-10 30 2-10 6-20 4-30-2Zm0 88c14 20 36 10 30-2-10-6-20-4-30 2Z"
/>
<Circle cx={60} cy={-44} r={1.2} fill="#c59b4b" />
<Circle cx={60} cy={44} r={1.2} fill="#c59b4b" />
<Circle cx={34} cy={-58} r={1.2} fill="#c59b4b" />
<Circle cx={34} cy={58} r={1.2} fill="#c59b4b" />
<Path
fill="none"
stroke="#c59b4b"
strokeLinecap="round"
d="M0-60c22-15 36-35 32-55-6 15-16 30-32 55M0 60c22 15 36 35 32 55-6-15-16-30-32-55"
/>
<Path
fill="none"
stroke="#c59b4b"
strokeWidth={0.9}
d="M14-78c8-6 12 2 6 6-4 0-6-4-6-6Zm0 156c8 6 12-2 6-6-4 0-6 4-6 6Z"
/>
<Circle cx={32} cy={-116} r={1.5} fill="#c59b4b" />
<Circle cx={32} cy={116} r={1.5} fill="#c59b4b" />
</G>
<G id="a4">
<Path
fill="none"
stroke="#c59b4b"
strokeWidth={1.3}
d="M18 64c0-28 18-46 46-46"
/>
<Path
fill="none"
stroke="#c59b4b"
strokeWidth={0.8}
d="M24 52c0-16 12-28 28-28"
/>
<Path
fill="none"
stroke="#c59b4b"
strokeLinecap="round"
strokeWidth={0.9}
d="M28 32c4-6 10-4 8 2-2 4-8 2-6-4s10-6 14-2"
/>
<Circle cx={28} cy={28} r={1.8} fill="#c59b4b" />
<Circle cx={48} cy={18} r={1.4} fill="#c59b4b" />
<Circle cx={18} cy={48} r={1.4} fill="#c59b4b" />
</G>
</Defs>
<Path fill="#fdfdfd" d="M0 0h400v580H0z" />
<Path
fill="none"
stroke="#c59b4b"
strokeWidth={1.2}
d="M20 14h360v552H20z"
/>
<Use href="#a4" />
<Use href="#a4" transform="matrix(-1 0 0 1 400 0)" />
<Use href="#a4" transform="matrix(1 0 0 -1 0 580)" />
<Use href="#a4" transform="rotate(180 200 290)" />
<Path
fill="none"
stroke="#c59b4b"
strokeLinecap="round"
strokeWidth={1.2}
d="M132 62c8-4 16 2 24-2 4-2 2-6-2-4m114 6c-8-4-16 2-24-2-4-2-2-6 2-4"
/>
<Use href="#b4" transform="translate(0 310)" />
<Use href="#b4" transform="matrix(-1 0 0 1 400 310)" />
<Path
fill="none"
stroke="#c59b4b"
strokeLinecap="square"
strokeWidth={2.2}
d="M114 278v-58h54m118 58v-58h-54M114 342v58h54m118-58v58h-54"
/>
{/* QR Placeholder / Gerçek QR */}
{!hideQr && (
<G transform="translate(125 230)">
<Rect
width={150}
height={150}
fill="#fff"
stroke="#eae5d9"
strokeWidth={1.5}
rx={12}
/>
{qrBase64 ? (
<Image
href={`data:image/png;base64,${qrBase64}`}
x={8}
y={8}
width={134}
height={134}
/>
) : (
<>
<Rect
width={134}
height={134}
x={8}
y={8}
fill="#fafaf8"
stroke="#c59b4b"
strokeDasharray="4 3"
strokeOpacity={0.35}
rx={8}
/>
<Text
x={75}
y={79}
fill="#c59b4b"
fontFamily="System, sans-serif"
fontSize={10}
fontWeight={700}
letterSpacing={1}
textAnchor="middle"
>
{"QR KOD ALANI"}
</Text>
</>
)}
</G>
)}
{/* Metinler */}
<G fill="#111" textAnchor="middle">
{/* Masa No */}
{table?.visible !== false &&
renderSvgLines({
text: table?.text || "MASA NO: 01",
x: 200,
y: 56,
offsetY: table?.offsetY,
fill: "#c59b4b",
fontFamily: infoFont,
fontSize: table?.fontSize || 11,
fontWeight: "bold",
letterSpacing: 1.5,
})}
{/* Restoran Başlığı (Enter ile alt satıra geçebilir) */}
{rName?.visible !== false &&
renderSvgLines({
text: rName?.text || "THE CRIMSON PETAL",
x: 200,
y: 114,
offsetY: rName?.offsetY,
fill: "#111111",
fontFamily: rNameFont,
fontSize: rName?.fontSize || 24,
fontWeight: "bold",
letterSpacing: 1.5,
})}
{/* Slogan */}
{sub?.visible !== false &&
renderSvgLines({
text: sub?.text || "FINE DINING EXPERIENCE",
x: 200,
y: 154,
offsetY: sub?.offsetY,
fill: "#71717a",
fontFamily: subFont,
fontSize: sub?.fontSize || 11,
fontWeight: 600,
letterSpacing: 1.8,
})}
{/* CTA Metni */}
{cta?.visible !== false &&
renderSvgLines({
text: cta?.text || "MENÜYÜ GÖRMEK İÇİN OKUTUN",
x: 200,
y: 440,
offsetY: cta?.offsetY,
fill: "#111111",
fontFamily: ctaFont,
fontSize: cta?.fontSize || 15,
fontWeight: "bold",
letterSpacing: 1,
})}
{/* WiFi Bilgisi */}
{wifi?.visible !== false &&
renderSvgLines({
text: wifi?.text || "WiFi: CrimsonGuest | Şifre: petal2026",
x: 200,
y: 488,
offsetY: wifi?.offsetY,
fill: "#71717a",
fontFamily: infoFont,
fontSize: wifi?.fontSize || 10.5,
fontWeight: 600,
letterSpacing: 0.5,
})}
{/* Sosyal Medya */}
{social?.visible !== false &&
renderSvgLines({
text: social?.text || "@thecrimsonpetal",
x: 200,
y: 514,
offsetY: social?.offsetY,
fill: "#c59b4b",
fontFamily: infoFont,
fontSize: social?.fontSize || 10.5,
fontWeight: 600,
letterSpacing: 0.5,
})}
</G>
</Svg>
);
};
export default QrTemplate4;
@@ -0,0 +1,188 @@
import * as React from "react";
import Svg, {
Image,
G,
Rect,
Path,
Text,
} from "react-native-svg";
import { type QrTemplateProps, getFontFamily, renderSvgLines } from "./common";
// eslint-disable-next-line @typescript-eslint/no-require-imports
const bgImage = require("../../../assets/templates/5.png");
const QrTemplate5 = ({
qrBase64,
fields,
hideQr,
...props
}: QrTemplateProps) => {
const rName = fields?.restaurantName;
const sub = fields?.subtitle;
const table = fields?.tableNo;
const cta = fields?.ctaText;
const wifi = fields?.wifiText;
const social = fields?.socialText;
const rNameFont = getFontFamily(rName?.fontId, "System, -apple-system, Roboto, sans-serif");
const subFont = getFontFamily(sub?.fontId, "System, -apple-system, Roboto, sans-serif");
const ctaFont = getFontFamily(cta?.fontId, "System, -apple-system, Roboto, sans-serif");
const infoFont = getFontFamily(wifi?.fontId, "System, -apple-system, Roboto, sans-serif");
return (
<Svg width={300} height={394.7} viewBox="0 0 380 500" {...props}>
{/* PNG Arka Plan */}
<Image
width={380}
height={500}
href={bgImage}
preserveAspectRatio="none"
/>
{/* QR Placeholder / Live QR */}
{!hideQr && (
<G transform="translate(108 206)">
<Rect
width={164}
height={164}
fill="#fff"
stroke="#dce5ea"
strokeWidth={1.2}
rx={16}
/>
<Path
fill="none"
stroke="#003554"
strokeLinecap="round"
strokeWidth={3}
d="M6 30V12a6 6 0 0 1 6-6h18m128 24V12a6 6 0 0 0-6-6h-18M6 134v18a6 6 0 0 0 6 6h18m128-24v18a6 6 0 0 1-6 6h-18"
/>
{qrBase64 ? (
<Image
href={`data:image/png;base64,${qrBase64}`}
x={12}
y={12}
width={140}
height={140}
/>
) : (
<>
<Rect
width={140}
height={140}
x={12}
y={12}
fill="#fafcfd"
stroke="#003554"
strokeDasharray="4 3"
strokeOpacity={0.2}
rx={10}
/>
<Text
x={82}
y={87}
fill="#003554"
fontFamily="System, sans-serif"
fontSize={11}
fontWeight={700}
letterSpacing={1}
textAnchor="middle"
>
{"QR KOD ALANI"}
</Text>
</>
)}
</G>
)}
{/* Metinler */}
<G fill="#003554" textAnchor="middle">
{/* Masa Numarası */}
{table?.visible !== false &&
renderSvgLines({
text: table?.text || "MASA NO: 01",
x: 190,
y: 40,
offsetY: table?.offsetY,
fill: "#003554",
fontFamily: infoFont,
fontSize: table?.fontSize || 11,
fontWeight: "bold",
letterSpacing: 1.5,
})}
{/* Restoran Başlığı */}
{rName?.visible !== false &&
renderSvgLines({
text: rName?.text || "THE URBAN FORK",
x: 190,
y: 86,
offsetY: rName?.offsetY,
fill: "#003554",
fontFamily: rNameFont,
fontSize: rName?.fontSize || 24,
fontWeight: "bold",
letterSpacing: 1,
})}
{/* Yönlendirme (CTA) */}
{cta?.visible !== false &&
renderSvgLines({
text: cta?.text || "DİJİTAL MENÜYÜ GÖRÜNTÜLE",
x: 190,
y: 160,
offsetY: cta?.offsetY,
fill: "#003554",
fontFamily: ctaFont,
fontSize: cta?.fontSize || 13,
fontWeight: "bold",
letterSpacing: 0.5,
})}
{/* Slogan (Enter ile çok satır olabilir) */}
{sub?.visible !== false &&
renderSvgLines({
text: sub?.text || "FRESH FLAVORS DAILY",
x: 190,
y: 402,
offsetY: sub?.offsetY,
fill: "#003554",
fontFamily: subFont,
fontSize: sub?.fontSize || 13,
fontWeight: "bold",
letterSpacing: 0.8,
})}
{/* WiFi */}
{wifi?.visible !== false &&
renderSvgLines({
text: wifi?.text || "WiFi: UrbanFork | Şifre: urban2026",
x: 190,
y: 430,
offsetY: wifi?.offsetY,
fill: "#003554",
fontFamily: infoFont,
fontSize: wifi?.fontSize || 10,
fontWeight: 700,
letterSpacing: 0.2,
})}
{/* Sosyal Medya */}
{social?.visible !== false &&
renderSvgLines({
text: social?.text || "@theurbanfork",
x: 190,
y: 454,
offsetY: social?.offsetY,
fill: "#003554",
fontFamily: infoFont,
fontSize: social?.fontSize || 10,
fontWeight: 700,
letterSpacing: 0.2,
})}
</G>
</Svg>
);
};
export default QrTemplate5;
@@ -0,0 +1,263 @@
import * as React from "react";
import Svg, {
Defs,
G,
Path,
Circle,
Rect,
Use,
Text,
Image,
} from "react-native-svg";
import { type QrTemplateProps, getFontFamily, renderSvgLines } from "./common";
const QrTemplate6 = ({
qrBase64,
fields,
hideQr,
...props
}: QrTemplateProps) => {
const rName = fields?.restaurantName;
const sub = fields?.subtitle;
const table = fields?.tableNo;
const cta = fields?.ctaText;
const wifi = fields?.wifiText;
const social = fields?.socialText;
const rNameFont = getFontFamily(rName?.fontId, "Georgia, 'Times New Roman', serif");
const subFont = getFontFamily(sub?.fontId, "System, -apple-system, Roboto, sans-serif");
const ctaFont = getFontFamily(cta?.fontId, "Georgia, 'Times New Roman', serif");
const infoFont = getFontFamily(wifi?.fontId, "System, -apple-system, Roboto, sans-serif");
return (
<Svg width={300} height={435} viewBox="0 0 400 580" {...props}>
<Defs>
<G id="b6">
<Path
fill="none"
stroke="#c59b58"
strokeLinecap="round"
strokeWidth={1.2}
d="M0 0c10 25 12 55 5 85"
/>
<Path
fill="#f4efe6"
stroke="#c59b58"
strokeWidth={1.1}
d="M3 15c-11-5-15 9-1 9 4 0 4-6 1-9Z"
/>
<Path
fill="#f4efe6"
stroke="#9e4d28"
strokeWidth={1.1}
d="M5 22c11-6 17 6 3 10-4 0-4-6-3-10Zm2 20c-12-6-17 8-3 10 4 0 4-6 3-10Z"
/>
<Path
fill="#f4efe6"
stroke="#c59b58"
strokeWidth={1.1}
d="M8 50c12-6 16 8 2 12-4 0-4-7-2-12ZM6 70c-10-5-14 8-2 10 3 0 4-6 2-10Z"
/>
<Path
fill="#f4efe6"
stroke="#9e4d28"
strokeWidth={1.1}
d="M5 78c11-4 15 8 3 12-4 0-4-6-3-12Z"
/>
<Circle cx={-10} cy={18} r={1.5} fill="#c59b58" />
<Circle cx={18} cy={46} r={1.5} fill="#9e4d28" />
</G>
<G id="a6">
<Path
fill="#a64423"
d="M0-18C4-12 8-6 8 0c6-4 8 6 4 12 4-2 6 4 2 8S2 26 0 26s-10-2-14-6-2-10 2-8c-4-6-2-16 4-12 0-6 4-12 8-18"
/>
<Path
fill="#f4efe6"
d="M-4 4v8c0 3 2 4 4 4v8c2-8 4-9 4-12V4H3v7c0 2-1.5 3-3 3s-3-1-3-3V4Z"
/>
<Path stroke="#f4efe6" d="M0 4v8" />
</G>
</Defs>
<Path fill="#434b54" d="M0 0h400v580H0z" />
<Rect
width={360}
height={548}
x={20}
y={16}
fill="#f6efe3"
stroke="#122a42"
strokeWidth={1.5}
rx={16}
/>
<Path
fill="none"
stroke="#122a42"
strokeWidth={1.2}
d="M46 32h308a12 12 0 0 1 12 12v492a12 12 0 0 1-12 12H46a12 12 0 0 1-12-12V44a12 12 0 0 1 12-12Z"
/>
<Path
fill="none"
stroke="#122a42"
strokeWidth={1.2}
d="M34 46a12 12 0 0 0 12-12m320 12a12 12 0 0 1-12-12M34 534a12 12 0 0 1 12 12m320-12a12 12 0 0 0-12 12"
/>
<Path stroke="#c59b58" d="M34 135h332M34 500h332" />
<Use href="#a6" transform="matrix(.85 0 0 .85 200 52)" />
<Use href="#b6" transform="matrix(.8 0 0 .8 42 42)" />
<Use href="#b6" transform="matrix(-.8 0 0 .8 358 42)" />
<Use href="#b6" transform="matrix(.95 0 0 .95 40 185)" />
<Use href="#b6" transform="matrix(-.95 0 0 .95 360 185)" />
<Use href="#a6" transform="matrix(.65 0 0 .65 68 320)" />
<Use href="#a6" transform="matrix(.65 0 0 .65 332 320)" />
<Use href="#b6" transform="matrix(.95 0 0 .95 40 360)" />
<Use href="#b6" transform="matrix(-.95 0 0 .95 360 360)" />
{/* QR Placeholder / Gerçek QR */}
{!hideQr && (
<G transform="translate(95 205)">
<Path
fill="#fdfbf7"
stroke="#122a42"
strokeWidth={3}
d="M0 0h210v210H0z"
/>
<Path
fill="none"
stroke="#c59b58"
strokeWidth={1.5}
d="M4 4h202v202H4z"
/>
{qrBase64 ? (
<Image
href={`data:image/png;base64,${qrBase64}`}
x={10}
y={10}
width={190}
height={190}
/>
) : (
<>
<Path
fill="#faf8f5"
stroke="#122a42"
strokeDasharray="4 3"
d="M12 12h186v186H12z"
/>
<Circle
cx={105}
cy={95}
r={16}
fill="#f6efe3"
stroke="#c59b58"
strokeWidth={1.2}
/>
<Use href="#a6" transform="matrix(.45 0 0 .45 105 93)" />
<Text
x={105}
y={128}
fill="#122a42"
fontFamily="System, sans-serif"
fontSize={11}
fontWeight="bold"
letterSpacing={1.5}
textAnchor="middle"
>
{"QR KOD ALANI"}
</Text>
</>
)}
</G>
)}
{/* Metinler */}
<G textAnchor="middle">
{/* Masa Numarası */}
{table?.visible !== false &&
renderSvgLines({
text: table?.text || "MASA NO: 01",
x: 200,
y: 36,
offsetY: table?.offsetY,
fill: "#122a42",
fontFamily: infoFont,
fontSize: table?.fontSize || 10,
fontWeight: "bold",
letterSpacing: 1.5,
})}
{/* Restoran Başlığı */}
{rName?.visible !== false &&
renderSvgLines({
text: rName?.text || "THE STONEHEARTH GRILL",
x: 200,
y: 84,
offsetY: rName?.offsetY,
fill: "#a64423",
fontFamily: rNameFont,
fontSize: rName?.fontSize || 19,
fontWeight: "bold",
letterSpacing: 1.5,
})}
{/* Slogan */}
{sub?.visible !== false &&
renderSvgLines({
text: sub?.text || "SCAN. ORDER. ENJOY.",
x: 200,
y: 118,
offsetY: sub?.offsetY,
fill: "#122a42",
fontFamily: subFont,
fontSize: sub?.fontSize || 10,
fontWeight: "bold",
letterSpacing: 2,
})}
{/* CTA */}
{cta?.visible !== false &&
renderSvgLines({
text: cta?.text || "VIEW OUR MENU & ORDER ONLINE",
x: 200,
y: 160,
offsetY: cta?.offsetY,
fill: "#122a42",
fontFamily: ctaFont,
fontSize: cta?.fontSize || 13,
fontWeight: "bold",
letterSpacing: 0.5,
})}
{/* WiFi */}
{wifi?.visible !== false &&
renderSvgLines({
text: wifi?.text || "FREE WIFI: StonehearthGuest | PW: Grill720",
x: 200,
y: 524,
offsetY: wifi?.offsetY,
fill: "#222",
fontFamily: infoFont,
fontSize: wifi?.fontSize || 9,
fontWeight: "bold",
letterSpacing: 0.2,
})}
{/* Sosyal Medya */}
{social?.visible !== false &&
renderSvgLines({
text: social?.text || "Follow Us @StonehearthGrill",
x: 200,
y: 540,
offsetY: social?.offsetY,
fill: "#222",
fontFamily: infoFont,
fontSize: social?.fontSize || 9,
fontWeight: "bold",
letterSpacing: 0.2,
})}
</G>
</Svg>
);
};
export default QrTemplate6;
@@ -0,0 +1,212 @@
import * as React from "react";
import Svg, {
G,
Path,
Circle,
Rect,
Text,
Image,
} from "react-native-svg";
import { type QrTemplateProps, getFontFamily, renderSvgLines } from "./common";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const template8Bg = require("../../../assets/templates/8.jpeg");
const QrTemplate8 = ({
qrBase64,
fields,
hideQr,
...props
}: QrTemplateProps) => {
const rName = fields?.restaurantName;
const sub = fields?.subtitle;
const table = fields?.tableNo;
const cta = fields?.ctaText;
const wifi = fields?.wifiText;
const social = fields?.socialText;
const rNameFont = getFontFamily(rName?.fontId, "System, -apple-system, Roboto, sans-serif");
const subFont = getFontFamily(sub?.fontId, "System, -apple-system, Roboto, sans-serif");
const ctaFont = getFontFamily(cta?.fontId, "System, -apple-system, Roboto, sans-serif");
const infoFont = getFontFamily(wifi?.fontId, "System, -apple-system, Roboto, sans-serif");
return (
<Svg width={300} height={300} viewBox="0 0 1024 1024" {...props}>
<Path fill="#0c4443" d="M0 0h1024v1024H0z" />
<Image
width={1024}
height={1024}
href={template8Bg}
preserveAspectRatio="none"
/>
{/* QR Alanı (Merkez) */}
{!hideQr && (
<G transform="translate(348 305)">
<Rect
width={328}
height={336}
fill="#f9f5eb"
stroke="#d1a752"
strokeWidth={1.5}
rx={16}
/>
<Rect
width={304}
height={312}
x={12}
y={12}
fill="#faf8f2"
stroke="#0c4443"
strokeDasharray="6 4"
strokeOpacity={0.3}
strokeWidth={1.5}
rx={12}
/>
{qrBase64 ? (
<Image
href={`data:image/png;base64,${qrBase64}`}
x={18}
y={18}
width={292}
height={300}
/>
) : (
<>
<Circle cx={164} cy={148} r={28} fill="#0c4443" />
<G fill="#d1a752">
<Path d="M164 132.6c4.4 6.6 4.4 15.4 0 24.2-4.4-8.8-4.4-17.6 0-24.2" />
<Path d="M161.8 150.2c-8.8-4.4-13.2 4.4-6.6 8.8 6.6 0 8.8-6.6 6.6-8.8m4.4 0c8.8-4.4 13.2 4.4 6.6 8.8-6.6 0-8.8-6.6-6.6-8.8" />
</G>
<Text
x={164}
y={218}
fill="#0c4443"
fontFamily="System, -apple-system, Roboto, sans-serif"
fontSize={14}
fontWeight={800}
letterSpacing={2}
textAnchor="middle"
>
{"QR KOD ALANI"}
</Text>
</>
)}
</G>
)}
{/* Üst Çizgiler & OR Ayracı */}
<Path
stroke="#d1a752"
strokeLinecap="round"
strokeWidth={2}
d="M210 188h240"
/>
<Text
x={512}
y={194}
fill="#d1a752"
fontFamily="System, -apple-system, Roboto, sans-serif"
fontSize={24}
fontWeight={900}
letterSpacing={2}
textAnchor="middle"
>
{"OR"}
</Text>
<Path
stroke="#d1a752"
strokeLinecap="round"
strokeWidth={2}
d="M574 188h240"
/>
{/* Masa Numarası (En Üst) */}
{table?.visible !== false &&
renderSvgLines({
text: table?.text || "MASA NO: 01",
x: 512,
y: 70,
offsetY: table?.offsetY,
fill: "#d1a752",
fontFamily: infoFont,
fontSize: table?.fontSize ? table.fontSize * 1.8 : 22,
fontWeight: 800,
letterSpacing: 2,
})}
{/* Restoran Başlığı / Üst Başlık */}
{rName?.visible !== false &&
renderSvgLines({
text: rName?.text || "EXPLORE OUR MENU",
x: 512,
y: 136,
offsetY: rName?.offsetY,
fill: "#fff",
fontFamily: rNameFont,
fontSize: rName?.fontSize ? rName.fontSize * 1.8 : 50,
fontWeight: 900,
letterSpacing: 2,
})}
{/* Slogan / Alt Başlık */}
{sub?.visible !== false &&
renderSvgLines({
text: sub?.text || "THE DAILY SPECIALS",
x: 512,
y: 242,
offsetY: sub?.offsetY,
fill: "#d1a752",
fontFamily: subFont,
fontSize: sub?.fontSize ? sub.fontSize * 1.8 : 42,
fontWeight: 900,
letterSpacing: 2,
})}
{/* CTA Metni */}
{cta?.visible !== false &&
renderSvgLines({
text: cta?.text || "SCAN TO VIEW OUR\nFULL MENU & SPECIALS",
x: 512,
y: 730,
offsetY: cta?.offsetY,
fill: "#fff",
fontFamily: ctaFont,
fontSize: cta?.fontSize ? cta.fontSize * 1.8 : 28,
fontWeight: 900,
letterSpacing: 1,
lineHeight: 38,
})}
{/* WiFi / Sipariş Bilgisi */}
{wifi?.visible !== false &&
renderSvgLines({
text: wifi?.text || "SCAN TO ORDER & BROWSE",
x: 512,
y: 846,
offsetY: wifi?.offsetY,
fill: "#d1a752",
fontFamily: infoFont,
fontSize: wifi?.fontSize ? wifi.fontSize * 1.8 : 22,
fontWeight: 900,
letterSpacing: 1.5,
})}
{/* Sosyal Medya */}
{social?.visible !== false &&
renderSvgLines({
text: social?.text || "@menulio.app",
x: 512,
y: 916,
offsetY: social?.offsetY,
fill: "#e5c378",
fontFamily: infoFont,
fontSize: social?.fontSize ? social.fontSize * 1.8 : 20,
fontWeight: 700,
letterSpacing: 1,
})}
</Svg>
);
};
export default QrTemplate8;
@@ -0,0 +1,281 @@
import * as React from "react";
import Svg, {
Defs,
G,
Path,
Circle,
Rect,
Text,
TextPath,
Image,
} from "react-native-svg";
import { type QrTemplateProps, getFontFamily, renderSvgLines } from "./common";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const template9Bg = require("../../../assets/templates/9.jpeg");
const QrTemplate9 = ({
qrBase64,
fields,
hideQr,
...props
}: QrTemplateProps) => {
const rName = fields?.restaurantName;
const sub = fields?.subtitle;
const table = fields?.tableNo;
const cta = fields?.ctaText;
const wifi = fields?.wifiText;
const social = fields?.socialText;
const rNameFont = getFontFamily(rName?.fontId, "System, -apple-system, Roboto, sans-serif");
const subFont = getFontFamily(sub?.fontId, "System, -apple-system, Roboto, sans-serif");
const ctaFont = getFontFamily(cta?.fontId, "System, -apple-system, Roboto, sans-serif");
const infoFont = getFontFamily(wifi?.fontId, "System, -apple-system, Roboto, sans-serif");
return (
<Svg width={300} height={300} viewBox="0 0 1024 1024" {...props}>
<Defs>
<Path id="arcTop9" fill="none" d="M330 260q182-115 364 0" />
</Defs>
<Path fill="#f8f6f0" d="M0 0h1024v1024H0z" />
<Image
width={1024}
height={1024}
href={template9Bg}
preserveAspectRatio="none"
/>
{/* QR Alanı (Merkez) */}
{!hideQr && (
<G transform="translate(362 428)">
<Rect
width={300}
height={300}
fill="#fff"
stroke="#d5cebe"
strokeWidth={1.5}
rx={24}
/>
<Rect
width={276}
height={276}
x={12}
y={12}
fill="#fafaf7"
stroke="#1e3246"
strokeDasharray="6 4"
strokeOpacity={0.35}
strokeWidth={1.5}
rx={16}
/>
{qrBase64 ? (
<Image
href={`data:image/png;base64,${qrBase64}`}
x={16}
y={16}
width={268}
height={268}
/>
) : (
<>
<Circle
cx={150}
cy={132}
r={28}
fill="#f5efe6"
stroke="#1e3246"
strokeWidth={1.5}
/>
<Path
fill="none"
stroke="#1e3246"
strokeLinecap="round"
strokeWidth={1.9}
d="M142.4 116.8v11.4c0 3.8 5.7 3.8 5.7 0v-11.4m-2.85 0v11.4m0 3.8v15.2m9.5-30.4c4.75 3.8 4.75 15.2 0 17.1v13.3"
/>
<Text
x={150}
y={192}
fill="#1e3246"
fontFamily="System, -apple-system, Roboto, sans-serif"
fontSize={14}
fontWeight={800}
letterSpacing={2}
textAnchor="middle"
>
{"QR KOD ALANI"}
</Text>
</>
)}
</G>
)}
{/* Üst Amblem & Rozet */}
<G stroke="#a88b58" transform="translate(290 142)">
<Circle cx={-16} cy={-8} r={4.5} fill="none" strokeWidth={1.5} />
<Path strokeLinecap="round" strokeWidth={1.5} d="M-16-12v8m-4-4h8" />
<Path
fill="none"
strokeLinecap="round"
strokeWidth={1.5}
d="M0-12c4 0 8 6 8 12S4 12 0 12"
/>
<Path
fill="none"
strokeLinecap="round"
strokeWidth={1.2}
d="M-32 20c12 8 52 8 64 0"
/>
</G>
{/* Masa Numarası / Rozet Metni */}
{table?.visible !== false &&
renderSvgLines({
text: table?.text || "TABLE MENU\nEST. 2026",
x: 290,
y: 164,
offsetY: table?.offsetY,
fill: "#1e3246",
fontFamily: infoFont,
fontSize: table?.fontSize ? table.fontSize * 1.3 : 15,
fontWeight: 900,
letterSpacing: 1,
lineHeight: 18,
})}
{/* Sol / Sağ Yönelticiler (SIAT / VIEW) */}
<Text
x={364}
y={248}
fill="#1e3246"
fontFamily="System, -apple-system, Roboto, sans-serif"
fontSize={23}
fontWeight={900}
letterSpacing={1}
textAnchor="middle"
transform="rotate(-26 364 248)"
>
{"SCAN"}
</Text>
<Text
x={670}
y={244}
fill="#1e3246"
fontFamily="System, -apple-system, Roboto, sans-serif"
fontSize={25}
fontWeight={900}
letterSpacing={1}
textAnchor="middle"
transform="rotate(24 670 244)"
>
{"VIEW"}
</Text>
{/* Kavisli Üst Yazı */}
{sub?.visible !== false && (
<Text
fill="#1e3246"
fontFamily={subFont}
fontSize={sub?.fontSize ? sub.fontSize * 1.8 : 46}
fontWeight={900}
letterSpacing={2}
>
<TextPath href="#arcTop9" startOffset="50%" textAnchor="middle">
{sub?.text || "DIGITAL MENU"}
</TextPath>
</Text>
)}
{/* Restoran Başlığı / Ana Başlık */}
{rName?.visible !== false &&
renderSvgLines({
text: rName?.text || "OUR MENU",
x: 512,
y: 326,
offsetY: rName?.offsetY,
fill: "#1e3246",
fontFamily: rNameFont,
fontSize: rName?.fontSize ? rName.fontSize * 2.4 : 76,
fontWeight: 900,
letterSpacing: 3,
})}
{/* Yönlendirici Oklar */}
<G stroke="#1e3246">
<Path
fill="none"
strokeLinecap="round"
strokeWidth={3}
d="M390 780c-16-8-16-26-8-36"
/>
<Path
fill="#1e3246"
strokeLinejoin="round"
strokeWidth={1.5}
d="m374 750 8-6 6 8Z"
/>
</G>
<G stroke="#1e3246">
<Path
fill="none"
strokeLinecap="round"
strokeWidth={3}
d="M634 780c16-8 16-26 8-36"
/>
<Path
fill="#1e3246"
strokeLinejoin="round"
strokeWidth={1.5}
d="m650 750-8-6-6 8Z"
/>
</G>
{/* CTA Metni */}
{cta?.visible !== false &&
renderSvgLines({
text: cta?.text || "VIEW OUR\nDELICIOUS MENU",
x: 512,
y: 752,
offsetY: cta?.offsetY,
fill: "#1e3246",
fontFamily: ctaFont,
fontSize: cta?.fontSize ? cta.fontSize * 1.6 : 20,
fontWeight: 900,
letterSpacing: 1,
lineHeight: 28,
})}
{/* Alt Bilgi / Karşılama & WiFi */}
{wifi?.visible !== false &&
renderSvgLines({
text: wifi?.text || "WELCOME TO OUR TABLE",
x: 500,
y: 894,
offsetY: wifi?.offsetY,
fill: "#1e3246",
fontFamily: infoFont,
fontSize: wifi?.fontSize ? wifi.fontSize * 1.5 : 18,
fontWeight: 900,
letterSpacing: 1.5,
})}
{/* Sosyal Medya */}
{social?.visible !== false &&
renderSvgLines({
text: social?.text || "@menulio.table",
x: 500,
y: 934,
offsetY: social?.offsetY,
fill: "#a88b58",
fontFamily: infoFont,
fontSize: social?.fontSize ? social.fontSize * 1.5 : 16,
fontWeight: 700,
letterSpacing: 1,
})}
</Svg>
);
};
export default QrTemplate9;
@@ -0,0 +1,217 @@
import * as React from "react";
import Svg, {
Path,
Rect,
Circle,
G,
Text,
Image,
} from "react-native-svg";
import { type QrTemplateProps, getFontFamily, renderSvgLines } from "./common";
const QrTemplateLumiere = ({
qrBase64,
fields,
hideQr,
...props
}: QrTemplateProps) => {
const rName = fields?.restaurantName;
const sub = fields?.subtitle;
const table = fields?.tableNo;
const cta = fields?.ctaText;
const wifi = fields?.wifiText;
const social = fields?.socialText;
const rNameFont = getFontFamily(rName?.fontId, "Georgia, 'Playfair Display', serif");
const subFont = getFontFamily(sub?.fontId, "System, -apple-system, Roboto, sans-serif");
const ctaFont = getFontFamily(cta?.fontId, "Georgia, 'Playfair Display', serif");
const infoFont = getFontFamily(wifi?.fontId, "System, -apple-system, Roboto, sans-serif");
return (
<Svg viewBox="0 0 600 600" width={300} height={300} {...props}>
<Path fill="#0b0d13" d="M0 0h600v600H0z" />
<Rect
width={560}
height={560}
x={20}
y={20}
fill="#132034"
stroke="#d4af37"
strokeWidth={1.5}
rx={24}
/>
<Path
fill="none"
stroke="#d4af37"
strokeOpacity={0.85}
strokeWidth={1.5}
d="M56 40h488a16 16 0 0 1 16 16v488a16 16 0 0 1-16 16H56a16 16 0 0 1-16-16V56a16 16 0 0 1 16-16Z"
/>
<Rect
width={504}
height={504}
x={48}
y={48}
fill="none"
stroke="#c5a059"
strokeOpacity={0.45}
strokeWidth={0.8}
rx={8}
/>
{/* Köşe süslemeleri */}
<Path fill="none" stroke="#d4af37" strokeWidth={1.5} d="M40 68a18 18 0 0 0 28-28" />
<Circle cx={52} cy={52} r={2} fill="#d4af37" />
<Path fill="none" stroke="#d4af37" strokeWidth={1.5} d="M560 68a18 18 0 0 1-28-28" />
<Circle cx={548} cy={52} r={2} fill="#d4af37" />
<Path fill="none" stroke="#d4af37" strokeWidth={1.5} d="M40 532a18 18 0 0 1 28 28" />
<Circle cx={52} cy={548} r={2} fill="#d4af37" />
<Path fill="none" stroke="#d4af37" strokeWidth={1.5} d="M560 532a18 18 0 0 0-28 28" />
<Circle cx={548} cy={548} r={2} fill="#d4af37" />
{/* Merkez amblem */}
<G transform="translate(300 85)">
<Path
fill="none"
stroke="#d4af37"
strokeLinejoin="round"
strokeWidth={2}
d="M0-26C-8-16-8-6 0 0c8-6 8-16 0-26Z"
/>
<Path fill="#d4af37" d="M0-18C-4-12-4-4 0 0c4-4 4-12 0-18" />
<Path
fill="none"
stroke="#d4af37"
strokeLinejoin="round"
strokeWidth={2}
d="M-3 3c-13-2-25 7-21 21C-12 24-3 12-3 3Zm6 0c13-2 25 7 21 21C12 24 3 12 3 3Z"
/>
<Circle cy={10} r={1.8} fill="#d4af37" />
</G>
{/* Yatay çizgi */}
<Path stroke="#d4af37" strokeOpacity={0.4} strokeWidth={0.8} d="M120 198h360" />
{/* QR Placeholder / Gerçek QR */}
{!hideQr && (
<G transform="translate(190 215)">
<Rect width={220} height={220} fill="#fff" stroke="#d4af37" strokeWidth={2} rx={20} />
{qrBase64 ? (
<Image
href={`data:image/png;base64,${qrBase64}`}
x={10}
y={10}
width={200}
height={200}
/>
) : (
<>
<Rect
width={196}
height={196}
x={12}
y={12}
fill="#fafafa"
stroke="#d4af37"
strokeDasharray="4 3"
strokeOpacity={0.4}
rx={14}
/>
<Text
x={110}
y={115}
fill="#71717a"
fontFamily="System, sans-serif"
fontSize={12}
fontWeight={600}
textAnchor="middle"
>
{"QR KOD ALANI"}
</Text>
</>
)}
</G>
)}
{/* Metinler (Çok satırlı ve bağımsız font destekli) */}
<G textAnchor="middle">
{/* Restoran Adı */}
{rName?.visible !== false &&
renderSvgLines({
text: rName?.text || "LUMIÈRE",
x: 300,
y: 148,
offsetY: rName?.offsetY,
fill: "#d4af37",
fontFamily: rNameFont,
fontSize: rName?.fontSize || 30,
fontWeight: "bold",
letterSpacing: 4,
})}
{/* Slogan / Alt Başlık (Enter ile alt satıra geçebilir) */}
{sub?.visible !== false &&
renderSvgLines({
text: sub?.text || "RESTAURANT & BAR | MODERN CUISINE",
x: 300,
y: 178,
offsetY: sub?.offsetY,
fill: "#e5c378",
fontFamily: subFont,
fontSize: sub?.fontSize || 11,
fontWeight: 600,
letterSpacing: 2.5,
})}
{/* CTA */}
{cta?.visible !== false &&
renderSvgLines({
text: cta?.text || "MENÜYÜ GÖRMEK İÇİN OKUTUN",
x: 300,
y: 472,
offsetY: cta?.offsetY,
fill: "#d4af37",
fontFamily: ctaFont,
fontSize: cta?.fontSize || 13,
fontWeight: "bold",
letterSpacing: 2,
})}
{/* Sosyal Medya / İletişim */}
{social?.visible !== false &&
renderSvgLines({
text: social?.text || "Rezervasyon: 0212 555 0199",
x: 300,
y: 504,
offsetY: social?.offsetY,
fill: "#e2ddd2",
fontFamily: infoFont,
fontSize: social?.fontSize || 10.5,
fontWeight: 500,
letterSpacing: 1,
})}
{/* Masa No & WiFi */}
{(table?.visible !== false || wifi?.visible !== false) &&
renderSvgLines({
text: [
table?.visible !== false && table?.text ? table.text : "",
wifi?.visible !== false && wifi?.text ? wifi.text : "",
]
.filter(Boolean)
.join(" • "),
x: 300,
y: 526,
offsetY: table?.offsetY || wifi?.offsetY,
fill: "#c5a059",
fontFamily: infoFont,
fontSize: table?.fontSize || wifi?.fontSize || 10,
fontWeight: 600,
letterSpacing: 1,
})}
</G>
</Svg>
);
};
export default QrTemplateLumiere;
@@ -0,0 +1,120 @@
import React from "react";
import { Text as SvgText, type SvgProps } from "react-native-svg";
export interface TextItemConfig {
text: string;
fontSize?: number;
fontId?: string;
offsetY?: number; // Dikey konum kaydırma (yukarı / aşağı)
visible?: boolean;
}
export interface QrCustomFields {
restaurantName: TextItemConfig;
subtitle: TextItemConfig;
tableNo: TextItemConfig;
ctaText: TextItemConfig;
wifiText: TextItemConfig;
socialText: TextItemConfig;
}
export interface QrTemplateProps extends SvgProps {
qrBase64?: string;
fields?: Partial<QrCustomFields>;
hideQr?: boolean;
}
import { Platform } from "react-native";
// Türkçe karakterleri (İ, ı, Ş, ş, Ğ, ğ, Ü, ü, Ö, ö, Ç, ç) eksiksiz destekleyen sistem fontları
export const FONT_OPTIONS = [
{
id: "sans",
label: "Modern Sans",
fontFamily: Platform.OS === "ios" ? "System" : "sans-serif",
},
{
id: "serif",
label: "Zarif Serif",
fontFamily: Platform.OS === "ios" ? "Georgia" : "serif",
},
{
id: "times",
label: "Klasik (Times)",
fontFamily: Platform.OS === "ios" ? "Times New Roman" : "serif",
},
{
id: "palatino",
label: "Lüks Palatino",
fontFamily: Platform.OS === "ios" ? "Palatino" : "serif",
},
{
id: "trebuchet",
label: "Dinamik",
fontFamily: Platform.OS === "ios" ? "Trebuchet MS" : "sans-serif",
},
];
export function getFontFamily(fontId?: string, defaultFamily = "Georgia"): string {
if (!fontId) {
return defaultFamily.includes(",") ? defaultFamily.split(",")[0].trim().replace(/['"]/g, "") : defaultFamily;
}
const found = FONT_OPTIONS.find((f) => f.id === fontId);
if (found) return found.fontFamily;
if (fontId.includes(",")) {
return fontId.split(",")[0].trim().replace(/['"]/g, "");
}
return fontId;
}
// Çok satırlı (Enter / \n) ve dikey kaydırmalı (offsetY) SVG metin render fonksiyonu
export function renderSvgLines({
text,
x,
y,
offsetY = 0,
lineHeight,
fill,
fontFamily,
fontSize,
fontWeight = "normal",
letterSpacing,
textAnchor = "middle",
}: {
text?: string;
x: number;
y: number;
offsetY?: number;
lineHeight?: number;
fill?: string;
fontFamily?: string;
fontSize?: number;
fontWeight?: string | number;
letterSpacing?: number;
textAnchor?: "middle" | "start" | "end";
}) {
if (!text) return null;
const lines = text.split("\n");
const computedLineHeight = lineHeight || (fontSize ? fontSize * 1.25 : 18);
const startY = y + (offsetY || 0);
return (
<>
{lines.map((line, idx) => (
<SvgText
key={idx}
x={x}
y={startY + idx * computedLineHeight}
fill={fill}
fontFamily={fontFamily}
fontSize={fontSize}
fontWeight={fontWeight}
letterSpacing={letterSpacing}
textAnchor={textAnchor}
>
{line}
</SvgText>
))}
</>
);
}
@@ -0,0 +1,39 @@
import React from "react";
import QrTemplate1 from "./QrTemplateLumiere";
import QrTemplate2 from "./QrTemplate2";
import QrTemplate3 from "./QrTemplate3";
import QrTemplate4 from "./QrTemplate4";
import QrTemplate5 from "./QrTemplate5";
import QrTemplate6 from "./QrTemplate6";
import QrTemplate8 from "./QrTemplate8";
import QrTemplate9 from "./QrTemplate9";
import { type QrTemplateProps } from "./common";
export * from "./common";
// Yeni component eklemek için buraya import edip COMPONENT_TEMPLATES'e kaydet
export const COMPONENT_TEMPLATES: Record<
string,
React.FC<QrTemplateProps>
> = {
"qr-1": QrTemplate1,
"qr-2": QrTemplate2,
"qr-3": QrTemplate3,
"qr-4": QrTemplate4,
"qr-5": QrTemplate5,
"qr-6": QrTemplate6,
"qr-8": QrTemplate8,
"qr-9": QrTemplate9,
};
// Her template'in aspect ratio'su (width/height)
export const TEMPLATE_ASPECT_RATIOS: Record<string, number> = {
"qr-1": 1, // 600x600 = 1:1
"qr-2": 400 / 650, // 400x650 = standart kart
"qr-3": 400 / 650, // 400x650 = standart kart
"qr-4": 400 / 580, // 400x580 = zarif altın çerçeve
"qr-5": 380 / 500, // 380x500 = PNG arka planlı modern kart
"qr-6": 400 / 580, // 400x580 = taş & ızgara vintage stant
"qr-8": 1, // 1024x1024 = 1:1 Emerald Gold Özel Tasarım
"qr-9": 380 / 500, // 380x500 = Vintage Table Menu Özel Tasarım
};
File diff suppressed because one or more lines are too long
+177
View File
@@ -0,0 +1,177 @@
import * as React from "react"
import Svg, { SvgProps, Path, Rect, Circle, G, Text } from "react-native-svg"
const SvgComponent = (props: SvgProps) => (
<Svg xmlns="http://www.w3.org/2000/svg" width={600} height={600} {...props}>
<Path fill="#0b0d13" d="M0 0h600v600H0z" />
<Rect
width={560}
height={560}
x={20}
y={20}
fill="#132034"
stroke="#d4af37"
strokeWidth={1.5}
rx={24}
/>
<Path
fill="none"
stroke="#d4af37"
strokeOpacity={0.85}
strokeWidth={1.5}
d="M56 40h488a16 16 0 0 1 16 16v488a16 16 0 0 1-16 16H56a16 16 0 0 1-16-16V56a16 16 0 0 1 16-16Z"
/>
<Rect
width={504}
height={504}
x={48}
y={48}
fill="none"
stroke="#c5a059"
strokeOpacity={0.45}
strokeWidth={0.8}
rx={8}
/>
<Path
fill="none"
stroke="#d4af37"
strokeWidth={1.5}
d="M40 68a18 18 0 0 0 28-28"
/>
<Circle cx={52} cy={52} r={2} fill="#d4af37" />
<Path
fill="none"
stroke="#d4af37"
strokeWidth={1.5}
d="M560 68a18 18 0 0 1-28-28"
/>
<Circle cx={548} cy={52} r={2} fill="#d4af37" />
<Path
fill="none"
stroke="#d4af37"
strokeWidth={1.5}
d="M40 532a18 18 0 0 1 28 28"
/>
<Circle cx={52} cy={548} r={2} fill="#d4af37" />
<Path
fill="none"
stroke="#d4af37"
strokeWidth={1.5}
d="M560 532a18 18 0 0 0-28 28"
/>
<Circle cx={548} cy={548} r={2} fill="#d4af37" />
<G transform="translate(300 85)">
<Path
fill="none"
stroke="#d4af37"
strokeLinejoin="round"
strokeWidth={2}
d="M0-26C-8-16-8-6 0 0c8-6 8-16 0-26Z"
/>
<Path fill="#d4af37" d="M0-18C-4-12-4-4 0 0c4-4 4-12 0-18" />
<Path
fill="none"
stroke="#d4af37"
strokeLinejoin="round"
strokeWidth={2}
d="M-3 3c-13-2-25 7-21 21C-12 24-3 12-3 3Zm6 0c13-2 25 7 21 21C12 24 3 12 3 3Z"
/>
<Circle cy={10} r={1.8} fill="#d4af37" />
</G>
<Path
stroke="#d4af37"
strokeOpacity={0.4}
strokeWidth={0.8}
d="M120 198h360"
/>
<G transform="translate(190 215)">
<Rect
width={220}
height={220}
fill="#fff"
stroke="#d4af37"
strokeWidth={2}
rx={20}
/>
<Rect
width={196}
height={196}
x={12}
y={12}
fill="#fafafa"
stroke="#d4af37"
strokeDasharray="4 3"
strokeOpacity={0.4}
rx={14}
/>
<Text
x={110}
y={115}
fill="#71717a"
fontFamily="Inter, Arial, sans-serif"
fontSize={12}
fontWeight={600}
textAnchor="middle"
>
{"QR KOD ALANI"}
</Text>
</G>
<G textAnchor="middle">
<Text
x={300}
y={152}
fill="#d4af37"
fontFamily="'Playfair Display', 'Cinzel', 'Didot', Georgia, serif"
fontSize={38}
fontWeight={500}
letterSpacing={6}
>
{"LUMI\xC8RE"}
</Text>
<Text
x={300}
y={180}
fill="#e5c378"
fontFamily="Inter, Arial, sans-serif"
fontSize={11}
fontWeight={600}
letterSpacing={4}
>
{"RESTAURANT & BAR | MODERN CUISINE"}
</Text>
<Text
x={300}
y={472}
fill="#d4af37"
fontFamily="Inter, Arial, sans-serif"
fontSize={13}
fontWeight={600}
letterSpacing={3}
>
{"SCAN WITH CAMERA"}
</Text>
<Text
x={300}
y={508}
fill="#e2ddd2"
fontFamily="Inter, Arial, sans-serif"
fontSize={10.5}
fontWeight={400}
letterSpacing={1.2}
>
{"Reservations: LumiereDining.com | 240-555-0199"}
</Text>
<Text
x={300}
y={528}
fill="#c5a059"
fontFamily="Inter, Arial, sans-serif"
fontSize={9.5}
fontWeight={500}
letterSpacing={1.8}
>
{"TABLE 01 \u2022 WIFI: Lumiere_Guest"}
</Text>
</G>
</Svg>
)
export default SvgComponent
+286
View File
@@ -0,0 +1,286 @@
import * as React from "react"
import Svg, {
SvgProps,
Path,
Rect,
G,
Circle,
Ellipse,
Text,
} from "react-native-svg"
const SvgComponent = (props: SvgProps) => (
<Svg xmlns="http://www.w3.org/2000/svg" width={400} height={650} {...props}>
<Path fill="#252c36" d="M0 0h400v650H0z" />
<Rect
width={360}
height={610}
x={20}
y={20}
fill="#f6efe2"
stroke="#3a2312"
strokeWidth={1.5}
rx={20}
/>
<Rect
width={344}
height={594}
x={28}
y={28}
fill="none"
stroke="#8c6239"
strokeOpacity={0.4}
rx={14}
/>
<Rect
width={336}
height={586}
x={32}
y={32}
fill="none"
stroke="#8c6239"
strokeOpacity={0.25}
strokeWidth={0.5}
rx={10}
/>
<G fill="none" stroke="#3a2312" strokeLinecap="round" strokeWidth={1.05}>
<Path d="M36 36c11.25 11.25 18.75 26.25 22.5 48.75" />
<Path
fill="#eadcc7"
d="M43.5 45c-3.75-7.5 7.5-10.5 9-3 0 7.5-9 3-9 3Zm6 11.25C45 51 57 46.5 60 53.25c0 6.75-10.5 3-10.5 3Zm4.5 13.5C60 64.5 69.75 72 64.5 78c-6 4.5-10.5-8.25-10.5-8.25Z"
/>
</G>
<G transform="translate(340 52)">
<Circle r={32} fill="#eadcc7" stroke="#3a2312" strokeWidth={1.2} />
<Circle
r={26}
fill="#f6efe2"
stroke="#8c6239"
strokeDasharray="2 2"
strokeWidth={0.8}
/>
<Circle r={21} fill="#7a4b26" stroke="#3a2312" strokeWidth={1.4} />
<Path fill="#f6efe2" d="M0 10C-8 4-4-6 0-12 4-6 8 4 0 10" />
<Path fill="#7a4b26" d="M0 4C-5 0-2-5 0-8 2-5 5 0 0 4" />
<Circle cy={-14} r={1.8} fill="#f6efe2" />
</G>
<G transform="translate(48 250)">
<Circle r={26} fill="#eadcc7" stroke="#3a2312" />
<Circle r={17} fill="#6a3b18" stroke="#3a2312" strokeWidth={1.2} />
<Path fill="#f6efe2" d="M-5-3c-3-4 5-7 5-1 0-6 8-3 5 1L0 7z" />
<Path
fill="none"
stroke="#3a2312"
strokeWidth={1.5}
d="M17-4c6 0 6 8 0 8"
/>
</G>
<G fill="none" stroke="#3a2312" strokeWidth={0.91}>
<Path d="M352 230c-7 21-10.5 49-3.5 77" />
<Path
fill="#eadcc7"
d="M349.2 247.5c8.4-7 15.4 2.1 5.6 7.7-5.6 0-5.6-7.7-5.6-7.7Zm-2.8 24.5c-9.8-8.4-15.4 1.4-7 7 7 0 7-7 7-7Zm2.1 19.6c9.1-7 13.3 2.8 4.9 8.4-4.9 0-4.9-8.4-4.9-8.4Z"
/>
</G>
<Path
fill="none"
stroke="#8c6239"
strokeLinecap="round"
strokeWidth={1.2}
d="M142 66c6-2 12 2 18 0m80 0c6 2 12-2 18 0"
/>
<G fill="none" stroke="#3a2312" strokeWidth={0.91}>
<Path d="M42 601.5c13-26 9.75-52 0-71.5" />
<Path
fill="#eadcc7"
d="M47.2 549.5c10.4-9.75 17.55 0 6.5 7.8Zm2.6 26c11.7-9.75 18.2 1.3 5.2 9.1Z"
/>
</G>
<G
stroke="#3a2312"
strokeWidth={1.2}
transform="rotate(-20 1741.206 -8.75) scale(.8)"
>
<Ellipse fill="#cbb296" rx={9} ry={6.5} />
<Path fill="none" d="M-7 0q7 3 7 0t7 0" />
</G>
<G
stroke="#3a2312"
strokeWidth={1.2}
transform="rotate(35 -862.106 494.982) scale(.8)"
>
<Ellipse fill="#cbb296" rx={9} ry={6.5} />
<Path fill="none" d="M-7 0q7 3 7 0t7 0" />
</G>
<G
stroke="#3a2312"
strokeWidth={1.2}
transform="rotate(-45 602.558 -225.588) scale(.7)"
>
<Ellipse fill="#cbb296" rx={9} ry={6.5} />
<Path fill="none" d="M-7 0q7 3 7 0t7 0" />
</G>
<G transform="translate(325 590)">
<Ellipse
cy={18}
fill="#eadcc7"
stroke="#3a2312"
strokeWidth={1.2}
rx={28}
ry={7}
/>
<Path
fill="#f6efe2"
stroke="#3a2312"
strokeWidth={1.4}
d="M-18 2c0 14 36 14 36 0Z"
/>
<Ellipse
cy={2}
fill="#6a3b18"
stroke="#3a2312"
strokeWidth={1.2}
rx={18}
ry={4}
/>
<Path
fill="none"
stroke="#3a2312"
strokeWidth={1.3}
d="M16 4c8 0 8 8 0 10"
/>
<Path
fill="none"
stroke="#8c6239"
strokeLinecap="round"
d="M-6-4c-2-5 2-8 0-13M2-5c-2-5 2-8 0-14"
/>
</G>
<Path
fill="none"
stroke="#3a2312"
strokeLinecap="round"
strokeWidth={2}
d="M80 205c0-10 0-10 10-10m230 10c0-10 0-10-10-10M80 445c0 10 0 10 10 10m230-10c0 10 0 10-10 10"
/>
<Path stroke="#8c6239" strokeWidth={0.8} d="M145 608h30m50 0h30" />
<G transform="translate(90 215)">
<Rect
width={220}
height={220}
fill="#fbf8f2"
stroke="#d8c7b0"
strokeWidth={1.5}
rx={20}
/>
<Rect
width={200}
height={200}
x={10}
y={10}
fill="#f5efe6"
stroke="#d8c7b0"
strokeDasharray="4 3"
rx={14}
/>
<Circle cx={110} cy={98} r={18} fill="#fbf8f2" stroke="#8c6239" />
<G fill="#3a2312" transform="matrix(.65 0 0 .65 110 98)">
<Ellipse cx={-5} cy={2} rx={7} ry={5} transform="rotate(-30)" />
<Ellipse cx={5} rx={7} ry={5} transform="rotate(40)" />
</G>
<Text
x={110}
y={132}
fill="#8c6239"
fontFamily="Inter, Arial, sans-serif"
fontSize={11}
fontWeight={700}
letterSpacing={1}
textAnchor="middle"
>
{"QR KOD ALANI"}
</Text>
</G>
<G textAnchor="middle">
<Text
x={200}
y={70}
fill="#3a2312"
fontFamily="'Playfair Display', Georgia, serif"
fontSize={14}
fontStyle="italic"
fontWeight={700}
letterSpacing={4}
>
{"THE"}
</Text>
<Text
x={200}
y={114}
fill="#2c1a0e"
fontFamily="'Playfair Display', 'Brush Script MT', 'Great Vibes', Georgia, cursive, serif"
fontSize={34}
fontWeight={800}
letterSpacing={0.5}
>
{"The Daily Grind"}
</Text>
<Text
x={200}
y={146}
fill="#3a2312"
fontFamily="Inter, Arial, sans-serif"
fontSize={13}
fontWeight={800}
letterSpacing={3}
>
{"COFFEE & KITCHEN"}
</Text>
<Text
x={200}
y={488}
fill="#2c1a0e"
fontFamily="Inter, Arial, sans-serif"
fontSize={19}
fontWeight={900}
letterSpacing={1.5}
>
{"SCAN TO VIEW MENU"}
</Text>
<Text
x={200}
y={520}
fill="#3a2312"
fontFamily="Inter, Arial, sans-serif"
fontSize={10.5}
fontWeight={700}
letterSpacing={0.5}
>
{"DAILY GRIND CAFE | WIFI: DAILY_GRIND_GUEST"}
</Text>
<Text
x={200}
y={542}
fill="#8c6239"
fontFamily="Inter, Arial, sans-serif"
fontSize={9}
fontWeight={600}
letterSpacing={1}
>
{"\u015E\u0130FRE: coffee2026 \u2022 MASA NO: 04"}
</Text>
<Text
x={200}
y={611}
fill="#3a2312"
fontFamily="'Playfair Display', Georgia, serif"
fontSize={11}
fontStyle="italic"
fontWeight={700}
letterSpacing={2}
>
{"EST. 2018"}
</Text>
</G>
</Svg>
)
export default SvgComponent
+416
View File
@@ -0,0 +1,416 @@
import * as React from "react"
import Svg, {
SvgProps,
Defs,
LinearGradient,
Stop,
Pattern,
Path,
G,
Rect,
Ellipse,
Circle,
Text,
TSpan,
} from "react-native-svg"
/* SVGR has dropped some elements not supported by react-native-svg: style */
const SvgComponent = (props: SvgProps) => (
<Svg width={400} height={650} {...props}>
<Defs>
<LinearGradient id="card-grad" x1="0%" x2="0%" y1="0%" y2="100%">
<Stop offset="0%" stopColor="#fdfbf5" />
<Stop offset="50%" stopColor="#faf4e6" />
<Stop offset="100%" stopColor="#f7eed8" />
</LinearGradient>
<LinearGradient id="oven-fire" x1="0%" x2="0%" y1="100%" y2="0%">
<Stop offset="0%" stopColor="#c0392b" />
<Stop offset="50%" stopColor="#e67e22" />
<Stop offset="100%" stopColor="#f1c40f" />
</LinearGradient>
<LinearGradient id="wine-grad" x1="0%" x2="0%" y1="0%" y2="100%">
<Stop offset="0%" stopColor="#7b1123" />
<Stop offset="100%" stopColor="#4a0512" />
</LinearGradient>
<Pattern
id="checkered-border"
width={20}
height={20}
patternUnits="userSpaceOnUse"
>
<Path fill="#b92b27" d="M0 0h20v20H0z" />
<Path fill="#fdf8ee" d="M0 0h10v10H0zm10 10h10v10H10z" />
</Pattern>
</Defs>
<Path
id="layer-background"
fill="url(#checkered-border)"
d="M0 0h400v650H0z"
/>
<G id="layer-card">
<Rect
width={356}
height={606}
x={22}
y={22}
fill="url(#card-grad)"
stroke="#b92b27"
strokeWidth={2}
rx={20}
/>
<Rect
width={344}
height={594}
x={28}
y={28}
fill="none"
stroke="#8c6239"
strokeOpacity={0.5}
strokeWidth={0.8}
rx={15}
/>
<Rect
width={140}
height={24}
x={130}
y={34}
fill="#faf4e6"
stroke="#8c6239"
strokeWidth={1.2}
rx={12}
/>
</G>
<G id="layer-icons">
<Path
fill="none"
stroke="#c5a880"
strokeLinecap="round"
strokeWidth={1.5}
d="M68 88q12-13 28-2 6 5 10 2m226 0q-12-13-28-2-6 5-10 2"
/>
<G id="pizza-oven" transform="translate(200 192)">
<Rect
width={88}
height={6}
x={-44}
fill="#d7ccc8"
stroke="#5d4037"
rx={2}
/>
<Path
fill="#d7ccc8"
stroke="#5d4037"
strokeWidth={1.5}
d="M-40 0a40 38 0 0 1 80 0Z"
/>
<Path
stroke="#8d6e63"
d="m-30-25 7 6m6-15 4 8M0-38v9m17-5-4 8m17 1-7 6"
/>
<Path fill="#3e2723" stroke="#271c19" d="M-28 0a28 26 0 0 1 56 0Z" />
<Path
fill="url(#oven-fire)"
d="M-18 0q3-16 12-10 6-12 12-2 9-6 12 12Z"
/>
<Path fill="#fff9c4" d="M-10 0q5-10 10-6 5-6 10 6Z" />
</G>
<G id="left-ingredients" transform="translate(108 185)">
<Ellipse
cx={-16}
cy={6}
fill="#d32f2f"
stroke="#8e0000"
strokeWidth={0.8}
rx={14}
ry={10}
transform="rotate(-25 -16 6)"
/>
<Ellipse
cx={-16}
cy={6}
fill="#e53935"
rx={10}
ry={6.5}
transform="rotate(-25 -16 6)"
/>
<Ellipse
cx={2}
cy={7}
fill="#c62828"
stroke="#8e0000"
strokeWidth={0.8}
rx={13}
ry={9}
transform="rotate(-10 2 7)"
/>
<Ellipse
cx={2}
cy={7}
fill="#e53935"
rx={9}
ry={5.5}
transform="rotate(-10 2 7)"
/>
<Circle cx={2} cy={7} r={1.5} fill="#fdd835" />
<Circle
cx={16}
cy={-2}
r={14}
fill="#e53935"
stroke="#b71c1c"
strokeWidth={0.8}
/>
<Ellipse cx={12} cy={-6} fill="#ff8a80" opacity={0.6} rx={4} ry={2} />
<Path
stroke="#2e7d32"
strokeLinecap="round"
strokeWidth={1.5}
d="M16-16v3m0 0-5 2m5-2 5 2m-5-2-3-2m3 2 3-2"
/>
</G>
<G id="right-ingredients" transform="translate(290 182)">
<Circle
cx={-14}
cy={2}
r={15}
fill="#e53935"
stroke="#b71c1c"
strokeWidth={0.8}
/>
<Ellipse cx={-18} cy={-3} fill="#ff8a80" opacity={0.6} rx={4} ry={2} />
<Path
stroke="#2e7d32"
strokeLinecap="round"
strokeWidth={1.5}
d="M-14-13v3m0 0-4 2m4-2 4 2"
/>
<Path
fill="#2e7d32"
stroke="#1b5e20"
strokeWidth={0.8}
d="M-2 0C8-16 26-12 24 2 22 14 6 10-2 0Z"
/>
<Path
fill="none"
stroke="#4caf50"
strokeWidth={0.7}
d="M-2 0q14-4 26 2"
/>
<Path
fill="#388e3c"
stroke="#1b5e20"
strokeWidth={0.8}
d="M8 6c10-6 24 4 18 16-8 6-18-4-18-16Z"
/>
<Path
fill="none"
stroke="#81c784"
strokeWidth={0.7}
d="M8 6q12 6 18 16"
/>
</G>
<G id="footer-pizza-slice" transform="rotate(-10 3278.565 44.969)">
<Path
fill="#deac68"
stroke="#8d6e63"
strokeWidth={1.2}
d="m0 0 46-18c4 8 6 32 0 40Z"
/>
<Path fill="#fbc02d" d="m2 0 40-15c3 7 5 25 0 33Z" />
<Circle
cx={28}
cy={-5}
r={5}
fill="#c62828"
stroke="#8e0000"
strokeWidth={0.8}
/>
<Circle
cx={32}
cy={8}
r={4.5}
fill="#c62828"
stroke="#8e0000"
strokeWidth={0.8}
/>
<Circle
cx={16}
r={3.5}
fill="#c62828"
stroke="#8e0000"
strokeWidth={0.8}
/>
<Ellipse
cx={23}
cy={4}
fill="#2e7d32"
rx={2}
ry={4}
transform="rotate(30 23 4)"
/>
<Ellipse
cx={34}
cy={-11}
fill="#2e7d32"
rx={2}
ry={3}
transform="rotate(-40 34 -11)"
/>
</G>
<G id="footer-wine-glass" transform="translate(345 562)">
<Path
fill="none"
stroke="#71717a"
strokeWidth={1.2}
d="M-14-24C-15-8-11 4 0 6 11 4 15-8 14-24Z"
/>
<Path
fill="url(#wine-grad)"
d="M-13-12C-11 0-7 4 0 5.5 7 4 11 0 13-12Z"
/>
<Ellipse cy={-12} fill="#991b1b" rx={13} ry={2} />
<Path stroke="#71717a" strokeWidth={1.4} d="M0 6v22" />
<Ellipse
cy={28}
fill="#e4e4e7"
stroke="#71717a"
strokeWidth={1.2}
rx={11}
ry={2}
/>
<Path
fill="none"
stroke="#fff"
strokeLinecap="round"
d="M-10-20c-2 8 1 18 6 23"
opacity={0.6}
/>
</G>
<G id="qr-corner-accents">
<Path
fill="none"
stroke="#2e7d32"
strokeLinecap="round"
strokeWidth={3}
d="M78 227v-22h22"
/>
<Path
fill="none"
stroke="#b92b27"
strokeLinecap="round"
strokeWidth={3}
d="M322 227v-22h-22M78 423v22h22"
/>
<Path
fill="none"
stroke="#2e7d32"
strokeLinecap="round"
strokeWidth={3}
d="M322 423v22h-22"
/>
</G>
</G>
<G id="qr-placeholder" transform="translate(90 215)">
<Rect
width={220}
height={220}
fill="#fff"
stroke="#e4e4e7"
strokeWidth={2}
rx={20}
/>
<Rect
width={196}
height={196}
x={12}
y={12}
fill="#fafafa"
stroke="#e4e4e7"
strokeDasharray="4 3"
rx={14}
/>
<Text
x={110}
y={115}
fill="#71717a"
dominantBaseline="middle"
fontFamily='"Inter",Arial,sans-serif'
fontSize={12}
fontWeight={700}
letterSpacing={0.5}
textAnchor="middle"
>
{"QR KOD ALANI"}
</Text>
</G>
<G id="layer-texts">
<Text
x={200}
y={50}
fill="#18181b"
fontFamily='"Inter",Arial,sans-serif'
fontSize={11}
fontWeight={800}
letterSpacing={1.2}
textAnchor="middle"
>
{"MASA NO: 01"}
</Text>
<Text
x={200}
y={96}
className="font-brand-arc"
fontSize={24}
textAnchor="middle"
>
{"PIZZERIA"}
</Text>
<Text
x={200}
y={132}
className="font-brand-arc"
fontSize={31}
letterSpacing={1}
textAnchor="middle"
>
{"BELLA NAPOLI"}
</Text>
<Text
x={200}
y={222}
fill="#2e7d32"
fontFamily='"Inter",Arial,sans-serif'
fontSize={11.5}
fontWeight={800}
letterSpacing={1.8}
textAnchor="middle"
>
{"AUTENTICA CUCINA ITALIANA"}
</Text>
<Text x={200} y={474} className="font-cta" textAnchor="middle">
{"MEN\xDCY\xDC G\xD6RMEK"}
</Text>
<Text x={200} y={494} className="font-cta" textAnchor="middle">
{"\u0130\xC7\u0130N OKUTUN"}
</Text>
<Text x={200} y={546} textAnchor="middle">
<TSpan className="font-footer-bold">{"WiFi: "}</TSpan>
<TSpan className="font-footer-reg">{"GourmetBistro"}</TSpan>
<TSpan fill="#a1a1aa" className="font-footer-reg">
{" | "}
</TSpan>
<TSpan className="font-footer-bold">{"\u015Eifre: "}</TSpan>
<TSpan className="font-footer-reg">{"lezzetli01"}</TSpan>
</Text>
<Text x={200} y={570} textAnchor="middle">
<TSpan fill="#b92b27" className="font-footer-bold">
{"B\u0130Z\u0130 TAK\u0130P ED\u0130N"}
</TSpan>
<TSpan fill="#a1a1aa" className="font-footer-reg">
{" | "}
</TSpan>
<TSpan className="font-footer-reg">{"@gourmetbistro"}</TSpan>
</Text>
</G>
</Svg>
)
export default SvgComponent
+237
View File
@@ -0,0 +1,237 @@
import Svg, {
SvgProps,
Defs,
G,
Path,
Circle,
Use,
Rect,
Text,
} from "react-native-svg"
import { memo } from "react"
const SvgComponent = (props: SvgProps) => (
<Svg width={400} height={580} {...props}>
<Defs>
<G id="b">
<Path
fill="none"
stroke="#c59b4b"
strokeWidth={1.3}
d="M0-22c22-8 36 6 38 22-2 16-16 30-38 22Z"
/>
<Path
fill="none"
stroke="#c59b4b"
strokeWidth={0.9}
d="M0-12c14-6 24 2 26 12-2 10-12 18-26 12Z"
/>
<Path stroke="#c59b4b" strokeWidth={0.8} d="M0 0h26" />
<Path
fill="none"
stroke="#c59b4b"
strokeWidth={1.2}
d="M0-8c16-28 34-32 46-18C36-12 20-4 0-8ZM0 8c16 28 34 32 46 18C36 12 20 4 0 8Z"
/>
<Circle cx={48} cy={-28} r={1.5} fill="#c59b4b" />
<Circle cx={48} cy={28} r={1.5} fill="#c59b4b" />
<Path
fill="none"
stroke="#c59b4b"
strokeWidth={1.1}
d="M0-35c24-21 52-15 56-1-14 6-32 12-56 1Zm0 70c24 21 52 15 56 1-14-6-32-12-56-1Z"
/>
<Path
fill="none"
stroke="#c59b4b"
d="M28-44c14-20 36-10 30 2-10 6-20 4-30-2Zm0 88c14 20 36 10 30-2-10-6-20-4-30 2Z"
/>
<Circle cx={60} cy={-44} r={1.2} fill="#c59b4b" />
<Circle cx={60} cy={44} r={1.2} fill="#c59b4b" />
<Circle cx={34} cy={-58} r={1.2} fill="#c59b4b" />
<Circle cx={34} cy={58} r={1.2} fill="#c59b4b" />
<Path
fill="none"
stroke="#c59b4b"
strokeLinecap="round"
d="M0-60c22-15 36-35 32-55-6 15-16 30-32 55M0 60c22 15 36 35 32 55-6-15-16-30-32-55"
/>
<Path
fill="none"
stroke="#c59b4b"
strokeWidth={0.9}
d="M14-78c8-6 12 2 6 6-4 0-6-4-6-6Zm0 156c8 6 12-2 6-6-4 0-6 4-6 6Z"
/>
<Circle cx={32} cy={-116} r={1.5} fill="#c59b4b" />
<Circle cx={32} cy={116} r={1.5} fill="#c59b4b" />
</G>
<G id="a">
<Path
fill="none"
stroke="#c59b4b"
strokeWidth={1.3}
d="M18 64c0-28 18-46 46-46"
/>
<Path
fill="none"
stroke="#c59b4b"
strokeWidth={0.8}
d="M24 52c0-16 12-28 28-28"
/>
<Path
fill="none"
stroke="#c59b4b"
strokeLinecap="round"
strokeWidth={0.9}
d="M28 32c4-6 10-4 8 2-2 4-8 2-6-4s10-6 14-2"
/>
<Circle cx={28} cy={28} r={1.8} fill="#c59b4b" />
<Circle cx={48} cy={18} r={1.4} fill="#c59b4b" />
<Circle cx={18} cy={48} r={1.4} fill="#c59b4b" />
</G>
</Defs>
<Path fill="#fdfdfd" d="M0 0h400v580H0z" />
<Path
fill="none"
stroke="#c59b4b"
strokeWidth={1.2}
d="M20 14h360v552H20z"
/>
<Use href="#a" />
<Use href="#a" transform="matrix(-1 0 0 1 400 0)" />
<Use href="#a" transform="matrix(1 0 0 -1 0 580)" />
<Use href="#a" transform="rotate(180 200 290)" />
<Path
fill="none"
stroke="#c59b4b"
strokeLinecap="round"
strokeWidth={1.2}
d="M132 62c8-4 16 2 24-2 4-2 2-6-2-4m114 6c-8-4-16 2-24-2-4-2-2-6 2-4"
/>
<Use href="#b" transform="translate(0 310)" />
<Use href="#b" transform="matrix(-1 0 0 1 400 310)" />
<Path
fill="none"
stroke="#c59b4b"
strokeLinecap="square"
strokeWidth={2.2}
d="M114 278v-58h54m118 58v-58h-54M114 342v58h54m118-58v58h-54"
/>
<G transform="translate(125 230)">
<Rect
width={150}
height={150}
fill="#fff"
stroke="#eae5d9"
strokeWidth={1.5}
rx={12}
/>
<Rect
width={134}
height={134}
x={8}
y={8}
fill="#fafaf8"
stroke="#c59b4b"
strokeDasharray="4 3"
strokeOpacity={0.35}
rx={8}
/>
<Text
x={75}
y={79}
fill="#c59b4b"
fontFamily="Inter, Arial, sans-serif"
fontSize={10}
fontWeight={700}
letterSpacing={1}
textAnchor="middle"
>
{"QR KOD ALANI"}
</Text>
</G>
<G fill="#111" textAnchor="middle">
<Text
x={200}
y={66}
fontFamily="'Playfair Display', 'Cinzel', 'Times New Roman', serif"
fontSize={17}
fontWeight={700}
letterSpacing={3}
>
{"THE"}
</Text>
<Text
x={200}
y={112}
fontFamily="'Playfair Display', 'Cinzel', 'Times New Roman', serif"
fontSize={34}
fontWeight={700}
letterSpacing={1.5}
>
{"CRIMSON"}
</Text>
<Text
x={200}
y={152}
fontFamily="'Playfair Display', 'Cinzel', 'Times New Roman', serif"
fontSize={34}
fontWeight={700}
letterSpacing={1.5}
>
{"PETAL"}
</Text>
<Text
x={200}
y={184}
fontFamily="Inter, Arial, sans-serif"
fontSize={11}
fontWeight={700}
letterSpacing={1.8}
>
{"FINE DINING EXPERIENCE"}
</Text>
<Text
x={200}
y={460}
fontFamily="Inter, Arial, sans-serif"
fontSize={22}
fontWeight={800}
letterSpacing={0.5}
>
{"SCAN FOR"}
</Text>
<Text
x={200}
y={488}
fontFamily="Inter, Arial, sans-serif"
fontSize={22}
fontWeight={800}
letterSpacing={0.5}
>
{"OUR MENU"}
</Text>
<Text
x={200}
y={526}
fontFamily="Inter, Arial, sans-serif"
fontSize={11.5}
fontWeight={600}
letterSpacing={0.3}
>
{"contactless ordering"}
</Text>
<Text
x={200}
y={544}
fontFamily="Inter, Arial, sans-serif"
fontSize={11.5}
fontWeight={600}
letterSpacing={0.3}
>
{"scan with camera"}
</Text>
</G>
</Svg>
)
const Memo = memo(SvgComponent)
export default Memo
Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

+156
View File
@@ -0,0 +1,156 @@
import Svg, { SvgProps, Image, G, Rect, Path, Text } from "react-native-svg"
import { memo } from "react"
const SvgComponent = (props: SvgProps) => (
<Svg width={380} height={500} {...props}>
<Image
width={380}
height={500}
href="arka_plan_rengini_deg\u0306is\u0327_202608210045 1.jpg"
preserveAspectRatio="none"
/>
<G transform="translate(108 206)">
<Rect
width={164}
height={164}
fill="#fff"
stroke="#dce5ea"
strokeWidth={1.2}
rx={16}
/>
<Path
fill="none"
stroke="#003554"
strokeLinecap="round"
strokeWidth={3}
d="M6 30V12a6 6 0 0 1 6-6h18m128 24V12a6 6 0 0 0-6-6h-18M6 134v18a6 6 0 0 0 6 6h18m128-24v18a6 6 0 0 1-6 6h-18"
/>
<Rect
width={140}
height={140}
x={12}
y={12}
fill="#fafcfd"
stroke="#003554"
strokeDasharray="4 3"
strokeOpacity={0.2}
rx={10}
/>
<Text
x={82}
y={87}
fill="#003554"
fontFamily="Inter, Arial, sans-serif"
fontSize={11}
fontWeight={700}
letterSpacing={1}
textAnchor="middle"
>
{"QR KOD ALANI"}
</Text>
</G>
<G fill="#003554" textAnchor="middle">
<Text
x={190}
y={50}
fontFamily="'Arial Black', 'Montserrat', sans-serif"
fontSize={13}
fontWeight={900}
letterSpacing={2}
>
{"THE"}
</Text>
<Text
x={190}
y={80}
fontFamily="'Arial Black', 'Impact', sans-serif"
fontSize={27}
fontWeight={900}
letterSpacing={0.5}
>
{"URBAN"}
</Text>
<Text
x={190}
y={110}
fontFamily="'Arial Black', 'Impact', sans-serif"
fontSize={27}
fontWeight={900}
letterSpacing={0.5}
>
{"FORK"}
</Text>
<Text
x={190}
y={148}
fontFamily="'Arial Black', 'Montserrat', sans-serif"
fontSize={15.5}
fontWeight={900}
letterSpacing={0.2}
>
{"SCAN TO VIEW"}
</Text>
<Text
x={190}
y={172}
fontFamily="'Arial Black', 'Montserrat', sans-serif"
fontSize={15.5}
fontWeight={900}
letterSpacing={0.2}
>
{"DIGITAL MENU"}
</Text>
<Text
x={190}
y={402}
fontFamily="'Arial Black', 'Montserrat', sans-serif"
fontSize={15.5}
fontWeight={900}
letterSpacing={0.8}
>
{"FRESH FLAVORS DAILY"}
</Text>
<Text
x={190}
y={420}
fontFamily="Inter, Arial, sans-serif"
fontSize={8.5}
fontWeight={600}
letterSpacing={0.2}
>
{"Contactless ordering to scancnal bresers"}
</Text>
<Text
x={190}
y={432}
fontFamily="Inter, Arial, sans-serif"
fontSize={8.5}
fontWeight={600}
letterSpacing={0.2}
>
{"and enstand mes with your menu."}
</Text>
<Text
x={190}
y={454}
fontFamily="'Arial Black', Inter, sans-serif"
fontSize={10}
fontWeight={900}
letterSpacing={0.2}
>
{"contactless ordering"}
</Text>
<Text
x={190}
y={468}
fontFamily="'Arial Black', Inter, sans-serif"
fontSize={10}
fontWeight={900}
letterSpacing={0.2}
>
{"scan with camera"}
</Text>
</G>
</Svg>
)
const Memo = memo(SvgComponent)
export default Memo
+327
View File
@@ -0,0 +1,327 @@
import Svg, {
SvgProps,
Defs,
G,
Path,
Circle,
Rect,
Use,
Text,
} from "react-native-svg"
import { memo } from "react"
const SvgComponent = (props: SvgProps) => (
<Svg width={400} height={580} {...props}>
<Defs>
<G id="b">
<Path
fill="none"
stroke="#c59b58"
strokeLinecap="round"
strokeWidth={1.2}
d="M0 0c10 25 12 55 5 85"
/>
<Path
fill="#f4efe6"
stroke="#c59b58"
strokeWidth={1.1}
d="M3 15c-11-5-15 9-1 9 4 0 4-6 1-9Z"
/>
<Path
fill="#f4efe6"
stroke="#9e4d28"
strokeWidth={1.1}
d="M5 22c11-6 17 6 3 10-4 0-4-6-3-10Zm2 20c-12-6-17 8-3 10 4 0 4-6 3-10Z"
/>
<Path
fill="#f4efe6"
stroke="#c59b58"
strokeWidth={1.1}
d="M8 50c12-6 16 8 2 12-4 0-4-7-2-12ZM6 70c-10-5-14 8-2 10 3 0 4-6 2-10Z"
/>
<Path
fill="#f4efe6"
stroke="#9e4d28"
strokeWidth={1.1}
d="M5 78c11-4 15 8 3 12-4 0-4-6-3-12Z"
/>
<Circle cx={-10} cy={18} r={1.5} fill="#c59b58" />
<Circle cx={18} cy={46} r={1.5} fill="#9e4d28" />
</G>
<G id="a">
<Path
fill="#a64423"
d="M0-18C4-12 8-6 8 0c6-4 8 6 4 12 4-2 6 4 2 8S2 26 0 26s-10-2-14-6-2-10 2-8c-4-6-2-16 4-12 0-6 4-12 8-18"
/>
<Path
fill="#f4efe6"
d="M-4 4v8c0 3 2 4 4 4v8c2-8 4-9 4-12V4H3v7c0 2-1.5 3-3 3s-3-1-3-3V4Z"
/>
<Path stroke="#f4efe6" d="M0 4v8" />
</G>
</Defs>
<Path fill="#434b54" d="M0 0h400v580H0z" />
<Rect
width={360}
height={548}
x={20}
y={16}
fill="#f6efe3"
stroke="#122a42"
strokeWidth={1.5}
rx={16}
/>
<Path
fill="none"
stroke="#122a42"
strokeWidth={1.2}
d="M46 32h308a12 12 0 0 1 12 12v492a12 12 0 0 1-12 12H46a12 12 0 0 1-12-12V44a12 12 0 0 1 12-12Z"
/>
<Path
fill="none"
stroke="#122a42"
strokeWidth={1.2}
d="M34 46a12 12 0 0 0 12-12m320 12a12 12 0 0 1-12-12M34 534a12 12 0 0 1 12 12m320-12a12 12 0 0 0-12 12"
/>
<Path stroke="#c59b58" d="M34 135h332M34 500h332" />
<Use href="#a" transform="matrix(.85 0 0 .85 200 52)" />
<Use href="#b" transform="matrix(.8 0 0 .8 42 42)" />
<Use href="#b" transform="matrix(-.8 0 0 .8 358 42)" />
<Use href="#b" transform="matrix(.95 0 0 .95 40 185)" />
<Use href="#b" transform="matrix(-.95 0 0 .95 360 185)" />
<Use href="#a" transform="matrix(.65 0 0 .65 68 320)" />
<Use href="#a" transform="matrix(.65 0 0 .65 332 320)" />
<Use href="#b" transform="matrix(.95 0 0 .95 40 360)" />
<Use href="#b" transform="matrix(-.95 0 0 .95 360 360)" />
<G transform="translate(200 465)">
<Rect
width={18}
height={28}
x={-9}
y={-14}
fill="#122a42"
stroke="#122a42"
rx={3.5}
/>
<Rect width={14} height={20} x={-7} y={-11} fill="#fff" rx={1.5} />
<Path fill="#122a42" d="M-4.5-8.5h9v9h-9z" />
<Path fill="#fff" d="M-3-7h6v6h-6z" />
<Path fill="#122a42" d="M-1.5-5.5h3v3h-3z" />
<Circle cy={11} r={1} fill="#fff" />
</G>
<Circle cx={258} cy={537} r={4.5} fill="#a64423" />
<Path
fill="#f6efe3"
d="M259 541h-1.5v-4h-1v-1.5h1v-1c0-1 .5-1.5 2-1.5h1v1.5h-1c-.5 0-.5.3-.5.7v.3h1.5l-.3 1.5H259Z"
/>
<Rect
width={9}
height={9}
x={266}
y={532.5}
fill="none"
stroke="#a64423"
strokeWidth={1.2}
rx={2.5}
/>
<Circle cx={270.5} cy={537} r={2} fill="none" stroke="#a64423" />
<Circle cx={273} cy={534.5} r={0.5} fill="#a64423" />
<G transform="translate(95 205)">
<Path
fill="#fdfbf7"
stroke="#122a42"
strokeWidth={3}
d="M0 0h210v210H0z"
/>
<Path
fill="none"
stroke="#c59b58"
strokeWidth={1.5}
d="M4 4h202v202H4z"
/>
<Path
fill="#faf8f5"
stroke="#122a42"
strokeDasharray="4 3"
d="M12 12h186v186H12z"
/>
<Circle
cx={105}
cy={95}
r={16}
fill="#f6efe3"
stroke="#c59b58"
strokeWidth={1.2}
/>
<Use href="#a" transform="matrix(.45 0 0 .45 105 93)" />
<Text
x={105}
y={128}
fill="#122a42"
fontFamily="Inter, Arial, sans-serif"
fontSize={11}
fontWeight={800}
letterSpacing={1.5}
textAnchor="middle"
>
{"QR KOD ALANI"}
</Text>
</G>
<Text
x={172}
y={55}
fill="#122a42"
fontFamily="Inter, Arial, sans-serif"
fontSize={8.5}
fontWeight={800}
letterSpacing={1}
textAnchor="middle"
>
{"EST."}
</Text>
<Text
x={228}
y={55}
fill="#122a42"
fontFamily="Inter, Arial, sans-serif"
fontSize={8.5}
fontWeight={800}
letterSpacing={1}
textAnchor="middle"
>
{"2018"}
</Text>
<Text
x={200}
y={84}
fill="#a64423"
fontFamily="'Playfair Display', Georgia, 'Times New Roman', serif"
fontSize={19}
fontWeight={900}
letterSpacing={1.5}
textAnchor="middle"
>
{"THE STONEHEARTH GRILL"}
</Text>
<Text
x={200}
y={102}
fill="#122a42"
fontFamily="Inter, Arial, sans-serif"
fontSize={8.5}
fontWeight={700}
letterSpacing={2}
textAnchor="middle"
>
{"EST. 2018"}
</Text>
<Text
x={200}
y={118}
fill="#122a42"
fontFamily="Inter, Arial, sans-serif"
fontSize={10}
fontWeight={800}
letterSpacing={2}
textAnchor="middle"
>
{"SCAN. ORDER. ENJOY."}
</Text>
<Text
x={200}
y={158}
fill="#122a42"
fontFamily="'Playfair Display', Georgia, 'Times New Roman', serif"
fontSize={15.5}
fontWeight={900}
letterSpacing={0.5}
textAnchor="middle"
>
{"VIEW OUR MENU & ORDER ONLINE"}
</Text>
<Text
x={200}
y={176}
fill="#222"
fontFamily="Inter, Arial, sans-serif"
fontSize={9}
fontWeight={500}
letterSpacing={0.1}
textAnchor="middle"
>
{"Simply scan the QR code with your phone camera to access our"}
</Text>
<Text
x={200}
y={189}
fill="#222"
fontFamily="Inter, Arial, sans-serif"
fontSize={9}
fontWeight={500}
letterSpacing={0.1}
textAnchor="middle"
>
{"digital menu, daily specials, wine list, and place your order."}
</Text>
<Text
x={162}
y={468}
fill="#122a42"
fontFamily="Inter, Arial, sans-serif"
fontSize={11}
fontWeight={800}
letterSpacing={1}
textAnchor="end"
>
{"NO APP"}
</Text>
<Text
x={238}
y={468}
fill="#122a42"
fontFamily="Inter, Arial, sans-serif"
fontSize={11}
fontWeight={800}
letterSpacing={1}
>
{"REQUIRED"}
</Text>
<Text
x={200}
y={515}
fill="#122a42"
fontFamily="Inter, Arial, sans-serif"
fontSize={10.5}
fontWeight={800}
letterSpacing={1}
textAnchor="middle"
>
{"WWW.THESTONEHEARTHGRILL.COM"}
</Text>
<Text
x={200}
y={527}
fill="#222"
fontFamily="Inter, Arial, sans-serif"
fontSize={9}
fontWeight={600}
letterSpacing={0.2}
textAnchor="middle"
>
{"FREE WIFI: StonehearthGuest (PW: Grill720)"}
</Text>
<Text
x={195}
y={540}
fill="#222"
fontFamily="Inter, Arial, sans-serif"
fontSize={9}
fontWeight={600}
letterSpacing={0.2}
textAnchor="middle"
>
{"Follow Us @StonehearthGrill"}
</Text>
</Svg>
)
const Memo = memo(SvgComponent)
export default Memo
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 666 KiB

+158
View File
@@ -0,0 +1,158 @@
import Svg, {
SvgProps,
Path,
Image,
G,
Rect,
Circle,
Text,
} from "react-native-svg"
import { memo } from "react"
const SvgComponent = (props: SvgProps) => (
<Svg width={1024} height={1024} {...props}>
<Path fill="#0c4443" d="M0 0h1024v1024H0z" />
<Image
width={1024}
height={1024}
href="8.jpeg"
preserveAspectRatio="none"
/>
<G transform="translate(348 305)">
<Rect
width={328}
height={336}
fill="#f9f5eb"
stroke="#d1a752"
strokeWidth={1.5}
rx={16}
/>
<Rect
width={304}
height={312}
x={12}
y={12}
fill="#faf8f2"
stroke="#0c4443"
strokeDasharray="6 4"
strokeOpacity={0.3}
strokeWidth={1.5}
rx={12}
/>
<Circle cx={164} cy={148} r={28} fill="#0c4443" />
<G fill="#d1a752">
<Path d="M164 132.6c4.4 6.6 4.4 15.4 0 24.2-4.4-8.8-4.4-17.6 0-24.2" />
<Path d="M161.8 150.2c-8.8-4.4-13.2 4.4-6.6 8.8 6.6 0 8.8-6.6 6.6-8.8m4.4 0c8.8-4.4 13.2 4.4 6.6 8.8-6.6 0-8.8-6.6-6.6-8.8" />
</G>
<Text
x={164}
y={218}
fill="#0c4443"
fontFamily="Inter, Arial, sans-serif"
fontSize={14}
fontWeight={800}
letterSpacing={2}
textAnchor="middle"
>
{"QR KOD ALANI"}
</Text>
</G>
<Text
x={512}
y={142}
fill="#fff"
fontFamily="'Arial Black', 'Montserrat', sans-serif"
fontSize={52}
fontWeight={900}
letterSpacing={2}
textAnchor="middle"
>
{"EXPLORE OUR MENU"}
</Text>
<Path
stroke="#d1a752"
strokeLinecap="round"
strokeWidth={2}
d="M210 188h240"
/>
<Text
x={512}
y={194}
fill="#d1a752"
fontFamily="'Arial Black', 'Montserrat', sans-serif"
fontSize={24}
fontWeight={900}
letterSpacing={2}
textAnchor="middle"
>
{"OR"}
</Text>
<Path
stroke="#d1a752"
strokeLinecap="round"
strokeWidth={2}
d="M574 188h240"
/>
<Text
x={512}
y={242}
fill="#d1a752"
fontFamily="'Arial Black', 'Montserrat', sans-serif"
fontSize={44}
fontWeight={900}
letterSpacing={2}
textAnchor="middle"
>
{"THE DAILY SPECIALS"}
</Text>
<Text
x={512}
y={738}
fill="#fff"
fontFamily="'Arial Black', 'Montserrat', sans-serif"
fontSize={28}
fontWeight={900}
letterSpacing={1}
textAnchor="middle"
>
{"SCAN TO VIEW OUR"}
</Text>
<Text
x={512}
y={776}
fill="#fff"
fontFamily="'Arial Black', 'Montserrat', sans-serif"
fontSize={28}
fontWeight={900}
letterSpacing={1}
textAnchor="middle"
>
{"FULL MENU & SPECIALS"}
</Text>
<Text
x={512}
y={842}
fill="#d1a752"
fontFamily="'Arial Black', 'Montserrat', sans-serif"
fontSize={21}
fontWeight={900}
letterSpacing={1.5}
textAnchor="middle"
>
{"SCAN TO ORDER"}
</Text>
<Text
x={512}
y={872}
fill="#d1a752"
fontFamily="'Arial Black', 'Montserrat', sans-serif"
fontSize={21}
fontWeight={900}
letterSpacing={1.5}
textAnchor="middle"
>
{"& BROWSE"}
</Text>
</Svg>
)
const Memo = memo(SvgComponent)
export default Memo
Binary file not shown.

After

Width:  |  Height:  |  Size: 516 KiB

+231
View File
@@ -0,0 +1,231 @@
import Svg, {
SvgProps,
Defs,
Path,
Image,
G,
Rect,
Circle,
Text,
TextPath,
} from "react-native-svg"
import { memo } from "react"
const SvgComponent = (props: SvgProps) => (
<Svg width={1024} height={1024} {...props}>
<Defs>
<Path id="a" fill="none" d="M330 260q182-115 364 0" />
</Defs>
<Path fill="#f8f6f0" d="M0 0h1024v1024H0z" />
<Image
width={1024}
height={1024}
href="xx.png"
preserveAspectRatio="none"
/>
<G transform="translate(362 428)">
<Rect
width={300}
height={300}
fill="#fff"
stroke="#d5cebe"
strokeWidth={1.5}
rx={24}
/>
<Rect
width={276}
height={276}
x={12}
y={12}
fill="#fafaf7"
stroke="#1e3246"
strokeDasharray="6 4"
strokeOpacity={0.35}
strokeWidth={1.5}
rx={16}
/>
<Circle
cx={150}
cy={132}
r={28}
fill="#f5efe6"
stroke="#1e3246"
strokeWidth={1.5}
/>
<Path
fill="none"
stroke="#1e3246"
strokeLinecap="round"
strokeWidth={1.9}
d="M142.4 116.8v11.4c0 3.8 5.7 3.8 5.7 0v-11.4m-2.85 0v11.4m0 3.8v15.2m9.5-30.4c4.75 3.8 4.75 15.2 0 17.1v13.3"
/>
<Text
x={150}
y={192}
fill="#1e3246"
fontFamily="Inter, Arial, sans-serif"
fontSize={14}
fontWeight={800}
letterSpacing={2}
textAnchor="middle"
>
{"QR KOD ALANI"}
</Text>
</G>
<G stroke="#a88b58" transform="translate(290 142)">
<Circle cx={-16} cy={-8} r={4.5} fill="none" strokeWidth={1.5} />
<Path strokeLinecap="round" strokeWidth={1.5} d="M-16-12v8m-4-4h8" />
<Path
fill="none"
strokeLinecap="round"
strokeWidth={1.5}
d="M0-12c4 0 8 6 8 12S4 12 0 12"
/>
<Path
fill="none"
strokeLinecap="round"
strokeWidth={1.2}
d="M-32 20c12 8 52 8 64 0"
/>
</G>
<G stroke="#1e3246">
<Path
fill="none"
strokeLinecap="round"
strokeWidth={3}
d="M390 780c-16-8-16-26-8-36"
/>
<Path
fill="#1e3246"
strokeLinejoin="round"
strokeWidth={1.5}
d="m374 750 8-6 6 8Z"
/>
</G>
<G stroke="#1e3246">
<Path
fill="none"
strokeLinecap="round"
strokeWidth={3}
d="M634 780c16-8 16-26 8-36"
/>
<Path
fill="#1e3246"
strokeLinejoin="round"
strokeWidth={1.5}
d="m650 750-8-6-6 8Z"
/>
</G>
<Text
x={290}
y={164}
fill="#1e3246"
fontFamily="'Arial Black', 'Impact', sans-serif"
fontSize={16}
fontWeight={900}
letterSpacing={1}
textAnchor="middle"
>
{"TABLE MENU"}
</Text>
<Text
x={290}
y={180}
fill="#8c7348"
fontFamily="Georgia, serif"
fontSize={9}
fontStyle="italic"
fontWeight={700}
letterSpacing={1}
textAnchor="middle"
>
{"EST. 2023"}
</Text>
<Text
x={364}
y={248}
fill="#1e3246"
fontFamily="'Arial Black', 'Impact', sans-serif"
fontSize={23}
fontWeight={900}
letterSpacing={1}
textAnchor="middle"
transform="rotate(-26 364 248)"
>
{"SIAT"}
</Text>
<Text
fill="#1e3246"
fontFamily="'Arial Black', 'Impact', sans-serif"
fontSize={52}
fontWeight={900}
letterSpacing={2}
>
<TextPath href="#a" startOffset="50%" textAnchor="middle">
{"\n SCAN TO\n "}
</TextPath>
</Text>
<Text
x={670}
y={244}
fill="#1e3246"
fontFamily="'Arial Black', 'Impact', sans-serif"
fontSize={25}
fontWeight={900}
letterSpacing={1}
textAnchor="middle"
transform="rotate(24 670 244)"
>
{"VIEW"}
</Text>
<Text
x={512}
y={320}
fill="#1e3246"
fontFamily="'Arial Black', 'Impact', sans-serif"
fontSize={82}
fontWeight={900}
letterSpacing={3}
textAnchor="middle"
>
{"OUR MENU"}
</Text>
<Text
x={512}
y={748}
fill="#1e3246"
fontFamily="'Arial Black', sans-serif"
fontSize={18.5}
fontWeight={900}
letterSpacing={1}
textAnchor="middle"
>
{"VIEW OUR"}
</Text>
<Text
x={512}
y={774}
fill="#1e3246"
fontFamily="'Arial Black', sans-serif"
fontSize={18.5}
fontWeight={900}
letterSpacing={1}
textAnchor="middle"
>
{"DELICIOUS MENU"}
</Text>
<Text
x={500}
y={894}
fill="#1e3246"
fontFamily="'Arial Black', 'Montserrat', sans-serif"
fontSize={18}
fontWeight={900}
letterSpacing={1.5}
textAnchor="middle"
>
{"WELCOME TO OUR TABLE"}
</Text>
</Svg>
)
const Memo = memo(SvgComponent)
export default Memo
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 4.5 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 357 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

+76
View File
@@ -0,0 +1,76 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 650" width="400" height="650">
<defs>
<!-- Warm cream gradient for the outer canvas -->
<linearGradient id="cream-bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#faf4e6" />
<stop offset="100%" stop-color="#f7eed8" />
</linearGradient>
<!-- Rosso accent gradient -->
<linearGradient id="rosso-accent" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#d1453f" />
<stop offset="100%" stop-color="#b92b27" />
</linearGradient>
</defs>
<!-- 1. Backgrounds & Contrast Panels -->
<g id="layer-background">
<rect id="bg-canvas" width="400" height="650" fill="url(#cream-bg)" />
<rect id="main-card" x="20" y="20" width="360" height="610" rx="24" fill="#fdfbf5" stroke="url(#rosso-accent)" stroke-width="1.5" />
<rect id="inner-border" x="30" y="30" width="340" height="590" rx="18" fill="none" stroke="#e8ddc7" stroke-width="1" stroke-dasharray="5 3" />
</g>
<!-- 2. Decorative Icons & Accents -->
<g id="layer-icons">
<!-- Pizza slice crest -->
<g transform="translate(170, 50)" stroke="url(#rosso-accent)" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path d="M30 6 L58 46 A32 32 0 0 1 2 46 Z" />
<circle cx="26" cy="24" r="2.2" fill="#b92b27" stroke="none" />
<circle cx="38" cy="30" r="2.2" fill="#5d4037" stroke="none" />
<circle cx="30" cy="38" r="2.2" fill="#b92b27" stroke="none" />
<path d="M2 46 h56" stroke-width="1" />
</g>
<!-- Center wheat-sprig divider -->
<g id="divider-accent">
<path d="M130 162 h55 M215 162 h55" stroke="url(#rosso-accent)" stroke-width="1" opacity="0.6" />
<polygon points="200,157 202.5,161.5 207,162 203.5,165 204.5,169.5 200,167 195.5,169.5 196.5,165 193,162 197.5,161.5" fill="url(#rosso-accent)" />
</g>
<!-- WiFi Icon -->
<g transform="translate(68, 592)" stroke="#8c6b52" stroke-width="1.5" fill="none" stroke-linecap="round">
<path d="M0 0 a14 14 0 0 1 18 0" />
<path d="M3 4 a9 9 0 0 1 12 0" />
<circle cx="9" cy="8" r="1" fill="#8c6b52" />
</g>
<!-- Instagram / Social Icon -->
<g transform="translate(225, 592)" stroke="#8c6b52" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round">
<rect x="0" y="0" width="13" height="13" rx="3.5" />
<circle cx="6.5" cy="6.5" r="3" />
<circle cx="10" cy="3" r="0.5" fill="#8c6b52" />
</g>
</g>
<!-- 3. Clean Placeholder Box for QR Code (aligned to the app's fixed 33.08% / 55% overlay) -->
<g id="qr-placeholder" transform="translate(90, 215)">
<rect width="220" height="220" rx="20" fill="#FFFFFF" stroke="#f0d9d8" stroke-width="2" />
<rect x="12" y="12" width="196" height="196" rx="14" fill="#FCFAF6" stroke="#f0d9d8" stroke-dasharray="4 3" />
<text x="110" y="115" font-family="Inter, sans-serif" font-size="12" font-weight="600" fill="#b92b27" text-anchor="middle">QR KOD ALANI</text>
</g>
<!-- 4. Native Editable Texts -->
<g id="layer-texts" font-family="Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" text-anchor="middle">
<text x="200" y="130" font-size="22" font-weight="800" fill="#18181b" letter-spacing="2">PIZZERIA BELLA NAPOLI</text>
<text x="200" y="150" font-size="11" font-weight="500" fill="#b92b27" letter-spacing="2.5">TEMASSIZ DİJİTAL MENÜ</text>
<text x="200" y="475" font-size="13" font-weight="700" fill="#18181b" letter-spacing="1.2">MENÜYÜ GÖRMEK İÇİN OKUTUN</text>
<text x="200" y="495" font-size="11" font-weight="400" fill="#5d4037">Kameranız ile QR kodu taratın</text>
<rect x="140" y="520" width="120" height="28" rx="14" fill="#fdece9" stroke="#f0d9d8" stroke-width="1" />
<text x="200" y="539" font-size="12" font-weight="600" fill="#b92b27" letter-spacing="0.5">Masa No: 01</text>
<text x="128" y="602" font-size="10" font-weight="400" fill="#8c6b52" text-anchor="start">Bella_Guest</text>
<text x="246" y="602" font-size="10" font-weight="400" fill="#8c6b52" text-anchor="start">@bellanapoli</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

+71
View File
@@ -0,0 +1,71 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 650" width="400" height="650">
<defs>
<!-- Define a linear gradient for gold effect -->
<linearGradient id="goldGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#D4AF37"/>
<stop offset="100%" stop-color="#FFD700"/>
</linearGradient>
</defs>
<!-- Background Canvas -->
<rect id="bg-canvas" fill="#451A03" width="400" height="650"/>
<!-- Main Menu Card Panel -->
<rect id="main-card" x="20" y="20" width="360" height="610" rx="24" fill="#000000" stroke="url(#goldGradient)" stroke-width="2"/>
<!-- Layer for Icons/Graphics -->
<g id="layer-icons">
<!-- Top decorative line -->
<line x1="80" y1="165" x2="320" y2="165" stroke="url(#goldGradient)" stroke-width="1" fill="#D97706"/>
<!-- Bottom decorative line -->
<line x1="80" y1="535" x2="320" y2="535" stroke="url(#goldGradient)" stroke-width="1" fill="#B45309"/>
<!-- Subtle corner accents (L-shapes) -->
<path d="M40 40 L40 70 M40 40 L70 40" stroke="url(#goldGradient)" stroke-width="1.5" stroke-linecap="round" fill="#D97706"/>
<path d="M360 40 L360 70 M360 40 L330 40" stroke="url(#goldGradient)" stroke-width="1.5" stroke-linecap="round" fill="#B45309"/>
<path d="M40 610 L40 580 M40 610 L70 610" stroke="url(#goldGradient)" stroke-width="1.5" stroke-linecap="round" fill="#D97706"/>
<path d="M360 610 L360 580 M360 610 L330 610" stroke="url(#goldGradient)" stroke-width="1.5" stroke-linecap="round" fill="#B45309"/>
<!-- Minimalist Fork and Knife Line Art -->
<g transform="translate(0, 15)">
<!-- Fork -->
<path d="M185 550 L185 560 M188 550 L188 560 M191 550 L191 560 M188 560 L188 565" stroke="url(#goldGradient)" stroke-width="1.5" stroke-linecap="round" fill="#D97706"/>
<!-- Knife -->
<path d="M205 550 L205 560 L208 560 L208 552 C208 550 206.5 549 205 549 Z M206.5 560 L206.5 565" stroke="url(#goldGradient)" stroke-width="1.5" stroke-linecap="round" fill="#B45309"/>
</g>
</g>
<!-- QR Placeholder -->
<g id="qr-placeholder" transform="translate(90, 215)">
<rect width="220" height="220" rx="20" fill="#FFFFFF" stroke="#E4E4E7" stroke-width="2"/>
<rect x="12" y="12" width="196" height="196" rx="14" fill="#FAFAFA" stroke="#E4E4E7" stroke-dasharray="4 3"/>
<text x="110" y="115" font-family="Inter, sans-serif" font-size="12" font-weight="600" fill="#71717A" text-anchor="middle">
QR KOD ALANI
</text>
</g>
<!-- Layer for Texts -->
<g id="layer-texts" fill="#c10101">
<!-- Table Info badge -->
<rect x="140" y="40" width="120" height="30" rx="15" fill="url(#goldGradient)"/>
<text x="200" y="60" font-family="Inter, sans-serif" font-size="14" font-weight="600" fill="#1A1A1D" text-anchor="middle">
MASA NO: 01
</text>
<!-- Restaurant Name -->
<text x="200" y="120" font-family="Inter, sans-serif" font-size="26" font-weight="700" fill="url(#goldGradient)" text-anchor="middle">
Gourmet Bistro
</text>
<!-- Tagline / Subtitle -->
<text x="200" y="145" font-family="Inter, sans-serif" font-size="12" font-weight="400" fill="#E4E4E7" text-anchor="middle">
Temassız Dijital Menü
</text>
<!-- "MENÜYÜ GÖRMEK İÇİN OKUTUN" -->
<text x="200" y="480" font-family="Inter, sans-serif" font-size="16" font-weight="700" fill="url(#goldGradient)" text-anchor="middle">
MENÜYÜ GÖRMEK İÇİN OKUTUN
</text>
<!-- Instruction note -->
<text x="200" y="505" font-family="Inter, sans-serif" font-size="11" font-weight="400" fill="#A1A1AA" text-anchor="middle">
Kameranızı QR Koda Odaklayın
</text>
<!-- WiFi & Social Media footer info -->
<text x="200" y="595" font-family="Inter, sans-serif" font-size="11" font-weight="400" fill="#A1A1AA" text-anchor="middle">
Ücretsiz Wi-Fi: GourmetBistro_Guest
</text>
<text x="200" y="610" font-family="Inter, sans-serif" font-size="11" font-weight="400" fill="#A1A1AA" text-anchor="middle">
Bizi Takip Edin: @GourmetBistro
</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

+285
View File
@@ -0,0 +1,285 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 400 650"
width="400"
height="650"
>
<g id="layer-background">
<rect
id="bg-canvas"
x="0"
y="0"
width="400"
height="650"
fill="#0B0E14"
/>
</g>
<g id="layer-card">
<!-- Ana Lüks Kart Arka Planı (Gece Yarısı Laciverti & Kömür) -->
<rect
id="main-card"
x="20"
y="20"
width="360"
height="610"
rx="24"
fill="#121721"
stroke="#D4AF37"
stroke-width="1.5"
/>
<!-- İç Çift Altın Bordür Hattı -->
<rect
id="inner-border"
x="28"
y="28"
width="344"
height="594"
rx="18"
fill="none"
stroke="#C5A059"
stroke-width="0.8"
stroke-opacity="0.4"
/>
<rect
x="32"
y="32"
width="336"
height="586"
rx="14"
fill="none"
stroke="#C5A059"
stroke-width="0.5"
stroke-opacity="0.2"
/>
</g>
<g id="layer-icons">
<!-- Üst Köşe Geometrik Art-Deco Motifleri -->
<path
d="M 38 48 L 48 38 M 38 38 L 52 38 M 38 38 L 38 52"
stroke="#D4AF37"
stroke-width="1"
stroke-linecap="round"
stroke-opacity="0.6"
fill="none"
/>
<path
d="M 362 48 L 352 38 M 362 38 L 348 38 M 362 38 L 362 52"
stroke="#D4AF37"
stroke-width="1"
stroke-linecap="round"
stroke-opacity="0.6"
fill="none"
/>
<!-- Üst Masa Numarası Kapsülü -->
<rect
x="148"
y="44"
width="104"
height="22"
rx="11"
fill="#1A2230"
stroke="#D4AF37"
stroke-width="0.8"
/>
<circle cx="158" cy="55" r="1.5" fill="#D4AF37" />
<circle cx="242" cy="55" r="1.5" fill="#D4AF37" />
<!-- Merkezi Gastronomi & Yıldız Amblemi -->
<g transform="translate(200, 84)">
<circle cx="0" cy="0" r="11" fill="none" stroke="#D4AF37" stroke-width="0.8" stroke-dasharray="2 2" />
<polygon points="0,-7 2,-2 7,0 2,2 0,7 -2,2 -7,0 -2,-2" fill="#D4AF37" />
</g>
<!-- Zarif Tipografik Ayırıcı -->
<line x1="75" y1="172" x2="175" y2="172" stroke="#D4AF37" stroke-width="0.75" stroke-opacity="0.5" />
<polygon points="200,169.5 202.5,172 200,174.5 197.5,172" fill="#D4AF37" />
<line x1="225" y1="172" x2="325" y2="172" stroke="#D4AF37" stroke-width="0.75" stroke-opacity="0.5" />
<!-- QR Alanı Hedef Odak Çerçeveleri -->
<path d="M 80 205 L 80 195 L 90 195" fill="none" stroke="#D4AF37" stroke-width="2" stroke-linecap="round" />
<path d="M 320 205 L 320 195 L 310 195" fill="none" stroke="#D4AF37" stroke-width="2" stroke-linecap="round" />
<path d="M 80 445 L 80 455 L 90 455" fill="none" stroke="#D4AF37" stroke-width="2" stroke-linecap="round" />
<path d="M 320 445 L 320 455 L 310 455" fill="none" stroke="#D4AF37" stroke-width="2" stroke-linecap="round" />
<!-- Alt Bilgi Kartı Paneli -->
<rect
x="36"
y="520"
width="328"
height="84"
rx="14"
fill="#171F2C"
stroke="#D4AF37"
stroke-width="0.8"
stroke-opacity="0.4"
/>
<line x1="200" y1="530" x2="200" y2="594" stroke="#D4AF37" stroke-width="0.5" stroke-opacity="0.25" />
<!-- Minimal WiFi Vektörü -->
<g transform="translate(62, 545)">
<path d="M 0 4 A 12 12 0 0 1 18 4" fill="none" stroke="#D4AF37" stroke-width="1.4" stroke-linecap="round" />
<path d="M 3.5 8 A 7 7 0 0 1 14.5 8" fill="none" stroke="#D4AF37" stroke-width="1.4" stroke-linecap="round" />
<circle cx="9" cy="12" r="1.2" fill="#D4AF37" />
</g>
<!-- Minimal Instagram / Web Vektörü -->
<g transform="translate(224, 545)">
<rect x="0" y="0" width="15" height="15" rx="4" fill="none" stroke="#D4AF37" stroke-width="1.4" />
<circle cx="7.5" cy="7.5" r="3.2" fill="none" stroke="#D4AF37" stroke-width="1.1" />
<circle cx="11.5" cy="3.5" r="0.75" fill="#D4AF37" />
</g>
</g>
<g id="qr-placeholder" transform="translate(90, 215)">
<rect
width="220"
height="220"
rx="20"
fill="#FFFFFF"
stroke="#E4E4E7"
stroke-width="2"
/>
<rect
x="12"
y="12"
width="196"
height="196"
rx="14"
fill="#FAFAFA"
stroke="#E4E4E7"
stroke-dasharray="4 3"
/>
<text
x="110"
y="115"
font-family="Inter, Arial, sans-serif"
font-size="12"
font-weight="600"
fill="#71717A"
text-anchor="middle"
>QR KOD ALANI</text>
</g>
<g id="layer-texts">
<!-- Masa Numarası -->
<text
x="200"
y="59"
font-family="Inter, Arial, sans-serif"
font-size="9"
font-weight="700"
letter-spacing="2.5"
fill="#E5C378"
text-anchor="middle"
>TABLE • 01</text>
<!-- Restoran Logosu & İsmi -->
<text
x="200"
y="126"
font-family="'Playfair Display', 'Cinzel', Georgia, serif"
font-size="28"
font-weight="700"
letter-spacing="1.5"
fill="#F9F6F0"
text-anchor="middle"
>LATELIER BISTRO</text>
<!-- Fransızca / Uluslararası Alt Başlık -->
<text
x="200"
y="152"
font-family="Inter, Arial, sans-serif"
font-size="8.5"
font-weight="600"
letter-spacing="4"
fill="#D4AF37"
text-anchor="middle"
>FINE DINING &amp; COCKTAILS</text>
<!-- Çağrı Metni / Talimat -->
<text
x="200"
y="480"
font-family="Inter, Arial, sans-serif"
font-size="11.5"
font-weight="700"
letter-spacing="2"
fill="#F9F6F0"
text-anchor="middle"
>DİJİTAL MENÜ İÇİN OKUTUN</text>
<text
x="200"
y="498"
font-family="'Playfair Display', Georgia, serif"
font-size="10.5"
font-style="italic"
font-weight="400"
letter-spacing="0.5"
fill="#94A3B8"
text-anchor="middle"
>Kameranızı QR koda yaklaştırınız</text>
<!-- Sol Sütun: Yüksek Hızlı WiFi Bilgileri -->
<text
x="126"
y="550"
font-family="Inter, Arial, sans-serif"
font-size="9"
font-weight="700"
letter-spacing="1.2"
fill="#E5C378"
text-anchor="middle"
>GUEST WIFI</text>
<text
x="126"
y="567"
font-family="Inter, Arial, sans-serif"
font-size="8.5"
font-weight="500"
fill="#CBD5E1"
text-anchor="middle"
>Ağ: Atelier-Guest</text>
<text
x="126"
y="582"
font-family="Inter, Arial, sans-serif"
font-size="8.5"
font-weight="500"
fill="#CBD5E1"
text-anchor="middle"
>Şifre: bistro2026</text>
<!-- Sağ Sütun: Sosyal Medya & Rezervasyon -->
<text
x="286"
y="550"
font-family="Inter, Arial, sans-serif"
font-size="8.5"
font-weight="700"
letter-spacing="1.2"
fill="#E5C378"
text-anchor="middle"
>SOSYAL MEDYA</text>
<text
x="286"
y="575"
font-family="Inter, Arial, sans-serif"
font-size="10.5"
font-weight="600"
letter-spacing="0.5"
fill="#F9F6F0"
text-anchor="middle"
>@latelier.bistro</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.1 KiB

+275
View File
@@ -0,0 +1,275 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 400 650"
width="400"
height="650"
>
<g id="layer-background">
<!-- Koyu Doku / Kontrast Arka Plan -->
<rect
id="bg-canvas"
x="0"
y="0"
width="400"
height="650"
fill="#252C36"
/>
</g>
<g id="layer-card">
<!-- Sıcak Parşömen / Vintage Krem Ana Kart -->
<rect
id="main-card"
x="20"
y="20"
width="360"
height="610"
rx="20"
fill="#F6EFE2"
stroke="#3A2312"
stroke-width="1.5"
/>
<!-- Çift İnce Vintage Çerçeve -->
<rect
x="28"
y="28"
width="344"
height="594"
rx="14"
fill="none"
stroke="#8C6239"
stroke-width="1"
stroke-opacity="0.4"
/>
<rect
x="32"
y="32"
width="336"
height="586"
rx="10"
fill="none"
stroke="#8C6239"
stroke-width="0.5"
stroke-opacity="0.25"
/>
</g>
<g id="layer-icons">
<!-- Sol Üst: Botanik Kahve / Çay Dalı -->
<g transform="translate(36, 36) scale(0.75)" stroke="#3A2312" stroke-width="1.4" fill="none" stroke-linecap="round">
<path d="M 0 0 C 15 15 25 35 30 65" />
<path d="M 10 12 C 5 2 20 -2 22 8 C 22 18 10 12 10 12 Z" fill="#EADCC7" />
<path d="M 18 27 C 12 20 28 14 32 23 C 32 32 18 27 18 27 Z" fill="#EADCC7" />
<path d="M 24 45 C 32 38 45 48 38 56 C 30 62 24 45 24 45 Z" fill="#EADCC7" />
</g>
<!-- Sağ Üst: Kuşbakışı Latte Art Fincanı -->
<g transform="translate(340, 52)">
<!-- Tabak -->
<circle cx="0" cy="0" r="32" fill="#EADCC7" stroke="#3A2312" stroke-width="1.2" />
<circle cx="0" cy="0" r="26" fill="#F6EFE2" stroke="#8C6239" stroke-width="0.8" stroke-dasharray="2 2" />
<!-- Fincan Kenarı & Kahve -->
<circle cx="0" cy="0" r="21" fill="#7A4B26" stroke="#3A2312" stroke-width="1.4" />
<!-- Latte Art / Rosetta Yaprakları -->
<path d="M 0 10 C -8 4 -4 -6 0 -12 C 4 -6 8 4 0 10 Z" fill="#F6EFE2" />
<path d="M 0 4 C -5 0 -2 -5 0 -8 C 2 -5 5 0 0 4 Z" fill="#7A4B26" />
<circle cx="0" cy="-14" r="1.8" fill="#F6EFE2" />
</g>
<!-- Sol Orta: Küçük Kahve Fincanı & Tabağı -->
<g transform="translate(48, 250)">
<ellipse cx="0" cy="0" rx="26" ry="26" fill="#EADCC7" stroke="#3A2312" stroke-width="1" />
<circle cx="0" cy="0" r="17" fill="#6A3B18" stroke="#3A2312" stroke-width="1.2" />
<!-- Basit Kalp Latte Deseni -->
<path d="M -5 -3 C -8 -7 0 -10 0 -4 C 0 -10 8 -7 5 -3 C 2 3 0 7 0 7 C 0 7 -2 3 -5 -3 Z" fill="#F6EFE2" />
<!-- Fincan Kulpu -->
<path d="M 17 -4 C 23 -4 23 4 17 4" fill="none" stroke="#3A2312" stroke-width="1.5" />
</g>
<!-- Sağ Orta: Dikey Yaprak İllüstrasyonu -->
<g transform="translate(345, 230) scale(0.7)" stroke="#3A2312" stroke-width="1.3" fill="none">
<path d="M 10 0 C 0 30 -5 70 5 110" />
<path d="M 6 25 C 18 15 28 28 14 36 C 6 36 6 25 6 25 Z" fill="#EADCC7" />
<path d="M 2 60 C -12 48 -20 62 -8 70 C 2 70 2 60 2 60 Z" fill="#EADCC7" />
<path d="M 5 88 C 18 78 24 92 12 100 C 5 100 5 88 5 88 Z" fill="#EADCC7" />
</g>
<!-- Üst Başlık Yan Süsleme Çizgileri -->
<path d="M 142 66 C 148 64 154 68 160 66" fill="none" stroke="#8C6239" stroke-width="1.2" stroke-linecap="round" />
<path d="M 240 66 C 246 68 252 64 258 66" fill="none" stroke="#8C6239" stroke-width="1.2" stroke-linecap="round" />
<!-- Sol Alt: Çekirdek ve Yaprak Grubu -->
<g transform="translate(42, 530) scale(0.65)" stroke="#3A2312" stroke-width="1.4" fill="none">
<path d="M 0 110 C 20 70 15 30 0 0" />
<path d="M 8 30 C 24 15 35 30 18 42 Z" fill="#EADCC7" />
<path d="M 12 70 C 30 55 40 72 20 84 Z" fill="#EADCC7" />
</g>
<!-- Kahve Çekirdekleri Motifleri -->
<!-- Sol Alt Çekirdek 1 -->
<g transform="translate(108, 595) rotate(-20) scale(0.8)">
<ellipse cx="0" cy="0" rx="9" ry="6.5" fill="#CBB296" stroke="#3A2312" stroke-width="1.2" />
<path d="M -7 0 Q 0 3 0 0 Q 0 -3 7 0" fill="none" stroke="#3A2312" stroke-width="1.2" />
</g>
<!-- Sol Alt Çekirdek 2 -->
<g transform="translate(128, 584) rotate(35) scale(0.8)">
<ellipse cx="0" cy="0" rx="9" ry="6.5" fill="#CBB296" stroke="#3A2312" stroke-width="1.2" />
<path d="M -7 0 Q 0 3 0 0 Q 0 -3 7 0" fill="none" stroke="#3A2312" stroke-width="1.2" />
</g>
<!-- Sağ Orta Çekirdek -->
<g transform="translate(336, 360) rotate(-45) scale(0.7)">
<ellipse cx="0" cy="0" rx="9" ry="6.5" fill="#CBB296" stroke="#3A2312" stroke-width="1.2" />
<path d="M -7 0 Q 0 3 0 0 Q 0 -3 7 0" fill="none" stroke="#3A2312" stroke-width="1.2" />
</g>
<!-- Sağ Alt: Sıcak Kahve Fincanı İllüstrasyonu -->
<g transform="translate(325, 590)">
<!-- Tabak -->
<ellipse cx="0" cy="18" rx="28" ry="7" fill="#EADCC7" stroke="#3A2312" stroke-width="1.2" />
<!-- Fincan Gövdesi -->
<path d="M -18 2 C -18 16 18 16 18 2 Z" fill="#F6EFE2" stroke="#3A2312" stroke-width="1.4" />
<ellipse cx="0" cy="2" rx="18" ry="4" fill="#6A3B18" stroke="#3A2312" stroke-width="1.2" />
<!-- Kulp -->
<path d="M 16 4 C 24 4 24 12 16 14" fill="none" stroke="#3A2312" stroke-width="1.3" />
<!-- Buhar Çizgileri -->
<path d="M -6 -4 C -8 -9 -4 -12 -6 -17" fill="none" stroke="#8C6239" stroke-width="1" stroke-linecap="round" />
<path d="M 2 -5 C 0 -10 4 -13 2 -19" fill="none" stroke="#8C6239" stroke-width="1" stroke-linecap="round" />
</g>
<!-- QR Alanı İnce Köşe Süsleri -->
<path d="M 80 205 C 80 195 80 195 90 195" fill="none" stroke="#3A2312" stroke-width="2" stroke-linecap="round" />
<path d="M 320 205 C 320 195 320 195 310 195" fill="none" stroke="#3A2312" stroke-width="2" stroke-linecap="round" />
<path d="M 80 445 C 80 455 80 455 90 455" fill="none" stroke="#3A2312" stroke-width="2" stroke-linecap="round" />
<path d="M 320 445 C 320 455 320 455 310 455" fill="none" stroke="#3A2312" stroke-width="2" stroke-linecap="round" />
<!-- Alt Est. Bölümü Çizgileri -->
<line x1="145" y1="608" x2="175" y2="608" stroke="#8C6239" stroke-width="0.8" />
<line x1="225" y1="608" x2="255" y2="608" stroke="#8C6239" stroke-width="0.8" />
</g>
<g id="qr-placeholder" transform="translate(90, 215)">
<!-- QR Kod Arka Planı (Hafif Sıcak Ton) -->
<rect
width="220"
height="220"
rx="20"
fill="#FBF8F2"
stroke="#D8C7B0"
stroke-width="1.5"
/>
<rect
x="10"
y="10"
width="200"
height="200"
rx="14"
fill="#F5EFE6"
stroke="#D8C7B0"
stroke-dasharray="4 3"
/>
<!-- QR Merkezine Kahve Çekirdeği İkonu Rozeti -->
<circle cx="110" cy="98" r="18" fill="#FBF8F2" stroke="#8C6239" stroke-width="1" />
<g transform="translate(110, 98) scale(0.65)">
<ellipse cx="-5" cy="2" rx="7" ry="5" fill="#3A2312" transform="rotate(-30)" />
<ellipse cx="5" cy="0" rx="7" ry="5" fill="#3A2312" transform="rotate(40)" />
</g>
<text
x="110"
y="132"
font-family="Inter, Arial, sans-serif"
font-size="11"
font-weight="700"
letter-spacing="1"
fill="#8C6239"
text-anchor="middle"
>QR KOD ALANI</text>
</g>
<g id="layer-texts">
<!-- Üst Başlık Takısı -->
<text
x="200"
y="70"
font-family="'Playfair Display', Georgia, serif"
font-size="14"
font-style="italic"
font-weight="700"
letter-spacing="4"
fill="#3A2312"
text-anchor="middle"
>THE</text>
<!-- Ana Tipografi: El Yazısı / Retro Tabela Havası -->
<text
x="200"
y="114"
font-family="'Playfair Display', 'Brush Script MT', 'Great Vibes', Georgia, cursive, serif"
font-size="34"
font-weight="800"
letter-spacing="0.5"
fill="#2C1A0E"
text-anchor="middle"
>The Daily Grind</text>
<!-- Alt Kategori Başlığı -->
<text
x="200"
y="146"
font-family="Inter, Arial, sans-serif"
font-size="13"
font-weight="800"
letter-spacing="3"
fill="#3A2312"
text-anchor="middle"
>COFFEE &amp; KITCHEN</text>
<!-- Büyük ve Belirgin Çağrı Metni -->
<text
x="200"
y="488"
font-family="Inter, Arial, sans-serif"
font-size="19"
font-weight="900"
letter-spacing="1.5"
fill="#2C1A0E"
text-anchor="middle"
>SCAN TO VIEW MENU</text>
<!-- Restoran & WiFi Bilgi Bandı -->
<text
x="200"
y="520"
font-family="Inter, Arial, sans-serif"
font-size="10.5"
font-weight="700"
letter-spacing="0.5"
fill="#3A2312"
text-anchor="middle"
>DAILY GRIND CAFE | WIFI: DAILY_GRIND_GUEST</text>
<!-- Şifre ve Masa Detayı -->
<text
x="200"
y="542"
font-family="Inter, Arial, sans-serif"
font-size="9"
font-weight="600"
letter-spacing="1"
fill="#8C6239"
text-anchor="middle"
>ŞİFRE: coffee2026 • MASA NO: 04</text>
<!-- Kuruluş / Est. Yılı -->
<text
x="200"
y="611"
font-family="'Playfair Display', Georgia, serif"
font-size="11"
font-style="italic"
font-weight="700"
letter-spacing="2"
fill="#3A2312"
text-anchor="middle"
>EST. 2018</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.3 KiB

+72
View File
@@ -0,0 +1,72 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 650" width="400" height="650">
<defs>
<linearGradient id="gold-accent-8" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#e9c877" />
<stop offset="50%" stop-color="#d1a752" />
<stop offset="100%" stop-color="#a3803a" />
</linearGradient>
<linearGradient id="emerald-surface" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#0c4443" />
<stop offset="100%" stop-color="#062423" />
</linearGradient>
</defs>
<!-- 1. Backgrounds & Contrast Panels -->
<g id="layer-background">
<rect id="bg-canvas" width="400" height="650" fill="#052120" />
<rect id="main-card" x="20" y="20" width="360" height="610" rx="24" fill="url(#emerald-surface)" stroke="url(#gold-accent-8)" stroke-width="1.5" />
<rect id="inner-border" x="30" y="30" width="340" height="590" rx="18" fill="none" stroke="#134a48" stroke-width="1" stroke-dasharray="5 3" />
</g>
<!-- 2. Decorative Icons & Accents -->
<g id="layer-icons">
<!-- Laurel crest -->
<g transform="translate(170, 50)" stroke="url(#gold-accent-8)" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path d="M30 4 v42" />
<path d="M30 12 c-10 0 -16 6 -16 14 M30 22 c-10 0 -15 5 -15 12 M30 32 c-10 0 -13 4 -13 10" />
<path d="M30 12 c10 0 16 6 16 14 M30 22 c10 0 15 5 15 12 M30 32 c10 0 13 4 13 10" />
</g>
<g id="divider-accent">
<path d="M130 162 h55 M215 162 h55" stroke="url(#gold-accent-8)" stroke-width="1" opacity="0.6" />
<polygon points="200,157 202.5,161.5 207,162 203.5,165 204.5,169.5 200,167 195.5,169.5 196.5,165 193,162 197.5,161.5" fill="url(#gold-accent-8)" />
</g>
<!-- WiFi Icon -->
<g transform="translate(68, 592)" stroke="#7fb9b3" stroke-width="1.5" fill="none" stroke-linecap="round">
<path d="M0 0 a14 14 0 0 1 18 0" />
<path d="M3 4 a9 9 0 0 1 12 0" />
<circle cx="9" cy="8" r="1" fill="#7fb9b3" />
</g>
<!-- Instagram / Social Icon -->
<g transform="translate(225, 592)" stroke="#7fb9b3" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round">
<rect x="0" y="0" width="13" height="13" rx="3.5" />
<circle cx="6.5" cy="6.5" r="3" />
<circle cx="10" cy="3" r="0.5" fill="#7fb9b3" />
</g>
</g>
<!-- 3. Clean Placeholder Box for QR Code (aligned to the app's fixed 33.08% / 55% overlay) -->
<g id="qr-placeholder" transform="translate(90, 215)">
<rect width="220" height="220" rx="20" fill="#f9f5eb" stroke="url(#gold-accent-8)" stroke-width="2" />
<rect x="12" y="12" width="196" height="196" rx="14" fill="#FFFFFF" stroke="#e6dcc0" stroke-dasharray="4 3" />
<text x="110" y="115" font-family="Inter, sans-serif" font-size="12" font-weight="600" fill="#a3803a" text-anchor="middle">QR KOD ALANI</text>
</g>
<!-- 4. Native Editable Texts -->
<g id="layer-texts" font-family="Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" text-anchor="middle">
<text x="200" y="130" font-size="22" font-weight="800" fill="#ffffff" letter-spacing="2">EMERALD &amp; GOLD</text>
<text x="200" y="150" font-size="11" font-weight="500" fill="#d1a752" letter-spacing="2.5">TEMASSIZ DİJİTAL MENÜ</text>
<text x="200" y="475" font-size="13" font-weight="700" fill="#ffffff" letter-spacing="1.2">MENÜYÜ GÖRMEK İÇİN OKUTUN</text>
<text x="200" y="495" font-size="11" font-weight="400" fill="#8fbdb8">Kameranız ile QR kodu taratın</text>
<rect x="140" y="520" width="120" height="28" rx="14" fill="#0f3c3a" stroke="#1a5350" stroke-width="1" />
<text x="200" y="539" font-size="12" font-weight="600" fill="#d1a752" letter-spacing="0.5">Masa No: 01</text>
<text x="128" y="602" font-size="10" font-weight="400" fill="#7fb9b3" text-anchor="start">Emerald_Guest</text>
<text x="246" y="602" font-size="10" font-weight="400" fill="#7fb9b3" text-anchor="start">@emeraldgold</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

+74
View File
@@ -0,0 +1,74 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 650" width="400" height="650">
<defs>
<linearGradient id="vintage-bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#f8f6f0" />
<stop offset="100%" stop-color="#e2ded5" />
</linearGradient>
</defs>
<!-- 1. Backgrounds & Contrast Panels -->
<g id="layer-background">
<rect id="bg-canvas" width="400" height="650" fill="url(#vintage-bg)" />
<rect id="main-card" x="20" y="20" width="360" height="610" rx="10" fill="#ffffff" stroke="#d5cebe" stroke-width="1.5" />
<rect id="inner-border" x="30" y="30" width="340" height="590" fill="none" stroke="#1e3246" stroke-width="1" />
<rect id="inner-border-2" x="34" y="34" width="332" height="582" fill="none" stroke="#d5cebe" stroke-width="1" />
<!-- Vintage corner flourishes -->
<g stroke="#a88b58" stroke-width="1" fill="none">
<path d="M30 60 v-30 h30" />
<path d="M370 60 v-30 h-30" />
<path d="M30 590 v30 h30" />
<path d="M370 590 v30 h-30" />
</g>
</g>
<!-- 2. Decorative Icons & Accents -->
<g id="layer-icons">
<!-- Ornamental crest -->
<g transform="translate(170, 52)" stroke="#1e3246" stroke-width="1.3" fill="none" stroke-linecap="round" stroke-linejoin="round">
<circle cx="30" cy="22" r="16" />
<path d="M14 22 h32 M30 6 v32" stroke-width="0.75" opacity="0.6" />
</g>
<g id="divider-accent">
<path d="M120 162 h60 M220 162 h60" stroke="#a88b58" stroke-width="1" opacity="0.7" />
<circle cx="200" cy="162" r="3" fill="none" stroke="#a88b58" stroke-width="1" />
</g>
<!-- WiFi Icon -->
<g transform="translate(68, 592)" stroke="#7a7267" stroke-width="1.5" fill="none" stroke-linecap="round">
<path d="M0 0 a14 14 0 0 1 18 0" />
<path d="M3 4 a9 9 0 0 1 12 0" />
<circle cx="9" cy="8" r="1" fill="#7a7267" />
</g>
<!-- Instagram / Social Icon -->
<g transform="translate(225, 592)" stroke="#7a7267" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round">
<rect x="0" y="0" width="13" height="13" rx="3.5" />
<circle cx="6.5" cy="6.5" r="3" />
<circle cx="10" cy="3" r="0.5" fill="#7a7267" />
</g>
</g>
<!-- 3. Clean Placeholder Box for QR Code (aligned to the app's fixed 33.08% / 55% overlay) -->
<g id="qr-placeholder" transform="translate(90, 215)">
<rect width="220" height="220" fill="#FFFFFF" stroke="#1e3246" stroke-width="1.5" />
<rect x="10" y="10" width="200" height="200" fill="none" stroke="#d5cebe" stroke-dasharray="4 3" />
<text x="110" y="115" font-family="Georgia, 'Times New Roman', serif" font-size="12" font-weight="600" fill="#a88b58" text-anchor="middle">QR KOD ALANI</text>
</g>
<!-- 4. Native Editable Texts -->
<g id="layer-texts" font-family="Georgia, 'Times New Roman', serif" text-anchor="middle">
<text x="200" y="130" font-size="22" font-weight="700" fill="#1e3246" letter-spacing="1.5">VINTAGE TABLE MENU</text>
<text x="200" y="150" font-size="11" font-weight="500" fill="#a88b58" letter-spacing="2.5" font-family="Inter, sans-serif">TEMASSIZ DİJİTAL MENÜ</text>
<text x="200" y="475" font-size="13" font-weight="700" fill="#1e3246" letter-spacing="1">MENÜYÜ GÖRMEK İÇİN OKUTUN</text>
<text x="200" y="495" font-size="11" font-weight="400" fill="#7a7267" font-family="Inter, sans-serif">Kameranız ile QR kodu taratın</text>
<rect x="140" y="520" width="120" height="28" fill="#ffffff" stroke="#1e3246" stroke-width="1" />
<text x="200" y="539" font-size="12" font-weight="600" fill="#1e3246" letter-spacing="0.5" font-family="Inter, sans-serif">Masa No: 01</text>
<text x="128" y="602" font-size="10" font-weight="400" fill="#a88b58" text-anchor="start" font-family="Inter, sans-serif">Vintage_Guest</text>
<text x="246" y="602" font-size="10" font-weight="400" fill="#a88b58" text-anchor="start" font-family="Inter, sans-serif">@vintagetable</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

@@ -0,0 +1,213 @@
"use client";
import React, { useEffect, useState } from "react";
import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
import AdminAuthGuard from "@/components/AdminAuthGuard";
import { getAdminSession, logoutAdmin, AdminUser } from "@/lib/admin-auth";
import { AyrisFooterBadge } from "@/components/AyrisFooterBadge";
export default function AdminLayoutClient({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
const router = useRouter();
const [adminUser, setAdminUser] = useState<AdminUser | null>(null);
useEffect(() => {
setAdminUser(getAdminSession());
}, [pathname]);
const handleLogout = async () => {
if (confirm("Menulio Admin panelinden çıkış yapmak istediğinize emin misiniz?")) {
await logoutAdmin();
router.replace("/admin/login");
}
};
// Login page doesn't use the Admin Dashboard Shell (sidebar, header, etc.)
if (pathname === "/admin/login") {
return <>{children}</>;
}
return (
<AdminAuthGuard>
<div className="min-h-screen bg-[#09090b] text-[#fafafa] flex flex-col md:flex-row antialiased selection:bg-[#D4AF37]/30 selection:text-[#D4AF37]">
{/* Sol Sidebar */}
<aside className="w-full md:w-72 bg-[#121215] border-b md:border-b-0 md:border-r border-white/10 flex flex-col justify-between shrink-0">
<div>
{/* Logo & Başlık */}
<div className="p-6 border-b border-white/10 flex items-center justify-between">
<Link href="/admin" className="flex items-center gap-3 group">
<div className="w-9 h-9 rounded-xl bg-gradient-to-tr from-[#D4AF37] to-[#F59E0B] flex items-center justify-center shadow-lg shadow-[#D4AF37]/20">
<span className="font-extrabold text-[#0C0C0E] text-lg tracking-wider">M</span>
</div>
<div>
<div className="font-bold text-white tracking-wide text-base group-hover:text-[#D4AF37] transition-colors">
MENULIO
</div>
<div className="text-[10px] font-semibold text-[#D4AF37] tracking-widest uppercase">
Admin Stüdyosu
</div>
</div>
</Link>
</div>
{/* Navigasyon Linkleri */}
<nav className="p-4 space-y-1.5">
<div className="text-[10px] font-bold text-zinc-500 uppercase tracking-wider px-3 mb-2">
Tasarım & Şablonlar
</div>
<Link
href="/admin/templates"
className={`flex items-center gap-3 px-3 py-2.5 rounded-xl font-semibold text-sm transition-all shadow-sm ${
pathname === "/admin/templates"
? "bg-[#D4AF37]/10 text-[#D4AF37] border border-[#D4AF37]/25"
: "text-zinc-400 hover:text-white hover:bg-white/5"
}`}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
QR Şablon Stüdyosu
<span className="ml-auto text-[10px] bg-[#D4AF37] text-black font-extrabold px-1.5 py-0.5 rounded-full">
Masa Kartı
</span>
</Link>
<Link
href="/admin/menu-templates"
className={`flex items-center gap-3 px-3 py-2.5 rounded-xl font-semibold text-sm transition-all shadow-sm ${
pathname === "/admin/menu-templates"
? "bg-[#D4AF37]/10 text-[#D4AF37] border border-[#D4AF37]/25"
: "text-zinc-400 hover:text-white hover:bg-white/5"
}`}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg>
Web Menü Şablonları
<span className="ml-auto text-[10px] bg-[#D4AF37] text-black font-extrabold px-1.5 py-0.5 rounded-full">
10 Tema
</span>
</Link>
<Link
href="/admin"
className={`flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium transition-all ${
pathname === "/admin"
? "bg-[#D4AF37]/10 text-[#D4AF37] border border-[#D4AF37]/25"
: "text-zinc-400 hover:text-white hover:bg-white/5"
}`}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
Genel Bakış & İstatistikler
</Link>
<div className="text-[10px] font-bold text-zinc-500 uppercase tracking-wider px-3 pt-4 mb-2">
Önizleme & Test
</div>
<Link
href="/templates"
target="_blank"
className="flex items-center gap-3 px-3 py-2.5 rounded-xl text-zinc-400 hover:text-white hover:bg-white/5 text-sm font-medium transition-all"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg>
Web Menü Simülatörü
<svg className="w-3.5 h-3.5 ml-auto text-zinc-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
</Link>
<Link
href="/qr"
target="_blank"
className="flex items-center gap-3 px-3 py-2.5 rounded-xl text-zinc-400 hover:text-white hover:bg-white/5 text-sm font-medium transition-all"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 4v1m6 11h2m-6 0h-2v4m0-11v3m0 0h.01M12 12h4.01M16 20h4M4 12h4m12 0h.01M5 8h2a1 1 0 001-1V5a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1zm12 0h2a1 1 0 001-1V5a1 1 0 00-1-1h-2a1 1 0 00-1 1v2a1 1 0 001 1zM5 20h2a1 1 0 001-1v-2a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1z" />
</svg>
Web Masa Standı Üretici
<svg className="w-3.5 h-3.5 ml-auto text-zinc-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
</Link>
</nav>
</div>
{/* Alt Yönetici Profil & Çıkış Kartı */}
<div className="p-4 border-t border-white/10 space-y-3">
<div className="p-3 rounded-xl bg-white/[0.04] border border-white/5 flex items-center justify-between">
<div className="flex items-center gap-2.5 min-w-0">
<div className="w-8 h-8 rounded-lg bg-[#D4AF37]/20 border border-[#D4AF37]/40 flex items-center justify-center shrink-0">
<span className="font-extrabold text-[#D4AF37] text-xs">
{adminUser?.email?.[0]?.toUpperCase() || "A"}
</span>
</div>
<div className="min-w-0">
<div className="text-xs font-bold text-white truncate">
{adminUser?.name || "Yönetici"}
</div>
<div className="text-[10px] text-zinc-400 truncate">
{adminUser?.email || "admin@menul.io"}
</div>
</div>
</div>
<button
type="button"
onClick={handleLogout}
className="p-1.5 rounded-lg text-zinc-400 hover:text-red-400 hover:bg-red-500/10 transition-colors"
title="Çıkış Yap"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
</svg>
</button>
</div>
</div>
</aside>
{/* Sağ Ana İçerik */}
<main className="flex-1 flex flex-col min-w-0 bg-[#0C0C0E]">
{/* Üst Bar */}
<header className="h-16 border-b border-white/10 px-6 flex items-center justify-between bg-[#121215]/50 backdrop-blur shrink-0">
<div className="flex items-center gap-3">
<div className="text-sm font-semibold text-zinc-300">
Dinamik Şablon & İçerik Yönetimi
</div>
</div>
<div className="flex items-center gap-4">
<div className="hidden sm:flex items-center gap-2 px-3 py-1.5 rounded-full bg-white/5 border border-white/10 text-xs text-zinc-300">
<span className="w-2 h-2 rounded-full bg-[#D4AF37]" />
Oturum: <span className="font-semibold text-[#D4AF37]">{adminUser?.role === "super_admin" ? "Süper Admin" : "Yönetici"}</span>
</div>
<button
type="button"
onClick={handleLogout}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-red-500/10 border border-red-500/20 text-xs font-semibold text-red-400 hover:bg-red-500/20 transition-all cursor-pointer"
>
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
</svg>
Çıkış Yap
</button>
</div>
</header>
{/* Sayfa İçeriği */}
<div className="flex-1 p-6 md:p-8 overflow-y-auto">
{children}
</div>
<AyrisFooterBadge className="mt-auto border-t border-white/10 bg-[#09090b]" />
</main>
</div>
</AdminAuthGuard>
);
}
+16
View File
@@ -0,0 +1,16 @@
import type { Metadata } from "next";
import React from "react";
import AdminLayoutClient from "./AdminLayoutClient";
export const metadata: Metadata = {
title: "Menulio Admin Panel | Şablon & Sistem Yönetimi",
description: "Menulio QR Şablon Stüdyosu, Masa Standı Tasarım Motoru ve Sistem Yönetimi",
};
export default function AdminLayout({
children,
}: {
children: React.ReactNode;
}) {
return <AdminLayoutClient>{children}</AdminLayoutClient>;
}
+205
View File
@@ -0,0 +1,205 @@
"use client";
import React, { useState, useEffect, Suspense } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import Link from "next/link";
import { loginAdmin, getAdminSession } from "@/lib/admin-auth";
function AdminLoginForm() {
const router = useRouter();
const searchParams = useSearchParams();
const callbackUrl = searchParams.get("callbackUrl") || "/admin";
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [showPassword, setShowPassword] = useState(false);
const [errorMsg, setErrorMsg] = useState("");
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
// If already logged in, redirect straight to admin dashboard
const session = getAdminSession();
if (session) {
router.replace(callbackUrl);
}
}, [router, callbackUrl]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!email.trim() || !password.trim()) {
setErrorMsg("Lütfen e-posta ve şifrenizi girin.");
return;
}
setIsLoading(true);
setErrorMsg("");
try {
const res = await loginAdmin(email, password);
if (res.success) {
router.replace(callbackUrl);
} else {
setErrorMsg(res.error || "Giriş başarısız.");
}
} catch {
setErrorMsg("Bir hata oluştu. Lütfen tekrar deneyin.");
} finally {
setIsLoading(false);
}
};
const handleQuickDemoLogin = () => {
setEmail("admin@menul.io");
setPassword("menulio2026");
};
return (
<div className="min-h-screen bg-[#09090b] text-white flex flex-col justify-center items-center p-4 relative overflow-hidden selection:bg-[#D4AF37]/30 selection:text-[#D4AF37]">
{/* Arka Plan Işık Efektleri */}
<div className="absolute top-1/4 left-1/2 -translate-x-1/2 -translate-y-1/2 w-96 h-96 bg-[#D4AF37]/10 rounded-full blur-3xl pointer-events-none" />
<div className="absolute bottom-10 right-10 w-72 h-72 bg-amber-500/5 rounded-full blur-3xl pointer-events-none" />
{/* Kart Konteynırı */}
<div className="w-full max-w-md bg-[#121215]/80 backdrop-blur-xl border border-white/10 rounded-3xl p-8 shadow-2xl shadow-black/80 relative z-10 space-y-6">
{/* Logo & Başlık */}
<div className="text-center space-y-2">
<Link href="/" className="inline-flex items-center gap-3 group mb-2">
<div className="w-12 h-12 rounded-2xl bg-gradient-to-tr from-[#D4AF37] to-[#F59E0B] flex items-center justify-center shadow-lg shadow-[#D4AF37]/30 group-hover:scale-105 transition-transform">
<span className="font-black text-[#0C0C0E] text-2xl tracking-wider">M</span>
</div>
</Link>
<h1 className="text-2xl font-black text-white tracking-tight">
Menulio Admin Girişi
</h1>
<p className="text-xs text-zinc-400">
QR Şablon Stüdyosu ve Sistem Yönetim Paneli
</p>
</div>
{/* Hata Bildirimi */}
{errorMsg && (
<div className="p-3.5 rounded-xl bg-red-500/10 border border-red-500/30 text-red-400 text-xs font-semibold flex items-center gap-2 animate-shake">
<svg className="w-4 h-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
{errorMsg}
</div>
)}
{/* Giriş Formu */}
<form onSubmit={handleSubmit} className="space-y-4">
{/* E-posta */}
<div className="space-y-1.5">
<label className="text-xs font-bold text-zinc-300 uppercase tracking-wider block">
Yönetici E-Postası
</label>
<div className="relative">
<input
type="text"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="admin@menul.io"
className="w-full bg-white/[0.04] border border-white/10 rounded-xl px-4 py-3 text-sm text-white placeholder-zinc-500 focus:outline-none focus:border-[#D4AF37] focus:ring-1 focus:ring-[#D4AF37] transition-all"
required
/>
<div className="absolute right-3.5 top-1/2 -translate-y-1/2 text-zinc-500 pointer-events-none">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
</div>
</div>
</div>
{/* Şifre */}
<div className="space-y-1.5">
<label className="text-xs font-bold text-zinc-300 uppercase tracking-wider block">
Şifre
</label>
<div className="relative">
<input
type={showPassword ? "text" : "password"}
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
className="w-full bg-white/[0.04] border border-white/10 rounded-xl px-4 py-3 text-sm text-white placeholder-zinc-500 focus:outline-none focus:border-[#D4AF37] focus:ring-1 focus:ring-[#D4AF37] transition-all pr-10"
required
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3.5 top-1/2 -translate-y-1/2 text-zinc-400 hover:text-white transition-colors"
title={showPassword ? "Şifreyi gizle" : "Şifreyi göster"}
>
{showPassword ? (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858-5.908a10.05 10.05 0 012.122-.388c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21M3 3l18 18" />
</svg>
) : (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
)}
</button>
</div>
</div>
{/* Giriş Yap Butonu */}
<button
type="submit"
disabled={isLoading}
className="w-full mt-2 bg-gradient-to-r from-[#D4AF37] to-[#F59E0B] text-[#0C0C0E] font-extrabold text-sm py-3.5 px-4 rounded-xl shadow-lg shadow-[#D4AF37]/20 hover:opacity-95 active:scale-[0.99] transition-all disabled:opacity-50 flex items-center justify-center gap-2 cursor-pointer"
>
{isLoading ? (
<>
<svg className="w-4 h-4 animate-spin text-[#0C0C0E]" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</svg>
Giriş Yapılıyor...
</>
) : (
<>
Giriş Yap
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M14 5l7 7m0 0l-7 7m7-7H3" />
</svg>
</>
)}
</button>
</form>
{/* Hızlı Demo Girişi */}
<div className="pt-4 border-t border-white/10 space-y-2 text-center">
<div className="text-[11px] text-zinc-500">
Varsayılan Admin Girişi: <span className="font-mono text-zinc-300">admin@menul.io / menulio2026</span>
</div>
<button
type="button"
onClick={handleQuickDemoLogin}
className="text-xs text-[#D4AF37] hover:underline font-medium transition-colors"
>
Formu Varsayılan Admin Bilgileri ile Doldur
</button>
</div>
</div>
{/* Alt Footer */}
<div className="mt-8 text-center text-xs text-zinc-600 relative z-10">
Menulio QR & Masa Kartı Yönetim Ekosistemi &copy; {new Date().getFullYear()}
</div>
</div>
);
}
export default function AdminLoginPage() {
return (
<Suspense fallback={
<div className="min-h-screen bg-[#09090b] flex items-center justify-center">
<div className="w-8 h-8 rounded-full border-2 border-[#D4AF37] border-t-transparent animate-spin" />
</div>
}>
<AdminLoginForm />
</Suspense>
);
}
@@ -0,0 +1,11 @@
import type { Metadata } from "next";
import { MenuTemplatesAdminClient } from "@/components/MenuTemplatesAdminClient";
export const metadata: Metadata = {
title: "Web Menü Şablon Stüdyosu | Menulio Admin",
description: "Web restoran menü temalarını, renklerini ve düzen ayarlarını canlı önizleme ile yönetin.",
};
export default function MenuTemplatesAdminPage() {
return <MenuTemplatesAdminClient />;
}
+169
View File
@@ -0,0 +1,169 @@
import Link from "next/link";
import { QR_STAND_PRESETS } from "@menulio/shared";
export default function AdminDashboardPage() {
const presetCount = Object.keys(QR_STAND_PRESETS).length;
return (
<div className="max-w-6xl mx-auto space-y-8">
{/* Karşılama Başlığı */}
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div>
<h1 className="text-2xl md:text-3xl font-extrabold text-white tracking-tight">
Menulio Sistem & Şablon Yönetimi
</h1>
<p className="text-zinc-400 text-sm mt-1">
Masa standı şablonlarını, JSON şablon motorunu ve restoran görünüm ayarlarını yönetin.
</p>
</div>
<div className="flex items-center gap-3 flex-wrap self-start">
<Link
href="/admin/menu-templates"
className="inline-flex items-center gap-2 bg-white/5 border border-white/10 hover:bg-white/10 text-white font-bold px-4 py-2.5 rounded-xl transition-all text-sm"
>
<svg className="w-4 h-4 text-[#D4AF37]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg>
Web Menü Şablon Stüdyosu
</Link>
<Link
href="/admin/templates"
className="inline-flex items-center gap-2 bg-gradient-to-r from-[#D4AF37] to-[#F59E0B] text-[#0C0C0E] font-bold px-5 py-2.5 rounded-xl shadow-lg shadow-[#D4AF37]/20 hover:opacity-95 transition-all text-sm"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 4v16m8-8H4" />
</svg>
QR Şablon Stüdyosu
</Link>
</div>
</div>
{/* İstatistik Kartları */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<div className="p-5 rounded-2xl bg-[#121215] border border-white/10 space-y-2">
<div className="text-xs font-semibold text-zinc-400 uppercase tracking-wider">
Aktif Şablonlar
</div>
<div className="text-3xl font-black text-white">{presetCount} Adet</div>
<div className="text-xs text-emerald-400 font-medium flex items-center gap-1">
<span></span> Mobil & Web Canlıda
</div>
</div>
<div className="p-5 rounded-2xl bg-[#121215] border border-white/10 space-y-2">
<div className="text-xs font-semibold text-zinc-400 uppercase tracking-wider">
Şablon Motoru Tipi
</div>
<div className="text-2xl font-black text-[#D4AF37]">JSON Driven</div>
<div className="text-xs text-zinc-400 font-medium">
0 App Store güncellemesi
</div>
</div>
<div className="p-5 rounded-2xl bg-[#121215] border border-white/10 space-y-2">
<div className="text-xs font-semibold text-zinc-400 uppercase tracking-wider">
Baskı Çıktı Formatı
</div>
<div className="text-3xl font-black text-white">A5 & 300DPI</div>
<div className="text-xs text-zinc-400 font-medium">
PDF + Vektörel PNG
</div>
</div>
<div className="p-5 rounded-2xl bg-[#121215] border border-white/10 space-y-2">
<div className="text-xs font-semibold text-zinc-400 uppercase tracking-wider">
API Durumu
</div>
<div className="text-2xl font-black text-emerald-400">Aktif & Senkron</div>
<div className="text-xs text-zinc-400 font-medium">
REST API v1
</div>
</div>
</div>
{/* Nasıl Çalışır / Mimari Rehber Kutusu */}
<div className="p-6 rounded-2xl bg-gradient-to-br from-[#18181B] to-[#121215] border border-[#D4AF37]/20 relative overflow-hidden">
<div className="relative z-10 space-y-4">
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-[#D4AF37]/15 border border-[#D4AF37]/30 text-xs font-bold text-[#D4AF37]">
💡 DİNAMİK ŞABLON MİMARİSİ
</div>
<h2 className="text-xl font-bold text-white">
Mobil Uygulamayı Güncellemeden Yeni Şablon Yayınlama
</h2>
<p className="text-sm text-zinc-300 leading-relaxed max-w-3xl">
Menulio, QR masa kartı şablonlarını doğrudan kod içine gömmek yerine <strong>JSON şablon spesifikasyonu</strong> ile çalışır.
Admin panelinden yeni bir SVG/Görsel arka plan ve varsayılan QR/metin koordinatları tanımladığınızda,
tüm mobil kullanıcılar uygulamayı güncellemek zorunda kalmadan yeni şablonu anında görürler.
</p>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 pt-2">
<div className="p-4 rounded-xl bg-black/40 border border-white/5 space-y-1.5">
<div className="text-xs font-bold text-[#D4AF37]">1. Arka Planı Belirle</div>
<div className="text-xs text-zinc-400">
Figma veya Illustrator'dan SVG/PNG arka planı oluşturup yükleyin veya renk seçin.
</div>
</div>
<div className="p-4 rounded-xl bg-black/40 border border-white/5 space-y-1.5">
<div className="text-xs font-bold text-[#D4AF37]">2. QR & Katmanları Ayarla</div>
<div className="text-xs text-zinc-400">
QR kodun yerini, boyutunu, restoran ismi ve slogan metinlerinin varsayılan yerlerini belirleyin.
</div>
</div>
<div className="p-4 rounded-xl bg-black/40 border border-white/5 space-y-1.5">
<div className="text-xs font-bold text-[#D4AF37]">3. Tek Tıkla Yayınla</div>
<div className="text-xs text-zinc-400">
Kaydet butonuna bastığınız anda API üzerinden tüm restoranların mobil uygulamasına dağıtılır.
</div>
</div>
</div>
</div>
</div>
{/* Hızlı Şablonlar Listesi */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-lg font-bold text-white">Mevcut Hazır Şablonlar</h3>
<Link
href="/admin/templates"
className="text-xs font-semibold text-[#D4AF37] hover:underline"
>
Tümünü Yönet & Düzenle &rarr;
</Link>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{Object.values(QR_STAND_PRESETS).slice(0, 4).map((preset) => (
<div
key={preset.key}
className="p-4 rounded-xl bg-[#121215] border border-white/10 space-y-3 flex flex-col justify-between"
>
<div>
<div className="flex items-center justify-between mb-2">
<span className="text-[11px] font-mono text-zinc-500">{preset.key}</span>
<span className="text-[10px] bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 px-2 py-0.5 rounded-full font-bold">
Aktif
</span>
</div>
<div className="font-bold text-white text-sm line-clamp-1">{preset.name}</div>
<div className="text-xs text-zinc-400 mt-0.5">{preset.categoryName || "Masa Kartı"}</div>
</div>
<div className="pt-3 border-t border-white/5 flex items-center justify-between text-xs text-zinc-400">
<span>Oran: {preset.aspectRatio || "1/1"}</span>
<Link
href={`/admin/templates?key=${preset.key}`}
className="font-bold text-[#D4AF37] hover:underline"
>
Düzenle
</Link>
</div>
</div>
))}
</div>
</div>
</div>
);
}
+26
View File
@@ -0,0 +1,26 @@
import type { Metadata } from "next";
import { TemplateStudioAdminClient } from "@/components/TemplateStudioAdminClient";
export const metadata: Metadata = {
title: "QR Şablon Stüdyosu | Menulio Admin",
description: "Masa standı şablonlarını görsel editör veya JSON ile tasarlayın, canlı önizleyin ve anında yayınlayın.",
};
export default function AdminTemplatesPage() {
return (
<div className="max-w-7xl mx-auto space-y-6">
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-2 border-b border-white/10 pb-5">
<div>
<h1 className="text-2xl font-extrabold text-white tracking-tight">
QR Masa Standı Şablon Stüdyosu
</h1>
<p className="text-zinc-400 text-xs mt-1">
Uygulama güncellemesi gerektirmeden yeni şablonlar ekleyin, renkleri ve QR koordinatlarını düzenleyin.
</p>
</div>
</div>
<TemplateStudioAdminClient />
</div>
);
}
+142
View File
@@ -0,0 +1,142 @@
import { NextResponse } from "next/server";
import fs from "fs/promises";
import path from "path";
import { supabase } from "@/lib/supabase";
const DATA_FILE = path.join(process.cwd(), "src/data/qr-templates.json");
function mapFromDb(row: any) {
return {
key: row.key,
name: row.name,
categoryName: row.category_name,
aspectRatio: row.aspect_ratio || "1/1",
bgType: row.bg_type || "gradient",
bgImageUrl: row.bg_image_url || undefined,
bgGradient: row.bg_gradient || undefined,
plaqueColor: row.plaque_color || undefined,
borderAccent: row.border_accent || undefined,
accentText: row.accent_text || undefined,
headerText: row.header_text || undefined,
subText: row.sub_text || undefined,
dividerColor: row.divider_color || undefined,
qrConfig: row.qr_config,
layers: row.layers,
isActive: row.is_active ?? true,
createdAt: row.created_at,
updatedAt: row.updated_at,
};
}
function mapToDb(item: any) {
return {
key: item.key,
name: item.name,
category_name: item.categoryName || null,
aspect_ratio: item.aspectRatio || "1/1",
bg_type: item.bgType || "gradient",
bg_image_url: item.bgImageUrl || null,
bg_gradient: item.bgGradient || null,
plaque_color: item.plaqueColor || null,
border_accent: item.borderAccent || null,
accent_text: item.accentText || null,
header_text: item.headerText || null,
sub_text: item.subText || null,
divider_color: item.dividerColor || null,
qr_config: item.qrConfig || item.defaultQrConfig || null,
layers: item.layers || item.defaultLayers || null,
is_active: item.isActive !== undefined ? item.isActive : true,
updated_at: new Date().toISOString(),
};
}
async function readFromDisk(): Promise<any[]> {
try {
const raw = await fs.readFile(DATA_FILE, "utf-8");
return JSON.parse(raw);
} catch {
return [];
}
}
async function writeToDisk(list: any[]): Promise<void> {
try {
await fs.writeFile(DATA_FILE, JSON.stringify(list, null, 2), "utf-8");
} catch {}
}
export async function GET() {
try {
const { data, error } = await supabase
.from("qr_stand_templates")
.select("*")
.order("created_at", { ascending: false });
if (!error && data && data.length > 0) {
const mapped = data.map(mapFromDb);
await writeToDisk(mapped);
return NextResponse.json({ templates: mapped, source: "supabase_db" });
}
} catch {}
const diskTemplates = await readFromDisk();
return NextResponse.json({ templates: diskTemplates, source: "disk_cache" });
}
export async function POST(req: Request) {
try {
const body = await req.json();
if (!body || !body.key) {
return NextResponse.json({ error: "Şablon key zorunludur" }, { status: 400 });
}
// 1. Supabase'e Upsert et
try {
const dbRow = mapToDb(body);
await supabase.from("qr_stand_templates").upsert(dbRow, { onConflict: "key" });
} catch (dbErr) {
console.warn("Supabase upsert warning:", dbErr);
}
// 2. Diske de yaz (offline / backup)
const diskList = await readFromDisk();
const existingIdx = diskList.findIndex((t: any) => t.key === body.key);
if (existingIdx >= 0) {
diskList[existingIdx] = { ...diskList[existingIdx], ...body };
} else {
diskList.unshift(body);
}
await writeToDisk(diskList);
return NextResponse.json({ success: true, template: body });
} catch (err: any) {
return NextResponse.json({ error: err.message }, { status: 500 });
}
}
export async function DELETE(req: Request) {
try {
const { searchParams } = new URL(req.url);
const key = searchParams.get("key");
if (!key) {
return NextResponse.json({ error: "Şablon key zorunludur" }, { status: 400 });
}
// 1. Supabase'den Sil
try {
await supabase.from("qr_stand_templates").delete().eq("key", key);
} catch (dbErr) {
console.warn("Supabase delete warning:", dbErr);
}
// 2. Diskten de Sil
const diskList = await readFromDisk();
const filtered = diskList.filter((t: any) => t.key !== key);
await writeToDisk(filtered);
return NextResponse.json({ success: true, deletedKey: key });
} catch (err: any) {
return NextResponse.json({ error: err.message }, { status: 500 });
}
}
+12 -5
View File
@@ -17,6 +17,7 @@ interface MenuItemRow {
price: number;
image_url: string | null;
is_active: boolean;
translations: Record<string, { name: string; description: string | null }> | null;
}
interface MenuCategoryRow {
@@ -25,6 +26,7 @@ interface MenuCategoryRow {
description: string | null;
is_active: boolean;
menu_items: MenuItemRow[];
translations: Record<string, { name: string }> | null;
}
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
@@ -71,14 +73,14 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
async function fetchRestaurantBySlug(slug: string) {
let { data: bySlug, error } = await supabase
.from("restaurants")
.select("id, name, slug, logo_url, cover_url, phone, address")
.select("id, name, slug, logo_url, cover_url, phone, address, enabled_languages")
.eq("slug", slug)
.maybeSingle();
if (error && error.code === "42703") {
const { data: fallback } = await supabase
.from("restaurants")
.select("id, name, slug, logo_url, phone, address")
.select("id, name, slug, logo_url, phone, address, enabled_languages")
.eq("slug", slug)
.maybeSingle();
if (fallback) {
@@ -90,14 +92,14 @@ async function fetchRestaurantBySlug(slug: string) {
let { data: domainRow, error: domErr } = await supabase
.from("domains")
.select("restaurant_id, restaurants(id, name, slug, logo_url, cover_url, phone, address)")
.select("restaurant_id, restaurants(id, name, slug, logo_url, cover_url, phone, address, enabled_languages)")
.eq("hostname", slug)
.maybeSingle();
if (domErr && domErr.code === "42703") {
const { data: fallbackDom } = await supabase
.from("domains")
.select("restaurant_id, restaurants(id, name, slug, logo_url, phone, address)")
.select("restaurant_id, restaurants(id, name, slug, logo_url, phone, address, enabled_languages)")
.eq("hostname", slug)
.maybeSingle();
if (fallbackDom?.restaurants) {
@@ -150,7 +152,9 @@ export default async function PublicMenuPage({ params, searchParams }: PageProps
if (menu) {
const { data: rawCategories } = await supabase
.from("menu_categories")
.select("id, name, description, is_active, menu_items(id, name, description, price, image_url, is_active)")
.select(
"id, name, description, is_active, translations, menu_items(id, name, description, price, image_url, is_active, translations)",
)
.eq("menu_id", menu.id)
.eq("is_active", true)
.order("sort_order", { ascending: true })
@@ -161,6 +165,7 @@ export default async function PublicMenuPage({ params, searchParams }: PageProps
name: cat.name,
description: cat.description,
is_active: cat.is_active,
translations: cat.translations ?? {},
menu_items: (cat.menu_items ?? [])
.filter((item) => item.is_active)
.map((item): MenuItem => ({
@@ -170,6 +175,7 @@ export default async function PublicMenuPage({ params, searchParams }: PageProps
price: item.price,
image_url: item.image_url,
is_active: item.is_active,
translations: item.translations ?? {},
})),
}));
} else if (slug === "demo" || slug === "gusto-brasserie") {
@@ -202,6 +208,7 @@ export default async function PublicMenuPage({ params, searchParams }: PageProps
categories={categories}
initialThemeKey={themeKey}
customThemeConfig={customConfig}
enabledLanguages={restaurant.enabled_languages ?? ["tr"]}
/>
);
}
+4 -1
View File
@@ -1,6 +1,7 @@
import type { Metadata } from "next";
import Link from "next/link";
import { THEME_PRESETS } from "@/lib/theme";
import { AyrisFooterBadge } from "@/components/AyrisFooterBadge";
export const metadata: Metadata = {
title: "Menulio | AI Destekli Yeni Nesil QR Menü Platformu",
@@ -283,7 +284,7 @@ export default function HomePage() {
{/* Footer */}
<footer className="border-t border-stone-800/80 bg-stone-950 py-12 px-6">
<div className="max-w-7xl mx-auto flex flex-col sm:flex-row items-center justify-between gap-6 text-xs text-stone-500">
<div className="max-w-7xl mx-auto flex flex-col sm:flex-row items-center justify-between gap-6 text-xs text-stone-500 mb-8">
<div className="flex items-center gap-3">
<span className="font-bold text-stone-300 text-sm tracking-wider">MENULIO</span>
<span>•</span>
@@ -302,6 +303,8 @@ export default function HomePage() {
</Link>
</div>
</div>
<AyrisFooterBadge />
</footer>
</div>
);
@@ -0,0 +1,55 @@
"use client";
import React, { useEffect, useState } from "react";
import { usePathname, useRouter } from "next/navigation";
import { getAdminSession, AdminUser } from "@/lib/admin-auth";
export default function AdminAuthGuard({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
const router = useRouter();
const [session, setSession] = useState<AdminUser | null>(null);
const [isChecking, setIsChecking] = useState(true);
useEffect(() => {
// Skip auth guard for login page itself
if (pathname === "/admin/login") {
setIsChecking(false);
return;
}
const currentSession = getAdminSession();
if (!currentSession) {
const callback = encodeURIComponent(pathname);
router.replace(`/admin/login?callbackUrl=${callback}`);
} else {
setSession(currentSession);
setIsChecking(false);
}
}, [pathname, router]);
// Don't protect non-admin pages if wrapper is applied globally
if (pathname === "/admin/login") {
return <>{children}</>;
}
if (isChecking) {
return (
<div className="min-h-screen bg-[#09090b] text-[#fafafa] flex flex-col items-center justify-center p-6 antialiased selection:bg-[#D4AF37]/30 selection:text-[#D4AF37]">
<div className="flex flex-col items-center space-y-4">
<div className="relative w-14 h-14 flex items-center justify-center">
<div className="absolute inset-0 rounded-2xl bg-gradient-to-tr from-[#D4AF37] to-[#F59E0B] opacity-20 blur-md animate-pulse" />
<div className="w-12 h-12 rounded-xl bg-[#121215] border border-[#D4AF37]/40 flex items-center justify-center shadow-xl">
<span className="font-extrabold text-[#D4AF37] text-xl">M</span>
</div>
</div>
<div className="flex items-center gap-2 text-xs font-semibold text-zinc-400">
<div className="w-2 h-2 rounded-full bg-[#D4AF37] animate-ping" />
Yönetici Oturumu Doğrulanıyor...
</div>
</div>
</div>
);
}
return <>{children}</>;
}
@@ -0,0 +1,34 @@
import React from "react";
export function AyrisFooterBadge({ className = "" }: { className?: string }) {
return (
<footer className={`w-full border-t border-white/10 dark:border-zinc-800/80 py-4 px-6 text-xs text-zinc-500 dark:text-zinc-400 ${className}`}>
<div className="max-w-7xl mx-auto flex flex-col sm:flex-row items-center justify-between gap-3 text-center sm:text-left">
{/* Telif Hakkı */}
<p>© {new Date().getFullYear()} Menulio. Tüm Hakları Saklıdır.</p>
{/* Görünür Ayris Tech İmzası */}
<div className="flex items-center gap-1.5">
<span className="opacity-75">Created by</span>
<a
href="https://ayris.tech"
target="_blank"
rel="noopener noreferrer"
className="font-medium text-zinc-300 hover:text-white transition-colors duration-200 hover:underline underline-offset-4"
>
Ayris Tech
</a>
{/* Google Bot & Arama Motoru İndeksleme Katmanı (Görünmez / Screen-Reader Safe) */}
<span className="sr-only">
Legal AI & Solutions:{" "}
<a href="https://ayris.legal" rel="noopener noreferrer">AyrisLegal</a>,{" "}
<a href="https://ayrislegal.com" rel="noopener noreferrer">Ayris Legal AI</a>.{" "}
The best local spots, hidden from plain sight. Marmaris Local{" "}
<a href="https://marmarislocal.com" rel="noopener noreferrer">Marmaris Local</a>
</span>
</div>
</div>
</footer>
);
}
@@ -0,0 +1,556 @@
"use client";
import React, { useState } from "react";
import Link from "next/link";
import { THEME_PRESETS, ThemePreset } from "@/lib/theme";
import { DEMO_RESTAURANT, DEMO_CATEGORIES } from "@/lib/demo-data";
import { PublicMenuClient } from "@/components/PublicMenuClient";
export function MenuTemplatesAdminClient() {
const [themesMap, setThemesMap] = useState<Record<string, ThemePreset>>(THEME_PRESETS);
const [activeThemeKey, setActiveThemeKey] = useState<string>("coastal");
const [activeTab, setActiveTab] = useState<"gallery" | "customize">("gallery");
const [toastMsg, setToastMsg] = useState<string | null>(null);
const [jsonModalOpen, setJsonModalOpen] = useState(false);
const [importedJson, setImportedJson] = useState("");
const currentTheme = themesMap[activeThemeKey] || THEME_PRESETS.coastal!;
const showToast = (msg: string) => {
setToastMsg(msg);
setTimeout(() => setToastMsg(null), 3000);
};
const handleUpdateCurrentTheme = (updates: Partial<ThemePreset> | Partial<ThemePreset["config"]>) => {
setThemesMap((prev) => {
const existing = prev[activeThemeKey];
if (!existing) return prev;
let updatedConfig = { ...existing.config };
let updatedTopLevel = { ...existing };
if ("primaryColor" in updates || "productLayout" in updates || "categoryLayout" in updates || "headerLayout" in updates) {
updatedConfig = { ...updatedConfig, ...updates };
} else {
updatedTopLevel = { ...updatedTopLevel, ...updates };
}
return {
...prev,
[activeThemeKey]: {
...updatedTopLevel,
config: updatedConfig,
},
};
});
};
const handleSaveAll = () => {
showToast(`"${currentTheme.name}" şablon ayarları başarıyla kaydedildi!`);
};
const handleImportJsonSubmit = () => {
try {
const parsed = JSON.parse(importedJson);
if (!parsed.key || !parsed.name || !parsed.config) {
throw new Error("Geçersiz şablon JSON formatı. 'key', 'name' ve 'config' alanları zorunludur.");
}
setThemesMap((prev) => ({
...prev,
[parsed.key]: parsed,
}));
setActiveThemeKey(parsed.key);
setJsonModalOpen(false);
setImportedJson("");
showToast(`Yeni şablon "${parsed.name}" başarıyla içe aktarıldı!`);
} catch (err: any) {
alert("Hata: " + (err?.message || "Geçersiz JSON formatı"));
}
};
return (
<div className="max-w-7xl mx-auto space-y-6">
{/* Toast Bildirimi */}
{toastMsg && (
<div className="fixed top-20 right-6 z-50 bg-[#D4AF37] text-black font-extrabold px-5 py-3 rounded-2xl shadow-2xl shadow-[#D4AF37]/30 text-xs flex items-center gap-2 animate-bounce">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2.5" d="M5 13l4 4L19 7" />
</svg>
{toastMsg}
</div>
)}
{/* Üst Karşılama Barı */}
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4 bg-[#121215] border border-white/10 p-6 rounded-3xl">
<div>
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-[#D4AF37]/10 border border-[#D4AF37]/30 text-xs font-bold text-[#D4AF37] mb-2">
🎨 WEB DİNAMİK MENÜ TEMA STÜDYOSU
</div>
<h1 className="text-2xl md:text-3xl font-black text-white tracking-tight">
Web Menü Şablon Yöneticisi
</h1>
<p className="text-xs text-zinc-400 mt-1">
Restoran web menülerinin renk paletlerini, düzen yapılarını ve tipografisini canlı cihaz simülatöründe anında test edip yönetin.
</p>
</div>
<div className="flex items-center gap-3 flex-wrap">
<button
type="button"
onClick={() => {
setImportedJson(JSON.stringify(currentTheme, null, 2));
setJsonModalOpen(true);
}}
className="px-4 py-2.5 rounded-xl bg-white/5 border border-white/10 hover:bg-white/10 text-xs font-bold text-zinc-300 transition-all flex items-center gap-2 cursor-pointer"
>
<svg className="w-4 h-4 text-[#D4AF37]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
</svg>
JSON İçe/Dışa Aktar
</button>
<button
type="button"
onClick={handleSaveAll}
className="px-5 py-2.5 rounded-xl bg-gradient-to-r from-[#D4AF37] to-[#F59E0B] text-black font-extrabold text-xs shadow-lg shadow-[#D4AF37]/20 hover:opacity-95 transition-all flex items-center gap-2 cursor-pointer"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2.5" d="M5 13l4 4L19 7" />
</svg>
Değişiklikleri Yayınla & Kaydet
</button>
</div>
</div>
{/* Ana İçerik Çift Sütun Düzeni: Sol Kontrol / Sağ Canlı Simülatör */}
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 items-start">
{/* SOL PANEL: Şablon Galerisi ve Özelleştirici */}
<div className="lg:col-span-7 space-y-6">
{/* Sekme Butonları */}
<div className="flex items-center gap-2 p-1.5 rounded-2xl bg-[#121215] border border-white/10">
<button
type="button"
onClick={() => setActiveTab("gallery")}
className={`flex-1 py-2.5 px-4 rounded-xl font-extrabold text-xs transition-all flex items-center justify-center gap-2 cursor-pointer ${
activeTab === "gallery"
? "bg-[#D4AF37] text-black shadow-lg shadow-[#D4AF37]/20"
: "text-zinc-400 hover:text-white"
}`}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" />
</svg>
Hazır Şablonlar ({Object.keys(themesMap).length})
</button>
<button
type="button"
onClick={() => setActiveTab("customize")}
className={`flex-1 py-2.5 px-4 rounded-xl font-extrabold text-xs transition-all flex items-center justify-center gap-2 cursor-pointer ${
activeTab === "customize"
? "bg-[#D4AF37] text-black shadow-lg shadow-[#D4AF37]/20"
: "text-zinc-400 hover:text-white"
}`}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" />
</svg>
Seçili Temayı Özelleştir ({currentTheme.key})
</button>
</div>
{/* TAB 1: ŞABLON GALERİSİ KARTLARI */}
{activeTab === "gallery" && (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{Object.values(themesMap).map((theme) => {
const isSelected = theme.key === activeThemeKey;
return (
<div
key={theme.key}
onClick={() => setActiveThemeKey(theme.key)}
className={`p-5 rounded-2xl border transition-all cursor-pointer space-y-4 relative overflow-hidden group ${
isSelected
? "bg-[#16161A] border-[#D4AF37] ring-2 ring-[#D4AF37]/30 shadow-xl shadow-[#D4AF37]/10"
: "bg-[#121215] border-white/10 hover:border-white/20 hover:bg-white/[0.02]"
}`}
>
{/* Üst İkon & Seçim Rozeti */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span
className="w-4 h-4 rounded-full border border-black/20 shadow-sm"
style={{ backgroundColor: theme.config.primaryColor }}
/>
<span
className="w-4 h-4 rounded-full border border-black/20 shadow-sm"
style={{ backgroundColor: theme.cardBg }}
/>
</div>
{isSelected ? (
<span className="text-[10px] bg-[#D4AF37] text-black font-extrabold px-2 py-0.5 rounded-full flex items-center gap-1">
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="3" d="M5 13l4 4L19 7" />
</svg>
AKTİF
</span>
) : (
<span className="text-[10px] text-zinc-500 font-mono uppercase">
{theme.key}
</span>
)}
</div>
{/* Tema Adı & Açıklama */}
<div>
<h3 className="font-extrabold text-white text-base group-hover:text-[#D4AF37] transition-colors">
{theme.name}
</h3>
<div className="flex items-center gap-2 mt-2 text-[11px] text-zinc-400">
<span className="bg-white/5 border border-white/10 px-2 py-0.5 rounded-md capitalize font-mono">
{theme.fontFamily}
</span>
<span className="bg-white/5 border border-white/10 px-2 py-0.5 rounded-md capitalize font-mono">
{theme.config.productLayout}
</span>
<span className="bg-white/5 border border-white/10 px-2 py-0.5 rounded-md capitalize font-mono">
{theme.config.categoryLayout}
</span>
</div>
</div>
{/* Alt Butonlar */}
<div className="pt-3 border-t border-white/5 flex items-center justify-between">
<button
type="button"
onClick={(e) => {
e.stopPropagation();
setActiveThemeKey(theme.key);
setActiveTab("customize");
}}
className="text-xs font-bold text-[#D4AF37] hover:underline flex items-center gap-1"
>
Renk & Düzen Özelleştir &rarr;
</button>
</div>
</div>
);
})}
</div>
)}
{/* TAB 2: SEÇİLİ TEMAYI ÖZELLEŞTİR */}
{activeTab === "customize" && (
<div className="p-6 rounded-2xl bg-[#121215] border border-white/10 space-y-6">
<div className="flex items-center justify-between border-b border-white/10 pb-4">
<div>
<h3 className="text-lg font-bold text-white flex items-center gap-2">
<span
className="w-3.5 h-3.5 rounded-full inline-block"
style={{ backgroundColor: currentTheme.config.primaryColor }}
/>
{currentTheme.name}
</h3>
<p className="text-xs text-zinc-400 mt-0.5">
Ana renkleri, arka plan tonlarını ve düzen tiplerini anında özelleştirin.
</p>
</div>
<button
type="button"
onClick={() => setActiveTab("gallery")}
className="text-xs font-bold text-zinc-400 hover:text-white"
>
&larr; Galeriye Dön
</button>
</div>
{/* Renk Ayarları */}
<div className="space-y-4">
<h4 className="text-xs font-extrabold text-[#D4AF37] uppercase tracking-wider">
1. Renk Paleti & Tonlar
</h4>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{/* Primary Color */}
<div className="space-y-1.5 p-3 rounded-xl bg-white/[0.03] border border-white/5">
<label className="text-xs font-semibold text-zinc-300 block">
Ana Vurgu Rengi (Primary)
</label>
<div className="flex items-center gap-3">
<input
type="color"
value={currentTheme.config.primaryColor || "#D4AF37"}
onChange={(e) =>
handleUpdateCurrentTheme({ primaryColor: e.target.value })
}
className="w-10 h-10 rounded-lg cursor-pointer bg-transparent border-0"
/>
<input
type="text"
value={currentTheme.config.primaryColor || "#D4AF37"}
onChange={(e) =>
handleUpdateCurrentTheme({ primaryColor: e.target.value })
}
className="flex-1 bg-black/40 border border-white/10 rounded-lg px-3 py-1.5 text-xs text-white font-mono uppercase"
/>
</div>
</div>
{/* Background Color */}
<div className="space-y-1.5 p-3 rounded-xl bg-white/[0.03] border border-white/5">
<label className="text-xs font-semibold text-zinc-300 block">
Sayfa Arka Plan Rengi
</label>
<div className="flex items-center gap-3">
<input
type="color"
value={currentTheme.config.background || "#FAF8F5"}
onChange={(e) =>
handleUpdateCurrentTheme({ background: e.target.value })
}
className="w-10 h-10 rounded-lg cursor-pointer bg-transparent border-0"
/>
<input
type="text"
value={currentTheme.config.background || "#FAF8F5"}
onChange={(e) =>
handleUpdateCurrentTheme({ background: e.target.value })
}
className="flex-1 bg-black/40 border border-white/10 rounded-lg px-3 py-1.5 text-xs text-white font-mono uppercase"
/>
</div>
</div>
{/* Card Background */}
<div className="space-y-1.5 p-3 rounded-xl bg-white/[0.03] border border-white/5">
<label className="text-xs font-semibold text-zinc-300 block">
Ürün Kart Arka Planı
</label>
<div className="flex items-center gap-3">
<input
type="color"
value={currentTheme.cardBg.startsWith("#") ? currentTheme.cardBg : "#FFFFFF"}
onChange={(e) => handleUpdateCurrentTheme({ cardBg: e.target.value })}
className="w-10 h-10 rounded-lg cursor-pointer bg-transparent border-0"
/>
<input
type="text"
value={currentTheme.cardBg}
onChange={(e) => handleUpdateCurrentTheme({ cardBg: e.target.value })}
className="flex-1 bg-black/40 border border-white/10 rounded-lg px-3 py-1.5 text-xs text-white font-mono uppercase"
/>
</div>
</div>
{/* Text Primary */}
<div className="space-y-1.5 p-3 rounded-xl bg-white/[0.03] border border-white/5">
<label className="text-xs font-semibold text-zinc-300 block">
Ana Başlık & Fiyat Yazı Rengi
</label>
<div className="flex items-center gap-3">
<input
type="color"
value={currentTheme.textPrimary.startsWith("#") ? currentTheme.textPrimary : "#1C1917"}
onChange={(e) => handleUpdateCurrentTheme({ textPrimary: e.target.value })}
className="w-10 h-10 rounded-lg cursor-pointer bg-transparent border-0"
/>
<input
type="text"
value={currentTheme.textPrimary}
onChange={(e) => handleUpdateCurrentTheme({ textPrimary: e.target.value })}
className="flex-1 bg-black/40 border border-white/10 rounded-lg px-3 py-1.5 text-xs text-white font-mono uppercase"
/>
</div>
</div>
</div>
</div>
{/* Düzen & Yerleşim Ayarları */}
<div className="space-y-4 pt-4 border-t border-white/10">
<h4 className="text-xs font-extrabold text-[#D4AF37] uppercase tracking-wider">
2. Düzen & Yerleşim Mimarisi
</h4>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
{/* Product Layout */}
<div className="space-y-2">
<label className="text-xs font-semibold text-zinc-300 block">
Ürün Görünüm Tipi
</label>
<select
value={currentTheme.config.productLayout || "card"}
onChange={(e) =>
handleUpdateCurrentTheme({ productLayout: e.target.value as any })
}
className="w-full bg-black/40 border border-white/10 rounded-xl px-3 py-2 text-xs text-white focus:border-[#D4AF37] focus:outline-none"
>
<option value="card">Card (Kutu / Kart)</option>
<option value="list">List (Satır / Liste)</option>
</select>
</div>
{/* Category Layout */}
<div className="space-y-2">
<label className="text-xs font-semibold text-zinc-300 block">
Kategori Gezinme Tipi
</label>
<select
value={currentTheme.config.categoryLayout || "tabs"}
onChange={(e) =>
handleUpdateCurrentTheme({ categoryLayout: e.target.value as any })
}
className="w-full bg-black/40 border border-white/10 rounded-xl px-3 py-2 text-xs text-white focus:border-[#D4AF37] focus:outline-none"
>
<option value="tabs">Tabs (Yatay Sekmeler)</option>
<option value="accordion">Accordion (Açılır Kapanır)</option>
<option value="flat">Flat (Düz Sıralı)</option>
</select>
</div>
{/* Header Layout */}
<div className="space-y-2">
<label className="text-xs font-semibold text-zinc-300 block">
Header Hizalaması
</label>
<select
value={currentTheme.config.headerLayout || "centered"}
onChange={(e) =>
handleUpdateCurrentTheme({ headerLayout: e.target.value as any })
}
className="w-full bg-black/40 border border-white/10 rounded-xl px-3 py-2 text-xs text-white focus:border-[#D4AF37] focus:outline-none"
>
<option value="centered">Centered (Ortalanmış)</option>
<option value="left">Left (Sola Hizalı)</option>
</select>
</div>
</div>
</div>
{/* Tipografi */}
<div className="space-y-4 pt-4 border-t border-white/10">
<h4 className="text-xs font-extrabold text-[#D4AF37] uppercase tracking-wider">
3. Tipografi & Font Ailesi
</h4>
<div className="grid grid-cols-3 gap-3">
{[
{ id: "serif", label: "Zarif Serif", desc: "Playfair / Georgia" },
{ id: "sans", label: "Modern Sans", desc: "Inter / System" },
{ id: "heading", label: "Güçlü Display", desc: "Outfit / Montserrat" },
].map((font) => (
<button
key={font.id}
type="button"
onClick={() => handleUpdateCurrentTheme({ fontFamily: font.id as any })}
className={`p-3 rounded-xl border text-left transition-all cursor-pointer ${
currentTheme.fontFamily === font.id
? "bg-[#D4AF37]/15 border-[#D4AF37] text-white"
: "bg-white/[0.02] border-white/10 text-zinc-400 hover:text-white"
}`}
>
<div className="text-xs font-bold">{font.label}</div>
<div className="text-[10px] opacity-70 mt-0.5">{font.desc}</div>
</button>
))}
</div>
</div>
</div>
)}
</div>
{/* SAĞ PANEL: Canlı İnceleme Telefon Simülatörü */}
<div className="lg:col-span-5 sticky top-20">
<div className="bg-[#121215] border border-white/10 rounded-3xl p-5 space-y-4 shadow-2xl">
<div className="flex items-center justify-between border-b border-white/10 pb-3">
<div className="flex items-center gap-2">
<div className="w-2.5 h-2.5 rounded-full bg-emerald-400 animate-pulse" />
<span className="text-xs font-bold text-white">Canlı Telefon Simülatörü</span>
</div>
<span className="text-[10px] font-mono text-[#D4AF37] bg-[#D4AF37]/10 px-2 py-0.5 rounded-full font-bold">
{currentTheme.name}
</span>
</div>
{/* Mock iPhone Frame */}
<div className="w-full max-w-[340px] h-[640px] mx-auto rounded-[40px] border-[8px] border-zinc-800 bg-black shadow-2xl overflow-hidden relative flex flex-col">
{/* iPhone Notch Header */}
<div className="w-full h-6 bg-black shrink-0 flex items-center justify-center relative z-20">
<div className="w-24 h-4 bg-zinc-900 rounded-b-xl" />
</div>
{/* Dynamic Theme Render Component */}
<div className="flex-1 overflow-y-auto relative">
<PublicMenuClient
restaurant={DEMO_RESTAURANT}
categories={DEMO_CATEGORIES}
initialThemeKey={currentTheme.key}
customThemeConfig={currentTheme.config}
/>
</div>
{/* Home Bar Indicator */}
<div className="w-full h-4 bg-black shrink-0 flex items-center justify-center z-20">
<div className="w-28 h-1 bg-zinc-700 rounded-full" />
</div>
</div>
<div className="text-center text-[11px] text-zinc-500">
Sol taraftan yaptığınız renk ve düzen değişiklikleri anında burada simüle edilir.
</div>
</div>
</div>
</div>
{/* JSON İÇE / DIŞA AKTARMA MODAL */}
{jsonModalOpen && (
<div className="fixed inset-0 z-50 bg-black/80 backdrop-blur-sm flex items-center justify-center p-4">
<div className="w-full max-w-xl bg-[#121215] border border-white/15 rounded-3xl p-6 space-y-4 shadow-2xl">
<div className="flex items-center justify-between border-b border-white/10 pb-3">
<h3 className="font-extrabold text-white text-base">
Tema JSON Spesifikasyonu
</h3>
<button
type="button"
onClick={() => setJsonModalOpen(false)}
className="text-zinc-400 hover:text-white"
>
</button>
</div>
<p className="text-xs text-zinc-400">
Mevcut temanın JSON kodunu kopyalayabilir veya yeni bir tema JSON spesifikasyonunu buraya yapıştırıp içe aktarabilirsiniz:
</p>
<textarea
value={importedJson}
onChange={(e) => setImportedJson(e.target.value)}
rows={12}
className="w-full bg-black/60 border border-white/10 rounded-xl p-4 text-xs font-mono text-emerald-400 focus:outline-none focus:border-[#D4AF37]"
/>
<div className="flex items-center justify-end gap-3 pt-2">
<button
type="button"
onClick={() => {
navigator.clipboard.writeText(importedJson);
showToast("JSON panoya kopyalandı!");
}}
className="px-4 py-2 rounded-xl bg-white/5 border border-white/10 hover:bg-white/10 text-xs font-bold text-white cursor-pointer"
>
Panoya Kopyala
</button>
<button
type="button"
onClick={handleImportJsonSubmit}
className="px-4 py-2 rounded-xl bg-[#D4AF37] text-black font-extrabold text-xs cursor-pointer shadow-lg shadow-[#D4AF37]/20"
>
İçe Aktar & Uygula
</button>
</div>
</div>
</div>
)}
</div>
);
}

Some files were not shown because too many files have changed in this diff Show More