diff --git a/src/app/(main)/(app)/create/page.tsx b/src/app/(main)/(app)/create/page.tsx index 5e3e9c5..dc3e9a8 100644 --- a/src/app/(main)/(app)/create/page.tsx +++ b/src/app/(main)/(app)/create/page.tsx @@ -62,6 +62,43 @@ const getFrameOptionsForContentType = (contentType: string) => { } }; +// Injects a caption element below a barcode SVG and expands its height/viewBox. +// Used so the "scanner app" hint is baked into the downloaded SVG. +function addBarcodeCaptionToSvg(svgElement: SVGElement, caption: string): string { + const cloned = svgElement.cloneNode(true) as SVGElement; + const NS = 'http://www.w3.org/2000/svg'; + + const widthAttr = cloned.getAttribute('width'); + const heightAttr = cloned.getAttribute('height'); + const width = widthAttr ? parseFloat(widthAttr) : 200; + const height = heightAttr ? parseFloat(heightAttr) : 100; + const extraHeight = 18; + + cloned.setAttribute('height', String(height + extraHeight)); + const viewBox = cloned.getAttribute('viewBox'); + if (viewBox) { + const parts = viewBox.split(/\s+/); + if (parts.length === 4) { + cloned.setAttribute( + 'viewBox', + `${parts[0]} ${parts[1]} ${parts[2]} ${parseFloat(parts[3]) + extraHeight}` + ); + } + } + + const text = document.createElementNS(NS, 'text'); + text.setAttribute('x', String(width / 2)); + text.setAttribute('y', String(height + 12)); + text.setAttribute('text-anchor', 'middle'); + text.setAttribute('font-size', '9'); + text.setAttribute('font-family', 'Arial, Helvetica, sans-serif'); + text.setAttribute('fill', '#666666'); + text.textContent = caption; + cloned.appendChild(text); + + return new XMLSerializer().serializeToString(cloned); +} + export default function CreatePage() { const router = useRouter(); const { t } = useTranslation(); @@ -212,7 +249,9 @@ export default function CreatePage() { if (frameType === 'none') { const svgElement = qrRef.current.querySelector('svg'); if (svgElement) { - const svgData = new XMLSerializer().serializeToString(svgElement); + const svgData = contentType === 'BARCODE' + ? addBarcodeCaptionToSvg(svgElement, 'Scan: iPhone -> Barcode Scanner App | Android -> Google Lens') + : new XMLSerializer().serializeToString(svgElement); const blob = new Blob([svgData], { type: 'image/svg+xml' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); @@ -651,11 +690,16 @@ export default function CreatePage() { <> {isDynamic ? ( <> -
- How dynamic barcodes work: The barcode encodes a short redirect URL - (e.g. qrmaster.net/r/…). When scanned with a - smartphone camera, it opens the browser and redirects to your destination — which you - can update anytime. Works with smartphone cameras, not POS laser scanners. +
+

+ How dynamic barcodes work: The barcode encodes a short redirect URL + (e.g. qrmaster.net/r/…) that you can update anytime. +

+

+ 📱 Scanner tip: Use a barcode scanner app on iPhone + (iOS Camera doesn't auto-open links from barcodes). Android Google Lens / Camera works + out of the box. Print min. 5 cm wide for reliable scanning. +

{t('create.preview')} -
+
{/* WRAPPER FOR REF AND FRAME */}
@@ -1060,7 +1103,7 @@ export default function CreatePage() { {contentType === 'BARCODE' ? ( qrContent ? ( -
+
+

+ Scan: iPhone → Barcode Scanner App · Android → Google Lens / Camera +

) : (
diff --git a/src/components/dashboard/QRCodeCard.tsx b/src/components/dashboard/QRCodeCard.tsx index 38bd17c..806816a 100644 --- a/src/components/dashboard/QRCodeCard.tsx +++ b/src/components/dashboard/QRCodeCard.tsx @@ -8,6 +8,41 @@ import { Badge } from '@/components/ui/Badge'; import { Dropdown, DropdownItem } from '@/components/ui/Dropdown'; import { formatDate } from '@/lib/utils'; +function addBarcodeCaptionToSvg(svgElement: SVGElement, caption: string): string { + const cloned = svgElement.cloneNode(true) as SVGElement; + const NS = 'http://www.w3.org/2000/svg'; + + const widthAttr = cloned.getAttribute('width'); + const heightAttr = cloned.getAttribute('height'); + const width = widthAttr ? parseFloat(widthAttr) : 200; + const height = heightAttr ? parseFloat(heightAttr) : 100; + const extraHeight = 18; + + cloned.setAttribute('height', String(height + extraHeight)); + const viewBox = cloned.getAttribute('viewBox'); + if (viewBox) { + const parts = viewBox.split(/\s+/); + if (parts.length === 4) { + cloned.setAttribute( + 'viewBox', + `${parts[0]} ${parts[1]} ${parts[2]} ${parseFloat(parts[3]) + extraHeight}` + ); + } + } + + const text = document.createElementNS(NS, 'text'); + text.setAttribute('x', String(width / 2)); + text.setAttribute('y', String(height + 12)); + text.setAttribute('text-anchor', 'middle'); + text.setAttribute('font-size', '9'); + text.setAttribute('font-family', 'Arial, Helvetica, sans-serif'); + text.setAttribute('fill', '#666666'); + text.textContent = caption; + cloned.appendChild(text); + + return new XMLSerializer().serializeToString(cloned); +} + interface QRCodeCardProps { qr: { id: string; @@ -113,7 +148,9 @@ END:VCARD`; const svgContainer = document.querySelector(`#qr-svg-${qr.id}`); const svg = svgContainer?.querySelector('svg'); if (svg) { - let svgData = new XMLSerializer().serializeToString(svg); + let svgData = qr.contentType === 'BARCODE' + ? addBarcodeCaptionToSvg(svg as SVGElement, 'Scan: iPhone -> Barcode Scanner App | Android -> Google Lens') + : new XMLSerializer().serializeToString(svg); if (qr.style?.cornerStyle === 'rounded') { const width = svg.getAttribute('width') || '96'; @@ -210,17 +247,23 @@ END:VCARD`; )}
{qr.contentType === 'BARCODE' ? ( - +
+ +

+ Scan: iPhone → Barcode Scanner App · Android → Google Lens +

+
) : (