feat(qr): complete QR studio, dynamic google fonts, mouse canvas drag, and template sync
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
auto-install-peers=true
|
||||
strict-peer-dependencies=false
|
||||
enable-pre-post-scripts=true
|
||||
only-built-dependencies=core-js,@swc/core,esbuild,sharp,@esbuild/darwin-arm64,@next/swc-darwin-arm64
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
[
|
||||
{
|
||||
"key": "qr-1",
|
||||
"name": "Lumière Restaurant (QR-1)",
|
||||
"categoryName": "Özel Masa Kartı",
|
||||
"svgTemplateUrl": "/qr-1.svg",
|
||||
"aspectRatio": "1/1",
|
||||
"bgType": "gradient",
|
||||
"bgGradient": "radial-gradient(circle at 50% 50%, #132034 0%, #0b0d13 100%)",
|
||||
"plaqueColor": "#132034",
|
||||
"borderAccent": "#d4af37",
|
||||
"accentText": "#d4af37",
|
||||
"headerText": "#FFFFFF",
|
||||
"subText": "#e5c378",
|
||||
"dividerColor": "rgba(212, 175, 55, 0.3)",
|
||||
"isActive": true,
|
||||
"qrConfig": {
|
||||
"xPercent": 50,
|
||||
"yPercent": 38,
|
||||
"size": 115,
|
||||
"borderRadius": 14,
|
||||
"borderColor": "#D4AF37",
|
||||
"bgBoxColor": "#FFFFFF"
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"id": "rName",
|
||||
"text": "{{RESTAURANT_NAME}}",
|
||||
"yPercent": 16,
|
||||
"fontSize": 18,
|
||||
"fontFamily": "Playfair Display, Georgia, serif",
|
||||
"color": "#D4AF37",
|
||||
"fontWeight": "800",
|
||||
"letterSpacing": 2
|
||||
},
|
||||
{
|
||||
"id": "sub",
|
||||
"text": "RESTAURANT & BAR",
|
||||
"yPercent": 26,
|
||||
"fontSize": 10,
|
||||
"fontFamily": "Plus Jakarta Sans, sans-serif",
|
||||
"color": "#E5C378",
|
||||
"fontWeight": "600",
|
||||
"letterSpacing": 1.5
|
||||
},
|
||||
{
|
||||
"id": "cta",
|
||||
"text": "MENÜYÜ GÖRMEK İÇİN OKUTUN",
|
||||
"yPercent": 80,
|
||||
"fontSize": 11,
|
||||
"fontFamily": "Playfair Display, Georgia, serif",
|
||||
"color": "#FFFFFF",
|
||||
"fontWeight": "700",
|
||||
"letterSpacing": 1
|
||||
},
|
||||
{
|
||||
"id": "table",
|
||||
"text": "MASA NO: 01 • WIFI: Bistro",
|
||||
"yPercent": 90,
|
||||
"fontSize": 9,
|
||||
"fontFamily": "Plus Jakarta Sans, sans-serif",
|
||||
"color": "#A1A1AA",
|
||||
"fontWeight": "600"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "qr-2",
|
||||
"name": "The Daily Grind Cafe (QR-2)",
|
||||
"categoryName": "Özel Masa Kartı",
|
||||
"svgTemplateUrl": "/qr-2.svg",
|
||||
"aspectRatio": "400/650",
|
||||
"bgType": "gradient",
|
||||
"bgGradient": "radial-gradient(circle at 50% 50%, #252c36 0%, #171c22 100%)",
|
||||
"plaqueColor": "#f6efe2",
|
||||
"borderAccent": "#3a2312",
|
||||
"accentText": "#8c6239",
|
||||
"headerText": "#2c1a0e",
|
||||
"subText": "#3a2312",
|
||||
"dividerColor": "rgba(140, 98, 57, 0.3)",
|
||||
"isActive": true,
|
||||
"qrConfig": {
|
||||
"xPercent": 50,
|
||||
"yPercent": 36,
|
||||
"size": 125,
|
||||
"borderRadius": 14,
|
||||
"borderColor": "#3a2312",
|
||||
"bgBoxColor": "#FFFFFF"
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"id": "rName",
|
||||
"text": "{{RESTAURANT_NAME}}",
|
||||
"yPercent": 14,
|
||||
"fontSize": 18,
|
||||
"fontFamily": "Playfair Display, Georgia, serif",
|
||||
"color": "#8c6239",
|
||||
"fontWeight": "800",
|
||||
"letterSpacing": 2
|
||||
},
|
||||
{
|
||||
"id": "cta",
|
||||
"text": "MENÜYÜ GÖRMEK İÇİN OKUTUN",
|
||||
"yPercent": 82,
|
||||
"fontSize": 11,
|
||||
"fontFamily": "Playfair Display, Georgia, serif",
|
||||
"color": "#2c1a0e",
|
||||
"fontWeight": "700",
|
||||
"letterSpacing": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "qr-3",
|
||||
"name": "Pizzeria Bella Napoli (QR-3)",
|
||||
"categoryName": "Özel Masa Kartı",
|
||||
"svgTemplateUrl": "/qr-3.svg",
|
||||
"aspectRatio": "400/650",
|
||||
"bgType": "gradient",
|
||||
"bgGradient": "radial-gradient(circle at 50% 50%, #faf4e6 0%, #f7eed8 100%)",
|
||||
"plaqueColor": "#fdfbf5",
|
||||
"borderAccent": "#b92b27",
|
||||
"accentText": "#b92b27",
|
||||
"headerText": "#18181b",
|
||||
"subText": "#5d4037",
|
||||
"dividerColor": "rgba(185, 43, 39, 0.3)",
|
||||
"isActive": true,
|
||||
"qrConfig": {
|
||||
"xPercent": 50,
|
||||
"yPercent": 36,
|
||||
"size": 125,
|
||||
"borderRadius": 14,
|
||||
"borderColor": "#b92b27",
|
||||
"bgBoxColor": "#FFFFFF"
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"id": "rName",
|
||||
"text": "{{RESTAURANT_NAME}}",
|
||||
"yPercent": 14,
|
||||
"fontSize": 18,
|
||||
"fontFamily": "Playfair Display, Georgia, serif",
|
||||
"color": "#b92b27",
|
||||
"fontWeight": "800",
|
||||
"letterSpacing": 2
|
||||
},
|
||||
{
|
||||
"id": "cta",
|
||||
"text": "MENÜYÜ GÖRMEK İÇİN OKUTUN",
|
||||
"yPercent": 82,
|
||||
"fontSize": 11,
|
||||
"fontFamily": "Playfair Display, Georgia, serif",
|
||||
"color": "#18181b",
|
||||
"fontWeight": "700",
|
||||
"letterSpacing": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "qr-8",
|
||||
"name": "Emerald & Gold Special (QR-8)",
|
||||
"categoryName": "Özel Kare Masa Kartı",
|
||||
"svgTemplateUrl": "/qr-8.svg",
|
||||
"aspectRatio": "1/1",
|
||||
"bgType": "gradient",
|
||||
"bgGradient": "radial-gradient(circle at 50% 50%, #0c4443 0%, #062423 100%)",
|
||||
"plaqueColor": "#f9f5eb",
|
||||
"borderAccent": "#d1a752",
|
||||
"accentText": "#d1a752",
|
||||
"headerText": "#ffffff",
|
||||
"subText": "#d1a752",
|
||||
"dividerColor": "rgba(209, 167, 82, 0.4)",
|
||||
"isActive": true,
|
||||
"qrConfig": {
|
||||
"xPercent": 50,
|
||||
"yPercent": 38,
|
||||
"size": 115,
|
||||
"borderRadius": 14,
|
||||
"borderColor": "#d1a752",
|
||||
"bgBoxColor": "#FFFFFF"
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"id": "rName",
|
||||
"text": "{{RESTAURANT_NAME}}",
|
||||
"yPercent": 16,
|
||||
"fontSize": 18,
|
||||
"fontFamily": "Playfair Display, Georgia, serif",
|
||||
"color": "#d1a752",
|
||||
"fontWeight": "800"
|
||||
},
|
||||
{
|
||||
"id": "cta",
|
||||
"text": "MENÜYÜ GÖRMEK İÇİN OKUTUN",
|
||||
"yPercent": 80,
|
||||
"fontSize": 11,
|
||||
"fontFamily": "Playfair Display, Georgia, serif",
|
||||
"color": "#ffffff",
|
||||
"fontWeight": "700"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -9,6 +9,7 @@ import { menuItemsRoutes } from "./routes/menu-items.js";
|
||||
import { menusRoutes } from "./routes/menus.js";
|
||||
import { meRoutes } from "./routes/me.js";
|
||||
import { qrRoutes } from "./routes/qr.js";
|
||||
import { qrTemplatesRoutes } from "./routes/qr-templates.js";
|
||||
import { restaurantsRoutes } from "./routes/restaurants.js";
|
||||
import { subscriptionRoutes } from "./routes/subscription.js";
|
||||
|
||||
@@ -42,6 +43,7 @@ await app.register(menusRoutes);
|
||||
await app.register(menuCategoriesRoutes);
|
||||
await app.register(menuItemsRoutes);
|
||||
await app.register(qrRoutes);
|
||||
await app.register(qrTemplatesRoutes);
|
||||
await app.register(aiImportsRoutes);
|
||||
await app.register(domainsRoutes);
|
||||
await app.register(analyticsRoutes);
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { type QrStandPreset } from "@menulio/shared";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const DATA_FILE = path.resolve(__dirname, "../data/qr-templates.json");
|
||||
|
||||
export interface DynamicQrTemplate extends QrStandPreset {
|
||||
id: string;
|
||||
isActive: boolean;
|
||||
bgType?: "gradient" | "image" | "svg";
|
||||
bgImageUrl?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
defaultQrConfig?: {
|
||||
xPercent: number;
|
||||
yPercent: number;
|
||||
size: number;
|
||||
borderRadius: number;
|
||||
borderColor: string;
|
||||
bgBoxColor: string;
|
||||
};
|
||||
defaultLayers?: Array<{
|
||||
id: string;
|
||||
text: string;
|
||||
xPercent?: number;
|
||||
yPercent: number;
|
||||
fontSize: number;
|
||||
fontFamily?: string;
|
||||
color: string;
|
||||
fontWeight?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
async function loadTemplatesFromDisk(): Promise<DynamicQrTemplate[]> {
|
||||
try {
|
||||
const content = await fs.readFile(DATA_FILE, "utf-8");
|
||||
const parsed = JSON.parse(content);
|
||||
return parsed.map((p: any) => ({
|
||||
...p,
|
||||
id: p.key,
|
||||
isActive: p.isActive ?? true,
|
||||
createdAt: p.createdAt || new Date().toISOString(),
|
||||
updatedAt: p.updatedAt || new Date().toISOString(),
|
||||
defaultQrConfig: p.defaultQrConfig || p.qrConfig,
|
||||
defaultLayers: p.defaultLayers || p.layers,
|
||||
}));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function saveTemplatesToDisk(templates: DynamicQrTemplate[]): Promise<void> {
|
||||
try {
|
||||
await fs.writeFile(DATA_FILE, JSON.stringify(templates, null, 2), "utf-8");
|
||||
} catch (err) {
|
||||
console.error("Failed to write qr-templates to disk:", err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function qrTemplatesRoutes(app: FastifyInstance) {
|
||||
// 1. Tüm Aktif Şablonları Listele (Mobil ve Web İçin)
|
||||
app.get("/qr-templates", async () => {
|
||||
const list = await loadTemplatesFromDisk();
|
||||
return {
|
||||
templates: list.filter((t) => t.isActive),
|
||||
total: list.length,
|
||||
};
|
||||
});
|
||||
|
||||
// 2. Admin Şablon Listesi (Aktif + Taslak hepsi)
|
||||
app.get("/admin/qr-templates", async () => {
|
||||
const list = await loadTemplatesFromDisk();
|
||||
return {
|
||||
templates: list,
|
||||
total: list.length,
|
||||
};
|
||||
});
|
||||
|
||||
// 3. Yeni Şablon Ekle veya Mevcut Olanı Güncelle
|
||||
app.post<{ Body: Partial<DynamicQrTemplate> }>("/admin/qr-templates", async (req, reply) => {
|
||||
const body = req.body;
|
||||
if (!body.name) {
|
||||
return reply.status(400).send({ error: "Şablon adı zorunludur." });
|
||||
}
|
||||
|
||||
const key = body.key || `qr-${Date.now()}`;
|
||||
const list = await loadTemplatesFromDisk();
|
||||
const existingIndex = list.findIndex((t) => t.id === key || t.key === key);
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const newTemplate: DynamicQrTemplate = {
|
||||
key,
|
||||
id: key,
|
||||
name: body.name,
|
||||
categoryName: body.categoryName || "Özel Şablon",
|
||||
svgTemplateUrl: body.svgTemplateUrl || `/templates/${key}.svg`,
|
||||
aspectRatio: body.aspectRatio || "1/1",
|
||||
bgType: body.bgType || "gradient",
|
||||
bgImageUrl: body.bgImageUrl,
|
||||
bgGradient: body.bgGradient || "radial-gradient(circle at 50% 50%, #132034 0%, #0b0d13 100%)",
|
||||
plaqueColor: body.plaqueColor || "#132034",
|
||||
borderAccent: body.borderAccent || "#D4AF37",
|
||||
accentText: body.accentText || "#D4AF37",
|
||||
headerText: body.headerText || "#FFFFFF",
|
||||
subText: body.subText || "#E5C378",
|
||||
dividerColor: body.dividerColor || "rgba(212, 175, 55, 0.3)",
|
||||
isActive: body.isActive !== undefined ? body.isActive : true,
|
||||
createdAt: existingIndex >= 0 ? list[existingIndex].createdAt : now,
|
||||
updatedAt: now,
|
||||
defaultQrConfig: body.defaultQrConfig || (body as any).qrConfig || {
|
||||
xPercent: 50,
|
||||
yPercent: 38,
|
||||
size: 115,
|
||||
borderRadius: 14,
|
||||
borderColor: body.borderAccent || "#D4AF37",
|
||||
bgBoxColor: "#FFFFFF",
|
||||
},
|
||||
defaultLayers: body.defaultLayers || (body as any).layers || [
|
||||
{
|
||||
id: "rName",
|
||||
text: "{{RESTAURANT_NAME}}",
|
||||
yPercent: 16,
|
||||
fontSize: 18,
|
||||
color: body.accentText || "#D4AF37",
|
||||
fontWeight: "800",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
list[existingIndex] = newTemplate;
|
||||
} else {
|
||||
list.unshift(newTemplate);
|
||||
}
|
||||
|
||||
await saveTemplatesToDisk(list);
|
||||
return { success: true, template: newTemplate };
|
||||
});
|
||||
|
||||
// 4. Şablon Sil
|
||||
app.delete<{ Params: { id: string } }>("/admin/qr-templates/:id", async (req, reply) => {
|
||||
const { id } = req.params;
|
||||
const list = await loadTemplatesFromDisk();
|
||||
const filtered = list.filter((t) => t.id !== id && t.key !== id);
|
||||
|
||||
if (filtered.length === list.length) {
|
||||
return reply.status(404).send({ error: "Şablon bulunamadı." });
|
||||
}
|
||||
|
||||
await saveTemplatesToDisk(filtered);
|
||||
return { success: true, deletedId: id };
|
||||
});
|
||||
}
|
||||
@@ -7,7 +7,8 @@
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/icon.png",
|
||||
"userInterfaceStyle": "automatic",
|
||||
"newArchEnabled": false,
|
||||
"newArchEnabled": true,
|
||||
"platforms": ["ios", "android"],
|
||||
"splash": {
|
||||
"image": "./assets/splash.png",
|
||||
"resizeMode": "contain",
|
||||
|
||||
@@ -15,6 +15,13 @@
|
||||
"distribution": "internal",
|
||||
"ios": {
|
||||
"image": "latest"
|
||||
},
|
||||
"env": {
|
||||
"EXPO_PUBLIC_SUPABASE_URL": "https://fwahdopsyvryvjaqande.supabase.co",
|
||||
"EXPO_PUBLIC_SUPABASE_ANON_KEY": "sb_publishable_9YsK7YU7xfzxJQsz3x5iMA_LHCqUN3b",
|
||||
"EXPO_PUBLIC_API_URL": "https://api.menul.io",
|
||||
"EXPO_PUBLIC_WEB_URL": "https://menul.io",
|
||||
"EXPO_PUBLIC_REVENUECAT_API_KEY": "appl_uQIvGUuvEthCNheZtcRfVixVhFy"
|
||||
}
|
||||
},
|
||||
"production": {
|
||||
|
||||
@@ -25,9 +25,9 @@
|
||||
"expo-haptics": "^14.0.1",
|
||||
"expo-image-picker": "~16.0.6",
|
||||
"expo-linking": "~7.0.5",
|
||||
"expo-print": "^57.0.1",
|
||||
"expo-print": "~14.0.3",
|
||||
"expo-router": "~4.0.0",
|
||||
"expo-sharing": "^57.0.13",
|
||||
"expo-sharing": "~13.0.1",
|
||||
"expo-splash-screen": "~0.29.24",
|
||||
"expo-status-bar": "~2.0.0",
|
||||
"react": "18.3.1",
|
||||
@@ -35,9 +35,9 @@
|
||||
"react-native-purchases": "^8.2.0",
|
||||
"react-native-safe-area-context": "4.12.0",
|
||||
"react-native-screens": "~4.4.0",
|
||||
"react-native-svg": "^15.15.5",
|
||||
"react-native-svg": "15.8.0",
|
||||
"react-native-url-polyfill": "^2.0.0",
|
||||
"react-native-view-shot": "^5.1.1"
|
||||
"react-native-view-shot": "~4.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.25.2",
|
||||
|
||||
@@ -4,7 +4,7 @@ import { StatusBar } from "expo-status-bar";
|
||||
export default function RootLayout() {
|
||||
return (
|
||||
<>
|
||||
<StatusBar style="dark" />
|
||||
<StatusBar style="light" />
|
||||
<Stack screenOptions={{ headerShown: false }} />
|
||||
</>
|
||||
);
|
||||
|
||||
+564
-135
@@ -3,22 +3,22 @@ import { router } from "expo-router";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
Modal,
|
||||
Image,
|
||||
Pressable,
|
||||
RefreshControl,
|
||||
ScrollView,
|
||||
Share,
|
||||
Switch,
|
||||
Text,
|
||||
TextInput,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
|
||||
import * as Haptics from "expo-haptics";
|
||||
import * as Clipboard from "expo-clipboard";
|
||||
import ViewShot from "react-native-view-shot";
|
||||
import * as Sharing from "expo-sharing";
|
||||
import * as Print from "expo-print";
|
||||
import { QR_STAND_PRESETS, type QrStandPreset } from "@menulio/shared";
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import { api } from "@/lib/api";
|
||||
import { getActiveRestaurant, type ActiveRestaurant } from "@/lib/active-restaurant";
|
||||
import {
|
||||
@@ -29,7 +29,9 @@ import {
|
||||
import {
|
||||
QrCanvasStudio,
|
||||
getDefaultLayersForTemplate,
|
||||
getDefaultQrConfigForTemplate,
|
||||
type CanvasTextLayer,
|
||||
type CanvasQrConfig,
|
||||
} from "@/components/qr-canvas";
|
||||
|
||||
const HIDE_ALL_TEMPLATE_FIELDS: QrCustomFields = {
|
||||
@@ -48,95 +50,290 @@ interface QrResponse {
|
||||
pngBase64: string;
|
||||
}
|
||||
|
||||
function getPresetAspectRatio(preset?: QrStandPreset | null): number {
|
||||
if (!preset) return 1;
|
||||
if (preset.aspectRatio) {
|
||||
const parts = preset.aspectRatio.split(/[/:]/).map(Number);
|
||||
if (parts[0] && parts[1]) {
|
||||
return parts[0] / parts[1];
|
||||
}
|
||||
}
|
||||
if (TEMPLATE_ASPECT_RATIOS[preset.key]) {
|
||||
return TEMPLATE_ASPECT_RATIOS[preset.key];
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
export default function QrScreen() {
|
||||
const insets = useSafeAreaInsets();
|
||||
const [active, setActive] = useState<ActiveRestaurant | null>(null);
|
||||
const [restaurantName, setRestaurantName] = useState<string>("Gourmet Bistro");
|
||||
const [qr, setQr] = useState<QrResponse | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const availablePresets = Object.values(QR_STAND_PRESETS).filter(
|
||||
(p) => !!COMPONENT_TEMPLATES[p.key]
|
||||
const [scrollEnabled, setScrollEnabled] = useState(true);
|
||||
|
||||
// Dinamik Şablon Listesi (API'den ve yerelden)
|
||||
const [presets, setPresets] = useState<QrStandPreset[]>(() =>
|
||||
Object.values(QR_STAND_PRESETS)
|
||||
);
|
||||
|
||||
const [selectedKey, setSelectedKey] = useState<string>(
|
||||
availablePresets[0]?.key || "qr-1"
|
||||
presets[0]?.key || "qr-1"
|
||||
);
|
||||
const [exportingPng, setExportingPng] = useState(false);
|
||||
const [exportingPdf, setExportingPdf] = useState(false);
|
||||
|
||||
// Instagram Story / Canva Tarzı Katmanlar
|
||||
// Aktif Şablon ve Oran Hesaplama (Şablondaki Dinamik Aspect Ratio Öncelikli)
|
||||
const currentPreset = presets.find((p) => p.key === selectedKey) || presets[0];
|
||||
const aspectRatio = getPresetAspectRatio(currentPreset);
|
||||
const displayWidth = 290;
|
||||
const displayHeight = Math.round(displayWidth / aspectRatio);
|
||||
|
||||
// Instagram Story / Canva Tarzı Katmanlar & QR Konfigürasyonu
|
||||
const [layers, setLayers] = useState<CanvasTextLayer[]>(() =>
|
||||
getDefaultLayersForTemplate("qr-1", "Gourmet Bistro")
|
||||
);
|
||||
const [qrConfig, setQrConfig] = useState<CanvasQrConfig>(() =>
|
||||
getDefaultQrConfigForTemplate("qr-1", 290, 290)
|
||||
);
|
||||
|
||||
const [selectedLayerId, setSelectedLayerId] = useState<string | null>(null);
|
||||
const [isQrSelected, setIsQrSelected] = useState<boolean>(false);
|
||||
|
||||
// Şablonlar Arasında Düzenlemeleri Koruyan Önbellek
|
||||
const templateCacheRef = useRef<
|
||||
Record<string, { layers: CanvasTextLayer[]; qrConfig: CanvasQrConfig }>
|
||||
>({});
|
||||
|
||||
const handleLayersChange = (updater: React.SetStateAction<CanvasTextLayer[]>) => {
|
||||
setLayers((prev) => {
|
||||
const next = typeof updater === "function" ? updater(prev) : updater;
|
||||
templateCacheRef.current[selectedKey] = {
|
||||
layers: next,
|
||||
qrConfig: templateCacheRef.current[selectedKey]?.qrConfig || qrConfig,
|
||||
};
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const handleQrConfigChange = (updater: React.SetStateAction<CanvasQrConfig>) => {
|
||||
setQrConfig((prev) => {
|
||||
const next = typeof updater === "function" ? updater(prev) : updater;
|
||||
templateCacheRef.current[selectedKey] = {
|
||||
layers: templateCacheRef.current[selectedKey]?.layers || layers,
|
||||
qrConfig: next,
|
||||
};
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const viewShotRef = useRef<any>(null);
|
||||
|
||||
const activePreset: QrStandPreset =
|
||||
QR_STAND_PRESETS[selectedKey] || availablePresets[0] || Object.values(QR_STAND_PRESETS)[0]!;
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [syncing, setSyncing] = useState(false);
|
||||
|
||||
// Component-based template sistemi
|
||||
const TemplateComponent =
|
||||
COMPONENT_TEMPLATES[selectedKey] ?? Object.values(COMPONENT_TEMPLATES)[0];
|
||||
const aspectRatio = TEMPLATE_ASPECT_RATIOS[selectedKey] ?? 1;
|
||||
const displayWidth = 300;
|
||||
const displayHeight = Math.round(displayWidth / aspectRatio);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const activeRest = await getActiveRestaurant();
|
||||
if (!activeRest) return;
|
||||
setActive(activeRest);
|
||||
// Şablon ve QR Verilerini Çek / Senkronize Et
|
||||
const fetchTemplatesAndQr = async (isManualRefresh = false) => {
|
||||
if (isManualRefresh) {
|
||||
setRefreshing(true);
|
||||
setSyncing(true);
|
||||
try { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); } catch {}
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await api.post<QrResponse>(`/restaurants/${activeRest.restaurantId}/qr`);
|
||||
setQr(data);
|
||||
|
||||
// Fetch detailed restaurant name
|
||||
// 1. Supabase ve API'den Dinamik Şablonları Çek
|
||||
let fetchedPresets: QrStandPreset[] = [];
|
||||
try {
|
||||
const restDetail = await api.get<{ name?: string }>(`/restaurants/${activeRest.restaurantId}`);
|
||||
if (restDetail?.name) {
|
||||
setRestaurantName(restDetail.name);
|
||||
setLayers((prev) =>
|
||||
prev.map((l) =>
|
||||
l.id === "rName" ? { ...l, text: restDetail.name!.toUpperCase() } : l
|
||||
)
|
||||
);
|
||||
const { data: dbTemplates, error: dbErr } = await supabase
|
||||
.from("qr_stand_templates")
|
||||
.select("*")
|
||||
.eq("is_active", true)
|
||||
.order("created_at", { ascending: false });
|
||||
|
||||
if (!dbErr && dbTemplates && dbTemplates.length > 0) {
|
||||
fetchedPresets = dbTemplates.map((row: any) => ({
|
||||
key: row.key,
|
||||
name: row.name,
|
||||
categoryName: row.category_name,
|
||||
svgTemplateUrl: row.svg_template_url || `/${row.key}.svg`,
|
||||
aspectRatio: row.aspect_ratio || "1/1",
|
||||
bgType: row.bg_type || "gradient",
|
||||
bgImageUrl: row.bg_image_url,
|
||||
bgGradient: row.bg_gradient,
|
||||
plaqueColor: row.plaque_color,
|
||||
borderAccent: row.border_accent,
|
||||
accentText: row.accent_text,
|
||||
headerText: row.header_text,
|
||||
subText: row.sub_text,
|
||||
dividerColor: row.divider_color,
|
||||
qrConfig: row.qr_config,
|
||||
layers: row.layers,
|
||||
}));
|
||||
} else {
|
||||
const tplData = await api.get<{ templates: QrStandPreset[] }>("/qr-templates");
|
||||
if (tplData?.templates && tplData.templates.length > 0) {
|
||||
fetchedPresets = tplData.templates;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// fallback
|
||||
}
|
||||
|
||||
let currentRestName = restaurantName;
|
||||
|
||||
// 2. Aktif Restoran ve QR Verilerini Çek
|
||||
const activeRest = await getActiveRestaurant();
|
||||
if (activeRest) {
|
||||
setActive(activeRest);
|
||||
try {
|
||||
const qrData = await api.post<QrResponse>(`/restaurants/${activeRest.restaurantId}/qr`);
|
||||
setQr(qrData);
|
||||
|
||||
const restDetail = await api.get<{ name?: string }>(`/restaurants/${activeRest.restaurantId}`);
|
||||
if (restDetail?.name) {
|
||||
currentRestName = restDetail.name;
|
||||
setRestaurantName(restDetail.name);
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (fetchedPresets.length > 0) {
|
||||
setPresets(fetchedPresets);
|
||||
|
||||
// Eğer mevcut seçili şablon silinmişse ilk geçerli şablona geç
|
||||
let effectiveKey = selectedKey;
|
||||
const exists = fetchedPresets.some((p) => p.key === selectedKey);
|
||||
if (!exists) {
|
||||
effectiveKey = fetchedPresets[0].key;
|
||||
setSelectedKey(effectiveKey);
|
||||
}
|
||||
|
||||
// Önbellekten silinen şablonları temizle
|
||||
const validKeys = new Set(fetchedPresets.map((p) => p.key));
|
||||
Object.keys(templateCacheRef.current).forEach((k) => {
|
||||
if (!validKeys.has(k)) {
|
||||
delete templateCacheRef.current[k];
|
||||
}
|
||||
});
|
||||
|
||||
// Aktif seçili şablonu yeni verilerle güncelle
|
||||
const activeTpl = fetchedPresets.find((p) => p.key === effectiveKey) || fetchedPresets[0];
|
||||
if (activeTpl) {
|
||||
const pRatio = getPresetAspectRatio(activeTpl);
|
||||
const calcHeight = Math.round(displayWidth / (pRatio || 1));
|
||||
|
||||
let newLayers: CanvasTextLayer[];
|
||||
if ((activeTpl as any).layers && (activeTpl as any).layers.length > 0) {
|
||||
newLayers = (activeTpl as any).layers.map((l: any) => ({
|
||||
id: l.id || `layer-${Math.random()}`,
|
||||
text: l.text === "{{RESTAURANT_NAME}}" ? currentRestName.toUpperCase() : l.text,
|
||||
x: l.x ?? (displayWidth - 200) / 2,
|
||||
y: l.y ?? Math.round((calcHeight * (l.yPercent || 15)) / 100),
|
||||
rotation: l.rotation || 0,
|
||||
fontSize: l.fontSize || 16,
|
||||
fontId: l.fontFamily ? l.fontFamily.split(",")[0].replace(/['"]/g, "").trim() : "serif",
|
||||
color: l.color || activeTpl.accentText || "#D4AF37",
|
||||
textAlign: l.textAlign || "center",
|
||||
bgStyle: l.bgStyle || "none",
|
||||
letterSpacing: l.letterSpacing || 1,
|
||||
}));
|
||||
} else {
|
||||
newLayers = getDefaultLayersForTemplate(activeTpl.key, currentRestName);
|
||||
}
|
||||
|
||||
let newQr: CanvasQrConfig;
|
||||
if ((activeTpl as any).qrConfig) {
|
||||
const c = (activeTpl as any).qrConfig;
|
||||
newQr = {
|
||||
x: Math.round(((displayWidth - (c.size || 115)) * (c.xPercent || 50)) / 100),
|
||||
y: Math.round(((calcHeight - (c.size || 115)) * (c.yPercent || 38)) / 100),
|
||||
size: c.size || 115,
|
||||
borderRadius: c.borderRadius || 14,
|
||||
borderWidth: c.borderWidth || 2,
|
||||
borderColor: c.borderColor || activeTpl.borderAccent || "#D4AF37",
|
||||
padding: c.padding || 8,
|
||||
bgBoxColor: c.bgBoxColor || "#FFFFFF",
|
||||
};
|
||||
} else {
|
||||
newQr = getDefaultQrConfigForTemplate(activeTpl.key, displayWidth, calcHeight);
|
||||
}
|
||||
|
||||
setLayers(newLayers);
|
||||
setQrConfig(newQr);
|
||||
|
||||
// Önbelleği de taze veriyle güncelle
|
||||
templateCacheRef.current[activeTpl.key] = {
|
||||
layers: newLayers,
|
||||
qrConfig: newQr,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (isManualRefresh) {
|
||||
try { Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); } catch {}
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "QR oluşturulamadı");
|
||||
setError(err instanceof Error ? err.message : "QR yüklenemedi");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
setSyncing(false);
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchTemplatesAndQr(false);
|
||||
|
||||
// Supabase Realtime Değişiklik Aboneliği (Admin kaydettiğinde anında güncelle)
|
||||
const channel = supabase
|
||||
.channel("public:qr_stand_templates")
|
||||
.on(
|
||||
"postgres_changes",
|
||||
{ event: "*", schema: "public", table: "qr_stand_templates" },
|
||||
() => {
|
||||
fetchTemplatesAndQr(false);
|
||||
}
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
return () => {
|
||||
supabase.removeChannel(channel);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
|
||||
|
||||
async function handleCopy() {
|
||||
if (!qr?.targetUrl) return;
|
||||
try {
|
||||
await Clipboard.setStringAsync(qr.targetUrl);
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
||||
} catch {}
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}
|
||||
|
||||
async function handleShareQuick() {
|
||||
if (!qr?.targetUrl) return;
|
||||
try {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
|
||||
await Share.share({
|
||||
title: "Dijital QR Menü",
|
||||
message: `${qr.targetUrl}\nMenümüzü incelemek için QR kodu taratın veya linke tıklayın.`,
|
||||
url: qr.targetUrl,
|
||||
});
|
||||
} catch {}
|
||||
}
|
||||
|
||||
async function handleExportPng() {
|
||||
if (!viewShotRef.current) return;
|
||||
setSelectedLayerId(null);
|
||||
setIsQrSelected(false);
|
||||
setExportingPng(true);
|
||||
try {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
|
||||
await new Promise((r) => setTimeout(r, 120));
|
||||
const uri = await viewShotRef.current.capture?.();
|
||||
if (uri) {
|
||||
await Sharing.shareAsync(uri, {
|
||||
@@ -152,10 +349,13 @@ export default function QrScreen() {
|
||||
}
|
||||
|
||||
async function handleExportPdf() {
|
||||
if (!qr || !active) return;
|
||||
if (!qr || !active || !viewShotRef.current) return;
|
||||
setSelectedLayerId(null);
|
||||
setIsQrSelected(false);
|
||||
setExportingPdf(true);
|
||||
try {
|
||||
// ViewShot ile önce PNG al, sonra A5 HTML içine göm
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy);
|
||||
await new Promise((r) => setTimeout(r, 120));
|
||||
const pngUri = await viewShotRef.current?.capture();
|
||||
if (!pngUri) throw new Error("Görsel alınamadı");
|
||||
|
||||
@@ -212,26 +412,30 @@ export default function QrScreen() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<View style={{ flex: 1, backgroundColor: "#FAF8F5", alignItems: "center", justifyContent: "center" }}>
|
||||
<ActivityIndicator size="large" color="#C8A96B" />
|
||||
<Text style={{ marginTop: 12, color: "#78716C", fontSize: 14 }}>QR Kod ve Şablonlar Hazırlanıyor...</Text>
|
||||
<View style={{ flex: 1, backgroundColor: "#0C0C0E", alignItems: "center", justifyContent: "center" }}>
|
||||
<ActivityIndicator size="large" color="#D4AF37" />
|
||||
<Text style={{ marginTop: 14, color: "#A1A1AA", fontSize: 14, fontWeight: "600" }}>
|
||||
QR Stant Stüdyosu Hazırlanıyor...
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !qr) {
|
||||
return (
|
||||
<View style={{ flex: 1, backgroundColor: "#FAF8F5", alignItems: "center", justifyContent: "center", padding: 24 }}>
|
||||
<Ionicons name="alert-circle-outline" size={48} color="#DC2626" style={{ marginBottom: 12 }} />
|
||||
<Text style={{ color: "#DC2626", fontSize: 16, fontWeight: "600", textAlign: "center" }}>
|
||||
<View style={{ flex: 1, backgroundColor: "#0C0C0E", alignItems: "center", justifyContent: "center", padding: 24 }}>
|
||||
<Ionicons name="alert-circle-outline" size={48} color="#EF4444" style={{ marginBottom: 12 }} />
|
||||
<Text style={{ color: "#EF4444", fontSize: 16, fontWeight: "600", textAlign: "center" }}>
|
||||
{error ?? "QR kod yüklenemedi"}
|
||||
</Text>
|
||||
<Pressable
|
||||
onPress={() => router.back()}
|
||||
style={{ marginTop: 20, backgroundColor: "#1C1917", paddingHorizontal: 20, paddingVertical: 10, borderRadius: 8 }}
|
||||
accessible={true}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Geri Dön"
|
||||
style={{ marginTop: 20, backgroundColor: "#242429", paddingHorizontal: 20, paddingVertical: 12, borderRadius: 10, minHeight: 44, justifyContent: "center" }}
|
||||
>
|
||||
<Text style={{ color: "#FFF", fontWeight: "600" }}>Geri Dön</Text>
|
||||
</Pressable>
|
||||
@@ -240,79 +444,221 @@ export default function QrScreen() {
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView contentContainerStyle={{ flexGrow: 1, backgroundColor: "#0C0C0E", paddingVertical: 20 }}>
|
||||
<View style={{ width: "100%", maxWidth: 440, alignSelf: "center", paddingHorizontal: 20 }}>
|
||||
{/* Header Badge */}
|
||||
<ScrollView
|
||||
scrollEnabled={scrollEnabled}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={() => fetchTemplatesAndQr(true)}
|
||||
tintColor="#D4AF37"
|
||||
colors={["#D4AF37"]}
|
||||
/>
|
||||
}
|
||||
contentContainerStyle={{
|
||||
flexGrow: 1,
|
||||
backgroundColor: "#0C0C0E",
|
||||
paddingTop: insets.top + 8,
|
||||
paddingBottom: insets.bottom + 40,
|
||||
}}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
keyboardDismissMode="on-drag"
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<View style={{ width: "100%", maxWidth: 440, alignSelf: "center", paddingHorizontal: 16 }}>
|
||||
{/* Üst Navigasyon & Başlık Çubuğu */}
|
||||
<View
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
backgroundColor: "rgba(212, 175, 55, 0.15)",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
marginBottom: 12,
|
||||
}}
|
||||
>
|
||||
{/* Geri Dön Butonu */}
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
try { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); } catch {}
|
||||
router.back();
|
||||
}}
|
||||
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
|
||||
accessible={true}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Geri Dön"
|
||||
style={({ pressed }) => ({
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 13,
|
||||
backgroundColor: "#18181B",
|
||||
borderWidth: 1,
|
||||
borderColor: "rgba(255,255,255,0.08)",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
opacity: pressed ? 0.75 : 1,
|
||||
})}
|
||||
>
|
||||
<Ionicons name="chevron-back" size={20} color="#FFFFFF" />
|
||||
</Pressable>
|
||||
|
||||
{/* Stüdyo Rozeti */}
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: "rgba(212, 175, 55, 0.12)",
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 99,
|
||||
marginBottom: 12,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
borderWidth: 1,
|
||||
borderColor: "rgba(212, 175, 55, 0.3)",
|
||||
borderColor: "rgba(212, 175, 55, 0.25)",
|
||||
}}
|
||||
>
|
||||
<Ionicons name="qr-code-outline" size={15} color="#D4AF37" />
|
||||
<Text style={{ color: "#D4AF37", fontSize: 12, fontWeight: "700" }}>
|
||||
MASA QR STANT JENERATÖRÜ
|
||||
<Ionicons name="sparkles" size={13} color="#D4AF37" />
|
||||
<Text style={{ color: "#D4AF37", fontSize: 11, fontWeight: "800", letterSpacing: 0.5 }}>
|
||||
INSTAGRAM QR STÜDYOSU
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Text style={{ fontSize: 24, fontWeight: "800", color: "#FFFFFF", marginBottom: 4, textAlign: "center" }}>
|
||||
Masa Stant Tasarımları
|
||||
</Text>
|
||||
<Text style={{ fontSize: 13, color: "#A1A1AA", textAlign: "center", marginBottom: 20, lineHeight: 18 }}>
|
||||
Şablon seçin; QR kodunuz otomatik yerleşsin. Baskıya hazır A5 PDF veya PNG olarak indirin.
|
||||
</Text>
|
||||
{/* Canlı Yenile / Senkronize Et Butonu */}
|
||||
<Pressable
|
||||
onPress={() => fetchTemplatesAndQr(true)}
|
||||
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
|
||||
accessible={true}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Şablonları Canlı Yenile"
|
||||
style={({ pressed }) => ({
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 13,
|
||||
backgroundColor: "#18181B",
|
||||
borderWidth: 1,
|
||||
borderColor: syncing ? "#D4AF37" : "rgba(255,255,255,0.08)",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
opacity: pressed ? 0.75 : 1,
|
||||
})}
|
||||
>
|
||||
<Ionicons
|
||||
name="sync"
|
||||
size={18}
|
||||
color={syncing ? "#D4AF37" : "#A1A1AA"}
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
{/* Horizontal Theme Selector */}
|
||||
{availablePresets.length > 1 && (
|
||||
{/* Yatay Şablon Seçici Bar */}
|
||||
{presets.length > 0 && (
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={{ gap: 10, paddingHorizontal: 4, marginBottom: 20 }}
|
||||
style={{ flexGrow: 0, marginBottom: 12 }}
|
||||
contentContainerStyle={{ gap: 8, paddingHorizontal: 2, alignItems: "center" }}
|
||||
>
|
||||
{availablePresets.map((preset) => {
|
||||
{presets.map((preset) => {
|
||||
const isSelected = preset.key === selectedKey;
|
||||
const pRatio = getPresetAspectRatio(preset);
|
||||
const calcHeight = Math.round(displayWidth / (pRatio || 1));
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
key={preset.key}
|
||||
onPress={() => {
|
||||
setSelectedKey(preset.key);
|
||||
setLayers(getDefaultLayersForTemplate(preset.key, restaurantName));
|
||||
try { Haptics.selectionAsync(); } catch {}
|
||||
|
||||
// 1. Mevcut şablonun son durumunu önbelleğe kaydet
|
||||
templateCacheRef.current[selectedKey] = {
|
||||
layers,
|
||||
qrConfig,
|
||||
};
|
||||
|
||||
const nextKey = preset.key;
|
||||
setSelectedKey(nextKey);
|
||||
|
||||
// 2. Daha önce düzenlenmişse önbellekten geri yükle
|
||||
if (templateCacheRef.current[nextKey]) {
|
||||
setLayers(templateCacheRef.current[nextKey].layers);
|
||||
setQrConfig(templateCacheRef.current[nextKey].qrConfig);
|
||||
} else {
|
||||
// İlk defa açılıyorsa başlat
|
||||
let initialLayers: CanvasTextLayer[];
|
||||
if ((preset as any).layers && (preset as any).layers.length > 0) {
|
||||
initialLayers = (preset as any).layers.map((l: any) => ({
|
||||
id: l.id || `layer-${Math.random()}`,
|
||||
text: l.text === "{{RESTAURANT_NAME}}" ? restaurantName.toUpperCase() : l.text,
|
||||
x: l.x ?? (displayWidth - 200) / 2,
|
||||
y: l.y ?? Math.round((calcHeight * (l.yPercent || 15)) / 100),
|
||||
rotation: l.rotation || 0,
|
||||
fontSize: l.fontSize || 16,
|
||||
fontId: l.fontId || "serif",
|
||||
color: l.color || "#D4AF37",
|
||||
textAlign: l.textAlign || "center",
|
||||
bgStyle: l.bgStyle || "none",
|
||||
letterSpacing: l.letterSpacing || 1,
|
||||
}));
|
||||
} else {
|
||||
initialLayers = getDefaultLayersForTemplate(nextKey, restaurantName);
|
||||
}
|
||||
|
||||
let initialQr: CanvasQrConfig;
|
||||
if ((preset as any).qrConfig) {
|
||||
const c = (preset as any).qrConfig;
|
||||
initialQr = {
|
||||
x: Math.round(((displayWidth - (c.size || 115)) * (c.xPercent || 50)) / 100),
|
||||
y: Math.round(((calcHeight - (c.size || 115)) * (c.yPercent || 38)) / 100),
|
||||
size: c.size || 115,
|
||||
borderRadius: c.borderRadius || 14,
|
||||
borderWidth: c.borderWidth || 2,
|
||||
borderColor: c.borderColor || preset.borderAccent || "#D4AF37",
|
||||
padding: c.padding || 8,
|
||||
bgBoxColor: c.bgBoxColor || "#FFFFFF",
|
||||
};
|
||||
} else {
|
||||
initialQr = getDefaultQrConfigForTemplate(nextKey, displayWidth, calcHeight);
|
||||
}
|
||||
|
||||
setLayers(initialLayers);
|
||||
setQrConfig(initialQr);
|
||||
templateCacheRef.current[nextKey] = {
|
||||
layers: initialLayers,
|
||||
qrConfig: initialQr,
|
||||
};
|
||||
}
|
||||
|
||||
setSelectedLayerId(null);
|
||||
setIsQrSelected(false);
|
||||
}}
|
||||
style={{
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 10,
|
||||
borderRadius: 14,
|
||||
backgroundColor: isSelected ? "#D4AF37" : "#1A1A1E",
|
||||
hitSlop={{ top: 6, bottom: 6, left: 4, right: 4 }}
|
||||
accessible={true}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={`${preset.name} şablonunu seç`}
|
||||
accessibilityState={{ selected: isSelected }}
|
||||
style={({ pressed }) => ({
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 8,
|
||||
minHeight: 36,
|
||||
borderRadius: 12,
|
||||
backgroundColor: isSelected ? "#D4AF37" : "#18181B",
|
||||
borderWidth: 1.5,
|
||||
borderColor: isSelected ? "#D4AF37" : "#2B2B32",
|
||||
borderColor: isSelected ? "#D4AF37" : "rgba(255,255,255,0.12)",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
}}
|
||||
gap: 6,
|
||||
opacity: pressed ? 0.85 : 1,
|
||||
})}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 14,
|
||||
height: 14,
|
||||
borderRadius: 7,
|
||||
backgroundColor: preset.plaqueColor || "#1A1A1E",
|
||||
width: 10,
|
||||
height: 10,
|
||||
borderRadius: 5,
|
||||
backgroundColor: preset.plaqueColor || "#18181B",
|
||||
borderWidth: 1,
|
||||
borderColor: "rgba(255,255,255,0.4)",
|
||||
borderColor: isSelected ? "#0C0C0E" : "rgba(255,255,255,0.5)",
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 13,
|
||||
fontSize: 12,
|
||||
fontWeight: "700",
|
||||
color: isSelected ? "#0C0C0E" : "#FFFFFF",
|
||||
}}
|
||||
@@ -325,53 +671,123 @@ export default function QrScreen() {
|
||||
</ScrollView>
|
||||
)}
|
||||
|
||||
{/* İnteraktif Instagram / Canva Tarzı QR Stant Stüdyosu */}
|
||||
<View
|
||||
style={{
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: "100%",
|
||||
marginBottom: 20,
|
||||
}}
|
||||
>
|
||||
<ViewShot ref={viewShotRef} options={{ format: "png", quality: 1.0 }}>
|
||||
{/* İnteraktif Instagram QR & Canva Stüdyosu */}
|
||||
<QrCanvasStudio
|
||||
templateKey={selectedKey}
|
||||
restaurantName={restaurantName}
|
||||
qrBase64={qr.pngBase64}
|
||||
width={displayWidth}
|
||||
height={displayHeight}
|
||||
renderBackground={() =>
|
||||
TemplateComponent ? (
|
||||
<TemplateComponent
|
||||
renderBackground={() => {
|
||||
const cur = presets.find((p) => p.key === selectedKey);
|
||||
const bgImg = (cur as any)?.bgImageUrl;
|
||||
|
||||
// 1. Eğer şablona özel bir Arka Plan Görseli / SVG yüklenmişse bunu render et
|
||||
if (bgImg) {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width: displayWidth,
|
||||
height: displayHeight,
|
||||
backgroundColor: cur?.plaqueColor || "#132034",
|
||||
borderRadius: 24,
|
||||
overflow: "hidden",
|
||||
borderWidth: 2,
|
||||
borderColor: cur?.borderAccent || "#D4AF37",
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
source={{ uri: bgImg }}
|
||||
style={{ width: "100%", height: "100%", position: "absolute" }}
|
||||
resizeMode="cover"
|
||||
/>
|
||||
{/* İç Dekoratif Çerçeve */}
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 10,
|
||||
left: 10,
|
||||
right: 10,
|
||||
bottom: 10,
|
||||
borderWidth: 1,
|
||||
borderColor: cur?.dividerColor || "rgba(212, 175, 55, 0.3)",
|
||||
borderRadius: 16,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Özel görsel yoksa ve Yerel Hardcoded Component varsa onu kullan
|
||||
if (COMPONENT_TEMPLATES[selectedKey]) {
|
||||
const Comp = COMPONENT_TEMPLATES[selectedKey];
|
||||
return (
|
||||
<Comp
|
||||
qrBase64={qr.pngBase64}
|
||||
width={displayWidth}
|
||||
height={displayHeight}
|
||||
fields={HIDE_ALL_TEMPLATE_FIELDS}
|
||||
hideQr={true}
|
||||
/>
|
||||
) : null
|
||||
);
|
||||
}
|
||||
layers={layers}
|
||||
onLayersChange={setLayers}
|
||||
|
||||
// 3. Fallback dinamik stant
|
||||
if (!cur) return null;
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width: displayWidth,
|
||||
height: displayHeight,
|
||||
backgroundColor: cur.plaqueColor || "#132034",
|
||||
borderRadius: 24,
|
||||
overflow: "hidden",
|
||||
borderWidth: 2,
|
||||
borderColor: cur.borderAccent || "#D4AF37",
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 10,
|
||||
left: 10,
|
||||
right: 10,
|
||||
bottom: 10,
|
||||
borderWidth: 1,
|
||||
borderColor: cur.dividerColor || "rgba(212, 175, 55, 0.3)",
|
||||
borderRadius: 16,
|
||||
}}
|
||||
/>
|
||||
</ViewShot>
|
||||
</View>
|
||||
);
|
||||
}}
|
||||
layers={layers}
|
||||
onLayersChange={handleLayersChange}
|
||||
qrConfig={qrConfig}
|
||||
onQrConfigChange={handleQrConfigChange}
|
||||
viewShotRef={viewShotRef}
|
||||
selectedLayerId={selectedLayerId}
|
||||
onSelectLayer={setSelectedLayerId}
|
||||
isQrSelected={isQrSelected}
|
||||
onSelectQr={setIsQrSelected}
|
||||
onDragStateChange={(dragging) => setScrollEnabled(!dragging)}
|
||||
/>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{/* Output Action Buttons */}
|
||||
<View style={{ width: "100%", gap: 10 }}>
|
||||
{/* PDF Download Button */}
|
||||
{/* Aksiyon Butonları Grubu */}
|
||||
<View style={{ width: "100%", gap: 10, marginTop: 4 }}>
|
||||
{/* PDF İndirme Butonu */}
|
||||
<Pressable
|
||||
onPress={handleExportPdf}
|
||||
disabled={exportingPdf}
|
||||
accessible={true}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="PDF Olarak İndir (A5 Matbaa Baskı)"
|
||||
style={({ pressed }) => ({
|
||||
backgroundColor: "#D4AF37",
|
||||
borderRadius: 14,
|
||||
padding: 16,
|
||||
minHeight: 52,
|
||||
padding: 15,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
@@ -379,9 +795,9 @@ export default function QrScreen() {
|
||||
opacity: pressed || exportingPdf ? 0.85 : 1,
|
||||
shadowColor: "#D4AF37",
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.2,
|
||||
shadowOpacity: 0.25,
|
||||
shadowRadius: 8,
|
||||
elevation: 3,
|
||||
elevation: 4,
|
||||
})}
|
||||
>
|
||||
{exportingPdf ? (
|
||||
@@ -396,16 +812,20 @@ export default function QrScreen() {
|
||||
)}
|
||||
</Pressable>
|
||||
|
||||
{/* PNG Download Button */}
|
||||
{/* PNG İndirme Butonu */}
|
||||
<Pressable
|
||||
onPress={handleExportPng}
|
||||
disabled={exportingPng}
|
||||
accessible={true}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="PNG Olarak Kaydet veya Paylaş"
|
||||
style={({ pressed }) => ({
|
||||
backgroundColor: "#1A1A1E",
|
||||
backgroundColor: "#18181B",
|
||||
borderWidth: 1.5,
|
||||
borderColor: "#2B2B32",
|
||||
borderColor: "rgba(255,255,255,0.12)",
|
||||
borderRadius: 14,
|
||||
padding: 16,
|
||||
minHeight: 48,
|
||||
padding: 14,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
@@ -417,23 +837,27 @@ export default function QrScreen() {
|
||||
<ActivityIndicator size="small" color="#FFFFFF" />
|
||||
) : (
|
||||
<>
|
||||
<Ionicons name="image-outline" size={20} color="#FFFFFF" />
|
||||
<Text style={{ color: "#FFFFFF", fontSize: 15, fontWeight: "700" }}>
|
||||
<Ionicons name="image-outline" size={19} color="#FFFFFF" />
|
||||
<Text style={{ color: "#FFFFFF", fontSize: 14, fontWeight: "700" }}>
|
||||
PNG Olarak Kaydet / Paylaş
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</Pressable>
|
||||
|
||||
{/* Direct Link Share Button */}
|
||||
{/* Menü Linki Paylaş */}
|
||||
<Pressable
|
||||
onPress={handleShareQuick}
|
||||
accessible={true}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Menü Linkini Paylaş"
|
||||
style={({ pressed }) => ({
|
||||
backgroundColor: "#1A1A1E",
|
||||
backgroundColor: "#18181B",
|
||||
borderWidth: 1,
|
||||
borderColor: "#2B2B32",
|
||||
borderColor: "rgba(255,255,255,0.08)",
|
||||
borderRadius: 14,
|
||||
padding: 14,
|
||||
minHeight: 44,
|
||||
padding: 13,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
@@ -441,28 +865,33 @@ export default function QrScreen() {
|
||||
opacity: pressed ? 0.85 : 1,
|
||||
})}
|
||||
>
|
||||
<Ionicons name="share-outline" size={18} color="#D4AF37" />
|
||||
<Text style={{ color: "#D4AF37", fontSize: 14, fontWeight: "700" }}>
|
||||
<Ionicons name="share-outline" size={17} color="#D4AF37" />
|
||||
<Text style={{ color: "#D4AF37", fontSize: 13.5, fontWeight: "700" }}>
|
||||
Menü Linkini Paylaş
|
||||
</Text>
|
||||
</Pressable>
|
||||
|
||||
{/* Quick Copy Link Box */}
|
||||
{/* Hızlı Kopyalama Kutusu */}
|
||||
<Pressable
|
||||
onPress={handleCopy}
|
||||
style={{
|
||||
backgroundColor: "#141417",
|
||||
accessible={true}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={`Menü linkini kopyala: ${qr.targetUrl}`}
|
||||
style={({ pressed }) => ({
|
||||
backgroundColor: "#121215",
|
||||
borderWidth: 1,
|
||||
borderColor: "#242429",
|
||||
borderRadius: 14,
|
||||
padding: 14,
|
||||
borderColor: "rgba(255,255,255,0.08)",
|
||||
borderRadius: 12,
|
||||
padding: 12,
|
||||
minHeight: 44,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
marginTop: 4,
|
||||
}}
|
||||
marginTop: 2,
|
||||
opacity: pressed ? 0.85 : 1,
|
||||
})}
|
||||
>
|
||||
<Text style={{ color: "#A1A1AA", fontSize: 12, flex: 1 }} numberOfLines={1}>
|
||||
<Text style={{ color: "#A1A1AA", fontSize: 11.5, flex: 1 }} numberOfLines={1}>
|
||||
{qr.targetUrl}
|
||||
</Text>
|
||||
<Ionicons
|
||||
|
||||
@@ -6,14 +6,19 @@ import {
|
||||
View,
|
||||
} from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import * as Haptics from "expo-haptics";
|
||||
import { type CanvasTextLayer, getCanvasFontFamily } from "./types";
|
||||
|
||||
interface CanvasLayerItemProps {
|
||||
layer: CanvasTextLayer;
|
||||
isSelected: boolean;
|
||||
onSelect: (id: string) => void;
|
||||
onEdit: (layer: CanvasTextLayer) => void;
|
||||
onUpdatePosition: (id: string, x: number, y: number) => void;
|
||||
onDelete: (id: string) => void;
|
||||
onDragStart?: () => void;
|
||||
onDragMove?: (rawX: number, rawY: number, itemWidth: number, itemHeight: number) => { snappedX: number; snappedY: number };
|
||||
onDragEnd?: (finalX: number, finalY: number, itemWidth: number, itemHeight: number) => void;
|
||||
canvasWidth: number;
|
||||
canvasHeight: number;
|
||||
}
|
||||
@@ -22,95 +27,213 @@ export const CanvasLayerItem: React.FC<CanvasLayerItemProps> = ({
|
||||
layer,
|
||||
isSelected,
|
||||
onSelect,
|
||||
onEdit,
|
||||
onUpdatePosition,
|
||||
onDelete,
|
||||
onDragStart,
|
||||
onDragMove,
|
||||
onDragEnd,
|
||||
canvasWidth,
|
||||
canvasHeight,
|
||||
}) => {
|
||||
const layerRef = useRef(layer);
|
||||
layerRef.current = layer;
|
||||
|
||||
const canvasWidthRef = useRef(canvasWidth);
|
||||
canvasWidthRef.current = canvasWidth;
|
||||
|
||||
const canvasHeightRef = useRef(canvasHeight);
|
||||
canvasHeightRef.current = canvasHeight;
|
||||
|
||||
const panStartRef = useRef({ x: layer.x, y: layer.y });
|
||||
const layoutSizeRef = useRef({ width: 70, height: 26 });
|
||||
const currentPosRef = useRef({ x: layer.x, y: layer.y });
|
||||
const lastTapRef = useRef<number>(0);
|
||||
const isDraggingRef = useRef(false);
|
||||
|
||||
const panResponder = useRef(
|
||||
PanResponder.create({
|
||||
onStartShouldSetPanResponder: () => true,
|
||||
onMoveShouldSetPanResponder: () => true,
|
||||
onStartShouldSetPanResponderCapture: () => true,
|
||||
onMoveShouldSetPanResponder: (_evt, gestureState) =>
|
||||
Math.abs(gestureState.dx) > 1 || Math.abs(gestureState.dy) > 1,
|
||||
onMoveShouldSetPanResponderCapture: (_evt, gestureState) =>
|
||||
Math.abs(gestureState.dx) > 1 || Math.abs(gestureState.dy) > 1,
|
||||
onPanResponderTerminationRequest: () => false,
|
||||
|
||||
onPanResponderGrant: () => {
|
||||
onSelect(layer.id);
|
||||
panStartRef.current = { x: layer.x, y: layer.y };
|
||||
const now = Date.now();
|
||||
const DOUBLE_TAP_DELAY = 300;
|
||||
const curLayer = layerRef.current;
|
||||
if (now - lastTapRef.current < DOUBLE_TAP_DELAY) {
|
||||
try {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
|
||||
} catch {}
|
||||
onEdit(curLayer);
|
||||
} else {
|
||||
onSelect(curLayer.id);
|
||||
}
|
||||
lastTapRef.current = now;
|
||||
|
||||
panStartRef.current = { x: curLayer.x, y: curLayer.y };
|
||||
currentPosRef.current = { x: curLayer.x, y: curLayer.y };
|
||||
isDraggingRef.current = false;
|
||||
onDragStart?.();
|
||||
},
|
||||
|
||||
onPanResponderMove: (_evt, gestureState) => {
|
||||
const newX = Math.max(0, Math.min(canvasWidth - 40, panStartRef.current.x + gestureState.dx));
|
||||
const newY = Math.max(0, Math.min(canvasHeight - 30, panStartRef.current.y + gestureState.dy));
|
||||
onUpdatePosition(layer.id, Math.round(newX), Math.round(newY));
|
||||
if (!isDraggingRef.current && (Math.abs(gestureState.dx) > 2 || Math.abs(gestureState.dy) > 2)) {
|
||||
isDraggingRef.current = true;
|
||||
}
|
||||
|
||||
const curLayer = layerRef.current;
|
||||
const itemW = layoutSizeRef.current.width;
|
||||
const itemH = layoutSizeRef.current.height;
|
||||
const cWidth = canvasWidthRef.current;
|
||||
const cHeight = canvasHeightRef.current;
|
||||
|
||||
const maxX = Math.max(0, cWidth - itemW);
|
||||
const maxY = Math.max(0, cHeight - itemH);
|
||||
const rawX = Math.max(0, Math.min(maxX, panStartRef.current.x + gestureState.dx));
|
||||
const rawY = Math.max(0, Math.min(maxY, panStartRef.current.y + gestureState.dy));
|
||||
|
||||
if (onDragMove) {
|
||||
const { snappedX, snappedY } = onDragMove(rawX, rawY, itemW, itemH);
|
||||
currentPosRef.current = { x: snappedX, y: snappedY };
|
||||
onUpdatePosition(curLayer.id, snappedX, snappedY);
|
||||
} else {
|
||||
const sx = Math.round(rawX);
|
||||
const sy = Math.round(rawY);
|
||||
currentPosRef.current = { x: sx, y: sy };
|
||||
onUpdatePosition(curLayer.id, sx, sy);
|
||||
}
|
||||
},
|
||||
|
||||
onPanResponderRelease: () => {
|
||||
const itemW = layoutSizeRef.current.width;
|
||||
const itemH = layoutSizeRef.current.height;
|
||||
onDragEnd?.(currentPosRef.current.x, currentPosRef.current.y, itemW, itemH);
|
||||
},
|
||||
|
||||
onPanResponderTerminate: () => {
|
||||
const itemW = layoutSizeRef.current.width;
|
||||
const itemH = layoutSizeRef.current.height;
|
||||
onDragEnd?.(currentPosRef.current.x, currentPosRef.current.y, itemW, itemH);
|
||||
},
|
||||
})
|
||||
).current;
|
||||
|
||||
const fontFamily = getCanvasFontFamily(layer.fontId);
|
||||
// Instagram Badge arka plan stili
|
||||
const renderBackgroundStyle = () => {
|
||||
switch (layer.bgStyle) {
|
||||
case "solid":
|
||||
return {
|
||||
backgroundColor: layer.bgBoxColor || "rgba(18, 18, 21, 0.92)",
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 5,
|
||||
borderRadius: 8,
|
||||
};
|
||||
case "soft":
|
||||
return {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.18)",
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 5,
|
||||
borderRadius: 8,
|
||||
borderWidth: 1,
|
||||
borderColor: "rgba(255, 255, 255, 0.25)",
|
||||
};
|
||||
case "outline":
|
||||
return {
|
||||
backgroundColor: "transparent",
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 8,
|
||||
borderWidth: 1.5,
|
||||
borderColor: layer.color || "#D4AF37",
|
||||
};
|
||||
case "none":
|
||||
default:
|
||||
return {
|
||||
backgroundColor: "transparent",
|
||||
paddingHorizontal: 4,
|
||||
paddingVertical: 2,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
{...panResponder.panHandlers}
|
||||
accessible={true}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={`Metin: ${layer.text}`}
|
||||
accessibilityHint="Metni düzenlemek için çift dokunun, taşımak için sürükleyin"
|
||||
accessibilityState={{ selected: isSelected }}
|
||||
onLayout={(e) => {
|
||||
const { width, height } = e.nativeEvent.layout;
|
||||
layoutSizeRef.current = { width, height };
|
||||
}}
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: layer.x,
|
||||
top: layer.y,
|
||||
transform: [{ rotate: `${layer.rotation || 0}deg` }],
|
||||
zIndex: isSelected ? 100 : 10,
|
||||
padding: 4,
|
||||
zIndex: isSelected ? 90 : 20,
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
borderWidth: isSelected ? 1.5 : 0,
|
||||
borderColor: "#D4AF37",
|
||||
borderStyle: "dashed",
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: layer.bgBoxColor ? 10 : 4,
|
||||
paddingVertical: layer.bgBoxColor ? 5 : 2,
|
||||
backgroundColor: layer.bgBoxColor || "transparent",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderColor: isSelected ? "#D4AF37" : "transparent",
|
||||
borderStyle: isSelected ? "dashed" : "solid",
|
||||
borderRadius: 10,
|
||||
padding: 2,
|
||||
}}
|
||||
>
|
||||
<View style={renderBackgroundStyle()}>
|
||||
<Text
|
||||
style={{
|
||||
fontFamily,
|
||||
fontSize: layer.fontSize,
|
||||
color: layer.color,
|
||||
fontFamily: getCanvasFontFamily(layer.fontId),
|
||||
fontWeight: layer.fontWeight || "bold",
|
||||
letterSpacing: layer.letterSpacing || 0.5,
|
||||
textAlign: "center",
|
||||
textShadowColor: layer.hasShadow ? "rgba(0,0,0,0.85)" : "transparent",
|
||||
textShadowOffset: layer.hasShadow ? { width: 1, height: 1 } : { width: 0, height: 0 },
|
||||
textShadowRadius: layer.hasShadow ? 4 : 0,
|
||||
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 || "Metin"}
|
||||
{layer.text}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Seçili İken Hızlı Silme Rozeti */}
|
||||
{/* Seçili İken Üst Köşedeki Küçük Silme Rozeti */}
|
||||
{isSelected && (
|
||||
<Pressable
|
||||
onPress={() => onDelete(layer.id)}
|
||||
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: -12,
|
||||
right: -12,
|
||||
top: -8,
|
||||
right: -8,
|
||||
backgroundColor: "#EF4444",
|
||||
width: 22,
|
||||
height: 22,
|
||||
borderRadius: 11,
|
||||
width: 18,
|
||||
height: 18,
|
||||
borderRadius: 9,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderWidth: 1.5,
|
||||
borderColor: "#FFFFFF",
|
||||
shadowColor: "#000",
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 3,
|
||||
elevation: 4,
|
||||
borderColor: "#0C0C0E",
|
||||
}}
|
||||
>
|
||||
<Ionicons name="close" size={12} color="#FFFFFF" />
|
||||
<Ionicons name="close" size={11} color="#FFFFFF" />
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
import React, { useRef } from "react";
|
||||
import {
|
||||
Image,
|
||||
PanResponder,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { type CanvasQrConfig } from "./types";
|
||||
|
||||
interface CanvasQrItemProps {
|
||||
qrConfig: CanvasQrConfig;
|
||||
qrBase64?: string;
|
||||
isSelected: boolean;
|
||||
onSelect: () => void;
|
||||
onUpdatePosition: (x: number, y: number) => void;
|
||||
onDragStart?: () => void;
|
||||
onDragMove?: (rawX: number, rawY: number, itemWidth: number, itemHeight: number) => { snappedX: number; snappedY: number };
|
||||
onDragEnd?: (finalX: number, finalY: number, itemWidth: number, itemHeight: number) => void;
|
||||
canvasWidth: number;
|
||||
canvasHeight: number;
|
||||
}
|
||||
|
||||
export const CanvasQrItem: React.FC<CanvasQrItemProps> = ({
|
||||
qrConfig,
|
||||
qrBase64,
|
||||
isSelected,
|
||||
onSelect,
|
||||
onUpdatePosition,
|
||||
onDragStart,
|
||||
onDragMove,
|
||||
onDragEnd,
|
||||
canvasWidth,
|
||||
canvasHeight,
|
||||
}) => {
|
||||
// Stale closure önlemi için ref'ler
|
||||
const qrConfigRef = useRef(qrConfig);
|
||||
qrConfigRef.current = qrConfig;
|
||||
|
||||
const canvasWidthRef = useRef(canvasWidth);
|
||||
canvasWidthRef.current = canvasWidth;
|
||||
|
||||
const canvasHeightRef = useRef(canvasHeight);
|
||||
canvasHeightRef.current = canvasHeight;
|
||||
|
||||
const panStartRef = useRef({ x: qrConfig.x, y: qrConfig.y });
|
||||
const currentPosRef = useRef({ x: qrConfig.x, y: qrConfig.y });
|
||||
|
||||
const panResponder = useRef(
|
||||
PanResponder.create({
|
||||
onStartShouldSetPanResponder: () => true,
|
||||
onStartShouldSetPanResponderCapture: () => true,
|
||||
onMoveShouldSetPanResponder: (_evt, gestureState) =>
|
||||
Math.abs(gestureState.dx) > 1 || Math.abs(gestureState.dy) > 1,
|
||||
onMoveShouldSetPanResponderCapture: (_evt, gestureState) =>
|
||||
Math.abs(gestureState.dx) > 1 || Math.abs(gestureState.dy) > 1,
|
||||
onPanResponderTerminationRequest: () => false,
|
||||
|
||||
onPanResponderGrant: () => {
|
||||
onSelect();
|
||||
const cur = qrConfigRef.current;
|
||||
panStartRef.current = { x: cur.x, y: cur.y };
|
||||
currentPosRef.current = { x: cur.x, y: cur.y };
|
||||
onDragStart?.();
|
||||
},
|
||||
|
||||
onPanResponderMove: (_evt, gestureState) => {
|
||||
const cur = qrConfigRef.current;
|
||||
const cWidth = canvasWidthRef.current;
|
||||
const cHeight = canvasHeightRef.current;
|
||||
|
||||
const maxX = Math.max(0, cWidth - cur.size);
|
||||
const maxY = Math.max(0, cHeight - cur.size);
|
||||
|
||||
const rawX = Math.max(0, Math.min(maxX, panStartRef.current.x + gestureState.dx));
|
||||
const rawY = Math.max(0, Math.min(maxY, panStartRef.current.y + gestureState.dy));
|
||||
|
||||
if (onDragMove) {
|
||||
const { snappedX, snappedY } = onDragMove(rawX, rawY, cur.size, cur.size);
|
||||
currentPosRef.current = { x: snappedX, y: snappedY };
|
||||
onUpdatePosition(snappedX, snappedY);
|
||||
} else {
|
||||
const sx = Math.round(rawX);
|
||||
const sy = Math.round(rawY);
|
||||
currentPosRef.current = { x: sx, y: sy };
|
||||
onUpdatePosition(sx, sy);
|
||||
}
|
||||
},
|
||||
|
||||
onPanResponderRelease: () => {
|
||||
const cur = qrConfigRef.current;
|
||||
onDragEnd?.(currentPosRef.current.x, currentPosRef.current.y, cur.size, cur.size);
|
||||
},
|
||||
|
||||
onPanResponderTerminate: () => {
|
||||
const cur = qrConfigRef.current;
|
||||
onDragEnd?.(currentPosRef.current.x, currentPosRef.current.y, cur.size, cur.size);
|
||||
},
|
||||
})
|
||||
).current;
|
||||
|
||||
return (
|
||||
<View
|
||||
{...panResponder.panHandlers}
|
||||
accessible={true}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="QR Kod Alanı"
|
||||
accessibilityHint="QR kodunu seçmek, boyutunu ayarlamak veya stant üzerinde taşımak için dokunun"
|
||||
accessibilityState={{ selected: isSelected }}
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: qrConfig.x,
|
||||
top: qrConfig.y,
|
||||
width: qrConfig.size,
|
||||
height: qrConfig.size,
|
||||
zIndex: isSelected ? 80 : 15,
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
backgroundColor: qrConfig.bgBoxColor || "#FFFFFF",
|
||||
borderRadius: qrConfig.borderRadius,
|
||||
borderWidth: isSelected ? 2 : qrConfig.borderWidth,
|
||||
borderColor: isSelected ? "#D4AF37" : qrConfig.borderColor,
|
||||
borderStyle: isSelected ? "dashed" : "solid",
|
||||
padding: qrConfig.padding,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.25,
|
||||
shadowRadius: 6,
|
||||
elevation: 4,
|
||||
}}
|
||||
>
|
||||
{qrBase64 ? (
|
||||
<Image
|
||||
source={{ uri: `data:image/png;base64,${qrBase64}` }}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
borderRadius: Math.max(0, qrConfig.borderRadius - 4),
|
||||
}}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
) : (
|
||||
<View style={{ alignItems: "center", justifyContent: "center", width: "100%", height: "100%" }}>
|
||||
<Ionicons name="qr-code" size={Math.round(qrConfig.size * 0.7)} color="#0C0C0E" />
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Seçili İken Taşıma Rozeti */}
|
||||
{isSelected && (
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: -8,
|
||||
right: -8,
|
||||
backgroundColor: "#D4AF37",
|
||||
width: 22,
|
||||
height: 22,
|
||||
borderRadius: 11,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderWidth: 1.5,
|
||||
borderColor: "#0C0C0E",
|
||||
}}
|
||||
>
|
||||
<Ionicons name="move" size={12} color="#0C0C0E" />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,610 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
KeyboardAvoidingView,
|
||||
Modal,
|
||||
Platform,
|
||||
Pressable,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextInput,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import * as Haptics from "expo-haptics";
|
||||
import {
|
||||
type CanvasTextLayer,
|
||||
CANVAS_FONT_OPTIONS,
|
||||
CANVAS_COLOR_PALETTE,
|
||||
getCanvasFontFamily,
|
||||
} from "./types";
|
||||
|
||||
interface InstagramTextEditorModalProps {
|
||||
visible: boolean;
|
||||
layer: CanvasTextLayer | null;
|
||||
onSave: (updated: Partial<CanvasTextLayer>) => void;
|
||||
onClose: () => void;
|
||||
onDelete?: (id: string) => void;
|
||||
}
|
||||
|
||||
export const InstagramTextEditorModal: React.FC<InstagramTextEditorModalProps> = ({
|
||||
visible,
|
||||
layer,
|
||||
onSave,
|
||||
onClose,
|
||||
onDelete,
|
||||
}) => {
|
||||
const insets = useSafeAreaInsets();
|
||||
const inputRef = useRef<TextInput>(null);
|
||||
|
||||
const [text, setText] = useState("");
|
||||
const [fontId, setFontId] = useState("sans");
|
||||
const [color, setColor] = useState("#FFFFFF");
|
||||
const [fontSize, setFontSize] = useState(16);
|
||||
const [textAlign, setTextAlign] = useState<"left" | "center" | "right">("center");
|
||||
const [bgStyle, setBgStyle] = useState<"none" | "solid" | "soft" | "outline">("none");
|
||||
const [bgBoxColor, setBgBoxColor] = useState<string | undefined>(undefined);
|
||||
|
||||
// Modal açıldığında layer verilerini yükle
|
||||
useEffect(() => {
|
||||
if (layer && visible) {
|
||||
setText(layer.text || "");
|
||||
setFontId(layer.fontId || "sans");
|
||||
setColor(layer.color || "#FFFFFF");
|
||||
setFontSize(layer.fontSize || 16);
|
||||
setTextAlign(layer.textAlign || "center");
|
||||
if (layer.bgBoxColor) {
|
||||
if (layer.bgBoxColor.includes("0.2") || layer.bgBoxColor.includes("0.3")) {
|
||||
setBgStyle("soft");
|
||||
} else {
|
||||
setBgStyle("solid");
|
||||
}
|
||||
setBgBoxColor(layer.bgBoxColor);
|
||||
} else {
|
||||
setBgStyle("none");
|
||||
setBgBoxColor(undefined);
|
||||
}
|
||||
|
||||
// Auto focus text input
|
||||
setTimeout(() => {
|
||||
inputRef.current?.focus();
|
||||
}, 150);
|
||||
}
|
||||
}, [layer, visible]);
|
||||
|
||||
// Arka plan pill stilini sırayla değiştir (Instagram "A" butonu mantığı)
|
||||
const handleToggleBgStyle = () => {
|
||||
try {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
} catch {}
|
||||
|
||||
if (bgStyle === "none") {
|
||||
setBgStyle("solid");
|
||||
// Solid: seçili rengi kutu yap, metin rengini zıt yap
|
||||
if (color === "#FFFFFF" || color === "#F6EFE3") {
|
||||
setBgBoxColor("rgba(18, 18, 21, 0.92)");
|
||||
} else {
|
||||
setBgBoxColor("rgba(212, 175, 55, 0.92)");
|
||||
}
|
||||
} else if (bgStyle === "solid") {
|
||||
setBgStyle("soft");
|
||||
setBgBoxColor("rgba(255, 255, 255, 0.22)");
|
||||
} else if (bgStyle === "soft") {
|
||||
setBgStyle("outline");
|
||||
setBgBoxColor(undefined);
|
||||
} else {
|
||||
setBgStyle("none");
|
||||
setBgBoxColor(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
// Metin Hizalama Değiştir (Sol -> Orta -> Sağ)
|
||||
const handleToggleAlign = () => {
|
||||
try {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
} catch {}
|
||||
setTextAlign((prev) => {
|
||||
if (prev === "center") return "left";
|
||||
if (prev === "left") return "right";
|
||||
return "center";
|
||||
});
|
||||
};
|
||||
|
||||
// Renk Değiştir
|
||||
const handleSelectColor = (selectedColor: string) => {
|
||||
try {
|
||||
Haptics.selectionAsync();
|
||||
} catch {}
|
||||
setColor(selectedColor);
|
||||
if (bgStyle === "solid") {
|
||||
if (selectedColor === "#FFFFFF" || selectedColor === "#F6EFE3") {
|
||||
setBgBoxColor("rgba(18, 18, 21, 0.92)");
|
||||
} else if (selectedColor === "#121215") {
|
||||
setBgBoxColor("rgba(255, 255, 255, 0.92)");
|
||||
} else {
|
||||
setBgBoxColor(selectedColor);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Font Değiştir
|
||||
const handleSelectFont = (fId: string) => {
|
||||
try {
|
||||
Haptics.selectionAsync();
|
||||
} catch {}
|
||||
setFontId(fId);
|
||||
};
|
||||
|
||||
// Bitti / Kaydet
|
||||
const handleDone = () => {
|
||||
try {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
|
||||
} catch {}
|
||||
|
||||
let finalBg = bgBoxColor;
|
||||
if (bgStyle === "none" || bgStyle === "outline") {
|
||||
finalBg = undefined;
|
||||
}
|
||||
|
||||
onSave({
|
||||
text: text.trim() ? text : "Metin",
|
||||
fontId,
|
||||
color,
|
||||
fontSize,
|
||||
textAlign,
|
||||
bgBoxColor: finalBg,
|
||||
bgStyle,
|
||||
hasShadow: bgStyle === "outline" || (!finalBg && color !== "#121215"),
|
||||
});
|
||||
onClose();
|
||||
};
|
||||
|
||||
const fontFamily = getCanvasFontFamily(fontId);
|
||||
|
||||
if (!visible || !layer) return null;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
transparent
|
||||
animationType="fade"
|
||||
statusBarTranslucent
|
||||
onRequestClose={onClose}
|
||||
>
|
||||
<View style={styles.overlayBackdrop}>
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||
style={[styles.container, { paddingTop: insets.top + 8, paddingBottom: insets.bottom + 12 }]}
|
||||
>
|
||||
{/* ÜST BAR - INSTAGRAM STORY KONTROLLERİ */}
|
||||
<View style={styles.topBar}>
|
||||
{/* Sol: İptal / Kapat */}
|
||||
<Pressable
|
||||
onPress={onClose}
|
||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||
style={styles.iconButton}
|
||||
>
|
||||
<Ionicons name="close" size={24} color="#FFFFFF" />
|
||||
</Pressable>
|
||||
|
||||
{/* Orta Araçlar: Hizalama & Arka Plan Rozeti */}
|
||||
<View style={styles.centerTools}>
|
||||
{/* Hizalama Butonu */}
|
||||
<Pressable
|
||||
onPress={handleToggleAlign}
|
||||
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
|
||||
style={styles.toolPill}
|
||||
>
|
||||
<Ionicons
|
||||
name={
|
||||
textAlign === "left"
|
||||
? "reorder-three-outline"
|
||||
: textAlign === "right"
|
||||
? "reorder-three-outline"
|
||||
: "reorder-two-outline"
|
||||
}
|
||||
size={20}
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
</Pressable>
|
||||
|
||||
{/* Instagram "A" Arka Plan Butonu */}
|
||||
<Pressable
|
||||
onPress={handleToggleBgStyle}
|
||||
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
|
||||
style={[
|
||||
styles.toolPill,
|
||||
bgStyle !== "none" && { backgroundColor: "#FFFFFF" },
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.bgPillText,
|
||||
bgStyle !== "none" && { color: "#0C0C0E", fontWeight: "900" },
|
||||
]}
|
||||
>
|
||||
A
|
||||
</Text>
|
||||
{bgStyle === "soft" && (
|
||||
<View style={styles.softIndicator} />
|
||||
)}
|
||||
{bgStyle === "outline" && (
|
||||
<View style={styles.outlineIndicator} />
|
||||
)}
|
||||
</Pressable>
|
||||
|
||||
{/* Katmanı Sil (Varsa) */}
|
||||
{onDelete && (
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
try {
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning);
|
||||
} catch {}
|
||||
onDelete(layer.id);
|
||||
onClose();
|
||||
}}
|
||||
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
|
||||
style={[styles.toolPill, { backgroundColor: "rgba(239, 68, 68, 0.2)" }]}
|
||||
>
|
||||
<Ionicons name="trash-outline" size={17} color="#EF4444" />
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Sağ: BİTTİ (Done) Butonu */}
|
||||
<Pressable
|
||||
onPress={handleDone}
|
||||
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
|
||||
style={styles.doneButton}
|
||||
>
|
||||
<Text style={styles.doneButtonText}>Bitti</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
{/* ORTA BÖLÜM: SOL BOYUT SLIDER & CANLI METİN ALANI */}
|
||||
<View style={styles.editorBody}>
|
||||
{/* Sol: Dikey Boyut Stepper / Gösterge */}
|
||||
<View style={styles.sizeControlCol}>
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
try { Haptics.selectionAsync(); } catch {}
|
||||
setFontSize((s) => Math.min(48, s + 2));
|
||||
}}
|
||||
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
|
||||
style={styles.sizeStepBtn}
|
||||
>
|
||||
<Ionicons name="add" size={16} color="#FFFFFF" />
|
||||
</Pressable>
|
||||
|
||||
<View style={styles.sizeTrack}>
|
||||
<View
|
||||
style={[
|
||||
styles.sizeTrackFill,
|
||||
{ height: `${Math.min(100, Math.max(10, ((fontSize - 8) / 40) * 100))}%` },
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
try { Haptics.selectionAsync(); } catch {}
|
||||
setFontSize((s) => Math.max(8, s - 2));
|
||||
}}
|
||||
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
|
||||
style={styles.sizeStepBtn}
|
||||
>
|
||||
<Ionicons name="remove" size={16} color="#FFFFFF" />
|
||||
</Pressable>
|
||||
|
||||
<Text style={styles.sizeLabel}>{fontSize}pt</Text>
|
||||
</View>
|
||||
|
||||
{/* Orta: Canlı İnstagram Yazı Alanı */}
|
||||
<View style={styles.textInputArea}>
|
||||
<View
|
||||
style={[
|
||||
styles.textPillWrapper,
|
||||
bgStyle === "solid" && {
|
||||
backgroundColor: bgBoxColor || "#D4AF37",
|
||||
borderRadius: 14,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
},
|
||||
bgStyle === "soft" && {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.22)",
|
||||
borderRadius: 14,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
borderWidth: 1,
|
||||
borderColor: "rgba(255, 255, 255, 0.3)",
|
||||
},
|
||||
bgStyle === "outline" && {
|
||||
borderWidth: 1.5,
|
||||
borderColor: color,
|
||||
borderRadius: 14,
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 6,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<TextInput
|
||||
ref={inputRef}
|
||||
value={text}
|
||||
onChangeText={setText}
|
||||
multiline
|
||||
placeholder="Yazmaya başla..."
|
||||
placeholderTextColor="rgba(255,255,255,0.4)"
|
||||
textAlign={textAlign}
|
||||
autoCapitalize="sentences"
|
||||
style={[
|
||||
styles.textInput,
|
||||
{
|
||||
fontFamily,
|
||||
fontSize,
|
||||
color:
|
||||
bgStyle === "solid" && (bgBoxColor === "#FFFFFF" || bgBoxColor === "#F6EFE3")
|
||||
? "#0C0C0E"
|
||||
: color,
|
||||
textAlign,
|
||||
letterSpacing: fontId === "serif" ? 1.5 : 0.5,
|
||||
textShadowColor: bgStyle === "outline" ? color : "rgba(0,0,0,0.7)",
|
||||
textShadowOffset: { width: 0, height: 1 },
|
||||
textShadowRadius: bgStyle === "outline" ? 8 : 4,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* ALT BÖLÜM 1: RENK SEÇİCİ BALONCUKLAR */}
|
||||
<View style={styles.colorPickerBar}>
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={styles.colorScrollContent}
|
||||
>
|
||||
{CANVAS_COLOR_PALETTE.map((c) => {
|
||||
const isSelected = color === c.color;
|
||||
return (
|
||||
<Pressable
|
||||
key={c.id}
|
||||
onPress={() => handleSelectColor(c.color)}
|
||||
style={[
|
||||
styles.colorCircleWrapper,
|
||||
isSelected && { borderColor: "#FFFFFF", transform: [{ scale: 1.15 }] },
|
||||
]}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
styles.colorCircle,
|
||||
{ backgroundColor: c.color },
|
||||
c.color === "#121215" && { borderWidth: 1, borderColor: "rgba(255,255,255,0.4)" },
|
||||
]}
|
||||
/>
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
||||
{/* ALT BÖLÜM 2: INSTAGRAM FONT STİLLERİ CAROUSEL */}
|
||||
<View style={styles.fontPickerBar}>
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={styles.fontScrollContent}
|
||||
>
|
||||
{CANVAS_FONT_OPTIONS.map((f) => {
|
||||
const isSelected = fontId === f.id;
|
||||
return (
|
||||
<Pressable
|
||||
key={f.id}
|
||||
onPress={() => handleSelectFont(f.id)}
|
||||
style={[
|
||||
styles.fontPill,
|
||||
isSelected && styles.fontPillActive,
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.fontPillText,
|
||||
{ fontFamily: f.fontFamily },
|
||||
isSelected && styles.fontPillTextActive,
|
||||
]}
|
||||
>
|
||||
{f.label}
|
||||
</Text>
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</ScrollView>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
overlayBackdrop: {
|
||||
flex: 1,
|
||||
backgroundColor: "rgba(10, 10, 14, 0.94)",
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
topBar: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
paddingHorizontal: 16,
|
||||
height: 52,
|
||||
zIndex: 10,
|
||||
},
|
||||
iconButton: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
backgroundColor: "rgba(255, 255, 255, 0.12)",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
centerTools: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 10,
|
||||
},
|
||||
toolPill: {
|
||||
width: 38,
|
||||
height: 38,
|
||||
borderRadius: 19,
|
||||
backgroundColor: "rgba(255, 255, 255, 0.12)",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
position: "relative",
|
||||
},
|
||||
bgPillText: {
|
||||
color: "#FFFFFF",
|
||||
fontSize: 16,
|
||||
fontWeight: "700",
|
||||
},
|
||||
softIndicator: {
|
||||
position: "absolute",
|
||||
bottom: 4,
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: 3,
|
||||
backgroundColor: "#60A5FA",
|
||||
},
|
||||
outlineIndicator: {
|
||||
position: "absolute",
|
||||
bottom: 4,
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: 3,
|
||||
backgroundColor: "#F59E0B",
|
||||
},
|
||||
doneButton: {
|
||||
backgroundColor: "#FFFFFF",
|
||||
paddingHorizontal: 18,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 20,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
doneButtonText: {
|
||||
color: "#0C0C0E",
|
||||
fontSize: 14,
|
||||
fontWeight: "800",
|
||||
},
|
||||
editorBody: {
|
||||
flex: 1,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
sizeControlCol: {
|
||||
width: 36,
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
},
|
||||
sizeStepBtn: {
|
||||
width: 30,
|
||||
height: 30,
|
||||
borderRadius: 15,
|
||||
backgroundColor: "rgba(255, 255, 255, 0.15)",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
sizeTrack: {
|
||||
width: 4,
|
||||
height: 120,
|
||||
backgroundColor: "rgba(255, 255, 255, 0.18)",
|
||||
borderRadius: 2,
|
||||
justifyContent: "flex-end",
|
||||
overflow: "hidden",
|
||||
},
|
||||
sizeTrackFill: {
|
||||
width: "100%",
|
||||
backgroundColor: "#D4AF37",
|
||||
borderRadius: 2,
|
||||
},
|
||||
sizeLabel: {
|
||||
color: "rgba(255, 255, 255, 0.6)",
|
||||
fontSize: 10,
|
||||
fontWeight: "700",
|
||||
},
|
||||
textInputArea: {
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 12,
|
||||
},
|
||||
textPillWrapper: {
|
||||
maxWidth: "100%",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
textInput: {
|
||||
minWidth: 140,
|
||||
maxWidth: "100%",
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
fontWeight: "800",
|
||||
},
|
||||
colorPickerBar: {
|
||||
paddingVertical: 10,
|
||||
},
|
||||
colorScrollContent: {
|
||||
paddingHorizontal: 20,
|
||||
gap: 12,
|
||||
alignItems: "center",
|
||||
},
|
||||
colorCircleWrapper: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 16,
|
||||
borderWidth: 2,
|
||||
borderColor: "transparent",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
colorCircle: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: 12,
|
||||
},
|
||||
fontPickerBar: {
|
||||
paddingVertical: 8,
|
||||
},
|
||||
fontScrollContent: {
|
||||
paddingHorizontal: 16,
|
||||
gap: 8,
|
||||
alignItems: "center",
|
||||
},
|
||||
fontPill: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 18,
|
||||
backgroundColor: "rgba(255, 255, 255, 0.08)",
|
||||
borderWidth: 1,
|
||||
borderColor: "rgba(255, 255, 255, 0.12)",
|
||||
},
|
||||
fontPillActive: {
|
||||
backgroundColor: "#FFFFFF",
|
||||
borderColor: "#FFFFFF",
|
||||
},
|
||||
fontPillText: {
|
||||
color: "rgba(255, 255, 255, 0.8)",
|
||||
fontSize: 12,
|
||||
fontWeight: "700",
|
||||
letterSpacing: 0.5,
|
||||
},
|
||||
fontPillTextActive: {
|
||||
color: "#0C0C0E",
|
||||
fontWeight: "900",
|
||||
},
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,5 @@
|
||||
export * from "./types";
|
||||
export * from "./CanvasLayerItem";
|
||||
export * from "./CanvasQrItem";
|
||||
export * from "./InstagramTextEditorModal";
|
||||
export * from "./QrCanvasStudio";
|
||||
|
||||
@@ -7,38 +7,61 @@ export interface CanvasTextLayer {
|
||||
fontSize: number;
|
||||
fontId: string;
|
||||
color: string;
|
||||
bgBoxColor?: string; // Instagram badge kutusu (örn: rgba(0,0,0,0.8), #D4AF37 vb.)
|
||||
bgBoxColor?: string; // Instagram badge kutusu
|
||||
bgStyle?: "none" | "solid" | "soft" | "outline";
|
||||
textAlign?: "left" | "center" | "right";
|
||||
hasShadow?: boolean;
|
||||
letterSpacing?: number;
|
||||
fontWeight?: "normal" | "600" | "700" | "800" | "900";
|
||||
}
|
||||
|
||||
export interface CanvasQrConfig {
|
||||
x: number;
|
||||
y: number;
|
||||
size: number;
|
||||
borderRadius: number;
|
||||
borderWidth: number;
|
||||
borderColor: string;
|
||||
padding: number;
|
||||
bgBoxColor: string;
|
||||
}
|
||||
|
||||
export const CANVAS_FONT_OPTIONS = [
|
||||
{
|
||||
id: "sans",
|
||||
label: "Modern Sans",
|
||||
label: "MODERN",
|
||||
fontFamily: "System, -apple-system, Roboto, 'Helvetica Neue', Arial, sans-serif",
|
||||
},
|
||||
{
|
||||
id: "serif",
|
||||
label: "Zarif Serif",
|
||||
label: "ZARİF",
|
||||
fontFamily: "Georgia, 'Times New Roman', serif",
|
||||
},
|
||||
{
|
||||
id: "times",
|
||||
label: "Klasik Times",
|
||||
label: "KLASİK",
|
||||
fontFamily: "'Times New Roman', Times, Georgia, serif",
|
||||
},
|
||||
{
|
||||
id: "typewriter",
|
||||
label: "DAKTİLO",
|
||||
fontFamily: "Courier, 'Courier New', monospace",
|
||||
},
|
||||
{
|
||||
id: "palatino",
|
||||
label: "Lüks Palatino",
|
||||
label: "LÜKS",
|
||||
fontFamily: "Palatino, 'Palatino Linotype', Georgia, serif",
|
||||
},
|
||||
{
|
||||
id: "trebuchet",
|
||||
label: "Dinamik",
|
||||
label: "DİNAMİK",
|
||||
fontFamily: "'Trebuchet MS', 'Segoe UI', Arial, sans-serif",
|
||||
},
|
||||
{
|
||||
id: "heavy",
|
||||
label: "GÜÇLÜ",
|
||||
fontFamily: "Impact, 'Arial Black', sans-serif",
|
||||
},
|
||||
];
|
||||
|
||||
export const CANVAS_COLOR_PALETTE = [
|
||||
@@ -46,16 +69,41 @@ export const CANVAS_COLOR_PALETTE = [
|
||||
{ id: "white", color: "#FFFFFF", label: "Beyaz" },
|
||||
{ id: "black", color: "#121215", label: "Siyah" },
|
||||
{ id: "cream", color: "#F6EFE3", label: "Krem" },
|
||||
{ id: "emerald", color: "#0C4443", label: "Zümrüt" },
|
||||
{ id: "crimson", color: "#A64423", label: "Kızıl" },
|
||||
{ id: "blue", color: "#003554", label: "Lacivert" },
|
||||
{ id: "emerald", color: "#10B981", label: "Yeşil" },
|
||||
{ id: "crimson", color: "#EF4444", label: "Kırmızı" },
|
||||
{ id: "amber", color: "#F59E0B", label: "Amber" },
|
||||
{ id: "rose", color: "#EC4899", label: "Pembe" },
|
||||
{ id: "indigo", color: "#6366F1", label: "Lila" },
|
||||
{ id: "cyan", color: "#06B6D4", label: "Turkuaz" },
|
||||
];
|
||||
|
||||
export const INSTAGRAM_BADGE_STYLES = [
|
||||
{ id: "none", label: "Düz", bgBoxColor: undefined },
|
||||
{ id: "dark", label: "Siyah Kutu", bgBoxColor: "rgba(18, 18, 21, 0.88)" },
|
||||
{ id: "gold", label: "Altın Kutu", bgBoxColor: "rgba(212, 175, 55, 0.92)" },
|
||||
{ id: "white", label: "Beyaz Kutu", bgBoxColor: "rgba(255, 255, 255, 0.92)" },
|
||||
{ id: "dark", label: "Koyu", bgBoxColor: "rgba(18, 18, 21, 0.88)" },
|
||||
{ id: "gold", label: "Altın", bgBoxColor: "rgba(212, 175, 55, 0.92)" },
|
||||
{ id: "white", label: "Beyaz", bgBoxColor: "rgba(255, 255, 255, 0.92)" },
|
||||
{ id: "glass", label: "Cam", bgBoxColor: "rgba(255, 255, 255, 0.2)" },
|
||||
];
|
||||
|
||||
export const QR_SIZE_PRESETS = [
|
||||
{ label: "Kompakt", size: 85 },
|
||||
{ label: "Standart", size: 115 },
|
||||
{ label: "Büyük", size: 145 },
|
||||
{ label: "Geniş", size: 175 },
|
||||
];
|
||||
|
||||
export const QR_RADIUS_PRESETS = [
|
||||
{ label: "Düz", radius: 0 },
|
||||
{ label: "Hafif", radius: 10 },
|
||||
{ label: "Yuvarlak", radius: 18 },
|
||||
{ label: "Oval", radius: 28 },
|
||||
];
|
||||
|
||||
export const QR_BORDER_COLORS = [
|
||||
{ id: "gold", color: "#D4AF37", label: "Altın" },
|
||||
{ id: "black", color: "#121215", label: "Siyah" },
|
||||
{ id: "white", color: "#FFFFFF", label: "Beyaz" },
|
||||
{ id: "none", color: "transparent", label: "Yok" },
|
||||
];
|
||||
|
||||
export function getCanvasFontFamily(fontId?: string): string {
|
||||
@@ -63,6 +111,39 @@ export function getCanvasFontFamily(fontId?: string): string {
|
||||
return found ? found.fontFamily : "System, -apple-system, Roboto, sans-serif";
|
||||
}
|
||||
|
||||
export function getDefaultQrConfigForTemplate(
|
||||
templateKey: string,
|
||||
canvasWidth: number,
|
||||
canvasHeight: number
|
||||
): CanvasQrConfig {
|
||||
if (templateKey === "qr-1" || templateKey === "qr-8" || templateKey === "qr-9") {
|
||||
const size = 110;
|
||||
return {
|
||||
x: Math.round((canvasWidth - size) / 2),
|
||||
y: 102,
|
||||
size,
|
||||
borderRadius: 14,
|
||||
borderWidth: 1.5,
|
||||
borderColor: "#D4AF37",
|
||||
padding: 6,
|
||||
bgBoxColor: "#FFFFFF",
|
||||
};
|
||||
}
|
||||
|
||||
// Standart Dikey Şablonlar (qr-2, qr-3, qr-4, qr-5, qr-6)
|
||||
const size = 125;
|
||||
return {
|
||||
x: Math.round((canvasWidth - size) / 2),
|
||||
y: Math.round(canvasHeight * 0.36),
|
||||
size,
|
||||
borderRadius: 14,
|
||||
borderWidth: 1.5,
|
||||
borderColor: "#D4AF37",
|
||||
padding: 6,
|
||||
bgBoxColor: "#FFFFFF",
|
||||
};
|
||||
}
|
||||
|
||||
// Her şablon için başlangıç varsayılan katmanları (1:1 veya standart orana göre)
|
||||
export function getDefaultLayersForTemplate(templateKey: string, restaurantName = "Gourmet Bistro"): CanvasTextLayer[] {
|
||||
if (templateKey === "qr-1") {
|
||||
@@ -78,6 +159,7 @@ export function getDefaultLayersForTemplate(templateKey: string, restaurantName
|
||||
color: "#D4AF37",
|
||||
fontWeight: "800",
|
||||
letterSpacing: 2,
|
||||
textAlign: "center",
|
||||
},
|
||||
{
|
||||
id: "sub",
|
||||
@@ -90,6 +172,7 @@ export function getDefaultLayersForTemplate(templateKey: string, restaurantName
|
||||
color: "#E5C378",
|
||||
fontWeight: "600",
|
||||
letterSpacing: 1.5,
|
||||
textAlign: "center",
|
||||
},
|
||||
{
|
||||
id: "cta",
|
||||
@@ -102,6 +185,7 @@ export function getDefaultLayersForTemplate(templateKey: string, restaurantName
|
||||
color: "#D4AF37",
|
||||
fontWeight: "700",
|
||||
letterSpacing: 1,
|
||||
textAlign: "center",
|
||||
},
|
||||
{
|
||||
id: "table",
|
||||
@@ -113,6 +197,7 @@ export function getDefaultLayersForTemplate(templateKey: string, restaurantName
|
||||
fontId: "sans",
|
||||
color: "#C5A059",
|
||||
fontWeight: "600",
|
||||
textAlign: "center",
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -130,6 +215,7 @@ export function getDefaultLayersForTemplate(templateKey: string, restaurantName
|
||||
color: "#FFFFFF",
|
||||
fontWeight: "900",
|
||||
letterSpacing: 1,
|
||||
textAlign: "center",
|
||||
},
|
||||
{
|
||||
id: "sub",
|
||||
@@ -141,6 +227,7 @@ export function getDefaultLayersForTemplate(templateKey: string, restaurantName
|
||||
fontId: "sans",
|
||||
color: "#D1A752",
|
||||
fontWeight: "800",
|
||||
textAlign: "center",
|
||||
},
|
||||
{
|
||||
id: "cta",
|
||||
@@ -152,6 +239,7 @@ export function getDefaultLayersForTemplate(templateKey: string, restaurantName
|
||||
fontId: "sans",
|
||||
color: "#FFFFFF",
|
||||
fontWeight: "800",
|
||||
textAlign: "center",
|
||||
},
|
||||
{
|
||||
id: "wifi",
|
||||
@@ -163,6 +251,7 @@ export function getDefaultLayersForTemplate(templateKey: string, restaurantName
|
||||
fontId: "sans",
|
||||
color: "#D1A752",
|
||||
fontWeight: "800",
|
||||
textAlign: "center",
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -179,6 +268,7 @@ export function getDefaultLayersForTemplate(templateKey: string, restaurantName
|
||||
fontId: "serif",
|
||||
color: "#D4AF37",
|
||||
fontWeight: "800",
|
||||
textAlign: "center",
|
||||
},
|
||||
{
|
||||
id: "sub",
|
||||
@@ -190,6 +280,7 @@ export function getDefaultLayersForTemplate(templateKey: string, restaurantName
|
||||
fontId: "sans",
|
||||
color: "#FFFFFF",
|
||||
fontWeight: "700",
|
||||
textAlign: "center",
|
||||
},
|
||||
{
|
||||
id: "cta",
|
||||
@@ -201,6 +292,7 @@ export function getDefaultLayersForTemplate(templateKey: string, restaurantName
|
||||
fontId: "serif",
|
||||
color: "#D4AF37",
|
||||
fontWeight: "800",
|
||||
textAlign: "center",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -8,11 +8,12 @@ import Svg, {
|
||||
Text,
|
||||
Image,
|
||||
} from "react-native-svg";
|
||||
import { type QrTemplateProps, getFontFamily, renderSvgLines } from "./index";
|
||||
import { type QrTemplateProps, getFontFamily, renderSvgLines } from "./common";
|
||||
|
||||
const QrTemplate2 = ({
|
||||
qrBase64,
|
||||
fields,
|
||||
hideQr,
|
||||
...props
|
||||
}: QrTemplateProps) => {
|
||||
const rName = fields?.restaurantName;
|
||||
@@ -159,6 +160,7 @@ const QrTemplate2 = ({
|
||||
<Path stroke="#8c6239" strokeWidth={0.8} d="M145 608h30m50 0h30" />
|
||||
|
||||
{/* QR Placeholder / Gerçek QR */}
|
||||
{!hideQr && (
|
||||
<G transform="translate(90 215)">
|
||||
<Rect width={220} height={220} fill="#fbf8f2" stroke="#d8c7b0" strokeWidth={1.5} rx={20} />
|
||||
{qrBase64 ? (
|
||||
@@ -201,6 +203,7 @@ const QrTemplate2 = ({
|
||||
</>
|
||||
)}
|
||||
</G>
|
||||
)}
|
||||
|
||||
{/* Metinler (Çok satırlı destekli) */}
|
||||
<G textAnchor="middle">
|
||||
|
||||
@@ -12,11 +12,12 @@ import Svg, {
|
||||
Text,
|
||||
Image,
|
||||
} from "react-native-svg";
|
||||
import { type QrTemplateProps, getFontFamily, renderSvgLines } from "./index";
|
||||
import { type QrTemplateProps, getFontFamily, renderSvgLines } from "./common";
|
||||
|
||||
const QrTemplate3 = ({
|
||||
qrBase64,
|
||||
fields,
|
||||
hideQr,
|
||||
...props
|
||||
}: QrTemplateProps) => {
|
||||
const rName = fields?.restaurantName;
|
||||
@@ -213,6 +214,7 @@ const QrTemplate3 = ({
|
||||
</G>
|
||||
|
||||
{/* QR Placeholder / Gerçek QR */}
|
||||
{!hideQr && (
|
||||
<G id="qr-placeholder" transform="translate(90 215)">
|
||||
<Rect width={220} height={220} fill="#fff" stroke="#e4e4e7" strokeWidth={2} rx={20} />
|
||||
{qrBase64 ? (
|
||||
@@ -249,6 +251,7 @@ const QrTemplate3 = ({
|
||||
</>
|
||||
)}
|
||||
</G>
|
||||
)}
|
||||
|
||||
{/* Metinler (Çok satırlı destekli) */}
|
||||
<G id="layer-texts">
|
||||
|
||||
@@ -9,11 +9,12 @@ import Svg, {
|
||||
Text,
|
||||
Image,
|
||||
} from "react-native-svg";
|
||||
import { type QrTemplateProps, getFontFamily, renderSvgLines } from "./index";
|
||||
import { type QrTemplateProps, getFontFamily, renderSvgLines } from "./common";
|
||||
|
||||
const QrTemplate4 = ({
|
||||
qrBase64,
|
||||
fields,
|
||||
hideQr,
|
||||
...props
|
||||
}: QrTemplateProps) => {
|
||||
const rName = fields?.restaurantName;
|
||||
@@ -136,7 +137,8 @@ const QrTemplate4 = ({
|
||||
d="M114 278v-58h54m118 58v-58h-54M114 342v58h54m118-58v58h-54"
|
||||
/>
|
||||
|
||||
{/* QR Placeholder / Live QR */}
|
||||
{/* QR Placeholder / Gerçek QR */}
|
||||
{!hideQr && (
|
||||
<G transform="translate(125 230)">
|
||||
<Rect
|
||||
width={150}
|
||||
@@ -182,6 +184,7 @@ const QrTemplate4 = ({
|
||||
</>
|
||||
)}
|
||||
</G>
|
||||
)}
|
||||
|
||||
{/* Metinler */}
|
||||
<G fill="#111" textAnchor="middle">
|
||||
|
||||
@@ -6,7 +6,7 @@ import Svg, {
|
||||
Path,
|
||||
Text,
|
||||
} from "react-native-svg";
|
||||
import { type QrTemplateProps, getFontFamily, renderSvgLines } from "./index";
|
||||
import { type QrTemplateProps, getFontFamily, renderSvgLines } from "./common";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const bgImage = require("../../../assets/templates/5.png");
|
||||
@@ -14,6 +14,7 @@ const bgImage = require("../../../assets/templates/5.png");
|
||||
const QrTemplate5 = ({
|
||||
qrBase64,
|
||||
fields,
|
||||
hideQr,
|
||||
...props
|
||||
}: QrTemplateProps) => {
|
||||
const rName = fields?.restaurantName;
|
||||
@@ -39,6 +40,7 @@ const QrTemplate5 = ({
|
||||
/>
|
||||
|
||||
{/* QR Placeholder / Live QR */}
|
||||
{!hideQr && (
|
||||
<G transform="translate(108 206)">
|
||||
<Rect
|
||||
width={164}
|
||||
@@ -91,6 +93,7 @@ const QrTemplate5 = ({
|
||||
</>
|
||||
)}
|
||||
</G>
|
||||
)}
|
||||
|
||||
{/* Metinler */}
|
||||
<G fill="#003554" textAnchor="middle">
|
||||
|
||||
@@ -9,11 +9,12 @@ import Svg, {
|
||||
Text,
|
||||
Image,
|
||||
} from "react-native-svg";
|
||||
import { type QrTemplateProps, getFontFamily, renderSvgLines } from "./index";
|
||||
import { type QrTemplateProps, getFontFamily, renderSvgLines } from "./common";
|
||||
|
||||
const QrTemplate6 = ({
|
||||
qrBase64,
|
||||
fields,
|
||||
hideQr,
|
||||
...props
|
||||
}: QrTemplateProps) => {
|
||||
const rName = fields?.restaurantName;
|
||||
@@ -113,6 +114,7 @@ const QrTemplate6 = ({
|
||||
<Use href="#b6" transform="matrix(-.95 0 0 .95 360 360)" />
|
||||
|
||||
{/* QR Placeholder / Gerçek QR */}
|
||||
{!hideQr && (
|
||||
<G transform="translate(95 205)">
|
||||
<Path
|
||||
fill="#fdfbf7"
|
||||
@@ -166,6 +168,7 @@ const QrTemplate6 = ({
|
||||
</>
|
||||
)}
|
||||
</G>
|
||||
)}
|
||||
|
||||
{/* Metinler */}
|
||||
<G textAnchor="middle">
|
||||
|
||||
@@ -7,7 +7,7 @@ import Svg, {
|
||||
Text,
|
||||
Image,
|
||||
} from "react-native-svg";
|
||||
import { type QrTemplateProps, getFontFamily, renderSvgLines } from "./index";
|
||||
import { type QrTemplateProps, getFontFamily, renderSvgLines } from "./common";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const template8Bg = require("../../../assets/templates/8.jpeg");
|
||||
@@ -15,6 +15,7 @@ const template8Bg = require("../../../assets/templates/8.jpeg");
|
||||
const QrTemplate8 = ({
|
||||
qrBase64,
|
||||
fields,
|
||||
hideQr,
|
||||
...props
|
||||
}: QrTemplateProps) => {
|
||||
const rName = fields?.restaurantName;
|
||||
@@ -40,6 +41,7 @@ const QrTemplate8 = ({
|
||||
/>
|
||||
|
||||
{/* QR Alanı (Merkez) */}
|
||||
{!hideQr && (
|
||||
<G transform="translate(348 305)">
|
||||
<Rect
|
||||
width={328}
|
||||
@@ -91,6 +93,7 @@ const QrTemplate8 = ({
|
||||
</>
|
||||
)}
|
||||
</G>
|
||||
)}
|
||||
|
||||
{/* Üst Çizgiler & OR Ayracı */}
|
||||
<Path
|
||||
|
||||
@@ -9,7 +9,7 @@ import Svg, {
|
||||
TextPath,
|
||||
Image,
|
||||
} from "react-native-svg";
|
||||
import { type QrTemplateProps, getFontFamily, renderSvgLines } from "./index";
|
||||
import { type QrTemplateProps, getFontFamily, renderSvgLines } from "./common";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const template9Bg = require("../../../assets/templates/9.jpeg");
|
||||
@@ -17,6 +17,7 @@ const template9Bg = require("../../../assets/templates/9.jpeg");
|
||||
const QrTemplate9 = ({
|
||||
qrBase64,
|
||||
fields,
|
||||
hideQr,
|
||||
...props
|
||||
}: QrTemplateProps) => {
|
||||
const rName = fields?.restaurantName;
|
||||
@@ -46,6 +47,7 @@ const QrTemplate9 = ({
|
||||
/>
|
||||
|
||||
{/* QR Alanı (Merkez) */}
|
||||
{!hideQr && (
|
||||
<G transform="translate(362 428)">
|
||||
<Rect
|
||||
width={300}
|
||||
@@ -108,6 +110,7 @@ const QrTemplate9 = ({
|
||||
</>
|
||||
)}
|
||||
</G>
|
||||
)}
|
||||
|
||||
{/* Üst Amblem & Rozet */}
|
||||
<G stroke="#a88b58" transform="translate(290 142)">
|
||||
|
||||
@@ -7,11 +7,12 @@ import Svg, {
|
||||
Text,
|
||||
Image,
|
||||
} from "react-native-svg";
|
||||
import { type QrTemplateProps, getFontFamily, renderSvgLines } from "./index";
|
||||
import { type QrTemplateProps, getFontFamily, renderSvgLines } from "./common";
|
||||
|
||||
const QrTemplateLumiere = ({
|
||||
qrBase64,
|
||||
fields,
|
||||
hideQr,
|
||||
...props
|
||||
}: QrTemplateProps) => {
|
||||
const rName = fields?.restaurantName;
|
||||
@@ -91,6 +92,7 @@ const QrTemplateLumiere = ({
|
||||
<Path stroke="#d4af37" strokeOpacity={0.4} strokeWidth={0.8} d="M120 198h360" />
|
||||
|
||||
{/* QR Placeholder / Gerçek QR */}
|
||||
{!hideQr && (
|
||||
<G transform="translate(190 215)">
|
||||
<Rect width={220} height={220} fill="#fff" stroke="#d4af37" strokeWidth={2} rx={20} />
|
||||
{qrBase64 ? (
|
||||
@@ -128,6 +130,7 @@ const QrTemplateLumiere = ({
|
||||
</>
|
||||
)}
|
||||
</G>
|
||||
)}
|
||||
|
||||
{/* Metinler (Çok satırlı ve bağımsız font destekli) */}
|
||||
<G textAnchor="middle">
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import React from "react";
|
||||
import { Text as SvgText, type SvgProps } from "react-native-svg";
|
||||
|
||||
export interface TextItemConfig {
|
||||
text: string;
|
||||
fontSize?: number;
|
||||
fontId?: string;
|
||||
offsetY?: number; // Dikey konum kaydırma (yukarı / aşağı)
|
||||
visible?: boolean;
|
||||
}
|
||||
|
||||
export interface QrCustomFields {
|
||||
restaurantName: TextItemConfig;
|
||||
subtitle: TextItemConfig;
|
||||
tableNo: TextItemConfig;
|
||||
ctaText: TextItemConfig;
|
||||
wifiText: TextItemConfig;
|
||||
socialText: TextItemConfig;
|
||||
}
|
||||
|
||||
export interface QrTemplateProps extends SvgProps {
|
||||
qrBase64?: string;
|
||||
fields?: Partial<QrCustomFields>;
|
||||
hideQr?: boolean;
|
||||
}
|
||||
|
||||
// Türkçe karakterleri (İ, ı, Ş, ş, Ğ, ğ, Ü, ü, Ö, ö, Ç, ç) eksiksiz destekleyen sistem fontları
|
||||
export const FONT_OPTIONS = [
|
||||
{
|
||||
id: "sans",
|
||||
label: "Modern Sans",
|
||||
fontFamily: "System, -apple-system, Roboto, 'Helvetica Neue', Arial, sans-serif",
|
||||
},
|
||||
{
|
||||
id: "serif",
|
||||
label: "Zarif Serif",
|
||||
fontFamily: "Georgia, 'Times New Roman', serif",
|
||||
},
|
||||
{
|
||||
id: "times",
|
||||
label: "Klasik (Times)",
|
||||
fontFamily: "'Times New Roman', Times, Georgia, serif",
|
||||
},
|
||||
{
|
||||
id: "palatino",
|
||||
label: "Lüks Palatino",
|
||||
fontFamily: "Palatino, 'Palatino Linotype', Georgia, serif",
|
||||
},
|
||||
{
|
||||
id: "trebuchet",
|
||||
label: "Dinamik",
|
||||
fontFamily: "'Trebuchet MS', 'Segoe UI', Arial, sans-serif",
|
||||
},
|
||||
];
|
||||
|
||||
export function getFontFamily(fontId?: string, defaultFamily = "Georgia, serif"): string {
|
||||
if (!fontId) return defaultFamily;
|
||||
const found = FONT_OPTIONS.find((f) => f.id === fontId);
|
||||
return found ? found.fontFamily : defaultFamily;
|
||||
}
|
||||
|
||||
// Çok satırlı (Enter / \n) ve dikey kaydırmalı (offsetY) SVG metin render fonksiyonu
|
||||
export function renderSvgLines({
|
||||
text,
|
||||
x,
|
||||
y,
|
||||
offsetY = 0,
|
||||
lineHeight,
|
||||
fill,
|
||||
fontFamily,
|
||||
fontSize,
|
||||
fontWeight = "normal",
|
||||
letterSpacing,
|
||||
textAnchor = "middle",
|
||||
}: {
|
||||
text?: string;
|
||||
x: number;
|
||||
y: number;
|
||||
offsetY?: number;
|
||||
lineHeight?: number;
|
||||
fill?: string;
|
||||
fontFamily?: string;
|
||||
fontSize?: number;
|
||||
fontWeight?: string | number;
|
||||
letterSpacing?: number;
|
||||
textAnchor?: "middle" | "start" | "end";
|
||||
}) {
|
||||
if (!text) return null;
|
||||
const lines = text.split("\n");
|
||||
const computedLineHeight = lineHeight || (fontSize ? fontSize * 1.25 : 18);
|
||||
const startY = y + (offsetY || 0);
|
||||
|
||||
return (
|
||||
<>
|
||||
{lines.map((line, idx) => (
|
||||
<SvgText
|
||||
key={idx}
|
||||
x={x}
|
||||
y={startY + idx * computedLineHeight}
|
||||
fill={fill}
|
||||
fontFamily={fontFamily}
|
||||
fontSize={fontSize}
|
||||
fontWeight={fontWeight}
|
||||
letterSpacing={letterSpacing}
|
||||
textAnchor={textAnchor}
|
||||
>
|
||||
{line}
|
||||
</SvgText>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from "react";
|
||||
import { Text as SvgText, type SvgProps } from "react-native-svg";
|
||||
import QrTemplate1 from "./QrTemplateLumiere";
|
||||
import QrTemplate2 from "./QrTemplate2";
|
||||
import QrTemplate3 from "./QrTemplate3";
|
||||
@@ -8,115 +7,9 @@ import QrTemplate5 from "./QrTemplate5";
|
||||
import QrTemplate6 from "./QrTemplate6";
|
||||
import QrTemplate8 from "./QrTemplate8";
|
||||
import QrTemplate9 from "./QrTemplate9";
|
||||
import { type QrTemplateProps } from "./common";
|
||||
|
||||
export interface TextItemConfig {
|
||||
text: string;
|
||||
fontSize?: number;
|
||||
fontId?: string;
|
||||
offsetY?: number; // Dikey konum kaydırma (yukarı / aşağı)
|
||||
visible?: boolean;
|
||||
}
|
||||
|
||||
export interface QrCustomFields {
|
||||
restaurantName: TextItemConfig;
|
||||
subtitle: TextItemConfig;
|
||||
tableNo: TextItemConfig;
|
||||
ctaText: TextItemConfig;
|
||||
wifiText: TextItemConfig;
|
||||
socialText: TextItemConfig;
|
||||
}
|
||||
|
||||
export interface QrTemplateProps extends SvgProps {
|
||||
qrBase64?: string;
|
||||
fields?: Partial<QrCustomFields>;
|
||||
}
|
||||
|
||||
// Türkçe karakterleri (İ, ı, Ş, ş, Ğ, ğ, Ü, ü, Ö, ö, Ç, ç) eksiksiz destekleyen sistem fontları
|
||||
export const FONT_OPTIONS = [
|
||||
{
|
||||
id: "sans",
|
||||
label: "Modern Sans",
|
||||
fontFamily: "System, -apple-system, Roboto, 'Helvetica Neue', Arial, sans-serif",
|
||||
},
|
||||
{
|
||||
id: "serif",
|
||||
label: "Zarif Serif",
|
||||
fontFamily: "Georgia, 'Times New Roman', serif",
|
||||
},
|
||||
{
|
||||
id: "times",
|
||||
label: "Klasik (Times)",
|
||||
fontFamily: "'Times New Roman', Times, Georgia, serif",
|
||||
},
|
||||
{
|
||||
id: "palatino",
|
||||
label: "Lüks Palatino",
|
||||
fontFamily: "Palatino, 'Palatino Linotype', Georgia, serif",
|
||||
},
|
||||
{
|
||||
id: "trebuchet",
|
||||
label: "Dinamik",
|
||||
fontFamily: "'Trebuchet MS', 'Segoe UI', Arial, sans-serif",
|
||||
},
|
||||
];
|
||||
|
||||
export function getFontFamily(fontId?: string, defaultFamily = "Georgia, serif"): string {
|
||||
if (!fontId) return defaultFamily;
|
||||
const found = FONT_OPTIONS.find((f) => f.id === fontId);
|
||||
return found ? found.fontFamily : defaultFamily;
|
||||
}
|
||||
|
||||
// Çok satırlı (Enter / \n) ve dikey kaydırmalı (offsetY) SVG metin render fonksiyonu
|
||||
export function renderSvgLines({
|
||||
text,
|
||||
x,
|
||||
y,
|
||||
offsetY = 0,
|
||||
lineHeight,
|
||||
fill,
|
||||
fontFamily,
|
||||
fontSize,
|
||||
fontWeight = "normal",
|
||||
letterSpacing,
|
||||
textAnchor = "middle",
|
||||
}: {
|
||||
text?: string;
|
||||
x: number;
|
||||
y: number;
|
||||
offsetY?: number;
|
||||
lineHeight?: number;
|
||||
fill?: string;
|
||||
fontFamily?: string;
|
||||
fontSize?: number;
|
||||
fontWeight?: string | number;
|
||||
letterSpacing?: number;
|
||||
textAnchor?: "middle" | "start" | "end";
|
||||
}) {
|
||||
if (!text) return null;
|
||||
const lines = text.split("\n");
|
||||
const computedLineHeight = lineHeight || (fontSize ? fontSize * 1.25 : 18);
|
||||
const startY = y + (offsetY || 0);
|
||||
|
||||
return (
|
||||
<>
|
||||
{lines.map((line, idx) => (
|
||||
<SvgText
|
||||
key={idx}
|
||||
x={x}
|
||||
y={startY + idx * computedLineHeight}
|
||||
fill={fill}
|
||||
fontFamily={fontFamily}
|
||||
fontSize={fontSize}
|
||||
fontWeight={fontWeight}
|
||||
letterSpacing={letterSpacing}
|
||||
textAnchor={textAnchor}
|
||||
>
|
||||
{line}
|
||||
</SvgText>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
export * from "./common";
|
||||
|
||||
// Yeni component eklemek için buraya import edip COMPONENT_TEMPLATES'e kaydet
|
||||
export const COMPONENT_TEMPLATES: Record<
|
||||
@@ -142,7 +35,5 @@ export const TEMPLATE_ASPECT_RATIOS: Record<string, number> = {
|
||||
"qr-5": 380 / 500, // 380x500 = PNG arka planlı modern kart
|
||||
"qr-6": 400 / 580, // 400x580 = taş & ızgara vintage stant
|
||||
"qr-8": 1, // 1024x1024 = 1:1 Emerald Gold Özel Tasarım
|
||||
"qr-9": 1, // 1024x1024 = 1:1 Vintage Table Menu Özel Tasarım
|
||||
"qr-9": 380 / 500, // 380x500 = Vintage Table Menu Özel Tasarım
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import React from "react";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Menulio Admin Panel | Şablon & Sistem Yönetimi",
|
||||
description: "Menulio QR Şablon Stüdyosu, Masa Standı Tasarım Motoru ve Sistem Yönetimi",
|
||||
};
|
||||
|
||||
export default function AdminLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<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,158 @@
|
||||
import Link from "next/link";
|
||||
import { QR_STAND_PRESETS } from "@menulio/shared";
|
||||
|
||||
export default function AdminDashboardPage() {
|
||||
const presetCount = Object.keys(QR_STAND_PRESETS).length;
|
||||
|
||||
return (
|
||||
<div className="max-w-6xl mx-auto space-y-8">
|
||||
{/* Karşılama Başlığı */}
|
||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl md:text-3xl font-extrabold text-white tracking-tight">
|
||||
Menulio Sistem & Şablon Yönetimi
|
||||
</h1>
|
||||
<p className="text-zinc-400 text-sm mt-1">
|
||||
Masa standı şablonlarını, JSON şablon motorunu ve restoran görünüm ayarlarını yönetin.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<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 self-start"
|
||||
>
|
||||
<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>
|
||||
Yeni Şablon Tasarla & Ekle
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* İstatistik Kartları */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div className="p-5 rounded-2xl bg-[#121215] border border-white/10 space-y-2">
|
||||
<div className="text-xs font-semibold text-zinc-400 uppercase tracking-wider">
|
||||
Aktif Şablonlar
|
||||
</div>
|
||||
<div className="text-3xl font-black text-white">{presetCount} Adet</div>
|
||||
<div className="text-xs text-emerald-400 font-medium flex items-center gap-1">
|
||||
<span>●</span> Mobil & Web Canlıda
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-5 rounded-2xl bg-[#121215] border border-white/10 space-y-2">
|
||||
<div className="text-xs font-semibold text-zinc-400 uppercase tracking-wider">
|
||||
Şablon Motoru Tipi
|
||||
</div>
|
||||
<div className="text-2xl font-black text-[#D4AF37]">JSON Driven</div>
|
||||
<div className="text-xs text-zinc-400 font-medium">
|
||||
0 App Store güncellemesi
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-5 rounded-2xl bg-[#121215] border border-white/10 space-y-2">
|
||||
<div className="text-xs font-semibold text-zinc-400 uppercase tracking-wider">
|
||||
Baskı Çıktı Formatı
|
||||
</div>
|
||||
<div className="text-3xl font-black text-white">A5 & 300DPI</div>
|
||||
<div className="text-xs text-zinc-400 font-medium">
|
||||
PDF + Vektörel PNG
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-5 rounded-2xl bg-[#121215] border border-white/10 space-y-2">
|
||||
<div className="text-xs font-semibold text-zinc-400 uppercase tracking-wider">
|
||||
API Durumu
|
||||
</div>
|
||||
<div className="text-2xl font-black text-emerald-400">Aktif & Senkron</div>
|
||||
<div className="text-xs text-zinc-400 font-medium">
|
||||
REST API v1
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Nasıl Çalışır / Mimari Rehber Kutusu */}
|
||||
<div className="p-6 rounded-2xl bg-gradient-to-br from-[#18181B] to-[#121215] border border-[#D4AF37]/20 relative overflow-hidden">
|
||||
<div className="relative z-10 space-y-4">
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-[#D4AF37]/15 border border-[#D4AF37]/30 text-xs font-bold text-[#D4AF37]">
|
||||
💡 DİNAMİK ŞABLON MİMARİSİ
|
||||
</div>
|
||||
|
||||
<h2 className="text-xl font-bold text-white">
|
||||
Mobil Uygulamayı Güncellemeden Yeni Şablon Yayınlama
|
||||
</h2>
|
||||
|
||||
<p className="text-sm text-zinc-300 leading-relaxed max-w-3xl">
|
||||
Menulio, QR masa kartı şablonlarını doğrudan kod içine gömmek yerine <strong>JSON şablon spesifikasyonu</strong> ile çalışır.
|
||||
Admin panelinden yeni bir SVG/Görsel arka plan ve varsayılan QR/metin koordinatları tanımladığınızda,
|
||||
tüm mobil kullanıcılar uygulamayı güncellemek zorunda kalmadan yeni şablonu anında görürler.
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 pt-2">
|
||||
<div className="p-4 rounded-xl bg-black/40 border border-white/5 space-y-1.5">
|
||||
<div className="text-xs font-bold text-[#D4AF37]">1. Arka Planı Belirle</div>
|
||||
<div className="text-xs text-zinc-400">
|
||||
Figma veya Illustrator'dan SVG/PNG arka planı oluşturup yükleyin veya renk seçin.
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 rounded-xl bg-black/40 border border-white/5 space-y-1.5">
|
||||
<div className="text-xs font-bold text-[#D4AF37]">2. QR & Katmanları Ayarla</div>
|
||||
<div className="text-xs text-zinc-400">
|
||||
QR kodun yerini, boyutunu, restoran ismi ve slogan metinlerinin varsayılan yerlerini belirleyin.
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 rounded-xl bg-black/40 border border-white/5 space-y-1.5">
|
||||
<div className="text-xs font-bold text-[#D4AF37]">3. Tek Tıkla Yayınla</div>
|
||||
<div className="text-xs text-zinc-400">
|
||||
Kaydet butonuna bastığınız anda API üzerinden tüm restoranların mobil uygulamasına dağıtılır.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hızlı Şablonlar Listesi */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-bold text-white">Mevcut Hazır Şablonlar</h3>
|
||||
<Link
|
||||
href="/admin/templates"
|
||||
className="text-xs font-semibold text-[#D4AF37] hover:underline"
|
||||
>
|
||||
Tümünü Yönet & Düzenle →
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{Object.values(QR_STAND_PRESETS).slice(0, 4).map((preset) => (
|
||||
<div
|
||||
key={preset.key}
|
||||
className="p-4 rounded-xl bg-[#121215] border border-white/10 space-y-3 flex flex-col justify-between"
|
||||
>
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-[11px] font-mono text-zinc-500">{preset.key}</span>
|
||||
<span className="text-[10px] bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 px-2 py-0.5 rounded-full font-bold">
|
||||
Aktif
|
||||
</span>
|
||||
</div>
|
||||
<div className="font-bold text-white text-sm line-clamp-1">{preset.name}</div>
|
||||
<div className="text-xs text-zinc-400 mt-0.5">{preset.categoryName || "Masa Kartı"}</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-3 border-t border-white/5 flex items-center justify-between text-xs text-zinc-400">
|
||||
<span>Oran: {preset.aspectRatio || "1/1"}</span>
|
||||
<Link
|
||||
href={`/admin/templates?key=${preset.key}`}
|
||||
className="font-bold text-[#D4AF37] hover:underline"
|
||||
>
|
||||
Düzenle
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { Metadata } from "next";
|
||||
import { TemplateStudioAdminClient } from "@/components/TemplateStudioAdminClient";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "QR Şablon Stüdyosu | Menulio Admin",
|
||||
description: "Masa standı şablonlarını görsel editör veya JSON ile tasarlayın, canlı önizleyin ve anında yayınlayın.",
|
||||
};
|
||||
|
||||
export default function AdminTemplatesPage() {
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto space-y-6">
|
||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-2 border-b border-white/10 pb-5">
|
||||
<div>
|
||||
<h1 className="text-2xl font-extrabold text-white tracking-tight">
|
||||
QR Masa Standı Şablon Stüdyosu
|
||||
</h1>
|
||||
<p className="text-zinc-400 text-xs mt-1">
|
||||
Uygulama güncellemesi gerektirmeden yeni şablonlar ekleyin, renkleri ve QR koordinatlarını düzenleyin.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TemplateStudioAdminClient />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import { supabase } from "@/lib/supabase";
|
||||
|
||||
const DATA_FILE = path.join(process.cwd(), "src/data/qr-templates.json");
|
||||
|
||||
function mapFromDb(row: any) {
|
||||
return {
|
||||
key: row.key,
|
||||
name: row.name,
|
||||
categoryName: row.category_name,
|
||||
aspectRatio: row.aspect_ratio || "1/1",
|
||||
bgType: row.bg_type || "gradient",
|
||||
bgImageUrl: row.bg_image_url || undefined,
|
||||
bgGradient: row.bg_gradient || undefined,
|
||||
plaqueColor: row.plaque_color || undefined,
|
||||
borderAccent: row.border_accent || undefined,
|
||||
accentText: row.accent_text || undefined,
|
||||
headerText: row.header_text || undefined,
|
||||
subText: row.sub_text || undefined,
|
||||
dividerColor: row.divider_color || undefined,
|
||||
qrConfig: row.qr_config,
|
||||
layers: row.layers,
|
||||
isActive: row.is_active ?? true,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at,
|
||||
};
|
||||
}
|
||||
|
||||
function mapToDb(item: any) {
|
||||
return {
|
||||
key: item.key,
|
||||
name: item.name,
|
||||
category_name: item.categoryName || null,
|
||||
aspect_ratio: item.aspectRatio || "1/1",
|
||||
bg_type: item.bgType || "gradient",
|
||||
bg_image_url: item.bgImageUrl || null,
|
||||
bg_gradient: item.bgGradient || null,
|
||||
plaque_color: item.plaqueColor || null,
|
||||
border_accent: item.borderAccent || null,
|
||||
accent_text: item.accentText || null,
|
||||
header_text: item.headerText || null,
|
||||
sub_text: item.subText || null,
|
||||
divider_color: item.dividerColor || null,
|
||||
qr_config: item.qrConfig || item.defaultQrConfig || null,
|
||||
layers: item.layers || item.defaultLayers || null,
|
||||
is_active: item.isActive !== undefined ? item.isActive : true,
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
async function readFromDisk(): Promise<any[]> {
|
||||
try {
|
||||
const raw = await fs.readFile(DATA_FILE, "utf-8");
|
||||
return JSON.parse(raw);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function writeToDisk(list: any[]): Promise<void> {
|
||||
try {
|
||||
await fs.writeFile(DATA_FILE, JSON.stringify(list, null, 2), "utf-8");
|
||||
} catch {}
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from("qr_stand_templates")
|
||||
.select("*")
|
||||
.order("created_at", { ascending: false });
|
||||
|
||||
if (!error && data && data.length > 0) {
|
||||
const mapped = data.map(mapFromDb);
|
||||
await writeToDisk(mapped);
|
||||
return NextResponse.json({ templates: mapped, source: "supabase_db" });
|
||||
}
|
||||
} catch {}
|
||||
|
||||
const diskTemplates = await readFromDisk();
|
||||
return NextResponse.json({ templates: diskTemplates, source: "disk_cache" });
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const body = await req.json();
|
||||
if (!body || !body.key) {
|
||||
return NextResponse.json({ error: "Şablon key zorunludur" }, { status: 400 });
|
||||
}
|
||||
|
||||
// 1. Supabase'e Upsert et
|
||||
try {
|
||||
const dbRow = mapToDb(body);
|
||||
await supabase.from("qr_stand_templates").upsert(dbRow, { onConflict: "key" });
|
||||
} catch (dbErr) {
|
||||
console.warn("Supabase upsert warning:", dbErr);
|
||||
}
|
||||
|
||||
// 2. Diske de yaz (offline / backup)
|
||||
const diskList = await readFromDisk();
|
||||
const existingIdx = diskList.findIndex((t: any) => t.key === body.key);
|
||||
if (existingIdx >= 0) {
|
||||
diskList[existingIdx] = { ...diskList[existingIdx], ...body };
|
||||
} else {
|
||||
diskList.unshift(body);
|
||||
}
|
||||
await writeToDisk(diskList);
|
||||
|
||||
return NextResponse.json({ success: true, template: body });
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: err.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(req: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const key = searchParams.get("key");
|
||||
if (!key) {
|
||||
return NextResponse.json({ error: "Şablon key zorunludur" }, { status: 400 });
|
||||
}
|
||||
|
||||
// 1. Supabase'den Sil
|
||||
try {
|
||||
await supabase.from("qr_stand_templates").delete().eq("key", key);
|
||||
} catch (dbErr) {
|
||||
console.warn("Supabase delete warning:", dbErr);
|
||||
}
|
||||
|
||||
// 2. Diskten de Sil
|
||||
const diskList = await readFromDisk();
|
||||
const filtered = diskList.filter((t: any) => t.key !== key);
|
||||
await writeToDisk(filtered);
|
||||
|
||||
return NextResponse.json({ success: true, deletedKey: key });
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: err.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
Generated
+23
-146
@@ -90,14 +90,14 @@ importers:
|
||||
specifier: ~7.0.5
|
||||
version: 7.0.5(expo@52.0.49(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@expo/metro-runtime@4.0.1(react-native@0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1)))(react-native@0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1))(react@18.3.1)(supports-color@8.1.1))(react-native@0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1))(react@18.3.1)(supports-color@8.1.1)
|
||||
expo-print:
|
||||
specifier: ^57.0.1
|
||||
version: 57.0.1(expo@52.0.49(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@expo/metro-runtime@4.0.1(react-native@0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1)))(react-native@0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1))(react@18.3.1)(supports-color@8.1.1))(react-native@0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1))
|
||||
specifier: ~14.0.3
|
||||
version: 14.0.3(expo@52.0.49(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@expo/metro-runtime@4.0.1(react-native@0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1)))(react-native@0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1))(react@18.3.1)(supports-color@8.1.1))(react-native@0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1))
|
||||
expo-router:
|
||||
specifier: ~4.0.0
|
||||
version: 4.0.22(1482d1318244a89f06fcfc2b6da7d585)
|
||||
expo-sharing:
|
||||
specifier: ^57.0.13
|
||||
version: 57.0.13(expo@52.0.49(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@expo/metro-runtime@4.0.1(react-native@0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1)))(react-native@0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1))(react@18.3.1)(supports-color@8.1.1))(react-native@0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1))(react@18.3.1)(supports-color@8.1.1)(typescript@5.9.3)
|
||||
specifier: ~13.0.1
|
||||
version: 13.0.1(expo@52.0.49(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@expo/metro-runtime@4.0.1(react-native@0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1)))(react-native@0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1))(react@18.3.1)(supports-color@8.1.1))
|
||||
expo-splash-screen:
|
||||
specifier: ~0.29.24
|
||||
version: 0.29.24(expo@52.0.49(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@expo/metro-runtime@4.0.1(react-native@0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1)))(react-native@0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1))(react@18.3.1)(supports-color@8.1.1))(supports-color@8.1.1)
|
||||
@@ -120,14 +120,14 @@ importers:
|
||||
specifier: ~4.4.0
|
||||
version: 4.4.0(react-native@0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1))(react@18.3.1)
|
||||
react-native-svg:
|
||||
specifier: ^15.15.5
|
||||
version: 15.15.5(react-native@0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1))(react@18.3.1)
|
||||
specifier: 15.8.0
|
||||
version: 15.8.0(react-native@0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1))(react@18.3.1)
|
||||
react-native-url-polyfill:
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0(react-native@0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1))
|
||||
react-native-view-shot:
|
||||
specifier: ^5.1.1
|
||||
version: 5.1.1(react-native@0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1))(react@18.3.1)
|
||||
specifier: ~4.0.3
|
||||
version: 4.0.3(react-native@0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1))(react@18.3.1)
|
||||
devDependencies:
|
||||
'@babel/core':
|
||||
specifier: ^7.25.2
|
||||
@@ -1274,18 +1274,12 @@ packages:
|
||||
'@expo/code-signing-certificates@0.0.6':
|
||||
resolution: {integrity: sha512-iNe0puxwBNEcuua9gmTGzq+SuMDa0iATai1FlFTMHJ/vUmKvN/V//drXoLJkVb5i5H3iE/n/qIJxyoBnXouD0w==}
|
||||
|
||||
'@expo/config-plugins@57.0.8':
|
||||
resolution: {integrity: sha512-x6lx4s/19i39/+1dMPwb9tFCc4WR843dG5Yi+C64lhKL3YHX+6oKrT2Kv72PCEalnUY+usQDkB3NrLSrYOWtDg==}
|
||||
|
||||
'@expo/config-plugins@9.0.17':
|
||||
resolution: {integrity: sha512-m24F1COquwOm7PBl5wRbkT9P9DviCXe0D7S7nQsolfbhdCWuvMkfXeoWmgjtdhy7sDlOyIgBrAdnB6MfsWKqIg==}
|
||||
|
||||
'@expo/config-types@52.0.5':
|
||||
resolution: {integrity: sha512-AMDeuDLHXXqd8W+0zSjIt7f37vUd/BP8p43k68NHpyAvQO+z8mbQZm3cNQVAMySeayK2XoPigAFB1JF2NFajaA==}
|
||||
|
||||
'@expo/config-types@57.0.2':
|
||||
resolution: {integrity: sha512-ewW08OonrcRIsRKIlFvvcmmafE5zemb1ocu3HkNwtVPyRtj2w42pZCAkMIROYpcVBaPnc3mDT9UZDzwXWC3i6g==}
|
||||
|
||||
'@expo/config@10.0.11':
|
||||
resolution: {integrity: sha512-nociJ4zr/NmbVfMNe9j/+zRlt7wz/siISu7PjdWE4WE+elEGxWWxsGzltdJG0llzrM+khx8qUiFK5aiVcdMBww==}
|
||||
|
||||
@@ -1329,20 +1323,9 @@ packages:
|
||||
'@expo/plist@0.2.2':
|
||||
resolution: {integrity: sha512-ZZGvTO6vEWq02UAPs3LIdja+HRO18+LRI5QuDl6Hs3Ps7KX7xU6Y6kjahWKY37Rx2YjNpX07dGpBFzzC+vKa2g==}
|
||||
|
||||
'@expo/plist@0.8.1':
|
||||
resolution: {integrity: sha512-3gTReGIUm0oRaMClsAJYxBnVPCl6fVpsl8HS+DTVxDhW4GyVyxg9E/Znm3BvcHtUJ51RJJI14pC1wvrNilCRHw==}
|
||||
|
||||
'@expo/prebuild-config@8.2.0':
|
||||
resolution: {integrity: sha512-CxiPpd980s0jyxi7eyN3i/7YKu3XL+8qPjBZUCYtc0+axpGweqIkq2CslyLSKHyqVyH/zlPkbVgWdyiYavFS5Q==}
|
||||
|
||||
'@expo/require-utils@57.0.4':
|
||||
resolution: {integrity: sha512-e7xbg/9BTQcsZE/oErafZXtI7kh5IgfasLJ97J5sFSzX2cA74pDvdlhW1KHVSaDkQyQv6h1LSLhsY7dEeOk7hw==}
|
||||
peerDependencies:
|
||||
typescript: ^5.0.0 || ^5.0.0-0 || ^6.0.0 || ^7.0.0
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
'@expo/rudder-sdk-node@1.1.1':
|
||||
resolution: {integrity: sha512-uy/hS/awclDJ1S88w9UGpc6Nm9XnNUjzOAAib1A3PVAnGQIwebg8DpFqOthFBTlZxeuV/BKbZ5jmTbtNZkp1WQ==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -2084,10 +2067,6 @@ packages:
|
||||
engines: {node: '>=10.0.0'}
|
||||
deprecated: this version has critical issues, please update to the latest version
|
||||
|
||||
'@xmldom/xmldom@0.8.14':
|
||||
resolution: {integrity: sha512-T4EDRUBVZYRldYApjEJiU0e1stYWaRAX7CuSnKzrpwdZKo53zGV8/pqfzV6FfwNl9YThD2OumQYvqtvjvgG7aQ==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
|
||||
'@xmldom/xmldom@0.9.11':
|
||||
resolution: {integrity: sha512-tW8bcK3hsG0/uqSnNz6TK4BkcuZSezoU7DlnYssILmZDktPnSHHuDJJFM0AJv+13gz2r0iGdrj6qqKeUnxXEDg==}
|
||||
engines: {node: '>=14.6'}
|
||||
@@ -2298,10 +2277,6 @@ packages:
|
||||
balanced-match@1.0.2:
|
||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||
|
||||
balanced-match@4.0.4:
|
||||
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
|
||||
engines: {node: 18 || 20 || >=22}
|
||||
|
||||
base64-arraybuffer@1.0.2:
|
||||
resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==}
|
||||
engines: {node: '>= 0.6.0'}
|
||||
@@ -2349,10 +2324,6 @@ packages:
|
||||
brace-expansion@2.1.4:
|
||||
resolution: {integrity: sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==}
|
||||
|
||||
brace-expansion@5.0.9:
|
||||
resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
braces@3.0.3:
|
||||
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -2928,8 +2899,8 @@ packages:
|
||||
expo-modules-core@2.2.3:
|
||||
resolution: {integrity: sha512-01QqZzpP/wWlxnNly4G06MsOBUTbMDj02DQigZoXfDh80vd/rk3/uVXqnZgOdLSggTs6DnvOgAUy0H2q30XdUg==}
|
||||
|
||||
expo-print@57.0.1:
|
||||
resolution: {integrity: sha512-cEfxM+sV5suZyqbXsOmDGRHD+UmfAh2GWabbakdkl1s70L4MFiObPtC73ATXNjeUe+mlzxLhudoTxsuyJxk8Mg==}
|
||||
expo-print@14.0.3:
|
||||
resolution: {integrity: sha512-VEGl7SX3q2Z3ZKwvxtOBAlXm9yDX8Jodewr+kouQcg2EaV0ciAH9rqLwRWZhAtEETpVdEG53ow7IAmwJU7jInA==}
|
||||
peerDependencies:
|
||||
expo: '*'
|
||||
react-native: '*'
|
||||
@@ -2953,12 +2924,10 @@ packages:
|
||||
react-native-reanimated:
|
||||
optional: true
|
||||
|
||||
expo-sharing@57.0.13:
|
||||
resolution: {integrity: sha512-8kRUBikuR+c90++PF4PaP8pj+McUNi7AIZkwmn0iXcA8QobkJDSAjSB3fP8qoFm8bzHDhyibHIlODTAsxrMYMw==}
|
||||
expo-sharing@13.0.1:
|
||||
resolution: {integrity: sha512-qych3Nw65wlFcnzE/gRrsdtvmdV0uF4U4qVMZBJYPG90vYyWh2QM9rp1gVu0KWOBc7N8CC2dSVYn4/BXqJy6Xw==}
|
||||
peerDependencies:
|
||||
expo: '*'
|
||||
react: '*'
|
||||
react-native: '*'
|
||||
|
||||
expo-splash-screen@0.29.24:
|
||||
resolution: {integrity: sha512-k2rdjbb3Qeg4g104Sdz6+qXXYba8QgiuZRSxHX8IpsSYiiTU48BmCCGy12sN+O1B+sD1/+WPL4duCa1Fy6+Y4g==}
|
||||
@@ -3186,10 +3155,6 @@ packages:
|
||||
resolution: {integrity: sha512-7yetJWqbS9sbn0vIfliPsFgoXMKn/YMF+Wuiog97x+urnSRRRZ7xB+uVkwGKzRgq9CDFfMQnE9ruL5DHv9c6Xg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
getenv@2.0.0:
|
||||
resolution: {integrity: sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
glob-parent@5.1.2:
|
||||
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
||||
engines: {node: '>= 6'}
|
||||
@@ -3203,10 +3168,6 @@ packages:
|
||||
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||
hasBin: true
|
||||
|
||||
glob@13.0.6:
|
||||
resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==}
|
||||
engines: {node: 18 || 20 || >=22}
|
||||
|
||||
glob@7.2.3:
|
||||
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
|
||||
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||
@@ -3648,10 +3609,6 @@ packages:
|
||||
lru-cache@10.4.3:
|
||||
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
||||
|
||||
lru-cache@11.5.2:
|
||||
resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
lru-cache@5.1.1:
|
||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
||||
|
||||
@@ -3784,10 +3741,6 @@ packages:
|
||||
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
minimatch@10.2.6:
|
||||
resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==}
|
||||
engines: {node: 18 || 20 || >=22}
|
||||
|
||||
minimatch@3.1.5:
|
||||
resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==}
|
||||
|
||||
@@ -4070,10 +4023,6 @@ packages:
|
||||
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
|
||||
engines: {node: '>=16 || 14 >=14.18'}
|
||||
|
||||
path-scurry@2.0.2:
|
||||
resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==}
|
||||
engines: {node: 18 || 20 || >=22}
|
||||
|
||||
path-type@4.0.0:
|
||||
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -4332,8 +4281,8 @@ packages:
|
||||
react: '*'
|
||||
react-native: '*'
|
||||
|
||||
react-native-svg@15.15.5:
|
||||
resolution: {integrity: sha512-L4go5jA+GWutdJ/JucuN20cjAbMg1HmMtAP+wZ+3JLCf6Jd0bhXQHxciRP/AQm/FlrIEZwkMcHNZP+FXAiic0w==}
|
||||
react-native-svg@15.8.0:
|
||||
resolution: {integrity: sha512-KHJzKpgOjwj1qeZzsBjxNdoIgv2zNCO9fVcoq2TEhTRsVV5DGTZ9JzUZwybd7q4giT/H3RdtqC3u44dWdO0Ffw==}
|
||||
peerDependencies:
|
||||
react: '*'
|
||||
react-native: '*'
|
||||
@@ -4343,12 +4292,11 @@ packages:
|
||||
peerDependencies:
|
||||
react-native: '*'
|
||||
|
||||
react-native-view-shot@5.1.1:
|
||||
resolution: {integrity: sha512-jOTLQrMGNBgv/nRA81VR2x2AdiCGoCjc63c9a5P4A0gStCbh9oDeJ6W+GtRxxQmxXXf6LDSrvG+mmWYulIrOkg==}
|
||||
engines: {node: '>=20.19.4', npm: '>=10'}
|
||||
react-native-view-shot@4.0.3:
|
||||
resolution: {integrity: sha512-USNjYmED7C0me02c1DxKA0074Hw+y/nxo+xJKlffMvfUWWzL5ELh/TJA/pTnVqFurIrzthZDPtDM7aBFJuhrHQ==}
|
||||
peerDependencies:
|
||||
react: '>=18.0.0'
|
||||
react-native: '>=0.76.0'
|
||||
react: '*'
|
||||
react-native: '*'
|
||||
|
||||
react-native@0.76.9:
|
||||
resolution: {integrity: sha512-+LRwecWmTDco7OweGsrECIqJu0iyrREd6CTCgC/uLLYipiHvk+MH9nd6drFtCw/6Blz6eoKTcH9YTTJusNtrWg==}
|
||||
@@ -6348,25 +6296,6 @@ snapshots:
|
||||
dependencies:
|
||||
node-forge: 1.4.0
|
||||
|
||||
'@expo/config-plugins@57.0.8(supports-color@8.1.1)(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@expo/config-types': 57.0.2
|
||||
'@expo/json-file': 11.0.1
|
||||
'@expo/plist': 0.8.1
|
||||
'@expo/require-utils': 57.0.4(supports-color@8.1.1)(typescript@5.9.3)
|
||||
'@expo/sdk-runtime-versions': 1.0.0
|
||||
chalk: 4.1.2
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
getenv: 2.0.0
|
||||
glob: 13.0.6
|
||||
semver: 7.8.5
|
||||
slugify: 1.6.9
|
||||
xcode: 3.0.1
|
||||
xml2js: 0.6.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
'@expo/config-plugins@9.0.17(supports-color@8.1.1)':
|
||||
dependencies:
|
||||
'@expo/config-types': 52.0.5
|
||||
@@ -6388,8 +6317,6 @@ snapshots:
|
||||
|
||||
'@expo/config-types@52.0.5': {}
|
||||
|
||||
'@expo/config-types@57.0.2': {}
|
||||
|
||||
'@expo/config@10.0.11(supports-color@8.1.1)':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.10.4
|
||||
@@ -6515,12 +6442,6 @@ snapshots:
|
||||
base64-js: 1.5.1
|
||||
xmlbuilder: 14.0.0
|
||||
|
||||
'@expo/plist@0.8.1':
|
||||
dependencies:
|
||||
'@xmldom/xmldom': 0.8.14
|
||||
base64-js: 1.5.1
|
||||
xmlbuilder: 15.1.1
|
||||
|
||||
'@expo/prebuild-config@8.2.0(supports-color@8.1.1)':
|
||||
dependencies:
|
||||
'@expo/config': 10.0.11(supports-color@8.1.1)
|
||||
@@ -6537,16 +6458,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@expo/require-utils@57.0.4(supports-color@8.1.1)(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.29.7
|
||||
'@babel/core': 7.29.7(supports-color@8.1.1)
|
||||
'@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)
|
||||
optionalDependencies:
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@expo/rudder-sdk-node@1.1.1':
|
||||
dependencies:
|
||||
'@expo/bunyan': 4.0.1
|
||||
@@ -7306,8 +7217,6 @@ snapshots:
|
||||
|
||||
'@xmldom/xmldom@0.7.13': {}
|
||||
|
||||
'@xmldom/xmldom@0.8.14': {}
|
||||
|
||||
'@xmldom/xmldom@0.9.11': {}
|
||||
|
||||
abort-controller@3.0.0:
|
||||
@@ -7542,8 +7451,6 @@ snapshots:
|
||||
|
||||
balanced-match@1.0.2: {}
|
||||
|
||||
balanced-match@4.0.4: {}
|
||||
|
||||
base64-arraybuffer@1.0.2: {}
|
||||
|
||||
base64-js@1.5.1: {}
|
||||
@@ -7585,10 +7492,6 @@ snapshots:
|
||||
dependencies:
|
||||
balanced-match: 1.0.2
|
||||
|
||||
brace-expansion@5.0.9:
|
||||
dependencies:
|
||||
balanced-match: 4.0.4
|
||||
|
||||
braces@3.0.3:
|
||||
dependencies:
|
||||
fill-range: 7.1.1
|
||||
@@ -8232,7 +8135,7 @@ snapshots:
|
||||
dependencies:
|
||||
invariant: 2.2.4
|
||||
|
||||
expo-print@57.0.1(expo@52.0.49(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@expo/metro-runtime@4.0.1(react-native@0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1)))(react-native@0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1))(react@18.3.1)(supports-color@8.1.1))(react-native@0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1)):
|
||||
expo-print@14.0.3(expo@52.0.49(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@expo/metro-runtime@4.0.1(react-native@0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1)))(react-native@0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1))(react@18.3.1)(supports-color@8.1.1))(react-native@0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1)):
|
||||
dependencies:
|
||||
expo: 52.0.49(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@expo/metro-runtime@4.0.1(react-native@0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1)))(react-native@0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1))(react@18.3.1)(supports-color@8.1.1)
|
||||
react-native: 0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1)
|
||||
@@ -8264,17 +8167,9 @@ snapshots:
|
||||
- react-native
|
||||
- supports-color
|
||||
|
||||
expo-sharing@57.0.13(expo@52.0.49(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@expo/metro-runtime@4.0.1(react-native@0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1)))(react-native@0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1))(react@18.3.1)(supports-color@8.1.1))(react-native@0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1))(react@18.3.1)(supports-color@8.1.1)(typescript@5.9.3):
|
||||
expo-sharing@13.0.1(expo@52.0.49(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@expo/metro-runtime@4.0.1(react-native@0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1)))(react-native@0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1))(react@18.3.1)(supports-color@8.1.1)):
|
||||
dependencies:
|
||||
'@expo/config-plugins': 57.0.8(supports-color@8.1.1)(typescript@5.9.3)
|
||||
'@expo/config-types': 57.0.2
|
||||
'@expo/plist': 0.8.1
|
||||
expo: 52.0.49(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@expo/metro-runtime@4.0.1(react-native@0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1)))(react-native@0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1))(react@18.3.1)(supports-color@8.1.1)
|
||||
react: 18.3.1
|
||||
react-native: 0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
expo-splash-screen@0.29.24(expo@52.0.49(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@expo/metro-runtime@4.0.1(react-native@0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1)))(react-native@0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1))(react@18.3.1)(supports-color@8.1.1))(supports-color@8.1.1):
|
||||
dependencies:
|
||||
@@ -8564,8 +8459,6 @@ snapshots:
|
||||
|
||||
getenv@1.0.0: {}
|
||||
|
||||
getenv@2.0.0: {}
|
||||
|
||||
glob-parent@5.1.2:
|
||||
dependencies:
|
||||
is-glob: 4.0.3
|
||||
@@ -8583,12 +8476,6 @@ snapshots:
|
||||
package-json-from-dist: 1.0.1
|
||||
path-scurry: 1.11.1
|
||||
|
||||
glob@13.0.6:
|
||||
dependencies:
|
||||
minimatch: 10.2.6
|
||||
minipass: 7.1.3
|
||||
path-scurry: 2.0.2
|
||||
|
||||
glob@7.2.3:
|
||||
dependencies:
|
||||
fs.realpath: 1.0.0
|
||||
@@ -9021,8 +8908,6 @@ snapshots:
|
||||
|
||||
lru-cache@10.4.3: {}
|
||||
|
||||
lru-cache@11.5.2: {}
|
||||
|
||||
lru-cache@5.1.1:
|
||||
dependencies:
|
||||
yallist: 3.1.1
|
||||
@@ -9257,10 +9142,6 @@ snapshots:
|
||||
|
||||
mimic-fn@2.1.0: {}
|
||||
|
||||
minimatch@10.2.6:
|
||||
dependencies:
|
||||
brace-expansion: 5.0.9
|
||||
|
||||
minimatch@3.1.5:
|
||||
dependencies:
|
||||
brace-expansion: 1.1.18
|
||||
@@ -9509,11 +9390,6 @@ snapshots:
|
||||
lru-cache: 10.4.3
|
||||
minipass: 7.1.3
|
||||
|
||||
path-scurry@2.0.2:
|
||||
dependencies:
|
||||
lru-cache: 11.5.2
|
||||
minipass: 7.1.3
|
||||
|
||||
path-type@4.0.0: {}
|
||||
|
||||
pathe@2.0.3: {}
|
||||
@@ -9768,19 +9644,20 @@ snapshots:
|
||||
react-native: 0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1)
|
||||
warn-once: 0.1.1
|
||||
|
||||
react-native-svg@15.15.5(react-native@0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1))(react@18.3.1):
|
||||
react-native-svg@15.8.0(react-native@0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1))(react@18.3.1):
|
||||
dependencies:
|
||||
css-select: 5.2.2
|
||||
css-tree: 1.1.3
|
||||
react: 18.3.1
|
||||
react-native: 0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1)
|
||||
warn-once: 0.1.1
|
||||
|
||||
react-native-url-polyfill@2.0.0(react-native@0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1)):
|
||||
dependencies:
|
||||
react-native: 0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1)
|
||||
whatwg-url-without-unicode: 8.0.0-3
|
||||
|
||||
react-native-view-shot@5.1.1(react-native@0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1))(react@18.3.1):
|
||||
react-native-view-shot@4.0.3(react-native@0.76.9(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1))(react@18.3.1):
|
||||
dependencies:
|
||||
html2canvas: 1.4.1
|
||||
react: 18.3.1
|
||||
|
||||
@@ -3,10 +3,17 @@ packages:
|
||||
- "packages/*"
|
||||
|
||||
allowBuilds:
|
||||
core-js: true
|
||||
esbuild: true
|
||||
sharp: true
|
||||
"@esbuild/darwin-arm64": true
|
||||
"@next/swc-darwin-arm64": true
|
||||
"@swc/core": true
|
||||
|
||||
onlyBuiltDependencies:
|
||||
- "core-js"
|
||||
- "@swc/core"
|
||||
- "esbuild"
|
||||
- "sharp"
|
||||
- "@esbuild/darwin-arm64"
|
||||
- "@next/swc-darwin-arm64"
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
-- QR Stand Templates table for dynamic server-driven template engine
|
||||
create table if not exists qr_stand_templates (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
key text not null unique,
|
||||
name text not null,
|
||||
category_name text,
|
||||
aspect_ratio text default '1/1',
|
||||
bg_type text default 'gradient',
|
||||
bg_image_url text,
|
||||
bg_gradient text,
|
||||
plaque_color text,
|
||||
border_accent text,
|
||||
accent_text text,
|
||||
header_text text,
|
||||
sub_text text,
|
||||
divider_color text,
|
||||
qr_config jsonb,
|
||||
layers jsonb,
|
||||
is_active boolean not null default true,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
-- Public read access for mobile and web
|
||||
alter table qr_stand_templates enable row level security;
|
||||
|
||||
create policy "Public read active templates"
|
||||
on qr_stand_templates for select
|
||||
using (is_active = true);
|
||||
|
||||
create policy "Admins can manage templates"
|
||||
on qr_stand_templates for all
|
||||
using (true);
|
||||
Reference in New Issue
Block a user