forked from phc/orario
Compare commits
68 Commits
mobile-hea
...
main
@ -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
|
@ -0,0 +1,9 @@
|
||||
{
|
||||
"printWidth": 90,
|
||||
"singleQuote": true,
|
||||
"quoteProps": "consistent",
|
||||
"tabWidth": 4,
|
||||
"semi": false,
|
||||
"arrowParens": "avoid",
|
||||
"proseWrap": "always"
|
||||
}
|
@ -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>
|
||||
|
Binary file not shown.
After Width: | Height: | Size: 2.0 KiB |
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
@ -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,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')} –{' '}
|
||||
{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 – 11:00
|
||||
</div>
|
||||
<div
|
||||
class="block"
|
||||
style={{
|
||||
'--start': 11 - 7,
|
||||
'--size': 2,
|
||||
}}
|
||||
>
|
||||
11:00 – 13:00
|
||||
</div>
|
||||
<div
|
||||
class="block skip-border"
|
||||
style={{
|
||||
'--start': 14 - 7,
|
||||
'--size': 2,
|
||||
}}
|
||||
>
|
||||
14:00 – 16:00
|
||||
</div>
|
||||
<div
|
||||
class="block"
|
||||
style={{
|
||||
'--start': 16 - 7,
|
||||
'--size': 2,
|
||||
}}
|
||||
>
|
||||
16:00 – 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 – 11:00
|
||||
</div>
|
||||
<div class="label" style={{ 'grid-column': '4 / span 2' }}>
|
||||
11:00 – 13:00
|
||||
</div>
|
||||
<div class="label" style={{ 'grid-column': '7 / span 2' }}>
|
||||
14:00 – 16:00
|
||||
</div>
|
||||
<div class="label" style={{ 'grid-column': '9 / span 2' }}>
|
||||
16:00 – 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>
|
||||
)
|
||||
}
|
@ -1,204 +1,302 @@
|
||||
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': '667e88275e9623041f0e43d4',
|
||||
'anno-2': '667e89055e9623041f0e43d6',
|
||||
'anno-3': '667e89fcf748ed0415a11dcc',
|
||||
'magistrale': '667ebae63379a3046517ffd4',
|
||||
}
|
||||
|
||||
// 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.docenti[0].nome === 'GIOVANNI') {
|
||||
evento.nome += ' (A)'
|
||||
}
|
||||
if (evento.docenti[0].nome === 'PAOLO') {
|
||||
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 groupIds = new Set(eventi.map(e => e.nome))
|
||||
const toolOverlayVisible = selectedCourses.length > 0
|
||||
|
||||
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>
|
||||
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
Loading…
Reference in New Issue