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" });