From 4acc28e4b22ad470672883c66c6e35e7983bac73 Mon Sep 17 00:00:00 2001 From: phuchuynhtan4103131 Date: Wed, 18 Mar 2026 16:00:59 +0700 Subject: [PATCH 01/59] initial commit for ae --- .../(marketing)/qrgenerator/QRgen.tsx | 0 .../(marketing)/qrgenerator/data.json | 32 +++++++++++++++++++ .../[locale]/(marketing)/qrgenerator/page.tsx | 29 +++++++++++++++++ apps/web/src/constants/public_paths.ts | 1 + bun.lock | 9 +++++- package.json | 5 ++- 6 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/app/[locale]/(marketing)/qrgenerator/QRgen.tsx create mode 100644 apps/web/src/app/[locale]/(marketing)/qrgenerator/data.json create mode 100644 apps/web/src/app/[locale]/(marketing)/qrgenerator/page.tsx diff --git a/apps/web/src/app/[locale]/(marketing)/qrgenerator/QRgen.tsx b/apps/web/src/app/[locale]/(marketing)/qrgenerator/QRgen.tsx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/web/src/app/[locale]/(marketing)/qrgenerator/data.json b/apps/web/src/app/[locale]/(marketing)/qrgenerator/data.json new file mode 100644 index 0000000000..a0122b649b --- /dev/null +++ b/apps/web/src/app/[locale]/(marketing)/qrgenerator/data.json @@ -0,0 +1,32 @@ +[ + { + "id": "1", + "name": "John Doe", + "dob": "1995-05-15", + "major": "Computer Science" + }, + { + "id": "2", + "name": "Jane Smith", + "dob": "1996-08-22", + "major": "Mathematics" + }, + { + "id": "3", + "name": "Emily Johnson", + "dob": "1994-12-10", + "major": "Physics" + }, + { + "id": "4", + "name": "Michael Brown", + "dob": "1997-03-30", + "major": "Chemistry" + }, + { + "id": "5", + "name": "Sarah Davis", + "dob": "1995-11-05", + "major": "Biology" + } +] diff --git a/apps/web/src/app/[locale]/(marketing)/qrgenerator/page.tsx b/apps/web/src/app/[locale]/(marketing)/qrgenerator/page.tsx new file mode 100644 index 0000000000..1364e72986 --- /dev/null +++ b/apps/web/src/app/[locale]/(marketing)/qrgenerator/page.tsx @@ -0,0 +1,29 @@ +// npm install qrcode.react +import { QRCodeSVG } from 'qrcode.react'; + +export default function ScanTicket() { + const ticketUrl = 'https://rmitnct.club'; + + return ( + // Tailwind used for the layout, background, and drop shadow +
+

+ Your Entry Ticket +

+ + {/* A white wrapper ensures the QR code is always scannable, even in dark mode */} +
+ +
+ +

+ Show this code at the front gate. +

+
+ ); +} diff --git a/apps/web/src/constants/public_paths.ts b/apps/web/src/constants/public_paths.ts index d203abfe4d..06181ec907 100644 --- a/apps/web/src/constants/public_paths.ts +++ b/apps/web/src/constants/public_paths.ts @@ -14,6 +14,7 @@ export const APP_PUBLIC_PATHS = [ '/neo-chess', '/blogs', '/neo-pacman', + '/qrgenerator', ].reduce((acc: string[], path) => { // Add the original path acc.push(path); diff --git a/bun.lock b/bun.lock index 7552b78382..d11975d26e 100644 --- a/bun.lock +++ b/bun.lock @@ -4,6 +4,9 @@ "workspaces": { "": { "name": "hub", + "dependencies": { + "react-qr-code": "^2.0.18", + }, "devDependencies": { "@biomejs/biome": "^2.4.7", "@ncthub/typescript-config": "workspace:*", @@ -2697,6 +2700,8 @@ "punycode.js": ["punycode.js@2.3.1", "", {}, "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="], + "qr.js": ["qr.js@0.0.0", "", {}, "sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ=="], + "qrcode.react": ["qrcode.react@4.2.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA=="], "qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="], @@ -2735,6 +2740,8 @@ "react-pdf": ["react-pdf@10.4.1", "", { "dependencies": { "clsx": "^2.0.0", "dequal": "^2.0.3", "make-cancellable-promise": "^2.0.0", "make-event-props": "^2.0.0", "merge-refs": "^2.0.0", "pdfjs-dist": "5.4.296", "tiny-invariant": "^1.0.0", "warning": "^4.0.0" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-kS/35staVCBqS29verTQJQZXw7RfsRCPO3fdJoW1KXylcv7A9dw6DZ3vJXC2w+bIBgLw5FN4pOFvKSQtkQhPfA=="], + "react-qr-code": ["react-qr-code@2.0.18", "", { "dependencies": { "prop-types": "^15.8.1", "qr.js": "0.0.0" }, "peerDependencies": { "react": "*" } }, "sha512-v1Jqz7urLMhkO6jkgJuBYhnqvXagzceg3qJUWayuCK/c6LTIonpWbwxR1f1APGd4xrW/QcQEovNrAojbUz65Tg=="], + "react-quizlet-flashcard": ["react-quizlet-flashcard@4.0.22", "", { "peerDependencies": { "react": "^19.1.0", "react-dom": "^19.1.0" } }, "sha512-YbkAY4BWi8anBNinqjuLIK6cLddlFw6iRgzcoJEXgXBR3Of/cO0uxfPN9Pr3+fO0fUTk8d1hc/tXQAupL+LhcQ=="], "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="], @@ -3151,7 +3158,7 @@ "ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], - "xlsx": ["xlsx@vendor/xlsx-0.20.3.tgz", { "bin": { "xlsx": "./bin/xlsx.njs" } }, "sha512-oLDq3jw7AcLqKWH2AhCpVTZl8mf6X2YReP+Neh0SJUzV/BdZYjth94tG5toiMB1PPrYtxOCfaoUCkvtuH+3AJA=="], + "xlsx": ["xlsx@vendor/xlsx-0.20.3.tgz", { "bin": { "xlsx": "./bin/xlsx.njs" } }], "xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="], diff --git a/package.json b/package.json index 73037c634f..9271d2a730 100644 --- a/package.json +++ b/package.json @@ -67,5 +67,8 @@ "sharp", "supabase", "unrs-resolver" - ] + ], + "dependencies": { + "react-qr-code": "^2.0.18" + } } From a3e052ace0c37f99111d3568aad2e39570afac4d Mon Sep 17 00:00:00 2001 From: phuchuynhtan4103131 Date: Wed, 18 Mar 2026 20:52:30 +0700 Subject: [PATCH 02/59] Added library and generate QR that can put link --- .../data.json | 0 .../(marketing)/neo-qr-generator/page.tsx | 34 +++++++++++++++++++ .../(marketing)/qrgenerator/QRgen.tsx | 0 .../[locale]/(marketing)/qrgenerator/page.tsx | 29 ---------------- apps/web/src/constants/public_paths.ts | 2 +- 5 files changed, 35 insertions(+), 30 deletions(-) rename apps/web/src/app/[locale]/(marketing)/{qrgenerator => neo-qr-generator}/data.json (100%) create mode 100644 apps/web/src/app/[locale]/(marketing)/neo-qr-generator/page.tsx delete mode 100644 apps/web/src/app/[locale]/(marketing)/qrgenerator/QRgen.tsx delete mode 100644 apps/web/src/app/[locale]/(marketing)/qrgenerator/page.tsx diff --git a/apps/web/src/app/[locale]/(marketing)/qrgenerator/data.json b/apps/web/src/app/[locale]/(marketing)/neo-qr-generator/data.json similarity index 100% rename from apps/web/src/app/[locale]/(marketing)/qrgenerator/data.json rename to apps/web/src/app/[locale]/(marketing)/neo-qr-generator/data.json diff --git a/apps/web/src/app/[locale]/(marketing)/neo-qr-generator/page.tsx b/apps/web/src/app/[locale]/(marketing)/neo-qr-generator/page.tsx new file mode 100644 index 0000000000..2c18cf75bc --- /dev/null +++ b/apps/web/src/app/[locale]/(marketing)/neo-qr-generator/page.tsx @@ -0,0 +1,34 @@ +// npm install qrcode.react +'use client'; +import { QRCodeSVG } from 'qrcode.react'; +import { useState } from 'react'; +export default function ScanTicket() { + const [qrData, setQrData] = useState('https://default'); + + return ( + // Tailwind used for the layout, background, and drop shadow +
+

Your QR

+ + {/* A white wrapper ensures the QR code is always scannable, even in dark mode */} +
+ + + + +
+
+ ); +} diff --git a/apps/web/src/app/[locale]/(marketing)/qrgenerator/QRgen.tsx b/apps/web/src/app/[locale]/(marketing)/qrgenerator/QRgen.tsx deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/apps/web/src/app/[locale]/(marketing)/qrgenerator/page.tsx b/apps/web/src/app/[locale]/(marketing)/qrgenerator/page.tsx deleted file mode 100644 index 1364e72986..0000000000 --- a/apps/web/src/app/[locale]/(marketing)/qrgenerator/page.tsx +++ /dev/null @@ -1,29 +0,0 @@ -// npm install qrcode.react -import { QRCodeSVG } from 'qrcode.react'; - -export default function ScanTicket() { - const ticketUrl = 'https://rmitnct.club'; - - return ( - // Tailwind used for the layout, background, and drop shadow -
-

- Your Entry Ticket -

- - {/* A white wrapper ensures the QR code is always scannable, even in dark mode */} -
- -
- -

- Show this code at the front gate. -

-
- ); -} diff --git a/apps/web/src/constants/public_paths.ts b/apps/web/src/constants/public_paths.ts index 06181ec907..a9a626afaf 100644 --- a/apps/web/src/constants/public_paths.ts +++ b/apps/web/src/constants/public_paths.ts @@ -14,7 +14,7 @@ export const APP_PUBLIC_PATHS = [ '/neo-chess', '/blogs', '/neo-pacman', - '/qrgenerator', + '/neo-qr-generator', ].reduce((acc: string[], path) => { // Add the original path acc.push(path); From de1c2722a0ac05609fa7cf34d66330a4cbdcd86f Mon Sep 17 00:00:00 2001 From: Nguyen Khang Date: Wed, 18 Mar 2026 00:51:40 +0700 Subject: [PATCH 03/59] feat(login): enhance sendOtpAction to conditionally handle Turnstile token verification --- .../app/[locale]/(marketing)/login/actions.ts | 25 +++++++++++++------ .../(marketing)/scanner/DatePicker.tsx | 2 +- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/apps/web/src/app/[locale]/(marketing)/login/actions.ts b/apps/web/src/app/[locale]/(marketing)/login/actions.ts index d9212529fc..ae2b0d8f8c 100644 --- a/apps/web/src/app/[locale]/(marketing)/login/actions.ts +++ b/apps/web/src/app/[locale]/(marketing)/login/actions.ts @@ -13,6 +13,7 @@ import { validateOtp, } from '@ncthub/utils/email'; import { headers } from 'next/headers'; +import { DEV_MODE } from '@/constants/common'; export interface SendOtpInput { email: string; @@ -50,14 +51,18 @@ export async function sendOtpAction( const userId = await checkIfUserExists({ email: validatedEmail }); const headersList = await headers(); const turnstile = resolveTurnstileToken({ + devMode: DEV_MODE, token: captchaToken, }); - await verifyTurnstileToken( - { headers: headersList }, - { - token: turnstile.captchaToken, - } - ); + + if (turnstile.isRequired) { + await verifyTurnstileToken( + { headers: headersList }, + { + token: turnstile.captchaToken, + } + ); + } const sbAdmin = await createAdminClient(); const supabase = await createClient(); @@ -80,7 +85,9 @@ export async function sendOtpAction( email: validatedEmail, options: { data: { locale, origin: 'TUTURUUU' }, - captchaToken: turnstile.captchaToken, + captchaToken: turnstile.isRequired + ? turnstile.captchaToken + : undefined, }, }); @@ -96,7 +103,9 @@ export async function sendOtpAction( password: randomPassword, options: { data: { locale, origin: 'TUTURUUU' }, - captchaToken: turnstile.captchaToken, + captchaToken: turnstile.isRequired + ? turnstile.captchaToken + : undefined, }, }); diff --git a/apps/web/src/app/[locale]/(marketing)/scanner/DatePicker.tsx b/apps/web/src/app/[locale]/(marketing)/scanner/DatePicker.tsx index 4319c8d0ee..7719b5f3a2 100644 --- a/apps/web/src/app/[locale]/(marketing)/scanner/DatePicker.tsx +++ b/apps/web/src/app/[locale]/(marketing)/scanner/DatePicker.tsx @@ -6,7 +6,7 @@ import { CalendarIcon } from '@ncthub/ui/icons'; import { Popover, PopoverContent, PopoverTrigger } from '@ncthub/ui/popover'; import { cn } from '@ncthub/utils/format'; import { format } from 'date-fns'; -import { Dispatch, SetStateAction } from 'react'; +import type { Dispatch, SetStateAction } from 'react'; interface DatePickerProps { date: Date | null; From 2d9cbd954137bba06af3f4ed2ede7bd95165035c Mon Sep 17 00:00:00 2001 From: Nguyen Khang Date: Wed, 18 Mar 2026 03:14:11 +0700 Subject: [PATCH 04/59] refactor: improve component imports and styling consistency across scanner module --- .../(marketing)/scanner/AddStudentDialog.tsx | 6 +- .../(marketing)/scanner/DatePicker.tsx | 2 +- .../(marketing)/scanner/StudentForm.tsx | 8 +- .../(marketing)/scanner/StudentList.tsx | 16 +- .../(marketing)/scanner/VideoCapture.tsx | 10 +- .../(marketing)/scanner/list/client.tsx | 14 +- .../(marketing)/scanner/list/page.tsx | 4 +- .../app/[locale]/(marketing)/scanner/page.tsx | 350 ++++++++++-------- 8 files changed, 223 insertions(+), 187 deletions(-) diff --git a/apps/web/src/app/[locale]/(marketing)/scanner/AddStudentDialog.tsx b/apps/web/src/app/[locale]/(marketing)/scanner/AddStudentDialog.tsx index cb13486e85..896c42e774 100644 --- a/apps/web/src/app/[locale]/(marketing)/scanner/AddStudentDialog.tsx +++ b/apps/web/src/app/[locale]/(marketing)/scanner/AddStudentDialog.tsx @@ -1,6 +1,5 @@ 'use client'; -import StudentForm, { type StudentFormData } from './StudentForm'; import { Dialog, DialogContent, @@ -11,6 +10,7 @@ import { } from '@ncthub/ui/dialog'; import { UserPlus } from '@ncthub/ui/icons'; import { useState } from 'react'; +import StudentForm, { type StudentFormData } from './StudentForm'; interface AddStudentDialogProps { trigger: React.ReactNode; @@ -34,11 +34,11 @@ export default function AddStudentDialog({
-
+
- + Add New Student diff --git a/apps/web/src/app/[locale]/(marketing)/scanner/DatePicker.tsx b/apps/web/src/app/[locale]/(marketing)/scanner/DatePicker.tsx index 7719b5f3a2..23a9aef797 100644 --- a/apps/web/src/app/[locale]/(marketing)/scanner/DatePicker.tsx +++ b/apps/web/src/app/[locale]/(marketing)/scanner/DatePicker.tsx @@ -38,7 +38,7 @@ export function DatePicker({ mode="single" selected={date || undefined} onSelect={(date: Date | undefined) => setDate(date || null)} - initialFocus + autoFocus /> diff --git a/apps/web/src/app/[locale]/(marketing)/scanner/StudentForm.tsx b/apps/web/src/app/[locale]/(marketing)/scanner/StudentForm.tsx index b55cbf60d4..314f5c9034 100644 --- a/apps/web/src/app/[locale]/(marketing)/scanner/StudentForm.tsx +++ b/apps/web/src/app/[locale]/(marketing)/scanner/StudentForm.tsx @@ -50,7 +50,7 @@ export default function StudentForm({ values, onSubmit }: StudentFormProps) { name="name" render={({ field }) => ( - + Full Name @@ -76,7 +76,7 @@ export default function StudentForm({ values, onSubmit }: StudentFormProps) { name="studentNumber" render={({ field }) => ( - + Student Number @@ -102,7 +102,7 @@ export default function StudentForm({ values, onSubmit }: StudentFormProps) { name="program" render={({ field }) => ( - + Program @@ -130,7 +130,7 @@ export default function StudentForm({ values, onSubmit }: StudentFormProps) { size="lg" disabled={isSubmitting} className={cn( - 'h-14 w-full text-base font-medium transition-all duration-200', + 'h-14 w-full font-medium text-base transition-all duration-200', 'bg-linear-to-r from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700', 'shadow-lg hover:shadow-xl' )} diff --git a/apps/web/src/app/[locale]/(marketing)/scanner/StudentList.tsx b/apps/web/src/app/[locale]/(marketing)/scanner/StudentList.tsx index 02ab6eb400..3733504181 100644 --- a/apps/web/src/app/[locale]/(marketing)/scanner/StudentList.tsx +++ b/apps/web/src/app/[locale]/(marketing)/scanner/StudentList.tsx @@ -1,6 +1,4 @@ -import AddStudentDialog from './AddStudentDialog'; -import { DatePicker } from './DatePicker'; -import { Student } from '@ncthub/types/primitives/Student'; +import type { Student } from '@ncthub/types/primitives/Student'; import { AlertDialog, AlertDialogAction, @@ -38,7 +36,9 @@ import { TableRow, } from '@ncthub/ui/table'; import { cn } from '@ncthub/utils/format'; -import React, { useState } from 'react'; +import { useState } from 'react'; +import AddStudentDialog from './AddStudentDialog'; +import { DatePicker } from './DatePicker'; interface StudentListProps { students: Student[]; @@ -220,7 +220,7 @@ export default function StudentList({
{/* Search and Filters */} - +
{/* Search Bar */} @@ -275,7 +275,7 @@ export default function StudentList({ {showFilters && (
-
-