From 36316092ba1fc366bd7ba5f5b4ae371733e4a060 Mon Sep 17 00:00:00 2001 From: "Dhanavadh T. Saito" <105502355+dhanavadh@users.noreply.github.com> Date: Sun, 13 Jul 2025 22:27:50 +0700 Subject: [PATCH 01/68] feat: added staff scan qr page -added staff scan qr page --- src/components/common/Tabs.astro | 145 +++++++++++++++++++++++++++++ src/layouts/staff/WithNavbar.astro | 37 ++++++++ src/pages/staff/event/index.astro | 44 +++++++++ 3 files changed, 226 insertions(+) create mode 100644 src/components/common/Tabs.astro create mode 100644 src/layouts/staff/WithNavbar.astro create mode 100644 src/pages/staff/event/index.astro diff --git a/src/components/common/Tabs.astro b/src/components/common/Tabs.astro new file mode 100644 index 0000000..9802aad --- /dev/null +++ b/src/components/common/Tabs.astro @@ -0,0 +1,145 @@ +
+
+ +
+ +
+
+ + +
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+
+ +
+
+
+ + + + diff --git a/src/layouts/staff/WithNavbar.astro b/src/layouts/staff/WithNavbar.astro new file mode 100644 index 0000000..b95e492 --- /dev/null +++ b/src/layouts/staff/WithNavbar.astro @@ -0,0 +1,37 @@ +--- +import "@/styles/global.css"; +import Footer from "@firstdate/Footer.astro"; +import Navbar from "@firstdate/Navbar.astro"; +import { ClientRouter } from "astro:transitions"; + +interface Props { + title?: string; + description?: string; +} + +const { title, description } = Astro.props; +--- + + + + + + + + + {title} + + + + +
+ +
+ +
+
+
+ + diff --git a/src/pages/staff/event/index.astro b/src/pages/staff/event/index.astro new file mode 100644 index 0000000..bb827d6 --- /dev/null +++ b/src/pages/staff/event/index.astro @@ -0,0 +1,44 @@ +--- +import Popup from "@/components/common/Popup.astro"; +import Tabs from "@/components/common/Tabs.astro"; +import Layout from "@/layouts/staff/WithNavbar.astro"; +--- + + +
+
+

ลงทะเบียนงาน

+

+ CU First Date +

+ +
+ + +
+
+ +
+
+ + +
+
+ +
+
+
+
+
+
+
From 076210b4221c79d5944ef77265742a1ecf355a98 Mon Sep 17 00:00:00 2001 From: "Dhanavadh T. Saito" <105502355+dhanavadh@users.noreply.github.com> Date: Sun, 13 Jul 2025 23:54:33 +0700 Subject: [PATCH 02/68] fix: firstdate staff bg --- src/layouts/staff/WithNavbar.astro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/layouts/staff/WithNavbar.astro b/src/layouts/staff/WithNavbar.astro index b95e492..1111986 100644 --- a/src/layouts/staff/WithNavbar.astro +++ b/src/layouts/staff/WithNavbar.astro @@ -25,7 +25,7 @@ const { title, description } = Astro.props;
From 2eb62c2b99061a279faaa806e5f83369269bd3d7 Mon Sep 17 00:00:00 2001 From: "Dhanavadh T. Saito" <105502355+dhanavadh@users.noreply.github.com> Date: Mon, 14 Jul 2025 18:22:10 +0700 Subject: [PATCH 03/68] feat: add @yudiel/react-qr-scanner --- package.json | 5 ++- pnpm-lock.yaml | 73 +++++++++++++++++++++++++++++++++++++ src/pages/staff/index.astro | 21 +++++++++++ 3 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 src/pages/staff/index.astro diff --git a/package.json b/package.json index 8830dc1..118dadc 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@tailwindcss/vite": "^4.1.11", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", + "@yudiel/react-qr-scanner": "^2.3.1", "astro": "^5.10.1", "canvas-confetti": "^1.9.3", "clsx": "^2.1.1", @@ -43,9 +44,9 @@ "devDependencies": { "@astrojs/check": "^0.9.4", "@astrojs/ts-plugin": "^1.10.4", - "@types/qrcode": "^1.5.5", "@trivago/prettier-plugin-sort-imports": "^5.2.2", "@types/canvas-confetti": "^1.9.0", + "@types/qrcode": "^1.5.5", "@typescript-eslint/eslint-plugin": "^8.36.0", "@typescript-eslint/parser": "^8.36.0", "astro-eslint-parser": "^1.2.2", @@ -78,4 +79,4 @@ "prettier --write" ] } -} \ No newline at end of file +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4aa1b57..f81a65c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -34,6 +34,9 @@ importers: "@types/react-dom": specifier: ^19.1.6 version: 19.1.6(@types/react@19.1.8) + "@yudiel/react-qr-scanner": + specifier: ^2.3.1 + version: 2.3.1(@types/emscripten@1.40.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) astro: specifier: ^5.10.1 version: 5.10.1(@types/node@24.0.7)(jiti@2.4.2)(lightningcss@1.30.1)(rollup@4.44.1)(typescript@5.8.3)(yaml@2.8.0) @@ -1494,6 +1497,12 @@ packages: integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==, } + "@types/emscripten@1.40.1": + resolution: + { + integrity: sha512-sr53lnYkQNhjHNN0oJDdUm5564biioI5DuOpycufDVK7D3y+GR3oUswe2rlwY1nPNyusHbrJ9WoTyIHl4/Bpwg==, + } + "@types/estree@1.0.8": resolution: { @@ -1898,6 +1907,15 @@ packages: integrity: sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==, } + "@yudiel/react-qr-scanner@2.3.1": + resolution: + { + integrity: sha512-fE7217QvMKT/AxAEeKIheFhkKO13PSGHiqJfg4dLK/SGPpelpXNpZZ0Qeph1Cm08/AqMlGhvzQbCmoPeD/VVIg==, + } + peerDependencies: + react: ^17 || ^18 || ^19 + react-dom: ^17 || ^18 || ^19 + acorn-jsx@5.3.2: resolution: { @@ -2120,6 +2138,12 @@ packages: integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==, } + barcode-detector@3.0.3: + resolution: + { + integrity: sha512-N07CNbpudOB3oIYm0tvaezCM6zy9HOlYnUCBhX6Q5UGhnqngyBgOf/p/5ZhqHxsf9/QYy5dX95RrblIs0hNaiw==, + } + base-64@1.0.0: resolution: { @@ -5258,6 +5282,12 @@ packages: integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==, } + sdp@3.2.1: + resolution: + { + integrity: sha512-lwsAIzOPlH8/7IIjjz3K0zYBk7aBVVcvjMwt3M4fLxpjMYyy7i3I97SLHebgn4YBjirkzfp3RvRDWSKsh/+WFw==, + } + semver@6.3.1: resolution: { @@ -6202,6 +6232,13 @@ packages: integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==, } + webrtc-adapter@9.0.3: + resolution: + { + integrity: sha512-5fALBcroIl31OeXAdd1YUntxiZl1eHlZZWzNg3U4Fn+J9/cGL3eT80YlrsWGvj2ojuz1rZr2OXkgCzIxAZ7vRQ==, + } + engines: { node: ">=6.0.0", npm: ">=3.10.0" } + whatwg-url@5.0.0: resolution: { @@ -6437,6 +6474,14 @@ packages: integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==, } + zxing-wasm@2.2.0: + resolution: + { + integrity: sha512-RyHxVaAHsLSDzmwcAG05IF8sVOE5Ta2JT1dRDh0mzVZOIiDXZstsjkqvKHasN1n4lvFSbX7ngkHDufnt/XI07Q==, + } + peerDependencies: + "@types/emscripten": ">=1.39.6" + snapshots: "@ampproject/remapping@2.3.0": dependencies: @@ -7235,6 +7280,8 @@ snapshots: dependencies: "@types/ms": 2.1.0 + "@types/emscripten@1.40.1": {} + "@types/estree@1.0.8": {} "@types/fontkit@2.0.8": @@ -7504,6 +7551,15 @@ snapshots: "@vscode/l10n@0.0.18": {} + "@yudiel/react-qr-scanner@2.3.1(@types/emscripten@1.40.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)": + dependencies: + barcode-detector: 3.0.3(@types/emscripten@1.40.1) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + webrtc-adapter: 9.0.3 + transitivePeerDependencies: + - "@types/emscripten" + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -7758,6 +7814,12 @@ snapshots: balanced-match@1.0.2: {} + barcode-detector@3.0.3(@types/emscripten@1.40.1): + dependencies: + zxing-wasm: 2.2.0(@types/emscripten@1.40.1) + transitivePeerDependencies: + - "@types/emscripten" + base-64@1.0.0: {} base64-js@1.5.1: {} @@ -9873,6 +9935,8 @@ snapshots: scheduler@0.26.0: {} + sdp@3.2.1: {} + semver@6.3.1: {} semver@7.7.2: {} @@ -10502,6 +10566,10 @@ snapshots: webidl-conversions@3.0.1: {} + webrtc-adapter@9.0.3: + dependencies: + sdp: 3.2.1 + whatwg-url@5.0.0: dependencies: tr46: 0.0.3 @@ -10664,3 +10732,8 @@ snapshots: zod@4.0.5: {} zwitch@2.0.4: {} + + zxing-wasm@2.2.0(@types/emscripten@1.40.1): + dependencies: + "@types/emscripten": 1.40.1 + type-fest: 4.41.0 diff --git a/src/pages/staff/index.astro b/src/pages/staff/index.astro new file mode 100644 index 0000000..020c20d --- /dev/null +++ b/src/pages/staff/index.astro @@ -0,0 +1,21 @@ +--- +import Box from "@/components/firstdate/Box.astro"; +import Layout from "@/layouts/firstdate/WithoutNavbar.astro"; +--- + + + +
+
+

ลงทะเบียนสำเร็จ

+

+ ขอบคุณที่ลงทะเบียนเข้าร่วมงาน CU First Date! +

+
+
+
+
From 560d6a0425889ac82a273f32449afdaa902b1f0f Mon Sep 17 00:00:00 2001 From: "Dhanavadh T. Saito" <105502355+dhanavadh@users.noreply.github.com> Date: Tue, 15 Jul 2025 05:26:02 +0700 Subject: [PATCH 04/68] Feat: added staff-qr page added staff-qr page --- src/components/staff/QRScanner.tsx | 209 ++++++++++ src/layouts/staff/WithNavbar.astro | 2 +- src/pages/staff/event/index.astro | 639 ++++++++++++++++++++++++++++- src/pages/staff/index.astro | 21 - 4 files changed, 837 insertions(+), 34 deletions(-) create mode 100644 src/components/staff/QRScanner.tsx delete mode 100644 src/pages/staff/index.astro diff --git a/src/components/staff/QRScanner.tsx b/src/components/staff/QRScanner.tsx new file mode 100644 index 0000000..ac607e8 --- /dev/null +++ b/src/components/staff/QRScanner.tsx @@ -0,0 +1,209 @@ +import React, { useCallback, useEffect, useState } from "react"; + +import { Scanner } from "@yudiel/react-qr-scanner"; + + +declare global { + interface Window { + handleQRScan?: (data: string) => Promise; + showErrorModal?: (message: string) => void; + } +} + +interface QRScannerProps { + onScan: (data: string) => void; + onError?: (error: Error) => void; +} + +const QRScanner: React.FC = ({ onScan, onError }) => { + const [permission, setPermission] = useState<"granted" | "denied" | "prompt">( + "prompt" + ); + const [cameraFacing, setCameraFacing] = useState<"environment" | "user">( + "environment" + ); + const [hasCamera, setHasCamera] = useState(true); + + const checkCameraAvailability = useCallback(async (): Promise => { + try { + if (!navigator.mediaDevices?.getUserMedia) { + setHasCamera(false); + return; + } + + const devices = await navigator.mediaDevices.enumerateDevices(); + const videoDevices = devices.filter( + (device): boolean => device.kind === "videoinput" + ); + + if (videoDevices.length === 0) { + setHasCamera(false); + return; + } + + const stream = await navigator.mediaDevices.getUserMedia({ video: true }); + stream.getTracks().forEach((track): void => track.stop()); + setPermission("granted"); + setHasCamera(true); + } catch { + setPermission("denied"); + setHasCamera(false); + } + }, []); + + useEffect(() => { + const isMobile = + /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( + navigator.userAgent + ); + setCameraFacing(isMobile ? "environment" : "user"); + void checkCameraAvailability(); + }, [checkCameraAvailability]); + + const handleScan = useCallback( + async (result: unknown): Promise => { + if (result && Array.isArray(result) && result.length > 0) { + const qrData = (result[0] as { rawValue: string }).rawValue; + const testQRData = + "id=7137ec7e-0f4b-4d65-88c4-982d36f7692f&userId=a8e7e644-ed12-4748-89fc-4599f868ab5d"; + + let finalQRData = qrData; + if ( + qrData.includes("id=7137ec7e-0f4b-4d65-88c4-982d36f7692f") && + qrData.includes("userId=a8e7e644-ed12-4748-89fc-4599f868ab5d") + ) { + finalQRData = testQRData; + } else { + const error = new Error("QR Code ไม่ถูกต้อง: ไม่ใช่ QR Code ของระบบ"); + if (onError) { + onError(error); + } + if (window.showErrorModal) { + window.showErrorModal("QR Code ไม่ถูกต้อง: ไม่ใช่ QR Code ของระบบ"); + } + return; + } + + try { + if (typeof onScan === "function") { + onScan(finalQRData); + } else if ( + window.handleQRScan && + typeof window.handleQRScan === "function" + ) { + await window.handleQRScan(finalQRData); + } + } catch (scanError) { + if (onError) { + onError(scanError as Error); + } + } + } + }, + [onScan, onError] + ); + + const handleError = useCallback( + (error: unknown): void => { + const err = error as Error; + if (err.name === "OverconstrainedError") { + const fallbackFacing = + cameraFacing === "environment" ? "user" : "environment"; + setCameraFacing(fallbackFacing); + return; + } + + if (onError) { + onError(err); + } + }, + [cameraFacing, onError] + ); + + const requestCameraPermission = useCallback(async (): Promise => { + try { + await navigator.mediaDevices.getUserMedia({ video: true }); + setPermission("granted"); + } catch { + setPermission("denied"); + } + }, []); + + const getCameraConstraints = useCallback(() => { + return { + facingMode: { ideal: cameraFacing }, + }; + }, [cameraFacing]); + + if (!hasCamera) { + return ( +
+

+ ไม่พบกล้องหรือไม่สามารถเข้าถึงกล้องได้ +

+

กรุณาใช้อุปกรณ์ที่มีกล้อง

+ +
+ ); + } + + if (permission === "denied") { + return ( +
+

ไม่สามารถเข้าถึงกล้องได้

+

+ กรุณาอนุญาตการเข้าถึงกล้องในการตั้งค่าเบราว์เซอร์ +

+ +
+ ); + } + + if (permission === "prompt") { + return ( +
+

กรุณาอนุญาตการเข้าถึงกล้องเพื่อสแกน QR Code

+ +
+ ); + } + + return ( +
+
+ + +
+
+
+
+
+ ); +}; + +export default QRScanner; diff --git a/src/layouts/staff/WithNavbar.astro b/src/layouts/staff/WithNavbar.astro index 1111986..dc4a193 100644 --- a/src/layouts/staff/WithNavbar.astro +++ b/src/layouts/staff/WithNavbar.astro @@ -31,7 +31,7 @@ const { title, description } = Astro.props;
-
+
diff --git a/src/pages/staff/event/index.astro b/src/pages/staff/event/index.astro index bb827d6..350ed57 100644 --- a/src/pages/staff/event/index.astro +++ b/src/pages/staff/event/index.astro @@ -1,44 +1,659 @@ --- +import ButtonFd from "@/components/common/ButtonFd.astro"; import Popup from "@/components/common/Popup.astro"; import Tabs from "@/components/common/Tabs.astro"; +import Box from "@/components/firstdate/Box.astro"; +import QRScanner from "@/components/staff/QRScanner.tsx"; import Layout from "@/layouts/staff/WithNavbar.astro"; ---
-

ลงทะเบียนงาน

+

สแกน QR เข้างาน

- CU First Date + กำลังโหลด...

+
+

+

+
+
-
+ > +
+ { + if (window.handleQRScan) { + return window.handleQRScan(qrData); + } else { + setTimeout(() => { + if (window.handleQRScan) { + window.handleQRScan(qrData); + } + }, 100); + } + }} + onError={(error) => { + if (window.showErrorModal) { + window.showErrorModal( + error.message || "เกิดข้อผิดพลาดในการสแกน QR Code" + ); + } + }} + /> +
+ +
-
-
- -
+ +
+
+
+
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+ +
+ + ลงทะเบียน + +
+
+ +
+ + + +
+ + diff --git a/src/pages/staff/index.astro b/src/pages/staff/index.astro deleted file mode 100644 index 020c20d..0000000 --- a/src/pages/staff/index.astro +++ /dev/null @@ -1,21 +0,0 @@ ---- -import Box from "@/components/firstdate/Box.astro"; -import Layout from "@/layouts/firstdate/WithoutNavbar.astro"; ---- - - - -
-
-

ลงทะเบียนสำเร็จ

-

- ขอบคุณที่ลงทะเบียนเข้าร่วมงาน CU First Date! -

-
-
-
-
From a5e278f8a144698f61c657ce4362c333f231a9e2 Mon Sep 17 00:00:00 2001 From: Thukdanai Thaothawin Date: Wed, 16 Jul 2025 22:52:33 +0700 Subject: [PATCH 05/68] chore: fix middleware doesn't work --- src/lib/api.ts | 2 +- src/middleware.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/api.ts b/src/lib/api.ts index 69cf678..9666b5c 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1,6 +1,6 @@ // API utility functions const API_BASE_URL = - import.meta.env.PUBLIC_API_URL || "http://localhost:8080/api"; + import.meta.env.PUBLIC_API_URL || "http://localhost:4321/api"; export interface ApiResponse { success: boolean; diff --git a/src/middleware.ts b/src/middleware.ts index 7911e75..44480c3 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -5,7 +5,7 @@ import { nonProtectRoutes } from "@/constants/NonProtectedRoutes"; export const onRequest = defineMiddleware(async (context, next) => { const url = context.url; - + // return next(); console.log(`middleware from ${context.url.pathname}`); const isProtectedRoute = !nonProtectRoutes.some((route) => { From 9846fdade74e7ca6acdbe3bcb88fd3ca1053ac7f Mon Sep 17 00:00:00 2001 From: Thukdanai Thaothawin Date: Thu, 17 Jul 2025 01:14:22 +0700 Subject: [PATCH 06/68] feat: Implement qr scan with api --- src/pages/staff/event/index.astro | 196 +++++++++++++++++++++++------- 1 file changed, 152 insertions(+), 44 deletions(-) diff --git a/src/pages/staff/event/index.astro b/src/pages/staff/event/index.astro index 350ed57..7ff3f73 100644 --- a/src/pages/staff/event/index.astro +++ b/src/pages/staff/event/index.astro @@ -306,52 +306,161 @@ import Layout from "@/layouts/staff/WithNavbar.astro"; diff --git a/src/components/staff/QRScanner.tsx b/src/components/staff/QRScanner.tsx new file mode 100644 index 0000000..9ed0e7c --- /dev/null +++ b/src/components/staff/QRScanner.tsx @@ -0,0 +1,251 @@ +import React, { useCallback, useEffect, useState } from "react"; + +import { Scanner } from "@yudiel/react-qr-scanner"; + + +declare global { + interface Window { + handleQRScan?: (data: string) => Promise; + showErrorModal?: (message: string) => void; + } +} + +interface QRScannerProps { + onScan: (data: string) => void; + onError?: (error: Error) => void; +} + +const QRScanner: React.FC = ({ onScan, onError }) => { + const [permission, setPermission] = useState<"granted" | "denied" | "prompt">( + "prompt" + ); + const [cameraFacing, setCameraFacing] = useState<"environment" | "user">( + "environment" + ); + const [hasCamera, setHasCamera] = useState(true); + const [isScanning, setIsScanning] = useState(true); + const [lastScannedData, setLastScannedData] = useState(null); + + const checkCameraAvailability = useCallback(async (): Promise => { + try { + if (!navigator.mediaDevices?.getUserMedia) { + setHasCamera(false); + return; + } + + const devices = await navigator.mediaDevices.enumerateDevices(); + const videoDevices = devices.filter( + (device): boolean => device.kind === "videoinput" + ); + + if (videoDevices.length === 0) { + setHasCamera(false); + return; + } + + const stream = await navigator.mediaDevices.getUserMedia({ video: true }); + stream.getTracks().forEach((track): void => track.stop()); + setPermission("granted"); + setHasCamera(true); + } catch { + setPermission("denied"); + setHasCamera(false); + } + }, []); + + useEffect(() => { + const isMobile = + /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( + navigator.userAgent + ); + setCameraFacing(isMobile ? "environment" : "user"); + void checkCameraAvailability(); + }, [checkCameraAvailability]); + + const handleScan = useCallback( + async (result: unknown): Promise => { + if ( + !isScanning || + !result || + !Array.isArray(result) || + result.length === 0 + ) { + return; + } + + const qrData = (result[0] as { rawValue: string }).rawValue; + + // Prevent scanning the same QR code immediately + if (qrData === lastScannedData) { + return; + } + + // Temporarily disable scanning to prevent multiple rapid scans + setIsScanning(false); + setLastScannedData(qrData); + + // Accept QR codes with studentId and citizenId format + let finalQRData = qrData; + if (qrData.includes("studentId=") && qrData.includes("citizenId=")) { + finalQRData = qrData; + } else if ( + qrData.includes("id=7137ec7e-0f4b-4d65-88c4-982d36f7692f") && + qrData.includes("userId=a8e7e644-ed12-4748-89fc-4599f868ab5d") + ) { + // Keep the old test QR support + finalQRData = + "id=7137ec7e-0f4b-4d65-88c4-982d36f7692f&userId=a8e7e644-ed12-4748-89fc-4599f868ab5d"; + } else { + const error = new Error("QR Code ไม่ถูกต้อง"); + if (onError) { + onError(error); + } + if (window.showErrorModal) { + window.showErrorModal("QR Code ไม่ถูกต้อง"); + } + + // Re-enable scanning after error + setTimeout(() => { + setIsScanning(true); + setLastScannedData(null); + }, 1000); + return; + } + + try { + if (typeof onScan === "function") { + onScan(finalQRData); + } else if ( + window.handleQRScan && + typeof window.handleQRScan === "function" + ) { + await window.handleQRScan(finalQRData); + } + } catch (scanError) { + if (onError) { + onError(scanError as Error); + } + } finally { + // Re-enable scanning after processing (with delay to prevent immediate re-scan) + setTimeout(() => { + setIsScanning(true); + setLastScannedData(null); + }, 2000); // 2-second delay before allowing next scan + } + }, + [isScanning, lastScannedData, onScan, onError] + ); + + const handleError = useCallback( + (error: unknown): void => { + const err = error as Error; + if (err.name === "OverconstrainedError") { + const fallbackFacing = + cameraFacing === "environment" ? "user" : "environment"; + setCameraFacing(fallbackFacing); + return; + } + + if (onError) { + onError(err); + } + }, + [cameraFacing, onError] + ); + + const requestCameraPermission = useCallback(async (): Promise => { + try { + await navigator.mediaDevices.getUserMedia({ video: true }); + setPermission("granted"); + } catch { + setPermission("denied"); + } + }, []); + + const getCameraConstraints = useCallback(() => { + return { + facingMode: { ideal: cameraFacing }, + }; + }, [cameraFacing]); + + if (!hasCamera) { + return ( +
+

+ ไม่พบกล้องหรือไม่สามารถเข้าถึงกล้องได้ +

+

กรุณาใช้อุปกรณ์ที่มีกล้อง

+ +
+ ); + } + + if (permission === "denied") { + return ( +
+

ไม่สามารถเข้าถึงกล้องได้

+

+ กรุณาอนุญาตการเข้าถึงกล้องในการตั้งค่าเบราว์เซอร์ +

+ +
+ ); + } + + if (permission === "prompt") { + return ( +
+

กรุณาอนุญาตการเข้าถึงกล้องเพื่อสแกน QR Code

+ +
+ ); + } + + return ( +
+
+ {isScanning ? ( + + ) : ( +
+
+
กำลังประมวลผล...
+
+
+
+ )} + +
+
+
+
+
+ ); +}; + +export default QRScanner; diff --git a/src/layouts/staff/WithNavbar.astro b/src/layouts/staff/WithNavbar.astro new file mode 100644 index 0000000..dc4a193 --- /dev/null +++ b/src/layouts/staff/WithNavbar.astro @@ -0,0 +1,37 @@ +--- +import "@/styles/global.css"; +import Footer from "@firstdate/Footer.astro"; +import Navbar from "@firstdate/Navbar.astro"; +import { ClientRouter } from "astro:transitions"; + +interface Props { + title?: string; + description?: string; +} + +const { title, description } = Astro.props; +--- + + + + + + + + + {title} + + + + +
+ +
+ +
+
+
+ + diff --git a/src/lib/api.ts b/src/lib/api.ts index 0c758b2..b21ecc8 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1,6 +1,6 @@ // API utility functions const API_BASE_URL = - import.meta.env.PUBLIC_API_URL || "http://localhost:8080/api"; + import.meta.env.PUBLIC_API_URL || "http://localhost:4321/api"; export interface ApiResponse { success: boolean; @@ -20,6 +20,25 @@ export class ApiError extends Error { } } +function getAuthToken(): string | null { + if (typeof window !== "undefined") { + const localToken = localStorage.getItem("auth_token"); + if (localToken) { + return localToken; + } + + const cookies = document.cookie.split("; "); + const tokenCookie = cookies.find((cookie) => cookie.startsWith("token=")); + return tokenCookie ? decodeURIComponent(tokenCookie.split("=")[1]) : null; + } + + return null; +} + +function getAuthHeaders(): Record { + const token = getAuthToken(); + return token ? { Authorization: `Bearer ${token}` } : {}; +} interface ApiResponseRaw { message?: string; error?: string; @@ -31,8 +50,11 @@ export async function apiRequest( ): Promise> { const url = `${API_BASE_URL}${endpoint}`; + const authHeaders = getAuthHeaders(); + const defaultHeaders = { "Content-Type": "application/json", + ...authHeaders, }; const requestOptions: RequestInit = { @@ -50,10 +72,11 @@ export async function apiRequest( let errorMsg = "Request failed"; if (response.body) { try { - const raw: ApiResponseRaw = JSON.parse(await response.text()); + const responseText = await response.text(); + const raw: ApiResponseRaw = JSON.parse(responseText); errorMsg = raw.error || raw.message || errorMsg; } catch { - errorMsg = await response.text(); + errorMsg = "Request failed"; } } throw new ApiError(errorMsg, response.status, response); @@ -62,8 +85,6 @@ export async function apiRequest( const data = await response.json(); return { success: true, data }; } catch (error) { - console.error("API request error:", error); - if (error instanceof ApiError) { return { success: false, error: error.message }; } diff --git a/src/lib/eventAPI.ts b/src/lib/eventAPI.ts index 0870730..8852c96 100644 --- a/src/lib/eventAPI.ts +++ b/src/lib/eventAPI.ts @@ -246,3 +246,57 @@ export const registerForEvent = async ( } } }; + +export const staffQRScanRegister = async ( + studentId: string, + citizenId: string +): Promise<{ + success: boolean; + data?: CheckinResponse; + error?: string; + needsLogin?: boolean; +}> => { + const token = getAuthToken(); + if (!token) { + return { + success: false, + error: "กรุณาเข้าสู่ระบบก่อน", + needsLogin: true, + }; + } + + const response = await api.post( + `/checkin/registerByStudentId`, + { + studentId, + citizenId, + }, + { headers: getAuthHeaders() } + ); + + if (response.success && response.data) { + return { success: true, data: response.data }; + } else { + const errorMessage = response.error || "QR scan registration failed"; + + if (errorMessage.includes("already exists")) { + return { success: false, error: "นิสิตคนนี้ได้ลงทะเบียนไปแล้ว" }; + } else if (errorMessage.includes("before register period")) { + return { success: false, error: "ยังไม่ถึงเวลาลงทะเบียน" }; + } else if (errorMessage.includes("after register period")) { + return { success: false, error: "หมดเวลาลงทะเบียนแล้ว" }; + } else if ( + errorMessage.includes("401") || + errorMessage.includes("unauthorized") + ) { + return { success: false, error: "กรุณาเข้าสู่ระบบก่อน" }; + } else if ( + errorMessage.includes("404") || + errorMessage.includes("not found") + ) { + return { success: false, error: "ไม่พบข้อมูลนิสิต" }; + } else { + return { success: false, error: errorMessage }; + } + } +}; diff --git a/src/pages/fest/ButtonProp/index.astro b/src/pages/fest/ButtonProp/index.astro index 94b154c..a7ee8bb 100644 --- a/src/pages/fest/ButtonProp/index.astro +++ b/src/pages/fest/ButtonProp/index.astro @@ -5,48 +5,51 @@ const questItems = [ { description: "เดินเดินจุ๊บๆ", questLabel: "Quest 3.1", - href: "/fest/quest-3-1" + href: "/fest/quest-3-1", }, { description: "เดินสามย่าน สายกินสายมู", questLabel: "Quest 3.2", - href: "/fest/quest-3-2" + href: "/fest/quest-3-2", }, { description: "Birdwalk", questLabel: "Quest 3.3", - href: "/fest/quest-3-3" + href: "/fest/quest-3-3", }, { description: "Trashvenger", questLabel: "Quest 3.4", - href: "/fest/quest-3-4" + href: "/fest/quest-3-4", }, { description: "Plantwalk", questLabel: "Quest 3.5", - href: "/fest/quest-3-5" + href: "/fest/quest-3-5", }, ]; --- -
{/* Added mx-auto and p-4 for centering within the overall page layout */} -

รายละเอียด Quest ย่อย

-
- {questItems.map(item => ( -
-

- {item.description} -

+
+ {/* Added mx-auto and p-4 for centering within the overall page layout */} +

+ รายละเอียด Quest ย่อย +

- {/* Explicitly set size="small" to use the predefined width */} - -

- {item.questLabel} +

+ { + questItems.map((item) => ( +
+

+ {item.description}

- -
- ))} -
-
\ No newline at end of file + {/* Explicitly set size="small" to use the predefined width */} + +

{item.questLabel}

+
+
+ )) + } +
+
diff --git a/src/pages/fest/quest-5.astro b/src/pages/fest/quest-5.astro index 95c3005..d14258c 100644 --- a/src/pages/fest/quest-5.astro +++ b/src/pages/fest/quest-5.astro @@ -84,7 +84,7 @@ const eventDetails = [