From ee8ae8829efe3ca0516e86d6f06cf4a06065a77f Mon Sep 17 00:00:00 2001 From: nessi Date: Sun, 2 Aug 2026 10:52:37 +0200 Subject: [PATCH] Add QR code generation and scanning for game join codes with camera integration Implemented QR code generation for created games using qrcode library with custom dark/light color scheme. Added camera-based QR scanner using BarcodeDetector API with environment-facing camera preference, automatic code extraction, and 350ms polling interval. Added toggle buttons for QR display/scanner with bilingual labels, video preview with styled container, and fallback error handling for unsupported browsers or denied --- frontend/package.json | 3 +- frontend/src/components/NewGameModal.jsx | 77 +++++++++++++++++++++++- 2 files changed, 78 insertions(+), 2 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index b05b867..7f0d5c4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,7 +10,8 @@ "dependencies": { "react": "^18.3.1", "react-dom": "^18.3.1", - "canvas-confetti": "^1.9.3" + "canvas-confetti": "^1.9.3", + "qrcode": "^1.5.4" }, "devDependencies": { "@vitejs/plugin-react": "^4.3.1", diff --git a/frontend/src/components/NewGameModal.jsx b/frontend/src/components/NewGameModal.jsx index 69ac42e..912b783 100644 --- a/frontend/src/components/NewGameModal.jsx +++ b/frontend/src/components/NewGameModal.jsx @@ -1,4 +1,5 @@ -import React, { useEffect, useMemo, useState } from "react"; +import React, { useEffect, useMemo, useRef, useState } from "react"; +import QRCode from "qrcode"; import { styles } from "../styles/styles"; import { stylesTokens } from "../styles/theme"; import { useLanguage } from "../i18n"; @@ -21,6 +22,11 @@ export default function NewGameModal({ const [err, setErr] = useState(""); const [created, setCreated] = useState(null); // { code } const [toast, setToast] = useState(""); + const [qrOpen, setQrOpen] = useState(false); + const [qrDataUrl, setQrDataUrl] = useState(""); + const [scannerOpen, setScannerOpen] = useState(false); + const videoRef = useRef(null); + const streamRef = useRef(null); const canJoin = useMemo(() => joinCode.trim().length >= 4, [joinCode]); @@ -32,6 +38,9 @@ export default function NewGameModal({ setToast(""); setJoinCode(""); setCreated(null); + setQrOpen(false); + setQrDataUrl(""); + setScannerOpen(false); // Wenn ein Spiel läuft (und nicht finished) -> nur Code anzeigen if (hasGame && !gameFinished) { @@ -41,6 +50,56 @@ export default function NewGameModal({ } }, [open, hasGame, gameFinished]); + useEffect(() => { + if (!qrOpen || !created?.code) return; + QRCode.toDataURL(created.code, { width: 280, margin: 2, errorCorrectionLevel: "M", color: { dark: "#17161b", light: "#f5efdc" } }) + .then(setQrDataUrl).catch(() => setQrDataUrl("")); + }, [qrOpen, created?.code]); + + useEffect(() => { + if (!scannerOpen) return undefined; + let alive = true; + let intervalId; + const startScanner = async () => { + if (!("BarcodeDetector" in window) || !navigator.mediaDevices?.getUserMedia) { + setErr(language === "en" ? "QR scanning is not supported here. Enter the code manually." : "QR-Scannen wird hier nicht unterstützt. Bitte Code manuell eingeben."); + setScannerOpen(false); + return; + } + try { + const detector = new window.BarcodeDetector({ formats: ["qr_code"] }); + const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: { ideal: "environment" } }, audio: false }); + if (!alive) { stream.getTracks().forEach((track) => track.stop()); return; } + streamRef.current = stream; + videoRef.current.srcObject = stream; + await videoRef.current.play(); + intervalId = window.setInterval(async () => { + if (!alive || !videoRef.current) return; + try { + const codes = await detector.detect(videoRef.current); + const raw = codes?.[0]?.rawValue?.trim() || ""; + if (raw) { + setJoinCode((raw.match(/[23456789ABCDEFGHJKMNPQRSTUVWXYZ]{4,}/i)?.[0] || raw).toUpperCase()); + setScannerOpen(false); + setToast(language === "en" ? "✅ QR code recognized" : "✅ QR-Code erkannt"); + } + } catch { /* frame not ready */ } + }, 350); + } catch { + setErr(language === "en" ? "Camera access was denied. Enter the code manually." : "Kamerazugriff wurde verweigert. Bitte Code manuell eingeben."); + setScannerOpen(false); + } + }; + startScanner(); + return () => { + alive = false; + if (intervalId) window.clearInterval(intervalId); + if (streamRef.current) streamRef.current.getTracks().forEach((track) => track.stop()); + streamRef.current = null; + if (videoRef.current) videoRef.current.srcObject = null; + }; + }, [scannerOpen, language]); + if (!open) return null; const showToast = (msg) => { @@ -202,6 +261,15 @@ export default function NewGameModal({ autoFocus /> + + + {scannerOpen &&
+
} +
+ + {qrOpen && qrDataUrl &&
+ {language +
{language === "en" ? "Scan this code to join the game." : "Scanne diesen Code, um dem Spiel beizutreten."}
+
}