fix(club): use real scannable QR code instead of pseudo-random grid
This commit is contained in:
@@ -10,6 +10,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"next": "16.3.1",
|
"next": "16.3.1",
|
||||||
|
"qrcode": "^1.5.4",
|
||||||
"react": "19.2.8",
|
"react": "19.2.8",
|
||||||
"react-dom": "19.2.8",
|
"react-dom": "19.2.8",
|
||||||
"sharp": "^0.35.3"
|
"sharp": "^0.35.3"
|
||||||
@@ -17,6 +18,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
|
"@types/qrcode": "^1.5.5",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
import qrcode from 'qrcode';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
type ClubMode = 'landing' | 'join' | 'card';
|
type ClubMode = 'landing' | 'join' | 'card';
|
||||||
@@ -101,43 +102,11 @@ function isIosDevice(): boolean {
|
|||||||
return /iphone|ipad|ipod/i.test(window.navigator.userAgent);
|
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 size = 21;
|
||||||
const grid = Array.from({ length: size }, () => Array.from({ length: size }, () => false));
|
return 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function InstallButton() {
|
function InstallButton() {
|
||||||
@@ -198,26 +167,37 @@ function InstallButton() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function MemberVisualCode({ memberCode }: { memberCode: string }) {
|
function MemberVisualCode({ memberCode }: { memberCode: string }) {
|
||||||
const cells = useMemo(() => buildVisualCode(memberCode), [memberCode]);
|
const [svg, setSvg] = useState<string>('');
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<svg viewBox="0 0 210 210" className="h-44 w-44 rounded-3xl bg-white p-4 shadow-inner">
|
<div
|
||||||
<rect width="210" height="210" rx="24" fill="white" />
|
className="h-44 w-44 rounded-3xl bg-white p-3 shadow-inner [&_svg]:h-full [&_svg]:w-full"
|
||||||
{cells.flatMap((row, rowIndex) =>
|
role="img"
|
||||||
row.map((value, colIndex) =>
|
aria-label={`Código QR del socio ${memberCode}`}
|
||||||
value ? (
|
dangerouslySetInnerHTML={{ __html: svg }}
|
||||||
<rect
|
/>
|
||||||
key={`${rowIndex}-${colIndex}`}
|
|
||||||
x={colIndex * 10}
|
|
||||||
y={rowIndex * 10}
|
|
||||||
width="10"
|
|
||||||
height="10"
|
|
||||||
rx="2"
|
|
||||||
fill="#1B4332"
|
|
||||||
/>
|
|
||||||
) : null,
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</svg>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user