Compare commits
30
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ae010a715 | ||
|
|
75af3ccd56 | ||
|
|
97e3758bc1 | ||
|
|
48445020c8 | ||
|
|
b21cc30c8a | ||
|
|
d0c2f58094 | ||
|
|
6ce7979719 | ||
|
|
e9539b7f35 | ||
|
|
c89b4a0837 | ||
|
|
4c98950822 | ||
|
|
39ccb715db | ||
|
|
d929aa6cfe | ||
|
|
db72898471 | ||
|
|
4d95a1a633 | ||
|
|
65b800806f | ||
|
|
935309147c | ||
|
|
fb551fb719 | ||
|
|
c731d9c8b9 | ||
|
|
d9d2aaed11 | ||
|
|
7bbd9ba139 | ||
|
|
62a9b80b21 | ||
|
|
c43bdc60e7 | ||
|
|
c8172ad88a | ||
|
|
c0b2894c22 | ||
|
|
fd0440465c | ||
|
|
bdbf031bce | ||
|
|
32dc9a6104 | ||
|
|
2d4a3ce664 | ||
|
|
af41177561 | ||
|
|
22ce830c80 |
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"printWidth": 90,
|
||||
"singleQuote": true,
|
||||
"quoteProps": "consistent",
|
||||
"tabWidth": 4,
|
||||
"semi": false,
|
||||
"arrowParens": "avoid",
|
||||
"proseWrap": "always"
|
||||
}
|
||||
-40
@@ -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"
|
||||
]
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -24,7 +24,6 @@ const viewModeMap = {
|
||||
|
||||
export const EventsView = ({ mode, source, ...viewProps }) => {
|
||||
// const Mode = viewModeMap[mode]
|
||||
|
||||
|
||||
if (source === 'orario') {
|
||||
return (
|
||||
|
||||
@@ -1,27 +1,17 @@
|
||||
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 { Icon } from './Icon.jsx'
|
||||
|
||||
export const HamburgerMenu = ({ onClose, theme, setTheme }) => {
|
||||
export const HamburgerMenu = ({ date, setDate, onClose, theme, setTheme }) => {
|
||||
return (
|
||||
<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">
|
||||
<h2>
|
||||
<Icon name="info" />
|
||||
|
||||
+17
-13
@@ -4,27 +4,31 @@ export const Help = ({}) => (
|
||||
<>
|
||||
<h3>Visualizzazione Corsi</h3>
|
||||
<p>
|
||||
Per visualizzare i corsi che ti interessano nella tabella
|
||||
dell'orario, devi prima selezionarli. Puoi selezionare dei corsi
|
||||
cliccandoli, cercandoli nelle sezioni Primo anno (<b>I</b>), Secondo
|
||||
anno (<b>II</b>), Terzo anno (<b>III</b>), Magistrale (<b>M</b>), o
|
||||
Tutti.
|
||||
Per visualizzare i corsi che ti interessano nella tabella dell'orario, devi
|
||||
prima selezionarli. Puoi selezionare dei corsi cliccandoli, cercandoli nelle
|
||||
sezioni Primo anno (<b>I</b>), Secondo anno (<b>II</b>), Terzo anno (
|
||||
<b>III</b>), Magistrale (<b>M</b>), o Tutti.
|
||||
</p>
|
||||
<p>
|
||||
Una volta compiuta la selezione, è possibile vedere la tabella delle
|
||||
lezioni andando nella visualizzazione Orario
|
||||
Una volta compiuta la selezione, è possibile vedere la tabella delle lezioni
|
||||
andando nella visualizzazione Orario (
|
||||
<Icon name="calendar_view_month" />)
|
||||
</p>
|
||||
<p>
|
||||
Per via di eventuali preferenze personali, è possibile cambiare
|
||||
l'orientazione della tabella Orario trasponendola, utilizzando il
|
||||
pulsante Trasponi (
|
||||
Per via di eventuali preferenze personali, è possibile cambiare l'orientazione
|
||||
della tabella Orario trasponendola, utilizzando il pulsante Trasponi (
|
||||
<Icon name="switch_left" style="transform: rotate(-45deg)" />)
|
||||
</p>
|
||||
<p>
|
||||
È anche possibile visualizzare in uno specchietto riassuntivo soltanto i corsi
|
||||
selezionati, andando nella visualizzazione Lista (
|
||||
<Icon name="list" />)
|
||||
</p>
|
||||
<h3>Stampa</h3>
|
||||
<p>
|
||||
Da desktop puoi stampare l'orario attualmente visibile con il
|
||||
bottone <Icon name="print" /> (è consigliato controllare le opzioni
|
||||
di stampa per ottenere un risultato soddisfacente).
|
||||
Da desktop puoi stampare l'orario attualmente visibile con il bottone{' '}
|
||||
<Icon name="print" /> (è consigliato controllare le opzioni di stampa per
|
||||
ottenere un risultato soddisfacente).
|
||||
</p>
|
||||
<h3>Bug & Contatti</h3>
|
||||
<p>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { CompoundButton } from './CompoundButton.jsx'
|
||||
import { Icon } from './Icon.jsx'
|
||||
|
||||
export const OptionBar = ({ source, setSource }) => {
|
||||
export const OptionBar = ({ view, setView }) => {
|
||||
return (
|
||||
<div class="option-bar">
|
||||
<div class="option-group">
|
||||
@@ -14,20 +14,25 @@ export const OptionBar = ({ source, setSource }) => {
|
||||
{ value: 'magistrale', label: 'M' },
|
||||
{ value: 'tutti', label: 'Tutti' },
|
||||
]}
|
||||
value={source}
|
||||
setValue={setSource}
|
||||
value={view}
|
||||
setValue={setView}
|
||||
/>
|
||||
</div>
|
||||
<CompoundButton
|
||||
options={[
|
||||
{
|
||||
value: 'orario',
|
||||
label: <Icon name="calendar_month" />,
|
||||
label: <Icon name="calendar_view_month" />,
|
||||
icon: true,
|
||||
},
|
||||
{
|
||||
value: 'lista',
|
||||
label: <Icon name="list" />,
|
||||
icon: true,
|
||||
},
|
||||
]}
|
||||
value={source}
|
||||
setValue={setSource}
|
||||
value={view}
|
||||
setValue={setView}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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
@@ -1,10 +1,14 @@
|
||||
import { CompoundButton } from './CompoundButton.jsx'
|
||||
import { DatePicker } from './DatePicker.jsx'
|
||||
import { Icon } from './Icon.jsx'
|
||||
|
||||
export const Toolbar = ({
|
||||
source,
|
||||
setSource,
|
||||
onShowMenu,
|
||||
date,
|
||||
setDate,
|
||||
showMobileMenu,
|
||||
setShowMobileMenu,
|
||||
onHelp,
|
||||
theme,
|
||||
setTheme,
|
||||
@@ -12,13 +16,15 @@ export const Toolbar = ({
|
||||
return (
|
||||
<div class="toolbar">
|
||||
<div class="mobile">
|
||||
<button class="flat icon" onClick={onShowMenu}>
|
||||
<Icon name="menu" />
|
||||
<button
|
||||
class="flat icon"
|
||||
onClick={() => setShowMobileMenu(!showMobileMenu)}
|
||||
>
|
||||
<Icon name={`${showMobileMenu ? 'close' : 'menu'}`} />
|
||||
</button>
|
||||
</div>
|
||||
<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 class="option-group">
|
||||
<div class="item option">
|
||||
@@ -38,12 +44,16 @@ export const Toolbar = ({
|
||||
<CompoundButton
|
||||
options={[
|
||||
{ value: 'orario', label: 'Orario' },
|
||||
{ value: 'lista', label: 'Lista' },
|
||||
]}
|
||||
value={source}
|
||||
setValue={setSource}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="option-group">
|
||||
<DatePicker date={date} setDate={setDate} />
|
||||
</div>
|
||||
<div class="option-group">
|
||||
<div class="item option">
|
||||
<button class="icon" onClick={() => window.print()}>
|
||||
@@ -53,13 +63,9 @@ export const Toolbar = ({
|
||||
<div class="item option">
|
||||
<button
|
||||
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>
|
||||
</div>
|
||||
<div class="item option">
|
||||
|
||||
@@ -3,12 +3,23 @@ import { format } from 'date-fns'
|
||||
import _ from 'lodash'
|
||||
import { useEffect, useRef, useState } from 'preact/hooks'
|
||||
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 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 =>
|
||||
_.uniq(events.flatMap(event => event.docenti))
|
||||
@@ -42,6 +53,15 @@ export const Courses = ({ source, timetables, selection, setSelection }) => {
|
||||
|
||||
return (
|
||||
<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">
|
||||
{Object.entries(eventsByCourse).map(([id, courseEvents]) => (
|
||||
<div
|
||||
|
||||
@@ -3,11 +3,7 @@ import { useEffect, useRef, useState } from 'preact/hooks'
|
||||
import _ from 'lodash'
|
||||
import { differenceInMinutes, startOfDay } from 'date-fns'
|
||||
|
||||
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 { Popup } from '../Popup.jsx'
|
||||
import { Icon } from '../Icon.jsx'
|
||||
@@ -17,28 +13,23 @@ const TransposePopup = ({ onClose }) => {
|
||||
<Popup
|
||||
title={
|
||||
<>
|
||||
<Icon name="info" /> Attenzione! La tabella è stata
|
||||
trasposta!
|
||||
<Icon name="info" /> Attenzione! La tabella è stata trasposta!
|
||||
</>
|
||||
}
|
||||
onClose={onClose}
|
||||
>
|
||||
<p>A grande richiesta popolare abbiamo trasposto la tabella dell'orario!</p>
|
||||
|
||||
<p>
|
||||
A grande richiesta popolare abbiamo trasposto la tabella
|
||||
dell'orario!
|
||||
Assicurati quindi di leggerla correttamente (dall'alto verso il basso
|
||||
invece che da sinistra verso destra).
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Assicurati quindi di leggerla correttamente (dall'alto verso il
|
||||
basso invece che da sinistra verso destra).
|
||||
</p>
|
||||
|
||||
<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
|
||||
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>
|
||||
</Popup>
|
||||
)
|
||||
@@ -46,18 +37,17 @@ const TransposePopup = ({ onClose }) => {
|
||||
|
||||
const NoCourseWarning = () => {
|
||||
return (
|
||||
<div class="no-courses-warning">
|
||||
<div class="warning">
|
||||
<p>Non hai ancora selezionato nessun corso.</p>
|
||||
<p>
|
||||
Clicca sui corsi nelle altre visuali per selezionarli e
|
||||
visualizzarli nell'orario
|
||||
Clicca sui corsi nelle altre visuali per selezionarli e visualizzarli
|
||||
nell'orario
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Layout = ({ layout, day, colors }) => {
|
||||
console.log(layout)
|
||||
return (
|
||||
<>
|
||||
{layout.map(block => (
|
||||
@@ -76,13 +66,10 @@ const Layout = ({ layout, day, colors }) => {
|
||||
style={{
|
||||
'--block-size': block.end - block.start,
|
||||
'--size': event.end - event.start,
|
||||
'--relative-start':
|
||||
event.start - block.start,
|
||||
'--relative-start': event.start - block.start,
|
||||
'--index': event.index,
|
||||
'--of': block.layers,
|
||||
'--color': `var(--event-${
|
||||
colors[event.id]
|
||||
})`,
|
||||
'--color': `var(--event-${colors[event.id]})`,
|
||||
}}
|
||||
>
|
||||
<div class="event">
|
||||
@@ -119,16 +106,11 @@ const ScheduleGrid = ({
|
||||
class="small"
|
||||
onClick={() =>
|
||||
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>
|
||||
</div>
|
||||
{[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(
|
||||
'transpose_info',
|
||||
'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 selectionSet = new Set(selection)
|
||||
|
||||
+3
-10
@@ -42,10 +42,7 @@ function layoutBlockEvents(events) {
|
||||
let viableIndex = 0
|
||||
while (
|
||||
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
|
||||
) {
|
||||
viableIndex += 1
|
||||
@@ -57,8 +54,7 @@ function layoutBlockEvents(events) {
|
||||
return result
|
||||
}
|
||||
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)
|
||||
|
||||
@@ -68,10 +64,7 @@ export function layoutEvents(events) {
|
||||
if (blocks.length > 0) {
|
||||
layout = layout.filter(block => !overlap(event, block))
|
||||
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)),
|
||||
events: blocks.flatMap(block => block.events).concat([event]),
|
||||
})
|
||||
|
||||
+144
-92
@@ -1,6 +1,7 @@
|
||||
import _ from 'lodash'
|
||||
import { render } from 'preact'
|
||||
import { useEffect, useState } from 'preact/hooks'
|
||||
import semesterData from './semester-data.json'
|
||||
|
||||
// import { ToolOverlay } from './components/ToolOverlay.jsx'
|
||||
//
|
||||
@@ -25,39 +26,39 @@ import {
|
||||
usePersistentState,
|
||||
} from './utils.jsx'
|
||||
|
||||
import { SettingsBar } from './components/SettingsBar.jsx'
|
||||
|
||||
// Che fanno queste due righe?
|
||||
window._ = _
|
||||
window.dataBuffer = {}
|
||||
|
||||
const TIMETABLE_IDS = {
|
||||
'anno-1': '64a7c1c651f079001d52e9c8',
|
||||
'anno-2': '6308e2dc09352a0208fefdd9',
|
||||
'anno-3': '6308e42a1df5cb026699ced4',
|
||||
'magistrale': '64a7c7091ab813002c5d9ede',
|
||||
}
|
||||
// NOTA: magistrale *non* è quello con i corsi a cavallo
|
||||
const TIMETABLE_IDS = semesterData.timetableIds
|
||||
|
||||
const DEFAULT_DATE_RANGE = {
|
||||
from: '2023-10-09T00:00:00.000Z',
|
||||
to: '2023-10-14T00:00:00.000Z',
|
||||
}
|
||||
// const DEFAULT_DATE_RANGE = {
|
||||
// from: '2023-10-09T00:00:00.000Z',
|
||||
// to: '2023-10-14T00:00:00.000Z',
|
||||
// }
|
||||
|
||||
const DATE_RANGES = {
|
||||
'64a7c1c651f079001d52e9c8': DEFAULT_DATE_RANGE,
|
||||
'6308e2dc09352a0208fefdd9': DEFAULT_DATE_RANGE,
|
||||
'6308e42a1df5cb026699ced4': DEFAULT_DATE_RANGE,
|
||||
'64a7c7091ab813002c5d9ede': DEFAULT_DATE_RANGE,
|
||||
}
|
||||
// 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
|
||||
// Il laboratorio del primo anno in realtà è in due gruppi separati
|
||||
eventi.forEach(evento => {
|
||||
if (
|
||||
evento.nome ===
|
||||
'LABORATORIO DI INTRODUZIONE ALLA MATEMATICA COMPUTAZIONALE'
|
||||
evento.nome === 'LABORATORIO DI INTRODUZIONE ALLA MATEMATICA COMPUTAZIONALE'
|
||||
) {
|
||||
evento.nome += ` (${i})`
|
||||
i++
|
||||
if (evento.evento.dettagliDidattici[0].partizione.descrizione === 'CORSO A') {
|
||||
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(),
|
||||
start: new Date(dataInizio),
|
||||
end: new Date(dataFine),
|
||||
docenti: docenti.map(({ nome, cognome }) =>
|
||||
prettyProfName(nome, cognome)
|
||||
),
|
||||
docenti: docenti.map(({ nome, cognome }) => prettyProfName(nome, cognome)),
|
||||
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) {
|
||||
// Almost directly copy-pasted from Chrome Dev Tools
|
||||
const req = await fetch(
|
||||
@@ -97,8 +107,8 @@ async function loadCalendari() {
|
||||
linkCalendarioId: id,
|
||||
clienteId: '628de8b9b63679f193b87046',
|
||||
pianificazioneTemplate: false,
|
||||
dataInizio: DATE_RANGES[id].from,
|
||||
dataFine: DATE_RANGES[id].to,
|
||||
dataInizio: monday.toISOString(),
|
||||
dataFine: saturday.toISOString(),
|
||||
}),
|
||||
method: 'POST',
|
||||
mode: 'cors',
|
||||
@@ -119,7 +129,7 @@ async function loadCalendari() {
|
||||
const timetablesRaw = results.map(timetable =>
|
||||
specialEventPatches(_.uniqBy(timetable, 'id'))
|
||||
)
|
||||
const allRaw = specialEventPatches(_.uniqBy(_.concat(...results), 'id'))
|
||||
const allRaw = specialEventPatches(_.concat(...results), 'id')
|
||||
|
||||
return {
|
||||
'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 = ({}) => {
|
||||
// Clear persistent states unless state_token corresponds to the one passed
|
||||
// as the argument. Useful with breaking updates. Change this token if your
|
||||
// (breaking) update needs a reset of persistent states to avoid crashes.
|
||||
//
|
||||
// Use any random string of your choice
|
||||
clearOldPersistentStates('e73cba02')
|
||||
// clearOldPersistentStates('e73cba02')
|
||||
|
||||
const [date, setDate] = useState(new Date().toISOString())
|
||||
|
||||
// Data Sources
|
||||
const [source, setSource] = usePersistentState('source', 'magistrale')
|
||||
const [view, setView] = usePersistentState('view', 'magistrale')
|
||||
const [timetables, setTimetables] = useState(null)
|
||||
useEffect(async () => {
|
||||
setTimetables(await loadCalendari())
|
||||
}, [])
|
||||
setTimetables(await loadCalendari(new Date(date)))
|
||||
}, [date])
|
||||
|
||||
// View Modes
|
||||
// const [mode, setMode] = usePersistentState('orario.mode', MODE_COURSES)
|
||||
|
||||
// Selection
|
||||
const [selectedCourses, setSelectedCourses] = usePersistentState(
|
||||
'selection',
|
||||
[]
|
||||
)
|
||||
const [selectedCourses, setSelectedCourses] = usePersistentState('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(
|
||||
'theme',
|
||||
'light'
|
||||
@@ -175,69 +206,90 @@ const App = ({}) => {
|
||||
<>
|
||||
<Toolbar
|
||||
{...{
|
||||
source,
|
||||
setSource,
|
||||
onShowMenu: () => setShowMobileMenu(true),
|
||||
source: view,
|
||||
setSource: setView,
|
||||
date: date,
|
||||
setDate: setDate,
|
||||
showMobileMenu: showMobileMenu,
|
||||
setShowMobileMenu: setShowMobileMenu,
|
||||
onHelp: () => setHelpVisible(true),
|
||||
theme,
|
||||
setTheme,
|
||||
}}
|
||||
/>
|
||||
<OptionBar
|
||||
{...{
|
||||
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
|
||||
{showMobileMenu ? (
|
||||
<SettingsBar
|
||||
{...{
|
||||
theme,
|
||||
setTheme,
|
||||
onClose: () => {
|
||||
setShowMobileMenu(false)
|
||||
},
|
||||
date,
|
||||
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 && (
|
||||
<Popup
|
||||
title={
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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
@@ -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
|
||||
|
||||
.panel {
|
||||
@@ -444,7 +485,8 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
.option-bar {
|
||||
.option-bar,
|
||||
.settings-bar {
|
||||
@extend .panel;
|
||||
|
||||
padding: 0.5rem;
|
||||
@@ -469,11 +511,21 @@ body {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.option-group {
|
||||
.option-group,
|
||||
.settings-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
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) {
|
||||
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 {
|
||||
padding: 1rem;
|
||||
padding: 0rem 1rem;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
|
||||
overflow-y: scroll;
|
||||
text-align: center;
|
||||
|
||||
gap: 1rem;
|
||||
|
||||
.wrap-container {
|
||||
display: grid;
|
||||
@@ -550,30 +622,18 @@ body {
|
||||
}
|
||||
|
||||
.schedule-view {
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
width: 100%;
|
||||
max-width: 57rem;
|
||||
margin: auto;
|
||||
|
||||
padding: 1rem 0.5rem;
|
||||
padding: 0rem 0.5rem;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
|
||||
.no-courses-warning {
|
||||
width: 100%;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
|
||||
p {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
.schedule-card {
|
||||
width: 100%;
|
||||
|
||||
@@ -584,8 +644,7 @@ body {
|
||||
border: 1px solid var(--border-600);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -640,10 +699,7 @@ body {
|
||||
|
||||
&.original {
|
||||
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,
|
||||
.day-label {
|
||||
@@ -670,22 +726,16 @@ body {
|
||||
}
|
||||
|
||||
.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);
|
||||
|
||||
.event-block {
|
||||
.event-wrapper {
|
||||
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)))
|
||||
translateY(
|
||||
calc(
|
||||
100% * var(--relative-start) /
|
||||
var(--size)
|
||||
)
|
||||
calc(100% * var(--relative-start) / var(--size))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -693,10 +743,7 @@ body {
|
||||
}
|
||||
&.transposed {
|
||||
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,
|
||||
.day-label {
|
||||
@@ -732,15 +779,10 @@ body {
|
||||
.event-block {
|
||||
.event-wrapper {
|
||||
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)))
|
||||
translateX(
|
||||
calc(
|
||||
100% * var(--relative-start) /
|
||||
var(--size)
|
||||
)
|
||||
calc(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 {
|
||||
@@ -808,67 +914,6 @@ body {
|
||||
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
|
||||
|
||||
@media screen and (min-width: $device-s-width) and (pointer: fine) {
|
||||
|
||||
+5
-18
@@ -12,15 +12,7 @@ export const WEEK_DAYS = [
|
||||
'Sabato',
|
||||
]
|
||||
|
||||
export const WEEK_DAYS_SHORT = [
|
||||
'Dom',
|
||||
'Lun',
|
||||
'Mar',
|
||||
'Mer',
|
||||
'Gio',
|
||||
'Ven',
|
||||
'Sab'
|
||||
]
|
||||
export const WEEK_DAYS_SHORT = ['Dom', 'Lun', 'Mar', 'Mer', 'Gio', 'Ven', 'Sab']
|
||||
|
||||
// Hashing
|
||||
|
||||
@@ -32,12 +24,8 @@ export function hashString(str, seed = 0) {
|
||||
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)
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -50,9 +38,7 @@ export function prettyCourseName(name) {
|
||||
.map(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[0].toUpperCase() + word.slice(1)
|
||||
})
|
||||
@@ -61,6 +47,7 @@ export function prettyCourseName(name) {
|
||||
.replaceAll('IIi', 'III')
|
||||
.replaceAll('Iii', 'III')
|
||||
.replaceAll(/'(.)/g, ({}, letter) => "'" + letter.toUpperCase())
|
||||
.replaceAll(/\((a|b)\)/g, ({}, letter) => '(' + letter.toUpperCase() + ')')
|
||||
}
|
||||
|
||||
export function prettyAulaName(name) {
|
||||
|
||||
Reference in New Issue
Block a user