Author SHA1 Message Date
BachoSeven 8ae010a715 feat: push new ids 2026-09-02 21:54:08 +02:00
BachoSeven 75af3ccd56 chore: update semester 2026-01-23 16:09:50 +01:00
BachoSeven 97e3758bc1 chore: refactor JSON timetable import 2025-08-28 15:19:18 +02:00
BachoSeven 48445020c8 feat: add manual semester data update script 2025-08-28 14:55:11 +02:00
BachoSeven b21cc30c8a Merge pull request 'chore: update calendar IDs for 1st semester 25/26' (#20) from dev into main
continuous-integration/drone/push Build is passing
Reviewed-on: #20
2025-08-26 22:38:46 +00:00
BachoSeven d0c2f58094 chore: update calendar IDs for 1st semester 25/26 2025-08-15 20:33:12 +02:00
BachoSeven 6ce7979719 Merge pull request 'Fix lab1 split courses recognition, add snapping!' (#18) from dev into main
continuous-integration/drone/push Build is passing
Reviewed-on: #18

:3
2025-02-14 15:41:25 +00:00
BachoSeven e9539b7f35 Fix lab1 split courses recognition, add snapping! 2025-02-14 16:39:35 +01:00
aziis98 c89b4a0837 Merge pull request 'chore: updated to new semester calendar ids' (#17) from dev into main
continuous-integration/drone/push Build is passing
Reviewed-on: #17
2025-02-04 09:59:34 +00:00
aziis98 4c98950822 chore: updated to new semester calendar ids 2025-02-04 10:57:14 +01:00
aziis98 39ccb715db Merge pull request 'chore: updated calendar ids to first semester of 2024/2025' (#16) from dev into main
continuous-integration/drone/push Build is passing
Reviewed-on: #16
2024-08-06 17:05:09 +00:00
aziis98 d929aa6cfe chore: updated calendar ids to first semester of 2024/2025 2024-08-06 19:00:20 +02:00
aziis98 db72898471 Merge pull request 'chore: empty commit to trigger deploy' (#15) from dev into main
continuous-integration/drone/push Build is passing
Reviewed-on: #15
2024-02-25 16:17:39 +00:00
aziis98 4d95a1a633 chore: empty commit to trigger deploy 2024-02-25 17:16:44 +01:00
aziis98 65b800806f Merge pull request 'Fixed iOS date picker bug' (#14) from dev into main
Reviewed-on: #14
2024-02-25 15:50:06 +00:00
aziis98 935309147c cmd: prettier -w src/ 2024-02-25 16:43:59 +01:00
aziis98 fb551fb719 fix: safari ios showPicker() bug 2024-02-25 16:42:10 +01:00
BachoSeven c731d9c8b9 Merge pull request 'chore: fix LIMCO courses' names' (#13) from dev into main
continuous-integration/drone/push Build is passing
Reviewed-on: #13
2024-02-04 15:35:53 +00:00
BachoSeven d9d2aaed11 chore: fix LIMCO courses' names 2024-02-04 16:35:20 +01:00
BachoSeven 7bbd9ba139 Merge pull request 'chore: update calendar IDs for the 2nd semester' (#12) from dev into main
continuous-integration/drone/push Build is passing
Reviewed-on: #12
2024-02-04 15:00:00 +00:00
BachoSeven 62a9b80b21 chore: update calendar IDs for the 2nd semester 2024-02-04 15:59:07 +01:00
Fran314 c43bdc60e7 Merge pull request 'dev' (#11) from dev into main
continuous-integration/drone/push Build is passing
Reviewed-on: #11
2023-10-09 13:30:58 +00:00
Fran314 c8172ad88a Added warning messages when date outside of lecture period 2023-10-09 15:29:51 +02:00
Fran314 c0b2894c22 Merge branch 'dev' of git.phc.dm.unipi.it:phc/orario into dev 2023-10-09 14:26:22 +02:00
Fran314 fd0440465c Added date picker and fixed menu rendering 2023-10-09 14:24:49 +02:00
aziis98 bdbf031bce Merge pull request 'fix: rimosso un console.log' (#8) from dev into main
continuous-integration/drone/push Build is passing
Reviewed-on: #8
2023-10-08 11:12:04 +00:00
aziis98 32dc9a6104 fix: rimosso un console.log 2023-10-08 13:09:57 +02:00
Fran314 2d4a3ce664 Merge pull request 'reintrodotta visualizzazione lista rimossa nel commit precedente' (#7) from dev into main
continuous-integration/drone/push Build is passing
Reviewed-on: #7
2023-10-07 21:05:33 +00:00
Fran314 af41177561 reintrodotta visualizzazione lista rimossa nel commit precedente 2023-10-07 22:49:03 +02:00
Fran314 22ce830c80 Merge pull request 'dev' (#6) from dev into main
continuous-integration/drone/push Build is passing
Reviewed-on: #6
2023-10-07 18:56:33 +00:00
18 changed files with 660 additions and 366 deletions
+9
View File
@@ -0,0 +1,9 @@
{
"printWidth": 90,
"singleQuote": true,
"quoteProps": "consistent",
"tabWidth": 4,
"semi": false,
"arrowParens": "avoid",
"proseWrap": "always"
}
Executable
BIN
View File
Binary file not shown.
-40
View File
@@ -1,40 +0,0 @@
[
"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"
]
+33
View File
@@ -0,0 +1,33 @@
import { useRef } from 'preact/hooks'
import { Icon } from './Icon.jsx'
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent)
export const DatePicker = ({ date, setDate }) => {
const input = useRef()
const [year, month, day] = date.split('T')[0].split('-')
return (
<div
class="date-picker"
onClick={() =>
isSafari ? input.current.focus() : input.current.showPicker()
}
>
<input
ref={input}
type="date"
value={`${year}-${month}-${day}`}
onChange={e => setDate(new Date(e.target.value).toISOString())}
/>
<div class="date-picker-render">
<div class="date">
{day}/{month}/{year}
</div>
<div class="calendar">
<Icon name="calendar_month" />
</div>
</div>
</div>
)
}
-1
View File
@@ -24,7 +24,6 @@ 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 (
+8 -18
View File
@@ -1,27 +1,17 @@
import { ComboBox } from './ComboBox.jsx' import { ComboBox } from './ComboBox.jsx'
import { MODE_COURSES, MODE_SCHEDULE, MODE_WORKWEEK, MODE_WORKWEEK_GRID } from './EventsView.jsx' import {
MODE_COURSES,
MODE_SCHEDULE,
MODE_WORKWEEK,
MODE_WORKWEEK_GRID,
} from './EventsView.jsx'
import { DatePicker } from './DatePicker.jsx'
import { Help } from './Help.jsx' import { Help } from './Help.jsx'
import { Icon } from './Icon.jsx' import { Icon } from './Icon.jsx'
export const HamburgerMenu = ({ onClose, theme, setTheme }) => { export const HamburgerMenu = ({ date, setDate, onClose, theme, setTheme }) => {
return ( return (
<div class="menu"> <div class="menu">
<div class="header">
<div class="option-group">
<button class="flat icon" onClick={onClose}>
<Icon name="close" />
</button>
<button
class="icon"
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
>
<Icon name={theme === 'dark' ? 'dark_mode' : 'light_mode'} />
</button>
</div>
<div class="item logo">
<img src="logo-circuit-board.svg" alt="logo" /> / <span>Orario</span>
</div>
</div>
<div class="help"> <div class="help">
<h2> <h2>
<Icon name="info" /> <Icon name="info" />
+17 -13
View File
@@ -4,27 +4,31 @@ export const Help = ({}) => (
<> <>
<h3>Visualizzazione Corsi</h3> <h3>Visualizzazione Corsi</h3>
<p> <p>
Per visualizzare i corsi che ti interessano nella tabella Per visualizzare i corsi che ti interessano nella tabella dell'orario, devi
dell'orario, devi prima selezionarli. Puoi selezionare dei corsi prima selezionarli. Puoi selezionare dei corsi cliccandoli, cercandoli nelle
cliccandoli, cercandoli nelle sezioni Primo anno (<b>I</b>), Secondo sezioni Primo anno (<b>I</b>), Secondo anno (<b>II</b>), Terzo anno (
anno (<b>II</b>), Terzo anno (<b>III</b>), Magistrale (<b>M</b>), o <b>III</b>), Magistrale (<b>M</b>), o Tutti.
Tutti.
</p> </p>
<p> <p>
Una volta compiuta la selezione, è possibile vedere la tabella delle Una volta compiuta la selezione, è possibile vedere la tabella delle lezioni
lezioni andando nella visualizzazione Orario andando nella visualizzazione Orario (
<Icon name="calendar_view_month" />)
</p> </p>
<p> <p>
Per via di eventuali preferenze personali, è possibile cambiare Per via di eventuali preferenze personali, è possibile cambiare l'orientazione
l'orientazione della tabella Orario trasponendola, utilizzando il della tabella Orario trasponendola, utilizzando il pulsante Trasponi (
pulsante Trasponi (
<Icon name="switch_left" style="transform: rotate(-45deg)" />) <Icon name="switch_left" style="transform: rotate(-45deg)" />)
</p> </p>
<p>
È anche possibile visualizzare in uno specchietto riassuntivo soltanto i corsi
selezionati, andando nella visualizzazione Lista (
<Icon name="list" />)
</p>
<h3>Stampa</h3> <h3>Stampa</h3>
<p> <p>
Da desktop puoi stampare l'orario attualmente visibile con il Da desktop puoi stampare l'orario attualmente visibile con il bottone{' '}
bottone <Icon name="print" /> (è consigliato controllare le opzioni <Icon name="print" /> (è consigliato controllare le opzioni di stampa per
di stampa per ottenere un risultato soddisfacente). ottenere un risultato soddisfacente).
</p> </p>
<h3>Bug &amp; Contatti</h3> <h3>Bug &amp; Contatti</h3>
<p> <p>
+11 -6
View File
@@ -1,7 +1,7 @@
import { CompoundButton } from './CompoundButton.jsx' import { CompoundButton } from './CompoundButton.jsx'
import { Icon } from './Icon.jsx' import { Icon } from './Icon.jsx'
export const OptionBar = ({ source, setSource }) => { export const OptionBar = ({ view, setView }) => {
return ( return (
<div class="option-bar"> <div class="option-bar">
<div class="option-group"> <div class="option-group">
@@ -14,20 +14,25 @@ export const OptionBar = ({ source, setSource }) => {
{ value: 'magistrale', label: 'M' }, { value: 'magistrale', label: 'M' },
{ value: 'tutti', label: 'Tutti' }, { value: 'tutti', label: 'Tutti' },
]} ]}
value={source} value={view}
setValue={setSource} setValue={setView}
/> />
</div> </div>
<CompoundButton <CompoundButton
options={[ options={[
{ {
value: 'orario', value: 'orario',
label: <Icon name="calendar_month" />, label: <Icon name="calendar_view_month" />,
icon: true,
},
{
value: 'lista',
label: <Icon name="list" />,
icon: true, icon: true,
}, },
]} ]}
value={source} value={view}
setValue={setSource} setValue={setView}
/> />
</div> </div>
</div> </div>
+18
View File
@@ -0,0 +1,18 @@
import { DatePicker } from './DatePicker.jsx'
import { Icon } from './Icon.jsx'
export const SettingsBar = ({ theme, setTheme, date, setDate }) => {
return (
<div class="settings-bar">
<div class="settings-group">
<button
class="icon"
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
>
<Icon name={theme === 'dark' ? 'dark_mode' : 'light_mode'} />
</button>
<DatePicker date={date} setDate={setDate} />
</div>
</div>
)
}
+17 -11
View File
@@ -1,10 +1,14 @@
import { CompoundButton } from './CompoundButton.jsx' import { CompoundButton } from './CompoundButton.jsx'
import { DatePicker } from './DatePicker.jsx'
import { Icon } from './Icon.jsx' import { Icon } from './Icon.jsx'
export const Toolbar = ({ export const Toolbar = ({
source, source,
setSource, setSource,
onShowMenu, date,
setDate,
showMobileMenu,
setShowMobileMenu,
onHelp, onHelp,
theme, theme,
setTheme, setTheme,
@@ -12,13 +16,15 @@ export const Toolbar = ({
return ( return (
<div class="toolbar"> <div class="toolbar">
<div class="mobile"> <div class="mobile">
<button class="flat icon" onClick={onShowMenu}> <button
<Icon name="menu" /> class="flat icon"
onClick={() => setShowMobileMenu(!showMobileMenu)}
>
<Icon name={`${showMobileMenu ? 'close' : 'menu'}`} />
</button> </button>
</div> </div>
<div class="item logo"> <div class="item logo">
<img src="logo-circuit-board.svg" alt="logo" /> /{' '} <img src="logo-circuit-board.svg" alt="logo" /> / <span>Orario</span>
<span>Orario</span>
</div> </div>
<div class="option-group"> <div class="option-group">
<div class="item option"> <div class="item option">
@@ -38,12 +44,16 @@ export const Toolbar = ({
<CompoundButton <CompoundButton
options={[ options={[
{ value: 'orario', label: 'Orario' }, { value: 'orario', label: 'Orario' },
{ value: 'lista', label: 'Lista' },
]} ]}
value={source} value={source}
setValue={setSource} setValue={setSource}
/> />
</div> </div>
</div> </div>
<div class="option-group">
<DatePicker date={date} setDate={setDate} />
</div>
<div class="option-group"> <div class="option-group">
<div class="item option"> <div class="item option">
<button class="icon" onClick={() => window.print()}> <button class="icon" onClick={() => window.print()}>
@@ -53,13 +63,9 @@ export const Toolbar = ({
<div class="item option"> <div class="item option">
<button <button
class="icon" class="icon"
onClick={() => onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
setTheme(theme === 'dark' ? 'light' : 'dark')
}
> >
<Icon <Icon name={theme === 'dark' ? 'dark_mode' : 'light_mode'} />
name={theme === 'dark' ? 'dark_mode' : 'light_mode'}
/>
</button> </button>
</div> </div>
<div class="item option"> <div class="item option">
+22 -2
View File
@@ -3,12 +3,23 @@ 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 { prettyCourseName, WEEK_DAYS } from '../../utils.jsx'
import { Icon } from '../Icon.jsx'
export const Courses = ({ source, timetables, selection, setSelection }) => { export const Courses = ({
source,
timetables,
selection,
setSelection,
hideOtherCourses,
}) => {
const events = timetables[source] const events = timetables[source]
const selectionSet = new Set(selection) const selectionSet = new Set(selection)
const eventsByCourse = _.groupBy(_.sortBy(events, 'id'), 'id') const visibleEvents = hideOtherCourses
? events.filter(e => selectionSet.has(e.id))
: events
const eventsByCourse = _.groupBy(_.sortBy(visibleEvents, 'id'), 'id')
const profsPerCourse = _.mapValues(eventsByCourse, events => const profsPerCourse = _.mapValues(eventsByCourse, events =>
_.uniq(events.flatMap(event => event.docenti)) _.uniq(events.flatMap(event => event.docenti))
@@ -42,6 +53,15 @@ export const Courses = ({ source, timetables, selection, setSelection }) => {
return ( return (
<div class="course-view" ref={element}> <div class="course-view" ref={element}>
{hideOtherCourses && selection.length === 0 && (
<div class="warning">
<p>Non hai ancora selezionato nessun corso.</p>
<p>
Clicca sui corsi nelle altre visuali per selezionarli e
visualizzarli nella lista
</p>
</div>
)}
<div class="wrap-container"> <div class="wrap-container">
{Object.entries(eventsByCourse).map(([id, courseEvents]) => ( {Object.entries(eventsByCourse).map(([id, courseEvents]) => (
<div <div
+20 -49
View File
@@ -3,11 +3,7 @@ import { useEffect, useRef, useState } from 'preact/hooks'
import _ from 'lodash' import _ from 'lodash'
import { differenceInMinutes, startOfDay } from 'date-fns' import { differenceInMinutes, startOfDay } from 'date-fns'
import { import { WEEK_DAYS_SHORT, prettyCourseName, usePersistentState } from '../../utils.jsx'
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'
@@ -17,28 +13,23 @@ const TransposePopup = ({ onClose }) => {
<Popup <Popup
title={ title={
<> <>
<Icon name="info" /> Attenzione! La tabella è stata <Icon name="info" /> Attenzione! La tabella è stata trasposta!
trasposta!
</> </>
} }
onClose={onClose} onClose={onClose}
> >
<p>A grande richiesta popolare abbiamo trasposto la tabella dell'orario!</p>
<p> <p>
A grande richiesta popolare abbiamo trasposto la tabella Assicurati quindi di leggerla correttamente (dall'alto verso il basso
dell'orario! invece che da sinistra verso destra).
</p> </p>
<p> <p>
Assicurati quindi di leggerla correttamente (dall'alto verso il Se preferisci utilizzare la versione vecchia, puoi utilizzare il pulsante
basso invece che da sinistra verso destra). Trasponi <Icon name="switch_left" style="transform: rotate(-45deg)" />{' '}
</p> nell'origine della tabella per trasporla. Questa scelta verrà salvata nei
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>
) )
@@ -46,18 +37,17 @@ const TransposePopup = ({ onClose }) => {
const NoCourseWarning = () => { const NoCourseWarning = () => {
return ( return (
<div class="no-courses-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 Clicca sui corsi nelle altre visuali per selezionarli e visualizzarli
visualizzarli nell'orario nell'orario
</p> </p>
</div> </div>
) )
} }
const Layout = ({ layout, day, colors }) => { const Layout = ({ layout, day, colors }) => {
console.log(layout)
return ( return (
<> <>
{layout.map(block => ( {layout.map(block => (
@@ -76,13 +66,10 @@ 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': '--relative-start': event.start - block.start,
event.start - block.start,
'--index': event.index, '--index': event.index,
'--of': block.layers, '--of': block.layers,
'--color': `var(--event-${ '--color': `var(--event-${colors[event.id]})`,
colors[event.id]
})`,
}} }}
> >
<div class="event"> <div class="event">
@@ -119,16 +106,11 @@ const ScheduleGrid = ({
class="small" class="small"
onClick={() => onClick={() =>
setOrientation( setOrientation(
orientation === 'original' orientation === 'original' ? 'transposed' : 'original'
? 'transposed'
: 'original'
) )
} }
> >
<Icon <Icon name="switch_left" style="transform: rotate(-45deg)" />
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 => (
@@ -211,25 +193,14 @@ const ScheduleCard = ({
) )
} }
export const Schedule = ({ timetables, selection, setSelection }) => { export const Schedule = ({ timetables, selection }) => {
const [hasSeenTranspose, setHasSeenTranspose] = usePersistentState( const [hasSeenTranspose, setHasSeenTranspose] = usePersistentState(
'transpose_info', 'transpose_info',
'false' 'false'
) )
const [orientation, setOrientation] = usePersistentState( const [orientation, setOrientation] = usePersistentState('orientation', 'original')
'orientation',
'original'
)
const colorList = [ const colorList = ['red', 'purple', 'blue', 'yellow', 'green', 'orange', 'lightblue']
'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)
+3 -10
View File
@@ -42,10 +42,7 @@ function layoutBlockEvents(events) {
let viableIndex = 0 let viableIndex = 0
while ( while (
result.filter( result.filter(
e => e => e.index === viableIndex && e.start < event.end && event.start < e.end
e.index === viableIndex &&
e.start < event.end &&
event.start < e.end
).length !== 0 ).length !== 0
) { ) {
viableIndex += 1 viableIndex += 1
@@ -57,8 +54,7 @@ function layoutBlockEvents(events) {
return result return result
} }
export function layoutEvents(events) { export function layoutEvents(events) {
const overlap = (event, block) => const overlap = (event, block) => event.start < block.end && block.start < event.end
event.start < block.end && block.start < event.end
events.sort((a, b) => a.start - b.start) events.sort((a, b) => a.start - b.start)
@@ -68,10 +64,7 @@ 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( start: Math.min(event.start, ...blocks.map(block => block.start)),
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]),
}) })
+144 -92
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 semesterData from './semester-data.json'
// import { ToolOverlay } from './components/ToolOverlay.jsx' // import { ToolOverlay } from './components/ToolOverlay.jsx'
// //
@@ -25,39 +26,39 @@ import {
usePersistentState, usePersistentState,
} from './utils.jsx' } from './utils.jsx'
import { SettingsBar } from './components/SettingsBar.jsx'
// Che fanno queste due righe? // Che fanno queste due righe?
window._ = _ window._ = _
window.dataBuffer = {} window.dataBuffer = {}
const TIMETABLE_IDS = { // NOTA: magistrale *non* è quello con i corsi a cavallo
'anno-1': '64a7c1c651f079001d52e9c8', const TIMETABLE_IDS = semesterData.timetableIds
'anno-2': '6308e2dc09352a0208fefdd9',
'anno-3': '6308e42a1df5cb026699ced4',
'magistrale': '64a7c7091ab813002c5d9ede',
}
const DEFAULT_DATE_RANGE = { // const DEFAULT_DATE_RANGE = {
from: '2023-10-09T00:00:00.000Z', // from: '2023-10-09T00:00:00.000Z',
to: '2023-10-14T00:00:00.000Z', // to: '2023-10-14T00:00:00.000Z',
} // }
const DATE_RANGES = { // const DATE_RANGES = {
'64a7c1c651f079001d52e9c8': DEFAULT_DATE_RANGE, // '64a7c1c651f079001d52e9c8': DEFAULT_DATE_RANGE,
'6308e2dc09352a0208fefdd9': DEFAULT_DATE_RANGE, // '6308e2dc09352a0208fefdd9': DEFAULT_DATE_RANGE,
'6308e42a1df5cb026699ced4': DEFAULT_DATE_RANGE, // '6308e42a1df5cb026699ced4': DEFAULT_DATE_RANGE,
'64a7c7091ab813002c5d9ede': DEFAULT_DATE_RANGE, // '64a7c7091ab813002c5d9ede': DEFAULT_DATE_RANGE,
} // }
function specialEventPatches(eventi) { function specialEventPatches(eventi) {
// Il laboratorio del primo anno in realtà è in due canali separati // Il laboratorio del primo anno in realtà è in due gruppi separati
let i = 1
eventi.forEach(evento => { eventi.forEach(evento => {
if ( if (
evento.nome === evento.nome === 'LABORATORIO DI INTRODUZIONE ALLA MATEMATICA COMPUTAZIONALE'
'LABORATORIO DI INTRODUZIONE ALLA MATEMATICA COMPUTAZIONALE'
) { ) {
evento.nome += ` (${i})` if (evento.evento.dettagliDidattici[0].partizione.descrizione === 'CORSO A') {
i++ evento.nome += ' (A)'
}
if (evento.evento.dettagliDidattici[0].partizione.descrizione === 'CORSO B') {
evento.nome += ' (B)'
}
} }
}) })
@@ -71,15 +72,24 @@ function formatEvents(timetable) {
name: _.split(nome, '-', 1)[0].trim(), name: _.split(nome, '-', 1)[0].trim(),
start: new Date(dataInizio), start: new Date(dataInizio),
end: new Date(dataFine), end: new Date(dataFine),
docenti: docenti.map(({ nome, cognome }) => docenti: docenti.map(({ nome, cognome }) => prettyProfName(nome, cognome)),
prettyProfName(nome, cognome)
),
aule: aule.map(aula => prettyAulaName(aula.codice)), aule: aule.map(aula => prettyAulaName(aula.codice)),
} }
}) })
} }
async function loadCalendari() { async function loadCalendari(date) {
function getMonday(d) {
const day = d.getDay()
const diff = d.getDate() - day + (day === 0 ? -6 : 1)
const monday = new Date(d.setDate(diff))
monday.setUTCHours(0, 0, 0, 0)
return monday
}
const monday = getMonday(date)
const saturday = new Date(monday)
saturday.setDate(monday.getDate() + 5)
async function req(id) { async function req(id) {
// Almost directly copy-pasted from Chrome Dev Tools // Almost directly copy-pasted from Chrome Dev Tools
const req = await fetch( const req = await fetch(
@@ -97,8 +107,8 @@ async function loadCalendari() {
linkCalendarioId: id, linkCalendarioId: id,
clienteId: '628de8b9b63679f193b87046', clienteId: '628de8b9b63679f193b87046',
pianificazioneTemplate: false, pianificazioneTemplate: false,
dataInizio: DATE_RANGES[id].from, dataInizio: monday.toISOString(),
dataFine: DATE_RANGES[id].to, dataFine: saturday.toISOString(),
}), }),
method: 'POST', method: 'POST',
mode: 'cors', mode: 'cors',
@@ -119,7 +129,7 @@ async function loadCalendari() {
const timetablesRaw = results.map(timetable => const timetablesRaw = results.map(timetable =>
specialEventPatches(_.uniqBy(timetable, 'id')) specialEventPatches(_.uniqBy(timetable, 'id'))
) )
const allRaw = specialEventPatches(_.uniqBy(_.concat(...results), 'id')) const allRaw = specialEventPatches(_.concat(...results), 'id')
return { return {
'anno-1': formatEvents(timetablesRaw[0]), 'anno-1': formatEvents(timetablesRaw[0]),
@@ -130,37 +140,58 @@ async function loadCalendari() {
} }
} }
const View = ({ view, selection, setSelection, timetables }) => {
if (view === 'orario') {
return <Schedule selection={selection} timetables={timetables} />
} else if (view === 'lista') {
return (
<Courses
selection={selection}
setSelection={setSelection}
source={'tutti'}
timetables={timetables}
hideOtherCourses={true}
/>
)
} else {
return (
<Courses
selection={selection}
setSelection={setSelection}
source={view}
timetables={timetables}
hideOtherCourses={false}
/>
)
}
}
const App = ({}) => { const App = ({}) => {
// Clear persistent states unless state_token corresponds to the one passed // Clear persistent states unless state_token corresponds to the one passed
// as the argument. Useful with breaking updates. Change this token if your // as the argument. Useful with breaking updates. Change this token if your
// (breaking) update needs a reset of persistent states to avoid crashes. // (breaking) update needs a reset of persistent states to avoid crashes.
// //
// Use any random string of your choice // Use any random string of your choice
clearOldPersistentStates('e73cba02') // clearOldPersistentStates('e73cba02')
const [date, setDate] = useState(new Date().toISOString())
// Data Sources // Data Sources
const [source, setSource] = usePersistentState('source', 'magistrale') const [view, setView] = usePersistentState('view', 'magistrale')
const [timetables, setTimetables] = useState(null) const [timetables, setTimetables] = useState(null)
useEffect(async () => { useEffect(async () => {
setTimetables(await loadCalendari()) setTimetables(await loadCalendari(new Date(date)))
}, []) }, [date])
// View Modes // View Modes
// const [mode, setMode] = usePersistentState('orario.mode', MODE_COURSES) // const [mode, setMode] = usePersistentState('orario.mode', MODE_COURSES)
// Selection // Selection
const [selectedCourses, setSelectedCourses] = usePersistentState( const [selectedCourses, setSelectedCourses] = usePersistentState('selection', [])
'selection',
[]
)
// 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'
@@ -175,69 +206,90 @@ const App = ({}) => {
<> <>
<Toolbar <Toolbar
{...{ {...{
source, source: view,
setSource, setSource: setView,
onShowMenu: () => setShowMobileMenu(true), date: date,
setDate: setDate,
showMobileMenu: showMobileMenu,
setShowMobileMenu: setShowMobileMenu,
onHelp: () => setHelpVisible(true), onHelp: () => setHelpVisible(true),
theme, theme,
setTheme, setTheme,
}} }}
/> />
<OptionBar {showMobileMenu ? (
{...{ <SettingsBar
source,
setSource,
onHelp: () => setHelpVisible(true),
}}
orizzontale
/>
{timetables && (
<div class="content">
{source === 'orario' ? (
<Schedule
selection={selectedCourses}
setSelection={setSelectedCourses}
start={new Date(2022, 10, 3)}
source={source}
timetables={timetables}
/>
) : (
<Courses
selection={selectedCourses}
setSelection={setSelectedCourses}
start={new Date(2022, 10, 3)}
source={source}
timetables={timetables}
/>
)}
</div>
)}
{/* toolOverlayVisible && (
<ToolOverlay
mode={mode}
toggleMode={() =>
setMode(
mode === MODE_COURSES ? MODE_SCHEDULE : MODE_COURSES
)
}
onClose={() => {
setSelectedCourses([])
setMode(MODE_COURSES)
}}
/>
) */}
{showMobileMenu && (
<HamburgerMenu
{...{ {...{
theme, theme,
setTheme, setTheme,
onClose: () => { date,
setShowMobileMenu(false) setDate,
},
}} }}
/> />
) : (
<OptionBar
{...{
view: view,
setView: setView,
onHelp: () => setHelpVisible(true),
}}
orizzontale
/>
)} )}
<div class="content">
{timetables &&
(showMobileMenu ? (
<HamburgerMenu
{...{
date,
setDate,
theme,
setTheme,
onClose: () => {
setShowMobileMenu(false)
},
}}
/>
) : timetables['tutti'].length === 0 ? (
<div class="warning">
<p>
Non esistono corsi per la settimana selezionata: buone
vacanze! 🎉
</p>
<p>
Clicca sul bottone qua sotto per vedere la prima settimana
completa di lezioni:
</p>
<button
onClick={() => {
setDate(semesterData.firstMondayDate)
}}
>
{semesterData.buttonText}
</button>
{/* <p>
Per cambiare settimana puoi usare il widget Calendario (
<Icon name="calendar_month" />) in alto a destra
<br />
In versione mobile, il widget Calendario è situato dentro
il Menu (
<Icon name="menu" />)
</p> */}
</div>
) : (
<View
selection={selectedCourses}
setSelection={setSelectedCourses}
view={view}
timetables={timetables}
/>
))}
</div>
{/* showMobileMenu && (
) */}
{helpVisible && ( {helpVisible && (
<Popup <Popup
title={ title={
+186
View File
@@ -0,0 +1,186 @@
#!/usr/bin/env node
// Manual update script for semester timetable data
// Run this script manually each semester to update timetable IDs and dates
// Usage: node src/scripts/update-semester-data.js
import https from 'https';
import fs from 'fs';
// Get current academic year
function getCurrentAcademicYear() {
const now = new Date();
const currentYear = now.getFullYear();
const currentMonth = now.getMonth(); // 0-based
// Academic year starts in September, but August belongs to the new year
const startYear = currentMonth >= 7 ? currentYear : currentYear - 1;
const endYear = startYear + 1;
return { startYear, endYear };
}
// Fetch webpage content
function fetchPage(url) {
return new Promise((resolve, reject) => {
https.get(url, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
resolve(data);
});
}).on('error', (err) => {
reject(err);
});
});
}
// Parse timetable IDs from the schedule page
async function parseTimetableIds() {
try {
const html = await fetchPage('https://www.dm.unipi.it/didattica/lezioni-esami/orario-delle-lezioni/');
const ids = {};
// Extract links with their text to match correctly
const linkPattern = /<a[^>]*href="[^"]*linkCalendarioId=([a-f0-9]{24})"[^>]*>([^<]+)<\/a>/g;
const links = [...html.matchAll(linkPattern)];
for (const [fullMatch, id, text] of links) {
const cleanText = text.trim();
if (cleanText === 'Triennale I anno') {
ids['anno-1'] = id;
} else if (cleanText === 'Triennale II anno') {
ids['anno-2'] = id;
} else if (cleanText === 'Triennale III anno') {
ids['anno-3'] = id;
} else if (cleanText === 'Magistrale') {
ids['magistrale'] = id;
}
}
return Object.keys(ids).length > 0 ? ids : null;
} catch (error) {
console.error('Error parsing timetable IDs:', error.message);
return null;
}
}
// Parse semester start date from calendar page
async function parseSemesterStartDate() {
try {
const { startYear, endYear } = getCurrentAcademicYear();
const currentMonth = new Date().getMonth();
const currentSemester = currentMonth >= 0 && currentMonth <= 6 ? 2 : 1;
const url = `https://www.dm.unipi.it/didattica/lezioni-esami/calendario-delle-attivita-didattiche/calendario-delle-attivita-didattiche-a-a-${startYear}-${endYear.toString().slice(-2)}/`;
const html = await fetchPage(url);
// Look for the appropriate semester start date
const semesterText = currentSemester === 1 ? 'I semestre' : 'II semestre';
const semesterRegex = new RegExp(`Lezioni\\s+${semesterText}.*?(\\d{1,2}\\s+\\w+\\s+\\d{4})`, 'si');
const match = html.match(semesterRegex);
if (match) {
return match[1];
} else {
console.error(`Could not find ${semesterText} start date in calendar`);
return null;
}
} catch (error) {
console.error('Error parsing semester start date:', error.message);
return null;
}
}
// Find the first Monday after a given date string
function getFirstMondayAfter(dateStr) {
// Parse Italian date format (e.g., "24 settembre 2025")
const [day, monthName, year] = dateStr.split(' ');
const monthMap = {
'gennaio': 0, 'febbraio': 1, 'marzo': 2, 'aprile': 3,
'maggio': 4, 'giugno': 5, 'luglio': 6, 'agosto': 7,
'settembre': 8, 'ottobre': 9, 'novembre': 10, 'dicembre': 11
};
const month = monthMap[monthName.toLowerCase()];
if (month === undefined) {
throw new Error(`Unknown month: ${monthName}`);
}
const startDate = new Date(parseInt(year), month, parseInt(day));
const dayOfWeek = startDate.getDay(); // 0=Sunday, 1=Monday, etc.
// Calculate days to add to get to the next Monday
let daysToAdd;
if (dayOfWeek === 0) { // Sunday
daysToAdd = 1; // Next day is Monday
} else if (dayOfWeek === 1) { // Monday
daysToAdd = 7; // Next Monday
} else { // Tuesday-Saturday (2-6)
daysToAdd = 8 - dayOfWeek; // Days until next Monday
}
const result = new Date(startDate);
result.setDate(startDate.getDate() + daysToAdd);
// Create a new date in UTC to avoid timezone issues with the calendar
return new Date(Date.UTC(result.getFullYear(), result.getMonth(), result.getDate()));
}
// Format date for button text (Italian format)
function formatDateForButton(date) {
const day = date.getUTCDate();
const monthNames = [
'gennaio', 'febbraio', 'marzo', 'aprile', 'maggio', 'giugno',
'luglio', 'agosto', 'settembre', 'ottobre', 'novembre', 'dicembre'
];
const month = monthNames[date.getUTCMonth()];
return `${day} ${month}`;
}
// Main function
async function updateAcademicData() {
const [timetableIds, semesterStartDateStr] = await Promise.all([
parseTimetableIds(),
parseSemesterStartDate()
]);
// Fail fast if parsing failed
if (!timetableIds) {
throw new Error('Failed to parse timetable IDs from the website');
}
if (!semesterStartDateStr) {
throw new Error('Failed to parse semester start date from the calendar');
}
const firstMonday = getFirstMondayAfter(semesterStartDateStr);
const semesterData = {
timetableIds,
semesterStartDateStr,
firstMondayDate: firstMonday.toISOString(),
buttonText: `Vai al ${formatDateForButton(firstMonday)}! 🚀`,
lastUpdated: new Date().toISOString(),
academicYear: getCurrentAcademicYear()
};
// Save to src/semester-data.json
fs.writeFileSync('src/semester-data.json', JSON.stringify(semesterData, null, 2));
console.log('✅ Semester data updated successfully');
}
// Run the script
updateAcademicData().catch(error => {
console.error('💥 Script failed:', error);
process.exit(1);
});
+16
View File
@@ -0,0 +1,16 @@
{
"timetableIds": {
"anno-1": "687e66d62d13e6001edd366d",
"anno-2": "687e681301ee9300198320e6",
"anno-3": "687e68bb01ee9300198320eb",
"magistrale": "687e6a9f96ea080019d439d5"
},
"semesterStartDateStr": "23 settembre 2026",
"firstMondayDate": "2026-09-28T00:00:00.000Z",
"buttonText": "Vai al 28 settembre! 🚀",
"lastUpdated": "2026-09-02T19:43:46.921Z",
"academicYear": {
"startYear": 2026,
"endYear": 2027
}
}
+151 -106
View File
@@ -374,6 +374,47 @@ button,
} }
} }
.date-picker {
position: relative;
display: flex;
// align-items: center;
justify-content: center;
height: 2.5rem;
padding-left: 0.75rem;
padding-right: 0.75rem;
background: var(--bg-500);
border: 2px solid var(--border-400);
border-radius: 0.5rem;
font-weight: 400;
user-select: none;
cursor: pointer;
input {
z-index: -1;
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 0;
opacity: 0.2;
}
.date-picker-render {
display: flex;
align-items: center;
gap: 0.5rem;
.material-symbols-outlined {
font-size: 18px;
}
}
}
// Extension Classes // Extension Classes
.panel { .panel {
@@ -444,7 +485,8 @@ body {
} }
} }
.option-bar { .option-bar,
.settings-bar {
@extend .panel; @extend .panel;
padding: 0.5rem; padding: 0.5rem;
@@ -469,11 +511,21 @@ body {
display: flex; display: flex;
} }
.option-group { .option-group,
.settings-group {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.75rem; gap: 0.75rem;
width: 100%;
justify-content: space-evenly;
}
}
.settings-bar {
@media screen and (max-width: $device-s-width), (pointer: coarse) {
display: flex;
justify-content: space-between;
} }
} }
@@ -482,17 +534,37 @@ body {
@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(100vh - 8rem);
} }
overflow-y: scroll;
padding: 1rem 0rem;
.warning {
@extend .text-block;
width: 100%;
display: flex;
flex-direction: column;
gap: 1rem;
align-items: center;
p {
text-align: center;
}
}
.course-view { .course-view {
padding: 1rem; padding: 0rem 1rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
height: 100%; min-height: 100%;
overflow-y: scroll; text-align: center;
gap: 1rem;
.wrap-container { .wrap-container {
display: grid; display: grid;
@@ -550,30 +622,18 @@ body {
} }
.schedule-view { .schedule-view {
height: 100%; min-height: 100%;
width: 100%; width: 100%;
max-width: 57rem; max-width: 57rem;
margin: auto; margin: auto;
padding: 1rem 0.5rem; padding: 0rem 0.5rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: 1rem; gap: 1rem;
.no-courses-warning {
width: 100%;
display: flex;
flex-direction: column;
gap: 1rem;
align-items: center;
p {
text-align: center;
}
}
.schedule-card { .schedule-card {
width: 100%; width: 100%;
@@ -584,8 +644,7 @@ 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), @media screen and (max-width: $device-s-width), (pointer: coarse) {
(pointer: coarse) {
font-size: 12px; font-size: 12px;
} }
@@ -640,10 +699,7 @@ body {
&.original { &.original {
grid-template-columns: auto repeat(5, 1fr); grid-template-columns: auto repeat(5, 1fr);
grid-template-rows: min-content repeat( grid-template-rows: min-content repeat(var(--time-slots), 1fr);
var(--time-slots),
1fr
);
.transpose-button, .transpose-button,
.day-label { .day-label {
@@ -670,22 +726,16 @@ body {
} }
.event-block-wrapper { .event-block-wrapper {
grid-row: calc(var(--time-start) + 2) / grid-row: calc(var(--time-start) + 2) / calc(var(--time-end) + 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( height: calc(100% * var(--size) / var(--block-size));
100% * var(--size) / var(--block-size)
);
transform: translateX(calc(100% * var(--index))) transform: translateX(calc(100% * var(--index)))
translateY( translateY(
calc( calc(100% * var(--relative-start) / var(--size))
100% * var(--relative-start) /
var(--size)
)
); );
} }
} }
@@ -693,10 +743,7 @@ body {
} }
&.transposed { &.transposed {
grid-template-rows: auto repeat(5, 1fr); grid-template-rows: auto repeat(5, 1fr);
grid-template-columns: min-content repeat( grid-template-columns: min-content repeat(var(--time-slots), 1fr);
var(--time-slots),
1fr
);
.transpose-button, .transpose-button,
.day-label { .day-label {
@@ -732,15 +779,10 @@ body {
.event-block { .event-block {
.event-wrapper { .event-wrapper {
height: calc(100% / var(--of)); height: calc(100% / var(--of));
width: calc( width: calc(100% * var(--size) / var(--block-size));
100% * var(--size) / var(--block-size)
);
transform: translateY(calc(100% * var(--index))) transform: translateY(calc(100% * var(--index)))
translateX( translateX(
calc( calc(100% * var(--relative-start) / var(--size))
100% * var(--relative-start) /
var(--size)
)
); );
} }
} }
@@ -773,6 +815,70 @@ body {
} }
} }
} }
.menu {
// position: absolute;
// top: 0;
// left: 0;
width: 100%;
min-height: 100%;
background: var(--bg-500);
z-index: 10;
.header {
height: 4rem;
padding: 0.75rem 1rem 0.75rem 0.75rem;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--border-500);
.option-group {
display: flex;
gap: 0.5rem;
}
}
.options {
padding: 1rem;
display: grid;
grid-template-columns: auto 1fr;
gap: 1rem;
align-items: center;
.label {
font-weight: 400;
}
}
hr {
position: relative;
width: calc(100% - 2rem);
height: 1px;
left: 1rem;
background: var(--border-500);
border: none;
}
.help {
@extend .text-block;
padding: 1rem 1rem 2.5rem;
height: 100%;
overflow-y: scroll;
}
}
} }
.overlay { .overlay {
@@ -808,67 +914,6 @@ body {
animation: fade-in 150ms ease-in forwards; animation: fade-in 150ms ease-in forwards;
} }
.menu {
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: var(--bg-500);
z-index: 10;
.header {
height: 4rem;
padding: 0.75rem 1rem 0.75rem 0.75rem;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--border-500);
.option-group {
display: flex;
gap: 0.5rem;
}
}
.options {
padding: 1rem;
display: grid;
grid-template-columns: auto 1fr;
gap: 1rem;
align-items: center;
.label {
font-weight: 400;
}
}
hr {
position: relative;
width: calc(100% - 2rem);
height: 1px;
left: 1rem;
background: var(--border-500);
border: none;
}
.help {
@extend .text-block;
padding: 1rem 1rem 2.5rem;
}
}
// not on mobile // not on mobile
@media screen and (min-width: $device-s-width) and (pointer: fine) { @media screen and (min-width: $device-s-width) and (pointer: fine) {
+5 -18
View File
@@ -12,15 +12,7 @@ export const WEEK_DAYS = [
'Sabato', 'Sabato',
] ]
export const WEEK_DAYS_SHORT = [ export const WEEK_DAYS_SHORT = ['Dom', 'Lun', 'Mar', 'Mer', 'Gio', 'Ven', 'Sab']
'Dom',
'Lun',
'Mar',
'Mer',
'Gio',
'Ven',
'Sab'
]
// Hashing // Hashing
@@ -32,12 +24,8 @@ 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 = h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909)
Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909)
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)
} }
@@ -50,9 +38,7 @@ 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( return /(^del|^nel|^di$|^dei$|^con$|^alla$|^per$|^e$|^la$)/.test(word)
word
)
? word ? word
: word[0].toUpperCase() + word.slice(1) : word[0].toUpperCase() + word.slice(1)
}) })
@@ -61,6 +47,7 @@ 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) {