From a2966f04ba69cea9d98fbaff4db92818617cfcee Mon Sep 17 00:00:00 2001 From: arsam Date: Fri, 18 Sep 2026 23:17:01 +0330 Subject: [PATCH] Import source: src/components/CenterPanel/QRPreview Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality. --- src/components/CenterPanel/QRPreview.tsx | 162 +++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 src/components/CenterPanel/QRPreview.tsx diff --git a/src/components/CenterPanel/QRPreview.tsx b/src/components/CenterPanel/QRPreview.tsx new file mode 100644 index 0000000..87ea455 --- /dev/null +++ b/src/components/CenterPanel/QRPreview.tsx @@ -0,0 +1,162 @@ +import { useEffect, useRef, useState, useCallback } from 'react'; +import QRCodeStyling from 'qr-code-styling'; +import { useQR } from '../../context/QRContext'; +import { useTheme } from '../../context/ThemeContext'; +import { encodeQR } from '../../utils/encodeQR'; +import { THEME_CARD_BG } from '../../config/styleOptions'; +import type { Pattern, CornerSquareStyle, CornerDotStyle } from '../../types'; + +const DOT_MAP: Record = { + square: 'square', dots: 'dots', rounded: 'rounded', + classy: 'classy', 'classy-rounded': 'classy-rounded', +}; +const CS_MAP: Record = { + square: 'square', dot: 'dot', 'extra-rounded': 'extra-rounded', +}; +const CD_MAP: Record = { square: 'square', dot: 'dot' }; +const SIZE_MAP = { social: 512, card: 1024, print: 2048 }; + +const DARK_BG: Record = { + paper: '#1C1A14', midnight: '#0D0E1A', pastel: '#1A0D12', +}; + +let qrInstance: QRCodeStyling | null = null; + +function buildOptions(state: ReturnType['state'], isDark: boolean, size = 340) { + const cardBg = isDark ? DARK_BG[state.theme] : THEME_CARD_BG[state.theme]; + const bgColor = state.bgColor ?? cardBg; + const dotColor = state.qrColor === 'rainbow' ? '#7C3AED' : state.qrColor; + + const dotsOptions: Record = state.gradientEnabled + ? { type: DOT_MAP[state.pattern], gradient: { type: 'linear', rotation: 45, colorStops: [{ offset: 0, color: dotColor }, { offset: 1, color: state.gradientColor2 }] } } + : { type: DOT_MAP[state.pattern], color: dotColor }; + + const cornersSquareOptions: Record = state.gradientEnabled + ? { type: CS_MAP[state.cornerSquareStyle], gradient: { type: 'linear', rotation: 45, colorStops: [{ offset: 0, color: dotColor }, { offset: 1, color: state.gradientColor2 }] } } + : { type: CS_MAP[state.cornerSquareStyle], color: dotColor }; + + const cornersDotOptions: Record = { type: CD_MAP[state.cornerDotStyle], color: dotColor }; + + return { + width: size, height: size, + data: encodeQR(state) || 'https://craftqr.app', + dotsOptions, cornersSquareOptions, cornersDotOptions, + backgroundOptions: { color: bgColor }, + ...(state.logoDataUrl ? { image: state.logoDataUrl, imageOptions: { margin: 6, imageSize: 0.28, crossOrigin: 'anonymous' } } : {}), + qrOptions: { errorCorrectionLevel: state.logoDataUrl ? 'H' : 'M' } as const, + }; +} + +export function QRPreview() { + const { state } = useQR(); + const { isDark } = useTheme(); + const cardRef = useRef(null); + const containerRef = useRef(null); + const wrapperRef = useRef(null); + const debounceRef = useRef | null>(null); + const [qrSize, setQrSize] = useState(340); + const [, setAnimKey] = useState(0); + + const qrData = encodeQR(state); + const cardBg = isDark ? DARK_BG[state.theme] : THEME_CARD_BG[state.theme]; + + // Adaptive QR size — tracks card width via ResizeObserver + useEffect(() => { + if (!cardRef.current) return; + const ro = new ResizeObserver(entries => { + const w = entries[0].contentRect.width; + const newSize = Math.min(Math.max(200, Math.floor(w - 48)), 600); + setQrSize(prev => { + if (Math.abs(prev - newSize) > 10) return newSize; + return prev; + }); + }); + ro.observe(cardRef.current); + return () => ro.disconnect(); + }, []); + + const initQR = useCallback((size: number) => { + if (!containerRef.current) return; + containerRef.current.replaceChildren(); + qrInstance = new QRCodeStyling(buildOptions(state, isDark, size) as never); + qrInstance.append(containerRef.current); + }, []); + + useEffect(() => { initQR(qrSize); return () => { qrInstance = null; }; }, [qrSize]); + + useEffect(() => { + if (debounceRef.current) clearTimeout(debounceRef.current); + debounceRef.current = setTimeout(() => { + if (!qrInstance) return; + qrInstance.update(buildOptions(state, isDark, qrSize) as never); + if (wrapperRef.current) { + wrapperRef.current.classList.remove('qr-appear'); + void wrapperRef.current.offsetHeight; + wrapperRef.current.classList.add('qr-appear'); + } + setAnimKey(k => k + 1); + }, 280); + return () => { if (debounceRef.current) clearTimeout(debounceRef.current); }; + }, [qrData, state.qrColor, state.bgColor, state.pattern, state.logoDataUrl, + state.cornerSquareStyle, state.cornerDotStyle, state.gradientEnabled, + state.gradientColor2, state.theme, isDark, qrSize]); + + return ( +
+
+
+ {!qrData && ( +
+ + + + + + + + + + +

+ Enter content to generate +

+
+ )} +
+ {state.frameLabel && ( +

+ {state.frameLabel} +

+ )} +
+ ); +} + +export function downloadQR(state: ReturnType['state'], isDark: boolean, format: 'png' | 'svg' = 'png') { + const size = SIZE_MAP[state.downloadSize]; + const dl = new QRCodeStyling(buildOptions(state, isDark, size) as never); + dl.download({ name: 'qrcode', extension: format }); +} + +export async function copyQR( + state: ReturnType['state'], + isDark: boolean, +): Promise { + const container = document.createElement('div'); + container.style.cssText = 'position:fixed;left:-9999px;top:-9999px;pointer-events:none;'; + document.body.appendChild(container); + const temp = new QRCodeStyling(buildOptions(state, isDark, 512) as never); + temp.append(container); + await new Promise(r => setTimeout(r, 220)); + try { + const svgEl = container.querySelector('svg'); + if (!svgEl) throw new Error('SVG not found'); + const blob = new Blob([new XMLSerializer().serializeToString(svgEl)], { type: 'image/svg+xml' }); + await navigator.clipboard.write([new ClipboardItem({ 'image/svg+xml': blob })]); + } finally { + document.body.removeChild(container); + } +}