|
|
|
import _ from 'lodash'
|
|
|
|
import { render } from 'preact'
|
|
|
|
import { useEffect, useState } from 'preact/hooks'
|
|
|
|
import { ToolOverlay } from './components/CourseVisibility.jsx'
|
|
|
|
|
|
|
|
import { EventsView, MODE_COURSES, MODE_SCHEDULE } from './components/EventsView.jsx'
|
|
|
|
import { HamburgerMenu } from './components/HamburgerMenu.jsx'
|
|
|
|
import { Help } from './components/Help.jsx'
|
|
|
|
import { Icon } from './components/Icon.jsx'
|
|
|
|
import { Popup } from './components/Popup.jsx'
|
|
|
|
import { Toolbar } from './components/Toolbar.jsx'
|
|
|
|
import { OptionBar } from './components/OptionBar.jsx'
|
|
|
|
import { prettyAulaName, prettyProfName, usePersistentState } from './utils.jsx'
|
|
|
|
|
|
|
|
window._ = _
|
|
|
|
window.dataBuffer = {}
|
|
|
|
|
|
|
|
const CALENDAR_IDS = {
|
|
|
|
'anno-1': ['64a7c1c651f079001d52e9c8'],
|
|
|
|
'anno-2': ['6308e2dc09352a0208fefdd9'],
|
|
|
|
'anno-3': ['6308e42a1df5cb026699ced4'],
|
|
|
|
'magistrale': ['64a7c7091ab813002c5d9ede'],
|
|
|
|
'tutti': [
|
|
|
|
'64a7c1c651f079001d52e9c8',
|
|
|
|
'6308e2dc09352a0208fefdd9',
|
|
|
|
'6308e42a1df5cb026699ced4',
|
|
|
|
'64a7c7091ab813002c5d9ede',
|
|
|
|
],
|
|
|
|
}
|
|
|
|
|
|
|
|
const TIMETABLE_IDS = {
|
|
|
|
'anno-1': '64a7c1c651f079001d52e9c8',
|
|
|
|
'anno-2': '6308e2dc09352a0208fefdd9',
|
|
|
|
'anno-3': '6308e42a1df5cb026699ced4',
|
|
|
|
'magistrale': '64a7c7091ab813002c5d9ede',
|
|
|
|
}
|
|
|
|
|
|
|
|
const DEFAULT_DATE_RANGE = {
|
|
|
|
from: '2023-10-02T00:00:00.000Z',
|
|
|
|
to: '2023-10-07T00:00:00.000Z',
|
|
|
|
}
|
|
|
|
|
|
|
|
const DATE_RANGES = {
|
|
|
|
'64a7c1c651f079001d52e9c8': DEFAULT_DATE_RANGE,
|
|
|
|
'6308e2dc09352a0208fefdd9': DEFAULT_DATE_RANGE,
|
|
|
|
'6308e42a1df5cb026699ced4': DEFAULT_DATE_RANGE,
|
|
|
|
'64a7c7091ab813002c5d9ede': DEFAULT_DATE_RANGE,
|
|
|
|
}
|
|
|
|
|
|
|
|
function specialEventPatches(eventi) {
|
|
|
|
// Il laboratorio del primo anno in realtà è in due canali separati
|
|
|
|
let i = 1
|
|
|
|
eventi.forEach(evento => {
|
|
|
|
if (evento.nome === 'LABORATORIO DI INTRODUZIONE ALLA MATEMATICA COMPUTAZIONALE') {
|
|
|
|
evento.nome += ` (${i})`
|
|
|
|
i++
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
return eventi
|
|
|
|
}
|
|
|
|
|
|
|
|
function formatEvents(timetable) {
|
|
|
|
return timetable.map(
|
|
|
|
({ nome, dataInizio, dataFine, docenti, aule }) => {
|
|
|
|
return {
|
|
|
|
id: nome,
|
|
|
|
name: _.split(nome, '-', 1)[0].trim(),
|
|
|
|
start: new Date(dataInizio),
|
|
|
|
end: new Date(dataFine),
|
|
|
|
docenti: docenti.map(({ nome, cognome }) => prettyProfName(nome, cognome)),
|
|
|
|
aule: aule.map(aula => prettyAulaName(aula.codice)),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
async function loadCalendari() {
|
|
|
|
async function req(id) {
|
|
|
|
// Almost directly copy-pasted from Chrome Dev Tools
|
|
|
|
const req = await fetch(
|
|
|
|
'https://apache.prod.up.cineca.it/api/Impegni/getImpegniCalendarioPubblico',
|
|
|
|
{
|
|
|
|
headers: {
|
|
|
|
'content-type': 'application/json;charset=UTF-8',
|
|
|
|
'sec-fetch-dest': 'empty',
|
|
|
|
'sec-fetch-mode': 'cors',
|
|
|
|
'sec-fetch-site': 'same-site',
|
|
|
|
},
|
|
|
|
body: JSON.stringify({
|
|
|
|
mostraImpegniAnnullati: true,
|
|
|
|
mostraIndisponibilitaTotali: false,
|
|
|
|
linkCalendarioId: id,
|
|
|
|
clienteId: '628de8b9b63679f193b87046',
|
|
|
|
pianificazioneTemplate: false,
|
|
|
|
dataInizio: DATE_RANGES[id].from,
|
|
|
|
dataFine: DATE_RANGES[id].to,
|
|
|
|
}),
|
|
|
|
method: 'POST',
|
|
|
|
mode: 'cors',
|
|
|
|
credentials: 'omit',
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
return await req.json()
|
|
|
|
}
|
|
|
|
|
|
|
|
const requests = [
|
|
|
|
req(TIMETABLE_IDS['anno-1']),
|
|
|
|
req(TIMETABLE_IDS['anno-2']),
|
|
|
|
req(TIMETABLE_IDS['anno-3']),
|
|
|
|
req(TIMETABLE_IDS['magistrale']),
|
|
|
|
]
|
|
|
|
const results = await Promise.all(requests)
|
|
|
|
const timetablesRaw = results.map(timetable => specialEventPatches(_.uniqBy(timetable, 'id')))
|
|
|
|
const allRaw = specialEventPatches(_.uniqBy(_.concat(...results), 'id'))
|
|
|
|
|
|
|
|
return {
|
|
|
|
'anno-1': formatEvents(timetablesRaw[0]),
|
|
|
|
'anno-2': formatEvents(timetablesRaw[1]),
|
|
|
|
'anno-3': formatEvents(timetablesRaw[2]),
|
|
|
|
'magistrale': formatEvents(timetablesRaw[3]),
|
|
|
|
'tutti': formatEvents(allRaw)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const App = ({}) => {
|
|
|
|
// Data Sources
|
|
|
|
const [source, setSource] = usePersistentState('orario.source', 'magistrale')
|
|
|
|
const [timetables, setTimetables] = useState(null)
|
|
|
|
useEffect(async () => {
|
|
|
|
setTimetables(await loadCalendari())
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
// View Modes
|
|
|
|
const [mode, setMode] = usePersistentState('orario.mode', MODE_COURSES)
|
|
|
|
|
|
|
|
// Selection
|
|
|
|
const [selectedCourses, setSelectedCourses] = usePersistentState('orario.selection', [])
|
|
|
|
|
|
|
|
// Menus
|
|
|
|
const [helpVisible, setHelpVisible] = useState(false)
|
|
|
|
const [showMobileMenu, setShowMobileMenu] = useState(false)
|
|
|
|
|
|
|
|
// const groupIds = new Set(eventi.map(e => e.nome))
|
|
|
|
const toolOverlayVisible = selectedCourses.length > 0
|
|
|
|
|
|
|
|
const [theme, setTheme] = usePersistentState(
|
|
|
|
'orario.theme',
|
|
|
|
window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
|
|
|
)
|
|
|
|
|
|
|
|
document.body.classList.toggle('dark-mode', theme === 'dark')
|
|
|
|
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
<Toolbar
|
|
|
|
{...{
|
|
|
|
mode,
|
|
|
|
setMode,
|
|
|
|
source,
|
|
|
|
setSource,
|
|
|
|
onShowMenu: () => setShowMobileMenu(true),
|
|
|
|
onHelp: () => setHelpVisible(true),
|
|
|
|
theme,
|
|
|
|
setTheme,
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
{mode === MODE_COURSES && (
|
|
|
|
<OptionBar
|
|
|
|
{...{
|
|
|
|
mode,
|
|
|
|
setMode,
|
|
|
|
source,
|
|
|
|
setSource,
|
|
|
|
onHelp: () => setHelpVisible(true),
|
|
|
|
}}orizzontale
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
{timetables && (
|
|
|
|
<EventsView
|
|
|
|
mode={mode}
|
|
|
|
selection={selectedCourses}
|
|
|
|
setSelection={setSelectedCourses}
|
|
|
|
start={new Date(2022, 10, 3)}
|
|
|
|
source={source}
|
|
|
|
timetables={timetables}
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
{toolOverlayVisible && (
|
|
|
|
<ToolOverlay
|
|
|
|
mode={mode}
|
|
|
|
toggleMode={() => setMode(mode === MODE_COURSES ? MODE_SCHEDULE : MODE_COURSES)}
|
|
|
|
onClose={() => {
|
|
|
|
setSelectedCourses([])
|
|
|
|
setMode(MODE_COURSES)
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
{showMobileMenu && (
|
|
|
|
<HamburgerMenu
|
|
|
|
{...{
|
|
|
|
mode,
|
|
|
|
setMode,
|
|
|
|
source,
|
|
|
|
setSource,
|
|
|
|
theme,
|
|
|
|
setTheme,
|
|
|
|
onClose: () => {
|
|
|
|
setShowMobileMenu(false)
|
|
|
|
},
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
{helpVisible && (
|
|
|
|
<Popup
|
|
|
|
title={
|
|
|
|
<>
|
|
|
|
<Icon name="info" /> Guida
|
|
|
|
</>
|
|
|
|
}
|
|
|
|
onClose={() => setHelpVisible(false)}
|
|
|
|
>
|
|
|
|
<Help />
|
|
|
|
</Popup>
|
|
|
|
)}
|
|
|
|
</>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
render(<App />, document.body)
|