From 4826ee1200429c16432389cac428b309671d0d1a Mon Sep 17 00:00:00 2001 From: leonardo Date: Fri, 8 May 2026 12:26:01 +0200 Subject: [PATCH 01/10] Allowed ids for AR experiences are now in a separate config file --- public/config/experiences.ts | 11 +++++++++++ src/utils/useQrScanner.ts | 10 +--------- 2 files changed, 12 insertions(+), 9 deletions(-) create mode 100644 public/config/experiences.ts diff --git a/public/config/experiences.ts b/public/config/experiences.ts new file mode 100644 index 0000000..8eb4c38 --- /dev/null +++ b/public/config/experiences.ts @@ -0,0 +1,11 @@ +/** + * list of allowed ids. + */ +export const ALLOWED_IDS = [ + "demo1", + "demoTrex", + "demoModelViewer", + "demoParticle", + "iceCore", + "glacierInTime", +]; diff --git a/src/utils/useQrScanner.ts b/src/utils/useQrScanner.ts index 76c3d25..1105b95 100644 --- a/src/utils/useQrScanner.ts +++ b/src/utils/useQrScanner.ts @@ -1,16 +1,8 @@ import { useRef, useState, useEffect } from "react"; import { useRouter } from "next/navigation"; import jsQR from "jsqr"; +import { ALLOWED_IDS } from "../../public/config/experiences"; -// Define authorized IDs here -const ALLOWED_IDS = [ - "demo1", - "demoTrex", - "demoModelViewer", - "demoParticle", - "iceCore", - "glacierInTime", -]; export function useQrScanner() { const router = useRouter(); From 4a815d30f9ce56441fc0b5c4f964639d647ef1e2 Mon Sep 17 00:00:00 2001 From: leonardo Date: Fri, 8 May 2026 12:26:52 +0200 Subject: [PATCH 02/10] Scanner status and message are now separate --- src/app/page.tsx | 11 +++-------- src/utils/useQrScanner.ts | 38 +++++++++++++++++++++++++------------- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index 85cfbda..1c18ce0 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -9,7 +9,7 @@ import { useQrScanner } from "../utils/useQrScanner"; const manrope = Manrope({ subsets: ["latin"] }); export default function Page() { - const { videoRef, state, qrData, startCamera } = useQrScanner(); + const { videoRef, status, message, qrData, startCamera } = useQrScanner(); const [isScanning, setIsScanning] = useState(false); // 1. Trigger startCamera ONLY after the DOM has updated and the video is mounted @@ -19,17 +19,13 @@ export default function Page() { } // We intentionally omit startCamera from the dependency array to avoid // infinite loops if the hook doesn't heavily memoize the function. - // eslint-disable-next-line react-hooks/exhaustive-deps }, [isScanning]); const handleStartCamera = () => { setIsScanning(true); }; - // 2. Helper to determine if the camera failed to initialize - const isError = - state.toLowerCase().includes("error") || - state.toLowerCase().includes("blocked"); + const isError = status === "error"; // If is not scanning, show the landing page. if (!isScanning) { @@ -103,12 +99,11 @@ export default function Page() { className={`${styles.statusBadge} ${ qrData ? styles.statusSuccess : "" }`} - // Add some inline styling to highlight errors in red style={ isError ? { backgroundColor: "#dc2626", color: "white" } : {} } > - {state} + {message} {/* Render a Retry/Back button if the camera fails */} diff --git a/src/utils/useQrScanner.ts b/src/utils/useQrScanner.ts index 1105b95..38e152c 100644 --- a/src/utils/useQrScanner.ts +++ b/src/utils/useQrScanner.ts @@ -3,6 +3,7 @@ import { useRouter } from "next/navigation"; import jsQR from "jsqr"; import { ALLOWED_IDS } from "../../public/config/experiences"; +export type ScannerStatus = "idle" | "scanning" | "success" | "error"; export function useQrScanner() { const router = useRouter(); @@ -18,7 +19,9 @@ export function useQrScanner() { const timeoutRef = useRef(null); const isMounted = useRef(true); - const [state, setState] = useState("Waiting for camera..."); + const [status, setStatus] = useState("idle"); + const [message, setMessage] = useState("Waiting for camera..."); + const [qrData, setQrData] = useState<{ originalUrl: string; parameters: Record; @@ -38,7 +41,8 @@ export function useQrScanner() { // Only update state if the component is still mounted if (isMounted.current) { - setState("Scanning..."); + setStatus("scanning"); + setMessage("Scanning..."); } }, 1500); } @@ -54,15 +58,16 @@ export function useQrScanner() { // Validate the ID against the whitelist if (ALLOWED_IDS.includes(id)) { isProcessing.current = true; - setState(`ID "${id}" verified! Redirecting...`); - + setStatus("success"); + setMessage(`ID "${id}" verified! Redirecting...`); // Stop animation before navigating if (requestRef.current) cancelAnimationFrame(requestRef.current); router.push(`/${id}`); return; } else { // ID found but not authorized - setState(`Access Denied: ID "${id}" is not valid.`); + setStatus("error"); + setMessage(`Access Denied: ID "${id}" is not valid.`); setQrData({ originalUrl: qrString, parameters }); // Briefly pause to show the error before allowing next scan @@ -71,11 +76,13 @@ export function useQrScanner() { } } - setState("QR detected but no ID parameter found."); + setStatus("error"); + setMessage("QR detected but no ID parameter found."); setQrData({ originalUrl: qrString, parameters }); pauseScanner(); } catch (err) { - setState("QR does not contain a valid URL."); + setStatus("error"); + setMessage("QR does not contain a valid URL."); setQrData({ originalUrl: qrString, parameters: {} }); pauseScanner(); } @@ -121,7 +128,10 @@ export function useQrScanner() { async function startCamera() { try { if (!navigator.mediaDevices?.getUserMedia) { - setState("Camera API blocked or not supported. Check HTTPS/localhost."); + setStatus("error"); + setMessage( + "Camera API blocked or not supported. Check HTTPS/localhost.", + ); return; } @@ -151,21 +161,23 @@ export function useQrScanner() { videoRef.current.setAttribute("playsinline", "true"); await videoRef.current.play(); - setState("Scanning..."); + setStatus("scanning"); + setMessage("Scanning..."); requestRef.current = requestAnimationFrame(scanFrame); } } catch (err) { + setStatus("error"); if (err instanceof DOMException && err.name === "NotFoundError") { - setState("Error: No camera found on this device."); + setMessage("Error: No camera found on this device."); } else if ( err instanceof DOMException && err.name === "NotAllowedError" ) { - setState( + setMessage( "Error: Camera permission denied. Please allow camera access.", ); } else if (err instanceof Error) { - setState(`Camera error: ${err.message}`); + setMessage(`Camera error: ${err.message}`); } console.error("getUserMedia error:", err); } @@ -195,5 +207,5 @@ export function useQrScanner() { }, []); // Expose startCamera to the component - return { videoRef, state, qrData, startCamera }; + return { videoRef, status, message, qrData, startCamera }; } From df4e32bade365f4c18ce3ffaee1962ded58a6d1c Mon Sep 17 00:00:00 2001 From: leonardo Date: Fri, 8 May 2026 12:56:49 +0200 Subject: [PATCH 03/10] Moved styles to separate css files --- .../glacierInTime/glacierInTime.module.css | 130 ++++++++++++++ src/app/glacierInTime/page.tsx | 145 ++------------- src/app/iceCore/iceCore.module.css | 130 ++++++++++++++ src/app/iceCore/page.tsx | 144 ++------------- src/app/page.tsx | 108 +++--------- src/app/scanner.module.css | 67 ------- src/app/style.module.css | 166 ++++++++++++++++++ 7 files changed, 474 insertions(+), 416 deletions(-) create mode 100644 src/app/glacierInTime/glacierInTime.module.css create mode 100644 src/app/iceCore/iceCore.module.css delete mode 100644 src/app/scanner.module.css create mode 100644 src/app/style.module.css diff --git a/src/app/glacierInTime/glacierInTime.module.css b/src/app/glacierInTime/glacierInTime.module.css new file mode 100644 index 0000000..f52c148 --- /dev/null +++ b/src/app/glacierInTime/glacierInTime.module.css @@ -0,0 +1,130 @@ +.container { + position: relative; + width: 100vw; + height: 100dvh; + overflow: hidden; + background-color: #000; +} + +/* Invisible overlay to catch click*/ +.clickOverlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 8; + cursor: pointer; +} + +/* Marker image */ +.markerImage { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + max-width: 80vw; + max-height: 80vh; + width: auto; + height: auto; + transition: opacity 0.6s ease-in-out; + pointer-events: none; + z-index: 5; +} + +/* UI */ +.uiContainer { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + padding: 40px 20px; + z-index: 10; + pointer-events: none; + text-align: center; + box-sizing: border-box; +} + +/* Higher text group*/ +.textGroup { + display: flex; + flex-direction: column; + gap: 10px; +} + +.instructionText { + font-size: 1.2rem; + color: #fff; + text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.8); + margin: 0; +} + +.pulseText { + font-size: 1.5rem; + color: #fff; + font-weight: bold; + animation: pulse 2s infinite; + text-transform: uppercase; + letter-spacing: 1px; + text-shadow: 2px 2px 6px rgba(0, 0, 0, 0.9); + pointer-events: none; +} + +/* Lower button group*/ +.bottomGroup { + display: flex; + flex-direction: column; + align-items: center; + gap: 20px; + margin-bottom: 20px; +} + +.backButton { + pointer-events: auto; + display: inline-block; + padding: 12px 24px; + background-color: #0070f3; + color: white; + text-decoration: none; + border-radius: 8px; + font-size: 16px; + font-weight: bold; + opacity: 0.8; + transition: opacity 0.2s ease; +} + +.backButton:hover { + opacity: 1; +} + +/* Iframe AR */ +.iframeStyle { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border: none; + z-index: 1; +} + +/* Animation */ +@keyframes pulse { + 0% { + transform: scale(1); + opacity: 0.8; + } + 50% { + transform: scale(1.05); + opacity: 1; + } + 100% { + transform: scale(1); + opacity: 0.8; + } +} diff --git a/src/app/glacierInTime/page.tsx b/src/app/glacierInTime/page.tsx index 7b16cf2..2e7e863 100644 --- a/src/app/glacierInTime/page.tsx +++ b/src/app/glacierInTime/page.tsx @@ -11,6 +11,8 @@ import { useIframeMessage, } from "@/utils/arHelper"; +import styles from "./glacierInTime.module.css"; + // Inizializza il font const manrope = Manrope({ subsets: ["latin"] }); @@ -86,130 +88,37 @@ export default function GlacierInTimePage() { }; return ( -
+
{/* Invisible overlay to capture the click*/} {isMarkerFound && !animationStarted && ( -
+
)} - {/* Overlay image */} + {/* Overlay image - L'opacità rimane inline perché dinamica */} Inquadra questa immagine -
-
+
+
{!isMarkerFound && ( -

- Frame the image to start -

+

Frame the image to start

)} {isMarkerFound && !animationStarted && ( -
- Tocca per iniziare -
+
Tocca per iniziare
)}
{/* Go back button */} -
- +
+ ← Back to the scanner
@@ -218,36 +127,10 @@ export default function GlacierInTimePage() {