Files
BelgianBitcoinEmbassy/frontend/lib/meetupEventTime.ts
T
bbeandCursor 6023991f5c feat: add SEO metadata, llms.txt, and markdown page mirrors
Centralize site metadata and social URLs for JSON-LD, expose llmstxt.org
content and per-page .md routes for LLM crawlers, and refactor blog/FAQ/events
pages with shared components.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 00:59:41 +02:00

116 lines
3.9 KiB
TypeScript
Raw 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));
}
/**
* Returns the event end instant in UTC when the time string carries a range
* (e.g. "18:00 - 21:00"), otherwise null. Used for schema.org Event.endDate.
*/
export function getMeetupEndUtc(dateStr: string, timeStr: string): Date | null {
const key = normalizeMeetupDateKey(dateStr);
if (!key) return null;
const parts = key.split("-").map(Number);
const year = parts[0];
const month = parts[1];
const day = parts[2];
if (!year || !month || !day) return null;
const timeParts = (timeStr?.trim() || "").split(/\s*[-]\s*/);
if (timeParts.length < 2 || !timeParts[1]?.trim()) return null;
const { h: endH, m: endM } = parseLocalTime(timeParts[1]);
const utcEndH = endH - BRUSSELS_OFFSET_HOURS;
return new Date(Date.UTC(year, month - 1, day, utcEndH, endM, 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 ?? "—";
}