From 7dfb505af037c8850e6194d6539a557036e4982d Mon Sep 17 00:00:00 2001 From: Jonas Braathen Date: Sun, 12 May 2024 16:06:49 +0200 Subject: [PATCH] organize events by months, weeks and days and print a calendar view --- web/src/components/events/EventContainer.tsx | 50 ++++++++---- .../events/eventContainer.module.scss | 17 +++- web/src/lib/date.ts | 16 +++- web/src/lib/event.ts | 78 +++++++++++++++++-- 4 files changed, 139 insertions(+), 22 deletions(-) diff --git a/web/src/components/events/EventContainer.tsx b/web/src/components/events/EventContainer.tsx index 2130bed..78c09ee 100644 --- a/web/src/components/events/EventContainer.tsx +++ b/web/src/components/events/EventContainer.tsx @@ -3,19 +3,20 @@ import { useState } from "react"; import { EventItem } from "./EventItem"; import { EventFilter } from "./EventFilter"; -import { EventFragment, getSingularEvents } from "@/lib/event"; import { - toLocalTime, - formatDate, - commonDateFormat, - isTodayOrFuture, -} from "@/lib/date"; + EventFragment, + getSingularEvents, + organizeEventsByDate, +} from "@/lib/event"; +import { isTodayOrFuture } from "@/lib/date"; import styles from "./eventContainer.module.scss"; +import { formatDate, formatYearMonth } from "@/lib/date"; +import { getYear, parse } from "date-fns"; type EventContainerMode = "list" | "calendar"; export const EventContainer = ({ events }: { events: EventFragment[] }) => { - const [mode, setMode] = useState("calendar"); + const [mode, setMode] = useState("list"); return (
@@ -43,15 +44,38 @@ const EventList = ({ events }: { events: EventFragment[] }) => { }; const EventCalendar = ({ events }: { events: EventFragment[] }) => { - const singularEvents = getSingularEvents(events).filter((x) => - x.occurrence?.start && isTodayOrFuture(x.occurrence.start) + const futureSingularEvents = getSingularEvents(events).filter( + (x) => x.occurrence?.start && isTodayOrFuture(x.occurrence.start) ); + const eventsByDate = organizeEventsByDate(futureSingularEvents); + return ( -
    - {singularEvents.map((event) => ( - +
    + {Object.keys(eventsByDate).map((yearMonth) => ( +
    +

    {formatYearMonth(yearMonth)}

    + {Object.keys(eventsByDate[yearMonth]).map((week) => ( +
    + {Object.keys(eventsByDate[yearMonth][week]).map((day) => ( +
    +

    + {formatDate( + parse(day, "yyyy-MM-dd", new Date()), + "eeee dd.MM." + )} +

    +
      + {eventsByDate[yearMonth][week][day].map((event) => ( + + ))} +
    +
    + ))} +
    + ))} +
    ))} -
+
); }; diff --git a/web/src/components/events/eventContainer.module.scss b/web/src/components/events/eventContainer.module.scss index a13ca88..0c75b75 100644 --- a/web/src/components/events/eventContainer.module.scss +++ b/web/src/components/events/eventContainer.module.scss @@ -21,4 +21,19 @@ button { border-radius: 0; } -} \ No newline at end of file +} + +.calendarYearMonth { + h2:first-letter { + text-transform: capitalize + } +} + +.calendarWeek {} + +.calendarDay { + h3:first-letter { + text-transform: capitalize + } +} + diff --git a/web/src/lib/date.ts b/web/src/lib/date.ts index 046fcb2..120e0f5 100644 --- a/web/src/lib/date.ts +++ b/web/src/lib/date.ts @@ -1,5 +1,6 @@ -import { isToday, isAfter, parseISO } from "date-fns"; -import { toZonedTime, format } from "date-fns-tz"; +import { isToday, isAfter, parse } from "date-fns"; +import { nb } from "date-fns/locale"; +import { toZonedTime, format} from "date-fns-tz"; const timeZone = "Europe/Oslo"; @@ -10,7 +11,16 @@ export function toLocalTime(date: Date | string | number) { } export function formatDate(date: Date | string | number, formatStr: string) { - return format(toLocalTime(date), formatStr, { timeZone }); + return format(toLocalTime(date), formatStr, { timeZone, locale: nb }); +} + +export function formatYearMonth(yearMonth: string) { + // full name of month if year is current year, otherwise name of month + year + const parsed = parse(yearMonth, "yyyy-MM", new Date()); + if (parsed.getFullYear === new Date().getFullYear) { + return formatDate(parsed, "MMMM"); + } + return formatDate(parsed, "MMMM yyyy"); } export function isTodayOrFuture( diff --git a/web/src/lib/event.ts b/web/src/lib/event.ts index 22fa680..e0b588c 100644 --- a/web/src/lib/event.ts +++ b/web/src/lib/event.ts @@ -1,7 +1,23 @@ +import { + compareAsc, + getYear, + getMonth, + getWeek, + getDate, + endOfWeek, + startOfWeek, + eachDayOfInterval, + addWeeks, +} from "date-fns"; +import { toLocalTime, formatDate } from "./date"; import { graphql } from "@/gql"; import { EventFragment, EventOccurrence } from "@/gql/graphql"; -export type { EventFragment } from "@/gql/graphql" +export type { EventFragment } from "@/gql/graphql"; + +export type SingularEvent = EventFragment & { + occurrence: EventOccurrence; +}; const EventFragmentDefinition = graphql(` fragment Event on EventPage { @@ -55,10 +71,6 @@ export const allEventsQuery = graphql(` } `); -export type SingularEvent = EventFragment & { - occurrence: EventOccurrence; -}; - export function getSingularEvents(events: EventFragment[]) { return events .map((event) => { @@ -77,3 +89,59 @@ export function getSingularEvents(events: EventFragment[]) { function isDefined(val: T | undefined | null): val is T { return val !== undefined && val !== null; } + +interface EventsByDate { + [yearMonth: string]: { + [week: number]: { + [day: number]: SingularEvent[]; + }; + }; +} + +export function organizeEventsByDate(events: SingularEvent[]): EventsByDate { + const sortedEvents = events.sort((a, b) => + compareAsc(new Date(a.occurrence.start), new Date(b.occurrence.start)) + ); + + const eventsByDate: EventsByDate = {}; + + const minDate = new Date(sortedEvents[0]?.occurrence.start); + const maxDate = new Date( + sortedEvents[sortedEvents.length - 1]?.occurrence.start + ); + + let currentDate = startOfWeek(minDate, { weekStartsOn: 1 }); + while (currentDate <= maxDate) { + const yearMonth = formatDate(currentDate, "yyyy-MM"); + const week = formatDate(currentDate, "w"); + const daysOfWeek = eachDayOfInterval({ + start: currentDate, + end: endOfWeek(currentDate, { weekStartsOn: 1 }), + }); + + if (!eventsByDate[yearMonth]) { + eventsByDate[yearMonth] = {}; + } + if (!eventsByDate[yearMonth][week]) { + eventsByDate[yearMonth][week] = {}; + } + daysOfWeek.forEach((day) => { + const formattedDay = formatDate(day, "yyyy-MM-dd"); + if (!eventsByDate[yearMonth][week][formattedDay]) { + eventsByDate[yearMonth][week][formattedDay] = []; + } + }); + + currentDate = addWeeks(currentDate, 1); + } + + sortedEvents.forEach((event) => { + const start = toLocalTime(event.occurrence.start); + const yearMonth = formatDate(start, "yyyy-MM"); + const week = formatDate(start, "w"); + const day = formatDate(start, "yyyy-MM-dd"); + eventsByDate[yearMonth][week][day].push(event); + }); + + return eventsByDate; +}