Skip to content

Commit

Permalink
Merge pull request #67 from reinhrst/event-list-views
Browse files Browse the repository at this point in the history
Use javascript to make a calendar
  • Loading branch information
reinhrst authored May 21, 2023
2 parents 70de52a + 320a5f4 commit c296ad3
Show file tree
Hide file tree
Showing 15 changed files with 503 additions and 128 deletions.
258 changes: 258 additions & 0 deletions assets/js/eventViews.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
const TZREGEX =
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(([+-])(\d{2}):?(\d{2})|Z)$/;

if (!(globalThis as any).eventViewsRun) {
(globalThis as any).eventViewsRun = true;

function createEventViews() {
document.querySelectorAll(".eventDates").forEach((el) => {
fixTabs(el as HTMLDivElement);
createEventViewsForDiv(el as HTMLDivElement);
});
}

interface BaseEvent {
element: HTMLElement;
startdt: number;
startdtTzOffset: number;
enddt: number;
enddtTzOffset: number;
cancelled: boolean;
comment: string | undefined;
}
interface ShortEvent extends BaseEvent {}

interface LongEvent extends BaseEvent {
title: string;
description: string;
url: string;
image: string | undefined;
}

function fixTabs(parentElement: HTMLDivElement) {
const tabs = [
...parentElement.querySelectorAll(".grid-tab"),
] as HTMLElement[];
const all_classes = tabs.map((el) => el.classList[1]);
tabs.forEach((tab) => {
tab.addEventListener("click", () => {
all_classes.forEach((klass) => parentElement.classList.remove(klass));
parentElement.classList.add(tab.classList[1]);
});
});
}

function getTZOffsetMS(datestring: string): number {
const [_, full_tz, sign, hours, minutes] = datestring.match(TZREGEX)!;
if (full_tz == "Z") {
return 0;
}
const isign = sign == "+" ? 1 : -1;
const total_minutes = parseInt(hours) * 60 + parseInt(minutes);
return isign * total_minutes * 60 * 1000;
}

function createEventViewsForDiv(parentElement: HTMLDivElement) {
const events: (ShortEvent | LongEvent)[] = [
...parentElement.querySelectorAll(".h-event"),
].map((el) => {
const element = el as HTMLElement;
const startDateTimeString = (
el.querySelector(".dt-start") as HTMLTimeElement
).dateTime;
const startdt = Date.parse(startDateTimeString);
const startdtTzOffset = getTZOffsetMS(startDateTimeString);
const endDateTimeString = (el.querySelector(".dt-end") as HTMLTimeElement)
.dateTime;
const enddt = Date.parse(endDateTimeString);
const enddtTzOffset = getTZOffsetMS(endDateTimeString);
const cancelled = !!el.querySelector(".cancelled");
const comment = (el.querySelector(".comment") as HTMLElement | null)?.innerText;
if (el.classList.contains("long")) {
const title = (el.querySelector(".p-name") as HTMLElement).innerText;
const description = (el.querySelector(".p-description") as HTMLElement).innerText;
const url = (el.querySelector(".u-url") as HTMLLinkElement).href;
const image = (el.querySelector(".p-image") as HTMLImageElement | null)
?.src;
return {
element,
startdt,
startdtTzOffset,
enddt,
enddtTzOffset,
cancelled,
comment,
title,
description,
url,
image,
};
} else {
return {
element,
startdt,
startdtTzOffset,
enddt,
enddtTzOffset,
cancelled,
comment,
};
}
});

const upcoming = parentElement.querySelector(".grid-body.upcoming")!;
const past = parentElement.querySelector(".grid-body.past")!;

// Items are already put in the right position at generation time, this JS moves them to correct position
// for view-time.
const now = Date.now();
events
.sort((a, b) => a.startdt - b.startdt)
.forEach((event) => {
event.element.classList.remove("now");
event.element.classList.remove("next24hrs");
if (event.enddt < now) {
past.prepend(event.element); // past events are in reverse order
} else {
upcoming.appendChild(event.element);
if (event.startdt < now) {
event.element.classList.add("now");
} else if (event.startdt < now + ONE_DAY_MS) {
event.element.classList.add("next24hrs");
}
}
});
const nowDate = new Date(now);
setCalendar(parentElement, events, nowDate.getUTCFullYear(), nowDate.getMonth());
}

function setCalendar(
parentElement: HTMLElement,
events: (ShortEvent | LongEvent)[],
year: number,
month: number,
) {
const date = new Date(year, month, 1);
const calendar = parentElement.querySelector(".grid-body.calendar")!;
[...calendar.childNodes].forEach((el) => el.remove());
calendar.appendChild(
createCalendar(date.getFullYear(), date.getMonth(), events)
);
calendar.querySelector(".previous-month")?.addEventListener(
"click", () => setCalendar(parentElement, events, year, month - 1))
calendar.querySelector(".next-month")?.addEventListener(
"click", () => setCalendar(parentElement, events, year, month + 1))
}

function getISOWeekNumber(date: Date): number {
var newdate = new Date(date.getTime());
newdate.setHours(0, 0, 0, 0);
// Thursday in current week decides the year.
newdate.setDate(newdate.getDate() + 3 - (newdate.getDay() + 6) % 7);
// January 4 is always in week 1.
var week1 = new Date(newdate.getFullYear(), 0, 4);
// Adjust to Thursday in week 1 and count number of weeks from date to week1.
return 1 + Math.round(((newdate.getTime() - week1.getTime()) / 86400000
- 3 + (week1.getDay() + 6) % 7) / 7);
}

function createEl(props: {name?: string, classes?: string[]} & ({text?: string} | {children?: HTMLElement[]})): HTMLElement {
const el = document.createElement(props.name ?? "div")
if (props.classes) {
props.classes.forEach(klass => el.classList.add(klass));
}
if ("text" in props && props.text !== undefined) {
el.innerText = props.text;
}
if ("children" in props) {
(props.children ?? []).forEach(child => el.appendChild(child))
}
return el
}

function eventIsOnDate(date: Date, event: ShortEvent | LongEvent): boolean {
// eventDate UTC time is now siteLocal event time
const eventDate = new Date(event.startdt + event.startdtTzOffset)
return eventDate.getUTCFullYear() === date.getFullYear()
&& eventDate.getUTCMonth() === date.getMonth()
&& eventDate.getUTCDate() === date.getDate();
}

function toUTCTimeString(date: Date): string {
const hours = date.getUTCHours()
const minutes = date.getUTCMinutes()
return hours.toString().padStart(2, "0") + ":" + minutes.toString().padStart(2, "0")
}

function createCalendar(
year: number,
month: number,
events: (ShortEvent | LongEvent)[]
): HTMLDivElement {
const first_day_of_month = new Date(year, month, 1);
const calendar = createEl({classes: ["month-calendar"], children: [
createEl({classes: ["header"], children: [
createEl({name: "span", text: "\u25C0", classes: ["previous-month"]}),
createEl({name: "span", text: first_day_of_month.toLocaleString(undefined, {month: "long", year: "numeric"}), classes: ["month-name"]}),
createEl({name: "span", text: "\u25B6", classes: ["next-month"]}),
]}),
createEl({classes: ["weeknumber"], text: "wk"})
]}) as HTMLDivElement;
for (let i = 1; i < 8; i++) {
const date = new Date(2023, /* May */ 4, i); // this month starts on Monday
calendar.appendChild(createEl({
classes: ["weekday-name"],
text: date.toLocaleDateString(undefined, { weekday: "short" })
}))
}
const today = new Date()
const isToday = (date: Date): boolean => date.toDateString() == today.toDateString()
const first_day_to_show = new Date(year, month, 1 - (first_day_of_month.getDay() + 6) % 7);
let nrrows = 0;
for (let i = 0; ; i++) {
let date = new Date(
first_day_to_show.getFullYear(), first_day_to_show.getMonth(), first_day_to_show.getDate() + i);
let eventsThisDay = events.filter(event => eventIsOnDate(date, event))
if ((i % 7) == 0) {
nrrows = i / 7;
if (i > 7 && date.getMonth() != month) {
break;
}
calendar.appendChild(createEl({
classes: ["weeknumber"],
text: `${getISOWeekNumber(date)}`,
}))
}

const day = createEl({classes: ["calendar-day"]})
if (date.getMonth() != month) {
day.classList.add("other-month")
}
if (isToday(date)) {
day.classList.add("today")
}
const daynr = createEl({
classes: ["daynumber"],
text: `${date.getDate()}`,
})
day.appendChild(daynr)
eventsThisDay.forEach(event => {
const long = "title" in event;
const slStartTime = new Date(event.startdt + event.startdtTzOffset)
const slEndTime = new Date(event.enddt + event.enddtTzOffset)
const title = long ? event.title : `- ${toUTCTimeString(slEndTime)}`;
const el = createEl({name: (long ? "a" : "div"), classes: ["event"], text: `${toUTCTimeString(slStartTime)} ${title}`})
if (long) {
;(el as HTMLLinkElement).href = event.url;
}
day.appendChild(el)
})
calendar.appendChild(day);
}
calendar.style.setProperty("--nr-week-rows", `${nrrows}`)
return calendar;
}

window.addEventListener("load", createEventViews);
}
103 changes: 103 additions & 0 deletions assets/scss/custom.scss
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,109 @@ img[src="/images/google-translate.png"] {
}

.eventDates {
border: 1px solid Hsl(0 0% 20%);
margin: 1em 0 1.5em;
display: grid;
grid-template-columns: repeat(3, Min(10em, 30%)) auto;
grid-template-rows: 2em auto;
.grid-tab {
user-select: none;
text-align: center;
cursor: pointer;
border-right: 1px solid Hsl(0 0% 20%);
background-color: Hsl(0 0% 80%);
color: Hsl(0 0% 20%);
}
&.upcoming > .grid-tab.upcoming,
&.past > .grid-tab.past,
&.calendar > .grid-tab.calendar,
{
background-color: unset;
padding-top: .2em;
font-weight: bold;
border-bottom: none;
}

&.long .grid-body {
max-height: 80vh;
}

&.long.calendar .grid-body, &.calendar .grid-body {
max-height: unset;
}

.grid-body {
grid-area: 2 / 1 / 2 / 5;
max-height: 10em;
min-height: 3em;
overflow-y: scroll;
display: none;
border-top: 1px solid Hsl(0 0% 20%);
padding: .7em;
}
&.upcoming > .grid-body.upcoming,
&.past > .grid-body.past,
&.calendar > .grid-body.calendar,
{
display: block;
}
.grid-body.calendar .month-calendar {
border: .5px solid Hsl(0 0% 20%);
display: grid;
grid-template-columns: 1.5em repeat(7, minmax(0, 1fr));
grid-template-rows: 1.5em 1.5em repeat(var(--nr-week-rows), 5em);
> * {
border: .5px solid Hsl(0 0% 20%);
&.weekday-name {
text-align: center;
}
&.header {
grid-area: 1 / 1 / 1 / 9;
text-align: center;
.month-name {
display: inline-block;
width: 12em;
}
.previous-month, .next-month {
user-select: none;
padding: 0 .8em;
cursor: pointer;
}
}
&.weeknumber {
font-size: .6em;
text-align: center;
color: Hsl(0, 0%, 60%);
}
&.calendar-day {
&.today:not(other-month) {
background-color: Hsl(40, 60%, 80%);
}
&.other-month {
background-color: Hsl(0, 0%, 80%);
}
.daynumber {
color: black;
font-size: .8em;
}
&.other-month .event {
background-color: Hsl(0, 10%, 45%);
}
.event {
width: 100%;
background-color: Hsl(0, 80%, 50%);
color: white;
font-size: .7em;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
border: 1.5px solid white;
border-width: 0 1.5px;
display: block;
}
}
}
}
text-align: left;
.h-event {
time {
Expand Down
11 changes: 11 additions & 0 deletions config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ openingDesc = "Otwarte całą dobę dla osób z kluczem."
openingHours = "Jeśli nie masz klucza [zapytaj na Discord](https://hs3.pl/join) czy ktoś jest w środku."
phone = "+48 661 691 133"

[languages.pl.params.events]
upcoming = "Nadchodzące"
past = "Przeszłe"
calendar = "Kalendarz"

[languages.en]
title = "Hackerspace Trójmiasto (Three-city)"
contentDir = 'content/en'
Expand All @@ -46,6 +51,12 @@ openingDesc = "Open 24/7 for members with a key."
openingHours = "For members without key and non-members: [ask on Discord](https://hs3.pl/join) if someone is there (in English or Polish)."
phone = "+48 661 691 133"

[languages.en.params.events]
upcoming = "Upcoming"
past = "Past"
calendar = "Calendar"


[params.map]
APIkey = "AIzaSyC9rV6yesIygoVKTD6QLf_iCa9eiIIHqZ0"
latitude = "54.3896193"
Expand Down
Loading

0 comments on commit c296ad3

Please sign in to comment.