From d65e37613cd647cdec25262eb7017322f9f764e9 Mon Sep 17 00:00:00 2001 From: fredrir Date: Sun, 21 Jul 2024 21:43:54 +0200 Subject: [PATCH 01/66] add room attribute --- lib/types/types.ts | 1 + pages/committee/[period-id]/[committee]/index.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/types/types.ts b/lib/types/types.ts index f1e28104..0091aad3 100644 --- a/lib/types/types.ts +++ b/lib/types/types.ts @@ -85,6 +85,7 @@ export type periodType = { export type AvailableTime = { start: string; end: string; + room: string; }; export type committeeInterviewType = { diff --git a/pages/committee/[period-id]/[committee]/index.tsx b/pages/committee/[period-id]/[committee]/index.tsx index f12571ec..0cfc9f23 100644 --- a/pages/committee/[period-id]/[committee]/index.tsx +++ b/pages/committee/[period-id]/[committee]/index.tsx @@ -54,7 +54,7 @@ const CommitteeApplicantOverView: NextPage = () => { if (!session || !periodId || !committee) return; const fetchCommitteeInterviewTimes = async () => { - if (!session) { + if (!session.user?.committees?.includes(committee)) { return; } if (period?._id === undefined) return; From 7def9c95506d95a2d89121d0cbfdb3e917797def Mon Sep 17 00:00:00 2001 From: fredrir Date: Sun, 21 Jul 2024 22:22:29 +0200 Subject: [PATCH 02/66] room name is now added in committee interview times --- .../committee/CommitteeInterviewTimes.tsx | 75 ++++++++++++++++--- lib/types/types.ts | 2 +- .../[period-id]/[committee]/index.tsx | 1 + 3 files changed, 65 insertions(+), 13 deletions(-) diff --git a/components/committee/CommitteeInterviewTimes.tsx b/components/committee/CommitteeInterviewTimes.tsx index d08b0d04..65b0033e 100644 --- a/components/committee/CommitteeInterviewTimes.tsx +++ b/components/committee/CommitteeInterviewTimes.tsx @@ -11,6 +11,7 @@ import Button from "../Button"; import ImportantNote from "../ImportantNote"; interface Interview { + title: string; start: string; end: string; } @@ -41,6 +42,10 @@ const CommitteeInterviewTimes = ({ useState(false); const [countdown, setCountdown] = useState(""); + const [isModalOpen, setIsModalOpen] = useState(false); + const [currentSelection, setCurrentSelection] = useState(null); + const [roomInput, setRoomInput] = useState(""); + useEffect(() => { if (period) { setVisibleRange({ @@ -65,6 +70,7 @@ const CommitteeInterviewTimes = ({ setHasAlreadySubmitted(true); const events = committeeInterviewTimes.availabletimes.map( (at: any) => ({ + title: at.room, start: new Date(at.start).toISOString(), end: new Date(at.end).toISOString(), }) @@ -80,17 +86,32 @@ const CommitteeInterviewTimes = ({ } }, [committeeInterviewTimes]); - const createInterval = (selectionInfo: any) => { + const handleDateSelect = (selectionInfo: any) => { + setCurrentSelection(selectionInfo); + setIsModalOpen(true); + }; + + const handleRoomSubmit = () => { + if (!roomInput) { + toast.error("Vennligst skriv inn et romnavn"); + return; + } + const event = { - title: "", - start: selectionInfo.start, - end: selectionInfo.end, + title: roomInput, + start: currentSelection.start, + end: currentSelection.end, }; - selectionInfo.view.calendar.addEvent(event); + + currentSelection.view.calendar.addEvent(event); addCell([ - selectionInfo.start.toISOString(), - selectionInfo.end.toISOString(), + roomInput, + currentSelection.start.toISOString(), + currentSelection.end.toISOString(), ]); + + setRoomInput(""); + setIsModalOpen(false); }; const submit = async (e: BaseSyntheticEvent) => { @@ -141,7 +162,10 @@ const CommitteeInterviewTimes = ({ }; const addCell = (cell: string[]) => { - setMarkedCells([...markedCells, { start: cell[0], end: cell[1] }]); + setMarkedCells([ + ...markedCells, + { title: cell[0], start: cell[1], end: cell[2] }, + ]); }; const updateInterviewInterval = (e: BaseSyntheticEvent) => { @@ -150,8 +174,10 @@ const CommitteeInterviewTimes = ({ const renderEventContent = (eventContent: any) => { return ( -
- {eventContent.timeText} +
+

+ {eventContent.event.title} +

{!hasAlreadySubmitted && ( )} @@ -186,6 +212,7 @@ const CommitteeInterviewTimes = ({ const startDateTime = new Date(startDateTimeString); const endDateTime = new Date(endDatetimeString); return { + room: event.title, start: startDateTime.toISOString(), end: endDateTime.toISOString(), }; @@ -332,7 +359,7 @@ const CommitteeInterviewTimes = ({ selectable={!hasAlreadySubmitted} selectMirror={true} height="auto" - select={createInterval} + select={handleDateSelect} slotDuration={`00:${interviewInterval}`} businessHours={{ startTime: "08:00", endTime: "18:00" }} weekends={false} @@ -378,6 +405,30 @@ const CommitteeInterviewTimes = ({ />
+ + {isModalOpen && ( +
+
+

+ Skriv inn navn på rom: +

+ setRoomInput(e.target.value)} + /> +
+
+
+
+ )}
); }; diff --git a/lib/types/types.ts b/lib/types/types.ts index 0091aad3..c918a9b4 100644 --- a/lib/types/types.ts +++ b/lib/types/types.ts @@ -83,9 +83,9 @@ export type periodType = { }; export type AvailableTime = { + room: string; start: string; end: string; - room: string; }; export type committeeInterviewType = { diff --git a/pages/committee/[period-id]/[committee]/index.tsx b/pages/committee/[period-id]/[committee]/index.tsx index 0cfc9f23..5d140c74 100644 --- a/pages/committee/[period-id]/[committee]/index.tsx +++ b/pages/committee/[period-id]/[committee]/index.tsx @@ -64,6 +64,7 @@ const CommitteeApplicantOverView: NextPage = () => { `/api/committees/times/${period?._id}/${committee}` ); const data = await response.json(); + if (response.ok) { setCommitteeInterviewTimes(data.committees[0]); } else { From 83685b728bf0807882a973d435a307fde0c67681 Mon Sep 17 00:00:00 2001 From: fredrir Date: Sun, 21 Jul 2024 22:32:50 +0200 Subject: [PATCH 03/66] responsivnes --- components/committee/CommitteeInterviewTimes.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/components/committee/CommitteeInterviewTimes.tsx b/components/committee/CommitteeInterviewTimes.tsx index 65b0033e..69e07692 100644 --- a/components/committee/CommitteeInterviewTimes.tsx +++ b/components/committee/CommitteeInterviewTimes.tsx @@ -174,13 +174,10 @@ const CommitteeInterviewTimes = ({ const renderEventContent = (eventContent: any) => { return ( -
-

- {eventContent.event.title} -

+
{!hasAlreadySubmitted && ( )} +

+ {eventContent.event.title} +

); }; From ad82ca247a3c98d06c2e7d9f203e42abae54f046 Mon Sep 17 00:00:00 2001 From: Julian Date: Sun, 21 Jul 2024 22:56:57 +0200 Subject: [PATCH 04/66] =?UTF-8?q?styling=20=F0=9F=AB=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/committee/CommitteeInterviewTimes.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/committee/CommitteeInterviewTimes.tsx b/components/committee/CommitteeInterviewTimes.tsx index 69e07692..a363cdb7 100644 --- a/components/committee/CommitteeInterviewTimes.tsx +++ b/components/committee/CommitteeInterviewTimes.tsx @@ -418,7 +418,7 @@ const CommitteeInterviewTimes = ({ value={roomInput} onChange={(e) => setRoomInput(e.target.value)} /> -
+
diff --git a/components/committee/CommitteeInterviewTimes.tsx b/components/committee/CommitteeInterviewTimes.tsx index 48792288..8863b785 100644 --- a/components/committee/CommitteeInterviewTimes.tsx +++ b/components/committee/CommitteeInterviewTimes.tsx @@ -321,7 +321,7 @@ const CommitteeInterviewTimes = ({ return ""; }; - if (!session || !session.user?.isCommitee) { + if (!session || !session.user?.isCommittee) { return ; } diff --git a/lib/types/next-auth.d.ts b/lib/types/next-auth.d.ts index 03334223..efea3a9b 100644 --- a/lib/types/next-auth.d.ts +++ b/lib/types/next-auth.d.ts @@ -16,6 +16,6 @@ declare module "next-auth" { phone?: string; grade?: number; committees?: string[]; - isCommitee: boolean; + isCommittee: boolean; } } diff --git a/lib/utils/apiChecks.ts b/lib/utils/apiChecks.ts index eeb7ff41..bb35799b 100644 --- a/lib/utils/apiChecks.ts +++ b/lib/utils/apiChecks.ts @@ -25,7 +25,7 @@ export const checkOwId = (res: NextApiResponse, session: any, id: string) => { }; export const isInCommitee = (res: NextApiResponse, session: any) => { - if (!session?.user?.isCommitee) { + if (!session?.user?.isCommittee) { res.status(403).json({ error: "Access denied, unauthorized" }); return false; } diff --git a/pages/api/auth/[...nextauth].ts b/pages/api/auth/[...nextauth].ts index f32d487d..4252c98c 100644 --- a/pages/api/auth/[...nextauth].ts +++ b/pages/api/auth/[...nextauth].ts @@ -57,7 +57,7 @@ export const authOptions: NextAuthOptions = { committees: committeeData.results.map((committee: any) => committee.name_short.toLowerCase() ), - isCommitee: userInfo.is_committee, + isCommittee: userInfo.is_committee, }; }, }), @@ -75,7 +75,7 @@ export const authOptions: NextAuthOptions = { token.grade = user.grade; token.subId = user.subId; token.committees = user.committees; - token.isCommitee = user.isCommitee; + token.isCommittee = user.isCommittee; token.role = adminEmails.includes(user.email) ? "admin" : "user"; } return token; @@ -91,7 +91,7 @@ export const authOptions: NextAuthOptions = { session.user.grade = token.grade as number; session.user.id = token.id as string; session.user.committees = token.committees as string[]; - session.user.isCommitee = token.isCommitee as boolean; + session.user.isCommittee = token.isCommittee as boolean; } return session; }, diff --git a/pages/api/committees/applicants/[period-id]/[committee].ts b/pages/api/committees/applicants/[period-id]/[committee].ts index 29f91c82..ab0dcf17 100644 --- a/pages/api/committees/applicants/[period-id]/[committee].ts +++ b/pages/api/committees/applicants/[period-id]/[committee].ts @@ -23,7 +23,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { .json({ error: "Invalid or missing periodId parameter" }); } - if (!session.user?.isCommitee || !session.user.committees) { + if (!session.user?.isCommittee || !session.user.committees) { return res.status(403).json({ error: "Access denied, unauthorized" }); } diff --git a/pages/committee/index.tsx b/pages/committee/index.tsx index 0f88306b..34a4aa30 100644 --- a/pages/committee/index.tsx +++ b/pages/committee/index.tsx @@ -81,7 +81,7 @@ const Committee: NextPage = () => { { label: "Intervju", field: "interview" }, ]; - if (!session || !session.user?.isCommitee) { + if (!session || !session.user?.isCommittee) { return

Ingen tilgang!

; } From a595074494e382c98499ad7ddaacc5fcc3ce87a2 Mon Sep 17 00:00:00 2001 From: Julian Ammouche Ottosen <42567826+julian-ao@users.noreply.github.com> Date: Wed, 24 Jul 2024 23:17:55 +0200 Subject: [PATCH 37/66] Added loading to period cards (#157) * added loading to period cards * Added skeleton loader to PeriodCard --------- Co-authored-by: fredrir --- components/PeriodCard.tsx | 13 +++++++++++++ pages/apply.tsx | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/components/PeriodCard.tsx b/components/PeriodCard.tsx index b679235e..909470d9 100644 --- a/components/PeriodCard.tsx +++ b/components/PeriodCard.tsx @@ -14,6 +14,7 @@ const PeriodCard = ({ period }: Props) => { const { data: session } = useSession(); const router = useRouter(); const [hasApplied, setHasApplied] = useState(false); + const [isLoading, setIsLoading] = useState(true); useEffect(() => { const checkApplicationStatus = async () => { @@ -24,6 +25,7 @@ const PeriodCard = ({ period }: Props) => { if (response.ok) { const data = await response.json(); setHasApplied(data.exists); + setIsLoading(false); } } }; @@ -33,6 +35,17 @@ const PeriodCard = ({ period }: Props) => { } }, [period._id, session?.user?.owId]); + if (isLoading) { + return ( +
+
+
+
+
+
+ ); + } + return (
diff --git a/pages/apply.tsx b/pages/apply.tsx index dc6f3f14..4fce96e6 100644 --- a/pages/apply.tsx +++ b/pages/apply.tsx @@ -74,7 +74,7 @@ const Apply = () => {

Nåværende opptaksperioder

-
+
{currentPeriods.map((period: periodType, index: number) => ( ))} From 5e949a91e7e0529b13ec0e4cb2a4583d2b699dc7 Mon Sep 17 00:00:00 2001 From: Julian Ammouche Ottosen <42567826+julian-ao@users.noreply.github.com> Date: Thu, 25 Jul 2024 18:31:10 +0200 Subject: [PATCH 38/66] use tanstack query for fetching of ow committees (#196) * use tanstack query for fetching of ow committees * typo --- lib/api/committees.ts | 3 ++ pages/admin/new-period.tsx | 55 ++++++++++++++++---------------- pages/committees.tsx | 65 ++++++++++++++------------------------ 3 files changed, 54 insertions(+), 69 deletions(-) create mode 100644 lib/api/committees.ts diff --git a/lib/api/committees.ts b/lib/api/committees.ts new file mode 100644 index 00000000..e14b45a9 --- /dev/null +++ b/lib/api/committees.ts @@ -0,0 +1,3 @@ +export const fetchOwCommittees = async () => { + return fetch(`/api/periods/ow-committees`).then((res) => res.json()); +}; diff --git a/pages/admin/new-period.tsx b/pages/admin/new-period.tsx index dc9a027a..438da9a5 100644 --- a/pages/admin/new-period.tsx +++ b/pages/admin/new-period.tsx @@ -10,6 +10,9 @@ import TextAreaInput from "../../components/form/TextAreaInput"; import TextInput from "../../components/form/TextInput"; import { DeepPartial, periodType } from "../../lib/types/types"; import { validatePeriod } from "../../lib/utils/PeriodValidator"; +import { useQuery } from "@tanstack/react-query"; +import { fetchOwCommittees } from "../../lib/api/committees"; +import ErrorPage from "../../components/ErrorPage"; const NewPeriod = () => { const router = useRouter(); @@ -31,6 +34,27 @@ const NewPeriod = () => { hasSentInterviewTimes: false, }); + const { + data: owCommitteeData, + isError: owCommitteeIsError, + isLoading: owCommitteeIsLoading, + } = useQuery({ + queryKey: ["ow-committees"], + queryFn: fetchOwCommittees, + }); + + useEffect(() => { + setAvailableCommittees( + owCommitteeData.map( + ({ name_short, email }: { name_short: string; email: string }) => ({ + name: name_short, + value: name_short, + description: email, + }) + ) + ); + }, [owCommitteeData]); + const updateApplicationPeriodDates = ({ start, end, @@ -66,33 +90,6 @@ const NewPeriod = () => { const [availableCommittees, setAvailableCommittees] = useState< { name: string; value: string; description: string }[] >([]); - const [isLoadingCommittees, setIsLoadingCommittees] = useState(true); - - useEffect(() => { - const fetchCommittees = async () => { - setIsLoadingCommittees(true); - try { - const response = await fetch("/api/periods/ow-committees"); - if (!response.ok) throw new Error("Failed to fetch committees"); - const committees = await response.json(); - setAvailableCommittees( - committees.map( - ({ name_short, email }: { name_short: string; email: string }) => ({ - name: name_short, - value: name_short, - description: email, - }) - ) - ); - } catch (error) { - console.error(error); - toast.error("Failed to load committees"); - } finally { - setIsLoadingCommittees(false); - } - }; - fetchCommittees(); - }, []); const handleAddPeriod = async () => { if (!validatePeriod(periodData)) { @@ -121,6 +118,8 @@ const NewPeriod = () => { setShowPreview((prev) => !prev); }; + if (owCommitteeIsError) return ; + return ( <>
@@ -163,7 +162,7 @@ const NewPeriod = () => { updateDates={updateInterviewPeriodDates} /> - {isLoadingCommittees ? ( + {owCommitteeIsLoading ? (
Laster komiteer...
) : (
diff --git a/pages/committees.tsx b/pages/committees.tsx index 0ab4a6b4..c015eebe 100644 --- a/pages/committees.tsx +++ b/pages/committees.tsx @@ -2,19 +2,36 @@ import { useEffect, useState } from "react"; import LoadingPage from "../components/LoadingPage"; import { owCommitteeType, periodType } from "../lib/types/types"; import CommitteeAboutCard from "../components/CommitteeAboutCard"; +import { useQuery } from "@tanstack/react-query"; +import { fetchOwCommittees } from "../lib/api/committees"; +import ErrorPage from "../components/ErrorPage"; + +const excludedCommittees = ["Faddere"]; const Committees = () => { const [isLoading, setIsLoading] = useState(true); const [committees, setCommittees] = useState([]); const [periods, setPeriods] = useState([]); - const excludedCommittees = ["Faddere"]; + const { + data: owCommitteeData, + isError: owCommitteeIsError, + isLoading: owCommitteeIsLoading, + } = useQuery({ + queryKey: ["ow-committees"], + queryFn: fetchOwCommittees, + }); + + useEffect(() => { + if (!owCommitteeData) return; - const filterCommittees = (committees: owCommitteeType[]) => { - return committees.filter( - (committee) => !excludedCommittees.includes(committee.name_short) + const filteredCommittees = owCommitteeData.filter( + (committee: owCommitteeType) => + !excludedCommittees.includes(committee.name_short) ); - }; + + setCommittees(filteredCommittees); + }, [owCommitteeData]); const fetchPeriods = async () => { try { @@ -23,48 +40,13 @@ const Committees = () => { setPeriods(data.periods); } catch (error) { console.error("Failed to fetch periods:", error); - } - }; - - const fetchCommittees = async () => { - try { - const response = await fetch("/api/periods/ow-committees"); - const data = await response.json(); - - const filteredData = filterCommittees(data); - - const cachedData = JSON.parse( - localStorage.getItem("committeesCache") || "[]" - ); - - if (JSON.stringify(filteredData) !== JSON.stringify(cachedData)) { - localStorage.setItem("committeesCache", JSON.stringify(filteredData)); - setCommittees(filteredData); - } else { - setCommittees(cachedData); - } - console.log(filteredData); - } catch (error) { - console.error("Failed to fetch committees:", error); - const cachedData = JSON.parse( - localStorage.getItem("committeesCache") || "[]" - ); - setCommittees(cachedData); } finally { setIsLoading(false); } }; useEffect(() => { - const cachedData = JSON.parse( - localStorage.getItem("committeesCache") || "[]" - ); - if (cachedData.length > 0) { - setCommittees(cachedData); - setIsLoading(false); - } fetchPeriods(); - fetchCommittees(); }, []); const hasPeriod = (committee: any) => { @@ -95,7 +77,8 @@ const Committees = () => { }); }; - if (isLoading) return ; + if (owCommitteeIsLoading || isLoading) return ; + if (owCommitteeIsError) return ; return (
From 97066e9f71196cebce0a27d32dd2916e6cbe96d4 Mon Sep 17 00:00:00 2001 From: fredrir Date: Thu, 25 Jul 2024 19:54:58 +0200 Subject: [PATCH 39/66] if check to prevent owCommitteeData being null --- pages/admin/new-period.tsx | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/pages/admin/new-period.tsx b/pages/admin/new-period.tsx index 438da9a5..709780da 100644 --- a/pages/admin/new-period.tsx +++ b/pages/admin/new-period.tsx @@ -44,15 +44,17 @@ const NewPeriod = () => { }); useEffect(() => { - setAvailableCommittees( - owCommitteeData.map( - ({ name_short, email }: { name_short: string; email: string }) => ({ - name: name_short, - value: name_short, - description: email, - }) - ) - ); + if (owCommitteeData) { + setAvailableCommittees( + owCommitteeData.map( + ({ name_short, email }: { name_short: string; email: string }) => ({ + name: name_short, + value: name_short, + description: email, + }) + ) + ); + } }, [owCommitteeData]); const updateApplicationPeriodDates = ({ From cb2401642d799a34bebc0dd78da4668365978c1a Mon Sep 17 00:00:00 2001 From: fredrir Date: Thu, 25 Jul 2024 20:08:05 +0200 Subject: [PATCH 40/66] Ensures applicantCard is populated before showing it --- pages/application/[period-id].tsx | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/pages/application/[period-id].tsx b/pages/application/[period-id].tsx index 3e60d47e..86c64a58 100644 --- a/pages/application/[period-id].tsx +++ b/pages/application/[period-id].tsx @@ -55,13 +55,21 @@ const Application: NextPage = () => { const [period, setPeriod] = useState(); const [isApplicationPeriodOver, setIsApplicationPeriodOver] = useState(false); - const { data: periodData, isError: periodIsError, isLoading: periodIsLoading } = useQuery({ - queryKey: ['periods', periodId], + const { + data: periodData, + isError: periodIsError, + isLoading: periodIsLoading, + } = useQuery({ + queryKey: ["periods", periodId], queryFn: fetchPeriodById, }); - const { data: applicantData, isError: applicantIsError, isLoading: applicantIsLoading } = useQuery({ - queryKey: ['applicants', periodId, applicantId], + const { + data: applicantData, + isError: applicantIsError, + isLoading: applicantIsLoading, + } = useQuery({ + queryKey: ["applicants", periodId, applicantId], queryFn: fetchApplicantByPeriodAndId, }); @@ -72,7 +80,9 @@ const Application: NextPage = () => { setPeriodExists(periodData.exists); const currentDate = new Date().toISOString(); - if (new Date(periodData.period.applicationPeriod.end) < new Date(currentDate)) { + if ( + new Date(periodData.period.applicationPeriod.end) < new Date(currentDate) + ) { setIsApplicationPeriodOver(true); } }, [periodData]); @@ -180,7 +190,7 @@ const Application: NextPage = () => { onClick={handleDeleteApplication} /> )} - {applicantData && ( + {applicantData.application && (
{ return (
- + Date: Thu, 25 Jul 2024 20:10:22 +0200 Subject: [PATCH 41/66] remove nesting --- pages/admin/new-period.tsx | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/pages/admin/new-period.tsx b/pages/admin/new-period.tsx index 709780da..23d0335c 100644 --- a/pages/admin/new-period.tsx +++ b/pages/admin/new-period.tsx @@ -44,17 +44,16 @@ const NewPeriod = () => { }); useEffect(() => { - if (owCommitteeData) { - setAvailableCommittees( - owCommitteeData.map( - ({ name_short, email }: { name_short: string; email: string }) => ({ - name: name_short, - value: name_short, - description: email, - }) - ) - ); - } + if (!owCommitteeData) return; + setAvailableCommittees( + owCommitteeData.map( + ({ name_short, email }: { name_short: string; email: string }) => ({ + name: name_short, + value: name_short, + description: email, + }) + ) + ); }, [owCommitteeData]); const updateApplicationPeriodDates = ({ From 7c41b537e091264b2a20526c7e84fd6189bc994e Mon Sep 17 00:00:00 2001 From: fredrir Date: Thu, 25 Jul 2024 20:16:19 +0200 Subject: [PATCH 42/66] more detailed check of intervals --- lib/utils/validators.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/utils/validators.ts b/lib/utils/validators.ts index 1a63646a..f49faf7c 100644 --- a/lib/utils/validators.ts +++ b/lib/utils/validators.ts @@ -94,7 +94,9 @@ export const validateCommittee = (data: any, period: periodType): boolean => { return ( startTime >= new Date(period.interviewPeriod.start) && - endTime <= new Date(period.interviewPeriod.end) + startTime <= new Date(period.interviewPeriod.end) && + endTime <= new Date(period.interviewPeriod.end) && + endTime >= new Date(period.interviewPeriod.start) ); } ); From 55ce31a1bee855fc447d4ca902ded5e14ae80f36 Mon Sep 17 00:00:00 2001 From: Julian Ammouche Ottosen <42567826+julian-ao@users.noreply.github.com> Date: Thu, 25 Jul 2024 20:21:08 +0200 Subject: [PATCH 43/66] use query forfetching periods (#200) --- lib/api/periodApi.ts | 12 ++-- pages/apply.tsx | 48 +++++++-------- pages/committee/index.tsx | 123 +++++++++++++++++++------------------- pages/committees.tsx | 39 ++++++------ 4 files changed, 109 insertions(+), 113 deletions(-) diff --git a/lib/api/periodApi.ts b/lib/api/periodApi.ts index 18a347a1..dc8becac 100644 --- a/lib/api/periodApi.ts +++ b/lib/api/periodApi.ts @@ -1,8 +1,10 @@ -import { QueryFunctionContext } from '@tanstack/react-query'; +import { QueryFunctionContext } from "@tanstack/react-query"; export const fetchPeriodById = async (context: QueryFunctionContext) => { const id = context.queryKey[1]; - return fetch(`/api/periods/${id}`).then(res => - res.json() - ); -} + return fetch(`/api/periods/${id}`).then((res) => res.json()); +}; + +export const fetchPeriods = async () => { + return fetch(`/api/periods`).then((res) => res.json()); +}; diff --git a/pages/apply.tsx b/pages/apply.tsx index 4fce96e6..74c1931c 100644 --- a/pages/apply.tsx +++ b/pages/apply.tsx @@ -1,40 +1,40 @@ -import { useSession } from "next-auth/react"; import { useEffect, useState } from "react"; import { periodType } from "../lib/types/types"; import PeriodCard from "../components/PeriodCard"; import LoadingPage from "../components/LoadingPage"; +import { fetchPeriods } from "../lib/api/periodApi"; +import { useQuery } from "@tanstack/react-query"; +import ErrorPage from "../components/ErrorPage"; const Apply = () => { - const { data: session } = useSession(); const [currentPeriods, setCurrentPeriods] = useState([]); - const [isLoading, setIsLoading] = useState(false); + + const { + data: periodsData, + isError: periodsIsError, + isLoading: periodsIsLoading, + } = useQuery({ + queryKey: ["periods"], + queryFn: fetchPeriods, + }); useEffect(() => { - const fetchPeriods = async () => { - try { - setIsLoading(true); - const res = await fetch("/api/periods"); - const data = await res.json(); - const today = new Date(); + if (!periodsData) return; - setCurrentPeriods( - data.periods.filter((period: periodType) => { - const startDate = new Date(period.applicationPeriod.start || ""); - const endDate = new Date(period.applicationPeriod.end || ""); + const today = new Date(); - return startDate <= today && endDate >= today; - }) - ); - setIsLoading(false); - } catch (error) { - console.error("Failed to fetch application periods:", error); - } - }; + setCurrentPeriods( + periodsData.periods.filter((period: periodType) => { + const startDate = new Date(period.applicationPeriod.start || ""); + const endDate = new Date(period.applicationPeriod.end || ""); - session && fetchPeriods(); - }, [session]); + return startDate <= today && endDate >= today; + }) + ); + }, [periodsData]); - if (isLoading) return ; + if (periodsIsLoading) return ; + if (periodsIsError) return ; return (
diff --git a/pages/committee/index.tsx b/pages/committee/index.tsx index 34a4aa30..efd4fe7a 100644 --- a/pages/committee/index.tsx +++ b/pages/committee/index.tsx @@ -5,75 +5,76 @@ import Table from "../../components/Table"; import { formatDate } from "../../lib/utils/dateUtils"; import { periodType } from "../../lib/types/types"; import LoadingPage from "../../components/LoadingPage"; +import { useQuery } from "@tanstack/react-query"; +import { fetchPeriods } from "../../lib/api/periodApi"; +import ErrorPage from "../../components/ErrorPage"; const Committee: NextPage = () => { const { data: session } = useSession(); const [periods, setPeriods] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const fetchPeriods = async () => { - try { - const response = await fetch("/api/periods"); - const data = await response.json(); - const userCommittees = session?.user?.committees || []; + const { + data: periodsData, + isError: periodsIsError, + isLoading: periodsIsLoading, + } = useQuery({ + queryKey: ["periods"], + queryFn: fetchPeriods, + }); - // Viser bare aktuelle perioder - const filteredPeriods = data.periods.filter((period: periodType) => - period.committees.some((committee: string) => - userCommittees.includes(committee.toLowerCase()) - ) - ); + useEffect(() => { + if (!periodsData) return; - setPeriods( - filteredPeriods.map((period: periodType) => { - const userCommittees = session?.user?.committees?.map((committee) => - committee.toLowerCase() - ); - const periodCommittees = period.committees.map((committee) => - committee.toLowerCase() - ); + const userCommittees = session?.user?.committees || []; - period.optionalCommittees.forEach((committee) => { - periodCommittees.push(committee.toLowerCase()); - }); + // Viser bare aktuelle perioder + const filteredPeriods = periodsData.periods.filter((period: periodType) => + period.committees.some((committee: string) => + userCommittees.includes(committee.toLowerCase()) + ) + ); - const commonCommittees = userCommittees!.filter((committee) => - periodCommittees.includes(committee) - ); + setPeriods( + filteredPeriods.map((period: periodType) => { + const userCommittees = session?.user?.committees?.map((committee) => + committee.toLowerCase() + ); + const periodCommittees = period.committees.map((committee) => + committee.toLowerCase() + ); - let uriLink = ""; + period.optionalCommittees.forEach((committee) => { + periodCommittees.push(committee.toLowerCase()); + }); - if (commonCommittees.length > 1) { - uriLink = `committee/${period._id}`; - } else { - uriLink = `committee/${period._id}/${commonCommittees[0]}`; - } + const commonCommittees = userCommittees!.filter((committee) => + periodCommittees.includes(committee) + ); - return { - name: period.name, - application: - formatDate(period.applicationPeriod.start) + - " til " + - formatDate(period.applicationPeriod.end), - interview: - formatDate(period.interviewPeriod.start) + - " til " + - formatDate(period.interviewPeriod.end), - committees: period.committees, - link: uriLink, - }; - }) - ); - } catch (error) { - console.error("Failed to fetch application periods:", error); - } finally { - setIsLoading(false); - } - }; + let uriLink = ""; - useEffect(() => { - fetchPeriods(); - }, []); + if (commonCommittees.length > 1) { + uriLink = `committee/${period._id}`; + } else { + uriLink = `committee/${period._id}/${commonCommittees[0]}`; + } + + return { + name: period.name, + application: + formatDate(period.applicationPeriod.start) + + " til " + + formatDate(period.applicationPeriod.end), + interview: + formatDate(period.interviewPeriod.start) + + " til " + + formatDate(period.interviewPeriod.end), + committees: period.committees, + link: uriLink, + }; + }) + ); + }, [periodsData, session]); const periodsColumns = [ { label: "Navn", field: "name" }, @@ -81,13 +82,9 @@ const Committee: NextPage = () => { { label: "Intervju", field: "interview" }, ]; - if (!session || !session.user?.isCommittee) { - return

Ingen tilgang!

; - } - - if (isLoading) { - return ; - } + if (!session || !session.user?.isCommittee) return

Ingen tilgang!

; + if (periodsIsLoading) return ; + if (periodsIsError) return ; return (
diff --git a/pages/committees.tsx b/pages/committees.tsx index c015eebe..e472303d 100644 --- a/pages/committees.tsx +++ b/pages/committees.tsx @@ -5,11 +5,11 @@ import CommitteeAboutCard from "../components/CommitteeAboutCard"; import { useQuery } from "@tanstack/react-query"; import { fetchOwCommittees } from "../lib/api/committees"; import ErrorPage from "../components/ErrorPage"; +import { fetchPeriods } from "../lib/api/periodApi"; const excludedCommittees = ["Faddere"]; const Committees = () => { - const [isLoading, setIsLoading] = useState(true); const [committees, setCommittees] = useState([]); const [periods, setPeriods] = useState([]); @@ -22,6 +22,15 @@ const Committees = () => { queryFn: fetchOwCommittees, }); + const { + data: periodsData, + isError: periodsIsError, + isLoading: periodsIsLoading, + } = useQuery({ + queryKey: ["periods"], + queryFn: fetchPeriods, + }); + useEffect(() => { if (!owCommitteeData) return; @@ -33,26 +42,14 @@ const Committees = () => { setCommittees(filteredCommittees); }, [owCommitteeData]); - const fetchPeriods = async () => { - try { - const response = await fetch("/api/periods"); - const data = await response.json(); - setPeriods(data.periods); - } catch (error) { - console.error("Failed to fetch periods:", error); - } finally { - setIsLoading(false); - } - }; - useEffect(() => { - fetchPeriods(); - }, []); + if (!periodsData) return; - const hasPeriod = (committee: any) => { - if (!Array.isArray(periods)) { - return false; - } + setPeriods(periodsData.periods); + }, [periodsData]); + + const hasPeriod = (committee: owCommitteeType) => { + if (!Array.isArray(periods)) return false; const today = new Date(); @@ -77,8 +74,8 @@ const Committees = () => { }); }; - if (owCommitteeIsLoading || isLoading) return ; - if (owCommitteeIsError) return ; + if (owCommitteeIsLoading || periodsIsLoading) return ; + if (owCommitteeIsError || periodsIsError) return ; return (
From 377a5a0f7ec0a07712eeb96e83e91ca639410394 Mon Sep 17 00:00:00 2001 From: Fredrik Hansteen Date: Thu, 25 Jul 2024 20:38:10 +0200 Subject: [PATCH 44/66] Update pages/application/[period-id].tsx Co-authored-by: Julian Ammouche Ottosen <42567826+julian-ao@users.noreply.github.com> --- pages/application/[period-id].tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/application/[period-id].tsx b/pages/application/[period-id].tsx index 86c64a58..042dc76c 100644 --- a/pages/application/[period-id].tsx +++ b/pages/application/[period-id].tsx @@ -190,7 +190,7 @@ const Application: NextPage = () => { onClick={handleDeleteApplication} /> )} - {applicantData.application && ( + {applicantData?.application && (
Date: Thu, 25 Jul 2024 22:00:17 +0200 Subject: [PATCH 45/66] fix bug (#212) --- pages/application/[period-id].tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pages/application/[period-id].tsx b/pages/application/[period-id].tsx index 042dc76c..b08b2bae 100644 --- a/pages/application/[period-id].tsx +++ b/pages/application/[period-id].tsx @@ -68,6 +68,7 @@ const Application: NextPage = () => { data: applicantData, isError: applicantIsError, isLoading: applicantIsLoading, + refetch: refetchApplicant, } = useQuery({ queryKey: ["applicants", periodId, applicantId], queryFn: fetchApplicantByPeriodAndId, @@ -109,6 +110,7 @@ const Application: NextPage = () => { if (response.ok) { toast.success("Søknad sendt inn"); setHasAlreadySubmitted(true); + refetchApplicant(); } else { if ( responseData.error === From 53e5e40f49ee86472b619209ca7d088176d08586 Mon Sep 17 00:00:00 2001 From: Julian Ammouche Ottosen <42567826+julian-ao@users.noreply.github.com> Date: Fri, 26 Jul 2024 14:46:03 +0200 Subject: [PATCH 46/66] useQuery in PeriodCard (#202) --- components/PeriodCard.tsx | 31 +++++++++++-------------------- 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/components/PeriodCard.tsx b/components/PeriodCard.tsx index 909470d9..82eab5a9 100644 --- a/components/PeriodCard.tsx +++ b/components/PeriodCard.tsx @@ -1,10 +1,11 @@ import React, { useEffect, useState } from "react"; -import { useRouter } from "next/router"; import { useSession } from "next-auth/react"; import { periodType } from "../lib/types/types"; import { formatDateNorwegian } from "../lib/utils/dateUtils"; import Button from "./Button"; import CheckIcon from "./icons/icons/CheckIcon"; +import { useQuery } from "@tanstack/react-query"; +import { fetchApplicantByPeriodAndId } from "../lib/api/applicantApi"; interface Props { period: periodType; @@ -12,30 +13,20 @@ interface Props { const PeriodCard = ({ period }: Props) => { const { data: session } = useSession(); - const router = useRouter(); const [hasApplied, setHasApplied] = useState(false); - const [isLoading, setIsLoading] = useState(true); - useEffect(() => { - const checkApplicationStatus = async () => { - if (session?.user?.owId) { - const response = await fetch( - `/api/applicants/${period._id}/${session.user.owId}` - ); - if (response.ok) { - const data = await response.json(); - setHasApplied(data.exists); - setIsLoading(false); - } - } - }; + const { data: applicantData, isLoading: applicantIsLoading } = useQuery({ + queryKey: ["applicants", period._id, session?.user?.owId], + queryFn: fetchApplicantByPeriodAndId, + }); - if (period._id && session?.user?.owId) { - checkApplicationStatus(); + useEffect(() => { + if (applicantData) { + setHasApplied(applicantData.exists); } - }, [period._id, session?.user?.owId]); + }, [applicantData]); - if (isLoading) { + if (applicantIsLoading) { return (
From 034a9a674b38b28e33c41621aeec0a896dd2d7ca Mon Sep 17 00:00:00 2001 From: Julian Ammouche Ottosen <42567826+julian-ao@users.noreply.github.com> Date: Fri, 26 Jul 2024 14:48:06 +0200 Subject: [PATCH 47/66] using tanstack to fetch /applicants (#208) * using tanstack to fetch /applicants * fix edge case if period doesn't exist * fix optionalCommittee filtering --------- Co-authored-by: fredrir --- .../applicantoverview/ApplicantsOverview.tsx | 94 +++++++------------ lib/api/applicantApi.ts | 27 +++++- pages/admin/[period-id]/index.tsx | 12 ++- pages/application/[period-id].tsx | 1 + 4 files changed, 68 insertions(+), 66 deletions(-) diff --git a/components/applicantoverview/ApplicantsOverview.tsx b/components/applicantoverview/ApplicantsOverview.tsx index 1c40ef39..55f5d2dd 100644 --- a/components/applicantoverview/ApplicantsOverview.tsx +++ b/components/applicantoverview/ApplicantsOverview.tsx @@ -8,6 +8,12 @@ import { import { AdjustmentsHorizontalIcon } from "@heroicons/react/24/outline"; import ApplicantTable from "./ApplicantTable"; import ApplicantOverviewSkeleton from "./ApplicantOverviewSkeleton"; +import { useQuery } from "@tanstack/react-query"; +import { + fetchApplicantsByPeriodId, + fetchApplicantsByPeriodIdAndCommittee, +} from "../../lib/api/applicantApi"; +import ErrorPage from "../ErrorPage"; interface Props { period?: periodType | null; @@ -42,54 +48,36 @@ const ApplicantsOverview = ({ const [applicants, setApplicants] = useState([]); const [years, setYears] = useState([]); - const [error, setError] = useState(null); - const [isLoading, setIsLoading] = useState(true); const bankomOptions: string[] = ["yes", "no", "maybe"]; - const apiUrl = includePreferences - ? `/api/applicants/${period?._id}` - : `/api/committees/applicants/${period?._id}/${committee}`; + const { + data: applicantsData, + isError: applicantsIsError, + isLoading: applicantsIsLoading, + } = useQuery({ + queryKey: ["applicants", period?._id, committee], + queryFn: includePreferences + ? fetchApplicantsByPeriodId + : fetchApplicantsByPeriodIdAndCommittee, + }); useEffect(() => { - const fetchApplicants = async () => { - try { - const response = await fetch(apiUrl, { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }); - - if (!response.ok) { - throw new Error("Failed to fetch applicants"); - } + if (!applicantsData) return; - const data = await response.json(); - const dataType = includePreferences - ? data.applications - : data.applicants; + const dataType = includePreferences + ? applicantsData.applications + : applicantsData.applicants; - setApplicants(dataType); + setApplicants(dataType); - const uniqueYears: string[] = Array.from( - new Set( - dataType.map((applicant: applicantType) => - applicant.grade.toString() - ) - ) - ); - setYears(uniqueYears); - } catch (error: any) { - setError(error.message); - } finally { - setIsLoading(false); - } - }; - if (period) { - fetchApplicants(); - } - }, [period]); + const uniqueYears: string[] = Array.from( + new Set( + dataType.map((applicant: applicantType) => applicant.grade.toString()) + ) + ); + setYears(uniqueYears); + }, [applicantsData, includePreferences]); useEffect(() => { let filtered: applicantType[] = applicants; @@ -103,13 +91,12 @@ const ApplicantsOverview = ({ applicant.preferences.second.toLowerCase() === selectedCommittee.toLowerCase() || applicant.preferences.third.toLowerCase() === - selectedCommittee.toLowerCase() - ); - } else { - return applicant.preferences.some( - (preference) => - preference.committee.toLowerCase() === - selectedCommittee.toLowerCase() + selectedCommittee.toLowerCase() || + applicant.optionalCommittees.some( + (optionalCommittee) => + optionalCommittee.toLowerCase() === + selectedCommittee.toLowerCase() + ) ); } }); @@ -165,17 +152,8 @@ const ApplicantsOverview = ({ }; }, [filterMenuRef]); - if (isLoading) { - return ; - } - - if (error) { - return ( -
-

Det skjedde en feil, vennligst prøv igjen

-
- ); - } + if (applicantsIsLoading) return ; + if (applicantsIsError) return ; return (
diff --git a/lib/api/applicantApi.ts b/lib/api/applicantApi.ts index d15f34f8..7f72a51c 100644 --- a/lib/api/applicantApi.ts +++ b/lib/api/applicantApi.ts @@ -1,9 +1,28 @@ -import { QueryFunctionContext } from '@tanstack/react-query'; +import { QueryFunctionContext } from "@tanstack/react-query"; -export const fetchApplicantByPeriodAndId = async (context: QueryFunctionContext) => { +export const fetchApplicantByPeriodAndId = async ( + context: QueryFunctionContext +) => { const periodId = context.queryKey[1]; const applicantId = context.queryKey[2]; - return fetch(`/api/applicants/${periodId}/${applicantId}`).then(res => + return fetch(`/api/applicants/${periodId}/${applicantId}`).then((res) => res.json() ); -} +}; + +export const fetchApplicantsByPeriodId = async ( + context: QueryFunctionContext +) => { + const periodId = context.queryKey[1]; + return fetch(`/api/applicants/${periodId}`).then((res) => res.json()); +}; + +export const fetchApplicantsByPeriodIdAndCommittee = async ( + context: QueryFunctionContext +) => { + const periodId = context.queryKey[1]; + const committee = context.queryKey[2]; + return fetch(`/api/committees/applicants/${periodId}/${committee}`).then( + (res) => res.json() + ); +}; diff --git a/pages/admin/[period-id]/index.tsx b/pages/admin/[period-id]/index.tsx index c06d9923..64569fee 100644 --- a/pages/admin/[period-id]/index.tsx +++ b/pages/admin/[period-id]/index.tsx @@ -4,7 +4,7 @@ import router from "next/router"; import { periodType } from "../../../lib/types/types"; import NotFound from "../../404"; import ApplicantsOverview from "../../../components/applicantoverview/ApplicantsOverview"; -import { useQuery } from '@tanstack/react-query'; +import { useQuery } from "@tanstack/react-query"; import { fetchPeriodById } from "../../../lib/api/periodApi"; import LoadingPage from "../../../components/LoadingPage"; import ErrorPage from "../../../components/ErrorPage"; @@ -17,15 +17,19 @@ const Admin = () => { const [committees, setCommittees] = useState(null); const { data, isError, isLoading } = useQuery({ - queryKey: ['periods', periodId], + queryKey: ["periods", periodId], queryFn: fetchPeriodById, }); useEffect(() => { - setPeriod(data?.period) - setCommittees(data?.period.committees) + setPeriod(data?.period); + setCommittees( + data?.period.committees.concat(data?.period.optionalCommittees) + ); }, [data, session?.user?.owId]); + console.log(committees); + if (session?.user?.role !== "admin") return ; if (isLoading) return ; if (isError) return ; diff --git a/pages/application/[period-id].tsx b/pages/application/[period-id].tsx index b08b2bae..34a68086 100644 --- a/pages/application/[period-id].tsx +++ b/pages/application/[period-id].tsx @@ -76,6 +76,7 @@ const Application: NextPage = () => { useEffect(() => { if (!periodData) return; + if (!periodData.period) return; setPeriod(periodData.period); setPeriodExists(periodData.exists); From 89d25052d8ca9199888085743a8edba6dbbbb89f Mon Sep 17 00:00:00 2001 From: Julian Ammouche Ottosen <42567826+julian-ao@users.noreply.github.com> Date: Fri, 26 Jul 2024 14:49:23 +0200 Subject: [PATCH 48/66] use mutation for creating and deleting periods (#213) --- lib/api/periodApi.ts | 17 +++++++ pages/admin/index.tsx | 96 +++++++++++++++++++++----------------- pages/admin/new-period.tsx | 40 ++++++++-------- 3 files changed, 92 insertions(+), 61 deletions(-) diff --git a/lib/api/periodApi.ts b/lib/api/periodApi.ts index dc8becac..a5662f53 100644 --- a/lib/api/periodApi.ts +++ b/lib/api/periodApi.ts @@ -1,4 +1,5 @@ import { QueryFunctionContext } from "@tanstack/react-query"; +import { periodType } from "../types/types"; export const fetchPeriodById = async (context: QueryFunctionContext) => { const id = context.queryKey[1]; @@ -8,3 +9,19 @@ export const fetchPeriodById = async (context: QueryFunctionContext) => { export const fetchPeriods = async () => { return fetch(`/api/periods`).then((res) => res.json()); }; + +export const deletePeriodById = async (id: string) => { + return fetch(`/api/periods/${id}`, { + method: "DELETE", + }); +}; + +export const createPeriod = async (period: periodType) => { + return fetch(`/api/periods`, { + method: "POST", + body: JSON.stringify(period), + headers: { + "Content-Type": "application/json", + }, + }); +}; diff --git a/pages/admin/index.tsx b/pages/admin/index.tsx index 16f69a00..eb83b00d 100644 --- a/pages/admin/index.tsx +++ b/pages/admin/index.tsx @@ -1,65 +1,77 @@ import { useSession } from "next-auth/react"; import Table, { RowType } from "../../components/Table"; import Button from "../../components/Button"; -import { useRouter } from "next/router"; import { useEffect, useState } from "react"; import { periodType } from "../../lib/types/types"; import { formatDate } from "../../lib/utils/dateUtils"; import NotFound from "../404"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { deletePeriodById, fetchPeriods } from "../../lib/api/periodApi"; +import LoadingPage from "../../components/LoadingPage"; +import ErrorPage from "../../components/ErrorPage"; +import toast from "react-hot-toast"; const Admin = () => { + const queryClient = useQueryClient(); const { data: session } = useSession(); - const router = useRouter(); const [periods, setPeriods] = useState([]); - const fetchPeriods = async () => { - try { - const response = await fetch("/api/periods"); - const data = await response.json(); - setPeriods( - data.periods.map((period: periodType) => { - return { - id: period._id, - name: period.name, - application: - formatDate(period.applicationPeriod.start) + - " til " + - formatDate(period.applicationPeriod.end), - interview: - formatDate(period.interviewPeriod.start) + - " til " + - formatDate(period.interviewPeriod.end), - committees: period.committees, - link: `/admin/${period._id}`, - }; - }) - ); - } catch (error) { - console.error("Failed to fetch application periods:", error); - } - }; + const { + data: periodsData, + isError: periodsIsError, + isLoading: periodsIsLoading, + } = useQuery({ + queryKey: ["periods"], + queryFn: fetchPeriods, + }); + + const deletePeriodByIdMutation = useMutation({ + mutationFn: deletePeriodById, + onSuccess: () => + queryClient.invalidateQueries({ + // TODO: try to update cache instead + queryKey: ["periods"], + }), + }); useEffect(() => { - fetchPeriods(); - }, []); + if (!periodsData) return; + + setPeriods( + periodsData.periods.map((period: periodType) => { + return { + id: period._id, + name: period.name, + application: + formatDate(period.applicationPeriod.start) + + " til " + + formatDate(period.applicationPeriod.end), + interview: + formatDate(period.interviewPeriod.start) + + " til " + + formatDate(period.interviewPeriod.end), + committees: period.committees, + link: `/admin/${period._id}`, + }; + }) + ); + }, [periodsData]); const deletePeriod = async (id: string, name: string) => { const isConfirmed = window.confirm( `Er det sikker på at du ønsker å slette ${name}?` ); if (!isConfirmed) return; - - try { - await fetch(`/api/periods/${id}`, { - method: "DELETE", - }); - setPeriods(periods.filter((period) => period.id !== id)); - } catch (error) { - console.error("Failed to delete period:", error); - } + deletePeriodByIdMutation.mutate(id); }; + useEffect(() => { + if (deletePeriodByIdMutation.isSuccess) toast.success("Periode slettet"); + if (deletePeriodByIdMutation.isError) + toast.error("Noe gikk galt, prøv igjen"); + }, [deletePeriodByIdMutation]); + const periodsColumns = [ { label: "Navn", field: "name" }, { label: "Søknad", field: "application" }, @@ -67,9 +79,9 @@ const Admin = () => { { label: "Delete", field: "delete" }, ]; - if (!session || session.user?.role !== "admin") { - return ; - } + if (!session || session.user?.role !== "admin") return ; + if (periodsIsLoading) return ; + if (periodsIsError) return ; return (
diff --git a/pages/admin/new-period.tsx b/pages/admin/new-period.tsx index 23d0335c..57642716 100644 --- a/pages/admin/new-period.tsx +++ b/pages/admin/new-period.tsx @@ -10,11 +10,13 @@ import TextAreaInput from "../../components/form/TextAreaInput"; import TextInput from "../../components/form/TextInput"; import { DeepPartial, periodType } from "../../lib/types/types"; import { validatePeriod } from "../../lib/utils/PeriodValidator"; -import { useQuery } from "@tanstack/react-query"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { fetchOwCommittees } from "../../lib/api/committees"; import ErrorPage from "../../components/ErrorPage"; +import { createPeriod } from "../../lib/api/periodApi"; const NewPeriod = () => { + const queryClient = useQueryClient(); const router = useRouter(); const [showPreview, setShowPreview] = useState(false); @@ -43,6 +45,15 @@ const NewPeriod = () => { queryFn: fetchOwCommittees, }); + const createPeriodMutation = useMutation({ + mutationFn: createPeriod, + onSuccess: () => + queryClient.invalidateQueries({ + // TODO: try to update cache instead + queryKey: ["periods"], + }), + }); + useEffect(() => { if (!owCommitteeData) return; setAvailableCommittees( @@ -92,27 +103,18 @@ const NewPeriod = () => { { name: string; value: string; description: string }[] >([]); - const handleAddPeriod = async () => { - if (!validatePeriod(periodData)) { - return; - } - try { - const response = await fetch("/api/periods", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(periodData), - }); - if (!response.ok) { - throw new Error(`Error creating applicant: ${response.statusText}`); - } - + useEffect(() => { + if (createPeriodMutation.isSuccess) { toast.success("Periode opprettet"); router.push("/admin"); - } catch (error) { - toast.error("Det skjedde en feil, vennligst prøv igjen"); } + if (createPeriodMutation.isError) toast.error("Noe gikk galt, prøv igjen"); + }, [createPeriodMutation, router]); + + const handleAddPeriod = async () => { + if (!validatePeriod(periodData)) return; + + createPeriodMutation.mutate(periodData as periodType); }; const handlePreviewPeriod = () => { From 04f353531b3a950d2654d8e73ba8cf16a26f49f1 Mon Sep 17 00:00:00 2001 From: Julian Date: Sat, 27 Jul 2024 21:21:49 +0200 Subject: [PATCH 49/66] test --- pages/_app.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/pages/_app.tsx b/pages/_app.tsx index 65ef4fe1..33f1c254 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -27,6 +27,7 @@ const SessionHandler: React.FC<{ children: React.ReactNode }> = ({ //Tihi useEffect(() => { console.log(Signature); + console.log("jo tester"); }, []); useEffect(() => { From 5034b0723ed22a54344e6767f4101f92a7a4af02 Mon Sep 17 00:00:00 2001 From: Fredrik Hansteen Date: Sun, 28 Jul 2024 13:59:57 +0200 Subject: [PATCH 50/66] 214 add skeleton loader for table component (#216) * reconfig * Add TableSkeletonCard loader * add skeleton to admin --- .../applicantoverview/ApplicantsOverview.tsx | 2 +- .../ApplicantOverviewSkeleton.tsx | 0 components/skeleton/TableSkeleton.tsx | 69 +++++++++++++++++++ pages/admin/index.tsx | 6 +- pages/committee/index.tsx | 6 +- 5 files changed, 78 insertions(+), 5 deletions(-) rename components/{applicantoverview => skeleton}/ApplicantOverviewSkeleton.tsx (100%) create mode 100644 components/skeleton/TableSkeleton.tsx diff --git a/components/applicantoverview/ApplicantsOverview.tsx b/components/applicantoverview/ApplicantsOverview.tsx index 55f5d2dd..790d959f 100644 --- a/components/applicantoverview/ApplicantsOverview.tsx +++ b/components/applicantoverview/ApplicantsOverview.tsx @@ -7,7 +7,7 @@ import { } from "../../lib/types/types"; import { AdjustmentsHorizontalIcon } from "@heroicons/react/24/outline"; import ApplicantTable from "./ApplicantTable"; -import ApplicantOverviewSkeleton from "./ApplicantOverviewSkeleton"; +import ApplicantOverviewSkeleton from "../skeleton/ApplicantOverviewSkeleton"; import { useQuery } from "@tanstack/react-query"; import { fetchApplicantsByPeriodId, diff --git a/components/applicantoverview/ApplicantOverviewSkeleton.tsx b/components/skeleton/ApplicantOverviewSkeleton.tsx similarity index 100% rename from components/applicantoverview/ApplicantOverviewSkeleton.tsx rename to components/skeleton/ApplicantOverviewSkeleton.tsx diff --git a/components/skeleton/TableSkeleton.tsx b/components/skeleton/TableSkeleton.tsx new file mode 100644 index 00000000..3e306c43 --- /dev/null +++ b/components/skeleton/TableSkeleton.tsx @@ -0,0 +1,69 @@ +import React from "react"; + +type ColumnType = { + label: string; + field: string; +}; +interface Props { + columns: ColumnType[]; +} + +export const TableSkeleton = ({ columns }: Props) => { + return ( + <> +
+ + + + {columns.map((column) => ( + + {column.label !== "Delete" ? ( + + ) : ( + + )} + + ))} + + + + {Array(3) + .fill(null) + .map((_, index) => ( + + {columns.map((column) => ( + + ))} + + ))} + +
+ {column.label} +
+
+
+
+ +
+
+
+
+
+
+
+
+ + ); +}; diff --git a/pages/admin/index.tsx b/pages/admin/index.tsx index eb83b00d..684dc56b 100644 --- a/pages/admin/index.tsx +++ b/pages/admin/index.tsx @@ -10,6 +10,7 @@ import { deletePeriodById, fetchPeriods } from "../../lib/api/periodApi"; import LoadingPage from "../../components/LoadingPage"; import ErrorPage from "../../components/ErrorPage"; import toast from "react-hot-toast"; +import { TableSkeleton } from "../../components/skeleton/TableSkeleton"; const Admin = () => { const queryClient = useQueryClient(); @@ -80,7 +81,6 @@ const Admin = () => { ]; if (!session || session.user?.role !== "admin") return ; - if (periodsIsLoading) return ; if (periodsIsError) return ; return ( @@ -97,7 +97,9 @@ const Admin = () => { />
- {periods.length > 0 && ( + {!periodsIsLoading ? ( + + ) : ( { const { data: session } = useSession(); @@ -83,14 +84,15 @@ const Committee: NextPage = () => { ]; if (!session || !session.user?.isCommittee) return

Ingen tilgang!

; - if (periodsIsLoading) return ; if (periodsIsError) return ; return (

Velg opptak

- {periods.length > 0 && ( + {periodsIsLoading ? ( + + ) : (
)} From a221b8e50b50078cc8b7fc9138ac72a2335fbeee Mon Sep 17 00:00:00 2001 From: Fredrik Hansteen Date: Sun, 28 Jul 2024 14:00:08 +0200 Subject: [PATCH 51/66] Shows one Period skeleton card when period is loading (#215) * add PeriodSkeletonPage * remove unused import --------- Co-authored-by: Julian Ammouche Ottosen <42567826+julian-ao@users.noreply.github.com> --- components/PeriodCard.tsx | 12 ++---------- components/PeriodSkeleton.tsx | 27 +++++++++++++++++++++++++++ pages/apply.tsx | 4 ++-- 3 files changed, 31 insertions(+), 12 deletions(-) create mode 100644 components/PeriodSkeleton.tsx diff --git a/components/PeriodCard.tsx b/components/PeriodCard.tsx index 82eab5a9..95ae5999 100644 --- a/components/PeriodCard.tsx +++ b/components/PeriodCard.tsx @@ -6,6 +6,7 @@ import Button from "./Button"; import CheckIcon from "./icons/icons/CheckIcon"; import { useQuery } from "@tanstack/react-query"; import { fetchApplicantByPeriodAndId } from "../lib/api/applicantApi"; +import { PeriodSkeleton } from "./PeriodSkeleton"; interface Props { period: periodType; @@ -26,16 +27,7 @@ const PeriodCard = ({ period }: Props) => { } }, [applicantData]); - if (applicantIsLoading) { - return ( -
-
-
-
-
-
- ); - } + if (applicantIsLoading) return ; return (
diff --git a/components/PeriodSkeleton.tsx b/components/PeriodSkeleton.tsx new file mode 100644 index 00000000..ef0249aa --- /dev/null +++ b/components/PeriodSkeleton.tsx @@ -0,0 +1,27 @@ +export const PeriodSkeleton = () => { + return ( +
+
+
+
+
+
+ ); +}; + +export const PeriodSkeletonPage = () => { + return ( +
+
+
+

+ Nåværende opptaksperioder +

+
+ +
+
+
+
+ ); +}; diff --git a/pages/apply.tsx b/pages/apply.tsx index 74c1931c..784e71c7 100644 --- a/pages/apply.tsx +++ b/pages/apply.tsx @@ -1,10 +1,10 @@ import { useEffect, useState } from "react"; import { periodType } from "../lib/types/types"; import PeriodCard from "../components/PeriodCard"; -import LoadingPage from "../components/LoadingPage"; import { fetchPeriods } from "../lib/api/periodApi"; import { useQuery } from "@tanstack/react-query"; import ErrorPage from "../components/ErrorPage"; +import { PeriodSkeletonPage } from "../components/PeriodSkeleton"; const Apply = () => { const [currentPeriods, setCurrentPeriods] = useState([]); @@ -33,7 +33,7 @@ const Apply = () => { ); }, [periodsData]); - if (periodsIsLoading) return ; + if (periodsIsLoading) return ; if (periodsIsError) return ; return ( From d0cad3b7921134f0488db88bb6f0a731c4897d58 Mon Sep 17 00:00:00 2001 From: Julian Date: Sun, 28 Jul 2024 16:10:30 +0200 Subject: [PATCH 52/66] fixed period loading bug --- pages/admin/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/admin/index.tsx b/pages/admin/index.tsx index 684dc56b..7d42efd7 100644 --- a/pages/admin/index.tsx +++ b/pages/admin/index.tsx @@ -97,7 +97,7 @@ const Admin = () => { />
- {!periodsIsLoading ? ( + {periodsIsLoading ? ( ) : (
Date: Mon, 29 Jul 2024 00:23:33 +0200 Subject: [PATCH 53/66] will now get a warning if you try to leave when you have unsaved changes --- .../committee/CommitteeInterviewTimes.tsx | 11 +++++ lib/utils/unSavedChangesWarning.ts | 41 +++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 lib/utils/unSavedChangesWarning.ts diff --git a/components/committee/CommitteeInterviewTimes.tsx b/components/committee/CommitteeInterviewTimes.tsx index 8863b785..1307f53b 100644 --- a/components/committee/CommitteeInterviewTimes.tsx +++ b/components/committee/CommitteeInterviewTimes.tsx @@ -9,6 +9,7 @@ import toast from "react-hot-toast"; import NotFound from "../../pages/404"; import Button from "../Button"; import ImportantNote from "../ImportantNote"; +import useUnsavedChangesWarning from "../../lib/utils/unSavedChangesWarning"; interface Interview { title: string; @@ -48,6 +49,9 @@ const CommitteeInterviewTimes = ({ const inputRef = useRef(null); const calendarRef = useRef(null); + const [unsavedChanges, setUnsavedChanges] = useState(false); + useUnsavedChangesWarning(unsavedChanges); + useEffect(() => { if (period) { setVisibleRange({ @@ -115,6 +119,7 @@ const CommitteeInterviewTimes = ({ const handleDateSelect = (selectionInfo: any) => { setCurrentSelection(selectionInfo); setIsModalOpen(true); + setUnsavedChanges(true); }; const handleRoomSubmit = () => { @@ -177,6 +182,7 @@ const CommitteeInterviewTimes = ({ const result = await response.json(); toast.success("Tidene er sendt inn!"); setHasAlreadySubmitted(true); + setUnsavedChanges(false); } catch (error) { toast.error("Kunne ikke sende inn!"); } @@ -189,6 +195,7 @@ const CommitteeInterviewTimes = ({ ) ); event.remove(); + setUnsavedChanges(true); }; const addCell = (cell: string[]) => { @@ -196,10 +203,12 @@ const CommitteeInterviewTimes = ({ ...markedCells, { title: cell[0], start: cell[1], end: cell[2] }, ]); + setUnsavedChanges(true); }; const updateInterviewInterval = (e: BaseSyntheticEvent) => { setInterviewInterval(parseInt(e.target.value)); + setUnsavedChanges(true); }; const renderEventContent = (eventContent: any) => { @@ -252,6 +261,7 @@ const CommitteeInterviewTimes = ({ const handleTimeslotSelection = (e: React.ChangeEvent) => { setSelectedTimeslot(e.target.value); + setUnsavedChanges(true); }; const deleteSubmission = async (e: BaseSyntheticEvent) => { @@ -273,6 +283,7 @@ const CommitteeInterviewTimes = ({ setHasAlreadySubmitted(false); setCalendarEvents([]); + setUnsavedChanges(false); } catch (error: any) { console.error("Error deleting submission:", error); toast.error("Klarte ikke å slette innsendingen"); diff --git a/lib/utils/unSavedChangesWarning.ts b/lib/utils/unSavedChangesWarning.ts new file mode 100644 index 00000000..48198fb3 --- /dev/null +++ b/lib/utils/unSavedChangesWarning.ts @@ -0,0 +1,41 @@ +import { useEffect } from "react"; +import { useRouter } from "next/router"; + +const useUnsavedChangesWarning = (unsavedChanges: any) => { + const router = useRouter(); + + useEffect(() => { + const handleBeforeUnload = (e: any) => { + if (unsavedChanges) { + const message = + "Du har endringer som ikke er lagret, er du sikker på at du vil forlate siden?"; + e.returnValue = message; + return message; + } + }; + + const handleRouteChange = (url: any) => { + if ( + unsavedChanges && + !window.confirm( + "Du har endringer som ikke er lagret, er du sikker på at du vil forlate siden?" + ) + ) { + router.events.emit("routeChangeError"); + throw `Route change to "${url}" was aborted (this error can be safely ignored).`; + } + }; + + window.addEventListener("beforeunload", handleBeforeUnload); + router.events.on("routeChangeStart", handleRouteChange); + + return () => { + window.removeEventListener("beforeunload", handleBeforeUnload); + router.events.off("routeChangeStart", handleRouteChange); + }; + }, [unsavedChanges, router.events]); + + return null; +}; + +export default useUnsavedChangesWarning; From e28a3a255a475714976d632becb86a629b7a5365 Mon Sep 17 00:00:00 2001 From: fredrir Date: Mon, 29 Jul 2024 00:29:27 +0200 Subject: [PATCH 54/66] will display the number of interviews planned --- .../committee/CommitteeInterviewTimes.tsx | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/components/committee/CommitteeInterviewTimes.tsx b/components/committee/CommitteeInterviewTimes.tsx index 8863b785..b745fd5f 100644 --- a/components/committee/CommitteeInterviewTimes.tsx +++ b/components/committee/CommitteeInterviewTimes.tsx @@ -36,6 +36,7 @@ const CommitteeInterviewTimes = ({ const [visibleRange, setVisibleRange] = useState({ start: "", end: "" }); const [selectedTimeslot, setSelectedTimeslot] = useState("15"); + const [interviewsPlanned, setInterviewsPlanned] = useState(0); const [calendarEvents, setCalendarEvents] = useState([]); const [hasAlreadySubmitted, setHasAlreadySubmitted] = @@ -112,6 +113,12 @@ const CommitteeInterviewTimes = ({ } }, [isModalOpen]); + useEffect(() => { + if (calendarEvents.length > 0) { + calculateInterviewsPlanned(); + } + }, [calendarEvents, selectedTimeslot]); + const handleDateSelect = (selectionInfo: any) => { setCurrentSelection(selectionInfo); setIsModalOpen(true); @@ -131,7 +138,7 @@ const CommitteeInterviewTimes = ({ const calendarApi = currentSelection.view.calendar; calendarApi.addEvent(event); - calendarApi.render(); // Force the calendar to re-render + calendarApi.render(); addCell([ roomInput, @@ -141,7 +148,7 @@ const CommitteeInterviewTimes = ({ setRoomInput(""); setIsModalOpen(false); - setCalendarEvents((prevEvents) => [...prevEvents, event]); // Trigger re-render + setCalendarEvents((prevEvents) => [...prevEvents, event]); }; const submit = async (e: BaseSyntheticEvent) => { @@ -321,6 +328,20 @@ const CommitteeInterviewTimes = ({ return ""; }; + const calculateInterviewsPlanned = () => { + const totalMinutes = calendarEvents.reduce((acc, event) => { + const start = new Date(event.start); + const end = new Date(event.end); + const duration = (end.getTime() - start.getTime()) / 1000 / 60; + return acc + duration; + }, 0); + + const plannedInterviews = Math.floor( + totalMinutes / parseInt(selectedTimeslot) + ); + setInterviewsPlanned(plannedInterviews); + }; + if (!session || !session.user?.isCommittee) { return ; } @@ -375,6 +396,7 @@ const CommitteeInterviewTimes = ({ )} +

{`${interviewsPlanned} intervjuer planlagt`}

Date: Mon, 29 Jul 2024 08:43:18 +0200 Subject: [PATCH 55/66] created applicant email template (#220) --- lib/email/applicantEmailTemplate.ts | 68 +++++++++++++++++++++++++++++ lib/{utils => email}/sendEmail.ts | 10 ++--- pages/api/applicants/index.ts | 10 ++--- 3 files changed, 76 insertions(+), 12 deletions(-) create mode 100644 lib/email/applicantEmailTemplate.ts rename lib/{utils => email}/sendEmail.ts (87%) diff --git a/lib/email/applicantEmailTemplate.ts b/lib/email/applicantEmailTemplate.ts new file mode 100644 index 00000000..ea428b19 --- /dev/null +++ b/lib/email/applicantEmailTemplate.ts @@ -0,0 +1,68 @@ +import { emailDataType } from "../types/types"; + +export const generateApplicantEmail = (emailData: emailDataType) => { + return ` + + + + + + + + +
+
+ Online Logo + Vi har mottatt din søknad! +
+
+

Dette er en bekreftelse på at vi har mottatt din søknad. Du vil motta en ny e-post med intervjutider etter søkeperioden er over.

+

Her er en oppsummering av din søknad:

+

E-post: ${emailData.emails[0]}

+

Fullt navn: ${emailData.name}

+

Telefonnummer: ${emailData.phone}

+

Trinn: ${emailData.grade}

+

Førstevalg: ${emailData.firstChoice}

+

Andrevalg: ${emailData.secondChoice}

+

Tredjevalg: ${emailData.thirdChoice}

+

Ønsker du å være økonomiansvarlig: ${emailData.bankom}

+

Komiteer søkt i tillegg: ${emailData.optionalCommittees}

+

Kort om deg selv:

+

${emailData.about}

+
+
+ + + `; +}; diff --git a/lib/utils/sendEmail.ts b/lib/email/sendEmail.ts similarity index 87% rename from lib/utils/sendEmail.ts rename to lib/email/sendEmail.ts index 74be47cd..053fcae2 100644 --- a/lib/utils/sendEmail.ts +++ b/lib/email/sendEmail.ts @@ -1,18 +1,18 @@ import { SESClient, SendEmailCommand } from "@aws-sdk/client-ses"; interface SendEmailProps { - sesClient: SESClient; - fromEmail: string; toEmails: string[]; subject: string; htmlContent: string; } export default async function sendEmail(emailParams: SendEmailProps) { + const sesClient = new SESClient({ region: "eu-north-1" }); + const fromEmail = "opptak@online.ntnu.no"; + try { - const sesClient = emailParams.sesClient; const params = { - Source: emailParams.fromEmail, + Source: fromEmail, Destination: { ToAddresses: emailParams.toEmails, CcAddresses: [], @@ -32,7 +32,7 @@ export default async function sendEmail(emailParams: SendEmailProps) { }, ReplyToAddresses: [], }; - + const command = new SendEmailCommand(params); await sesClient.send(command); diff --git a/pages/api/applicants/index.ts b/pages/api/applicants/index.ts index da40e134..5f971757 100644 --- a/pages/api/applicants/index.ts +++ b/pages/api/applicants/index.ts @@ -6,10 +6,10 @@ import { getServerSession } from "next-auth"; import { emailDataType } from "../../../lib/types/types"; import { isApplicantType } from "../../../lib/utils/validators"; import { isAdmin, hasSession, checkOwId } from "../../../lib/utils/apiChecks"; -import { SESClient, SendEmailCommand } from "@aws-sdk/client-ses"; import capitalizeFirstLetter from "../../../lib/utils/capitalizeFirstLetter"; -import sendEmail from "../../../lib/utils/sendEmail"; +import sendEmail from "../../../lib/email/sendEmail"; import { changeDisplayName } from "../../../lib/utils/toString"; +import { generateApplicantEmail } from "../../../lib/email/applicantEmailTemplate"; const handler = async (req: NextApiRequest, res: NextApiResponse) => { const session = await getServerSession(req, res, authOptions); @@ -55,8 +55,6 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { const { applicant, error } = await createApplicant(requestBody); if (error) throw new Error(error); - const sesClient = new SESClient({ region: "eu-north-1" }); - if (applicant != null) { let optionalCommitteesString = ""; if (applicant.optionalCommittees.length > 0) { @@ -103,11 +101,9 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { try { await sendEmail({ - sesClient: sesClient, - fromEmail: "opptak@online.ntnu.no", toEmails: emailData.emails, subject: "Vi har mottatt din søknad!", - htmlContent: `Dette er en bekreftelse på at vi har mottatt din søknad. Du vil motta en ny e-post med intervjutider etter søkeperioden er over. Her er en oppsummering av din søknad:

E-post: ${emailData.emails[0]}

Fullt navn: ${emailData.name}

Telefonnummer: ${emailData.phone}

Trinn: ${emailData.grade}

Førstevalg: ${emailData.firstChoice}

Andrevalg: ${emailData.secondChoice}

Tredjevalg: ${emailData.thirdChoice}

Ønsker du å være økonomiansvarlig: ${emailData.bankom}

Andre valg: ${emailData.optionalCommittees}

Kort om deg selv:
${emailData.about}`, + htmlContent: generateApplicantEmail(emailData), }); console.log("Email sent to: ", emailData.emails); From 55c3a8929cde609dc0364472d2506579e2a423ba Mon Sep 17 00:00:00 2001 From: Julian Ammouche Ottosen <42567826+julian-ao@users.noreply.github.com> Date: Mon, 29 Jul 2024 08:43:37 +0200 Subject: [PATCH 56/66] removed output from committee page (#219) --- pages/committees.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/committees.tsx b/pages/committees.tsx index e472303d..ce28e9e1 100644 --- a/pages/committees.tsx +++ b/pages/committees.tsx @@ -7,7 +7,7 @@ import { fetchOwCommittees } from "../lib/api/committees"; import ErrorPage from "../components/ErrorPage"; import { fetchPeriods } from "../lib/api/periodApi"; -const excludedCommittees = ["Faddere"]; +const excludedCommittees = ["Faddere", "Output"]; const Committees = () => { const [committees, setCommittees] = useState([]); From fff5dfa6ad32e99e0e1b685b609b79f2e0ec99c2 Mon Sep 17 00:00:00 2001 From: Julian Ammouche Ottosen <42567826+julian-ao@users.noreply.github.com> Date: Mon, 29 Jul 2024 08:46:08 +0200 Subject: [PATCH 57/66] changed link to our own committee page (#218) * changed lint to our own committee page * using next link tags --- pages/apply.tsx | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/pages/apply.tsx b/pages/apply.tsx index 784e71c7..d6b4ed61 100644 --- a/pages/apply.tsx +++ b/pages/apply.tsx @@ -4,6 +4,7 @@ import PeriodCard from "../components/PeriodCard"; import { fetchPeriods } from "../lib/api/periodApi"; import { useQuery } from "@tanstack/react-query"; import ErrorPage from "../components/ErrorPage"; +import Link from "next/link"; import { PeriodSkeletonPage } from "../components/PeriodSkeleton"; const Apply = () => { @@ -44,29 +45,28 @@ const Apply = () => {

Ingen åpne opptak for øyeblikket

Opptak til{" "} - - komiteene - {" "} + + + komiteene + + {" "} skjer vanligvis i august etter fadderuka. Noen komiteer har - vanligvis suppleringsopptak i februar.{

}

Følg - med på{" "} - - online.ntnu.no - {" "} + vanligvis suppleringsopptak i februar. +
+
+ Følg med på{" "} + + + online.ntnu.no + + {" "} eller på vår{" "} - - Facebook - {" "} - side for kunngjøringer! + + + Facebook-gruppe + + {" "} + for kunngjøringer!

) : ( From 698e077f3df4e543d04e36fc45e8ed3324602208 Mon Sep 17 00:00:00 2001 From: Julian Ammouche Ottosen <42567826+julian-ao@users.noreply.github.com> Date: Mon, 29 Jul 2024 08:47:42 +0200 Subject: [PATCH 58/66] use tanstack to create and delete applicant (#217) --- lib/api/applicantApi.ts | 35 +++++++++ pages/application/[period-id].tsx | 124 ++++++++++++------------------ 2 files changed, 84 insertions(+), 75 deletions(-) diff --git a/lib/api/applicantApi.ts b/lib/api/applicantApi.ts index 7f72a51c..ac0ef8c5 100644 --- a/lib/api/applicantApi.ts +++ b/lib/api/applicantApi.ts @@ -1,4 +1,5 @@ import { QueryFunctionContext } from "@tanstack/react-query"; +import { applicantType } from "../types/types"; export const fetchApplicantByPeriodAndId = async ( context: QueryFunctionContext @@ -26,3 +27,37 @@ export const fetchApplicantsByPeriodIdAndCommittee = async ( (res) => res.json() ); }; + +export const createApplicant = async (applicant: applicantType) => { + const response = await fetch(`/api/applicants/`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(applicant), + }); + + const data = await response.json(); + if (!response.ok) { + throw new Error(data.error || "Unknown error occurred"); + } + return data; +}; + +export const deleteApplicant = async ({ + periodId, + owId, +}: { + periodId: string; + owId: string; +}) => { + const response = await fetch(`/api/applicants/${periodId}/${owId}`, { + method: "DELETE", + }); + + if (!response.ok) { + throw new Error("Failed to delete the application"); + } + + return response; +}; diff --git a/pages/application/[period-id].tsx b/pages/application/[period-id].tsx index 34a68086..88064337 100644 --- a/pages/application/[period-id].tsx +++ b/pages/application/[period-id].tsx @@ -16,25 +16,22 @@ import ApplicantCard from "../../components/applicantoverview/ApplicantCard"; import LoadingPage from "../../components/LoadingPage"; import { formatDateNorwegian } from "../../lib/utils/dateUtils"; import PageTitle from "../../components/PageTitle"; -import { useQuery } from "@tanstack/react-query"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { fetchPeriodById } from "../../lib/api/periodApi"; -import { fetchApplicantByPeriodAndId } from "../../lib/api/applicantApi"; +import { + createApplicant, + deleteApplicant, + fetchApplicantByPeriodAndId, +} from "../../lib/api/applicantApi"; import ErrorPage from "../../components/ErrorPage"; -interface FetchedApplicationData { - exists: boolean; - application: applicantType; -} - const Application: NextPage = () => { + const queryClient = useQueryClient(); const { data: session } = useSession(); const router = useRouter(); const periodId = router.query["period-id"] as string; const applicantId = session?.user?.owId; - const [hasAlreadySubmitted, setHasAlreadySubmitted] = useState(true); - const [periodExists, setPeriodExists] = useState(false); - const [activeTab, setActiveTab] = useState(0); const [applicationData, setApplicationData] = useState< DeepPartial @@ -68,18 +65,43 @@ const Application: NextPage = () => { data: applicantData, isError: applicantIsError, isLoading: applicantIsLoading, - refetch: refetchApplicant, } = useQuery({ - queryKey: ["applicants", periodId, applicantId], + queryKey: ["applicant", periodId, applicantId], queryFn: fetchApplicantByPeriodAndId, }); + const createApplicantMutation = useMutation({ + mutationFn: createApplicant, + onSuccess: () => { + queryClient.setQueryData(["applicant", periodId, applicantId], { + applicant: applicationData, + exists: true, + }); + toast.success("Søknad sendt inn"); + }, + onError: (error) => { + if (error.message === "409 Application already exists for this period") { + toast.error("Du har allerede søkt for denne perioden"); + } else { + toast.error("Det skjedde en feil, vennligst prøv igjen"); + } + }, + }); + + const deleteApplicantMutation = useMutation({ + mutationFn: deleteApplicant, + onSuccess: () => { + queryClient.setQueryData(["applicant", periodId, applicantId], null); + toast.success("Søknad trukket tilbake"); + setActiveTab(0); + }, + onError: () => toast.error("Det skjedde en feil, vennligst prøv igjen"), + }); + useEffect(() => { - if (!periodData) return; - if (!periodData.period) return; + if (!periodData || !periodData.period) return; setPeriod(periodData.period); - setPeriodExists(periodData.exists); const currentDate = new Date().toISOString(); if ( @@ -89,46 +111,10 @@ const Application: NextPage = () => { } }, [periodData]); - useEffect(() => { - setHasAlreadySubmitted(applicantData?.exists); - }, [applicantData]); - const handleSubmitApplication = async () => { if (!validateApplication(applicationData)) return; - - try { - applicationData.periodId = periodId as string; - const response = await fetch("/api/applicants", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(applicationData), - }); - - const responseData = await response.json(); - - if (response.ok) { - toast.success("Søknad sendt inn"); - setHasAlreadySubmitted(true); - refetchApplicant(); - } else { - if ( - responseData.error === - "409 Application already exists for this period" - ) { - toast.error("Du har allerede søkt for denne perioden"); - } else { - throw new Error(`Error creating applicant: ${response.statusText}`); - } - } - } catch (error) { - if (error instanceof Error) { - toast.error(error.message); - } else { - toast.error("Det skjedde en feil, vennligst prøv igjen"); - } - } + applicationData.periodId = periodId as string; + createApplicantMutation.mutate(applicationData as applicantType); }; const handleDeleteApplication = async () => { @@ -139,29 +125,19 @@ const Application: NextPage = () => { if (!confirm("Er du sikker på at du vil trekke tilbake søknaden?")) return; - try { - const response = await fetch( - `/api/applicants/${periodId}/${session.user.owId}`, - { - method: "DELETE", - } - ); - - if (!response.ok) { - throw new Error("Failed to delete the application"); - } - - toast.success("Søknad trukket tilbake"); - setHasAlreadySubmitted(false); - } catch (error) { - toast.error("Det skjedde en feil, vennligst prøv igjen"); - } + deleteApplicantMutation.mutate({ periodId, owId: session?.user?.owId }); }; - if (periodIsLoading || applicantIsLoading) return ; + if ( + periodIsLoading || + applicantIsLoading || + createApplicantMutation.isPending || + deleteApplicantMutation.isPending + ) + return ; if (periodIsError || applicantIsError) return ; - if (!periodExists) { + if (!periodData?.exists) return (

@@ -169,9 +145,8 @@ const Application: NextPage = () => {

); - } - if (hasAlreadySubmitted) { + if (applicantData?.exists) return (
@@ -203,7 +178,6 @@ const Application: NextPage = () => { )}
); - } return (
From 89a51ad97f20fa3aee4c059a101373a05507dde2 Mon Sep 17 00:00:00 2001 From: fredrir Date: Mon, 29 Jul 2024 12:52:04 +0200 Subject: [PATCH 59/66] export useState "unsavedChanges" --- components/committee/CommitteeInterviewTimes.tsx | 3 +-- lib/utils/unSavedChangesWarning.ts | 7 ++++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/components/committee/CommitteeInterviewTimes.tsx b/components/committee/CommitteeInterviewTimes.tsx index 1307f53b..3b0d4ccb 100644 --- a/components/committee/CommitteeInterviewTimes.tsx +++ b/components/committee/CommitteeInterviewTimes.tsx @@ -49,8 +49,7 @@ const CommitteeInterviewTimes = ({ const inputRef = useRef(null); const calendarRef = useRef(null); - const [unsavedChanges, setUnsavedChanges] = useState(false); - useUnsavedChangesWarning(unsavedChanges); + const { unsavedChanges, setUnsavedChanges } = useUnsavedChangesWarning(); useEffect(() => { if (period) { diff --git a/lib/utils/unSavedChangesWarning.ts b/lib/utils/unSavedChangesWarning.ts index 48198fb3..4fef5a8e 100644 --- a/lib/utils/unSavedChangesWarning.ts +++ b/lib/utils/unSavedChangesWarning.ts @@ -1,7 +1,8 @@ -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { useRouter } from "next/router"; -const useUnsavedChangesWarning = (unsavedChanges: any) => { +const useUnsavedChangesWarning = () => { + const [unsavedChanges, setUnsavedChanges] = useState(false); const router = useRouter(); useEffect(() => { @@ -35,7 +36,7 @@ const useUnsavedChangesWarning = (unsavedChanges: any) => { }; }, [unsavedChanges, router.events]); - return null; + return { unsavedChanges, setUnsavedChanges }; }; export default useUnsavedChangesWarning; From 140fe118796055f289b00cca4e05134668b3814d Mon Sep 17 00:00:00 2001 From: fredrir Date: Mon, 29 Jul 2024 13:50:59 +0200 Subject: [PATCH 60/66] change deadline to applicationPeriod.end --- components/committee/CommitteeInterviewTimes.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/committee/CommitteeInterviewTimes.tsx b/components/committee/CommitteeInterviewTimes.tsx index 8863b785..f987cd32 100644 --- a/components/committee/CommitteeInterviewTimes.tsx +++ b/components/committee/CommitteeInterviewTimes.tsx @@ -290,7 +290,7 @@ const CommitteeInterviewTimes = ({ }, [period]); const getSubmissionDeadline = (): string => { - const deadlineIso = period!.interviewPeriod.start; + const deadlineIso = period!.applicationPeriod.end; if (deadlineIso != null) { const deadlineDate = new Date(deadlineIso); @@ -325,7 +325,7 @@ const CommitteeInterviewTimes = ({ return ; } - if (period!.interviewPeriod.start < new Date()) { + if (period!.applicationPeriod.end < new Date()) { return (

From c1656d5bab05ef48395c56b14960af505effbfb8 Mon Sep 17 00:00:00 2001 From: fredrir Date: Mon, 29 Jul 2024 13:52:50 +0200 Subject: [PATCH 61/66] add backend validation --- lib/utils/validators.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/utils/validators.ts b/lib/utils/validators.ts index f49faf7c..47b88887 100644 --- a/lib/utils/validators.ts +++ b/lib/utils/validators.ts @@ -79,6 +79,8 @@ export const validateCommittee = (data: any, period: periodType): boolean => { const isPeriodNameValid = data.periodId === String(period._id); + const isBeforeDeadline = new Date() <= new Date(period.applicationPeriod.end); + const committeeExists = period.committees.some((committee) => { return committee.toLowerCase() === data.committee.toLowerCase(); @@ -105,7 +107,8 @@ export const validateCommittee = (data: any, period: periodType): boolean => { hasBasicFields && isPeriodNameValid && committeeExists && - isWithinInterviewPeriod + isWithinInterviewPeriod && + isBeforeDeadline ); }; From ddf9d4d46740f6fe93844aa17b634916fada2c26 Mon Sep 17 00:00:00 2001 From: fredrir Date: Mon, 29 Jul 2024 13:58:08 +0200 Subject: [PATCH 62/66] add bool deadLineHasPassed --- components/committee/CommitteeInterviewTimes.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/components/committee/CommitteeInterviewTimes.tsx b/components/committee/CommitteeInterviewTimes.tsx index f987cd32..5cbb2b1c 100644 --- a/components/committee/CommitteeInterviewTimes.tsx +++ b/components/committee/CommitteeInterviewTimes.tsx @@ -48,6 +48,8 @@ const CommitteeInterviewTimes = ({ const inputRef = useRef(null); const calendarRef = useRef(null); + const [deadLineHasPassed, setDeadLineHasPassed] = useState(false); + useEffect(() => { if (period) { setVisibleRange({ @@ -292,10 +294,14 @@ const CommitteeInterviewTimes = ({ const getSubmissionDeadline = (): string => { const deadlineIso = period!.applicationPeriod.end; - if (deadlineIso != null) { + if (deadlineIso != null && !deadLineHasPassed) { const deadlineDate = new Date(deadlineIso); const now = new Date(); + if (now > deadlineDate) { + setDeadLineHasPassed(true); + } + let delta = Math.floor((deadlineDate.getTime() - now.getTime()) / 1000); const days = Math.floor(delta / 86400); @@ -325,7 +331,7 @@ const CommitteeInterviewTimes = ({ return ; } - if (period!.applicationPeriod.end < new Date()) { + if (deadLineHasPassed) { return (

From 29c00a01099e77273eb37025109a4abf8612b7ea Mon Sep 17 00:00:00 2001 From: fredrir Date: Mon, 29 Jul 2024 14:00:25 +0200 Subject: [PATCH 63/66] add deadLine for add message --- components/committee/SendCommitteeMessage.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/components/committee/SendCommitteeMessage.tsx b/components/committee/SendCommitteeMessage.tsx index 8e1e85b4..85f0d716 100644 --- a/components/committee/SendCommitteeMessage.tsx +++ b/components/committee/SendCommitteeMessage.tsx @@ -15,6 +15,7 @@ interface Props { } const SendCommitteeMessage = ({ + period, committee, committeeInterviewTimes, }: Props) => { @@ -79,6 +80,16 @@ const SendCommitteeMessage = ({ } }; + if (new Date(period!.applicationPeriod.end) < new Date()) { + return ( +
+

+ Det er ikke lenger mulig å legge inn tider! +

+
+ ); + } + return (

From 56e2defdd2aba8cb5eed54536b1d1c74a7160d69 Mon Sep 17 00:00:00 2001 From: fredrir Date: Mon, 29 Jul 2024 14:03:41 +0200 Subject: [PATCH 64/66] add more backend validation --- .../times/[period-id]/[committee].ts | 19 +++++++++++++++++++ .../api/committees/times/[period-id]/index.ts | 4 ++++ 2 files changed, 23 insertions(+) diff --git a/pages/api/committees/times/[period-id]/[committee].ts b/pages/api/committees/times/[period-id]/[committee].ts index 13871657..ff20f15e 100644 --- a/pages/api/committees/times/[period-id]/[committee].ts +++ b/pages/api/committees/times/[period-id]/[committee].ts @@ -7,6 +7,7 @@ import { import { getServerSession } from "next-auth"; import { authOptions } from "../../../auth/[...nextauth]"; import { hasSession, isInCommitee } from "../../../../../lib/utils/apiChecks"; +import { getPeriodById } from "../../../../../lib/mongo/periods"; const handler = async (req: NextApiRequest, res: NextApiResponse) => { const session = await getServerSession(req, res, authOptions); @@ -51,6 +52,15 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { return res.status(400).json({ error: "Invalid message parameter" }); } + const { period } = await getPeriodById(String(periodId)); + if (!period) { + return res.status(400).json({ error: "Invalid periodId" }); + } + + if (new Date() > new Date(period.applicationPeriod.end)) { + return res.status(400).json({ error: "Application period has ended" }); + } + const { updatedMessage, error } = await updateCommitteeMessage( selectedCommittee, periodId, @@ -67,6 +77,15 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { if (req.method === "DELETE") { try { + const { period } = await getPeriodById(String(periodId)); + if (!period) { + return res.status(400).json({ error: "Invalid periodId" }); + } + + if (new Date() > new Date(period.applicationPeriod.end)) { + return res.status(400).json({ error: "Application period has ended" }); + } + const { error } = await deleteCommittee( selectedCommittee, periodId, diff --git a/pages/api/committees/times/[period-id]/index.ts b/pages/api/committees/times/[period-id]/index.ts index c9acf71e..9cd92f1b 100644 --- a/pages/api/committees/times/[period-id]/index.ts +++ b/pages/api/committees/times/[period-id]/index.ts @@ -40,6 +40,10 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { return res.status(400).json({ error: "Invalid periodId" }); } + if (new Date() > new Date(period.applicationPeriod.end)) { + return res.status(400).json({ error: "Application period has ended" }); + } + if (!validateCommittee(committeeData, period)) { return res.status(400).json({ error: "Invalid data format" }); } From d34cea05cf81c227e72b4373acd38505fd25e5ef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jul 2024 15:11:46 +0200 Subject: [PATCH 65/66] Bump postcss from 8.4.39 to 8.4.40 (#233) Bumps [postcss](https://github.com/postcss/postcss) from 8.4.39 to 8.4.40. - [Release notes](https://github.com/postcss/postcss/releases) - [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/postcss/postcss/compare/8.4.39...8.4.40) --- updated-dependencies: - dependency-name: postcss dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 899cebb9..1e84e0a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,7 +36,7 @@ "autoprefixer": "^10.4.12", "eslint": "8.24.0", "eslint-config-next": "12.3.1", - "postcss": "^8.4.16", + "postcss": "^8.4.40", "prettier": "3.0.3", "tailwindcss": "^3.1.8", "typescript": "4.8.3" @@ -5160,9 +5160,9 @@ } }, "node_modules/postcss": { - "version": "8.4.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz", - "integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==", + "version": "8.4.40", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.40.tgz", + "integrity": "sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==", "funding": [ { "type": "opencollective", diff --git a/package.json b/package.json index e69364f2..da813f3e 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "autoprefixer": "^10.4.12", "eslint": "8.24.0", "eslint-config-next": "12.3.1", - "postcss": "^8.4.16", + "postcss": "^8.4.40", "prettier": "3.0.3", "tailwindcss": "^3.1.8", "typescript": "4.8.3" From 4d8a253c0106e23399cb4c2297ca7046c8f99e33 Mon Sep 17 00:00:00 2001 From: Julian Ammouche Ottosen <42567826+julian-ao@users.noreply.github.com> Date: Mon, 29 Jul 2024 16:54:36 +0200 Subject: [PATCH 66/66] removed adding 0 to start of day, and shortened all month prefixes (#221) --- lib/utils/dateUtils.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/utils/dateUtils.ts b/lib/utils/dateUtils.ts index d6837583..9b1569c8 100644 --- a/lib/utils/dateUtils.ts +++ b/lib/utils/dateUtils.ts @@ -13,15 +13,15 @@ export const formatDate = (inputDate: undefined | Date) => { export const formatDateNorwegian = (inputDate?: Date): string => { const date = new Date(inputDate || ""); - const day = date.getDate().toString().padStart(2, "0"); + const day = date.getDate().toString().padStart(2); const monthsNorwegian = [ "jan", "feb", - "mars", - "april", + "mar", + "apr", "mai", - "juni", - "juli", + "jun", + "jul", "aug", "sep", "okt",