diff --git a/apps/web/app/i18n/messages/ar.json b/apps/web/app/i18n/messages/ar.json
index 3418d85..5d24f56 100644
--- a/apps/web/app/i18n/messages/ar.json
+++ b/apps/web/app/i18n/messages/ar.json
@@ -67,7 +67,8 @@
"applyToJoin": "قدّم طلب الانضمام",
"viewUpcomingSessions": "اطّلع على الجلسات القادمة",
"submitYourUpdate": "شاركونا مستجداتكم",
- "addToCalendar": "أضف إلى التقويم",
+ "addToGoogleCalendar": "أضف إلى تقويم Google",
+ "downloadIcsFile": "تنزيل ملف .ics",
"tryAgain": "حاول مرة أخرى"
},
"filters": {
diff --git a/apps/web/app/i18n/messages/en.json b/apps/web/app/i18n/messages/en.json
index 375295e..8adee14 100644
--- a/apps/web/app/i18n/messages/en.json
+++ b/apps/web/app/i18n/messages/en.json
@@ -67,7 +67,8 @@
"applyToJoin": "Apply to join",
"viewUpcomingSessions": "View upcoming sessions",
"submitYourUpdate": "Submit your update",
- "addToCalendar": "Add to calendar",
+ "addToGoogleCalendar": "Add to Google Calendar",
+ "downloadIcsFile": "Download .ics file",
"tryAgain": "Try again"
},
"filters": {
diff --git a/apps/web/app/routes/_events-opportunities/copy.ts b/apps/web/app/routes/_events-opportunities/copy.ts
index 87a98cc..30c4408 100644
--- a/apps/web/app/routes/_events-opportunities/copy.ts
+++ b/apps/web/app/routes/_events-opportunities/copy.ts
@@ -65,7 +65,8 @@ const EN = {
eventCompleted: "This event has already taken place.",
joinOnlineLabel: "Joining link",
addToCalendarHint:
- "Downloads an .ics file you can open in any calendar application.",
+ "The .ics file is compatible with Calendar on Apple devices and " +
+ "other calendar apps.",
timeZoneNote: "All times are shown in Libya time (UTC+2).",
/**
* `event-registration.consentGdpr` is REQUIRED in the CMS, so the reader
@@ -259,7 +260,9 @@ const AR: RouteCopy = {
"اكتمل العدد في هذه الفعالية. انضم إلى قائمة الانتظار وسنتواصل معك في حال توفّر مكان.",
eventCompleted: "انتهت هذه الفعالية.",
joinOnlineLabel: "رابط الانضمام",
- addToCalendarHint: "يُنزّل ملف .ics يمكن فتحه في أي تطبيق تقويم.",
+ addToCalendarHint:
+ "ملف .ics متوافق مع تطبيق التقويم على أجهزة Apple ومع تطبيقات " +
+ "التقويم الأخرى.",
timeZoneNote: "جميع الأوقات معروضة بتوقيت ليبيا (UTC+2).",
consentLabel:
"أوافق على أن يحتفظ مشروع SHARP-ER بالبيانات أعلاه لإدارة تسجيلي في هذه الفعالية.",
diff --git a/apps/web/app/routes/_events-opportunities/ics.ts b/apps/web/app/routes/_events-opportunities/ics.ts
index 919af85..17a4649 100644
--- a/apps/web/app/routes/_events-opportunities/ics.ts
+++ b/apps/web/app/routes/_events-opportunities/ics.ts
@@ -1,7 +1,7 @@
/* ============================================================================
iCALENDAR (RFC 5545)
----------------------------------------------------------------------------
- Builds the file behind "Add to calendar". Pure, dependency-free and written
+ Builds the file behind "Download .ics file". Pure, dependency-free and written
against the spec rather than against one client's tolerance:
* CRLF line endings, and content lines folded at 75 OCTETS - folded on
diff --git a/apps/web/app/routes/events-calendar.$slug.tsx b/apps/web/app/routes/events-calendar.$slug.tsx
index a2a5e07..81fd126 100644
--- a/apps/web/app/routes/events-calendar.$slug.tsx
+++ b/apps/web/app/routes/events-calendar.$slug.tsx
@@ -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() {
{copy.eventDetail.addToCalendarHint} diff --git a/apps/web/public/assets/icons/google-calendar.svg b/apps/web/public/assets/icons/google-calendar.svg new file mode 100644 index 0000000..011cf5e --- /dev/null +++ b/apps/web/public/assets/icons/google-calendar.svg @@ -0,0 +1,148 @@ + +