feat: Add 19 free QR code tools with SEO optimization
- Added PayPal, Zoom, Teams QR generators - Added lazy loading for html-to-image (performance) - Created 19 OG images for social sharing - Added robots.txt and updated sitemap - Fixed mobile navigation with accordion menu - Added 7 color options per generator - Fixed crypto QR with universal/wallet mode toggle - Hero QR codes all point to qrmaster.net
This commit is contained in:
@@ -74,88 +74,74 @@ END:VCARD`;
|
||||
console.log(`DYNAMIC QR [${qr.title}]: ${qrUrl}`);
|
||||
}
|
||||
|
||||
const downloadQR = (format: 'png' | 'svg') => {
|
||||
const svg = document.querySelector(`#qr-${qr.id} svg`);
|
||||
if (!svg) return;
|
||||
const downloadQR = async (format: 'png' | 'svg') => {
|
||||
// Use the tight download wrapper
|
||||
const container = document.querySelector(`#qr-download-${qr.id}`);
|
||||
if (!container) return;
|
||||
|
||||
if (format === 'svg') {
|
||||
let svgData = new XMLSerializer().serializeToString(svg);
|
||||
// Dynamic import of html-to-image
|
||||
const { toPng } = await import('html-to-image');
|
||||
|
||||
// If rounded corners, wrap in a clipped SVG
|
||||
if (qr.style?.cornerStyle === 'rounded') {
|
||||
const width = svg.getAttribute('width') || '96';
|
||||
const height = svg.getAttribute('height') || '96';
|
||||
const borderRadius = 10; // Smaller radius for dashboard
|
||||
try {
|
||||
if (format === 'png') {
|
||||
const dataUrl = await toPng(container as HTMLElement, {
|
||||
cacheBust: true,
|
||||
pixelRatio: 3,
|
||||
backgroundColor: '#ffffff' // White background for clean export
|
||||
});
|
||||
const link = document.createElement('a');
|
||||
link.download = `${qr.title.replace(/\s+/g, '-').toLowerCase()}.png`;
|
||||
link.href = dataUrl;
|
||||
link.click();
|
||||
} else {
|
||||
// For SVG, if no frame, export just the QR code SVG for vector quality
|
||||
// If frame exists, use toPng as fallback since HTML-to-SVG is complex
|
||||
if (qr.style?.frameType && qr.style.frameType !== 'none') {
|
||||
// Frame exists - use PNG for full capture
|
||||
const dataUrl = await toPng(container as HTMLElement, {
|
||||
cacheBust: true,
|
||||
pixelRatio: 3,
|
||||
backgroundColor: '#ffffff'
|
||||
});
|
||||
const link = document.createElement('a');
|
||||
link.download = `${qr.title.replace(/\s+/g, '-').toLowerCase()}.png`;
|
||||
link.href = dataUrl;
|
||||
link.click();
|
||||
} else {
|
||||
// No frame - export clean SVG from the svg wrapper
|
||||
const svgContainer = document.querySelector(`#qr-svg-${qr.id}`);
|
||||
const svg = svgContainer?.querySelector('svg');
|
||||
if (svg) {
|
||||
let svgData = new XMLSerializer().serializeToString(svg);
|
||||
|
||||
svgData = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
|
||||
<defs>
|
||||
<clipPath id="rounded-corners-${qr.id}">
|
||||
<rect x="0" y="0" width="${width}" height="${height}" rx="${borderRadius}" ry="${borderRadius}"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g clip-path="url(#rounded-corners-${qr.id})">
|
||||
${svgData}
|
||||
</g>
|
||||
</svg>`;
|
||||
}
|
||||
if (qr.style?.cornerStyle === 'rounded') {
|
||||
const width = svg.getAttribute('width') || '96';
|
||||
const height = svg.getAttribute('height') || '96';
|
||||
const borderRadius = 10;
|
||||
svgData = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
|
||||
<defs>
|
||||
<clipPath id="rounded-corners-${qr.id}">
|
||||
<rect x="0" y="0" width="${width}" height="${height}" rx="${borderRadius}" ry="${borderRadius}"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g clip-path="url(#rounded-corners-${qr.id})">
|
||||
${svgData}
|
||||
</g>
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
const blob = new Blob([svgData], { type: 'image/svg+xml' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${qr.title.replace(/\s+/g, '-').toLowerCase()}.svg`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
} else {
|
||||
// Convert SVG to PNG
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
const img = new Image();
|
||||
const svgData = new XMLSerializer().serializeToString(svg);
|
||||
const blob = new Blob([svgData], { type: 'image/svg+xml' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
img.onload = () => {
|
||||
canvas.width = 300;
|
||||
canvas.height = 300;
|
||||
|
||||
// Apply rounded corners if needed
|
||||
if (qr.style?.cornerStyle === 'rounded') {
|
||||
const borderRadius = 30; // Scale up for 300px canvas
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(borderRadius, 0);
|
||||
ctx.lineTo(300 - borderRadius, 0);
|
||||
ctx.quadraticCurveTo(300, 0, 300, borderRadius);
|
||||
ctx.lineTo(300, 300 - borderRadius);
|
||||
ctx.quadraticCurveTo(300, 300, 300 - borderRadius, 300);
|
||||
ctx.lineTo(borderRadius, 300);
|
||||
ctx.quadraticCurveTo(0, 300, 0, 300 - borderRadius);
|
||||
ctx.lineTo(0, borderRadius);
|
||||
ctx.quadraticCurveTo(0, 0, borderRadius, 0);
|
||||
ctx.closePath();
|
||||
ctx.clip();
|
||||
}
|
||||
|
||||
ctx.drawImage(img, 0, 0, 300, 300);
|
||||
canvas.toBlob((blob) => {
|
||||
if (blob) {
|
||||
const blob = new Blob([svgData], { type: 'image/svg+xml' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${qr.title.replace(/\s+/g, '-').toLowerCase()}.png`;
|
||||
document.body.appendChild(a);
|
||||
a.download = `${qr.title.replace(/\s+/g, '-').toLowerCase()}.svg`;
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
});
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
img.src = url;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Download failed:', err);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -193,21 +179,48 @@ END:VCARD`;
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
<div id={`qr-${qr.id}`} className="flex items-center justify-center bg-gray-50 rounded-lg p-4 mb-3">
|
||||
<div className={qr.style?.cornerStyle === 'rounded' ? 'rounded-lg overflow-hidden' : ''}>
|
||||
<QRCodeSVG
|
||||
value={qrUrl}
|
||||
size={96}
|
||||
fgColor={qr.style?.foregroundColor || '#000000'}
|
||||
bgColor={qr.style?.backgroundColor || '#FFFFFF'}
|
||||
level="H"
|
||||
imageSettings={qr.style?.imageSettings ? {
|
||||
src: qr.style.imageSettings.src,
|
||||
height: qr.style.imageSettings.height * (96 / 200), // Scale logo for smaller QR
|
||||
width: qr.style.imageSettings.width * (96 / 200),
|
||||
excavate: qr.style.imageSettings.excavate,
|
||||
} : undefined}
|
||||
/>
|
||||
<div className="flex flex-col items-center justify-center bg-gray-50 rounded-lg p-4 mb-3">
|
||||
{/* Download wrapper - tightly wraps content */}
|
||||
<div id={`qr-download-${qr.id}`} className="inline-flex flex-col items-center bg-white p-4 rounded-xl">
|
||||
{/* Frame Label */}
|
||||
{qr.style?.frameType && qr.style.frameType !== 'none' && (
|
||||
<div
|
||||
className="mb-3 px-4 py-1.5 rounded-full font-bold text-xs tracking-widest uppercase shadow-sm text-white"
|
||||
style={{ backgroundColor: qr.style?.foregroundColor || '#000000' }}
|
||||
>
|
||||
{qr.style.frameType === 'scanme' ? 'Scan Me' :
|
||||
qr.style.frameType === 'website' ? 'Website' :
|
||||
qr.style.frameType === 'visit' ? 'Visit' :
|
||||
qr.style.frameType === 'callme' ? 'Call Me' :
|
||||
qr.style.frameType === 'call' ? 'Call' :
|
||||
qr.style.frameType === 'findus' ? 'Find Us' :
|
||||
qr.style.frameType === 'navigate' ? 'Navigate' :
|
||||
qr.style.frameType === 'contact' ? 'Contact' :
|
||||
qr.style.frameType === 'save' ? 'Save' :
|
||||
qr.style.frameType === 'textme' ? 'Text Me' :
|
||||
qr.style.frameType === 'message' ? 'Message' :
|
||||
qr.style.frameType === 'chatme' ? 'Chat Me' :
|
||||
qr.style.frameType === 'whatsapp' ? 'WhatsApp' :
|
||||
qr.style.frameType === 'read' ? 'Read' :
|
||||
qr.style.frameType === 'info' ? 'Info' :
|
||||
qr.style.frameType.charAt(0).toUpperCase() + qr.style.frameType.slice(1)}
|
||||
</div>
|
||||
)}
|
||||
<div id={`qr-svg-${qr.id}`} className={qr.style?.cornerStyle === 'rounded' ? 'rounded-lg overflow-hidden' : ''}>
|
||||
<QRCodeSVG
|
||||
value={qrUrl}
|
||||
size={96}
|
||||
fgColor={qr.style?.foregroundColor || '#000000'}
|
||||
bgColor={qr.style?.backgroundColor || '#FFFFFF'}
|
||||
level="H"
|
||||
imageSettings={qr.style?.imageSettings ? {
|
||||
src: qr.style.imageSettings.src,
|
||||
height: qr.style.imageSettings.height * (96 / 200), // Scale logo for smaller QR
|
||||
width: qr.style.imageSettings.width * (96 / 200),
|
||||
excavate: qr.style.imageSettings.excavate,
|
||||
} : undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user