Merge remote-tracking branch 'origin/main' into codex/clean-calendar-ui
# Conflicts: # src/content/pages/events.html # src/pages/events.astro
This commit is contained in:
commit
4eeed4b1e9
11 changed files with 647 additions and 383 deletions
|
|
@ -9,7 +9,303 @@ import mainHtml from "../content/pages/events.html?raw";
|
|||
bodyClass="archive post-type-archive post-type-archive-tribe_events custom-background metaslider-plugin parabola-image-three caption-light meta-light parabola_triagles parabola-menu-left"
|
||||
pathname="/events.html"
|
||||
canonical="/events.html"
|
||||
robots="noindex, follow, max-image-preview:large"
|
||||
>
|
||||
<Fragment set:html={mainHtml} />
|
||||
<script is:inline slot="scripts">
|
||||
(() => {
|
||||
const calendar = document.querySelector("[data-calendar]");
|
||||
|
||||
if (!calendar) {
|
||||
return;
|
||||
}
|
||||
|
||||
const recurringEvents = [
|
||||
{
|
||||
id: "sunday-service",
|
||||
title: "Sunday Service",
|
||||
startTime: "11:00",
|
||||
endTime: "12:00",
|
||||
location: "19 North Great Georges St., Dublin 1, Ireland",
|
||||
details: "Weekly Sunday Service video upload and community worship.",
|
||||
url: "/services.html",
|
||||
weekday: 0,
|
||||
},
|
||||
];
|
||||
|
||||
const monthTitle = calendar.querySelector("[data-calendar-panel-title]");
|
||||
const grid = calendar.querySelector("[data-calendar-grid]");
|
||||
const list = calendar.querySelector("[data-calendar-list]");
|
||||
const title = calendar.querySelector("#calendar-month-title");
|
||||
const today = new Date();
|
||||
let visibleMonth = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
let selectedDate = toDateKey(today);
|
||||
|
||||
const monthFormatter = new Intl.DateTimeFormat("en-IE", { month: "long", year: "numeric" });
|
||||
const dayFormatter = new Intl.DateTimeFormat("en-IE", { weekday: "long", day: "numeric", month: "long", year: "numeric" });
|
||||
|
||||
function pad(value) {
|
||||
return String(value).padStart(2, "0");
|
||||
}
|
||||
|
||||
function toDateKey(date) {
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
||||
}
|
||||
|
||||
function fromDateKey(key) {
|
||||
const [year, month, day] = key.split("-").map(Number);
|
||||
return new Date(year, month - 1, day);
|
||||
}
|
||||
|
||||
function addDays(date, days) {
|
||||
const next = new Date(date);
|
||||
next.setDate(next.getDate() + days);
|
||||
return next;
|
||||
}
|
||||
|
||||
function isSameMonth(date, month) {
|
||||
return date.getFullYear() === month.getFullYear() && date.getMonth() === month.getMonth();
|
||||
}
|
||||
|
||||
function monthStartGridDate(month) {
|
||||
const first = new Date(month.getFullYear(), month.getMonth(), 1);
|
||||
const mondayIndex = (first.getDay() + 6) % 7;
|
||||
return addDays(first, -mondayIndex);
|
||||
}
|
||||
|
||||
function monthEndGridDate(month) {
|
||||
const last = new Date(month.getFullYear(), month.getMonth() + 1, 0);
|
||||
const sundayIndex = (7 - last.getDay()) % 7;
|
||||
return addDays(last, sundayIndex);
|
||||
}
|
||||
|
||||
function eventsBetween(start, end) {
|
||||
const events = [];
|
||||
|
||||
for (let date = new Date(start); date <= end; date = addDays(date, 1)) {
|
||||
for (const recurringEvent of recurringEvents) {
|
||||
if (date.getDay() !== recurringEvent.weekday) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const dateKey = toDateKey(date);
|
||||
events.push({
|
||||
...recurringEvent,
|
||||
id: `${recurringEvent.id}-${dateKey}`,
|
||||
date: dateKey,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
function formatEventTime(event) {
|
||||
return `${event.startTime} - ${event.endTime}`;
|
||||
}
|
||||
|
||||
function renderGrid() {
|
||||
const start = monthStartGridDate(visibleMonth);
|
||||
const end = monthEndGridDate(visibleMonth);
|
||||
const events = eventsBetween(start, end);
|
||||
const eventsByDate = new Map();
|
||||
|
||||
for (const event of events) {
|
||||
const dayEvents = eventsByDate.get(event.date) || [];
|
||||
dayEvents.push(event);
|
||||
eventsByDate.set(event.date, dayEvents);
|
||||
}
|
||||
|
||||
title.textContent = monthFormatter.format(visibleMonth);
|
||||
grid.innerHTML = "";
|
||||
|
||||
for (let date = new Date(start); date <= end; date = addDays(date, 1)) {
|
||||
const dateKey = toDateKey(date);
|
||||
const dayEvents = eventsByDate.get(dateKey) || [];
|
||||
const cell = document.createElement("button");
|
||||
cell.type = "button";
|
||||
cell.className = "calendar-day";
|
||||
cell.dataset.date = dateKey;
|
||||
cell.setAttribute("role", "gridcell");
|
||||
cell.setAttribute("aria-label", `${dayFormatter.format(date)}${dayEvents.length ? `, ${dayEvents.length} event` : ""}`);
|
||||
|
||||
if (!isSameMonth(date, visibleMonth)) {
|
||||
cell.classList.add("calendar-day-muted");
|
||||
}
|
||||
|
||||
if (dateKey === toDateKey(today)) {
|
||||
cell.classList.add("calendar-day-today");
|
||||
}
|
||||
|
||||
if (dateKey === selectedDate) {
|
||||
cell.classList.add("calendar-day-selected");
|
||||
cell.setAttribute("aria-selected", "true");
|
||||
}
|
||||
|
||||
const number = document.createElement("span");
|
||||
number.className = "calendar-day-number";
|
||||
number.textContent = String(date.getDate());
|
||||
cell.append(number);
|
||||
|
||||
const eventStack = document.createElement("span");
|
||||
eventStack.className = "calendar-day-events";
|
||||
for (const event of dayEvents.slice(0, 2)) {
|
||||
const pill = document.createElement("span");
|
||||
pill.className = "calendar-event-pill";
|
||||
pill.textContent = event.title;
|
||||
eventStack.append(pill);
|
||||
}
|
||||
|
||||
if (dayEvents.length > 2) {
|
||||
const more = document.createElement("span");
|
||||
more.className = "calendar-event-more";
|
||||
more.textContent = `+${dayEvents.length - 2} more`;
|
||||
eventStack.append(more);
|
||||
}
|
||||
|
||||
cell.append(eventStack);
|
||||
cell.addEventListener("click", () => {
|
||||
selectedDate = dateKey;
|
||||
render();
|
||||
});
|
||||
grid.append(cell);
|
||||
}
|
||||
}
|
||||
|
||||
function renderList() {
|
||||
const selected = fromDateKey(selectedDate);
|
||||
const monthStart = new Date(visibleMonth.getFullYear(), visibleMonth.getMonth(), 1);
|
||||
const monthEnd = new Date(visibleMonth.getFullYear(), visibleMonth.getMonth() + 1, 0);
|
||||
const selectedEvents = eventsBetween(selected, selected);
|
||||
const upcomingEvents = eventsBetween(monthStart, monthEnd)
|
||||
.filter((event) => event.date >= toDateKey(today))
|
||||
.slice(0, 8);
|
||||
const events = selectedEvents.length ? selectedEvents : upcomingEvents;
|
||||
|
||||
monthTitle.textContent = selectedEvents.length
|
||||
? dayFormatter.format(selected)
|
||||
: "Upcoming Events";
|
||||
list.innerHTML = "";
|
||||
|
||||
if (!events.length) {
|
||||
const empty = document.createElement("p");
|
||||
empty.className = "calendar-empty";
|
||||
empty.textContent = "No events for this view.";
|
||||
list.append(empty);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const event of events) {
|
||||
const item = document.createElement("article");
|
||||
item.className = "calendar-event-card";
|
||||
const eventDate = fromDateKey(event.date);
|
||||
|
||||
const date = document.createElement("p");
|
||||
date.className = "calendar-event-date";
|
||||
date.textContent = dayFormatter.format(eventDate);
|
||||
|
||||
const heading = document.createElement("h4");
|
||||
heading.textContent = event.title;
|
||||
|
||||
const time = document.createElement("p");
|
||||
time.textContent = formatEventTime(event);
|
||||
|
||||
const location = document.createElement("p");
|
||||
location.textContent = event.location;
|
||||
|
||||
const details = document.createElement("p");
|
||||
details.textContent = event.details;
|
||||
|
||||
const link = document.createElement("a");
|
||||
link.href = event.url;
|
||||
link.textContent = "Sunday Services";
|
||||
|
||||
item.append(date, heading, time, location, details, link);
|
||||
list.append(item);
|
||||
}
|
||||
}
|
||||
|
||||
function icsDate(dateKey, time) {
|
||||
const [year, month, day] = dateKey.split("-");
|
||||
const [hours, minutes] = time.split(":");
|
||||
return `${year}${month}${day}T${hours}${minutes}00`;
|
||||
}
|
||||
|
||||
function escapeIcs(value) {
|
||||
return String(value)
|
||||
.replace(/\\/g, "\\\\")
|
||||
.replace(/;/g, "\\;")
|
||||
.replace(/,/g, "\\,")
|
||||
.replace(/\n/g, "\\n");
|
||||
}
|
||||
|
||||
function exportCalendar() {
|
||||
const start = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
const end = new Date(today.getFullYear() + 1, today.getMonth() + 1, 0);
|
||||
const events = eventsBetween(start, end);
|
||||
const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
|
||||
const siteUrl = "https://familyfed.ie";
|
||||
const lines = [
|
||||
"BEGIN:VCALENDAR",
|
||||
"VERSION:2.0",
|
||||
"PRODID:-//FFWPU Ireland//Events Calendar//EN",
|
||||
"CALSCALE:GREGORIAN",
|
||||
"METHOD:PUBLISH",
|
||||
];
|
||||
|
||||
for (const event of events) {
|
||||
lines.push(
|
||||
"BEGIN:VEVENT",
|
||||
`UID:${event.id}@familyfed.ie`,
|
||||
`DTSTAMP:${stamp}`,
|
||||
`DTSTART:${icsDate(event.date, event.startTime)}`,
|
||||
`DTEND:${icsDate(event.date, event.endTime)}`,
|
||||
`SUMMARY:${escapeIcs(event.title)}`,
|
||||
`DESCRIPTION:${escapeIcs(event.details)}`,
|
||||
`LOCATION:${escapeIcs(event.location)}`,
|
||||
`URL:${siteUrl}${event.url}`,
|
||||
"END:VEVENT",
|
||||
);
|
||||
}
|
||||
|
||||
lines.push("END:VCALENDAR");
|
||||
|
||||
const blob = new Blob([lines.join("\r\n")], { type: "text/calendar;charset=utf-8" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = "familyfed-events.ics";
|
||||
document.body.append(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function render() {
|
||||
renderGrid();
|
||||
renderList();
|
||||
}
|
||||
|
||||
calendar.querySelector("[data-calendar-prev]")?.addEventListener("click", () => {
|
||||
visibleMonth = new Date(visibleMonth.getFullYear(), visibleMonth.getMonth() - 1, 1);
|
||||
selectedDate = toDateKey(new Date(visibleMonth.getFullYear(), visibleMonth.getMonth(), 1));
|
||||
render();
|
||||
});
|
||||
|
||||
calendar.querySelector("[data-calendar-next]")?.addEventListener("click", () => {
|
||||
visibleMonth = new Date(visibleMonth.getFullYear(), visibleMonth.getMonth() + 1, 1);
|
||||
selectedDate = toDateKey(new Date(visibleMonth.getFullYear(), visibleMonth.getMonth(), 1));
|
||||
render();
|
||||
});
|
||||
|
||||
calendar.querySelector("[data-calendar-today]")?.addEventListener("click", () => {
|
||||
visibleMonth = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
selectedDate = toDateKey(today);
|
||||
render();
|
||||
});
|
||||
|
||||
calendar.querySelector("[data-calendar-export]")?.addEventListener("click", exportCalendar);
|
||||
|
||||
render();
|
||||
})();
|
||||
</script>
|
||||
</SiteLayout>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue