From 446a4cd22cb1dfa7c6224c36f3eef1cee3b33b88 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 4 Aug 2025 10:34:12 +0000 Subject: [PATCH 1/6] Initial plan From 2dd9284a8ab3012a905b0848ea04dd9d27fa421f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 4 Aug 2025 10:57:12 +0000 Subject: [PATCH 2/6] Implement QR Code Generator with customization features and logo support Co-authored-by: peckz <18050177+peckz@users.noreply.github.com> --- README.md | 1 + components/seo/QrCodeSEO.tsx | 242 ++++++++ .../utils/qr-code-generator.utils.test.ts | 242 ++++++++ components/utils/qr-code-generator.utils.ts | 242 ++++++++ components/utils/tools-list.ts | 6 + package-lock.json | 27 + package.json | 2 + pages/utilities/qr-code-generator.tsx | 523 ++++++++++++++++++ 8 files changed, 1285 insertions(+) create mode 100644 components/seo/QrCodeSEO.tsx create mode 100644 components/utils/qr-code-generator.utils.test.ts create mode 100644 components/utils/qr-code-generator.utils.ts create mode 100644 pages/utilities/qr-code-generator.tsx diff --git a/README.md b/README.md index 3698a1f..99ae078 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ Here is the list of all utilities: - [Lorem Ipsum Generator](https://jam.dev/utilities/lorem-ipsum-generator) - [WebP Converter](https://jam.dev/utilities/webp-converter) - [SQL Minifer](https://jam.dev/utilities/sql-minifier) +- [QR Code Generator](https://jam.dev/utilities/qr-code-generator) ### Built With diff --git a/components/seo/QrCodeSEO.tsx b/components/seo/QrCodeSEO.tsx new file mode 100644 index 0000000..8531aa1 --- /dev/null +++ b/components/seo/QrCodeSEO.tsx @@ -0,0 +1,242 @@ +import CodeExample from "../CodeExample"; +import GetJamForFree from "./GetJamForFree"; + +export default function QrCodeSEO() { + return ( +
+
+

+ Create customizable QR codes with our free online generator. Add your logo, + choose different styles (dots, squares, rounded corners), and customize colors. + Perfect for marketing materials, business cards, and digital campaigns. + No sign-up required — generate and download instantly. Made with 💜 by the + developers building Jam. +

+
+ +
+

How to Use the QR Code Generator:

+

+ Generate professional QR codes in seconds. Enter your text, URL, or data, + customize the appearance with different styles and colors, and optionally + add your logo to the center. Perfect for branding and marketing campaigns. +

+ +

Use Cases:

+ +
+ +
+

QR Code Customization Options:

+

+ Our QR code generator offers extensive customization options to match your + brand and style preferences: +

+ +
+ +
+

Understanding QR Code Error Correction:

+

+ QR codes include built-in error correction that allows them to be read even + when partially damaged or obscured. This feature is particularly important + when adding logos: +

+ +
+ +
+

QR Code Best Practices:

+

+ Follow these guidelines to create effective and scannable QR codes: +

+ +
+ +
+

Working with QR Codes in JavaScript:

+

+ You can generate and work with QR codes programmatically using JavaScript + libraries. Here's an example using a popular QR code library: +

+ {qrCodeExample} +

+ This code demonstrates basic QR code generation. For advanced features like + custom styling and logos, consider using specialized libraries like + qr-code-styling or similar tools. +

+
+ +
+

Meet Jam: The Ultimate Tool for Debugging Web Apps

+

+ While this tool helps you create professional QR codes quickly,{" "} + + Jam + {" "} + streamlines your entire development workflow. +

+

+ Jam is{" "} + + the browser extension + {" "} + helping over 140,000 users debug faster. It captures console logs, + network requests, and more with just one click. Perfect for testing + QR code landing pages and debugging web applications. +

+
+ +
+ +
+ +
+

FAQs:

+ +
+
+ ); +} + +const qrCodeExample = `// Basic QR code generation example +import QRCode from 'qrcode'; + +// Generate QR code as data URL +const generateQRCode = async (text) => { + try { + const dataURL = await QRCode.toDataURL(text, { + width: 300, + margin: 2, + color: { + dark: '#000000', + light: '#FFFFFF' + } + }); + return dataURL; + } catch (error) { + console.error('Error generating QR code:', error); + } +}; + +// Usage +generateQRCode('https://example.com') + .then(dataURL => { + // Use the data URL to display or download the QR code + console.log('QR Code generated:', dataURL); + });`; \ No newline at end of file diff --git a/components/utils/qr-code-generator.utils.test.ts b/components/utils/qr-code-generator.utils.test.ts new file mode 100644 index 0000000..0c792e3 --- /dev/null +++ b/components/utils/qr-code-generator.utils.test.ts @@ -0,0 +1,242 @@ +import { + QRCodeGenerator, + createQRCode, + validateQRCodeText, + validateImageFile, + imageToBase64, + DEFAULT_QR_OPTIONS, + getErrorCorrectionLevels, + getDotsTypeOptions, + getCornerSquareTypeOptions, + getCornerDotTypeOptions, +} from "./qr-code-generator.utils"; + +// Mock the QRCodeStyling library +jest.mock("qr-code-styling", () => { + return jest.fn().mockImplementation(() => ({ + update: jest.fn(), + append: jest.fn(), + download: jest.fn(), + getRawData: jest.fn().mockResolvedValue(new Blob(["test"], { type: "image/png" })), + _options: { + qrOptions: { + errorCorrectionLevel: "M", + }, + }, + })); +}); + +describe("QR Code Generator Utils", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe("validateQRCodeText", () => { + it("should return true for valid text", () => { + expect(validateQRCodeText("Hello World")).toBe(true); + expect(validateQRCodeText("https://example.com")).toBe(true); + expect(validateQRCodeText("12345")).toBe(true); + }); + + it("should return false for empty or whitespace-only text", () => { + expect(validateQRCodeText("")).toBe(false); + expect(validateQRCodeText(" ")).toBe(false); + expect(validateQRCodeText("\t\n")).toBe(false); + }); + }); + + describe("validateImageFile", () => { + it("should return true for valid image files", () => { + const validFile1 = new File(["test"], "test.png", { type: "image/png" }); + const validFile2 = new File(["test"], "test.jpg", { type: "image/jpeg" }); + const validFile3 = new File(["test"], "test.gif", { type: "image/gif" }); + + expect(validateImageFile(validFile1)).toBe(true); + expect(validateImageFile(validFile2)).toBe(true); + expect(validateImageFile(validFile3)).toBe(true); + }); + + it("should return false for invalid file types", () => { + const invalidFile = new File(["test"], "test.txt", { type: "text/plain" }); + expect(validateImageFile(invalidFile)).toBe(false); + }); + + it("should return false for files that are too large", () => { + const largeFile = new File([new ArrayBuffer(6 * 1024 * 1024)], "large.png", { + type: "image/png" + }); + expect(validateImageFile(largeFile)).toBe(false); + }); + }); + + describe("imageToBase64", () => { + it("should convert file to base64", async () => { + const file = new File(["test content"], "test.png", { type: "image/png" }); + + // Mock FileReader + const mockFileReader = { + readAsDataURL: jest.fn(), + result: "data:image/png;base64,dGVzdCBjb250ZW50", + onload: null as unknown, + onerror: null as unknown, + }; + + (global as unknown).FileReader = jest.fn(() => mockFileReader); + + const promise = imageToBase64(file); + + // Trigger the onload event + mockFileReader.onload(); + + const result = await promise; + expect(result).toBe("data:image/png;base64,dGVzdCBjb250ZW50"); + expect(mockFileReader.readAsDataURL).toHaveBeenCalledWith(file); + }); + }); + + describe("createQRCode", () => { + it("should create a QRCodeGenerator instance with default options", () => { + const qrCode = createQRCode(); + expect(qrCode).toBeInstanceOf(QRCodeGenerator); + }); + + it("should create a QRCodeGenerator instance with custom options", () => { + const options = { + text: "Test QR Code", + width: 400, + height: 400, + }; + const qrCode = createQRCode(options); + expect(qrCode).toBeInstanceOf(QRCodeGenerator); + }); + }); + + describe("QRCodeGenerator", () => { + let qrGenerator: QRCodeGenerator; + + beforeEach(() => { + qrGenerator = new QRCodeGenerator(); + }); + + it("should initialize with default options", () => { + expect(qrGenerator).toBeInstanceOf(QRCodeGenerator); + }); + + it("should update QR code options", () => { + const updateSpy = jest.spyOn(qrGenerator["qrCode"], "update"); + + qrGenerator.update({ + text: "Updated text", + width: 500, + }); + + expect(updateSpy).toHaveBeenCalledWith({ + data: "Updated text", + width: 500, + }); + }); + + it("should append to DOM element", () => { + const element = document.createElement("div"); + const appendSpy = jest.spyOn(qrGenerator["qrCode"], "append"); + + qrGenerator.append(element); + + expect(appendSpy).toHaveBeenCalledWith(element); + }); + + it("should download QR code", async () => { + const downloadSpy = jest.spyOn(qrGenerator["qrCode"], "download"); + downloadSpy.mockResolvedValue(undefined); + + await qrGenerator.download("png"); + + expect(downloadSpy).toHaveBeenCalledWith({ + name: "qr-code", + extension: "png", + }); + }); + + it("should get data URL for image formats", async () => { + const blob = new Blob(["test"], { type: "image/png" }); + jest.spyOn(qrGenerator["qrCode"], "getRawData").mockResolvedValue(blob); + + // Mock FileReader for data URL conversion + const mockFileReader = { + readAsDataURL: jest.fn(), + result: "data:image/png;base64,dGVzdA==", + onload: null as unknown, + onerror: null as unknown, + }; + (global as unknown).FileReader = jest.fn(() => mockFileReader); + + const promise = qrGenerator.getDataURL("png"); + + // Trigger the onload event + setTimeout(() => mockFileReader.onload(), 0); + + const result = await promise; + expect(result).toBe("data:image/png;base64,dGVzdA=="); + }); + + it("should get data URL for SVG format", async () => { + const svgString = "test"; + jest.spyOn(qrGenerator["qrCode"], "getRawData").mockResolvedValue(svgString); + + const result = await qrGenerator.getDataURL("svg"); + expect(result).toBe(svgString); + }); + }); + + describe("Option getters", () => { + it("should return error correction levels", () => { + const levels = getErrorCorrectionLevels(); + expect(levels).toHaveLength(4); + expect(levels[0]).toEqual({ + value: "L", + label: "Low (~7%)", + description: "Smallest QR code" + }); + }); + + it("should return dots type options", () => { + const types = getDotsTypeOptions(); + expect(types).toHaveLength(6); + expect(types[0]).toEqual({ + value: "square", + label: "Square" + }); + }); + + it("should return corner square type options", () => { + const types = getCornerSquareTypeOptions(); + expect(types).toHaveLength(3); + expect(types[0]).toEqual({ + value: "square", + label: "Square" + }); + }); + + it("should return corner dot type options", () => { + const types = getCornerDotTypeOptions(); + expect(types).toHaveLength(2); + expect(types[0]).toEqual({ + value: "square", + label: "Square" + }); + }); + }); + + describe("DEFAULT_QR_OPTIONS", () => { + it("should have correct default values", () => { + expect(DEFAULT_QR_OPTIONS.text).toBe(""); + expect(DEFAULT_QR_OPTIONS.width).toBe(300); + expect(DEFAULT_QR_OPTIONS.height).toBe(300); + expect(DEFAULT_QR_OPTIONS.format).toBe("png"); + expect(DEFAULT_QR_OPTIONS.errorCorrectionLevel).toBe("M"); + expect(DEFAULT_QR_OPTIONS.dotsOptions.color).toBe("#000000"); + expect(DEFAULT_QR_OPTIONS.dotsOptions.type).toBe("square"); + expect(DEFAULT_QR_OPTIONS.backgroundOptions.color).toBe("#ffffff"); + }); + }); +}); \ No newline at end of file diff --git a/components/utils/qr-code-generator.utils.ts b/components/utils/qr-code-generator.utils.ts new file mode 100644 index 0000000..fee6cf6 --- /dev/null +++ b/components/utils/qr-code-generator.utils.ts @@ -0,0 +1,242 @@ +import QRCodeStyling from "qr-code-styling"; + +export type QRCodeFormat = "png" | "svg" | "jpeg" | "webp"; + +export type QRCodeCornerSquareType = "square" | "extra-rounded" | "dot"; +export type QRCodeCornerDotType = "square" | "dot"; +export type QRCodeDotsType = "square" | "rounded" | "dots" | "classy" | "classy-rounded" | "extra-rounded"; + +export type QRCodeErrorCorrectionLevel = "L" | "M" | "Q" | "H"; + +export interface QRCodeOptions { + text: string; + width: number; + height: number; + format: QRCodeFormat; + errorCorrectionLevel: QRCodeErrorCorrectionLevel; + dotsOptions: { + color: string; + type: QRCodeDotsType; + }; + backgroundOptions: { + color: string; + }; + cornersSquareOptions?: { + color: string; + type: QRCodeCornerSquareType; + }; + cornersDotOptions?: { + color: string; + type: QRCodeCornerDotType; + }; + image?: string; // Base64 string or URL + imageOptions?: { + hideBackgroundDots: boolean; + imageSize: number; + margin: number; + crossOrigin: string; + }; +} + +export const DEFAULT_QR_OPTIONS: QRCodeOptions = { + text: "", + width: 300, + height: 300, + format: "png", + errorCorrectionLevel: "M", + dotsOptions: { + color: "#000000", + type: "square", + }, + backgroundOptions: { + color: "#ffffff", + }, + cornersSquareOptions: { + color: "#000000", + type: "square", + }, + cornersDotOptions: { + color: "#000000", + type: "square", + }, + imageOptions: { + hideBackgroundDots: true, + imageSize: 0.4, + margin: 8, + crossOrigin: "anonymous", + }, +}; + +export class QRCodeGenerator { + private qrCode: QRCodeStyling; + + constructor(options: Partial = {}) { + const fullOptions = { ...DEFAULT_QR_OPTIONS, ...options }; + + this.qrCode = new QRCodeStyling({ + width: fullOptions.width, + height: fullOptions.height, + type: "svg", + data: fullOptions.text, + image: fullOptions.image, + dotsOptions: fullOptions.dotsOptions, + backgroundOptions: fullOptions.backgroundOptions, + cornersSquareOptions: fullOptions.cornersSquareOptions, + cornersDotOptions: fullOptions.cornersDotOptions, + imageOptions: fullOptions.imageOptions, + qrOptions: { + typeNumber: 0, + mode: "Byte", + errorCorrectionLevel: fullOptions.errorCorrectionLevel, + }, + }); + } + + update(options: Partial): void { + const updateOptions: Record = {}; + + if (options.text !== undefined) { + updateOptions.data = options.text; + } + if (options.width !== undefined) { + updateOptions.width = options.width; + } + if (options.height !== undefined) { + updateOptions.height = options.height; + } + if (options.image !== undefined) { + updateOptions.image = options.image; + } + if (options.dotsOptions) { + updateOptions.dotsOptions = options.dotsOptions; + } + if (options.backgroundOptions) { + updateOptions.backgroundOptions = options.backgroundOptions; + } + if (options.cornersSquareOptions) { + updateOptions.cornersSquareOptions = options.cornersSquareOptions; + } + if (options.cornersDotOptions) { + updateOptions.cornersDotOptions = options.cornersDotOptions; + } + if (options.imageOptions) { + updateOptions.imageOptions = options.imageOptions; + } + if (options.errorCorrectionLevel) { + updateOptions.qrOptions = { + ...this.qrCode._options.qrOptions, + errorCorrectionLevel: options.errorCorrectionLevel, + }; + } + + this.qrCode.update(updateOptions); + } + + append(element: HTMLElement): void { + this.qrCode.append(element); + } + + async download(format: QRCodeFormat = "png"): Promise { + return this.qrCode.download({ + name: `qr-code`, + extension: format, + }); + } + + async getRawData(format: QRCodeFormat = "png"): Promise { + const rawData = await this.qrCode.getRawData(format); + if (rawData === null) { + throw new Error("Failed to generate QR code data"); + } + + // Handle Buffer type from Node.js environments + if (typeof Buffer !== 'undefined' && rawData instanceof Buffer) { + return new Blob([rawData]); + } + + return rawData as Blob | string; + } + + async getDataURL(format: QRCodeFormat = "png"): Promise { + const rawData = await this.getRawData(format); + + if (typeof rawData === "string") { + return rawData; // SVG returns string + } + + // For other formats, convert Blob to data URL + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result as string); + reader.onerror = reject; + reader.readAsDataURL(rawData); + }); + } +} + +export const createQRCode = (options: Partial = {}): QRCodeGenerator => { + return new QRCodeGenerator(options); +}; + +export const validateQRCodeText = (text: string): boolean => { + return text.trim().length > 0; +}; + +export const getQRCodeSizeInBytes = async ( + qrCode: QRCodeGenerator, + format: QRCodeFormat = "png" +): Promise => { + const rawData = await qrCode.getRawData(format); + + if (typeof rawData === "string") { + return new Blob([rawData]).size; + } + + return rawData.size; +}; + +export const imageToBase64 = (file: File): Promise => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => { + const result = reader.result as string; + resolve(result); + }; + reader.onerror = reject; + reader.readAsDataURL(file); + }); +}; + +export const validateImageFile = (file: File): boolean => { + const validTypes = ["image/jpeg", "image/jpg", "image/png", "image/gif", "image/webp"]; + const maxSize = 5 * 1024 * 1024; // 5MB + + return validTypes.includes(file.type) && file.size <= maxSize; +}; + +export const getErrorCorrectionLevels = () => [ + { value: "L" as const, label: "Low (~7%)", description: "Smallest QR code" }, + { value: "M" as const, label: "Medium (~15%)", description: "Balanced" }, + { value: "Q" as const, label: "Quartile (~25%)", description: "Good for noisy environments" }, + { value: "H" as const, label: "High (~30%)", description: "Best for logos and customization" }, +]; + +export const getDotsTypeOptions = () => [ + { value: "square" as const, label: "Square" }, + { value: "rounded" as const, label: "Rounded" }, + { value: "dots" as const, label: "Dots" }, + { value: "classy" as const, label: "Classy" }, + { value: "classy-rounded" as const, label: "Classy Rounded" }, + { value: "extra-rounded" as const, label: "Extra Rounded" }, +]; + +export const getCornerSquareTypeOptions = () => [ + { value: "square" as const, label: "Square" }, + { value: "extra-rounded" as const, label: "Extra Rounded" }, + { value: "dot" as const, label: "Dot" }, +]; + +export const getCornerDotTypeOptions = () => [ + { value: "square" as const, label: "Square" }, + { value: "dot" as const, label: "Dot" }, +]; \ No newline at end of file diff --git a/components/utils/tools-list.ts b/components/utils/tools-list.ts index ef41923..80e43db 100644 --- a/components/utils/tools-list.ts +++ b/components/utils/tools-list.ts @@ -149,4 +149,10 @@ export const tools = [ "Minify SQL by removing comments, extra spaces, and formatting for cleaner, optimized queries.", link: "/utilities/sql-minifier", }, + { + title: "QR Code Generator", + description: + "Create customizable QR codes with logos, different styles (dots, squares, rounded), and custom colors. Perfect for marketing materials and business cards.", + link: "/utilities/qr-code-generator", + }, ]; diff --git a/package-lock.json b/package-lock.json index 91b3505..b2af11e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "next": "14.2.4", "next-themes": "^0.3.0", "papaparse": "^5.4.1", + "qr-code-styling": "^1.9.2", "react": "^18", "react-dom": "^18", "react-syntax-highlighter": "^15.5.0", @@ -40,6 +41,7 @@ "@types/js-yaml": "^4.0.9", "@types/node": "^20", "@types/papaparse": "^5.3.14", + "@types/qrcode-generator": "^0.0.16", "@types/react": "^18", "@types/react-dom": "^18", "@types/react-syntax-highlighter": "^15.5.13", @@ -3508,6 +3510,13 @@ "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", "devOptional": true }, + "node_modules/@types/qrcode-generator": { + "version": "0.0.16", + "resolved": "https://registry.npmjs.org/@types/qrcode-generator/-/qrcode-generator-0.0.16.tgz", + "integrity": "sha512-i+wPpeHH64qJHUIz51+tPq5FRkgMxE9/uhDE5eSlj3qujIjHm3U1wHfy8nMD/m1VKaXHO/uDjHamPVz2Fr4IPA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/react": { "version": "18.3.3", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", @@ -10336,6 +10345,24 @@ } ] }, + "node_modules/qr-code-styling": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/qr-code-styling/-/qr-code-styling-1.9.2.tgz", + "integrity": "sha512-RgJaZJ1/RrXJ6N0j7a+pdw3zMBmzZU4VN2dtAZf8ZggCfRB5stEQ3IoDNGaNhYY3nnZKYlYSLl5YkfWN5dPutg==", + "license": "MIT", + "dependencies": { + "qrcode-generator": "^1.4.4" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/qrcode-generator": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/qrcode-generator/-/qrcode-generator-1.5.2.tgz", + "integrity": "sha512-pItrW0Z9HnDBnFmgiNrY1uxRdri32Uh9EjNYLPVC2zZ3ZRIIEqBoDgm4DkvDwNNDHTK7FNkmr8zAa77BYc9xNw==", + "license": "MIT" + }, "node_modules/querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", diff --git a/package.json b/package.json index 0f71bea..fbecf59 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "next": "14.2.4", "next-themes": "^0.3.0", "papaparse": "^5.4.1", + "qr-code-styling": "^1.9.2", "react": "^18", "react-dom": "^18", "react-syntax-highlighter": "^15.5.0", @@ -44,6 +45,7 @@ "@types/js-yaml": "^4.0.9", "@types/node": "^20", "@types/papaparse": "^5.3.14", + "@types/qrcode-generator": "^0.0.16", "@types/react": "^18", "@types/react-dom": "^18", "@types/react-syntax-highlighter": "^15.5.13", diff --git a/pages/utilities/qr-code-generator.tsx b/pages/utilities/qr-code-generator.tsx new file mode 100644 index 0000000..97b5cd6 --- /dev/null +++ b/pages/utilities/qr-code-generator.tsx @@ -0,0 +1,523 @@ +import { useCallback, useState, useEffect, useRef } from "react"; +import PageHeader from "@/components/PageHeader"; +import { Card } from "@/components/ds/CardComponent"; +import { Button } from "@/components/ds/ButtonComponent"; +import { Label } from "@/components/ds/LabelComponent"; +import Header from "@/components/Header"; +import { CMDK } from "@/components/CMDK"; +import CallToActionGrid from "@/components/CallToActionGrid"; +import Meta from "@/components/Meta"; +import { Input } from "@/components/ds/InputComponent"; +import { Textarea } from "@/components/ds/TextareaComponent"; +import { Combobox } from "@/components/ds/ComboboxComponent"; +import { ImageUploadComponent } from "@/components/ds/ImageUploadComponent"; +import { DownloadIcon, RefreshCwIcon } from "lucide-react"; +import GitHubContribution from "@/components/GitHubContribution"; +import QrCodeSEO from "@/components/seo/QrCodeSEO"; +import { + QRCodeGenerator, + QRCodeFormat, + QRCodeErrorCorrectionLevel, + QRCodeDotsType, + QRCodeCornerSquareType, + QRCodeCornerDotType, + createQRCode, + validateQRCodeText, + validateImageFile, + imageToBase64, + getErrorCorrectionLevels, + getDotsTypeOptions, + getCornerSquareTypeOptions, + getCornerDotTypeOptions, +} from "@/components/utils/qr-code-generator.utils"; + +const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB + +interface FormatOption { + value: QRCodeFormat; + label: string; +} + +const formatOptions: FormatOption[] = [ + { value: "png", label: "PNG" }, + { value: "svg", label: "SVG" }, + { value: "jpeg", label: "JPEG" }, + { value: "webp", label: "WebP" }, +]; + +export default function QrCodeGenerator() { + const [text, setText] = useState(""); + const [qrCodeInstance, setQrCodeInstance] = useState(null); + const [format, setFormat] = useState("png"); + const [size, setSize] = useState(300); + const [errorCorrectionLevel, setErrorCorrectionLevel] = useState("M"); + const [dotsType, setDotsType] = useState("square"); + const [dotsColor, setDotsColor] = useState("#000000"); + const [backgroundColor, setBackgroundColor] = useState("#ffffff"); + const [cornerSquareType, setCornerSquareType] = useState("square"); + const [cornerSquareColor, setCornerSquareColor] = useState("#000000"); + const [cornerDotType, setCornerDotType] = useState("square"); + const [cornerDotColor, setCornerDotColor] = useState("#000000"); + const [logoFile, setLogoFile] = useState(null); + const [logoBase64, setLogoBase64] = useState(""); + const [logoSize, setLogoSize] = useState(0.4); + const [logoMargin, setLogoMargin] = useState(8); + const [hideBackgroundDots, setHideBackgroundDots] = useState(true); + const [isGenerating, setIsGenerating] = useState(false); + + const qrContainerRef = useRef(null); + + // Initialize QR code instance + useEffect(() => { + const qr = createQRCode({ + text: text || "https://jam.dev", + width: size, + height: size, + format, + errorCorrectionLevel, + dotsOptions: { + color: dotsColor, + type: dotsType, + }, + backgroundOptions: { + color: backgroundColor, + }, + cornersSquareOptions: { + color: cornerSquareColor, + type: cornerSquareType, + }, + cornersDotOptions: { + color: cornerDotColor, + type: cornerDotType, + }, + image: logoBase64, + imageOptions: { + hideBackgroundDots, + imageSize: logoSize, + margin: logoMargin, + crossOrigin: "anonymous", + }, + }); + + setQrCodeInstance(qr); + + // Clean up previous QR code + if (qrContainerRef.current) { + qrContainerRef.current.innerHTML = ""; + qr.append(qrContainerRef.current); + } + + return () => { + // Cleanup if needed + }; + }, []); + + // Update QR code when options change + useEffect(() => { + if (qrCodeInstance && validateQRCodeText(text)) { + setIsGenerating(true); + + qrCodeInstance.update({ + text, + width: size, + height: size, + errorCorrectionLevel, + dotsOptions: { + color: dotsColor, + type: dotsType, + }, + backgroundOptions: { + color: backgroundColor, + }, + cornersSquareOptions: { + color: cornerSquareColor, + type: cornerSquareType, + }, + cornersDotOptions: { + color: cornerDotColor, + type: cornerDotType, + }, + image: logoBase64, + imageOptions: { + hideBackgroundDots, + imageSize: logoSize, + margin: logoMargin, + crossOrigin: "anonymous", + }, + }); + + setTimeout(() => setIsGenerating(false), 300); + } + }, [ + qrCodeInstance, + text, + size, + errorCorrectionLevel, + dotsType, + dotsColor, + backgroundColor, + cornerSquareType, + cornerSquareColor, + cornerDotType, + cornerDotColor, + logoBase64, + logoSize, + logoMargin, + hideBackgroundDots, + ]); + + const handleTextChange = useCallback((e: React.ChangeEvent) => { + setText(e.target.value); + }, []); + + const handleFormatSelect = useCallback((value: string) => { + setFormat(value as QRCodeFormat); + }, []); + + const handleErrorCorrectionSelect = useCallback((value: string) => { + setErrorCorrectionLevel(value as QRCodeErrorCorrectionLevel); + }, []); + + const handleDotsTypeSelect = useCallback((value: string) => { + setDotsType(value as QRCodeDotsType); + }, []); + + const handleCornerSquareTypeSelect = useCallback((value: string) => { + setCornerSquareType(value as QRCodeCornerSquareType); + }, []); + + const handleCornerDotTypeSelect = useCallback((value: string) => { + setCornerDotType(value as QRCodeCornerDotType); + }, []); + + const handleLogoSelect = useCallback(async (file: File) => { + if (validateImageFile(file)) { + setLogoFile(file); + try { + const base64 = await imageToBase64(file); + setLogoBase64(base64); + } catch (error) { + console.error("Error converting image to base64:", error); + } + } + }, []); + + const handleDownload = useCallback(async () => { + if (qrCodeInstance && validateQRCodeText(text)) { + try { + await qrCodeInstance.download(format); + } catch (error) { + console.error("Error downloading QR code:", error); + } + } + }, [qrCodeInstance, text, format]); + + const handleRemoveLogo = useCallback(() => { + setLogoFile(null); + setLogoBase64(""); + }, []); + + const handleSizeChange = useCallback((e: React.ChangeEvent) => { + const newSize = Math.max(100, Math.min(800, parseInt(e.target.value) || 300)); + setSize(newSize); + }, []); + + const handleLogoSizeChange = useCallback((e: React.ChangeEvent) => { + const newSize = Math.max(0.1, Math.min(0.8, parseFloat(e.target.value) || 0.4)); + setLogoSize(newSize); + }, []); + + const handleLogoMarginChange = useCallback((e: React.ChangeEvent) => { + const newMargin = Math.max(0, Math.min(20, parseInt(e.target.value) || 8)); + setLogoMargin(newMargin); + }, []); + + const isValidText = validateQRCodeText(text); + + return ( +
+ +
+ + +
+ +
+ +
+
+ {/* Controls */} + +
+ {/* Text Input */} +
+ +