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