forked from phc/orario
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
377 lines
15 KiB
JavaScript
377 lines
15 KiB
JavaScript
//
|
|
// Modes
|
|
//
|
|
|
|
import { differenceInMinutes, format, startOfDay } from 'date-fns'
|
|
|
|
import _ from 'lodash'
|
|
import { useEffect, useRef, useState } from 'preact/hooks'
|
|
import { layoutIntervals } from '../interval-layout.js'
|
|
|
|
const WEEK_DAYS = ['Domenica', 'Lunedì', 'Martedì', 'Mercoledì', 'Giovedì', 'Venerdì', 'Sabato']
|
|
|
|
function hashString(str, seed = 0) {
|
|
let h1 = 0xdeadbeef ^ seed,
|
|
h2 = 0x41c6ce57 ^ seed
|
|
for (let i = 0, ch; i < str.length; i++) {
|
|
ch = str.charCodeAt(i)
|
|
h1 = Math.imul(h1 ^ ch, 2654435761)
|
|
h2 = Math.imul(h2 ^ ch, 1597334677)
|
|
}
|
|
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909)
|
|
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909)
|
|
return 4294967296 * (2097151 & h2) + (h1 >>> 0)
|
|
}
|
|
|
|
function normalizeCourseName(name) {
|
|
return name
|
|
.split(' ')
|
|
.map(word => word.toLowerCase())
|
|
.map(word => {
|
|
if (word.trim().length === 0) return word
|
|
|
|
return /(^del|^nel|^di$|^dei$|^con$|^alla$|^per$|^e$|^la$)/.test(word)
|
|
? word
|
|
: word[0].toUpperCase() + word.slice(1)
|
|
})
|
|
.join(' ')
|
|
.replaceAll('Ii', 'II')
|
|
.replaceAll('Iii', 'III')
|
|
.replaceAll(/'(.)/g, ({}, letter) => "'" + letter.toUpperCase())
|
|
}
|
|
|
|
const WorkWeekView = ({ events }) => {
|
|
const eventsByWeekday = _.groupBy(events, event => event.start.getDay())
|
|
|
|
// For each weekday compute the interval layout
|
|
const rowLayouts = _.mapValues(eventsByWeekday, events =>
|
|
layoutIntervals(
|
|
events.map(e => ({
|
|
start: differenceInMinutes(e.start, startOfDay(e.start)),
|
|
end: differenceInMinutes(e.end, startOfDay(e.start)),
|
|
data: e,
|
|
}))
|
|
)
|
|
)
|
|
|
|
return (
|
|
<div class="work-week-h-view">
|
|
<div class="week">
|
|
{WEEK_DAYS.slice(1, 6).map((label, index) => (
|
|
<div class="day" style={{ '--size': rowLayouts[index + 1]?.length ?? 0 }}>
|
|
{label}
|
|
</div>
|
|
))}
|
|
</div>
|
|
<div class="events">
|
|
<div class="header">
|
|
<div class="label" style={{ 'grid-column': '2 / span 2' }}>
|
|
9:00 – 11:00
|
|
</div>
|
|
<div class="label" style={{ 'grid-column': '4 / span 2' }}>
|
|
11:00 – 13:00
|
|
</div>
|
|
<div class="label" style={{ 'grid-column': '7 / span 2' }}>
|
|
14:00 – 16:00
|
|
</div>
|
|
<div class="label" style={{ 'grid-column': '9 / span 2' }}>
|
|
16:00 – 18:00
|
|
</div>
|
|
</div>
|
|
<div class="days">
|
|
{Object.values(rowLayouts).map(layout => (
|
|
<div class="day" style={{ '--size': layout.length }}>
|
|
{layout.map((events, rowIndex) =>
|
|
events.map(event => {
|
|
const Local = () => {
|
|
const eventRef = useRef()
|
|
|
|
const updateMinWidth = () => {
|
|
eventRef.current.style.minWidth =
|
|
eventRef.current.offsetWidth + 'px'
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (eventRef.current) {
|
|
setTimeout(updateMinWidth, 100)
|
|
window.addEventListener('resize', updateMinWidth)
|
|
|
|
return () => {
|
|
window.removeEventListener(
|
|
'resize',
|
|
updateMinWidth
|
|
)
|
|
}
|
|
}
|
|
}, [eventRef.current])
|
|
|
|
return (
|
|
<div
|
|
class="event"
|
|
style={{
|
|
'--row': rowIndex + 1,
|
|
'--start': event.start / 60 - 7,
|
|
'--size': (event.end - event.start) / 60,
|
|
'--hue':
|
|
(Math.abs(
|
|
hashString('seed3' + event.data.name)
|
|
) %
|
|
360) +
|
|
'deg',
|
|
}}
|
|
ref={eventRef}
|
|
>
|
|
{normalizeCourseName(event.data.name)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return <Local />
|
|
})
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const WorkWeekVerticalView = ({ events, selection, setSelection, hideOtherCourses }) => {
|
|
const selectionSet = new Set(selection)
|
|
|
|
// const base = {
|
|
// 1: [],
|
|
// 2: [],
|
|
// 3: [],
|
|
// 4: [],
|
|
// 5: [],
|
|
// }
|
|
|
|
const eventsByWeekday = _.groupBy(
|
|
!hideOtherCourses ? events : events.filter(e => selectionSet.has(e.name)),
|
|
event => event.start.getDay()
|
|
)
|
|
|
|
// const dayIntervalLayout = _.mapValues(Object.assign(base, eventsByWeekday), events =>
|
|
const dayIntervalLayout = _.mapValues(eventsByWeekday, events =>
|
|
layoutIntervals(
|
|
events.map(e => ({
|
|
start: differenceInMinutes(e.start, startOfDay(e.start)),
|
|
end: differenceInMinutes(e.end, startOfDay(e.start)),
|
|
data: e,
|
|
}))
|
|
)
|
|
)
|
|
|
|
const [currentlyHovered, setCurrentlyHovered] = useState(null)
|
|
const element = useRef()
|
|
|
|
useEffect(() => {
|
|
if (element.current) {
|
|
const l = e => {
|
|
const $event = e.target.closest('.event')
|
|
if ($event) {
|
|
setCurrentlyHovered($event.dataset.eventId)
|
|
} else {
|
|
setCurrentlyHovered(null)
|
|
}
|
|
}
|
|
|
|
element.current.addEventListener('mousemove', l)
|
|
element.current.addEventListener('mouseleave', l)
|
|
|
|
return () => {
|
|
element.current.removeEventListener('mousemove', l)
|
|
element.current.removeEventListener('mouseleave', l)
|
|
}
|
|
}
|
|
}, [element.current])
|
|
|
|
return (
|
|
<div class="work-week-v-view" ref={element}>
|
|
<div class="pivot"></div>
|
|
<div class="left-header">
|
|
<div class="blocks">
|
|
<div
|
|
class="block skip-border"
|
|
style={{
|
|
'--start': 9 - 7,
|
|
'--size': 2,
|
|
}}
|
|
>
|
|
9:00 – 11:00
|
|
</div>
|
|
<div
|
|
class="block"
|
|
style={{
|
|
'--start': 11 - 7,
|
|
'--size': 2,
|
|
}}
|
|
>
|
|
11:00 – 13:00
|
|
</div>
|
|
<div
|
|
class="block skip-border"
|
|
style={{
|
|
'--start': 14 - 7,
|
|
'--size': 2,
|
|
}}
|
|
>
|
|
14:00 – 16:00
|
|
</div>
|
|
<div
|
|
class="block"
|
|
style={{
|
|
'--start': 16 - 7,
|
|
'--size': 2,
|
|
}}
|
|
>
|
|
16:00 – 18:00
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{Object.entries(dayIntervalLayout).map(([index, layout]) => (
|
|
<div class="day" style={{ '--size': Math.max(1, layout.length) }}>
|
|
<div class="top-header">{WEEK_DAYS[parseInt(index)]}</div>
|
|
<div class="events">
|
|
{layout.map((events, stackIndex) => (
|
|
<>
|
|
{events.map(event => (
|
|
<div
|
|
class={
|
|
'event' +
|
|
(currentlyHovered === event.data.name
|
|
? ' highlight'
|
|
: '') +
|
|
(selectionSet.has(event.data.name) ? ' selected' : '')
|
|
}
|
|
data-event-id={event.data.name}
|
|
style={{
|
|
'--start': event.start / 60 - 7,
|
|
'--stack': stackIndex + 1,
|
|
'--size': (event.end - event.start) / 60,
|
|
'--hue':
|
|
(Math.abs(hashString('seed3' + event.data.name)) %
|
|
360) +
|
|
'deg',
|
|
}}
|
|
onClick={() => {
|
|
if (!selectionSet.has(event.data.name))
|
|
setSelection([...selection, event.data.name])
|
|
else
|
|
setSelection(
|
|
selection.filter(
|
|
name => event.data.name !== name
|
|
)
|
|
)
|
|
}}
|
|
>
|
|
<div class="title">
|
|
{normalizeCourseName(event.data.name)}
|
|
</div>
|
|
<div class="aula">{event.data.aula}</div>
|
|
</div>
|
|
))}
|
|
</>
|
|
))}
|
|
|
|
{/* Grid Tracks */}
|
|
{[1, 3, 5].map(i => (
|
|
<div class="grid-line-h" style={{ '--track': i }}></div>
|
|
))}
|
|
{[6, 8, 10].map(i => (
|
|
<div class="grid-line-h" style={{ '--track': i }}></div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const CourseView = ({ events, selection, setSelection, hideOtherCourses }) => {
|
|
const selectionSet = new Set(selection)
|
|
|
|
const visibleEvents = !hideOtherCourses ? events : events.filter(e => selectionSet.has(e.name))
|
|
|
|
const eventsByCourse = _.groupBy(_.sortBy(visibleEvents, 'name'), 'name')
|
|
|
|
const [currentlyHovered, setCurrentlyHovered] = useState(null)
|
|
const element = useRef()
|
|
|
|
useEffect(() => {
|
|
if (element.current) {
|
|
const l = e => {
|
|
const $course = e.target.closest('.course')
|
|
if ($course) {
|
|
setCurrentlyHovered($course.dataset.courseId)
|
|
} else {
|
|
setCurrentlyHovered(null)
|
|
}
|
|
}
|
|
|
|
element.current.addEventListener('mousemove', l)
|
|
element.current.addEventListener('mouseleave', l)
|
|
|
|
return () => {
|
|
element.current.removeEventListener('mousemove', l)
|
|
element.current.removeEventListener('mouseleave', l)
|
|
}
|
|
}
|
|
}, [element.current])
|
|
|
|
return (
|
|
<div class="course-view" ref={element}>
|
|
<div class="wrap-container">
|
|
{Object.entries(eventsByCourse).map(([name, courseEvents]) => (
|
|
<div
|
|
class={
|
|
'course' +
|
|
(currentlyHovered === name ? ' highlight' : '') +
|
|
(selectionSet.has(name) ? ' selected' : '')
|
|
}
|
|
data-course-id={name}
|
|
onClick={() => {
|
|
if (!selectionSet.has(name)) setSelection([...selection, name])
|
|
else setSelection(selection.filter(n => n !== name))
|
|
}}
|
|
>
|
|
<div class="title">{normalizeCourseName(name)}</div>
|
|
<div class="docenti">{courseEvents[0].docenti.join(', ')}</div>
|
|
<div class="events">
|
|
{courseEvents.map(course => (
|
|
<div>
|
|
{WEEK_DAYS[course.start.getDay()]}{' '}
|
|
{format(course.start, 'H:mm')} –
|
|
{format(course.end, 'H:mm')} {course.aula}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
//
|
|
// EventsView
|
|
//
|
|
|
|
const viewModeMap = {
|
|
'work-week-h': WorkWeekView,
|
|
'work-week-v': WorkWeekVerticalView,
|
|
'course': CourseView,
|
|
}
|
|
|
|
export const EventsView = ({ mode, ...viewProps }) => {
|
|
const Mode = viewModeMap[mode]
|
|
|
|
return (
|
|
<div class="events-view">
|
|
<Mode {...viewProps} />
|
|
</div>
|
|
)
|
|
}
|