feat(qr): complete QR studio, dynamic google fonts, mouse canvas drag, and template sync

This commit is contained in:
AyrisAI
2026-08-22 16:00:30 +03:00
parent c1569c0879
commit e7ba6c7807
34 changed files with 5689 additions and 1251 deletions
+1
View File
@@ -1,3 +1,4 @@
auto-install-peers=true auto-install-peers=true
strict-peer-dependencies=false strict-peer-dependencies=false
enable-pre-post-scripts=true enable-pre-post-scripts=true
only-built-dependencies=core-js,@swc/core,esbuild,sharp,@esbuild/darwin-arm64,@next/swc-darwin-arm64
+203
View File
@@ -0,0 +1,203 @@
[
{
"key": "qr-1",
"name": "Lumière Restaurant (QR-1)",
"categoryName": "Özel Masa Kartı",
"svgTemplateUrl": "/qr-1.svg",
"aspectRatio": "1/1",
"bgType": "gradient",
"bgGradient": "radial-gradient(circle at 50% 50%, #132034 0%, #0b0d13 100%)",
"plaqueColor": "#132034",
"borderAccent": "#d4af37",
"accentText": "#d4af37",
"headerText": "#FFFFFF",
"subText": "#e5c378",
"dividerColor": "rgba(212, 175, 55, 0.3)",
"isActive": true,
"qrConfig": {
"xPercent": 50,
"yPercent": 38,
"size": 115,
"borderRadius": 14,
"borderColor": "#D4AF37",
"bgBoxColor": "#FFFFFF"
},
"layers": [
{
"id": "rName",
"text": "{{RESTAURANT_NAME}}",
"yPercent": 16,
"fontSize": 18,
"fontFamily": "Playfair Display, Georgia, serif",
"color": "#D4AF37",
"fontWeight": "800",
"letterSpacing": 2
},
{
"id": "sub",
"text": "RESTAURANT & BAR",
"yPercent": 26,
"fontSize": 10,
"fontFamily": "Plus Jakarta Sans, sans-serif",
"color": "#E5C378",
"fontWeight": "600",
"letterSpacing": 1.5
},
{
"id": "cta",
"text": "MENÜYÜ GÖRMEK İÇİN OKUTUN",
"yPercent": 80,
"fontSize": 11,
"fontFamily": "Playfair Display, Georgia, serif",
"color": "#FFFFFF",
"fontWeight": "700",
"letterSpacing": 1
},
{
"id": "table",
"text": "MASA NO: 01 • WIFI: Bistro",
"yPercent": 90,
"fontSize": 9,
"fontFamily": "Plus Jakarta Sans, sans-serif",
"color": "#A1A1AA",
"fontWeight": "600"
}
]
},
{
"key": "qr-2",
"name": "The Daily Grind Cafe (QR-2)",
"categoryName": "Özel Masa Kartı",
"svgTemplateUrl": "/qr-2.svg",
"aspectRatio": "400/650",
"bgType": "gradient",
"bgGradient": "radial-gradient(circle at 50% 50%, #252c36 0%, #171c22 100%)",
"plaqueColor": "#f6efe2",
"borderAccent": "#3a2312",
"accentText": "#8c6239",
"headerText": "#2c1a0e",
"subText": "#3a2312",
"dividerColor": "rgba(140, 98, 57, 0.3)",
"isActive": true,
"qrConfig": {
"xPercent": 50,
"yPercent": 36,
"size": 125,
"borderRadius": 14,
"borderColor": "#3a2312",
"bgBoxColor": "#FFFFFF"
},
"layers": [
{
"id": "rName",
"text": "{{RESTAURANT_NAME}}",
"yPercent": 14,
"fontSize": 18,
"fontFamily": "Playfair Display, Georgia, serif",
"color": "#8c6239",
"fontWeight": "800",
"letterSpacing": 2
},
{
"id": "cta",
"text": "MENÜYÜ GÖRMEK İÇİN OKUTUN",
"yPercent": 82,
"fontSize": 11,
"fontFamily": "Playfair Display, Georgia, serif",
"color": "#2c1a0e",
"fontWeight": "700",
"letterSpacing": 1
}
]
},
{
"key": "qr-3",
"name": "Pizzeria Bella Napoli (QR-3)",
"categoryName": "Özel Masa Kartı",
"svgTemplateUrl": "/qr-3.svg",
"aspectRatio": "400/650",
"bgType": "gradient",
"bgGradient": "radial-gradient(circle at 50% 50%, #faf4e6 0%, #f7eed8 100%)",
"plaqueColor": "#fdfbf5",
"borderAccent": "#b92b27",
"accentText": "#b92b27",
"headerText": "#18181b",
"subText": "#5d4037",
"dividerColor": "rgba(185, 43, 39, 0.3)",
"isActive": true,
"qrConfig": {
"xPercent": 50,
"yPercent": 36,
"size": 125,
"borderRadius": 14,
"borderColor": "#b92b27",
"bgBoxColor": "#FFFFFF"
},
"layers": [
{
"id": "rName",
"text": "{{RESTAURANT_NAME}}",
"yPercent": 14,
"fontSize": 18,
"fontFamily": "Playfair Display, Georgia, serif",
"color": "#b92b27",
"fontWeight": "800",
"letterSpacing": 2
},
{
"id": "cta",
"text": "MENÜYÜ GÖRMEK İÇİN OKUTUN",
"yPercent": 82,
"fontSize": 11,
"fontFamily": "Playfair Display, Georgia, serif",
"color": "#18181b",
"fontWeight": "700",
"letterSpacing": 1
}
]
},
{
"key": "qr-8",
"name": "Emerald & Gold Special (QR-8)",
"categoryName": "Özel Kare Masa Kartı",
"svgTemplateUrl": "/qr-8.svg",
"aspectRatio": "1/1",
"bgType": "gradient",
"bgGradient": "radial-gradient(circle at 50% 50%, #0c4443 0%, #062423 100%)",
"plaqueColor": "#f9f5eb",
"borderAccent": "#d1a752",
"accentText": "#d1a752",
"headerText": "#ffffff",
"subText": "#d1a752",
"dividerColor": "rgba(209, 167, 82, 0.4)",
"isActive": true,
"qrConfig": {
"xPercent": 50,
"yPercent": 38,
"size": 115,
"borderRadius": 14,
"borderColor": "#d1a752",
"bgBoxColor": "#FFFFFF"
},
"layers": [
{
"id": "rName",
"text": "{{RESTAURANT_NAME}}",
"yPercent": 16,
"fontSize": 18,
"fontFamily": "Playfair Display, Georgia, serif",
"color": "#d1a752",
"fontWeight": "800"
},
{
"id": "cta",
"text": "MENÜYÜ GÖRMEK İÇİN OKUTUN",
"yPercent": 80,
"fontSize": 11,
"fontFamily": "Playfair Display, Georgia, serif",
"color": "#ffffff",
"fontWeight": "700"
}
]
}
]
+2
View File
@@ -9,6 +9,7 @@ import { menuItemsRoutes } from "./routes/menu-items.js";
import { menusRoutes } from "./routes/menus.js"; import { menusRoutes } from "./routes/menus.js";
import { meRoutes } from "./routes/me.js"; import { meRoutes } from "./routes/me.js";
import { qrRoutes } from "./routes/qr.js"; import { qrRoutes } from "./routes/qr.js";
import { qrTemplatesRoutes } from "./routes/qr-templates.js";
import { restaurantsRoutes } from "./routes/restaurants.js"; import { restaurantsRoutes } from "./routes/restaurants.js";
import { subscriptionRoutes } from "./routes/subscription.js"; import { subscriptionRoutes } from "./routes/subscription.js";
@@ -42,6 +43,7 @@ await app.register(menusRoutes);
await app.register(menuCategoriesRoutes); await app.register(menuCategoriesRoutes);
await app.register(menuItemsRoutes); await app.register(menuItemsRoutes);
await app.register(qrRoutes); await app.register(qrRoutes);
await app.register(qrTemplatesRoutes);
await app.register(aiImportsRoutes); await app.register(aiImportsRoutes);
await app.register(domainsRoutes); await app.register(domainsRoutes);
await app.register(analyticsRoutes); await app.register(analyticsRoutes);
+156
View File
@@ -0,0 +1,156 @@
import type { FastifyInstance } from "fastify";
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { type QrStandPreset } from "@menulio/shared";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const DATA_FILE = path.resolve(__dirname, "../data/qr-templates.json");
export interface DynamicQrTemplate extends QrStandPreset {
id: string;
isActive: boolean;
bgType?: "gradient" | "image" | "svg";
bgImageUrl?: string;
createdAt: string;
updatedAt: string;
defaultQrConfig?: {
xPercent: number;
yPercent: number;
size: number;
borderRadius: number;
borderColor: string;
bgBoxColor: string;
};
defaultLayers?: Array<{
id: string;
text: string;
xPercent?: number;
yPercent: number;
fontSize: number;
fontFamily?: string;
color: string;
fontWeight?: string;
}>;
}
async function loadTemplatesFromDisk(): Promise<DynamicQrTemplate[]> {
try {
const content = await fs.readFile(DATA_FILE, "utf-8");
const parsed = JSON.parse(content);
return parsed.map((p: any) => ({
...p,
id: p.key,
isActive: p.isActive ?? true,
createdAt: p.createdAt || new Date().toISOString(),
updatedAt: p.updatedAt || new Date().toISOString(),
defaultQrConfig: p.defaultQrConfig || p.qrConfig,
defaultLayers: p.defaultLayers || p.layers,
}));
} catch {
return [];
}
}
async function saveTemplatesToDisk(templates: DynamicQrTemplate[]): Promise<void> {
try {
await fs.writeFile(DATA_FILE, JSON.stringify(templates, null, 2), "utf-8");
} catch (err) {
console.error("Failed to write qr-templates to disk:", err);
}
}
export async function qrTemplatesRoutes(app: FastifyInstance) {
// 1. Tüm Aktif Şablonları Listele (Mobil ve Web İçin)
app.get("/qr-templates", async () => {
const list = await loadTemplatesFromDisk();
return {
templates: list.filter((t) => t.isActive),
total: list.length,
};
});
// 2. Admin Şablon Listesi (Aktif + Taslak hepsi)
app.get("/admin/qr-templates", async () => {
const list = await loadTemplatesFromDisk();
return {
templates: list,
total: list.length,
};
});
// 3. Yeni Şablon Ekle veya Mevcut Olanı Güncelle
app.post<{ Body: Partial<DynamicQrTemplate> }>("/admin/qr-templates", async (req, reply) => {
const body = req.body;
if (!body.name) {
return reply.status(400).send({ error: "Şablon adı zorunludur." });
}
const key = body.key || `qr-${Date.now()}`;
const list = await loadTemplatesFromDisk();
const existingIndex = list.findIndex((t) => t.id === key || t.key === key);
const now = new Date().toISOString();
const newTemplate: DynamicQrTemplate = {
key,
id: key,
name: body.name,
categoryName: body.categoryName || "Özel Şablon",
svgTemplateUrl: body.svgTemplateUrl || `/templates/${key}.svg`,
aspectRatio: body.aspectRatio || "1/1",
bgType: body.bgType || "gradient",
bgImageUrl: body.bgImageUrl,
bgGradient: body.bgGradient || "radial-gradient(circle at 50% 50%, #132034 0%, #0b0d13 100%)",
plaqueColor: body.plaqueColor || "#132034",
borderAccent: body.borderAccent || "#D4AF37",
accentText: body.accentText || "#D4AF37",
headerText: body.headerText || "#FFFFFF",
subText: body.subText || "#E5C378",
dividerColor: body.dividerColor || "rgba(212, 175, 55, 0.3)",
isActive: body.isActive !== undefined ? body.isActive : true,
createdAt: existingIndex >= 0 ? list[existingIndex].createdAt : now,
updatedAt: now,
defaultQrConfig: body.defaultQrConfig || (body as any).qrConfig || {
xPercent: 50,
yPercent: 38,
size: 115,
borderRadius: 14,
borderColor: body.borderAccent || "#D4AF37",
bgBoxColor: "#FFFFFF",
},
defaultLayers: body.defaultLayers || (body as any).layers || [
{
id: "rName",
text: "{{RESTAURANT_NAME}}",
yPercent: 16,
fontSize: 18,
color: body.accentText || "#D4AF37",
fontWeight: "800",
},
],
};
if (existingIndex >= 0) {
list[existingIndex] = newTemplate;
} else {
list.unshift(newTemplate);
}
await saveTemplatesToDisk(list);
return { success: true, template: newTemplate };
});
// 4. Şablon Sil
app.delete<{ Params: { id: string } }>("/admin/qr-templates/:id", async (req, reply) => {
const { id } = req.params;
const list = await loadTemplatesFromDisk();
const filtered = list.filter((t) => t.id !== id && t.key !== id);
if (filtered.length === list.length) {
return reply.status(404).send({ error: "Şablon bulunamadı." });
}
await saveTemplatesToDisk(filtered);
return { success: true, deletedId: id };
});
}
+2 -1
View File
@@ -7,7 +7,8 @@
"orientation": "portrait", "orientation": "portrait",
"icon": "./assets/icon.png", "icon": "./assets/icon.png",
"userInterfaceStyle": "automatic", "userInterfaceStyle": "automatic",
"newArchEnabled": false, "newArchEnabled": true,
"platforms": ["ios", "android"],
"splash": { "splash": {
"image": "./assets/splash.png", "image": "./assets/splash.png",
"resizeMode": "contain", "resizeMode": "contain",
+7
View File
@@ -15,6 +15,13 @@
"distribution": "internal", "distribution": "internal",
"ios": { "ios": {
"image": "latest" "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": { "production": {
+4 -4
View File
@@ -25,9 +25,9 @@
"expo-haptics": "^14.0.1", "expo-haptics": "^14.0.1",
"expo-image-picker": "~16.0.6", "expo-image-picker": "~16.0.6",
"expo-linking": "~7.0.5", "expo-linking": "~7.0.5",
"expo-print": "^57.0.1", "expo-print": "~14.0.3",
"expo-router": "~4.0.0", "expo-router": "~4.0.0",
"expo-sharing": "^57.0.13", "expo-sharing": "~13.0.1",
"expo-splash-screen": "~0.29.24", "expo-splash-screen": "~0.29.24",
"expo-status-bar": "~2.0.0", "expo-status-bar": "~2.0.0",
"react": "18.3.1", "react": "18.3.1",
@@ -35,9 +35,9 @@
"react-native-purchases": "^8.2.0", "react-native-purchases": "^8.2.0",
"react-native-safe-area-context": "4.12.0", "react-native-safe-area-context": "4.12.0",
"react-native-screens": "~4.4.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-url-polyfill": "^2.0.0",
"react-native-view-shot": "^5.1.1" "react-native-view-shot": "~4.0.3"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.25.2", "@babel/core": "^7.25.2",
+1 -1
View File
@@ -4,7 +4,7 @@ import { StatusBar } from "expo-status-bar";
export default function RootLayout() { export default function RootLayout() {
return ( return (
<> <>
<StatusBar style="dark" /> <StatusBar style="light" />
<Stack screenOptions={{ headerShown: false }} /> <Stack screenOptions={{ headerShown: false }} />
</> </>
); );
+589 -160
View File
@@ -3,22 +3,22 @@ import { router } from "expo-router";
import { import {
ActivityIndicator, ActivityIndicator,
Alert, Alert,
Modal, Image,
Pressable, Pressable,
RefreshControl,
ScrollView, ScrollView,
Share, Share,
Switch,
Text, Text,
TextInput,
View, View,
} from "react-native"; } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import * as Haptics from "expo-haptics";
import * as Clipboard from "expo-clipboard"; import * as Clipboard from "expo-clipboard";
import ViewShot from "react-native-view-shot";
import * as Sharing from "expo-sharing"; import * as Sharing from "expo-sharing";
import * as Print from "expo-print"; import * as Print from "expo-print";
import { QR_STAND_PRESETS, type QrStandPreset } from "@menulio/shared"; import { QR_STAND_PRESETS, type QrStandPreset } from "@menulio/shared";
import { supabase } from "@/lib/supabase";
import { api } from "@/lib/api"; import { api } from "@/lib/api";
import { getActiveRestaurant, type ActiveRestaurant } from "@/lib/active-restaurant"; import { getActiveRestaurant, type ActiveRestaurant } from "@/lib/active-restaurant";
import { import {
@@ -29,7 +29,9 @@ import {
import { import {
QrCanvasStudio, QrCanvasStudio,
getDefaultLayersForTemplate, getDefaultLayersForTemplate,
getDefaultQrConfigForTemplate,
type CanvasTextLayer, type CanvasTextLayer,
type CanvasQrConfig,
} from "@/components/qr-canvas"; } from "@/components/qr-canvas";
const HIDE_ALL_TEMPLATE_FIELDS: QrCustomFields = { const HIDE_ALL_TEMPLATE_FIELDS: QrCustomFields = {
@@ -48,95 +50,290 @@ interface QrResponse {
pngBase64: string; 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() { export default function QrScreen() {
const insets = useSafeAreaInsets();
const [active, setActive] = useState<ActiveRestaurant | null>(null); const [active, setActive] = useState<ActiveRestaurant | null>(null);
const [restaurantName, setRestaurantName] = useState<string>("Gourmet Bistro"); const [restaurantName, setRestaurantName] = useState<string>("Gourmet Bistro");
const [qr, setQr] = useState<QrResponse | null>(null); const [qr, setQr] = useState<QrResponse | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const availablePresets = Object.values(QR_STAND_PRESETS).filter( const [scrollEnabled, setScrollEnabled] = useState(true);
(p) => !!COMPONENT_TEMPLATES[p.key]
// Dinamik Şablon Listesi (API'den ve yerelden)
const [presets, setPresets] = useState<QrStandPreset[]>(() =>
Object.values(QR_STAND_PRESETS)
); );
const [selectedKey, setSelectedKey] = useState<string>( const [selectedKey, setSelectedKey] = useState<string>(
availablePresets[0]?.key || "qr-1" presets[0]?.key || "qr-1"
); );
const [exportingPng, setExportingPng] = useState(false); const [exportingPng, setExportingPng] = useState(false);
const [exportingPdf, setExportingPdf] = 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[]>(() => const [layers, setLayers] = useState<CanvasTextLayer[]>(() =>
getDefaultLayersForTemplate("qr-1", "Gourmet Bistro") 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 // eslint-disable-next-line @typescript-eslint/no-explicit-any
const viewShotRef = useRef<any>(null); const viewShotRef = useRef<any>(null);
const activePreset: QrStandPreset = const [refreshing, setRefreshing] = useState(false);
QR_STAND_PRESETS[selectedKey] || availablePresets[0] || Object.values(QR_STAND_PRESETS)[0]!; const [syncing, setSyncing] = useState(false);
// Component-based template sistemi // Şablon ve QR Verilerini Çek / Senkronize Et
const TemplateComponent = const fetchTemplatesAndQr = async (isManualRefresh = false) => {
COMPONENT_TEMPLATES[selectedKey] ?? Object.values(COMPONENT_TEMPLATES)[0]; if (isManualRefresh) {
const aspectRatio = TEMPLATE_ASPECT_RATIOS[selectedKey] ?? 1; setRefreshing(true);
const displayWidth = 300; setSyncing(true);
const displayHeight = Math.round(displayWidth / aspectRatio); try { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); } catch {}
}
useEffect(() => {
(async () => {
const activeRest = await getActiveRestaurant();
if (!activeRest) return;
setActive(activeRest);
try {
// 1. Supabase ve API'den Dinamik Şablonları Çek
let fetchedPresets: QrStandPreset[] = [];
try { try {
const data = await api.post<QrResponse>(`/restaurants/${activeRest.restaurantId}/qr`); const { data: dbTemplates, error: dbErr } = await supabase
setQr(data); .from("qr_stand_templates")
.select("*")
.eq("is_active", true)
.order("created_at", { ascending: false });
// Fetch detailed restaurant name 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 { try {
const qrData = await api.post<QrResponse>(`/restaurants/${activeRest.restaurantId}/qr`);
setQr(qrData);
const restDetail = await api.get<{ name?: string }>(`/restaurants/${activeRest.restaurantId}`); const restDetail = await api.get<{ name?: string }>(`/restaurants/${activeRest.restaurantId}`);
if (restDetail?.name) { if (restDetail?.name) {
currentRestName = restDetail.name;
setRestaurantName(restDetail.name); setRestaurantName(restDetail.name);
setLayers((prev) =>
prev.map((l) =>
l.id === "rName" ? { ...l, text: restDetail.name!.toUpperCase() } : l
)
);
} }
} catch { } catch {}
// fallback
}
} catch (err) {
setError(err instanceof Error ? err.message : "QR oluşturulamadı");
} finally {
setLoading(false);
} }
})();
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 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() { async function handleCopy() {
if (!qr?.targetUrl) return; if (!qr?.targetUrl) return;
await Clipboard.setStringAsync(qr.targetUrl); try {
await Clipboard.setStringAsync(qr.targetUrl);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
} catch {}
setCopied(true); setCopied(true);
setTimeout(() => setCopied(false), 2000); setTimeout(() => setCopied(false), 2000);
} }
async function handleShareQuick() { async function handleShareQuick() {
if (!qr?.targetUrl) return; if (!qr?.targetUrl) return;
await Share.share({ try {
title: "Dijital QR Menü", Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
message: `${qr.targetUrl}\nMenümüzü incelemek için QR kodu taratın veya linke tıklayın.`, await Share.share({
url: qr.targetUrl, 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() { async function handleExportPng() {
if (!viewShotRef.current) return; if (!viewShotRef.current) return;
setSelectedLayerId(null);
setIsQrSelected(false);
setExportingPng(true); setExportingPng(true);
try { try {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
await new Promise((r) => setTimeout(r, 120));
const uri = await viewShotRef.current.capture?.(); const uri = await viewShotRef.current.capture?.();
if (uri) { if (uri) {
await Sharing.shareAsync(uri, { await Sharing.shareAsync(uri, {
@@ -152,10 +349,13 @@ export default function QrScreen() {
} }
async function handleExportPdf() { async function handleExportPdf() {
if (!qr || !active) return; if (!qr || !active || !viewShotRef.current) return;
setSelectedLayerId(null);
setIsQrSelected(false);
setExportingPdf(true); setExportingPdf(true);
try { 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(); const pngUri = await viewShotRef.current?.capture();
if (!pngUri) throw new Error("Görsel alınamadı"); if (!pngUri) throw new Error("Görsel alınamadı");
@@ -212,26 +412,30 @@ export default function QrScreen() {
} }
} }
if (loading) { if (loading) {
return ( return (
<View style={{ flex: 1, backgroundColor: "#FAF8F5", alignItems: "center", justifyContent: "center" }}> <View style={{ flex: 1, backgroundColor: "#0C0C0E", alignItems: "center", justifyContent: "center" }}>
<ActivityIndicator size="large" color="#C8A96B" /> <ActivityIndicator size="large" color="#D4AF37" />
<Text style={{ marginTop: 12, color: "#78716C", fontSize: 14 }}>QR Kod ve Şablonlar Hazırlanıyor...</Text> <Text style={{ marginTop: 14, color: "#A1A1AA", fontSize: 14, fontWeight: "600" }}>
QR Stant Stüdyosu Hazırlanıyor...
</Text>
</View> </View>
); );
} }
if (error || !qr) { if (error || !qr) {
return ( return (
<View style={{ flex: 1, backgroundColor: "#FAF8F5", alignItems: "center", justifyContent: "center", padding: 24 }}> <View style={{ flex: 1, backgroundColor: "#0C0C0E", alignItems: "center", justifyContent: "center", padding: 24 }}>
<Ionicons name="alert-circle-outline" size={48} color="#DC2626" style={{ marginBottom: 12 }} /> <Ionicons name="alert-circle-outline" size={48} color="#EF4444" style={{ marginBottom: 12 }} />
<Text style={{ color: "#DC2626", fontSize: 16, fontWeight: "600", textAlign: "center" }}> <Text style={{ color: "#EF4444", fontSize: 16, fontWeight: "600", textAlign: "center" }}>
{error ?? "QR kod yüklenemedi"} {error ?? "QR kod yüklenemedi"}
</Text> </Text>
<Pressable <Pressable
onPress={() => router.back()} 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> <Text style={{ color: "#FFF", fontWeight: "600" }}>Geri Dön</Text>
</Pressable> </Pressable>
@@ -240,79 +444,221 @@ export default function QrScreen() {
} }
return ( return (
<ScrollView contentContainerStyle={{ flexGrow: 1, backgroundColor: "#0C0C0E", paddingVertical: 20 }}> <ScrollView
<View style={{ width: "100%", maxWidth: 440, alignSelf: "center", paddingHorizontal: 20 }}> scrollEnabled={scrollEnabled}
{/* Header Badge */} 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 <View
style={{ style={{
alignSelf: "center",
backgroundColor: "rgba(212, 175, 55, 0.15)",
paddingHorizontal: 14,
paddingVertical: 6,
borderRadius: 99,
marginBottom: 12,
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
gap: 6, justifyContent: "space-between",
borderWidth: 1, marginBottom: 12,
borderColor: "rgba(212, 175, 55, 0.3)",
}} }}
> >
<Ionicons name="qr-code-outline" size={15} color="#D4AF37" /> {/* Geri Dön Butonu */}
<Text style={{ color: "#D4AF37", fontSize: 12, fontWeight: "700" }}> <Pressable
MASA QR STANT JENERATÖRÜ onPress={() => {
</Text> 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,
flexDirection: "row",
alignItems: "center",
gap: 6,
borderWidth: 1,
borderColor: "rgba(212, 175, 55, 0.25)",
}}
>
<Ionicons name="sparkles" size={13} color="#D4AF37" />
<Text style={{ color: "#D4AF37", fontSize: 11, fontWeight: "800", letterSpacing: 0.5 }}>
INSTAGRAM QR STÜDYOSU
</Text>
</View>
{/* 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> </View>
<Text style={{ fontSize: 24, fontWeight: "800", color: "#FFFFFF", marginBottom: 4, textAlign: "center" }}> {/* Yatay Şablon Seçici Bar */}
Masa Stant Tasarımları {presets.length > 0 && (
</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>
{/* Horizontal Theme Selector */}
{availablePresets.length > 1 && (
<ScrollView <ScrollView
horizontal horizontal
showsHorizontalScrollIndicator={false} 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 isSelected = preset.key === selectedKey;
const pRatio = getPresetAspectRatio(preset);
const calcHeight = Math.round(displayWidth / (pRatio || 1));
return ( return (
<Pressable <Pressable
key={preset.key} key={preset.key}
onPress={() => { onPress={() => {
setSelectedKey(preset.key); try { Haptics.selectionAsync(); } catch {}
setLayers(getDefaultLayersForTemplate(preset.key, restaurantName));
// 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={{ hitSlop={{ top: 6, bottom: 6, left: 4, right: 4 }}
paddingHorizontal: 16, accessible={true}
paddingVertical: 10, accessibilityRole="button"
borderRadius: 14, accessibilityLabel={`${preset.name} şablonunu seç`}
backgroundColor: isSelected ? "#D4AF37" : "#1A1A1E", accessibilityState={{ selected: isSelected }}
style={({ pressed }) => ({
paddingHorizontal: 12,
paddingVertical: 8,
minHeight: 36,
borderRadius: 12,
backgroundColor: isSelected ? "#D4AF37" : "#18181B",
borderWidth: 1.5, borderWidth: 1.5,
borderColor: isSelected ? "#D4AF37" : "#2B2B32", borderColor: isSelected ? "#D4AF37" : "rgba(255,255,255,0.12)",
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
gap: 8, gap: 6,
}} opacity: pressed ? 0.85 : 1,
})}
> >
<View <View
style={{ style={{
width: 14, width: 10,
height: 14, height: 10,
borderRadius: 7, borderRadius: 5,
backgroundColor: preset.plaqueColor || "#1A1A1E", backgroundColor: preset.plaqueColor || "#18181B",
borderWidth: 1, borderWidth: 1,
borderColor: "rgba(255,255,255,0.4)", borderColor: isSelected ? "#0C0C0E" : "rgba(255,255,255,0.5)",
}} }}
/> />
<Text <Text
style={{ style={{
fontSize: 13, fontSize: 12,
fontWeight: "700", fontWeight: "700",
color: isSelected ? "#0C0C0E" : "#FFFFFF", color: isSelected ? "#0C0C0E" : "#FFFFFF",
}} }}
@@ -325,53 +671,123 @@ export default function QrScreen() {
</ScrollView> </ScrollView>
)} )}
{/* İnteraktif Instagram / Canva Tarzı QR Stant Stüdyosu */} {/* İnteraktif Instagram QR & Canva Stüdyosu */}
<View <QrCanvasStudio
style={{ templateKey={selectedKey}
alignItems: "center", restaurantName={restaurantName}
justifyContent: "center", qrBase64={qr.pngBase64}
width: "100%", width={displayWidth}
marginBottom: 20, height={displayHeight}
}} renderBackground={() => {
> const cur = presets.find((p) => p.key === selectedKey);
<ViewShot ref={viewShotRef} options={{ format: "png", quality: 1.0 }}> const bgImg = (cur as any)?.bgImageUrl;
<QrCanvasStudio
templateKey={selectedKey} // 1. Eğer şablona özel bir Arka Plan Görseli / SVG yüklenmişse bunu render et
restaurantName={restaurantName} if (bgImg) {
qrBase64={qr.pngBase64} return (
width={displayWidth} <View
height={displayHeight} style={{
renderBackground={() => width: displayWidth,
TemplateComponent ? ( height: displayHeight,
<TemplateComponent backgroundColor: cur?.plaqueColor || "#132034",
qrBase64={qr.pngBase64} borderRadius: 24,
width={displayWidth} overflow: "hidden",
height={displayHeight} borderWidth: 2,
fields={HIDE_ALL_TEMPLATE_FIELDS} borderColor: cur?.borderAccent || "#D4AF37",
}}
>
<Image
source={{ uri: bgImg }}
style={{ width: "100%", height: "100%", position: "absolute" }}
resizeMode="cover"
/> />
) : null {/* İç Dekoratif Çerçeve */}
} <View
layers={layers} style={{
onLayersChange={setLayers} position: "absolute",
/> top: 10,
</ViewShot> left: 10,
</View> 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}
/>
);
}
// 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,
}}
/>
</View>
);
}}
layers={layers}
onLayersChange={handleLayersChange}
qrConfig={qrConfig}
onQrConfigChange={handleQrConfigChange}
viewShotRef={viewShotRef}
selectedLayerId={selectedLayerId}
onSelectLayer={setSelectedLayerId}
isQrSelected={isQrSelected}
onSelectQr={setIsQrSelected}
onDragStateChange={(dragging) => setScrollEnabled(!dragging)}
/>
{/* Aksiyon Butonları Grubu */}
<View style={{ width: "100%", gap: 10, marginTop: 4 }}>
{/* Output Action Buttons */} {/* PDF İndirme Butonu */}
<View style={{ width: "100%", gap: 10 }}>
{/* PDF Download Button */}
<Pressable <Pressable
onPress={handleExportPdf} onPress={handleExportPdf}
disabled={exportingPdf} disabled={exportingPdf}
accessible={true}
accessibilityRole="button"
accessibilityLabel="PDF Olarak İndir (A5 Matbaa Baskı)"
style={({ pressed }) => ({ style={({ pressed }) => ({
backgroundColor: "#D4AF37", backgroundColor: "#D4AF37",
borderRadius: 14, borderRadius: 14,
padding: 16, minHeight: 52,
padding: 15,
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
@@ -379,9 +795,9 @@ export default function QrScreen() {
opacity: pressed || exportingPdf ? 0.85 : 1, opacity: pressed || exportingPdf ? 0.85 : 1,
shadowColor: "#D4AF37", shadowColor: "#D4AF37",
shadowOffset: { width: 0, height: 4 }, shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.2, shadowOpacity: 0.25,
shadowRadius: 8, shadowRadius: 8,
elevation: 3, elevation: 4,
})} })}
> >
{exportingPdf ? ( {exportingPdf ? (
@@ -396,16 +812,20 @@ export default function QrScreen() {
)} )}
</Pressable> </Pressable>
{/* PNG Download Button */} {/* PNG İndirme Butonu */}
<Pressable <Pressable
onPress={handleExportPng} onPress={handleExportPng}
disabled={exportingPng} disabled={exportingPng}
accessible={true}
accessibilityRole="button"
accessibilityLabel="PNG Olarak Kaydet veya Paylaş"
style={({ pressed }) => ({ style={({ pressed }) => ({
backgroundColor: "#1A1A1E", backgroundColor: "#18181B",
borderWidth: 1.5, borderWidth: 1.5,
borderColor: "#2B2B32", borderColor: "rgba(255,255,255,0.12)",
borderRadius: 14, borderRadius: 14,
padding: 16, minHeight: 48,
padding: 14,
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
@@ -417,23 +837,27 @@ export default function QrScreen() {
<ActivityIndicator size="small" color="#FFFFFF" /> <ActivityIndicator size="small" color="#FFFFFF" />
) : ( ) : (
<> <>
<Ionicons name="image-outline" size={20} color="#FFFFFF" /> <Ionicons name="image-outline" size={19} color="#FFFFFF" />
<Text style={{ color: "#FFFFFF", fontSize: 15, fontWeight: "700" }}> <Text style={{ color: "#FFFFFF", fontSize: 14, fontWeight: "700" }}>
PNG Olarak Kaydet / Paylaş PNG Olarak Kaydet / Paylaş
</Text> </Text>
</> </>
)} )}
</Pressable> </Pressable>
{/* Direct Link Share Button */} {/* Menü Linki Paylaş */}
<Pressable <Pressable
onPress={handleShareQuick} onPress={handleShareQuick}
accessible={true}
accessibilityRole="button"
accessibilityLabel="Menü Linkini Paylaş"
style={({ pressed }) => ({ style={({ pressed }) => ({
backgroundColor: "#1A1A1E", backgroundColor: "#18181B",
borderWidth: 1, borderWidth: 1,
borderColor: "#2B2B32", borderColor: "rgba(255,255,255,0.08)",
borderRadius: 14, borderRadius: 14,
padding: 14, minHeight: 44,
padding: 13,
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
@@ -441,28 +865,33 @@ export default function QrScreen() {
opacity: pressed ? 0.85 : 1, opacity: pressed ? 0.85 : 1,
})} })}
> >
<Ionicons name="share-outline" size={18} color="#D4AF37" /> <Ionicons name="share-outline" size={17} color="#D4AF37" />
<Text style={{ color: "#D4AF37", fontSize: 14, fontWeight: "700" }}> <Text style={{ color: "#D4AF37", fontSize: 13.5, fontWeight: "700" }}>
Menü Linkini Paylaş Menü Linkini Paylaş
</Text> </Text>
</Pressable> </Pressable>
{/* Quick Copy Link Box */} {/* Hızlı Kopyalama Kutusu */}
<Pressable <Pressable
onPress={handleCopy} onPress={handleCopy}
style={{ accessible={true}
backgroundColor: "#141417", accessibilityRole="button"
accessibilityLabel={`Menü linkini kopyala: ${qr.targetUrl}`}
style={({ pressed }) => ({
backgroundColor: "#121215",
borderWidth: 1, borderWidth: 1,
borderColor: "#242429", borderColor: "rgba(255,255,255,0.08)",
borderRadius: 14, borderRadius: 12,
padding: 14, padding: 12,
minHeight: 44,
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
justifyContent: "space-between", 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} {qr.targetUrl}
</Text> </Text>
<Ionicons <Ionicons
@@ -6,14 +6,19 @@ import {
View, View,
} from "react-native"; } from "react-native";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import * as Haptics from "expo-haptics";
import { type CanvasTextLayer, getCanvasFontFamily } from "./types"; import { type CanvasTextLayer, getCanvasFontFamily } from "./types";
interface CanvasLayerItemProps { interface CanvasLayerItemProps {
layer: CanvasTextLayer; layer: CanvasTextLayer;
isSelected: boolean; isSelected: boolean;
onSelect: (id: string) => void; onSelect: (id: string) => void;
onEdit: (layer: CanvasTextLayer) => void;
onUpdatePosition: (id: string, x: number, y: number) => void; onUpdatePosition: (id: string, x: number, y: number) => void;
onDelete: (id: string) => 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; canvasWidth: number;
canvasHeight: number; canvasHeight: number;
} }
@@ -22,95 +27,213 @@ export const CanvasLayerItem: React.FC<CanvasLayerItemProps> = ({
layer, layer,
isSelected, isSelected,
onSelect, onSelect,
onEdit,
onUpdatePosition, onUpdatePosition,
onDelete, onDelete,
onDragStart,
onDragMove,
onDragEnd,
canvasWidth, canvasWidth,
canvasHeight, 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 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( const panResponder = useRef(
PanResponder.create({ PanResponder.create({
onStartShouldSetPanResponder: () => true, 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: () => { onPanResponderGrant: () => {
onSelect(layer.id); const now = Date.now();
panStartRef.current = { x: layer.x, y: layer.y }; 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) => { onPanResponderMove: (_evt, gestureState) => {
const newX = Math.max(0, Math.min(canvasWidth - 40, panStartRef.current.x + gestureState.dx)); if (!isDraggingRef.current && (Math.abs(gestureState.dx) > 2 || Math.abs(gestureState.dy) > 2)) {
const newY = Math.max(0, Math.min(canvasHeight - 30, panStartRef.current.y + gestureState.dy)); isDraggingRef.current = true;
onUpdatePosition(layer.id, Math.round(newX), Math.round(newY)); }
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; ).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 ( return (
<View <View
{...panResponder.panHandlers} {...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={{ style={{
position: "absolute", position: "absolute",
left: layer.x, left: layer.x,
top: layer.y, top: layer.y,
transform: [{ rotate: `${layer.rotation || 0}deg` }], transform: [{ rotate: `${layer.rotation || 0}deg` }],
zIndex: isSelected ? 100 : 10, zIndex: isSelected ? 90 : 20,
padding: 4,
}} }}
> >
<View <View
style={{ style={{
borderWidth: isSelected ? 1.5 : 0, borderWidth: isSelected ? 1.5 : 0,
borderColor: "#D4AF37", borderColor: isSelected ? "#D4AF37" : "transparent",
borderStyle: "dashed", borderStyle: isSelected ? "dashed" : "solid",
borderRadius: 8, borderRadius: 10,
paddingHorizontal: layer.bgBoxColor ? 10 : 4, padding: 2,
paddingVertical: layer.bgBoxColor ? 5 : 2,
backgroundColor: layer.bgBoxColor || "transparent",
alignItems: "center",
justifyContent: "center",
}} }}
> >
<Text <View style={renderBackgroundStyle()}>
style={{ <Text
fontFamily, style={{
fontSize: layer.fontSize, fontSize: layer.fontSize,
color: layer.color, color: layer.color,
fontWeight: layer.fontWeight || "bold", fontFamily: getCanvasFontFamily(layer.fontId),
letterSpacing: layer.letterSpacing || 0.5, fontWeight: layer.fontWeight || "bold",
textAlign: "center", textAlign: layer.textAlign || "center",
textShadowColor: layer.hasShadow ? "rgba(0,0,0,0.85)" : "transparent", letterSpacing: layer.letterSpacing ?? 0.5,
textShadowOffset: layer.hasShadow ? { width: 1, height: 1 } : { width: 0, height: 0 }, textShadowColor: layer.hasShadow ? "rgba(0,0,0,0.8)" : "transparent",
textShadowRadius: layer.hasShadow ? 4 : 0, textShadowOffset: { width: 0, height: 1 },
}} textShadowRadius: 3,
> }}
{layer.text || "Metin"} >
</Text> {layer.text}
</Text>
</View>
{/* Seçili İken Hızlı Silme Rozeti */} {/* Seçili İken Üst Köşedeki Küçük Silme Rozeti */}
{isSelected && ( {isSelected && (
<Pressable <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={{ style={{
position: "absolute", position: "absolute",
top: -12, top: -8,
right: -12, right: -8,
backgroundColor: "#EF4444", backgroundColor: "#EF4444",
width: 22, width: 18,
height: 22, height: 18,
borderRadius: 11, borderRadius: 9,
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
borderWidth: 1.5, borderWidth: 1.5,
borderColor: "#FFFFFF", borderColor: "#0C0C0E",
shadowColor: "#000",
shadowOpacity: 0.3,
shadowRadius: 3,
elevation: 4,
}} }}
> >
<Ionicons name="close" size={12} color="#FFFFFF" /> <Ionicons name="close" size={11} color="#FFFFFF" />
</Pressable> </Pressable>
)} )}
</View> </View>
@@ -0,0 +1,176 @@
import React, { useRef } from "react";
import {
Image,
PanResponder,
View,
} from "react-native";
import { Ionicons } from "@expo/vector-icons";
import { type CanvasQrConfig } from "./types";
interface CanvasQrItemProps {
qrConfig: CanvasQrConfig;
qrBase64?: string;
isSelected: boolean;
onSelect: () => void;
onUpdatePosition: (x: number, y: number) => void;
onDragStart?: () => void;
onDragMove?: (rawX: number, rawY: number, itemWidth: number, itemHeight: number) => { snappedX: number; snappedY: number };
onDragEnd?: (finalX: number, finalY: number, itemWidth: number, itemHeight: number) => void;
canvasWidth: number;
canvasHeight: number;
}
export const CanvasQrItem: React.FC<CanvasQrItemProps> = ({
qrConfig,
qrBase64,
isSelected,
onSelect,
onUpdatePosition,
onDragStart,
onDragMove,
onDragEnd,
canvasWidth,
canvasHeight,
}) => {
// Stale closure önlemi için ref'ler
const qrConfigRef = useRef(qrConfig);
qrConfigRef.current = qrConfig;
const canvasWidthRef = useRef(canvasWidth);
canvasWidthRef.current = canvasWidth;
const canvasHeightRef = useRef(canvasHeight);
canvasHeightRef.current = canvasHeight;
const panStartRef = useRef({ x: qrConfig.x, y: qrConfig.y });
const currentPosRef = useRef({ x: qrConfig.x, y: qrConfig.y });
const panResponder = useRef(
PanResponder.create({
onStartShouldSetPanResponder: () => true,
onStartShouldSetPanResponderCapture: () => true,
onMoveShouldSetPanResponder: (_evt, gestureState) =>
Math.abs(gestureState.dx) > 1 || Math.abs(gestureState.dy) > 1,
onMoveShouldSetPanResponderCapture: (_evt, gestureState) =>
Math.abs(gestureState.dx) > 1 || Math.abs(gestureState.dy) > 1,
onPanResponderTerminationRequest: () => false,
onPanResponderGrant: () => {
onSelect();
const cur = qrConfigRef.current;
panStartRef.current = { x: cur.x, y: cur.y };
currentPosRef.current = { x: cur.x, y: cur.y };
onDragStart?.();
},
onPanResponderMove: (_evt, gestureState) => {
const cur = qrConfigRef.current;
const cWidth = canvasWidthRef.current;
const cHeight = canvasHeightRef.current;
const maxX = Math.max(0, cWidth - cur.size);
const maxY = Math.max(0, cHeight - cur.size);
const rawX = Math.max(0, Math.min(maxX, panStartRef.current.x + gestureState.dx));
const rawY = Math.max(0, Math.min(maxY, panStartRef.current.y + gestureState.dy));
if (onDragMove) {
const { snappedX, snappedY } = onDragMove(rawX, rawY, cur.size, cur.size);
currentPosRef.current = { x: snappedX, y: snappedY };
onUpdatePosition(snappedX, snappedY);
} else {
const sx = Math.round(rawX);
const sy = Math.round(rawY);
currentPosRef.current = { x: sx, y: sy };
onUpdatePosition(sx, sy);
}
},
onPanResponderRelease: () => {
const cur = qrConfigRef.current;
onDragEnd?.(currentPosRef.current.x, currentPosRef.current.y, cur.size, cur.size);
},
onPanResponderTerminate: () => {
const cur = qrConfigRef.current;
onDragEnd?.(currentPosRef.current.x, currentPosRef.current.y, cur.size, cur.size);
},
})
).current;
return (
<View
{...panResponder.panHandlers}
accessible={true}
accessibilityRole="button"
accessibilityLabel="QR Kod Alanı"
accessibilityHint="QR kodunu seçmek, boyutunu ayarlamak veya stant üzerinde taşımak için dokunun"
accessibilityState={{ selected: isSelected }}
style={{
position: "absolute",
left: qrConfig.x,
top: qrConfig.y,
width: qrConfig.size,
height: qrConfig.size,
zIndex: isSelected ? 80 : 15,
}}
>
<View
style={{
width: "100%",
height: "100%",
backgroundColor: qrConfig.bgBoxColor || "#FFFFFF",
borderRadius: qrConfig.borderRadius,
borderWidth: isSelected ? 2 : qrConfig.borderWidth,
borderColor: isSelected ? "#D4AF37" : qrConfig.borderColor,
borderStyle: isSelected ? "dashed" : "solid",
padding: qrConfig.padding,
alignItems: "center",
justifyContent: "center",
shadowColor: "#000",
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.25,
shadowRadius: 6,
elevation: 4,
}}
>
{qrBase64 ? (
<Image
source={{ uri: `data:image/png;base64,${qrBase64}` }}
style={{
width: "100%",
height: "100%",
borderRadius: Math.max(0, qrConfig.borderRadius - 4),
}}
resizeMode="contain"
/>
) : (
<View style={{ alignItems: "center", justifyContent: "center", width: "100%", height: "100%" }}>
<Ionicons name="qr-code" size={Math.round(qrConfig.size * 0.7)} color="#0C0C0E" />
</View>
)}
{/* Seçili İken Taşıma Rozeti */}
{isSelected && (
<View
style={{
position: "absolute",
top: -8,
right: -8,
backgroundColor: "#D4AF37",
width: 22,
height: 22,
borderRadius: 11,
alignItems: "center",
justifyContent: "center",
borderWidth: 1.5,
borderColor: "#0C0C0E",
}}
>
<Ionicons name="move" size={12} color="#0C0C0E" />
</View>
)}
</View>
</View>
);
};
@@ -0,0 +1,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 "./types";
export * from "./CanvasLayerItem"; export * from "./CanvasLayerItem";
export * from "./CanvasQrItem";
export * from "./InstagramTextEditorModal";
export * from "./QrCanvasStudio"; export * from "./QrCanvasStudio";
+104 -12
View File
@@ -7,38 +7,61 @@ export interface CanvasTextLayer {
fontSize: number; fontSize: number;
fontId: string; fontId: string;
color: 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; hasShadow?: boolean;
letterSpacing?: number; letterSpacing?: number;
fontWeight?: "normal" | "600" | "700" | "800" | "900"; 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 = [ export const CANVAS_FONT_OPTIONS = [
{ {
id: "sans", id: "sans",
label: "Modern Sans", label: "MODERN",
fontFamily: "System, -apple-system, Roboto, 'Helvetica Neue', Arial, sans-serif", fontFamily: "System, -apple-system, Roboto, 'Helvetica Neue', Arial, sans-serif",
}, },
{ {
id: "serif", id: "serif",
label: "Zarif Serif", label: "ZARİF",
fontFamily: "Georgia, 'Times New Roman', serif", fontFamily: "Georgia, 'Times New Roman', serif",
}, },
{ {
id: "times", id: "times",
label: "Klasik Times", label: "KLASİK",
fontFamily: "'Times New Roman', Times, Georgia, serif", fontFamily: "'Times New Roman', Times, Georgia, serif",
}, },
{
id: "typewriter",
label: "DAKTİLO",
fontFamily: "Courier, 'Courier New', monospace",
},
{ {
id: "palatino", id: "palatino",
label: "Lüks Palatino", label: "LÜKS",
fontFamily: "Palatino, 'Palatino Linotype', Georgia, serif", fontFamily: "Palatino, 'Palatino Linotype', Georgia, serif",
}, },
{ {
id: "trebuchet", id: "trebuchet",
label: "Dinamik", label: "DİNAMİK",
fontFamily: "'Trebuchet MS', 'Segoe UI', Arial, sans-serif", fontFamily: "'Trebuchet MS', 'Segoe UI', Arial, sans-serif",
}, },
{
id: "heavy",
label: "GÜÇLÜ",
fontFamily: "Impact, 'Arial Black', sans-serif",
},
]; ];
export const CANVAS_COLOR_PALETTE = [ export const CANVAS_COLOR_PALETTE = [
@@ -46,16 +69,41 @@ export const CANVAS_COLOR_PALETTE = [
{ id: "white", color: "#FFFFFF", label: "Beyaz" }, { id: "white", color: "#FFFFFF", label: "Beyaz" },
{ id: "black", color: "#121215", label: "Siyah" }, { id: "black", color: "#121215", label: "Siyah" },
{ id: "cream", color: "#F6EFE3", label: "Krem" }, { id: "cream", color: "#F6EFE3", label: "Krem" },
{ id: "emerald", color: "#0C4443", label: "Zümrüt" }, { id: "emerald", color: "#10B981", label: "Yeşil" },
{ id: "crimson", color: "#A64423", label: "Kızıl" }, { id: "crimson", color: "#EF4444", label: "Kırmızı" },
{ id: "blue", color: "#003554", label: "Lacivert" }, { 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 = [ export const INSTAGRAM_BADGE_STYLES = [
{ id: "none", label: "Düz", bgBoxColor: undefined }, { id: "none", label: "Düz", bgBoxColor: undefined },
{ id: "dark", label: "Siyah Kutu", bgBoxColor: "rgba(18, 18, 21, 0.88)" }, { id: "dark", label: "Koyu", bgBoxColor: "rgba(18, 18, 21, 0.88)" },
{ id: "gold", label: "Altın Kutu", bgBoxColor: "rgba(212, 175, 55, 0.92)" }, { id: "gold", label: "Altın", bgBoxColor: "rgba(212, 175, 55, 0.92)" },
{ id: "white", label: "Beyaz Kutu", bgBoxColor: "rgba(255, 255, 255, 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 { 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"; 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) // 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[] { export function getDefaultLayersForTemplate(templateKey: string, restaurantName = "Gourmet Bistro"): CanvasTextLayer[] {
if (templateKey === "qr-1") { if (templateKey === "qr-1") {
@@ -78,6 +159,7 @@ export function getDefaultLayersForTemplate(templateKey: string, restaurantName
color: "#D4AF37", color: "#D4AF37",
fontWeight: "800", fontWeight: "800",
letterSpacing: 2, letterSpacing: 2,
textAlign: "center",
}, },
{ {
id: "sub", id: "sub",
@@ -90,6 +172,7 @@ export function getDefaultLayersForTemplate(templateKey: string, restaurantName
color: "#E5C378", color: "#E5C378",
fontWeight: "600", fontWeight: "600",
letterSpacing: 1.5, letterSpacing: 1.5,
textAlign: "center",
}, },
{ {
id: "cta", id: "cta",
@@ -102,6 +185,7 @@ export function getDefaultLayersForTemplate(templateKey: string, restaurantName
color: "#D4AF37", color: "#D4AF37",
fontWeight: "700", fontWeight: "700",
letterSpacing: 1, letterSpacing: 1,
textAlign: "center",
}, },
{ {
id: "table", id: "table",
@@ -113,6 +197,7 @@ export function getDefaultLayersForTemplate(templateKey: string, restaurantName
fontId: "sans", fontId: "sans",
color: "#C5A059", color: "#C5A059",
fontWeight: "600", fontWeight: "600",
textAlign: "center",
}, },
]; ];
} }
@@ -130,6 +215,7 @@ export function getDefaultLayersForTemplate(templateKey: string, restaurantName
color: "#FFFFFF", color: "#FFFFFF",
fontWeight: "900", fontWeight: "900",
letterSpacing: 1, letterSpacing: 1,
textAlign: "center",
}, },
{ {
id: "sub", id: "sub",
@@ -141,6 +227,7 @@ export function getDefaultLayersForTemplate(templateKey: string, restaurantName
fontId: "sans", fontId: "sans",
color: "#D1A752", color: "#D1A752",
fontWeight: "800", fontWeight: "800",
textAlign: "center",
}, },
{ {
id: "cta", id: "cta",
@@ -152,6 +239,7 @@ export function getDefaultLayersForTemplate(templateKey: string, restaurantName
fontId: "sans", fontId: "sans",
color: "#FFFFFF", color: "#FFFFFF",
fontWeight: "800", fontWeight: "800",
textAlign: "center",
}, },
{ {
id: "wifi", id: "wifi",
@@ -163,6 +251,7 @@ export function getDefaultLayersForTemplate(templateKey: string, restaurantName
fontId: "sans", fontId: "sans",
color: "#D1A752", color: "#D1A752",
fontWeight: "800", fontWeight: "800",
textAlign: "center",
}, },
]; ];
} }
@@ -179,6 +268,7 @@ export function getDefaultLayersForTemplate(templateKey: string, restaurantName
fontId: "serif", fontId: "serif",
color: "#D4AF37", color: "#D4AF37",
fontWeight: "800", fontWeight: "800",
textAlign: "center",
}, },
{ {
id: "sub", id: "sub",
@@ -190,6 +280,7 @@ export function getDefaultLayersForTemplate(templateKey: string, restaurantName
fontId: "sans", fontId: "sans",
color: "#FFFFFF", color: "#FFFFFF",
fontWeight: "700", fontWeight: "700",
textAlign: "center",
}, },
{ {
id: "cta", id: "cta",
@@ -201,6 +292,7 @@ export function getDefaultLayersForTemplate(templateKey: string, restaurantName
fontId: "serif", fontId: "serif",
color: "#D4AF37", color: "#D4AF37",
fontWeight: "800", fontWeight: "800",
textAlign: "center",
}, },
]; ];
} }
@@ -8,11 +8,12 @@ import Svg, {
Text, Text,
Image, Image,
} from "react-native-svg"; } from "react-native-svg";
import { type QrTemplateProps, getFontFamily, renderSvgLines } from "./index"; import { type QrTemplateProps, getFontFamily, renderSvgLines } from "./common";
const QrTemplate2 = ({ const QrTemplate2 = ({
qrBase64, qrBase64,
fields, fields,
hideQr,
...props ...props
}: QrTemplateProps) => { }: QrTemplateProps) => {
const rName = fields?.restaurantName; const rName = fields?.restaurantName;
@@ -159,48 +160,50 @@ const QrTemplate2 = ({
<Path stroke="#8c6239" strokeWidth={0.8} d="M145 608h30m50 0h30" /> <Path stroke="#8c6239" strokeWidth={0.8} d="M145 608h30m50 0h30" />
{/* QR Placeholder / Gerçek QR */} {/* QR Placeholder / Gerçek QR */}
<G transform="translate(90 215)"> {!hideQr && (
<Rect width={220} height={220} fill="#fbf8f2" stroke="#d8c7b0" strokeWidth={1.5} rx={20} /> <G transform="translate(90 215)">
{qrBase64 ? ( <Rect width={220} height={220} fill="#fbf8f2" stroke="#d8c7b0" strokeWidth={1.5} rx={20} />
<Image {qrBase64 ? (
href={`data:image/png;base64,${qrBase64}`} <Image
x={10} href={`data:image/png;base64,${qrBase64}`}
y={10}
width={200}
height={200}
/>
) : (
<>
<Rect
width={200}
height={200}
x={10} x={10}
y={10} y={10}
fill="#f5efe6" width={200}
stroke="#d8c7b0" height={200}
strokeDasharray="4 3"
rx={14}
/> />
<Circle cx={110} cy={98} r={18} fill="#fbf8f2" stroke="#8c6239" /> ) : (
<G fill="#3a2312" transform="matrix(.65 0 0 .65 110 98)"> <>
<Ellipse cx={-5} cy={2} rx={7} ry={5} transform="rotate(-30)" /> <Rect
<Ellipse cx={5} rx={7} ry={5} transform="rotate(40)" /> width={200}
</G> height={200}
<Text x={10}
x={110} y={10}
y={132} fill="#f5efe6"
fill="#8c6239" stroke="#d8c7b0"
fontFamily="System, sans-serif" strokeDasharray="4 3"
fontSize={11} rx={14}
fontWeight={700} />
letterSpacing={1} <Circle cx={110} cy={98} r={18} fill="#fbf8f2" stroke="#8c6239" />
textAnchor="middle" <G fill="#3a2312" transform="matrix(.65 0 0 .65 110 98)">
> <Ellipse cx={-5} cy={2} rx={7} ry={5} transform="rotate(-30)" />
{"QR KOD ALANI"} <Ellipse cx={5} rx={7} ry={5} transform="rotate(40)" />
</Text> </G>
</> <Text
)} x={110}
</G> y={132}
fill="#8c6239"
fontFamily="System, sans-serif"
fontSize={11}
fontWeight={700}
letterSpacing={1}
textAnchor="middle"
>
{"QR KOD ALANI"}
</Text>
</>
)}
</G>
)}
{/* Metinler (Çok satırlı destekli) */} {/* Metinler (Çok satırlı destekli) */}
<G textAnchor="middle"> <G textAnchor="middle">
@@ -12,11 +12,12 @@ import Svg, {
Text, Text,
Image, Image,
} from "react-native-svg"; } from "react-native-svg";
import { type QrTemplateProps, getFontFamily, renderSvgLines } from "./index"; import { type QrTemplateProps, getFontFamily, renderSvgLines } from "./common";
const QrTemplate3 = ({ const QrTemplate3 = ({
qrBase64, qrBase64,
fields, fields,
hideQr,
...props ...props
}: QrTemplateProps) => { }: QrTemplateProps) => {
const rName = fields?.restaurantName; const rName = fields?.restaurantName;
@@ -213,42 +214,44 @@ const QrTemplate3 = ({
</G> </G>
{/* QR Placeholder / Gerçek QR */} {/* QR Placeholder / Gerçek QR */}
<G id="qr-placeholder" transform="translate(90 215)"> {!hideQr && (
<Rect width={220} height={220} fill="#fff" stroke="#e4e4e7" strokeWidth={2} rx={20} /> <G id="qr-placeholder" transform="translate(90 215)">
{qrBase64 ? ( <Rect width={220} height={220} fill="#fff" stroke="#e4e4e7" strokeWidth={2} rx={20} />
<Image {qrBase64 ? (
href={`data:image/png;base64,${qrBase64}`} <Image
x={10} href={`data:image/png;base64,${qrBase64}`}
y={10} x={10}
width={200} y={10}
height={200} width={200}
/> height={200}
) : (
<>
<Rect
width={196}
height={196}
x={12}
y={12}
fill="#fafafa"
stroke="#e4e4e7"
strokeDasharray="4 3"
rx={14}
/> />
<Text ) : (
x={110} <>
y={115} <Rect
fill="#71717a" width={196}
fontFamily="System, sans-serif" height={196}
fontSize={12} x={12}
fontWeight={700} y={12}
textAnchor="middle" fill="#fafafa"
> stroke="#e4e4e7"
{"QR KOD ALANI"} strokeDasharray="4 3"
</Text> rx={14}
</> />
)} <Text
</G> x={110}
y={115}
fill="#71717a"
fontFamily="System, sans-serif"
fontSize={12}
fontWeight={700}
textAnchor="middle"
>
{"QR KOD ALANI"}
</Text>
</>
)}
</G>
)}
{/* Metinler (Çok satırlı destekli) */} {/* Metinler (Çok satırlı destekli) */}
<G id="layer-texts"> <G id="layer-texts">
@@ -9,11 +9,12 @@ import Svg, {
Text, Text,
Image, Image,
} from "react-native-svg"; } from "react-native-svg";
import { type QrTemplateProps, getFontFamily, renderSvgLines } from "./index"; import { type QrTemplateProps, getFontFamily, renderSvgLines } from "./common";
const QrTemplate4 = ({ const QrTemplate4 = ({
qrBase64, qrBase64,
fields, fields,
hideQr,
...props ...props
}: QrTemplateProps) => { }: QrTemplateProps) => {
const rName = fields?.restaurantName; const rName = fields?.restaurantName;
@@ -136,52 +137,54 @@ const QrTemplate4 = ({
d="M114 278v-58h54m118 58v-58h-54M114 342v58h54m118-58v58h-54" d="M114 278v-58h54m118 58v-58h-54M114 342v58h54m118-58v58h-54"
/> />
{/* QR Placeholder / Live QR */} {/* QR Placeholder / Gerçek QR */}
<G transform="translate(125 230)"> {!hideQr && (
<Rect <G transform="translate(125 230)">
width={150} <Rect
height={150} width={150}
fill="#fff" height={150}
stroke="#eae5d9" fill="#fff"
strokeWidth={1.5} stroke="#eae5d9"
rx={12} strokeWidth={1.5}
/> rx={12}
{qrBase64 ? (
<Image
href={`data:image/png;base64,${qrBase64}`}
x={8}
y={8}
width={134}
height={134}
/> />
) : ( {qrBase64 ? (
<> <Image
<Rect href={`data:image/png;base64,${qrBase64}`}
width={134}
height={134}
x={8} x={8}
y={8} y={8}
fill="#fafaf8" width={134}
stroke="#c59b4b" height={134}
strokeDasharray="4 3"
strokeOpacity={0.35}
rx={8}
/> />
<Text ) : (
x={75} <>
y={79} <Rect
fill="#c59b4b" width={134}
fontFamily="System, sans-serif" height={134}
fontSize={10} x={8}
fontWeight={700} y={8}
letterSpacing={1} fill="#fafaf8"
textAnchor="middle" stroke="#c59b4b"
> strokeDasharray="4 3"
{"QR KOD ALANI"} strokeOpacity={0.35}
</Text> rx={8}
</> />
)} <Text
</G> x={75}
y={79}
fill="#c59b4b"
fontFamily="System, sans-serif"
fontSize={10}
fontWeight={700}
letterSpacing={1}
textAnchor="middle"
>
{"QR KOD ALANI"}
</Text>
</>
)}
</G>
)}
{/* Metinler */} {/* Metinler */}
<G fill="#111" textAnchor="middle"> <G fill="#111" textAnchor="middle">
@@ -6,7 +6,7 @@ import Svg, {
Path, Path,
Text, Text,
} from "react-native-svg"; } 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 // eslint-disable-next-line @typescript-eslint/no-require-imports
const bgImage = require("../../../assets/templates/5.png"); const bgImage = require("../../../assets/templates/5.png");
@@ -14,6 +14,7 @@ const bgImage = require("../../../assets/templates/5.png");
const QrTemplate5 = ({ const QrTemplate5 = ({
qrBase64, qrBase64,
fields, fields,
hideQr,
...props ...props
}: QrTemplateProps) => { }: QrTemplateProps) => {
const rName = fields?.restaurantName; const rName = fields?.restaurantName;
@@ -39,58 +40,60 @@ const QrTemplate5 = ({
/> />
{/* QR Placeholder / Live QR */} {/* QR Placeholder / Live QR */}
<G transform="translate(108 206)"> {!hideQr && (
<Rect <G transform="translate(108 206)">
width={164} <Rect
height={164} width={164}
fill="#fff" height={164}
stroke="#dce5ea" fill="#fff"
strokeWidth={1.2} stroke="#dce5ea"
rx={16} strokeWidth={1.2}
/> rx={16}
<Path
fill="none"
stroke="#003554"
strokeLinecap="round"
strokeWidth={3}
d="M6 30V12a6 6 0 0 1 6-6h18m128 24V12a6 6 0 0 0-6-6h-18M6 134v18a6 6 0 0 0 6 6h18m128-24v18a6 6 0 0 1-6 6h-18"
/>
{qrBase64 ? (
<Image
href={`data:image/png;base64,${qrBase64}`}
x={12}
y={12}
width={140}
height={140}
/> />
) : ( <Path
<> fill="none"
<Rect stroke="#003554"
width={140} strokeLinecap="round"
height={140} strokeWidth={3}
d="M6 30V12a6 6 0 0 1 6-6h18m128 24V12a6 6 0 0 0-6-6h-18M6 134v18a6 6 0 0 0 6 6h18m128-24v18a6 6 0 0 1-6 6h-18"
/>
{qrBase64 ? (
<Image
href={`data:image/png;base64,${qrBase64}`}
x={12} x={12}
y={12} y={12}
fill="#fafcfd" width={140}
stroke="#003554" height={140}
strokeDasharray="4 3"
strokeOpacity={0.2}
rx={10}
/> />
<Text ) : (
x={82} <>
y={87} <Rect
fill="#003554" width={140}
fontFamily="System, sans-serif" height={140}
fontSize={11} x={12}
fontWeight={700} y={12}
letterSpacing={1} fill="#fafcfd"
textAnchor="middle" stroke="#003554"
> strokeDasharray="4 3"
{"QR KOD ALANI"} strokeOpacity={0.2}
</Text> rx={10}
</> />
)} <Text
</G> x={82}
y={87}
fill="#003554"
fontFamily="System, sans-serif"
fontSize={11}
fontWeight={700}
letterSpacing={1}
textAnchor="middle"
>
{"QR KOD ALANI"}
</Text>
</>
)}
</G>
)}
{/* Metinler */} {/* Metinler */}
<G fill="#003554" textAnchor="middle"> <G fill="#003554" textAnchor="middle">
@@ -9,11 +9,12 @@ import Svg, {
Text, Text,
Image, Image,
} from "react-native-svg"; } from "react-native-svg";
import { type QrTemplateProps, getFontFamily, renderSvgLines } from "./index"; import { type QrTemplateProps, getFontFamily, renderSvgLines } from "./common";
const QrTemplate6 = ({ const QrTemplate6 = ({
qrBase64, qrBase64,
fields, fields,
hideQr,
...props ...props
}: QrTemplateProps) => { }: QrTemplateProps) => {
const rName = fields?.restaurantName; const rName = fields?.restaurantName;
@@ -113,59 +114,61 @@ const QrTemplate6 = ({
<Use href="#b6" transform="matrix(-.95 0 0 .95 360 360)" /> <Use href="#b6" transform="matrix(-.95 0 0 .95 360 360)" />
{/* QR Placeholder / Gerçek QR */} {/* QR Placeholder / Gerçek QR */}
<G transform="translate(95 205)"> {!hideQr && (
<Path <G transform="translate(95 205)">
fill="#fdfbf7" <Path
stroke="#122a42" fill="#fdfbf7"
strokeWidth={3} stroke="#122a42"
d="M0 0h210v210H0z" strokeWidth={3}
/> d="M0 0h210v210H0z"
<Path
fill="none"
stroke="#c59b58"
strokeWidth={1.5}
d="M4 4h202v202H4z"
/>
{qrBase64 ? (
<Image
href={`data:image/png;base64,${qrBase64}`}
x={10}
y={10}
width={190}
height={190}
/> />
) : ( <Path
<> fill="none"
<Path stroke="#c59b58"
fill="#faf8f5" strokeWidth={1.5}
stroke="#122a42" d="M4 4h202v202H4z"
strokeDasharray="4 3" />
d="M12 12h186v186H12z" {qrBase64 ? (
<Image
href={`data:image/png;base64,${qrBase64}`}
x={10}
y={10}
width={190}
height={190}
/> />
<Circle ) : (
cx={105} <>
cy={95} <Path
r={16} fill="#faf8f5"
fill="#f6efe3" stroke="#122a42"
stroke="#c59b58" strokeDasharray="4 3"
strokeWidth={1.2} d="M12 12h186v186H12z"
/> />
<Use href="#a6" transform="matrix(.45 0 0 .45 105 93)" /> <Circle
<Text cx={105}
x={105} cy={95}
y={128} r={16}
fill="#122a42" fill="#f6efe3"
fontFamily="System, sans-serif" stroke="#c59b58"
fontSize={11} strokeWidth={1.2}
fontWeight="bold" />
letterSpacing={1.5} <Use href="#a6" transform="matrix(.45 0 0 .45 105 93)" />
textAnchor="middle" <Text
> x={105}
{"QR KOD ALANI"} y={128}
</Text> fill="#122a42"
</> fontFamily="System, sans-serif"
)} fontSize={11}
</G> fontWeight="bold"
letterSpacing={1.5}
textAnchor="middle"
>
{"QR KOD ALANI"}
</Text>
</>
)}
</G>
)}
{/* Metinler */} {/* Metinler */}
<G textAnchor="middle"> <G textAnchor="middle">
@@ -7,7 +7,7 @@ import Svg, {
Text, Text,
Image, Image,
} from "react-native-svg"; } 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 // eslint-disable-next-line @typescript-eslint/no-var-requires
const template8Bg = require("../../../assets/templates/8.jpeg"); const template8Bg = require("../../../assets/templates/8.jpeg");
@@ -15,6 +15,7 @@ const template8Bg = require("../../../assets/templates/8.jpeg");
const QrTemplate8 = ({ const QrTemplate8 = ({
qrBase64, qrBase64,
fields, fields,
hideQr,
...props ...props
}: QrTemplateProps) => { }: QrTemplateProps) => {
const rName = fields?.restaurantName; const rName = fields?.restaurantName;
@@ -40,57 +41,59 @@ const QrTemplate8 = ({
/> />
{/* QR Alanı (Merkez) */} {/* QR Alanı (Merkez) */}
<G transform="translate(348 305)"> {!hideQr && (
<Rect <G transform="translate(348 305)">
width={328} <Rect
height={336} width={328}
fill="#f9f5eb" height={336}
stroke="#d1a752" fill="#f9f5eb"
strokeWidth={1.5} stroke="#d1a752"
rx={16} strokeWidth={1.5}
/> rx={16}
<Rect
width={304}
height={312}
x={12}
y={12}
fill="#faf8f2"
stroke="#0c4443"
strokeDasharray="6 4"
strokeOpacity={0.3}
strokeWidth={1.5}
rx={12}
/>
{qrBase64 ? (
<Image
href={`data:image/png;base64,${qrBase64}`}
x={18}
y={18}
width={292}
height={300}
/> />
) : ( <Rect
<> width={304}
<Circle cx={164} cy={148} r={28} fill="#0c4443" /> height={312}
<G fill="#d1a752"> x={12}
<Path d="M164 132.6c4.4 6.6 4.4 15.4 0 24.2-4.4-8.8-4.4-17.6 0-24.2" /> y={12}
<Path d="M161.8 150.2c-8.8-4.4-13.2 4.4-6.6 8.8 6.6 0 8.8-6.6 6.6-8.8m4.4 0c8.8-4.4 13.2 4.4 6.6 8.8-6.6 0-8.8-6.6-6.6-8.8" /> fill="#faf8f2"
</G> stroke="#0c4443"
<Text strokeDasharray="6 4"
x={164} strokeOpacity={0.3}
y={218} strokeWidth={1.5}
fill="#0c4443" rx={12}
fontFamily="System, -apple-system, Roboto, sans-serif" />
fontSize={14} {qrBase64 ? (
fontWeight={800} <Image
letterSpacing={2} href={`data:image/png;base64,${qrBase64}`}
textAnchor="middle" x={18}
> y={18}
{"QR KOD ALANI"} width={292}
</Text> height={300}
</> />
)} ) : (
</G> <>
<Circle cx={164} cy={148} r={28} fill="#0c4443" />
<G fill="#d1a752">
<Path d="M164 132.6c4.4 6.6 4.4 15.4 0 24.2-4.4-8.8-4.4-17.6 0-24.2" />
<Path d="M161.8 150.2c-8.8-4.4-13.2 4.4-6.6 8.8 6.6 0 8.8-6.6 6.6-8.8m4.4 0c8.8-4.4 13.2 4.4 6.6 8.8-6.6 0-8.8-6.6-6.6-8.8" />
</G>
<Text
x={164}
y={218}
fill="#0c4443"
fontFamily="System, -apple-system, Roboto, sans-serif"
fontSize={14}
fontWeight={800}
letterSpacing={2}
textAnchor="middle"
>
{"QR KOD ALANI"}
</Text>
</>
)}
</G>
)}
{/* Üst Çizgiler & OR Ayracı */} {/* Üst Çizgiler & OR Ayracı */}
<Path <Path
@@ -9,7 +9,7 @@ import Svg, {
TextPath, TextPath,
Image, Image,
} from "react-native-svg"; } 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 // eslint-disable-next-line @typescript-eslint/no-var-requires
const template9Bg = require("../../../assets/templates/9.jpeg"); const template9Bg = require("../../../assets/templates/9.jpeg");
@@ -17,6 +17,7 @@ const template9Bg = require("../../../assets/templates/9.jpeg");
const QrTemplate9 = ({ const QrTemplate9 = ({
qrBase64, qrBase64,
fields, fields,
hideQr,
...props ...props
}: QrTemplateProps) => { }: QrTemplateProps) => {
const rName = fields?.restaurantName; const rName = fields?.restaurantName;
@@ -46,68 +47,70 @@ const QrTemplate9 = ({
/> />
{/* QR Alanı (Merkez) */} {/* QR Alanı (Merkez) */}
<G transform="translate(362 428)"> {!hideQr && (
<Rect <G transform="translate(362 428)">
width={300} <Rect
height={300} width={300}
fill="#fff" height={300}
stroke="#d5cebe" fill="#fff"
strokeWidth={1.5} stroke="#d5cebe"
rx={24} strokeWidth={1.5}
/> rx={24}
<Rect
width={276}
height={276}
x={12}
y={12}
fill="#fafaf7"
stroke="#1e3246"
strokeDasharray="6 4"
strokeOpacity={0.35}
strokeWidth={1.5}
rx={16}
/>
{qrBase64 ? (
<Image
href={`data:image/png;base64,${qrBase64}`}
x={16}
y={16}
width={268}
height={268}
/> />
) : ( <Rect
<> width={276}
<Circle height={276}
cx={150} x={12}
cy={132} y={12}
r={28} fill="#fafaf7"
fill="#f5efe6" stroke="#1e3246"
stroke="#1e3246" strokeDasharray="6 4"
strokeWidth={1.5} strokeOpacity={0.35}
strokeWidth={1.5}
rx={16}
/>
{qrBase64 ? (
<Image
href={`data:image/png;base64,${qrBase64}`}
x={16}
y={16}
width={268}
height={268}
/> />
<Path ) : (
fill="none" <>
stroke="#1e3246" <Circle
strokeLinecap="round" cx={150}
strokeWidth={1.9} cy={132}
d="M142.4 116.8v11.4c0 3.8 5.7 3.8 5.7 0v-11.4m-2.85 0v11.4m0 3.8v15.2m9.5-30.4c4.75 3.8 4.75 15.2 0 17.1v13.3" r={28}
/> fill="#f5efe6"
<Text stroke="#1e3246"
x={150} strokeWidth={1.5}
y={192} />
fill="#1e3246" <Path
fontFamily="System, -apple-system, Roboto, sans-serif" fill="none"
fontSize={14} stroke="#1e3246"
fontWeight={800} strokeLinecap="round"
letterSpacing={2} strokeWidth={1.9}
textAnchor="middle" d="M142.4 116.8v11.4c0 3.8 5.7 3.8 5.7 0v-11.4m-2.85 0v11.4m0 3.8v15.2m9.5-30.4c4.75 3.8 4.75 15.2 0 17.1v13.3"
> />
{"QR KOD ALANI"} <Text
</Text> x={150}
</> y={192}
)} fill="#1e3246"
</G> fontFamily="System, -apple-system, Roboto, sans-serif"
fontSize={14}
fontWeight={800}
letterSpacing={2}
textAnchor="middle"
>
{"QR KOD ALANI"}
</Text>
</>
)}
</G>
)}
{/* Üst Amblem & Rozet */} {/* Üst Amblem & Rozet */}
<G stroke="#a88b58" transform="translate(290 142)"> <G stroke="#a88b58" transform="translate(290 142)">
@@ -7,11 +7,12 @@ import Svg, {
Text, Text,
Image, Image,
} from "react-native-svg"; } from "react-native-svg";
import { type QrTemplateProps, getFontFamily, renderSvgLines } from "./index"; import { type QrTemplateProps, getFontFamily, renderSvgLines } from "./common";
const QrTemplateLumiere = ({ const QrTemplateLumiere = ({
qrBase64, qrBase64,
fields, fields,
hideQr,
...props ...props
}: QrTemplateProps) => { }: QrTemplateProps) => {
const rName = fields?.restaurantName; const rName = fields?.restaurantName;
@@ -91,43 +92,45 @@ const QrTemplateLumiere = ({
<Path stroke="#d4af37" strokeOpacity={0.4} strokeWidth={0.8} d="M120 198h360" /> <Path stroke="#d4af37" strokeOpacity={0.4} strokeWidth={0.8} d="M120 198h360" />
{/* QR Placeholder / Gerçek QR */} {/* QR Placeholder / Gerçek QR */}
<G transform="translate(190 215)"> {!hideQr && (
<Rect width={220} height={220} fill="#fff" stroke="#d4af37" strokeWidth={2} rx={20} /> <G transform="translate(190 215)">
{qrBase64 ? ( <Rect width={220} height={220} fill="#fff" stroke="#d4af37" strokeWidth={2} rx={20} />
<Image {qrBase64 ? (
href={`data:image/png;base64,${qrBase64}`} <Image
x={10} href={`data:image/png;base64,${qrBase64}`}
y={10} x={10}
width={200} y={10}
height={200} width={200}
/> height={200}
) : (
<>
<Rect
width={196}
height={196}
x={12}
y={12}
fill="#fafafa"
stroke="#d4af37"
strokeDasharray="4 3"
strokeOpacity={0.4}
rx={14}
/> />
<Text ) : (
x={110} <>
y={115} <Rect
fill="#71717a" width={196}
fontFamily="System, sans-serif" height={196}
fontSize={12} x={12}
fontWeight={600} y={12}
textAnchor="middle" fill="#fafafa"
> stroke="#d4af37"
{"QR KOD ALANI"} strokeDasharray="4 3"
</Text> strokeOpacity={0.4}
</> rx={14}
)} />
</G> <Text
x={110}
y={115}
fill="#71717a"
fontFamily="System, sans-serif"
fontSize={12}
fontWeight={600}
textAnchor="middle"
>
{"QR KOD ALANI"}
</Text>
</>
)}
</G>
)}
{/* Metinler (Çok satırlı ve bağımsız font destekli) */} {/* Metinler (Çok satırlı ve bağımsız font destekli) */}
<G textAnchor="middle"> <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 React from "react";
import { Text as SvgText, type SvgProps } from "react-native-svg";
import QrTemplate1 from "./QrTemplateLumiere"; import QrTemplate1 from "./QrTemplateLumiere";
import QrTemplate2 from "./QrTemplate2"; import QrTemplate2 from "./QrTemplate2";
import QrTemplate3 from "./QrTemplate3"; import QrTemplate3 from "./QrTemplate3";
@@ -8,115 +7,9 @@ import QrTemplate5 from "./QrTemplate5";
import QrTemplate6 from "./QrTemplate6"; import QrTemplate6 from "./QrTemplate6";
import QrTemplate8 from "./QrTemplate8"; import QrTemplate8 from "./QrTemplate8";
import QrTemplate9 from "./QrTemplate9"; import QrTemplate9 from "./QrTemplate9";
import { type QrTemplateProps } from "./common";
export interface TextItemConfig { export * from "./common";
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>
))}
</>
);
}
// Yeni component eklemek için buraya import edip COMPONENT_TEMPLATES'e kaydet // Yeni component eklemek için buraya import edip COMPONENT_TEMPLATES'e kaydet
export const COMPONENT_TEMPLATES: Record< 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-5": 380 / 500, // 380x500 = PNG arka planlı modern kart
"qr-6": 400 / 580, // 400x580 = taş & ızgara vintage stant "qr-6": 400 / 580, // 400x580 = taş & ızgara vintage stant
"qr-8": 1, // 1024x1024 = 1:1 Emerald Gold Özel Tasarım "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
}; };
+147
View File
@@ -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 &rarr;
</Link>
</div>
</header>
{/* Sayfa İçeriği */}
<div className="flex-1 p-6 md:p-8 overflow-y-auto">
{children}
</div>
</main>
</div>
);
}
+158
View File
@@ -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 &rarr;
</Link>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{Object.values(QR_STAND_PRESETS).slice(0, 4).map((preset) => (
<div
key={preset.key}
className="p-4 rounded-xl bg-[#121215] border border-white/10 space-y-3 flex flex-col justify-between"
>
<div>
<div className="flex items-center justify-between mb-2">
<span className="text-[11px] font-mono text-zinc-500">{preset.key}</span>
<span className="text-[10px] bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 px-2 py-0.5 rounded-full font-bold">
Aktif
</span>
</div>
<div className="font-bold text-white text-sm line-clamp-1">{preset.name}</div>
<div className="text-xs text-zinc-400 mt-0.5">{preset.categoryName || "Masa Kartı"}</div>
</div>
<div className="pt-3 border-t border-white/5 flex items-center justify-between text-xs text-zinc-400">
<span>Oran: {preset.aspectRatio || "1/1"}</span>
<Link
href={`/admin/templates?key=${preset.key}`}
className="font-bold text-[#D4AF37] hover:underline"
>
Düzenle
</Link>
</div>
</div>
))}
</div>
</div>
</div>
);
}
+26
View File
@@ -0,0 +1,26 @@
import type { Metadata } from "next";
import { TemplateStudioAdminClient } from "@/components/TemplateStudioAdminClient";
export const metadata: Metadata = {
title: "QR Şablon Stüdyosu | Menulio Admin",
description: "Masa standı şablonlarını görsel editör veya JSON ile tasarlayın, canlı önizleyin ve anında yayınlayın.",
};
export default function AdminTemplatesPage() {
return (
<div className="max-w-7xl mx-auto space-y-6">
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-2 border-b border-white/10 pb-5">
<div>
<h1 className="text-2xl font-extrabold text-white tracking-tight">
QR Masa Standı Şablon Stüdyosu
</h1>
<p className="text-zinc-400 text-xs mt-1">
Uygulama güncellemesi gerektirmeden yeni şablonlar ekleyin, renkleri ve QR koordinatlarını düzenleyin.
</p>
</div>
</div>
<TemplateStudioAdminClient />
</div>
);
}
+142
View File
@@ -0,0 +1,142 @@
import { NextResponse } from "next/server";
import fs from "fs/promises";
import path from "path";
import { supabase } from "@/lib/supabase";
const DATA_FILE = path.join(process.cwd(), "src/data/qr-templates.json");
function mapFromDb(row: any) {
return {
key: row.key,
name: row.name,
categoryName: row.category_name,
aspectRatio: row.aspect_ratio || "1/1",
bgType: row.bg_type || "gradient",
bgImageUrl: row.bg_image_url || undefined,
bgGradient: row.bg_gradient || undefined,
plaqueColor: row.plaque_color || undefined,
borderAccent: row.border_accent || undefined,
accentText: row.accent_text || undefined,
headerText: row.header_text || undefined,
subText: row.sub_text || undefined,
dividerColor: row.divider_color || undefined,
qrConfig: row.qr_config,
layers: row.layers,
isActive: row.is_active ?? true,
createdAt: row.created_at,
updatedAt: row.updated_at,
};
}
function mapToDb(item: any) {
return {
key: item.key,
name: item.name,
category_name: item.categoryName || null,
aspect_ratio: item.aspectRatio || "1/1",
bg_type: item.bgType || "gradient",
bg_image_url: item.bgImageUrl || null,
bg_gradient: item.bgGradient || null,
plaque_color: item.plaqueColor || null,
border_accent: item.borderAccent || null,
accent_text: item.accentText || null,
header_text: item.headerText || null,
sub_text: item.subText || null,
divider_color: item.dividerColor || null,
qr_config: item.qrConfig || item.defaultQrConfig || null,
layers: item.layers || item.defaultLayers || null,
is_active: item.isActive !== undefined ? item.isActive : true,
updated_at: new Date().toISOString(),
};
}
async function readFromDisk(): Promise<any[]> {
try {
const raw = await fs.readFile(DATA_FILE, "utf-8");
return JSON.parse(raw);
} catch {
return [];
}
}
async function writeToDisk(list: any[]): Promise<void> {
try {
await fs.writeFile(DATA_FILE, JSON.stringify(list, null, 2), "utf-8");
} catch {}
}
export async function GET() {
try {
const { data, error } = await supabase
.from("qr_stand_templates")
.select("*")
.order("created_at", { ascending: false });
if (!error && data && data.length > 0) {
const mapped = data.map(mapFromDb);
await writeToDisk(mapped);
return NextResponse.json({ templates: mapped, source: "supabase_db" });
}
} catch {}
const diskTemplates = await readFromDisk();
return NextResponse.json({ templates: diskTemplates, source: "disk_cache" });
}
export async function POST(req: Request) {
try {
const body = await req.json();
if (!body || !body.key) {
return NextResponse.json({ error: "Şablon key zorunludur" }, { status: 400 });
}
// 1. Supabase'e Upsert et
try {
const dbRow = mapToDb(body);
await supabase.from("qr_stand_templates").upsert(dbRow, { onConflict: "key" });
} catch (dbErr) {
console.warn("Supabase upsert warning:", dbErr);
}
// 2. Diske de yaz (offline / backup)
const diskList = await readFromDisk();
const existingIdx = diskList.findIndex((t: any) => t.key === body.key);
if (existingIdx >= 0) {
diskList[existingIdx] = { ...diskList[existingIdx], ...body };
} else {
diskList.unshift(body);
}
await writeToDisk(diskList);
return NextResponse.json({ success: true, template: body });
} catch (err: any) {
return NextResponse.json({ error: err.message }, { status: 500 });
}
}
export async function DELETE(req: Request) {
try {
const { searchParams } = new URL(req.url);
const key = searchParams.get("key");
if (!key) {
return NextResponse.json({ error: "Şablon key zorunludur" }, { status: 400 });
}
// 1. Supabase'den Sil
try {
await supabase.from("qr_stand_templates").delete().eq("key", key);
} catch (dbErr) {
console.warn("Supabase delete warning:", dbErr);
}
// 2. Diskten de Sil
const diskList = await readFromDisk();
const filtered = diskList.filter((t: any) => t.key !== key);
await writeToDisk(filtered);
return NextResponse.json({ success: true, deletedKey: key });
} catch (err: any) {
return NextResponse.json({ error: err.message }, { status: 500 });
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+23 -146
View File
@@ -90,14 +90,14 @@ importers:
specifier: ~7.0.5 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) 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: expo-print:
specifier: ^57.0.1 specifier: ~14.0.3
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)) 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: expo-router:
specifier: ~4.0.0 specifier: ~4.0.0
version: 4.0.22(1482d1318244a89f06fcfc2b6da7d585) version: 4.0.22(1482d1318244a89f06fcfc2b6da7d585)
expo-sharing: expo-sharing:
specifier: ^57.0.13 specifier: ~13.0.1
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) 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: expo-splash-screen:
specifier: ~0.29.24 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) 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 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) 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: react-native-svg:
specifier: ^15.15.5 specifier: 15.8.0
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) 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: react-native-url-polyfill:
specifier: ^2.0.0 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)) 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: react-native-view-shot:
specifier: ^5.1.1 specifier: ~4.0.3
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) 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: devDependencies:
'@babel/core': '@babel/core':
specifier: ^7.25.2 specifier: ^7.25.2
@@ -1274,18 +1274,12 @@ packages:
'@expo/code-signing-certificates@0.0.6': '@expo/code-signing-certificates@0.0.6':
resolution: {integrity: sha512-iNe0puxwBNEcuua9gmTGzq+SuMDa0iATai1FlFTMHJ/vUmKvN/V//drXoLJkVb5i5H3iE/n/qIJxyoBnXouD0w==} 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': '@expo/config-plugins@9.0.17':
resolution: {integrity: sha512-m24F1COquwOm7PBl5wRbkT9P9DviCXe0D7S7nQsolfbhdCWuvMkfXeoWmgjtdhy7sDlOyIgBrAdnB6MfsWKqIg==} resolution: {integrity: sha512-m24F1COquwOm7PBl5wRbkT9P9DviCXe0D7S7nQsolfbhdCWuvMkfXeoWmgjtdhy7sDlOyIgBrAdnB6MfsWKqIg==}
'@expo/config-types@52.0.5': '@expo/config-types@52.0.5':
resolution: {integrity: sha512-AMDeuDLHXXqd8W+0zSjIt7f37vUd/BP8p43k68NHpyAvQO+z8mbQZm3cNQVAMySeayK2XoPigAFB1JF2NFajaA==} resolution: {integrity: sha512-AMDeuDLHXXqd8W+0zSjIt7f37vUd/BP8p43k68NHpyAvQO+z8mbQZm3cNQVAMySeayK2XoPigAFB1JF2NFajaA==}
'@expo/config-types@57.0.2':
resolution: {integrity: sha512-ewW08OonrcRIsRKIlFvvcmmafE5zemb1ocu3HkNwtVPyRtj2w42pZCAkMIROYpcVBaPnc3mDT9UZDzwXWC3i6g==}
'@expo/config@10.0.11': '@expo/config@10.0.11':
resolution: {integrity: sha512-nociJ4zr/NmbVfMNe9j/+zRlt7wz/siISu7PjdWE4WE+elEGxWWxsGzltdJG0llzrM+khx8qUiFK5aiVcdMBww==} resolution: {integrity: sha512-nociJ4zr/NmbVfMNe9j/+zRlt7wz/siISu7PjdWE4WE+elEGxWWxsGzltdJG0llzrM+khx8qUiFK5aiVcdMBww==}
@@ -1329,20 +1323,9 @@ packages:
'@expo/plist@0.2.2': '@expo/plist@0.2.2':
resolution: {integrity: sha512-ZZGvTO6vEWq02UAPs3LIdja+HRO18+LRI5QuDl6Hs3Ps7KX7xU6Y6kjahWKY37Rx2YjNpX07dGpBFzzC+vKa2g==} resolution: {integrity: sha512-ZZGvTO6vEWq02UAPs3LIdja+HRO18+LRI5QuDl6Hs3Ps7KX7xU6Y6kjahWKY37Rx2YjNpX07dGpBFzzC+vKa2g==}
'@expo/plist@0.8.1':
resolution: {integrity: sha512-3gTReGIUm0oRaMClsAJYxBnVPCl6fVpsl8HS+DTVxDhW4GyVyxg9E/Znm3BvcHtUJ51RJJI14pC1wvrNilCRHw==}
'@expo/prebuild-config@8.2.0': '@expo/prebuild-config@8.2.0':
resolution: {integrity: sha512-CxiPpd980s0jyxi7eyN3i/7YKu3XL+8qPjBZUCYtc0+axpGweqIkq2CslyLSKHyqVyH/zlPkbVgWdyiYavFS5Q==} 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': '@expo/rudder-sdk-node@1.1.1':
resolution: {integrity: sha512-uy/hS/awclDJ1S88w9UGpc6Nm9XnNUjzOAAib1A3PVAnGQIwebg8DpFqOthFBTlZxeuV/BKbZ5jmTbtNZkp1WQ==} resolution: {integrity: sha512-uy/hS/awclDJ1S88w9UGpc6Nm9XnNUjzOAAib1A3PVAnGQIwebg8DpFqOthFBTlZxeuV/BKbZ5jmTbtNZkp1WQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
@@ -2084,10 +2067,6 @@ packages:
engines: {node: '>=10.0.0'} engines: {node: '>=10.0.0'}
deprecated: this version has critical issues, please update to the latest version 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': '@xmldom/xmldom@0.9.11':
resolution: {integrity: sha512-tW8bcK3hsG0/uqSnNz6TK4BkcuZSezoU7DlnYssILmZDktPnSHHuDJJFM0AJv+13gz2r0iGdrj6qqKeUnxXEDg==} resolution: {integrity: sha512-tW8bcK3hsG0/uqSnNz6TK4BkcuZSezoU7DlnYssILmZDktPnSHHuDJJFM0AJv+13gz2r0iGdrj6qqKeUnxXEDg==}
engines: {node: '>=14.6'} engines: {node: '>=14.6'}
@@ -2298,10 +2277,6 @@ packages:
balanced-match@1.0.2: balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 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: base64-arraybuffer@1.0.2:
resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==} resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==}
engines: {node: '>= 0.6.0'} engines: {node: '>= 0.6.0'}
@@ -2349,10 +2324,6 @@ packages:
brace-expansion@2.1.4: brace-expansion@2.1.4:
resolution: {integrity: sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==} 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: braces@3.0.3:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -2928,8 +2899,8 @@ packages:
expo-modules-core@2.2.3: expo-modules-core@2.2.3:
resolution: {integrity: sha512-01QqZzpP/wWlxnNly4G06MsOBUTbMDj02DQigZoXfDh80vd/rk3/uVXqnZgOdLSggTs6DnvOgAUy0H2q30XdUg==} resolution: {integrity: sha512-01QqZzpP/wWlxnNly4G06MsOBUTbMDj02DQigZoXfDh80vd/rk3/uVXqnZgOdLSggTs6DnvOgAUy0H2q30XdUg==}
expo-print@57.0.1: expo-print@14.0.3:
resolution: {integrity: sha512-cEfxM+sV5suZyqbXsOmDGRHD+UmfAh2GWabbakdkl1s70L4MFiObPtC73ATXNjeUe+mlzxLhudoTxsuyJxk8Mg==} resolution: {integrity: sha512-VEGl7SX3q2Z3ZKwvxtOBAlXm9yDX8Jodewr+kouQcg2EaV0ciAH9rqLwRWZhAtEETpVdEG53ow7IAmwJU7jInA==}
peerDependencies: peerDependencies:
expo: '*' expo: '*'
react-native: '*' react-native: '*'
@@ -2953,12 +2924,10 @@ packages:
react-native-reanimated: react-native-reanimated:
optional: true optional: true
expo-sharing@57.0.13: expo-sharing@13.0.1:
resolution: {integrity: sha512-8kRUBikuR+c90++PF4PaP8pj+McUNi7AIZkwmn0iXcA8QobkJDSAjSB3fP8qoFm8bzHDhyibHIlODTAsxrMYMw==} resolution: {integrity: sha512-qych3Nw65wlFcnzE/gRrsdtvmdV0uF4U4qVMZBJYPG90vYyWh2QM9rp1gVu0KWOBc7N8CC2dSVYn4/BXqJy6Xw==}
peerDependencies: peerDependencies:
expo: '*' expo: '*'
react: '*'
react-native: '*'
expo-splash-screen@0.29.24: expo-splash-screen@0.29.24:
resolution: {integrity: sha512-k2rdjbb3Qeg4g104Sdz6+qXXYba8QgiuZRSxHX8IpsSYiiTU48BmCCGy12sN+O1B+sD1/+WPL4duCa1Fy6+Y4g==} resolution: {integrity: sha512-k2rdjbb3Qeg4g104Sdz6+qXXYba8QgiuZRSxHX8IpsSYiiTU48BmCCGy12sN+O1B+sD1/+WPL4duCa1Fy6+Y4g==}
@@ -3186,10 +3155,6 @@ packages:
resolution: {integrity: sha512-7yetJWqbS9sbn0vIfliPsFgoXMKn/YMF+Wuiog97x+urnSRRRZ7xB+uVkwGKzRgq9CDFfMQnE9ruL5DHv9c6Xg==} resolution: {integrity: sha512-7yetJWqbS9sbn0vIfliPsFgoXMKn/YMF+Wuiog97x+urnSRRRZ7xB+uVkwGKzRgq9CDFfMQnE9ruL5DHv9c6Xg==}
engines: {node: '>=6'} engines: {node: '>=6'}
getenv@2.0.0:
resolution: {integrity: sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ==}
engines: {node: '>=6'}
glob-parent@5.1.2: glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'} 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 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 hasBin: true
glob@13.0.6:
resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==}
engines: {node: 18 || 20 || >=22}
glob@7.2.3: glob@7.2.3:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} 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 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: lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} 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: lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
@@ -3784,10 +3741,6 @@ packages:
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
engines: {node: '>=6'} engines: {node: '>=6'}
minimatch@10.2.6:
resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==}
engines: {node: 18 || 20 || >=22}
minimatch@3.1.5: minimatch@3.1.5:
resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==}
@@ -4070,10 +4023,6 @@ packages:
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
engines: {node: '>=16 || 14 >=14.18'} 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: path-type@4.0.0:
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -4332,8 +4281,8 @@ packages:
react: '*' react: '*'
react-native: '*' react-native: '*'
react-native-svg@15.15.5: react-native-svg@15.8.0:
resolution: {integrity: sha512-L4go5jA+GWutdJ/JucuN20cjAbMg1HmMtAP+wZ+3JLCf6Jd0bhXQHxciRP/AQm/FlrIEZwkMcHNZP+FXAiic0w==} resolution: {integrity: sha512-KHJzKpgOjwj1qeZzsBjxNdoIgv2zNCO9fVcoq2TEhTRsVV5DGTZ9JzUZwybd7q4giT/H3RdtqC3u44dWdO0Ffw==}
peerDependencies: peerDependencies:
react: '*' react: '*'
react-native: '*' react-native: '*'
@@ -4343,12 +4292,11 @@ packages:
peerDependencies: peerDependencies:
react-native: '*' react-native: '*'
react-native-view-shot@5.1.1: react-native-view-shot@4.0.3:
resolution: {integrity: sha512-jOTLQrMGNBgv/nRA81VR2x2AdiCGoCjc63c9a5P4A0gStCbh9oDeJ6W+GtRxxQmxXXf6LDSrvG+mmWYulIrOkg==} resolution: {integrity: sha512-USNjYmED7C0me02c1DxKA0074Hw+y/nxo+xJKlffMvfUWWzL5ELh/TJA/pTnVqFurIrzthZDPtDM7aBFJuhrHQ==}
engines: {node: '>=20.19.4', npm: '>=10'}
peerDependencies: peerDependencies:
react: '>=18.0.0' react: '*'
react-native: '>=0.76.0' react-native: '*'
react-native@0.76.9: react-native@0.76.9:
resolution: {integrity: sha512-+LRwecWmTDco7OweGsrECIqJu0iyrREd6CTCgC/uLLYipiHvk+MH9nd6drFtCw/6Blz6eoKTcH9YTTJusNtrWg==} resolution: {integrity: sha512-+LRwecWmTDco7OweGsrECIqJu0iyrREd6CTCgC/uLLYipiHvk+MH9nd6drFtCw/6Blz6eoKTcH9YTTJusNtrWg==}
@@ -6348,25 +6296,6 @@ snapshots:
dependencies: dependencies:
node-forge: 1.4.0 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)': '@expo/config-plugins@9.0.17(supports-color@8.1.1)':
dependencies: dependencies:
'@expo/config-types': 52.0.5 '@expo/config-types': 52.0.5
@@ -6388,8 +6317,6 @@ snapshots:
'@expo/config-types@52.0.5': {} '@expo/config-types@52.0.5': {}
'@expo/config-types@57.0.2': {}
'@expo/config@10.0.11(supports-color@8.1.1)': '@expo/config@10.0.11(supports-color@8.1.1)':
dependencies: dependencies:
'@babel/code-frame': 7.10.4 '@babel/code-frame': 7.10.4
@@ -6515,12 +6442,6 @@ snapshots:
base64-js: 1.5.1 base64-js: 1.5.1
xmlbuilder: 14.0.0 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)': '@expo/prebuild-config@8.2.0(supports-color@8.1.1)':
dependencies: dependencies:
'@expo/config': 10.0.11(supports-color@8.1.1) '@expo/config': 10.0.11(supports-color@8.1.1)
@@ -6537,16 +6458,6 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - 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': '@expo/rudder-sdk-node@1.1.1':
dependencies: dependencies:
'@expo/bunyan': 4.0.1 '@expo/bunyan': 4.0.1
@@ -7306,8 +7217,6 @@ snapshots:
'@xmldom/xmldom@0.7.13': {} '@xmldom/xmldom@0.7.13': {}
'@xmldom/xmldom@0.8.14': {}
'@xmldom/xmldom@0.9.11': {} '@xmldom/xmldom@0.9.11': {}
abort-controller@3.0.0: abort-controller@3.0.0:
@@ -7542,8 +7451,6 @@ snapshots:
balanced-match@1.0.2: {} balanced-match@1.0.2: {}
balanced-match@4.0.4: {}
base64-arraybuffer@1.0.2: {} base64-arraybuffer@1.0.2: {}
base64-js@1.5.1: {} base64-js@1.5.1: {}
@@ -7585,10 +7492,6 @@ snapshots:
dependencies: dependencies:
balanced-match: 1.0.2 balanced-match: 1.0.2
brace-expansion@5.0.9:
dependencies:
balanced-match: 4.0.4
braces@3.0.3: braces@3.0.3:
dependencies: dependencies:
fill-range: 7.1.1 fill-range: 7.1.1
@@ -8232,7 +8135,7 @@ snapshots:
dependencies: dependencies:
invariant: 2.2.4 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: 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) 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-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 - react-native
- supports-color - 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: 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) 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): 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: dependencies:
@@ -8564,8 +8459,6 @@ snapshots:
getenv@1.0.0: {} getenv@1.0.0: {}
getenv@2.0.0: {}
glob-parent@5.1.2: glob-parent@5.1.2:
dependencies: dependencies:
is-glob: 4.0.3 is-glob: 4.0.3
@@ -8583,12 +8476,6 @@ snapshots:
package-json-from-dist: 1.0.1 package-json-from-dist: 1.0.1
path-scurry: 1.11.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: glob@7.2.3:
dependencies: dependencies:
fs.realpath: 1.0.0 fs.realpath: 1.0.0
@@ -9021,8 +8908,6 @@ snapshots:
lru-cache@10.4.3: {} lru-cache@10.4.3: {}
lru-cache@11.5.2: {}
lru-cache@5.1.1: lru-cache@5.1.1:
dependencies: dependencies:
yallist: 3.1.1 yallist: 3.1.1
@@ -9257,10 +9142,6 @@ snapshots:
mimic-fn@2.1.0: {} mimic-fn@2.1.0: {}
minimatch@10.2.6:
dependencies:
brace-expansion: 5.0.9
minimatch@3.1.5: minimatch@3.1.5:
dependencies: dependencies:
brace-expansion: 1.1.18 brace-expansion: 1.1.18
@@ -9509,11 +9390,6 @@ snapshots:
lru-cache: 10.4.3 lru-cache: 10.4.3
minipass: 7.1.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: {} path-type@4.0.0: {}
pathe@2.0.3: {} 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) 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 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: dependencies:
css-select: 5.2.2 css-select: 5.2.2
css-tree: 1.1.3 css-tree: 1.1.3
react: 18.3.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) 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)): 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: 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) 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 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: dependencies:
html2canvas: 1.4.1 html2canvas: 1.4.1
react: 18.3.1 react: 18.3.1
+7
View File
@@ -3,10 +3,17 @@ packages:
- "packages/*" - "packages/*"
allowBuilds: allowBuilds:
core-js: true
esbuild: true esbuild: true
sharp: true sharp: true
"@esbuild/darwin-arm64": true
"@next/swc-darwin-arm64": true
"@swc/core": true
onlyBuiltDependencies: onlyBuiltDependencies:
- "core-js"
- "@swc/core" - "@swc/core"
- "esbuild" - "esbuild"
- "sharp" - "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);