From bb2630fd73f014bba265f60bf60f4a2c24d02284 Mon Sep 17 00:00:00 2001 From: Johnny Tan Date: Wed, 23 Apr 2025 01:42:38 -0400 Subject: [PATCH 1/6] Refactored EventCard --- package-lock.json | 12 -- public/locales/en/home.json | 2 +- public/locales/es/home.json | 2 +- src/app/api/timeSlot/route.client.ts | 14 ++- src/app/api/timeSlot/route.ts | 29 +++-- src/app/private/page.tsx | 168 +++++++++++++++++---------- src/app/utils.ts | 20 ++++ src/components/EventCard.tsx | 113 +++++------------- 8 files changed, 178 insertions(+), 182 deletions(-) create mode 100644 src/app/utils.ts diff --git a/package-lock.json b/package-lock.json index 83de7ab..9de643e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1986,7 +1986,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -2560,7 +2559,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -2678,7 +2676,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -2688,7 +2685,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -2726,7 +2722,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -2739,7 +2734,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -3616,7 +3610,6 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -3641,7 +3634,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -3774,7 +3766,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3852,7 +3843,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3865,7 +3855,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -4895,7 +4884,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" diff --git a/public/locales/en/home.json b/public/locales/en/home.json index 41d70ec..0dd83b3 100644 --- a/public/locales/en/home.json +++ b/public/locales/en/home.json @@ -1,7 +1,7 @@ { "welcome_title": "Thanks for checking in", "welcome_subtitle": "What's the next event you want to join", - "upcoming_events": "Upcoming events", + "upcoming_times": "Your upcoming volunteer times", "volunteer_hours": "Personal volunteer hours", "events_attended": "Events attended" } diff --git a/public/locales/es/home.json b/public/locales/es/home.json index aa724c3..9deca7a 100644 --- a/public/locales/es/home.json +++ b/public/locales/es/home.json @@ -1,7 +1,7 @@ { "welcome_title": "Gracias por visitar nuestro sitio", "welcome_subtitle": "¿Cuál es el próximo evento al que quieres unirte?", - "upcoming_events": "Próximos eventos", + "upcoming_times": "Tus próximos tiempos de voluntariado", "volunteer_hours": "Horas de voluntariado", "events_attended": "Eventos asistidos" } diff --git a/src/app/api/timeSlot/route.client.ts b/src/app/api/timeSlot/route.client.ts index d165971..8d6f01c 100644 --- a/src/app/api/timeSlot/route.client.ts +++ b/src/app/api/timeSlot/route.client.ts @@ -40,15 +40,21 @@ export const fetchApi = async ( export const addTimeSlot = async (timeSlot: CreateTimeSlotInput) => fetchApi("/api/timeSlot", "POST", { timeSlot }); +export const getTimeSlots = async (userId: string) => { + const url = `/api/timeSlot?userId=${userId}`; + return fetchApi(url, "GET"); +}; + export const getTimeSlotsByDate = async (userId: string, date: Date) => { const isoDate = date.toISOString().split("T")[0]; - const url = userId - ? `/api/timeSlot?userId=${userId}&date=${isoDate}` - : `/api/timeSlot?date=${isoDate}`; - + const url = `/api/timeSlot?userId=${userId}&date=${isoDate}`; return fetchApi(url, "GET"); }; +export const getTimeSlotsByStatus = async (status: string) => { + const url = `/api/timeSlot?status=${status}`; + return fetchApi(url, "GET"); +}; export const deleteTimeSlot = async ( userId: string, startTime: Date, diff --git a/src/app/api/timeSlot/route.ts b/src/app/api/timeSlot/route.ts index 8af80e9..572e498 100644 --- a/src/app/api/timeSlot/route.ts +++ b/src/app/api/timeSlot/route.ts @@ -1,4 +1,4 @@ -import { PrismaClient } from "@prisma/client"; +import { PrismaClient, TimeSlotStatus } from "@prisma/client"; import { NextRequest, NextResponse } from "next/server"; const prisma = new PrismaClient(); @@ -41,10 +41,11 @@ export const POST = async (request: NextRequest) => { export const GET = async (request: NextRequest) => { const { searchParams } = new URL(request.url); - const userId = searchParams.get("userId"); - const date = searchParams.get("date"); + const userId: string | undefined = searchParams.get("userId") || undefined; + const date: string | undefined = searchParams.get("date") || undefined; + const status: string | undefined = searchParams.get("status") || undefined; - if (!userId || !date) { + if (!userId && !date && !status) { return NextResponse.json( { code: "BAD_REQUEST", @@ -54,23 +55,19 @@ export const GET = async (request: NextRequest) => { ); } - const [year, month, day] = date.split("-").map(Number); - const start = new Date(year, month - 1, day); - start.setHours(0, 0, 0, 0); - const end = new Date(start); - end.setDate(start.getDate() + 1); - try { const slots = await prisma.timeSlot.findMany({ where: { - userId, - date: { - gte: start, - lt: end, - }, + ...(userId && { userId }), + ...(date && { + date: { + gte: new Date(date), + lt: new Date(new Date(date).setDate(new Date(date).getDate() + 1)), + }, + }), + ...(status && { status: status as TimeSlotStatus }), }, }); - return NextResponse.json( { code: "SUCCESS", diff --git a/src/app/private/page.tsx b/src/app/private/page.tsx index cbda7b5..9a49413 100644 --- a/src/app/private/page.tsx +++ b/src/app/private/page.tsx @@ -4,7 +4,7 @@ import { useSession } from "next-auth/react"; import StatsCard from "@components/StatsCard"; import EventCard from "@components/EventCard"; import VolunteerTable from "@components/VolunteerTable"; -import { Role, Event } from "@prisma/client"; +import { Role, Event, TimeSlot, TimeSlotStatus } from "@prisma/client"; import React, { useEffect } from "react"; import { getUsersByRole } from "@api/user/route.client"; import { Icon } from "@iconify/react/dist/iconify.js"; @@ -12,6 +12,8 @@ import { useRouter } from "next/navigation"; import { UserWithVolunteerDetail } from "../types"; import { getAllEvents } from "../api/event/route.client"; import { useTranslation } from "react-i18next"; +import { getTimeSlots, getTimeSlotsByStatus } from "@api/timeSlot/route.client"; +import { sortedStandardTimeSlots } from "../utils"; export default function HomePage() { const router = useRouter(); @@ -20,6 +22,42 @@ export default function HomePage() { const [users, setUsers] = React.useState([]); const [events, setEvents] = React.useState([]); const [loading, setLoading] = React.useState(true); + const [timeSlots, setTimeSlots] = React.useState([]); + const [daySlots, setDaySlots] = React.useState<{ + [key: string]: Set; + }>({}); + + useEffect(() => { + const fetchTimeSlots = async () => { + if (session?.user.role === Role.VOLUNTEER) { + const res = await getTimeSlots(session?.user.id as string); + setTimeSlots(res.data); + } else if (session?.user.role === Role.ADMIN) { + const res = await getTimeSlotsByStatus(TimeSlotStatus.AVAILABLE); + res.data.forEach((timeSlot: TimeSlot) => { + const date = new Date(timeSlot.startTime).toLocaleDateString( + "en-US", + { + weekday: "long", + year: "numeric", + month: "long", + day: "numeric", + } + ); + + setDaySlots((prev) => { + if (!prev[date]) { + prev[date] = new Set(); + } + prev[date].add(timeSlot.id); + return { ...prev }; + }); + }); + } + }; + + fetchTimeSlots(); + }, []); useEffect(() => { const fetchData = async () => { @@ -49,6 +87,37 @@ export default function HomePage() { ); } + const formatVolunteerSubtexts = (startTime: Date, endTime: Date) => { + const dateText = new Date(startTime).toLocaleDateString("en-US", { + weekday: "long", + year: "numeric", + month: "long", + day: "numeric", + }); + + const timeText = + new Date(startTime).toLocaleTimeString("en-US", { + hour: "numeric", + minute: "2-digit", + }) + + " - " + + new Date(endTime).toLocaleTimeString("en-US", { + hour: "numeric", + minute: "2-digit", + }); + + return [ + { text: dateText, icon: "tabler:calendar" }, + { text: timeText, icon: "majesticons:clock" }, + ]; + }; + + const volunteerButton = ( + + ); + return (
@@ -117,9 +186,11 @@ export default function HomePage() {
-
+

- {t("upcoming_events", { ns: "home" })} + {session.user.role === Role.ADMIN + ? "Upcoming Events" + : t("upcoming_times", { ns: "home" })}

-
-
- -
-
- -
-
- -
+
{session.user.role === Role.VOLUNTEER ? ( - <> -
+
+ {timeSlots.map((timeSlot, index) => ( -
-
- -
-
+ ))} +
+ ) : ( +
+ {sortedStandardTimeSlots(Object.keys(daySlots)).map((date) => ( -
- - ) : null} + ))} +
+ )}
{session.user.role === Role.ADMIN ? ( diff --git a/src/app/utils.ts b/src/app/utils.ts new file mode 100644 index 0000000..619cb63 --- /dev/null +++ b/src/app/utils.ts @@ -0,0 +1,20 @@ +import { TimeSlot } from "@prisma/client"; +import { time } from "console"; + +export const compareStandardTimeSlots = (a: string, b: string) => { + const dateA = new Date(a); + const dateB = new Date(b); + + if (dateA < dateB) { + return -1; + } + if (dateA > dateB) { + return 1; + } + return 0; +}; + +// StandardTimeSlots are in format "Day, Month Date, Year" +export const sortedStandardTimeSlots = (sTimeSlots: string[]) => { + return sTimeSlots.sort(compareStandardTimeSlots); +}; diff --git a/src/components/EventCard.tsx b/src/components/EventCard.tsx index bde73df..0decf9b 100644 --- a/src/components/EventCard.tsx +++ b/src/components/EventCard.tsx @@ -3,72 +3,27 @@ import { Icon } from "@iconify/react/dist/iconify.js"; import Image from "next/image"; import { useTranslation } from "react-i18next"; +interface Subtexts { + text: string; + icon: string; +} + interface EventCardProps { title: string; - start: Date; - end: Date; - address: string; - volunteers: number; - maxVolunteers: number; + subtexts?: Subtexts[]; + actionButton?: React.ReactNode; + onClick?: () => void; width?: string | number; imageSrc?: string; // option to add image for eventCards in event page } const EventCard = ({ title, - start, - end, - address, - volunteers, - maxVolunteers, + subtexts, + actionButton, width = 360, imageSrc, }: EventCardProps) => { - const { t } = useTranslation(["translation"]); - const isFull = volunteers === maxVolunteers ? true : false; - const volunteerText = - volunteers + "/" + maxVolunteers + " " + t("volunteers"); - const optionsTime: Intl.DateTimeFormatOptions = { - hour: "numeric", - minute: "numeric", - hour12: true, - }; - - const optionsDate: Intl.DateTimeFormatOptions = { - month: "long", - day: "numeric", - year: "numeric", - }; - - // Helper function to get ordinal suffix - function getOrdinalSuffix(day: number): string { - if (day >= 11 && day <= 13) return "th"; - switch (day % 10) { - case 1: - return "st"; - case 2: - return "nd"; - case 3: - return "rd"; - default: - return "th"; - } - } - - // Format time for both start and end dates - const startTime = start.toLocaleString("en-US", optionsTime); - const endTime = end.toLocaleString("en-US", optionsTime); - - // Format the date - const dateParts = start.toLocaleString("en-US", optionsDate).split(" "); - const month = dateParts[0]; - const day = parseInt(dateParts[1], 10); - const year = dateParts[2]; - const dayWithSuffix = `${day}${getOrdinalSuffix(day)}`; - - // Construct the final string - const dateText = `${startTime} - ${endTime} / ${month} ${dayWithSuffix}, ${year}`; - return (
-
- - {dateText} -
-
- - {address} -
-
-
-
- {volunteerText} + {subtexts?.map((subtext) => ( +
+ + {subtext.text}
- + ))} + +
+ {actionButton && ( +
+ {actionButton} +
+ )}
From 1cf158db41b41bbe800ae10d9fe8b305d54a825e Mon Sep 17 00:00:00 2001 From: Johnny Tan Date: Thu, 24 Apr 2025 14:13:25 -0400 Subject: [PATCH 2/6] Gap changes --- src/app/private/page.tsx | 42 +++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/src/app/private/page.tsx b/src/app/private/page.tsx index 9a49413..bc4a564 100644 --- a/src/app/private/page.tsx +++ b/src/app/private/page.tsx @@ -207,10 +207,10 @@ export default function HomePage() { />
-
+
{session.user.role === Role.VOLUNTEER ? ( -
- {timeSlots.map((timeSlot, index) => ( +
+ {timeSlots.slice(0, 6).map((timeSlot, index) => ( ) : (
- {sortedStandardTimeSlots(Object.keys(daySlots)).map((date) => ( - - ))} + {sortedStandardTimeSlots(Object.keys(daySlots)) + .slice(0, 3) + .map((date) => ( + + ))}
)}
From 258ab98b762f2b725e5d0b35247e73f37e486c26 Mon Sep 17 00:00:00 2001 From: Johnny Tan Date: Fri, 25 Apr 2025 02:22:48 -0400 Subject: [PATCH 3/6] Route Admin/Volunteer --- src/app/private/events/page.tsx | 6 +++++- src/app/private/page.tsx | 29 +++++++++++++++++++------- src/app/utils.ts | 37 ++++++++++++++++++++++++++------- src/components/EventCard.tsx | 7 +++++-- 4 files changed, 62 insertions(+), 17 deletions(-) diff --git a/src/app/private/events/page.tsx b/src/app/private/events/page.tsx index 1cf7e76..f68b8e4 100644 --- a/src/app/private/events/page.tsx +++ b/src/app/private/events/page.tsx @@ -15,12 +15,16 @@ import { } from "@api/timeSlot/route.client"; import VolunteerTable from "@components/VolunteerTable"; import { getUsersByDate } from "@api/user/route.client"; +import { useSearchParams } from "next/navigation"; +import { getStandardDate } from "../../utils"; export default function EventsPage() { const { data: session } = useSession(); + const searchParams = useSearchParams(); + const date = searchParams.get("date"); const [selectedDate, setSelectedDate] = React.useState( - new Date() + getStandardDate(date ?? "") ); const [timeSlots, setTimeSlots] = React.useState([ { start: "", end: "", submitted: false }, diff --git a/src/app/private/page.tsx b/src/app/private/page.tsx index bc4a564..0a41e5b 100644 --- a/src/app/private/page.tsx +++ b/src/app/private/page.tsx @@ -13,7 +13,7 @@ import { UserWithVolunteerDetail } from "../types"; import { getAllEvents } from "../api/event/route.client"; import { useTranslation } from "react-i18next"; import { getTimeSlots, getTimeSlotsByStatus } from "@api/timeSlot/route.client"; -import { sortedStandardTimeSlots } from "../utils"; +import { getStandardDateString, sortedReadableTimeSlots } from "../utils"; export default function HomePage() { const router = useRouter(); @@ -112,9 +112,12 @@ export default function HomePage() { ]; }; - const volunteerButton = ( - ); @@ -218,13 +221,19 @@ export default function HomePage() { timeSlot.startTime, timeSlot.endTime )} - actionButton={volunteerButton} + actionButton={cardButton(() => { + router.push( + `/private/events?date=${getStandardDateString( + timeSlot.startTime + )}` + ); + })} /> ))}
) : (
- {sortedStandardTimeSlots(Object.keys(daySlots)) + {sortedReadableTimeSlots(Object.keys(daySlots)) .slice(0, 3) .map((date) => ( { + router.push( + `/private/events?date=${getStandardDateString( + new Date(date) + )}` + ); + })} /> ))}
diff --git a/src/app/utils.ts b/src/app/utils.ts index 619cb63..c19cb52 100644 --- a/src/app/utils.ts +++ b/src/app/utils.ts @@ -1,7 +1,4 @@ -import { TimeSlot } from "@prisma/client"; -import { time } from "console"; - -export const compareStandardTimeSlots = (a: string, b: string) => { +export const compareReadableTimeSlots = (a: string, b: string) => { const dateA = new Date(a); const dateB = new Date(b); @@ -14,7 +11,33 @@ export const compareStandardTimeSlots = (a: string, b: string) => { return 0; }; -// StandardTimeSlots are in format "Day, Month Date, Year" -export const sortedStandardTimeSlots = (sTimeSlots: string[]) => { - return sTimeSlots.sort(compareStandardTimeSlots); +// Readabletimeslots are in format "Day, Month Date, Year" +export const sortedReadableTimeSlots = (sTimeSlots: string[]) => { + return sTimeSlots.sort(compareReadableTimeSlots); +}; + +// StandardTimeSlots are in format "YYYYMMDD" +export const getStandardDateString = (timeSlot: Date) => { + const date = new Date(timeSlot); + const parts = date + .toLocaleDateString("en-US", { + year: "numeric", + month: "2-digit", + day: "2-digit", + }) + .split("/"); + + return parts[0] + parts[1] + parts[2]; +}; + +export const getStandardDate = (dateString: string) => { + if (dateString.length !== 8) { + return new Date(); + } + + const month = parseInt(dateString.substring(0, 2), 10) - 1; + const day = parseInt(dateString.substring(2, 4), 10); + const year = parseInt(dateString.substring(4, 8), 10); + + return new Date(year, month, day); }; diff --git a/src/components/EventCard.tsx b/src/components/EventCard.tsx index 0decf9b..17fc6d8 100644 --- a/src/components/EventCard.tsx +++ b/src/components/EventCard.tsx @@ -49,8 +49,11 @@ const EventCard = ({
- {subtexts?.map((subtext) => ( -
+ {subtexts?.map((subtext, index) => ( +
Date: Sat, 26 Apr 2025 23:33:27 -0400 Subject: [PATCH 4/6] eslint fix --- src/app/private/page.tsx | 2 +- src/components/EventCard.tsx | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/app/private/page.tsx b/src/app/private/page.tsx index 0a41e5b..1b60354 100644 --- a/src/app/private/page.tsx +++ b/src/app/private/page.tsx @@ -57,7 +57,7 @@ export default function HomePage() { }; fetchTimeSlots(); - }, []); + }, [session?.user.id, session?.user.role]); useEffect(() => { const fetchData = async () => { diff --git a/src/components/EventCard.tsx b/src/components/EventCard.tsx index 17fc6d8..0a59c3f 100644 --- a/src/components/EventCard.tsx +++ b/src/components/EventCard.tsx @@ -1,7 +1,6 @@ import React from "react"; import { Icon } from "@iconify/react/dist/iconify.js"; import Image from "next/image"; -import { useTranslation } from "react-i18next"; interface Subtexts { text: string; From e1977889216e475cff1081a2c9543dbe8c5ba1c7 Mon Sep 17 00:00:00 2001 From: wkim10 Date: Sun, 27 Apr 2025 00:36:19 -0400 Subject: [PATCH 5/6] add fetching hours/days logic and small changes to header and communications page --- src/app/api/volunteerSession/route.client.ts | 5 + src/app/api/volunteerSession/route.ts | 38 ++-- src/app/private/communication/page.tsx | 1 + src/app/private/page.tsx | 185 ++++++++++++------- src/components/TopHeader.tsx | 5 +- 5 files changed, 152 insertions(+), 82 deletions(-) diff --git a/src/app/api/volunteerSession/route.client.ts b/src/app/api/volunteerSession/route.client.ts index 83960b2..d82a858 100644 --- a/src/app/api/volunteerSession/route.client.ts +++ b/src/app/api/volunteerSession/route.client.ts @@ -51,3 +51,8 @@ export const getVolunteerSessions = async (userId: string) => { const url = `/api/volunteerSession?userId=${userId}`; return fetchApi(url, "GET"); }; + +export const getAllVolunteerSessions = async () => { + const url = `/api/volunteerSession`; + return fetchApi(url, "GET"); +}; diff --git a/src/app/api/volunteerSession/route.ts b/src/app/api/volunteerSession/route.ts index 441496e..4ca517a 100644 --- a/src/app/api/volunteerSession/route.ts +++ b/src/app/api/volunteerSession/route.ts @@ -113,22 +113,36 @@ export const GET = async (request: NextRequest) => { const { searchParams } = new URL(request.url); const userId: string | undefined = searchParams.get("userId") || undefined; - if (!userId) { - return NextResponse.json( - { - code: "BAD_REQUEST", - message: "User ID is required.", - }, - { - status: 400, + try { + if (userId) { + const volunteerSessions = await prisma.volunteerSession.findMany({ + where: { + userId: userId, + NOT: [{ checkOutTime: null }, { durationHours: null }], + }, + }); + + if (!volunteerSessions) { + return NextResponse.json( + { + code: "NOT_FOUND", + message: "No volunteer sessions found", + }, + { status: 404 } + ); } - ); - } - try { + return NextResponse.json( + { + code: "SUCCESS", + data: volunteerSessions, + }, + { status: 200 } + ); + } + const volunteerSessions = await prisma.volunteerSession.findMany({ where: { - userId: userId, NOT: [{ checkOutTime: null }, { durationHours: null }], }, }); diff --git a/src/app/private/communication/page.tsx b/src/app/private/communication/page.tsx index a81e451..25c4418 100644 --- a/src/app/private/communication/page.tsx +++ b/src/app/private/communication/page.tsx @@ -197,6 +197,7 @@ export default function CommunicationPage() { placeholder="Type your email content" value={text} onChange={(e) => setText(e.target.value)} + rows={10} />
diff --git a/src/app/private/page.tsx b/src/app/private/page.tsx index 1b60354..b833b90 100644 --- a/src/app/private/page.tsx +++ b/src/app/private/page.tsx @@ -4,9 +4,15 @@ import { useSession } from "next-auth/react"; import StatsCard from "@components/StatsCard"; import EventCard from "@components/EventCard"; import VolunteerTable from "@components/VolunteerTable"; -import { Role, Event, TimeSlot, TimeSlotStatus } from "@prisma/client"; +import { + Role, + Event, + TimeSlot, + TimeSlotStatus, + VolunteerSession, +} from "@prisma/client"; import React, { useEffect } from "react"; -import { getUsersByRole } from "@api/user/route.client"; +import { getUser, getUsersByRole } from "@api/user/route.client"; import { Icon } from "@iconify/react/dist/iconify.js"; import { useRouter } from "next/navigation"; import { UserWithVolunteerDetail } from "../types"; @@ -14,6 +20,8 @@ import { getAllEvents } from "../api/event/route.client"; import { useTranslation } from "react-i18next"; import { getTimeSlots, getTimeSlotsByStatus } from "@api/timeSlot/route.client"; import { getStandardDateString, sortedReadableTimeSlots } from "../utils"; +import Image from "next/image"; +import { getAllVolunteerSessions } from "@api/volunteerSession/route.client"; export default function HomePage() { const router = useRouter(); @@ -26,39 +34,86 @@ export default function HomePage() { const [daySlots, setDaySlots] = React.useState<{ [key: string]: Set; }>({}); + const [sessions, setSessions] = React.useState([]); + const [hours, setHours] = React.useState("..."); + const [days, setDays] = React.useState(0); useEffect(() => { const fetchTimeSlots = async () => { if (session?.user.role === Role.VOLUNTEER) { + const userRes = await getUser(session?.user.id as string); const res = await getTimeSlots(session?.user.id as string); - setTimeSlots(res.data); + const now = new Date(); + const upcomingSlots = res.data.filter( + (slot: TimeSlot) => new Date(slot.startTime) > now + ); + + setSessions(userRes.data.volunteerSessions); + setTimeSlots(upcomingSlots); } else if (session?.user.role === Role.ADMIN) { const res = await getTimeSlotsByStatus(TimeSlotStatus.AVAILABLE); - res.data.forEach((timeSlot: TimeSlot) => { - const date = new Date(timeSlot.startTime).toLocaleDateString( - "en-US", - { - weekday: "long", - year: "numeric", - month: "long", - day: "numeric", - } - ); + const sessionsRes = await getAllVolunteerSessions(); + console.log(sessionsRes.data); + + const now = new Date(); + const today = new Date( + now.getFullYear(), + now.getMonth(), + now.getDate() + ); - setDaySlots((prev) => { - if (!prev[date]) { - prev[date] = new Set(); - } - prev[date].add(timeSlot.id); - return { ...prev }; + const daysAhead = 3; + const daySlotsTemp: { [key: string]: Set } = {}; + + for (let i = 0; i < daysAhead; i++) { + const date = new Date(today); + date.setDate(today.getDate() + i); + + const dateString = date.toLocaleDateString("en-US", { + weekday: "long", + year: "numeric", + month: "long", + day: "numeric", }); + + daySlotsTemp[dateString] = new Set(); + } + + res.data.forEach((timeSlot: TimeSlot) => { + const start = new Date(timeSlot.startTime); + const dateString = start.toLocaleDateString("en-US", { + weekday: "long", + year: "numeric", + month: "long", + day: "numeric", + }); + + if (daySlotsTemp[dateString]) { + daySlotsTemp[dateString].add(timeSlot.id); + } }); + + setSessions(sessionsRes.data); + setDaySlots(daySlotsTemp); } }; fetchTimeSlots(); }, [session?.user.id, session?.user.role]); + React.useEffect(() => { + const totalHours = sessions.reduce((acc, session) => { + return acc + (session.durationHours ?? 0); + }, 0); + + const uniqueDays = new Set( + sessions.map((session) => new Date(session.dateWorked).toDateString()) + ); + + setHours(totalHours.toFixed(1)); + setDays(uniqueDays.size); + }, [sessions]); + useEffect(() => { const fetchData = async () => { try { @@ -128,7 +183,7 @@ export default function HomePage() { {t("welcome_title", { ns: "home" })}, {session.user.firstName} 👋

- ======= Stats updated by:{" "} + Stats updated by:{" "} {(() => { const date = new Date().toLocaleDateString("en-GB", { weekday: "long", @@ -158,34 +213,16 @@ export default function HomePage() { ? "Total volunteer hours" : t("volunteer_hours", { ns: "home" }) } - value={ - !loading - ? session.user.role === Role.ADMIN && users - ? users.reduce( - (sum, user) => - sum + (user.volunteerDetails?.hoursWorked || 0), - 0 - ) - : session.user.volunteerDetails?.hoursWorked || 0 - : "..." - } + value={hours} icon="tabler:clock-check" /> - + {session.user.role === Role.VOLUNTEER && ( + + )}

@@ -210,27 +247,43 @@ export default function HomePage() { />
-
+
{session.user.role === Role.VOLUNTEER ? ( -
- {timeSlots.slice(0, 6).map((timeSlot, index) => ( - { - router.push( - `/private/events?date=${getStandardDateString( - timeSlot.startTime - )}` - ); - })} - /> - ))} -
+ timeSlots.length === 0 ? ( +
+
+ Empty List +
+
+ It seems like you have not signed up for any time slots yet! +
+
+ ) : ( +
+ {timeSlots.slice(0, 6).map((timeSlot, index) => ( + { + router.push( + `/private/events?date=${getStandardDateString( + timeSlot.startTime + )}` + ); + })} + /> + ))} +
+ ) ) : (
{sortedReadableTimeSlots(Object.keys(daySlots)) diff --git a/src/components/TopHeader.tsx b/src/components/TopHeader.tsx index 3060c02..a931ee3 100644 --- a/src/components/TopHeader.tsx +++ b/src/components/TopHeader.tsx @@ -73,10 +73,7 @@ const TopHeader = ({ user }: TopHeaderProps) => {
{pathname === "/private" ? ( - +
Home
) : pathname === "/private/events" && user.role === Role.ADMIN ? (