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:
+51
-474
@@ -24,11 +24,22 @@ import { getActiveRestaurant, type ActiveRestaurant } from "@/lib/active-restaur
|
|||||||
import {
|
import {
|
||||||
COMPONENT_TEMPLATES,
|
COMPONENT_TEMPLATES,
|
||||||
TEMPLATE_ASPECT_RATIOS,
|
TEMPLATE_ASPECT_RATIOS,
|
||||||
FONT_OPTIONS,
|
|
||||||
type QrCustomFields,
|
type QrCustomFields,
|
||||||
type TextItemConfig,
|
|
||||||
} from "@/components/qr-templates";
|
} 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 {
|
interface QrResponse {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -52,64 +63,11 @@ export default function QrScreen() {
|
|||||||
);
|
);
|
||||||
const [exportingPng, setExportingPng] = useState(false);
|
const [exportingPng, setExportingPng] = useState(false);
|
||||||
const [exportingPdf, setExportingPdf] = useState(false);
|
const [exportingPdf, setExportingPdf] = useState(false);
|
||||||
const [showCustomizePanel, setShowCustomizePanel] = useState(false);
|
|
||||||
|
|
||||||
const [fields, setFields] = useState<QrCustomFields>({
|
// Instagram Story / Canva Tarzı Katmanlar
|
||||||
restaurantName: {
|
const [layers, setLayers] = useState<CanvasTextLayer[]>(() =>
|
||||||
text: "Gourmet Bistro",
|
getDefaultLayersForTemplate("qr-1", "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" },
|
|
||||||
];
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const viewShotRef = useRef<any>(null);
|
const viewShotRef = useRef<any>(null);
|
||||||
@@ -124,10 +82,6 @@ export default function QrScreen() {
|
|||||||
const displayWidth = 300;
|
const displayWidth = 300;
|
||||||
const displayHeight = Math.round(displayWidth / aspectRatio);
|
const displayHeight = Math.round(displayWidth / aspectRatio);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
const activeRest = await getActiveRestaurant();
|
const activeRest = await getActiveRestaurant();
|
||||||
@@ -143,13 +97,11 @@ export default function QrScreen() {
|
|||||||
const restDetail = await api.get<{ name?: string }>(`/restaurants/${activeRest.restaurantId}`);
|
const restDetail = await api.get<{ name?: string }>(`/restaurants/${activeRest.restaurantId}`);
|
||||||
if (restDetail?.name) {
|
if (restDetail?.name) {
|
||||||
setRestaurantName(restDetail.name);
|
setRestaurantName(restDetail.name);
|
||||||
setFields((prev) => ({
|
setLayers((prev) =>
|
||||||
...prev,
|
prev.map((l) =>
|
||||||
restaurantName: {
|
l.id === "rName" ? { ...l, text: restDetail.name!.toUpperCase() } : l
|
||||||
...prev.restaurantName,
|
)
|
||||||
text: restDetail.name || "Gourmet Bistro",
|
);
|
||||||
},
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// fallback
|
// fallback
|
||||||
@@ -164,6 +116,7 @@ export default function QrScreen() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async function handleCopy() {
|
async function handleCopy() {
|
||||||
if (!qr?.targetUrl) return;
|
if (!qr?.targetUrl) return;
|
||||||
await Clipboard.setStringAsync(qr.targetUrl);
|
await Clipboard.setStringAsync(qr.targetUrl);
|
||||||
@@ -331,14 +284,17 @@ export default function QrScreen() {
|
|||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
key={preset.key}
|
key={preset.key}
|
||||||
onPress={() => setSelectedKey(preset.key)}
|
onPress={() => {
|
||||||
|
setSelectedKey(preset.key);
|
||||||
|
setLayers(getDefaultLayersForTemplate(preset.key, restaurantName));
|
||||||
|
}}
|
||||||
style={{
|
style={{
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingVertical: 10,
|
||||||
|
borderRadius: 14,
|
||||||
backgroundColor: isSelected ? "#D4AF37" : "#1A1A1E",
|
backgroundColor: isSelected ? "#D4AF37" : "#1A1A1E",
|
||||||
borderWidth: 1.5,
|
borderWidth: 1.5,
|
||||||
borderColor: isSelected ? "#D4AF37" : "#2B2B32",
|
borderColor: isSelected ? "#D4AF37" : "#2B2B32",
|
||||||
borderRadius: 14,
|
|
||||||
paddingHorizontal: 16,
|
|
||||||
paddingVertical: 10,
|
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: 8,
|
gap: 8,
|
||||||
@@ -369,417 +325,38 @@ export default function QrScreen() {
|
|||||||
</ScrollView>
|
</ScrollView>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Live Stand Preview Wrapped in ViewShot */}
|
{/* İnteraktif Instagram / Canva Tarzı QR Stant Stüdyosu */}
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
padding: 16,
|
width: "100%",
|
||||||
borderRadius: 28,
|
|
||||||
backgroundColor: "#18181B",
|
|
||||||
marginBottom: 20,
|
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 }}>
|
<ViewShot ref={viewShotRef} options={{ format: "png", quality: 1.0 }}>
|
||||||
{/* Component-based template — no XML parsing, pixel-perfect */}
|
<QrCanvasStudio
|
||||||
<View
|
templateKey={selectedKey}
|
||||||
style={{
|
restaurantName={restaurantName}
|
||||||
width: displayWidth,
|
qrBase64={qr.pngBase64}
|
||||||
height: displayHeight,
|
width={displayWidth}
|
||||||
borderRadius: 24,
|
height={displayHeight}
|
||||||
overflow: "hidden",
|
renderBackground={() =>
|
||||||
backgroundColor: "#0C0C0E",
|
TemplateComponent ? (
|
||||||
}}
|
<TemplateComponent
|
||||||
>
|
qrBase64={qr.pngBase64}
|
||||||
{TemplateComponent ? (
|
width={displayWidth}
|
||||||
<TemplateComponent
|
height={displayHeight}
|
||||||
qrBase64={qr.pngBase64}
|
fields={HIDE_ALL_TEMPLATE_FIELDS}
|
||||||
width={displayWidth}
|
/>
|
||||||
height={displayHeight}
|
) : null
|
||||||
fields={fields}
|
}
|
||||||
/>
|
layers={layers}
|
||||||
) : null}
|
onLayersChange={setLayers}
|
||||||
</View>
|
/>
|
||||||
</ViewShot>
|
</ViewShot>
|
||||||
</View>
|
</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