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/77] 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/77] 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/77] 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/77] 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/77] 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/77] 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 = [
    { - eventDetails.map((item, index) => ( + eventDetails.map((item) => (
  • {item} diff --git a/src/pages/firstdate/events/[eventId]/index.astro b/src/pages/firstdate/events/[eventId]/index.astro index cf559c4..cf14b66 100644 --- a/src/pages/firstdate/events/[eventId]/index.astro +++ b/src/pages/firstdate/events/[eventId]/index.astro @@ -28,7 +28,20 @@ function parseCookie(cookie: string): Record { const cookie = Astro.request.headers.get("cookie") ?? ""; const authToken = parseCookie(cookie).token; -const eventStatus = await getEventStatus(eventType, authToken); + +let eventStatus; +try { + eventStatus = await getEventStatus(eventType, authToken); +} catch (error) { + console.error("Error getting event status:", error); + // Default to not registered if there's an error + eventStatus = { + isRegistered: false, + isLate: false, + isComingSoon: false, + }; +} + const justRegistered = new URL(Astro.request.url).searchParams.get("registered") === "true"; diff --git a/src/pages/staff/event/index.astro b/src/pages/staff/event/index.astro index 0f5ff58..e756505 100644 --- a/src/pages/staff/event/index.astro +++ b/src/pages/staff/event/index.astro @@ -1,18 +1,322 @@ --- -import StaffWithNavbar from "@/layouts/firstdate/StaffWithNavbar.astro"; +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 เข้างาน

    +

    + กำลังโหลด... +

    + +
    +

    +

    +
    + +
    + +
    +
    + +
    + { + 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" + ); + } + }} + /> +
    + +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    + + ลงทะเบียน + +
    +
    + +
    +
    +
    +
    +
    +
    + + + + + + + + +

    +
    @@ -182,7 +193,7 @@ const member2 = groupData.memberCount > 2 ? groupData.users[2] : null;

    - จับกลุ่มเพื่อน (2/3) + {`จับกลุ่มเพื่อน (${groupData.memberCount}/3)`}

    2 ? groupData.users[2] : null;
    - - - + + +
    @@ -279,14 +290,16 @@ const member2 = groupData.memberCount > 2 ? groupData.users[2] : null;

    -
    - +
    -

    {headName}

    -

    {headId}

    +

    +

    @@ -336,7 +349,7 @@ const member2 = groupData.memberCount > 2 ? groupData.users[2] : null;

    คุณได้จับกลุ่มกับ

    -

    {headName}

    +

    สำเร็จแล้ว

    @@ -354,6 +367,9 @@ const member2 = groupData.memberCount > 2 ? groupData.users[2] : null; From 96e5daf79b0d444bdb2e26ba2649c45cc8542efd Mon Sep 17 00:00:00 2001 From: neennera Date: Sat, 19 Jul 2025 16:03:43 +0700 Subject: [PATCH 15/77] fix: qrcode --- src/components/staff/QRScanner.tsx | 125 +++++++++++++++++++---------- 1 file changed, 82 insertions(+), 43 deletions(-) diff --git a/src/components/staff/QRScanner.tsx b/src/components/staff/QRScanner.tsx index cde8898..9ed0e7c 100644 --- a/src/components/staff/QRScanner.tsx +++ b/src/components/staff/QRScanner.tsx @@ -23,6 +23,8 @@ const QRScanner: React.FC = ({ onScan, onError }) => { "environment" ); const [hasCamera, setHasCamera] = useState(true); + const [isScanning, setIsScanning] = useState(true); + const [lastScannedData, setLastScannedData] = useState(null); const checkCameraAvailability = useCallback(async (): Promise => { try { @@ -62,48 +64,76 @@ const QRScanner: React.FC = ({ onScan, onError }) => { const handleScan = useCallback( async (result: unknown): Promise => { - if (result && Array.isArray(result) && result.length > 0) { - const qrData = (result[0] as { rawValue: string }).rawValue; + if ( + !isScanning || + !result || + !Array.isArray(result) || + result.length === 0 + ) { + return; + } - // Accept QR codes with studentId and citizenId format - let finalQRData = qrData; - if (qrData.includes("studentId=") && qrData.includes("citizenId=")) { - finalQRData = qrData; + 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 ( - qrData.includes("id=7137ec7e-0f4b-4d65-88c4-982d36f7692f") && - qrData.includes("userId=a8e7e644-ed12-4748-89fc-4599f868ab5d") + window.handleQRScan && + typeof window.handleQRScan === "function" ) { - // 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 ไม่ถูกต้อง"); - } - return; + await window.handleQRScan(finalQRData); } - - 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); - } + } 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 } }, - [onScan, onError] + [isScanning, lastScannedData, onScan, onError] ); const handleError = useCallback( @@ -192,14 +222,23 @@ const QRScanner: React.FC = ({ onScan, onError }) => { return (
    - + {isScanning ? ( + + ) : ( +
    +
    +
    กำลังประมวลผล...
    +
    +
    +
    + )}
    From f6e264a7f2d022fb1d0aa8846a668586b1457675 Mon Sep 17 00:00:00 2001 From: neennera Date: Fri, 18 Jul 2025 01:08:26 +0700 Subject: [PATCH 16/77] add : checkin not found & remove freshmennight --- src/lib/eventAPI.ts | 5 ++++- src/pages/staff/event/index.astro | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/lib/eventAPI.ts b/src/lib/eventAPI.ts index 8852c96..ab34b71 100644 --- a/src/lib/eventAPI.ts +++ b/src/lib/eventAPI.ts @@ -154,7 +154,10 @@ export const getEventStatus = async ( const errorMessage = response.error; // Check for specific error patterns from backend - if (errorMessage.includes("before register period")) { + if ( + errorMessage.includes("before register period") || + errorMessage.includes("Check-in not found") + ) { return { isRegistered: false, isLate: false, diff --git a/src/pages/staff/event/index.astro b/src/pages/staff/event/index.astro index e756505..4edaa2f 100644 --- a/src/pages/staff/event/index.astro +++ b/src/pages/staff/event/index.astro @@ -539,7 +539,7 @@ import Layout from "@/layouts/staff/WithNavbar.astro"; // } async function determineActiveEvent(): Promise { - const events: EventType[] = ["rpkm", "firstdate", "freshmen-night"]; + const events: EventType[] = ["rpkm", "firstdate"]; for (const eventId of events) { try { From d0acd9f463d810e5a0d37a5f44a7f2bb302a7d02 Mon Sep 17 00:00:00 2001 From: BadLuckZ Date: Fri, 18 Jul 2025 01:18:31 +0700 Subject: [PATCH 17/77] feat: joinGroup and leaveGroup --- .../rpkm/components/group-picker.astro | 42 ++++++++------ src/lib/groupAPI.ts | 58 +++++++++++++++++++ 2 files changed, 81 insertions(+), 19 deletions(-) diff --git a/src/components/rpkm/components/group-picker.astro b/src/components/rpkm/components/group-picker.astro index e842fe5..60dcfcd 100644 --- a/src/components/rpkm/components/group-picker.astro +++ b/src/components/rpkm/components/group-picker.astro @@ -367,7 +367,7 @@ const member2 = groupData.memberCount > 2 ? groupData.users[2] : null; diff --git a/src/components/rpkm/components/house-picker.astro b/src/components/rpkm/components/house-picker.astro index b25553b..ac280e5 100644 --- a/src/components/rpkm/components/house-picker.astro +++ b/src/components/rpkm/components/house-picker.astro @@ -14,6 +14,7 @@ import HousePickingCard from "@rpkm/elements/house-picking-card.astro"; import Divider from "@/components/common/Divider.astro"; import Frame from "@/components/common/Frame.astro"; import { getGroupData } from "@/lib/groupAPI"; +import { getProfile } from "@/lib/profileAPI"; import type { Group } from "@/types/common"; const houseSizes = ["ทั้งหมด", "S", "M", "L", "XL", "XXL"]; @@ -27,7 +28,9 @@ let housePrefData: HousePrefType = { houseRank5: null, houseRankSub: null, }; + let groupData: Group | null = null; +let user: User | null = null; const token = Astro.cookies.get("token")?.value; if (!token) { @@ -35,12 +38,18 @@ if (!token) { } else { houseDisplayData = await getHouses(token); housePrefData = await getPrefHouses(token); + const userResponse = await getProfile(token); + if (userResponse.success && userResponse.data) { + user = userResponse.data.user as User; + } const groupDataResponse = await getGroupData(token); if (groupDataResponse.success && groupDataResponse.data) { groupData = groupDataResponse.data.data as Group; } } const isConfirmed = groupData ? groupData.isConfirmed || false : false; +const isOwner = + groupData && user ? groupData.ownerId == user.id || false : false; --- @@ -248,90 +257,105 @@ const isConfirmed = groupData ? groupData.isConfirmed || false : false;

    +
    - - -
    + + // Handle house card click in popup + houseCardContainer?.addEventListener("click", (e) => { + const wrapper = e.target.closest(".house-card-wrapper"); + if (!wrapper) return; + + e.preventDefault(); + e.stopPropagation(); + + const nameThai = wrapper.getAttribute("data-thname"); + selectedHouseData = houseDisplayData.find((h) => h.nameThai === nameThai); + + // Prevent showing popup for already selected houses + if (!selectedHouseData || selectedHouses.has(selectedHouseData.id)) + return; + + // Show house popup with data + showHousePopup(selectedHouseData); + }); + + // Cancel button + btnCancelHouse?.addEventListener("click", () => { + housePopup?.classList.add("hidden"); + housePopup?.classList.remove("flex"); + selectedHouseData = null; + }); + + // Add house button + btnAddHouse?.addEventListener("click", () => { + if (!selectedHouseData || !currentRank) return; + + const nameThai = selectedHouseData.nameThai; + const houseObj = houseDisplayData.find((h) => h.nameThai === nameThai); + + // Prevent adding already selected houses + if (selectedHouses.has(houseObj.id)) return; + + // ถ้าอันดับนี้มีบ้านอยู่แล้ว ให้ลบบ้านเก่าออกจาก selectedHouses + if (pickedHouses[currentRank]) { + selectedHouses.delete(pickedHouses[currentRank]); + } + + // Update state - เลือกไปที่ rank ที่ user กดไว้ + pickedHouses[currentRank] = houseObj.id; + + selectedHouses.add(houseObj.id); + + // Update UI + updatePickedHousesDisplay(); + updateAvailableHouses(); + updateHouseCounts(); + + // Save to localStorage + saveHouseSelection(); + + // Close both popups + housePopup?.classList.add("hidden"); + housePopup?.classList.remove("flex"); + closePopup(); + + // Enable submit button if all main ranks are filled + const mainRanks = [ + "houseRank1", + "houseRank2", + "houseRank3", + "houseRank4", + "houseRank5", + ]; + const allMainRanksFilled = mainRanks.every( + (rank) => pickedHouses[rank] !== null + ); + submitBtn.disabled = !allMainRanksFilled; + + // Reset selected house data + selectedHouseData = null; + }); + }); + From 49270cb569567f8c61809cdcf017b2779d8aac6a Mon Sep 17 00:00:00 2001 From: BadLuckZ Date: Fri, 18 Jul 2025 23:40:21 +0700 Subject: [PATCH 45/77] fix: Bugs from PM --- src/components/rpkm/components/house-picker.astro | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/rpkm/components/house-picker.astro b/src/components/rpkm/components/house-picker.astro index ac280e5..621375c 100644 --- a/src/components/rpkm/components/house-picker.astro +++ b/src/components/rpkm/components/house-picker.astro @@ -550,7 +550,7 @@ const isOwner = }); } else { submitBtn.innerHTML = `

    ยืนยันการเลือกบ้าน

    `; - submitBtn.disabled = false; + submitBtn.disabled = !allMainRanksFilled; } // If there are picked houses, show picking view @@ -956,7 +956,7 @@ const isOwner = // Handle house picking card click document.addEventListener("click", (e) => { if (!isOwner) return; - + if (isSubmitted) return; if (isConfirmed) return; const btn = e.target.closest("[data-rank]"); @@ -981,6 +981,7 @@ const isOwner = // Handle bin icon click document.addEventListener("click", (e) => { if (!isOwner) return; + if (isSubmitted) return; if (isConfirmed) return; const binBtn = e.target.closest('img[src*="bin-icon.svg"]'); @@ -995,7 +996,8 @@ const isOwner = if (!loadingIcon) { loadingIcon = document.createElement("img"); loadingIcon.src = "/images/rpkm/loading-icon.png"; - loadingIcon.className = "loading-icon w-5 h-5 animate-spin"; + loadingIcon.className = + "loading-icon w-5 h-5 animate-spin cursor-default"; bin.parentNode.appendChild(loadingIcon); } loadingIcon.style.display = "inline-block"; From 72c2ba97b3244a8ebbd950c528e92f80d741c67c Mon Sep 17 00:00:00 2001 From: BadLuckZ Date: Sat, 19 Jul 2025 02:26:30 +0700 Subject: [PATCH 46/77] fix: Bugs Again --- .../rpkm/components/group-picker.astro | 19 ++++++------ .../rpkm/components/house-picker.astro | 29 ++++++++++++++----- 2 files changed, 32 insertions(+), 16 deletions(-) diff --git a/src/components/rpkm/components/group-picker.astro b/src/components/rpkm/components/group-picker.astro index 7189be1..bf42692 100644 --- a/src/components/rpkm/components/group-picker.astro +++ b/src/components/rpkm/components/group-picker.astro @@ -54,7 +54,9 @@ const member2 = groupData.memberCount > 2 ? groupData.users[2] : null; > -

    จับกลุ่มเพื่อน (1/3)

    +

    + {`จับกลุ่มเพื่อน (${memberCount}/3)`} +

    @@ -121,7 +123,7 @@ const member2 = groupData.memberCount > 2 ? groupData.users[2] : null;

    {"<"} @@ -398,17 +400,10 @@ const member2 = groupData.memberCount > 2 ? groupData.users[2] : null; const { user, groupData } = await getInformation(); - console.log("Group Data:", groupData); - console.log("User Data:", user); - const isConfirmed = groupData ? groupData.isConfirmed : undefined; const isOwner = groupData && user ? groupData.ownerId === user.id : undefined; const memberCount = groupData ? groupData.memberCount : -1; - console.log("isConfirmed:", isConfirmed); - console.log("isOwner:", isOwner); - console.log("memberCount:", memberCount); - const input = document.getElementById("room-id-insert") as HTMLInputElement; const headGroupBtn = document.getElementById( @@ -664,6 +659,12 @@ const member2 = groupData.memberCount > 2 ? groupData.users[2] : null; backBtn?.classList.remove("hidden"); } + if (memberCount !== 1) { + backBtn?.classList.add("hidden"); + } else { + backBtn?.classList.remove("hidden"); + } + backBtn?.addEventListener("click", () => { if (!isConfirmed) { groupDefaultView?.classList.remove("hidden"); diff --git a/src/components/rpkm/components/house-picker.astro b/src/components/rpkm/components/house-picker.astro index 621375c..1b7029f 100644 --- a/src/components/rpkm/components/house-picker.astro +++ b/src/components/rpkm/components/house-picker.astro @@ -50,6 +50,15 @@ if (!token) { const isConfirmed = groupData ? groupData.isConfirmed || false : false; const isOwner = groupData && user ? groupData.ownerId == user.id || false : false; + +const isPickAllMainHouses = + housePrefData.houseRank1 && + housePrefData.houseRank2 && + housePrefData.houseRank3 && + housePrefData.houseRank4 && + housePrefData.houseRank5; + +console.log("Is Confirmed:", isConfirmed); --- @@ -141,6 +150,7 @@ const isOwner = id="btn-submit-house" class="cut-edge-all flex cursor-pointer items-center justify-center gap-2 bg-[linear-gradient(90deg,_#8A2BE4_0%,_#2B2D42_50%,_#8A2BE4_100%)] disabled:cursor-not-allowed disabled:opacity-50" type="button" + disabled={!isPickAllMainHouses} style={`width: 260px; height: 48px;`} >

    ยืนยันการเลือกบ้าน

    @@ -510,7 +520,6 @@ const isOwner = isConfirmed: false, }; } - // Removed localStorage fallback if (selectionData) { // Restore picked houses @@ -541,6 +550,10 @@ const isOwner = ); submitBtn.disabled = !allMainRanksFilled; + const hasAnyMainHouse = mainRanks.some( + (rank) => pickedHouses[rank] !== null + ); + // If submitted, update button and disable interactions if (isConfirmed) { submitBtn.innerHTML = `

    ส่งการเลือกบ้านแล้ว

    `; @@ -548,15 +561,22 @@ const isOwner = document.querySelectorAll("[data-rank]").forEach((card) => { card.classList.add("pointer-events-none"); }); + submitBtn.classList.add("cursor-not-allowed", "opacity-50"); + submitBtn.classList.remove("cursor-pointer", "opacity-100"); } else { submitBtn.innerHTML = `

    ยืนยันการเลือกบ้าน

    `; submitBtn.disabled = !allMainRanksFilled; } // If there are picked houses, show picking view - if (Object.values(pickedHouses).some((house) => house !== null)) { + if (hasAnyMainHouse) { notPickingView?.classList.add("hidden"); pickingView?.classList.remove("hidden"); + submitBtn.disabled = !allMainRanksFilled; + } else { + notPickingView?.classList.remove("hidden"); + pickingView?.classList.add("hidden"); + submitBtn.disabled = true; } } } catch (error) { @@ -889,11 +909,6 @@ const isOwner = updateHouseCounts(); }, 100); - // Add reset function for development/testing - window.resetHouseSelection = function () { - // Removed clearHouseSelection and reload - }; - // Add confirmation function for popup window.confirmHouseSubmission = function () { isSubmitted = true; From 83accf580ac0af9d4d970cfce3718476e0632071 Mon Sep 17 00:00:00 2001 From: BadLuckZ Date: Sat, 19 Jul 2025 02:46:23 +0700 Subject: [PATCH 47/77] fix: Bugs Again... --- .../rpkm/components/group-picker.astro | 490 +++++++++--------- .../rpkm/components/house-picker.astro | 2 - 2 files changed, 251 insertions(+), 241 deletions(-) diff --git a/src/components/rpkm/components/group-picker.astro b/src/components/rpkm/components/group-picker.astro index bf42692..d7ee0ac 100644 --- a/src/components/rpkm/components/group-picker.astro +++ b/src/components/rpkm/components/group-picker.astro @@ -122,12 +122,17 @@ const member2 = groupData.memberCount > 2 ? groupData.users[2] : null;
    -

    - {"<"} -

    + { + !isConfirmed && memberCount === 1 && ( +

    + {"<"} +

    + ) + } +

    {`จับกลุ่มเพื่อน (${memberCount}/3)`}

    @@ -390,163 +395,169 @@ const member2 = groupData.memberCount > 2 ? groupData.users[2] : null; import { getGroupData } from "@/lib/groupAPI"; import { getProfile } from "@/lib/profileAPI"; - async function getInformation() { - const resProfile = await getProfile(); - const resGroup = await getGroupData(); - const user = resProfile.data?.user ?? null; - const groupData = resGroup.data?.data ?? null; - return { user, groupData }; - } + document.addEventListener("DOMContentLoaded", async () => { + async function getInformation() { + const resProfile = await getProfile(); + const resGroup = await getGroupData(); + const user = resProfile.data?.user ?? null; + const groupData = resGroup.data?.data ?? null; + return { user, groupData }; + } - const { user, groupData } = await getInformation(); - - const isConfirmed = groupData ? groupData.isConfirmed : undefined; - const isOwner = groupData && user ? groupData.ownerId === user.id : undefined; - const memberCount = groupData ? groupData.memberCount : -1; - - const input = document.getElementById("room-id-insert") as HTMLInputElement; - - const headGroupBtn = document.getElementById( - "btn-head-group" - ) as HTMLButtonElement; - const backBtn = document.getElementById("btn-back") as HTMLButtonElement; - const joinGroupButton = document.getElementById( - "btn-join-group" - ) as HTMLButtonElement; - const submitGroupButton = document.getElementById( - "btn-submit-group" - ) as HTMLButtonElement; - const cancelGroupButton = document.getElementById( - "btn-cancel-group" - ) as HTMLButtonElement; - const closeGroupPopupButton = document.getElementById( - "btn-close-popup" - ) as HTMLButtonElement; - const exitButton = document.getElementById("btn-exit") as HTMLButtonElement; - - const groupDefaultView = document.getElementById("view-group-default"); - const groupHeadView = document.getElementById("view-group-head"); - const groupMemberView = document.getElementById("view-group-member"); - const groupPopupView = document.getElementById("view-group-popup"); - const groupPopupSuccessView = document.getElementById( - "view-group-success-popup" - ); - - const roomInviteHeadContainer = document.getElementById( - "room-invite-url-head-container" - ); - const roomCodeHeadContainer = document.getElementById( - "room-code-head-container" - ); - const roomInviteMemberContainer = document.getElementById( - "room-invite-url-member-container" - ); - const roomCodeMemberContainer = document.getElementById( - "room-code-member-container" - ); - const errorRoomMessage = document.getElementById( - "error-room-message" - ) as HTMLParagraphElement; - - const targetHeadName = document.getElementById( - "target-head-name" - ) as HTMLParagraphElement; - const targetHeadId = document.getElementById( - "target-head-id" - ) as HTMLParagraphElement; - - const targetHeadNamePopup = document.getElementById( - "target-head-name-popup" - ) as HTMLParagraphElement; - - let targetGroupData: any = null; - let inviteCode = ""; - - function formatRoomId(raw: string): string { - return raw.toUpperCase().replace(/[^A-Z0-9]/g, ""); - } + const { user, groupData } = await getInformation(); + + const isConfirmed = groupData ? groupData.isConfirmed : undefined; + const isOwner = + groupData && user ? groupData.ownerId === user.id : undefined; + const memberCount = groupData ? groupData.memberCount : -1; + + const input = document.getElementById("room-id-insert") as HTMLInputElement; + + const headGroupBtn = document.getElementById( + "btn-head-group" + ) as HTMLButtonElement; + const backBtn = document.getElementById("btn-back") as HTMLButtonElement; + const joinGroupButton = document.getElementById( + "btn-join-group" + ) as HTMLButtonElement; + const submitGroupButton = document.getElementById( + "btn-submit-group" + ) as HTMLButtonElement; + const cancelGroupButton = document.getElementById( + "btn-cancel-group" + ) as HTMLButtonElement; + const closeGroupPopupButton = document.getElementById( + "btn-close-popup" + ) as HTMLButtonElement; + const exitButton = document.getElementById("btn-exit") as HTMLButtonElement; + + const groupDefaultView = document.getElementById("view-group-default"); + const groupHeadView = document.getElementById("view-group-head"); + const groupMemberView = document.getElementById("view-group-member"); + const groupPopupView = document.getElementById("view-group-popup"); + const groupPopupSuccessView = document.getElementById( + "view-group-success-popup" + ); + + const roomInviteHeadContainer = document.getElementById( + "room-invite-url-head-container" + ); + const roomCodeHeadContainer = document.getElementById( + "room-code-head-container" + ); + const roomInviteMemberContainer = document.getElementById( + "room-invite-url-member-container" + ); + const roomCodeMemberContainer = document.getElementById( + "room-code-member-container" + ); + const errorRoomMessage = document.getElementById( + "error-room-message" + ) as HTMLParagraphElement; + + const targetHeadName = document.getElementById( + "target-head-name" + ) as HTMLParagraphElement; + const targetHeadId = document.getElementById( + "target-head-id" + ) as HTMLParagraphElement; + + const targetHeadNamePopup = document.getElementById( + "target-head-name-popup" + ) as HTMLParagraphElement; + + let targetGroupData: any = null; + let inviteCode = ""; + + function formatRoomId(raw: string): string { + return raw.toUpperCase().replace(/[^A-Z0-9]/g, ""); + } - function toggleSubmitButton() { - if (!input || !joinGroupButton || !errorRoomMessage) return; + function toggleSubmitButton() { + if (!input || !joinGroupButton || !errorRoomMessage) return; - if (document.activeElement === input) { - errorRoomMessage.innerHTML = ""; - } + if (document.activeElement === input) { + errorRoomMessage.innerHTML = ""; + } - // Input Validation - const formattedRoomId = formatRoomId(input.value); - input.value = formattedRoomId; + // Input Validation + const formattedRoomId = formatRoomId(input.value); + input.value = formattedRoomId; - const isValid = formattedRoomId.length === 6; + const isValid = formattedRoomId.length === 6; - joinGroupButton.disabled = !isValid; + joinGroupButton.disabled = !isValid; - if (isValid) { - joinGroupButton.classList.remove( - "brightness-75", - "opacity-50", - "cursor-not-allowed" - ); - joinGroupButton.classList.add("cursor-pointer"); + if (isValid) { + joinGroupButton.classList.remove( + "brightness-75", + "opacity-50", + "cursor-not-allowed" + ); + joinGroupButton.classList.add("cursor-pointer"); - const outputURL = document.getElementById( - "room-invite-url-member" - ) as HTMLSpanElement; - const outputCode = document.getElementById( - "room-code-member" - ) as HTMLSpanElement; + const outputURL = document.getElementById( + "room-invite-url-member" + ) as HTMLSpanElement; + const outputCode = document.getElementById( + "room-code-member" + ) as HTMLSpanElement; - outputURL.textContent = "rpkm.sgcu.in.th/" + input.value; - outputCode.textContent = input.value; - } else { - joinGroupButton.classList.add( - "brightness-75", - "opacity-50", - "cursor-not-allowed" - ); - joinGroupButton.classList.remove("cursor-pointer"); + outputURL.textContent = "rpkm.sgcu.in.th/" + input.value; + outputCode.textContent = input.value; + } else { + joinGroupButton.classList.add( + "brightness-75", + "opacity-50", + "cursor-not-allowed" + ); + joinGroupButton.classList.remove("cursor-pointer"); + } } - } - input?.addEventListener("input", toggleSubmitButton); - toggleSubmitButton(); - - headGroupBtn?.addEventListener("click", () => { - groupDefaultView?.classList.add("hidden"); - groupHeadView?.classList.remove("hidden"); - input.value = ""; + input?.addEventListener("input", toggleSubmitButton); toggleSubmitButton(); - }); - - joinGroupButton?.addEventListener("click", async () => { - if (!input.value) return; - - inviteCode = formatRoomId(input.value); - if (inviteCode.length !== 6) return; - errorRoomMessage.innerHTML = ""; - - // Add loading state - joinGroupButton.disabled = true; - joinGroupButton.textContent = "กำลังค้นหา..."; + headGroupBtn?.addEventListener("click", () => { + groupDefaultView?.classList.add("hidden"); + groupHeadView?.classList.remove("hidden"); + input.value = ""; + toggleSubmitButton(); + }); - try { - const response = await getGroupByInviteCode(inviteCode); - if (response.success && response.data) { - targetGroupData = response.data.data; + joinGroupButton?.addEventListener("click", async () => { + if (!input.value) return; - const profileContainer = document.getElementById("target-head-profile"); + inviteCode = formatRoomId(input.value); + if (inviteCode.length !== 6) return; - if (profileContainer && targetGroupData.owner) { - const avatarSrc = - targetGroupData.owner?.avatarId && - AvatarMap[targetGroupData.owner.avatarId as keyof typeof AvatarMap] - ? AvatarMap[ - targetGroupData.owner.avatarId as keyof typeof AvatarMap - ] - : "/images/rpkm/profile/profile-unknown.png"; + errorRoomMessage.innerHTML = ""; - profileContainer.innerHTML = ` + // Add loading state + joinGroupButton.disabled = true; + joinGroupButton.textContent = "กำลังค้นหา..."; + + try { + const response = await getGroupByInviteCode(inviteCode); + if (response.success && response.data) { + targetGroupData = response.data.data; + + const profileContainer = document.getElementById( + "target-head-profile" + ); + + if (profileContainer && targetGroupData.owner) { + const avatarSrc = + targetGroupData.owner?.avatarId && + AvatarMap[ + targetGroupData.owner.avatarId as keyof typeof AvatarMap + ] + ? AvatarMap[ + targetGroupData.owner.avatarId as keyof typeof AvatarMap + ] + : "/images/rpkm/profile/profile-unknown.png"; + + profileContainer.innerHTML = ` `; + } + + targetHeadName.innerHTML = + targetGroupData.owner.firstName + + " " + + targetGroupData.owner.lastName + + " (" + + targetGroupData.owner.nickname + + ")"; + targetHeadId.innerHTML = targetGroupData.owner.studentId; + + groupPopupView?.classList.remove("hidden"); + } else { + errorRoomMessage.innerHTML = response.error || "ไม่พบห้องที่ระบุ"; } + } catch (error) { + console.error("Unexpected error:", error); + errorRoomMessage.innerHTML = "เกิดข้อผิดพลาดในการค้นหาห้อง"; + } finally { + joinGroupButton.disabled = false; + joinGroupButton.innerHTML = + '

    ยืนยัน

    '; + input.value = ""; + toggleSubmitButton(); + } + }); - targetHeadName.innerHTML = + submitGroupButton?.addEventListener("click", async () => { + const response = await joinGroup(inviteCode); + + if (response.success) { + targetHeadNamePopup.innerHTML = targetGroupData.owner.firstName + " " + targetGroupData.owner.lastName + " (" + targetGroupData.owner.nickname + ")"; - targetHeadId.innerHTML = targetGroupData.owner.studentId; - - groupPopupView?.classList.remove("hidden"); + groupPopupView?.classList.add("hidden"); + groupPopupSuccessView?.classList.remove("hidden"); + groupDefaultView?.classList.add("hidden"); + groupMemberView?.classList.remove("hidden"); } else { - errorRoomMessage.innerHTML = response.error || "ไม่พบห้องที่ระบุ"; + console.error("Failed to leave group before:", response.error); } - } catch (error) { - console.error("Unexpected error:", error); - errorRoomMessage.innerHTML = "เกิดข้อผิดพลาดในการค้นหาห้อง"; - } finally { - joinGroupButton.disabled = false; - joinGroupButton.innerHTML = - '

    ยืนยัน

    '; + }); + + cancelGroupButton?.addEventListener("click", () => { + groupPopupView?.classList.add("hidden"); input.value = ""; toggleSubmitButton(); - } - }); + }); - submitGroupButton?.addEventListener("click", async () => { - const response = await joinGroup(inviteCode); - - if (response.success) { - targetHeadNamePopup.innerHTML = - targetGroupData.owner.firstName + - " " + - targetGroupData.owner.lastName + - " (" + - targetGroupData.owner.nickname + - ")"; - groupPopupView?.classList.add("hidden"); - groupPopupSuccessView?.classList.remove("hidden"); - groupDefaultView?.classList.add("hidden"); - groupMemberView?.classList.remove("hidden"); - } else { - console.error("Failed to leave group before:", response.error); - } - }); + closeGroupPopupButton?.addEventListener("click", () => { + groupPopupSuccessView?.classList.add("hidden"); + }); - cancelGroupButton?.addEventListener("click", () => { - groupPopupView?.classList.add("hidden"); - input.value = ""; - toggleSubmitButton(); - }); + roomInviteHeadContainer?.addEventListener("click", () => { + const urlElement = document.getElementById("room-invite-url-head"); + if (urlElement && urlElement.textContent) { + const url = urlElement.textContent.trim(); + navigator.clipboard.writeText(url); + } + }); - closeGroupPopupButton?.addEventListener("click", () => { - groupPopupSuccessView?.classList.add("hidden"); - }); + roomCodeHeadContainer?.addEventListener("click", () => { + const codeElement = document.getElementById("room-code-head"); + if (codeElement && codeElement.textContent) { + const code = codeElement.textContent.trim(); + navigator.clipboard.writeText(code); + } + }); - roomInviteHeadContainer?.addEventListener("click", () => { - const urlElement = document.getElementById("room-invite-url-head"); - if (urlElement && urlElement.textContent) { - const url = urlElement.textContent.trim(); - navigator.clipboard.writeText(url); - } - }); + roomInviteMemberContainer?.addEventListener("click", () => { + const urlElement = document.getElementById("room-invite-url-member"); + if (urlElement && urlElement.textContent) { + const url = urlElement.textContent.trim(); + navigator.clipboard.writeText(url); + } + }); - roomCodeHeadContainer?.addEventListener("click", () => { - const codeElement = document.getElementById("room-code-head"); - if (codeElement && codeElement.textContent) { - const code = codeElement.textContent.trim(); - navigator.clipboard.writeText(code); - } - }); + roomCodeMemberContainer?.addEventListener("click", () => { + const codeElement = document.getElementById("room-code-member"); + if (codeElement && codeElement.textContent) { + const code = codeElement.textContent.trim(); + navigator.clipboard.writeText(code); + } + }); - roomInviteMemberContainer?.addEventListener("click", () => { - const urlElement = document.getElementById("room-invite-url-member"); - if (urlElement && urlElement.textContent) { - const url = urlElement.textContent.trim(); - navigator.clipboard.writeText(url); + if (isConfirmed) { + exitButton?.classList.add("hidden"); + backBtn?.classList.add("hidden"); + } else { + exitButton?.classList.remove("hidden"); + backBtn?.classList.remove("hidden"); } - }); - roomCodeMemberContainer?.addEventListener("click", () => { - const codeElement = document.getElementById("room-code-member"); - if (codeElement && codeElement.textContent) { - const code = codeElement.textContent.trim(); - navigator.clipboard.writeText(code); + if (memberCount !== 1) { + backBtn?.classList.add("hidden"); + } else { + backBtn?.classList.remove("hidden"); } - }); - - if (isConfirmed) { - exitButton?.classList.add("hidden"); - backBtn?.classList.add("hidden"); - } else { - exitButton?.classList.remove("hidden"); - backBtn?.classList.remove("hidden"); - } - if (memberCount !== 1) { - backBtn?.classList.add("hidden"); - } else { - backBtn?.classList.remove("hidden"); - } - - backBtn?.addEventListener("click", () => { - if (!isConfirmed) { - groupDefaultView?.classList.remove("hidden"); - groupHeadView?.classList.add("hidden"); - } - }); + backBtn?.addEventListener("click", () => { + if (!isConfirmed) { + groupDefaultView?.classList.remove("hidden"); + groupHeadView?.classList.add("hidden"); + } + }); - exitButton?.addEventListener("click", () => { - if (!isConfirmed) { - groupMemberView?.classList.add("hidden"); - groupDefaultView?.classList.remove("hidden"); - return; - } + exitButton?.addEventListener("click", () => { + if (!isConfirmed) { + groupMemberView?.classList.add("hidden"); + groupDefaultView?.classList.remove("hidden"); + return; + } + }); }); diff --git a/src/components/rpkm/components/house-picker.astro b/src/components/rpkm/components/house-picker.astro index 1b7029f..ef12d6d 100644 --- a/src/components/rpkm/components/house-picker.astro +++ b/src/components/rpkm/components/house-picker.astro @@ -57,8 +57,6 @@ const isPickAllMainHouses = housePrefData.houseRank3 && housePrefData.houseRank4 && housePrefData.houseRank5; - -console.log("Is Confirmed:", isConfirmed); --- From 1a8d9b54e167484c819e70e5468f3e0f6d2ed780 Mon Sep 17 00:00:00 2001 From: BadLuckZ Date: Sat, 19 Jul 2025 03:57:03 +0700 Subject: [PATCH 48/77] feat: Join Group --- .../rpkm/components/group-picker.astro | 114 ++++++++++++------ .../rpkm/components/house-picker.astro | 56 ++++++--- src/lib/groupAPI.ts | 40 +++--- 3 files changed, 131 insertions(+), 79 deletions(-) diff --git a/src/components/rpkm/components/group-picker.astro b/src/components/rpkm/components/group-picker.astro index d7ee0ac..17e97bb 100644 --- a/src/components/rpkm/components/group-picker.astro +++ b/src/components/rpkm/components/group-picker.astro @@ -3,31 +3,36 @@ import GroupProfile from "@rpkm/rubpuen/GroupProfile"; import Divider from "@/components/common/Divider.astro"; import Frame from "@/components/common/Frame.astro"; -import { getGroupData } from "@/lib/groupAPI"; +import { getGroupByGroupId } from "@/lib/groupAPI"; import { getProfile } from "@/lib/profileAPI"; import type { Group, User } from "@/types/common"; const token = Astro.cookies.get("token")?.value; -let groupData = null; -let user = null; +let groupData: Group | null = null; +let user: User | null = null; if (token) { - const response = await getGroupData(token); - if (response.success && response.data) { - groupData = response.data.data as Group; - } - - try { - const userResponse = await getProfile(token); - if (userResponse.success && userResponse.data) { - user = userResponse.data.user as User; + let groupID = ""; + const userResponse = await getProfile(token); + if (userResponse.success && userResponse.data) { + user = userResponse.data.user as User; + groupID = user.groupId ? user.groupId : ""; + if (groupID) { + const groupResponse = await getGroupByGroupId(groupID, token); + if (groupResponse.success && groupResponse.data) { + groupData = groupResponse.data.data as Group; + } else { + return Astro.redirect("/login"); + } } else { - console.error("Failed to fetch user data:", userResponse.error); + return Astro.redirect("/login"); } - } catch (error) { - console.error("Error fetching user data:", error); + } else { + console.error("Failed to fetch user data:", userResponse.error); } +} else { + return Astro.redirect("/login"); } if (!groupData || !user) { @@ -40,7 +45,8 @@ const isConfirmed = groupData.isConfirmed; const memberCount = groupData.memberCount; const canDisplayDefault = !isConfirmed && memberCount === 1 && isOwner; -const canDisplayHead = (isConfirmed && isOwner) || memberCount != 1; +const canDisplayHead = + (isConfirmed && isOwner) || (!isConfirmed && isOwner && memberCount != 1); const canDisplayMember = !isOwner; const member1 = groupData.memberCount > 1 ? groupData.users[1] : null; @@ -218,13 +224,17 @@ const member2 = groupData.memberCount > 2 ? groupData.users[2] : null;

    {`จับกลุ่มเพื่อน (${memberCount}/3)`}

    - + { + !isConfirmed && ( + + ) + }
    @@ -251,6 +261,7 @@ const member2 = groupData.memberCount > 2 ? groupData.users[2] : null; id="room-invite-url-member" class="h-[36px] w-full bg-transparent px-3 py-1 text-center text-lg font-medium text-[#0FF] outline-none" > + {"rpkm.sgcu.in.th/" + inviteCode} 2 ? groupData.users[2] : null; id="room-code-member" class="h-[36px] w-full bg-transparent px-3 py-1 text-center text-2xl font-semibold tracking-widest text-[#0FF] outline-none" > + {inviteCode} 2 ? groupData.users[2] : null; diff --git a/src/components/rpkm/components/house-picker.astro b/src/components/rpkm/components/house-picker.astro index 4292816..b58dbe4 100644 --- a/src/components/rpkm/components/house-picker.astro +++ b/src/components/rpkm/components/house-picker.astro @@ -1,7 +1,6 @@ --- import { getHouses, - getPrefHouses, type HouseDisplayType, type HousePrefType, } from "@rpkm/data/house"; @@ -13,8 +12,9 @@ import HousePickingCard from "@rpkm/elements/house-picking-card.astro"; import Divider from "@/components/common/Divider.astro"; import Frame from "@/components/common/Frame.astro"; -import { getGroupData } from "@/lib/groupAPI"; -import type { Group } from "@/types/common"; +import { getGroupByGroupId } from "@/lib/groupAPI"; +import { getProfile } from "@/lib/profileAPI"; +import type { Group, User } from "@/types/common"; const houseSizes = ["ทั้งหมด", "S", "M", "L", "XL", "XXL"]; @@ -27,20 +27,58 @@ let housePrefData: HousePrefType = { houseRank5: null, houseRankSub: null, }; + let groupData: Group | null = null; +let user: User | null = null; const token = Astro.cookies.get("token")?.value; if (!token) { - Astro.redirect("/login"); -} else { - houseDisplayData = await getHouses(token); - housePrefData = await getPrefHouses(token); - const groupDataResponse = await getGroupData(token); - if (groupDataResponse.success && groupDataResponse.data) { - groupData = groupDataResponse.data.data as Group; + return Astro.redirect("/login"); +} + +let groupID = ""; +const userResponse = await getProfile(token); +if (userResponse.success && userResponse.data) { + user = userResponse.data.user as User; + groupID = user.groupId ? user.groupId : ""; + if (groupID) { + const groupResponse = await getGroupByGroupId(groupID, token); + if (groupResponse.success && groupResponse.data) { + groupData = groupResponse.data.data as Group; + } else { + return Astro.redirect("/login"); + } + } else { + return Astro.redirect("/login"); } +} else { + console.error("Failed to fetch user data:", userResponse.error); } + +if (!groupData || !user) { + return Astro.redirect("/login"); +} + +houseDisplayData = await getHouses(token); +housePrefData = { + houseRank1: groupData.houseRank1 || null, + houseRank2: groupData.houseRank2 || null, + houseRank3: groupData.houseRank3 || null, + houseRank4: groupData.houseRank4 || null, + houseRank5: groupData.houseRank5 || null, + houseRankSub: groupData.houseRankSub || null, +}; + const isConfirmed = groupData ? groupData.isConfirmed || false : false; +const isOwner = + groupData && user ? groupData.ownerId == user.id || false : false; + +const isPickAllMainHouses = + housePrefData.houseRank1 && + housePrefData.houseRank2 && + housePrefData.houseRank3 && + housePrefData.houseRank4 && + housePrefData.houseRank5; --- @@ -76,7 +114,7 @@ const isConfirmed = groupData ? groupData.isConfirmed || false : false;

    บ้านรับเพื่อนที่สนใจ

    -
    +
    { [ "houseRank1", @@ -132,6 +170,7 @@ const isConfirmed = groupData ? groupData.isConfirmed || false : false; id="btn-submit-house" class="cut-edge-all flex cursor-pointer items-center justify-center gap-2 bg-[linear-gradient(90deg,_#8A2BE4_0%,_#2B2D42_50%,_#8A2BE4_100%)] disabled:cursor-not-allowed disabled:opacity-50" type="button" + disabled={!isPickAllMainHouses} style={`width: 260px; height: 48px;`} >

    ยืนยันการเลือกบ้าน

    @@ -248,90 +287,111 @@ const isConfirmed = groupData ? groupData.isConfirmed || false : false;

    +
    - - -
    + + // Handle house card click in popup + houseCardContainer?.addEventListener("click", (e) => { + const wrapper = e.target.closest(".house-card-wrapper"); + if (!wrapper) return; + + e.preventDefault(); + e.stopPropagation(); + + const nameThai = wrapper.getAttribute("data-thname"); + selectedHouseData = houseDisplayData.find((h) => h.nameThai === nameThai); + + // Prevent showing popup for already selected houses + if (!selectedHouseData || selectedHouses.has(selectedHouseData.id)) + return; + + // Show house popup with data + showHousePopup(selectedHouseData); + }); + + // Cancel button + btnCancelHouse?.addEventListener("click", () => { + housePopup?.classList.add("hidden"); + housePopup?.classList.remove("flex"); + selectedHouseData = null; + }); + + // Add house button + btnAddHouse?.addEventListener("click", () => { + if (!selectedHouseData || !currentRank) return; + + const nameThai = selectedHouseData.nameThai; + const houseObj = houseDisplayData.find((h) => h.nameThai === nameThai); + + // Prevent adding already selected houses + if (selectedHouses.has(houseObj.id)) return; + + // ถ้าอันดับนี้มีบ้านอยู่แล้ว ให้ลบบ้านเก่าออกจาก selectedHouses + if (pickedHouses[currentRank]) { + selectedHouses.delete(pickedHouses[currentRank]); + } + + // Update state - เลือกไปที่ rank ที่ user กดไว้ + pickedHouses[currentRank] = houseObj.id; + + selectedHouses.add(houseObj.id); + + // Update UI + updatePickedHousesDisplay(); + updateAvailableHouses(); + updateHouseCounts(); + + // Save to localStorage + saveHouseSelection(); + + // Close both popups + housePopup?.classList.add("hidden"); + housePopup?.classList.remove("flex"); + closePopup(); + + // Enable submit button if all main ranks are filled + const mainRanks = [ + "houseRank1", + "houseRank2", + "houseRank3", + "houseRank4", + "houseRank5", + ]; + const allMainRanksFilled = mainRanks.every( + (rank) => pickedHouses[rank] !== null + ); + submitBtn.disabled = !allMainRanksFilled; + + // Reset selected house data + selectedHouseData = null; + }); + }); + diff --git a/src/components/rpkm/data/house.ts b/src/components/rpkm/data/house.ts index c926e5f..15c4c82 100644 --- a/src/components/rpkm/data/house.ts +++ b/src/components/rpkm/data/house.ts @@ -1,8 +1,4 @@ -import { - type HouseResponse, - getAllHouses, - getHousePreferences, -} from "@/lib/houseAPI"; +import { type HouseResponse, getAllHouses } from "@/lib/houseAPI"; type ExtraHouseType = { nameThai: string; @@ -180,43 +176,3 @@ export async function getHouses(token?: string): Promise { }); return houses; } - -export async function getPrefHouses(token?: string): Promise { - const houseResult: HousePrefType = { - houseRank1: null, - houseRank2: null, - houseRank3: null, - houseRank4: null, - houseRank5: null, - houseRankSub: null, - }; - - const response = await getHousePreferences(token); - if (!response.success || !response.data) { - console.error("Failed to fetch house data:", response.error); - return houseResult; - } - - const houseData = response.data.data; - - houseResult.houseRank1 = houseData.houseRank1 - ? (houseData.houseRank1.id ?? null) - : null; - houseResult.houseRank2 = houseData.houseRank2 - ? (houseData.houseRank2.id ?? null) - : null; - houseResult.houseRank3 = houseData.houseRank3 - ? (houseData.houseRank3.id ?? null) - : null; - houseResult.houseRank4 = houseData.houseRank4 - ? (houseData.houseRank4.id ?? null) - : null; - houseResult.houseRank5 = houseData.houseRank5 - ? (houseData.houseRank5.id ?? null) - : null; - houseResult.houseRankSub = houseData.houseRankSub - ? (houseData.houseRankSub.id ?? null) - : null; - - return houseResult; -} diff --git a/src/components/rpkm/elements/house-confirm-popup.astro b/src/components/rpkm/elements/house-confirm-popup.astro index 5d5ed20..b8478a4 100644 --- a/src/components/rpkm/elements/house-confirm-popup.astro +++ b/src/components/rpkm/elements/house-confirm-popup.astro @@ -168,6 +168,7 @@ if (!token) { // Close button - hide success popup btnClose?.addEventListener("click", () => { hideSuccessPopup(); + window.location.reload(); }); // Click outside to close success popup diff --git a/src/lib/groupAPI.ts b/src/lib/groupAPI.ts index 08eae04..2a0fa67 100644 --- a/src/lib/groupAPI.ts +++ b/src/lib/groupAPI.ts @@ -2,12 +2,8 @@ import { type ApiResponse, api } from "@/lib/api"; import { getAuthHeaders, getAuthToken } from "@/lib/auth"; import type { Group } from "@/types/common"; -/** - * Fetches group data from the backend. - * @param authToken - Optional authentication token for server-side use. - * @returns ApiResponse from the backend. - */ -export async function getGroupData( +export async function getGroupByGroupId( + groupId?: string, authToken?: string ): Promise> { const token = authToken || getAuthToken(); @@ -18,21 +14,27 @@ export async function getGroupData( }; } - const response = await api.get<{ data: Group }>("/group", { - headers: getAuthHeaders(token), - }); + if (!groupId) { + return { + success: false, + error: "กรุณาใส่รหัสกลุ่ม", + }; + } + + const response = await api.post<{ data: Group }>( + "/group/getGroupByGroupId", + { groupId }, + { + headers: getAuthHeaders(token), + } + ); + if (!response.success) { - console.error("Failed to fetch group data:", response.error); + console.error("Failed to fetch group by group ID:", response.error); } return response; } -/** - * Fetches group data by invite code from the backend. - * @param inviteCode - The invite code to search for. - * @param authToken - Optional authentication token for server-side use. - * @returns ApiResponse from the backend. - */ export async function getGroupByInviteCode( inviteCode: string, authToken?: string @@ -59,12 +61,6 @@ export async function getGroupByInviteCode( return response; } -/** - * Join group data by invite code from the backend. - * @param inviteCode - The invite code to search for. - * @param authToken - Optional authentication token for server-side use. - * @returns ApiResponse from the backend. - */ export async function joinGroup( inviteCode: string, authToken?: string @@ -91,12 +87,8 @@ export async function joinGroup( return response; } -/** - * Leave the current group. - * @param authToken - Optional authentication token for server-side use. - * @returns ApiResponse from the backend. - */ export async function leaveGroup( + groupId: string, authToken?: string ): Promise> { const token = authToken || getAuthToken(); @@ -107,9 +99,20 @@ export async function leaveGroup( }; } - const response = await api.delete<{ message: string }>("/group/leave", { - headers: getAuthHeaders(token), - }); + if (!groupId) { + return { + success: false, + error: "กรุณาใส่รหัสกลุ่ม", + }; + } + + const response = await api.patch<{ message: string }>( + "/group/leave", + { groupId }, + { + headers: getAuthHeaders(token), + } + ); if (!response.success) { console.error("Failed to leave group:", response.error); diff --git a/src/lib/houseAPI.ts b/src/lib/houseAPI.ts index 0bf5e19..8fa9719 100644 --- a/src/lib/houseAPI.ts +++ b/src/lib/houseAPI.ts @@ -1,24 +1,6 @@ import { type ApiResponse, api } from "@/lib/api"; import { getAuthHeaders, getAuthToken } from "@/lib/auth"; -export interface SetHousePreferencesRequest { - houseRank1: string; - houseRank2: string; - houseRank3: string; - houseRank4: string; - houseRank5: string; - houseRankSub?: string; -} - -export interface GetHousePreferencesRequest { - houseRank1: HouseResponse; - houseRank2: HouseResponse; - houseRank3: HouseResponse; - houseRank4: HouseResponse; - houseRank5: HouseResponse; - houseRankSub?: HouseResponse; -} - export type HouseResponse = { id: string; nameThai: string; @@ -33,62 +15,6 @@ export type HouseResponse = { tiktok: string; }; -export async function setHousePreferences( - preferences: SetHousePreferencesRequest, - authToken?: string -): Promise< - ApiResponse<{ - groupId: string; - updatedPreferences: SetHousePreferencesRequest; - message: string; - }> -> { - const token = authToken || getAuthToken(); - if (!token) { - return { - success: false, - error: "กรุณาเข้าสู่ระบบก่อนลงทะเบียน", - }; - } - - const response = await api.post<{ - groupId: string; - updatedPreferences: SetHousePreferencesRequest; - message: string; - }>("/group/house-preferences", preferences, { - headers: getAuthHeaders(token), - }); - - if (!response.success) { - console.error("Failed to set house preferences:", response.error); - } - return response; -} - -export async function getHousePreferences( - authToken?: string -): Promise> { - const token = authToken || getAuthToken(); - if (!token) { - return { - success: false, - error: "กรุณาเข้าสู่ระบบก่อนลงทะเบียน", - }; - } - - const response = await api.get<{ data: GetHousePreferencesRequest }>( - "/group/house-preferences", - { - headers: getAuthHeaders(token), - } - ); - - if (!response.success) { - console.error("Failed to fetch house preferences:", response.error); - } - return response; -} - export async function getAllHouses( authToken?: string ): Promise> { From b5e229701e0972dbeacc83bfb0ae48320d788264 Mon Sep 17 00:00:00 2001 From: "Dhanavadh T. Saito" <105502355+dhanavadh@users.noreply.github.com> Date: Sat, 19 Jul 2025 14:41:27 +0700 Subject: [PATCH 56/77] feat: added Freshmen night regis - added fm night regis button for student role - cleared hardcode for checking the opening event --- .../firstdate/events/[eventId]/index.astro | 2 +- src/pages/firstdate/home/index.astro | 8 ++ src/pages/staff/event/index.astro | 95 ++++++++++++------- 3 files changed, 72 insertions(+), 33 deletions(-) diff --git a/src/pages/firstdate/events/[eventId]/index.astro b/src/pages/firstdate/events/[eventId]/index.astro index 8a7119a..46f7737 100644 --- a/src/pages/firstdate/events/[eventId]/index.astro +++ b/src/pages/firstdate/events/[eventId]/index.astro @@ -111,7 +111,7 @@ if (eventStatus.isRegistered && eventStatus.checkinData) { {(eventType === "firstdate" || eventType === "freshmen-night") && ( - + แผนที่งาน )} diff --git a/src/pages/firstdate/home/index.astro b/src/pages/firstdate/home/index.astro index a0b0077..f8f2382 100644 --- a/src/pages/firstdate/home/index.astro +++ b/src/pages/firstdate/home/index.astro @@ -58,6 +58,14 @@ import Layout from "@/layouts/firstdate/WithNavbar.astro"; > รับเพื่อนก้าวใหม่ + + Freshmen Night +
    diff --git a/src/pages/staff/event/index.astro b/src/pages/staff/event/index.astro index 9537058..4dfafbd 100644 --- a/src/pages/staff/event/index.astro +++ b/src/pages/staff/event/index.astro @@ -330,8 +330,6 @@ import StaffWithNavbar from "@/layouts/firstdate/StaffWithNavbar.astro";