From 84f80008c682da2cf7f5c1502de04ef914a28fd9 Mon Sep 17 00:00:00 2001 From: TeeGooodGood <122004889+TeeGoood@users.noreply.github.com> Date: Sat, 1 Feb 2025 17:16:40 +0700 Subject: [PATCH 1/2] feat: scan qr code --- src/app/config.ts | 2 +- src/components/profile/Ticket/EditButton.tsx | 2 +- src/components/staff/qr/Modal.tsx | 9 ++- src/components/staff/qr/qrbutton.tsx | 79 ++++++++------------ src/const/timer.ts | 3 +- src/utils/qr.ts | 66 ++++++++++++++++ 6 files changed, 108 insertions(+), 53 deletions(-) create mode 100644 src/utils/qr.ts diff --git a/src/app/config.ts b/src/app/config.ts index c273ce4..96a2bd2 100644 --- a/src/app/config.ts +++ b/src/app/config.ts @@ -1,4 +1,4 @@ export const config = { liffId: process.env.NEXT_PUBLIC_LIFF_ID || '', - baseURL: process.env.NEXT_PUBLIC_BASE_URL || '', + baseURL: process.env.NEXT_PUBLIC_BASE_URL || '' }; diff --git a/src/components/profile/Ticket/EditButton.tsx b/src/components/profile/Ticket/EditButton.tsx index 5a72f65..bb4675f 100644 --- a/src/components/profile/Ticket/EditButton.tsx +++ b/src/components/profile/Ticket/EditButton.tsx @@ -15,7 +15,7 @@ export default function Edit() {

บัญชีของฉัน

diff --git a/src/components/staff/qr/Modal.tsx b/src/components/staff/qr/Modal.tsx index 98876af..4e8346b 100644 --- a/src/components/staff/qr/Modal.tsx +++ b/src/components/staff/qr/Modal.tsx @@ -4,7 +4,7 @@ import { getImageURL } from '@/utils/image'; interface ModalProps { modalType: 'confirm' | 'invalid' | 'already'; - userInfo: string | null; + userInfo: string | undefined; scanAgain: () => void; closeFn: () => void; time?: string; // Optional prop for 'already' type @@ -111,10 +111,13 @@ const Modal: React.FC = ({ }; return ( -
+
-
+
e.stopPropagation()} + > {renderContent()}
diff --git a/src/components/staff/qr/qrbutton.tsx b/src/components/staff/qr/qrbutton.tsx index 3d76321..f8c62bc 100644 --- a/src/components/staff/qr/qrbutton.tsx +++ b/src/components/staff/qr/qrbutton.tsx @@ -3,74 +3,61 @@ import { useState } from 'react'; import { useLiff } from '@/contexts/liff'; import Modal from './Modal'; import { ScanLine } from 'lucide-react'; -import { apiClient } from '@/utils/axios'; import { useAuth } from '@/contexts/auth'; +import { scanQR } from '@/utils/qr'; export default function QrButton() { const { client } = useLiff(); const { token } = useAuth(); - const [qrCodeValue, setQrCodeValue] = useState(null); - const [modalType, setModalType] = useState< - 'confirm' | 'invalid' | 'already' | null - >(null); - const [isModalOpen, setIsModalOpen] = useState(true); + const [isModalOpen, setIsModalOpen] = useState(false); + const [modalType, setModalType] = useState<'confirm' | 'invalid' | 'already'>( + 'invalid', + ); + const [userInfo, setUserInfo] = useState(); + const [time, setTime] = useState(); const openQRScanner = async () => { if (!client?.isInClient()) { alert('Please use this feature inside the LINE app.'); return; } - try { - const result = await client.scanCodeV2(); - const value = result.value ?? ''; - // Post the QR code result to the API - const status = await postQrCodeToApi(value ?? ''); - setModalType(status?.toString() as 'confirm' | 'invalid' | 'already'); - setQrCodeValue(value); - setIsModalOpen(true); - } catch (error) { - console.error('Error scanning QR code:', error); - alert('Failed to scan QR code. Please try again.'); - setModalType('invalid'); - setIsModalOpen(true); + if (!token?.accessToken) { + console.error('Failed scaning QR: no accessToken'); + return; } - }; - const postQrCodeToApi = async (value: string) => { - try { - const response = await apiClient.post( - `/users/qr/${value}`, - {}, - { - headers: { - Authorization: `Bearer ${token?.accessToken}`, - }, - }, - ); - if (response.status === 200) { - return 'confirm'; - } else if (response.status === 400) { - return 'already'; - } else if (response.status === 500) { - return 'invalid'; - } else { - return 'invalid'; - } - } catch (error) { - console.error('Error posting QR code to API:', error); - return error; + const scanResult = await client.scanCodeV2(); + const userId = scanResult.value; + + if (!userId) { + console.error('Failed scaning QR: No userId'); + return; } + + const { modalType, time, userInfo } = await scanQR( + userId, + token.accessToken, + ); + + setIsModalOpen(true); + setModalType(modalType); + setTime(time); + setUserInfo(userInfo); }; + return ( -
+
-

คลิกเพื่อสแกน

+

+ คลิกเพื่อสแกน +

{isModalOpen && modalType != null && ( setIsModalOpen(false)} modalType={modalType} diff --git a/src/const/timer.ts b/src/const/timer.ts index c0812ba..d317c3f 100644 --- a/src/const/timer.ts +++ b/src/const/timer.ts @@ -9,8 +9,7 @@ if (!rawTargetDate) { throw new Error('Environment variable NEXT_PUBLIC_TARGET_DATE is missing.'); } -const t = rawTargetDate.split(/[^0-9]/).map(t => parseInt(t)); -const parsedDate = new Date(t[0], t[1] - 1, t[2]); +const parsedDate = new Date(rawTargetDate); if (isNaN(parsedDate.getTime())) { throw new Error( `Invalid date format in NEXT_PUBLIC_TARGET_DATE: ${rawTargetDate}`, diff --git a/src/utils/qr.ts b/src/utils/qr.ts new file mode 100644 index 0000000..7fd0515 --- /dev/null +++ b/src/utils/qr.ts @@ -0,0 +1,66 @@ +import axios from 'axios'; +import { apiClient } from './axios'; + +export interface ScanQRResp { + age: 'string'; + chronicDisease: 'string'; + drugAllergy: 'string'; + education: 'studying'; + email: 'string'; + faculty: 'string'; + foodLimitation: 'string'; + graduatedYear: 'string'; + id: 'string'; + imageUrl: 'string'; + invitationCode: 'string'; + lastEntered: 'string'; + name: 'string'; + phone: 'string'; + role: 'member'; + sizeJersey: 'string'; + status: 'chula_student'; + uid: 'string'; + university: 'string'; +} + +export interface ScanQRResult { + modalType: 'confirm' | 'invalid' | 'already'; + userInfo?: string; + time?: string; +} + +export interface ScanQRError { + error: string; + message: string; +} + +export async function scanQR( + userId: string, + accessToken: string, +): Promise { + try { + const resp = await apiClient.post( + `/users/qr/${userId}`, + {}, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + return { modalType: 'confirm', userInfo: resp.data.name }; + } catch (err: unknown) { + if (axios.isAxiosError(err)) { + switch (err.response?.status) { + case 400: + return { + modalType: 'already', + time: new Date(err.response.data.message).toLocaleString(), + }; + default: + return { modalType: 'invalid' }; + } + } + return { modalType: 'invalid' }; + } +} From b243c3e832cd33f00a34ccde199db835d77a2177 Mon Sep 17 00:00:00 2001 From: TeeGooodGood <122004889+TeeGoood@users.noreply.github.com> Date: Sat, 1 Feb 2025 22:17:04 +0700 Subject: [PATCH 2/2] feat: add isAcroPhobia field --- src/app/(user)/edit/_components/form.tsx | 9 +++++++++ src/app/(user)/register/_components/subpages/two.tsx | 10 +++++----- src/schema/edit.ts | 1 + src/schema/register.ts | 1 + src/schema/user.ts | 1 + 5 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/app/(user)/edit/_components/form.tsx b/src/app/(user)/edit/_components/form.tsx index b13e4ee..5d0176b 100644 --- a/src/app/(user)/edit/_components/form.tsx +++ b/src/app/(user)/edit/_components/form.tsx @@ -22,6 +22,7 @@ import { SubmitHandler, useForm } from 'react-hook-form'; import { useAuth } from '@/contexts/auth'; import { useLiff } from '@/contexts/liff'; import toast from 'react-hot-toast'; +import Consent from '../../register/_components/policy/consent'; export default function Form() { const { editError, edit, isEditing, user: defaultUser } = useAuth(); @@ -48,6 +49,7 @@ export default function Form() { graduatedYear: defaultUser.graduatedYear, faculty: defaultUser.faculty, age: defaultUser.age, + isAcroPhobia: defaultUser.isAcroPhobia, sizeJersey: defaultUser.sizeJersey, foodLimitation: defaultUser.foodLimitation, chronicDisease: defaultUser.chronicDisease, @@ -252,6 +254,13 @@ export default function Form() {
+
+ setValue('isAcroPhobia', val)} + label="คุณกลัวความสูงหรือไม่" + isRequired={false} + /> {errors.chronicDisease?.message}
diff --git a/src/app/(user)/register/_components/subpages/two.tsx b/src/app/(user)/register/_components/subpages/two.tsx index 1f9afbb..3d396f9 100644 --- a/src/app/(user)/register/_components/subpages/two.tsx +++ b/src/app/(user)/register/_components/subpages/two.tsx @@ -23,7 +23,7 @@ import { universities } from '@/const/universities'; import { faculties } from '@/const/faculties'; import { sizeJersey } from '@/const/size'; import { statusMap } from '@/const/status'; -// import Consent from '../policy/consent'; +import Consent from '../policy/consent'; interface TwoProps { setStep: (value: number) => void; @@ -255,13 +255,13 @@ export default function Two({ setStep, form }: TwoProps) {
- {/*
+
{}} + value={user.isAcroPhobia} + setValue={(val: boolean) => setValue('isAcroPhobia', val)} label="คุณกลัวความสูงหรือไม่" isRequired={false} - /> */} + /> {errors.chronicDisease?.message}
diff --git a/src/schema/edit.ts b/src/schema/edit.ts index e9fced7..9163a53 100644 --- a/src/schema/edit.ts +++ b/src/schema/edit.ts @@ -18,6 +18,7 @@ export const EditSchema = z.object({ sizeJersey: z.enum(sizeJersey, { message: 'กรุณาเลือกขนาดเสื้อ', }), + isAcroPhobia: z.boolean(), education: z.enum(educations, { message: 'กรุณาเลือกการศึกษา' }), foodLimitation: z.string(), chronicDisease: z.string(), diff --git a/src/schema/register.ts b/src/schema/register.ts index d2b8686..a5e948a 100644 --- a/src/schema/register.ts +++ b/src/schema/register.ts @@ -18,6 +18,7 @@ export const RegisterSchema = z.object({ sizeJersey: z.enum(sizeJersey, { message: 'กรุณาเลือกขนาดเสื้อ', }), + isAcroPhobia: z.boolean().default(false), education: z.enum(educations, { message: 'กรุณาเลือกการศึกษา' }), foodLimitation: z.string(), chronicDisease: z.string(), diff --git a/src/schema/user.ts b/src/schema/user.ts index 3047f4e..0453dac 100644 --- a/src/schema/user.ts +++ b/src/schema/user.ts @@ -18,6 +18,7 @@ export const UserSchema = z.object({ imageURL: z.string(), invitationCode: z.string(), lastEntered: z.string(), + isAcroPhobia: z.boolean(), phone: z.string(), role: z.enum(roles), sizeJersey: z.enum(sizeJersey),