feat: add QR code generator tool
Add a sixth tool with live SVG preview, customizable foreground/background colors, error correction level, margin control, and export as PNG (256–2048px) or SVG. URL params enable shareable state. All processing runs client-side via the qrcode package. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
39
lib/qrcode/qrcodeService.ts
Normal file
39
lib/qrcode/qrcodeService.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import QRCode from 'qrcode';
|
||||
import type { ErrorCorrectionLevel } from '@/types/qrcode';
|
||||
|
||||
export async function generateSvg(
|
||||
text: string,
|
||||
errorCorrection: ErrorCorrectionLevel,
|
||||
foregroundColor: string,
|
||||
backgroundColor: string,
|
||||
margin: number,
|
||||
): Promise<string> {
|
||||
return QRCode.toString(text, {
|
||||
type: 'svg',
|
||||
errorCorrectionLevel: errorCorrection,
|
||||
color: {
|
||||
dark: foregroundColor,
|
||||
light: backgroundColor,
|
||||
},
|
||||
margin,
|
||||
});
|
||||
}
|
||||
|
||||
export async function generateDataUrl(
|
||||
text: string,
|
||||
errorCorrection: ErrorCorrectionLevel,
|
||||
foregroundColor: string,
|
||||
backgroundColor: string,
|
||||
margin: number,
|
||||
size: number,
|
||||
): Promise<string> {
|
||||
return QRCode.toDataURL(text, {
|
||||
errorCorrectionLevel: errorCorrection,
|
||||
color: {
|
||||
dark: foregroundColor,
|
||||
light: backgroundColor,
|
||||
},
|
||||
margin,
|
||||
width: size,
|
||||
});
|
||||
}
|
||||
85
lib/qrcode/urlSharing.ts
Normal file
85
lib/qrcode/urlSharing.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
'use client';
|
||||
|
||||
import type { ErrorCorrectionLevel } from '@/types/qrcode';
|
||||
|
||||
export interface QRShareableState {
|
||||
text?: string;
|
||||
errorCorrection?: ErrorCorrectionLevel;
|
||||
foregroundColor?: string;
|
||||
backgroundColor?: string;
|
||||
margin?: number;
|
||||
}
|
||||
|
||||
const DEFAULTS = {
|
||||
errorCorrection: 'M' as ErrorCorrectionLevel,
|
||||
foregroundColor: '#000000',
|
||||
backgroundColor: '#ffffff',
|
||||
margin: 4,
|
||||
};
|
||||
|
||||
export function decodeQRFromUrl(): QRShareableState | null {
|
||||
if (typeof window === 'undefined') return null;
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const text = params.get('text');
|
||||
const ec = params.get('ec') as ErrorCorrectionLevel | null;
|
||||
const fg = params.get('fg');
|
||||
const bg = params.get('bg');
|
||||
const margin = params.get('margin');
|
||||
|
||||
if (!text && !ec && !fg && !bg && !margin) return null;
|
||||
|
||||
return {
|
||||
text: text || undefined,
|
||||
errorCorrection: ec || undefined,
|
||||
foregroundColor: fg ? `#${fg}` : undefined,
|
||||
backgroundColor: bg ? `#${bg}` : undefined,
|
||||
margin: margin ? parseInt(margin, 10) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function updateQRUrl(
|
||||
text: string,
|
||||
errorCorrection: ErrorCorrectionLevel,
|
||||
foregroundColor: string,
|
||||
backgroundColor: string,
|
||||
margin: number,
|
||||
): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (text) params.set('text', text);
|
||||
if (errorCorrection !== DEFAULTS.errorCorrection) params.set('ec', errorCorrection);
|
||||
if (foregroundColor !== DEFAULTS.foregroundColor) params.set('fg', foregroundColor.replace('#', ''));
|
||||
if (backgroundColor !== DEFAULTS.backgroundColor) params.set('bg', backgroundColor.replace('#', ''));
|
||||
if (margin !== DEFAULTS.margin) params.set('margin', String(margin));
|
||||
|
||||
const query = params.toString();
|
||||
const newUrl = query
|
||||
? `${window.location.pathname}?${query}`
|
||||
: window.location.pathname;
|
||||
|
||||
window.history.replaceState({}, '', newUrl);
|
||||
}
|
||||
|
||||
export function getQRShareableUrl(
|
||||
text: string,
|
||||
errorCorrection: ErrorCorrectionLevel,
|
||||
foregroundColor: string,
|
||||
backgroundColor: string,
|
||||
margin: number,
|
||||
): string {
|
||||
if (typeof window === 'undefined') return '';
|
||||
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (text) params.set('text', text);
|
||||
if (errorCorrection !== DEFAULTS.errorCorrection) params.set('ec', errorCorrection);
|
||||
if (foregroundColor !== DEFAULTS.foregroundColor) params.set('fg', foregroundColor.replace('#', ''));
|
||||
if (backgroundColor !== DEFAULTS.backgroundColor) params.set('bg', backgroundColor.replace('#', ''));
|
||||
if (margin !== DEFAULTS.margin) params.set('margin', String(margin));
|
||||
|
||||
const query = params.toString();
|
||||
return `${window.location.origin}${window.location.pathname}${query ? `?${query}` : ''}`;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ColorIcon, UnitsIcon, ASCIIIcon, MediaIcon, FaviconIcon } from '@/components/AppIcons';
|
||||
import { ColorIcon, UnitsIcon, ASCIIIcon, MediaIcon, FaviconIcon, QRCodeIcon } from '@/components/AppIcons';
|
||||
|
||||
export interface Tool {
|
||||
/** Short display name (e.g. "Color") */
|
||||
@@ -15,10 +15,6 @@ export interface Tool {
|
||||
summary: string;
|
||||
/** Icon component */
|
||||
icon: React.ElementType;
|
||||
/** Tailwind gradient utility class for the landing card */
|
||||
gradient: string;
|
||||
/** Hex accent color for the landing card */
|
||||
accentColor: string;
|
||||
/** Badge labels for the landing card */
|
||||
badges: string[];
|
||||
}
|
||||
@@ -33,8 +29,6 @@ export const tools: Tool[] = [
|
||||
summary:
|
||||
'Modern color manipulation toolkit with palette generation, accessibility testing, and format conversion. Supports hex, RGB, HSL, Lab, and more.',
|
||||
icon: ColorIcon,
|
||||
gradient: 'gradient-indigo-purple',
|
||||
accentColor: '#a855f7',
|
||||
badges: ['Open Source', 'WCAG', 'Free'],
|
||||
},
|
||||
{
|
||||
@@ -46,8 +40,6 @@ export const tools: Tool[] = [
|
||||
summary:
|
||||
'Smart unit converter with 187 units across 23 categories. Real-time bidirectional conversion with fuzzy search.',
|
||||
icon: UnitsIcon,
|
||||
gradient: 'gradient-cyan-purple',
|
||||
accentColor: '#2dd4bf',
|
||||
badges: ['Open Source', 'Real-time', 'Free'],
|
||||
},
|
||||
{
|
||||
@@ -59,8 +51,6 @@ export const tools: Tool[] = [
|
||||
summary:
|
||||
'ASCII art text generator with 373 fonts. Create stunning text banners, terminal art, and retro designs with live preview and multiple export formats.',
|
||||
icon: ASCIIIcon,
|
||||
gradient: 'gradient-yellow-amber',
|
||||
accentColor: '#eab308',
|
||||
badges: ['Open Source', 'ASCII Art', 'Free'],
|
||||
},
|
||||
{
|
||||
@@ -72,8 +62,6 @@ export const tools: Tool[] = [
|
||||
summary:
|
||||
'Modern browser-based file converter powered by WebAssembly. Convert videos, images, and audio locally without server uploads. Privacy-first with no file size limits.',
|
||||
icon: MediaIcon,
|
||||
gradient: 'gradient-green-teal',
|
||||
accentColor: '#10b981',
|
||||
badges: ['Open Source', 'Converter', 'Free'],
|
||||
},
|
||||
{
|
||||
@@ -85,8 +73,17 @@ export const tools: Tool[] = [
|
||||
summary:
|
||||
'Generate a complete set of favicons for your website. Includes PWA manifest and HTML embed code. All processing happens locally in your browser.',
|
||||
icon: FaviconIcon,
|
||||
gradient: 'gradient-blue-cyan',
|
||||
accentColor: '#3b82f6',
|
||||
badges: ['Open Source', 'Generator', 'Free'],
|
||||
},
|
||||
{
|
||||
shortTitle: 'QR Code',
|
||||
title: 'QR Code Generator',
|
||||
navTitle: 'QR Code Generator',
|
||||
href: '/qrcode',
|
||||
description: 'Generate QR codes with custom colors, error correction, and multi-format export.',
|
||||
summary:
|
||||
'Generate QR codes with live preview, customizable colors, error correction levels, and export as PNG or SVG. All processing happens locally in your browser.',
|
||||
icon: QRCodeIcon,
|
||||
badges: ['Open Source', 'Generator', 'Free'],
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user