feat(events): added "Add to Google Calendar" button; and clarify .ics download

This commit is contained in:
2026-10-02 12:49:11 +02:00
parent 5310272715
commit a5c531be00
6 changed files with 221 additions and 8 deletions
+63 -3
View File
@@ -13,8 +13,8 @@
refuses to submit without it. It is never a hidden input: consent that the
reader did not give is not consent.
"Add to calendar" points at the sibling resource route,
/{locale}/events-calendar/{slug}.ics, which streams a real RFC 5545 file.
The .ics download points at the sibling resource route,
/{locale}/events-calendar/{slug}.ics, which streams a real RFC 5545 file.
ROUTE
Registered in app/routes.ts as "eventsDetail", a SIBLING of the listing route
@@ -62,6 +62,7 @@ import {
formatEventWhen,
isRegistrationOpen,
isWaitlisted,
type EventDetailData,
} from "./_events-opportunities/events";
import { fetchEvent } from "./_events-opportunities/events.server";
import {
@@ -98,6 +99,47 @@ import { RichText } from "./_shared/RichText";
*/
export const handle = SELF_ALTERNATES_HANDLE;
function addCalendarDay(date: Date): Date {
const next = new Date(date);
next.setUTCDate(next.getUTCDate() + 1);
return next;
}
function toGoogleCalendarDate(date: Date): string {
return date.toISOString().slice(0, 10).replace(/-/g, "");
}
function toGoogleCalendarDateTime(date: Date): string {
return date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}/, "");
}
function buildGoogleCalendarHref(event: EventDetailData): string {
const start = new Date(event.start);
const parsedEnd = event.end ? new Date(event.end) : undefined;
const end =
parsedEnd && !Number.isNaN(parsedEnd.getTime()) ? parsedEnd : undefined;
const dates = event.allDay
? `${toGoogleCalendarDate(start)}/${toGoogleCalendarDate(
addCalendarDay(end ?? start),
)}`
: `${toGoogleCalendarDateTime(start)}/${toGoogleCalendarDateTime(
end && end.getTime() > start.getTime() ? end : start,
)}`;
const params = new URLSearchParams({
action: "TEMPLATE",
text: event.title,
dates,
});
const details = [event.summary, event.onlineUrl].filter(Boolean).join("\n\n");
const location = event.place ?? event.venue;
if (details) params.set("details", details);
if (location) params.set("location", location);
return `https://calendar.google.com/calendar/render?${params.toString()}`;
}
export async function loader({ params }: Route.LoaderArgs) {
const locale = requireLocale(params.lang);
const slug = params.slug ?? "";
@@ -429,6 +471,24 @@ export default function EventDetail() {
<DetailList heading={copy.eventDetail.detailsHeading} items={details} />
<div className="flex flex-col gap-xs">
<Button
as="a"
href={buildGoogleCalendarHref(event)}
target="_blank"
rel="noopener noreferrer"
variant="secondary"
iconStart={
<img
src="/assets/icons/google-calendar.svg"
alt=""
aria-hidden="true"
height={20}
/>
}
fullWidth
>
{t("actions.addToGoogleCalendar")}
</Button>
<Button
as="a"
href={`${detailPath}.ics`}
@@ -436,7 +496,7 @@ export default function EventDetail() {
variant="secondary"
fullWidth
>
{t("actions.addToCalendar")}
{t("actions.downloadIcsFile")}
</Button>
<p className="body-sm text-text-tertiary">
{copy.eventDetail.addToCalendarHint}