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:
2026-02-28 00:58:57 +01:00
parent 695ba434e2
commit f917891a31
13 changed files with 776 additions and 16 deletions

View 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
View 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}` : ''}`;
}

View File

@@ -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'],
},
];