diff --git a/api/package-lock.json b/api/package-lock.json index b20dcf9..b1cf52f 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -15,6 +15,7 @@ "graphile-worker": "^0.16.6", "hono": "^4.7.7", "hono-sessions": "^0.7.2", + "luxon": "^3.7.2", "multer": "^2.0.2", "nodemailer": "^7.0.4", "stream-chat": "^9.8.0", @@ -3905,6 +3906,14 @@ "get-func-name": "^2.0.1" } }, + "node_modules/luxon": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", + "engines": { + "node": ">=12" + } + }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", diff --git a/api/package.json b/api/package.json index 5cd3dea..254a1cf 100644 --- a/api/package.json +++ b/api/package.json @@ -21,6 +21,7 @@ "graphile-worker": "^0.16.6", "hono": "^4.7.7", "hono-sessions": "^0.7.2", + "luxon": "^3.7.2", "multer": "^2.0.2", "nodemailer": "^7.0.4", "stream-chat": "^9.8.0", @@ -41,5 +42,5 @@ }, "overrides": { "linkifyjs": "^4.3.2" - } + } } diff --git a/api/src/__tests__/slots.test.ts b/api/src/__tests__/slots.test.ts index 136ccd9..8e3cce5 100644 --- a/api/src/__tests__/slots.test.ts +++ b/api/src/__tests__/slots.test.ts @@ -5,7 +5,7 @@ import { type EventTypeConfig, type Exception, generateTimeSlots, - getDateString, + getDateStringCET, getDayOfWeek, type WeeklyAvailability, } from "../slots.js"; @@ -734,8 +734,10 @@ describe("generateTimeSlots", () => { expect(slot09_00?.available, "09:00 should be available").to.be.true; expect(slot09_30?.available, "09:30 should not be available").to.be.false; - expect(slot10_00?.available, "10:00 should not be unavailable").to.be.false; // Within buffered time - expect(slot10_30?.available, "10:30 should not be unavailable").to.be.false; // Within buffered time + expect(slot10_00?.available, "10:00 should not be unavailable").to.be + .false; // Within buffered time + expect(slot10_30?.available, "10:30 should not be unavailable").to.be + .false; // Within buffered time expect(slot11_00?.available, "11:00 should be available").to.be.true; // After buffered time }); @@ -1143,8 +1145,12 @@ describe("generateTimeSlots", () => { }); it("should format date strings correctly", () => { - expect(getDateString(new Date("2024-01-15T10:30:00Z"))).to.equal("2024-01-15"); - expect(getDateString(new Date("2024-12-31T23:59:59Z"))).to.equal("2024-12-31"); + expect(getDateStringCET(new Date("2024-01-15T10:30:00Z"))).to.equal( + "2024-01-15" + ); + expect(getDateStringCET(new Date("2024-12-31T23:59:59Z"))).to.equal( + "2025-01-01" + ); }); }); }); diff --git a/api/src/public.ts b/api/src/public.ts index 5843249..3ad5668 100644 --- a/api/src/public.ts +++ b/api/src/public.ts @@ -6,40 +6,12 @@ import { type EventTypeConfig, type Exception, generateTimeSlots, - getDateString, + getDateStringCET, getDayOfWeek, type TimeSlot, type WeeklyAvailability, } from "./slots.js"; -// Helper function to get current time in CET -function getCETTime(): Date { - const utcNow = new Date(); - - // Use Intl.DateTimeFormat to get the correct CET/CEST time - const formatter = new Intl.DateTimeFormat("en", { - timeZone: "Europe/Paris", - year: "numeric", - month: "2-digit", - day: "2-digit", - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - hour12: false, - }); - - const parts = formatter.formatToParts(utcNow); - const year = parseInt(parts.find((p) => p.type === "year")?.value || "0"); - const month = - parseInt(parts.find((p) => p.type === "month")?.value || "0") - 1; // Month is 0-indexed - const day = parseInt(parts.find((p) => p.type === "day")?.value || "0"); - const hour = parseInt(parts.find((p) => p.type === "hour")?.value || "0"); - const minute = parseInt(parts.find((p) => p.type === "minute")?.value || "0"); - const second = parseInt(parts.find((p) => p.type === "second")?.value || "0"); - - return new Date(year, month, day, hour, minute, second); -} - export const publicRouter = new Hono<{ Variables: { supabase: SupabaseClient; @@ -102,7 +74,7 @@ publicRouter.get("/slots/:shortUserId/:standardName", async (c) => { // Get existing events for the next month // Use CET time for availability calculations - const now = getCETTime(); + const now = new Date(); const nextMonth = new Date(now); nextMonth.setMonth(now.getMonth() + 2); @@ -110,8 +82,8 @@ publicRouter.get("/slots/:shortUserId/:standardName", async (c) => { .from("events") .select("*") .eq("created_by", user.id) - .gte("start_date", getDateString(now)) - .lte("start_date", getDateString(nextMonth)) + .gte("start_date", getDateStringCET(now)) + .lte("start_date", getDateStringCET(nextMonth)) .is("deleted_at", null); if (eventsError) { diff --git a/api/src/slots.ts b/api/src/slots.ts index b509129..7e5d5e4 100644 --- a/api/src/slots.ts +++ b/api/src/slots.ts @@ -1,30 +1,6 @@ import type { Tables } from "./database.types.js"; -// Helper function to convert UTC date to CET -function convertToCET(utcDate: Date): Date { - // Use Intl.DateTimeFormat to get the correct CET/CEST offset - const formatter = new Intl.DateTimeFormat("en", { - timeZone: "Europe/Paris", - year: "numeric", - month: "2-digit", - day: "2-digit", - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - hour12: false, - }); - - const parts = formatter.formatToParts(utcDate); - const year = parseInt(parts.find((p) => p.type === "year")?.value || "0"); - const month = - parseInt(parts.find((p) => p.type === "month")?.value || "0") - 1; // Month is 0-indexed - const day = parseInt(parts.find((p) => p.type === "day")?.value || "0"); - const hour = parseInt(parts.find((p) => p.type === "hour")?.value || "0"); - const minute = parseInt(parts.find((p) => p.type === "minute")?.value || "0"); - const second = parseInt(parts.find((p) => p.type === "second")?.value || "0"); - - return new Date(year, month, day, hour, minute, second); -} +import { DateTime } from "luxon"; // Types for availability calculation type TimeRange = { @@ -139,39 +115,38 @@ function getMinAdvanceBookingDate( config: EventTypeConfig, currentDate: Date ): { date: string; time: string } { - // Convert current UTC date to CET - const cetCurrentDate = convertToCET(currentDate); - if (!config.minAdvanceBooking) { return { - date: getDateString(cetCurrentDate), - time: formatTime(cetCurrentDate.getHours(), cetCurrentDate.getMinutes()), + date: getDateStringCET(currentDate), + time: formatTime(currentDate.getHours(), currentDate.getMinutes()), }; } const { value, unit } = config.minAdvanceBooking; - const advanceDate = new Date(cetCurrentDate); + const advanceDate = new Date(currentDate); switch (unit) { case "minutes": - advanceDate.setMinutes(cetCurrentDate.getMinutes() + value); + advanceDate.setMinutes(currentDate.getMinutes() + value); break; case "hours": - advanceDate.setHours(cetCurrentDate.getHours() + value); + advanceDate.setHours(currentDate.getHours() + value); break; case "days": - advanceDate.setDate(cetCurrentDate.getDate() + value); + advanceDate.setDate(currentDate.getDate() + value); break; } return { - date: getDateString(advanceDate), + date: getDateStringCET(advanceDate), time: formatTime(advanceDate.getHours(), advanceDate.getMinutes()), }; } -export function getDateString(date: Date): string { - return date.toISOString().split("T")[0]; +export function getDateStringCET(date: Date): string { + return DateTime.fromJSDate(date) + .setZone("Europe/Paris") + .toFormat("yyyy-MM-dd"); } export function generateTimeSlots( @@ -182,14 +157,13 @@ export function generateTimeSlots( exceptions: Exception[], existingEvents: Tables<"events">[] ): TimeSlot[] { - const dateStr = getDateString(date); + const dateStr = getDateStringCET(date); const slots: TimeSlot[] = []; // Check if this date has an exception const exception = exceptions.find((e) => { const exceptionDate = new Date(e.date); - const exceptionDateCET = convertToCET(exceptionDate); - const exceptionDateStr = getDateString(exceptionDateCET); + const exceptionDateStr = getDateStringCET(exceptionDate); return exceptionDateStr === dateStr; }); diff --git a/ui/src/pages/EmbeddedBookingPage.tsx b/ui/src/pages/EmbeddedBookingPage.tsx index 2af4859..04e4baa 100644 --- a/ui/src/pages/EmbeddedBookingPage.tsx +++ b/ui/src/pages/EmbeddedBookingPage.tsx @@ -435,8 +435,8 @@ export function EmbeddedBookingPage() { } return ( -
-
+
+
{/* Left Side - Event Details */}