From 02178df8d341e2e5ed8f85fca6a5b4609f6ec2ae Mon Sep 17 00:00:00 2001 From: Siddharth Kumar Sah Date: Mon, 13 Apr 2026 02:51:25 +0800 Subject: [PATCH] feat(qr-generate): add Zustand store for QR generator state --- apps/web/src/stores/qr-store.ts | 252 ++++++++++++++++++++++++++++++++ 1 file changed, 252 insertions(+) create mode 100644 apps/web/src/stores/qr-store.ts diff --git a/apps/web/src/stores/qr-store.ts b/apps/web/src/stores/qr-store.ts new file mode 100644 index 00000000..8c7e6c13 --- /dev/null +++ b/apps/web/src/stores/qr-store.ts @@ -0,0 +1,252 @@ +import { create } from "zustand"; + +export type ContentType = "url" | "text" | "wifi" | "vcard" | "email" | "phone" | "sms"; + +export type DotType = "square" | "rounded" | "dots" | "classy" | "classy-rounded" | "extra-rounded"; +export type CornerSquareType = "square" | "rounded" | "dot" | "extra-rounded"; +export type CornerDotType = "square" | "rounded" | "dot"; +export type ErrorCorrectionLevel = "L" | "M" | "Q" | "H"; +export type DownloadFormat = "png" | "svg" | "jpeg" | "webp"; + +export interface WifiData { + ssid: string; + password: string; + encryption: "WPA" | "WEP" | "nopass"; + hidden: boolean; +} + +export interface VCardData { + firstName: string; + lastName: string; + phone: string; + email: string; + organization: string; + title: string; + url: string; +} + +export interface EmailData { + to: string; + subject: string; + body: string; +} + +export interface SmsData { + phone: string; + message: string; +} + +interface QrState { + // Content + contentType: ContentType; + textData: string; + wifiData: WifiData; + vcardData: VCardData; + emailData: EmailData; + phoneData: string; + smsData: SmsData; + + // Style + dotType: DotType; + cornerSquareType: CornerSquareType; + cornerDotType: CornerDotType; + + // Colors + dotColor: string; + dotGradientEnabled: boolean; + dotGradientType: "linear" | "radial"; + dotGradientColor1: string; + dotGradientColor2: string; + dotGradientRotation: number; + bgColor: string; + bgTransparent: boolean; + cornerSquareColor: string; + cornerDotColor: string; + useCustomCornerColors: boolean; + + // Logo + logoFile: File | null; + logoDataUrl: string | null; + logoSize: number; + logoMargin: number; + hideBackgroundDots: boolean; + + // Output + size: number; + errorCorrection: ErrorCorrectionLevel; + downloadFormat: DownloadFormat; + + // Actions + setContentType: (t: ContentType) => void; + setTextData: (v: string) => void; + setWifiData: (v: Partial) => void; + setVcardData: (v: Partial) => void; + setEmailData: (v: Partial) => void; + setPhoneData: (v: string) => void; + setSmsData: (v: Partial) => void; + setDotType: (v: DotType) => void; + setCornerSquareType: (v: CornerSquareType) => void; + setCornerDotType: (v: CornerDotType) => void; + setDotColor: (v: string) => void; + setDotGradientEnabled: (v: boolean) => void; + setDotGradientType: (v: "linear" | "radial") => void; + setDotGradientColor1: (v: string) => void; + setDotGradientColor2: (v: string) => void; + setDotGradientRotation: (v: number) => void; + setBgColor: (v: string) => void; + setBgTransparent: (v: boolean) => void; + setCornerSquareColor: (v: string) => void; + setCornerDotColor: (v: string) => void; + setUseCustomCornerColors: (v: boolean) => void; + setLogoFile: (f: File | null) => void; + setLogoSize: (v: number) => void; + setLogoMargin: (v: number) => void; + setHideBackgroundDots: (v: boolean) => void; + setSize: (v: number) => void; + setErrorCorrection: (v: ErrorCorrectionLevel) => void; + setDownloadFormat: (v: DownloadFormat) => void; + reset: () => void; +} + +const initialWifi: WifiData = { + ssid: "", + password: "", + encryption: "WPA", + hidden: false, +}; +const initialVcard: VCardData = { + firstName: "", + lastName: "", + phone: "", + email: "", + organization: "", + title: "", + url: "", +}; +const initialEmail: EmailData = { to: "", subject: "", body: "" }; +const initialSms: SmsData = { phone: "", message: "" }; + +const DEFAULTS = { + contentType: "url" as ContentType, + textData: "", + wifiData: { ...initialWifi }, + vcardData: { ...initialVcard }, + emailData: { ...initialEmail }, + phoneData: "", + smsData: { ...initialSms }, + dotType: "rounded" as DotType, + cornerSquareType: "rounded" as CornerSquareType, + cornerDotType: "rounded" as CornerDotType, + dotColor: "#000000", + dotGradientEnabled: false, + dotGradientType: "linear" as const, + dotGradientColor1: "#000000", + dotGradientColor2: "#4338ca", + dotGradientRotation: 0, + bgColor: "#FFFFFF", + bgTransparent: false, + cornerSquareColor: "#000000", + cornerDotColor: "#000000", + useCustomCornerColors: false, + logoFile: null as File | null, + logoDataUrl: null as string | null, + logoSize: 0.4, + logoMargin: 5, + hideBackgroundDots: true, + size: 1024, + errorCorrection: "Q" as ErrorCorrectionLevel, + downloadFormat: "png" as DownloadFormat, +}; + +export const useQrStore = create((set) => ({ + ...DEFAULTS, + + setContentType: (t) => set({ contentType: t }), + setTextData: (v) => set({ textData: v }), + setWifiData: (v) => set((s) => ({ wifiData: { ...s.wifiData, ...v } })), + setVcardData: (v) => set((s) => ({ vcardData: { ...s.vcardData, ...v } })), + setEmailData: (v) => set((s) => ({ emailData: { ...s.emailData, ...v } })), + setPhoneData: (v) => set({ phoneData: v }), + setSmsData: (v) => set((s) => ({ smsData: { ...s.smsData, ...v } })), + setDotType: (v) => set({ dotType: v }), + setCornerSquareType: (v) => set({ cornerSquareType: v }), + setCornerDotType: (v) => set({ cornerDotType: v }), + setDotColor: (v) => set({ dotColor: v }), + setDotGradientEnabled: (v) => set({ dotGradientEnabled: v }), + setDotGradientType: (v) => set({ dotGradientType: v }), + setDotGradientColor1: (v) => set({ dotGradientColor1: v }), + setDotGradientColor2: (v) => set({ dotGradientColor2: v }), + setDotGradientRotation: (v) => set({ dotGradientRotation: v }), + setBgColor: (v) => set({ bgColor: v }), + setBgTransparent: (v) => set({ bgTransparent: v }), + setCornerSquareColor: (v) => set({ cornerSquareColor: v }), + setCornerDotColor: (v) => set({ cornerDotColor: v }), + setUseCustomCornerColors: (v) => set({ useCustomCornerColors: v }), + setLogoFile: (f) => { + if (!f) { + set({ logoFile: null, logoDataUrl: null }); + return; + } + const reader = new FileReader(); + reader.onload = () => set({ logoFile: f, logoDataUrl: reader.result as string }); + reader.readAsDataURL(f); + }, + setLogoSize: (v) => set({ logoSize: v }), + setLogoMargin: (v) => set({ logoMargin: v }), + setHideBackgroundDots: (v) => set({ hideBackgroundDots: v }), + setSize: (v) => set({ size: v }), + setErrorCorrection: (v) => set({ errorCorrection: v }), + setDownloadFormat: (v) => set({ downloadFormat: v }), + reset: () => + set({ + ...DEFAULTS, + wifiData: { ...initialWifi }, + vcardData: { ...initialVcard }, + emailData: { ...initialEmail }, + smsData: { ...initialSms }, + }), +})); + +/** + * Encodes the current store state into the string payload for the QR code. + */ +export function encodeQrData(state: QrState): string { + switch (state.contentType) { + case "url": + case "text": + return state.textData; + case "wifi": { + const w = state.wifiData; + return `WIFI:T:${w.encryption};S:${w.ssid};P:${w.password};H:${w.hidden};;`; + } + case "vcard": { + const v = state.vcardData; + const lines = [ + "BEGIN:VCARD", + "VERSION:3.0", + `N:${v.lastName};${v.firstName}`, + `FN:${[v.firstName, v.lastName].filter(Boolean).join(" ")}`, + ]; + if (v.phone) lines.push(`TEL:${v.phone}`); + if (v.email) lines.push(`EMAIL:${v.email}`); + if (v.organization) lines.push(`ORG:${v.organization}`); + if (v.title) lines.push(`TITLE:${v.title}`); + if (v.url) lines.push(`URL:${v.url}`); + lines.push("END:VCARD"); + return lines.join("\n"); + } + case "email": { + const e = state.emailData; + const params = []; + if (e.subject) params.push(`subject=${encodeURIComponent(e.subject)}`); + if (e.body) params.push(`body=${encodeURIComponent(e.body)}`); + return `mailto:${e.to}${params.length ? `?${params.join("&")}` : ""}`; + } + case "phone": + return `tel:${state.phoneData}`; + case "sms": { + const s = state.smsData; + return `smsto:${s.phone}:${s.message}`; + } + } +}