feat(qr): complete QR studio, dynamic google fonts, mouse canvas drag, and template sync
This commit is contained in:
@@ -7,7 +7,8 @@
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/icon.png",
|
||||
"userInterfaceStyle": "automatic",
|
||||
"newArchEnabled": false,
|
||||
"newArchEnabled": true,
|
||||
"platforms": ["ios", "android"],
|
||||
"splash": {
|
||||
"image": "./assets/splash.png",
|
||||
"resizeMode": "contain",
|
||||
|
||||
@@ -15,6 +15,13 @@
|
||||
"distribution": "internal",
|
||||
"ios": {
|
||||
"image": "latest"
|
||||
},
|
||||
"env": {
|
||||
"EXPO_PUBLIC_SUPABASE_URL": "https://fwahdopsyvryvjaqande.supabase.co",
|
||||
"EXPO_PUBLIC_SUPABASE_ANON_KEY": "sb_publishable_9YsK7YU7xfzxJQsz3x5iMA_LHCqUN3b",
|
||||
"EXPO_PUBLIC_API_URL": "https://api.menul.io",
|
||||
"EXPO_PUBLIC_WEB_URL": "https://menul.io",
|
||||
"EXPO_PUBLIC_REVENUECAT_API_KEY": "appl_uQIvGUuvEthCNheZtcRfVixVhFy"
|
||||
}
|
||||
},
|
||||
"production": {
|
||||
|
||||
@@ -25,9 +25,9 @@
|
||||
"expo-haptics": "^14.0.1",
|
||||
"expo-image-picker": "~16.0.6",
|
||||
"expo-linking": "~7.0.5",
|
||||
"expo-print": "^57.0.1",
|
||||
"expo-print": "~14.0.3",
|
||||
"expo-router": "~4.0.0",
|
||||
"expo-sharing": "^57.0.13",
|
||||
"expo-sharing": "~13.0.1",
|
||||
"expo-splash-screen": "~0.29.24",
|
||||
"expo-status-bar": "~2.0.0",
|
||||
"react": "18.3.1",
|
||||
@@ -35,9 +35,9 @@
|
||||
"react-native-purchases": "^8.2.0",
|
||||
"react-native-safe-area-context": "4.12.0",
|
||||
"react-native-screens": "~4.4.0",
|
||||
"react-native-svg": "^15.15.5",
|
||||
"react-native-svg": "15.8.0",
|
||||
"react-native-url-polyfill": "^2.0.0",
|
||||
"react-native-view-shot": "^5.1.1"
|
||||
"react-native-view-shot": "~4.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.25.2",
|
||||
|
||||
@@ -4,7 +4,7 @@ import { StatusBar } from "expo-status-bar";
|
||||
export default function RootLayout() {
|
||||
return (
|
||||
<>
|
||||
<StatusBar style="dark" />
|
||||
<StatusBar style="light" />
|
||||
<Stack screenOptions={{ headerShown: false }} />
|
||||
</>
|
||||
);
|
||||
|
||||
+589
-160
@@ -3,22 +3,22 @@ import { router } from "expo-router";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
Modal,
|
||||
Image,
|
||||
Pressable,
|
||||
RefreshControl,
|
||||
ScrollView,
|
||||
Share,
|
||||
Switch,
|
||||
Text,
|
||||
TextInput,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
|
||||
import * as Haptics from "expo-haptics";
|
||||
import * as Clipboard from "expo-clipboard";
|
||||
import ViewShot from "react-native-view-shot";
|
||||
import * as Sharing from "expo-sharing";
|
||||
import * as Print from "expo-print";
|
||||
import { QR_STAND_PRESETS, type QrStandPreset } from "@menulio/shared";
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import { api } from "@/lib/api";
|
||||
import { getActiveRestaurant, type ActiveRestaurant } from "@/lib/active-restaurant";
|
||||
import {
|
||||
@@ -29,7 +29,9 @@ import {
|
||||
import {
|
||||
QrCanvasStudio,
|
||||
getDefaultLayersForTemplate,
|
||||
getDefaultQrConfigForTemplate,
|
||||
type CanvasTextLayer,
|
||||
type CanvasQrConfig,
|
||||
} from "@/components/qr-canvas";
|
||||
|
||||
const HIDE_ALL_TEMPLATE_FIELDS: QrCustomFields = {
|
||||
@@ -48,95 +50,290 @@ interface QrResponse {
|
||||
pngBase64: string;
|
||||
}
|
||||
|
||||
function getPresetAspectRatio(preset?: QrStandPreset | null): number {
|
||||
if (!preset) return 1;
|
||||
if (preset.aspectRatio) {
|
||||
const parts = preset.aspectRatio.split(/[/:]/).map(Number);
|
||||
if (parts[0] && parts[1]) {
|
||||
return parts[0] / parts[1];
|
||||
}
|
||||
}
|
||||
if (TEMPLATE_ASPECT_RATIOS[preset.key]) {
|
||||
return TEMPLATE_ASPECT_RATIOS[preset.key];
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
export default function QrScreen() {
|
||||
const insets = useSafeAreaInsets();
|
||||
const [active, setActive] = useState<ActiveRestaurant | null>(null);
|
||||
const [restaurantName, setRestaurantName] = useState<string>("Gourmet Bistro");
|
||||
const [qr, setQr] = useState<QrResponse | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const availablePresets = Object.values(QR_STAND_PRESETS).filter(
|
||||
(p) => !!COMPONENT_TEMPLATES[p.key]
|
||||
const [scrollEnabled, setScrollEnabled] = useState(true);
|
||||
|
||||
// Dinamik Şablon Listesi (API'den ve yerelden)
|
||||
const [presets, setPresets] = useState<QrStandPreset[]>(() =>
|
||||
Object.values(QR_STAND_PRESETS)
|
||||
);
|
||||
|
||||
const [selectedKey, setSelectedKey] = useState<string>(
|
||||
availablePresets[0]?.key || "qr-1"
|
||||
presets[0]?.key || "qr-1"
|
||||
);
|
||||
const [exportingPng, setExportingPng] = useState(false);
|
||||
const [exportingPdf, setExportingPdf] = useState(false);
|
||||
|
||||
// Instagram Story / Canva Tarzı Katmanlar
|
||||
// Aktif Şablon ve Oran Hesaplama (Şablondaki Dinamik Aspect Ratio Öncelikli)
|
||||
const currentPreset = presets.find((p) => p.key === selectedKey) || presets[0];
|
||||
const aspectRatio = getPresetAspectRatio(currentPreset);
|
||||
const displayWidth = 290;
|
||||
const displayHeight = Math.round(displayWidth / aspectRatio);
|
||||
|
||||
// Instagram Story / Canva Tarzı Katmanlar & QR Konfigürasyonu
|
||||
const [layers, setLayers] = useState<CanvasTextLayer[]>(() =>
|
||||
getDefaultLayersForTemplate("qr-1", "Gourmet Bistro")
|
||||
);
|
||||
const [qrConfig, setQrConfig] = useState<CanvasQrConfig>(() =>
|
||||
getDefaultQrConfigForTemplate("qr-1", 290, 290)
|
||||
);
|
||||
|
||||
const [selectedLayerId, setSelectedLayerId] = useState<string | null>(null);
|
||||
const [isQrSelected, setIsQrSelected] = useState<boolean>(false);
|
||||
|
||||
// Şablonlar Arasında Düzenlemeleri Koruyan Önbellek
|
||||
const templateCacheRef = useRef<
|
||||
Record<string, { layers: CanvasTextLayer[]; qrConfig: CanvasQrConfig }>
|
||||
>({});
|
||||
|
||||
const handleLayersChange = (updater: React.SetStateAction<CanvasTextLayer[]>) => {
|
||||
setLayers((prev) => {
|
||||
const next = typeof updater === "function" ? updater(prev) : updater;
|
||||
templateCacheRef.current[selectedKey] = {
|
||||
layers: next,
|
||||
qrConfig: templateCacheRef.current[selectedKey]?.qrConfig || qrConfig,
|
||||
};
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const handleQrConfigChange = (updater: React.SetStateAction<CanvasQrConfig>) => {
|
||||
setQrConfig((prev) => {
|
||||
const next = typeof updater === "function" ? updater(prev) : updater;
|
||||
templateCacheRef.current[selectedKey] = {
|
||||
layers: templateCacheRef.current[selectedKey]?.layers || layers,
|
||||
qrConfig: next,
|
||||
};
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const viewShotRef = useRef<any>(null);
|
||||
|
||||
const activePreset: QrStandPreset =
|
||||
QR_STAND_PRESETS[selectedKey] || availablePresets[0] || Object.values(QR_STAND_PRESETS)[0]!;
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [syncing, setSyncing] = useState(false);
|
||||
|
||||
// Component-based template sistemi
|
||||
const TemplateComponent =
|
||||
COMPONENT_TEMPLATES[selectedKey] ?? Object.values(COMPONENT_TEMPLATES)[0];
|
||||
const aspectRatio = TEMPLATE_ASPECT_RATIOS[selectedKey] ?? 1;
|
||||
const displayWidth = 300;
|
||||
const displayHeight = Math.round(displayWidth / aspectRatio);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const activeRest = await getActiveRestaurant();
|
||||
if (!activeRest) return;
|
||||
setActive(activeRest);
|
||||
// Şablon ve QR Verilerini Çek / Senkronize Et
|
||||
const fetchTemplatesAndQr = async (isManualRefresh = false) => {
|
||||
if (isManualRefresh) {
|
||||
setRefreshing(true);
|
||||
setSyncing(true);
|
||||
try { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); } catch {}
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. Supabase ve API'den Dinamik Şablonları Çek
|
||||
let fetchedPresets: QrStandPreset[] = [];
|
||||
try {
|
||||
const data = await api.post<QrResponse>(`/restaurants/${activeRest.restaurantId}/qr`);
|
||||
setQr(data);
|
||||
const { data: dbTemplates, error: dbErr } = await supabase
|
||||
.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 {
|
||||
const qrData = await api.post<QrResponse>(`/restaurants/${activeRest.restaurantId}/qr`);
|
||||
setQr(qrData);
|
||||
|
||||
const restDetail = await api.get<{ name?: string }>(`/restaurants/${activeRest.restaurantId}`);
|
||||
if (restDetail?.name) {
|
||||
currentRestName = restDetail.name;
|
||||
setRestaurantName(restDetail.name);
|
||||
setLayers((prev) =>
|
||||
prev.map((l) =>
|
||||
l.id === "rName" ? { ...l, text: restDetail.name!.toUpperCase() } : l
|
||||
)
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// fallback
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "QR oluşturulamadı");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
} catch {}
|
||||
}
|
||||
})();
|
||||
|
||||
if (fetchedPresets.length > 0) {
|
||||
setPresets(fetchedPresets);
|
||||
|
||||
// Eğer mevcut seçili şablon silinmişse ilk geçerli şablona geç
|
||||
let effectiveKey = selectedKey;
|
||||
const exists = fetchedPresets.some((p) => p.key === selectedKey);
|
||||
if (!exists) {
|
||||
effectiveKey = fetchedPresets[0].key;
|
||||
setSelectedKey(effectiveKey);
|
||||
}
|
||||
|
||||
// Önbellekten silinen şablonları temizle
|
||||
const validKeys = new Set(fetchedPresets.map((p) => p.key));
|
||||
Object.keys(templateCacheRef.current).forEach((k) => {
|
||||
if (!validKeys.has(k)) {
|
||||
delete templateCacheRef.current[k];
|
||||
}
|
||||
});
|
||||
|
||||
// Aktif seçili şablonu yeni verilerle güncelle
|
||||
const activeTpl = fetchedPresets.find((p) => p.key === effectiveKey) || fetchedPresets[0];
|
||||
if (activeTpl) {
|
||||
const pRatio = getPresetAspectRatio(activeTpl);
|
||||
const calcHeight = Math.round(displayWidth / (pRatio || 1));
|
||||
|
||||
let newLayers: CanvasTextLayer[];
|
||||
if ((activeTpl as any).layers && (activeTpl as any).layers.length > 0) {
|
||||
newLayers = (activeTpl as any).layers.map((l: any) => ({
|
||||
id: l.id || `layer-${Math.random()}`,
|
||||
text: l.text === "{{RESTAURANT_NAME}}" ? currentRestName.toUpperCase() : l.text,
|
||||
x: l.x ?? (displayWidth - 200) / 2,
|
||||
y: l.y ?? Math.round((calcHeight * (l.yPercent || 15)) / 100),
|
||||
rotation: l.rotation || 0,
|
||||
fontSize: l.fontSize || 16,
|
||||
fontId: l.fontFamily ? l.fontFamily.split(",")[0].replace(/['"]/g, "").trim() : "serif",
|
||||
color: l.color || activeTpl.accentText || "#D4AF37",
|
||||
textAlign: l.textAlign || "center",
|
||||
bgStyle: l.bgStyle || "none",
|
||||
letterSpacing: l.letterSpacing || 1,
|
||||
}));
|
||||
} else {
|
||||
newLayers = getDefaultLayersForTemplate(activeTpl.key, currentRestName);
|
||||
}
|
||||
|
||||
let newQr: CanvasQrConfig;
|
||||
if ((activeTpl as any).qrConfig) {
|
||||
const c = (activeTpl as any).qrConfig;
|
||||
newQr = {
|
||||
x: Math.round(((displayWidth - (c.size || 115)) * (c.xPercent || 50)) / 100),
|
||||
y: Math.round(((calcHeight - (c.size || 115)) * (c.yPercent || 38)) / 100),
|
||||
size: c.size || 115,
|
||||
borderRadius: c.borderRadius || 14,
|
||||
borderWidth: c.borderWidth || 2,
|
||||
borderColor: c.borderColor || activeTpl.borderAccent || "#D4AF37",
|
||||
padding: c.padding || 8,
|
||||
bgBoxColor: c.bgBoxColor || "#FFFFFF",
|
||||
};
|
||||
} else {
|
||||
newQr = getDefaultQrConfigForTemplate(activeTpl.key, displayWidth, calcHeight);
|
||||
}
|
||||
|
||||
setLayers(newLayers);
|
||||
setQrConfig(newQr);
|
||||
|
||||
// Önbelleği de taze veriyle güncelle
|
||||
templateCacheRef.current[activeTpl.key] = {
|
||||
layers: newLayers,
|
||||
qrConfig: newQr,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (isManualRefresh) {
|
||||
try { Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); } catch {}
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "QR yüklenemedi");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
setSyncing(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchTemplatesAndQr(false);
|
||||
|
||||
// Supabase Realtime Değişiklik Aboneliği (Admin kaydettiğinde anında güncelle)
|
||||
const channel = supabase
|
||||
.channel("public:qr_stand_templates")
|
||||
.on(
|
||||
"postgres_changes",
|
||||
{ event: "*", schema: "public", table: "qr_stand_templates" },
|
||||
() => {
|
||||
fetchTemplatesAndQr(false);
|
||||
}
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
return () => {
|
||||
supabase.removeChannel(channel);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
|
||||
|
||||
async function handleCopy() {
|
||||
if (!qr?.targetUrl) return;
|
||||
await Clipboard.setStringAsync(qr.targetUrl);
|
||||
try {
|
||||
await Clipboard.setStringAsync(qr.targetUrl);
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
||||
} catch {}
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}
|
||||
|
||||
async function handleShareQuick() {
|
||||
if (!qr?.targetUrl) return;
|
||||
await Share.share({
|
||||
title: "Dijital QR Menü",
|
||||
message: `${qr.targetUrl}\nMenümüzü incelemek için QR kodu taratın veya linke tıklayın.`,
|
||||
url: qr.targetUrl,
|
||||
});
|
||||
try {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
|
||||
await Share.share({
|
||||
title: "Dijital QR Menü",
|
||||
message: `${qr.targetUrl}\nMenümüzü incelemek için QR kodu taratın veya linke tıklayın.`,
|
||||
url: qr.targetUrl,
|
||||
});
|
||||
} catch {}
|
||||
}
|
||||
|
||||
async function handleExportPng() {
|
||||
if (!viewShotRef.current) return;
|
||||
setSelectedLayerId(null);
|
||||
setIsQrSelected(false);
|
||||
setExportingPng(true);
|
||||
try {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
|
||||
await new Promise((r) => setTimeout(r, 120));
|
||||
const uri = await viewShotRef.current.capture?.();
|
||||
if (uri) {
|
||||
await Sharing.shareAsync(uri, {
|
||||
@@ -152,10 +349,13 @@ export default function QrScreen() {
|
||||
}
|
||||
|
||||
async function handleExportPdf() {
|
||||
if (!qr || !active) return;
|
||||
if (!qr || !active || !viewShotRef.current) return;
|
||||
setSelectedLayerId(null);
|
||||
setIsQrSelected(false);
|
||||
setExportingPdf(true);
|
||||
try {
|
||||
// ViewShot ile önce PNG al, sonra A5 HTML içine göm
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy);
|
||||
await new Promise((r) => setTimeout(r, 120));
|
||||
const pngUri = await viewShotRef.current?.capture();
|
||||
if (!pngUri) throw new Error("Görsel alınamadı");
|
||||
|
||||
@@ -212,26 +412,30 @@ export default function QrScreen() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<View style={{ flex: 1, backgroundColor: "#FAF8F5", alignItems: "center", justifyContent: "center" }}>
|
||||
<ActivityIndicator size="large" color="#C8A96B" />
|
||||
<Text style={{ marginTop: 12, color: "#78716C", fontSize: 14 }}>QR Kod ve Şablonlar Hazırlanıyor...</Text>
|
||||
<View style={{ flex: 1, backgroundColor: "#0C0C0E", alignItems: "center", justifyContent: "center" }}>
|
||||
<ActivityIndicator size="large" color="#D4AF37" />
|
||||
<Text style={{ marginTop: 14, color: "#A1A1AA", fontSize: 14, fontWeight: "600" }}>
|
||||
QR Stant Stüdyosu Hazırlanıyor...
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !qr) {
|
||||
return (
|
||||
<View style={{ flex: 1, backgroundColor: "#FAF8F5", alignItems: "center", justifyContent: "center", padding: 24 }}>
|
||||
<Ionicons name="alert-circle-outline" size={48} color="#DC2626" style={{ marginBottom: 12 }} />
|
||||
<Text style={{ color: "#DC2626", fontSize: 16, fontWeight: "600", textAlign: "center" }}>
|
||||
<View style={{ flex: 1, backgroundColor: "#0C0C0E", alignItems: "center", justifyContent: "center", padding: 24 }}>
|
||||
<Ionicons name="alert-circle-outline" size={48} color="#EF4444" style={{ marginBottom: 12 }} />
|
||||
<Text style={{ color: "#EF4444", fontSize: 16, fontWeight: "600", textAlign: "center" }}>
|
||||
{error ?? "QR kod yüklenemedi"}
|
||||
</Text>
|
||||
<Pressable
|
||||
onPress={() => router.back()}
|
||||
style={{ marginTop: 20, backgroundColor: "#1C1917", paddingHorizontal: 20, paddingVertical: 10, borderRadius: 8 }}
|
||||
accessible={true}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Geri Dön"
|
||||
style={{ marginTop: 20, backgroundColor: "#242429", paddingHorizontal: 20, paddingVertical: 12, borderRadius: 10, minHeight: 44, justifyContent: "center" }}
|
||||
>
|
||||
<Text style={{ color: "#FFF", fontWeight: "600" }}>Geri Dön</Text>
|
||||
</Pressable>
|
||||
@@ -240,79 +444,221 @@ export default function QrScreen() {
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView contentContainerStyle={{ flexGrow: 1, backgroundColor: "#0C0C0E", paddingVertical: 20 }}>
|
||||
<View style={{ width: "100%", maxWidth: 440, alignSelf: "center", paddingHorizontal: 20 }}>
|
||||
{/* Header Badge */}
|
||||
<ScrollView
|
||||
scrollEnabled={scrollEnabled}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={() => fetchTemplatesAndQr(true)}
|
||||
tintColor="#D4AF37"
|
||||
colors={["#D4AF37"]}
|
||||
/>
|
||||
}
|
||||
contentContainerStyle={{
|
||||
flexGrow: 1,
|
||||
backgroundColor: "#0C0C0E",
|
||||
paddingTop: insets.top + 8,
|
||||
paddingBottom: insets.bottom + 40,
|
||||
}}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
keyboardDismissMode="on-drag"
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<View style={{ width: "100%", maxWidth: 440, alignSelf: "center", paddingHorizontal: 16 }}>
|
||||
{/* Üst Navigasyon & Başlık Çubuğu */}
|
||||
<View
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
backgroundColor: "rgba(212, 175, 55, 0.15)",
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 99,
|
||||
marginBottom: 12,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
borderWidth: 1,
|
||||
borderColor: "rgba(212, 175, 55, 0.3)",
|
||||
justifyContent: "space-between",
|
||||
marginBottom: 12,
|
||||
}}
|
||||
>
|
||||
<Ionicons name="qr-code-outline" size={15} color="#D4AF37" />
|
||||
<Text style={{ color: "#D4AF37", fontSize: 12, fontWeight: "700" }}>
|
||||
MASA QR STANT JENERATÖRÜ
|
||||
</Text>
|
||||
{/* Geri Dön Butonu */}
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
try { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); } catch {}
|
||||
router.back();
|
||||
}}
|
||||
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
|
||||
accessible={true}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Geri Dön"
|
||||
style={({ pressed }) => ({
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 13,
|
||||
backgroundColor: "#18181B",
|
||||
borderWidth: 1,
|
||||
borderColor: "rgba(255,255,255,0.08)",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
opacity: pressed ? 0.75 : 1,
|
||||
})}
|
||||
>
|
||||
<Ionicons name="chevron-back" size={20} color="#FFFFFF" />
|
||||
</Pressable>
|
||||
|
||||
{/* Stüdyo Rozeti */}
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: "rgba(212, 175, 55, 0.12)",
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 99,
|
||||
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>
|
||||
|
||||
<Text style={{ fontSize: 24, fontWeight: "800", color: "#FFFFFF", marginBottom: 4, textAlign: "center" }}>
|
||||
Masa Stant Tasarımları
|
||||
</Text>
|
||||
<Text style={{ fontSize: 13, color: "#A1A1AA", textAlign: "center", marginBottom: 20, lineHeight: 18 }}>
|
||||
Şablon seçin; QR kodunuz otomatik yerleşsin. Baskıya hazır A5 PDF veya PNG olarak indirin.
|
||||
</Text>
|
||||
|
||||
{/* Horizontal Theme Selector */}
|
||||
{availablePresets.length > 1 && (
|
||||
{/* Yatay Şablon Seçici Bar */}
|
||||
{presets.length > 0 && (
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={{ gap: 10, paddingHorizontal: 4, marginBottom: 20 }}
|
||||
style={{ flexGrow: 0, marginBottom: 12 }}
|
||||
contentContainerStyle={{ gap: 8, paddingHorizontal: 2, alignItems: "center" }}
|
||||
>
|
||||
{availablePresets.map((preset) => {
|
||||
{presets.map((preset) => {
|
||||
const isSelected = preset.key === selectedKey;
|
||||
const pRatio = getPresetAspectRatio(preset);
|
||||
const calcHeight = Math.round(displayWidth / (pRatio || 1));
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
key={preset.key}
|
||||
onPress={() => {
|
||||
setSelectedKey(preset.key);
|
||||
setLayers(getDefaultLayersForTemplate(preset.key, restaurantName));
|
||||
try { Haptics.selectionAsync(); } catch {}
|
||||
|
||||
// 1. Mevcut şablonun son durumunu önbelleğe kaydet
|
||||
templateCacheRef.current[selectedKey] = {
|
||||
layers,
|
||||
qrConfig,
|
||||
};
|
||||
|
||||
const nextKey = preset.key;
|
||||
setSelectedKey(nextKey);
|
||||
|
||||
// 2. Daha önce düzenlenmişse önbellekten geri yükle
|
||||
if (templateCacheRef.current[nextKey]) {
|
||||
setLayers(templateCacheRef.current[nextKey].layers);
|
||||
setQrConfig(templateCacheRef.current[nextKey].qrConfig);
|
||||
} else {
|
||||
// İlk defa açılıyorsa başlat
|
||||
let initialLayers: CanvasTextLayer[];
|
||||
if ((preset as any).layers && (preset as any).layers.length > 0) {
|
||||
initialLayers = (preset as any).layers.map((l: any) => ({
|
||||
id: l.id || `layer-${Math.random()}`,
|
||||
text: l.text === "{{RESTAURANT_NAME}}" ? restaurantName.toUpperCase() : l.text,
|
||||
x: l.x ?? (displayWidth - 200) / 2,
|
||||
y: l.y ?? Math.round((calcHeight * (l.yPercent || 15)) / 100),
|
||||
rotation: l.rotation || 0,
|
||||
fontSize: l.fontSize || 16,
|
||||
fontId: l.fontId || "serif",
|
||||
color: l.color || "#D4AF37",
|
||||
textAlign: l.textAlign || "center",
|
||||
bgStyle: l.bgStyle || "none",
|
||||
letterSpacing: l.letterSpacing || 1,
|
||||
}));
|
||||
} else {
|
||||
initialLayers = getDefaultLayersForTemplate(nextKey, restaurantName);
|
||||
}
|
||||
|
||||
let initialQr: CanvasQrConfig;
|
||||
if ((preset as any).qrConfig) {
|
||||
const c = (preset as any).qrConfig;
|
||||
initialQr = {
|
||||
x: Math.round(((displayWidth - (c.size || 115)) * (c.xPercent || 50)) / 100),
|
||||
y: Math.round(((calcHeight - (c.size || 115)) * (c.yPercent || 38)) / 100),
|
||||
size: c.size || 115,
|
||||
borderRadius: c.borderRadius || 14,
|
||||
borderWidth: c.borderWidth || 2,
|
||||
borderColor: c.borderColor || preset.borderAccent || "#D4AF37",
|
||||
padding: c.padding || 8,
|
||||
bgBoxColor: c.bgBoxColor || "#FFFFFF",
|
||||
};
|
||||
} else {
|
||||
initialQr = getDefaultQrConfigForTemplate(nextKey, displayWidth, calcHeight);
|
||||
}
|
||||
|
||||
setLayers(initialLayers);
|
||||
setQrConfig(initialQr);
|
||||
templateCacheRef.current[nextKey] = {
|
||||
layers: initialLayers,
|
||||
qrConfig: initialQr,
|
||||
};
|
||||
}
|
||||
|
||||
setSelectedLayerId(null);
|
||||
setIsQrSelected(false);
|
||||
}}
|
||||
style={{
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 10,
|
||||
borderRadius: 14,
|
||||
backgroundColor: isSelected ? "#D4AF37" : "#1A1A1E",
|
||||
hitSlop={{ top: 6, bottom: 6, left: 4, right: 4 }}
|
||||
accessible={true}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={`${preset.name} şablonunu seç`}
|
||||
accessibilityState={{ selected: isSelected }}
|
||||
style={({ pressed }) => ({
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 8,
|
||||
minHeight: 36,
|
||||
borderRadius: 12,
|
||||
backgroundColor: isSelected ? "#D4AF37" : "#18181B",
|
||||
borderWidth: 1.5,
|
||||
borderColor: isSelected ? "#D4AF37" : "#2B2B32",
|
||||
borderColor: isSelected ? "#D4AF37" : "rgba(255,255,255,0.12)",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
}}
|
||||
gap: 6,
|
||||
opacity: pressed ? 0.85 : 1,
|
||||
})}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 14,
|
||||
height: 14,
|
||||
borderRadius: 7,
|
||||
backgroundColor: preset.plaqueColor || "#1A1A1E",
|
||||
width: 10,
|
||||
height: 10,
|
||||
borderRadius: 5,
|
||||
backgroundColor: preset.plaqueColor || "#18181B",
|
||||
borderWidth: 1,
|
||||
borderColor: "rgba(255,255,255,0.4)",
|
||||
borderColor: isSelected ? "#0C0C0E" : "rgba(255,255,255,0.5)",
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 13,
|
||||
fontSize: 12,
|
||||
fontWeight: "700",
|
||||
color: isSelected ? "#0C0C0E" : "#FFFFFF",
|
||||
}}
|
||||
@@ -325,53 +671,123 @@ export default function QrScreen() {
|
||||
</ScrollView>
|
||||
)}
|
||||
|
||||
{/* İnteraktif Instagram / Canva Tarzı QR Stant Stüdyosu */}
|
||||
<View
|
||||
style={{
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: "100%",
|
||||
marginBottom: 20,
|
||||
}}
|
||||
>
|
||||
<ViewShot ref={viewShotRef} options={{ format: "png", quality: 1.0 }}>
|
||||
<QrCanvasStudio
|
||||
templateKey={selectedKey}
|
||||
restaurantName={restaurantName}
|
||||
qrBase64={qr.pngBase64}
|
||||
width={displayWidth}
|
||||
height={displayHeight}
|
||||
renderBackground={() =>
|
||||
TemplateComponent ? (
|
||||
<TemplateComponent
|
||||
qrBase64={qr.pngBase64}
|
||||
width={displayWidth}
|
||||
height={displayHeight}
|
||||
fields={HIDE_ALL_TEMPLATE_FIELDS}
|
||||
{/* İnteraktif Instagram QR & Canva Stüdyosu */}
|
||||
<QrCanvasStudio
|
||||
templateKey={selectedKey}
|
||||
restaurantName={restaurantName}
|
||||
qrBase64={qr.pngBase64}
|
||||
width={displayWidth}
|
||||
height={displayHeight}
|
||||
renderBackground={() => {
|
||||
const cur = presets.find((p) => p.key === selectedKey);
|
||||
const bgImg = (cur as any)?.bgImageUrl;
|
||||
|
||||
// 1. Eğer şablona özel bir Arka Plan Görseli / SVG yüklenmişse bunu render et
|
||||
if (bgImg) {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width: displayWidth,
|
||||
height: displayHeight,
|
||||
backgroundColor: cur?.plaqueColor || "#132034",
|
||||
borderRadius: 24,
|
||||
overflow: "hidden",
|
||||
borderWidth: 2,
|
||||
borderColor: cur?.borderAccent || "#D4AF37",
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
source={{ uri: bgImg }}
|
||||
style={{ width: "100%", height: "100%", position: "absolute" }}
|
||||
resizeMode="cover"
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
layers={layers}
|
||||
onLayersChange={setLayers}
|
||||
/>
|
||||
</ViewShot>
|
||||
</View>
|
||||
{/* İç Dekoratif Çerçeve */}
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 10,
|
||||
left: 10,
|
||||
right: 10,
|
||||
bottom: 10,
|
||||
borderWidth: 1,
|
||||
borderColor: cur?.dividerColor || "rgba(212, 175, 55, 0.3)",
|
||||
borderRadius: 16,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Özel görsel yoksa ve Yerel Hardcoded Component varsa onu kullan
|
||||
if (COMPONENT_TEMPLATES[selectedKey]) {
|
||||
const Comp = COMPONENT_TEMPLATES[selectedKey];
|
||||
return (
|
||||
<Comp
|
||||
qrBase64={qr.pngBase64}
|
||||
width={displayWidth}
|
||||
height={displayHeight}
|
||||
fields={HIDE_ALL_TEMPLATE_FIELDS}
|
||||
hideQr={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 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)}
|
||||
/>
|
||||
|
||||
|
||||
|
||||
{/* Output Action Buttons */}
|
||||
<View style={{ width: "100%", gap: 10 }}>
|
||||
{/* PDF Download Button */}
|
||||
{/* Aksiyon Butonları Grubu */}
|
||||
<View style={{ width: "100%", gap: 10, marginTop: 4 }}>
|
||||
{/* PDF İndirme Butonu */}
|
||||
<Pressable
|
||||
onPress={handleExportPdf}
|
||||
disabled={exportingPdf}
|
||||
accessible={true}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="PDF Olarak İndir (A5 Matbaa Baskı)"
|
||||
style={({ pressed }) => ({
|
||||
backgroundColor: "#D4AF37",
|
||||
borderRadius: 14,
|
||||
padding: 16,
|
||||
minHeight: 52,
|
||||
padding: 15,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
@@ -379,9 +795,9 @@ export default function QrScreen() {
|
||||
opacity: pressed || exportingPdf ? 0.85 : 1,
|
||||
shadowColor: "#D4AF37",
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.2,
|
||||
shadowOpacity: 0.25,
|
||||
shadowRadius: 8,
|
||||
elevation: 3,
|
||||
elevation: 4,
|
||||
})}
|
||||
>
|
||||
{exportingPdf ? (
|
||||
@@ -396,16 +812,20 @@ export default function QrScreen() {
|
||||
)}
|
||||
</Pressable>
|
||||
|
||||
{/* PNG Download Button */}
|
||||
{/* PNG İndirme Butonu */}
|
||||
<Pressable
|
||||
onPress={handleExportPng}
|
||||
disabled={exportingPng}
|
||||
accessible={true}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="PNG Olarak Kaydet veya Paylaş"
|
||||
style={({ pressed }) => ({
|
||||
backgroundColor: "#1A1A1E",
|
||||
backgroundColor: "#18181B",
|
||||
borderWidth: 1.5,
|
||||
borderColor: "#2B2B32",
|
||||
borderColor: "rgba(255,255,255,0.12)",
|
||||
borderRadius: 14,
|
||||
padding: 16,
|
||||
minHeight: 48,
|
||||
padding: 14,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
@@ -417,23 +837,27 @@ export default function QrScreen() {
|
||||
<ActivityIndicator size="small" color="#FFFFFF" />
|
||||
) : (
|
||||
<>
|
||||
<Ionicons name="image-outline" size={20} color="#FFFFFF" />
|
||||
<Text style={{ color: "#FFFFFF", fontSize: 15, fontWeight: "700" }}>
|
||||
<Ionicons name="image-outline" size={19} color="#FFFFFF" />
|
||||
<Text style={{ color: "#FFFFFF", fontSize: 14, fontWeight: "700" }}>
|
||||
PNG Olarak Kaydet / Paylaş
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</Pressable>
|
||||
|
||||
{/* Direct Link Share Button */}
|
||||
{/* Menü Linki Paylaş */}
|
||||
<Pressable
|
||||
onPress={handleShareQuick}
|
||||
accessible={true}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Menü Linkini Paylaş"
|
||||
style={({ pressed }) => ({
|
||||
backgroundColor: "#1A1A1E",
|
||||
backgroundColor: "#18181B",
|
||||
borderWidth: 1,
|
||||
borderColor: "#2B2B32",
|
||||
borderColor: "rgba(255,255,255,0.08)",
|
||||
borderRadius: 14,
|
||||
padding: 14,
|
||||
minHeight: 44,
|
||||
padding: 13,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
@@ -441,28 +865,33 @@ export default function QrScreen() {
|
||||
opacity: pressed ? 0.85 : 1,
|
||||
})}
|
||||
>
|
||||
<Ionicons name="share-outline" size={18} color="#D4AF37" />
|
||||
<Text style={{ color: "#D4AF37", fontSize: 14, fontWeight: "700" }}>
|
||||
<Ionicons name="share-outline" size={17} color="#D4AF37" />
|
||||
<Text style={{ color: "#D4AF37", fontSize: 13.5, fontWeight: "700" }}>
|
||||
Menü Linkini Paylaş
|
||||
</Text>
|
||||
</Pressable>
|
||||
|
||||
{/* Quick Copy Link Box */}
|
||||
{/* Hızlı Kopyalama Kutusu */}
|
||||
<Pressable
|
||||
onPress={handleCopy}
|
||||
style={{
|
||||
backgroundColor: "#141417",
|
||||
accessible={true}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={`Menü linkini kopyala: ${qr.targetUrl}`}
|
||||
style={({ pressed }) => ({
|
||||
backgroundColor: "#121215",
|
||||
borderWidth: 1,
|
||||
borderColor: "#242429",
|
||||
borderRadius: 14,
|
||||
padding: 14,
|
||||
borderColor: "rgba(255,255,255,0.08)",
|
||||
borderRadius: 12,
|
||||
padding: 12,
|
||||
minHeight: 44,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
marginTop: 4,
|
||||
}}
|
||||
marginTop: 2,
|
||||
opacity: pressed ? 0.85 : 1,
|
||||
})}
|
||||
>
|
||||
<Text style={{ color: "#A1A1AA", fontSize: 12, flex: 1 }} numberOfLines={1}>
|
||||
<Text style={{ color: "#A1A1AA", fontSize: 11.5, flex: 1 }} numberOfLines={1}>
|
||||
{qr.targetUrl}
|
||||
</Text>
|
||||
<Ionicons
|
||||
|
||||
@@ -6,14 +6,19 @@ import {
|
||||
View,
|
||||
} from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import * as Haptics from "expo-haptics";
|
||||
import { type CanvasTextLayer, getCanvasFontFamily } from "./types";
|
||||
|
||||
interface CanvasLayerItemProps {
|
||||
layer: CanvasTextLayer;
|
||||
isSelected: boolean;
|
||||
onSelect: (id: string) => void;
|
||||
onEdit: (layer: CanvasTextLayer) => void;
|
||||
onUpdatePosition: (id: string, x: number, y: number) => void;
|
||||
onDelete: (id: string) => void;
|
||||
onDragStart?: () => void;
|
||||
onDragMove?: (rawX: number, rawY: number, itemWidth: number, itemHeight: number) => { snappedX: number; snappedY: number };
|
||||
onDragEnd?: (finalX: number, finalY: number, itemWidth: number, itemHeight: number) => void;
|
||||
canvasWidth: number;
|
||||
canvasHeight: number;
|
||||
}
|
||||
@@ -22,95 +27,213 @@ export const CanvasLayerItem: React.FC<CanvasLayerItemProps> = ({
|
||||
layer,
|
||||
isSelected,
|
||||
onSelect,
|
||||
onEdit,
|
||||
onUpdatePosition,
|
||||
onDelete,
|
||||
onDragStart,
|
||||
onDragMove,
|
||||
onDragEnd,
|
||||
canvasWidth,
|
||||
canvasHeight,
|
||||
}) => {
|
||||
const layerRef = useRef(layer);
|
||||
layerRef.current = layer;
|
||||
|
||||
const canvasWidthRef = useRef(canvasWidth);
|
||||
canvasWidthRef.current = canvasWidth;
|
||||
|
||||
const canvasHeightRef = useRef(canvasHeight);
|
||||
canvasHeightRef.current = canvasHeight;
|
||||
|
||||
const panStartRef = useRef({ x: layer.x, y: layer.y });
|
||||
const layoutSizeRef = useRef({ width: 70, height: 26 });
|
||||
const currentPosRef = useRef({ x: layer.x, y: layer.y });
|
||||
const lastTapRef = useRef<number>(0);
|
||||
const isDraggingRef = useRef(false);
|
||||
|
||||
const panResponder = useRef(
|
||||
PanResponder.create({
|
||||
onStartShouldSetPanResponder: () => true,
|
||||
onMoveShouldSetPanResponder: () => true,
|
||||
onStartShouldSetPanResponderCapture: () => true,
|
||||
onMoveShouldSetPanResponder: (_evt, gestureState) =>
|
||||
Math.abs(gestureState.dx) > 1 || Math.abs(gestureState.dy) > 1,
|
||||
onMoveShouldSetPanResponderCapture: (_evt, gestureState) =>
|
||||
Math.abs(gestureState.dx) > 1 || Math.abs(gestureState.dy) > 1,
|
||||
onPanResponderTerminationRequest: () => false,
|
||||
|
||||
onPanResponderGrant: () => {
|
||||
onSelect(layer.id);
|
||||
panStartRef.current = { x: layer.x, y: layer.y };
|
||||
const now = Date.now();
|
||||
const DOUBLE_TAP_DELAY = 300;
|
||||
const curLayer = layerRef.current;
|
||||
if (now - lastTapRef.current < DOUBLE_TAP_DELAY) {
|
||||
try {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
|
||||
} catch {}
|
||||
onEdit(curLayer);
|
||||
} else {
|
||||
onSelect(curLayer.id);
|
||||
}
|
||||
lastTapRef.current = now;
|
||||
|
||||
panStartRef.current = { x: curLayer.x, y: curLayer.y };
|
||||
currentPosRef.current = { x: curLayer.x, y: curLayer.y };
|
||||
isDraggingRef.current = false;
|
||||
onDragStart?.();
|
||||
},
|
||||
|
||||
onPanResponderMove: (_evt, gestureState) => {
|
||||
const newX = Math.max(0, Math.min(canvasWidth - 40, panStartRef.current.x + gestureState.dx));
|
||||
const newY = Math.max(0, Math.min(canvasHeight - 30, panStartRef.current.y + gestureState.dy));
|
||||
onUpdatePosition(layer.id, Math.round(newX), Math.round(newY));
|
||||
if (!isDraggingRef.current && (Math.abs(gestureState.dx) > 2 || Math.abs(gestureState.dy) > 2)) {
|
||||
isDraggingRef.current = true;
|
||||
}
|
||||
|
||||
const curLayer = layerRef.current;
|
||||
const itemW = layoutSizeRef.current.width;
|
||||
const itemH = layoutSizeRef.current.height;
|
||||
const cWidth = canvasWidthRef.current;
|
||||
const cHeight = canvasHeightRef.current;
|
||||
|
||||
const maxX = Math.max(0, cWidth - itemW);
|
||||
const maxY = Math.max(0, cHeight - itemH);
|
||||
const rawX = Math.max(0, Math.min(maxX, panStartRef.current.x + gestureState.dx));
|
||||
const rawY = Math.max(0, Math.min(maxY, panStartRef.current.y + gestureState.dy));
|
||||
|
||||
if (onDragMove) {
|
||||
const { snappedX, snappedY } = onDragMove(rawX, rawY, itemW, itemH);
|
||||
currentPosRef.current = { x: snappedX, y: snappedY };
|
||||
onUpdatePosition(curLayer.id, snappedX, snappedY);
|
||||
} else {
|
||||
const sx = Math.round(rawX);
|
||||
const sy = Math.round(rawY);
|
||||
currentPosRef.current = { x: sx, y: sy };
|
||||
onUpdatePosition(curLayer.id, sx, sy);
|
||||
}
|
||||
},
|
||||
|
||||
onPanResponderRelease: () => {
|
||||
const itemW = layoutSizeRef.current.width;
|
||||
const itemH = layoutSizeRef.current.height;
|
||||
onDragEnd?.(currentPosRef.current.x, currentPosRef.current.y, itemW, itemH);
|
||||
},
|
||||
|
||||
onPanResponderTerminate: () => {
|
||||
const itemW = layoutSizeRef.current.width;
|
||||
const itemH = layoutSizeRef.current.height;
|
||||
onDragEnd?.(currentPosRef.current.x, currentPosRef.current.y, itemW, itemH);
|
||||
},
|
||||
})
|
||||
).current;
|
||||
|
||||
const fontFamily = getCanvasFontFamily(layer.fontId);
|
||||
// Instagram Badge arka plan stili
|
||||
const renderBackgroundStyle = () => {
|
||||
switch (layer.bgStyle) {
|
||||
case "solid":
|
||||
return {
|
||||
backgroundColor: layer.bgBoxColor || "rgba(18, 18, 21, 0.92)",
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 5,
|
||||
borderRadius: 8,
|
||||
};
|
||||
case "soft":
|
||||
return {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.18)",
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 5,
|
||||
borderRadius: 8,
|
||||
borderWidth: 1,
|
||||
borderColor: "rgba(255, 255, 255, 0.25)",
|
||||
};
|
||||
case "outline":
|
||||
return {
|
||||
backgroundColor: "transparent",
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 8,
|
||||
borderWidth: 1.5,
|
||||
borderColor: layer.color || "#D4AF37",
|
||||
};
|
||||
case "none":
|
||||
default:
|
||||
return {
|
||||
backgroundColor: "transparent",
|
||||
paddingHorizontal: 4,
|
||||
paddingVertical: 2,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
{...panResponder.panHandlers}
|
||||
accessible={true}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={`Metin: ${layer.text}`}
|
||||
accessibilityHint="Metni düzenlemek için çift dokunun, taşımak için sürükleyin"
|
||||
accessibilityState={{ selected: isSelected }}
|
||||
onLayout={(e) => {
|
||||
const { width, height } = e.nativeEvent.layout;
|
||||
layoutSizeRef.current = { width, height };
|
||||
}}
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: layer.x,
|
||||
top: layer.y,
|
||||
transform: [{ rotate: `${layer.rotation || 0}deg` }],
|
||||
zIndex: isSelected ? 100 : 10,
|
||||
padding: 4,
|
||||
zIndex: isSelected ? 90 : 20,
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
borderWidth: isSelected ? 1.5 : 0,
|
||||
borderColor: "#D4AF37",
|
||||
borderStyle: "dashed",
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: layer.bgBoxColor ? 10 : 4,
|
||||
paddingVertical: layer.bgBoxColor ? 5 : 2,
|
||||
backgroundColor: layer.bgBoxColor || "transparent",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderColor: isSelected ? "#D4AF37" : "transparent",
|
||||
borderStyle: isSelected ? "dashed" : "solid",
|
||||
borderRadius: 10,
|
||||
padding: 2,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontFamily,
|
||||
fontSize: layer.fontSize,
|
||||
color: layer.color,
|
||||
fontWeight: layer.fontWeight || "bold",
|
||||
letterSpacing: layer.letterSpacing || 0.5,
|
||||
textAlign: "center",
|
||||
textShadowColor: layer.hasShadow ? "rgba(0,0,0,0.85)" : "transparent",
|
||||
textShadowOffset: layer.hasShadow ? { width: 1, height: 1 } : { width: 0, height: 0 },
|
||||
textShadowRadius: layer.hasShadow ? 4 : 0,
|
||||
}}
|
||||
>
|
||||
{layer.text || "Metin"}
|
||||
</Text>
|
||||
<View style={renderBackgroundStyle()}>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: layer.fontSize,
|
||||
color: layer.color,
|
||||
fontFamily: getCanvasFontFamily(layer.fontId),
|
||||
fontWeight: layer.fontWeight || "bold",
|
||||
textAlign: layer.textAlign || "center",
|
||||
letterSpacing: layer.letterSpacing ?? 0.5,
|
||||
textShadowColor: layer.hasShadow ? "rgba(0,0,0,0.8)" : "transparent",
|
||||
textShadowOffset: { width: 0, height: 1 },
|
||||
textShadowRadius: 3,
|
||||
}}
|
||||
>
|
||||
{layer.text}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Seçili İken Hızlı Silme Rozeti */}
|
||||
{/* Seçili İken Üst Köşedeki Küçük Silme Rozeti */}
|
||||
{isSelected && (
|
||||
<Pressable
|
||||
onPress={() => onDelete(layer.id)}
|
||||
onPress={(e) => {
|
||||
e.stopPropagation?.();
|
||||
try {
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning);
|
||||
} catch {}
|
||||
onDelete(layer.id);
|
||||
}}
|
||||
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: -12,
|
||||
right: -12,
|
||||
top: -8,
|
||||
right: -8,
|
||||
backgroundColor: "#EF4444",
|
||||
width: 22,
|
||||
height: 22,
|
||||
borderRadius: 11,
|
||||
width: 18,
|
||||
height: 18,
|
||||
borderRadius: 9,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderWidth: 1.5,
|
||||
borderColor: "#FFFFFF",
|
||||
shadowColor: "#000",
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 3,
|
||||
elevation: 4,
|
||||
borderColor: "#0C0C0E",
|
||||
}}
|
||||
>
|
||||
<Ionicons name="close" size={12} color="#FFFFFF" />
|
||||
<Ionicons name="close" size={11} color="#FFFFFF" />
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
import React, { useRef } from "react";
|
||||
import {
|
||||
Image,
|
||||
PanResponder,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { type CanvasQrConfig } from "./types";
|
||||
|
||||
interface CanvasQrItemProps {
|
||||
qrConfig: CanvasQrConfig;
|
||||
qrBase64?: string;
|
||||
isSelected: boolean;
|
||||
onSelect: () => void;
|
||||
onUpdatePosition: (x: number, y: number) => void;
|
||||
onDragStart?: () => void;
|
||||
onDragMove?: (rawX: number, rawY: number, itemWidth: number, itemHeight: number) => { snappedX: number; snappedY: number };
|
||||
onDragEnd?: (finalX: number, finalY: number, itemWidth: number, itemHeight: number) => void;
|
||||
canvasWidth: number;
|
||||
canvasHeight: number;
|
||||
}
|
||||
|
||||
export const CanvasQrItem: React.FC<CanvasQrItemProps> = ({
|
||||
qrConfig,
|
||||
qrBase64,
|
||||
isSelected,
|
||||
onSelect,
|
||||
onUpdatePosition,
|
||||
onDragStart,
|
||||
onDragMove,
|
||||
onDragEnd,
|
||||
canvasWidth,
|
||||
canvasHeight,
|
||||
}) => {
|
||||
// Stale closure önlemi için ref'ler
|
||||
const qrConfigRef = useRef(qrConfig);
|
||||
qrConfigRef.current = qrConfig;
|
||||
|
||||
const canvasWidthRef = useRef(canvasWidth);
|
||||
canvasWidthRef.current = canvasWidth;
|
||||
|
||||
const canvasHeightRef = useRef(canvasHeight);
|
||||
canvasHeightRef.current = canvasHeight;
|
||||
|
||||
const panStartRef = useRef({ x: qrConfig.x, y: qrConfig.y });
|
||||
const currentPosRef = useRef({ x: qrConfig.x, y: qrConfig.y });
|
||||
|
||||
const panResponder = useRef(
|
||||
PanResponder.create({
|
||||
onStartShouldSetPanResponder: () => true,
|
||||
onStartShouldSetPanResponderCapture: () => true,
|
||||
onMoveShouldSetPanResponder: (_evt, gestureState) =>
|
||||
Math.abs(gestureState.dx) > 1 || Math.abs(gestureState.dy) > 1,
|
||||
onMoveShouldSetPanResponderCapture: (_evt, gestureState) =>
|
||||
Math.abs(gestureState.dx) > 1 || Math.abs(gestureState.dy) > 1,
|
||||
onPanResponderTerminationRequest: () => false,
|
||||
|
||||
onPanResponderGrant: () => {
|
||||
onSelect();
|
||||
const cur = qrConfigRef.current;
|
||||
panStartRef.current = { x: cur.x, y: cur.y };
|
||||
currentPosRef.current = { x: cur.x, y: cur.y };
|
||||
onDragStart?.();
|
||||
},
|
||||
|
||||
onPanResponderMove: (_evt, gestureState) => {
|
||||
const cur = qrConfigRef.current;
|
||||
const cWidth = canvasWidthRef.current;
|
||||
const cHeight = canvasHeightRef.current;
|
||||
|
||||
const maxX = Math.max(0, cWidth - cur.size);
|
||||
const maxY = Math.max(0, cHeight - cur.size);
|
||||
|
||||
const rawX = Math.max(0, Math.min(maxX, panStartRef.current.x + gestureState.dx));
|
||||
const rawY = Math.max(0, Math.min(maxY, panStartRef.current.y + gestureState.dy));
|
||||
|
||||
if (onDragMove) {
|
||||
const { snappedX, snappedY } = onDragMove(rawX, rawY, cur.size, cur.size);
|
||||
currentPosRef.current = { x: snappedX, y: snappedY };
|
||||
onUpdatePosition(snappedX, snappedY);
|
||||
} else {
|
||||
const sx = Math.round(rawX);
|
||||
const sy = Math.round(rawY);
|
||||
currentPosRef.current = { x: sx, y: sy };
|
||||
onUpdatePosition(sx, sy);
|
||||
}
|
||||
},
|
||||
|
||||
onPanResponderRelease: () => {
|
||||
const cur = qrConfigRef.current;
|
||||
onDragEnd?.(currentPosRef.current.x, currentPosRef.current.y, cur.size, cur.size);
|
||||
},
|
||||
|
||||
onPanResponderTerminate: () => {
|
||||
const cur = qrConfigRef.current;
|
||||
onDragEnd?.(currentPosRef.current.x, currentPosRef.current.y, cur.size, cur.size);
|
||||
},
|
||||
})
|
||||
).current;
|
||||
|
||||
return (
|
||||
<View
|
||||
{...panResponder.panHandlers}
|
||||
accessible={true}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="QR Kod Alanı"
|
||||
accessibilityHint="QR kodunu seçmek, boyutunu ayarlamak veya stant üzerinde taşımak için dokunun"
|
||||
accessibilityState={{ selected: isSelected }}
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: qrConfig.x,
|
||||
top: qrConfig.y,
|
||||
width: qrConfig.size,
|
||||
height: qrConfig.size,
|
||||
zIndex: isSelected ? 80 : 15,
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
backgroundColor: qrConfig.bgBoxColor || "#FFFFFF",
|
||||
borderRadius: qrConfig.borderRadius,
|
||||
borderWidth: isSelected ? 2 : qrConfig.borderWidth,
|
||||
borderColor: isSelected ? "#D4AF37" : qrConfig.borderColor,
|
||||
borderStyle: isSelected ? "dashed" : "solid",
|
||||
padding: qrConfig.padding,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.25,
|
||||
shadowRadius: 6,
|
||||
elevation: 4,
|
||||
}}
|
||||
>
|
||||
{qrBase64 ? (
|
||||
<Image
|
||||
source={{ uri: `data:image/png;base64,${qrBase64}` }}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
borderRadius: Math.max(0, qrConfig.borderRadius - 4),
|
||||
}}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
) : (
|
||||
<View style={{ alignItems: "center", justifyContent: "center", width: "100%", height: "100%" }}>
|
||||
<Ionicons name="qr-code" size={Math.round(qrConfig.size * 0.7)} color="#0C0C0E" />
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Seçili İken Taşıma Rozeti */}
|
||||
{isSelected && (
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: -8,
|
||||
right: -8,
|
||||
backgroundColor: "#D4AF37",
|
||||
width: 22,
|
||||
height: 22,
|
||||
borderRadius: 11,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderWidth: 1.5,
|
||||
borderColor: "#0C0C0E",
|
||||
}}
|
||||
>
|
||||
<Ionicons name="move" size={12} color="#0C0C0E" />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,610 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
KeyboardAvoidingView,
|
||||
Modal,
|
||||
Platform,
|
||||
Pressable,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextInput,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import * as Haptics from "expo-haptics";
|
||||
import {
|
||||
type CanvasTextLayer,
|
||||
CANVAS_FONT_OPTIONS,
|
||||
CANVAS_COLOR_PALETTE,
|
||||
getCanvasFontFamily,
|
||||
} from "./types";
|
||||
|
||||
interface InstagramTextEditorModalProps {
|
||||
visible: boolean;
|
||||
layer: CanvasTextLayer | null;
|
||||
onSave: (updated: Partial<CanvasTextLayer>) => void;
|
||||
onClose: () => void;
|
||||
onDelete?: (id: string) => void;
|
||||
}
|
||||
|
||||
export const InstagramTextEditorModal: React.FC<InstagramTextEditorModalProps> = ({
|
||||
visible,
|
||||
layer,
|
||||
onSave,
|
||||
onClose,
|
||||
onDelete,
|
||||
}) => {
|
||||
const insets = useSafeAreaInsets();
|
||||
const inputRef = useRef<TextInput>(null);
|
||||
|
||||
const [text, setText] = useState("");
|
||||
const [fontId, setFontId] = useState("sans");
|
||||
const [color, setColor] = useState("#FFFFFF");
|
||||
const [fontSize, setFontSize] = useState(16);
|
||||
const [textAlign, setTextAlign] = useState<"left" | "center" | "right">("center");
|
||||
const [bgStyle, setBgStyle] = useState<"none" | "solid" | "soft" | "outline">("none");
|
||||
const [bgBoxColor, setBgBoxColor] = useState<string | undefined>(undefined);
|
||||
|
||||
// Modal açıldığında layer verilerini yükle
|
||||
useEffect(() => {
|
||||
if (layer && visible) {
|
||||
setText(layer.text || "");
|
||||
setFontId(layer.fontId || "sans");
|
||||
setColor(layer.color || "#FFFFFF");
|
||||
setFontSize(layer.fontSize || 16);
|
||||
setTextAlign(layer.textAlign || "center");
|
||||
if (layer.bgBoxColor) {
|
||||
if (layer.bgBoxColor.includes("0.2") || layer.bgBoxColor.includes("0.3")) {
|
||||
setBgStyle("soft");
|
||||
} else {
|
||||
setBgStyle("solid");
|
||||
}
|
||||
setBgBoxColor(layer.bgBoxColor);
|
||||
} else {
|
||||
setBgStyle("none");
|
||||
setBgBoxColor(undefined);
|
||||
}
|
||||
|
||||
// Auto focus text input
|
||||
setTimeout(() => {
|
||||
inputRef.current?.focus();
|
||||
}, 150);
|
||||
}
|
||||
}, [layer, visible]);
|
||||
|
||||
// Arka plan pill stilini sırayla değiştir (Instagram "A" butonu mantığı)
|
||||
const handleToggleBgStyle = () => {
|
||||
try {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
} catch {}
|
||||
|
||||
if (bgStyle === "none") {
|
||||
setBgStyle("solid");
|
||||
// Solid: seçili rengi kutu yap, metin rengini zıt yap
|
||||
if (color === "#FFFFFF" || color === "#F6EFE3") {
|
||||
setBgBoxColor("rgba(18, 18, 21, 0.92)");
|
||||
} else {
|
||||
setBgBoxColor("rgba(212, 175, 55, 0.92)");
|
||||
}
|
||||
} else if (bgStyle === "solid") {
|
||||
setBgStyle("soft");
|
||||
setBgBoxColor("rgba(255, 255, 255, 0.22)");
|
||||
} else if (bgStyle === "soft") {
|
||||
setBgStyle("outline");
|
||||
setBgBoxColor(undefined);
|
||||
} else {
|
||||
setBgStyle("none");
|
||||
setBgBoxColor(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
// Metin Hizalama Değiştir (Sol -> Orta -> Sağ)
|
||||
const handleToggleAlign = () => {
|
||||
try {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
} catch {}
|
||||
setTextAlign((prev) => {
|
||||
if (prev === "center") return "left";
|
||||
if (prev === "left") return "right";
|
||||
return "center";
|
||||
});
|
||||
};
|
||||
|
||||
// Renk Değiştir
|
||||
const handleSelectColor = (selectedColor: string) => {
|
||||
try {
|
||||
Haptics.selectionAsync();
|
||||
} catch {}
|
||||
setColor(selectedColor);
|
||||
if (bgStyle === "solid") {
|
||||
if (selectedColor === "#FFFFFF" || selectedColor === "#F6EFE3") {
|
||||
setBgBoxColor("rgba(18, 18, 21, 0.92)");
|
||||
} else if (selectedColor === "#121215") {
|
||||
setBgBoxColor("rgba(255, 255, 255, 0.92)");
|
||||
} else {
|
||||
setBgBoxColor(selectedColor);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Font Değiştir
|
||||
const handleSelectFont = (fId: string) => {
|
||||
try {
|
||||
Haptics.selectionAsync();
|
||||
} catch {}
|
||||
setFontId(fId);
|
||||
};
|
||||
|
||||
// Bitti / Kaydet
|
||||
const handleDone = () => {
|
||||
try {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
|
||||
} catch {}
|
||||
|
||||
let finalBg = bgBoxColor;
|
||||
if (bgStyle === "none" || bgStyle === "outline") {
|
||||
finalBg = undefined;
|
||||
}
|
||||
|
||||
onSave({
|
||||
text: text.trim() ? text : "Metin",
|
||||
fontId,
|
||||
color,
|
||||
fontSize,
|
||||
textAlign,
|
||||
bgBoxColor: finalBg,
|
||||
bgStyle,
|
||||
hasShadow: bgStyle === "outline" || (!finalBg && color !== "#121215"),
|
||||
});
|
||||
onClose();
|
||||
};
|
||||
|
||||
const fontFamily = getCanvasFontFamily(fontId);
|
||||
|
||||
if (!visible || !layer) return null;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
transparent
|
||||
animationType="fade"
|
||||
statusBarTranslucent
|
||||
onRequestClose={onClose}
|
||||
>
|
||||
<View style={styles.overlayBackdrop}>
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||
style={[styles.container, { paddingTop: insets.top + 8, paddingBottom: insets.bottom + 12 }]}
|
||||
>
|
||||
{/* ÜST BAR - INSTAGRAM STORY KONTROLLERİ */}
|
||||
<View style={styles.topBar}>
|
||||
{/* Sol: İptal / Kapat */}
|
||||
<Pressable
|
||||
onPress={onClose}
|
||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||
style={styles.iconButton}
|
||||
>
|
||||
<Ionicons name="close" size={24} color="#FFFFFF" />
|
||||
</Pressable>
|
||||
|
||||
{/* Orta Araçlar: Hizalama & Arka Plan Rozeti */}
|
||||
<View style={styles.centerTools}>
|
||||
{/* Hizalama Butonu */}
|
||||
<Pressable
|
||||
onPress={handleToggleAlign}
|
||||
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
|
||||
style={styles.toolPill}
|
||||
>
|
||||
<Ionicons
|
||||
name={
|
||||
textAlign === "left"
|
||||
? "reorder-three-outline"
|
||||
: textAlign === "right"
|
||||
? "reorder-three-outline"
|
||||
: "reorder-two-outline"
|
||||
}
|
||||
size={20}
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
</Pressable>
|
||||
|
||||
{/* Instagram "A" Arka Plan Butonu */}
|
||||
<Pressable
|
||||
onPress={handleToggleBgStyle}
|
||||
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
|
||||
style={[
|
||||
styles.toolPill,
|
||||
bgStyle !== "none" && { backgroundColor: "#FFFFFF" },
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.bgPillText,
|
||||
bgStyle !== "none" && { color: "#0C0C0E", fontWeight: "900" },
|
||||
]}
|
||||
>
|
||||
A
|
||||
</Text>
|
||||
{bgStyle === "soft" && (
|
||||
<View style={styles.softIndicator} />
|
||||
)}
|
||||
{bgStyle === "outline" && (
|
||||
<View style={styles.outlineIndicator} />
|
||||
)}
|
||||
</Pressable>
|
||||
|
||||
{/* Katmanı Sil (Varsa) */}
|
||||
{onDelete && (
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
try {
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning);
|
||||
} catch {}
|
||||
onDelete(layer.id);
|
||||
onClose();
|
||||
}}
|
||||
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
|
||||
style={[styles.toolPill, { backgroundColor: "rgba(239, 68, 68, 0.2)" }]}
|
||||
>
|
||||
<Ionicons name="trash-outline" size={17} color="#EF4444" />
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Sağ: BİTTİ (Done) Butonu */}
|
||||
<Pressable
|
||||
onPress={handleDone}
|
||||
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
|
||||
style={styles.doneButton}
|
||||
>
|
||||
<Text style={styles.doneButtonText}>Bitti</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
{/* ORTA BÖLÜM: SOL BOYUT SLIDER & CANLI METİN ALANI */}
|
||||
<View style={styles.editorBody}>
|
||||
{/* Sol: Dikey Boyut Stepper / Gösterge */}
|
||||
<View style={styles.sizeControlCol}>
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
try { Haptics.selectionAsync(); } catch {}
|
||||
setFontSize((s) => Math.min(48, s + 2));
|
||||
}}
|
||||
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
|
||||
style={styles.sizeStepBtn}
|
||||
>
|
||||
<Ionicons name="add" size={16} color="#FFFFFF" />
|
||||
</Pressable>
|
||||
|
||||
<View style={styles.sizeTrack}>
|
||||
<View
|
||||
style={[
|
||||
styles.sizeTrackFill,
|
||||
{ height: `${Math.min(100, Math.max(10, ((fontSize - 8) / 40) * 100))}%` },
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
try { Haptics.selectionAsync(); } catch {}
|
||||
setFontSize((s) => Math.max(8, s - 2));
|
||||
}}
|
||||
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
|
||||
style={styles.sizeStepBtn}
|
||||
>
|
||||
<Ionicons name="remove" size={16} color="#FFFFFF" />
|
||||
</Pressable>
|
||||
|
||||
<Text style={styles.sizeLabel}>{fontSize}pt</Text>
|
||||
</View>
|
||||
|
||||
{/* Orta: Canlı İnstagram Yazı Alanı */}
|
||||
<View style={styles.textInputArea}>
|
||||
<View
|
||||
style={[
|
||||
styles.textPillWrapper,
|
||||
bgStyle === "solid" && {
|
||||
backgroundColor: bgBoxColor || "#D4AF37",
|
||||
borderRadius: 14,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
},
|
||||
bgStyle === "soft" && {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.22)",
|
||||
borderRadius: 14,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
borderWidth: 1,
|
||||
borderColor: "rgba(255, 255, 255, 0.3)",
|
||||
},
|
||||
bgStyle === "outline" && {
|
||||
borderWidth: 1.5,
|
||||
borderColor: color,
|
||||
borderRadius: 14,
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 6,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<TextInput
|
||||
ref={inputRef}
|
||||
value={text}
|
||||
onChangeText={setText}
|
||||
multiline
|
||||
placeholder="Yazmaya başla..."
|
||||
placeholderTextColor="rgba(255,255,255,0.4)"
|
||||
textAlign={textAlign}
|
||||
autoCapitalize="sentences"
|
||||
style={[
|
||||
styles.textInput,
|
||||
{
|
||||
fontFamily,
|
||||
fontSize,
|
||||
color:
|
||||
bgStyle === "solid" && (bgBoxColor === "#FFFFFF" || bgBoxColor === "#F6EFE3")
|
||||
? "#0C0C0E"
|
||||
: color,
|
||||
textAlign,
|
||||
letterSpacing: fontId === "serif" ? 1.5 : 0.5,
|
||||
textShadowColor: bgStyle === "outline" ? color : "rgba(0,0,0,0.7)",
|
||||
textShadowOffset: { width: 0, height: 1 },
|
||||
textShadowRadius: bgStyle === "outline" ? 8 : 4,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* ALT BÖLÜM 1: RENK SEÇİCİ BALONCUKLAR */}
|
||||
<View style={styles.colorPickerBar}>
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={styles.colorScrollContent}
|
||||
>
|
||||
{CANVAS_COLOR_PALETTE.map((c) => {
|
||||
const isSelected = color === c.color;
|
||||
return (
|
||||
<Pressable
|
||||
key={c.id}
|
||||
onPress={() => handleSelectColor(c.color)}
|
||||
style={[
|
||||
styles.colorCircleWrapper,
|
||||
isSelected && { borderColor: "#FFFFFF", transform: [{ scale: 1.15 }] },
|
||||
]}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
styles.colorCircle,
|
||||
{ backgroundColor: c.color },
|
||||
c.color === "#121215" && { borderWidth: 1, borderColor: "rgba(255,255,255,0.4)" },
|
||||
]}
|
||||
/>
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
||||
{/* ALT BÖLÜM 2: INSTAGRAM FONT STİLLERİ CAROUSEL */}
|
||||
<View style={styles.fontPickerBar}>
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={styles.fontScrollContent}
|
||||
>
|
||||
{CANVAS_FONT_OPTIONS.map((f) => {
|
||||
const isSelected = fontId === f.id;
|
||||
return (
|
||||
<Pressable
|
||||
key={f.id}
|
||||
onPress={() => handleSelectFont(f.id)}
|
||||
style={[
|
||||
styles.fontPill,
|
||||
isSelected && styles.fontPillActive,
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.fontPillText,
|
||||
{ fontFamily: f.fontFamily },
|
||||
isSelected && styles.fontPillTextActive,
|
||||
]}
|
||||
>
|
||||
{f.label}
|
||||
</Text>
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</ScrollView>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
overlayBackdrop: {
|
||||
flex: 1,
|
||||
backgroundColor: "rgba(10, 10, 14, 0.94)",
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
topBar: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
paddingHorizontal: 16,
|
||||
height: 52,
|
||||
zIndex: 10,
|
||||
},
|
||||
iconButton: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
backgroundColor: "rgba(255, 255, 255, 0.12)",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
centerTools: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 10,
|
||||
},
|
||||
toolPill: {
|
||||
width: 38,
|
||||
height: 38,
|
||||
borderRadius: 19,
|
||||
backgroundColor: "rgba(255, 255, 255, 0.12)",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
position: "relative",
|
||||
},
|
||||
bgPillText: {
|
||||
color: "#FFFFFF",
|
||||
fontSize: 16,
|
||||
fontWeight: "700",
|
||||
},
|
||||
softIndicator: {
|
||||
position: "absolute",
|
||||
bottom: 4,
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: 3,
|
||||
backgroundColor: "#60A5FA",
|
||||
},
|
||||
outlineIndicator: {
|
||||
position: "absolute",
|
||||
bottom: 4,
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: 3,
|
||||
backgroundColor: "#F59E0B",
|
||||
},
|
||||
doneButton: {
|
||||
backgroundColor: "#FFFFFF",
|
||||
paddingHorizontal: 18,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 20,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
doneButtonText: {
|
||||
color: "#0C0C0E",
|
||||
fontSize: 14,
|
||||
fontWeight: "800",
|
||||
},
|
||||
editorBody: {
|
||||
flex: 1,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
sizeControlCol: {
|
||||
width: 36,
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
},
|
||||
sizeStepBtn: {
|
||||
width: 30,
|
||||
height: 30,
|
||||
borderRadius: 15,
|
||||
backgroundColor: "rgba(255, 255, 255, 0.15)",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
sizeTrack: {
|
||||
width: 4,
|
||||
height: 120,
|
||||
backgroundColor: "rgba(255, 255, 255, 0.18)",
|
||||
borderRadius: 2,
|
||||
justifyContent: "flex-end",
|
||||
overflow: "hidden",
|
||||
},
|
||||
sizeTrackFill: {
|
||||
width: "100%",
|
||||
backgroundColor: "#D4AF37",
|
||||
borderRadius: 2,
|
||||
},
|
||||
sizeLabel: {
|
||||
color: "rgba(255, 255, 255, 0.6)",
|
||||
fontSize: 10,
|
||||
fontWeight: "700",
|
||||
},
|
||||
textInputArea: {
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 12,
|
||||
},
|
||||
textPillWrapper: {
|
||||
maxWidth: "100%",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
textInput: {
|
||||
minWidth: 140,
|
||||
maxWidth: "100%",
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
fontWeight: "800",
|
||||
},
|
||||
colorPickerBar: {
|
||||
paddingVertical: 10,
|
||||
},
|
||||
colorScrollContent: {
|
||||
paddingHorizontal: 20,
|
||||
gap: 12,
|
||||
alignItems: "center",
|
||||
},
|
||||
colorCircleWrapper: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 16,
|
||||
borderWidth: 2,
|
||||
borderColor: "transparent",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
colorCircle: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: 12,
|
||||
},
|
||||
fontPickerBar: {
|
||||
paddingVertical: 8,
|
||||
},
|
||||
fontScrollContent: {
|
||||
paddingHorizontal: 16,
|
||||
gap: 8,
|
||||
alignItems: "center",
|
||||
},
|
||||
fontPill: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 18,
|
||||
backgroundColor: "rgba(255, 255, 255, 0.08)",
|
||||
borderWidth: 1,
|
||||
borderColor: "rgba(255, 255, 255, 0.12)",
|
||||
},
|
||||
fontPillActive: {
|
||||
backgroundColor: "#FFFFFF",
|
||||
borderColor: "#FFFFFF",
|
||||
},
|
||||
fontPillText: {
|
||||
color: "rgba(255, 255, 255, 0.8)",
|
||||
fontSize: 12,
|
||||
fontWeight: "700",
|
||||
letterSpacing: 0.5,
|
||||
},
|
||||
fontPillTextActive: {
|
||||
color: "#0C0C0E",
|
||||
fontWeight: "900",
|
||||
},
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,5 @@
|
||||
export * from "./types";
|
||||
export * from "./CanvasLayerItem";
|
||||
export * from "./CanvasQrItem";
|
||||
export * from "./InstagramTextEditorModal";
|
||||
export * from "./QrCanvasStudio";
|
||||
|
||||
@@ -7,38 +7,61 @@ export interface CanvasTextLayer {
|
||||
fontSize: number;
|
||||
fontId: string;
|
||||
color: string;
|
||||
bgBoxColor?: string; // Instagram badge kutusu (örn: rgba(0,0,0,0.8), #D4AF37 vb.)
|
||||
bgBoxColor?: string; // Instagram badge kutusu
|
||||
bgStyle?: "none" | "solid" | "soft" | "outline";
|
||||
textAlign?: "left" | "center" | "right";
|
||||
hasShadow?: boolean;
|
||||
letterSpacing?: number;
|
||||
fontWeight?: "normal" | "600" | "700" | "800" | "900";
|
||||
}
|
||||
|
||||
export interface CanvasQrConfig {
|
||||
x: number;
|
||||
y: number;
|
||||
size: number;
|
||||
borderRadius: number;
|
||||
borderWidth: number;
|
||||
borderColor: string;
|
||||
padding: number;
|
||||
bgBoxColor: string;
|
||||
}
|
||||
|
||||
export const CANVAS_FONT_OPTIONS = [
|
||||
{
|
||||
id: "sans",
|
||||
label: "Modern Sans",
|
||||
label: "MODERN",
|
||||
fontFamily: "System, -apple-system, Roboto, 'Helvetica Neue', Arial, sans-serif",
|
||||
},
|
||||
{
|
||||
id: "serif",
|
||||
label: "Zarif Serif",
|
||||
label: "ZARİF",
|
||||
fontFamily: "Georgia, 'Times New Roman', serif",
|
||||
},
|
||||
{
|
||||
id: "times",
|
||||
label: "Klasik Times",
|
||||
label: "KLASİK",
|
||||
fontFamily: "'Times New Roman', Times, Georgia, serif",
|
||||
},
|
||||
{
|
||||
id: "typewriter",
|
||||
label: "DAKTİLO",
|
||||
fontFamily: "Courier, 'Courier New', monospace",
|
||||
},
|
||||
{
|
||||
id: "palatino",
|
||||
label: "Lüks Palatino",
|
||||
label: "LÜKS",
|
||||
fontFamily: "Palatino, 'Palatino Linotype', Georgia, serif",
|
||||
},
|
||||
{
|
||||
id: "trebuchet",
|
||||
label: "Dinamik",
|
||||
label: "DİNAMİK",
|
||||
fontFamily: "'Trebuchet MS', 'Segoe UI', Arial, sans-serif",
|
||||
},
|
||||
{
|
||||
id: "heavy",
|
||||
label: "GÜÇLÜ",
|
||||
fontFamily: "Impact, 'Arial Black', sans-serif",
|
||||
},
|
||||
];
|
||||
|
||||
export const CANVAS_COLOR_PALETTE = [
|
||||
@@ -46,16 +69,41 @@ export const CANVAS_COLOR_PALETTE = [
|
||||
{ id: "white", color: "#FFFFFF", label: "Beyaz" },
|
||||
{ id: "black", color: "#121215", label: "Siyah" },
|
||||
{ id: "cream", color: "#F6EFE3", label: "Krem" },
|
||||
{ id: "emerald", color: "#0C4443", label: "Zümrüt" },
|
||||
{ id: "crimson", color: "#A64423", label: "Kızıl" },
|
||||
{ id: "blue", color: "#003554", label: "Lacivert" },
|
||||
{ id: "emerald", color: "#10B981", label: "Yeşil" },
|
||||
{ id: "crimson", color: "#EF4444", label: "Kırmızı" },
|
||||
{ id: "amber", color: "#F59E0B", label: "Amber" },
|
||||
{ id: "rose", color: "#EC4899", label: "Pembe" },
|
||||
{ id: "indigo", color: "#6366F1", label: "Lila" },
|
||||
{ id: "cyan", color: "#06B6D4", label: "Turkuaz" },
|
||||
];
|
||||
|
||||
export const INSTAGRAM_BADGE_STYLES = [
|
||||
{ id: "none", label: "Düz", bgBoxColor: undefined },
|
||||
{ id: "dark", label: "Siyah Kutu", bgBoxColor: "rgba(18, 18, 21, 0.88)" },
|
||||
{ id: "gold", label: "Altın Kutu", bgBoxColor: "rgba(212, 175, 55, 0.92)" },
|
||||
{ id: "white", label: "Beyaz Kutu", bgBoxColor: "rgba(255, 255, 255, 0.92)" },
|
||||
{ id: "dark", label: "Koyu", bgBoxColor: "rgba(18, 18, 21, 0.88)" },
|
||||
{ id: "gold", label: "Altın", bgBoxColor: "rgba(212, 175, 55, 0.92)" },
|
||||
{ id: "white", label: "Beyaz", bgBoxColor: "rgba(255, 255, 255, 0.92)" },
|
||||
{ id: "glass", label: "Cam", bgBoxColor: "rgba(255, 255, 255, 0.2)" },
|
||||
];
|
||||
|
||||
export const QR_SIZE_PRESETS = [
|
||||
{ label: "Kompakt", size: 85 },
|
||||
{ label: "Standart", size: 115 },
|
||||
{ label: "Büyük", size: 145 },
|
||||
{ label: "Geniş", size: 175 },
|
||||
];
|
||||
|
||||
export const QR_RADIUS_PRESETS = [
|
||||
{ label: "Düz", radius: 0 },
|
||||
{ label: "Hafif", radius: 10 },
|
||||
{ label: "Yuvarlak", radius: 18 },
|
||||
{ label: "Oval", radius: 28 },
|
||||
];
|
||||
|
||||
export const QR_BORDER_COLORS = [
|
||||
{ id: "gold", color: "#D4AF37", label: "Altın" },
|
||||
{ id: "black", color: "#121215", label: "Siyah" },
|
||||
{ id: "white", color: "#FFFFFF", label: "Beyaz" },
|
||||
{ id: "none", color: "transparent", label: "Yok" },
|
||||
];
|
||||
|
||||
export function getCanvasFontFamily(fontId?: string): string {
|
||||
@@ -63,6 +111,39 @@ export function getCanvasFontFamily(fontId?: string): string {
|
||||
return found ? found.fontFamily : "System, -apple-system, Roboto, sans-serif";
|
||||
}
|
||||
|
||||
export function getDefaultQrConfigForTemplate(
|
||||
templateKey: string,
|
||||
canvasWidth: number,
|
||||
canvasHeight: number
|
||||
): CanvasQrConfig {
|
||||
if (templateKey === "qr-1" || templateKey === "qr-8" || templateKey === "qr-9") {
|
||||
const size = 110;
|
||||
return {
|
||||
x: Math.round((canvasWidth - size) / 2),
|
||||
y: 102,
|
||||
size,
|
||||
borderRadius: 14,
|
||||
borderWidth: 1.5,
|
||||
borderColor: "#D4AF37",
|
||||
padding: 6,
|
||||
bgBoxColor: "#FFFFFF",
|
||||
};
|
||||
}
|
||||
|
||||
// Standart Dikey Şablonlar (qr-2, qr-3, qr-4, qr-5, qr-6)
|
||||
const size = 125;
|
||||
return {
|
||||
x: Math.round((canvasWidth - size) / 2),
|
||||
y: Math.round(canvasHeight * 0.36),
|
||||
size,
|
||||
borderRadius: 14,
|
||||
borderWidth: 1.5,
|
||||
borderColor: "#D4AF37",
|
||||
padding: 6,
|
||||
bgBoxColor: "#FFFFFF",
|
||||
};
|
||||
}
|
||||
|
||||
// Her şablon için başlangıç varsayılan katmanları (1:1 veya standart orana göre)
|
||||
export function getDefaultLayersForTemplate(templateKey: string, restaurantName = "Gourmet Bistro"): CanvasTextLayer[] {
|
||||
if (templateKey === "qr-1") {
|
||||
@@ -78,6 +159,7 @@ export function getDefaultLayersForTemplate(templateKey: string, restaurantName
|
||||
color: "#D4AF37",
|
||||
fontWeight: "800",
|
||||
letterSpacing: 2,
|
||||
textAlign: "center",
|
||||
},
|
||||
{
|
||||
id: "sub",
|
||||
@@ -90,6 +172,7 @@ export function getDefaultLayersForTemplate(templateKey: string, restaurantName
|
||||
color: "#E5C378",
|
||||
fontWeight: "600",
|
||||
letterSpacing: 1.5,
|
||||
textAlign: "center",
|
||||
},
|
||||
{
|
||||
id: "cta",
|
||||
@@ -102,6 +185,7 @@ export function getDefaultLayersForTemplate(templateKey: string, restaurantName
|
||||
color: "#D4AF37",
|
||||
fontWeight: "700",
|
||||
letterSpacing: 1,
|
||||
textAlign: "center",
|
||||
},
|
||||
{
|
||||
id: "table",
|
||||
@@ -113,6 +197,7 @@ export function getDefaultLayersForTemplate(templateKey: string, restaurantName
|
||||
fontId: "sans",
|
||||
color: "#C5A059",
|
||||
fontWeight: "600",
|
||||
textAlign: "center",
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -130,6 +215,7 @@ export function getDefaultLayersForTemplate(templateKey: string, restaurantName
|
||||
color: "#FFFFFF",
|
||||
fontWeight: "900",
|
||||
letterSpacing: 1,
|
||||
textAlign: "center",
|
||||
},
|
||||
{
|
||||
id: "sub",
|
||||
@@ -141,6 +227,7 @@ export function getDefaultLayersForTemplate(templateKey: string, restaurantName
|
||||
fontId: "sans",
|
||||
color: "#D1A752",
|
||||
fontWeight: "800",
|
||||
textAlign: "center",
|
||||
},
|
||||
{
|
||||
id: "cta",
|
||||
@@ -152,6 +239,7 @@ export function getDefaultLayersForTemplate(templateKey: string, restaurantName
|
||||
fontId: "sans",
|
||||
color: "#FFFFFF",
|
||||
fontWeight: "800",
|
||||
textAlign: "center",
|
||||
},
|
||||
{
|
||||
id: "wifi",
|
||||
@@ -163,6 +251,7 @@ export function getDefaultLayersForTemplate(templateKey: string, restaurantName
|
||||
fontId: "sans",
|
||||
color: "#D1A752",
|
||||
fontWeight: "800",
|
||||
textAlign: "center",
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -179,6 +268,7 @@ export function getDefaultLayersForTemplate(templateKey: string, restaurantName
|
||||
fontId: "serif",
|
||||
color: "#D4AF37",
|
||||
fontWeight: "800",
|
||||
textAlign: "center",
|
||||
},
|
||||
{
|
||||
id: "sub",
|
||||
@@ -190,6 +280,7 @@ export function getDefaultLayersForTemplate(templateKey: string, restaurantName
|
||||
fontId: "sans",
|
||||
color: "#FFFFFF",
|
||||
fontWeight: "700",
|
||||
textAlign: "center",
|
||||
},
|
||||
{
|
||||
id: "cta",
|
||||
@@ -201,6 +292,7 @@ export function getDefaultLayersForTemplate(templateKey: string, restaurantName
|
||||
fontId: "serif",
|
||||
color: "#D4AF37",
|
||||
fontWeight: "800",
|
||||
textAlign: "center",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -8,11 +8,12 @@ import Svg, {
|
||||
Text,
|
||||
Image,
|
||||
} from "react-native-svg";
|
||||
import { type QrTemplateProps, getFontFamily, renderSvgLines } from "./index";
|
||||
import { type QrTemplateProps, getFontFamily, renderSvgLines } from "./common";
|
||||
|
||||
const QrTemplate2 = ({
|
||||
qrBase64,
|
||||
fields,
|
||||
hideQr,
|
||||
...props
|
||||
}: QrTemplateProps) => {
|
||||
const rName = fields?.restaurantName;
|
||||
@@ -159,48 +160,50 @@ const QrTemplate2 = ({
|
||||
<Path stroke="#8c6239" strokeWidth={0.8} d="M145 608h30m50 0h30" />
|
||||
|
||||
{/* QR Placeholder / Gerçek QR */}
|
||||
<G transform="translate(90 215)">
|
||||
<Rect width={220} height={220} fill="#fbf8f2" stroke="#d8c7b0" strokeWidth={1.5} rx={20} />
|
||||
{qrBase64 ? (
|
||||
<Image
|
||||
href={`data:image/png;base64,${qrBase64}`}
|
||||
x={10}
|
||||
y={10}
|
||||
width={200}
|
||||
height={200}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Rect
|
||||
width={200}
|
||||
height={200}
|
||||
{!hideQr && (
|
||||
<G transform="translate(90 215)">
|
||||
<Rect width={220} height={220} fill="#fbf8f2" stroke="#d8c7b0" strokeWidth={1.5} rx={20} />
|
||||
{qrBase64 ? (
|
||||
<Image
|
||||
href={`data:image/png;base64,${qrBase64}`}
|
||||
x={10}
|
||||
y={10}
|
||||
fill="#f5efe6"
|
||||
stroke="#d8c7b0"
|
||||
strokeDasharray="4 3"
|
||||
rx={14}
|
||||
width={200}
|
||||
height={200}
|
||||
/>
|
||||
<Circle cx={110} cy={98} r={18} fill="#fbf8f2" stroke="#8c6239" />
|
||||
<G fill="#3a2312" transform="matrix(.65 0 0 .65 110 98)">
|
||||
<Ellipse cx={-5} cy={2} rx={7} ry={5} transform="rotate(-30)" />
|
||||
<Ellipse cx={5} rx={7} ry={5} transform="rotate(40)" />
|
||||
</G>
|
||||
<Text
|
||||
x={110}
|
||||
y={132}
|
||||
fill="#8c6239"
|
||||
fontFamily="System, sans-serif"
|
||||
fontSize={11}
|
||||
fontWeight={700}
|
||||
letterSpacing={1}
|
||||
textAnchor="middle"
|
||||
>
|
||||
{"QR KOD ALANI"}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</G>
|
||||
) : (
|
||||
<>
|
||||
<Rect
|
||||
width={200}
|
||||
height={200}
|
||||
x={10}
|
||||
y={10}
|
||||
fill="#f5efe6"
|
||||
stroke="#d8c7b0"
|
||||
strokeDasharray="4 3"
|
||||
rx={14}
|
||||
/>
|
||||
<Circle cx={110} cy={98} r={18} fill="#fbf8f2" stroke="#8c6239" />
|
||||
<G fill="#3a2312" transform="matrix(.65 0 0 .65 110 98)">
|
||||
<Ellipse cx={-5} cy={2} rx={7} ry={5} transform="rotate(-30)" />
|
||||
<Ellipse cx={5} rx={7} ry={5} transform="rotate(40)" />
|
||||
</G>
|
||||
<Text
|
||||
x={110}
|
||||
y={132}
|
||||
fill="#8c6239"
|
||||
fontFamily="System, sans-serif"
|
||||
fontSize={11}
|
||||
fontWeight={700}
|
||||
letterSpacing={1}
|
||||
textAnchor="middle"
|
||||
>
|
||||
{"QR KOD ALANI"}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</G>
|
||||
)}
|
||||
|
||||
{/* Metinler (Çok satırlı destekli) */}
|
||||
<G textAnchor="middle">
|
||||
|
||||
@@ -12,11 +12,12 @@ import Svg, {
|
||||
Text,
|
||||
Image,
|
||||
} from "react-native-svg";
|
||||
import { type QrTemplateProps, getFontFamily, renderSvgLines } from "./index";
|
||||
import { type QrTemplateProps, getFontFamily, renderSvgLines } from "./common";
|
||||
|
||||
const QrTemplate3 = ({
|
||||
qrBase64,
|
||||
fields,
|
||||
hideQr,
|
||||
...props
|
||||
}: QrTemplateProps) => {
|
||||
const rName = fields?.restaurantName;
|
||||
@@ -213,42 +214,44 @@ const QrTemplate3 = ({
|
||||
</G>
|
||||
|
||||
{/* QR Placeholder / Gerçek QR */}
|
||||
<G id="qr-placeholder" transform="translate(90 215)">
|
||||
<Rect width={220} height={220} fill="#fff" stroke="#e4e4e7" strokeWidth={2} rx={20} />
|
||||
{qrBase64 ? (
|
||||
<Image
|
||||
href={`data:image/png;base64,${qrBase64}`}
|
||||
x={10}
|
||||
y={10}
|
||||
width={200}
|
||||
height={200}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Rect
|
||||
width={196}
|
||||
height={196}
|
||||
x={12}
|
||||
y={12}
|
||||
fill="#fafafa"
|
||||
stroke="#e4e4e7"
|
||||
strokeDasharray="4 3"
|
||||
rx={14}
|
||||
{!hideQr && (
|
||||
<G id="qr-placeholder" transform="translate(90 215)">
|
||||
<Rect width={220} height={220} fill="#fff" stroke="#e4e4e7" strokeWidth={2} rx={20} />
|
||||
{qrBase64 ? (
|
||||
<Image
|
||||
href={`data:image/png;base64,${qrBase64}`}
|
||||
x={10}
|
||||
y={10}
|
||||
width={200}
|
||||
height={200}
|
||||
/>
|
||||
<Text
|
||||
x={110}
|
||||
y={115}
|
||||
fill="#71717a"
|
||||
fontFamily="System, sans-serif"
|
||||
fontSize={12}
|
||||
fontWeight={700}
|
||||
textAnchor="middle"
|
||||
>
|
||||
{"QR KOD ALANI"}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</G>
|
||||
) : (
|
||||
<>
|
||||
<Rect
|
||||
width={196}
|
||||
height={196}
|
||||
x={12}
|
||||
y={12}
|
||||
fill="#fafafa"
|
||||
stroke="#e4e4e7"
|
||||
strokeDasharray="4 3"
|
||||
rx={14}
|
||||
/>
|
||||
<Text
|
||||
x={110}
|
||||
y={115}
|
||||
fill="#71717a"
|
||||
fontFamily="System, sans-serif"
|
||||
fontSize={12}
|
||||
fontWeight={700}
|
||||
textAnchor="middle"
|
||||
>
|
||||
{"QR KOD ALANI"}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</G>
|
||||
)}
|
||||
|
||||
{/* Metinler (Çok satırlı destekli) */}
|
||||
<G id="layer-texts">
|
||||
|
||||
@@ -9,11 +9,12 @@ import Svg, {
|
||||
Text,
|
||||
Image,
|
||||
} from "react-native-svg";
|
||||
import { type QrTemplateProps, getFontFamily, renderSvgLines } from "./index";
|
||||
import { type QrTemplateProps, getFontFamily, renderSvgLines } from "./common";
|
||||
|
||||
const QrTemplate4 = ({
|
||||
qrBase64,
|
||||
fields,
|
||||
hideQr,
|
||||
...props
|
||||
}: QrTemplateProps) => {
|
||||
const rName = fields?.restaurantName;
|
||||
@@ -136,52 +137,54 @@ const QrTemplate4 = ({
|
||||
d="M114 278v-58h54m118 58v-58h-54M114 342v58h54m118-58v58h-54"
|
||||
/>
|
||||
|
||||
{/* QR Placeholder / Live QR */}
|
||||
<G transform="translate(125 230)">
|
||||
<Rect
|
||||
width={150}
|
||||
height={150}
|
||||
fill="#fff"
|
||||
stroke="#eae5d9"
|
||||
strokeWidth={1.5}
|
||||
rx={12}
|
||||
/>
|
||||
{qrBase64 ? (
|
||||
<Image
|
||||
href={`data:image/png;base64,${qrBase64}`}
|
||||
x={8}
|
||||
y={8}
|
||||
width={134}
|
||||
height={134}
|
||||
{/* QR Placeholder / Gerçek QR */}
|
||||
{!hideQr && (
|
||||
<G transform="translate(125 230)">
|
||||
<Rect
|
||||
width={150}
|
||||
height={150}
|
||||
fill="#fff"
|
||||
stroke="#eae5d9"
|
||||
strokeWidth={1.5}
|
||||
rx={12}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Rect
|
||||
width={134}
|
||||
height={134}
|
||||
{qrBase64 ? (
|
||||
<Image
|
||||
href={`data:image/png;base64,${qrBase64}`}
|
||||
x={8}
|
||||
y={8}
|
||||
fill="#fafaf8"
|
||||
stroke="#c59b4b"
|
||||
strokeDasharray="4 3"
|
||||
strokeOpacity={0.35}
|
||||
rx={8}
|
||||
width={134}
|
||||
height={134}
|
||||
/>
|
||||
<Text
|
||||
x={75}
|
||||
y={79}
|
||||
fill="#c59b4b"
|
||||
fontFamily="System, sans-serif"
|
||||
fontSize={10}
|
||||
fontWeight={700}
|
||||
letterSpacing={1}
|
||||
textAnchor="middle"
|
||||
>
|
||||
{"QR KOD ALANI"}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</G>
|
||||
) : (
|
||||
<>
|
||||
<Rect
|
||||
width={134}
|
||||
height={134}
|
||||
x={8}
|
||||
y={8}
|
||||
fill="#fafaf8"
|
||||
stroke="#c59b4b"
|
||||
strokeDasharray="4 3"
|
||||
strokeOpacity={0.35}
|
||||
rx={8}
|
||||
/>
|
||||
<Text
|
||||
x={75}
|
||||
y={79}
|
||||
fill="#c59b4b"
|
||||
fontFamily="System, sans-serif"
|
||||
fontSize={10}
|
||||
fontWeight={700}
|
||||
letterSpacing={1}
|
||||
textAnchor="middle"
|
||||
>
|
||||
{"QR KOD ALANI"}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</G>
|
||||
)}
|
||||
|
||||
{/* Metinler */}
|
||||
<G fill="#111" textAnchor="middle">
|
||||
|
||||
@@ -6,7 +6,7 @@ import Svg, {
|
||||
Path,
|
||||
Text,
|
||||
} from "react-native-svg";
|
||||
import { type QrTemplateProps, getFontFamily, renderSvgLines } from "./index";
|
||||
import { type QrTemplateProps, getFontFamily, renderSvgLines } from "./common";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const bgImage = require("../../../assets/templates/5.png");
|
||||
@@ -14,6 +14,7 @@ const bgImage = require("../../../assets/templates/5.png");
|
||||
const QrTemplate5 = ({
|
||||
qrBase64,
|
||||
fields,
|
||||
hideQr,
|
||||
...props
|
||||
}: QrTemplateProps) => {
|
||||
const rName = fields?.restaurantName;
|
||||
@@ -39,58 +40,60 @@ const QrTemplate5 = ({
|
||||
/>
|
||||
|
||||
{/* QR Placeholder / Live QR */}
|
||||
<G transform="translate(108 206)">
|
||||
<Rect
|
||||
width={164}
|
||||
height={164}
|
||||
fill="#fff"
|
||||
stroke="#dce5ea"
|
||||
strokeWidth={1.2}
|
||||
rx={16}
|
||||
/>
|
||||
<Path
|
||||
fill="none"
|
||||
stroke="#003554"
|
||||
strokeLinecap="round"
|
||||
strokeWidth={3}
|
||||
d="M6 30V12a6 6 0 0 1 6-6h18m128 24V12a6 6 0 0 0-6-6h-18M6 134v18a6 6 0 0 0 6 6h18m128-24v18a6 6 0 0 1-6 6h-18"
|
||||
/>
|
||||
{qrBase64 ? (
|
||||
<Image
|
||||
href={`data:image/png;base64,${qrBase64}`}
|
||||
x={12}
|
||||
y={12}
|
||||
width={140}
|
||||
height={140}
|
||||
{!hideQr && (
|
||||
<G transform="translate(108 206)">
|
||||
<Rect
|
||||
width={164}
|
||||
height={164}
|
||||
fill="#fff"
|
||||
stroke="#dce5ea"
|
||||
strokeWidth={1.2}
|
||||
rx={16}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Rect
|
||||
width={140}
|
||||
height={140}
|
||||
<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}
|
||||
fill="#fafcfd"
|
||||
stroke="#003554"
|
||||
strokeDasharray="4 3"
|
||||
strokeOpacity={0.2}
|
||||
rx={10}
|
||||
width={140}
|
||||
height={140}
|
||||
/>
|
||||
<Text
|
||||
x={82}
|
||||
y={87}
|
||||
fill="#003554"
|
||||
fontFamily="System, sans-serif"
|
||||
fontSize={11}
|
||||
fontWeight={700}
|
||||
letterSpacing={1}
|
||||
textAnchor="middle"
|
||||
>
|
||||
{"QR KOD ALANI"}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</G>
|
||||
) : (
|
||||
<>
|
||||
<Rect
|
||||
width={140}
|
||||
height={140}
|
||||
x={12}
|
||||
y={12}
|
||||
fill="#fafcfd"
|
||||
stroke="#003554"
|
||||
strokeDasharray="4 3"
|
||||
strokeOpacity={0.2}
|
||||
rx={10}
|
||||
/>
|
||||
<Text
|
||||
x={82}
|
||||
y={87}
|
||||
fill="#003554"
|
||||
fontFamily="System, sans-serif"
|
||||
fontSize={11}
|
||||
fontWeight={700}
|
||||
letterSpacing={1}
|
||||
textAnchor="middle"
|
||||
>
|
||||
{"QR KOD ALANI"}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</G>
|
||||
)}
|
||||
|
||||
{/* Metinler */}
|
||||
<G fill="#003554" textAnchor="middle">
|
||||
|
||||
@@ -9,11 +9,12 @@ import Svg, {
|
||||
Text,
|
||||
Image,
|
||||
} from "react-native-svg";
|
||||
import { type QrTemplateProps, getFontFamily, renderSvgLines } from "./index";
|
||||
import { type QrTemplateProps, getFontFamily, renderSvgLines } from "./common";
|
||||
|
||||
const QrTemplate6 = ({
|
||||
qrBase64,
|
||||
fields,
|
||||
hideQr,
|
||||
...props
|
||||
}: QrTemplateProps) => {
|
||||
const rName = fields?.restaurantName;
|
||||
@@ -113,59 +114,61 @@ const QrTemplate6 = ({
|
||||
<Use href="#b6" transform="matrix(-.95 0 0 .95 360 360)" />
|
||||
|
||||
{/* QR Placeholder / Gerçek QR */}
|
||||
<G transform="translate(95 205)">
|
||||
<Path
|
||||
fill="#fdfbf7"
|
||||
stroke="#122a42"
|
||||
strokeWidth={3}
|
||||
d="M0 0h210v210H0z"
|
||||
/>
|
||||
<Path
|
||||
fill="none"
|
||||
stroke="#c59b58"
|
||||
strokeWidth={1.5}
|
||||
d="M4 4h202v202H4z"
|
||||
/>
|
||||
{qrBase64 ? (
|
||||
<Image
|
||||
href={`data:image/png;base64,${qrBase64}`}
|
||||
x={10}
|
||||
y={10}
|
||||
width={190}
|
||||
height={190}
|
||||
{!hideQr && (
|
||||
<G transform="translate(95 205)">
|
||||
<Path
|
||||
fill="#fdfbf7"
|
||||
stroke="#122a42"
|
||||
strokeWidth={3}
|
||||
d="M0 0h210v210H0z"
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Path
|
||||
fill="#faf8f5"
|
||||
stroke="#122a42"
|
||||
strokeDasharray="4 3"
|
||||
d="M12 12h186v186H12z"
|
||||
<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}
|
||||
/>
|
||||
<Circle
|
||||
cx={105}
|
||||
cy={95}
|
||||
r={16}
|
||||
fill="#f6efe3"
|
||||
stroke="#c59b58"
|
||||
strokeWidth={1.2}
|
||||
/>
|
||||
<Use href="#a6" transform="matrix(.45 0 0 .45 105 93)" />
|
||||
<Text
|
||||
x={105}
|
||||
y={128}
|
||||
fill="#122a42"
|
||||
fontFamily="System, sans-serif"
|
||||
fontSize={11}
|
||||
fontWeight="bold"
|
||||
letterSpacing={1.5}
|
||||
textAnchor="middle"
|
||||
>
|
||||
{"QR KOD ALANI"}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</G>
|
||||
) : (
|
||||
<>
|
||||
<Path
|
||||
fill="#faf8f5"
|
||||
stroke="#122a42"
|
||||
strokeDasharray="4 3"
|
||||
d="M12 12h186v186H12z"
|
||||
/>
|
||||
<Circle
|
||||
cx={105}
|
||||
cy={95}
|
||||
r={16}
|
||||
fill="#f6efe3"
|
||||
stroke="#c59b58"
|
||||
strokeWidth={1.2}
|
||||
/>
|
||||
<Use href="#a6" transform="matrix(.45 0 0 .45 105 93)" />
|
||||
<Text
|
||||
x={105}
|
||||
y={128}
|
||||
fill="#122a42"
|
||||
fontFamily="System, sans-serif"
|
||||
fontSize={11}
|
||||
fontWeight="bold"
|
||||
letterSpacing={1.5}
|
||||
textAnchor="middle"
|
||||
>
|
||||
{"QR KOD ALANI"}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</G>
|
||||
)}
|
||||
|
||||
{/* Metinler */}
|
||||
<G textAnchor="middle">
|
||||
|
||||
@@ -7,7 +7,7 @@ import Svg, {
|
||||
Text,
|
||||
Image,
|
||||
} from "react-native-svg";
|
||||
import { type QrTemplateProps, getFontFamily, renderSvgLines } from "./index";
|
||||
import { type QrTemplateProps, getFontFamily, renderSvgLines } from "./common";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const template8Bg = require("../../../assets/templates/8.jpeg");
|
||||
@@ -15,6 +15,7 @@ const template8Bg = require("../../../assets/templates/8.jpeg");
|
||||
const QrTemplate8 = ({
|
||||
qrBase64,
|
||||
fields,
|
||||
hideQr,
|
||||
...props
|
||||
}: QrTemplateProps) => {
|
||||
const rName = fields?.restaurantName;
|
||||
@@ -40,57 +41,59 @@ const QrTemplate8 = ({
|
||||
/>
|
||||
|
||||
{/* QR Alanı (Merkez) */}
|
||||
<G transform="translate(348 305)">
|
||||
<Rect
|
||||
width={328}
|
||||
height={336}
|
||||
fill="#f9f5eb"
|
||||
stroke="#d1a752"
|
||||
strokeWidth={1.5}
|
||||
rx={16}
|
||||
/>
|
||||
<Rect
|
||||
width={304}
|
||||
height={312}
|
||||
x={12}
|
||||
y={12}
|
||||
fill="#faf8f2"
|
||||
stroke="#0c4443"
|
||||
strokeDasharray="6 4"
|
||||
strokeOpacity={0.3}
|
||||
strokeWidth={1.5}
|
||||
rx={12}
|
||||
/>
|
||||
{qrBase64 ? (
|
||||
<Image
|
||||
href={`data:image/png;base64,${qrBase64}`}
|
||||
x={18}
|
||||
y={18}
|
||||
width={292}
|
||||
height={300}
|
||||
{!hideQr && (
|
||||
<G transform="translate(348 305)">
|
||||
<Rect
|
||||
width={328}
|
||||
height={336}
|
||||
fill="#f9f5eb"
|
||||
stroke="#d1a752"
|
||||
strokeWidth={1.5}
|
||||
rx={16}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<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>
|
||||
<Rect
|
||||
width={304}
|
||||
height={312}
|
||||
x={12}
|
||||
y={12}
|
||||
fill="#faf8f2"
|
||||
stroke="#0c4443"
|
||||
strokeDasharray="6 4"
|
||||
strokeOpacity={0.3}
|
||||
strokeWidth={1.5}
|
||||
rx={12}
|
||||
/>
|
||||
{qrBase64 ? (
|
||||
<Image
|
||||
href={`data:image/png;base64,${qrBase64}`}
|
||||
x={18}
|
||||
y={18}
|
||||
width={292}
|
||||
height={300}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Circle cx={164} cy={148} r={28} fill="#0c4443" />
|
||||
<G fill="#d1a752">
|
||||
<Path d="M164 132.6c4.4 6.6 4.4 15.4 0 24.2-4.4-8.8-4.4-17.6 0-24.2" />
|
||||
<Path d="M161.8 150.2c-8.8-4.4-13.2 4.4-6.6 8.8 6.6 0 8.8-6.6 6.6-8.8m4.4 0c8.8-4.4 13.2 4.4 6.6 8.8-6.6 0-8.8-6.6-6.6-8.8" />
|
||||
</G>
|
||||
<Text
|
||||
x={164}
|
||||
y={218}
|
||||
fill="#0c4443"
|
||||
fontFamily="System, -apple-system, Roboto, sans-serif"
|
||||
fontSize={14}
|
||||
fontWeight={800}
|
||||
letterSpacing={2}
|
||||
textAnchor="middle"
|
||||
>
|
||||
{"QR KOD ALANI"}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</G>
|
||||
)}
|
||||
|
||||
{/* Üst Çizgiler & OR Ayracı */}
|
||||
<Path
|
||||
|
||||
@@ -9,7 +9,7 @@ import Svg, {
|
||||
TextPath,
|
||||
Image,
|
||||
} from "react-native-svg";
|
||||
import { type QrTemplateProps, getFontFamily, renderSvgLines } from "./index";
|
||||
import { type QrTemplateProps, getFontFamily, renderSvgLines } from "./common";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const template9Bg = require("../../../assets/templates/9.jpeg");
|
||||
@@ -17,6 +17,7 @@ const template9Bg = require("../../../assets/templates/9.jpeg");
|
||||
const QrTemplate9 = ({
|
||||
qrBase64,
|
||||
fields,
|
||||
hideQr,
|
||||
...props
|
||||
}: QrTemplateProps) => {
|
||||
const rName = fields?.restaurantName;
|
||||
@@ -46,68 +47,70 @@ const QrTemplate9 = ({
|
||||
/>
|
||||
|
||||
{/* QR Alanı (Merkez) */}
|
||||
<G transform="translate(362 428)">
|
||||
<Rect
|
||||
width={300}
|
||||
height={300}
|
||||
fill="#fff"
|
||||
stroke="#d5cebe"
|
||||
strokeWidth={1.5}
|
||||
rx={24}
|
||||
/>
|
||||
<Rect
|
||||
width={276}
|
||||
height={276}
|
||||
x={12}
|
||||
y={12}
|
||||
fill="#fafaf7"
|
||||
stroke="#1e3246"
|
||||
strokeDasharray="6 4"
|
||||
strokeOpacity={0.35}
|
||||
strokeWidth={1.5}
|
||||
rx={16}
|
||||
/>
|
||||
|
||||
{qrBase64 ? (
|
||||
<Image
|
||||
href={`data:image/png;base64,${qrBase64}`}
|
||||
x={16}
|
||||
y={16}
|
||||
width={268}
|
||||
height={268}
|
||||
{!hideQr && (
|
||||
<G transform="translate(362 428)">
|
||||
<Rect
|
||||
width={300}
|
||||
height={300}
|
||||
fill="#fff"
|
||||
stroke="#d5cebe"
|
||||
strokeWidth={1.5}
|
||||
rx={24}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Circle
|
||||
cx={150}
|
||||
cy={132}
|
||||
r={28}
|
||||
fill="#f5efe6"
|
||||
stroke="#1e3246"
|
||||
strokeWidth={1.5}
|
||||
<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}
|
||||
/>
|
||||
<Path
|
||||
fill="none"
|
||||
stroke="#1e3246"
|
||||
strokeLinecap="round"
|
||||
strokeWidth={1.9}
|
||||
d="M142.4 116.8v11.4c0 3.8 5.7 3.8 5.7 0v-11.4m-2.85 0v11.4m0 3.8v15.2m9.5-30.4c4.75 3.8 4.75 15.2 0 17.1v13.3"
|
||||
/>
|
||||
<Text
|
||||
x={150}
|
||||
y={192}
|
||||
fill="#1e3246"
|
||||
fontFamily="System, -apple-system, Roboto, sans-serif"
|
||||
fontSize={14}
|
||||
fontWeight={800}
|
||||
letterSpacing={2}
|
||||
textAnchor="middle"
|
||||
>
|
||||
{"QR KOD ALANI"}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</G>
|
||||
) : (
|
||||
<>
|
||||
<Circle
|
||||
cx={150}
|
||||
cy={132}
|
||||
r={28}
|
||||
fill="#f5efe6"
|
||||
stroke="#1e3246"
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
<Path
|
||||
fill="none"
|
||||
stroke="#1e3246"
|
||||
strokeLinecap="round"
|
||||
strokeWidth={1.9}
|
||||
d="M142.4 116.8v11.4c0 3.8 5.7 3.8 5.7 0v-11.4m-2.85 0v11.4m0 3.8v15.2m9.5-30.4c4.75 3.8 4.75 15.2 0 17.1v13.3"
|
||||
/>
|
||||
<Text
|
||||
x={150}
|
||||
y={192}
|
||||
fill="#1e3246"
|
||||
fontFamily="System, -apple-system, Roboto, sans-serif"
|
||||
fontSize={14}
|
||||
fontWeight={800}
|
||||
letterSpacing={2}
|
||||
textAnchor="middle"
|
||||
>
|
||||
{"QR KOD ALANI"}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</G>
|
||||
)}
|
||||
|
||||
{/* Üst Amblem & Rozet */}
|
||||
<G stroke="#a88b58" transform="translate(290 142)">
|
||||
|
||||
@@ -7,11 +7,12 @@ import Svg, {
|
||||
Text,
|
||||
Image,
|
||||
} from "react-native-svg";
|
||||
import { type QrTemplateProps, getFontFamily, renderSvgLines } from "./index";
|
||||
import { type QrTemplateProps, getFontFamily, renderSvgLines } from "./common";
|
||||
|
||||
const QrTemplateLumiere = ({
|
||||
qrBase64,
|
||||
fields,
|
||||
hideQr,
|
||||
...props
|
||||
}: QrTemplateProps) => {
|
||||
const rName = fields?.restaurantName;
|
||||
@@ -91,43 +92,45 @@ const QrTemplateLumiere = ({
|
||||
<Path stroke="#d4af37" strokeOpacity={0.4} strokeWidth={0.8} d="M120 198h360" />
|
||||
|
||||
{/* QR Placeholder / Gerçek QR */}
|
||||
<G transform="translate(190 215)">
|
||||
<Rect width={220} height={220} fill="#fff" stroke="#d4af37" strokeWidth={2} rx={20} />
|
||||
{qrBase64 ? (
|
||||
<Image
|
||||
href={`data:image/png;base64,${qrBase64}`}
|
||||
x={10}
|
||||
y={10}
|
||||
width={200}
|
||||
height={200}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Rect
|
||||
width={196}
|
||||
height={196}
|
||||
x={12}
|
||||
y={12}
|
||||
fill="#fafafa"
|
||||
stroke="#d4af37"
|
||||
strokeDasharray="4 3"
|
||||
strokeOpacity={0.4}
|
||||
rx={14}
|
||||
{!hideQr && (
|
||||
<G transform="translate(190 215)">
|
||||
<Rect width={220} height={220} fill="#fff" stroke="#d4af37" strokeWidth={2} rx={20} />
|
||||
{qrBase64 ? (
|
||||
<Image
|
||||
href={`data:image/png;base64,${qrBase64}`}
|
||||
x={10}
|
||||
y={10}
|
||||
width={200}
|
||||
height={200}
|
||||
/>
|
||||
<Text
|
||||
x={110}
|
||||
y={115}
|
||||
fill="#71717a"
|
||||
fontFamily="System, sans-serif"
|
||||
fontSize={12}
|
||||
fontWeight={600}
|
||||
textAnchor="middle"
|
||||
>
|
||||
{"QR KOD ALANI"}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</G>
|
||||
) : (
|
||||
<>
|
||||
<Rect
|
||||
width={196}
|
||||
height={196}
|
||||
x={12}
|
||||
y={12}
|
||||
fill="#fafafa"
|
||||
stroke="#d4af37"
|
||||
strokeDasharray="4 3"
|
||||
strokeOpacity={0.4}
|
||||
rx={14}
|
||||
/>
|
||||
<Text
|
||||
x={110}
|
||||
y={115}
|
||||
fill="#71717a"
|
||||
fontFamily="System, sans-serif"
|
||||
fontSize={12}
|
||||
fontWeight={600}
|
||||
textAnchor="middle"
|
||||
>
|
||||
{"QR KOD ALANI"}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</G>
|
||||
)}
|
||||
|
||||
{/* Metinler (Çok satırlı ve bağımsız font destekli) */}
|
||||
<G textAnchor="middle">
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import React from "react";
|
||||
import { Text as SvgText, type SvgProps } from "react-native-svg";
|
||||
|
||||
export interface TextItemConfig {
|
||||
text: string;
|
||||
fontSize?: number;
|
||||
fontId?: string;
|
||||
offsetY?: number; // Dikey konum kaydırma (yukarı / aşağı)
|
||||
visible?: boolean;
|
||||
}
|
||||
|
||||
export interface QrCustomFields {
|
||||
restaurantName: TextItemConfig;
|
||||
subtitle: TextItemConfig;
|
||||
tableNo: TextItemConfig;
|
||||
ctaText: TextItemConfig;
|
||||
wifiText: TextItemConfig;
|
||||
socialText: TextItemConfig;
|
||||
}
|
||||
|
||||
export interface QrTemplateProps extends SvgProps {
|
||||
qrBase64?: string;
|
||||
fields?: Partial<QrCustomFields>;
|
||||
hideQr?: boolean;
|
||||
}
|
||||
|
||||
// Türkçe karakterleri (İ, ı, Ş, ş, Ğ, ğ, Ü, ü, Ö, ö, Ç, ç) eksiksiz destekleyen sistem fontları
|
||||
export const FONT_OPTIONS = [
|
||||
{
|
||||
id: "sans",
|
||||
label: "Modern Sans",
|
||||
fontFamily: "System, -apple-system, Roboto, 'Helvetica Neue', Arial, sans-serif",
|
||||
},
|
||||
{
|
||||
id: "serif",
|
||||
label: "Zarif Serif",
|
||||
fontFamily: "Georgia, 'Times New Roman', serif",
|
||||
},
|
||||
{
|
||||
id: "times",
|
||||
label: "Klasik (Times)",
|
||||
fontFamily: "'Times New Roman', Times, Georgia, serif",
|
||||
},
|
||||
{
|
||||
id: "palatino",
|
||||
label: "Lüks Palatino",
|
||||
fontFamily: "Palatino, 'Palatino Linotype', Georgia, serif",
|
||||
},
|
||||
{
|
||||
id: "trebuchet",
|
||||
label: "Dinamik",
|
||||
fontFamily: "'Trebuchet MS', 'Segoe UI', Arial, sans-serif",
|
||||
},
|
||||
];
|
||||
|
||||
export function getFontFamily(fontId?: string, defaultFamily = "Georgia, serif"): string {
|
||||
if (!fontId) return defaultFamily;
|
||||
const found = FONT_OPTIONS.find((f) => f.id === fontId);
|
||||
return found ? found.fontFamily : defaultFamily;
|
||||
}
|
||||
|
||||
// Çok satırlı (Enter / \n) ve dikey kaydırmalı (offsetY) SVG metin render fonksiyonu
|
||||
export function renderSvgLines({
|
||||
text,
|
||||
x,
|
||||
y,
|
||||
offsetY = 0,
|
||||
lineHeight,
|
||||
fill,
|
||||
fontFamily,
|
||||
fontSize,
|
||||
fontWeight = "normal",
|
||||
letterSpacing,
|
||||
textAnchor = "middle",
|
||||
}: {
|
||||
text?: string;
|
||||
x: number;
|
||||
y: number;
|
||||
offsetY?: number;
|
||||
lineHeight?: number;
|
||||
fill?: string;
|
||||
fontFamily?: string;
|
||||
fontSize?: number;
|
||||
fontWeight?: string | number;
|
||||
letterSpacing?: number;
|
||||
textAnchor?: "middle" | "start" | "end";
|
||||
}) {
|
||||
if (!text) return null;
|
||||
const lines = text.split("\n");
|
||||
const computedLineHeight = lineHeight || (fontSize ? fontSize * 1.25 : 18);
|
||||
const startY = y + (offsetY || 0);
|
||||
|
||||
return (
|
||||
<>
|
||||
{lines.map((line, idx) => (
|
||||
<SvgText
|
||||
key={idx}
|
||||
x={x}
|
||||
y={startY + idx * computedLineHeight}
|
||||
fill={fill}
|
||||
fontFamily={fontFamily}
|
||||
fontSize={fontSize}
|
||||
fontWeight={fontWeight}
|
||||
letterSpacing={letterSpacing}
|
||||
textAnchor={textAnchor}
|
||||
>
|
||||
{line}
|
||||
</SvgText>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from "react";
|
||||
import { Text as SvgText, type SvgProps } from "react-native-svg";
|
||||
import QrTemplate1 from "./QrTemplateLumiere";
|
||||
import QrTemplate2 from "./QrTemplate2";
|
||||
import QrTemplate3 from "./QrTemplate3";
|
||||
@@ -8,115 +7,9 @@ import QrTemplate5 from "./QrTemplate5";
|
||||
import QrTemplate6 from "./QrTemplate6";
|
||||
import QrTemplate8 from "./QrTemplate8";
|
||||
import QrTemplate9 from "./QrTemplate9";
|
||||
import { type QrTemplateProps } from "./common";
|
||||
|
||||
export interface TextItemConfig {
|
||||
text: string;
|
||||
fontSize?: number;
|
||||
fontId?: string;
|
||||
offsetY?: number; // Dikey konum kaydırma (yukarı / aşağı)
|
||||
visible?: boolean;
|
||||
}
|
||||
|
||||
export interface QrCustomFields {
|
||||
restaurantName: TextItemConfig;
|
||||
subtitle: TextItemConfig;
|
||||
tableNo: TextItemConfig;
|
||||
ctaText: TextItemConfig;
|
||||
wifiText: TextItemConfig;
|
||||
socialText: TextItemConfig;
|
||||
}
|
||||
|
||||
export interface QrTemplateProps extends SvgProps {
|
||||
qrBase64?: string;
|
||||
fields?: Partial<QrCustomFields>;
|
||||
}
|
||||
|
||||
// Türkçe karakterleri (İ, ı, Ş, ş, Ğ, ğ, Ü, ü, Ö, ö, Ç, ç) eksiksiz destekleyen sistem fontları
|
||||
export const FONT_OPTIONS = [
|
||||
{
|
||||
id: "sans",
|
||||
label: "Modern Sans",
|
||||
fontFamily: "System, -apple-system, Roboto, 'Helvetica Neue', Arial, sans-serif",
|
||||
},
|
||||
{
|
||||
id: "serif",
|
||||
label: "Zarif Serif",
|
||||
fontFamily: "Georgia, 'Times New Roman', serif",
|
||||
},
|
||||
{
|
||||
id: "times",
|
||||
label: "Klasik (Times)",
|
||||
fontFamily: "'Times New Roman', Times, Georgia, serif",
|
||||
},
|
||||
{
|
||||
id: "palatino",
|
||||
label: "Lüks Palatino",
|
||||
fontFamily: "Palatino, 'Palatino Linotype', Georgia, serif",
|
||||
},
|
||||
{
|
||||
id: "trebuchet",
|
||||
label: "Dinamik",
|
||||
fontFamily: "'Trebuchet MS', 'Segoe UI', Arial, sans-serif",
|
||||
},
|
||||
];
|
||||
|
||||
export function getFontFamily(fontId?: string, defaultFamily = "Georgia, serif"): string {
|
||||
if (!fontId) return defaultFamily;
|
||||
const found = FONT_OPTIONS.find((f) => f.id === fontId);
|
||||
return found ? found.fontFamily : defaultFamily;
|
||||
}
|
||||
|
||||
// Çok satırlı (Enter / \n) ve dikey kaydırmalı (offsetY) SVG metin render fonksiyonu
|
||||
export function renderSvgLines({
|
||||
text,
|
||||
x,
|
||||
y,
|
||||
offsetY = 0,
|
||||
lineHeight,
|
||||
fill,
|
||||
fontFamily,
|
||||
fontSize,
|
||||
fontWeight = "normal",
|
||||
letterSpacing,
|
||||
textAnchor = "middle",
|
||||
}: {
|
||||
text?: string;
|
||||
x: number;
|
||||
y: number;
|
||||
offsetY?: number;
|
||||
lineHeight?: number;
|
||||
fill?: string;
|
||||
fontFamily?: string;
|
||||
fontSize?: number;
|
||||
fontWeight?: string | number;
|
||||
letterSpacing?: number;
|
||||
textAnchor?: "middle" | "start" | "end";
|
||||
}) {
|
||||
if (!text) return null;
|
||||
const lines = text.split("\n");
|
||||
const computedLineHeight = lineHeight || (fontSize ? fontSize * 1.25 : 18);
|
||||
const startY = y + (offsetY || 0);
|
||||
|
||||
return (
|
||||
<>
|
||||
{lines.map((line, idx) => (
|
||||
<SvgText
|
||||
key={idx}
|
||||
x={x}
|
||||
y={startY + idx * computedLineHeight}
|
||||
fill={fill}
|
||||
fontFamily={fontFamily}
|
||||
fontSize={fontSize}
|
||||
fontWeight={fontWeight}
|
||||
letterSpacing={letterSpacing}
|
||||
textAnchor={textAnchor}
|
||||
>
|
||||
{line}
|
||||
</SvgText>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
export * from "./common";
|
||||
|
||||
// Yeni component eklemek için buraya import edip COMPONENT_TEMPLATES'e kaydet
|
||||
export const COMPONENT_TEMPLATES: Record<
|
||||
@@ -142,7 +35,5 @@ export const TEMPLATE_ASPECT_RATIOS: Record<string, number> = {
|
||||
"qr-5": 380 / 500, // 380x500 = PNG arka planlı modern kart
|
||||
"qr-6": 400 / 580, // 400x580 = taş & ızgara vintage stant
|
||||
"qr-8": 1, // 1024x1024 = 1:1 Emerald Gold Özel Tasarım
|
||||
"qr-9": 1, // 1024x1024 = 1:1 Vintage Table Menu Özel Tasarım
|
||||
"qr-9": 380 / 500, // 380x500 = Vintage Table Menu Özel Tasarım
|
||||
};
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user