Address issue with timezones and improve embed

This commit is contained in:
Arthur Belleville 2025-10-19 14:58:03 +02:00
parent 2da8984a8b
commit 4043ec775b
No known key found for this signature in database
6 changed files with 42 additions and 80 deletions

9
api/package-lock.json generated
View file

@ -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",

View file

@ -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"
}
}
}

View file

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

View file

@ -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) {

View file

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

View file

@ -435,8 +435,8 @@ export function EmbeddedBookingPage() {
}
return (
<div className="w-[1130px] h-[700px] m-6 bg-transparent overflow-hidden">
<div className="h-full bg-white dark:bg-gray-800 rounded-2xl shadow-2xl border border-gray-200 dark:border-gray-700 flex overflow-hidden">
<div className="w-[1130px] h-[700px] px-6 mt-6 mb-4 bg-transparent overflow-hidden">
<div className="h-full bg-white dark:bg-gray-800 rounded-2xl border border-gray-200 dark:border-gray-700 flex overflow-hidden">
{/* Left Side - Event Details */}
<div
className={twMerge(