feat(mobile): add QrTemplate3 Pizzeria, fix PDF export for component system
This commit is contained in:
@@ -14,11 +14,10 @@ import * as Clipboard from "expo-clipboard";
|
||||
import ViewShot from "react-native-view-shot";
|
||||
import * as Sharing from "expo-sharing";
|
||||
import * as Print from "expo-print";
|
||||
import { SvgXml } from "react-native-svg";
|
||||
import { QR_STAND_PRESETS, type QrStandPreset } from "@menulio/shared";
|
||||
import { api } from "@/lib/api";
|
||||
import { getActiveRestaurant, type ActiveRestaurant } from "@/lib/active-restaurant";
|
||||
import { MOBILE_SVG_TEMPLATES } from "@/lib/svg-templates";
|
||||
import { COMPONENT_TEMPLATES, TEMPLATE_ASPECT_RATIOS } from "@/components/qr-templates";
|
||||
|
||||
interface QrResponse {
|
||||
id: string;
|
||||
@@ -42,54 +41,12 @@ export default function QrScreen() {
|
||||
const viewShotRef = useRef<any>(null);
|
||||
|
||||
const activePreset: QrStandPreset = (QR_STAND_PRESETS[selectedKey] || Object.values(QR_STAND_PRESETS)[0])!;
|
||||
const currentSvgRaw = MOBILE_SVG_TEMPLATES[selectedKey] || MOBILE_SVG_TEMPLATES["qr-4"] || Object.values(MOBILE_SVG_TEMPLATES)[0] || "";
|
||||
|
||||
// Inject the live QR code image directly into the SVG's #qr-placeholder group
|
||||
// Yorum satırlarını temizle + iç içe <g> taglarını doğru saymak için depth tracking kullan
|
||||
function buildSvgWithQr(svgRaw: string, pngBase64: string): string {
|
||||
// react-native-svg XML parser <!-- yorum satırlarını --> desteklemez
|
||||
// Ayrıca text node'lardaki & < gibi entity'leri decode et
|
||||
const clean = svgRaw
|
||||
.replace(/<!--[\s\S]*?-->/g, "")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/'/g, "'")
|
||||
.replace(/"/g, '"');
|
||||
|
||||
|
||||
const startMarker = '<g id="qr-placeholder"';
|
||||
const startIdx = clean.indexOf(startMarker);
|
||||
if (startIdx === -1) return clean;
|
||||
|
||||
// İç içe <g> taglarını sayarak doğru kapanış </g>'yi bul
|
||||
let depth = 0;
|
||||
let i = startIdx;
|
||||
let endIdx = -1;
|
||||
while (i < clean.length - 3) {
|
||||
if (clean[i] === "<") {
|
||||
if (clean[i + 1] === "g" && (clean[i + 2] === " " || clean[i + 2] === ">")) {
|
||||
depth++;
|
||||
} else if (clean[i + 1] === "/" && clean[i + 2] === "g" && clean[i + 3] === ">") {
|
||||
depth--;
|
||||
if (depth === 0) {
|
||||
endIdx = i + 4; // </g> uzunluğu = 4
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
if (endIdx === -1) return clean;
|
||||
|
||||
const qrGroup = `<g id="qr-placeholder" transform="translate(90, 215)">
|
||||
<rect width="220" height="220" rx="20" fill="#FFFFFF"/>
|
||||
<image href="data:image/png;base64,${pngBase64}" x="10" y="10" width="200" height="200" preserveAspectRatio="xMidYMid meet"/>
|
||||
</g>`;
|
||||
|
||||
return clean.substring(0, startIdx) + qrGroup + clean.substring(endIdx);
|
||||
}
|
||||
// Component-based template sistemi
|
||||
const TemplateComponent = COMPONENT_TEMPLATES[selectedKey] ?? Object.values(COMPONENT_TEMPLATES)[0];
|
||||
const aspectRatio = TEMPLATE_ASPECT_RATIOS[selectedKey] ?? 1;
|
||||
const displayWidth = 300;
|
||||
const displayHeight = Math.round(displayWidth / aspectRatio);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
@@ -153,19 +110,12 @@ export default function QrScreen() {
|
||||
}
|
||||
|
||||
async function handleExportPdf() {
|
||||
if (!qr || !active || !currentSvgRaw) return;
|
||||
if (!qr || !active) return;
|
||||
setExportingPdf(true);
|
||||
try {
|
||||
const qrImageUri = `data:image/png;base64,${qr.pngBase64}`;
|
||||
|
||||
// Inject QR code directly into the SVG for print-ready A5 PDF
|
||||
const svgWithQr = currentSvgRaw.replace(
|
||||
/<g id="qr-placeholder"[^>]*>[\s\S]*?<\/g>/,
|
||||
`<g id="qr-placeholder" transform="translate(90, 215)">
|
||||
<rect width="220" height="220" rx="20" fill="#FFFFFF" stroke="#E4E4E7" stroke-width="2"/>
|
||||
<image href="${qrImageUri}" x="10" y="10" width="200" height="200" preserveAspectRatio="xMidYMid meet" />
|
||||
</g>`
|
||||
);
|
||||
// ViewShot ile önce PNG al, sonra A5 HTML içine göm
|
||||
const pngUri = await viewShotRef.current?.capture();
|
||||
if (!pngUri) throw new Error("Görsel alınamadı");
|
||||
|
||||
const htmlContent = `
|
||||
<!DOCTYPE html>
|
||||
@@ -181,14 +131,14 @@ export default function QrScreen() {
|
||||
background: #000000;
|
||||
-webkit-print-color-adjust: exact;
|
||||
}
|
||||
.svg-wrap {
|
||||
.img-wrap {
|
||||
width: 140mm;
|
||||
height: 200mm;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
svg {
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
@@ -196,8 +146,8 @@ export default function QrScreen() {
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="svg-wrap">
|
||||
${svgWithQr}
|
||||
<div class="img-wrap">
|
||||
<img src="${pngUri}" />
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -220,6 +170,7 @@ export default function QrScreen() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<View style={{ flex: 1, backgroundColor: "#FAF8F5", alignItems: "center", justifyContent: "center" }}>
|
||||
@@ -347,24 +298,23 @@ export default function QrScreen() {
|
||||
}}
|
||||
>
|
||||
<ViewShot ref={viewShotRef} options={{ format: "png", quality: 1.0 }}>
|
||||
{/* SVG with QR code injected directly — no overlay, pixel-perfect */}
|
||||
{/* Component-based template — no XML parsing, pixel-perfect */}
|
||||
<View
|
||||
style={{
|
||||
width: 300,
|
||||
height: 487.5,
|
||||
width: displayWidth,
|
||||
height: displayHeight,
|
||||
borderRadius: 24,
|
||||
overflow: "hidden",
|
||||
backgroundColor: "#0C0C0E",
|
||||
}}
|
||||
>
|
||||
{currentSvgRaw ? (
|
||||
<SvgXml
|
||||
xml={buildSvgWithQr(currentSvgRaw, qr.pngBase64)}
|
||||
width={300}
|
||||
height={487.5}
|
||||
{TemplateComponent ? (
|
||||
<TemplateComponent
|
||||
qrBase64={qr.pngBase64}
|
||||
width={displayWidth}
|
||||
height={displayHeight}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
</View>
|
||||
</ViewShot>
|
||||
</View>
|
||||
|
||||
Reference in New Issue
Block a user