Skip to content
This repository has been archived by the owner on Jan 3, 2025. It is now read-only.

Commit

Permalink
Schedule calendar view (#452)
Browse files Browse the repository at this point in the history
* Add fullcalendar dependencies

* Add initial (flawed, incomplete) calendar view

* Set calendar start/end times

* Prevent height change on room de-select

* Fix start date change bug

* lint
  • Loading branch information
kr-matthews authored Feb 7, 2024
1 parent 510b05a commit caff9cb
Show file tree
Hide file tree
Showing 6 changed files with 187 additions and 9 deletions.
3 changes: 3 additions & 0 deletions Frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
},
"dependencies": {
"@dinero.js/currencies": "2.0.0-alpha.14",
"@fullcalendar/luxon3": "^6.1.10",
"@fullcalendar/react": "^6.1.10",
"@fullcalendar/timegrid": "^6.1.10",
"@stripe/react-stripe-js": "2.4.0",
"@stripe/stripe-js": "2.4.0",
"@tanstack/react-query": "4.33.0",
Expand Down
56 changes: 56 additions & 0 deletions Frontend/src/lib/activities.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { Activity } from '@wca/helpers'
import { DateTime } from 'luxon'
import {
addEndBufferWithinDay,
doesRangeCrossMidnight,
roundBackToHour,
todayWithTime,
} from './dates'

export const earliestWithLongestTieBreaker = (a: Activity, b: Activity) => {
if (a.startTime < b.startTime) {
Expand Down Expand Up @@ -47,3 +54,52 @@ export const getActivityEvent = (activity: Activity) => {
export const getActivityRoundId = (activity: Activity) => {
return activity.activityCode.split('-').slice(0, 2).join('-')
}

export const earliestTimeOfDayWithBuffer = (
activities: Activity[],
timeZone: string
) => {
if (activities.length === 0) return undefined

const doesAnyCrossMidnight = activities.some(({ startTime, endTime }) =>
doesRangeCrossMidnight(startTime, endTime, timeZone)
)
if (doesAnyCrossMidnight) {
return '00:00:00'
}

const startTimes = activities.map(({ startTime }) =>
todayWithTime(startTime, timeZone)
)
return roundBackToHour(DateTime.min(...startTimes)).toISOTime({
suppressMilliseconds: true,
includeOffset: false,
})
}

export const latestTimeOfDayWithBuffer = (
activities: Activity[],
timeZone: string
) => {
if (activities.length === 0) return undefined

const doesAnyCrossMidnight = activities.some(({ startTime, endTime }) =>
doesRangeCrossMidnight(startTime, endTime, timeZone)
)
if (doesAnyCrossMidnight) {
return '24:00:00'
}

const endTimes = activities.map(({ endTime }) =>
todayWithTime(endTime, timeZone)
)
const result = addEndBufferWithinDay(DateTime.max(...endTimes)).toISOTime({
suppressMilliseconds: true,
includeOffset: false,
})

if (result === '00:00:00') {
return '24:00:00'
}
return result
}
32 changes: 32 additions & 0 deletions Frontend/src/lib/dates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,35 @@ export const getLongDate = (date: string, timeZone: string) => {
timeZone,
})
}

export const doesRangeCrossMidnight = (
start: string,
end: string,
timeZone: string
) => {
const luxonStart = DateTime.fromISO(start).setZone(timeZone)
const luxonEnd = DateTime.fromISO(end).setZone(timeZone)
return luxonStart.day !== luxonEnd.day
}

export const todayWithTime = (date: string, timeZone: string) => {
const luxonDate = DateTime.fromISO(date).setZone(timeZone)
return DateTime.utc().set({
hour: luxonDate.hour,
minute: luxonDate.minute,
second: luxonDate.second,
millisecond: luxonDate.millisecond,
})
}

export const roundBackToHour = (date: DateTime) => {
return date.set({ minute: 0, second: 0, millisecond: 0 })
}

export const addEndBufferWithinDay = (date: DateTime) => {
const buffered = date.plus({ minutes: 10 })
if (buffered.day !== date.day) {
return date
}
return buffered
}
94 changes: 90 additions & 4 deletions Frontend/src/pages/schedule/CalendarView.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,94 @@
import luxonPlugin from '@fullcalendar/luxon3'
import FullCalendar from '@fullcalendar/react'
import timeGridPlugin from '@fullcalendar/timegrid'
import React from 'react'
import {
earliestTimeOfDayWithBuffer,
getActivityEvent,
latestTimeOfDayWithBuffer,
} from '../../lib/activities'
import { getTextColor } from '../../lib/colors'

// TODO
// based on monolith code: https://github.com/thewca/worldcubeassociation.org/blob/0882a86cf5d83c3a0dbc667a59be05ce8845c3e4/WcaOnRails/app/webpacker/components/EditSchedule/EditActivities/index.js

// { dates, timeZone, venuesShown, events }
export default function CalendarView() {
return <p>Calendar view does not exist yet.</p>
// TODO: make column date consistent with table view dates
// TODO: add tooltip or popup on events for more details
// TODO: set calendar's locale
// TODO: table has 24h, calendar has 12h - make consistent (fixed by setting locale?)
// TODO: indicate that event split across days are such?
// TODO: add add-to-calendar functionality?

export default function CalendarView({
dates,
timeZone,
activeVenues,
activeRooms,
activeEvents,
}) {
const activeEventIds = activeEvents.map(({ id }) => id)
const fcActivities = activeRooms.flatMap((room) =>
room.activities
.filter((activity) =>
['other', ...activeEventIds].includes(getActivityEvent(activity))
)
.map((activity) => ({
title: activity.name,
start: activity.startTime,
end: activity.endTime,
backgroundColor: room.color,
textColor: getTextColor(room.color),
}))
)

// independent of which activities are visible,
// to prevent calendar height jumping around
const activeVenuesActivities = activeVenues.flatMap((venue) =>
venue.rooms.flatMap((room) => room.activities)
)
const calendarStart =
earliestTimeOfDayWithBuffer(activeVenuesActivities, timeZone) ?? '00:00:00'
const calendarEnd =
latestTimeOfDayWithBuffer(activeVenuesActivities, timeZone) ?? '00:00:00'

const onEventClick = () => {
/* TODO */
}

return (
<>
<FullCalendar
// plugins for the basic FullCalendar implementation.
// - timeGridPlugin: Display days as vertical grid
// - luxonPlugin: Support timezones
plugins={[timeGridPlugin, luxonPlugin]}
// define our "own" view
initialView="agendaForComp"
views={{
agendaForComp: {
type: 'timeGrid',
// specify start/end rather than duration/initialDate, since
// dates may change when changing time zone
visibleRange: { start: dates[0], end: dates[dates.length - 1] },
},
}}
// by default, FC offers support for separate "whole-day" events
allDaySlot={false}
// by default, FC would show a "skip to next day" toolbar
headerToolbar={false}
slotMinTime={calendarStart}
slotMaxTime={calendarEnd}
slotDuration="00:30:00"
height="auto"
// localization settings
// TODO get locale
// locale={calendarLocale}
timeZone={timeZone}
events={fcActivities}
eventClick={onEventClick}
/>
{fcActivities.length === 0 && (
<em>No activities for the selected rooms/events.</em>
)}
</>
)
}
5 changes: 3 additions & 2 deletions Frontend/src/pages/schedule/Schedule.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -215,14 +215,15 @@ export default function Schedule({ wcif }) {
<CalendarView
dates={activeDates}
timeZone={activeTimeZone}
venuesShown={activeVenues}
activeVenues={activeVenues}
activeRooms={activeRooms}
activeEvents={activeEvents}
/>
) : (
<TableView
dates={activeDates}
timeZone={activeTimeZone}
rooms={activeRooms}
activeRooms={activeRooms}
activeEvents={activeEvents}
activeVenueOrNull={activeVenueOrNull}
/>
Expand Down
6 changes: 3 additions & 3 deletions Frontend/src/pages/schedule/TableView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ import AddToCalendar from './AddToCalendar'
export default function TableView({
dates,
timeZone,
rooms,
activeRooms,
activeEvents,
activeVenueOrNull,
}) {
const rounds = activeEvents.flatMap((event) => event.rounds)

const [isExpanded, setIsExpanded] = useState(false)

const sortedActivities = rooms
const sortedActivities = activeRooms
.flatMap((room) => room.activities)
.sort(earliestWithLongestTieBreaker)

Expand Down Expand Up @@ -57,7 +57,7 @@ export default function TableView({
timeZone={timeZone}
groupedActivities={groupedActivitiesForDay}
rounds={rounds}
rooms={rooms}
rooms={activeRooms}
isExpanded={isExpanded}
activeVenueOrNull={activeVenueOrNull}
/>
Expand Down

0 comments on commit caff9cb

Please sign in to comment.