feat(events): added "Add to Google Calendar" button; and clarify .ics download
This commit is contained in:
@@ -67,7 +67,8 @@
|
||||
"applyToJoin": "قدّم طلب الانضمام",
|
||||
"viewUpcomingSessions": "اطّلع على الجلسات القادمة",
|
||||
"submitYourUpdate": "شاركونا مستجداتكم",
|
||||
"addToCalendar": "أضف إلى التقويم",
|
||||
"addToGoogleCalendar": "أضف إلى تقويم Google",
|
||||
"downloadIcsFile": "تنزيل ملف .ics",
|
||||
"tryAgain": "حاول مرة أخرى"
|
||||
},
|
||||
"filters": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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 بالبيانات أعلاه لإدارة تسجيلي في هذه الفعالية.",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
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,
|
||||
The .ics download points at the sibling resource route,
|
||||
/{locale}/events-calendar/{slug}.ics, which streams a real RFC 5545 file.
|
||||
|
||||
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}
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="800"
|
||||
height="859.0954"
|
||||
fill="none"
|
||||
version="1.1"
|
||||
id="svg10"
|
||||
sodipodi:docname="Calendar2026.svg"
|
||||
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview10"
|
||||
pagecolor="#505050"
|
||||
bordercolor="#ffffff"
|
||||
borderopacity="1"
|
||||
inkscape:showpageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="1"
|
||||
inkscape:deskcolor="#505050"
|
||||
inkscape:zoom="4.3177083"
|
||||
inkscape:cx="76.313631"
|
||||
inkscape:cy="87.893848"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1009"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg10" />
|
||||
<path
|
||||
fill="#bbe2ff"
|
||||
d="M 64.743253,150.86554 C 64.743253,67.543758 132.28701,0 215.60879,0 h 368.78242 c 83.32178,0 150.86554,67.543758 150.86554,150.86554 v 159.24695 c 0,83.32178 -67.54376,150.86554 -150.86554,150.86554 H 215.60879 c -83.32178,0 -150.865537,-67.54376 -150.865537,-150.86554 z"
|
||||
id="path1"
|
||||
style="stroke-width:5.23839" />
|
||||
<path
|
||||
fill="#3c90ff"
|
||||
d="M 1.1859076,216.8273 C -9.5475474,135.25514 53.952176,62.86064 136.22104,62.86064 h 527.55792 c 82.2741,0 145.76859,72.3945 135.03513,153.96666 l -32.12702,244.15073 32.12702,244.15072 c 10.73346,81.57216 -52.76103,153.96666 -135.03513,153.96666 H 136.22104 c -82.274102,0 -145.7685874,-72.3945 -135.0351324,-153.96666 L 33.312933,460.97803 Z"
|
||||
id="path2"
|
||||
style="stroke-width:5.23839" />
|
||||
<mask
|
||||
id="a"
|
||||
width="154"
|
||||
height="152"
|
||||
x="19"
|
||||
y="20"
|
||||
maskUnits="userSpaceOnUse">
|
||||
<path
|
||||
fill="#3c90ff"
|
||||
d="M 19.867,49.392 C 17.818,33.82 29.94,20 45.645,20 h 100.71 c 15.706,0 27.827,13.82 25.778,29.392 L 166,96 l 6.133,46.608 C 174.182,158.18 162.061,172 146.355,172 H 45.645 C 29.939,172 17.818,158.18 19.867,142.608 L 26,96 Z"
|
||||
id="path3" />
|
||||
</mask>
|
||||
<g
|
||||
mask="url(#a)"
|
||||
id="g4"
|
||||
transform="matrix(5.2383867,0,0,5.2383867,-102.88512,-41.907093)">
|
||||
<path
|
||||
fill="url(#b)"
|
||||
d="M 0,0 H 166 V 76 H 0 Z"
|
||||
transform="matrix(1,0,0,-1,13,172)"
|
||||
id="path4"
|
||||
style="fill:url(#b)" />
|
||||
</g>
|
||||
<mask
|
||||
id="c"
|
||||
width="154"
|
||||
height="152"
|
||||
x="19"
|
||||
y="20"
|
||||
maskUnits="userSpaceOnUse">
|
||||
<path
|
||||
fill="#3186ff"
|
||||
d="M 19.867,49.392 C 17.818,33.82 29.94,20 45.645,20 h 100.71 c 15.706,0 27.827,13.82 25.778,29.392 L 166,96 l 6.133,46.608 C 174.182,158.18 162.061,172 146.355,172 H 45.645 C 29.939,172 17.818,158.18 19.867,142.608 L 26,96 Z"
|
||||
id="path5" />
|
||||
</mask>
|
||||
<g
|
||||
mask="url(#c)"
|
||||
id="g6"
|
||||
transform="matrix(5.2383867,0,0,5.2383867,-102.88512,-41.907093)">
|
||||
<path
|
||||
fill="url(#d)"
|
||||
d="M 32,27.2 C 32,16.596 40.596,8 51.2,8 h 89.6 C 151.404,8 160,16.596 160,27.2 V 96 H 32 Z"
|
||||
filter="url(#e)"
|
||||
id="path6"
|
||||
style="fill:url(#d)" />
|
||||
</g>
|
||||
<path
|
||||
fill="#ffffff"
|
||||
d="m 291.84303,656.55843 q -32.90754,0 -56.45409,-10.70202 -23.54655,-10.70203 -39.86412,-28.62779 -16.05566,-18.19815 -22.74508,-35.58436 -6.68942,-17.3862 -5.34839,-21.13689 a 10.84346,10.84346 0 0 1 5.34839,-5.88794 l 29.70165,-11.77066 q 3.74021,-1.8701 7.4909,-0.53431 3.74021,1.06863 8.82668,12.30497 5.35363,11.23634 14.98178,23.8137 a 74.908929,74.908929 0 0 0 23.54655,19.52871 q 13.65124,6.95658 33.70902,6.95658 32.37323,0 51.37286,-18.72724 19.26155,-18.72723 19.26155,-47.62217 0,-31.3046 -20.33542,-48.16173 Q 321.00513,473.283 287.55803,473.283 H 259.4698 a 9.9529347,9.9529347 0 0 1 -6.95657,-2.67158 q -2.67158,-2.94397 -2.67682,-6.68942 v -28.62778 q 0,-4.01785 2.67158,-6.68942 a 9.5338637,9.5338637 0 0 1 6.96181,-2.94398 h 24.34279 q 29.96881,0 48.16172,-16.32281 18.19292,-16.32281 18.19292,-42.27378 0,-25.67857 -16.32281,-41.46707 -16.32282,-15.7885 -44.94536,-15.7885 -16.05566,0 -27.82631,5.35364 a 60.241447,60.241447 0 0 0 -20.33542,14.98178 118.91138,118.91138 0 0 0 -14.71463,19.8011 q -6.14986,10.16771 -9.90055,11.23634 -3.7402,0.80148 -7.22373,-1.33579 l -28.09347,-13.64599 q -3.47829,-1.87535 -4.54692,-5.88795 -1.06863,-4.0126 6.42226,-18.72723 7.75805,-14.98179 23.54131,-30.50313 a 110.00612,110.00612 0 0 1 36.92539,-24.08086 q 21.13689,-8.56476 49.23036,-8.55953 52.17433,0 82.67222,27.55392 30.50312,27.29199 30.50312,72.24259 0,31.03744 -14.98178,53.77728 -14.71987,22.7346 -41.73947,32.11131 v 1.06863 q 32.64039,9.62815 51.36762,35.31196 18.99963,25.42189 18.99439,60.73386 0,50.57138 -35.3172,82.94461 -35.30673,32.37323 -92.03846,32.37323 z m 268.46732,-6.1551 q -4.54692,0 -8.03045,-3.47829 a 11.78637,11.78637 0 0 1 -3.20589,-8.29237 V 341.11326 l -60.19954,43.34241 q -3.21637,2.40966 -7.49613,1.60819 a 10.267238,10.267238 0 0 1 -6.41702,-4.0126 l -17.39145,-24.62042 a 10.372006,10.372006 0 0 1 -1.87534,-7.4909 q 0.80147,-4.27452 4.27976,-6.68418 l 106.75308,-76.25519 q 1.34103,-1.06863 2.94398,-1.60295 1.60818,-0.80147 3.74544,-0.80147 h 22.47792 q 4.54692,0 7.22374,3.21113 2.94397,2.9335 2.94397,7.49089 v 363.3345 q 0,4.81932 -3.47829,8.29237 a 10.476773,10.476773 0 0 1 -8.03045,3.47829 z"
|
||||
id="path7"
|
||||
style="stroke-width:5.23839" />
|
||||
<defs
|
||||
id="defs10">
|
||||
<linearGradient
|
||||
id="b"
|
||||
x1="83"
|
||||
x2="83"
|
||||
y1="76"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop
|
||||
stop-color="#4fa0ff"
|
||||
id="stop7" />
|
||||
<stop
|
||||
offset="1"
|
||||
stop-color="#3186ff"
|
||||
id="stop8" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="d"
|
||||
x1="89.059998"
|
||||
x2="89.059998"
|
||||
y1="21.75"
|
||||
y2="96.389999"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop
|
||||
stop-color="#a9a8ff"
|
||||
id="stop9" />
|
||||
<stop
|
||||
offset=".8"
|
||||
stop-color="#3c90ff"
|
||||
id="stop10" />
|
||||
</linearGradient>
|
||||
<filter
|
||||
id="e"
|
||||
width="152"
|
||||
height="112"
|
||||
x="20"
|
||||
y="-4"
|
||||
color-interpolation-filters="sRGB"
|
||||
filterUnits="userSpaceOnUse">
|
||||
<feFlood
|
||||
flood-opacity="0"
|
||||
result="BackgroundImageFix"
|
||||
id="feFlood10" />
|
||||
<feBlend
|
||||
in="SourceGraphic"
|
||||
in2="BackgroundImageFix"
|
||||
result="shape"
|
||||
id="feBlend10"
|
||||
mode="normal" />
|
||||
<feGaussianBlur
|
||||
result="effect1_foregroundBlur_37330_7673"
|
||||
stdDeviation="6"
|
||||
id="feGaussianBlur10" />
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.5 KiB |
Reference in New Issue
Block a user