Author SHA1 Message Date
Fran314 3ddf325c03 prototipo eventi custom 2023-10-16 10:34:51 +02:00
Fran314 973acad17a small fix on dynamic viewport height 2023-10-09 15:36:32 +02:00
16 changed files with 389 additions and 154 deletions
-9
View File
@@ -1,9 +0,0 @@
{
"printWidth": 90,
"singleQuote": true,
"quoteProps": "consistent",
"tabWidth": 4,
"semi": false,
"arrowParens": "avoid",
"proseWrap": "always"
}
BIN
View File
Binary file not shown.
+40
View File
@@ -0,0 +1,40 @@
[
"ALGEBRA 1",
"ANALISI ARMONICA - ANALISI ARMONICA/a",
"ANALISI MATEMATICA 1",
"ANALISI MATEMATICA 2",
"ANALISI MATEMATICA 3",
"ANALISI NUMERICA CON LABORATORIO - ANALISI NUMERICA",
"ARITMETICA",
"ASPETTI MATEMATICI NELLA COMPUTAZIONE QUANTISTICA",
"CALCOLO SCIENTIFICO",
"COMBINATORIA ALGEBRICA",
"DETERMINAZIONE ORBITALE",
"DINAMICA DEL SISTEMA SOLARE",
"ELEMENTI DI GEOMETRIA ALGEBRICA",
"ELEMENTI DI TOPOLOGIA ALGEBRICA",
"FISICA II",
"FONDAMENTI DI PROGRAMMAZIONE CON LABORATORIO - FONDAMENTI DI PROGRAMMAZIONE ",
"GEOMETRIA 1",
"GEOMETRIA 2 - GEOMETRIA 2 A",
"GEOMETRIA E TOPOLOGIA DIFFERENZIALE",
"INGLESE SCIENTIFICO",
"ISTITUZIONI DI ALGEBRA",
"ISTITUZIONI DI ANALISI MATEMATICA",
"ISTITUZIONI DI DIDATTICA DELLA MATEMATICA",
"ISTITUZIONI DI FISICA MATEMATICA",
"LABORATORIO COMPUTAZIONALE",
"LABORATORIO DI INTRODUZIONE ALLA MATEMATICA COMPUTAZIONALE",
"LOGICA MATEMATICA",
"MECCANICA SUPERIORE - MECCANICA SUPERIORE/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",
"PROBABILITÀ",
"RICERCA OPERATIVA",
"SISTEMI DINAMICI",
"STORIA DELLA MATEMATICA",
"TECNOLOGIE PER LA DIDATTICA",
"TEORIA ANALITICA DEI NUMERI A - TEORIA ANALITICA DEI NUMERI A/a",
"TEORIA E METODI DELL'OTTIMIZZAZIONE",
"TOPOLOGIA DIFFERENZIALE - TOPOLOGIA DIFFERENZIALE/a"
]
+2 -1
View File
@@ -16,7 +16,8 @@
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"preact": "^10.10.6", "preact": "^10.10.6",
"sass": "^1.54.8", "sass": "^1.54.8",
"vite": "^3.0.9" "vite": "^3.0.9",
"yaml": "^2.3.2"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.18.13", "@babel/core": "^7.18.13",
+8
View File
@@ -23,6 +23,9 @@ dependencies:
vite: vite:
specifier: ^3.0.9 specifier: ^3.0.9
version: 3.0.9(sass@1.54.8) version: 3.0.9(sass@1.54.8)
yaml:
specifier: ^2.3.2
version: 2.3.2
devDependencies: devDependencies:
'@babel/core': '@babel/core':
@@ -937,3 +940,8 @@ packages:
sass: 1.54.8 sass: 1.54.8
optionalDependencies: optionalDependencies:
fsevents: 2.3.2 fsevents: 2.3.2
/yaml@2.3.2:
resolution: {integrity: sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==}
engines: {node: '>= 14'}
dev: false
+1 -8
View File
@@ -1,19 +1,12 @@
import { useRef } from 'preact/hooks' import { useRef } from 'preact/hooks'
import { Icon } from './Icon.jsx' import { Icon } from './Icon.jsx'
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent)
export const DatePicker = ({ date, setDate }) => { export const DatePicker = ({ date, setDate }) => {
const input = useRef() const input = useRef()
const [year, month, day] = date.split('T')[0].split('-') const [year, month, day] = date.split('T')[0].split('-')
return ( return (
<div <div class="date-picker" onClick={() => input.current.showPicker()}>
class="date-picker"
onClick={() =>
isSafari ? input.current.focus() : input.current.showPicker()
}
>
<input <input
ref={input} ref={input}
type="date" type="date"
+1
View File
@@ -25,6 +25,7 @@ const viewModeMap = {
export const EventsView = ({ mode, source, ...viewProps }) => { export const EventsView = ({ mode, source, ...viewProps }) => {
// const Mode = viewModeMap[mode] // const Mode = viewModeMap[mode]
if (source === 'orario') { if (source === 'orario') {
return ( return (
<div class="events-view"> <div class="events-view">
+15 -13
View File
@@ -4,31 +4,33 @@ export const Help = ({}) => (
<> <>
<h3>Visualizzazione Corsi</h3> <h3>Visualizzazione Corsi</h3>
<p> <p>
Per visualizzare i corsi che ti interessano nella tabella dell'orario, devi Per visualizzare i corsi che ti interessano nella tabella
prima selezionarli. Puoi selezionare dei corsi cliccandoli, cercandoli nelle dell'orario, devi prima selezionarli. Puoi selezionare dei corsi
sezioni Primo anno (<b>I</b>), Secondo anno (<b>II</b>), Terzo anno ( cliccandoli, cercandoli nelle sezioni Primo anno (<b>I</b>), Secondo
<b>III</b>), Magistrale (<b>M</b>), o Tutti. anno (<b>II</b>), Terzo anno (<b>III</b>), Magistrale (<b>M</b>), o
Tutti.
</p> </p>
<p> <p>
Una volta compiuta la selezione, è possibile vedere la tabella delle lezioni Una volta compiuta la selezione, è possibile vedere la tabella delle
andando nella visualizzazione Orario ( lezioni andando nella visualizzazione Orario (
<Icon name="calendar_view_month" />) <Icon name="calendar_view_month" />)
</p> </p>
<p> <p>
Per via di eventuali preferenze personali, è possibile cambiare l'orientazione Per via di eventuali preferenze personali, è possibile cambiare
della tabella Orario trasponendola, utilizzando il pulsante Trasponi ( l'orientazione della tabella Orario trasponendola, utilizzando il
pulsante Trasponi (
<Icon name="switch_left" style="transform: rotate(-45deg)" />) <Icon name="switch_left" style="transform: rotate(-45deg)" />)
</p> </p>
<p> <p>
È anche possibile visualizzare in uno specchietto riassuntivo soltanto i corsi È anche possibile visualizzare in uno specchietto riassuntivo
selezionati, andando nella visualizzazione Lista ( soltanto i corsi selezionati, andando nella visualizzazione Lista (
<Icon name="list" />) <Icon name="list" />)
</p> </p>
<h3>Stampa</h3> <h3>Stampa</h3>
<p> <p>
Da desktop puoi stampare l'orario attualmente visibile con il bottone{' '} Da desktop puoi stampare l'orario attualmente visibile con il
<Icon name="print" /> (è consigliato controllare le opzioni di stampa per bottone <Icon name="print" /> (è consigliato controllare le opzioni
ottenere un risultato soddisfacente). di stampa per ottenere un risultato soddisfacente).
</p> </p>
<h3>Bug &amp; Contatti</h3> <h3>Bug &amp; Contatti</h3>
<p> <p>
+16 -2
View File
@@ -7,9 +7,23 @@ export const SettingsBar = ({ theme, setTheme, date, setDate }) => {
<div class="settings-group"> <div class="settings-group">
<button <button
class="icon" class="icon"
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')} onClick={() =>
setTheme(theme === 'dark' ? 'light' : 'dark')
}
> >
<Icon name={theme === 'dark' ? 'dark_mode' : 'light_mode'} /> <Icon
name={theme === 'dark' ? 'dark_mode' : 'light_mode'}
/>
</button>
<button
class="icon"
onClick={() =>
setTheme(theme === 'dark' ? 'light' : 'dark')
}
>
<Icon
name="add"
/>
</button> </button>
<DatePicker date={date} setDate={setDate} /> <DatePicker date={date} setDate={setDate} />
</div> </div>
+13 -6
View File
@@ -24,7 +24,8 @@ export const Toolbar = ({
</button> </button>
</div> </div>
<div class="item logo"> <div class="item logo">
<img src="logo-circuit-board.svg" alt="logo" /> / <span>Orario</span> <img src="logo-circuit-board.svg" alt="logo" /> /{' '}
<span>Orario</span>
</div> </div>
<div class="option-group"> <div class="option-group">
<div class="item option"> <div class="item option">
@@ -35,6 +36,7 @@ export const Toolbar = ({
{ value: 'anno-3', label: 'III' }, { value: 'anno-3', label: 'III' },
{ value: 'magistrale', label: 'Magistrale' }, { value: 'magistrale', label: 'Magistrale' },
{ value: 'tutti', label: 'Tutti' }, { value: 'tutti', label: 'Tutti' },
{ value: 'custom', label: 'Custom' },
]} ]}
value={source} value={source}
setValue={setSource} setValue={setSource}
@@ -50,9 +52,10 @@ export const Toolbar = ({
setValue={setSource} setValue={setSource}
/> />
</div> </div>
</div> <div class="empty"></div>
<div class="option-group"> <div class="item option">
<DatePicker date={date} setDate={setDate} /> <DatePicker date={date} setDate={setDate} />
</div>
</div> </div>
<div class="option-group"> <div class="option-group">
<div class="item option"> <div class="item option">
@@ -63,9 +66,13 @@ export const Toolbar = ({
<div class="item option"> <div class="item option">
<button <button
class="icon" class="icon"
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')} onClick={() =>
setTheme(theme === 'dark' ? 'light' : 'dark')
}
> >
<Icon name={theme === 'dark' ? 'dark_mode' : 'light_mode'} /> <Icon
name={theme === 'dark' ? 'dark_mode' : 'light_mode'}
/>
</button> </button>
</div> </div>
<div class="item option"> <div class="item option">
+36 -15
View File
@@ -2,7 +2,7 @@ import { format } from 'date-fns'
import _ from 'lodash' import _ from 'lodash'
import { useEffect, useRef, useState } from 'preact/hooks' import { useEffect, useRef, useState } from 'preact/hooks'
import { prettyCourseName, WEEK_DAYS } from '../../utils.jsx' import { parseCustomEvents, prettyCourseName, WEEK_DAYS } from '../../utils.jsx'
import { Icon } from '../Icon.jsx' import { Icon } from '../Icon.jsx'
export const Courses = ({ export const Courses = ({
@@ -10,13 +10,18 @@ export const Courses = ({
timetables, timetables,
selection, selection,
setSelection, setSelection,
hideOtherCourses, custom,
isRestrictedList,
}) => { }) => {
const events = timetables[source] const events = isRestrictedList ? timetables['tutti'] : timetables[source]
const selectionSet = new Set(selection) const selectionSet = new Set(selection)
const visibleEvents = hideOtherCourses console.log(events)
? events.filter(e => selectionSet.has(e.id))
const visibleEvents = isRestrictedList
? events
.filter(e => selectionSet.has(e.id))
.concat(parseCustomEvents(custom))
: events : events
const eventsByCourse = _.groupBy(_.sortBy(visibleEvents, 'id'), 'id') const eventsByCourse = _.groupBy(_.sortBy(visibleEvents, 'id'), 'id')
@@ -53,7 +58,7 @@ export const Courses = ({
return ( return (
<div class="course-view" ref={element}> <div class="course-view" ref={element}>
{hideOtherCourses && selection.length === 0 && ( {isRestrictedList && selection.length === 0 && (
<div class="warning"> <div class="warning">
<p>Non hai ancora selezionato nessun corso.</p> <p>Non hai ancora selezionato nessun corso.</p>
<p> <p>
@@ -67,23 +72,39 @@ export const Courses = ({
<div <div
class={ class={
'course' + 'course' +
(currentlyHovered === id ? ' highlight' : '') + (currentlyHovered === id && !isRestrictedList
(selectionSet.has(id) ? ' selected' : '') ? ' highlight'
: '') +
(selectionSet.has(id) && !isRestrictedList
? ' selected'
: '')
} }
data-course-id={id} data-course-id={id}
onClick={() => { onClick={() => {
if (!selectionSet.has(id)) setSelection([...selection, id]) if (isRestrictedList) return
else setSelection(selection.filter(selId => selId !== id)) if (!selectionSet.has(id))
setSelection([...selection, id])
else
setSelection(
selection.filter(selId => selId !== id)
)
}} }}
> >
<div class="title">{prettyCourseName(courseEvents[0].name)}</div> <div class="title">
<div class="docenti">{profsPerCourse[id].join(', ')}</div> {prettyCourseName(courseEvents[0].name)}
</div>
<div class="docenti">
{profsPerCourse[id].join(', ')}
</div>
<div class="events"> <div class="events">
{courseEvents.map(course => ( {courseEvents.map(course => (
<div> <div>
{WEEK_DAYS[course.start.getDay()]}{' '} {WEEK_DAYS[course.day]} {course.start / 60}:
{format(course.start, 'H:mm')}&ndash; {String(course.start % 60).padStart(2, '0')}
{format(course.end, 'H:mm')} {course.aule.join(', ')} &ndash;
{course.end / 60}:
{String(course.end % 60).padStart(2, '0')}{' '}
{course.aule.join(', ')}
</div> </div>
))} ))}
</div> </div>
+52 -28
View File
@@ -1,9 +1,15 @@
import { useEffect, useRef, useState } from 'preact/hooks' import { useEffect, useRef, useState } from 'preact/hooks'
import _ from 'lodash' import _, { parseInt } from 'lodash'
import { differenceInMinutes, startOfDay } from 'date-fns' import { differenceInMinutes, startOfDay } from 'date-fns'
import { parse } from 'yaml'
import { parseCustomEvents } from '../../utils.jsx'
import { WEEK_DAYS_SHORT, prettyCourseName, usePersistentState } from '../../utils.jsx' import {
WEEK_DAYS_SHORT,
prettyCourseName,
usePersistentState,
} from '../../utils.jsx'
import { layoutEvents, layoutIntervals } from '../../interval-layout.js' import { layoutEvents, layoutIntervals } from '../../interval-layout.js'
import { Popup } from '../Popup.jsx' import { Popup } from '../Popup.jsx'
import { Icon } from '../Icon.jsx' import { Icon } from '../Icon.jsx'
@@ -13,23 +19,28 @@ const TransposePopup = ({ onClose }) => {
<Popup <Popup
title={ title={
<> <>
<Icon name="info" /> Attenzione! La tabella è stata trasposta! <Icon name="info" /> Attenzione! La tabella è stata
trasposta!
</> </>
} }
onClose={onClose} onClose={onClose}
> >
<p>A grande richiesta popolare abbiamo trasposto la tabella dell'orario!</p>
<p> <p>
Assicurati quindi di leggerla correttamente (dall'alto verso il basso A grande richiesta popolare abbiamo trasposto la tabella
invece che da sinistra verso destra). dell'orario!
</p> </p>
<p> <p>
Se preferisci utilizzare la versione vecchia, puoi utilizzare il pulsante Assicurati quindi di leggerla correttamente (dall'alto verso il
Trasponi <Icon name="switch_left" style="transform: rotate(-45deg)" />{' '} basso invece che da sinistra verso destra).
nell'origine della tabella per trasporla. Questa scelta verrà salvata nei </p>
cookie e verrà ricordata in futuro
<p>
Se preferisci utilizzare la versione vecchia, puoi utilizzare il
pulsante Trasponi{' '}
<Icon name="switch_left" style="transform: rotate(-45deg)" />{' '}
nell'origine della tabella per trasporla. Questa scelta verrà
salvata nei cookie e verrà ricordata in futuro
</p> </p>
</Popup> </Popup>
) )
@@ -40,8 +51,8 @@ const NoCourseWarning = () => {
<div class="warning"> <div class="warning">
<p>Non hai ancora selezionato nessun corso.</p> <p>Non hai ancora selezionato nessun corso.</p>
<p> <p>
Clicca sui corsi nelle altre visuali per selezionarli e visualizzarli Clicca sui corsi nelle altre visuali per selezionarli e
nell'orario visualizzarli nell'orario
</p> </p>
</div> </div>
) )
@@ -66,10 +77,13 @@ const Layout = ({ layout, day, colors }) => {
style={{ style={{
'--block-size': block.end - block.start, '--block-size': block.end - block.start,
'--size': event.end - event.start, '--size': event.end - event.start,
'--relative-start': event.start - block.start, '--relative-start':
event.start - block.start,
'--index': event.index, '--index': event.index,
'--of': block.layers, '--of': block.layers,
'--color': `var(--event-${colors[event.id]})`, '--color': `var(--event-${
colors[event.id]
})`,
}} }}
> >
<div class="event"> <div class="event">
@@ -106,11 +120,16 @@ const ScheduleGrid = ({
class="small" class="small"
onClick={() => onClick={() =>
setOrientation( setOrientation(
orientation === 'original' ? 'transposed' : 'original' orientation === 'original'
? 'transposed'
: 'original'
) )
} }
> >
<Icon name="switch_left" style="transform: rotate(-45deg)" /> <Icon
name="switch_left"
style="transform: rotate(-45deg)"
/>
</button> </button>
</div> </div>
{[1, 2, 3, 4, 5].map(n => ( {[1, 2, 3, 4, 5].map(n => (
@@ -193,25 +212,30 @@ const ScheduleCard = ({
) )
} }
export const Schedule = ({ timetables, selection }) => { export const Schedule = ({ timetables, selection, custom }) => {
const [hasSeenTranspose, setHasSeenTranspose] = usePersistentState( const [hasSeenTranspose, setHasSeenTranspose] = usePersistentState(
'transpose_info', 'transpose_info',
'false' 'false'
) )
const [orientation, setOrientation] = usePersistentState('orientation', 'original') const [orientation, setOrientation] = usePersistentState(
'orientation',
'original'
)
const colorList = ['red', 'purple', 'blue', 'yellow', 'green', 'orange', 'lightblue'] const colorList = [
'red',
'purple',
'blue',
'yellow',
'green',
'orange',
'lightblue',
]
const allEvents = timetables['tutti'] const allEvents = timetables['tutti']
const selectionSet = new Set(selection) const selectionSet = new Set(selection)
const events = allEvents const events = allEvents.filter(e => selectionSet.has(e.id))
.filter(e => selectionSet.has(e.id)) parseCustomEvents(custom).forEach(event => events.push(event))
.map(e => ({
...e,
day: e.start.getDay(),
start: differenceInMinutes(e.start, startOfDay(e.start)),
end: differenceInMinutes(e.end, startOfDay(e.start)),
}))
const weekStart = Math.min(...events.map(e => e.start), 9 * 60) / 30 const weekStart = Math.min(...events.map(e => e.start), 9 * 60) / 30
const weekEnd = Math.max(...events.map(e => e.end), 18 * 60) / 30 const weekEnd = Math.max(...events.map(e => e.end), 18 * 60) / 30
+10 -3
View File
@@ -42,7 +42,10 @@ function layoutBlockEvents(events) {
let viableIndex = 0 let viableIndex = 0
while ( while (
result.filter( result.filter(
e => e.index === viableIndex && e.start < event.end && event.start < e.end e =>
e.index === viableIndex &&
e.start < event.end &&
event.start < e.end
).length !== 0 ).length !== 0
) { ) {
viableIndex += 1 viableIndex += 1
@@ -54,7 +57,8 @@ function layoutBlockEvents(events) {
return result return result
} }
export function layoutEvents(events) { export function layoutEvents(events) {
const overlap = (event, block) => event.start < block.end && block.start < event.end const overlap = (event, block) =>
event.start < block.end && block.start < event.end
events.sort((a, b) => a.start - b.start) events.sort((a, b) => a.start - b.start)
@@ -64,7 +68,10 @@ export function layoutEvents(events) {
if (blocks.length > 0) { if (blocks.length > 0) {
layout = layout.filter(block => !overlap(event, block)) layout = layout.filter(block => !overlap(event, block))
layout.push({ layout.push({
start: Math.min(event.start, ...blocks.map(block => block.start)), start: Math.min(
event.start,
...blocks.map(block => block.start)
),
end: Math.max(event.end, ...blocks.map(block => block.end)), end: Math.max(event.end, ...blocks.map(block => block.end)),
events: blocks.flatMap(block => block.events).concat([event]), events: blocks.flatMap(block => block.events).concat([event]),
}) })
+77 -52
View File
@@ -1,6 +1,7 @@
import _ from 'lodash' import _ from 'lodash'
import { render } from 'preact' import { render } from 'preact'
import { useEffect, useState } from 'preact/hooks' import { useEffect, useState } from 'preact/hooks'
import { differenceInMinutes, startOfDay } from 'date-fns'
// import { ToolOverlay } from './components/ToolOverlay.jsx' // import { ToolOverlay } from './components/ToolOverlay.jsx'
// //
@@ -18,25 +19,18 @@ import { Icon } from './components/Icon.jsx'
import { Popup } from './components/Popup.jsx' import { Popup } from './components/Popup.jsx'
import { Toolbar } from './components/Toolbar.jsx' import { Toolbar } from './components/Toolbar.jsx'
import { OptionBar } from './components/OptionBar.jsx' import { OptionBar } from './components/OptionBar.jsx'
import { import { prettyAulaName, prettyProfName, usePersistentState } from './utils.jsx'
prettyAulaName,
prettyProfName,
clearOldPersistentStates,
usePersistentState,
} from './utils.jsx'
import { SettingsBar } from './components/SettingsBar.jsx' import { SettingsBar } from './components/SettingsBar.jsx'
// Che fanno queste due righe? // Che fanno queste due righe?
window._ = _ window._ = _
window.dataBuffer = {} window.dataBuffer = {}
// NOTA: magistrale *non* è quello con i corsi a cavallo
const TIMETABLE_IDS = { const TIMETABLE_IDS = {
'anno-1': '687e66d62d13e6001edd366d', 'anno-1': '64a7c1c651f079001d52e9c8',
'anno-2': '687e681301ee9300198320e6', 'anno-2': '6308e2dc09352a0208fefdd9',
'anno-3': '687e68bb01ee9300198320eb', 'anno-3': '6308e42a1df5cb026699ced4',
'magistrale': '687e6a9f96ea080019d439d5', 'magistrale': '64a7c7091ab813002c5d9ede',
} }
// const DEFAULT_DATE_RANGE = { // const DEFAULT_DATE_RANGE = {
@@ -52,17 +46,15 @@ const TIMETABLE_IDS = {
// } // }
function specialEventPatches(eventi) { function specialEventPatches(eventi) {
// Il laboratorio del primo anno in realtà è in due gruppi separati // Il laboratorio del primo anno in realtà è in due canali separati
let i = 1
eventi.forEach(evento => { eventi.forEach(evento => {
if ( if (
evento.nome === 'LABORATORIO DI INTRODUZIONE ALLA MATEMATICA COMPUTAZIONALE' evento.nome ===
'LABORATORIO DI INTRODUZIONE ALLA MATEMATICA COMPUTAZIONALE'
) { ) {
if (evento.evento.dettagliDidattici[0].partizione.descrizione === 'CORSO A') { evento.nome += ` (${i})`
evento.nome += ' (A)' i++
}
if (evento.evento.dettagliDidattici[0].partizione.descrizione === 'CORSO B') {
evento.nome += ' (B)'
}
} }
}) })
@@ -71,12 +63,17 @@ function specialEventPatches(eventi) {
function formatEvents(timetable) { function formatEvents(timetable) {
return timetable.map(({ nome, dataInizio, dataFine, docenti, aule }) => { return timetable.map(({ nome, dataInizio, dataFine, docenti, aule }) => {
const start = new Date(dataInizio)
const end = new Date(dataFine)
return { return {
id: nome, id: nome,
name: _.split(nome, '-', 1)[0].trim(), name: _.split(nome, '-', 1)[0].trim(),
start: new Date(dataInizio), day: start.getDay(),
end: new Date(dataFine), start: differenceInMinutes(start, startOfDay(start)),
docenti: docenti.map(({ nome, cognome }) => prettyProfName(nome, cognome)), end: differenceInMinutes(end, startOfDay(start)),
docenti: docenti.map(({ nome, cognome }) =>
prettyProfName(nome, cognome)
),
aule: aule.map(aula => prettyAulaName(aula.codice)), aule: aule.map(aula => prettyAulaName(aula.codice)),
} }
}) })
@@ -133,7 +130,7 @@ async function loadCalendari(date) {
const timetablesRaw = results.map(timetable => const timetablesRaw = results.map(timetable =>
specialEventPatches(_.uniqBy(timetable, 'id')) specialEventPatches(_.uniqBy(timetable, 'id'))
) )
const allRaw = specialEventPatches(_.concat(...results), 'id') const allRaw = specialEventPatches(_.uniqBy(_.concat(...results), 'id'))
return { return {
'anno-1': formatEvents(timetablesRaw[0]), 'anno-1': formatEvents(timetablesRaw[0]),
@@ -144,19 +141,50 @@ async function loadCalendari(date) {
} }
} }
const View = ({ view, selection, setSelection, timetables }) => { const View = ({
view,
selection,
setSelection,
timetables,
custom,
setCustom,
}) => {
if (view === 'orario') { if (view === 'orario') {
return <Schedule selection={selection} timetables={timetables} /> return (
<Schedule
selection={selection}
timetables={timetables}
custom={custom}
/>
)
} else if (view === 'lista') { } else if (view === 'lista') {
return ( return (
<Courses <Courses
selection={selection} selection={selection}
setSelection={setSelection} setSelection={setSelection}
source={'tutti'}
timetables={timetables} timetables={timetables}
hideOtherCourses={true} custom={custom}
isRestrictedList={true}
/> />
) )
} else if (view === 'custom') {
return (
<div class="custom-events-view">
<textarea
value={custom}
onChange={e => setCustom(e.target.value)}
/>
<p>Esempio di evento personalizzato</p>
<p>
Nome evento [label globale]:
<br />
- Lun 9-11
<br />
- Mar 9:00-11:00
<br />- Gio 8:00-12:00 [label locale]
</p>
</div>
)
} else { } else {
return ( return (
<Courses <Courses
@@ -164,7 +192,7 @@ const View = ({ view, selection, setSelection, timetables }) => {
setSelection={setSelection} setSelection={setSelection}
source={view} source={view}
timetables={timetables} timetables={timetables}
hideOtherCourses={false} isRestrictedList={false}
/> />
) )
} }
@@ -180,22 +208,28 @@ const App = ({}) => {
const [date, setDate] = useState(new Date().toISOString()) const [date, setDate] = useState(new Date().toISOString())
// Data Sources // Data Sources
const [view, setView] = usePersistentState('view', 'tutti') const [view, setView] = usePersistentState('view', 'magistrale')
const [timetables, setTimetables] = useState(null) const [timetables, setTimetables] = useState(null)
useEffect(async () => { useEffect(async () => {
setTimetables(await loadCalendari(new Date(date))) setTimetables(await loadCalendari(new Date(date)))
}, [date]) }, [date])
// View Modes
// const [mode, setMode] = usePersistentState('orario.mode', MODE_COURSES)
// Selection // Selection
const [selectedCourses, setSelectedCourses] = usePersistentState('selection', []) const [selectedCourses, setSelectedCourses] = usePersistentState(
'selection',
[]
)
// Custom Events
const [custom, setCustom] = usePersistentState('custom', [])
// Menus // Menus
const [helpVisible, setHelpVisible] = useState(false) const [helpVisible, setHelpVisible] = useState(false)
const [showMobileMenu, setShowMobileMenu] = 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( const [theme, setTheme] = usePersistentState(
'theme', 'theme',
'light' 'light'
@@ -258,29 +292,18 @@ const App = ({}) => {
) : timetables['tutti'].length === 0 ? ( ) : timetables['tutti'].length === 0 ? (
<div class="warning"> <div class="warning">
<p> <p>
Non esistono corsi per la settimana selezionata: buone Non esistono corsi per la settimana selezionata:
vacanze! 🎉 buone vacanze! 🎉
</p> </p>
<p> <p>
Clicca sul bottone qua sotto per vedere la prima settimana Per cambiare settimana puoi usare il widget
completa di lezioni: Calendario (
</p>
<button
onClick={() => {
setDate('2025-09-29T00:00:00.000Z')
}}
>
Vai al 29 settembre! 🚀
</button>
{/* <p>
Per cambiare settimana puoi usare il widget Calendario (
<Icon name="calendar_month" />) in alto a destra <Icon name="calendar_month" />) in alto a destra
<br /> <br />
In versione mobile, il widget Calendario è situato dentro In versione mobile, il widget Calendario è
il Menu ( situato dentro il Menu (
<Icon name="menu" />) <Icon name="menu" />)
</p> */} </p>
</div> </div>
) : ( ) : (
<View <View
@@ -288,6 +311,8 @@ const App = ({}) => {
setSelection={setSelectedCourses} setSelection={setSelectedCourses}
view={view} view={view}
timetables={timetables} timetables={timetables}
custom={custom}
setCustom={setCustom}
/> />
))} ))}
</div> </div>
+50 -11
View File
@@ -530,9 +530,9 @@ body {
} }
.content { .content {
height: calc(100vh - 4rem); height: calc(100dvh - 4rem);
@media screen and (max-width: $device-s-width), (pointer: coarse) { @media screen and (max-width: $device-s-width), (pointer: coarse) {
height: calc(100vh - 8rem); height: calc(100dvh - 8rem);
} }
overflow-y: scroll; overflow-y: scroll;
@@ -624,7 +624,7 @@ body {
.schedule-view { .schedule-view {
min-height: 100%; min-height: 100%;
width: 100%; width: 100%;
max-width: 57rem; max-width: 55rem;
margin: auto; margin: auto;
padding: 0rem 0.5rem; padding: 0rem 0.5rem;
@@ -644,7 +644,8 @@ body {
border: 1px solid var(--border-600); border: 1px solid var(--border-600);
border-radius: 10px 10px 0 0; border-radius: 10px 10px 0 0;
@media screen and (max-width: $device-s-width), (pointer: coarse) { @media screen and (max-width: $device-s-width),
(pointer: coarse) {
font-size: 12px; font-size: 12px;
} }
@@ -699,7 +700,10 @@ body {
&.original { &.original {
grid-template-columns: auto repeat(5, 1fr); grid-template-columns: auto repeat(5, 1fr);
grid-template-rows: min-content repeat(var(--time-slots), 1fr); grid-template-rows: min-content repeat(
var(--time-slots),
1fr
);
.transpose-button, .transpose-button,
.day-label { .day-label {
@@ -726,16 +730,22 @@ body {
} }
.event-block-wrapper { .event-block-wrapper {
grid-row: calc(var(--time-start) + 2) / calc(var(--time-end) + 2); grid-row: calc(var(--time-start) + 2) /
calc(var(--time-end) + 2);
grid-column: calc(var(--day-position) + 1); grid-column: calc(var(--day-position) + 1);
.event-block { .event-block {
.event-wrapper { .event-wrapper {
width: calc(100% / var(--of)); width: calc(100% / var(--of));
height: calc(100% * var(--size) / var(--block-size)); height: calc(
100% * var(--size) / var(--block-size)
);
transform: translateX(calc(100% * var(--index))) transform: translateX(calc(100% * var(--index)))
translateY( translateY(
calc(100% * var(--relative-start) / var(--size)) calc(
100% * var(--relative-start) /
var(--size)
)
); );
} }
} }
@@ -743,7 +753,10 @@ body {
} }
&.transposed { &.transposed {
grid-template-rows: auto repeat(5, 1fr); grid-template-rows: auto repeat(5, 1fr);
grid-template-columns: min-content repeat(var(--time-slots), 1fr); grid-template-columns: min-content repeat(
var(--time-slots),
1fr
);
.transpose-button, .transpose-button,
.day-label { .day-label {
@@ -779,10 +792,15 @@ body {
.event-block { .event-block {
.event-wrapper { .event-wrapper {
height: calc(100% / var(--of)); height: calc(100% / var(--of));
width: calc(100% * var(--size) / var(--block-size)); width: calc(
100% * var(--size) / var(--block-size)
);
transform: translateY(calc(100% * var(--index))) transform: translateY(calc(100% * var(--index)))
translateX( translateX(
calc(100% * var(--relative-start) / var(--size)) calc(
100% * var(--relative-start) /
var(--size)
)
); );
} }
} }
@@ -879,6 +897,27 @@ body {
overflow-y: scroll; overflow-y: scroll;
} }
} }
.custom-events-view {
min-height: 100%;
width: 100%;
max-width: 30rem;
margin: auto;
padding: 1rem 1rem;
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
textarea {
width: 100%;
min-height: 20rem;
font-size: 20px;
}
}
} }
.overlay { .overlay {
+68 -6
View File
@@ -1,6 +1,7 @@
// Calendar
import { useEffect, useState } from 'preact/hooks' import { useEffect, useState } from 'preact/hooks'
import { parse } from 'yaml'
// Calendar
export const WEEK_DAYS = [ export const WEEK_DAYS = [
'Domenica', 'Domenica',
@@ -24,8 +25,12 @@ export function hashString(str, seed = 0) {
h1 = Math.imul(h1 ^ ch, 2654435761) h1 = Math.imul(h1 ^ ch, 2654435761)
h2 = Math.imul(h2 ^ ch, 1597334677) h2 = Math.imul(h2 ^ ch, 1597334677)
} }
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909) h1 =
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909) 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) return 4294967296 * (2097151 & h2) + (h1 >>> 0)
} }
@@ -38,7 +43,9 @@ export function prettyCourseName(name) {
.map(word => { .map(word => {
if (word.trim().length === 0) return word if (word.trim().length === 0) return word
return /(^del|^nel|^di$|^dei$|^con$|^alla$|^per$|^e$|^la$)/.test(word) return /(^del|^nel|^di$|^dei$|^con$|^alla$|^per$|^e$|^la$)/.test(
word
)
? word ? word
: word[0].toUpperCase() + word.slice(1) : word[0].toUpperCase() + word.slice(1)
}) })
@@ -47,7 +54,6 @@ export function prettyCourseName(name) {
.replaceAll('IIi', 'III') .replaceAll('IIi', 'III')
.replaceAll('Iii', 'III') .replaceAll('Iii', 'III')
.replaceAll(/'(.)/g, ({}, letter) => "'" + letter.toUpperCase()) .replaceAll(/'(.)/g, ({}, letter) => "'" + letter.toUpperCase())
.replaceAll(/\((a|b)\)/g, ({}, letter) => '(' + letter.toUpperCase() + ')')
} }
export function prettyAulaName(name) { export function prettyAulaName(name) {
@@ -64,6 +70,62 @@ export function prettyProfName(nome, cognome) {
.join('') .join('')
} }
// Custom events
export function parseCustomEvents(customEventsString) {
try {
const customEvents = []
const parsedEvents = parse(customEventsString)
for (const customCourse in parsedEvents) {
const [_, name, teachers] = /([^\[]*)(?:\[(.*)\])?/.exec(
customCourse
)
const docenti = teachers ? teachers.split(',') : []
for (const customEvent of parsedEvents[customCourse]) {
const [_b, day, startH, startM, endH, endM, label] =
/(lun|lunedì|mar|martedì|mer|mercoledì|gio|giovedì|ven|venerdì)\s*(\d{1,2})(?:\:(\d\d))?\s*-\s*(\d{1,2})(?:\:(\d\d))?\s*(.*)?/i.exec(
customEvent
)
const dayNumber = {
lun: 1,
lunedì: 1,
mar: 2,
martedì: 2,
mer: 3,
mercoledì: 3,
gio: 4,
giovedì: 4,
ven: 5,
venerdì: 5,
}[day.toLowerCase()]
const start = startM
? parseInt(startH) * 60 + parseInt(startM)
: parseInt(startH) * 60
const end = endM
? parseInt(endH) * 60 + parseInt(endM)
: parseInt(endH) * 60
const aule = label ? label.split(',') : []
customEvents.push({
id: name,
name,
docenti,
start,
end,
day: dayNumber,
aule,
})
}
}
return customEvents
} catch (e) {
console.log('ehi', e)
return []
}
}
// JSX // JSX
export const withClasses = classes => export const withClasses = classes =>