feat: admin auth, web menu template studio, live text editing & Ayris Tech footer backlink

This commit is contained in:
AyrisAI
2026-08-23 01:45:00 +03:00
parent e7ba6c7807
commit 1f8195ff67
48 changed files with 5588 additions and 402 deletions
@@ -1,11 +1,9 @@
import React, { useRef } from "react";
import {
PanResponder,
Pressable,
Text,
View,
} from "react-native";
import { Ionicons } from "@expo/vector-icons";
import * as Haptics from "expo-haptics";
import { type CanvasTextLayer, getCanvasFontFamily } from "./types";
@@ -23,6 +21,78 @@ interface CanvasLayerItemProps {
canvasHeight: number;
}
export const CurvedTextNative: React.FC<{
text: string;
fontSize: number;
color: string;
fontFamily: string;
fontWeight?: any;
letterSpacing?: number;
curveDegree: number;
hasShadow?: boolean;
}> = ({
text,
fontSize,
color,
fontFamily,
fontWeight = "800",
letterSpacing = 0.5,
curveDegree,
hasShadow,
}) => {
const isUp = curveDegree > 0;
const absDegree = Math.abs(curveDegree);
const radius = Math.max(40, 420 - absDegree * 3.5);
const charWidth = fontSize * 0.52 + (letterSpacing || 0);
const numChars = text.length;
const arcLength = numChars * charWidth;
const totalAngleRad = arcLength / radius;
const stepRad = numChars > 1 ? totalAngleRad / (numChars - 1) : 0;
const startAngleRad = -totalAngleRad / 2;
const maxOffsetY = Math.abs(-radius * (Math.cos(totalAngleRad / 2) - 1));
const containerHeight = fontSize + maxOffsetY + 12;
const containerWidth = Math.max(numChars * charWidth, radius * 2 * Math.sin(totalAngleRad / 2)) + 16;
return (
<View style={{ width: containerWidth, height: containerHeight, alignItems: "center", justifyContent: "center" }}>
{text.split("").map((ch, i) => {
const angleRad = startAngleRad + i * stepRad;
const angleDeg = angleRad * (180 / Math.PI);
const charX = radius * Math.sin(angleRad);
const charY = isUp
? -radius * (Math.cos(angleRad) - 1)
: radius * (Math.cos(angleRad) - 1);
const charRotate = isUp ? angleDeg : -angleDeg;
return (
<Text
key={i}
style={{
position: "absolute",
fontSize,
color,
fontFamily,
fontWeight,
letterSpacing,
textShadowColor: hasShadow ? "rgba(0,0,0,0.8)" : "transparent",
textShadowOffset: { width: 0, height: 1 },
textShadowRadius: 3,
transform: [
{ translateX: charX },
{ translateY: charY },
{ rotate: `${charRotate}deg` },
],
}}
>
{ch === " " ? "\u00A0" : ch}
</Text>
);
})}
</View>
);
};
export const CanvasLayerItem: React.FC<CanvasLayerItemProps> = ({
layer,
isSelected,
@@ -191,51 +261,35 @@ export const CanvasLayerItem: React.FC<CanvasLayerItemProps> = ({
}}
>
<View style={renderBackgroundStyle()}>
<Text
style={{
fontSize: layer.fontSize,
color: layer.color,
fontFamily: getCanvasFontFamily(layer.fontId),
fontWeight: layer.fontWeight || "bold",
textAlign: layer.textAlign || "center",
letterSpacing: layer.letterSpacing ?? 0.5,
textShadowColor: layer.hasShadow ? "rgba(0,0,0,0.8)" : "transparent",
textShadowOffset: { width: 0, height: 1 },
textShadowRadius: 3,
}}
>
{layer.text}
</Text>
{layer.curveDegree && layer.curveDegree !== 0 ? (
<CurvedTextNative
text={layer.text}
fontSize={layer.fontSize}
color={layer.color}
fontFamily={getCanvasFontFamily(layer.fontId)}
fontWeight={layer.fontWeight || "bold"}
letterSpacing={layer.letterSpacing ?? 0.5}
curveDegree={layer.curveDegree}
hasShadow={layer.hasShadow}
/>
) : (
<Text
style={{
fontSize: layer.fontSize,
color: layer.color,
fontFamily: getCanvasFontFamily(layer.fontId),
fontWeight: layer.fontWeight || "bold",
textAlign: layer.textAlign || "center",
letterSpacing: layer.letterSpacing ?? 0.5,
textShadowColor: layer.hasShadow ? "rgba(0,0,0,0.8)" : "transparent",
textShadowOffset: { width: 0, height: 1 },
textShadowRadius: 3,
}}
>
{layer.text}
</Text>
)}
</View>
{/* Seçili İken Üst Köşedeki Küçük Silme Rozeti */}
{isSelected && (
<Pressable
onPress={(e) => {
e.stopPropagation?.();
try {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning);
} catch {}
onDelete(layer.id);
}}
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
style={{
position: "absolute",
top: -8,
right: -8,
backgroundColor: "#EF4444",
width: 18,
height: 18,
borderRadius: 9,
alignItems: "center",
justifyContent: "center",
borderWidth: 1.5,
borderColor: "#0C0C0E",
}}
>
<Ionicons name="close" size={11} color="#FFFFFF" />
</Pressable>
)}
</View>
</View>
);
@@ -45,6 +45,7 @@ export const InstagramTextEditorModal: React.FC<InstagramTextEditorModalProps> =
const [textAlign, setTextAlign] = useState<"left" | "center" | "right">("center");
const [bgStyle, setBgStyle] = useState<"none" | "solid" | "soft" | "outline">("none");
const [bgBoxColor, setBgBoxColor] = useState<string | undefined>(undefined);
const [curveDegree, setCurveDegree] = useState<number>(0);
// Modal açıldığında layer verilerini yükle
useEffect(() => {
@@ -54,6 +55,7 @@ export const InstagramTextEditorModal: React.FC<InstagramTextEditorModalProps> =
setColor(layer.color || "#FFFFFF");
setFontSize(layer.fontSize || 16);
setTextAlign(layer.textAlign || "center");
setCurveDegree(layer.curveDegree || 0);
if (layer.bgBoxColor) {
if (layer.bgBoxColor.includes("0.2") || layer.bgBoxColor.includes("0.3")) {
setBgStyle("soft");
@@ -73,6 +75,19 @@ export const InstagramTextEditorModal: React.FC<InstagramTextEditorModalProps> =
}
}, [layer, visible]);
// Kavis Modunu Değiştir (Düz (0) -> Yukarı Kavis (40) -> Ters Kavis (-40) -> Düz)
const handleToggleCurveDegree = () => {
try {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
} catch {}
setCurveDegree((prev) => {
if (prev === 0) return 40;
if (prev === 40) return 60;
if (prev === 60) return -40;
return 0;
});
};
// Arka plan pill stilini sırayla değiştir (Instagram "A" butonu mantığı)
const handleToggleBgStyle = () => {
try {
@@ -155,6 +170,7 @@ export const InstagramTextEditorModal: React.FC<InstagramTextEditorModalProps> =
textAlign,
bgBoxColor: finalBg,
bgStyle,
curveDegree,
hasShadow: bgStyle === "outline" || (!finalBg && color !== "#121215"),
});
onClose();
@@ -234,6 +250,26 @@ export const InstagramTextEditorModal: React.FC<InstagramTextEditorModalProps> =
)}
</Pressable>
{/* Kavis (Yay / Eğri Yazı) Toggle Butonu */}
<Pressable
onPress={handleToggleCurveDegree}
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
style={[
styles.toolPill,
curveDegree !== 0 && { backgroundColor: "#D4AF37" },
]}
>
<Text
style={[
styles.bgPillText,
{ fontSize: 14 },
curveDegree !== 0 ? { color: "#0C0C0E", fontWeight: "900" } : { color: "#FFFFFF" },
]}
>
{curveDegree > 0 ? "◠" : curveDegree < 0 ? "◡" : "⌒"}
</Text>
</Pressable>
{/* Katmanı Sil (Varsa) */}
{onDelete && (
<Pressable
@@ -358,6 +394,69 @@ export const InstagramTextEditorModal: React.FC<InstagramTextEditorModalProps> =
</View>
</View>
{/* ALT BÖLÜM 0: KAVİS HASSAS AYAR BARI */}
<View style={{ flexDirection: "row", alignItems: "center", justifyContent: "center", paddingHorizontal: 16, paddingVertical: 4, gap: 10 }}>
<Text style={{ color: "#A1A1AA", fontSize: 11, fontWeight: "700" }}>KAVİS:</Text>
<View style={{ flexDirection: "row", alignItems: "center", backgroundColor: "rgba(255,255,255,0.1)", borderRadius: 14, paddingHorizontal: 6, paddingVertical: 3, gap: 8 }}>
<Pressable
onPress={() => {
try { Haptics.selectionAsync(); } catch {}
setCurveDegree((deg) => Math.max(-100, deg - 5));
}}
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
style={{ width: 26, height: 26, borderRadius: 13, backgroundColor: "rgba(255,255,255,0.15)", alignItems: "center", justifyContent: "center" }}
>
<Ionicons name="remove" size={15} color="#FFFFFF" />
</Pressable>
<Text style={{ color: "#D4AF37", fontSize: 13, fontWeight: "900", minWidth: 36, textAlign: "center" }}>
{curveDegree}°
</Text>
<Pressable
onPress={() => {
try { Haptics.selectionAsync(); } catch {}
setCurveDegree((deg) => Math.min(100, deg + 5));
}}
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
style={{ width: 26, height: 26, borderRadius: 13, backgroundColor: "rgba(255,255,255,0.15)", alignItems: "center", justifyContent: "center" }}
>
<Ionicons name="add" size={15} color="#FFFFFF" />
</Pressable>
</View>
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ gap: 6 }}>
{[
{ label: "Düz (0°)", deg: 0 },
{ label: "25° ◠", deg: 25 },
{ label: "45° ◠", deg: 45 },
{ label: "65° ◠", deg: 65 },
{ label: "-35° ◡", deg: -35 },
].map((item) => {
const isActive = curveDegree === item.deg;
return (
<Pressable
key={item.deg}
onPress={() => {
try { Haptics.selectionAsync(); } catch {}
setCurveDegree(item.deg);
}}
style={{
paddingHorizontal: 10,
paddingVertical: 5,
borderRadius: 12,
backgroundColor: isActive ? "#D4AF37" : "rgba(255,255,255,0.1)",
}}
>
<Text style={{ color: isActive ? "#0C0C0E" : "#FFFFFF", fontSize: 11, fontWeight: isActive ? "800" : "600" }}>
{item.label}
</Text>
</Pressable>
);
})}
</ScrollView>
</View>
{/* ALT BÖLÜM 1: RENK SEÇİCİ BALONCUKLAR */}
<View style={styles.colorPickerBar}>
<ScrollView
@@ -3,6 +3,7 @@ import {
Pressable,
ScrollView,
Text,
TextInput,
View,
} from "react-native";
import { Ionicons } from "@expo/vector-icons";
@@ -14,6 +15,7 @@ import { InstagramTextEditorModal } from "./InstagramTextEditorModal";
import {
type CanvasTextLayer,
type CanvasQrConfig,
CANVAS_FONT_OPTIONS,
QR_SIZE_PRESETS,
QR_RADIUS_PRESETS,
getDefaultLayersForTemplate,
@@ -537,7 +539,79 @@ export const QrCanvasStudio: React.FC<QrCanvasStudioProps> = ({
</View>
</View>
{/* Alt Sıra: Hızlı Döndürme Açısı Hapları */}
{/* Hızlı Metin Değiştirme Girdisi */}
<View
style={{
flexDirection: "row",
alignItems: "center",
gap: 8,
backgroundColor: "#0C0C0E",
borderRadius: 12,
paddingHorizontal: 10,
paddingVertical: 6,
borderWidth: 1,
borderColor: "rgba(212, 175, 55, 0.4)",
}}
>
<Ionicons name="text-outline" size={14} color="#D4AF37" />
<TextInput
value={selectedLayer.text}
onChangeText={(newText: string) => {
onLayersChange((prev) =>
prev.map((l) => (l.id === selectedLayer.id ? { ...l, text: newText } : l))
);
}}
placeholder="Metin yazın..."
placeholderTextColor="#71717A"
style={{
flex: 1,
color: "#FFFFFF",
fontSize: 13,
fontWeight: "700",
padding: 0,
}}
/>
</View>
{/* Alt Sıra 1: Font Stili Seçici */}
<View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
<Text style={{ color: "#71717A", fontSize: 10, fontWeight: "700" }}>FONT:</Text>
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ gap: 6 }}>
{CANVAS_FONT_OPTIONS.map((f) => {
const isActive = (selectedLayer.fontId || "sans") === f.id;
return (
<Pressable
key={f.id}
onPress={() => {
try { Haptics.selectionAsync(); } catch {}
onLayersChange((prev) =>
prev.map((l) => (l.id === selectedLayer.id ? { ...l, fontId: f.id } : l))
);
}}
style={{
paddingHorizontal: 10,
paddingVertical: 4,
borderRadius: 10,
backgroundColor: isActive ? "#D4AF37" : "#242429",
}}
>
<Text
style={{
color: isActive ? "#0C0C0E" : "#FFFFFF",
fontSize: 10.5,
fontWeight: isActive ? "900" : "700",
fontFamily: f.fontFamily,
}}
>
{f.label}
</Text>
</Pressable>
);
})}
</ScrollView>
</View>
{/* Alt Sıra 2: Hızlı Döndürme Açısı Hapları */}
<View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
<Text style={{ color: "#71717A", fontSize: 10, fontWeight: "700" }}>EĞİM:</Text>
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ gap: 6 }}>
@@ -573,6 +647,94 @@ export const QrCanvasStudio: React.FC<QrCanvasStudioProps> = ({
})}
</ScrollView>
</View>
{/* Alt Sıra 3: Kavis (Eğri Yazı) Stepper & Preset Hapları */}
<View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
<Text style={{ color: "#71717A", fontSize: 10, fontWeight: "700" }}>KAVİS:</Text>
{/* Stepper Butonları (- / +) */}
<View style={{ flexDirection: "row", alignItems: "center", backgroundColor: "#242429", borderRadius: 10, paddingHorizontal: 4, paddingVertical: 2, gap: 4 }}>
<Pressable
onPress={() => {
try { Haptics.selectionAsync(); } catch {}
onLayersChange((prev) =>
prev.map((l) =>
l.id === selectedLayer.id
? { ...l, curveDegree: Math.max(-100, (l.curveDegree || 0) - 5) }
: l
)
);
}}
hitSlop={{ top: 6, bottom: 6, left: 6, right: 6 }}
style={{ width: 22, height: 22, borderRadius: 11, backgroundColor: "rgba(255,255,255,0.12)", alignItems: "center", justifyContent: "center" }}
>
<Ionicons name="remove" size={13} color="#FFFFFF" />
</Pressable>
<Text style={{ color: "#D4AF37", fontSize: 11, fontWeight: "900", minWidth: 30, textAlign: "center" }}>
{(selectedLayer.curveDegree || 0)}°
</Text>
<Pressable
onPress={() => {
try { Haptics.selectionAsync(); } catch {}
onLayersChange((prev) =>
prev.map((l) =>
l.id === selectedLayer.id
? { ...l, curveDegree: Math.min(100, (l.curveDegree || 0) + 5) }
: l
)
);
}}
hitSlop={{ top: 6, bottom: 6, left: 6, right: 6 }}
style={{ width: 22, height: 22, borderRadius: 11, backgroundColor: "rgba(255,255,255,0.12)", alignItems: "center", justifyContent: "center" }}
>
<Ionicons name="add" size={13} color="#FFFFFF" />
</Pressable>
</View>
{/* Hazır Kavis Presetleri */}
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ gap: 6 }}>
{[
{ label: "0° (Düz)", deg: 0 },
{ label: "20° ◠", deg: 20 },
{ label: "35° ◠", deg: 35 },
{ label: "50° ◠", deg: 50 },
{ label: "70° ◠", deg: 70 },
{ label: "-35° ◡", deg: -35 },
{ label: "-50° ◡", deg: -50 },
].map((item) => {
const isActive = (selectedLayer.curveDegree || 0) === item.deg;
return (
<Pressable
key={item.deg}
onPress={() => {
try { Haptics.selectionAsync(); } catch {}
onLayersChange((prev) =>
prev.map((l) => (l.id === selectedLayer.id ? { ...l, curveDegree: item.deg } : l))
);
}}
style={{
paddingHorizontal: 9,
paddingVertical: 4,
borderRadius: 10,
backgroundColor: isActive ? "#D4AF37" : "#242429",
}}
>
<Text
style={{
color: isActive ? "#0C0C0E" : "#FFFFFF",
fontSize: 10.5,
fontWeight: isActive ? "800" : "600",
}}
>
{item.label}
</Text>
</Pressable>
);
})}
</ScrollView>
</View>
</View>
)}
+23 -12
View File
@@ -1,3 +1,5 @@
import { Platform } from "react-native";
export interface CanvasTextLayer {
id: string;
text: string;
@@ -13,6 +15,7 @@ export interface CanvasTextLayer {
hasShadow?: boolean;
letterSpacing?: number;
fontWeight?: "normal" | "600" | "700" | "800" | "900";
curveDegree?: number; // -100 (Aşağı Kavis ◡) ile +100 (Yukarı Kavis ◠) arası kavis derecesi
}
export interface CanvasQrConfig {
@@ -30,40 +33,53 @@ export const CANVAS_FONT_OPTIONS = [
{
id: "sans",
label: "MODERN",
fontFamily: "System, -apple-system, Roboto, 'Helvetica Neue', Arial, sans-serif",
fontFamily: Platform.OS === "ios" ? "System" : "sans-serif",
},
{
id: "serif",
label: "ZARİF",
fontFamily: "Georgia, 'Times New Roman', serif",
fontFamily: Platform.OS === "ios" ? "Georgia" : "serif",
},
{
id: "times",
label: "KLASİK",
fontFamily: "'Times New Roman', Times, Georgia, serif",
fontFamily: Platform.OS === "ios" ? "Times New Roman" : "serif",
},
{
id: "typewriter",
label: "DAKTİLO",
fontFamily: "Courier, 'Courier New', monospace",
fontFamily: Platform.OS === "ios" ? "Courier" : "monospace",
},
{
id: "palatino",
label: "LÜKS",
fontFamily: "Palatino, 'Palatino Linotype', Georgia, serif",
fontFamily: Platform.OS === "ios" ? "Palatino" : "serif",
},
{
id: "trebuchet",
label: "DİNAMİK",
fontFamily: "'Trebuchet MS', 'Segoe UI', Arial, sans-serif",
fontFamily: Platform.OS === "ios" ? "Trebuchet MS" : "sans-serif",
},
{
id: "heavy",
label: "GÜÇLÜ",
fontFamily: "Impact, 'Arial Black', sans-serif",
fontFamily: Platform.OS === "ios" ? "Impact" : "sans-serif-black",
},
];
export function getCanvasFontFamily(fontId?: string): string {
if (!fontId) return Platform.OS === "ios" ? "System" : "sans-serif";
const found = CANVAS_FONT_OPTIONS.find((f) => f.id === fontId);
if (found) {
return found.fontFamily;
}
if (fontId.includes(",")) {
const first = fontId.split(",")[0].trim().replace(/['"]/g, "");
return first || (Platform.OS === "ios" ? "System" : "sans-serif");
}
return fontId;
}
export const CANVAS_COLOR_PALETTE = [
{ id: "gold", color: "#D4AF37", label: "Altın" },
{ id: "white", color: "#FFFFFF", label: "Beyaz" },
@@ -106,11 +122,6 @@ export const QR_BORDER_COLORS = [
{ id: "none", color: "transparent", label: "Yok" },
];
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";
}
export function getDefaultQrConfigForTemplate(
templateKey: string,
canvasWidth: number,
@@ -24,39 +24,47 @@ export interface QrTemplateProps extends SvgProps {
hideQr?: boolean;
}
import { Platform } from "react-native";
// Türkçe karakterleri (İ, ı, Ş, ş, Ğ, ğ, Ü, ü, Ö, ö, Ç, ç) eksiksiz destekleyen sistem fontları
export const FONT_OPTIONS = [
{
id: "sans",
label: "Modern Sans",
fontFamily: "System, -apple-system, Roboto, 'Helvetica Neue', Arial, sans-serif",
fontFamily: Platform.OS === "ios" ? "System" : "sans-serif",
},
{
id: "serif",
label: "Zarif Serif",
fontFamily: "Georgia, 'Times New Roman', serif",
fontFamily: Platform.OS === "ios" ? "Georgia" : "serif",
},
{
id: "times",
label: "Klasik (Times)",
fontFamily: "'Times New Roman', Times, Georgia, serif",
fontFamily: Platform.OS === "ios" ? "Times New Roman" : "serif",
},
{
id: "palatino",
label: "Lüks Palatino",
fontFamily: "Palatino, 'Palatino Linotype', Georgia, serif",
fontFamily: Platform.OS === "ios" ? "Palatino" : "serif",
},
{
id: "trebuchet",
label: "Dinamik",
fontFamily: "'Trebuchet MS', 'Segoe UI', Arial, sans-serif",
fontFamily: Platform.OS === "ios" ? "Trebuchet MS" : "sans-serif",
},
];
export function getFontFamily(fontId?: string, defaultFamily = "Georgia, serif"): string {
if (!fontId) return defaultFamily;
export function getFontFamily(fontId?: string, defaultFamily = "Georgia"): string {
if (!fontId) {
return defaultFamily.includes(",") ? defaultFamily.split(",")[0].trim().replace(/['"]/g, "") : defaultFamily;
}
const found = FONT_OPTIONS.find((f) => f.id === fontId);
return found ? found.fontFamily : defaultFamily;
if (found) return found.fontFamily;
if (fontId.includes(",")) {
return fontId.split(",")[0].trim().replace(/['"]/g, "");
}
return fontId;
}
// Çok satırlı (Enter / \n) ve dikey kaydırmalı (offsetY) SVG metin render fonksiyonu