Import source: src/components/CenterPanel/QRPreview
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
@@ -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<Pattern, string> = {
|
||||||
|
square: 'square', dots: 'dots', rounded: 'rounded',
|
||||||
|
classy: 'classy', 'classy-rounded': 'classy-rounded',
|
||||||
|
};
|
||||||
|
const CS_MAP: Record<CornerSquareStyle, string> = {
|
||||||
|
square: 'square', dot: 'dot', 'extra-rounded': 'extra-rounded',
|
||||||
|
};
|
||||||
|
const CD_MAP: Record<CornerDotStyle, string> = { square: 'square', dot: 'dot' };
|
||||||
|
const SIZE_MAP = { social: 512, card: 1024, print: 2048 };
|
||||||
|
|
||||||
|
const DARK_BG: Record<string, string> = {
|
||||||
|
paper: '#1C1A14', midnight: '#0D0E1A', pastel: '#1A0D12',
|
||||||
|
};
|
||||||
|
|
||||||
|
let qrInstance: QRCodeStyling | null = null;
|
||||||
|
|
||||||
|
function buildOptions(state: ReturnType<typeof useQR>['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<string, unknown> = 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<string, unknown> = 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<string, unknown> = { 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<HTMLDivElement>(null);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||||
|
const debounceRef = useRef<ReturnType<typeof setTimeout> | 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 (
|
||||||
|
<div ref={cardRef} className="rounded-[18px] p-4 sm:p-6 flex flex-col items-center gap-3 transition-all duration-300"
|
||||||
|
style={{ background: cardBg, boxShadow: 'var(--preview-shadow)' }}>
|
||||||
|
<div ref={wrapperRef} className="qr-appear rounded-xl overflow-hidden relative">
|
||||||
|
<div ref={containerRef} />
|
||||||
|
{!qrData && (
|
||||||
|
<div className="absolute inset-0 flex flex-col items-center justify-center rounded-xl"
|
||||||
|
style={{ background: `${cardBg}CC`, backdropFilter: 'blur(2px)' }}>
|
||||||
|
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" style={{ opacity: 0.3 }}>
|
||||||
|
<rect x="2" y="2" width="13" height="13" rx="2" stroke="currentColor" strokeWidth="2"/>
|
||||||
|
<rect x="21" y="2" width="13" height="13" rx="2" stroke="currentColor" strokeWidth="2"/>
|
||||||
|
<rect x="2" y="21" width="13" height="13" rx="2" stroke="currentColor" strokeWidth="2"/>
|
||||||
|
<rect x="5.5" y="5.5" width="6" height="6" rx="1" fill="currentColor" style={{ opacity: 0.5 }}/>
|
||||||
|
<rect x="24.5" y="5.5" width="6" height="6" rx="1" fill="currentColor" style={{ opacity: 0.5 }}/>
|
||||||
|
<rect x="5.5" y="24.5" width="6" height="6" rx="1" fill="currentColor" style={{ opacity: 0.5 }}/>
|
||||||
|
<circle cx="26" cy="26" r="6" stroke="currentColor" strokeWidth="2" style={{ opacity: 0.4 }}/>
|
||||||
|
<path d="M23 26h6M26 23v6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" style={{ opacity: 0.4 }}/>
|
||||||
|
</svg>
|
||||||
|
<p className="text-[13px] font-medium mt-2" style={{ color: 'var(--text-secondary)', opacity: 0.7 }}>
|
||||||
|
Enter content to generate
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{state.frameLabel && (
|
||||||
|
<p className="text-[13px] sm:text-[14px] font-semibold tracking-widest uppercase text-center px-4"
|
||||||
|
style={{ color: isDark ? 'rgba(255,255,255,0.65)' : 'rgba(0,0,0,0.55)', letterSpacing: '0.2em' }}>
|
||||||
|
{state.frameLabel}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function downloadQR(state: ReturnType<typeof useQR>['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<typeof useQR>['state'],
|
||||||
|
isDark: boolean,
|
||||||
|
): Promise<void> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user