+ Use this demo to test QR code generation, customization, and sharing features. The QR codes can be downloaded, printed, or shared via the modal dialog.
+
+
+
+
+
+ {/* Share Modal */}
+ setShowShareModal(false)}
+ shareUrl={shareUrl}
+ title="Share TeachLink Content"
+ description="Scan the QR code or copy the link to share"
+ qrSize={qrSize}
+ fgColor={fgColor}
+ bgColor={bgColor}
+ />
+
+
+ );
+}
diff --git a/src/components/QRCode.tsx b/src/components/QRCode.tsx
new file mode 100644
index 00000000..1d7e5c86
--- /dev/null
+++ b/src/components/QRCode.tsx
@@ -0,0 +1,91 @@
+'use client';
+
+import { useRef, forwardRef } from 'react';
+import QRCode from 'qrcode.react';
+import { QRCodeOptions, DEFAULT_QR_OPTIONS } from '@/utils/generate-qr';
+
+export interface QRCodeComponentProps {
+ /** URL or text to encode in QR code */
+ value: string;
+ /** Size of the QR code in pixels */
+ size?: number;
+ /** Error correction level */
+ level?: 'L' | 'M' | 'Q' | 'H';
+ /** Include margin/quiet zone */
+ includeMargin?: boolean;
+ /** Background color */
+ bgColor?: string;
+ /** Foreground/module color */
+ fgColor?: string;
+ /** Additional CSS class names */
+ className?: string;
+ /** Callback when QR code is rendered */
+ onRender?: (ref: HTMLCanvasElement | null) => void;
+}
+
+/**
+ * QRCode Component
+ * Renders a QR code for sharing URLs, text, or other data.
+ * Supports custom styling, sizing, and error correction levels.
+ *
+ * @example
+ * ```tsx
+ *
+ * ```
+ */
+export const QRCodeComponent = forwardRef(
+ (
+ {
+ value,
+ size = DEFAULT_QR_OPTIONS.size,
+ level = DEFAULT_QR_OPTIONS.level,
+ includeMargin = DEFAULT_QR_OPTIONS.includeMargin,
+ bgColor = DEFAULT_QR_OPTIONS.bgColor,
+ fgColor = DEFAULT_QR_OPTIONS.fgColor,
+ className = '',
+ onRender,
+ },
+ ref,
+ ) => {
+ const localRef = useRef(null);
+ const canvasRef = (ref || localRef) as React.RefObject;
+
+ // Handle render callback
+ const handleRender = () => {
+ if (onRender && canvasRef.current) {
+ onRender(canvasRef.current);
+ }
+ };
+
+ if (!value) {
+ return (
+
+
No value provided
+
+ );
+ }
+
+ return (
+
+
+
+ );
+ },
+);
+
+QRCodeComponent.displayName = 'QRCodeComponent';
+
+export default QRCodeComponent;
diff --git a/src/components/ShareModal.tsx b/src/components/ShareModal.tsx
new file mode 100644
index 00000000..b91f5bbe
--- /dev/null
+++ b/src/components/ShareModal.tsx
@@ -0,0 +1,213 @@
+'use client';
+
+import { useRef, useState, useCallback } from 'react';
+import { Download, Printer, Copy, X } from 'lucide-react';
+import { Modal } from '@/components/ui/Modal';
+import QRCodeComponent from '@/components/QRCode';
+import { downloadQRCode, printQRCode, copyQRCodeToClipboard } from '@/utils/generate-qr';
+import toast from 'react-hot-toast';
+
+export interface ShareModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+ /** URL or data to share via QR code */
+ shareUrl: string;
+ /** Title for the modal */
+ title?: string;
+ /** Description of what's being shared */
+ description?: string;
+ /** Size of the QR code */
+ qrSize?: number;
+ /** Custom styling for QR code */
+ fgColor?: string;
+ bgColor?: string;
+}
+
+/**
+ * ShareModal Component
+ * Displays a QR code with options to download, print, or copy.
+ * Ideal for sharing post links, profiles, or other resources.
+ *
+ * @example
+ * ```tsx
+ * const [showShare, setShowShare] = useState(false);
+ *
+ * return (
+ * <>
+ *
+ * setShowShare(false)}
+ * shareUrl="https://teachlink.com/post/123"
+ * title="Share this post"
+ * description="Scan to view the post"
+ * />
+ * >
+ * );
+ * ```
+ */
+export function ShareModal({
+ isOpen,
+ onClose,
+ shareUrl,
+ title = 'Share this content',
+ description = 'Scan the QR code to open',
+ qrSize = 256,
+ fgColor = '#000000',
+ bgColor = '#ffffff',
+}: ShareModalProps) {
+ const qrRef = useRef(null);
+ const [isLoading, setIsLoading] = useState(false);
+
+ const handleDownload = useCallback(async () => {
+ if (!qrRef.current) return;
+
+ try {
+ setIsLoading(true);
+ await downloadQRCode(qrRef.current, 'teachlink-qrcode.png');
+ toast.success('QR code downloaded successfully');
+ } catch (error) {
+ toast.error('Failed to download QR code');
+ console.error(error);
+ } finally {
+ setIsLoading(false);
+ }
+ }, []);
+
+ const handlePrint = useCallback(async () => {
+ if (!qrRef.current) return;
+
+ try {
+ setIsLoading(true);
+ await printQRCode(qrRef.current);
+ toast.success('Print dialog opened');
+ } catch (error) {
+ toast.error('Failed to open print dialog');
+ console.error(error);
+ } finally {
+ setIsLoading(false);
+ }
+ }, []);
+
+ const handleCopy = useCallback(async () => {
+ if (!qrRef.current) return;
+
+ try {
+ setIsLoading(true);
+ await copyQRCodeToClipboard(qrRef.current);
+ toast.success('QR code copied to clipboard');
+ } catch (error) {
+ toast.error('Failed to copy QR code');
+ console.error(error);
+ } finally {
+ setIsLoading(false);
+ }
+ }, []);
+
+ const handleCopyUrl = useCallback(async () => {
+ try {
+ await navigator.clipboard.writeText(shareUrl);
+ toast.success('URL copied to clipboard');
+ } catch (error) {
+ toast.error('Failed to copy URL');
+ console.error(error);
+ }
+ }, [shareUrl]);
+
+ if (!isOpen) return null;
+
+ return (
+
+
+ {/* Description */}
+ {description &&
{description}
}
+
+ {/* QR Code Display */}
+
+
+
+
+ {/* Action Buttons */}
+
+
+
+
+
+
+
+
+ {/* URL Copy Section */}
+
+
+
+
+
+
+
+
+ {/* Close Button */}
+
+
+
+
+
+ );
+}
+
+export default ShareModal;
diff --git a/src/components/index.ts b/src/components/index.ts
index a2accd40..4f8c8f86 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -9,3 +9,5 @@ export * from './ui/Toast';
export * from './ui/EmptyState';
export * from './shared/EnvGuard';
export * from './errors/ErrorBoundarySystem';
+export { QRCodeComponent } from './QRCode';
+export { ShareModal } from './ShareModal';
diff --git a/src/utils/__tests__/generate-qr.test.ts b/src/utils/__tests__/generate-qr.test.ts
new file mode 100644
index 00000000..e0f89a69
--- /dev/null
+++ b/src/utils/__tests__/generate-qr.test.ts
@@ -0,0 +1,104 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+import {
+ isValidQRUrl,
+ generateQRCodeData,
+ downloadQRCode,
+ printQRCode,
+ copyQRCodeToClipboard,
+ generateQRCodeUrl,
+ DEFAULT_QR_OPTIONS,
+} from '../generate-qr';
+
+describe('generate-qr utilities', () => {
+ describe('isValidQRUrl', () => {
+ it('should validate valid URLs', () => {
+ expect(isValidQRUrl('https://teachlink.com')).toBe(true);
+ expect(isValidQRUrl('http://example.com')).toBe(true);
+ expect(isValidQRUrl('/relative/path')).toBe(true);
+ });
+
+ it('should reject invalid URLs', () => {
+ expect(isValidQRUrl('')).toBe(false);
+ expect(isValidQRUrl(null as unknown as string)).toBe(false);
+ expect(isValidQRUrl(undefined as unknown as string)).toBe(false);
+ });
+ });
+
+ describe('generateQRCodeData', () => {
+ it('should generate QR code data with defaults', () => {
+ const data = generateQRCodeData('https://teachlink.com');
+ expect(data.text).toBe('https://teachlink.com');
+ expect(data.options).toEqual(DEFAULT_QR_OPTIONS);
+ });
+
+ it('should merge custom options with defaults', () => {
+ const customOptions = { size: 512, fgColor: '#3b82f6' };
+ const data = generateQRCodeData('https://teachlink.com', customOptions);
+ expect(data.options.size).toBe(512);
+ expect(data.options.fgColor).toBe('#3b82f6');
+ expect(data.options.level).toBe(DEFAULT_QR_OPTIONS.level);
+ });
+
+ it('should throw error for invalid URLs', () => {
+ expect(() => generateQRCodeData('')).toThrow();
+ });
+ });
+
+ describe('downloadQRCode', () => {
+ let mockCanvas: HTMLCanvasElement;
+
+ beforeEach(() => {
+ mockCanvas = document.createElement('canvas');
+ mockCanvas.toDataURL = vi.fn(() => 'data:image/png;base64,test');
+
+ // Mock DOM methods
+ vi.spyOn(document, 'createElement').mockImplementation((tag: string) => {
+ if (tag === 'a') {
+ return {
+ href: '',
+ download: '',
+ click: vi.fn(),
+ style: {},
+ } as unknown as HTMLElement;
+ }
+ return document.createElement(tag);
+ });
+
+ vi.spyOn(document.body, 'appendChild').mockImplementation(() => mockCanvas);
+ vi.spyOn(document.body, 'removeChild').mockImplementation(() => mockCanvas);
+ });
+
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ it('should download QR code', async () => {
+ await expect(downloadQRCode(mockCanvas)).resolves.not.toThrow();
+ });
+
+ it('should use custom filename', async () => {
+ const link = document.createElement('a');
+ vi.spyOn(document, 'createElement').mockReturnValueOnce(link);
+ await downloadQRCode(mockCanvas, 'custom-qr.png');
+ expect(link.download).toBe('custom-qr.png');
+ });
+ });
+
+ describe('generateQRCodeUrl', () => {
+ it('should generate valid QR code URL', () => {
+ const url = generateQRCodeUrl('https://teachlink.com/post/123');
+ expect(url).toContain('https://api.qrserver.com');
+ expect(url).toContain('data=https');
+ });
+
+ it('should encode special characters', () => {
+ const url = generateQRCodeUrl('https://teachlink.com/path?param=value');
+ expect(url).toContain('%3F');
+ expect(url).toContain('%3D');
+ });
+
+ it('should throw error for invalid URLs', () => {
+ expect(() => generateQRCodeUrl('')).toThrow();
+ });
+ });
+});
diff --git a/src/utils/generate-qr.ts b/src/utils/generate-qr.ts
new file mode 100644
index 00000000..b2eaecdb
--- /dev/null
+++ b/src/utils/generate-qr.ts
@@ -0,0 +1,141 @@
+/**
+ * QR Code Generation Utilities
+ * Provides functions for generating and styling QR codes, with support for custom colors and sizes.
+ */
+
+export interface QRCodeOptions {
+ /** Size of the QR code in pixels */
+ size?: number;
+ /** Error correction level: 'L', 'M', 'Q', 'H' */
+ level?: 'L' | 'M' | 'Q' | 'H';
+ /** Include margin/quiet zone around QR code */
+ includeMargin?: boolean;
+ /** Background color (hex or CSS color) */
+ bgColor?: string;
+ /** Foreground/module color (hex or CSS color) */
+ fgColor?: string;
+}
+
+/**
+ * Default QR code configuration
+ */
+export const DEFAULT_QR_OPTIONS: QRCodeOptions = {
+ size: 256,
+ level: 'H',
+ includeMargin: true,
+ bgColor: '#ffffff',
+ fgColor: '#000000',
+};
+
+/**
+ * Validates a URL for QR code generation
+ * @param url - The URL to validate
+ * @returns boolean indicating if URL is valid
+ */
+export function isValidQRUrl(url: string): boolean {
+ if (!url || typeof url !== 'string') return false;
+
+ try {
+ // Check if it's a valid URL or a path
+ new URL(url, 'http://example.com');
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+/**
+ * Generates a shareable QR code URL
+ * This can be used to generate QR codes from external services if needed
+ * @param text - Text or URL to encode
+ * @param options - QR code options
+ * @returns Generated QR code data
+ */
+export function generateQRCodeData(text: string, options: QRCodeOptions = DEFAULT_QR_OPTIONS) {
+ if (!isValidQRUrl(text)) {
+ throw new Error('Invalid URL or text for QR code generation');
+ }
+
+ return {
+ text,
+ options: {
+ ...DEFAULT_QR_OPTIONS,
+ ...options,
+ },
+ };
+}
+
+/**
+ * Downloads a QR code as an image
+ * @param canvas - Canvas element containing the QR code
+ * @param filename - Name of the downloaded file
+ */
+export async function downloadQRCode(canvas: HTMLCanvasElement, filename: string = 'qrcode.png') {
+ try {
+ const url = canvas.toDataURL('image/png');
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = filename;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ } catch (error) {
+ console.error('Failed to download QR code:', error);
+ throw new Error('Failed to download QR code');
+ }
+}
+
+/**
+ * Prints a QR code
+ * @param canvas - Canvas element containing the QR code
+ */
+export async function printQRCode(canvas: HTMLCanvasElement) {
+ try {
+ const url = canvas.toDataURL('image/png');
+ const printWindow = window.open();
+ if (!printWindow) {
+ throw new Error('Failed to open print window');
+ }
+ printWindow.document.write(``);
+ printWindow.document.close();
+ printWindow.print();
+ } catch (error) {
+ console.error('Failed to print QR code:', error);
+ throw new Error('Failed to print QR code');
+ }
+}
+
+/**
+ * Copies QR code data URL to clipboard
+ * @param canvas - Canvas element containing the QR code
+ */
+export async function copyQRCodeToClipboard(canvas: HTMLCanvasElement) {
+ try {
+ const url = canvas.toDataURL('image/png');
+ const blob = await fetch(url).then(res => res.blob());
+ await navigator.clipboard.write([
+ new ClipboardItem({
+ 'image/png': blob,
+ }),
+ ]);
+ } catch (error) {
+ console.error('Failed to copy QR code to clipboard:', error);
+ throw new Error('Failed to copy QR code to clipboard');
+ }
+}
+
+/**
+ * Generates a QR code data URL for sharing
+ * @param text - Text or URL to encode
+ * @returns Data URL that can be used for sharing
+ */
+export function generateQRCodeUrl(text: string): string {
+ if (!isValidQRUrl(text)) {
+ throw new Error('Invalid URL or text for QR code generation');
+ }
+
+ // Using a QR code API service as fallback
+ // You can replace with your preferred QR code generation endpoint
+ const encodedText = encodeURIComponent(text);
+ return `https://api.qrserver.com/v1/create-qr-code/?size=256x256&data=${encodedText}`;
+}