Skip to content

Commit

Permalink
feat: staff check-in page (#68)
Browse files Browse the repository at this point in the history
* feat: staff check-in page

* fix: route

* refactor: move file

* feat: api integration

* refactor(staff-check): finalize pages

---------

Co-authored-by: shalluv <[email protected]>
  • Loading branch information
D33102 and shalluv authored Jan 15, 2024
1 parent 21a6ea0 commit 41a90a7
Show file tree
Hide file tree
Showing 8 changed files with 403 additions and 0 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"@astrojs/ts-plugin": "1.3.1",
"@fontsource/ibm-plex-sans-thai": "^5.0.8",
"@radix-ui/react-icons": "1.3.0",
"@radix-ui/react-switch": "1.0.3",
"@radix-ui/react-toast": "1.1.5",
"astro": "^4.0.8",
"class-variance-authority": "0.7.0",
Expand All @@ -31,6 +32,7 @@
"qrcode": "1.5.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-qr-reader": "3.0.0-beta-1",
"sharp": "^0.33.1",
"tailwind-merge": "2.2.0",
"tailwindcss": "^3.4.0",
Expand Down
113 changes: 113 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions src/components/Staff/Checkin/Failed.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const Failed = () => {
return (
<div className="mt-8 grid h-16 w-80 place-content-center rounded-2xl border-2 bg-indigo-950 font-bold">
ไม่พบบัญชีใช้นี้ <br /> <p className="text-sm">Profile not found</p>
</div>
);
};

export default Failed;
154 changes: 154 additions & 0 deletions src/components/Staff/Checkin/QR.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { useState } from "react";
import { QrReader } from "react-qr-reader";

import { Switch } from "@/components/ui/switch";
import { useToast } from "@/components/ui/use-toast";
import Failed from "./Failed";
import User from "./User";

import type { CheckInDataDTO, UserShowedData } from "@/types/staff";

interface Props {
token: string;
}

const QR = ({ token }: Props): JSX.Element => {
const { toast } = useToast();
const [showModal, setShowModal] = useState<boolean>(false);
const [scanndedData, setScannedData] = useState<UserShowedData>();
const [textValue, setTextValue] = useState<string>("");
const [isScanning, setIsScanning] = useState<boolean>(true);
const [isPauseAfterScan, setIsPauseAfterScan] = useState<boolean>(true);

const checkIn = async (userId: string) => {
if (!userId || !isScanning) return;
if (userId.length > 5 && userId.slice(0, 2) === "67")
userId = userId.slice(2, 7);
setTextValue(userId);
setScannedData(undefined);
const res = await fetch(
import.meta.env.PUBLIC_API_BASE_URL + "/staff/checkin/" + userId,
{ method: "POST", headers: { Authorization: `Bearer ${token}` } }
);
setShowModal(true);

if (!res.ok) {
setTextValue("");
const error = await res.json();
if (error.title === "invalid-token") {
toast({
title: "Unauthorized staff",
variant: "destructive",
description: "Your staff token is invalid",
});
} else if (error.title === "forbidden") {
toast({
title: "Forbidden",
variant: "destructive",
description: `UserId (${userId}) is invalid`,
});
} else if (error.title === "not-found") {
toast({
title: "User Not found",
variant: "destructive",
description: `UserId (${userId}) is not found`,
});
} else {
toast({
title: "Something went wrong",
variant: "destructive",
description: error.title,
});
}
return;
}

const user: CheckInDataDTO = await res.json();
if (user.already_checkin) {
toast({
title: "Already check-in",
variant: "destructive",
description: "This user already check-in",
});
}

if (isPauseAfterScan) {
setIsScanning(false);
} else {
setTextValue("");
setIsScanning(true);
}
setScannedData({ ...user.user, id: userId });
};

return (
<div className="flex w-full flex-col items-center px-4 pb-8 text-white">
<div className="relative flex aspect-square w-full rounded-2xl bg-indigo-950">
{!isScanning && (
<div className="absolute z-10 flex aspect-square w-full items-center justify-center rounded-2xl bg-indigo-950">
<button
className="z-20 rounded-2xl border-2 border-white px-4 py-2 text-xl font-bold shadow-inner shadow-white"
onClick={() => {
setIsScanning(true);
setShowModal(false);
setTextValue("");
}}
>
ไปต่อ / Continue
</button>
</div>
)}
<QrReader
className="w-full"
onResult={(res) => {
if (res) checkIn(res.getText());
}}
constraints={{ facingMode: "environment" }}
/>
</div>
{showModal &&
(scanndedData ? (
<User
id={scanndedData.id}
first_name={scanndedData.first_name}
last_name={scanndedData.last_name}
allergies={scanndedData.allergies}
medical_condition={scanndedData.medical_condition}
/>
) : (
<Failed />
))}
<div className="my-6 flex flex-col items-center">
<p className="text-xl font-bold">แสกน QR Code เพื่อเช็คอิน</p>
<p className="font-medium">Scan QR Code to check-in</p>
</div>
<div className="mt-6 w-4/5 max-w-2xl">
<div className="w-full text-sm">เลข ID / ID Number</div>
<input
className="h-12 w-full rounded-2xl p-3 font-medium text-pink-550 placeholder:text-pink-400"
value={textValue}
onChange={(e) => setTextValue(e.target.value)}
placeholder="กรอกเลข ID ที่นี่ / Enter ID here"
></input>
</div>
<button
className="mt-8 rounded-2xl border-2 border-white px-3 py-2 text-xl font-bold shadow-inner shadow-white backdrop-blur-2xl"
onClick={() => checkIn(textValue)}
>
เช็คอิน / Check-in
</button>
<div className="mt-8 flex items-center justify-center gap-2">
<Switch
id="pauseAfterScan"
checked={isPauseAfterScan}
onCheckedChange={(checked) => setIsPauseAfterScan(checked)}
/>
<label htmlFor="pauseAfterScan">
พักหลังจากสแกนสำเร็จ / Pause after scan
</label>
</div>
</div>
);
};

export default QR;
32 changes: 32 additions & 0 deletions src/components/Staff/Checkin/User.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { UserShowedData } from "@/types/staff";
const User = ({
id,
first_name,
last_name,
allergies,
medical_condition,
}: UserShowedData): JSX.Element => {
return (
<div className="mt-12 flex h-max min-h-52 w-72 flex-col justify-between gap-1 rounded-2xl border-2 border-white bg-indigo-900 font-medium text-white shadow-inner shadow-white backdrop-blur-2xl">
<section className="flex w-full flex-col p-4">
<div className="mb-4 w-fit rounded-lg bg-white px-2 py-1 text-xs font-bold text-indigo-950">
เช็คอินเสร็จสิ้น / Checked-in successfully
</div>
<p className="px-3 font-bold">ID: {id} </p>
<p className="px-3 text-2xl">
{first_name} {last_name}
</p>
{allergies && <p className="mt-4 px-3 text-sm">แพ้: {allergies}</p>}
{medical_condition && (
<p className="px-3 text-sm">โรคประจำตัว: {medical_condition}</p>
)}
</section>
<section className="flex h-14 w-full flex-col gap-1 rounded-b-2xl bg-pink-550 px-4 py-2 font-libre text-sm leading-4">
Chula Open House 2024
<p className="text-xs">20-21 January 2024</p>
</section>
</div>
);
};

export default User;
Loading

0 comments on commit 41a90a7

Please sign in to comment.