diff --git a/project/frontend/package.json b/project/frontend/package.json index ad1cea2..f08754e 100644 --- a/project/frontend/package.json +++ b/project/frontend/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "next": "16.3.1", + "qrcode": "^1.5.4", "react": "19.2.8", "react-dom": "19.2.8", "sharp": "^0.35.3" @@ -17,6 +18,7 @@ "devDependencies": { "@tailwindcss/postcss": "^4", "@types/node": "^20", + "@types/qrcode": "^1.5.5", "@types/react": "^19", "@types/react-dom": "^19", "eslint": "^9", diff --git a/project/frontend/src/components/club/ClubExperience.tsx b/project/frontend/src/components/club/ClubExperience.tsx index c5ac399..0da11cd 100644 --- a/project/frontend/src/components/club/ClubExperience.tsx +++ b/project/frontend/src/components/club/ClubExperience.tsx @@ -2,6 +2,7 @@ import Link from 'next/link'; import { useRouter } from 'next/navigation'; +import qrcode from 'qrcode'; import { useEffect, useMemo, useState } from 'react'; type ClubMode = 'landing' | 'join' | 'card'; @@ -101,43 +102,11 @@ function isIosDevice(): boolean { return /iphone|ipad|ipod/i.test(window.navigator.userAgent); } -function buildVisualCode(seed: string): boolean[][] { +function buildVisualCode(_seed: string): boolean[][] { + // DEPRECATED: replaced by MemberQrCode component using real QR codes. + // Kept as a no-op stub for backward compatibility (tests, etc.). const size = 21; - const grid = Array.from({ length: size }, () => Array.from({ length: size }, () => false)); - - const drawFinder = (startRow: number, startCol: number) => { - for (let row = 0; row < 7; row += 1) { - for (let col = 0; col < 7; col += 1) { - const edge = row === 0 || row === 6 || col === 0 || col === 6; - const center = row >= 2 && row <= 4 && col >= 2 && col <= 4; - grid[startRow + row]![startCol + col] = edge || center; - } - } - }; - - drawFinder(0, 0); - drawFinder(0, size - 7); - drawFinder(size - 7, 0); - - let hash = 0; - for (let index = 0; index < seed.length; index += 1) { - hash = (hash * 33 + seed.charCodeAt(index)) >>> 0; - } - - for (let row = 0; row < size; row += 1) { - for (let col = 0; col < size; col += 1) { - const inFinder = - (row < 7 && col < 7) || - (row < 7 && col >= size - 7) || - (row >= size - 7 && col < 7); - if (inFinder) continue; - const bit = ((hash >> ((row + col) % 24)) ^ ((row + 1) * 17) ^ ((col + 1) * 31)) & 1; - grid[row]![col] = bit === 1; - hash = ((hash * 1664525 + 1013904223) >>> 0) ^ (row * 97 + col * 53); - } - } - - return grid; + return Array.from({ length: size }, () => Array.from({ length: size }, () => false)); } function InstallButton() { @@ -198,26 +167,37 @@ function InstallButton() { } function MemberVisualCode({ memberCode }: { memberCode: string }) { - const cells = useMemo(() => buildVisualCode(memberCode), [memberCode]); + const [svg, setSvg] = useState(''); + + useEffect(() => { + let cancelled = false; + qrcode + .toString(memberCode, { + type: 'svg', + errorCorrectionLevel: 'M', + margin: 2, + width: 220, + color: { dark: '#1B4332', light: '#ffffff' }, + }) + .then((output) => { + if (!cancelled) setSvg(output); + }) + .catch(() => { + if (!cancelled) setSvg(''); + }); + return () => { + cancelled = true; + }; + }, [memberCode]); + + if (!svg) return null; return ( - - - {cells.flatMap((row, rowIndex) => - row.map((value, colIndex) => - value ? ( - - ) : null, - ), - )} - +
); }