feat(mobile): add QrTemplate6 Stonehearth Grill, modal text editor and vertical positioning (offsetY) controls

This commit is contained in:
AyrisAI
2026-08-21 01:00:29 +03:00
parent 2530ab4005
commit 2c3599839b
10 changed files with 1070 additions and 186 deletions
+360 -117
View File
@@ -3,6 +3,7 @@ import { router } from "expo-router";
import {
ActivityIndicator,
Alert,
Modal,
Pressable,
ScrollView,
Share,
@@ -12,6 +13,7 @@ import {
View,
} from "react-native";
import { Ionicons } from "@expo/vector-icons";
import * as Clipboard from "expo-clipboard";
import ViewShot from "react-native-view-shot";
import * as Sharing from "expo-sharing";
@@ -91,6 +93,24 @@ export default function QrScreen() {
},
});
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" },
];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const viewShotRef = useRef<any>(null);
@@ -107,6 +127,7 @@ export default function QrScreen() {
useEffect(() => {
(async () => {
const activeRest = await getActiveRestaurant();
@@ -389,7 +410,55 @@ export default function QrScreen() {
</ViewShot>
</View>
{/* Metin & Font & Boyut Özelleştirme Kartı (Açılır/Kapanır) */}
{/* Hızlı Katman Seçici (Tıklayınca Modal Açar) */}
<View style={{ width: "100%", marginBottom: 16 }}>
<Text style={{ color: "#D4AF37", fontSize: 12, fontWeight: "700", marginBottom: 8, letterSpacing: 0.5 }}>
DÜZENLEMEK VE KONUMUNU DEĞİŞTİRMEK İÇİN TIKLAYIN:
</Text>
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ gap: 8 }}>
{FIELD_DEFINITIONS.map(({ key, label, icon }) => {
const item = fields[key];
return (
<Pressable
key={key}
onPress={() => setSelectedEditField(key)}
style={({ pressed }) => ({
backgroundColor: item.visible ? "#242429" : "#1A1A1E",
paddingHorizontal: 12,
paddingVertical: 9,
borderRadius: 12,
borderWidth: 1.5,
borderColor: item.visible ? "rgba(212, 175, 55, 0.4)" : "#2B2B32",
flexDirection: "row",
alignItems: "center",
gap: 6,
opacity: pressed ? 0.8 : 1,
})}
>
<Ionicons name={icon as any} size={14} color={item.visible ? "#D4AF37" : "#71717A"} />
<Text
style={{
color: item.visible ? "#FFFFFF" : "#71717A",
fontSize: 12,
fontWeight: "700",
}}
>
{label}
</Text>
{item.offsetY ? (
<View style={{ backgroundColor: "#D4AF37", borderRadius: 6, paddingHorizontal: 4, paddingVertical: 1 }}>
<Text style={{ color: "#0C0C0E", fontSize: 9, fontWeight: "800" }}>
{item.offsetY > 0 ? `+${item.offsetY}` : item.offsetY}px
</Text>
</View>
) : null}
</Pressable>
);
})}
</ScrollView>
</View>
{/* Metin & Font & Boyut Özelleştirme Kartı (Açılır/Kapanır Liste) */}
<View
style={{
backgroundColor: "#18181B",
@@ -423,10 +492,10 @@ export default function QrScreen() {
</View>
<View>
<Text style={{ color: "#FFFFFF", fontSize: 15, fontWeight: "700" }}>
Metin, Font & Boyut Özelleştir
Tüm Katmanları Listele
</Text>
<Text style={{ color: "#A1A1AA", fontSize: 12 }}>
Çok satırlı metin (Enter), özel font ve boyut ayarı
Tıkla & Düzenle, dikey konum kaydırma ve font ayarı
</Text>
</View>
</View>
@@ -438,59 +507,133 @@ export default function QrScreen() {
</Pressable>
{showCustomizePanel && (
<View style={{ marginTop: 16, gap: 16, borderTopWidth: 1, borderTopColor: "rgba(255,255,255,0.06)", paddingTop: 16 }}>
{/* Yardımcı Açıklama */}
<View
style={{
backgroundColor: "rgba(212, 175, 55, 0.08)",
borderRadius: 10,
padding: 10,
flexDirection: "row",
alignItems: "center",
gap: 8,
}}
>
<Ionicons name="information-circle-outline" size={16} color="#D4AF37" />
<Text style={{ color: "#E5C378", fontSize: 11.5, flex: 1 }}>
İpucu: Uzun metinlerde alt satıra geçmek için klavyeden Enter&apos;a basabilirsiniz.
</Text>
</View>
{/* Alan Düzenleyicileri */}
{(
[
{ key: "restaurantName", label: "RESTORAN BAŞLIĞI", placeholder: "Restoran Adı", min: 14, max: 48 },
{ key: "subtitle", label: "SLOGAN / ALT BAŞLIK", placeholder: "Slogan (Enter ile alt satıra geçebilir)", min: 8, max: 24 },
{ key: "ctaText", label: "YÖNLENDİRME (CTA) METNİ", placeholder: "Yönlendirme Metni", min: 9, max: 28 },
{ key: "tableNo", label: "MASA NUMARASI", placeholder: "Örn: MASA NO: 01", min: 8, max: 22 },
{ key: "wifiText", label: "WIFI BİLGİSİ", placeholder: "Örn: WiFi: Bistro | Şifre: 12345", min: 7, max: 20 },
{ key: "socialText", label: "SOSYAL MEDYA / İLETİŞİM", placeholder: "Örn: @menulio", min: 7, max: 20 },
] as const
).map(({ key, label, placeholder, min, max }) => {
<View style={{ marginTop: 16, gap: 12, borderTopWidth: 1, borderTopColor: "rgba(255,255,255,0.06)", paddingTop: 16 }}>
{FIELD_DEFINITIONS.map(({ key, label, icon }) => {
const item = fields[key];
return (
<View
<Pressable
key={key}
onPress={() => setSelectedEditField(key)}
style={({ pressed }) => ({
backgroundColor: "#141417",
borderRadius: 14,
padding: 12,
borderWidth: 1,
borderColor: "rgba(255,255,255,0.06)",
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
opacity: pressed ? 0.8 : 1,
})}
>
<View style={{ flexDirection: "row", alignItems: "center", gap: 10, flex: 1 }}>
<Ionicons name={icon as any} size={18} color="#D4AF37" />
<View style={{ flex: 1 }}>
<Text style={{ color: "#FFFFFF", fontSize: 13, fontWeight: "700" }}>
{label}
</Text>
<Text style={{ color: "#A1A1AA", fontSize: 11 }} numberOfLines={1}>
{item.text || "Boş"}
</Text>
</View>
</View>
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
<View
style={{
gap: 8,
paddingBottom: 12,
borderBottomWidth: 1,
borderBottomColor: "rgba(255,255,255,0.06)",
backgroundColor: "#242429",
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 8,
}}
>
{/* Başlık ve Görünürlük Anahtarı */}
<Text style={{ color: "#D4AF37", fontSize: 10, fontWeight: "700" }}>
{item.fontSize || 12}px
</Text>
</View>
<Ionicons name="create-outline" size={18} color="#D4AF37" />
</View>
</Pressable>
);
})}
</View>
)}
</View>
{/* Modal: Tıklanan Metin Katmanını Detaylı Düzenleme */}
<Modal
visible={!!selectedEditField}
animationType="slide"
transparent
onRequestClose={() => setSelectedEditField(null)}
>
<View
style={{
flex: 1,
backgroundColor: "rgba(0,0,0,0.75)",
justifyContent: "flex-end",
}}
>
{selectedEditField && (() => {
const def = FIELD_DEFINITIONS.find((d) => d.key === selectedEditField)!;
const item = fields[selectedEditField];
return (
<View
style={{
backgroundColor: "#18181B",
borderTopLeftRadius: 28,
borderTopRightRadius: 28,
padding: 20,
maxHeight: "85%",
gap: 16,
borderTopWidth: 1,
borderColor: "rgba(212, 175, 55, 0.3)",
}}
>
{/* Modal Başlığı & Kapat Butonu */}
<View style={{ flexDirection: "row", justifyContent: "space-between", alignItems: "center" }}>
<Text style={{ color: "#D4AF37", fontSize: 12, fontWeight: "700" }}>{label}</Text>
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
<Text style={{ color: item.visible ? "#10B981" : "#71717A", fontSize: 11, fontWeight: "600" }}>
{item.visible ? "Açık" : "Gizli"}
<Ionicons name={def.icon as any} size={20} color="#D4AF37" />
<Text style={{ color: "#FFFFFF", fontSize: 17, fontWeight: "800" }}>
{def.label}
</Text>
</View>
<Pressable
onPress={() => setSelectedEditField(null)}
style={{
width: 32,
height: 32,
borderRadius: 16,
backgroundColor: "#242429",
alignItems: "center",
justifyContent: "center",
}}
>
<Ionicons name="close" size={18} color="#FFFFFF" />
</Pressable>
</View>
<ScrollView showsVerticalScrollIndicator={false} contentContainerStyle={{ gap: 16, paddingBottom: 20 }}>
{/* Görünürlük Anahtarı */}
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
backgroundColor: "#121215",
padding: 12,
borderRadius: 14,
}}
>
<Text style={{ color: "#FFFFFF", fontSize: 13, fontWeight: "700" }}>
Stantta Göster
</Text>
<Switch
value={item.visible}
onValueChange={(v) =>
setFields((prev) => ({
...prev,
[key]: { ...prev[key], visible: v },
[selectedEditField]: { ...prev[selectedEditField], visible: v },
}))
}
trackColor={{ false: "#27272A", true: "#D4AF37" }}
@@ -498,44 +641,130 @@ export default function QrScreen() {
ios_backgroundColor="#27272A"
/>
</View>
</View>
{item.visible && (
<>
{/* Çok Satırlı TextInput */}
{/* Metin Girişi (Multiline / Enter Destekli) */}
<View style={{ gap: 6 }}>
<Text style={{ color: "#D4AF37", fontSize: 12, fontWeight: "700" }}>
METİN İÇERİĞİ (ENTER İLE YENİ SATIR)
</Text>
<TextInput
value={item.text}
onChangeText={(t) =>
setFields((prev) => ({
...prev,
[key]: { ...prev[key], text: t },
[selectedEditField]: { ...prev[selectedEditField], text: t },
}))
}
placeholder={placeholder}
placeholder={def.placeholder}
placeholderTextColor="#71717A"
multiline
style={{
backgroundColor: "#121215",
borderWidth: 1,
borderColor: "rgba(255,255,255,0.1)",
borderRadius: 12,
paddingHorizontal: 12,
paddingVertical: 8,
borderColor: "rgba(255,255,255,0.12)",
borderRadius: 14,
paddingHorizontal: 14,
paddingVertical: 12,
color: "#FFFFFF",
fontSize: 13,
minHeight: 42,
fontSize: 14,
minHeight: 60,
}}
/>
</View>
{/* Font ve Boyut Kontrol Çubuğu */}
<View style={{ flexDirection: "row", alignItems: "center", justifyContent: "space-between", gap: 8 }}>
{/* Font Hapları */}
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={{ gap: 6 }}
style={{ flex: 1 }}
{/* DİKEY KONUM / YUKARI - AŞAĞI TAŞIMA (offsetY) */}
<View style={{ gap: 8, backgroundColor: "#121215", padding: 12, borderRadius: 14 }}>
<View style={{ flexDirection: "row", justifyContent: "space-between", alignItems: "center" }}>
<Text style={{ color: "#D4AF37", fontSize: 12, fontWeight: "700" }}>
DİKEY KONUM (YUKARI / AŞAĞI)
</Text>
<Text style={{ color: "#E5C378", fontSize: 12, 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: 10,
borderRadius: 10,
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: 10,
borderRadius: 10,
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: 10,
borderRadius: 10,
alignItems: "center",
justifyContent: "center",
}}
>
<Text style={{ color: "#EF4444", fontSize: 11, fontWeight: "700" }}>Sıfırla</Text>
</Pressable>
) : null}
</View>
</View>
{/* YAZI TİPİ (FONT) SEÇİMİ */}
<View style={{ gap: 8 }}>
<Text style={{ color: "#D4AF37", fontSize: 12, fontWeight: "700" }}>
YAZI TİPİ (TÜRKÇE DESTEKLİ FONT)
</Text>
<View style={{ flexDirection: "row", flexWrap: "wrap", gap: 8 }}>
{FONT_OPTIONS.map((f) => {
const isSelected = item.fontId === f.id;
return (
@@ -544,23 +773,23 @@ export default function QrScreen() {
onPress={() =>
setFields((prev) => ({
...prev,
[key]: { ...prev[key], fontId: f.id },
[selectedEditField]: { ...prev[selectedEditField], fontId: f.id },
}))
}
style={{
backgroundColor: isSelected ? "#D4AF37" : "#242429",
paddingHorizontal: 9,
paddingVertical: 5,
borderRadius: 8,
borderWidth: 1,
paddingHorizontal: 14,
paddingVertical: 10,
borderRadius: 12,
borderWidth: 1.5,
borderColor: isSelected ? "#D4AF37" : "rgba(255,255,255,0.06)",
}}
>
<Text
style={{
color: isSelected ? "#0C0C0E" : "#D4D4D8",
fontSize: 10.5,
fontWeight: isSelected ? "700" : "500",
color: isSelected ? "#0C0C0E" : "#FFFFFF",
fontSize: 12,
fontWeight: isSelected ? "800" : "500",
}}
>
{f.label}
@@ -568,69 +797,83 @@ export default function QrScreen() {
</Pressable>
);
})}
</ScrollView>
</View>
</View>
{/* Boyut Artır / Azalt */}
<View
style={{
flexDirection: "row",
alignItems: "center",
backgroundColor: "#242429",
borderRadius: 8,
paddingHorizontal: 4,
borderWidth: 1,
borderColor: "rgba(255,255,255,0.06)",
}}
>
<Pressable
onPress={() =>
setFields((prev) => ({
...prev,
[key]: {
...prev[key],
fontSize: Math.max(min, (prev[key].fontSize || 12) - 1),
},
}))
}
style={{ padding: 6 }}
>
<Ionicons name="remove" size={13} color="#FFFFFF" />
</Pressable>
<Text
style={{
color: "#D4AF37",
fontSize: 11,
fontWeight: "700",
minWidth: 26,
textAlign: "center",
}}
>
{item.fontSize || 12}
{/* YAZI BOYUTU (FONT SIZE) */}
<View style={{ gap: 8, backgroundColor: "#121215", padding: 12, borderRadius: 14 }}>
<Text style={{ color: "#D4AF37", fontSize: 12, fontWeight: "700" }}>
YAZI BOYUTU
</Text>
<View style={{ flexDirection: "row", alignItems: "center", justifyContent: "space-between" }}>
<Pressable
onPress={() =>
setFields((prev) => ({
...prev,
[key]: {
...prev[key],
fontSize: Math.min(max, (prev[key].fontSize || 12) + 1),
[selectedEditField]: {
...prev[selectedEditField],
fontSize: Math.max(def.min, (prev[selectedEditField].fontSize || 12) - 1),
},
}))
}
style={{ padding: 6 }}
style={{
backgroundColor: "#242429",
paddingHorizontal: 16,
paddingVertical: 10,
borderRadius: 10,
}}
>
<Ionicons name="add" size={13} color="#FFFFFF" />
<Ionicons name="remove" size={18} color="#FFFFFF" />
</Pressable>
<Text style={{ color: "#D4AF37", fontSize: 18, fontWeight: "800" }}>
{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: 16,
paddingVertical: 10,
borderRadius: 10,
}}
>
<Ionicons name="add" size={18} color="#FFFFFF" />
</Pressable>
</View>
</View>
</>
)}
{/* Kaydet & Tamamla Butonu */}
<Pressable
onPress={() => setSelectedEditField(null)}
style={{
backgroundColor: "#D4AF37",
borderRadius: 14,
paddingVertical: 14,
alignItems: "center",
marginTop: 6,
}}
>
<Text style={{ color: "#0C0C0E", fontSize: 15, fontWeight: "800" }}>
Tamamla & Uygula
</Text>
</Pressable>
</ScrollView>
</View>
);
})}
</View>
)}
})()}
</View>
</Modal>
@@ -223,6 +223,7 @@ const QrTemplate2 = ({
text: rName?.text || "The Daily Grind",
x: 200,
y: 110,
offsetY: rName?.offsetY,
fill: "#2c1a0e",
fontFamily: rNameFont,
fontSize: rName?.fontSize || 28,
@@ -236,6 +237,7 @@ const QrTemplate2 = ({
text: sub?.text || "COFFEE & KITCHEN",
x: 200,
y: 144,
offsetY: sub?.offsetY,
fill: "#3a2312",
fontFamily: subFont,
fontSize: sub?.fontSize || 12,
@@ -249,6 +251,7 @@ const QrTemplate2 = ({
text: cta?.text || "MENÜYÜ GÖRMEK İÇİN OKUTUN",
x: 200,
y: 486,
offsetY: cta?.offsetY,
fill: "#2c1a0e",
fontFamily: ctaFont,
fontSize: cta?.fontSize || 15,
@@ -262,6 +265,7 @@ const QrTemplate2 = ({
text: wifi?.text || "WiFi: DailyGrind | Şifre: coffee2026",
x: 200,
y: 520,
offsetY: wifi?.offsetY,
fill: "#3a2312",
fontFamily: infoFont,
fontSize: wifi?.fontSize || 10.5,
@@ -280,6 +284,7 @@ const QrTemplate2 = ({
.join(" • "),
x: 200,
y: 544,
offsetY: table?.offsetY || social?.offsetY,
fill: "#8c6239",
fontFamily: infoFont,
fontSize: table?.fontSize || social?.fontSize || 10,
@@ -287,6 +292,7 @@ const QrTemplate2 = ({
letterSpacing: 1,
})}
<Text
x={200}
y={611}
@@ -258,6 +258,7 @@ const QrTemplate3 = ({
text: table?.text || "MASA NO: 01",
x: 200,
y: 50,
offsetY: table?.offsetY,
fill: "#18181b",
fontFamily: infoFont,
fontSize: table?.fontSize || 11,
@@ -282,6 +283,7 @@ const QrTemplate3 = ({
text: rName?.text || "BELLA NAPOLI",
x: 200,
y: 130,
offsetY: rName?.offsetY,
fill: "#18181b",
fontFamily: rNameFont,
fontSize: rName?.fontSize || 26,
@@ -295,6 +297,7 @@ const QrTemplate3 = ({
text: sub?.text || "AUTENTICA CUCINA ITALIANA",
x: 200,
y: 222,
offsetY: sub?.offsetY,
fill: "#2e7d32",
fontFamily: subFont,
fontSize: sub?.fontSize || 11.5,
@@ -308,6 +311,7 @@ const QrTemplate3 = ({
text: cta?.text || "MENÜYÜ GÖRMEK İÇİN OKUTUN",
x: 200,
y: 474,
offsetY: cta?.offsetY,
fill: "#b92b27",
fontFamily: ctaFont,
fontSize: cta?.fontSize || 13,
@@ -321,6 +325,7 @@ const QrTemplate3 = ({
text: wifi?.text || "WiFi: GourmetBistro | Şifre: lezzetli01",
x: 200,
y: 546,
offsetY: wifi?.offsetY,
fill: "#3e2723",
fontFamily: infoFont,
fontSize: wifi?.fontSize || 10.5,
@@ -333,12 +338,14 @@ const QrTemplate3 = ({
text: social?.text || "BİZİ TAKİP EDİN | @gourmetbistro",
x: 200,
y: 570,
offsetY: social?.offsetY,
fill: "#b92b27",
fontFamily: infoFont,
fontSize: social?.fontSize || 10.5,
fontWeight: "bold",
letterSpacing: 0.5,
})}
</G>
</Svg>
);
@@ -191,6 +191,7 @@ const QrTemplate4 = ({
text: table?.text || "MASA NO: 01",
x: 200,
y: 56,
offsetY: table?.offsetY,
fill: "#c59b4b",
fontFamily: infoFont,
fontSize: table?.fontSize || 11,
@@ -204,6 +205,7 @@ const QrTemplate4 = ({
text: rName?.text || "THE CRIMSON PETAL",
x: 200,
y: 114,
offsetY: rName?.offsetY,
fill: "#111111",
fontFamily: rNameFont,
fontSize: rName?.fontSize || 24,
@@ -217,6 +219,7 @@ const QrTemplate4 = ({
text: sub?.text || "FINE DINING EXPERIENCE",
x: 200,
y: 154,
offsetY: sub?.offsetY,
fill: "#71717a",
fontFamily: subFont,
fontSize: sub?.fontSize || 11,
@@ -230,6 +233,7 @@ const QrTemplate4 = ({
text: cta?.text || "MENÜYÜ GÖRMEK İÇİN OKUTUN",
x: 200,
y: 440,
offsetY: cta?.offsetY,
fill: "#111111",
fontFamily: ctaFont,
fontSize: cta?.fontSize || 15,
@@ -243,6 +247,7 @@ const QrTemplate4 = ({
text: wifi?.text || "WiFi: CrimsonGuest | Şifre: petal2026",
x: 200,
y: 488,
offsetY: wifi?.offsetY,
fill: "#71717a",
fontFamily: infoFont,
fontSize: wifi?.fontSize || 10.5,
@@ -256,6 +261,7 @@ const QrTemplate4 = ({
text: social?.text || "@thecrimsonpetal",
x: 200,
y: 514,
offsetY: social?.offsetY,
fill: "#c59b4b",
fontFamily: infoFont,
fontSize: social?.fontSize || 10.5,
@@ -263,6 +269,7 @@ const QrTemplate4 = ({
letterSpacing: 0.5,
})}
</G>
</Svg>
);
};
@@ -100,6 +100,7 @@ const QrTemplate5 = ({
text: table?.text || "MASA NO: 01",
x: 190,
y: 40,
offsetY: table?.offsetY,
fill: "#003554",
fontFamily: infoFont,
fontSize: table?.fontSize || 11,
@@ -113,6 +114,7 @@ const QrTemplate5 = ({
text: rName?.text || "THE URBAN FORK",
x: 190,
y: 86,
offsetY: rName?.offsetY,
fill: "#003554",
fontFamily: rNameFont,
fontSize: rName?.fontSize || 24,
@@ -126,6 +128,7 @@ const QrTemplate5 = ({
text: cta?.text || "DİJİTAL MENÜYÜ GÖRÜNTÜLE",
x: 190,
y: 160,
offsetY: cta?.offsetY,
fill: "#003554",
fontFamily: ctaFont,
fontSize: cta?.fontSize || 13,
@@ -139,6 +142,7 @@ const QrTemplate5 = ({
text: sub?.text || "FRESH FLAVORS DAILY",
x: 190,
y: 402,
offsetY: sub?.offsetY,
fill: "#003554",
fontFamily: subFont,
fontSize: sub?.fontSize || 13,
@@ -152,6 +156,7 @@ const QrTemplate5 = ({
text: wifi?.text || "WiFi: UrbanFork | Şifre: urban2026",
x: 190,
y: 430,
offsetY: wifi?.offsetY,
fill: "#003554",
fontFamily: infoFont,
fontSize: wifi?.fontSize || 10,
@@ -165,6 +170,7 @@ const QrTemplate5 = ({
text: social?.text || "@theurbanfork",
x: 190,
y: 454,
offsetY: social?.offsetY,
fill: "#003554",
fontFamily: infoFont,
fontSize: social?.fontSize || 10,
@@ -0,0 +1,260 @@
import * as React from "react";
import Svg, {
Defs,
G,
Path,
Circle,
Rect,
Use,
Text,
Image,
} from "react-native-svg";
import { type QrTemplateProps, getFontFamily, renderSvgLines } from "./index";
const QrTemplate6 = ({
qrBase64,
fields,
...props
}: QrTemplateProps) => {
const rName = fields?.restaurantName;
const sub = fields?.subtitle;
const table = fields?.tableNo;
const cta = fields?.ctaText;
const wifi = fields?.wifiText;
const social = fields?.socialText;
const rNameFont = getFontFamily(rName?.fontId, "Georgia, 'Times New Roman', serif");
const subFont = getFontFamily(sub?.fontId, "System, -apple-system, Roboto, sans-serif");
const ctaFont = getFontFamily(cta?.fontId, "Georgia, 'Times New Roman', serif");
const infoFont = getFontFamily(wifi?.fontId, "System, -apple-system, Roboto, sans-serif");
return (
<Svg width={300} height={435} viewBox="0 0 400 580" {...props}>
<Defs>
<G id="b6">
<Path
fill="none"
stroke="#c59b58"
strokeLinecap="round"
strokeWidth={1.2}
d="M0 0c10 25 12 55 5 85"
/>
<Path
fill="#f4efe6"
stroke="#c59b58"
strokeWidth={1.1}
d="M3 15c-11-5-15 9-1 9 4 0 4-6 1-9Z"
/>
<Path
fill="#f4efe6"
stroke="#9e4d28"
strokeWidth={1.1}
d="M5 22c11-6 17 6 3 10-4 0-4-6-3-10Zm2 20c-12-6-17 8-3 10 4 0 4-6 3-10Z"
/>
<Path
fill="#f4efe6"
stroke="#c59b58"
strokeWidth={1.1}
d="M8 50c12-6 16 8 2 12-4 0-4-7-2-12ZM6 70c-10-5-14 8-2 10 3 0 4-6 2-10Z"
/>
<Path
fill="#f4efe6"
stroke="#9e4d28"
strokeWidth={1.1}
d="M5 78c11-4 15 8 3 12-4 0-4-6-3-12Z"
/>
<Circle cx={-10} cy={18} r={1.5} fill="#c59b58" />
<Circle cx={18} cy={46} r={1.5} fill="#9e4d28" />
</G>
<G id="a6">
<Path
fill="#a64423"
d="M0-18C4-12 8-6 8 0c6-4 8 6 4 12 4-2 6 4 2 8S2 26 0 26s-10-2-14-6-2-10 2-8c-4-6-2-16 4-12 0-6 4-12 8-18"
/>
<Path
fill="#f4efe6"
d="M-4 4v8c0 3 2 4 4 4v8c2-8 4-9 4-12V4H3v7c0 2-1.5 3-3 3s-3-1-3-3V4Z"
/>
<Path stroke="#f4efe6" d="M0 4v8" />
</G>
</Defs>
<Path fill="#434b54" d="M0 0h400v580H0z" />
<Rect
width={360}
height={548}
x={20}
y={16}
fill="#f6efe3"
stroke="#122a42"
strokeWidth={1.5}
rx={16}
/>
<Path
fill="none"
stroke="#122a42"
strokeWidth={1.2}
d="M46 32h308a12 12 0 0 1 12 12v492a12 12 0 0 1-12 12H46a12 12 0 0 1-12-12V44a12 12 0 0 1 12-12Z"
/>
<Path
fill="none"
stroke="#122a42"
strokeWidth={1.2}
d="M34 46a12 12 0 0 0 12-12m320 12a12 12 0 0 1-12-12M34 534a12 12 0 0 1 12 12m320-12a12 12 0 0 0-12 12"
/>
<Path stroke="#c59b58" d="M34 135h332M34 500h332" />
<Use href="#a6" transform="matrix(.85 0 0 .85 200 52)" />
<Use href="#b6" transform="matrix(.8 0 0 .8 42 42)" />
<Use href="#b6" transform="matrix(-.8 0 0 .8 358 42)" />
<Use href="#b6" transform="matrix(.95 0 0 .95 40 185)" />
<Use href="#b6" transform="matrix(-.95 0 0 .95 360 185)" />
<Use href="#a6" transform="matrix(.65 0 0 .65 68 320)" />
<Use href="#a6" transform="matrix(.65 0 0 .65 332 320)" />
<Use href="#b6" transform="matrix(.95 0 0 .95 40 360)" />
<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}
/>
) : (
<>
<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">
{/* Masa Numarası */}
{table?.visible !== false &&
renderSvgLines({
text: table?.text || "MASA NO: 01",
x: 200,
y: 36,
offsetY: table?.offsetY,
fill: "#122a42",
fontFamily: infoFont,
fontSize: table?.fontSize || 10,
fontWeight: "bold",
letterSpacing: 1.5,
})}
{/* Restoran Başlığı */}
{rName?.visible !== false &&
renderSvgLines({
text: rName?.text || "THE STONEHEARTH GRILL",
x: 200,
y: 84,
offsetY: rName?.offsetY,
fill: "#a64423",
fontFamily: rNameFont,
fontSize: rName?.fontSize || 19,
fontWeight: "bold",
letterSpacing: 1.5,
})}
{/* Slogan */}
{sub?.visible !== false &&
renderSvgLines({
text: sub?.text || "SCAN. ORDER. ENJOY.",
x: 200,
y: 118,
offsetY: sub?.offsetY,
fill: "#122a42",
fontFamily: subFont,
fontSize: sub?.fontSize || 10,
fontWeight: "bold",
letterSpacing: 2,
})}
{/* CTA */}
{cta?.visible !== false &&
renderSvgLines({
text: cta?.text || "VIEW OUR MENU & ORDER ONLINE",
x: 200,
y: 160,
offsetY: cta?.offsetY,
fill: "#122a42",
fontFamily: ctaFont,
fontSize: cta?.fontSize || 13,
fontWeight: "bold",
letterSpacing: 0.5,
})}
{/* WiFi */}
{wifi?.visible !== false &&
renderSvgLines({
text: wifi?.text || "FREE WIFI: StonehearthGuest | PW: Grill720",
x: 200,
y: 524,
offsetY: wifi?.offsetY,
fill: "#222",
fontFamily: infoFont,
fontSize: wifi?.fontSize || 9,
fontWeight: "bold",
letterSpacing: 0.2,
})}
{/* Sosyal Medya */}
{social?.visible !== false &&
renderSvgLines({
text: social?.text || "Follow Us @StonehearthGrill",
x: 200,
y: 540,
offsetY: social?.offsetY,
fill: "#222",
fontFamily: infoFont,
fontSize: social?.fontSize || 9,
fontWeight: "bold",
letterSpacing: 0.2,
})}
</G>
</Svg>
);
};
export default QrTemplate6;
@@ -137,6 +137,7 @@ const QrTemplateLumiere = ({
text: rName?.text || "LUMIÈRE",
x: 300,
y: 148,
offsetY: rName?.offsetY,
fill: "#d4af37",
fontFamily: rNameFont,
fontSize: rName?.fontSize || 30,
@@ -150,6 +151,7 @@ const QrTemplateLumiere = ({
text: sub?.text || "RESTAURANT & BAR | MODERN CUISINE",
x: 300,
y: 178,
offsetY: sub?.offsetY,
fill: "#e5c378",
fontFamily: subFont,
fontSize: sub?.fontSize || 11,
@@ -163,6 +165,7 @@ const QrTemplateLumiere = ({
text: cta?.text || "MENÜYÜ GÖRMEK İÇİN OKUTUN",
x: 300,
y: 472,
offsetY: cta?.offsetY,
fill: "#d4af37",
fontFamily: ctaFont,
fontSize: cta?.fontSize || 13,
@@ -176,6 +179,7 @@ const QrTemplateLumiere = ({
text: social?.text || "Rezervasyon: 0212 555 0199",
x: 300,
y: 504,
offsetY: social?.offsetY,
fill: "#e2ddd2",
fontFamily: infoFont,
fontSize: social?.fontSize || 10.5,
@@ -194,12 +198,14 @@ const QrTemplateLumiere = ({
.join(" • "),
x: 300,
y: 526,
offsetY: table?.offsetY || wifi?.offsetY,
fill: "#c5a059",
fontFamily: infoFont,
fontSize: table?.fontSize || wifi?.fontSize || 10,
fontWeight: 600,
letterSpacing: 1,
})}
</G>
</Svg>
);
@@ -5,11 +5,13 @@ import QrTemplate2 from "./QrTemplate2";
import QrTemplate3 from "./QrTemplate3";
import QrTemplate4 from "./QrTemplate4";
import QrTemplate5 from "./QrTemplate5";
import QrTemplate6 from "./QrTemplate6";
export interface TextItemConfig {
text: string;
fontSize?: number;
fontId?: string;
offsetY?: number; // Dikey konum kaydırma (yukarı / aşağı)
visible?: boolean;
}
@@ -62,11 +64,12 @@ export function getFontFamily(fontId?: string, defaultFamily = "Georgia, serif")
return found ? found.fontFamily : defaultFamily;
}
// Çok satırlı (Enter / \n) metinleri SVG içinde alt alta kusursuz render eden yardımcı fonksiyon
// Ç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,
@@ -78,6 +81,7 @@ export function renderSvgLines({
text?: string;
x: number;
y: number;
offsetY?: number;
lineHeight?: number;
fill?: string;
fontFamily?: string;
@@ -89,6 +93,7 @@ export function renderSvgLines({
if (!text) return null;
const lines = text.split("\n");
const computedLineHeight = lineHeight || (fontSize ? fontSize * 1.25 : 18);
const startY = y + (offsetY || 0);
return (
<>
@@ -96,7 +101,7 @@ export function renderSvgLines({
<SvgText
key={idx}
x={x}
y={y + idx * computedLineHeight}
y={startY + idx * computedLineHeight}
fill={fill}
fontFamily={fontFamily}
fontSize={fontSize}
@@ -121,6 +126,7 @@ export const COMPONENT_TEMPLATES: Record<
"qr-3": QrTemplate3,
"qr-4": QrTemplate4,
"qr-5": QrTemplate5,
"qr-6": QrTemplate6,
};
// Her template'in aspect ratio'su (width/height)
@@ -130,4 +136,5 @@ export const TEMPLATE_ASPECT_RATIOS: Record<string, number> = {
"qr-3": 400 / 650, // 400x650 = standart kart
"qr-4": 400 / 580, // 400x580 = zarif altın çerçeve
"qr-5": 380 / 500, // 380x500 = PNG arka planlı modern kart
"qr-6": 400 / 580, // 400x580 = taş & ızgara vintage stant
};
+327
View File
@@ -0,0 +1,327 @@
import Svg, {
SvgProps,
Defs,
G,
Path,
Circle,
Rect,
Use,
Text,
} from "react-native-svg"
import { memo } from "react"
const SvgComponent = (props: SvgProps) => (
<Svg width={400} height={580} {...props}>
<Defs>
<G id="b">
<Path
fill="none"
stroke="#c59b58"
strokeLinecap="round"
strokeWidth={1.2}
d="M0 0c10 25 12 55 5 85"
/>
<Path
fill="#f4efe6"
stroke="#c59b58"
strokeWidth={1.1}
d="M3 15c-11-5-15 9-1 9 4 0 4-6 1-9Z"
/>
<Path
fill="#f4efe6"
stroke="#9e4d28"
strokeWidth={1.1}
d="M5 22c11-6 17 6 3 10-4 0-4-6-3-10Zm2 20c-12-6-17 8-3 10 4 0 4-6 3-10Z"
/>
<Path
fill="#f4efe6"
stroke="#c59b58"
strokeWidth={1.1}
d="M8 50c12-6 16 8 2 12-4 0-4-7-2-12ZM6 70c-10-5-14 8-2 10 3 0 4-6 2-10Z"
/>
<Path
fill="#f4efe6"
stroke="#9e4d28"
strokeWidth={1.1}
d="M5 78c11-4 15 8 3 12-4 0-4-6-3-12Z"
/>
<Circle cx={-10} cy={18} r={1.5} fill="#c59b58" />
<Circle cx={18} cy={46} r={1.5} fill="#9e4d28" />
</G>
<G id="a">
<Path
fill="#a64423"
d="M0-18C4-12 8-6 8 0c6-4 8 6 4 12 4-2 6 4 2 8S2 26 0 26s-10-2-14-6-2-10 2-8c-4-6-2-16 4-12 0-6 4-12 8-18"
/>
<Path
fill="#f4efe6"
d="M-4 4v8c0 3 2 4 4 4v8c2-8 4-9 4-12V4H3v7c0 2-1.5 3-3 3s-3-1-3-3V4Z"
/>
<Path stroke="#f4efe6" d="M0 4v8" />
</G>
</Defs>
<Path fill="#434b54" d="M0 0h400v580H0z" />
<Rect
width={360}
height={548}
x={20}
y={16}
fill="#f6efe3"
stroke="#122a42"
strokeWidth={1.5}
rx={16}
/>
<Path
fill="none"
stroke="#122a42"
strokeWidth={1.2}
d="M46 32h308a12 12 0 0 1 12 12v492a12 12 0 0 1-12 12H46a12 12 0 0 1-12-12V44a12 12 0 0 1 12-12Z"
/>
<Path
fill="none"
stroke="#122a42"
strokeWidth={1.2}
d="M34 46a12 12 0 0 0 12-12m320 12a12 12 0 0 1-12-12M34 534a12 12 0 0 1 12 12m320-12a12 12 0 0 0-12 12"
/>
<Path stroke="#c59b58" d="M34 135h332M34 500h332" />
<Use href="#a" transform="matrix(.85 0 0 .85 200 52)" />
<Use href="#b" transform="matrix(.8 0 0 .8 42 42)" />
<Use href="#b" transform="matrix(-.8 0 0 .8 358 42)" />
<Use href="#b" transform="matrix(.95 0 0 .95 40 185)" />
<Use href="#b" transform="matrix(-.95 0 0 .95 360 185)" />
<Use href="#a" transform="matrix(.65 0 0 .65 68 320)" />
<Use href="#a" transform="matrix(.65 0 0 .65 332 320)" />
<Use href="#b" transform="matrix(.95 0 0 .95 40 360)" />
<Use href="#b" transform="matrix(-.95 0 0 .95 360 360)" />
<G transform="translate(200 465)">
<Rect
width={18}
height={28}
x={-9}
y={-14}
fill="#122a42"
stroke="#122a42"
rx={3.5}
/>
<Rect width={14} height={20} x={-7} y={-11} fill="#fff" rx={1.5} />
<Path fill="#122a42" d="M-4.5-8.5h9v9h-9z" />
<Path fill="#fff" d="M-3-7h6v6h-6z" />
<Path fill="#122a42" d="M-1.5-5.5h3v3h-3z" />
<Circle cy={11} r={1} fill="#fff" />
</G>
<Circle cx={258} cy={537} r={4.5} fill="#a64423" />
<Path
fill="#f6efe3"
d="M259 541h-1.5v-4h-1v-1.5h1v-1c0-1 .5-1.5 2-1.5h1v1.5h-1c-.5 0-.5.3-.5.7v.3h1.5l-.3 1.5H259Z"
/>
<Rect
width={9}
height={9}
x={266}
y={532.5}
fill="none"
stroke="#a64423"
strokeWidth={1.2}
rx={2.5}
/>
<Circle cx={270.5} cy={537} r={2} fill="none" stroke="#a64423" />
<Circle cx={273} cy={534.5} r={0.5} fill="#a64423" />
<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"
/>
<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="#a" transform="matrix(.45 0 0 .45 105 93)" />
<Text
x={105}
y={128}
fill="#122a42"
fontFamily="Inter, Arial, sans-serif"
fontSize={11}
fontWeight={800}
letterSpacing={1.5}
textAnchor="middle"
>
{"QR KOD ALANI"}
</Text>
</G>
<Text
x={172}
y={55}
fill="#122a42"
fontFamily="Inter, Arial, sans-serif"
fontSize={8.5}
fontWeight={800}
letterSpacing={1}
textAnchor="middle"
>
{"EST."}
</Text>
<Text
x={228}
y={55}
fill="#122a42"
fontFamily="Inter, Arial, sans-serif"
fontSize={8.5}
fontWeight={800}
letterSpacing={1}
textAnchor="middle"
>
{"2018"}
</Text>
<Text
x={200}
y={84}
fill="#a64423"
fontFamily="'Playfair Display', Georgia, 'Times New Roman', serif"
fontSize={19}
fontWeight={900}
letterSpacing={1.5}
textAnchor="middle"
>
{"THE STONEHEARTH GRILL"}
</Text>
<Text
x={200}
y={102}
fill="#122a42"
fontFamily="Inter, Arial, sans-serif"
fontSize={8.5}
fontWeight={700}
letterSpacing={2}
textAnchor="middle"
>
{"EST. 2018"}
</Text>
<Text
x={200}
y={118}
fill="#122a42"
fontFamily="Inter, Arial, sans-serif"
fontSize={10}
fontWeight={800}
letterSpacing={2}
textAnchor="middle"
>
{"SCAN. ORDER. ENJOY."}
</Text>
<Text
x={200}
y={158}
fill="#122a42"
fontFamily="'Playfair Display', Georgia, 'Times New Roman', serif"
fontSize={15.5}
fontWeight={900}
letterSpacing={0.5}
textAnchor="middle"
>
{"VIEW OUR MENU & ORDER ONLINE"}
</Text>
<Text
x={200}
y={176}
fill="#222"
fontFamily="Inter, Arial, sans-serif"
fontSize={9}
fontWeight={500}
letterSpacing={0.1}
textAnchor="middle"
>
{"Simply scan the QR code with your phone camera to access our"}
</Text>
<Text
x={200}
y={189}
fill="#222"
fontFamily="Inter, Arial, sans-serif"
fontSize={9}
fontWeight={500}
letterSpacing={0.1}
textAnchor="middle"
>
{"digital menu, daily specials, wine list, and place your order."}
</Text>
<Text
x={162}
y={468}
fill="#122a42"
fontFamily="Inter, Arial, sans-serif"
fontSize={11}
fontWeight={800}
letterSpacing={1}
textAnchor="end"
>
{"NO APP"}
</Text>
<Text
x={238}
y={468}
fill="#122a42"
fontFamily="Inter, Arial, sans-serif"
fontSize={11}
fontWeight={800}
letterSpacing={1}
>
{"REQUIRED"}
</Text>
<Text
x={200}
y={515}
fill="#122a42"
fontFamily="Inter, Arial, sans-serif"
fontSize={10.5}
fontWeight={800}
letterSpacing={1}
textAnchor="middle"
>
{"WWW.THESTONEHEARTHGRILL.COM"}
</Text>
<Text
x={200}
y={527}
fill="#222"
fontFamily="Inter, Arial, sans-serif"
fontSize={9}
fontWeight={600}
letterSpacing={0.2}
textAnchor="middle"
>
{"FREE WIFI: StonehearthGuest (PW: Grill720)"}
</Text>
<Text
x={195}
y={540}
fill="#222"
fontFamily="Inter, Arial, sans-serif"
fontSize={9}
fontWeight={600}
letterSpacing={0.2}
textAnchor="middle"
>
{"Follow Us @StonehearthGrill"}
</Text>
</Svg>
)
const Memo = memo(SvgComponent)
export default Memo
+15
View File
@@ -90,4 +90,19 @@ export const QR_STAND_PRESETS: Record<string, QrStandPreset> = {
subText: "#0284c7",
dividerColor: "rgba(0, 53, 84, 0.3)",
},
"qr-6": {
key: "qr-6",
name: "The Stonehearth Grill (QR-6)",
categoryName: "Özel Masa Kartı",
svgTemplateUrl: "/qr-6.svg",
aspectRatio: "400/580",
bgGradient: "radial-gradient(circle at 50% 50%, #434b54 0%, #1a2228 100%)",
plaqueColor: "#f6efe3",
borderAccent: "#c59b58",
accentText: "#a64423",
headerText: "#122a42",
subText: "#c59b58",
dividerColor: "rgba(197, 155, 88, 0.3)",
},
};