Files
BelgianBitcoinEmbassy/frontend/lib/meetupEventTime.ts
bbe 78271ea110 feat: organizers, meetups UI, Plausible analytics, and migration tooling
- Add organizer model/API, admin and public organizer pages, meetup cards
- Refresh events/home/contact; add calendar dialog and carousel components
- Optional Plausible via NEXT_PUBLIC_PLAUSIBLE_* env vars in root layout
- Prisma migration, seed updates, baseline-and-migrate script

Made-with: Cursor
2026-04-04 21:55:34 +02:00

95 lines
3.1 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Event start times in Brussels local wall time, converted to UTC the same way as
* backend/src/api/calendar.ts (parseEventDates). Keeps admin/public UI aligned with ICS.
*/
// Parse "HH:MM", "H:MM am/pm", "Hpm" etc.
function parseLocalTime(t: string): { h: number; m: number } {
const clean = t.trim();
const m24 = clean.match(/^(\d{1,2}):(\d{2})$/);
if (m24) return { h: parseInt(m24[1], 10), m: parseInt(m24[2], 10) };
const mAp = clean.match(/^(\d{1,2})(?::(\d{2}))?\s*(am|pm)$/i);
if (mAp) {
let h = parseInt(mAp[1], 10);
const m = mAp[2] ? parseInt(mAp[2], 10) : 0;
if (mAp[3].toLowerCase() === "pm" && h !== 12) h += 12;
if (mAp[3].toLowerCase() === "am" && h === 12) h = 0;
return { h, m };
}
return { h: 18, m: 0 };
}
// Brussels is UTC+1 (CET) / UTC+2 (CEST). Same as calendar.ts.
const BRUSSELS_OFFSET_HOURS = 1;
/** Extract YYYY-MM-DD from stored date (ISO date-only or full ISO datetime). */
export function normalizeMeetupDateKey(dateStr: string): string | null {
const s = dateStr?.trim();
if (!s) return null;
const dayPart = s.includes("T") ? s.split("T")[0]! : s.slice(0, 10);
if (!/^\d{4}-\d{2}-\d{2}$/.test(dayPart)) return null;
return dayPart;
}
/**
* Returns event start instant in UTC, or Invalid Date if date/time cannot be parsed.
*/
export function getMeetupStartUtc(dateStr: string, timeStr: string): Date {
const key = normalizeMeetupDateKey(dateStr);
if (!key) return new Date(NaN);
const parts = key.split("-").map(Number);
const year = parts[0];
const month = parts[1];
const day = parts[2];
if (!year || !month || !day) return new Date(NaN);
const t = timeStr?.trim() ? timeStr : "00:00";
const timeParts = t.split(/\s*[-]\s*/);
const { h: startH, m: startM } = parseLocalTime(timeParts[0] ?? "");
const utcStartH = startH - BRUSSELS_OFFSET_HOURS;
return new Date(Date.UTC(year, month - 1, day, utcStartH, startM, 0));
}
const UTC_CAL_OPTS = { timeZone: "UTC" } as const;
/**
* Format the stored meetup calendar date (YYYY-MM-DD) for display without shifting
* by the viewer's timezone. Date-only strings parsed as new Date("YYYY-MM-DD") are
* UTC midnight and show the wrong local day in western zones.
*/
export function formatMeetupCivilDate(dateStr: string): {
monthShort: string;
day: string;
full: string;
} | null {
const key = normalizeMeetupDateKey(dateStr);
if (!key) return null;
const parts = key.split("-").map(Number);
const year = parts[0];
const month = parts[1];
const dayNum = parts[2];
if (!year || !month || !dayNum) return null;
const ref = new Date(Date.UTC(year, month - 1, dayNum, 12, 0, 0));
return {
monthShort: ref
.toLocaleString("en-US", { ...UTC_CAL_OPTS, month: "short" })
.toUpperCase(),
day: String(ref.getUTCDate()),
full: ref.toLocaleString("en-US", {
...UTC_CAL_OPTS,
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
}),
};
}
/** Long single-line civil date for event detail (same rules as formatMeetupCivilDate). */
export function formatMeetupCivilDateLong(dateStr: string): string {
return formatMeetupCivilDate(dateStr)?.full ?? "—";
}