Skip to content

Commit b0db5ce

Browse files
committed
Add Prizes screen
1 parent 08340d3 commit b0db5ce

File tree

20 files changed

+332
-71
lines changed

20 files changed

+332
-71
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"use client";
2+
3+
import EventDayButton from "@/components/EventDayButton";
4+
import GridList from "@/components/GridList";
5+
import List from "@/components/List";
6+
import { PrizeTile } from "@/components/prize";
7+
import { getEventFullDate } from "@/utils/utils";
8+
import { useEffect, useMemo, useState } from "react";
9+
10+
interface DailyPrizesTable {
11+
prizes: Prize[];
12+
}
13+
14+
export default function DailyPrizesTable({ prizes }: DailyPrizesTable) {
15+
const [showingDay, setShowingDay] = useState<string | null>(null);
16+
17+
const prizesByDay = useMemo(() => {
18+
const sortedPrizes = prizes.sort((a, b) => a.name.localeCompare(b.name));
19+
return sortedPrizes.reduce(
20+
(acc, sp) => {
21+
const days = sp.days!.map((d) => d.split("T")[0]);
22+
return days.reduce(
23+
(a, d) => ({ ...a, [d]: [...(a[d] || []), sp] }),
24+
acc,
25+
);
26+
},
27+
{} as Record<string, Prize[]>,
28+
);
29+
}, [prizes]);
30+
31+
const sortedDays = useMemo(
32+
() => Object.keys(prizesByDay).sort(),
33+
[prizesByDay],
34+
);
35+
36+
useEffect(() => {
37+
const today = new Date().toISOString().split("T")[0];
38+
setShowingDay(sortedDays.find((d) => d === today) || sortedDays[0]);
39+
}, [sortedDays]);
40+
41+
return (
42+
<>
43+
<GridList>
44+
{sortedDays.map((d) => (
45+
<EventDayButton
46+
key={`event-day-${d}`}
47+
date={d}
48+
onClick={() => setShowingDay(d)}
49+
selected={showingDay === d}
50+
/>
51+
))}
52+
</GridList>
53+
{sortedDays
54+
.filter((d) => !showingDay || d === showingDay)
55+
.map((d) => (
56+
<List key={d} description={getEventFullDate(d)}>
57+
{prizesByDay[d].map((p) => (
58+
<PrizeTile key={p.id} prize={p} />
59+
))}
60+
</List>
61+
))}
62+
</>
63+
);
64+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"use client";
2+
3+
import List from "@/components/List";
4+
import { SessionTile } from "@/components/session";
5+
import { useState } from "react";
6+
7+
interface PrizeSessionsProps {
8+
sessions: SINFOSession[];
9+
compressedSize?: number;
10+
}
11+
12+
export default function PrizeSessions({
13+
sessions,
14+
compressedSize = 3,
15+
}: PrizeSessionsProps) {
16+
const [expandList, setExpandList] = useState(false);
17+
18+
return (
19+
<div className="flex flex-col gap-y-2 py-2">
20+
<span className="text-sm text-gray-600">
21+
To win the prize above, go to a session below:
22+
</span>
23+
{sessions.slice(0, expandList ? undefined : compressedSize).map((s) => (
24+
<SessionTile key={s.id} session={s} />
25+
))}
26+
{sessions.length > compressedSize && (
27+
<label
28+
role="button"
29+
className="text-sm font-semibold hover:cursor-pointer"
30+
onClick={() => setExpandList((e) => !e)}
31+
>
32+
{expandList ? "Show less" : "Show more sessions"}
33+
</label>
34+
)}
35+
</div>
36+
);
37+
}
+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import authOptions from "@/app/api/auth/[...nextauth]/authOptions";
2+
import List from "@/components/List";
3+
import { PrizeTile } from "@/components/prize";
4+
import { AchievementService } from "@/services/AchievementService";
5+
import { PrizeService } from "@/services/PrizeService";
6+
import { SessionService } from "@/services/SessionService";
7+
import { UserService } from "@/services/UserService";
8+
import { isCompany, isMember } from "@/utils/utils";
9+
import { getServerSession } from "next-auth";
10+
import PrizeSessions from "./PrizeSessions";
11+
import MessageCard from "@/components/MessageCard";
12+
import DailyPrizesTable from "./DailyPrizesTable";
13+
14+
export default async function Prizes() {
15+
const prizes = await PrizeService.getPrizes();
16+
17+
if (!prizes) {
18+
return <div>Prizes not found.</div>;
19+
}
20+
21+
const session = await getServerSession(authOptions);
22+
const user = await UserService.getMe(session!.cannonToken);
23+
24+
// Kinds of prizes
25+
const cvPrize = prizes.find((p) => p.cv);
26+
const dailyPrizes = prizes.filter((p) => p.days?.length);
27+
const sessionPrizes = prizes.filter((p) => p.sessions?.length);
28+
29+
const sinfoSessions = (await SessionService.getSessions()) || [];
30+
// TODO: Sort sessions and put at first the next ones
31+
32+
async function getCVPrizeParticipants() {
33+
const achievements = await AchievementService.getAchievements();
34+
const cvPrizeUsers = achievements?.find((a) => a.kind === "cv")?.users;
35+
36+
return cvPrizeUsers?.map((u) => ({ userId: u })) || [];
37+
}
38+
39+
return (
40+
<div className="container m-auto h-full">
41+
<div className="flex flex-col items-start gap-y-2 p-4 text-start text-sm">
42+
<h1 className="text-2xl font-bold">Prizes</h1>
43+
{user && isCompany(user.role) && (
44+
<MessageCard
45+
type="info"
46+
content="As a company you are not allowed to participate in this prize."
47+
/>
48+
)}
49+
</div>
50+
51+
{/* CV */}
52+
{cvPrize && (
53+
<List
54+
title="CV Prize"
55+
description="Submit your CV and get the chance to win"
56+
>
57+
<PrizeTile
58+
prize={cvPrize}
59+
participants={await getCVPrizeParticipants()}
60+
pickWinner={!!user && isMember(user.role)}
61+
cannonToken={session?.cannonToken}
62+
/>
63+
</List>
64+
)}
65+
66+
{/* Daily Prizes */}
67+
<div className="flex flex-col">
68+
<div className="flex flex-col px-4">
69+
<h3 className="text-lg font-bold">Daily prizes</h3>
70+
<span className="text-sm text-gray-600">
71+
Visit SINFO and get the chance to win
72+
</span>
73+
</div>
74+
<DailyPrizesTable prizes={dailyPrizes} />
75+
</div>
76+
77+
{/* Sessions Prizes */}
78+
<List
79+
title="Session prizes"
80+
description="Go to a session and get the chance to win"
81+
>
82+
{sessionPrizes.map((sessionPrize) => (
83+
<div key={sessionPrize.id} className="py-4">
84+
<PrizeTile prize={sessionPrize} />
85+
<PrizeSessions
86+
sessions={sinfoSessions.filter((s) =>
87+
sessionPrize.sessions?.includes(s.id),
88+
)}
89+
/>
90+
</div>
91+
))}
92+
</List>
93+
</div>
94+
);
95+
}

src/app/(authenticated)/profile/achievements/page.tsx

+5-6
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ export default async function Achievements() {
1616
const session = await getServerSession(authOptions);
1717
const user: User | null = await UserService.getMe(session!.cannonToken);
1818

19+
const userAchievements =
20+
user && achievements?.filter((a) => a.users?.includes(user.id));
21+
1922
const achievementsByKind = achievements.reduce(
2023
(acc, a) => {
2124
const kindAchievements = [...(acc[a.kind] || []), a];
@@ -33,7 +36,7 @@ export default async function Achievements() {
3336
<h1 className="text-2xl font-bold">Achievements</h1>
3437
<span className="text-gray-600">
3538
Total points:{" "}
36-
{user?.achievements?.reduce((acc, a) => acc + a.value, 0) || 0}
39+
{userAchievements?.reduce((acc, a) => acc + a.value, 0) || 0}
3740
</span>
3841
{/* TODO: Add bullshit text, ask ChatGPT or Copilot */}
3942
</div>
@@ -43,11 +46,7 @@ export default async function Achievements() {
4346
<AchievementTile
4447
key={a.id}
4548
achievement={a}
46-
achieved={
47-
!!user?.achievements?.find(
48-
(achievement) => achievement.id === a.id,
49-
)
50-
}
49+
achieved={!!user && a.users?.includes(user.id)}
5150
/>
5251
))}
5352
</GridList>
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,15 @@
11
import authOptions from "@/app/api/auth/[...nextauth]/authOptions";
22
import List from "@/components/List";
3-
import { UserTile } from "@/components/user/UserTile";
43
import { UserService } from "@/services/UserService";
54
import { getServerSession } from "next-auth";
65

76
export default async function Connections() {
87
const session = await getServerSession(authOptions);
98
const user: User | null = await UserService.getMe(session!.cannonToken);
109

11-
if (!user?.connections) {
12-
return <div>Connections not found.</div>;
13-
}
14-
1510
return (
1611
<div className="container m-auto h-full">
17-
<List title="Connections">
18-
{user.connections.map((u) => (
19-
<UserTile key={u.id} user={u} />
20-
))}
21-
</List>
12+
<List title="Connections"></List>
2213
</div>
2314
);
2415
}

src/app/(authenticated)/profile/page.tsx

+12-7
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@ import MessageCard from "@/components/MessageCard";
66
import AchievementTile from "@/components/user/AchievementTile";
77
import CurriculumVitae from "@/components/user/CurriculumVitae";
88
import ProfileHeader from "@/components/user/ProfileHeader";
9-
import { UserTile } from "@/components/user/UserTile";
9+
import { AchievementService } from "@/services/AchievementService";
1010
import { UserService } from "@/services/UserService";
1111
import { isCompany } from "@/utils/utils";
1212
import { Award, UserPen } from "lucide-react";
1313
import { getServerSession } from "next-auth";
1414
import Link from "next/link";
1515

1616
const N_ACHIEVEMENTS = 5;
17-
const N_CONNECTIONS = 3;
17+
// const N_CONNECTIONS = 3;
1818

1919
export default async function Profile() {
2020
const session = (await getServerSession(authOptions))!;
@@ -24,6 +24,11 @@ export default async function Profile() {
2424
return <div>Profile not found</div>;
2525
}
2626

27+
const achievements = await AchievementService.getAchievements();
28+
const userAchievements = achievements?.filter((a) =>
29+
a.users?.includes(user.id),
30+
);
31+
2732
return (
2833
<div className="container m-auto h-full">
2934
<ProfileHeader user={user} />
@@ -72,12 +77,12 @@ export default async function Profile() {
7277
{/* Achievements */}
7378
<GridList
7479
title="Achievements"
75-
description={`Total points: ${user.achievements?.reduce((acc, a) => acc + a.value, 0) || 0}`}
80+
description={`Total points: ${userAchievements?.reduce((acc, a) => acc + a.value, 0) || 0}`}
7681
link="/profile/achievements"
7782
linkText="See all"
7883
>
79-
{user.achievements?.length ? (
80-
user.achievements
84+
{userAchievements?.length ? (
85+
userAchievements
8186
?.slice(0, N_ACHIEVEMENTS)
8287
.map((a) => <AchievementTile key={a.id} achievement={a} achieved />)
8388
) : (
@@ -92,7 +97,7 @@ export default async function Profile() {
9297
</GridList>
9398

9499
{/* Connections */}
95-
{user.connections?.length ? (
100+
{/* user.connections?.length ? (
96101
<List
97102
title="Connections"
98103
link="/profile/connections"
@@ -104,7 +109,7 @@ export default async function Profile() {
104109
</List>
105110
) : (
106111
<></>
107-
)}
112+
) */}
108113
</div>
109114
);
110115
}

src/app/(authenticated)/qr/page.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export default async function QR() {
7070
{company && (
7171
<Link href={`/companies/${company.id}`}>
7272
<Image
73-
className="object-contain"
73+
className="h-[100px] w-[100px] object-contain"
7474
width={100}
7575
height={100}
7676
src={company.img}

0 commit comments

Comments
 (0)