feat(mobile): add full text editing, show/hide switches and font family selector for QR stands
This commit is contained in:
@@ -6,7 +6,9 @@ import {
|
||||
Pressable,
|
||||
ScrollView,
|
||||
Share,
|
||||
Switch,
|
||||
Text,
|
||||
TextInput,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
@@ -17,7 +19,13 @@ import * as Print from "expo-print";
|
||||
import { QR_STAND_PRESETS, type QrStandPreset } from "@menulio/shared";
|
||||
import { api } from "@/lib/api";
|
||||
import { getActiveRestaurant, type ActiveRestaurant } from "@/lib/active-restaurant";
|
||||
import { COMPONENT_TEMPLATES, TEMPLATE_ASPECT_RATIOS } from "@/components/qr-templates";
|
||||
import {
|
||||
COMPONENT_TEMPLATES,
|
||||
TEMPLATE_ASPECT_RATIOS,
|
||||
FONT_OPTIONS,
|
||||
type QrCustomTextConfig,
|
||||
} from "@/components/qr-templates";
|
||||
|
||||
|
||||
interface QrResponse {
|
||||
id: string;
|
||||
@@ -41,6 +49,21 @@ export default function QrScreen() {
|
||||
);
|
||||
const [exportingPng, setExportingPng] = useState(false);
|
||||
const [exportingPdf, setExportingPdf] = useState(false);
|
||||
const [showCustomizePanel, setShowCustomizePanel] = useState(false);
|
||||
const [selectedFontId, setSelectedFontId] = useState<string>("didot");
|
||||
const [customText, setCustomText] = useState<QrCustomTextConfig>({
|
||||
restaurantName: "Gourmet Bistro",
|
||||
subtitle: "RESTAURANT & BAR | MODERN CUISINE",
|
||||
tableNo: "MASA NO: 01",
|
||||
ctaText: "MENÜYÜ GÖRMEK İÇİN OKUTUN",
|
||||
wifiText: "WiFi: Gourmet_Guest | Şifre: lezzetli01",
|
||||
socialText: "@gourmetbistro",
|
||||
showTableNo: true,
|
||||
showSubtitle: true,
|
||||
showCta: true,
|
||||
showWifi: true,
|
||||
showSocial: true,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const viewShotRef = useRef<any>(null);
|
||||
@@ -48,6 +71,10 @@ export default function QrScreen() {
|
||||
const activePreset: QrStandPreset =
|
||||
QR_STAND_PRESETS[selectedKey] || availablePresets[0] || Object.values(QR_STAND_PRESETS)[0]!;
|
||||
|
||||
const activeFont =
|
||||
FONT_OPTIONS.find((f) => f.id === selectedFontId)?.fontFamily ||
|
||||
"'Playfair Display', Georgia, serif";
|
||||
|
||||
// Component-based template sistemi
|
||||
const TemplateComponent =
|
||||
COMPONENT_TEMPLATES[selectedKey] ?? Object.values(COMPONENT_TEMPLATES)[0];
|
||||
@@ -56,6 +83,7 @@ export default function QrScreen() {
|
||||
const displayHeight = Math.round(displayWidth / aspectRatio);
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const activeRest = await getActiveRestaurant();
|
||||
@@ -71,6 +99,10 @@ export default function QrScreen() {
|
||||
const restDetail = await api.get<{ name?: string }>(`/restaurants/${activeRest.restaurantId}`);
|
||||
if (restDetail?.name) {
|
||||
setRestaurantName(restDetail.name);
|
||||
setCustomText((prev) => ({
|
||||
...prev,
|
||||
restaurantName: restDetail.name,
|
||||
}));
|
||||
}
|
||||
} catch {
|
||||
// fallback
|
||||
@@ -83,6 +115,7 @@ export default function QrScreen() {
|
||||
})();
|
||||
}, []);
|
||||
|
||||
|
||||
async function handleCopy() {
|
||||
if (!qr?.targetUrl) return;
|
||||
await Clipboard.setStringAsync(qr.targetUrl);
|
||||
@@ -322,12 +355,332 @@ export default function QrScreen() {
|
||||
qrBase64={qr.pngBase64}
|
||||
width={displayWidth}
|
||||
height={displayHeight}
|
||||
restaurantName={customText.restaurantName}
|
||||
subtitle={customText.subtitle}
|
||||
tableNo={customText.tableNo}
|
||||
ctaText={customText.ctaText}
|
||||
wifiText={customText.wifiText}
|
||||
socialText={customText.socialText}
|
||||
fontFamily={activeFont}
|
||||
showTableNo={customText.showTableNo}
|
||||
showSubtitle={customText.showSubtitle}
|
||||
showCta={customText.showCta}
|
||||
showWifi={customText.showWifi}
|
||||
showSocial={customText.showSocial}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
</ViewShot>
|
||||
</View>
|
||||
|
||||
{/* Metin & Font Özelleştirme Kartı (Açılır/Kapanır) */}
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: "#18181B",
|
||||
borderRadius: 20,
|
||||
borderWidth: 1,
|
||||
borderColor: "rgba(255, 255, 255, 0.08)",
|
||||
padding: 16,
|
||||
marginBottom: 20,
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
onPress={() => setShowCustomizePanel(!showCustomizePanel)}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<View style={{ flexDirection: "row", alignItems: "center", gap: 10 }}>
|
||||
<View
|
||||
style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 8,
|
||||
backgroundColor: "rgba(212, 175, 55, 0.15)",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Ionicons name="text-outline" size={18} color="#D4AF37" />
|
||||
</View>
|
||||
<View>
|
||||
<Text style={{ color: "#FFFFFF", fontSize: 15, fontWeight: "700" }}>
|
||||
Metin ve Yazı Tipi Özelleştir
|
||||
</Text>
|
||||
<Text style={{ color: "#A1A1AA", fontSize: 12 }}>
|
||||
İstediğiniz alanları düzenleyin veya kaldırın
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Ionicons
|
||||
name={showCustomizePanel ? "chevron-up" : "chevron-down"}
|
||||
size={20}
|
||||
color="#A1A1AA"
|
||||
/>
|
||||
</Pressable>
|
||||
|
||||
{showCustomizePanel && (
|
||||
<View style={{ marginTop: 16, gap: 16, borderTopWidth: 1, borderTopColor: "rgba(255,255,255,0.06)", paddingTop: 16 }}>
|
||||
{/* Yazı Tipi (Font) Seçici */}
|
||||
<View>
|
||||
<Text style={{ color: "#D4AF37", fontSize: 12, fontWeight: "700", marginBottom: 8, letterSpacing: 0.5 }}>
|
||||
YAZI TİPİ (FONT)
|
||||
</Text>
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ gap: 8 }}>
|
||||
{FONT_OPTIONS.map((f) => {
|
||||
const isSelected = f.id === selectedFontId;
|
||||
return (
|
||||
<Pressable
|
||||
key={f.id}
|
||||
onPress={() => setSelectedFontId(f.id)}
|
||||
style={{
|
||||
backgroundColor: isSelected ? "#D4AF37" : "#242429",
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 10,
|
||||
borderWidth: 1,
|
||||
borderColor: isSelected ? "#D4AF37" : "rgba(255,255,255,0.08)",
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: isSelected ? "#0C0C0E" : "#FFFFFF",
|
||||
fontSize: 12,
|
||||
fontWeight: isSelected ? "700" : "500",
|
||||
}}
|
||||
>
|
||||
{f.label}
|
||||
</Text>
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
||||
{/* Alan 1: Restoran Başlığı */}
|
||||
<View style={{ gap: 6 }}>
|
||||
<Text style={{ color: "#D4AF37", fontSize: 12, fontWeight: "700" }}>
|
||||
RESTORAN BAŞLIĞI
|
||||
</Text>
|
||||
<TextInput
|
||||
value={customText.restaurantName}
|
||||
onChangeText={(t) => setCustomText((p) => ({ ...p, restaurantName: t }))}
|
||||
placeholder="Restoran Adı"
|
||||
placeholderTextColor="#71717A"
|
||||
style={{
|
||||
backgroundColor: "#121215",
|
||||
borderWidth: 1,
|
||||
borderColor: "rgba(255,255,255,0.1)",
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 10,
|
||||
color: "#FFFFFF",
|
||||
fontSize: 14,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Alan 2: Slogan / Alt Başlık */}
|
||||
<View style={{ gap: 6 }}>
|
||||
<View style={{ flexDirection: "row", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<Text style={{ color: "#D4AF37", fontSize: 12, fontWeight: "700" }}>
|
||||
SLOGAN / ALT BAŞLIK
|
||||
</Text>
|
||||
<View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
|
||||
<Text style={{ color: "#71717A", fontSize: 11 }}>
|
||||
{customText.showSubtitle ? "Göster" : "Gizle"}
|
||||
</Text>
|
||||
<Switch
|
||||
value={customText.showSubtitle}
|
||||
onValueChange={(v) => setCustomText((p) => ({ ...p, showSubtitle: v }))}
|
||||
trackColor={{ false: "#27272A", true: "#D4AF37" }}
|
||||
thumbColor="#FFFFFF"
|
||||
ios_backgroundColor="#27272A"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
{customText.showSubtitle && (
|
||||
<TextInput
|
||||
value={customText.subtitle}
|
||||
onChangeText={(t) => setCustomText((p) => ({ ...p, subtitle: t }))}
|
||||
placeholder="Slogan / Açıklama"
|
||||
placeholderTextColor="#71717A"
|
||||
style={{
|
||||
backgroundColor: "#121215",
|
||||
borderWidth: 1,
|
||||
borderColor: "rgba(255,255,255,0.1)",
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 10,
|
||||
color: "#FFFFFF",
|
||||
fontSize: 14,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Alan 3: Masa Numarası */}
|
||||
<View style={{ gap: 6 }}>
|
||||
<View style={{ flexDirection: "row", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<Text style={{ color: "#D4AF37", fontSize: 12, fontWeight: "700" }}>
|
||||
MASA NUMARASI
|
||||
</Text>
|
||||
<View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
|
||||
<Text style={{ color: "#71717A", fontSize: 11 }}>
|
||||
{customText.showTableNo ? "Göster" : "Gizle"}
|
||||
</Text>
|
||||
<Switch
|
||||
value={customText.showTableNo}
|
||||
onValueChange={(v) => setCustomText((p) => ({ ...p, showTableNo: v }))}
|
||||
trackColor={{ false: "#27272A", true: "#D4AF37" }}
|
||||
thumbColor="#FFFFFF"
|
||||
ios_backgroundColor="#27272A"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
{customText.showTableNo && (
|
||||
<TextInput
|
||||
value={customText.tableNo}
|
||||
onChangeText={(t) => setCustomText((p) => ({ ...p, tableNo: t }))}
|
||||
placeholder="Örn: MASA NO: 01"
|
||||
placeholderTextColor="#71717A"
|
||||
style={{
|
||||
backgroundColor: "#121215",
|
||||
borderWidth: 1,
|
||||
borderColor: "rgba(255,255,255,0.1)",
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 10,
|
||||
color: "#FFFFFF",
|
||||
fontSize: 14,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Alan 4: Yönlendirme (CTA) */}
|
||||
<View style={{ gap: 6 }}>
|
||||
<View style={{ flexDirection: "row", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<Text style={{ color: "#D4AF37", fontSize: 12, fontWeight: "700" }}>
|
||||
YÖNLENDİRME (CTA) METNİ
|
||||
</Text>
|
||||
<View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
|
||||
<Text style={{ color: "#71717A", fontSize: 11 }}>
|
||||
{customText.showCta ? "Göster" : "Gizle"}
|
||||
</Text>
|
||||
<Switch
|
||||
value={customText.showCta}
|
||||
onValueChange={(v) => setCustomText((p) => ({ ...p, showCta: v }))}
|
||||
trackColor={{ false: "#27272A", true: "#D4AF37" }}
|
||||
thumbColor="#FFFFFF"
|
||||
ios_backgroundColor="#27272A"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
{customText.showCta && (
|
||||
<TextInput
|
||||
value={customText.ctaText}
|
||||
onChangeText={(t) => setCustomText((p) => ({ ...p, ctaText: t }))}
|
||||
placeholder="Örn: MENÜYÜ GÖRMEK İÇİN OKUTUN"
|
||||
placeholderTextColor="#71717A"
|
||||
style={{
|
||||
backgroundColor: "#121215",
|
||||
borderWidth: 1,
|
||||
borderColor: "rgba(255,255,255,0.1)",
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 10,
|
||||
color: "#FFFFFF",
|
||||
fontSize: 14,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Alan 5: WiFi Bilgileri */}
|
||||
<View style={{ gap: 6 }}>
|
||||
<View style={{ flexDirection: "row", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<Text style={{ color: "#D4AF37", fontSize: 12, fontWeight: "700" }}>
|
||||
WIFI BİLGİSİ
|
||||
</Text>
|
||||
<View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
|
||||
<Text style={{ color: "#71717A", fontSize: 11 }}>
|
||||
{customText.showWifi ? "Göster" : "Gizle"}
|
||||
</Text>
|
||||
<Switch
|
||||
value={customText.showWifi}
|
||||
onValueChange={(v) => setCustomText((p) => ({ ...p, showWifi: v }))}
|
||||
trackColor={{ false: "#27272A", true: "#D4AF37" }}
|
||||
thumbColor="#FFFFFF"
|
||||
ios_backgroundColor="#27272A"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
{customText.showWifi && (
|
||||
<TextInput
|
||||
value={customText.wifiText}
|
||||
onChangeText={(t) => setCustomText((p) => ({ ...p, wifiText: t }))}
|
||||
placeholder="Örn: WiFi: Bistro | Şifre: 12345"
|
||||
placeholderTextColor="#71717A"
|
||||
style={{
|
||||
backgroundColor: "#121215",
|
||||
borderWidth: 1,
|
||||
borderColor: "rgba(255,255,255,0.1)",
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 10,
|
||||
color: "#FFFFFF",
|
||||
fontSize: 14,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Alan 6: Sosyal Medya / İletişim */}
|
||||
<View style={{ gap: 6 }}>
|
||||
<View style={{ flexDirection: "row", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<Text style={{ color: "#D4AF37", fontSize: 12, fontWeight: "700" }}>
|
||||
SOSYAL MEDYA / İLETİŞİM
|
||||
</Text>
|
||||
<View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
|
||||
<Text style={{ color: "#71717A", fontSize: 11 }}>
|
||||
{customText.showSocial ? "Göster" : "Gizle"}
|
||||
</Text>
|
||||
<Switch
|
||||
value={customText.showSocial}
|
||||
onValueChange={(v) => setCustomText((p) => ({ ...p, showSocial: v }))}
|
||||
trackColor={{ false: "#27272A", true: "#D4AF37" }}
|
||||
thumbColor="#FFFFFF"
|
||||
ios_backgroundColor="#27272A"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
{customText.showSocial && (
|
||||
<TextInput
|
||||
value={customText.socialText}
|
||||
onChangeText={(t) => setCustomText((p) => ({ ...p, socialText: t }))}
|
||||
placeholder="Örn: @menulio"
|
||||
placeholderTextColor="#71717A"
|
||||
style={{
|
||||
backgroundColor: "#121215",
|
||||
borderWidth: 1,
|
||||
borderColor: "rgba(255,255,255,0.1)",
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 10,
|
||||
color: "#FFFFFF",
|
||||
fontSize: 14,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
|
||||
{/* Output Action Buttons */}
|
||||
<View style={{ width: "100%", gap: 10 }}>
|
||||
{/* PDF Download Button */}
|
||||
|
||||
Reference in New Issue
Block a user