Refactor delle view-modes, readme and better dockerfile

compressed-week-view
Antonio De Lucreziis 2 years ago
parent 23c1510455
commit 01fa4b16e2

@ -2,7 +2,8 @@ FROM node:18
WORKDIR /app WORKDIR /app
COPY . ./ COPY package.json ./
RUN npm install RUN npm install
COPY . .
CMD ["npm", "run", "build"] CMD ["npm", "run", "build"]

@ -0,0 +1,16 @@
# Orario
Sito per visualizzare l'orario delle lezioni per i vari anni di corso.
## Usage
You need to have installed `node` and `npm` (or `pnpm`). To setup the project just run `npm install`.
### Development
To start the development server run `npm run dev`.
### Production
To build the ViteJS project run `npm run build`, for deployment a `.env` files can be used to set the `BASE_URL` variable.

@ -1,81 +1,40 @@
[ [
"ALGEBRA 1",
"ANALISI ARMONICA - ANALISI ARMONICA/a", "ANALISI ARMONICA - ANALISI ARMONICA/a",
"ANALISI MATEMATICA 1",
"ANALISI MATEMATICA 2",
"ANALISI MATEMATICA 3", "ANALISI MATEMATICA 3",
"ANALISI NUMERICA CON LABORATORIO - ANALISI NUMERICA",
"ARITMETICA",
"ASPETTI MATEMATICI NELLA COMPUTAZIONE QUANTISTICA", "ASPETTI MATEMATICI NELLA COMPUTAZIONE QUANTISTICA",
"CALCOLO SCIENTIFICO", "CALCOLO SCIENTIFICO",
"COMBINATORIA ALGEBRICA", "COMBINATORIA ALGEBRICA",
"DETERMINAZIONE ORBITALE", "DETERMINAZIONE ORBITALE",
"DINAMICA DEL SISTEMA SOLARE", "DINAMICA DEL SISTEMA SOLARE",
"DINAMICA DEL SISTEMA SOLARE",
"ELEMENTI DI GEOMETRIA ALGEBRICA",
"ELEMENTI DI GEOMETRIA ALGEBRICA",
"ELEMENTI DI GEOMETRIA ALGEBRICA",
"ELEMENTI DI GEOMETRIA ALGEBRICA", "ELEMENTI DI GEOMETRIA ALGEBRICA",
"ELEMENTI DI TOPOLOGIA ALGEBRICA", "ELEMENTI DI TOPOLOGIA ALGEBRICA",
"ELEMENTI DI TOPOLOGIA ALGEBRICA",
"ELEMENTI DI TOPOLOGIA ALGEBRICA",
"ELEMENTI DI TOPOLOGIA ALGEBRICA",
"FISICA II",
"FISICA II",
"FISICA II",
"FISICA II",
"FISICA II",
"FISICA II",
"FISICA II", "FISICA II",
"FISICA II", "FONDAMENTI DI PROGRAMMAZIONE CON LABORATORIO - FONDAMENTI DI PROGRAMMAZIONE ",
"GEOMETRIA E TOPOLOGIA DIFFERENZIALE", "GEOMETRIA 1",
"GEOMETRIA E TOPOLOGIA DIFFERENZIALE", "GEOMETRIA 2 - GEOMETRIA 2 A",
"GEOMETRIA E TOPOLOGIA DIFFERENZIALE",
"GEOMETRIA E TOPOLOGIA DIFFERENZIALE",
"GEOMETRIA E TOPOLOGIA DIFFERENZIALE",
"GEOMETRIA E TOPOLOGIA DIFFERENZIALE", "GEOMETRIA E TOPOLOGIA DIFFERENZIALE",
"INGLESE SCIENTIFICO",
"ISTITUZIONI DI ALGEBRA", "ISTITUZIONI DI ALGEBRA",
"ISTITUZIONI DI ALGEBRA",
"ISTITUZIONI DI ANALISI MATEMATICA",
"ISTITUZIONI DI ANALISI MATEMATICA",
"ISTITUZIONI DI ANALISI MATEMATICA", "ISTITUZIONI DI ANALISI MATEMATICA",
"ISTITUZIONI DI DIDATTICA DELLA MATEMATICA", "ISTITUZIONI DI DIDATTICA DELLA MATEMATICA",
"ISTITUZIONI DI DIDATTICA DELLA MATEMATICA",
"ISTITUZIONI DI DIDATTICA DELLA MATEMATICA",
"ISTITUZIONI DI FISICA MATEMATICA",
"ISTITUZIONI DI FISICA MATEMATICA", "ISTITUZIONI DI FISICA MATEMATICA",
"LABORATORIO COMPUTAZIONALE", "LABORATORIO COMPUTAZIONALE",
"LABORATORIO DI INTRODUZIONE ALLA MATEMATICA COMPUTAZIONALE",
"LOGICA MATEMATICA", "LOGICA MATEMATICA",
"LOGICA MATEMATICA",
"LOGICA MATEMATICA",
"LOGICA MATEMATICA",
"MECCANICA SUPERIORE - MECCANICA SUPERIORE/a",
"MECCANICA SUPERIORE - MECCANICA SUPERIORE/a", "MECCANICA SUPERIORE - MECCANICA SUPERIORE/a",
"METODI NUMERICI PER CATENE DI MARKOV - METODI NUMERICI PER CATENE DI MARKOV/a", "METODI NUMERICI PER CATENE DI MARKOV - METODI NUMERICI PER CATENE DI MARKOV/a",
"METODI NUMERICI PER CATENE DI MARKOV - METODI NUMERICI PER CATENE DI MARKOV/a",
"METODI NUMERICI PER LA GRAFICA - METODI NUMERICI PER LA GRAFICA/a",
"METODI NUMERICI PER LA GRAFICA - METODI NUMERICI PER LA GRAFICA/a", "METODI NUMERICI PER LA GRAFICA - METODI NUMERICI PER LA GRAFICA/a",
"PROBABILITÀ", "PROBABILITÀ",
"PROBABILITÀ",
"PROBABILITÀ",
"PROBABILITÀ",
"PROBABILITÀ",
"PROBABILITÀ",
"RICERCA OPERATIVA",
"RICERCA OPERATIVA",
"RICERCA OPERATIVA", "RICERCA OPERATIVA",
"RICERCA OPERATIVA",
"RICERCA OPERATIVA",
"RICERCA OPERATIVA",
"SISTEMI DINAMICI",
"SISTEMI DINAMICI",
"SISTEMI DINAMICI",
"SISTEMI DINAMICI", "SISTEMI DINAMICI",
"STORIA DELLA MATEMATICA", "STORIA DELLA MATEMATICA",
"STORIA DELLA MATEMATICA",
"STORIA DELLA MATEMATICA",
"STORIA DELLA MATEMATICA",
"TECNOLOGIE PER LA DIDATTICA", "TECNOLOGIE PER LA DIDATTICA",
"TECNOLOGIE PER LA DIDATTICA",
"TEORIA ANALITICA DEI NUMERI A - TEORIA ANALITICA DEI NUMERI A/a",
"TEORIA ANALITICA DEI NUMERI A - TEORIA ANALITICA DEI NUMERI A/a", "TEORIA ANALITICA DEI NUMERI A - TEORIA ANALITICA DEI NUMERI A/a",
"TEORIA E METODI DELL'OTTIMIZZAZIONE", "TEORIA E METODI DELL'OTTIMIZZAZIONE",
"TEORIA E METODI DELL'OTTIMIZZAZIONE",
"TOPOLOGIA DIFFERENZIALE - TOPOLOGIA DIFFERENZIALE/a",
"TOPOLOGIA DIFFERENZIALE - TOPOLOGIA DIFFERENZIALE/a" "TOPOLOGIA DIFFERENZIALE - TOPOLOGIA DIFFERENZIALE/a"
] ]

61819
data.json

File diff suppressed because it is too large Load Diff

@ -1,25 +0,0 @@
// import { createContext } from 'preact'
// import { toChildArray } from 'preact'
// import { useContext, useState } from 'preact/hooks'
// const AnimationContext = createContext()
// export const useAnimation = animation => {
// const [styles, setStyles] = useState({})
// const start = () => {
// setStyles({ animation })
// }
// return [
// start,
// {
// styles,
// onAnimationEnd: e => {
// console.log('animation end')
// },
// },
// ]
// }
// export const Animation = ({ animation, children }) => {}

@ -1,28 +1,18 @@
import { useRef, useState } from 'preact/hooks'
import { Icon } from './Icon.jsx' import { Icon } from './Icon.jsx'
let ids = 0
function generateId() {
return 'combo-id-' + ids++
}
export const ComboBox = ({ options, value, setValue }) => { export const ComboBox = ({ options, value, setValue }) => {
const [uid] = useState(() => generateId())
const selectRef = useRef()
return ( return (
<div class="input-combo"> <div class="input-combo">
<select ref={selectRef} id={uid} onInput={e => setValue(e.target.value)}> <select onInput={e => setValue(e.target.value)}>
{options.map(option => ( {options.map(option => (
<option value={option.value} selected={option.value === value}> <option value={option.value} selected={option.value === value}>
{option.label} {option.label}
</option> </option>
))} ))}
</select> </select>
<label class="icon" for={uid}> <div class="icon">
<Icon name="expand_more" /> <Icon name="expand_more" />
</label> </div>
</div> </div>
) )
} }

@ -1,368 +1,15 @@
// import { Course } from './view/Course.jsx'
// Modes import { WorkWeek } from './view/WorkWeek.jsx'
// import { WorkWeekTranspose } from './view/WorkWeekTranspose.jsx'
import { differenceInMinutes, format, startOfDay } from 'date-fns' export const MODE_COURSE = 'course'
export const MODE_WORKWEEK = 'work-week'
import _ from 'lodash' export const MODE_WORKWEEK_TRANSPOSE = 'work-week-transpose'
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 &ndash; 11:00
</div>
<div class="label" style={{ 'grid-column': '4 / span 2' }}>
11:00 &ndash; 13:00
</div>
<div class="label" style={{ 'grid-column': '7 / span 2' }}>
14:00 &ndash; 16:00
</div>
<div class="label" style={{ 'grid-column': '9 / span 2' }}>
16:00 &ndash; 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 &ndash; 11:00
</div>
<div
class="block"
style={{
'--start': 11 - 7,
'--size': 2,
}}
>
11:00 &ndash; 13:00
</div>
<div
class="block skip-border"
style={{
'--start': 14 - 7,
'--size': 2,
}}
>
14:00 &ndash; 16:00
</div>
<div
class="block"
style={{
'--start': 16 - 7,
'--size': 2,
}}
>
16:00 &ndash; 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')} &ndash;{' '}
{format(course.end, 'H:mm')} {course.aula}
</div>
))}
</div>
</div>
))}
</div>
</div>
)
}
//
// EventsView
//
const viewModeMap = { const viewModeMap = {
'work-week-h': WorkWeekView, [MODE_COURSE]: Course,
'work-week-v': WorkWeekVerticalView, [MODE_WORKWEEK]: WorkWeek,
'course': CourseView, [MODE_WORKWEEK_TRANSPOSE]: WorkWeekTranspose,
} }
export const EventsView = ({ mode, ...viewProps }) => { export const EventsView = ({ mode, ...viewProps }) => {

@ -1,4 +1,5 @@
import { ComboBox } from './ComboBox.jsx' import { ComboBox } from './ComboBox.jsx'
import { MODE_COURSE, MODE_WORKWEEK } from './EventsView.jsx'
import { Help } from './Help.jsx' import { Help } from './Help.jsx'
import { Icon } from './Icon.jsx' import { Icon } from './Icon.jsx'
@ -39,12 +40,8 @@ export const HamburgerMenu = ({ onClose, mode, setMode, source, setSource, theme
value={mode} value={mode}
setValue={setMode} setValue={setMode}
options={[ options={[
{ value: 'course', label: 'Corsi' }, { value: MODE_COURSE, label: 'Corsi' },
{ value: 'work-week-v', label: 'Settimana' }, { value: MODE_WORKWEEK, label: 'Settimana' },
// {
// value: 'work-week-h',
// label: 'Settimana (Trasposto)',
// },
]} ]}
/> />
</div> </div>

@ -1,4 +1,5 @@
import { CompoundButton } from './CompoundButton.jsx' import { CompoundButton } from './CompoundButton.jsx'
import { MODE_COURSE, MODE_WORKWEEK } from './EventsView.jsx'
import { Icon } from './Icon.jsx' import { Icon } from './Icon.jsx'
export const Toolbar = ({ export const Toolbar = ({
@ -40,17 +41,8 @@ export const Toolbar = ({
<div class="item option"> <div class="item option">
<CompoundButton <CompoundButton
options={[ options={[
{ value: 'course', label: 'Corsi' }, { value: MODE_COURSE, label: 'Corsi' },
{ value: 'work-week-v', label: 'Settimana' }, { value: MODE_WORKWEEK, label: 'Settimana' },
// {
// value: 'work-week-h',
// label: (
// <>
// <Icon name="warning" />
// Settimana<sup>T</sup>
// </>
// ),
// },
]} ]}
value={mode} value={mode}
setValue={setMode} setValue={setMode}

@ -0,0 +1,70 @@
import { format } from 'date-fns'
import _ from 'lodash'
import { useEffect, useRef, useState } from 'preact/hooks'
import { normalizeCourseName, WEEK_DAYS } from '../../utils.jsx'
export const Course = ({ 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')} &ndash;{' '}
{format(course.end, 'H:mm')} {course.aula}
</div>
))}
</div>
</div>
))}
</div>
</div>
)
}

@ -0,0 +1,160 @@
import { useEffect, useRef, useState } from 'preact/hooks'
import _ from 'lodash'
import { differenceInMinutes, startOfDay } from 'date-fns'
import { hashString, normalizeCourseName, WEEK_DAYS } from '../../utils.jsx'
import { layoutIntervals } from '../../interval-layout.js'
export const WorkWeek = ({ 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 &ndash; 11:00
</div>
<div
class="block"
style={{
'--start': 11 - 7,
'--size': 2,
}}
>
11:00 &ndash; 13:00
</div>
<div
class="block skip-border"
style={{
'--start': 14 - 7,
'--size': 2,
}}
>
14:00 &ndash; 16:00
</div>
<div
class="block"
style={{
'--start': 16 - 7,
'--size': 2,
}}
>
16:00 &ndash; 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>
)
}

@ -0,0 +1,104 @@
import { useEffect, useRef } from 'preact/hooks'
import _ from 'lodash'
import { differenceInMinutes, startOfDay } from 'date-fns'
import { hashString, normalizeCourseName, WEEK_DAYS } from '../../utils.jsx'
import { layoutIntervals } from '../../interval-layout.js'
export const WorkWeekTranspose = ({ 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 &ndash; 11:00
</div>
<div class="label" style={{ 'grid-column': '4 / span 2' }}>
11:00 &ndash; 13:00
</div>
<div class="label" style={{ 'grid-column': '7 / span 2' }}>
14:00 &ndash; 16:00
</div>
<div class="label" style={{ 'grid-column': '9 / span 2' }}>
16:00 &ndash; 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>
)
}

@ -68,13 +68,20 @@ async function loadEventi(ids) {
} }
const App = ({}) => { const App = ({}) => {
// Data Sources
const [source, setSource] = useState('magistrale') const [source, setSource] = useState('magistrale')
const [eventi, setEventi] = useState([]) const [eventi, setEventi] = useState([])
// View Modes
const [mode, setMode] = useState('course')
// Selection
const [selectedCourses, setSelectedCourses] = useState([]) const [selectedCourses, setSelectedCourses] = useState([])
const [hideOtherCourses, setHideOtherCourses] = useState(false)
useEffect(() => { // Menus
setSelectedCourses([]) const [helpVisible, setHelpVisible] = useState(false)
}, [source]) const [showMobileMenu, setShowMobileMenu] = useState(false)
useEffect(async () => { useEffect(async () => {
const eventi = await loadEventi(CALENDAR_IDS[source]) const eventi = await loadEventi(CALENDAR_IDS[source])
@ -84,25 +91,24 @@ const App = ({}) => {
setEventi(eventi) setEventi(eventi)
}, [source]) }, [source])
const [helpVisible, setHelpVisible] = useState(false) useEffect(() => {
setSelectedCourses(selectedCourses => {
const nextCoursesGroup = new Set(eventi.map(e => e.nome))
const [mode, setMode] = useState('course') // Here the filter is on "selection" because most of the times |selection| <= |nextCoursesGroup|
const intersectionSize = selectedCourses.filter(nome =>
nextCoursesGroup.has(nome)
).length
const [hideOtherCourses, setHideOtherCourses] = useState(false) return intersectionSize === 0 ? [] : selectedCourses
})
}, [eventi, selectedCourses])
// TODO: Should wrap in "useEffect"? // TODO: Should wrap in "useEffect"?
if (selectedCourses.length === 0) { if (selectedCourses.length === 0) {
setHideOtherCourses(false) setHideOtherCourses(false)
} }
const [showMobileMenu, setShowMobileMenu] = useState(false)
useEffect(async () => {
const eventi = await loadEventi(CALENDAR_IDS[source])
// window.dataBuffer[source] = eventi
setEventi(eventi)
}, [source])
const [theme, setTheme] = useState( const [theme, setTheme] = useState(
localStorage.getItem('theme') ?? localStorage.getItem('theme') ??
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')

@ -1,3 +1,51 @@
// Calendar
export const WEEK_DAYS = [
'Domenica',
'Lunedì',
'Martedì',
'Mercoledì',
'Giovedì',
'Venerdì',
'Sabato',
]
// Hashing
export 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)
}
// Courses
export 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())
}
// JSX
export const withClasses = classes => export const withClasses = classes =>
Array.isArray(classes) Array.isArray(classes)
? classes.filter(e => !!e).join(' ') ? classes.filter(e => !!e).join(' ')

Loading…
Cancel
Save