Skip to content

Commit 199fbba

Browse files
committed
Fix CheckIn and add Participants page
1 parent 0fe9a86 commit 199fbba

File tree

7 files changed

+210
-52
lines changed

7 files changed

+210
-52
lines changed

src/app/(authenticated)/sessions/[id]/check-in/SessionCheckInScanner.tsx

Lines changed: 107 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import QRCodeScanner from "@/components/QRCodeScanner";
66
import { SessionTile } from "@/components/session";
77
import { SessionService } from "@/services/SessionService";
88
import { getUserFromQRCode } from "@/utils/utils";
9-
import { Ghost, UserMinus, UserPlus, Users } from "lucide-react";
10-
import { ReactNode, useEffect, useState } from "react";
9+
import { Ghost, ScanEye, UserMinus, UserPlus, Users } from "lucide-react";
10+
import { ReactNode, useCallback, useEffect, useRef, useState } from "react";
1111

1212
interface SessionCheckInScannerProps {
1313
cannonToken: string;
@@ -25,58 +25,105 @@ export default function SessionCheckInScanner({
2525
const [topCard, setTopCard] = useState<ReactNode | undefined>();
2626
const [bottomCard, setBottomCard] = useState<ReactNode | undefined>();
2727
const [statusCard, setStatusCard] = useState<ReactNode | undefined>();
28-
let cardsTimeout: NodeJS.Timeout;
28+
const [unregisteredUsersCounter, setUnregisteredUsersCounter] =
29+
useState<number>(0);
30+
const cardsTimeout = useRef<NodeJS.Timeout>();
31+
const updateSessionStatusTimeout = useRef<NodeJS.Timeout>();
2932

30-
async function handleQRCodeScanned(data: string) {
31-
const scannedUser = getUserFromQRCode(data);
32-
cardsTimeout && clearTimeout(cardsTimeout);
33-
34-
if (scannedUser) {
35-
setBottomCard(
36-
<ListCard img={scannedUser.img} title={scannedUser.name} />,
37-
);
33+
const sessionUpdate = useCallback(
34+
async (data?: { users?: string[]; unregisteredUsers?: number }) => {
3835
const sessionStatus = await SessionService.checkInUser(
3936
cannonToken,
4037
sinfoSession.id,
41-
{
42-
users: [scannedUser.id],
43-
},
38+
data || {},
4439
);
45-
if (!sessionStatus)
46-
setStatusCard(
47-
<MessageCard
48-
type="danger"
49-
content="Failed to check-in user. Try again!"
50-
/>,
51-
);
52-
else {
53-
if (sessionStatus.status === "already")
54-
setStatusCard(
55-
<MessageCard type="warning" content="Already checked in" />,
56-
);
57-
else if (sessionStatus.status === "success")
58-
setStatusCard(
59-
<MessageCard type="success" content="User checked in" />,
60-
);
61-
else
62-
setStatusCard(
63-
<MessageCard type="danger" content="Invalid QR-Code" />,
64-
);
40+
if (sessionStatus) {
41+
updateSessionStatusTimeout.current &&
42+
clearTimeout(updateSessionStatusTimeout.current);
43+
updateSessionStatusTimeout.current = setTimeout(
44+
() => sessionUpdate(),
45+
5 * 1000,
46+
); // Update every 5 seconds
6547
setStatus({
48+
participantsNumber: sessionStatus.participantsNumber,
6649
unregisteredParticipantsNumber:
6750
sessionStatus.unregisteredParticipantsNumber,
68-
participantsNumber: sessionStatus.participantsNumber,
6951
});
7052
}
71-
} else {
72-
setBottomCard(<MessageCard type="danger" content="Invalid QR-Code" />);
73-
}
53+
return sessionStatus;
54+
},
55+
[cannonToken, sinfoSession.id],
56+
);
7457

75-
cardsTimeout = setTimeout(() => {
76-
setBottomCard(null);
77-
setStatusCard(null);
78-
}, 10 * 1000); // 10 seconds
79-
}
58+
const handleQRCodeScanned = useCallback(
59+
async (data: string) => {
60+
const scannedUser = getUserFromQRCode(data);
61+
cardsTimeout.current && clearTimeout(cardsTimeout.current);
62+
63+
if (scannedUser) {
64+
setBottomCard(
65+
<ListCard img={scannedUser.img} title={scannedUser.name} />,
66+
);
67+
const sessionStatus = await sessionUpdate({ users: [scannedUser.id] });
68+
if (!sessionStatus)
69+
setStatusCard(
70+
<MessageCard
71+
type="danger"
72+
content="Failed to check-in user. Try again!"
73+
/>,
74+
);
75+
else {
76+
if (sessionStatus.status === "already")
77+
setStatusCard(
78+
<MessageCard type="warning" content="Already checked in" />,
79+
);
80+
else if (sessionStatus.status === "success")
81+
setStatusCard(
82+
<MessageCard type="success" content="User checked in" />,
83+
);
84+
else
85+
setStatusCard(
86+
<MessageCard type="danger" content="Invalid QR-Code" />,
87+
);
88+
}
89+
} else {
90+
setBottomCard(<MessageCard type="danger" content="Invalid QR-Code" />);
91+
}
92+
93+
cardsTimeout.current = setTimeout(() => {
94+
setBottomCard(null);
95+
setStatusCard(null);
96+
}, 10 * 1000); // 10 seconds
97+
},
98+
[sessionUpdate],
99+
);
100+
101+
const handleUnregisteredUser = useCallback(
102+
async (count: number) => {
103+
const sessionStatus = sessionUpdate({ unregisteredUsers: count });
104+
if (!sessionStatus) {
105+
setBottomCard(
106+
<MessageCard
107+
type="danger"
108+
content="Failed to update unregistered users"
109+
/>,
110+
);
111+
} else {
112+
setBottomCard(
113+
<MessageCard
114+
type="success"
115+
content={
116+
count > 0
117+
? `Added ${count} unregistered user`
118+
: `Removed ${count} unregistered user`
119+
}
120+
/>,
121+
);
122+
setUnregisteredUsersCounter((c) => c + count);
123+
}
124+
},
125+
[sessionUpdate],
126+
);
80127

81128
useEffect(() => {
82129
setBottomCard((card) => (
@@ -92,7 +139,11 @@ export default function SessionCheckInScanner({
92139
<div className="flex flex-col justify-start gap-y-1">
93140
<SessionTile session={sinfoSession} />
94141
<div className="flex justify-center items-center gap-x-4">
95-
<button className="button button-primary !bg-sinfo-secondary flex-1">
142+
<button
143+
className="button button-primary !bg-sinfo-secondary flex-1"
144+
onClick={() => handleUnregisteredUser(-1)}
145+
disabled={unregisteredUsersCounter <= 0}
146+
>
96147
<UserMinus size={32} strokeWidth={1} />
97148
</button>
98149
<div className="flex justify-center items-center bg-white rounded-md text-sm flex-1 p-4 gap-x-2">
@@ -101,14 +152,24 @@ export default function SessionCheckInScanner({
101152
/
102153
<Ghost size={16} />
103154
<span>{status.unregisteredParticipantsNumber}</span>
155+
/
156+
<ScanEye size={16} />
157+
<span>{unregisteredUsersCounter}</span>
104158
</div>
105-
<button className="button button-primary flex-1">
159+
<button
160+
className="button button-primary flex-1"
161+
onClick={() => handleUnregisteredUser(1)}
162+
>
106163
<UserPlus size={32} strokeWidth={1} />
107164
</button>
108165
</div>
109166
</div>,
110167
);
111-
}, [status, sinfoSession]);
168+
}, [status, sinfoSession, handleUnregisteredUser, unregisteredUsersCounter]);
169+
170+
useEffect(() => {
171+
sessionUpdate();
172+
}, [sessionUpdate]);
112173

113174
return (
114175
<QRCodeScanner

src/app/(authenticated)/sessions/[id]/page.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,10 @@ import MessageCard from "@/components/MessageCard";
66
import { ShowMore } from "@/components/ShowMore";
77
import { PrizeTile } from "@/components/prize";
88
import AddToCalendarButton from "@/components/session/AddToCalendarButton";
9-
import { CalendarButton } from "@/components/session/CalendarButton";
109
import { SessionService } from "@/services/SessionService";
1110
import { UserService } from "@/services/UserService";
1211
import { generateTimeInterval, isMember } from "@/utils/utils";
13-
import { Calendar, CalendarClock, MapPin, Scan, Users } from "lucide-react";
12+
import { CalendarClock, MapPin, Scan, Users } from "lucide-react";
1413
import { getServerSession } from "next-auth";
1514
import Image from "next/image";
1615
import Link from "next/link";
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import authOptions from "@/app/api/auth/[...nextauth]/authOptions";
2+
import List from "@/components/List";
3+
import { UserTile } from "@/components/user/UserTile";
4+
import { AchievementService } from "@/services/AchievementService";
5+
import { SessionService } from "@/services/SessionService";
6+
import { UserService } from "@/services/UserService";
7+
import { getServerSession } from "next-auth";
8+
9+
interface SessionParticipantsParams {
10+
id: string;
11+
}
12+
13+
export default async function SessionParticipants({
14+
params,
15+
}: {
16+
params: SessionParticipantsParams;
17+
}) {
18+
const { id: sessionID } = params;
19+
20+
const sinfoSession = await SessionService.getSession(sessionID);
21+
22+
if (!sinfoSession) {
23+
return <div>Session not found</div>;
24+
}
25+
26+
const session = await getServerSession(authOptions);
27+
28+
const sessionAchievement = await AchievementService.getAchievementBySession(
29+
session!.cannonToken,
30+
sessionID,
31+
);
32+
if (!sessionAchievement) {
33+
return <div>Session achievement not found</div>;
34+
}
35+
36+
async function getUserTile(userId: string) {
37+
const user = await UserService.getUser(session!.cannonToken, userId);
38+
if (!user) return undefined;
39+
return <UserTile key={user.id} user={user} />;
40+
}
41+
42+
const unregisteredUsers = sessionAchievement.unregisteredUsers || 0;
43+
const totalUsers =
44+
(sessionAchievement.users?.length ?? 0) + unregisteredUsers;
45+
46+
return (
47+
<div className="container m-auto h-full">
48+
<List
49+
title="Participants"
50+
description={`Total ${totalUsers} participants (${unregisteredUsers} unregistered users)`}
51+
>
52+
{sessionAchievement.users?.length ? (
53+
await Promise.all(
54+
sessionAchievement.users.map((id) => getUserTile(id)),
55+
)
56+
) : (
57+
<div>There are no registered users at this session</div>
58+
)}
59+
</List>
60+
</div>
61+
);
62+
}

src/services/AchievementService.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,23 @@ export const AchievementService = (() => {
1212
return null;
1313
};
1414

15-
return { getAchievements };
15+
const getAchievementBySession = async (
16+
cannonToken: string,
17+
sessionId: string,
18+
): Promise<Achievement | null> => {
19+
try {
20+
const resp = await fetch(`${achievementsEndpoint}/session/${sessionId}`, {
21+
headers: {
22+
Authorization: `Bearer ${cannonToken}`,
23+
},
24+
next: { revalidate: 5 },
25+
});
26+
if (resp.ok) return (await resp.json()) as Achievement;
27+
} catch (e) {
28+
console.error(e);
29+
}
30+
return null;
31+
};
32+
33+
return { getAchievements, getAchievementBySession };
1634
})();

src/services/SessionService.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { revalidatePath } from "next/cache";
2+
13
export const SessionService = (() => {
24
const sessionsEndpoint = process.env.NEXT_PUBLIC_CANNON_URL + "/session";
35

@@ -37,15 +39,23 @@ export const SessionService = (() => {
3739
users,
3840
unregisteredUsers,
3941
};
40-
const resp = await fetch(`${sessionsEndpoint}/${sessionId}/check-in`, {
42+
const resp = await fetch(`${sessionsEndpoint}s/${sessionId}/check-in`, {
4143
method: "POST",
4244
headers: {
4345
"Content-Type": "application/json",
4446
Authorization: `Bearer ${cannonToken}`,
4547
},
4648
body: JSON.stringify(data),
4749
});
48-
if (resp.ok) (await resp.json()) as SINFOSessionStatus;
50+
if (resp.ok) {
51+
// TODO: Revalidate achievements path
52+
const achievementData = (await resp.json()) as Achievement;
53+
return {
54+
status: "success",
55+
participantsNumber: achievementData.users?.length,
56+
unregisteredParticipantsNumber: achievementData.unregisteredUsers,
57+
} as SINFOSessionStatus;
58+
}
4959
} catch (error) {
5060
console.error(error);
5161
}

src/services/UserService.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,14 @@ export const UserService = (() => {
44
const usersEndpoint = process.env.NEXT_PUBLIC_CANNON_URL + "/users";
55
const filesEndpoint = process.env.NEXT_PUBLIC_CANNON_URL + "/files";
66

7-
const getUser = async (id: string): Promise<User | null> => {
7+
const getUser = async (
8+
cannonToken: string,
9+
id: string,
10+
): Promise<User | null> => {
811
const resp = await fetch(`${usersEndpoint}/${id}`, {
12+
headers: {
13+
Authorization: `Bearer ${cannonToken}`,
14+
},
915
next: {
1016
revalidate: 86400, // 1 day
1117
},

src/types/globals.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ type Achievement = {
6767
};
6868
session?: Session;
6969
company?: Company;
70+
users?: string[];
71+
unregisteredUsers?: number;
7072
updated?: string;
7173
};
7274

0 commit comments

Comments
 (0)