Files
menulio/apps/mobile/src/components/qr-canvas/QrCanvasStudio.tsx
T

1053 lines
38 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useRef, useState } from "react";
import {
Pressable,
ScrollView,
Text,
TextInput,
View,
} from "react-native";
import { Ionicons } from "@expo/vector-icons";
import * as Haptics from "expo-haptics";
import ViewShot from "react-native-view-shot";
import { CanvasLayerItem } from "./CanvasLayerItem";
import { CanvasQrItem } from "./CanvasQrItem";
import { InstagramTextEditorModal } from "./InstagramTextEditorModal";
import {
type CanvasTextLayer,
type CanvasQrConfig,
CANVAS_FONT_OPTIONS,
QR_SIZE_PRESETS,
QR_RADIUS_PRESETS,
getDefaultLayersForTemplate,
getDefaultQrConfigForTemplate,
} from "./types";
interface QrCanvasStudioProps {
templateKey: string;
restaurantName: string;
qrBase64?: string;
width: number;
height: number;
renderBackground: () => React.ReactNode;
layers: CanvasTextLayer[];
onLayersChange: React.Dispatch<React.SetStateAction<CanvasTextLayer[]>>;
qrConfig: CanvasQrConfig;
onQrConfigChange: React.Dispatch<React.SetStateAction<CanvasQrConfig>>;
viewShotRef?: React.RefObject<any>;
selectedLayerId: string | null;
onSelectLayer: (id: string | null) => void;
isQrSelected: boolean;
onSelectQr: (selected: boolean) => void;
onDragStateChange?: (isDragging: boolean) => void;
}
export const QrCanvasStudio: React.FC<QrCanvasStudioProps> = ({
templateKey,
restaurantName,
qrBase64,
width,
height,
renderBackground,
layers,
onLayersChange,
qrConfig,
onQrConfigChange,
viewShotRef,
selectedLayerId,
onSelectLayer,
isQrSelected,
onSelectQr,
onDragStateChange,
}) => {
const selectedLayer = layers.find((l) => l.id === selectedLayerId) || null;
// Instagram Metin Düzenleyici Modal Durumu
const [editingLayer, setEditingLayer] = useState<CanvasTextLayer | null>(null);
const [isTextModalOpen, setIsTextModalOpen] = useState(false);
// Instagram/Figma Kılavuz Çizgileri
const [guideLines, setGuideLines] = useState<{
verticalCenter: boolean;
horizontalCenter: boolean;
}>({ verticalCenter: false, horizontalCenter: false });
const lastVRef = useRef(false);
const lastHRef = useRef(false);
// Sürükleme Başlangıcı
const handleDragStart = () => {
onDragStateChange?.(true);
};
// Manyetik Yapışma (Snap) & Kılavuz Çizgileri
const handleSnap = (rawX: number, rawY: number, itemWidth: number, itemHeight: number) => {
const SNAP_THRESHOLD = 6;
let snappedX = Math.round(rawX);
let snappedY = Math.round(rawY);
let vCenter = false;
let hCenter = false;
// 1. Dikey Kılavuz (Yatayda Tam Ortalandığında)
const itemCenterX = rawX + itemWidth / 2;
const canvasCenterX = width / 2;
if (Math.abs(itemCenterX - canvasCenterX) <= SNAP_THRESHOLD) {
snappedX = Math.round(canvasCenterX - itemWidth / 2);
vCenter = true;
}
// 2. Yatay Kılavuz (Dikeyde Tam Ortalandığında)
const itemCenterY = rawY + itemHeight / 2;
const canvasCenterY = height / 2;
if (Math.abs(itemCenterY - canvasCenterY) <= SNAP_THRESHOLD) {
snappedY = Math.round(canvasCenterY - itemHeight / 2);
hCenter = true;
}
// Dokunsal Titreşim
if ((vCenter && !lastVRef.current) || (hCenter && !lastHRef.current)) {
try {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
} catch {}
}
lastVRef.current = vCenter;
lastHRef.current = hCenter;
setGuideLines({ verticalCenter: vCenter, horizontalCenter: hCenter });
return { snappedX, snappedY };
};
// Sürükleme Bitişi
const handleDragEnd = () => {
setGuideLines({ verticalCenter: false, horizontalCenter: false });
lastVRef.current = false;
lastHRef.current = false;
onDragStateChange?.(false);
};
// Metin Katmanı Pozisyon Güncelleme
const handleUpdateLayerPosition = (id: string, x: number, y: number) => {
onLayersChange((prev) =>
prev.map((l) => (l.id === id ? { ...l, x, y } : l))
);
};
// QR Kod Pozisyon Güncelleme
const handleUpdateQrPosition = (x: number, y: number) => {
onQrConfigChange((prev) => ({ ...prev, x, y }));
};
// QR Kodu Yatayda Ortala
const handleCenterQrHorizontally = () => {
const centeredX = Math.round((width - qrConfig.size) / 2);
onQrConfigChange((prev) => ({ ...prev, x: centeredX }));
try {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
} catch {}
};
// Seçili Metni Yatayda Ortala
const handleCenterLayerHorizontally = (id: string) => {
const target = layers.find((l) => l.id === id);
if (!target) return;
const approxW = 80;
const centeredX = Math.round((width - approxW) / 2);
onLayersChange((prev) =>
prev.map((l) => (l.id === id ? { ...l, x: centeredX } : l))
);
try {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
} catch {}
};
// Yeni Metin Katmanı Ekle (Instagram "Aa" butonu)
const handleAddTextLayer = () => {
try {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
} catch {}
const newLayer: CanvasTextLayer = {
id: `layer_${Date.now()}`,
text: "Yeni Metin",
x: Math.round(width / 2 - 45),
y: Math.round(height / 2 - 20),
rotation: 0,
fontSize: 16,
fontId: "sans",
color: "#FFFFFF",
bgBoxColor: "rgba(18, 18, 21, 0.88)",
fontWeight: "800",
textAlign: "center",
};
onLayersChange((prev) => [...prev, newLayer]);
onSelectQr(false);
onSelectLayer(newLayer.id);
// Doğrudan Instagram Text Editor'ı
setEditingLayer(newLayer);
setIsTextModalOpen(true);
};
// Katman Düzenle Modalını
const handleOpenTextEditor = (layer: CanvasTextLayer) => {
setEditingLayer(layer);
setIsTextModalOpen(true);
};
// Katman Düzenleme Kaydet
const handleSaveTextEditor = (updated: Partial<CanvasTextLayer>) => {
if (!editingLayer) return;
onLayersChange((prev) =>
prev.map((l) => (l.id === editingLayer.id ? { ...l, ...updated } : l))
);
};
// Katman Sil
const handleDeleteLayer = (id: string) => {
try {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning);
} catch {}
onLayersChange((prev) => prev.filter((l) => l.id !== id));
if (selectedLayerId === id) {
onSelectLayer(null);
}
};
// Katman Çoğalt
const handleDuplicateLayer = (id: string) => {
try {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
} catch {}
const target = layers.find((l) => l.id === id);
if (!target) return;
const duplicated: CanvasTextLayer = {
...target,
id: `layer_${Date.now()}`,
x: Math.min(width - 50, target.x + 12),
y: Math.min(height - 30, target.y + 12),
};
onLayersChange((prev) => [...prev, duplicated]);
onSelectQr(false);
onSelectLayer(duplicated.id);
};
// Şablonu Sıfırla
const handleResetLayers = () => {
try {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
} catch {}
const defaultLayers = getDefaultLayersForTemplate(templateKey, restaurantName);
const defaultQr = getDefaultQrConfigForTemplate(templateKey, width, height);
onLayersChange(defaultLayers);
onQrConfigChange(defaultQr);
onSelectQr(false);
onSelectLayer(null);
};
return (
<View style={{ width: "100%", alignItems: "center" }}>
{/* 1. INSTAGRAM STORY / CANVA STİLİ ÜST HIZLI ARAÇLAR */}
<View
style={{
width: "100%",
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
marginBottom: 12,
paddingHorizontal: 4,
}}
>
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
{/* Metin Ekle (+ Aa) */}
<Pressable
onPress={handleAddTextLayer}
hitSlop={{ top: 6, bottom: 6, left: 6, right: 6 }}
accessible={true}
accessibilityRole="button"
accessibilityLabel="Yeni Metin Ekle"
style={({ pressed }) => ({
backgroundColor: "#D4AF37",
paddingHorizontal: 14,
paddingVertical: 8,
borderRadius: 20,
flexDirection: "row",
alignItems: "center",
gap: 6,
opacity: pressed ? 0.8 : 1,
})}
>
<Text style={{ color: "#0C0C0E", fontSize: 13, fontWeight: "900" }}>+ Aa</Text>
<Text style={{ color: "#0C0C0E", fontSize: 12, fontWeight: "700" }}>Metin</Text>
</Pressable>
{/* QR Seçici / Ayar */}
<Pressable
onPress={() => {
try { Haptics.selectionAsync(); } catch {}
onSelectLayer(null);
onSelectQr(!isQrSelected);
}}
hitSlop={{ top: 6, bottom: 6, left: 6, right: 6 }}
style={({ pressed }) => ({
backgroundColor: isQrSelected ? "#D4AF37" : "#18181B",
borderWidth: 1,
borderColor: isQrSelected ? "#D4AF37" : "rgba(255,255,255,0.12)",
paddingHorizontal: 12,
paddingVertical: 8,
borderRadius: 20,
flexDirection: "row",
alignItems: "center",
gap: 5,
opacity: pressed ? 0.8 : 1,
})}
>
<Ionicons
name="qr-code-outline"
size={15}
color={isQrSelected ? "#0C0C0E" : "#FFFFFF"}
/>
<Text
style={{
color: isQrSelected ? "#0C0C0E" : "#FFFFFF",
fontSize: 12,
fontWeight: "700",
}}
>
QR Kodu
</Text>
</Pressable>
</View>
{/* Sağ: Sıfırla */}
<Pressable
onPress={handleResetLayers}
hitSlop={{ top: 6, bottom: 6, left: 6, right: 6 }}
style={({ pressed }) => ({
backgroundColor: "#18181B",
borderWidth: 1,
borderColor: "rgba(255,255,255,0.1)",
paddingHorizontal: 10,
paddingVertical: 7,
borderRadius: 16,
flexDirection: "row",
alignItems: "center",
gap: 4,
opacity: pressed ? 0.8 : 1,
})}
>
<Ionicons name="refresh-outline" size={14} color="#A1A1AA" />
<Text style={{ color: "#A1A1AA", fontSize: 11, fontWeight: "600" }}>Sıfırla</Text>
</Pressable>
</View>
{/* 2. TUVAL ALANI (INSTAGRAM KORUYUCU DOKUNMATİK ÇERÇEVE) */}
<Pressable
onPress={() => {
// Tuvalin boş alanına dokunulduğunda seçimi kaldır (temiz önizleme)
onSelectLayer(null);
onSelectQr(false);
}}
style={{
width,
height,
borderRadius: 24,
overflow: "hidden",
backgroundColor: "#0C0C0E",
shadowColor: "#000",
shadowOffset: { width: 0, height: 10 },
shadowOpacity: 0.5,
shadowRadius: 20,
elevation: 8,
marginBottom: 14,
position: "relative",
}}
>
<ViewShot
ref={viewShotRef}
options={{ format: "png", quality: 1.0 }}
style={{ width, height, position: "relative" }}
>
{/* Arka Plan Şablonu */}
<View pointerEvents="none" style={{ position: "absolute", top: 0, left: 0, width, height }}>
{renderBackground()}
</View>
{/* Sürüklenebilir & Boyutlandırılabilir QR Kod */}
<CanvasQrItem
qrConfig={qrConfig}
qrBase64={qrBase64}
isSelected={isQrSelected}
onSelect={() => {
try { Haptics.selectionAsync(); } catch {}
onSelectLayer(null);
onSelectQr(true);
}}
onUpdatePosition={handleUpdateQrPosition}
onDragStart={handleDragStart}
onDragMove={handleSnap}
onDragEnd={handleDragEnd}
canvasWidth={width}
canvasHeight={height}
/>
{/* Sürüklenebilir & Çift Dokunmalı Metin Katmanları */}
{layers.map((layer) => (
<CanvasLayerItem
key={layer.id}
layer={layer}
isSelected={selectedLayerId === layer.id}
onSelect={(id) => {
try { Haptics.selectionAsync(); } catch {}
onSelectQr(false);
onSelectLayer(id);
}}
onEdit={handleOpenTextEditor}
onUpdatePosition={handleUpdateLayerPosition}
onDelete={handleDeleteLayer}
onDragStart={handleDragStart}
onDragMove={handleSnap}
onDragEnd={handleDragEnd}
canvasWidth={width}
canvasHeight={height}
/>
))}
</ViewShot>
{/* 📐 FIGMA / INSTAGRAM MANYETİK KILAVUZ ÇİZGİLERİ */}
{guideLines.verticalCenter && (
<View
pointerEvents="none"
style={{
position: "absolute",
top: 0,
bottom: 0,
left: Math.round(width / 2) - 0.75,
width: 1.5,
backgroundColor: "#D4AF37",
zIndex: 999,
shadowColor: "#D4AF37",
shadowOffset: { width: 0, height: 0 },
shadowOpacity: 0.9,
shadowRadius: 4,
}}
/>
)}
{guideLines.horizontalCenter && (
<View
pointerEvents="none"
style={{
position: "absolute",
left: 0,
right: 0,
top: Math.round(height / 2) - 0.75,
height: 1.5,
backgroundColor: "#D4AF37",
zIndex: 999,
shadowColor: "#D4AF37",
shadowOffset: { width: 0, height: 0 },
shadowOpacity: 0.9,
shadowRadius: 4,
}}
/>
)}
</Pressable>
{/* 3. SEÇİLİ ÖĞE İÇİN INSTAGRAM TARZI YÜZEN HIZLI ARAÇLAR (FLOATING DOCK) */}
{/* 3A: SEÇİLİ METİN KATMANI ARAÇLARI */}
{selectedLayer && !isQrSelected && (
<View
style={{
width: "100%",
backgroundColor: "#18181B",
borderRadius: 18,
borderWidth: 1,
borderColor: "rgba(212, 175, 55, 0.3)",
padding: 12,
marginBottom: 14,
gap: 10,
}}
>
{/* Üst Sıra: Hızlı Butonlar */}
<View style={{ flexDirection: "row", alignItems: "center", justifyContent: "space-between" }}>
<Text style={{ color: "#D4AF37", fontSize: 11, fontWeight: "800", letterSpacing: 0.5 }}>
SEÇİLİ METİN
</Text>
<View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
{/* Düzenle (Aa) Modalını Aç */}
<Pressable
onPress={() => handleOpenTextEditor(selectedLayer)}
hitSlop={{ top: 6, bottom: 6, left: 6, right: 6 }}
style={{
backgroundColor: "#D4AF37",
paddingHorizontal: 12,
paddingVertical: 5,
borderRadius: 14,
flexDirection: "row",
alignItems: "center",
gap: 4,
}}
>
<Ionicons name="create-outline" size={13} color="#0C0C0E" />
<Text style={{ color: "#0C0C0E", fontSize: 11, fontWeight: "800" }}>Aa Düzenle</Text>
</Pressable>
{/* Yatayda Ortala */}
<Pressable
onPress={() => handleCenterLayerHorizontally(selectedLayer.id)}
hitSlop={{ top: 6, bottom: 6, left: 6, right: 6 }}
style={{
backgroundColor: "#242429",
paddingHorizontal: 10,
paddingVertical: 5,
borderRadius: 14,
flexDirection: "row",
alignItems: "center",
gap: 4,
}}
>
<Ionicons name="swap-horizontal" size={12} color="#FFFFFF" />
<Text style={{ color: "#FFFFFF", fontSize: 11, fontWeight: "600" }}>Ortala</Text>
</Pressable>
{/* Çoğalt */}
<Pressable
onPress={() => handleDuplicateLayer(selectedLayer.id)}
hitSlop={{ top: 6, bottom: 6, left: 6, right: 6 }}
style={{
backgroundColor: "#242429",
paddingHorizontal: 8,
paddingVertical: 5,
borderRadius: 14,
}}
>
<Ionicons name="copy-outline" size={13} color="#FFFFFF" />
</Pressable>
{/* Sil */}
<Pressable
onPress={() => handleDeleteLayer(selectedLayer.id)}
hitSlop={{ top: 6, bottom: 6, left: 6, right: 6 }}
style={{
backgroundColor: "rgba(239, 68, 68, 0.15)",
paddingHorizontal: 8,
paddingVertical: 5,
borderRadius: 14,
}}
>
<Ionicons name="trash-outline" size={13} color="#EF4444" />
</Pressable>
</View>
</View>
{/* Hızlı Metin Değiştirme Girdisi */}
<View
style={{
flexDirection: "row",
alignItems: "center",
gap: 8,
backgroundColor: "#0C0C0E",
borderRadius: 12,
paddingHorizontal: 10,
paddingVertical: 6,
borderWidth: 1,
borderColor: "rgba(212, 175, 55, 0.4)",
}}
>
<Ionicons name="text-outline" size={14} color="#D4AF37" />
<TextInput
value={selectedLayer.text}
onChangeText={(newText: string) => {
onLayersChange((prev) =>
prev.map((l) => (l.id === selectedLayer.id ? { ...l, text: newText } : l))
);
}}
placeholder="Metin yazın..."
placeholderTextColor="#71717A"
style={{
flex: 1,
color: "#FFFFFF",
fontSize: 13,
fontWeight: "700",
padding: 0,
}}
/>
</View>
{/* Alt Sıra 1: Font Stili Seçici */}
<View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
<Text style={{ color: "#71717A", fontSize: 10, fontWeight: "700" }}>FONT:</Text>
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ gap: 6 }}>
{CANVAS_FONT_OPTIONS.map((f) => {
const isActive = (selectedLayer.fontId || "sans") === f.id;
return (
<Pressable
key={f.id}
onPress={() => {
try { Haptics.selectionAsync(); } catch {}
onLayersChange((prev) =>
prev.map((l) => (l.id === selectedLayer.id ? { ...l, fontId: f.id } : l))
);
}}
style={{
paddingHorizontal: 10,
paddingVertical: 4,
borderRadius: 10,
backgroundColor: isActive ? "#D4AF37" : "#242429",
}}
>
<Text
style={{
color: isActive ? "#0C0C0E" : "#FFFFFF",
fontSize: 10.5,
fontWeight: isActive ? "900" : "700",
fontFamily: f.fontFamily,
}}
>
{f.label}
</Text>
</Pressable>
);
})}
</ScrollView>
</View>
{/* Alt Sıra 2: Hızlı Döndürme Açısı Hapları */}
<View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
<Text style={{ color: "#71717A", fontSize: 10, fontWeight: "700" }}>EĞİM:</Text>
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ gap: 6 }}>
{[-15, -6, 0, 6, 15].map((deg) => {
const isActive = (selectedLayer.rotation || 0) === deg;
return (
<Pressable
key={deg}
onPress={() => {
try { Haptics.selectionAsync(); } catch {}
onLayersChange((prev) =>
prev.map((l) => (l.id === selectedLayer.id ? { ...l, rotation: deg } : l))
);
}}
style={{
paddingHorizontal: 10,
paddingVertical: 4,
borderRadius: 10,
backgroundColor: isActive ? "#D4AF37" : "#242429",
}}
>
<Text
style={{
color: isActive ? "#0C0C0E" : "#FFFFFF",
fontSize: 10.5,
fontWeight: isActive ? "800" : "600",
}}
>
{deg}°
</Text>
</Pressable>
);
})}
</ScrollView>
</View>
{/* Alt Sıra 3: Kavis (Eğri Yazı) Stepper & Preset Hapları */}
<View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
<Text style={{ color: "#71717A", fontSize: 10, fontWeight: "700" }}>KAVİS:</Text>
{/* Stepper Butonları (- / +) */}
<View style={{ flexDirection: "row", alignItems: "center", backgroundColor: "#242429", borderRadius: 10, paddingHorizontal: 4, paddingVertical: 2, gap: 4 }}>
<Pressable
onPress={() => {
try { Haptics.selectionAsync(); } catch {}
onLayersChange((prev) =>
prev.map((l) =>
l.id === selectedLayer.id
? { ...l, curveDegree: Math.max(-100, (l.curveDegree || 0) - 5) }
: l
)
);
}}
hitSlop={{ top: 6, bottom: 6, left: 6, right: 6 }}
style={{ width: 22, height: 22, borderRadius: 11, backgroundColor: "rgba(255,255,255,0.12)", alignItems: "center", justifyContent: "center" }}
>
<Ionicons name="remove" size={13} color="#FFFFFF" />
</Pressable>
<Text style={{ color: "#D4AF37", fontSize: 11, fontWeight: "900", minWidth: 30, textAlign: "center" }}>
{(selectedLayer.curveDegree || 0)}°
</Text>
<Pressable
onPress={() => {
try { Haptics.selectionAsync(); } catch {}
onLayersChange((prev) =>
prev.map((l) =>
l.id === selectedLayer.id
? { ...l, curveDegree: Math.min(100, (l.curveDegree || 0) + 5) }
: l
)
);
}}
hitSlop={{ top: 6, bottom: 6, left: 6, right: 6 }}
style={{ width: 22, height: 22, borderRadius: 11, backgroundColor: "rgba(255,255,255,0.12)", alignItems: "center", justifyContent: "center" }}
>
<Ionicons name="add" size={13} color="#FFFFFF" />
</Pressable>
</View>
{/* Hazır Kavis Presetleri */}
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ gap: 6 }}>
{[
{ label: "0° (Düz)", deg: 0 },
{ label: "20° ◠", deg: 20 },
{ label: "35° ◠", deg: 35 },
{ label: "50° ◠", deg: 50 },
{ label: "70° ◠", deg: 70 },
{ label: "-35° ◡", deg: -35 },
{ label: "-50° ◡", deg: -50 },
].map((item) => {
const isActive = (selectedLayer.curveDegree || 0) === item.deg;
return (
<Pressable
key={item.deg}
onPress={() => {
try { Haptics.selectionAsync(); } catch {}
onLayersChange((prev) =>
prev.map((l) => (l.id === selectedLayer.id ? { ...l, curveDegree: item.deg } : l))
);
}}
style={{
paddingHorizontal: 9,
paddingVertical: 4,
borderRadius: 10,
backgroundColor: isActive ? "#D4AF37" : "#242429",
}}
>
<Text
style={{
color: isActive ? "#0C0C0E" : "#FFFFFF",
fontSize: 10.5,
fontWeight: isActive ? "800" : "600",
}}
>
{item.label}
</Text>
</Pressable>
);
})}
</ScrollView>
</View>
</View>
)}
{/* 3B: SEÇİLİ QR KOD ARAÇLARI (TAM GELİŞMİŞ AYARLAR) */}
{isQrSelected && (
<View
style={{
width: "100%",
backgroundColor: "#18181B",
borderRadius: 20,
borderWidth: 1.5,
borderColor: "rgba(212, 175, 55, 0.45)",
padding: 14,
marginBottom: 14,
gap: 12,
shadowColor: "#000",
shadowOffset: { width: 0, height: 6 },
shadowOpacity: 0.35,
shadowRadius: 10,
elevation: 6,
}}
>
{/* Üst Başlık & Ortala / Kapat */}
<View style={{ flexDirection: "row", alignItems: "center", justifyContent: "space-between" }}>
<View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
<Ionicons name="qr-code" size={15} color="#D4AF37" />
<Text style={{ color: "#D4AF37", fontSize: 12, fontWeight: "800", letterSpacing: 0.5 }}>
QR KOD AYARLARI
</Text>
</View>
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
<Pressable
onPress={handleCenterQrHorizontally}
hitSlop={{ top: 6, bottom: 6, left: 6, right: 6 }}
style={({ pressed }) => ({
backgroundColor: "#242429",
paddingHorizontal: 11,
paddingVertical: 5,
borderRadius: 14,
borderWidth: 1,
borderColor: "rgba(212, 175, 55, 0.3)",
flexDirection: "row",
alignItems: "center",
gap: 5,
opacity: pressed ? 0.8 : 1,
})}
>
<Ionicons name="swap-horizontal" size={13} color="#D4AF37" />
<Text style={{ color: "#D4AF37", fontSize: 11, fontWeight: "700" }}>Ortala</Text>
</Pressable>
<Pressable
onPress={() => onSelectQr(false)}
hitSlop={{ top: 6, bottom: 6, left: 6, right: 6 }}
style={{
backgroundColor: "rgba(255,255,255,0.08)",
paddingHorizontal: 9,
paddingVertical: 5,
borderRadius: 14,
}}
>
<Text style={{ color: "#A1A1AA", fontSize: 11, fontWeight: "600" }}>Kapat</Text>
</Pressable>
</View>
</View>
{/* 1. Hassas Boyut Stepper'ı & Hızlı Seçenekler */}
<View style={{ gap: 6 }}>
<View style={{ flexDirection: "row", alignItems: "center", justifyContent: "space-between" }}>
<Text style={{ color: "#71717A", fontSize: 10.5, fontWeight: "700" }}>BOYUT:</Text>
<View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
<Pressable
onPress={() => {
try { Haptics.selectionAsync(); } catch {}
onQrConfigChange((prev) => ({ ...prev, size: Math.max(70, prev.size - 5) }));
}}
style={{
backgroundColor: "#27272A",
width: 28,
height: 24,
borderRadius: 8,
alignItems: "center",
justifyContent: "center",
}}
>
<Text style={{ color: "#FFF", fontWeight: "800", fontSize: 13 }}>-</Text>
</Pressable>
<View
style={{
backgroundColor: "#0C0C0E",
paddingHorizontal: 8,
paddingVertical: 3,
borderRadius: 6,
borderWidth: 1,
borderColor: "rgba(255,255,255,0.1)",
}}
>
<Text style={{ color: "#D4AF37", fontSize: 11, fontWeight: "800", fontVariant: ["tabular-nums"] }}>
{qrConfig.size} px
</Text>
</View>
<Pressable
onPress={() => {
try { Haptics.selectionAsync(); } catch {}
onQrConfigChange((prev) => ({ ...prev, size: Math.min(220, prev.size + 5) }));
}}
style={{
backgroundColor: "#27272A",
width: 28,
height: 24,
borderRadius: 8,
alignItems: "center",
justifyContent: "center",
}}
>
<Text style={{ color: "#FFF", fontWeight: "800", fontSize: 13 }}>+</Text>
</Pressable>
</View>
</View>
<View style={{ flexDirection: "row", gap: 5 }}>
{[
{ label: "Kompakt", size: 95 },
{ label: "Standart", size: 115 },
{ label: "Büyük", size: 135 },
{ label: "Geniş", size: 155 },
].map((p) => {
const isActive = Math.abs(qrConfig.size - p.size) <= 5;
return (
<Pressable
key={p.label}
onPress={() => {
try { Haptics.selectionAsync(); } catch {}
onQrConfigChange((prev) => ({ ...prev, size: p.size }));
}}
style={{
flex: 1,
backgroundColor: isActive ? "#D4AF37" : "#242429",
paddingVertical: 5,
borderRadius: 8,
alignItems: "center",
}}
>
<Text
style={{
color: isActive ? "#0C0C0E" : "#FFFFFF",
fontSize: 10,
fontWeight: isActive ? "800" : "600",
}}
>
{p.label}
</Text>
</Pressable>
);
})}
</View>
</View>
{/* 2. Kenarlık Rengi (Border Color) */}
<View style={{ gap: 6 }}>
<Text style={{ color: "#71717A", fontSize: 10.5, fontWeight: "700" }}>ÇERÇEVE KENARLIK RENGİ:</Text>
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ gap: 8, alignItems: "center" }}>
{[
{ color: "#D4AF37", name: "Altın" },
{ color: "#F6EFE2", name: "Krem" },
{ color: "#FFFFFF", name: "Beyaz" },
{ color: "#18181B", name: "Siyah" },
{ color: "#8C6239", name: "Bronz" },
{ color: "#0C4443", name: "Zümrüt" },
{ color: "#B92B27", name: "Kırmızı" },
{ color: "transparent", name: "Yok" },
].map((item) => {
const isSelected = (qrConfig.borderColor || "#D4AF37").toLowerCase() === item.color.toLowerCase();
return (
<Pressable
key={item.name}
onPress={() => {
try { Haptics.selectionAsync(); } catch {}
onQrConfigChange((prev) => ({ ...prev, borderColor: item.color }));
}}
style={{
flexDirection: "row",
alignItems: "center",
gap: 5,
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 10,
backgroundColor: isSelected ? "rgba(212, 175, 55, 0.25)" : "#242429",
borderWidth: 1.5,
borderColor: isSelected ? "#D4AF37" : "transparent",
}}
>
<View
style={{
width: 14,
height: 14,
borderRadius: 7,
backgroundColor: item.color === "transparent" ? "#3F3F46" : item.color,
borderWidth: 1,
borderColor: "rgba(255,255,255,0.4)",
}}
/>
<Text style={{ color: isSelected ? "#D4AF37" : "#E4E4E7", fontSize: 10.5, fontWeight: "700" }}>
{item.name}
</Text>
</Pressable>
);
})}
</ScrollView>
</View>
{/* 3. Köşe Yuvarlaklığı & Kenarlık Kalınlığı */}
<View style={{ flexDirection: "row", gap: 10 }}>
{/* Köşe */}
<View style={{ flex: 1, gap: 5 }}>
<Text style={{ color: "#71717A", fontSize: 10, fontWeight: "700" }}>KÖŞE OVAL:</Text>
<View style={{ flexDirection: "row", gap: 4 }}>
{[
{ label: "Düz", radius: 0 },
{ label: "Hafif", radius: 8 },
{ label: "Zarif", radius: 14 },
{ label: "Yuvarlak", radius: 24 },
].map((r) => {
const isActive = qrConfig.borderRadius === r.radius;
return (
<Pressable
key={r.label}
onPress={() => {
try { Haptics.selectionAsync(); } catch {}
onQrConfigChange((prev) => ({ ...prev, borderRadius: r.radius }));
}}
style={{
flex: 1,
backgroundColor: isActive ? "#D4AF37" : "#242429",
paddingVertical: 5,
borderRadius: 7,
alignItems: "center",
}}
>
<Text
style={{
color: isActive ? "#0C0C0E" : "#FFFFFF",
fontSize: 9.5,
fontWeight: isActive ? "800" : "600",
}}
>
{r.label}
</Text>
</Pressable>
);
})}
</View>
</View>
{/* Kalınlık */}
<View style={{ flex: 1, gap: 5 }}>
<Text style={{ color: "#71717A", fontSize: 10, fontWeight: "700" }}>KALINLIK:</Text>
<View style={{ flexDirection: "row", gap: 4 }}>
{[
{ label: "0px", width: 0 },
{ label: "1px", width: 1 },
{ label: "2px", width: 2 },
{ label: "3px", width: 3 },
].map((w) => {
const isActive = (qrConfig.borderWidth ?? 2) === w.width;
return (
<Pressable
key={w.label}
onPress={() => {
try { Haptics.selectionAsync(); } catch {}
onQrConfigChange((prev) => ({ ...prev, borderWidth: w.width }));
}}
style={{
flex: 1,
backgroundColor: isActive ? "#D4AF37" : "#242429",
paddingVertical: 5,
borderRadius: 7,
alignItems: "center",
}}
>
<Text
style={{
color: isActive ? "#0C0C0E" : "#FFFFFF",
fontSize: 9.5,
fontWeight: isActive ? "800" : "600",
}}
>
{w.label}
</Text>
</Pressable>
);
})}
</View>
</View>
</View>
</View>
)}
{/* 4. INSTAGRAM METİN DÜZENLEME MODALI */}
<InstagramTextEditorModal
visible={isTextModalOpen}
layer={editingLayer}
onSave={handleSaveTextEditor}
onClose={() => {
setIsTextModalOpen(false);
setEditingLayer(null);
}}
onDelete={handleDeleteLayer}
/>
</View>
);
};