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:
@@ -1,25 +1,51 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
import { toPng } from 'html-to-image';
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { Select } from '@/components/ui/Select';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { calculateContrast } from '@/lib/utils';
|
||||
import { calculateContrast, cn } from '@/lib/utils';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useCsrf } from '@/hooks/useCsrf';
|
||||
import { showToast } from '@/components/ui/Toast';
|
||||
|
||||
// Content-type specific frame options
|
||||
const getFrameOptionsForContentType = (contentType: string) => {
|
||||
const baseOptions = [{ id: 'none', label: 'No Frame' }, { id: 'scanme', label: 'Scan Me' }];
|
||||
|
||||
switch (contentType) {
|
||||
case 'URL':
|
||||
return [...baseOptions, { id: 'website', label: 'Website' }, { id: 'visit', label: 'Visit' }];
|
||||
case 'PHONE':
|
||||
return [...baseOptions, { id: 'callme', label: 'Call Me' }, { id: 'call', label: 'Call' }];
|
||||
case 'GEO':
|
||||
return [...baseOptions, { id: 'findus', label: 'Find Us' }, { id: 'navigate', label: 'Navigate' }];
|
||||
case 'VCARD':
|
||||
return [...baseOptions, { id: 'contact', label: 'Contact' }, { id: 'save', label: 'Save' }];
|
||||
case 'SMS':
|
||||
return [...baseOptions, { id: 'textme', label: 'Text Me' }, { id: 'message', label: 'Message' }];
|
||||
case 'WHATSAPP':
|
||||
return [...baseOptions, { id: 'chatme', label: 'Chat Me' }, { id: 'whatsapp', label: 'WhatsApp' }];
|
||||
case 'TEXT':
|
||||
return [...baseOptions, { id: 'read', label: 'Read' }, { id: 'info', label: 'Info' }];
|
||||
default:
|
||||
return [...baseOptions, { id: 'website', label: 'Website' }, { id: 'visit', label: 'Visit' }];
|
||||
}
|
||||
};
|
||||
|
||||
export default function CreatePage() {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
const { fetchWithCsrf } = useCsrf();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [userPlan, setUserPlan] = useState<string>('FREE');
|
||||
const qrRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Form state
|
||||
const [title, setTitle] = useState('');
|
||||
@@ -32,6 +58,18 @@ export default function CreatePage() {
|
||||
const [backgroundColor, setBackgroundColor] = useState('#FFFFFF');
|
||||
const [cornerStyle, setCornerStyle] = useState('square');
|
||||
const [size, setSize] = useState(200);
|
||||
const [frameType, setFrameType] = useState('none');
|
||||
|
||||
// Get frame options for current content type
|
||||
const frameOptions = getFrameOptionsForContentType(contentType);
|
||||
|
||||
// Reset frame type when content type changes (if current frame is not valid)
|
||||
useEffect(() => {
|
||||
const validIds = frameOptions.map(f => f.id);
|
||||
if (!validIds.includes(frameType)) {
|
||||
setFrameType('none');
|
||||
}
|
||||
}, [contentType, frameOptions, frameType]);
|
||||
|
||||
// Logo state
|
||||
const [logoUrl, setLogoUrl] = useState('');
|
||||
@@ -97,61 +135,58 @@ export default function CreatePage() {
|
||||
|
||||
const qrContent = getQRContent();
|
||||
|
||||
const getFrameLabel = () => {
|
||||
const frame = frameOptions.find((f: { id: string; label: string }) => f.id === frameType);
|
||||
return frame?.id !== 'none' ? frame?.label : null;
|
||||
};
|
||||
|
||||
const downloadQR = async (format: 'svg' | 'png') => {
|
||||
if (!qrRef.current) return;
|
||||
try {
|
||||
// Get the content based on content type
|
||||
let qrContent = '';
|
||||
switch (contentType) {
|
||||
case 'URL':
|
||||
qrContent = content.url || '';
|
||||
break;
|
||||
case 'PHONE':
|
||||
qrContent = `tel:${content.phone || ''}`;
|
||||
break;
|
||||
case 'EMAIL':
|
||||
qrContent = `mailto:${content.email || ''}${content.subject ? `?subject=${encodeURIComponent(content.subject)}` : ''}`;
|
||||
break;
|
||||
case 'TEXT':
|
||||
qrContent = content.text || '';
|
||||
break;
|
||||
default:
|
||||
qrContent = content.url || '';
|
||||
}
|
||||
|
||||
if (!qrContent) return;
|
||||
|
||||
const QRCode = (await import('qrcode')).default;
|
||||
|
||||
if (format === 'svg') {
|
||||
const svg = await QRCode.toString(qrContent, {
|
||||
type: 'svg',
|
||||
width: size,
|
||||
margin: 2,
|
||||
color: {
|
||||
dark: foregroundColor,
|
||||
light: backgroundColor,
|
||||
},
|
||||
});
|
||||
|
||||
const blob = new Blob([svg], { type: 'image/svg+xml' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `qrcode-${title || 'download'}.svg`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
if (format === 'png') {
|
||||
const dataUrl = await toPng(qrRef.current, { cacheBust: true, pixelRatio: 3, backgroundColor: 'transparent' });
|
||||
const link = document.createElement('a');
|
||||
link.download = `qrcode-${title || 'download'}.png`;
|
||||
link.href = dataUrl;
|
||||
link.click();
|
||||
} else {
|
||||
const a = document.createElement('a');
|
||||
a.href = qrDataUrl;
|
||||
a.download = `qrcode-${title || 'download'}.png`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
// For SVG, we might still want to use the library or just toPng if SVG export of HTML is not needed
|
||||
// Simplest is to check if we can export the SVG element directly but that misses the frame HTML.
|
||||
// html-to-image can generate SVG too.
|
||||
// But usually for SVG users want the vector. Capturing HTML to SVG is possible but complex.
|
||||
// For now, let's just stick to the SVG code export if NO FRAME is selected,
|
||||
// otherwise warn or use toPng (as SVG).
|
||||
// Actually, the previous implementation was good for pure QR.
|
||||
// If frame is selected, we MUST use a raster export (PNG) or complex HTML-to-SVG.
|
||||
// Let's rely on toPng for consistency with frames.
|
||||
const dataUrl = await toPng(qrRef.current, { cacheBust: true, pixelRatio: 3, backgroundColor: 'transparent' });
|
||||
// Wait, exporting HTML to valid vector SVG is hard.
|
||||
// Let's just offer PNG for frames for now to be safe, or just use the same PNG download for both buttons if frame is active?
|
||||
// No, let's try to grab the INNER SVG if no frame, else...
|
||||
if (frameType === 'none') {
|
||||
const svgElement = qrRef.current.querySelector('svg');
|
||||
if (svgElement) {
|
||||
const svgData = new XMLSerializer().serializeToString(svgElement);
|
||||
const blob = new Blob([svgData], { type: 'image/svg+xml' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `qrcode-${title || 'download'}.svg`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
} else {
|
||||
showToast('SVG download not available with frames yet. Downloading PNG instead.', 'info');
|
||||
const dataUrl = await toPng(qrRef.current, { cacheBust: true, pixelRatio: 3, backgroundColor: 'transparent' });
|
||||
const link = document.createElement('a');
|
||||
link.download = `qrcode-${title || 'download'}.png`;
|
||||
link.href = dataUrl;
|
||||
link.click();
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error downloading QR code:', err);
|
||||
showToast('Error downloading QR code', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -220,6 +255,7 @@ export default function CreatePage() {
|
||||
width: logoSize,
|
||||
excavate,
|
||||
} : undefined,
|
||||
frameType, // Save frame type
|
||||
},
|
||||
};
|
||||
|
||||
@@ -448,7 +484,7 @@ export default function CreatePage() {
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<CardContent className="space-y-6">
|
||||
{!canCustomizeColors && (
|
||||
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg mb-4">
|
||||
<p className="text-sm text-blue-900">
|
||||
@@ -461,6 +497,29 @@ export default function CreatePage() {
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Frame Options */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-3">Frame</label>
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{frameOptions.map((frame: { id: string; label: string }) => (
|
||||
<button
|
||||
key={frame.id}
|
||||
type="button"
|
||||
onClick={() => setFrameType(frame.id)}
|
||||
className={cn(
|
||||
"py-2 px-3 rounded-lg text-sm font-medium transition-all border",
|
||||
frameType === frame.id
|
||||
? "bg-slate-900 text-white border-slate-900"
|
||||
: "bg-gray-50 text-gray-600 border-gray-200 hover:border-gray-300"
|
||||
)}
|
||||
>
|
||||
{frame.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
@@ -635,27 +694,48 @@ export default function CreatePage() {
|
||||
</CardHeader>
|
||||
<CardContent className="text-center">
|
||||
<div id="create-qr-preview" className="flex justify-center mb-4">
|
||||
{qrContent ? (
|
||||
<div className={cornerStyle === 'rounded' ? 'rounded-lg overflow-hidden' : ''}>
|
||||
<QRCodeSVG
|
||||
value={qrContent}
|
||||
size={200}
|
||||
fgColor={foregroundColor}
|
||||
bgColor={backgroundColor}
|
||||
level="H"
|
||||
imageSettings={logoUrl ? {
|
||||
src: logoUrl,
|
||||
height: logoSize,
|
||||
width: logoSize,
|
||||
excavate: excavate,
|
||||
} : undefined}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-[200px] h-[200px] bg-gray-100 rounded flex items-center justify-center text-gray-500">
|
||||
Enter content
|
||||
</div>
|
||||
)}
|
||||
{/* WRAPPER FOR REF AND FRAME */}
|
||||
<div
|
||||
ref={qrRef}
|
||||
className="relative bg-white rounded-xl p-4 flex flex-col items-center justify-center transition-all duration-300"
|
||||
style={{
|
||||
minWidth: '280px',
|
||||
minHeight: '280px',
|
||||
}}
|
||||
>
|
||||
{/* Frame Label */}
|
||||
{getFrameLabel() && (
|
||||
<div
|
||||
className="mb-4 px-6 py-2 rounded-full font-bold text-sm tracking-widest uppercase shadow-md text-white"
|
||||
style={{ backgroundColor: foregroundColor }}
|
||||
>
|
||||
{getFrameLabel()}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{qrContent ? (
|
||||
<div className={cornerStyle === 'rounded' ? 'rounded-lg overflow-hidden' : ''}>
|
||||
<QRCodeSVG
|
||||
value={qrContent}
|
||||
size={size}
|
||||
fgColor={foregroundColor}
|
||||
bgColor={backgroundColor}
|
||||
level="H"
|
||||
includeMargin={false}
|
||||
imageSettings={logoUrl ? {
|
||||
src: logoUrl,
|
||||
height: logoSize,
|
||||
width: logoSize,
|
||||
excavate: excavate,
|
||||
} : undefined}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-[200px] h-[200px] bg-gray-100 rounded flex items-center justify-center text-gray-500">
|
||||
Enter content
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
@@ -663,38 +743,7 @@ export default function CreatePage() {
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const svg = document.querySelector('#create-qr-preview svg');
|
||||
if (!svg) return;
|
||||
|
||||
let svgData = new XMLSerializer().serializeToString(svg);
|
||||
|
||||
// If rounded corners, wrap in a clipped SVG
|
||||
if (cornerStyle === 'rounded') {
|
||||
const width = svg.getAttribute('width') || '200';
|
||||
const height = svg.getAttribute('height') || '200';
|
||||
const borderRadius = 20;
|
||||
|
||||
svgData = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
|
||||
<defs>
|
||||
<clipPath id="rounded-corners">
|
||||
<rect x="0" y="0" width="${width}" height="${height}" rx="${borderRadius}" ry="${borderRadius}"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g clip-path="url(#rounded-corners)">
|
||||
${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 = `${title || 'qrcode'}.svg`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}}
|
||||
onClick={() => downloadQR('svg')}
|
||||
disabled={!qrContent}
|
||||
>
|
||||
Download SVG
|
||||
@@ -703,54 +752,7 @@ export default function CreatePage() {
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const svg = document.querySelector('#create-qr-preview svg');
|
||||
if (!svg) return;
|
||||
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 = 200;
|
||||
canvas.height = 200;
|
||||
|
||||
// Apply rounded corners if needed
|
||||
if (cornerStyle === 'rounded') {
|
||||
const borderRadius = 20;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(borderRadius, 0);
|
||||
ctx.lineTo(200 - borderRadius, 0);
|
||||
ctx.quadraticCurveTo(200, 0, 200, borderRadius);
|
||||
ctx.lineTo(200, 200 - borderRadius);
|
||||
ctx.quadraticCurveTo(200, 200, 200 - borderRadius, 200);
|
||||
ctx.lineTo(borderRadius, 200);
|
||||
ctx.quadraticCurveTo(0, 200, 0, 200 - borderRadius);
|
||||
ctx.lineTo(0, borderRadius);
|
||||
ctx.quadraticCurveTo(0, 0, borderRadius, 0);
|
||||
ctx.closePath();
|
||||
ctx.clip();
|
||||
}
|
||||
|
||||
ctx.drawImage(img, 0, 0);
|
||||
canvas.toBlob((blob) => {
|
||||
if (blob) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${title || 'qrcode'}.png`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
});
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
img.src = url;
|
||||
}}
|
||||
onClick={() => downloadQR('png')}
|
||||
disabled={!qrContent}
|
||||
>
|
||||
Download PNG
|
||||
|
||||
Reference in New Issue
Block a user