Compare commits

..

72 Commits

Author SHA1 Message Date
Francesco Minnocci 6ce7979719 Merge pull request 'Fix lab1 split courses recognition, add snapping!' (#18) from dev into main
continuous-integration/drone/push Build is passing Details
Reviewed-on: phc/orario#18

:3
2 weeks ago
Francesco Minnocci e9539b7f35 Fix lab1 split courses recognition, add snapping! 2 weeks ago
Antonio De Lucreziis c89b4a0837 Merge pull request 'chore: updated to new semester calendar ids' (#17) from dev into main
continuous-integration/drone/push Build is passing Details
Reviewed-on: phc/orario#17
4 weeks ago
Antonio De Lucreziis 4c98950822 chore: updated to new semester calendar ids 4 weeks ago
Antonio De Lucreziis 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 Details
Reviewed-on: phc/orario#16
7 months ago
Antonio De Lucreziis d929aa6cfe chore: updated calendar ids to first semester of 2024/2025 7 months ago
Antonio De Lucreziis db72898471 Merge pull request 'chore: empty commit to trigger deploy' (#15) from dev into main
continuous-integration/drone/push Build is passing Details
Reviewed-on: phc/orario#15
1 year ago
Antonio De Lucreziis 4d95a1a633 chore: empty commit to trigger deploy 1 year ago
Antonio De Lucreziis 65b800806f Merge pull request 'Fixed iOS date picker bug' (#14) from dev into main
Reviewed-on: phc/orario#14
1 year ago
Antonio De Lucreziis 935309147c cmd: prettier -w src/ 1 year ago
Antonio De Lucreziis fb551fb719 fix: safari ios showPicker() bug 1 year ago
Francesco Minnocci c731d9c8b9 Merge pull request 'chore: fix LIMCO courses' names' (#13) from dev into main
continuous-integration/drone/push Build is passing Details
Reviewed-on: phc/orario#13
1 year ago
Francesco Minnocci d9d2aaed11
chore: fix LIMCO courses' names 1 year ago
Francesco Minnocci 7bbd9ba139 Merge pull request 'chore: update calendar IDs for the 2nd semester' (#12) from dev into main
continuous-integration/drone/push Build is passing Details
Reviewed-on: phc/orario#12
1 year ago
Francesco Minnocci 62a9b80b21
chore: update calendar IDs for the 2nd semester 1 year ago
Fran314 c43bdc60e7 Merge pull request 'dev' (#11) from dev into main
continuous-integration/drone/push Build is passing Details
Reviewed-on: phc/orario#11
1 year ago
Francesco Baldino c8172ad88a Added warning messages when date outside of lecture period 1 year ago
Francesco Baldino c0b2894c22 Merge branch 'dev' of git.phc.dm.unipi.it:phc/orario into dev 1 year ago
Francesco Baldino fd0440465c Added date picker and fixed menu rendering 1 year ago
Antonio De Lucreziis bdbf031bce Merge pull request 'fix: rimosso un console.log' (#8) from dev into main
continuous-integration/drone/push Build is passing Details
Reviewed-on: phc/orario#8
1 year ago
Antonio De Lucreziis 32dc9a6104 fix: rimosso un console.log 1 year ago
Fran314 2d4a3ce664 Merge pull request 'reintrodotta visualizzazione lista rimossa nel commit precedente' (#7) from dev into main
continuous-integration/drone/push Build is passing Details
Reviewed-on: phc/orario#7
1 year ago
Francesco Baldino af41177561 reintrodotta visualizzazione lista rimossa nel commit precedente 1 year ago
Fran314 22ce830c80 Merge pull request 'dev' (#6) from dev into main
continuous-integration/drone/push Build is passing Details
Reviewed-on: phc/orario#6
1 year ago
Francesco Baldino 7efb311a61 Aggiunta trasposizione 1 year ago
Francesco Baldino a34d36257b first brutal rework and first prototype 2 years ago
Francesco Minnocci 04b6d5640d Merge pull request 'chore: update date ranges' (#5) from dev into main
continuous-integration/drone/push Build is passing Details
Reviewed-on: phc/orario#5
2 years ago
Francesco Minnocci 0caf967e14
chore: update date ranges 2 years ago
Antonio De Lucreziis 291aa72fd0 Merge branch 'dev'
continuous-integration/drone/push Build is passing Details
2 years ago
Antonio De Lucreziis 17188b17ee fix: tolto definitivamente l'effetto hover da mobile 2 years ago
Antonio De Lucreziis e9d40598eb Merge pull request 'fix: aggiornato il range di date alla settimana del 2023-03-06' (#4) from dev into main
continuous-integration/drone/push Build is passing Details
Reviewed-on: phc/orario#4
2 years ago
Antonio De Lucreziis 9a8fa0c440 fix: aggiornato il range di date alla settimana del 2023-03-06 2 years ago
Francesco Minnocci c831bb1715
WorkWeek: Add 18-20 slot, CSS seems to be fine.
continuous-integration/drone/push Build is passing Details
2 years ago
Antonio De Lucreziis ce2f76da14 drone: now it actually works
continuous-integration/drone/push Build is passing Details
2 years ago
Antonio De Lucreziis 5ebc62b844 drone: testing per branch config
continuous-integration/drone/push Build is passing Details
2 years ago
Antonio De Lucreziis d2f143133e fix: no hover on mobile
continuous-integration/drone/push Build is passing Details
2 years ago
Antonio De Lucreziis 82c21d2af9 fix: missing base url while building
continuous-integration/drone/push Build is passing Details
2 years ago
Antonio De Lucreziis d4cd070cce fix: ora forse tutto funge
continuous-integration/drone/push Build is passing Details
2 years ago
Antonio De Lucreziis bc1196f216 boh
continuous-integration/drone/push Build is passing Details
2 years ago
Antonio De Lucreziis e77b4a19e3 boh
continuous-integration/drone/push Build encountered an error Details
2 years ago
Antonio De Lucreziis 4589cda93d fix: wrong dir again
continuous-integration/drone/push Build is passing Details
2 years ago
Antonio De Lucreziis 63b73ed0b3 fix: wrong folder
continuous-integration/drone/push Build is passing Details
2 years ago
Antonio De Lucreziis 1219eeb408 fix: testing drone
continuous-integration/drone/push Build is passing Details
2 years ago
Antonio De Lucreziis 15446e3cbc fix: missing command
continuous-integration/drone/push Build is passing Details
2 years ago
Antonio De Lucreziis 0af55b4755 fix: wrong image name
continuous-integration/drone/push Build is failing Details
2 years ago
Antonio De Lucreziis 4b01e91fa7 trying out drone deploy
continuous-integration/drone Build encountered an error Details
2 years ago
Antonio De Lucreziis e7b145a02f fix: aggiunta un'altra patch per i nomi dei delle aule 2 years ago
Antonio De Lucreziis b6eb7a5ec1 fix: ehm avevo dimenticato una cosa 2 years ago
Antonio De Lucreziis 03f9db6e76 fix: aggiornati gli id dei calendari 2 years ago
Antonio De Lucreziis e7f59c4693 fix: aggiunta finalmente un'icona 2 years ago
Antonio De Lucreziis f86edb9f6c fix: Beh che dire 2 years ago
Antonio De Lucreziis 025a44472d fix: tutti gli orario ora sono nel range del secondo semestre 2023 2 years ago
Antonio De Lucreziis 9f66f40984 fix: non so perché prima c'era 22 2 years ago
Antonio De Lucreziis 9c12cbf759 chore: added new calendars 2 years ago
Antonio De Lucreziis 4bf696ea75 feat: added analytics 2 years ago
Antonio De Lucreziis e432b60b54 Fixed half-hour lessons for work-week-v 2 years ago
Antonio De Lucreziis abb98dfbc3 Fixed grid lines again 2 years ago
Antonio De Lucreziis 4af3529b6e Fixed grid lines offset 2 years ago
Antonio De Lucreziis 8f9fbb2bc5 Changed dark theme colors a bit 2 years ago
Francesco Minnocci e7f538c53c
Update Help.jsx 2 years ago
Francesco Minnocci 864a0d23da
Revamp Help popup 2 years ago
Antonio De Lucreziis 7cbbf243c4 Rimossa visualizzazione giornata dalla modalità desktop 2 years ago
Antonio De Lucreziis 7d20ccab10 Fix: corsi con più aule, prof dei corsi, patch speciale per il lab di primo anno 2 years ago
Antonio De Lucreziis 96ca0fef8f Added localstorage support 2 years ago
Antonio De Lucreziis d623df9158 Some fixes 2 years ago
Antonio De Lucreziis 142c5e1b67 Finalizzazione nuovo layout da mobile 2 years ago
Antonio De Lucreziis 5d8fbc9363 Missing conflicts 2 years ago
Antonio De Lucreziis 0b1dbaebbd Merged conflicts 2 years ago
Antonio De Lucreziis 22aa06ebf6 Some adjustments 2 years ago
Antonio De Lucreziis 41195462f1 Merge pull request 'Added optionbar for mobile' (#2) from Fran314/orario:mobile-header into dev
Reviewed-on: phc/orario#2
2 years ago
Antonio De Lucreziis 4a5994210e Formatting 2 years ago
Antonio De Lucreziis 16233adcf1 Formatting 2 years ago

@ -0,0 +1,46 @@
kind: pipeline
name: default
steps:
- name: deploy
image: node:latest
volumes:
- name: host-orario-dist
path: /drone/src/dist
environment:
BASE_URL:
from_secret: base_url
commands:
- pwd
- npm install
- npm run build
volumes:
- name: host-orario-dist
host:
path: /var/www/orario
trigger:
branch:
- main
event:
- push
---
kind: pipeline
type: exec
name: caddy-permissions
depends_on:
- default
steps:
- name: chown
commands:
- chown -R caddy:caddy /var/www/orario
trigger:
branch:
- main
event:
- push

2
.gitignore vendored

@ -6,3 +6,5 @@ node_modules/
.env
.env.production
.vscode/

@ -0,0 +1,9 @@
{
"printWidth": 90,
"singleQuote": true,
"quoteProps": "consistent",
"tabWidth": 4,
"semi": false,
"arrowParens": "avoid",
"proseWrap": "always"
}

@ -10,6 +10,22 @@ You need to have installed `node` and `npm` (or `pnpm`). To setup the project ju
To start the development server run `npm run dev`.
### Editor
È meglio se prima di fare commit il codice sia formattato con Prettier, nel caso di VSCode c'è un'estensione omonima e le opzioni da aggiungere al proprio JSON di impostazioni sono
```json
...
"prettier.printWidth": 100,
"prettier.singleQuote": true,
"prettier.quoteProps": "consistent",
"prettier.tabWidth": 4,
"prettier.semi": false,
"prettier.arrowParens": "avoid",
"editor.formatOnSave": true,
...
```
### Production
To build the ViteJS project run `npm run build`, for deployment a `.env` files can be used to set the `BASE_URL` variable.

Binary file not shown.

@ -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"
]

@ -1,24 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Orario DM Unipi</title>
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Orario DM Unipi</title>
<meta property="og:title" content="PHC - Orario" />
<meta property="og:description" content="Sito per visualizzare l'orario delle lezioni per i vari anni di corso" />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://lab.phc.dm.unipi.it/orario" />
<meta property="og:title" content="PHC - Orario" />
<meta property="og:description" content="Sito per visualizzare l'orario delle lezioni per i vari anni di corso" />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://lab.phc.dm.unipi.it/orario" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@48,400,1,0" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@48,400,1,0" />
<link rel="stylesheet" href="/src/styles/main.scss">
</head>
<body>
<script type="module" src="/src/main.jsx"></script>
</body>
<link rel="icon" type="image/png" href="icon.png" />
<link rel="stylesheet" href="/src/styles/main.scss" />
<script data-goatcounter="https://analytics.phc.dm.unipi.it/count" async src="//analytics.phc.dm.unipi.it/count.js"></script>
</head>
<body>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

@ -1,30 +1,40 @@
lockfileVersion: 5.4
specifiers:
'@babel/core': ^7.18.13
'@preact/preset-vite': ^2.3.0
date-fns: ^2.29.2
lodash: ^4.17.21
lodash-es: ^4.17.21
preact: ^10.10.6
sass: ^1.54.8
vite: ^3.0.9
lockfileVersion: '6.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
dependencies:
date-fns: 2.29.2
lodash: 4.17.21
lodash-es: 4.17.21
preact: 10.10.6
sass: 1.54.8
vite: 3.0.9_sass@1.54.8
date-fns:
specifier: ^2.29.2
version: 2.29.2
lodash:
specifier: ^4.17.21
version: 4.17.21
lodash-es:
specifier: ^4.17.21
version: 4.17.21
preact:
specifier: ^10.10.6
version: 10.10.6
sass:
specifier: ^1.54.8
version: 1.54.8
vite:
specifier: ^3.0.9
version: 3.0.9(sass@1.54.8)
devDependencies:
'@babel/core': 7.18.13
'@preact/preset-vite': 2.3.0_wibdudaz52xr4lhcmpvnys3x3u
'@babel/core':
specifier: ^7.18.13
version: 7.18.13
'@preact/preset-vite':
specifier: ^2.3.0
version: 2.3.0(@babel/core@7.18.13)(preact@10.10.6)(vite@3.0.9)
packages:
/@ampproject/remapping/2.2.0:
/@ampproject/remapping@2.2.0:
resolution: {integrity: sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==}
engines: {node: '>=6.0.0'}
dependencies:
@ -32,26 +42,26 @@ packages:
'@jridgewell/trace-mapping': 0.3.15
dev: true
/@babel/code-frame/7.18.6:
/@babel/code-frame@7.18.6:
resolution: {integrity: sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/highlight': 7.18.6
dev: true
/@babel/compat-data/7.18.13:
/@babel/compat-data@7.18.13:
resolution: {integrity: sha512-5yUzC5LqyTFp2HLmDoxGQelcdYgSpP9xsnMWBphAscOdFrHSAVbLNzWiy32sVNDqJRDiJK6klfDnAgu6PAGSHw==}
engines: {node: '>=6.9.0'}
dev: true
/@babel/core/7.18.13:
/@babel/core@7.18.13:
resolution: {integrity: sha512-ZisbOvRRusFktksHSG6pjj1CSvkPkcZq/KHD45LAkVP/oiHJkNBZWfpvlLmX8OtHDG8IuzsFlVRWo08w7Qxn0A==}
engines: {node: '>=6.9.0'}
dependencies:
'@ampproject/remapping': 2.2.0
'@babel/code-frame': 7.18.6
'@babel/generator': 7.18.13
'@babel/helper-compilation-targets': 7.18.9_@babel+core@7.18.13
'@babel/helper-compilation-targets': 7.18.9(@babel/core@7.18.13)
'@babel/helper-module-transforms': 7.18.9
'@babel/helpers': 7.18.9
'@babel/parser': 7.18.13
@ -67,7 +77,7 @@ packages:
- supports-color
dev: true
/@babel/generator/7.18.13:
/@babel/generator@7.18.13:
resolution: {integrity: sha512-CkPg8ySSPuHTYPJYo7IRALdqyjM9HCbt/3uOBEFbzyGVP6Mn8bwFPB0jX6982JVNBlYzM1nnPkfjuXSOPtQeEQ==}
engines: {node: '>=6.9.0'}
dependencies:
@ -76,14 +86,14 @@ packages:
jsesc: 2.5.2
dev: true
/@babel/helper-annotate-as-pure/7.18.6:
/@babel/helper-annotate-as-pure@7.18.6:
resolution: {integrity: sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/types': 7.18.13
dev: true
/@babel/helper-compilation-targets/7.18.9_@babel+core@7.18.13:
/@babel/helper-compilation-targets@7.18.9(@babel/core@7.18.13):
resolution: {integrity: sha512-tzLCyVmqUiFlcFoAPLA/gL9TeYrF61VLNtb+hvkuVaB5SUjW7jcfrglBIX1vUIoT7CLP3bBlIMeyEsIl2eFQNg==}
engines: {node: '>=6.9.0'}
peerDependencies:
@ -96,12 +106,12 @@ packages:
semver: 6.3.0
dev: true
/@babel/helper-environment-visitor/7.18.9:
/@babel/helper-environment-visitor@7.18.9:
resolution: {integrity: sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==}
engines: {node: '>=6.9.0'}
dev: true
/@babel/helper-function-name/7.18.9:
/@babel/helper-function-name@7.18.9:
resolution: {integrity: sha512-fJgWlZt7nxGksJS9a0XdSaI4XvpExnNIgRP+rVefWh5U7BL8pPuir6SJUmFKRfjWQ51OtWSzwOxhaH/EBWWc0A==}
engines: {node: '>=6.9.0'}
dependencies:
@ -109,21 +119,21 @@ packages:
'@babel/types': 7.18.13
dev: true
/@babel/helper-hoist-variables/7.18.6:
/@babel/helper-hoist-variables@7.18.6:
resolution: {integrity: sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/types': 7.18.13
dev: true
/@babel/helper-module-imports/7.18.6:
/@babel/helper-module-imports@7.18.6:
resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/types': 7.18.13
dev: true
/@babel/helper-module-transforms/7.18.9:
/@babel/helper-module-transforms@7.18.9:
resolution: {integrity: sha512-KYNqY0ICwfv19b31XzvmI/mfcylOzbLtowkw+mfvGPAQ3kfCnMLYbED3YecL5tPd8nAYFQFAd6JHp2LxZk/J1g==}
engines: {node: '>=6.9.0'}
dependencies:
@ -139,41 +149,41 @@ packages:
- supports-color
dev: true
/@babel/helper-plugin-utils/7.18.9:
/@babel/helper-plugin-utils@7.18.9:
resolution: {integrity: sha512-aBXPT3bmtLryXaoJLyYPXPlSD4p1ld9aYeR+sJNOZjJJGiOpb+fKfh3NkcCu7J54nUJwCERPBExCCpyCOHnu/w==}
engines: {node: '>=6.9.0'}
dev: true
/@babel/helper-simple-access/7.18.6:
/@babel/helper-simple-access@7.18.6:
resolution: {integrity: sha512-iNpIgTgyAvDQpDj76POqg+YEt8fPxx3yaNBg3S30dxNKm2SWfYhD0TGrK/Eu9wHpUW63VQU894TsTg+GLbUa1g==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/types': 7.18.13
dev: true
/@babel/helper-split-export-declaration/7.18.6:
/@babel/helper-split-export-declaration@7.18.6:
resolution: {integrity: sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/types': 7.18.13
dev: true
/@babel/helper-string-parser/7.18.10:
/@babel/helper-string-parser@7.18.10:
resolution: {integrity: sha512-XtIfWmeNY3i4t7t4D2t02q50HvqHybPqW2ki1kosnvWCwuCMeo81Jf0gwr85jy/neUdg5XDdeFE/80DXiO+njw==}
engines: {node: '>=6.9.0'}
dev: true
/@babel/helper-validator-identifier/7.18.6:
/@babel/helper-validator-identifier@7.18.6:
resolution: {integrity: sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==}
engines: {node: '>=6.9.0'}
dev: true
/@babel/helper-validator-option/7.18.6:
/@babel/helper-validator-option@7.18.6:
resolution: {integrity: sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==}
engines: {node: '>=6.9.0'}
dev: true
/@babel/helpers/7.18.9:
/@babel/helpers@7.18.9:
resolution: {integrity: sha512-Jf5a+rbrLoR4eNdUmnFu8cN5eNJT6qdTdOg5IHIzq87WwyRw9PwguLFOWYgktN/60IP4fgDUawJvs7PjQIzELQ==}
engines: {node: '>=6.9.0'}
dependencies:
@ -184,7 +194,7 @@ packages:
- supports-color
dev: true
/@babel/highlight/7.18.6:
/@babel/highlight@7.18.6:
resolution: {integrity: sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==}
engines: {node: '>=6.9.0'}
dependencies:
@ -193,7 +203,7 @@ packages:
js-tokens: 4.0.0
dev: true
/@babel/parser/7.18.13:
/@babel/parser@7.18.13:
resolution: {integrity: sha512-dgXcIfMuQ0kgzLB2b9tRZs7TTFFaGM2AbtA4fJgUUYukzGH4jwsS7hzQHEGs67jdehpm22vkgKwvbU+aEflgwg==}
engines: {node: '>=6.0.0'}
hasBin: true
@ -201,7 +211,7 @@ packages:
'@babel/types': 7.18.13
dev: true
/@babel/plugin-syntax-jsx/7.18.6_@babel+core@7.18.13:
/@babel/plugin-syntax-jsx@7.18.6(@babel/core@7.18.13):
resolution: {integrity: sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==}
engines: {node: '>=6.9.0'}
peerDependencies:
@ -211,17 +221,17 @@ packages:
'@babel/helper-plugin-utils': 7.18.9
dev: true
/@babel/plugin-transform-react-jsx-development/7.18.6_@babel+core@7.18.13:
/@babel/plugin-transform-react-jsx-development@7.18.6(@babel/core@7.18.13):
resolution: {integrity: sha512-SA6HEjwYFKF7WDjWcMcMGUimmw/nhNRDWxr+KaLSCrkD/LMDBvWRmHAYgE1HDeF8KUuI8OAu+RT6EOtKxSW2qA==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.18.13
'@babel/plugin-transform-react-jsx': 7.18.10_@babel+core@7.18.13
'@babel/plugin-transform-react-jsx': 7.18.10(@babel/core@7.18.13)
dev: true
/@babel/plugin-transform-react-jsx/7.18.10_@babel+core@7.18.13:
/@babel/plugin-transform-react-jsx@7.18.10(@babel/core@7.18.13):
resolution: {integrity: sha512-gCy7Iikrpu3IZjYZolFE4M1Sm+nrh1/6za2Ewj77Z+XirT4TsbJcvOFOyF+fRPwU6AKKK136CZxx6L8AbSFG6A==}
engines: {node: '>=6.9.0'}
peerDependencies:
@ -231,11 +241,11 @@ packages:
'@babel/helper-annotate-as-pure': 7.18.6
'@babel/helper-module-imports': 7.18.6
'@babel/helper-plugin-utils': 7.18.9
'@babel/plugin-syntax-jsx': 7.18.6_@babel+core@7.18.13
'@babel/plugin-syntax-jsx': 7.18.6(@babel/core@7.18.13)
'@babel/types': 7.18.13
dev: true
/@babel/template/7.18.10:
/@babel/template@7.18.10:
resolution: {integrity: sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==}
engines: {node: '>=6.9.0'}
dependencies:
@ -244,7 +254,7 @@ packages:
'@babel/types': 7.18.13
dev: true
/@babel/traverse/7.18.13:
/@babel/traverse@7.18.13:
resolution: {integrity: sha512-N6kt9X1jRMLPxxxPYWi7tgvJRH/rtoU+dbKAPDM44RFHiMH8igdsaSBgFeskhSl/kLWLDUvIh1RXCrTmg0/zvA==}
engines: {node: '>=6.9.0'}
dependencies:
@ -262,7 +272,7 @@ packages:
- supports-color
dev: true
/@babel/types/7.18.13:
/@babel/types@7.18.13:
resolution: {integrity: sha512-ePqfTihzW0W6XAU+aMw2ykilisStJfDnsejDCXRchCcMJ4O0+8DhPXf2YUbZ6wjBlsEmZwLK/sPweWtu8hcJYQ==}
engines: {node: '>=6.9.0'}
dependencies:
@ -271,7 +281,7 @@ packages:
to-fast-properties: 2.0.0
dev: true
/@esbuild/linux-loong64/0.14.54:
/@esbuild/linux-loong64@0.14.54:
resolution: {integrity: sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==}
engines: {node: '>=12'}
cpu: [loong64]
@ -279,7 +289,7 @@ packages:
requiresBuild: true
optional: true
/@jridgewell/gen-mapping/0.1.1:
/@jridgewell/gen-mapping@0.1.1:
resolution: {integrity: sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==}
engines: {node: '>=6.0.0'}
dependencies:
@ -287,7 +297,7 @@ packages:
'@jridgewell/sourcemap-codec': 1.4.14
dev: true
/@jridgewell/gen-mapping/0.3.2:
/@jridgewell/gen-mapping@0.3.2:
resolution: {integrity: sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==}
engines: {node: '>=6.0.0'}
dependencies:
@ -296,53 +306,53 @@ packages:
'@jridgewell/trace-mapping': 0.3.15
dev: true
/@jridgewell/resolve-uri/3.1.0:
/@jridgewell/resolve-uri@3.1.0:
resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==}
engines: {node: '>=6.0.0'}
dev: true
/@jridgewell/set-array/1.1.2:
/@jridgewell/set-array@1.1.2:
resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==}
engines: {node: '>=6.0.0'}
dev: true
/@jridgewell/sourcemap-codec/1.4.14:
/@jridgewell/sourcemap-codec@1.4.14:
resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==}
dev: true
/@jridgewell/trace-mapping/0.3.15:
/@jridgewell/trace-mapping@0.3.15:
resolution: {integrity: sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g==}
dependencies:
'@jridgewell/resolve-uri': 3.1.0
'@jridgewell/sourcemap-codec': 1.4.14
dev: true
/@preact/preset-vite/2.3.0_wibdudaz52xr4lhcmpvnys3x3u:
/@preact/preset-vite@2.3.0(@babel/core@7.18.13)(preact@10.10.6)(vite@3.0.9):
resolution: {integrity: sha512-0kOuz7wdrQLqrPlyI/Ypw9IWDF2++GGcOHMRBYO5T2w2+dheelaBH+XrIN/okqdsGIflzFIFNyIGubo5BC8wbQ==}
peerDependencies:
'@babel/core': 7.x
vite: 2.x || 3.x
dependencies:
'@babel/core': 7.18.13
'@babel/plugin-transform-react-jsx': 7.18.10_@babel+core@7.18.13
'@babel/plugin-transform-react-jsx-development': 7.18.6_@babel+core@7.18.13
'@prefresh/vite': 2.2.8_preact@10.10.6+vite@3.0.9
'@babel/plugin-transform-react-jsx': 7.18.10(@babel/core@7.18.13)
'@babel/plugin-transform-react-jsx-development': 7.18.6(@babel/core@7.18.13)
'@prefresh/vite': 2.2.8(preact@10.10.6)(vite@3.0.9)
'@rollup/pluginutils': 4.2.1
babel-plugin-transform-hook-names: 1.0.2_@babel+core@7.18.13
babel-plugin-transform-hook-names: 1.0.2(@babel/core@7.18.13)
debug: 4.3.4
kolorist: 1.5.1
resolve: 1.22.1
vite: 3.0.9_sass@1.54.8
vite: 3.0.9(sass@1.54.8)
transitivePeerDependencies:
- preact
- supports-color
dev: true
/@prefresh/babel-plugin/0.4.3:
/@prefresh/babel-plugin@0.4.3:
resolution: {integrity: sha512-fYAWbU1WDSLn108kKY4eDaaeUcnszFqXjgaGKYXNZ5NLulpRTpsrY+Sbfo9q8LDpWrBpqIgzjrwNnvglWI1xNQ==}
dev: true
/@prefresh/core/1.3.4_preact@10.10.6:
/@prefresh/core@1.3.4(preact@10.10.6):
resolution: {integrity: sha512-s7iNsnyJ3lZEUrYIgmVIB/hKtp4U6mdD91a31Zg7Q8M49O0x2KThrbrMQYraoDDrs4STdFB8Zv6bceUguOoX1A==}
peerDependencies:
preact: ^10.0.0
@ -350,11 +360,11 @@ packages:
preact: 10.10.6
dev: true
/@prefresh/utils/1.1.3:
/@prefresh/utils@1.1.3:
resolution: {integrity: sha512-Mb9abhJTOV4yCfkXrMrcgFiFT7MfNOw8sDa+XyZBdq/Ai2p4Zyxqsb3EgHLOEdHpMj6J9aiZ54W8H6FTam1u+A==}
dev: true
/@prefresh/vite/2.2.8_preact@10.10.6+vite@3.0.9:
/@prefresh/vite@2.2.8(preact@10.10.6)(vite@3.0.9):
resolution: {integrity: sha512-yGGa+PKPYPTzMlxgQ8aBgxw9K69I8X4iQ0E6KOcIvls96WKqKLLOYZW9SUgCve446jpUXvc9udviPBZjCeZIIQ==}
peerDependencies:
preact: ^10.4.0
@ -362,16 +372,16 @@ packages:
dependencies:
'@babel/core': 7.18.13
'@prefresh/babel-plugin': 0.4.3
'@prefresh/core': 1.3.4_preact@10.10.6
'@prefresh/core': 1.3.4(preact@10.10.6)
'@prefresh/utils': 1.1.3
'@rollup/pluginutils': 4.2.1
preact: 10.10.6
vite: 3.0.9_sass@1.54.8
vite: 3.0.9(sass@1.54.8)
transitivePeerDependencies:
- supports-color
dev: true
/@rollup/pluginutils/4.2.1:
/@rollup/pluginutils@4.2.1:
resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==}
engines: {node: '>= 8.0.0'}
dependencies:
@ -379,21 +389,21 @@ packages:
picomatch: 2.3.1
dev: true
/ansi-styles/3.2.1:
/ansi-styles@3.2.1:
resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==}
engines: {node: '>=4'}
dependencies:
color-convert: 1.9.3
dev: true
/anymatch/3.1.2:
/anymatch@3.1.2:
resolution: {integrity: sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==}
engines: {node: '>= 8'}
dependencies:
normalize-path: 3.0.0
picomatch: 2.3.1
/babel-plugin-transform-hook-names/1.0.2_@babel+core@7.18.13:
/babel-plugin-transform-hook-names@1.0.2(@babel/core@7.18.13):
resolution: {integrity: sha512-5gafyjyyBTTdX/tQQ0hRgu4AhNHG/hqWi0ZZmg2xvs2FgRkJXzDNKBZCyoYqgFkovfDrgM8OoKg8karoUvWeCw==}
peerDependencies:
'@babel/core': ^7.12.10
@ -401,17 +411,17 @@ packages:
'@babel/core': 7.18.13
dev: true
/binary-extensions/2.2.0:
/binary-extensions@2.2.0:
resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
engines: {node: '>=8'}
/braces/3.0.2:
/braces@3.0.2:
resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==}
engines: {node: '>=8'}
dependencies:
fill-range: 7.0.1
/browserslist/4.21.3:
/browserslist@4.21.3:
resolution: {integrity: sha512-898rgRXLAyRkM1GryrrBHGkqA5hlpkV5MhtZwg9QXeiyLUYs2k00Un05aX5l2/yJIOObYKOpS2JNo8nJDE7fWQ==}
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
@ -419,14 +429,14 @@ packages:
caniuse-lite: 1.0.30001388
electron-to-chromium: 1.4.241
node-releases: 2.0.6
update-browserslist-db: 1.0.7_browserslist@4.21.3
update-browserslist-db: 1.0.7(browserslist@4.21.3)
dev: true
/caniuse-lite/1.0.30001388:
/caniuse-lite@1.0.30001388:
resolution: {integrity: sha512-znVbq4OUjqgLxMxoNX2ZeeLR0d7lcDiE5uJ4eUiWdml1J1EkxbnQq6opT9jb9SMfJxB0XA16/ziHwni4u1I3GQ==}
dev: true
/chalk/2.4.2:
/chalk@2.4.2:
resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
engines: {node: '>=4'}
dependencies:
@ -435,7 +445,7 @@ packages:
supports-color: 5.5.0
dev: true
/chokidar/3.5.3:
/chokidar@3.5.3:
resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==}
engines: {node: '>= 8.10.0'}
dependencies:
@ -449,28 +459,28 @@ packages:
optionalDependencies:
fsevents: 2.3.2
/color-convert/1.9.3:
/color-convert@1.9.3:
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
dependencies:
color-name: 1.1.3
dev: true
/color-name/1.1.3:
/color-name@1.1.3:
resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==}
dev: true
/convert-source-map/1.8.0:
/convert-source-map@1.8.0:
resolution: {integrity: sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==}
dependencies:
safe-buffer: 5.1.2
dev: true
/date-fns/2.29.2:
/date-fns@2.29.2:
resolution: {integrity: sha512-0VNbwmWJDS/G3ySwFSJA3ayhbURMTJLtwM2DTxf9CWondCnh6DTNlO9JgRSq6ibf4eD0lfMJNBxUdEAHHix+bA==}
engines: {node: '>=0.11'}
dev: false
/debug/4.3.4:
/debug@4.3.4:
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
engines: {node: '>=6.0'}
peerDependencies:
@ -482,11 +492,11 @@ packages:
ms: 2.1.2
dev: true
/electron-to-chromium/1.4.241:
/electron-to-chromium@1.4.241:
resolution: {integrity: sha512-e7Wsh4ilaioBZ5bMm6+F4V5c11dh56/5Jwz7Hl5Tu1J7cnB+Pqx5qIF2iC7HPpfyQMqGSvvLP5bBAIDd2gAtGw==}
dev: true
/esbuild-android-64/0.14.54:
/esbuild-android-64@0.14.54:
resolution: {integrity: sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ==}
engines: {node: '>=12'}
cpu: [x64]
@ -494,7 +504,7 @@ packages:
requiresBuild: true
optional: true
/esbuild-android-arm64/0.14.54:
/esbuild-android-arm64@0.14.54:
resolution: {integrity: sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg==}
engines: {node: '>=12'}
cpu: [arm64]
@ -502,7 +512,7 @@ packages:
requiresBuild: true
optional: true
/esbuild-darwin-64/0.14.54:
/esbuild-darwin-64@0.14.54:
resolution: {integrity: sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug==}
engines: {node: '>=12'}
cpu: [x64]
@ -510,7 +520,7 @@ packages:
requiresBuild: true
optional: true
/esbuild-darwin-arm64/0.14.54:
/esbuild-darwin-arm64@0.14.54:
resolution: {integrity: sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw==}
engines: {node: '>=12'}
cpu: [arm64]
@ -518,7 +528,7 @@ packages:
requiresBuild: true
optional: true
/esbuild-freebsd-64/0.14.54:
/esbuild-freebsd-64@0.14.54:
resolution: {integrity: sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg==}
engines: {node: '>=12'}
cpu: [x64]
@ -526,7 +536,7 @@ packages:
requiresBuild: true
optional: true
/esbuild-freebsd-arm64/0.14.54:
/esbuild-freebsd-arm64@0.14.54:
resolution: {integrity: sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q==}
engines: {node: '>=12'}
cpu: [arm64]
@ -534,7 +544,7 @@ packages:
requiresBuild: true
optional: true
/esbuild-linux-32/0.14.54:
/esbuild-linux-32@0.14.54:
resolution: {integrity: sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw==}
engines: {node: '>=12'}
cpu: [ia32]
@ -542,7 +552,7 @@ packages:
requiresBuild: true
optional: true
/esbuild-linux-64/0.14.54:
/esbuild-linux-64@0.14.54:
resolution: {integrity: sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==}
engines: {node: '>=12'}
cpu: [x64]
@ -550,23 +560,23 @@ packages:
requiresBuild: true
optional: true
/esbuild-linux-arm/0.14.54:
resolution: {integrity: sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw==}
/esbuild-linux-arm64@0.14.54:
resolution: {integrity: sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig==}
engines: {node: '>=12'}
cpu: [arm]
cpu: [arm64]
os: [linux]
requiresBuild: true
optional: true
/esbuild-linux-arm64/0.14.54:
resolution: {integrity: sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig==}
/esbuild-linux-arm@0.14.54:
resolution: {integrity: sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw==}
engines: {node: '>=12'}
cpu: [arm64]
cpu: [arm]
os: [linux]
requiresBuild: true
optional: true
/esbuild-linux-mips64le/0.14.54:
/esbuild-linux-mips64le@0.14.54:
resolution: {integrity: sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw==}
engines: {node: '>=12'}
cpu: [mips64el]
@ -574,7 +584,7 @@ packages:
requiresBuild: true
optional: true
/esbuild-linux-ppc64le/0.14.54:
/esbuild-linux-ppc64le@0.14.54:
resolution: {integrity: sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ==}
engines: {node: '>=12'}
cpu: [ppc64]
@ -582,7 +592,7 @@ packages:
requiresBuild: true
optional: true
/esbuild-linux-riscv64/0.14.54:
/esbuild-linux-riscv64@0.14.54:
resolution: {integrity: sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg==}
engines: {node: '>=12'}
cpu: [riscv64]
@ -590,7 +600,7 @@ packages:
requiresBuild: true
optional: true
/esbuild-linux-s390x/0.14.54:
/esbuild-linux-s390x@0.14.54:
resolution: {integrity: sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA==}
engines: {node: '>=12'}
cpu: [s390x]
@ -598,7 +608,7 @@ packages:
requiresBuild: true
optional: true
/esbuild-netbsd-64/0.14.54:
/esbuild-netbsd-64@0.14.54:
resolution: {integrity: sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w==}
engines: {node: '>=12'}
cpu: [x64]
@ -606,7 +616,7 @@ packages:
requiresBuild: true
optional: true
/esbuild-openbsd-64/0.14.54:
/esbuild-openbsd-64@0.14.54:
resolution: {integrity: sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw==}
engines: {node: '>=12'}
cpu: [x64]
@ -614,7 +624,7 @@ packages:
requiresBuild: true
optional: true
/esbuild-sunos-64/0.14.54:
/esbuild-sunos-64@0.14.54:
resolution: {integrity: sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw==}
engines: {node: '>=12'}
cpu: [x64]
@ -622,7 +632,7 @@ packages:
requiresBuild: true
optional: true
/esbuild-windows-32/0.14.54:
/esbuild-windows-32@0.14.54:
resolution: {integrity: sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w==}
engines: {node: '>=12'}
cpu: [ia32]
@ -630,7 +640,7 @@ packages:
requiresBuild: true
optional: true
/esbuild-windows-64/0.14.54:
/esbuild-windows-64@0.14.54:
resolution: {integrity: sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ==}
engines: {node: '>=12'}
cpu: [x64]
@ -638,7 +648,7 @@ packages:
requiresBuild: true
optional: true
/esbuild-windows-arm64/0.14.54:
/esbuild-windows-arm64@0.14.54:
resolution: {integrity: sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg==}
engines: {node: '>=12'}
cpu: [arm64]
@ -646,7 +656,7 @@ packages:
requiresBuild: true
optional: true
/esbuild/0.14.54:
/esbuild@0.14.54:
resolution: {integrity: sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==}
engines: {node: '>=12'}
hasBin: true
@ -674,147 +684,147 @@ packages:
esbuild-windows-64: 0.14.54
esbuild-windows-arm64: 0.14.54
/escalade/3.1.1:
/escalade@3.1.1:
resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==}
engines: {node: '>=6'}
dev: true
/escape-string-regexp/1.0.5:
/escape-string-regexp@1.0.5:
resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
engines: {node: '>=0.8.0'}
dev: true
/estree-walker/2.0.2:
/estree-walker@2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
dev: true
/fill-range/7.0.1:
/fill-range@7.0.1:
resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==}
engines: {node: '>=8'}
dependencies:
to-regex-range: 5.0.1
/fsevents/2.3.2:
/fsevents@2.3.2:
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
requiresBuild: true
optional: true
/function-bind/1.1.1:
/function-bind@1.1.1:
resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==}
/gensync/1.0.0-beta.2:
/gensync@1.0.0-beta.2:
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
engines: {node: '>=6.9.0'}
dev: true
/glob-parent/5.1.2:
/glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
dependencies:
is-glob: 4.0.3
/globals/11.12.0:
/globals@11.12.0:
resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==}
engines: {node: '>=4'}
dev: true
/has-flag/3.0.0:
/has-flag@3.0.0:
resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
engines: {node: '>=4'}
dev: true
/has/1.0.3:
/has@1.0.3:
resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==}
engines: {node: '>= 0.4.0'}
dependencies:
function-bind: 1.1.1
/immutable/4.1.0:
/immutable@4.1.0:
resolution: {integrity: sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==}
/is-binary-path/2.1.0:
/is-binary-path@2.1.0:
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
engines: {node: '>=8'}
dependencies:
binary-extensions: 2.2.0
/is-core-module/2.10.0:
/is-core-module@2.10.0:
resolution: {integrity: sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==}
dependencies:
has: 1.0.3
/is-extglob/2.1.1:
/is-extglob@2.1.1:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'}
/is-glob/4.0.3:
/is-glob@4.0.3:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
dependencies:
is-extglob: 2.1.1
/is-number/7.0.0:
/is-number@7.0.0:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
/js-tokens/4.0.0:
/js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
dev: true
/jsesc/2.5.2:
/jsesc@2.5.2:
resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==}
engines: {node: '>=4'}
hasBin: true
dev: true
/json5/2.2.1:
/json5@2.2.1:
resolution: {integrity: sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==}
engines: {node: '>=6'}
hasBin: true
dev: true
/kolorist/1.5.1:
/kolorist@1.5.1:
resolution: {integrity: sha512-lxpCM3HTvquGxKGzHeknB/sUjuVoUElLlfYnXZT73K8geR9jQbroGlSCFBax9/0mpGoD3kzcMLnOlGQPJJNyqQ==}
dev: true
/lodash-es/4.17.21:
/lodash-es@4.17.21:
resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
dev: false
/lodash/4.17.21:
/lodash@4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
dev: false
/ms/2.1.2:
/ms@2.1.2:
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
dev: true
/nanoid/3.3.4:
/nanoid@3.3.4:
resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
/node-releases/2.0.6:
/node-releases@2.0.6:
resolution: {integrity: sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==}
dev: true
/normalize-path/3.0.0:
/normalize-path@3.0.0:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
/path-parse/1.0.7:
/path-parse@1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
/picocolors/1.0.0:
/picocolors@1.0.0:
resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
/picomatch/2.3.1:
/picomatch@2.3.1:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'}
/postcss/8.4.16:
/postcss@8.4.16:
resolution: {integrity: sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==}
engines: {node: ^10 || ^12 || >=14}
dependencies:
@ -822,16 +832,16 @@ packages:
picocolors: 1.0.0
source-map-js: 1.0.2
/preact/10.10.6:
/preact@10.10.6:
resolution: {integrity: sha512-w0mCL5vICUAZrh1DuHEdOWBjxdO62lvcO++jbzr8UhhYcTbFkpegLH9XX+7MadjTl/y0feoqwQ/zAnzkc/EGog==}
/readdirp/3.6.0:
/readdirp@3.6.0:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'}
dependencies:
picomatch: 2.3.1
/resolve/1.22.1:
/resolve@1.22.1:
resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==}
hasBin: true
dependencies:
@ -839,18 +849,18 @@ packages:
path-parse: 1.0.7
supports-preserve-symlinks-flag: 1.0.0
/rollup/2.77.3:
/rollup@2.77.3:
resolution: {integrity: sha512-/qxNTG7FbmefJWoeeYJFbHehJ2HNWnjkAFRKzWN/45eNBBF/r8lo992CwcJXEzyVxs5FmfId+vTSTQDb+bxA+g==}
engines: {node: '>=10.0.0'}
hasBin: true
optionalDependencies:
fsevents: 2.3.2
/safe-buffer/5.1.2:
/safe-buffer@5.1.2:
resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
dev: true
/sass/1.54.8:
/sass@1.54.8:
resolution: {integrity: sha512-ib4JhLRRgbg6QVy6bsv5uJxnJMTS2soVcCp9Y88Extyy13A8vV0G1fAwujOzmNkFQbR3LvedudAMbtuNRPbQww==}
engines: {node: '>=12.0.0'}
hasBin: true
@ -859,38 +869,38 @@ packages:
immutable: 4.1.0
source-map-js: 1.0.2
/semver/6.3.0:
/semver@6.3.0:
resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==}
hasBin: true
dev: true
/source-map-js/1.0.2:
/source-map-js@1.0.2:
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
engines: {node: '>=0.10.0'}
/supports-color/5.5.0:
/supports-color@5.5.0:
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
engines: {node: '>=4'}
dependencies:
has-flag: 3.0.0
dev: true
/supports-preserve-symlinks-flag/1.0.0:
/supports-preserve-symlinks-flag@1.0.0:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
/to-fast-properties/2.0.0:
/to-fast-properties@2.0.0:
resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
engines: {node: '>=4'}
dev: true
/to-regex-range/5.0.1:
/to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
dependencies:
is-number: 7.0.0
/update-browserslist-db/1.0.7_browserslist@4.21.3:
/update-browserslist-db@1.0.7(browserslist@4.21.3):
resolution: {integrity: sha512-iN/XYesmZ2RmmWAiI4Z5rq0YqSiv0brj9Ce9CfhNE4xIW2h+MFxcgkxIzZ+ShkFPUkjU3gQ+3oypadD3RAMtrg==}
hasBin: true
peerDependencies:
@ -901,7 +911,7 @@ packages:
picocolors: 1.0.0
dev: true
/vite/3.0.9_sass@1.54.8:
/vite@3.0.9(sass@1.54.8):
resolution: {integrity: sha512-waYABTM+G6DBTCpYAxvevpG50UOlZuynR0ckTK5PawNVt7ebX6X7wNXHaGIO6wYYFXSM7/WcuFuO2QzhBB6aMw==}
engines: {node: ^14.18.0 || >=16.0.0}
hasBin: true

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

@ -5,7 +5,11 @@ export const CompoundButton = ({ options, value, setValue }) => {
<div class="compound">
{options.map(option => (
<button
class={withClasses(['radio', option.value === value && 'selected'])}
class={withClasses([
'radio',
option.value === value && 'selected',
option.icon && 'icon',
])}
onClick={() => setValue(option.value)}
>
{option.label}

@ -1,12 +0,0 @@
import { Icon } from './Icon.jsx'
export const ToolOverlay = ({ visibility, toggleVisibility, onClose }) => (
<div class="overlay">
<button class="icon primary" onClick={toggleVisibility}>
<Icon name={visibility ? 'visibility_off' : 'visibility'} />
</button>
<button class="icon" onClick={onClose}>
<Icon name="close" />
</button>
</div>
)

@ -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,29 +1,41 @@
import { Course } from './view/Course.jsx'
import { Courses } from './view/Courses.jsx'
import { Schedule } from './view/Schedule.jsx'
import { WorkWeek } from './view/WorkWeek.jsx'
import { WorkWeekGrid } from './view/WorkWeekGrid.jsx'
import { WorkWeekTranspose } from './view/WorkWeekTranspose.jsx'
export const MODE_COURSE = 'course'
export const MODE_COURSES = 'course'
export const MODE_WORKWEEK = 'work-week'
export const MODE_SCHEDULE = 'schedule'
export const MODE_WORKWEEK_GRID = 'work-week-grid'
export const MODE_WORKWEEK_TRANSPOSE = 'work-week-transpose'
const viewModeMap = {
[MODE_COURSE]: Course,
[MODE_WORKWEEK]: WorkWeek,
[MODE_WORKWEEK_GRID]: WorkWeekGrid,
[MODE_WORKWEEK_TRANSPOSE]: WorkWeekTranspose,
[MODE_COURSES]: Courses,
[MODE_SCHEDULE]: Schedule,
}
export const EventsView = ({ mode, ...viewProps }) => {
const Mode = viewModeMap[mode]
// export const EventsView = ({ mode, ...viewProps }) => {
// const Mode = viewModeMap[mode]
//
// return (
// <div class="events-view">
// <Mode {...viewProps} />
// </div>
// )
// }
return (
<div class="events-view">
<Mode {...viewProps} />
</div>
)
export const EventsView = ({ mode, source, ...viewProps }) => {
// const Mode = viewModeMap[mode]
if (source === 'orario') {
return (
<div class="events-view">
<Schedule source={source} {...viewProps} />
</div>
)
} else {
return (
<div class="events-view">
<Courses source={source} {...viewProps} />
</div>
)
}
}

@ -1,60 +1,24 @@
import { ComboBox } from './ComboBox.jsx'
import { MODE_COURSE, 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, mode, setMode, source, setSource, 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="options">
<div class="label">Gruppo Corsi</div>
<ComboBox
value={source}
setValue={setSource}
options={[
{ value: 'anno-1', label: 'I' },
{ value: 'anno-2', label: 'II' },
{ value: 'anno-3', label: 'III' },
{ value: 'magistrale', label: 'Magistrale' },
{ value: 'tutti', label: 'Tutti' },
]}
/>
<div class="label">Visualizzazione</div>
<ComboBox
value={mode}
setValue={setMode}
options={[
{ value: MODE_COURSE, label: 'Corsi' },
{ value: MODE_SCHEDULE, label: 'Giornaliera' },
{ value: MODE_WORKWEEK, label: 'Settimana' },
{ value: MODE_WORKWEEK_GRID, label: 'Schema' },
]}
/>
</div>
<hr /> */}
<div class="help">
<h2>
<Icon name="info" />
Guida
</h2>
<Help />
</div>
</div>
)
export const HamburgerMenu = ({ date, setDate, onClose, theme, setTheme }) => {
return (
<div class="menu">
<div class="help">
<h2>
<Icon name="info" />
Guida
</h2>
<Help />
</div>
</div>
)
}

@ -2,38 +2,45 @@ import { Icon } from './Icon.jsx'
export const Help = ({}) => (
<>
<h3>Selezione Corsi</h3>
<h3>Visualizzazione Corsi</h3>
<p>
Per visualizzare solo una selezione dei corsi disponibili, clicca su quelli che ti
interessano e poi nascondi gli altri con il tasto <Icon name="visibility" /> che
comparirà in basso a destra.
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>
Per vedere l'orario settimanale, clicca su <b>Settimana</b>, che mostrerà un calendario
con i corsi attualmente selezionati.
Una volta compiuta la selezione, è possibile vedere la tabella delle lezioni
andando nella visualizzazione Orario (
<Icon name="calendar_view_month" />)
</p>
<p>
La selezione effettuata verrà preservata dalla visualizzazione <b>Corsi</b> a quella
Settimana, permettendo di individuare eventuali sovrapposizioni di orari.
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>
I corsi sono raggruppati per: primo, secondo, terzo anno (<b>I</b>,<b>II</b>,<b>III</b>)
e magistrale. Alternativamente, puoi scegliere fra tutti i corsi disponibili cliccando
su <b>Tutti</b>.
È 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 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 &amp; Contatti</h3>
<p>
In caso di bug puoi creare una issue sulla repo git del progetto{' '}
<a href="https://git.phc.dm.unipi.it/phc/orario">
https://git.phc.dm.unipi.it/phc/orario
</a>
, contattarci all'indirizzo{' '}
<a href="mailto:macchinisti@lists.dm.unipi.it">
macchinisti@lists.dm.unipi.it
</a>{' '}
oppure puoi contattarci direttamente all'indirizzo{' '}
<a href="mailto:macchinisti@lists.dm.unipi.it">macchinisti@lists.dm.unipi.it</a>.
oppure passare direttamente in PHC.
</p>
</>
)

@ -0,0 +1,40 @@
import { CompoundButton } from './CompoundButton.jsx'
import { Icon } from './Icon.jsx'
export const OptionBar = ({ view, setView }) => {
return (
<div class="option-bar">
<div class="option-group">
<div class="item option">
<CompoundButton
options={[
{ value: 'anno-1', label: 'I' },
{ value: 'anno-2', label: 'II' },
{ value: 'anno-3', label: 'III' },
{ value: 'magistrale', label: 'M' },
{ value: 'tutti', label: 'Tutti' },
]}
value={view}
setValue={setView}
/>
</div>
<CompoundButton
options={[
{
value: 'orario',
label: <Icon name="calendar_view_month" />,
icon: true,
},
{
value: 'lista',
label: <Icon name="list" />,
icon: true,
},
]}
value={view}
setValue={setView}
/>
</div>
</div>
)
}

@ -1,41 +0,0 @@
import { CompoundButton } from './CompoundButton.jsx'
import { MODE_COURSE, MODE_SCHEDULE, MODE_WORKWEEK, MODE_WORKWEEK_GRID } from './EventsView.jsx'
import { Icon } from './Icon.jsx'
export const Optionbar = ({ mode, setMode, source, setSource, onHelp }) => {
return (
<div class="optionbar">
<div class="option-group">
<div class="item option">
<CompoundButton
options={[
{ value: 'anno-1', label: 'I' },
{ value: 'anno-2', label: 'II' },
{ value: 'anno-3', label: 'III' },
{ value: 'magistrale', label: 'M' },
{ value: 'tutti', label: <Icon name="apps" /> },
]}
value={source}
setValue={setSource}
/>
</div>
</div>
<div class="option-group">
<div class="item option">
<CompoundButton
options={[
{ value: MODE_COURSE, label: <Icon name="list" /> },
{
value: MODE_WORKWEEK_GRID,
label: <Icon name="calendar_view_month" />,
},
{ value: MODE_SCHEDULE, label: <Icon name="calendar_view_day" /> },
]}
value={mode}
setValue={setMode}
/>
</div>
</div>
</div>
)
}

@ -9,17 +9,21 @@ export const Popup = ({ title, children, onClose }) => {
class="popup-container"
ref={popupLeaveRegionRef}
onClick={e =>
popupLeaveRegionRef.current && popupLeaveRegionRef.current === e.target && onClose()
popupLeaveRegionRef.current &&
popupLeaveRegionRef.current === e.target &&
onClose()
}
>
<div class="popup">
<div class="header">
<div class="title">{title}</div>
<button class="flat icon" onClick={onClose}>
<Icon name="close" />
</button>
<div class="popup-wrapper">
<div class="popup">
<div class="header">
<div class="title">{title}</div>
<button class="flat icon" onClick={onClose}>
<Icon name="close" />
</button>
</div>
<div class="content">{children}</div>
</div>
<div class="content">{children}</div>
</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>
)
}

@ -0,0 +1,13 @@
import { Icon } from './Icon.jsx'
import { MODE_COURSES } from './EventsView.jsx'
export const ToolOverlay = ({ mode, toggleMode, onClose }) => (
<div class="overlay">
<button class="icon primary" onClick={toggleMode}>
<Icon name={mode === MODE_COURSES ? 'calendar_month' : 'view_list'} />
</button>
<button class="icon" onClick={onClose}>
<Icon name="close" />
</button>
</div>
)

@ -1,13 +1,14 @@
import { CompoundButton } from './CompoundButton.jsx'
import { MODE_COURSE, MODE_SCHEDULE, MODE_WORKWEEK, MODE_WORKWEEK_GRID } from './EventsView.jsx'
import { DatePicker } from './DatePicker.jsx'
import { Icon } from './Icon.jsx'
export const Toolbar = ({
mode,
setMode,
source,
setSource,
onShowMenu,
date,
setDate,
showMobileMenu,
setShowMobileMenu,
onHelp,
theme,
setTheme,
@ -15,8 +16,11 @@ 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">
@ -36,20 +40,21 @@ export const Toolbar = ({
setValue={setSource}
/>
</div>
</div>
<div class="option-group">
<div class="item option">
<CompoundButton
options={[
{ value: MODE_COURSE, label: 'Corsi' },
{ value: MODE_WORKWEEK, label: 'Settimana' },
{ value: MODE_WORKWEEK_GRID, label: 'Schema' },
{ value: MODE_SCHEDULE, label: 'Giorno' },
{ value: 'orario', label: 'Orario' },
{ value: 'lista', label: 'Lista' },
]}
value={mode}
setValue={setMode}
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()}>
<Icon name="print" />

@ -2,23 +2,39 @@ import { format } from 'date-fns'
import _ from 'lodash'
import { useEffect, useRef, useState } from 'preact/hooks'
import { normalizeCourseName, WEEK_DAYS } from '../../utils.jsx'
import { prettyCourseName, WEEK_DAYS } from '../../utils.jsx'
import { Icon } from '../Icon.jsx'
export const Course = ({ events, selection, setSelection, hideOtherCourses }) => {
export const Courses = ({
source,
timetables,
selection,
setSelection,
hideOtherCourses,
}) => {
const events = timetables[source]
const selectionSet = new Set(selection)
const visibleEvents = !hideOtherCourses ? events : events.filter(e => selectionSet.has(e.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))
)
const [currentlyHovered, setCurrentlyHovered] = useState(null)
const element = useRef()
useEffect(() => {
if (element.current) {
const l = e => {
const isMobile = window.matchMedia('(pointer: coarse)').matches
const $course = e.target.closest('.course')
if ($course) {
if ($course && !isMobile) {
setCurrentlyHovered($course.dataset.courseId)
} else {
setCurrentlyHovered(null)
@ -37,6 +53,15 @@ export const Course = ({ events, selection, setSelection, hideOtherCourses }) =>
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
@ -51,14 +76,14 @@ export const Course = ({ events, selection, setSelection, hideOtherCourses }) =>
else setSelection(selection.filter(selId => selId !== id))
}}
>
<div class="title">{normalizeCourseName(courseEvents[0].name)}</div>
<div class="docenti">{courseEvents[0].docenti.join(', ')}</div>
<div class="title">{prettyCourseName(courseEvents[0].name)}</div>
<div class="docenti">{profsPerCourse[id].join(', ')}</div>
<div class="events">
{courseEvents.map(course => (
<div>
{WEEK_DAYS[course.start.getDay()]}{' '}
{format(course.start, 'H:mm')} &ndash;{' '}
{format(course.end, 'H:mm')} {course.aula}
{format(course.start, 'H:mm')}&ndash;
{format(course.end, 'H:mm')} {course.aule.join(', ')}
</div>
))}
</div>

@ -1,54 +1,266 @@
import { format } from 'date-fns'
import { useEffect, useRef, useState } from 'preact/hooks'
import _ from 'lodash'
import { normalizeCourseName, WEEK_DAYS, withClasses } from '../../utils.jsx'
import { differenceInMinutes, startOfDay } from 'date-fns'
export const Schedule = ({ events, selection, setSelection, hideOtherCourses }) => {
const selectionSet = new Set(selection)
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'
const TransposePopup = ({ onClose }) => {
return (
<Popup
title={
<>
<Icon name="info" /> Attenzione! La tabella è stata trasposta!
</>
}
onClose={onClose}
>
<p>A grande richiesta popolare abbiamo trasposto la tabella dell'orario!</p>
<p>
Assicurati quindi di leggerla correttamente (dall'alto verso il basso
invece che da sinistra verso destra).
</p>
const visibleEvents = !hideOtherCourses ? events : events.filter(e => selectionSet.has(e.id))
<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>
</Popup>
)
}
const eventsByWeekday = _.mapValues(
_.groupBy(visibleEvents, e => e.start.getDay()),
dailyEvents => _.groupBy(dailyEvents, e => e.start.getHours())
const NoCourseWarning = () => {
return (
<div class="warning">
<p>Non hai ancora selezionato nessun corso.</p>
<p>
Clicca sui corsi nelle altre visuali per selezionarli e visualizzarli
nell'orario
</p>
</div>
)
}
const Layout = ({ layout, day, colors }) => {
return (
<div class="schedule-view">
{Object.entries(eventsByWeekday).map(([index, dailyEvents]) => (
<>
{layout.map(block => (
<div
class="event-block-wrapper"
style={{
'--time-start': block.start,
'--time-end': block.end,
'--day-position': day,
}}
>
<div class="event-block">
{block.events.map((event, index) => (
<div
class="event-wrapper"
style={{
'--block-size': block.end - block.start,
'--size': event.end - event.start,
'--relative-start': event.start - block.start,
'--index': event.index,
'--of': block.layers,
'--color': `var(--event-${colors[event.id]})`,
}}
>
<div class="event">
{event.aule.map(aula => (
<div>{aula.replace(/^Fib /, '')}</div>
))}
</div>
</div>
))}
</div>
</div>
))}
</>
)
}
const ScheduleGrid = ({
orientation,
setOrientation,
weekStart,
weekEnd,
dayBlocksLayout,
colors,
}) => {
return (
<div
class={`grid ${orientation}`}
style={{
'--time-slots': weekEnd - weekStart,
}}
>
<div class="transpose-button">
<button
class="small"
onClick={() =>
setOrientation(
orientation === 'original' ? 'transposed' : 'original'
)
}
>
<Icon name="switch_left" style="transform: rotate(-45deg)" />
</button>
</div>
{[1, 2, 3, 4, 5].map(n => (
<>
<div class="header giorno">
<div class="inner">{WEEK_DAYS[index]}</div>
<div class="day-label" style={`--position: ${n + 1}`}>
{WEEK_DAYS_SHORT[n]}
</div>
{Object.values(dailyEvents).map(events => (
<>
<div class="header orario">{format(events[0].start, 'H:mm')}</div>
{events.map(event => (
<div
class={withClasses([
'event',
selectionSet.has(event.id) && 'selected',
])}
onClick={() => {
if (!selectionSet.has(event.id))
setSelection([...selection, event.id])
else
setSelection(
selection.filter(selId => selId !== event.id)
)
}}
>
<div class="title">{normalizeCourseName(event.name)}</div>
<div class="orario">
{format(event.start, 'H:mm')} &ndash;{' '}
{format(event.end, 'H:mm')}
</div>
<div class="aula">{event.aula}</div>
</div>
))}
</>
))}
<div class="day-line" style={`--position: ${n + 1}`}></div>
</>
))}
{[9, 11, 14, 16].map(n => (
<div
class="time-label"
style={{
'--position': n * 2 - weekStart,
}}
>
{n}-{n + 2}
</div>
))}
<div class="time-line" style="--position: 0"></div>
{[9, 11, 13, 14, 16, 18].map(n => (
<>
{n * 2 > weekStart && n * 2 < weekEnd && (
<div
class="time-line"
style={{
'--position': n * 2 - weekStart,
}}
></div>
)}
</>
))}
{Object.entries(dayBlocksLayout).map(([day, layout]) => (
<Layout layout={layout} day={day} colors={colors} />
))}
</div>
)
}
const ScheduleLegend = ({ courses, colors }) => {
return (
<div class="legend">
{courses.map(course => (
<>
<div
class="color"
style={{
'--color': `var(--event-${colors[course.id]})`,
}}
></div>
<div class="name">{prettyCourseName(course.name)}</div>
</>
))}
</div>
)
}
const ScheduleCard = ({
orientation,
setOrientation,
weekStart,
weekEnd,
dayBlocksLayout,
courses,
colors,
}) => {
return (
<div class="schedule-card">
<ScheduleGrid
orientation={orientation}
setOrientation={setOrientation}
weekStart={weekStart}
weekEnd={weekEnd}
dayBlocksLayout={dayBlocksLayout}
colors={colors}
/>
<ScheduleLegend courses={courses} colors={colors} />
</div>
)
}
export const Schedule = ({ timetables, selection }) => {
const [hasSeenTranspose, setHasSeenTranspose] = usePersistentState(
'transpose_info',
'false'
)
const [orientation, setOrientation] = usePersistentState('orientation', 'original')
const colorList = ['red', 'purple', 'blue', 'yellow', 'green', 'orange', 'lightblue']
const allEvents = timetables['tutti']
const selectionSet = new Set(selection)
const events = allEvents
.filter(e => selectionSet.has(e.id))
.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 weekEnd = Math.max(...events.map(e => e.end), 18 * 60) / 30
const relativeEvents = events.map(e => ({
...e,
start: Math.round(e.start / 30 - weekStart),
end: Math.round(e.end / 30 - weekStart),
}))
const courses = _.uniqBy(events, event => event.id)
const colors = Object.fromEntries(
courses.map((course, index) => {
return [course.id, colorList[index % colorList.length]]
})
)
const base = {
1: [],
2: [],
3: [],
4: [],
5: [],
}
const eventsByWeekday = {
...base,
..._.groupBy(relativeEvents, event => event.day),
}
const dayBlocksLayout = _.mapValues(eventsByWeekday, dayEvents =>
layoutEvents(dayEvents)
)
return (
<>
{hasSeenTranspose === 'false' && (
<TransposePopup onClose={() => setHasSeenTranspose('true')} />
)}
<div class="schedule-view">
{selection.length === 0 && <NoCourseWarning />}
<ScheduleCard
orientation={orientation}
setOrientation={setOrientation}
weekStart={weekStart}
weekEnd={weekEnd}
dayBlocksLayout={dayBlocksLayout}
courses={courses}
colors={colors}
/>
</div>
</>
)
}

@ -1,150 +0,0 @@
import { useEffect, useRef, useState } from 'preact/hooks'
import _ from 'lodash'
import { differenceInMinutes, startOfDay } from 'date-fns'
import { hashString, normalizeCourseName, WEEK_DAYS } from '../../utils.jsx'
import { layoutIntervals } from '../../interval-layout.js'
export const WorkWeek = ({ events, selection, setSelection, hideOtherCourses }) => {
const selectionSet = new Set(selection)
const eventsByWeekday = _.groupBy(
!hideOtherCourses ? events : events.filter(e => selectionSet.has(e.id)),
event => event.start.getDay()
)
// const dayIntervalLayout = _.mapValues(Object.assign(base, eventsByWeekday), events =>
const dayIntervalLayout = _.mapValues(eventsByWeekday, events =>
layoutIntervals(
events.map(e => ({
start: differenceInMinutes(e.start, startOfDay(e.start)),
end: differenceInMinutes(e.end, startOfDay(e.start)),
data: e,
}))
)
)
const [currentlyHovered, setCurrentlyHovered] = useState(null)
const element = useRef()
useEffect(() => {
if (element.current) {
const l = e => {
const $event = e.target.closest('.event')
if ($event) {
setCurrentlyHovered($event.dataset.eventId)
} else {
setCurrentlyHovered(null)
}
}
element.current.addEventListener('mousemove', l)
element.current.addEventListener('mouseleave', l)
return () => {
element.current.removeEventListener('mousemove', l)
element.current.removeEventListener('mouseleave', l)
}
}
}, [element.current])
return (
<div class="work-week-v-view" ref={element}>
<div class="pivot"></div>
<div class="left-header">
<div class="blocks">
<div
class="block skip-border"
style={{
'--start': 9 - 7,
'--size': 2,
}}
>
9:00 &ndash; 11:00
</div>
<div
class="block"
style={{
'--start': 11 - 7,
'--size': 2,
}}
>
11:00 &ndash; 13:00
</div>
<div
class="block skip-border"
style={{
'--start': 14 - 7,
'--size': 2,
}}
>
14:00 &ndash; 16:00
</div>
<div
class="block"
style={{
'--start': 16 - 7,
'--size': 2,
}}
>
16:00 &ndash; 18:00
</div>
</div>
</div>
{Object.entries(dayIntervalLayout).map(([index, layout]) => (
<div class="day" style={{ '--size': Math.max(1, layout.length) }}>
<div class="top-header">{WEEK_DAYS[parseInt(index)]}</div>
<div class="events">
{layout.map((events, stackIndex) => (
<>
{events.map(event => (
<div
class={
'event' +
(currentlyHovered === event.data.id
? ' highlight'
: '') +
(selectionSet.has(event.data.id) ? ' selected' : '')
}
data-event-id={event.data.id}
style={{
'--start': event.start / 60 - 7,
'--stack': stackIndex + 1,
'--size': (event.end - event.start) / 60,
'--hue':
(Math.abs(hashString('seed3' + event.data.id)) %
360) +
'deg',
}}
onClick={() => {
if (!selectionSet.has(event.data.id))
setSelection([...selection, event.data.id])
else
setSelection(
selection.filter(id => event.data.id !== id)
)
}}
>
<div class="title">
{normalizeCourseName(event.data.name)}
</div>
<div class="aula">{event.data.aula}</div>
</div>
))}
</>
))}
{/* Grid Tracks */}
{[1, 3, 5].map(i => (
<div class="grid-line-h" style={{ '--track': i }}></div>
))}
{[6, 8, 10].map(i => (
<div class="grid-line-h" style={{ '--track': i }}></div>
))}
</div>
</div>
))}
</div>
)
}

@ -1,200 +0,0 @@
import { useEffect, useRef, useState } from 'preact/hooks'
import _ from 'lodash'
import { differenceInMinutes, startOfDay } from 'date-fns'
import { hashString, normalizeCourseName, WEEK_DAYS } from '../../utils.jsx'
import { layoutIntervals } from '../../interval-layout.js'
export const WorkWeekGrid = ({ events, selection, setSelection, hideOtherCourses }) => {
const selectionSet = new Set(selection)
const colorList = [
'red',
'purple',
'blue',
'yellow',
'green',
'orange',
'lightblue',
];
const courses = _.uniqBy(!hideOtherCourses ? events : events.filter(e => selectionSet.has(e.id)), event => event.id);
const colors = Object.fromEntries(courses.map((course, index) => {
return [
course.id,
[
colorList[index % colorList.length],
courses.length <= colorList.length ? '' : String.fromCharCode(65 + Math.floor(index / colorList.length))
]
];
}));
const base = {
1: [],
2: [],
3: [],
4: [],
5: [],
}
const eventsByWeekday = {
... base,
... _.groupBy(
!hideOtherCourses ? events : events.filter(e => selectionSet.has(e.id)),
event => event.start.getDay()
)
}
const dayIntervalLayout = _.mapValues(eventsByWeekday, events =>
layoutIntervals(
events.map(e => ({
start: differenceInMinutes(e.start, startOfDay(e.start)),
end: differenceInMinutes(e.end, startOfDay(e.start)),
data: e,
}))
)
)
const startsAndEnds = Object.entries(dayIntervalLayout).flatMap(([index, layout]) => layout.flatMap(events => events.map(event => [event.start, event.end])));
const minStart = Math.min(...startsAndEnds.map(([s, e]) => s)) / 60;
const maxEnd = Math.max(...startsAndEnds.map(([s, e]) => e)) / 60;
// const timeStart = minStart < 9 ? 7 :
// minStart < 11 ? 9 :
// minStart < 13 ? 11 :
// minStart < 14 ? 13 :
// minStart < 16 ? 14 :
// minStart < 18 ? 16 :
// 18;
// const timeEnd = maxEnd > 18 ? 20 :
// maxEnd > 16 ? 18 :
// maxEnd > 14 ? 16 :
// maxEnd > 13 ? 14 :
// maxEnd > 11 ? 13 :
// maxEnd > 9 ? 11 :
// 9;
const timeStart = minStart < 9 ? 7 : 9;
const timeEnd = maxEnd > 18 ? 20 : 18;
const daySizes = Object.entries(dayIntervalLayout).map(([index, layout]) => Math.max(1, layout.length));
const dayOffsets = daySizes.map((v, i) => {
let sum = 0;
for(let j = 0; j < i; j++) {
sum += daySizes[j];
}
return sum;
});
const daysLength = daySizes[4] + dayOffsets[4];
const [currentlyHovered, setCurrentlyHovered] = useState(null)
const element = useRef()
useEffect(() => {
if (element.current) {
const l = e => {
const $event = e.target.closest('.event')
if ($event) {
setCurrentlyHovered($event.dataset.eventId)
} else {
setCurrentlyHovered(null)
}
}
element.current.addEventListener('mousemove', l)
element.current.addEventListener('mouseleave', l)
return () => {
element.current.removeEventListener('mousemove', l)
element.current.removeEventListener('mouseleave', l)
}
}
}, [element.current])
return (
<div class="work-week-grid-view" ref={element}>
<div class="grid" style={{'--days-length': daysLength, '--time-length': timeEnd - timeStart}}>
{[7, 9, 11, 14, 16, 18].map(n =>
<>
{n + 2 > timeStart && n < timeEnd && (
<div class="time" style={{'--offset': n - timeStart + 2}}>{n}-{n+2}</div>
)}
</>
)}
<div class="vline" style="--offset: 2"></div>
{[9, 11, 13, 14, 16, 18].map(n =>
<>
{n > timeStart && n < timeEnd && (
<div class="vline" style={{'--offset': n - timeStart + 2}}></div>
)}
</>
)}
<div class="day-name" style={{'--line': 2 + dayOffsets[0]}}>Lun</div>
<div class="day-name" style={{'--line': 2 + dayOffsets[1]}}>Mar</div>
<div class="day-name" style={{'--line': 2 + dayOffsets[2]}}>Mer</div>
<div class="day-name" style={{'--line': 2 + dayOffsets[3]}}>Gio</div>
<div class="day-name" style={{'--line': 2 + dayOffsets[4]}}>Ven</div>
<div class="hline" style={{'--line': 2 + dayOffsets[0]}}></div>
<div class="hline" style={{'--line': 2 + dayOffsets[1]}}></div>
<div class="hline" style={{'--line': 2 + dayOffsets[2]}}></div>
<div class="hline" style={{'--line': 2 + dayOffsets[3]}}></div>
<div class="hline" style={{'--line': 2 + dayOffsets[4]}}></div>
{Object.entries(dayIntervalLayout).map(([index, layout]) => (
<>
{layout.map((events, stackIndex) => (
<>
{events.map(event => (
<div class={
'event' +
(currentlyHovered === event.data.id
? ' highlight'
: '') +
(selectionSet.has(event.data.id) ? ' selected' : '')
}
data-event-id={event.data.id}
style={{
'--line': 2 + dayOffsets[index-1] + stackIndex,
'--offset': event.start / 60 - timeStart + 2,
'--length': (event.end - event.start) / 60,
'--color': `var(--bubble-${colors[event.data.id][0]})`,
'--border-color': `var(--bubble-border-${colors[event.data.id][0]})`,
'--highlight-color': `var(--bubble-highlight-${colors[event.data.id][0]})`
}}
onClick={() => {
if (!selectionSet.has(event.data.id))
setSelection([...selection, event.data.id])
else
setSelection(
selection.filter(id => event.data.id !== id)
)
}}
>
{colors[event.data.id][1] !== '' && (
<div className="distinguisher">{`(${colors[event.data.id][1]})`}</div>
)}
{event.data.aula}</div>
))}
</>
))}
</>
))}
</div>
<div class="legend">
{courses.map(course => (
<>
<div class="color"
style={{
'--color': `var(--bubble-${colors[course.id][0]})`,
'--border-color': `var(--bubble-border-${colors[course.id][0]})`
}}
>
{colors[course.id][1]}
</div>
<div class="name">{normalizeCourseName(course.name)}</div>
</>
))}
</div>
</div>
)
}

@ -1,104 +0,0 @@
import { useEffect, useRef } from 'preact/hooks'
import _ from 'lodash'
import { differenceInMinutes, startOfDay } from 'date-fns'
import { hashString, normalizeCourseName, WEEK_DAYS } from '../../utils.jsx'
import { layoutIntervals } from '../../interval-layout.js'
export const WorkWeekTranspose = ({ events }) => {
const eventsByWeekday = _.groupBy(events, event => event.start.getDay())
// For each weekday compute the interval layout
const rowLayouts = _.mapValues(eventsByWeekday, events =>
layoutIntervals(
events.map(e => ({
start: differenceInMinutes(e.start, startOfDay(e.start)),
end: differenceInMinutes(e.end, startOfDay(e.start)),
data: e,
}))
)
)
return (
<div class="work-week-h-view">
<div class="week">
{WEEK_DAYS.slice(1, 6).map((label, index) => (
<div class="day" style={{ '--size': rowLayouts[index + 1]?.length ?? 0 }}>
{label}
</div>
))}
</div>
<div class="events">
<div class="header">
<div class="label" style={{ 'grid-column': '2 / span 2' }}>
9:00 &ndash; 11:00
</div>
<div class="label" style={{ 'grid-column': '4 / span 2' }}>
11:00 &ndash; 13:00
</div>
<div class="label" style={{ 'grid-column': '7 / span 2' }}>
14:00 &ndash; 16:00
</div>
<div class="label" style={{ 'grid-column': '9 / span 2' }}>
16:00 &ndash; 18:00
</div>
</div>
<div class="days">
{Object.values(rowLayouts).map(layout => (
<div class="day" style={{ '--size': layout.length }}>
{layout.map((events, rowIndex) =>
events.map(event => {
const Local = () => {
const eventRef = useRef()
const updateMinWidth = () => {
eventRef.current.style.minWidth =
eventRef.current.offsetWidth + 'px'
}
useEffect(() => {
if (eventRef.current) {
setTimeout(updateMinWidth, 100)
window.addEventListener('resize', updateMinWidth)
return () => {
window.removeEventListener(
'resize',
updateMinWidth
)
}
}
}, [eventRef.current])
return (
<div
class="event"
style={{
'--row': rowIndex + 1,
'--start': event.start / 60 - 7,
'--size': (event.end - event.start) / 60,
'--hue':
(Math.abs(
hashString('seed3' + event.data.id)
) %
360) +
'deg',
}}
ref={eventRef}
>
{normalizeCourseName(event.data.name)}
</div>
)
}
return <Local />
})
)}
</div>
))}
</div>
</div>
</div>
)
}

@ -34,6 +34,55 @@ export function layoutIntervals(intervals, { tight } = {}) {
return stack.map(({ intervals }) => intervals)
}
function layoutBlockEvents(events) {
events.sort((a, b) => a.start - b.start)
let result = []
for (const event of events) {
let viableIndex = 0
while (
result.filter(
e => e.index === viableIndex && e.start < event.end && event.start < e.end
).length !== 0
) {
viableIndex += 1
}
result.push({ ...event, index: viableIndex })
}
return result
}
export function layoutEvents(events) {
const overlap = (event, block) => event.start < block.end && block.start < event.end
events.sort((a, b) => a.start - b.start)
let layout = []
for (const event of events) {
const blocks = layout.filter(block => overlap(event, block))
if (blocks.length > 0) {
layout = layout.filter(block => !overlap(event, block))
layout.push({
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]),
})
} else {
layout.push({ start: event.start, end: event.end, events: [event] })
}
}
return layout.map(block => {
const events = layoutBlockEvents(block.events)
return {
...block,
layers: Math.max(...events.map(event => event.index)) + 1,
events: events,
}
})
}
// //
// // Testing...
// //

@ -1,204 +1,311 @@
import _ from 'lodash'
import { render } from 'preact'
import { useEffect, useState } from 'preact/hooks'
import { ToolOverlay } from './components/CourseVisibility.jsx'
import { EventsView, MODE_WORKWEEK_GRID } from './components/EventsView.jsx'
// import { ToolOverlay } from './components/ToolOverlay.jsx'
//
// import {
// EventsView,
// MODE_COURSES,
// MODE_SCHEDULE,
// } from './components/EventsView.jsx'
import { Courses } from './components/view/Courses.jsx'
import { Schedule } from './components/view/Schedule.jsx'
import { HamburgerMenu } from './components/HamburgerMenu.jsx'
import { Help } from './components/Help.jsx'
import { Icon } from './components/Icon.jsx'
import { Popup } from './components/Popup.jsx'
import { Toolbar } from './components/Toolbar.jsx'
import { Optionbar } from './components/Optionbar.jsx'
import { OptionBar } from './components/OptionBar.jsx'
import {
prettyAulaName,
prettyProfName,
clearOldPersistentStates,
usePersistentState,
} from './utils.jsx'
import { SettingsBar } from './components/SettingsBar.jsx'
// Che fanno queste due righe?
window._ = _
window.dataBuffer = {}
const CALENDAR_IDS = {
'anno-1': ['6308cfcb1df5cb026699ce32'],
'anno-2': ['6308e2dc09352a0208fefdd9'],
'anno-3': ['6308e42a1df5cb026699ced4'],
'magistrale': ['6308e8ea0c34e703bb1f7e85'],
'tutti': [
'6308cfcb1df5cb026699ce32',
'6308e2dc09352a0208fefdd9',
'6308e42a1df5cb026699ced4',
'6308e8ea0c34e703bb1f7e85',
],
const TIMETABLE_IDS = {
'anno-1': '6798fa77c5e9ff00195153f0',
'anno-2': '6798fab7c5e9ff00195153f2',
'anno-3': '6798fb0038d6380019ee9e57',
'magistrale': '679a6b874528520019aa9e66',
}
// 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,
// }
function specialEventPatches(eventi) {
// Il laboratorio del primo anno in realtà è in due gruppi separati
eventi.forEach(evento => {
if (
evento.nome === 'LABORATORIO DI INTRODUZIONE ALLA MATEMATICA COMPUTAZIONALE'
) {
if (evento.evento.dettagliDidattici[0].partizione.descrizione === 'CORSO A') {
evento.nome += ' (A)'
}
if (evento.evento.dettagliDidattici[0].partizione.descrizione === 'CORSO B') {
evento.nome += ' (B)'
}
}
})
return eventi
}
async function loadEventi(ids) {
const calendari = await Promise.all(
ids.map(async id => {
// Almost directly copy-pasted from Chrome Dev Tools
const req = await fetch(
'https://apache.prod.up.cineca.it/api/Impegni/getImpegniCalendarioPubblico',
{
headers: {
'content-type': 'application/json;charset=UTF-8',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-site',
},
body: JSON.stringify({
mostraImpegniAnnullati: true,
mostraIndisponibilitaTotali: false,
linkCalendarioId: id,
clienteId: '628de8b9b63679f193b87046',
pianificazioneTemplate: false,
dataInizio: '2022-10-02T22:00:00.000Z',
dataFine: '2022-10-07T22:00:00.000Z',
}),
method: 'POST',
mode: 'cors',
credentials: 'omit',
}
)
return await req.json()
})
)
// console.log(calendari)
if (ids.length === 1) {
return calendari[0]
}
return _.uniqBy(_.concat(...calendari), 'id')
function formatEvents(timetable) {
return timetable.map(({ nome, dataInizio, dataFine, docenti, aule }) => {
return {
id: nome,
name: _.split(nome, '-', 1)[0].trim(),
start: new Date(dataInizio),
end: new Date(dataFine),
docenti: docenti.map(({ nome, cognome }) => prettyProfName(nome, cognome)),
aule: aule.map(aula => prettyAulaName(aula.codice)),
}
})
}
async function loadCalendari(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(
'https://apache.prod.up.cineca.it/api/Impegni/getImpegniCalendarioPubblico',
{
headers: {
'content-type': 'application/json;charset=UTF-8',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-site',
},
body: JSON.stringify({
mostraImpegniAnnullati: true,
mostraIndisponibilitaTotali: false,
linkCalendarioId: id,
clienteId: '628de8b9b63679f193b87046',
pianificazioneTemplate: false,
dataInizio: monday.toISOString(),
dataFine: saturday.toISOString(),
}),
method: 'POST',
mode: 'cors',
credentials: 'omit',
}
)
return await req.json()
}
const requests = [
req(TIMETABLE_IDS['anno-1']),
req(TIMETABLE_IDS['anno-2']),
req(TIMETABLE_IDS['anno-3']),
req(TIMETABLE_IDS['magistrale']),
]
const results = await Promise.all(requests)
const timetablesRaw = results.map(timetable =>
specialEventPatches(_.uniqBy(timetable, 'id'))
)
const allRaw = specialEventPatches(_.concat(...results), 'id')
return {
'anno-1': formatEvents(timetablesRaw[0]),
'anno-2': formatEvents(timetablesRaw[1]),
'anno-3': formatEvents(timetablesRaw[2]),
'magistrale': formatEvents(timetablesRaw[3]),
'tutti': formatEvents(allRaw),
}
}
const 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 = ({}) => {
// Data Sources
const [source, setSource] = useState('anno-1')
const [eventi, setEventi] = useState([])
// View Modes
const [mode, setMode] = useState(MODE_WORKWEEK_GRID)
// Selection
const [selectedCourses, setSelectedCourses] = useState([])
const [hideOtherCourses, setHideOtherCourses] = useState(false)
// Menus
const [helpVisible, setHelpVisible] = useState(false)
const [showMobileMenu, setShowMobileMenu] = useState(false)
useEffect(async () => {
console.log('source changed')
const eventi = await loadEventi(CALENDAR_IDS[source])
window.dataBuffer[source] = eventi
setEventi(eventi)
}, [source])
const groupIds = new Set(eventi.map(e => e.nome))
const toolOverlayVisible =
selectedCourses.length > 0 && selectedCourses.filter(id => groupIds.has(id)).length > 0
useEffect(() => {
console.log('course length changed')
const groupIds = new Set(eventi.map(e => e.nome))
if (
selectedCourses.length === 0 ||
selectedCourses.filter(id => groupIds.has(id)).length === 0
) {
setHideOtherCourses(false)
}
}, [eventi, selectedCourses.length])
const [theme, setTheme] = useState(
localStorage.getItem('theme') ??
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
)
document.body.classList.toggle('dark-mode', theme === 'dark')
localStorage.setItem('theme', theme)
return (
<>
<Toolbar
{...{
mode,
setMode,
source,
setSource,
onShowMenu: () => setShowMobileMenu(true),
onHelp: () => setHelpVisible(true),
theme,
setTheme,
}}
/>
<Optionbar
{...{
mode,
setMode,
source,
setSource,
onHelp: () => setHelpVisible(true),
}}
/>
<EventsView
mode={mode}
selection={selectedCourses}
setSelection={setSelectedCourses}
hideOtherCourses={hideOtherCourses}
start={new Date(2022, 10, 3)}
events={eventi.map(({ nome, dataInizio, dataFine, docenti, aule }) => ({
id: nome,
name: _.split(nome, '-', 1)[0].trim(),
start: new Date(dataInizio),
end: new Date(dataFine),
docenti: docenti.map(({ nome, cognome }) =>
_.startCase(_.lowerCase(nome) + ' ' + _.lowerCase(cognome))
),
aula: _.startCase(aule[0].codice.toLowerCase()).replace(
/([A-Z]) ([1-9])/,
'$1$2'
),
}))}
/>
{toolOverlayVisible && (
<ToolOverlay
visibility={hideOtherCourses}
toggleVisibility={() => setHideOtherCourses(s => !s)}
onClose={() => {
setSelectedCourses([])
setHideOtherCourses(false)
}}
/>
)}
{showMobileMenu && (
<HamburgerMenu
{...{
mode,
setMode,
source,
setSource,
theme,
setTheme,
onClose: () => {
setShowMobileMenu(false)
},
}}
/>
)}
{helpVisible && (
<Popup
title={
<>
<Icon name="info" /> Guida
</>
}
onClose={() => setHelpVisible(false)}
>
<Help />
</Popup>
)}
</>
)
// 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')
const [date, setDate] = useState(new Date().toISOString())
// Data Sources
const [view, setView] = usePersistentState('view', 'magistrale')
const [timetables, setTimetables] = useState(null)
useEffect(async () => {
setTimetables(await loadCalendari(new Date(date)))
}, [date])
// View Modes
// const [mode, setMode] = usePersistentState('orario.mode', MODE_COURSES)
// Selection
const [selectedCourses, setSelectedCourses] = usePersistentState('selection', [])
// Menus
const [helpVisible, setHelpVisible] = useState(false)
const [showMobileMenu, setShowMobileMenu] = useState(false)
const [theme, setTheme] = usePersistentState(
'theme',
'light'
// window.matchMedia('(prefers-color-scheme: dark)').matches
// ? 'dark'
// : 'light'
)
document.body.classList.toggle('dark-mode', theme === 'dark')
return (
<>
<Toolbar
{...{
source: view,
setSource: setView,
date: date,
setDate: setDate,
showMobileMenu: showMobileMenu,
setShowMobileMenu: setShowMobileMenu,
onHelp: () => setHelpVisible(true),
theme,
setTheme,
}}
/>
{showMobileMenu ? (
<SettingsBar
{...{
theme,
setTheme,
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('2025-03-03T00:00:00.000Z')
}}
>
Vai al 3 marzo! 🚀
</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={
<>
<Icon name="info" /> Guida
</>
}
onClose={() => setHelpVisible(false)}
>
<Help />
</Popup>
)}
</>
)
}
render(<App />, document.body)

File diff suppressed because it is too large Load Diff

@ -1,5 +1,7 @@
// Calendar
import { useEffect, useState } from 'preact/hooks'
export const WEEK_DAYS = [
'Domenica',
'Lunedì',
@ -10,6 +12,8 @@ export const WEEK_DAYS = [
'Sabato',
]
export const WEEK_DAYS_SHORT = ['Dom', 'Lun', 'Mar', 'Mer', 'Gio', 'Ven', 'Sab']
// Hashing
export function hashString(str, seed = 0) {
@ -27,7 +31,7 @@ export function hashString(str, seed = 0) {
// Courses
export function normalizeCourseName(name) {
export function prettyCourseName(name) {
return name
.split(' ')
.map(word => word.toLowerCase())
@ -40,8 +44,24 @@ export function normalizeCourseName(name) {
})
.join(' ')
.replaceAll('Ii', 'II')
.replaceAll('IIi', 'III')
.replaceAll('Iii', 'III')
.replaceAll(/'(.)/g, ({}, letter) => "'" + letter.toUpperCase())
.replaceAll(/\((a|b)\)/g, ({}, letter) => '(' + letter.toUpperCase() + ')')
}
export function prettyAulaName(name) {
return name
.replace('FIB', 'Fib')
.replace('RIUNIONI', 'Riunioni')
.replace(/([A-Z0-9])\-LAB/, 'Lab $1')
}
export function prettyProfName(nome, cognome) {
return (nome + ' ' + cognome)
.split(/\b/)
.map(word => _.capitalize(word))
.join('')
}
// JSX
@ -53,3 +73,30 @@ export const withClasses = classes =>
.filter(([, value]) => value)
.map(([key]) => key)
.join(' ')
// LocalStorage stuff
export const clearOldPersistentStates = stateToken => {
const lastStateToken = localStorage.getItem('orario.state_token')
if (lastStateToken === null || lastStateToken !== stateToken) {
localStorage.clear()
localStorage.setItem('orario.state_token', stateToken)
}
}
// Hooks
export const usePersistentState = (key, initialValue) => {
const previousValue = localStorage.getItem(`orario.${key}`)
const [value, setValue] = useState(
previousValue !== null ? JSON.parse(previousValue) : initialValue
)
useEffect(() => {
localStorage.setItem(`orario.${key}`, JSON.stringify(value))
}, [value])
return [value, setValue]
}

Loading…
Cancel
Save