From 33f391e1a09088017d898799006b2c8100eebfa6 Mon Sep 17 00:00:00 2001 From: Julian Ammouche Ottosen Date: Tue, 20 Jan 2026 13:20:10 +0100 Subject: [PATCH 1/2] Date cleanup --- .../committee/CommitteeInterviewTimes.tsx | 33 +- components/form/DatePickerInput.tsx | 21 +- lib/mongo/applicants.ts | 33 +- lib/mongo/committees.ts | 2 +- lib/mongo/periods.ts | 10 +- .../formatInterviewEmail.ts | 11 +- lib/sendInterviewTimes/formatInterviewSMS.ts | 6 +- lib/types/types.ts | 26 +- lib/utils/dateUtils.ts | 67 +- lib/utils/validateApplication.ts | 4 +- lib/utils/validators.ts | 52 +- package-lock.json | 673 +++++++++++++++++- package.json | 3 + pages/admin/new-period.tsx | 16 +- pages/api/applicants/[period-id]/[id].ts | 6 +- pages/api/applicants/index.ts | 1 - .../times/[period-id]/[committee].ts | 5 +- .../api/committees/times/[period-id]/index.ts | 5 +- pages/apply/[period-id].tsx | 9 +- pages/apply/index.tsx | 15 +- .../[period-id]/[committee]/index.tsx | 14 +- pages/committees.tsx | 39 +- 22 files changed, 835 insertions(+), 216 deletions(-) diff --git a/components/committee/CommitteeInterviewTimes.tsx b/components/committee/CommitteeInterviewTimes.tsx index 88a98947..8bfd12f2 100644 --- a/components/committee/CommitteeInterviewTimes.tsx +++ b/components/committee/CommitteeInterviewTimes.tsx @@ -4,7 +4,7 @@ import { useSession } from "next-auth/react"; import FullCalendar from "@fullcalendar/react"; import timeGridPlugin from "@fullcalendar/timegrid"; import interactionPlugin from "@fullcalendar/interaction"; -import { periodType, committeeInterviewType } from "../../lib/types/types"; +import { periodType, committeeInterviewType, AvailableTime } from "../../lib/types/types"; import toast from "react-hot-toast"; import NotFound from "../../pages/404"; import Button from "../Button"; @@ -19,8 +19,8 @@ import { XMarkIcon } from "@heroicons/react/24/solid"; interface Interview { id: string; title: string; - start: string; - end: string; + start: Date; + end: Date; } interface Props { @@ -63,8 +63,8 @@ const CommitteeInterviewTimes = ({ useEffect(() => { if (period) { setVisibleRange({ - start: new Date(period!.interviewPeriod.start).toISOString(), - end: new Date(period!.interviewPeriod.end).toISOString(), + start: period.interviewPeriod.start.toISOString(), + end: period.interviewPeriod.end.toISOString(), }); } }, [period]); @@ -98,11 +98,11 @@ const CommitteeInterviewTimes = ({ if (cleanCommittee === cleanSelectedCommittee) { setHasAlreadySubmitted(true); const events = committeeInterviewTimes.availabletimes.map( - (at: any) => ({ + (availableTime: AvailableTime) => ({ id: crypto.getRandomValues(new Uint32Array(1))[0].toString(), - title: at.room, - start: new Date(at.start).toISOString(), - end: new Date(at.end).toISOString(), + title: availableTime.room, + start: availableTime.start.toISOString(), + end: availableTime.end.toISOString(), }) ); @@ -262,17 +262,12 @@ const CommitteeInterviewTimes = ({ ); }; - const formatEventsForExport = (events: Interview[]) => { - return events.map((event) => { - const startDateTime = new Date(event.start); - const endDateTime = new Date(event.end); - return { + const formatEventsForExport = (events: Interview[]) => + events.map((event) => ({ room: event.title, - start: startDateTime.toISOString(), - end: endDateTime.toISOString(), - }; - }); - }; + start: event.start.toISOString(), + end: event.end.toISOString(), + })); const handleTimeslotSelection = (e: React.ChangeEvent) => { setSelectedTimeslot(e.target.value); diff --git a/components/form/DatePickerInput.tsx b/components/form/DatePickerInput.tsx index 6ef4474c..4e8045a3 100644 --- a/components/form/DatePickerInput.tsx +++ b/components/form/DatePickerInput.tsx @@ -1,7 +1,10 @@ import { useEffect, useState } from "react"; +import { fromZonedTime } from 'date-fns-tz'; +import { timezone } from "../../lib/utils/dateUtils"; + interface Props { label?: string; - updateDates: (dates: { start: string; end: string }) => void; + updateDates: (dates: { start: Date; end: Date }) => void; } const DatePickerInput = (props: Props) => { @@ -9,8 +12,18 @@ const DatePickerInput = (props: Props) => { const [toDate, setToDate] = useState(""); useEffect(() => { - const startDate = fromDate ? `${fromDate}T00:00` : ""; - const endDate = toDate ? `${toDate}T23:59` : ""; + if (!fromDate || !toDate) return; + + // Parse as Norwegian time, convert to UTC Date object + const startDate = fromZonedTime( + `${fromDate}T00:00:00`, + timezone + ); + const endDate = fromZonedTime( + `${toDate}T23:59:59`, + timezone + ); + props.updateDates({ start: startDate, end: endDate }); }, [fromDate, toDate]); @@ -42,4 +55,4 @@ const DatePickerInput = (props: Props) => { ); }; -export default DatePickerInput; +export default DatePickerInput; \ No newline at end of file diff --git a/lib/mongo/applicants.ts b/lib/mongo/applicants.ts index 212aff32..b5522b25 100644 --- a/lib/mongo/applicants.ts +++ b/lib/mongo/applicants.ts @@ -1,7 +1,8 @@ import { Collection, Db, MongoClient, ObjectId } from "mongodb"; import clientPromise from "./mongodb"; -import { applicantType, periodType, preferencesType } from "../types/types"; +import { applicantType, Nullable, periodType, preferencesType } from "../types/types"; import { getPeriodById } from "./periods"; +import { addDays, isAfter } from 'date-fns'; let client: MongoClient; let db: Db; @@ -153,7 +154,7 @@ export const getApplicantsForCommittee = async ( // Filtrerer søkerne slik at kun brukere som er i komiteen som har blitt søkt på ser søkeren // Fjerner prioriterings informasjon const filteredApplicants = result - .map((applicant) => { + .map((applicant: Nullable) => { let preferencesArray: string[] = []; if (isPreferencesType(applicant.preferences)) { preferencesArray = [ @@ -182,24 +183,22 @@ export const getApplicantsForCommittee = async ( applicant.optionalCommittees = []; - const today = new Date(); - const sevenDaysAfterInterviewEnd = new Date(period.interviewPeriod.end); - sevenDaysAfterInterviewEnd.setDate( - sevenDaysAfterInterviewEnd.getDate() + 5 - ); + const now = new Date(); + const sevenDaysAfterInterviewEnd = addDays(period.interviewPeriod.end, 7); + // Sletter sensitiv informasjon etter intervju perioden + 7 dager, for å forhindre snoking i tidligere søknader if ( - new Date(period.applicationPeriod.end) > today || - today > sevenDaysAfterInterviewEnd + isAfter(now, period.applicationPeriod.end) || + isAfter(now, sevenDaysAfterInterviewEnd) ) { - applicant.owId = "Skjult"; - applicant.name = "Skjult"; - applicant.date = today; - applicant.phone = "Skjult"; - applicant.email = "Skjult"; - applicant.about = "Skjult"; - applicant.grade = "-"; - applicant.selectedTimes = [{ start: "Skjult", end: "Skjult" }]; + applicant.owId = null; + applicant.name = null; + applicant.phone = null; + applicant.grade = null; + applicant.email = null; + applicant.about = null; + applicant.selectedTimes = null; + applicant.date = null; } const isSelectedCommitteePresent = diff --git a/lib/mongo/committees.ts b/lib/mongo/committees.ts index e978fef6..aa83707b 100644 --- a/lib/mongo/committees.ts +++ b/lib/mongo/committees.ts @@ -1,4 +1,4 @@ -import { Collection, Db, MongoClient, ObjectId, UpdateResult } from "mongodb"; +import { Collection, Db, MongoClient, ObjectId } from "mongodb"; import clientPromise from "./mongodb"; import { committeeInterviewType } from "../types/types"; diff --git a/lib/mongo/periods.ts b/lib/mongo/periods.ts index dccc87d8..04a39612 100644 --- a/lib/mongo/periods.ts +++ b/lib/mongo/periods.ts @@ -59,19 +59,19 @@ export const getCurrentPeriods = async () => { try { if (!periods) await init(); - const currentDate = new Date().toISOString(); + const now = new Date(); const filter = { $or: [ { // Check if current ISO date string is within the application period - "applicationPeriod.start": { $lte: currentDate }, - "applicationPeriod.end": { $gte: currentDate }, + "applicationPeriod.start": { $lte: now }, + "applicationPeriod.end": { $gte: now }, }, { // Check if current ISO date string is within the interview period - "interviewPeriod.start": { $lte: currentDate }, - "interviewPeriod.end": { $gte: currentDate }, + "interviewPeriod.start": { $lte: now }, + "interviewPeriod.end": { $gte: now }, }, ], }; diff --git a/lib/sendInterviewTimes/formatInterviewEmail.ts b/lib/sendInterviewTimes/formatInterviewEmail.ts index b6b91d9f..e000deb9 100644 --- a/lib/sendInterviewTimes/formatInterviewEmail.ts +++ b/lib/sendInterviewTimes/formatInterviewEmail.ts @@ -23,13 +23,12 @@ export const formatApplicantInterviewEmail = ( committee.committeeName )}
`; - if (committee.interviewTime.start !== "Ikke satt") { + if (committee.interviewTime.start != null) { emailBody += `Tid: ${formatDateHours( committee.interviewTime.start, committee.interviewTime.end )}
`; - } - if (committee.interviewTime.start === "Ikke satt") { + } else { emailBody += `Tid: Ikke satt. Komitéen vil ta kontakt med deg for å avtale tidspunkt.
`; } @@ -61,14 +60,12 @@ export const formatCommitteeInterviewEmail = ( emailBody += `
  • Navn: ${applicant.applicantName}
    `; emailBody += `Telefon: ${applicant.applicantPhone}
    `; - if (applicant.interviewTime.start !== "Ikke satt") { + if (applicant.interviewTime.start != null) { emailBody += `Tid: ${formatDateHours( applicant.interviewTime.start, applicant.interviewTime.end )}
    `; - } - - if (applicant.interviewTime.start === "Ikke satt") { + } else { emailBody += `Tid: Ikke satt. Ta kontakt med søker for å avtale tidspunkt.`; } emailBody += `Rom: ${applicant.interviewTime.room}

  • `; diff --git a/lib/sendInterviewTimes/formatInterviewSMS.ts b/lib/sendInterviewTimes/formatInterviewSMS.ts index e9b42822..8c621390 100644 --- a/lib/sendInterviewTimes/formatInterviewSMS.ts +++ b/lib/sendInterviewTimes/formatInterviewSMS.ts @@ -15,14 +15,12 @@ export const formatInterviewSMS = (applicant: emailApplicantInterviewType) => { applicant.committees.forEach((committee) => { phoneBody += `Komité: ${changeDisplayName(committee.committeeName)} \n`; - if (committee.interviewTime.start !== "Ikke satt") { + if (committee.interviewTime.start != null) { phoneBody += `Tid: ${formatDateHours( committee.interviewTime.start, committee.interviewTime.end )}\n`; - } - - if (committee.interviewTime.start === "Ikke satt") { + } else { phoneBody += `Tid: Ikke satt. Komitéen vil ta kontakt for å avtale tidspunkt. \n`; } diff --git a/lib/types/types.ts b/lib/types/types.ts index 58176b46..d94069f7 100644 --- a/lib/types/types.ts +++ b/lib/types/types.ts @@ -4,6 +4,10 @@ export type DeepPartial = { [P in keyof T]?: T[P] extends object ? DeepPartial : T[P]; }; +export type Nullable = { + [K in keyof T]: T[K] | null; +} + export type preferencesType = { first: string; second: string; @@ -27,8 +31,8 @@ export type applicantType = { optionalCommittees: string[]; selectedTimes: [ { - start: string; - end: string; + start: Date; + end: Date; }, ]; date: Date; @@ -69,8 +73,8 @@ export type periodType = { }; export type AvailableTime = { - start: string; - end: string; + start: Date; + end: Date; room: string; }; @@ -80,7 +84,7 @@ export type committeeInterviewType = { committee: string; committeeEmail: string; availabletimes: AvailableTime[]; - timeslot: string; + timeslot: string; // duration of each interview in minutes message: string; }; @@ -103,8 +107,8 @@ export type OwCommittee = { export type algorithmType = { applicantId: string; interviews: { - start: string; - end: string; + start: Date; + end: Date; committeeName: string; room: string; }[]; @@ -125,8 +129,8 @@ export type emailCommitteeInterviewType = { applicantPhone: string; applicantEmail: string; interviewTime: { - start: string; - end: string; + start: Date; + end: Date; room: string; }; }[]; @@ -142,8 +146,8 @@ export type emailApplicantInterviewType = { committeeName: string; committeeEmail: string; interviewTime: { - start: string; - end: string; + start: Date; + end: Date; room: string; }; }[]; diff --git a/lib/utils/dateUtils.ts b/lib/utils/dateUtils.ts index 8ab5de14..ab9f1cfd 100644 --- a/lib/utils/dateUtils.ts +++ b/lib/utils/dateUtils.ts @@ -1,55 +1,32 @@ -export const formatDate = (inputDate: undefined | Date) => { - const date = new Date(inputDate || ""); +import { isBefore, isAfter, isEqual } from 'date-fns'; +import { formatInTimeZone } from 'date-fns-tz'; +import { nb } from 'date-fns/locale'; - const day = date.getDate().toString().padStart(2, "0"); - const month = (date.getMonth() + 1).toString().padStart(2, "0"); - const year = date.getFullYear(); - const hours = date.getHours().toString().padStart(2, "0"); - const minutes = date.getMinutes().toString().padStart(2, "0"); +export const timezone = 'Europe/Oslo'; // Norwegian Time Zone - return `${day}.${month}.${year}`; // - ${hours}:${minutes} +export const formatDate = (inputDate: undefined | Date) => { + if (!inputDate) return ""; + + return formatInTimeZone(inputDate, timezone, 'dd.MM.yyyy'); // - HH:mm }; -export const formatDateHours = ( - start: string, - end: string -) => { - const startDate = new Date(Date.parse(start)); - - const startTime = start.split("T")[1].slice(0, 5); - const endTime = end.split("T")[1].slice(0, 5); +export const formatDateHours = (start: Date, end: Date): string => { - return `${formatDateNorwegian( - startDate - )}, ${startTime} til ${endTime}`; + const dateStr = formatInTimeZone(start, timezone, 'd. MMMM yyyy', { locale: nb }); + const startTime = formatInTimeZone(start, timezone, 'HH:mm'); + const endTime = formatInTimeZone(end, timezone, 'HH:mm'); + + return `${dateStr}, ${startTime} til ${endTime}`; }; -export const formatDateNorwegian = (inputDate?: Date | string) => { +export const formatDateNorwegian = (inputDate: Date): string => { if (!inputDate) return ""; + + return formatInTimeZone(inputDate, timezone, 'd. MMM', { locale: nb }); +}; - let date: Date; - if (inputDate instanceof Date) { - date = inputDate; - } else { - date = new Date(inputDate); - } - - const day = date.getUTCDate().toString().padStart(2, "0"); - const monthsNorwegian = [ - "jan", - "feb", - "mar", - "apr", - "mai", - "jun", - "jul", - "aug", - "sep", - "okt", - "nov", - "des", - ]; - const month = monthsNorwegian[date.getUTCMonth()]; +export const isBeforeOrEqual = (date: Date, dateToCompare: Date) => + isBefore(date, dateToCompare) || isEqual(date, dateToCompare); - return `${day}. ${month}`; -}; +export const isAfterOrEqual = (date: Date, dateToCompare: Date) => + isAfter(date, dateToCompare) || isEqual(date, dateToCompare); \ No newline at end of file diff --git a/lib/utils/validateApplication.ts b/lib/utils/validateApplication.ts index eada7a89..0fc6e012 100644 --- a/lib/utils/validateApplication.ts +++ b/lib/utils/validateApplication.ts @@ -71,8 +71,8 @@ export const validateApplication = (applicationData: any) => { } for (const time of selectedTimes) { - const startTime = new Date(time.start); - const endTime = new Date(time.end); + const startTime = time.start; + const endTime = time.end; if (isNaN(startTime.getTime()) || isNaN(endTime.getTime())) { toast.error("Ugyldig start- eller sluttid"); return false; diff --git a/lib/utils/validators.ts b/lib/utils/validators.ts index e4433c4f..1eefcd56 100644 --- a/lib/utils/validators.ts +++ b/lib/utils/validators.ts @@ -4,6 +4,7 @@ import { periodType, preferencesType, } from "../types/types"; +import { isBeforeOrEqual } from "./dateUtils"; export const isApplicantType = ( applicant: applicantType, @@ -59,14 +60,12 @@ export const isApplicantType = ( const hasSelectedTimes = Array.isArray(applicant.selectedTimes) && applicant.selectedTimes.every( - (time: { start: string; end: string }) => - typeof time.start === "string" && - typeof time.end === "string" && - new Date(time.start) >= interviewPeriodStart && - new Date(time.start) <= interviewPeriodEnd && - new Date(time.end) <= interviewPeriodEnd && - new Date(time.end) >= interviewPeriodStart && - new Date(time.start) < new Date(time.end) + (time: { start: Date; end: Date }) => + time.start >= interviewPeriodStart && + time.start <= interviewPeriodEnd && + time.end <= interviewPeriodEnd && + time.end >= interviewPeriodStart && + time.start < time.end ); const periodOptionalCommittees = period.optionalCommittees.map((committee) => @@ -117,7 +116,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 now = new Date(); + const isBeforeDeadline = isBeforeOrEqual(now, period.applicationPeriod.end); const committeeExists = period.committees.some((committee) => { @@ -128,15 +128,15 @@ export const validateCommittee = (data: any, period: periodType): boolean => { }); const isWithinInterviewPeriod = data.availabletimes.every( - (time: { start: string; end: string }) => { - const startTime = new Date(time.start); - const endTime = new Date(time.end); + (time: { start: Date; end: Date }) => { + const startTime = time.start; + const endTime = time.end; return ( - startTime >= new Date(period.interviewPeriod.start) && - startTime <= new Date(period.interviewPeriod.end) && - endTime <= new Date(period.interviewPeriod.end) && - endTime >= new Date(period.interviewPeriod.start) && + startTime >= period.interviewPeriod.start && + startTime <= period.interviewPeriod.end && + endTime <= period.interviewPeriod.end && + endTime >= period.interviewPeriod.start && startTime < endTime ); } @@ -152,31 +152,27 @@ export const validateCommittee = (data: any, period: periodType): boolean => { }; export const isPeriodType = (data: any): data is periodType => { - const isDateString = (str: any): boolean => { - return typeof str === "string" && !isNaN(Date.parse(str)); - }; + const isDateDate = (date: any): boolean => { + return date instanceof Date && !isNaN(date.getTime()); + } const isValidPeriod = (period: any): boolean => { return ( typeof period === "object" && period !== null && - isDateString(period.start) && - isDateString(period.end) + isDateDate(period.start) && + isDateDate(period.end) ); }; - const isChronological = (start: string, end: string): boolean => { - return new Date(start) <= new Date(end); - }; - const arePeriodsValid = ( applicationPeriod: any, interviewPeriod: any ): boolean => { return ( - isChronological(applicationPeriod.start, applicationPeriod.end) && - isChronological(interviewPeriod.start, interviewPeriod.end) && - new Date(applicationPeriod.end) <= new Date(interviewPeriod.start) + isBeforeOrEqual(applicationPeriod.start, applicationPeriod.end) && + isBeforeOrEqual(interviewPeriod.start, interviewPeriod.end) && + isBeforeOrEqual(applicationPeriod.end, interviewPeriod.start) ); }; diff --git a/package-lock.json b/package-lock.json index 6887aec3..3f3f4fb7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,9 @@ "@trpc/tanstack-react-query": "^11.6.0", "@types/mongodb": "^4.0.7", "@vercel/analytics": "^1.1.2", + "date-fns": "^4.1.0", + "date-fns-tz": "^3.2.0", + "isomorphic-dompurify": "^2.30.0", "lucide-react": "^0.441.0", "mongodb": "^6.1.0", "next": "^14.2.35", @@ -61,6 +64,56 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.1.tgz", + "integrity": "sha512-B0Hv6G3gWGMn0xKJ0txEi/jM5iFpT3MfDxmhZFb4W047GvytCf1DHQ1D69W3zHI4yWe2aTZAA0JnbMZ7Xc8DuQ==", + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "lru-cache": "^11.2.4" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.7.6", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.7.6.tgz", + "integrity": "sha512-hBaJER6A9MpdG3WgdlOolHmbOYvSk46y7IQN/1+iqiCuUu6iWdQrs9DGKF8ocqsEqWujWf/V7b7vaDgiUmIvUg==", + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.1.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.4" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "license": "MIT" + }, "node_modules/@aws-crypto/sha256-browser": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", @@ -280,7 +333,6 @@ "version": "3.629.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.629.0.tgz", "integrity": "sha512-3if0LauNJPqubGYf8vnlkp+B3yAeKRuRNxfNbHlE6l510xWGcKK/ZsEmiFmfePzKKSRrDh/cxMFMScgOrXptNg==", - "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -333,7 +385,6 @@ "version": "3.629.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.629.0.tgz", "integrity": "sha512-RjOs371YwnSVGxhPjuluJKaxl4gcPYTAky0nPjwBime0i9/iS9nI8R8l5j7k7ec9tpFWjBPvNnThCU07pvjdzw==", - "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -699,6 +750,135 @@ "node": ">=6.9.0" } }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.0.25", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.25.tgz", + "integrity": "sha512-g0Kw9W3vjx5BEBAF8c5Fm2NcB/Fs8jJXh85aXqwEXiL+tqtOut07TWgyaGzAAfTM+gKckrrncyeGEZPcaRgm2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint/eslintrc": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.4.1.tgz", @@ -726,7 +906,6 @@ "version": "6.1.15", "resolved": "https://registry.npmjs.org/@fullcalendar/core/-/core-6.1.15.tgz", "integrity": "sha512-BuX7o6ALpLb84cMw1FCB9/cSgF4JbVO894cjJZ6kP74jzbUZNjtwffwRdA+Id8rrLjT30d/7TrkW90k4zbXB5Q==", - "peer": true, "dependencies": { "preact": "~10.12.1" } @@ -1704,7 +1883,6 @@ "version": "5.90.2", "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.2.tgz", "integrity": "sha512-CLABiR+h5PYfOWr/z+vWFt5VsOA2ekQeRQBFSKlcoW6Ndx/f8rfyVmq4LbgOM4GG2qtxAxjLYLOpCNTYm4uKzw==", - "peer": true, "dependencies": { "@tanstack/query-core": "5.90.2" }, @@ -1723,7 +1901,6 @@ "funding": [ "https://trpc.io/sponsor" ], - "peer": true, "peerDependencies": { "@trpc/server": "11.6.0", "typescript": ">=5.7.2" @@ -1826,6 +2003,13 @@ "integrity": "sha512-YIoDCTH3Af6XM5VuwGG/QL/CJqga1Zm3NkU3HZ4ZHK2fRMPYP1VczsTUqtsf43PH/iJNVlPHAo2oWX7BSdB2Hw==", "dev": true }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/validator": { "version": "13.12.0", "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.12.0.tgz", @@ -1988,7 +2172,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "dev": true, - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2347,6 +2530,15 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -2403,7 +2595,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001646", "electron-to-chromium": "^1.5.4", @@ -2631,6 +2822,19 @@ "node": ">= 8" } }, + "node_modules/css-tree": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.12.2", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -2642,11 +2846,34 @@ "node": ">=4" } }, + "node_modules/cssstyle": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.7.tgz", + "integrity": "sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==", + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^4.1.1", + "@csstools/css-syntax-patches-for-csstree": "^1.0.21", + "css-tree": "^3.1.0", + "lru-cache": "^11.2.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cssstyle/node_modules/lru-cache": { + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "peer": true + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "node_modules/damerau-levenshtein": { "version": "1.0.8", @@ -2654,6 +2881,62 @@ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "dev": true }, + "node_modules/data-urls": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.1.tgz", + "integrity": "sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ==", + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^15.1.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz", + "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", + "license": "MIT", + "dependencies": { + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/data-view-buffer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", @@ -2705,6 +2988,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/date-fns-tz": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-3.2.0.tgz", + "integrity": "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==", + "license": "MIT", + "peerDependencies": { + "date-fns": "^3.0.0 || ^4.0.0" + } + }, "node_modules/dayjs": { "version": "1.11.13", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", @@ -2726,6 +3028,12 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "license": "MIT" + }, "node_modules/deep-equal": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", @@ -2839,6 +3147,15 @@ "node": ">=6.0.0" } }, + "node_modules/dompurify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz", + "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -2863,6 +3180,18 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-abstract": { "version": "1.23.3", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", @@ -3065,7 +3394,6 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.24.0.tgz", "integrity": "sha512-dWFaPhGhTAiPcCgm3f6LI2MBWbogMnTJzFBbhXVRQDJPkr9pGZvVjlVfXd+vyDcWPA2Ic9L2AXPIQM0+vk/cSQ==", "dev": true, - "peer": true, "dependencies": { "@eslint/eslintrc": "^1.3.2", "@humanwhocodes/config-array": "^0.10.5", @@ -3235,7 +3563,6 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", "dev": true, - "peer": true, "dependencies": { "array-includes": "^3.1.7", "array.prototype.findlastindex": "^1.2.3", @@ -3999,6 +4326,40 @@ "node": ">= 0.4" } }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-agent/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -4011,6 +4372,18 @@ "node": ">= 6" } }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4319,6 +4692,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -4469,6 +4848,19 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, + "node_modules/isomorphic-dompurify": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/isomorphic-dompurify/-/isomorphic-dompurify-2.30.0.tgz", + "integrity": "sha512-f4qgn5X2A/gfP0/OeXhJ3iu4zH5IHma0upZg6zEEe3iN5QVHoWr6s9TVQxKSAAo4rW7MNrUROp4TIobY7LAEgg==", + "license": "MIT", + "dependencies": { + "dompurify": "^3.3.0", + "jsdom": "^27.0.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/iterator.prototype": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz", @@ -4539,6 +4931,101 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "27.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.0.1.tgz", + "integrity": "sha512-SNSQteBL1IlV2zqhwwolaG9CwhIhTvVHWg3kTss/cLE7H/X4644mtPQqYvCfsSrGQWt9hSZcgOXX8bOZaMN+kA==", + "license": "MIT", + "dependencies": { + "@asamuzakjp/dom-selector": "^6.7.2", + "cssstyle": "^5.3.1", + "data-urls": "^6.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "parse5": "^8.0.0", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.1.0", + "ws": "^8.18.3", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/jsdom/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/jsdom/node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/jsdom/node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/jsdom/node_modules/whatwg-url": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz", + "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", + "license": "MIT", + "dependencies": { + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -4787,6 +5274,12 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" } }, + "node_modules/mdn-data": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "license": "CC0-1.0" + }, "node_modules/memory-pager": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", @@ -4965,7 +5458,6 @@ "resolved": "https://registry.npmjs.org/next/-/next-14.2.35.tgz", "integrity": "sha512-KhYd2Hjt/O1/1aZVX3dCwGXM1QmOV4eNM2UTacK5gipDdPN/oHHK/4oVGy7X8GMfPMsUTUEmGlsy0EY1YGAkig==", "license": "MIT", - "peer": true, "dependencies": { "@next/env": "14.2.35", "@swc/helpers": "0.5.5", @@ -5333,6 +5825,18 @@ "node": ">=6" } }, + "node_modules/parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", + "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5452,7 +5956,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.0.1", @@ -5598,7 +6101,6 @@ "version": "10.12.1", "resolved": "https://registry.npmjs.org/preact/-/preact-10.12.1.tgz", "integrity": "sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" @@ -5704,7 +6206,6 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -5716,7 +6217,6 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" @@ -5853,6 +6353,15 @@ "url": "https://github.com/sponsors/mysticatea" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -5903,6 +6412,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "license": "MIT" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -5979,6 +6494,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -6448,11 +6981,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "license": "MIT" + }, "node_modules/tailwindcss": { "version": "3.4.9", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.9.tgz", "integrity": "sha512-1SEOvRr6sSdV5IDf9iC+NU4dhwdqzF4zKKq3sAbasUWHEM6lsMhX+eNN5gkPx1BvLFEnZQEUFbXnGj8Qlp83Pg==", - "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -6530,6 +7068,24 @@ "node": ">=0.8" } }, + "node_modules/tldts": { + "version": "7.0.19", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.19.tgz", + "integrity": "sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==", + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.19" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.19", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.19.tgz", + "integrity": "sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==", + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -6541,6 +7097,18 @@ "node": ">=8.0" } }, + "node_modules/tough-cookie": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", + "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/tr46": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", @@ -6713,7 +7281,6 @@ "version": "5.9.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6801,6 +7368,18 @@ "node": ">= 0.10" } }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -6809,6 +7388,28 @@ "node": ">=12" } }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/whatwg-url": { "version": "13.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz", @@ -7016,6 +7617,36 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, "node_modules/xmlbuilder": { "version": "13.0.2", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-13.0.2.tgz", @@ -7024,6 +7655,12 @@ "node": ">=6.0" } }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "license": "MIT" + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", diff --git a/package.json b/package.json index 619f2dee..d7351097 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,9 @@ "@trpc/tanstack-react-query": "^11.6.0", "@types/mongodb": "^4.0.7", "@vercel/analytics": "^1.1.2", + "date-fns": "^4.1.0", + "date-fns-tz": "^3.2.0", + "isomorphic-dompurify": "^2.30.0", "lucide-react": "^0.441.0", "mongodb": "^6.1.0", "next": "^14.2.35", diff --git a/pages/admin/new-period.tsx b/pages/admin/new-period.tsx index 58190a84..4d792df4 100644 --- a/pages/admin/new-period.tsx +++ b/pages/admin/new-period.tsx @@ -85,14 +85,14 @@ const NewPeriod = () => { start, end, }: { - start: string; - end: string; + start: Date; + end: Date; }) => { setPeriodData((prevData) => ({ ...prevData, applicationPeriod: { - start: start ? new Date(start) : undefined, - end: end ? new Date(end) : undefined, + start: start, + end: end, }, })); }; @@ -101,14 +101,14 @@ const NewPeriod = () => { start, end, }: { - start: string; - end: string; + start: Date; + end: Date; }) => { setPeriodData((prevData) => ({ ...prevData, interviewPeriod: { - start: start ? new Date(start) : undefined, - end: end ? new Date(end) : undefined, + start: start, + end: end, }, })); }; diff --git a/pages/api/applicants/[period-id]/[id].ts b/pages/api/applicants/[period-id]/[id].ts index 3c391e1b..2289c80f 100644 --- a/pages/api/applicants/[period-id]/[id].ts +++ b/pages/api/applicants/[period-id]/[id].ts @@ -7,10 +7,10 @@ import { getServerSession } from "next-auth"; import { authOptions } from "../../auth/[...nextauth]"; import { hasSession, - isAdmin, checkOwId, } from "../../../../lib/utils/apiChecks"; import { getPeriodById } from "../../../../lib/mongo/periods"; +import { isBefore } from "date-fns/isBefore"; const handler = async (req: NextApiRequest, res: NextApiResponse) => { const session = await getServerSession(req, res, authOptions); @@ -40,9 +40,9 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { } return res.status(200).json({ exists, application }); } else if (req.method === "DELETE") { - const currentDate = new Date().toISOString(); + const now = new Date(); - if (new Date(period.applicationPeriod.end) < new Date(currentDate)) { + if (isBefore(period.applicationPeriod.end, now)) { return res.status(403).json({ error: "Application period is over" }); } diff --git a/pages/api/applicants/index.ts b/pages/api/applicants/index.ts index e1308d3c..16d56a20 100644 --- a/pages/api/applicants/index.ts +++ b/pages/api/applicants/index.ts @@ -25,7 +25,6 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { if (req.method === "POST") { const requestBody: applicantType = req.body; - requestBody.date = new Date(new Date().getTime() + 60 * 60 * 2000); // add date with norwegain time (GMT+2) const { period } = await getPeriodById(String(requestBody.periodId)); diff --git a/pages/api/committees/times/[period-id]/[committee].ts b/pages/api/committees/times/[period-id]/[committee].ts index 573b31ed..4f62f3ab 100644 --- a/pages/api/committees/times/[period-id]/[committee].ts +++ b/pages/api/committees/times/[period-id]/[committee].ts @@ -7,6 +7,7 @@ import { getServerSession } from "next-auth"; import { authOptions } from "../../../auth/[...nextauth]"; import { hasSession, isInCommitee } from "../../../../../lib/utils/apiChecks"; import { getPeriodById } from "../../../../../lib/mongo/periods"; +import { isAfter } from "date-fns"; const handler = async (req: NextApiRequest, res: NextApiResponse) => { const session = await getServerSession(req, res, authOptions); @@ -50,7 +51,9 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { return res.status(400).json({ error: "Invalid periodId" }); } - if (new Date() > new Date(period.applicationPeriod.end)) { + const now = new Date(); + + if (isAfter(now, period.interviewPeriod.end)) { return res.status(400).json({ error: "Application period has ended" }); } diff --git a/pages/api/committees/times/[period-id]/index.ts b/pages/api/committees/times/[period-id]/index.ts index 297a0016..5ffeb089 100644 --- a/pages/api/committees/times/[period-id]/index.ts +++ b/pages/api/committees/times/[period-id]/index.ts @@ -9,6 +9,7 @@ import { } from "../../../../../lib/utils/validators"; import { getPeriodById } from "../../../../../lib/mongo/periods"; import { committeeInterviewType } from "../../../../../lib/types/types"; +import { isAfter } from "date-fns"; const handler = async (req: NextApiRequest, res: NextApiResponse) => { const session = await getServerSession(req, res, authOptions); @@ -35,7 +36,9 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { return res.status(400).json({ error: "Invalid periodId" }); } - if (new Date() > new Date(period.applicationPeriod.end)) { + const now = new Date(); + + if (isAfter(now, period.interviewPeriod.end)) { return res.status(400).json({ error: "Application period has ended" }); } diff --git a/pages/apply/[period-id].tsx b/pages/apply/[period-id].tsx index 2f6836e3..be66a641 100644 --- a/pages/apply/[period-id].tsx +++ b/pages/apply/[period-id].tsx @@ -25,6 +25,7 @@ import { import ErrorPage from "../../components/ErrorPage"; import { MainTitle, SimpleTitle } from "../../components/Typography"; import { getCommitteeDisplayNameFactory } from "../../lib/utils/getCommitteeDisplayNameFactory"; +import { isBefore } from "date-fns"; const Application: NextPage = () => { const queryClient = useQueryClient(); @@ -116,9 +117,9 @@ const Application: NextPage = () => { setPeriod(periodData.period); - const currentDate = new Date().toISOString(); + const now = new Date(); if ( - new Date(periodData.period.applicationPeriod.end) < new Date(currentDate) + isBefore(periodData.period.applicationPeriod.end, now) ) { setIsApplicationPeriodOver(true); } @@ -160,7 +161,7 @@ const Application: NextPage = () => { return ; if (periodIsError || applicantIsError) return ; - if (!periodData?.exists) + if (!periodData?.exists || !period) return ; if (fetchedApplicationData?.exists) @@ -173,7 +174,7 @@ const Application: NextPage = () => {

    Du vil få enda en e-post med intervjutider når søknadsperioden er over - (rundt {formatDateNorwegian(period?.applicationPeriod?.end)}). + (rundt {formatDateNorwegian(period?.applicationPeriod.end)}).

    (Hvis du ikke finner e-posten din, sjekk søppelpost- eller diff --git a/pages/apply/index.tsx b/pages/apply/index.tsx index 4a7867bf..78851fd9 100644 --- a/pages/apply/index.tsx +++ b/pages/apply/index.tsx @@ -7,6 +7,7 @@ import { fetchPeriods } from "../../lib/api/periodApi"; import { periodType } from "../../lib/types/types"; import { PeriodSkeletonPage } from "../../components/PeriodSkeleton"; import { SimpleTitle } from "../../components/Typography"; +import { isAfterOrEqual, isBeforeOrEqual } from "../../lib/utils/dateUtils"; const Apply = () => { const [currentPeriods, setCurrentPeriods] = useState([]); @@ -24,23 +25,19 @@ const Apply = () => { useEffect(() => { if (!periodsData) return; - const today = new Date(); + const now = new Date(); setCurrentPeriods( periodsData.periods.filter((period: periodType) => { - const startDate = new Date(period.applicationPeriod.start || ""); - const endDate = new Date(period.applicationPeriod.end || ""); + const startDate = period.applicationPeriod.start; + const endDate = period.applicationPeriod.end; - return startDate <= today && endDate >= today; + return isBeforeOrEqual(startDate, now) && isAfterOrEqual(endDate, now); }) ); setUpcomingPeriods( - periodsData.periods.filter((period: periodType) => { - const startDate = new Date(period.applicationPeriod.start || ""); - - return startDate >= today - }) + periodsData.periods.filter((period: periodType) => isAfterOrEqual(period.applicationPeriod.start, now)) ) }, [periodsData]); diff --git a/pages/committee/[period-id]/[committee]/index.tsx b/pages/committee/[period-id]/[committee]/index.tsx index b5f51989..510147d0 100644 --- a/pages/committee/[period-id]/[committee]/index.tsx +++ b/pages/committee/[period-id]/[committee]/index.tsx @@ -9,7 +9,6 @@ import { useRouter } from "next/router"; import ApplicantsOverview from "../../../../components/applicantoverview/ApplicantsOverview"; import { CalendarIcon, - InboxIcon, UserGroupIcon, } from "@heroicons/react/24/solid"; import { Tabs } from "../../../../components/Tabs"; @@ -23,6 +22,7 @@ import { fetchPeriodById } from "../../../../lib/api/periodApi"; import ErrorPage from "../../../../components/ErrorPage"; import { fetchCommitteeTimes } from "../../../../lib/api/committeesApi"; import { MainTitle, SimpleTitle } from "../../../../components/Typography"; +import { addDays, isBefore } from "date-fns"; const CommitteeApplicantOverview: NextPage = () => { const { data: session } = useSession(); @@ -82,15 +82,11 @@ const CommitteeApplicantOverview: NextPage = () => { if (periodIsError || interviewTimesIsError) return ; if (!hasAccess) return ; - const interviewPeriodEnd = period?.interviewPeriod.end - ? new Date(period.interviewPeriod.end) - : null; - // Satt frist til 14 dager etter intervju perioden, så får man ikke tilgang - const interviewAccessExpired = - interviewPeriodEnd && - interviewPeriodEnd.getTime() + 14 * 24 * 60 * 60 * 1000 < - new Date().getTime(); + + const now = new Date(); + const fourteenDaysAfterInterview = addDays(period.interviewPeriod.end, 14); + const interviewAccessExpired = isBefore(fourteenDaysAfterInterview, now); if (interviewAccessExpired) { return ( diff --git a/pages/committees.tsx b/pages/committees.tsx index 2b316473..c95704c9 100644 --- a/pages/committees.tsx +++ b/pages/committees.tsx @@ -11,6 +11,7 @@ import { UsersIcon } from "@heroicons/react/24/outline"; import { Tabs } from "../components/Tabs"; import { UserIcon, BellAlertIcon } from "@heroicons/react/24/solid"; import { shuffleList, partition } from "../lib/utils/arrays"; +import { isAfterOrEqual, isBeforeOrEqual } from "../lib/utils/dateUtils"; // Page Component export default function Committees() { @@ -104,17 +105,17 @@ export default function Committees() { }, ...(committeesInActivePeriod.length > 0 ? [ - { - title: "Har opptak", - icon: , - content: ( - - ), - }, - ] + { + title: "Har opptak", + icon: , + content: ( + + ), + }, + ] : []), ]} /> @@ -166,12 +167,12 @@ const committeeIsInActivePeriod = ( ) => { if (!Array.isArray(periods)) return false; - const today = new Date(); + const now = new Date(); const activePeriods = periods.filter((period) => { - const applicationStart = new Date(period.applicationPeriod.start); - const applicationEnd = new Date(period.applicationPeriod.end); - return applicationStart <= today && applicationEnd >= today; + const applicationStart = period.applicationPeriod.start; + const applicationEnd = period.applicationPeriod.end; + return isBeforeOrEqual(applicationStart, now) && isAfterOrEqual(applicationEnd, now); }); // Bankom is always active, since you can be a representative of bankom from each committee @@ -192,12 +193,12 @@ const committeeIsCurrentlyInterviewing = ( ) => { if (!Array.isArray(periods)) return false; - const today = new Date(); + const now = new Date(); const periodsWithInterviewsCurrently = periods.filter((period) => { - const interviewStart = new Date(period.interviewPeriod.start); - const interviewEnd = new Date(period.interviewPeriod.end); - return interviewStart <= today && interviewEnd >= today; + const interviewStart = period.interviewPeriod.start; + const interviewEnd = period.interviewPeriod.end; + return isBeforeOrEqual(interviewStart, now) && isAfterOrEqual(interviewEnd, now); }); // Bankom is always active, since you can be a representative of bankom from each committee From f0c5cb963659f8ac7b19a1abe4545427496a06ec Mon Sep 17 00:00:00 2001 From: Julian Ammouche Ottosen Date: Thu, 22 Jan 2026 11:07:34 +0100 Subject: [PATCH 2/2] Yess --- components/form/DatePickerInput.tsx | 14 ++++++++----- lib/mongo/periods.ts | 4 ++-- .../formatInterviewEmail.ts | 21 ++++++++----------- lib/sendInterviewTimes/formatInterviewSMS.ts | 11 +++++----- pages/api/applicants/[period-id]/[id].ts | 2 +- pages/api/applicants/index.ts | 3 ++- 6 files changed, 28 insertions(+), 27 deletions(-) diff --git a/components/form/DatePickerInput.tsx b/components/form/DatePickerInput.tsx index 4e8045a3..9cd0f3a1 100644 --- a/components/form/DatePickerInput.tsx +++ b/components/form/DatePickerInput.tsx @@ -13,17 +13,18 @@ const DatePickerInput = (props: Props) => { useEffect(() => { if (!fromDate || !toDate) return; - - // Parse as Norwegian time, convert to UTC Date object + + + // Convert to Date objects in correct timezone const startDate = fromZonedTime( - `${fromDate}T00:00:00`, + `${fromDate}T00:00:00`, timezone ); const endDate = fromZonedTime( - `${toDate}T23:59:59`, + `${toDate}T23:59:59`, timezone ); - + props.updateDates({ start: startDate, end: endDate }); }, [fromDate, toDate]); @@ -51,6 +52,9 @@ const DatePickerInput = (props: Props) => { className="border text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 border-gray-300 text-gray-900 dark:border-gray-600 dark:bg-online-darkBlue dark:text-gray-200" /> +

    + NB: Alle tider er i norsk tidssone (GMT+1) +

    ); }; diff --git a/lib/mongo/periods.ts b/lib/mongo/periods.ts index 04a39612..e40814b3 100644 --- a/lib/mongo/periods.ts +++ b/lib/mongo/periods.ts @@ -64,12 +64,12 @@ export const getCurrentPeriods = async () => { const filter = { $or: [ { - // Check if current ISO date string is within the application period + // Check if current date is within the application period "applicationPeriod.start": { $lte: now }, "applicationPeriod.end": { $gte: now }, }, { - // Check if current ISO date string is within the interview period + // Check if current date is within the interview period "interviewPeriod.start": { $lte: now }, "interviewPeriod.end": { $gte: now }, }, diff --git a/lib/sendInterviewTimes/formatInterviewEmail.ts b/lib/sendInterviewTimes/formatInterviewEmail.ts index e000deb9..80282cf1 100644 --- a/lib/sendInterviewTimes/formatInterviewEmail.ts +++ b/lib/sendInterviewTimes/formatInterviewEmail.ts @@ -1,3 +1,4 @@ +import { compareAsc } from "date-fns"; import { emailApplicantInterviewType, emailCommitteeInterviewType, @@ -11,12 +12,10 @@ export const formatApplicantInterviewEmail = ( ) => { let emailBody = `

    Hei ${applicant.applicantName},

    Her er dine intervjutider for ${applicant.period_name}:


      `; - applicant.committees.sort((a, b) => { - return ( - new Date(a.interviewTime.start).getTime() - - new Date(b.interviewTime.start).getTime() - ); - }); + // Sort committees by interview start time + applicant.committees.sort((a, b) => + compareAsc(a.interviewTime.start, b.interviewTime.start) + ); applicant.committees.forEach((committee) => { emailBody += `
    • Komité: ${changeDisplayName( @@ -49,12 +48,10 @@ export const formatCommitteeInterviewEmail = ( committee.applicants.length } søkere:

        `; - committee.applicants.sort((a, b) => { - return ( - new Date(a.interviewTime.start).getTime() - - new Date(b.interviewTime.start).getTime() - ); - }); + // Sort applicants by interview start time + committee.applicants.sort((a, b) => + compareAsc(a.interviewTime.start, b.interviewTime.start) + ); committee.applicants.forEach((applicant) => { emailBody += `
      • Navn: ${applicant.applicantName}
        `; diff --git a/lib/sendInterviewTimes/formatInterviewSMS.ts b/lib/sendInterviewTimes/formatInterviewSMS.ts index 8c621390..08e5de91 100644 --- a/lib/sendInterviewTimes/formatInterviewSMS.ts +++ b/lib/sendInterviewTimes/formatInterviewSMS.ts @@ -1,3 +1,4 @@ +import { compareAsc } from "date-fns"; import { emailApplicantInterviewType } from "../types/types"; import { formatDateHours } from "../utils/dateUtils"; import { changeDisplayName } from "../utils/toString"; @@ -5,12 +6,10 @@ import { changeDisplayName } from "../utils/toString"; export const formatInterviewSMS = (applicant: emailApplicantInterviewType) => { let phoneBody = `Hei ${applicant.applicantName}, her er dine intervjutider for ${applicant.period_name}: \n \n`; - applicant.committees.sort((a, b) => { - return ( - new Date(a.interviewTime.start).getTime() - - new Date(b.interviewTime.start).getTime() - ); - }); + // Sort committees by interview start time + applicant.committees.sort((a, b) => + compareAsc(a.interviewTime.start, b.interviewTime.start) + ); applicant.committees.forEach((committee) => { phoneBody += `Komité: ${changeDisplayName(committee.committeeName)} \n`; diff --git a/pages/api/applicants/[period-id]/[id].ts b/pages/api/applicants/[period-id]/[id].ts index 2289c80f..e2799a5f 100644 --- a/pages/api/applicants/[period-id]/[id].ts +++ b/pages/api/applicants/[period-id]/[id].ts @@ -10,7 +10,7 @@ import { checkOwId, } from "../../../../lib/utils/apiChecks"; import { getPeriodById } from "../../../../lib/mongo/periods"; -import { isBefore } from "date-fns/isBefore"; +import { isBefore } from "date-fns"; const handler = async (req: NextApiRequest, res: NextApiResponse) => { const session = await getServerSession(req, res, authOptions); diff --git a/pages/api/applicants/index.ts b/pages/api/applicants/index.ts index 8736170a..20d06878 100644 --- a/pages/api/applicants/index.ts +++ b/pages/api/applicants/index.ts @@ -8,6 +8,7 @@ import { isApplicantType } from "../../../lib/utils/validators"; import { isAdmin, hasSession, checkOwId } from "../../../lib/utils/apiChecks"; import { sendConfirmationSMS } from "../../../lib/sms/sendConfirmationSMS"; import { sendConfirmationEmail } from "../../../lib/email/sendConfirmationEmail"; +import { isAfter, isBefore } from "date-fns"; const handler = async (req: NextApiRequest, res: NextApiResponse) => { const session = await getServerSession(req, res, authOptions); @@ -43,7 +44,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { const applicationEnd = period.applicationPeriod.end; // Check if the current time is within the application period - if (now < applicationStart || now > applicationEnd) { + if (isBefore(now, applicationStart) || isAfter(now, applicationEnd)) { return res .status(400) .json({ error: "Not within the application period" });