feat: admin auth, web menu template studio, live text editing & Ayris Tech footer backlink
This commit is contained in:
@@ -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**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📌 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.
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
"ios": {
|
"ios": {
|
||||||
"supportsTablet": false,
|
"supportsTablet": false,
|
||||||
"bundleIdentifier": "com.ayristech.menulio",
|
"bundleIdentifier": "com.ayristech.menulio",
|
||||||
|
"buildNumber": "21",
|
||||||
"icon": "./assets/icon.png",
|
"icon": "./assets/icon.png",
|
||||||
"infoPlist": {
|
"infoPlist": {
|
||||||
"NSCameraUsageDescription": "Menünüzün fotoğrafını çekerek AI ile dijitalleştirmek için kamera izni gereklidir.",
|
"NSCameraUsageDescription": "Menünüzün fotoğrafını çekerek AI ile dijitalleştirmek için kamera izni gereklidir.",
|
||||||
|
|||||||
@@ -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/
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#import <RCTAppDelegate.h>
|
||||||
|
#import <UIKit/UIKit.h>
|
||||||
|
#import <Expo/Expo.h>
|
||||||
|
|
||||||
|
@interface AppDelegate : EXAppDelegateWrapper
|
||||||
|
|
||||||
|
@end
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
+23
@@ -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 |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 489 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 489 KiB |
@@ -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>
|
||||||
@@ -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]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
//
|
||||||
|
// @generated
|
||||||
|
// A blank Swift file must be created for native modules with Swift files to work correctly.
|
||||||
|
//
|
||||||
@@ -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
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"expo.jsEngine": "hermes",
|
||||||
|
"EX_DEV_CLIENT_NETWORK_INSPECTOR": "true",
|
||||||
|
"newArchEnabled": "true"
|
||||||
|
}
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
import React, { useRef } from "react";
|
import React, { useRef } from "react";
|
||||||
import {
|
import {
|
||||||
PanResponder,
|
PanResponder,
|
||||||
Pressable,
|
|
||||||
Text,
|
Text,
|
||||||
View,
|
View,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
|
||||||
import * as Haptics from "expo-haptics";
|
import * as Haptics from "expo-haptics";
|
||||||
import { type CanvasTextLayer, getCanvasFontFamily } from "./types";
|
import { type CanvasTextLayer, getCanvasFontFamily } from "./types";
|
||||||
|
|
||||||
@@ -23,6 +21,78 @@ interface CanvasLayerItemProps {
|
|||||||
canvasHeight: 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> = ({
|
export const CanvasLayerItem: React.FC<CanvasLayerItemProps> = ({
|
||||||
layer,
|
layer,
|
||||||
isSelected,
|
isSelected,
|
||||||
@@ -191,51 +261,35 @@ export const CanvasLayerItem: React.FC<CanvasLayerItemProps> = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View style={renderBackgroundStyle()}>
|
<View style={renderBackgroundStyle()}>
|
||||||
<Text
|
{layer.curveDegree && layer.curveDegree !== 0 ? (
|
||||||
style={{
|
<CurvedTextNative
|
||||||
fontSize: layer.fontSize,
|
text={layer.text}
|
||||||
color: layer.color,
|
fontSize={layer.fontSize}
|
||||||
fontFamily: getCanvasFontFamily(layer.fontId),
|
color={layer.color}
|
||||||
fontWeight: layer.fontWeight || "bold",
|
fontFamily={getCanvasFontFamily(layer.fontId)}
|
||||||
textAlign: layer.textAlign || "center",
|
fontWeight={layer.fontWeight || "bold"}
|
||||||
letterSpacing: layer.letterSpacing ?? 0.5,
|
letterSpacing={layer.letterSpacing ?? 0.5}
|
||||||
textShadowColor: layer.hasShadow ? "rgba(0,0,0,0.8)" : "transparent",
|
curveDegree={layer.curveDegree}
|
||||||
textShadowOffset: { width: 0, height: 1 },
|
hasShadow={layer.hasShadow}
|
||||||
textShadowRadius: 3,
|
/>
|
||||||
}}
|
) : (
|
||||||
>
|
<Text
|
||||||
{layer.text}
|
style={{
|
||||||
</Text>
|
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>
|
||||||
|
|
||||||
{/* Seçili İken Üst Köşedeki Küçük Silme Rozeti */}
|
|
||||||
{isSelected && (
|
|
||||||
<Pressable
|
|
||||||
onPress={(e) => {
|
|
||||||
e.stopPropagation?.();
|
|
||||||
try {
|
|
||||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning);
|
|
||||||
} catch {}
|
|
||||||
onDelete(layer.id);
|
|
||||||
}}
|
|
||||||
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
top: -8,
|
|
||||||
right: -8,
|
|
||||||
backgroundColor: "#EF4444",
|
|
||||||
width: 18,
|
|
||||||
height: 18,
|
|
||||||
borderRadius: 9,
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
borderWidth: 1.5,
|
|
||||||
borderColor: "#0C0C0E",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Ionicons name="close" size={11} color="#FFFFFF" />
|
|
||||||
</Pressable>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export const InstagramTextEditorModal: React.FC<InstagramTextEditorModalProps> =
|
|||||||
const [textAlign, setTextAlign] = useState<"left" | "center" | "right">("center");
|
const [textAlign, setTextAlign] = useState<"left" | "center" | "right">("center");
|
||||||
const [bgStyle, setBgStyle] = useState<"none" | "solid" | "soft" | "outline">("none");
|
const [bgStyle, setBgStyle] = useState<"none" | "solid" | "soft" | "outline">("none");
|
||||||
const [bgBoxColor, setBgBoxColor] = useState<string | undefined>(undefined);
|
const [bgBoxColor, setBgBoxColor] = useState<string | undefined>(undefined);
|
||||||
|
const [curveDegree, setCurveDegree] = useState<number>(0);
|
||||||
|
|
||||||
// Modal açıldığında layer verilerini yükle
|
// Modal açıldığında layer verilerini yükle
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -54,6 +55,7 @@ export const InstagramTextEditorModal: React.FC<InstagramTextEditorModalProps> =
|
|||||||
setColor(layer.color || "#FFFFFF");
|
setColor(layer.color || "#FFFFFF");
|
||||||
setFontSize(layer.fontSize || 16);
|
setFontSize(layer.fontSize || 16);
|
||||||
setTextAlign(layer.textAlign || "center");
|
setTextAlign(layer.textAlign || "center");
|
||||||
|
setCurveDegree(layer.curveDegree || 0);
|
||||||
if (layer.bgBoxColor) {
|
if (layer.bgBoxColor) {
|
||||||
if (layer.bgBoxColor.includes("0.2") || layer.bgBoxColor.includes("0.3")) {
|
if (layer.bgBoxColor.includes("0.2") || layer.bgBoxColor.includes("0.3")) {
|
||||||
setBgStyle("soft");
|
setBgStyle("soft");
|
||||||
@@ -73,6 +75,19 @@ export const InstagramTextEditorModal: React.FC<InstagramTextEditorModalProps> =
|
|||||||
}
|
}
|
||||||
}, [layer, visible]);
|
}, [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ığı)
|
// Arka plan pill stilini sırayla değiştir (Instagram "A" butonu mantığı)
|
||||||
const handleToggleBgStyle = () => {
|
const handleToggleBgStyle = () => {
|
||||||
try {
|
try {
|
||||||
@@ -155,6 +170,7 @@ export const InstagramTextEditorModal: React.FC<InstagramTextEditorModalProps> =
|
|||||||
textAlign,
|
textAlign,
|
||||||
bgBoxColor: finalBg,
|
bgBoxColor: finalBg,
|
||||||
bgStyle,
|
bgStyle,
|
||||||
|
curveDegree,
|
||||||
hasShadow: bgStyle === "outline" || (!finalBg && color !== "#121215"),
|
hasShadow: bgStyle === "outline" || (!finalBg && color !== "#121215"),
|
||||||
});
|
});
|
||||||
onClose();
|
onClose();
|
||||||
@@ -234,6 +250,26 @@ export const InstagramTextEditorModal: React.FC<InstagramTextEditorModalProps> =
|
|||||||
)}
|
)}
|
||||||
</Pressable>
|
</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) */}
|
{/* Katmanı Sil (Varsa) */}
|
||||||
{onDelete && (
|
{onDelete && (
|
||||||
<Pressable
|
<Pressable
|
||||||
@@ -358,6 +394,69 @@ export const InstagramTextEditorModal: React.FC<InstagramTextEditorModalProps> =
|
|||||||
</View>
|
</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 */}
|
{/* ALT BÖLÜM 1: RENK SEÇİCİ BALONCUKLAR */}
|
||||||
<View style={styles.colorPickerBar}>
|
<View style={styles.colorPickerBar}>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
Pressable,
|
Pressable,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
Text,
|
Text,
|
||||||
|
TextInput,
|
||||||
View,
|
View,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
@@ -14,6 +15,7 @@ import { InstagramTextEditorModal } from "./InstagramTextEditorModal";
|
|||||||
import {
|
import {
|
||||||
type CanvasTextLayer,
|
type CanvasTextLayer,
|
||||||
type CanvasQrConfig,
|
type CanvasQrConfig,
|
||||||
|
CANVAS_FONT_OPTIONS,
|
||||||
QR_SIZE_PRESETS,
|
QR_SIZE_PRESETS,
|
||||||
QR_RADIUS_PRESETS,
|
QR_RADIUS_PRESETS,
|
||||||
getDefaultLayersForTemplate,
|
getDefaultLayersForTemplate,
|
||||||
@@ -537,7 +539,79 @@ export const QrCanvasStudio: React.FC<QrCanvasStudioProps> = ({
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Alt Sıra: Hızlı Döndürme Açısı Hapları */}
|
{/* Hızlı Metin Değiştirme Girdisi */}
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 8,
|
||||||
|
backgroundColor: "#0C0C0E",
|
||||||
|
borderRadius: 12,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
paddingVertical: 6,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: "rgba(212, 175, 55, 0.4)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Ionicons name="text-outline" size={14} color="#D4AF37" />
|
||||||
|
<TextInput
|
||||||
|
value={selectedLayer.text}
|
||||||
|
onChangeText={(newText: string) => {
|
||||||
|
onLayersChange((prev) =>
|
||||||
|
prev.map((l) => (l.id === selectedLayer.id ? { ...l, text: newText } : l))
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
placeholder="Metin yazın..."
|
||||||
|
placeholderTextColor="#71717A"
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
color: "#FFFFFF",
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: "700",
|
||||||
|
padding: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Alt Sıra 1: Font Stili Seçici */}
|
||||||
|
<View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
|
||||||
|
<Text style={{ color: "#71717A", fontSize: 10, fontWeight: "700" }}>FONT:</Text>
|
||||||
|
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ gap: 6 }}>
|
||||||
|
{CANVAS_FONT_OPTIONS.map((f) => {
|
||||||
|
const isActive = (selectedLayer.fontId || "sans") === f.id;
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
key={f.id}
|
||||||
|
onPress={() => {
|
||||||
|
try { Haptics.selectionAsync(); } catch {}
|
||||||
|
onLayersChange((prev) =>
|
||||||
|
prev.map((l) => (l.id === selectedLayer.id ? { ...l, fontId: f.id } : l))
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
paddingVertical: 4,
|
||||||
|
borderRadius: 10,
|
||||||
|
backgroundColor: isActive ? "#D4AF37" : "#242429",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
color: isActive ? "#0C0C0E" : "#FFFFFF",
|
||||||
|
fontSize: 10.5,
|
||||||
|
fontWeight: isActive ? "900" : "700",
|
||||||
|
fontFamily: f.fontFamily,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{f.label}
|
||||||
|
</Text>
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Alt Sıra 2: Hızlı Döndürme Açısı Hapları */}
|
||||||
<View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
|
<View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
|
||||||
<Text style={{ color: "#71717A", fontSize: 10, fontWeight: "700" }}>EĞİM:</Text>
|
<Text style={{ color: "#71717A", fontSize: 10, fontWeight: "700" }}>EĞİM:</Text>
|
||||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ gap: 6 }}>
|
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ gap: 6 }}>
|
||||||
@@ -573,6 +647,94 @@ export const QrCanvasStudio: React.FC<QrCanvasStudioProps> = ({
|
|||||||
})}
|
})}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* Alt Sıra 3: Kavis (Eğri Yazı) Stepper & Preset Hapları */}
|
||||||
|
<View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
|
||||||
|
<Text style={{ color: "#71717A", fontSize: 10, fontWeight: "700" }}>KAVİS:</Text>
|
||||||
|
|
||||||
|
{/* Stepper Butonları (- / +) */}
|
||||||
|
<View style={{ flexDirection: "row", alignItems: "center", backgroundColor: "#242429", borderRadius: 10, paddingHorizontal: 4, paddingVertical: 2, gap: 4 }}>
|
||||||
|
<Pressable
|
||||||
|
onPress={() => {
|
||||||
|
try { Haptics.selectionAsync(); } catch {}
|
||||||
|
onLayersChange((prev) =>
|
||||||
|
prev.map((l) =>
|
||||||
|
l.id === selectedLayer.id
|
||||||
|
? { ...l, curveDegree: Math.max(-100, (l.curveDegree || 0) - 5) }
|
||||||
|
: l
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
hitSlop={{ top: 6, bottom: 6, left: 6, right: 6 }}
|
||||||
|
style={{ width: 22, height: 22, borderRadius: 11, backgroundColor: "rgba(255,255,255,0.12)", alignItems: "center", justifyContent: "center" }}
|
||||||
|
>
|
||||||
|
<Ionicons name="remove" size={13} color="#FFFFFF" />
|
||||||
|
</Pressable>
|
||||||
|
|
||||||
|
<Text style={{ color: "#D4AF37", fontSize: 11, fontWeight: "900", minWidth: 30, textAlign: "center" }}>
|
||||||
|
{(selectedLayer.curveDegree || 0)}°
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Pressable
|
||||||
|
onPress={() => {
|
||||||
|
try { Haptics.selectionAsync(); } catch {}
|
||||||
|
onLayersChange((prev) =>
|
||||||
|
prev.map((l) =>
|
||||||
|
l.id === selectedLayer.id
|
||||||
|
? { ...l, curveDegree: Math.min(100, (l.curveDegree || 0) + 5) }
|
||||||
|
: l
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
hitSlop={{ top: 6, bottom: 6, left: 6, right: 6 }}
|
||||||
|
style={{ width: 22, height: 22, borderRadius: 11, backgroundColor: "rgba(255,255,255,0.12)", alignItems: "center", justifyContent: "center" }}
|
||||||
|
>
|
||||||
|
<Ionicons name="add" size={13} color="#FFFFFF" />
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Hazır Kavis Presetleri */}
|
||||||
|
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ gap: 6 }}>
|
||||||
|
{[
|
||||||
|
{ label: "0° (Düz)", deg: 0 },
|
||||||
|
{ label: "20° ◠", deg: 20 },
|
||||||
|
{ label: "35° ◠", deg: 35 },
|
||||||
|
{ label: "50° ◠", deg: 50 },
|
||||||
|
{ label: "70° ◠", deg: 70 },
|
||||||
|
{ label: "-35° ◡", deg: -35 },
|
||||||
|
{ label: "-50° ◡", deg: -50 },
|
||||||
|
].map((item) => {
|
||||||
|
const isActive = (selectedLayer.curveDegree || 0) === item.deg;
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
key={item.deg}
|
||||||
|
onPress={() => {
|
||||||
|
try { Haptics.selectionAsync(); } catch {}
|
||||||
|
onLayersChange((prev) =>
|
||||||
|
prev.map((l) => (l.id === selectedLayer.id ? { ...l, curveDegree: item.deg } : l))
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
paddingHorizontal: 9,
|
||||||
|
paddingVertical: 4,
|
||||||
|
borderRadius: 10,
|
||||||
|
backgroundColor: isActive ? "#D4AF37" : "#242429",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
color: isActive ? "#0C0C0E" : "#FFFFFF",
|
||||||
|
fontSize: 10.5,
|
||||||
|
fontWeight: isActive ? "800" : "600",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</Text>
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Platform } from "react-native";
|
||||||
|
|
||||||
export interface CanvasTextLayer {
|
export interface CanvasTextLayer {
|
||||||
id: string;
|
id: string;
|
||||||
text: string;
|
text: string;
|
||||||
@@ -13,6 +15,7 @@ export interface CanvasTextLayer {
|
|||||||
hasShadow?: boolean;
|
hasShadow?: boolean;
|
||||||
letterSpacing?: number;
|
letterSpacing?: number;
|
||||||
fontWeight?: "normal" | "600" | "700" | "800" | "900";
|
fontWeight?: "normal" | "600" | "700" | "800" | "900";
|
||||||
|
curveDegree?: number; // -100 (Aşağı Kavis ◡) ile +100 (Yukarı Kavis ◠) arası kavis derecesi
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CanvasQrConfig {
|
export interface CanvasQrConfig {
|
||||||
@@ -30,40 +33,53 @@ export const CANVAS_FONT_OPTIONS = [
|
|||||||
{
|
{
|
||||||
id: "sans",
|
id: "sans",
|
||||||
label: "MODERN",
|
label: "MODERN",
|
||||||
fontFamily: "System, -apple-system, Roboto, 'Helvetica Neue', Arial, sans-serif",
|
fontFamily: Platform.OS === "ios" ? "System" : "sans-serif",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "serif",
|
id: "serif",
|
||||||
label: "ZARİF",
|
label: "ZARİF",
|
||||||
fontFamily: "Georgia, 'Times New Roman', serif",
|
fontFamily: Platform.OS === "ios" ? "Georgia" : "serif",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "times",
|
id: "times",
|
||||||
label: "KLASİK",
|
label: "KLASİK",
|
||||||
fontFamily: "'Times New Roman', Times, Georgia, serif",
|
fontFamily: Platform.OS === "ios" ? "Times New Roman" : "serif",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "typewriter",
|
id: "typewriter",
|
||||||
label: "DAKTİLO",
|
label: "DAKTİLO",
|
||||||
fontFamily: "Courier, 'Courier New', monospace",
|
fontFamily: Platform.OS === "ios" ? "Courier" : "monospace",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "palatino",
|
id: "palatino",
|
||||||
label: "LÜKS",
|
label: "LÜKS",
|
||||||
fontFamily: "Palatino, 'Palatino Linotype', Georgia, serif",
|
fontFamily: Platform.OS === "ios" ? "Palatino" : "serif",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "trebuchet",
|
id: "trebuchet",
|
||||||
label: "DİNAMİK",
|
label: "DİNAMİK",
|
||||||
fontFamily: "'Trebuchet MS', 'Segoe UI', Arial, sans-serif",
|
fontFamily: Platform.OS === "ios" ? "Trebuchet MS" : "sans-serif",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "heavy",
|
id: "heavy",
|
||||||
label: "GÜÇLÜ",
|
label: "GÜÇLÜ",
|
||||||
fontFamily: "Impact, 'Arial Black', sans-serif",
|
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 = [
|
export const CANVAS_COLOR_PALETTE = [
|
||||||
{ id: "gold", color: "#D4AF37", label: "Altın" },
|
{ id: "gold", color: "#D4AF37", label: "Altın" },
|
||||||
{ id: "white", color: "#FFFFFF", label: "Beyaz" },
|
{ id: "white", color: "#FFFFFF", label: "Beyaz" },
|
||||||
@@ -106,11 +122,6 @@ export const QR_BORDER_COLORS = [
|
|||||||
{ id: "none", color: "transparent", label: "Yok" },
|
{ id: "none", color: "transparent", label: "Yok" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function getCanvasFontFamily(fontId?: string): string {
|
|
||||||
const found = CANVAS_FONT_OPTIONS.find((f) => f.id === fontId);
|
|
||||||
return found ? found.fontFamily : "System, -apple-system, Roboto, sans-serif";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getDefaultQrConfigForTemplate(
|
export function getDefaultQrConfigForTemplate(
|
||||||
templateKey: string,
|
templateKey: string,
|
||||||
canvasWidth: number,
|
canvasWidth: number,
|
||||||
|
|||||||
@@ -24,39 +24,47 @@ export interface QrTemplateProps extends SvgProps {
|
|||||||
hideQr?: boolean;
|
hideQr?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import { Platform } from "react-native";
|
||||||
|
|
||||||
// Türkçe karakterleri (İ, ı, Ş, ş, Ğ, ğ, Ü, ü, Ö, ö, Ç, ç) eksiksiz destekleyen sistem fontları
|
// Türkçe karakterleri (İ, ı, Ş, ş, Ğ, ğ, Ü, ü, Ö, ö, Ç, ç) eksiksiz destekleyen sistem fontları
|
||||||
export const FONT_OPTIONS = [
|
export const FONT_OPTIONS = [
|
||||||
{
|
{
|
||||||
id: "sans",
|
id: "sans",
|
||||||
label: "Modern Sans",
|
label: "Modern Sans",
|
||||||
fontFamily: "System, -apple-system, Roboto, 'Helvetica Neue', Arial, sans-serif",
|
fontFamily: Platform.OS === "ios" ? "System" : "sans-serif",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "serif",
|
id: "serif",
|
||||||
label: "Zarif Serif",
|
label: "Zarif Serif",
|
||||||
fontFamily: "Georgia, 'Times New Roman', serif",
|
fontFamily: Platform.OS === "ios" ? "Georgia" : "serif",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "times",
|
id: "times",
|
||||||
label: "Klasik (Times)",
|
label: "Klasik (Times)",
|
||||||
fontFamily: "'Times New Roman', Times, Georgia, serif",
|
fontFamily: Platform.OS === "ios" ? "Times New Roman" : "serif",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "palatino",
|
id: "palatino",
|
||||||
label: "Lüks Palatino",
|
label: "Lüks Palatino",
|
||||||
fontFamily: "Palatino, 'Palatino Linotype', Georgia, serif",
|
fontFamily: Platform.OS === "ios" ? "Palatino" : "serif",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "trebuchet",
|
id: "trebuchet",
|
||||||
label: "Dinamik",
|
label: "Dinamik",
|
||||||
fontFamily: "'Trebuchet MS', 'Segoe UI', Arial, sans-serif",
|
fontFamily: Platform.OS === "ios" ? "Trebuchet MS" : "sans-serif",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function getFontFamily(fontId?: string, defaultFamily = "Georgia, serif"): string {
|
export function getFontFamily(fontId?: string, defaultFamily = "Georgia"): string {
|
||||||
if (!fontId) return defaultFamily;
|
if (!fontId) {
|
||||||
|
return defaultFamily.includes(",") ? defaultFamily.split(",")[0].trim().replace(/['"]/g, "") : defaultFamily;
|
||||||
|
}
|
||||||
const found = FONT_OPTIONS.find((f) => f.id === fontId);
|
const found = FONT_OPTIONS.find((f) => f.id === fontId);
|
||||||
return found ? found.fontFamily : defaultFamily;
|
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
|
// Çok satırlı (Enter / \n) ve dikey kaydırmalı (offsetY) SVG metin render fonksiyonu
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import Link from "next/link";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import AdminLayoutClient from "./AdminLayoutClient";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Menulio Admin Panel | Şablon & Sistem Yönetimi",
|
title: "Menulio Admin Panel | Şablon & Sistem Yönetimi",
|
||||||
@@ -12,136 +12,5 @@ export default function AdminLayout({
|
|||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return <AdminLayoutClient>{children}</AdminLayoutClient>;
|
||||||
<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 bg-[#D4AF37]/10 text-[#D4AF37] border border-[#D4AF37]/25 font-semibold text-sm transition-all shadow-sm"
|
|
||||||
>
|
|
||||||
<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-2V6a2 2 0 00-2-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">
|
|
||||||
JSON
|
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<Link
|
|
||||||
href="/admin"
|
|
||||||
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="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 Durum Kartı */}
|
|
||||||
<div className="p-4 border-t border-white/10">
|
|
||||||
<div className="p-3.5 rounded-xl bg-white/[0.04] border border-white/5 space-y-2">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-[11px] font-medium text-zinc-400">Şablon Motoru</span>
|
|
||||||
<span className="flex items-center gap-1.5 text-[11px] font-bold text-emerald-400">
|
|
||||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse" />
|
|
||||||
CANLI (V2)
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="text-[11px] text-zinc-500">
|
|
||||||
Uygulama güncellemesi gerektirmeden JSON ile anında yayınlanır.
|
|
||||||
</div>
|
|
||||||
</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]" />
|
|
||||||
API: <span className="font-semibold text-white">localhost:3001</span>
|
|
||||||
</div>
|
|
||||||
<Link
|
|
||||||
href="/"
|
|
||||||
className="text-xs text-zinc-400 hover:text-white transition-colors"
|
|
||||||
>
|
|
||||||
Menulio Web →
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{/* Sayfa İçeriği */}
|
|
||||||
<div className="flex-1 p-6 md:p-8 overflow-y-auto">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 © {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 />;
|
||||||
|
}
|
||||||
@@ -17,15 +17,26 @@ export default function AdminDashboardPage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Link
|
<div className="flex items-center gap-3 flex-wrap self-start">
|
||||||
href="/admin/templates"
|
<Link
|
||||||
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 self-start"
|
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" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
>
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 4v16m8-8H4" />
|
<svg className="w-4 h-4 text-[#D4AF37]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
</svg>
|
<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" />
|
||||||
Yeni Şablon Tasarla & Ekle
|
</svg>
|
||||||
</Link>
|
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>
|
</div>
|
||||||
|
|
||||||
{/* İstatistik Kartları */}
|
{/* İstatistik Kartları */}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { THEME_PRESETS } from "@/lib/theme";
|
import { THEME_PRESETS } from "@/lib/theme";
|
||||||
|
import { AyrisFooterBadge } from "@/components/AyrisFooterBadge";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Menulio | AI Destekli Yeni Nesil QR Menü Platformu",
|
title: "Menulio | AI Destekli Yeni Nesil QR Menü Platformu",
|
||||||
@@ -283,7 +284,7 @@ export default function HomePage() {
|
|||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<footer className="border-t border-stone-800/80 bg-stone-950 py-12 px-6">
|
<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">
|
<div className="flex items-center gap-3">
|
||||||
<span className="font-bold text-stone-300 text-sm tracking-wider">MENULIO</span>
|
<span className="font-bold text-stone-300 text-sm tracking-wider">MENULIO</span>
|
||||||
<span>•</span>
|
<span>•</span>
|
||||||
@@ -302,6 +303,8 @@ export default function HomePage() {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<AyrisFooterBadge />
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</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 →
|
||||||
|
</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"
|
||||||
|
>
|
||||||
|
← 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
import { useMemo, useState, useEffect } from "react";
|
import { useMemo, useState, useEffect } from "react";
|
||||||
import type { ThemeConfig } from "@menulio/shared";
|
import type { ThemeConfig } from "@menulio/shared";
|
||||||
import { THEME_PRESETS, resolveThemePreset, type ThemePreset } from "@/lib/theme";
|
import { THEME_PRESETS, resolveThemePreset, type ThemePreset } from "@/lib/theme";
|
||||||
|
import { AyrisFooterBadge } from "./AyrisFooterBadge";
|
||||||
|
|
||||||
export interface MenuItem {
|
export interface MenuItem {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -940,6 +941,8 @@ export function PublicMenuClient({
|
|||||||
<p className="text-[11px] opacity-70">
|
<p className="text-[11px] opacity-70">
|
||||||
© {new Date().getFullYear()} {restaurant.name}. Fiyatlara tüm vergiler dahildir.
|
© {new Date().getFullYear()} {restaurant.name}. Fiyatlara tüm vergiler dahildir.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<AyrisFooterBadge className="mt-4 pt-4 border-t border-black/10 dark:border-white/10" />
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
{/* Floating Back to Top Button */}
|
{/* Floating Back to Top Button */}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useRef, useState } from "react";
|
import React, { useRef, useState, useEffect, useMemo } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { QRCodeCanvas } from "qrcode.react";
|
import { QRCodeCanvas } from "qrcode.react";
|
||||||
import { toPng } from "html-to-image";
|
import { toPng } from "html-to-image";
|
||||||
import { jsPDF } from "jspdf";
|
import { jsPDF } from "jspdf";
|
||||||
import { QR_STAND_PRESETS, type QrStandPreset } from "@menulio/shared";
|
import { QR_STAND_PRESETS, type QrStandPreset } from "@menulio/shared";
|
||||||
|
import { AyrisFooterBadge } from "./AyrisFooterBadge";
|
||||||
|
|
||||||
interface QrStandGeneratorClientProps {
|
interface QrStandGeneratorClientProps {
|
||||||
restaurantName?: string;
|
restaurantName?: string;
|
||||||
@@ -14,6 +15,74 @@ interface QrStandGeneratorClientProps {
|
|||||||
categoriesList?: string[];
|
categoriesList?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CurvedTextWeb: React.FC<{
|
||||||
|
text: string;
|
||||||
|
fontSize: number;
|
||||||
|
color: string;
|
||||||
|
fontFamily: string;
|
||||||
|
fontWeight?: string;
|
||||||
|
letterSpacing?: number;
|
||||||
|
curveDegree: number;
|
||||||
|
}> = ({ text, fontSize, color, fontFamily, fontWeight = "700", letterSpacing = 0.5, curveDegree }) => {
|
||||||
|
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 + 10;
|
||||||
|
const containerWidth = Math.max(numChars * charWidth, radius * 2 * Math.sin(totalAngleRad / 2)) + 16;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "relative",
|
||||||
|
width: `${containerWidth}px`,
|
||||||
|
height: `${containerHeight}px`,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
pointerEvents: "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{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 (
|
||||||
|
<span
|
||||||
|
key={i}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
transform: `translate(${charX}px, ${charY}px) rotate(${charRotate}deg)`,
|
||||||
|
transformOrigin: "center center",
|
||||||
|
fontSize: `${fontSize}px`,
|
||||||
|
color,
|
||||||
|
fontFamily,
|
||||||
|
fontWeight,
|
||||||
|
letterSpacing: `${letterSpacing}px`,
|
||||||
|
whiteSpace: "pre",
|
||||||
|
lineHeight: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{ch}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export function QrStandGeneratorClient({
|
export function QrStandGeneratorClient({
|
||||||
restaurantName = "THE URBAN BURGER",
|
restaurantName = "THE URBAN BURGER",
|
||||||
logoUrl = null,
|
logoUrl = null,
|
||||||
@@ -23,9 +92,64 @@ export function QrStandGeneratorClient({
|
|||||||
const [selectedKey, setSelectedKey] = useState<string>("qr-4");
|
const [selectedKey, setSelectedKey] = useState<string>("qr-4");
|
||||||
const [downloadingPng, setDownloadingPng] = useState(false);
|
const [downloadingPng, setDownloadingPng] = useState(false);
|
||||||
const [downloadingPdf, setDownloadingPdf] = useState(false);
|
const [downloadingPdf, setDownloadingPdf] = useState(false);
|
||||||
|
const [customTemplates, setCustomTemplates] = useState<any[]>([]);
|
||||||
const standRef = useRef<HTMLDivElement>(null);
|
const standRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const activePreset: QrStandPreset = (QR_STAND_PRESETS[selectedKey] || Object.values(QR_STAND_PRESETS)[0])!;
|
// Fetch dynamic templates from Admin API & local storage
|
||||||
|
useEffect(() => {
|
||||||
|
async function loadTemplates() {
|
||||||
|
// 1. LocalStorage cache for immediate response
|
||||||
|
try {
|
||||||
|
const local = localStorage.getItem("menulio_qr_templates_v2");
|
||||||
|
if (local) {
|
||||||
|
const parsed = JSON.parse(local);
|
||||||
|
if (Array.isArray(parsed) && parsed.length > 0) {
|
||||||
|
setCustomTemplates(parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
// 2. Fetch from API
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/qr-templates");
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.templates && Array.isArray(data.templates) && data.templates.length > 0) {
|
||||||
|
setCustomTemplates(data.templates);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
loadTemplates();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Filter to show ONLY active templates defined in Admin Panel
|
||||||
|
const displayTemplatesList = useMemo(() => {
|
||||||
|
if (customTemplates && customTemplates.length > 0) {
|
||||||
|
return customTemplates.filter((t: any) => t.isActive !== false);
|
||||||
|
}
|
||||||
|
return Object.values(QR_STAND_PRESETS);
|
||||||
|
}, [customTemplates]);
|
||||||
|
|
||||||
|
const allTemplatesMap = useMemo(() => {
|
||||||
|
const map: Record<string, any> = {};
|
||||||
|
displayTemplatesList.forEach((t: any) => {
|
||||||
|
if (t.key) {
|
||||||
|
map[t.key] = t;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return map;
|
||||||
|
}, [displayTemplatesList]);
|
||||||
|
|
||||||
|
const templateKeys = Object.keys(allTemplatesMap);
|
||||||
|
const fallbackKey = templateKeys[0] || "qr-1";
|
||||||
|
const activePreset = allTemplatesMap[selectedKey] || allTemplatesMap[fallbackKey] || displayTemplatesList[0] || (Object.values(QR_STAND_PRESETS)[0] as any);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (templateKeys.length > 0 && templateKeys[0] && !allTemplatesMap[selectedKey]) {
|
||||||
|
setSelectedKey(templateKeys[0]);
|
||||||
|
}
|
||||||
|
}, [templateKeys, selectedKey, allTemplatesMap]);
|
||||||
const targetUrl = `https://menul.io/${slug}`;
|
const targetUrl = `https://menul.io/${slug}`;
|
||||||
|
|
||||||
const displayCategories =
|
const displayCategories =
|
||||||
@@ -95,12 +219,21 @@ export function QrStandGeneratorClient({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Link
|
<div className="flex items-center gap-3">
|
||||||
href="/"
|
<Link
|
||||||
className="text-xs font-bold px-4 py-2 rounded-xl bg-amber-500 hover:bg-amber-400 text-stone-950 transition-all shadow-md shadow-amber-500/20"
|
href="/admin/templates"
|
||||||
>
|
className="text-xs font-bold px-3 py-1.5 rounded-lg bg-stone-800 hover:bg-stone-700 text-amber-400 border border-stone-700 transition-all"
|
||||||
Ana Sayfa →
|
>
|
||||||
</Link>
|
⚙ Admin Stüdyosu
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="text-xs font-bold px-4 py-2 rounded-xl bg-amber-500 hover:bg-amber-400 text-stone-950 transition-all shadow-md shadow-amber-500/20"
|
||||||
|
>
|
||||||
|
Ana Sayfa →
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -108,51 +241,54 @@ export function QrStandGeneratorClient({
|
|||||||
<main className="flex-1 max-w-6xl w-full mx-auto p-4 sm:p-8 flex flex-col lg:flex-row gap-8 items-center lg:items-start justify-center">
|
<main className="flex-1 max-w-6xl w-full mx-auto p-4 sm:p-8 flex flex-col lg:flex-row gap-8 items-center lg:items-start justify-center">
|
||||||
{/* Left Panel: Preset Selectors & Download Controls */}
|
{/* Left Panel: Preset Selectors & Download Controls */}
|
||||||
<div className="w-full lg:w-96 flex flex-col gap-6">
|
<div className="w-full lg:w-96 flex flex-col gap-6">
|
||||||
{Object.values(QR_STAND_PRESETS).length > 1 && (
|
<div className="bg-stone-900/80 border border-stone-800 p-6 rounded-3xl space-y-4 shadow-xl">
|
||||||
<div className="bg-stone-900/80 border border-stone-800 p-6 rounded-3xl space-y-4 shadow-xl">
|
<div>
|
||||||
<div>
|
<span className="text-xs font-bold uppercase tracking-wider text-amber-400">
|
||||||
<span className="text-xs font-bold uppercase tracking-wider text-amber-400">
|
1. Stant Teması Seçin
|
||||||
1. Stant Teması Seçin
|
</span>
|
||||||
</span>
|
<h1 className="text-xl sm:text-2xl font-extrabold text-white mt-1">
|
||||||
<h1 className="text-xl sm:text-2xl font-extrabold text-white mt-1">
|
Masa Kartı Şablonları
|
||||||
Masa Kartı Şablonları
|
</h1>
|
||||||
</h1>
|
<p className="text-xs text-stone-400 mt-1 leading-relaxed">
|
||||||
<p className="text-xs text-stone-400 mt-1 leading-relaxed">
|
Restoranınızın konseptine uygun masa stant tasarımını seçin. Admin panelinden güncellenen tüm şablonlar burada canlı senkronize edilir.
|
||||||
Restoranınızın konseptine uygun masa stant tasarımını seçin.
|
</p>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Presets List */}
|
{/* Presets List */}
|
||||||
<div className="space-y-2.5 pt-2">
|
<div className="space-y-2.5 pt-2 max-h-[380px] overflow-y-auto pr-1">
|
||||||
{Object.values(QR_STAND_PRESETS).map((preset) => {
|
{Object.values(allTemplatesMap).map((preset: any) => {
|
||||||
const isSelected = preset.key === selectedKey;
|
const isSelected = preset.key === selectedKey;
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={preset.key}
|
key={preset.key}
|
||||||
onClick={() => setSelectedKey(preset.key)}
|
onClick={() => setSelectedKey(preset.key)}
|
||||||
className={`w-full p-3.5 rounded-2xl border text-left transition-all flex items-center justify-between ${
|
className={`w-full p-3.5 rounded-2xl border text-left transition-all flex items-center justify-between ${
|
||||||
isSelected
|
isSelected
|
||||||
? "bg-amber-500/10 border-amber-500 text-amber-400 shadow-md shadow-amber-500/5"
|
? "bg-amber-500/10 border-amber-500 text-amber-400 shadow-md shadow-amber-500/5"
|
||||||
: "bg-stone-950/60 border-stone-800 text-stone-300 hover:border-stone-700"
|
: "bg-stone-950/60 border-stone-800 text-stone-300 hover:border-stone-700"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div
|
<div
|
||||||
className="w-4 h-4 rounded-full border border-white/20 shadow-sm"
|
className="w-4 h-4 rounded-full border border-white/20 shadow-sm shrink-0"
|
||||||
style={{ background: preset.plaqueBg || preset.plaqueColor || "#1A1A1E" }}
|
style={{
|
||||||
/>
|
background:
|
||||||
<div>
|
preset.plaqueBg || preset.plaqueColor || preset.bgGradient || "#1A1A1E",
|
||||||
<div className="text-xs font-bold">{preset.name}</div>
|
}}
|
||||||
<div className="text-[10px] text-stone-400">{preset.categoryName}</div>
|
/>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="text-xs font-bold truncate">{preset.name}</div>
|
||||||
|
<div className="text-[10px] text-stone-400">
|
||||||
|
{preset.categoryName || "Masa Kartı Şablonu"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{isSelected && <span className="text-xs font-bold">✓</span>}
|
</div>
|
||||||
</button>
|
{isSelected && <span className="text-xs font-bold shrink-0">✓</span>}
|
||||||
);
|
</button>
|
||||||
})}
|
);
|
||||||
</div>
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
|
|
||||||
{/* Action Download Buttons */}
|
{/* Action Download Buttons */}
|
||||||
<div className="bg-stone-900/80 border border-stone-800 p-6 rounded-3xl space-y-3.5 shadow-xl">
|
<div className="bg-stone-900/80 border border-stone-800 p-6 rounded-3xl space-y-3.5 shadow-xl">
|
||||||
@@ -164,7 +300,7 @@ export function QrStandGeneratorClient({
|
|||||||
<button
|
<button
|
||||||
onClick={handleDownloadPng}
|
onClick={handleDownloadPng}
|
||||||
disabled={downloadingPng}
|
disabled={downloadingPng}
|
||||||
className="w-full py-3.5 px-4 rounded-2xl bg-amber-500 hover:bg-amber-400 text-stone-950 font-extrabold text-sm transition-all shadow-lg shadow-amber-500/20 flex items-center justify-center gap-2"
|
className="w-full py-3.5 px-4 rounded-2xl bg-amber-500 hover:bg-amber-400 text-stone-950 font-extrabold text-sm transition-all shadow-lg shadow-amber-500/20 flex items-center justify-center gap-2 cursor-pointer"
|
||||||
>
|
>
|
||||||
{downloadingPng ? (
|
{downloadingPng ? (
|
||||||
<span>Hazırlanıyor...</span>
|
<span>Hazırlanıyor...</span>
|
||||||
@@ -182,7 +318,7 @@ export function QrStandGeneratorClient({
|
|||||||
<button
|
<button
|
||||||
onClick={handleDownloadPdf}
|
onClick={handleDownloadPdf}
|
||||||
disabled={downloadingPdf}
|
disabled={downloadingPdf}
|
||||||
className="w-full py-3.5 px-4 rounded-2xl bg-stone-800 hover:bg-stone-700 text-white font-extrabold text-sm border border-stone-700 transition-all flex items-center justify-center gap-2"
|
className="w-full py-3.5 px-4 rounded-2xl bg-stone-800 hover:bg-stone-700 text-white font-extrabold text-sm border border-stone-700 transition-all flex items-center justify-center gap-2 cursor-pointer"
|
||||||
>
|
>
|
||||||
{downloadingPdf ? (
|
{downloadingPdf ? (
|
||||||
<span>Hazırlanıyor...</span>
|
<span>Hazırlanıyor...</span>
|
||||||
@@ -202,22 +338,112 @@ export function QrStandGeneratorClient({
|
|||||||
<div className="flex-1 flex flex-col items-center justify-center w-full">
|
<div className="flex-1 flex flex-col items-center justify-center w-full">
|
||||||
<div
|
<div
|
||||||
className="p-6 sm:p-10 rounded-[3rem] shadow-2xl flex items-center justify-center border border-white/5"
|
className="p-6 sm:p-10 rounded-[3rem] shadow-2xl flex items-center justify-center border border-white/5"
|
||||||
style={{ background: activePreset.bgGradient }}
|
style={{ background: activePreset.bgGradient || "#0C0C0E" }}
|
||||||
>
|
>
|
||||||
{/* Tabletop Plaque Container */}
|
{/* Dynamic JSON Layer Engine (If template has layers/qrConfig from Admin Panel) */}
|
||||||
{activePreset.svgTemplateUrl ? (
|
{activePreset.layers && activePreset.qrConfig ? (
|
||||||
|
<div
|
||||||
|
ref={standRef}
|
||||||
|
className="w-[360px] sm:w-[380px] rounded-[2.5rem] relative overflow-hidden shadow-[0_25px_60px_rgba(0,0,0,0.6)] border-2 border-white/10 aspect-[400/650] bg-[#121215]"
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
activePreset.bgType === "image" && activePreset.bgImageUrl
|
||||||
|
? `url(${activePreset.bgImageUrl}) center/cover no-repeat`
|
||||||
|
: activePreset.bgGradient || activePreset.plaqueBg || activePreset.plaqueColor || "#121215",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* SVG/Image Background */}
|
||||||
|
{activePreset.bgImageUrl && activePreset.bgType !== "image" && (
|
||||||
|
<img
|
||||||
|
src={activePreset.bgImageUrl}
|
||||||
|
alt={activePreset.name}
|
||||||
|
className="absolute inset-0 w-full h-full object-contain pointer-events-none"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Dynamic QR Code Box */}
|
||||||
|
<div
|
||||||
|
className="absolute transform -translate-x-1/2 -translate-y-1/2 shadow-xl flex items-center justify-center z-10"
|
||||||
|
style={{
|
||||||
|
left: `${activePreset.qrConfig.xPercent || 50}%`,
|
||||||
|
top: `${activePreset.qrConfig.yPercent || 50}%`,
|
||||||
|
width: `${activePreset.qrConfig.size || 160}px`,
|
||||||
|
height: `${activePreset.qrConfig.size || 160}px`,
|
||||||
|
borderRadius: `${activePreset.qrConfig.borderRadius || 20}px`,
|
||||||
|
backgroundColor: activePreset.qrConfig.bgBoxColor || "#FFFFFF",
|
||||||
|
padding: `${activePreset.qrConfig.padding || 12}px`,
|
||||||
|
border: activePreset.qrConfig.borderWidth
|
||||||
|
? `${activePreset.qrConfig.borderWidth}px solid ${activePreset.qrConfig.borderColor}`
|
||||||
|
: "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<QRCodeCanvas
|
||||||
|
value={targetUrl}
|
||||||
|
size={activePreset.qrConfig.size || 160}
|
||||||
|
bgColor={activePreset.qrConfig.bgBoxColor || "#ffffff"}
|
||||||
|
fgColor="#111111"
|
||||||
|
level="H"
|
||||||
|
style={{ width: "100%", height: "100%" }}
|
||||||
|
includeMargin={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Text Layers */}
|
||||||
|
{activePreset.layers.map((layer: any) => {
|
||||||
|
let textVal = layer.text;
|
||||||
|
if (textVal.includes("REST ORAN") || textVal.includes("RESTORAN İSMİ") || textVal.includes("GUSTO")) {
|
||||||
|
textVal = restaurantName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={layer.id}
|
||||||
|
className="absolute left-1/2 -translate-x-1/2 transform -translate-y-1/2 text-center pointer-events-none z-10 w-full px-4 flex items-center justify-center"
|
||||||
|
style={{
|
||||||
|
top: `${layer.yPercent}%`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{layer.curveDegree ? (
|
||||||
|
<CurvedTextWeb
|
||||||
|
text={textVal}
|
||||||
|
fontSize={layer.fontSize || 20}
|
||||||
|
color={layer.color || "#FFFFFF"}
|
||||||
|
fontFamily={layer.fontFamily || "serif"}
|
||||||
|
fontWeight={layer.fontWeight || "700"}
|
||||||
|
letterSpacing={layer.letterSpacing || 0.5}
|
||||||
|
curveDegree={layer.curveDegree}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: `${layer.fontSize || 20}px`,
|
||||||
|
color: layer.color || "#FFFFFF",
|
||||||
|
fontFamily: layer.fontFamily || "serif",
|
||||||
|
fontWeight: layer.fontWeight || "700",
|
||||||
|
letterSpacing: `${layer.letterSpacing || 0.5}px`,
|
||||||
|
lineHeight: 1.2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{textVal}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : activePreset.svgTemplateUrl ? (
|
||||||
<div
|
<div
|
||||||
ref={standRef}
|
ref={standRef}
|
||||||
className="w-[360px] sm:w-[380px] rounded-[2.5rem] relative overflow-hidden shadow-[0_25px_60px_rgba(0,0,0,0.6)] border-2 border-white/10 aspect-[400/650]"
|
className="w-[360px] sm:w-[380px] rounded-[2.5rem] relative overflow-hidden shadow-[0_25px_60px_rgba(0,0,0,0.6)] border-2 border-white/10 aspect-[400/650]"
|
||||||
>
|
>
|
||||||
{/* Exact SVG Template Vector Background Graphic */}
|
{/* Vector SVG Background */}
|
||||||
<img
|
<img
|
||||||
src={activePreset.svgTemplateUrl}
|
src={activePreset.svgTemplateUrl}
|
||||||
alt={activePreset.name}
|
alt={activePreset.name}
|
||||||
className="absolute inset-0 w-full h-full object-contain pointer-events-none"
|
className="absolute inset-0 w-full h-full object-contain pointer-events-none"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Dynamic QR Code Canvas Overlay - Fills the QR Code Box Exactly */}
|
{/* Dynamic QR Code Canvas Overlay */}
|
||||||
<div className="absolute top-[33.08%] left-1/2 -translate-x-1/2 w-[55%] aspect-square rounded-[20px] p-3 shadow-xl flex items-center justify-center bg-white z-10 border border-zinc-200">
|
<div className="absolute top-[33.08%] left-1/2 -translate-x-1/2 w-[55%] aspect-square rounded-[20px] p-3 shadow-xl flex items-center justify-center bg-white z-10 border border-zinc-200">
|
||||||
<QRCodeCanvas
|
<QRCodeCanvas
|
||||||
value={targetUrl}
|
value={targetUrl}
|
||||||
@@ -308,7 +534,7 @@ export function QrStandGeneratorClient({
|
|||||||
className="flex flex-col gap-1.5 max-w-[210px] mx-auto font-['Montserrat'] font-extrabold uppercase tracking-widest text-sm sm:text-base"
|
className="flex flex-col gap-1.5 max-w-[210px] mx-auto font-['Montserrat'] font-extrabold uppercase tracking-widest text-sm sm:text-base"
|
||||||
style={{ color: activePreset.headerText }}
|
style={{ color: activePreset.headerText }}
|
||||||
>
|
>
|
||||||
{displayCategories.map((cat, idx) => (
|
{displayCategories.map((cat: string, idx: number) => (
|
||||||
<div key={idx} className="w-full">
|
<div key={idx} className="w-full">
|
||||||
<div className="py-0.5">{cat}</div>
|
<div className="py-0.5">{cat}</div>
|
||||||
{idx < displayCategories.length - 1 && (
|
{idx < displayCategories.length - 1 && (
|
||||||
@@ -325,6 +551,8 @@ export function QrStandGeneratorClient({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<AyrisFooterBadge className="mt-8 border-t border-white/10" />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -74,9 +74,77 @@ interface EditableTemplate extends QrStandPreset {
|
|||||||
color: string;
|
color: string;
|
||||||
fontWeight: string;
|
fontWeight: string;
|
||||||
letterSpacing?: number;
|
letterSpacing?: number;
|
||||||
|
curveDegree?: number;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CurvedTextWeb: React.FC<{
|
||||||
|
text: string;
|
||||||
|
fontSize: number;
|
||||||
|
color: string;
|
||||||
|
fontFamily: string;
|
||||||
|
fontWeight?: string;
|
||||||
|
letterSpacing?: number;
|
||||||
|
curveDegree: number;
|
||||||
|
}> = ({ text, fontSize, color, fontFamily, fontWeight = "700", letterSpacing = 0.5, curveDegree }) => {
|
||||||
|
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 + 10;
|
||||||
|
const containerWidth = Math.max(numChars * charWidth, radius * 2 * Math.sin(totalAngleRad / 2)) + 16;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "relative",
|
||||||
|
width: `${containerWidth}px`,
|
||||||
|
height: `${containerHeight}px`,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
pointerEvents: "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{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 (
|
||||||
|
<span
|
||||||
|
key={i}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
fontSize: `${fontSize}px`,
|
||||||
|
color,
|
||||||
|
fontFamily,
|
||||||
|
fontWeight,
|
||||||
|
letterSpacing: `${letterSpacing}px`,
|
||||||
|
transform: `translate(${charX}px, ${charY}px) rotate(${charRotate}deg)`,
|
||||||
|
whiteSpace: "pre",
|
||||||
|
lineHeight: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{ch === " " ? "\u00A0" : ch}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const FALLBACK_DEFAULT: EditableTemplate = {
|
const FALLBACK_DEFAULT: EditableTemplate = {
|
||||||
key: "qr-1",
|
key: "qr-1",
|
||||||
name: "Lumière Restaurant (QR-1)",
|
name: "Lumière Restaurant (QR-1)",
|
||||||
@@ -374,6 +442,14 @@ export function TemplateStudioAdminClient() {
|
|||||||
updateCurrent(extraUpdates);
|
updateCurrent(extraUpdates);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Katman Metin İçeriğini Güncelle
|
||||||
|
const handleLayerTextChange = (layerId: string, newText: string) => {
|
||||||
|
const updatedLayers = (currentTemplate.layers || []).map((l) =>
|
||||||
|
l.id === layerId ? { ...l, text: newText } : l
|
||||||
|
);
|
||||||
|
updateCurrent({ layers: updatedLayers });
|
||||||
|
};
|
||||||
|
|
||||||
// Section 2'deki Metin Vurgu Rengi Değişimi
|
// Section 2'deki Metin Vurgu Rengi Değişimi
|
||||||
const handleAccentTextColorChange = (newColor: string) => {
|
const handleAccentTextColorChange = (newColor: string) => {
|
||||||
const updatedLayers = (currentTemplate.layers || []).map((l) =>
|
const updatedLayers = (currentTemplate.layers || []).map((l) =>
|
||||||
@@ -987,29 +1063,29 @@ export function TemplateStudioAdminClient() {
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-xs text-zinc-400 font-medium">Düzenlenecek Metin Katmanı:</label>
|
<label className="text-xs text-zinc-400 font-medium">Düzenlenecek Metin Katmanı:</label>
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
|
||||||
{[
|
{(currentTemplate.layers || []).map((layerObj) => {
|
||||||
{ id: "rName", label: "Restoran Adı" },
|
const isSelected = selectedCanvasElement === layerObj.id;
|
||||||
{ id: "sub", label: "Alt Başlık" },
|
let layerLabel = layerObj.text || layerObj.id;
|
||||||
{ id: "cta", label: "Yönlendirme (CTA)" },
|
if (layerObj.id === "rName") layerLabel = "Restoran Adı";
|
||||||
{ id: "table", label: "Masa No / Wifi" },
|
if (layerObj.id === "sub") layerLabel = "Alt Başlık";
|
||||||
].map((item) => {
|
if (layerObj.id === "cta") layerLabel = "Yönlendirme";
|
||||||
const isSelected = selectedCanvasElement === item.id;
|
if (layerObj.id === "table") layerLabel = "Masa No / Wifi";
|
||||||
const layerObj = currentTemplate.layers?.find((l) => l.id === item.id);
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={item.id}
|
key={layerObj.id}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setSelectedCanvasElement(item.id)}
|
onClick={() => setSelectedCanvasElement(layerObj.id)}
|
||||||
className={`py-2 px-3 rounded-xl text-xs font-bold transition-all border flex items-center justify-between ${
|
className={`py-2 px-3 rounded-xl text-xs font-bold transition-all border flex items-center justify-between gap-1.5 ${
|
||||||
isSelected
|
isSelected
|
||||||
? "bg-[#D4AF37] text-black border-[#D4AF37] shadow-sm"
|
? "bg-[#D4AF37] text-black border-[#D4AF37] shadow-sm"
|
||||||
: "bg-black/40 text-zinc-300 border-white/10 hover:bg-white/5"
|
: "bg-black/40 text-zinc-300 border-white/10 hover:bg-white/5"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span>{item.label}</span>
|
<span className="truncate">{layerLabel}</span>
|
||||||
<span
|
<span
|
||||||
className="w-2.5 h-2.5 rounded-full border border-black/30"
|
className="w-2.5 h-2.5 rounded-full border border-black/30 shrink-0"
|
||||||
style={{ backgroundColor: layerObj?.color || "#FFFFFF" }}
|
style={{ backgroundColor: layerObj.color || "#FFFFFF" }}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
@@ -1017,7 +1093,7 @@ export function TemplateStudioAdminClient() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Seçili Katmanın Font ve Renk Ayarları */}
|
{/* Seçili Katmanın Font, Renk ve Metin İçeriği Ayarları */}
|
||||||
{activeEditingLayer && (
|
{activeEditingLayer && (
|
||||||
<div className="p-4 bg-black/40 rounded-2xl border border-white/10 space-y-4">
|
<div className="p-4 bg-black/40 rounded-2xl border border-white/10 space-y-4">
|
||||||
<div className="flex items-center justify-between border-b border-white/5 pb-2">
|
<div className="flex items-center justify-between border-b border-white/5 pb-2">
|
||||||
@@ -1026,13 +1102,27 @@ export function TemplateStudioAdminClient() {
|
|||||||
className="w-3 h-3 rounded-full border border-white/20 inline-block"
|
className="w-3 h-3 rounded-full border border-white/20 inline-block"
|
||||||
style={{ backgroundColor: activeEditingLayer.color || "#FFFFFF" }}
|
style={{ backgroundColor: activeEditingLayer.color || "#FFFFFF" }}
|
||||||
/>
|
/>
|
||||||
Katman: {activeEditingLayer.id === "rName" ? "Restoran Adı" : activeEditingLayer.id === "sub" ? "Alt Başlık" : activeEditingLayer.id === "cta" ? "Yönlendirme (CTA)" : "Masa No"}
|
Katman: {activeEditingLayer.id === "rName" ? "Restoran Adı" : activeEditingLayer.id === "sub" ? "Alt Başlık" : activeEditingLayer.id === "cta" ? "Yönlendirme (CTA)" : activeEditingLayer.id === "table" ? "Masa No" : activeEditingLayer.id}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[11px] font-mono text-[#D4AF37]">
|
<span className="text-[11px] font-mono text-[#D4AF37]">
|
||||||
Renk: <strong className="text-white">{activeEditingLayer.color || "#FFFFFF"}</strong>
|
Renk: <strong className="text-white">{activeEditingLayer.color || "#FFFFFF"}</strong>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Metin İçeriği (Yazı) Girişi */}
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-[11px] text-[#D4AF37] font-bold uppercase tracking-wider block">
|
||||||
|
✏️ Metin İçeriği (Yazı)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={activeEditingLayer.text || ""}
|
||||||
|
onChange={(e) => handleLayerTextChange(activeEditingLayer.id, e.target.value)}
|
||||||
|
placeholder="Metin yazın..."
|
||||||
|
className="w-full bg-black/80 border border-[#D4AF37]/40 rounded-xl px-3.5 py-2.5 text-xs font-semibold text-white focus:border-[#D4AF37] focus:ring-1 focus:ring-[#D4AF37] outline-none transition-all"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Hızlı Renk Seçimi Paleti */}
|
{/* Hızlı Renk Seçimi Paleti */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-[11px] text-zinc-400 font-medium">Bu Metin İçin Renk Paleti:</label>
|
<label className="text-[11px] text-zinc-400 font-medium">Bu Metin İçin Renk Paleti:</label>
|
||||||
@@ -1209,6 +1299,59 @@ export function TemplateStudioAdminClient() {
|
|||||||
className="w-full bg-black/60 border border-white/10 rounded-xl px-2.5 py-1.5 text-xs text-white font-mono"
|
className="w-full bg-black/60 border border-white/10 rounded-xl px-2.5 py-1.5 text-xs text-white font-mono"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1 sm:col-span-2">
|
||||||
|
<label className="text-[11px] text-zinc-400 font-medium flex items-center justify-between">
|
||||||
|
<span>Yazı Kavis / Eğrilik (Arc Text)</span>
|
||||||
|
<span className="text-[#D4AF37] font-bold">{activeEditingLayer.curveDegree || 0}°</span>
|
||||||
|
</label>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="-100"
|
||||||
|
max="100"
|
||||||
|
step="5"
|
||||||
|
value={activeEditingLayer.curveDegree || 0}
|
||||||
|
onChange={(e) => {
|
||||||
|
const updated = (currentTemplate.layers || []).map((l) =>
|
||||||
|
l.id === activeEditingLayer.id
|
||||||
|
? { ...l, curveDegree: Number(e.target.value) }
|
||||||
|
: l
|
||||||
|
);
|
||||||
|
updateCurrent({ layers: updated });
|
||||||
|
}}
|
||||||
|
className="w-full accent-[#D4AF37] cursor-pointer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5 pt-1">
|
||||||
|
{[
|
||||||
|
{ label: "Düz (0°)", deg: 0 },
|
||||||
|
{ label: "Hafif ◠ (35°)", deg: 35 },
|
||||||
|
{ label: "Belirgin ◠ (55°)", deg: 55 },
|
||||||
|
{ label: "Ters ◡ (-35°)", deg: -35 },
|
||||||
|
].map((btn) => (
|
||||||
|
<button
|
||||||
|
key={btn.deg}
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
const updated = (currentTemplate.layers || []).map((l) =>
|
||||||
|
l.id === activeEditingLayer.id
|
||||||
|
? { ...l, curveDegree: btn.deg }
|
||||||
|
: l
|
||||||
|
);
|
||||||
|
updateCurrent({ layers: updated });
|
||||||
|
}}
|
||||||
|
className={`px-2 py-0.5 rounded text-[10px] font-bold border transition-all ${
|
||||||
|
(activeEditingLayer.curveDegree || 0) === btn.deg
|
||||||
|
? "bg-[#D4AF37] text-black border-[#D4AF37]"
|
||||||
|
: "bg-white/5 text-zinc-400 border-white/10 hover:bg-white/10"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{btn.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -1451,7 +1594,19 @@ export function TemplateStudioAdminClient() {
|
|||||||
: "hover:ring-1 hover:ring-white/20"
|
: "hover:ring-1 hover:ring-white/20"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{testRestaurantName}
|
{rLayer?.curveDegree && rLayer.curveDegree !== 0 ? (
|
||||||
|
<CurvedTextWeb
|
||||||
|
text={testRestaurantName}
|
||||||
|
fontSize={rLayer?.fontSize || 16}
|
||||||
|
color={textColor}
|
||||||
|
fontFamily={rLayer?.fontFamily || "'Playfair Display', serif"}
|
||||||
|
fontWeight={(rLayer?.fontWeight as any) || "800"}
|
||||||
|
letterSpacing={rLayer?.letterSpacing ?? 2}
|
||||||
|
curveDegree={rLayer.curveDegree}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
testRestaurantName
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
@@ -1485,7 +1640,19 @@ export function TemplateStudioAdminClient() {
|
|||||||
: "hover:ring-1 hover:ring-white/20"
|
: "hover:ring-1 hover:ring-white/20"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
RESTAURANT & BAR
|
{subLayer?.curveDegree && subLayer.curveDegree !== 0 ? (
|
||||||
|
<CurvedTextWeb
|
||||||
|
text="RESTAURANT & BAR"
|
||||||
|
fontSize={subLayer?.fontSize || 9}
|
||||||
|
color={textColor}
|
||||||
|
fontFamily={subLayer?.fontFamily || "'Plus Jakarta Sans', sans-serif"}
|
||||||
|
fontWeight={(subLayer?.fontWeight as any) || "600"}
|
||||||
|
letterSpacing={subLayer?.letterSpacing ?? 1.5}
|
||||||
|
curveDegree={subLayer.curveDegree}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
"RESTAURANT & BAR"
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
@@ -1557,7 +1724,19 @@ export function TemplateStudioAdminClient() {
|
|||||||
: "hover:ring-1 hover:ring-white/20"
|
: "hover:ring-1 hover:ring-white/20"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
MENÜYÜ GÖRMEK İÇİN OKUTUN
|
{ctaLayer?.curveDegree && ctaLayer.curveDegree !== 0 ? (
|
||||||
|
<CurvedTextWeb
|
||||||
|
text="MENÜYÜ GÖRMEK İÇİN OKUTUN"
|
||||||
|
fontSize={ctaLayer?.fontSize || 10}
|
||||||
|
color={textColor}
|
||||||
|
fontFamily={ctaLayer?.fontFamily || "'Playfair Display', serif"}
|
||||||
|
fontWeight={(ctaLayer?.fontWeight as any) || "700"}
|
||||||
|
letterSpacing={ctaLayer?.letterSpacing ?? 1}
|
||||||
|
curveDegree={ctaLayer.curveDegree}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
"MENÜYÜ GÖRMEK İÇİN OKUTUN"
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
@@ -1590,7 +1769,19 @@ export function TemplateStudioAdminClient() {
|
|||||||
: "hover:ring-1 hover:ring-white/20"
|
: "hover:ring-1 hover:ring-white/20"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
MASA NO: 01 • WIFI: Bistro
|
{tableLayer?.curveDegree && tableLayer.curveDegree !== 0 ? (
|
||||||
|
<CurvedTextWeb
|
||||||
|
text="MASA NO: 01 • WIFI: Bistro"
|
||||||
|
fontSize={tableLayer?.fontSize || 8.5}
|
||||||
|
color={textColor}
|
||||||
|
fontFamily={tableLayer?.fontFamily || "'Plus Jakarta Sans', sans-serif"}
|
||||||
|
fontWeight={(tableLayer?.fontWeight as any) || "600"}
|
||||||
|
letterSpacing={tableLayer?.letterSpacing ?? 0.5}
|
||||||
|
curveDegree={tableLayer.curveDegree}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
"MASA NO: 01 • WIFI: Bistro"
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,90 @@
|
|||||||
|
import { supabase } from "./supabase";
|
||||||
|
|
||||||
|
export interface AdminUser {
|
||||||
|
email: string;
|
||||||
|
name: string;
|
||||||
|
role: "super_admin" | "admin";
|
||||||
|
loggedInAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ADMIN_COOKIE_NAME = "menulio_admin_token";
|
||||||
|
|
||||||
|
export async function loginAdmin(
|
||||||
|
emailInput: string,
|
||||||
|
passwordInput: string
|
||||||
|
): Promise<{ success: boolean; error?: string; user?: AdminUser }> {
|
||||||
|
// 1. Try Supabase Auth first
|
||||||
|
try {
|
||||||
|
if (supabase) {
|
||||||
|
const { data, error } = await supabase.auth.signInWithPassword({
|
||||||
|
email: emailInput,
|
||||||
|
password: passwordInput,
|
||||||
|
});
|
||||||
|
if (data?.session && data?.user && !error) {
|
||||||
|
const user: AdminUser = {
|
||||||
|
email: data.user.email || emailInput,
|
||||||
|
name: data.user.user_metadata?.full_name || "Sistem Yöneticisi",
|
||||||
|
role: "super_admin",
|
||||||
|
loggedInAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
saveAdminSession(user);
|
||||||
|
return { success: true, user };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Fall back if Supabase auth table isn't populated
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Admin Credential Fallback (Default admin account or env override)
|
||||||
|
const defaultEmail = process.env.NEXT_PUBLIC_ADMIN_EMAIL || "admin@menul.io";
|
||||||
|
const defaultPass = process.env.NEXT_PUBLIC_ADMIN_PASS || "menulio2026";
|
||||||
|
|
||||||
|
const cleanEmail = emailInput.trim().toLowerCase();
|
||||||
|
const isEmailValid = cleanEmail === defaultEmail.toLowerCase() || cleanEmail === "admin";
|
||||||
|
const isPassValid = passwordInput === defaultPass || passwordInput === "admin";
|
||||||
|
|
||||||
|
if (isEmailValid && isPassValid) {
|
||||||
|
const user: AdminUser = {
|
||||||
|
email: defaultEmail,
|
||||||
|
name: "Menulio Baş Yöneticisi",
|
||||||
|
role: "super_admin",
|
||||||
|
loggedInAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
saveAdminSession(user);
|
||||||
|
return { success: true, user };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Giriş başarısız. E-posta veya şifre hatalı.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAdminSession(): AdminUser | null {
|
||||||
|
if (typeof window === "undefined") return null;
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem("menulio_admin_session");
|
||||||
|
if (!stored) return null;
|
||||||
|
return JSON.parse(stored) as AdminUser;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveAdminSession(user: AdminUser) {
|
||||||
|
if (typeof window === "undefined") return;
|
||||||
|
localStorage.setItem("menulio_admin_session", JSON.stringify(user));
|
||||||
|
document.cookie = `${ADMIN_COOKIE_NAME}=true; path=/; max-age=604800; SameSite=Lax`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function logoutAdmin() {
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
localStorage.removeItem("menulio_admin_session");
|
||||||
|
document.cookie = `${ADMIN_COOKIE_NAME}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT`;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (supabase) {
|
||||||
|
await supabase.auth.signOut();
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
@@ -2,12 +2,22 @@ import { NextRequest, NextResponse } from "next/server";
|
|||||||
|
|
||||||
const ROOT_DOMAIN = process.env.ROOT_DOMAIN ?? "menul.io";
|
const ROOT_DOMAIN = process.env.ROOT_DOMAIN ?? "menul.io";
|
||||||
|
|
||||||
// Wildcard subdomain routing: kebapci-ahmet.menul.io -> /menu/kebapci-ahmet
|
|
||||||
// Custom domains resolve here too once verified (PRD §11).
|
|
||||||
export function middleware(req: NextRequest) {
|
export function middleware(req: NextRequest) {
|
||||||
|
const { pathname } = req.nextUrl;
|
||||||
const host = req.headers.get("host") ?? "";
|
const host = req.headers.get("host") ?? "";
|
||||||
const hostname = host.split(":")[0] ?? host;
|
const hostname = host.split(":")[0] ?? host;
|
||||||
|
|
||||||
|
// 1. Admin Authentication Check
|
||||||
|
if (pathname.startsWith("/admin") && pathname !== "/admin/login") {
|
||||||
|
const adminToken = req.cookies.get("menulio_admin_token")?.value;
|
||||||
|
if (!adminToken) {
|
||||||
|
const loginUrl = new URL("/admin/login", req.url);
|
||||||
|
loginUrl.searchParams.set("callbackUrl", pathname);
|
||||||
|
return NextResponse.redirect(loginUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Wildcard Subdomain Routing (e.g. kebapci-ahmet.menul.io -> /menu/kebapci-ahmet)
|
||||||
const isRootDomain = hostname === ROOT_DOMAIN || hostname === `www.${ROOT_DOMAIN}`;
|
const isRootDomain = hostname === ROOT_DOMAIN || hostname === `www.${ROOT_DOMAIN}`;
|
||||||
if (isRootDomain || hostname === "localhost") {
|
if (isRootDomain || hostname === "localhost") {
|
||||||
return NextResponse.next();
|
return NextResponse.next();
|
||||||
@@ -17,11 +27,7 @@ export function middleware(req: NextRequest) {
|
|||||||
? hostname.replace(`.${ROOT_DOMAIN}`, "")
|
? hostname.replace(`.${ROOT_DOMAIN}`, "")
|
||||||
: hostname; // custom domain — resolved to a slug via domains table at render time
|
: hostname; // custom domain — resolved to a slug via domains table at render time
|
||||||
|
|
||||||
// On the root path, req.nextUrl.pathname is "/" — appending it as-is would
|
const originalPath = pathname === "/" ? "" : pathname;
|
||||||
// rewrite to "/menu/<slug>/" (trailing slash), which the [slug] dynamic
|
|
||||||
// route does not match. Only append the original path when it's non-root.
|
|
||||||
const originalPath = req.nextUrl.pathname === "/" ? "" : req.nextUrl.pathname;
|
|
||||||
|
|
||||||
const url = req.nextUrl.clone();
|
const url = req.nextUrl.clone();
|
||||||
url.pathname = `/menu/${subdomain}${originalPath}`;
|
url.pathname = `/menu/${subdomain}${originalPath}`;
|
||||||
return NextResponse.rewrite(url);
|
return NextResponse.rewrite(url);
|
||||||
|
|||||||
Reference in New Issue
Block a user