feat(mobile): integrate Instagram/Canva style interactive QR Canvas Studio with touch drag, rotate, badge boxes, shadows and dynamic layers
This commit is contained in:
+46
-469
@@ -24,11 +24,22 @@ import { getActiveRestaurant, type ActiveRestaurant } from "@/lib/active-restaur
|
||||
import {
|
||||
COMPONENT_TEMPLATES,
|
||||
TEMPLATE_ASPECT_RATIOS,
|
||||
FONT_OPTIONS,
|
||||
type QrCustomFields,
|
||||
type TextItemConfig,
|
||||
} from "@/components/qr-templates";
|
||||
import {
|
||||
QrCanvasStudio,
|
||||
getDefaultLayersForTemplate,
|
||||
type CanvasTextLayer,
|
||||
} from "@/components/qr-canvas";
|
||||
|
||||
const HIDE_ALL_TEMPLATE_FIELDS: QrCustomFields = {
|
||||
restaurantName: { text: "", visible: false },
|
||||
subtitle: { text: "", visible: false },
|
||||
tableNo: { text: "", visible: false },
|
||||
ctaText: { text: "", visible: false },
|
||||
wifiText: { text: "", visible: false },
|
||||
socialText: { text: "", visible: false },
|
||||
};
|
||||
|
||||
interface QrResponse {
|
||||
id: string;
|
||||
@@ -52,64 +63,11 @@ export default function QrScreen() {
|
||||
);
|
||||
const [exportingPng, setExportingPng] = useState(false);
|
||||
const [exportingPdf, setExportingPdf] = useState(false);
|
||||
const [showCustomizePanel, setShowCustomizePanel] = useState(false);
|
||||
|
||||
const [fields, setFields] = useState<QrCustomFields>({
|
||||
restaurantName: {
|
||||
text: "Gourmet Bistro",
|
||||
fontSize: 26,
|
||||
fontId: "serif",
|
||||
visible: true,
|
||||
},
|
||||
subtitle: {
|
||||
text: "RESTAURANT & BAR\nMODERN CUISINE",
|
||||
fontSize: 11,
|
||||
fontId: "sans",
|
||||
visible: true,
|
||||
},
|
||||
tableNo: {
|
||||
text: "MASA NO: 01",
|
||||
fontSize: 11,
|
||||
fontId: "sans",
|
||||
visible: true,
|
||||
},
|
||||
ctaText: {
|
||||
text: "MENÜYÜ GÖRMEK İÇİN OKUTUN",
|
||||
fontSize: 13,
|
||||
fontId: "serif",
|
||||
visible: true,
|
||||
},
|
||||
wifiText: {
|
||||
text: "WiFi: Gourmet_Guest | Şifre: lezzetli01",
|
||||
fontSize: 10,
|
||||
fontId: "sans",
|
||||
visible: true,
|
||||
},
|
||||
socialText: {
|
||||
text: "@gourmetbistro",
|
||||
fontSize: 10,
|
||||
fontId: "sans",
|
||||
visible: true,
|
||||
},
|
||||
});
|
||||
|
||||
const [selectedEditField, setSelectedEditField] = useState<keyof QrCustomFields | null>(null);
|
||||
|
||||
const FIELD_DEFINITIONS: Array<{
|
||||
key: keyof QrCustomFields;
|
||||
label: string;
|
||||
placeholder: string;
|
||||
min: number;
|
||||
max: number;
|
||||
icon: string;
|
||||
}> = [
|
||||
{ key: "restaurantName", label: "Restoran Başlığı", placeholder: "Restoran Adı", min: 14, max: 48, icon: "restaurant-outline" },
|
||||
{ key: "subtitle", label: "Slogan / Alt Başlık", placeholder: "Slogan (Enter ile alt satıra geçebilir)", min: 8, max: 26, icon: "chatbubble-outline" },
|
||||
{ key: "ctaText", label: "Yönlendirme (CTA) Metni", placeholder: "Örn: MENÜYÜ GÖRMEK İÇİN OKUTUN", min: 9, max: 28, icon: "scan-outline" },
|
||||
{ key: "tableNo", label: "Masa Numarası", placeholder: "Örn: MASA NO: 01", min: 8, max: 22, icon: "grid-outline" },
|
||||
{ key: "wifiText", label: "WiFi & Şifre Bilgisi", placeholder: "Örn: WiFi: Bistro | Şifre: 12345", min: 7, max: 20, icon: "wifi-outline" },
|
||||
{ key: "socialText", label: "Sosyal Medya / İletişim", placeholder: "Örn: @menulio", min: 7, max: 20, icon: "share-social-outline" },
|
||||
];
|
||||
// Instagram Story / Canva Tarzı Katmanlar
|
||||
const [layers, setLayers] = useState<CanvasTextLayer[]>(() =>
|
||||
getDefaultLayersForTemplate("qr-1", "Gourmet Bistro")
|
||||
);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const viewShotRef = useRef<any>(null);
|
||||
@@ -124,10 +82,6 @@ export default function QrScreen() {
|
||||
const displayWidth = 300;
|
||||
const displayHeight = Math.round(displayWidth / aspectRatio);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const activeRest = await getActiveRestaurant();
|
||||
@@ -143,13 +97,11 @@ export default function QrScreen() {
|
||||
const restDetail = await api.get<{ name?: string }>(`/restaurants/${activeRest.restaurantId}`);
|
||||
if (restDetail?.name) {
|
||||
setRestaurantName(restDetail.name);
|
||||
setFields((prev) => ({
|
||||
...prev,
|
||||
restaurantName: {
|
||||
...prev.restaurantName,
|
||||
text: restDetail.name || "Gourmet Bistro",
|
||||
},
|
||||
}));
|
||||
setLayers((prev) =>
|
||||
prev.map((l) =>
|
||||
l.id === "rName" ? { ...l, text: restDetail.name!.toUpperCase() } : l
|
||||
)
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// fallback
|
||||
@@ -164,6 +116,7 @@ export default function QrScreen() {
|
||||
|
||||
|
||||
|
||||
|
||||
async function handleCopy() {
|
||||
if (!qr?.targetUrl) return;
|
||||
await Clipboard.setStringAsync(qr.targetUrl);
|
||||
@@ -331,14 +284,17 @@ export default function QrScreen() {
|
||||
return (
|
||||
<Pressable
|
||||
key={preset.key}
|
||||
onPress={() => setSelectedKey(preset.key)}
|
||||
onPress={() => {
|
||||
setSelectedKey(preset.key);
|
||||
setLayers(getDefaultLayersForTemplate(preset.key, restaurantName));
|
||||
}}
|
||||
style={{
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 10,
|
||||
borderRadius: 14,
|
||||
backgroundColor: isSelected ? "#D4AF37" : "#1A1A1E",
|
||||
borderWidth: 1.5,
|
||||
borderColor: isSelected ? "#D4AF37" : "#2B2B32",
|
||||
borderRadius: 14,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 10,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
@@ -369,417 +325,38 @@ export default function QrScreen() {
|
||||
</ScrollView>
|
||||
)}
|
||||
|
||||
{/* Live Stand Preview Wrapped in ViewShot */}
|
||||
{/* İnteraktif Instagram / Canva Tarzı QR Stant Stüdyosu */}
|
||||
<View
|
||||
style={{
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: 16,
|
||||
borderRadius: 28,
|
||||
backgroundColor: "#18181B",
|
||||
width: "100%",
|
||||
marginBottom: 20,
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 10 },
|
||||
shadowOpacity: 0.5,
|
||||
shadowRadius: 20,
|
||||
elevation: 8,
|
||||
borderWidth: 1,
|
||||
borderColor: "rgba(255,255,255,0.08)",
|
||||
}}
|
||||
>
|
||||
<ViewShot ref={viewShotRef} options={{ format: "png", quality: 1.0 }}>
|
||||
{/* Component-based template — no XML parsing, pixel-perfect */}
|
||||
<View
|
||||
style={{
|
||||
width: displayWidth,
|
||||
height: displayHeight,
|
||||
borderRadius: 24,
|
||||
overflow: "hidden",
|
||||
backgroundColor: "#0C0C0E",
|
||||
}}
|
||||
>
|
||||
{TemplateComponent ? (
|
||||
<QrCanvasStudio
|
||||
templateKey={selectedKey}
|
||||
restaurantName={restaurantName}
|
||||
qrBase64={qr.pngBase64}
|
||||
width={displayWidth}
|
||||
height={displayHeight}
|
||||
renderBackground={() =>
|
||||
TemplateComponent ? (
|
||||
<TemplateComponent
|
||||
qrBase64={qr.pngBase64}
|
||||
width={displayWidth}
|
||||
height={displayHeight}
|
||||
fields={fields}
|
||||
fields={HIDE_ALL_TEMPLATE_FIELDS}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
layers={layers}
|
||||
onLayersChange={setLayers}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
</ViewShot>
|
||||
</View>
|
||||
|
||||
{/* Hızlı Katman Seçici (Canlı Seçim) */}
|
||||
<View style={{ width: "100%", marginBottom: 12 }}>
|
||||
<View style={{ flexDirection: "row", justifyContent: "space-between", alignItems: "center", marginBottom: 8 }}>
|
||||
<Text style={{ color: "#D4AF37", fontSize: 12, fontWeight: "800", letterSpacing: 0.5 }}>
|
||||
CANLI DÜZENLENECEK ALANI SEÇİN:
|
||||
</Text>
|
||||
{selectedEditField && (
|
||||
<Pressable
|
||||
onPress={() => setSelectedEditField(null)}
|
||||
style={{
|
||||
backgroundColor: "rgba(255,255,255,0.08)",
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 3,
|
||||
borderRadius: 6,
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: "#A1A1AA", fontSize: 11, fontWeight: "600" }}>Kapat</Text>
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ gap: 8 }}>
|
||||
{FIELD_DEFINITIONS.map(({ key, label, icon }) => {
|
||||
const item = fields[key];
|
||||
const isSelected = selectedEditField === key;
|
||||
return (
|
||||
<Pressable
|
||||
key={key}
|
||||
onPress={() => setSelectedEditField(isSelected ? null : key)}
|
||||
style={({ pressed }) => ({
|
||||
backgroundColor: isSelected ? "#D4AF37" : item.visible ? "#242429" : "#1A1A1E",
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 9,
|
||||
borderRadius: 12,
|
||||
borderWidth: 1.5,
|
||||
borderColor: isSelected ? "#D4AF37" : item.visible ? "rgba(212, 175, 55, 0.3)" : "#2B2B32",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
opacity: pressed ? 0.8 : 1,
|
||||
})}
|
||||
>
|
||||
<Ionicons
|
||||
name={icon as any}
|
||||
size={14}
|
||||
color={isSelected ? "#0C0C0E" : item.visible ? "#D4AF37" : "#71717A"}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
color: isSelected ? "#0C0C0E" : item.visible ? "#FFFFFF" : "#71717A",
|
||||
fontSize: 12,
|
||||
fontWeight: isSelected ? "800" : "700",
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
{item.offsetY ? (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: isSelected ? "#0C0C0E" : "#D4AF37",
|
||||
borderRadius: 6,
|
||||
paddingHorizontal: 4,
|
||||
paddingVertical: 1,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: isSelected ? "#D4AF37" : "#0C0C0E",
|
||||
fontSize: 9,
|
||||
fontWeight: "800",
|
||||
}}
|
||||
>
|
||||
{item.offsetY > 0 ? `+${item.offsetY}` : item.offsetY}px
|
||||
</Text>
|
||||
</View>
|
||||
) : null}
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
||||
{/* CANLI DÜZENLEYİCİ KARTI (Sayfa İçinde Anında Görsel Geri Bildirim) */}
|
||||
{selectedEditField && (() => {
|
||||
const def = FIELD_DEFINITIONS.find((d) => d.key === selectedEditField)!;
|
||||
const item = fields[selectedEditField];
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
backgroundColor: "#18181B",
|
||||
borderRadius: 20,
|
||||
borderWidth: 1.5,
|
||||
borderColor: "#D4AF37",
|
||||
padding: 16,
|
||||
marginBottom: 20,
|
||||
gap: 14,
|
||||
shadowColor: "#D4AF37",
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.15,
|
||||
shadowRadius: 10,
|
||||
elevation: 4,
|
||||
}}
|
||||
>
|
||||
{/* Başlık & Görünürlük */}
|
||||
<View style={{ flexDirection: "row", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
||||
<View
|
||||
style={{
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 8,
|
||||
backgroundColor: "#D4AF37",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Ionicons name={def.icon as any} size={16} color="#0C0C0E" />
|
||||
</View>
|
||||
<Text style={{ color: "#FFFFFF", fontSize: 15, fontWeight: "800" }}>
|
||||
{def.label}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Stantta Göster / Gizle */}
|
||||
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
||||
<Text style={{ color: item.visible ? "#10B981" : "#71717A", fontSize: 11, fontWeight: "700" }}>
|
||||
{item.visible ? "Görünür" : "Gizli"}
|
||||
</Text>
|
||||
<Switch
|
||||
value={item.visible}
|
||||
onValueChange={(v) =>
|
||||
setFields((prev) => ({
|
||||
...prev,
|
||||
[selectedEditField]: { ...prev[selectedEditField], visible: v },
|
||||
}))
|
||||
}
|
||||
trackColor={{ false: "#27272A", true: "#D4AF37" }}
|
||||
thumbColor="#FFFFFF"
|
||||
ios_backgroundColor="#27272A"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{item.visible && (
|
||||
<>
|
||||
{/* Metin Girişi (Enter ile alt satır destekli) */}
|
||||
<View style={{ gap: 4 }}>
|
||||
<Text style={{ color: "#D4AF37", fontSize: 11, fontWeight: "700" }}>
|
||||
METİN İÇERİĞİ (ENTER İLE YENİ SATIRA GEÇEBİLİR):
|
||||
</Text>
|
||||
<TextInput
|
||||
value={item.text}
|
||||
onChangeText={(t) =>
|
||||
setFields((prev) => ({
|
||||
...prev,
|
||||
[selectedEditField]: { ...prev[selectedEditField], text: t },
|
||||
}))
|
||||
}
|
||||
placeholder={def.placeholder}
|
||||
placeholderTextColor="#71717A"
|
||||
multiline
|
||||
style={{
|
||||
backgroundColor: "#121215",
|
||||
borderWidth: 1,
|
||||
borderColor: "rgba(255,255,255,0.12)",
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
color: "#FFFFFF",
|
||||
fontSize: 14,
|
||||
minHeight: 46,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* DİKEY KONUM / YUKARI - AŞAĞI TAŞIMA (offsetY) */}
|
||||
<View style={{ gap: 6, backgroundColor: "#121215", padding: 12, borderRadius: 12 }}>
|
||||
<View style={{ flexDirection: "row", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<Text style={{ color: "#D4AF37", fontSize: 11, fontWeight: "700" }}>
|
||||
DİKEY KONUM (YUKARI / AŞAĞI TAŞIMA)
|
||||
</Text>
|
||||
<Text style={{ color: "#E5C378", fontSize: 11, fontWeight: "800" }}>
|
||||
{item.offsetY ? `${item.offsetY > 0 ? "+" : ""}${item.offsetY} px` : "Orijinal (0px)"}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={{ flexDirection: "row", gap: 8 }}>
|
||||
<Pressable
|
||||
onPress={() =>
|
||||
setFields((prev) => ({
|
||||
...prev,
|
||||
[selectedEditField]: {
|
||||
...prev[selectedEditField],
|
||||
offsetY: (prev[selectedEditField].offsetY || 0) - 3,
|
||||
},
|
||||
}))
|
||||
}
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: "#242429",
|
||||
paddingVertical: 8,
|
||||
borderRadius: 8,
|
||||
alignItems: "center",
|
||||
flexDirection: "row",
|
||||
justifyContent: "center",
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
<Ionicons name="arrow-up" size={14} color="#D4AF37" />
|
||||
<Text style={{ color: "#FFFFFF", fontSize: 12, fontWeight: "700" }}>Yukarı (-3px)</Text>
|
||||
</Pressable>
|
||||
|
||||
<Pressable
|
||||
onPress={() =>
|
||||
setFields((prev) => ({
|
||||
...prev,
|
||||
[selectedEditField]: {
|
||||
...prev[selectedEditField],
|
||||
offsetY: (prev[selectedEditField].offsetY || 0) + 3,
|
||||
},
|
||||
}))
|
||||
}
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: "#242429",
|
||||
paddingVertical: 8,
|
||||
borderRadius: 8,
|
||||
alignItems: "center",
|
||||
flexDirection: "row",
|
||||
justifyContent: "center",
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
<Ionicons name="arrow-down" size={14} color="#D4AF37" />
|
||||
<Text style={{ color: "#FFFFFF", fontSize: 12, fontWeight: "700" }}>Aşağı (+3px)</Text>
|
||||
</Pressable>
|
||||
|
||||
{item.offsetY ? (
|
||||
<Pressable
|
||||
onPress={() =>
|
||||
setFields((prev) => ({
|
||||
...prev,
|
||||
[selectedEditField]: {
|
||||
...prev[selectedEditField],
|
||||
offsetY: 0,
|
||||
},
|
||||
}))
|
||||
}
|
||||
style={{
|
||||
backgroundColor: "#3A2A2A",
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 8,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: "#EF4444", fontSize: 11, fontWeight: "700" }}>Sıfırla</Text>
|
||||
</Pressable>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* YAZI TİPİ (FONT) & BOYUT YAN YANA */}
|
||||
<View style={{ gap: 6 }}>
|
||||
<Text style={{ color: "#D4AF37", fontSize: 11, fontWeight: "700" }}>
|
||||
YAZI TİPİ (TÜRKÇE DESTEKLİ FONT)
|
||||
</Text>
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ gap: 6 }}>
|
||||
{FONT_OPTIONS.map((f) => {
|
||||
const isSelected = item.fontId === f.id;
|
||||
return (
|
||||
<Pressable
|
||||
key={f.id}
|
||||
onPress={() =>
|
||||
setFields((prev) => ({
|
||||
...prev,
|
||||
[selectedEditField]: { ...prev[selectedEditField], fontId: f.id },
|
||||
}))
|
||||
}
|
||||
style={{
|
||||
backgroundColor: isSelected ? "#D4AF37" : "#242429",
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 7,
|
||||
borderRadius: 10,
|
||||
borderWidth: 1.5,
|
||||
borderColor: isSelected ? "#D4AF37" : "rgba(255,255,255,0.06)",
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: isSelected ? "#0C0C0E" : "#FFFFFF",
|
||||
fontSize: 11.5,
|
||||
fontWeight: isSelected ? "800" : "600",
|
||||
}}
|
||||
>
|
||||
{f.label}
|
||||
</Text>
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
||||
{/* YAZI BOYUTU (FONT SIZE) */}
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
backgroundColor: "#121215",
|
||||
padding: 10,
|
||||
borderRadius: 12,
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: "#D4AF37", fontSize: 12, fontWeight: "700" }}>
|
||||
YAZI BOYUTU:
|
||||
</Text>
|
||||
<View style={{ flexDirection: "row", alignItems: "center", gap: 10 }}>
|
||||
<Pressable
|
||||
onPress={() =>
|
||||
setFields((prev) => ({
|
||||
...prev,
|
||||
[selectedEditField]: {
|
||||
...prev[selectedEditField],
|
||||
fontSize: Math.max(def.min, (prev[selectedEditField].fontSize || 12) - 1),
|
||||
},
|
||||
}))
|
||||
}
|
||||
style={{
|
||||
backgroundColor: "#242429",
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 8,
|
||||
}}
|
||||
>
|
||||
<Ionicons name="remove" size={16} color="#FFFFFF" />
|
||||
</Pressable>
|
||||
|
||||
<Text style={{ color: "#D4AF37", fontSize: 16, fontWeight: "800", minWidth: 40, textAlign: "center" }}>
|
||||
{item.fontSize || 12} px
|
||||
</Text>
|
||||
|
||||
<Pressable
|
||||
onPress={() =>
|
||||
setFields((prev) => ({
|
||||
...prev,
|
||||
[selectedEditField]: {
|
||||
...prev[selectedEditField],
|
||||
fontSize: Math.min(def.max, (prev[selectedEditField].fontSize || 12) + 1),
|
||||
},
|
||||
}))
|
||||
}
|
||||
style={{
|
||||
backgroundColor: "#242429",
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 8,
|
||||
}}
|
||||
>
|
||||
<Ionicons name="add" size={16} color="#FFFFFF" />
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
})()}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import React, { useRef } from "react";
|
||||
import {
|
||||
PanResponder,
|
||||
Pressable,
|
||||
Text,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { type CanvasTextLayer, getCanvasFontFamily } from "./types";
|
||||
|
||||
interface CanvasLayerItemProps {
|
||||
layer: CanvasTextLayer;
|
||||
isSelected: boolean;
|
||||
onSelect: (id: string) => void;
|
||||
onUpdatePosition: (id: string, x: number, y: number) => void;
|
||||
onDelete: (id: string) => void;
|
||||
canvasWidth: number;
|
||||
canvasHeight: number;
|
||||
}
|
||||
|
||||
export const CanvasLayerItem: React.FC<CanvasLayerItemProps> = ({
|
||||
layer,
|
||||
isSelected,
|
||||
onSelect,
|
||||
onUpdatePosition,
|
||||
onDelete,
|
||||
canvasWidth,
|
||||
canvasHeight,
|
||||
}) => {
|
||||
const panStartRef = useRef({ x: layer.x, y: layer.y });
|
||||
|
||||
const panResponder = useRef(
|
||||
PanResponder.create({
|
||||
onStartShouldSetPanResponder: () => true,
|
||||
onMoveShouldSetPanResponder: () => true,
|
||||
onPanResponderGrant: () => {
|
||||
onSelect(layer.id);
|
||||
panStartRef.current = { x: layer.x, y: layer.y };
|
||||
},
|
||||
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));
|
||||
},
|
||||
})
|
||||
).current;
|
||||
|
||||
const fontFamily = getCanvasFontFamily(layer.fontId);
|
||||
|
||||
return (
|
||||
<View
|
||||
{...panResponder.panHandlers}
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: layer.x,
|
||||
top: layer.y,
|
||||
transform: [{ rotate: `${layer.rotation || 0}deg` }],
|
||||
zIndex: isSelected ? 100 : 10,
|
||||
padding: 4,
|
||||
}}
|
||||
>
|
||||
<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",
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
|
||||
{/* Seçili İken Hızlı Silme Rozeti */}
|
||||
{isSelected && (
|
||||
<Pressable
|
||||
onPress={() => onDelete(layer.id)}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: -12,
|
||||
right: -12,
|
||||
backgroundColor: "#EF4444",
|
||||
width: 22,
|
||||
height: 22,
|
||||
borderRadius: 11,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderWidth: 1.5,
|
||||
borderColor: "#FFFFFF",
|
||||
shadowColor: "#000",
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 3,
|
||||
elevation: 4,
|
||||
}}
|
||||
>
|
||||
<Ionicons name="close" size={12} color="#FFFFFF" />
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,573 @@
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
Pressable,
|
||||
ScrollView,
|
||||
Switch,
|
||||
Text,
|
||||
TextInput,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { CanvasLayerItem } from "./CanvasLayerItem";
|
||||
import {
|
||||
type CanvasTextLayer,
|
||||
CANVAS_FONT_OPTIONS,
|
||||
CANVAS_COLOR_PALETTE,
|
||||
INSTAGRAM_BADGE_STYLES,
|
||||
getDefaultLayersForTemplate,
|
||||
} 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[]>>;
|
||||
}
|
||||
|
||||
export const QrCanvasStudio: React.FC<QrCanvasStudioProps> = ({
|
||||
templateKey,
|
||||
restaurantName,
|
||||
width,
|
||||
height,
|
||||
renderBackground,
|
||||
layers,
|
||||
onLayersChange,
|
||||
}) => {
|
||||
const [selectedLayerId, setSelectedLayerId] = useState<string | null>(layers[0]?.id || null);
|
||||
|
||||
const selectedLayer = layers.find((l) => l.id === selectedLayerId);
|
||||
|
||||
// Pozisyon Güncelleme
|
||||
const handleUpdatePosition = (id: string, x: number, y: number) => {
|
||||
onLayersChange((prev) =>
|
||||
prev.map((l) => (l.id === id ? { ...l, x, y } : l))
|
||||
);
|
||||
};
|
||||
|
||||
// Yeni Katman Ekle
|
||||
const handleAddTextLayer = () => {
|
||||
const newLayer: CanvasTextLayer = {
|
||||
id: `layer_${Date.now()}`,
|
||||
text: "Yeni Metin",
|
||||
x: Math.round(width / 2 - 40),
|
||||
y: Math.round(height / 2 - 20),
|
||||
rotation: 0,
|
||||
fontSize: 14,
|
||||
fontId: "sans",
|
||||
color: "#FFFFFF",
|
||||
bgBoxColor: "rgba(18, 18, 21, 0.85)",
|
||||
fontWeight: "800",
|
||||
};
|
||||
onLayersChange((prev) => [...prev, newLayer]);
|
||||
setSelectedLayerId(newLayer.id);
|
||||
};
|
||||
|
||||
// Katman Sil
|
||||
const handleDeleteLayer = (id: string) => {
|
||||
onLayersChange((prev) => prev.filter((l) => l.id !== id));
|
||||
if (selectedLayerId === id) {
|
||||
setSelectedLayerId(null);
|
||||
}
|
||||
};
|
||||
|
||||
// Katman Kopyala
|
||||
const handleDuplicateLayer = (id: string) => {
|
||||
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 + 15),
|
||||
y: Math.min(height - 30, target.y + 15),
|
||||
};
|
||||
onLayersChange((prev) => [...prev, duplicated]);
|
||||
setSelectedLayerId(duplicated.id);
|
||||
};
|
||||
|
||||
// Şablonu Sıfırla
|
||||
const handleResetLayers = () => {
|
||||
const defaults = getDefaultLayersForTemplate(templateKey, restaurantName);
|
||||
onLayersChange(defaults);
|
||||
setSelectedLayerId(defaults[0]?.id || null);
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={{ width: "100%", alignItems: "center" }}>
|
||||
{/* 1. İNTERAKTİF TUVAL ALANI */}
|
||||
<View
|
||||
style={{
|
||||
width,
|
||||
height,
|
||||
borderRadius: 24,
|
||||
overflow: "hidden",
|
||||
backgroundColor: "#0C0C0E",
|
||||
position: "relative",
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 10 },
|
||||
shadowOpacity: 0.5,
|
||||
shadowRadius: 20,
|
||||
elevation: 8,
|
||||
}}
|
||||
>
|
||||
{/* Arka Plan Şablonu & Sabit QR */}
|
||||
<View style={{ position: "absolute", top: 0, left: 0, width, height }}>
|
||||
{renderBackground()}
|
||||
</View>
|
||||
|
||||
{/* Serbest Sürüklenebilir Katmanlar */}
|
||||
{layers.map((layer) => (
|
||||
<CanvasLayerItem
|
||||
key={layer.id}
|
||||
layer={layer}
|
||||
isSelected={selectedLayerId === layer.id}
|
||||
onSelect={setSelectedLayerId}
|
||||
onUpdatePosition={handleUpdatePosition}
|
||||
onDelete={handleDeleteLayer}
|
||||
canvasWidth={width}
|
||||
canvasHeight={height}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* 2. STÜDYO EYLEM BUTONLARI */}
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
marginVertical: 14,
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
{/* Yeni Metin Ekle */}
|
||||
<Pressable
|
||||
onPress={handleAddTextLayer}
|
||||
style={({ pressed }) => ({
|
||||
flex: 1,
|
||||
backgroundColor: "#D4AF37",
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 12,
|
||||
borderRadius: 12,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 6,
|
||||
opacity: pressed ? 0.85 : 1,
|
||||
})}
|
||||
>
|
||||
<Ionicons name="add-circle" size={18} color="#0C0C0E" />
|
||||
<Text style={{ color: "#0C0C0E", fontSize: 13, fontWeight: "800" }}>
|
||||
+ Metin Ekle
|
||||
</Text>
|
||||
</Pressable>
|
||||
|
||||
{/* Seçiliyi Kopyala */}
|
||||
{selectedLayer && (
|
||||
<Pressable
|
||||
onPress={() => handleDuplicateLayer(selectedLayer.id)}
|
||||
style={({ pressed }) => ({
|
||||
backgroundColor: "#242429",
|
||||
borderWidth: 1,
|
||||
borderColor: "rgba(255,255,255,0.1)",
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 12,
|
||||
borderRadius: 12,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
opacity: pressed ? 0.85 : 1,
|
||||
})}
|
||||
>
|
||||
<Ionicons name="copy-outline" size={16} color="#FFFFFF" />
|
||||
<Text style={{ color: "#FFFFFF", fontSize: 12, fontWeight: "700" }}>
|
||||
Çoğalt
|
||||
</Text>
|
||||
</Pressable>
|
||||
)}
|
||||
|
||||
{/* Sıfırla */}
|
||||
<Pressable
|
||||
onPress={handleResetLayers}
|
||||
style={({ pressed }) => ({
|
||||
backgroundColor: "#242429",
|
||||
borderWidth: 1,
|
||||
borderColor: "rgba(255,255,255,0.1)",
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 12,
|
||||
borderRadius: 12,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
opacity: pressed ? 0.85 : 1,
|
||||
})}
|
||||
>
|
||||
<Ionicons name="refresh-outline" size={16} color="#A1A1AA" />
|
||||
<Text style={{ color: "#A1A1AA", fontSize: 12, fontWeight: "600" }}>
|
||||
Sıfırla
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
{/* 3. SEÇİLİ METİN INSTAGRAM TARZI DÜZENLEYİCİ */}
|
||||
{selectedLayer ? (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
backgroundColor: "#18181B",
|
||||
borderRadius: 20,
|
||||
borderWidth: 1.5,
|
||||
borderColor: "#D4AF37",
|
||||
padding: 16,
|
||||
marginBottom: 20,
|
||||
gap: 14,
|
||||
shadowColor: "#D4AF37",
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.15,
|
||||
shadowRadius: 10,
|
||||
elevation: 4,
|
||||
}}
|
||||
>
|
||||
{/* Başlık & Kapat */}
|
||||
<View style={{ flexDirection: "row", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
||||
<View
|
||||
style={{
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 8,
|
||||
backgroundColor: "#D4AF37",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Ionicons name="sparkles" size={15} color="#0C0C0E" />
|
||||
</View>
|
||||
<Text style={{ color: "#FFFFFF", fontSize: 15, fontWeight: "800" }}>
|
||||
Metin & Stil Stüdyosu
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Pressable
|
||||
onPress={() => setSelectedLayerId(null)}
|
||||
style={{
|
||||
backgroundColor: "rgba(255,255,255,0.08)",
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 8,
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: "#A1A1AA", fontSize: 11, fontWeight: "600" }}>Seçimi Kaldır</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
{/* Metin İçeriği (Enter Destekli) */}
|
||||
<View style={{ gap: 4 }}>
|
||||
<Text style={{ color: "#D4AF37", fontSize: 11, fontWeight: "700" }}>
|
||||
METİN İÇERİĞİ (ENTER İLE YENİ SATIR):
|
||||
</Text>
|
||||
<TextInput
|
||||
value={selectedLayer.text}
|
||||
onChangeText={(t) =>
|
||||
onLayersChange((prev) =>
|
||||
prev.map((l) => (l.id === selectedLayer.id ? { ...l, text: t } : l))
|
||||
)
|
||||
}
|
||||
placeholder="Metninizi yazın..."
|
||||
placeholderTextColor="#71717A"
|
||||
multiline
|
||||
style={{
|
||||
backgroundColor: "#121215",
|
||||
borderWidth: 1,
|
||||
borderColor: "rgba(255,255,255,0.12)",
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
color: "#FFFFFF",
|
||||
fontSize: 14,
|
||||
minHeight: 46,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* DÖNDÜRME & EĞME AÇISI (ROTATION) */}
|
||||
<View style={{ gap: 6, backgroundColor: "#121215", padding: 12, borderRadius: 12 }}>
|
||||
<View style={{ flexDirection: "row", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<Text style={{ color: "#D4AF37", fontSize: 11, fontWeight: "700" }}>
|
||||
EĞİM & DÖNDÜRME AÇISI (ROTATION)
|
||||
</Text>
|
||||
<Text style={{ color: "#E5C378", fontSize: 11, fontWeight: "800" }}>
|
||||
{selectedLayer.rotation || 0}°
|
||||
</Text>
|
||||
</View>
|
||||
<View style={{ flexDirection: "row", gap: 6 }}>
|
||||
{[-15, -6, 0, 6, 15].map((deg) => {
|
||||
const isActive = selectedLayer.rotation === deg;
|
||||
return (
|
||||
<Pressable
|
||||
key={deg}
|
||||
onPress={() =>
|
||||
onLayersChange((prev) =>
|
||||
prev.map((l) =>
|
||||
l.id === selectedLayer.id ? { ...l, rotation: deg } : l
|
||||
)
|
||||
)
|
||||
}
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: isActive ? "#D4AF37" : "#242429",
|
||||
paddingVertical: 8,
|
||||
borderRadius: 8,
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: isActive ? "#0C0C0E" : "#FFFFFF",
|
||||
fontSize: 11,
|
||||
fontWeight: isActive ? "800" : "600",
|
||||
}}
|
||||
>
|
||||
{deg > 0 ? `+${deg}°` : `${deg}°`}
|
||||
</Text>
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* INSTAGRAM TARZI VURGU / KUTU STİLİ */}
|
||||
<View style={{ gap: 6 }}>
|
||||
<Text style={{ color: "#D4AF37", fontSize: 11, fontWeight: "700" }}>
|
||||
INSTAGRAM KUTUCUK STİLİ (BADGE):
|
||||
</Text>
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ gap: 6 }}>
|
||||
{INSTAGRAM_BADGE_STYLES.map((b) => {
|
||||
const isActive = selectedLayer.bgBoxColor === b.bgBoxColor;
|
||||
return (
|
||||
<Pressable
|
||||
key={b.id}
|
||||
onPress={() =>
|
||||
onLayersChange((prev) =>
|
||||
prev.map((l) =>
|
||||
l.id === selectedLayer.id ? { ...l, bgBoxColor: b.bgBoxColor } : l
|
||||
)
|
||||
)
|
||||
}
|
||||
style={{
|
||||
backgroundColor: isActive ? "#D4AF37" : "#242429",
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 7,
|
||||
borderRadius: 10,
|
||||
borderWidth: 1.5,
|
||||
borderColor: isActive ? "#D4AF37" : "rgba(255,255,255,0.06)",
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: isActive ? "#0C0C0E" : "#FFFFFF",
|
||||
fontSize: 11.5,
|
||||
fontWeight: isActive ? "800" : "600",
|
||||
}}
|
||||
>
|
||||
{b.label}
|
||||
</Text>
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
||||
{/* YAZI RENGİ PALETİ */}
|
||||
<View style={{ gap: 6 }}>
|
||||
<Text style={{ color: "#D4AF37", fontSize: 11, fontWeight: "700" }}>
|
||||
YAZI RENGİ:
|
||||
</Text>
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ gap: 8 }}>
|
||||
{CANVAS_COLOR_PALETTE.map((c) => {
|
||||
const isActive = selectedLayer.color === c.color;
|
||||
return (
|
||||
<Pressable
|
||||
key={c.id}
|
||||
onPress={() =>
|
||||
onLayersChange((prev) =>
|
||||
prev.map((l) =>
|
||||
l.id === selectedLayer.id ? { ...l, color: c.color } : l
|
||||
)
|
||||
)
|
||||
}
|
||||
style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 16,
|
||||
backgroundColor: c.color,
|
||||
borderWidth: isActive ? 3 : 1.5,
|
||||
borderColor: isActive ? "#D4AF37" : "rgba(255,255,255,0.3)",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
{isActive && (
|
||||
<Ionicons
|
||||
name="checkmark"
|
||||
size={16}
|
||||
color={c.color === "#FFFFFF" || c.color === "#F6EFE3" ? "#0C0C0E" : "#FFFFFF"}
|
||||
/>
|
||||
)}
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
||||
{/* YAZI TİPİ (FONT) */}
|
||||
<View style={{ gap: 6 }}>
|
||||
<Text style={{ color: "#D4AF37", fontSize: 11, fontWeight: "700" }}>
|
||||
YAZI TİPİ (TÜRKÇE UYUMLU):
|
||||
</Text>
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ gap: 6 }}>
|
||||
{CANVAS_FONT_OPTIONS.map((f) => {
|
||||
const isActive = selectedLayer.fontId === f.id;
|
||||
return (
|
||||
<Pressable
|
||||
key={f.id}
|
||||
onPress={() =>
|
||||
onLayersChange((prev) =>
|
||||
prev.map((l) =>
|
||||
l.id === selectedLayer.id ? { ...l, fontId: f.id } : l
|
||||
)
|
||||
)
|
||||
}
|
||||
style={{
|
||||
backgroundColor: isActive ? "#D4AF37" : "#242429",
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 7,
|
||||
borderRadius: 10,
|
||||
borderWidth: 1.5,
|
||||
borderColor: isActive ? "#D4AF37" : "rgba(255,255,255,0.06)",
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: isActive ? "#0C0C0E" : "#FFFFFF",
|
||||
fontSize: 11.5,
|
||||
fontWeight: isActive ? "800" : "600",
|
||||
}}
|
||||
>
|
||||
{f.label}
|
||||
</Text>
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
||||
{/* BOYUT & GÖLGE AYARI */}
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
backgroundColor: "#121215",
|
||||
padding: 10,
|
||||
borderRadius: 12,
|
||||
}}
|
||||
>
|
||||
{/* Boyut */}
|
||||
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
||||
<Text style={{ color: "#D4AF37", fontSize: 12, fontWeight: "700" }}>
|
||||
Boyut:
|
||||
</Text>
|
||||
<Pressable
|
||||
onPress={() =>
|
||||
onLayersChange((prev) =>
|
||||
prev.map((l) =>
|
||||
l.id === selectedLayer.id
|
||||
? { ...l, fontSize: Math.max(8, l.fontSize - 1) }
|
||||
: l
|
||||
)
|
||||
)
|
||||
}
|
||||
style={{
|
||||
backgroundColor: "#242429",
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 8,
|
||||
}}
|
||||
>
|
||||
<Ionicons name="remove" size={15} color="#FFFFFF" />
|
||||
</Pressable>
|
||||
<Text style={{ color: "#D4AF37", fontSize: 15, fontWeight: "800", minWidth: 32, textAlign: "center" }}>
|
||||
{selectedLayer.fontSize}px
|
||||
</Text>
|
||||
<Pressable
|
||||
onPress={() =>
|
||||
onLayersChange((prev) =>
|
||||
prev.map((l) =>
|
||||
l.id === selectedLayer.id
|
||||
? { ...l, fontSize: Math.min(60, l.fontSize + 1) }
|
||||
: l
|
||||
)
|
||||
)
|
||||
}
|
||||
style={{
|
||||
backgroundColor: "#242429",
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 8,
|
||||
}}
|
||||
>
|
||||
<Ionicons name="add" size={15} color="#FFFFFF" />
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
{/* Gölge Anahtarı */}
|
||||
<View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
|
||||
<Text style={{ color: "#A1A1AA", fontSize: 11, fontWeight: "700" }}>
|
||||
Gölge
|
||||
</Text>
|
||||
<Switch
|
||||
value={selectedLayer.hasShadow || false}
|
||||
onValueChange={(v) =>
|
||||
onLayersChange((prev) =>
|
||||
prev.map((l) =>
|
||||
l.id === selectedLayer.id ? { ...l, hasShadow: v } : l
|
||||
)
|
||||
)
|
||||
}
|
||||
trackColor={{ false: "#27272A", true: "#D4AF37" }}
|
||||
thumbColor="#FFFFFF"
|
||||
ios_backgroundColor="#27272A"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
backgroundColor: "#18181B",
|
||||
borderRadius: 16,
|
||||
padding: 14,
|
||||
marginBottom: 20,
|
||||
alignItems: "center",
|
||||
flexDirection: "row",
|
||||
justifyContent: "center",
|
||||
gap: 8,
|
||||
borderWidth: 1,
|
||||
borderColor: "rgba(255,255,255,0.06)",
|
||||
}}
|
||||
>
|
||||
<Ionicons name="hand-left-outline" size={18} color="#D4AF37" />
|
||||
<Text style={{ color: "#A1A1AA", fontSize: 12.5, fontWeight: "600" }}>
|
||||
Stant üzerindeki herhangi bir metne dokunarak sürükleyin veya düzenleyin.
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./types";
|
||||
export * from "./CanvasLayerItem";
|
||||
export * from "./QrCanvasStudio";
|
||||
@@ -0,0 +1,206 @@
|
||||
export interface CanvasTextLayer {
|
||||
id: string;
|
||||
text: string;
|
||||
x: number; // px from canvas left
|
||||
y: number; // px from canvas top
|
||||
rotation: number; // degrees (-180 to 180)
|
||||
fontSize: number;
|
||||
fontId: string;
|
||||
color: string;
|
||||
bgBoxColor?: string; // Instagram badge kutusu (örn: rgba(0,0,0,0.8), #D4AF37 vb.)
|
||||
hasShadow?: boolean;
|
||||
letterSpacing?: number;
|
||||
fontWeight?: "normal" | "600" | "700" | "800" | "900";
|
||||
}
|
||||
|
||||
export const CANVAS_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 const CANVAS_COLOR_PALETTE = [
|
||||
{ id: "gold", color: "#D4AF37", label: "Altın" },
|
||||
{ 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" },
|
||||
];
|
||||
|
||||
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)" },
|
||||
];
|
||||
|
||||
export function getCanvasFontFamily(fontId?: string): string {
|
||||
const found = CANVAS_FONT_OPTIONS.find((f) => f.id === fontId);
|
||||
return found ? found.fontFamily : "System, -apple-system, Roboto, sans-serif";
|
||||
}
|
||||
|
||||
// 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") {
|
||||
return [
|
||||
{
|
||||
id: "rName",
|
||||
text: restaurantName.toUpperCase(),
|
||||
x: 40,
|
||||
y: 50,
|
||||
rotation: 0,
|
||||
fontSize: 18,
|
||||
fontId: "serif",
|
||||
color: "#D4AF37",
|
||||
fontWeight: "800",
|
||||
letterSpacing: 2,
|
||||
},
|
||||
{
|
||||
id: "sub",
|
||||
text: "RESTAURANT & BAR",
|
||||
x: 65,
|
||||
y: 78,
|
||||
rotation: 0,
|
||||
fontSize: 9,
|
||||
fontId: "sans",
|
||||
color: "#E5C378",
|
||||
fontWeight: "600",
|
||||
letterSpacing: 1.5,
|
||||
},
|
||||
{
|
||||
id: "cta",
|
||||
text: "MENÜYÜ GÖRMEK İÇİN OKUTUN",
|
||||
x: 35,
|
||||
y: 236,
|
||||
rotation: 0,
|
||||
fontSize: 10,
|
||||
fontId: "serif",
|
||||
color: "#D4AF37",
|
||||
fontWeight: "700",
|
||||
letterSpacing: 1,
|
||||
},
|
||||
{
|
||||
id: "table",
|
||||
text: "MASA NO: 01 • WiFi: Bistro",
|
||||
x: 45,
|
||||
y: 265,
|
||||
rotation: 0,
|
||||
fontSize: 9,
|
||||
fontId: "sans",
|
||||
color: "#C5A059",
|
||||
fontWeight: "600",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (templateKey === "qr-8") {
|
||||
return [
|
||||
{
|
||||
id: "rName",
|
||||
text: "EXPLORE OUR MENU",
|
||||
x: 35,
|
||||
y: 35,
|
||||
rotation: 0,
|
||||
fontSize: 16,
|
||||
fontId: "sans",
|
||||
color: "#FFFFFF",
|
||||
fontWeight: "900",
|
||||
letterSpacing: 1,
|
||||
},
|
||||
{
|
||||
id: "sub",
|
||||
text: "THE DAILY SPECIALS",
|
||||
x: 50,
|
||||
y: 68,
|
||||
rotation: -4,
|
||||
fontSize: 13,
|
||||
fontId: "sans",
|
||||
color: "#D1A752",
|
||||
fontWeight: "800",
|
||||
},
|
||||
{
|
||||
id: "cta",
|
||||
text: "SCAN TO VIEW\nFULL MENU & SPECIALS",
|
||||
x: 55,
|
||||
y: 215,
|
||||
rotation: 0,
|
||||
fontSize: 9.5,
|
||||
fontId: "sans",
|
||||
color: "#FFFFFF",
|
||||
fontWeight: "800",
|
||||
},
|
||||
{
|
||||
id: "wifi",
|
||||
text: "SCAN TO ORDER & BROWSE",
|
||||
x: 55,
|
||||
y: 258,
|
||||
rotation: 0,
|
||||
fontSize: 9,
|
||||
fontId: "sans",
|
||||
color: "#D1A752",
|
||||
fontWeight: "800",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// Varsayılan
|
||||
return [
|
||||
{
|
||||
id: "rName",
|
||||
text: restaurantName,
|
||||
x: 50,
|
||||
y: 45,
|
||||
rotation: 0,
|
||||
fontSize: 17,
|
||||
fontId: "serif",
|
||||
color: "#D4AF37",
|
||||
fontWeight: "800",
|
||||
},
|
||||
{
|
||||
id: "sub",
|
||||
text: "SCAN TO ORDER",
|
||||
x: 75,
|
||||
y: 75,
|
||||
rotation: 0,
|
||||
fontSize: 10,
|
||||
fontId: "sans",
|
||||
color: "#FFFFFF",
|
||||
fontWeight: "700",
|
||||
},
|
||||
{
|
||||
id: "cta",
|
||||
text: "DİJİTAL MENÜ",
|
||||
x: 75,
|
||||
y: 245,
|
||||
rotation: 0,
|
||||
fontSize: 11,
|
||||
fontId: "serif",
|
||||
color: "#D4AF37",
|
||||
fontWeight: "800",
|
||||
},
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user