Aggiunta trasposizione
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { Courses } from './view/Courses.jsx'
|
||||
import { WorkWeekGrid as Schedule } from './view/WorkWeekGrid.jsx'
|
||||
import { Schedule } from './view/Schedule.jsx'
|
||||
|
||||
export const MODE_COURSES = 'course'
|
||||
export const MODE_WORKWEEK = 'work-week'
|
||||
@@ -12,12 +12,31 @@ const viewModeMap = {
|
||||
[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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { MODE_COURSES, MODE_SCHEDULE, MODE_WORKWEEK, MODE_WORKWEEK_GRID } from '
|
||||
import { Help } from './Help.jsx'
|
||||
import { Icon } from './Icon.jsx'
|
||||
|
||||
export const HamburgerMenu = ({ onClose, mode, setMode, source, setSource, theme, setTheme }) => {
|
||||
export const HamburgerMenu = ({ onClose, theme, setTheme }) => {
|
||||
return (
|
||||
<div class="menu">
|
||||
<div class="header">
|
||||
|
||||
+19
-35
@@ -2,47 +2,29 @@ import { Icon } from './Icon.jsx'
|
||||
|
||||
export const Help = ({}) => (
|
||||
<>
|
||||
<h3>Selezione Corsi</h3>
|
||||
<h3>Visualizzazione Corsi</h3>
|
||||
<p>
|
||||
Puoi selezionare dei corsi cliccandoli, e nascondere 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>
|
||||
La selezione effettuata verrà preservata su tutte le modalità di visualizzazione disponibili, permettendo di individuare eventuali sovrapposizioni.
|
||||
Una volta compiuta la selezione, è possibile vedere la tabella delle
|
||||
lezioni andando nella visualizzazione Orario
|
||||
</p>
|
||||
<p>
|
||||
I corsi sono raggruppati per: primo, secondo, terzo anno e magistrale (rispettivamente <b>I</b>, <b>II</b>, <b>III</b> ed <b>M</b> su mobile).
|
||||
Altrimenti, puoi scegliere fra tutti i corsi disponibili cliccando su <b>Tutti</b>{' '}
|
||||
(tasto <Icon name="apps" /> su mobile).
|
||||
</p>
|
||||
<h3>Mobile</h3>
|
||||
<p>
|
||||
Da mobile, ci sono tre visualizzazioni possibili:
|
||||
<ul>
|
||||
<li>
|
||||
<Icon name="list" /> <b>Corsi</b> mostra una lista dei corsi in ordine
|
||||
alfabetico con ogni lezione per corso
|
||||
</li>
|
||||
<li>
|
||||
<Icon name="calendar_view_month" /> <b>Schema</b> mostra uno schema compatto dei
|
||||
corsi selezionati
|
||||
</li>
|
||||
<li>
|
||||
<Icon name="calendar_view_day" />
|
||||
<b>Giorno</b> dà una visualizzazione giornaliera della settimana
|
||||
</li>
|
||||
</ul>
|
||||
</p>
|
||||
<h3>Desktop</h3>
|
||||
<p>
|
||||
Da desktop, si può inoltre visualizzare l'orario settimanale esteso cliccando su <b>Settimana</b>, che
|
||||
mostrerà un calendario con i corsi attualmente selezionati.
|
||||
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>
|
||||
<h3>Stampa</h3>
|
||||
<p>
|
||||
Da desktop puoi stampare l'orario attualmente visibile con il bottone{' '}
|
||||
<Icon name="print" /> (è consigliato controllare le opzioni di stampa per ottenere un
|
||||
risultato soddisfacente).
|
||||
Da desktop puoi stampare l'orario attualmente visibile con il
|
||||
bottone <Icon name="print" /> (è consigliato controllare le opzioni
|
||||
di stampa per ottenere un risultato soddisfacente).
|
||||
</p>
|
||||
<h3>Bug & Contatti</h3>
|
||||
<p>
|
||||
@@ -51,8 +33,10 @@ export const Help = ({}) => (
|
||||
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
|
||||
passare direttamente in PHC.
|
||||
<a href="mailto:macchinisti@lists.dm.unipi.it">
|
||||
macchinisti@lists.dm.unipi.it
|
||||
</a>{' '}
|
||||
oppure passare direttamente in PHC.
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -12,12 +12,23 @@ export const OptionBar = ({ source, setSource }) => {
|
||||
{ value: 'anno-2', label: 'II' },
|
||||
{ value: 'anno-3', label: 'III' },
|
||||
{ value: 'magistrale', label: 'M' },
|
||||
{ value: 'tutti', label: <Icon name="apps" />, icon: true },
|
||||
{ value: 'tutti', label: 'Tutti' },
|
||||
]}
|
||||
value={source}
|
||||
setValue={setSource}
|
||||
/>
|
||||
</div>
|
||||
<CompoundButton
|
||||
options={[
|
||||
{
|
||||
value: 'orario',
|
||||
label: <Icon name="calendar_month" />,
|
||||
icon: true,
|
||||
},
|
||||
]}
|
||||
value={source}
|
||||
setValue={setSource}
|
||||
/>
|
||||
</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>
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@ 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_view_month' : 'list'} />
|
||||
<Icon name={mode === MODE_COURSES ? 'calendar_month' : 'view_list'} />
|
||||
</button>
|
||||
<button class="icon" onClick={onClose}>
|
||||
<Icon name="close" />
|
||||
@@ -17,7 +17,8 @@ export const Toolbar = ({
|
||||
</button>
|
||||
</div>
|
||||
<div class="item logo">
|
||||
<img src="logo-circuit-board.svg" alt="logo" /> / <span>Orario</span>
|
||||
<img src="logo-circuit-board.svg" alt="logo" /> /{' '}
|
||||
<span>Orario</span>
|
||||
</div>
|
||||
<div class="option-group">
|
||||
<div class="item option">
|
||||
@@ -33,6 +34,15 @@ export const Toolbar = ({
|
||||
setValue={setSource}
|
||||
/>
|
||||
</div>
|
||||
<div class="item option">
|
||||
<CompoundButton
|
||||
options={[
|
||||
{ value: 'orario', label: 'Orario' },
|
||||
]}
|
||||
value={source}
|
||||
setValue={setSource}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="option-group">
|
||||
<div class="item option">
|
||||
@@ -43,9 +53,13 @@ export const Toolbar = ({
|
||||
<div class="item option">
|
||||
<button
|
||||
class="icon"
|
||||
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
|
||||
onClick={() =>
|
||||
setTheme(theme === 'dark' ? 'light' : 'dark')
|
||||
}
|
||||
>
|
||||
<Icon name={theme === 'dark' ? 'dark_mode' : 'light_mode'} />
|
||||
<Icon
|
||||
name={theme === 'dark' ? 'dark_mode' : 'light_mode'}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="item option">
|
||||
|
||||
@@ -1,55 +1,295 @@
|
||||
import { format } from 'date-fns'
|
||||
import { useEffect, useRef, useState } from 'preact/hooks'
|
||||
|
||||
import _ from 'lodash'
|
||||
import { prettyCourseName, WEEK_DAYS, withClasses } from '../../utils.jsx'
|
||||
import { differenceInMinutes, startOfDay } from 'date-fns'
|
||||
|
||||
export const Schedule = ({ timetables, selection, setSelection }) => {
|
||||
const events = timetables['tutti']
|
||||
const selectionSet = new Set(selection)
|
||||
|
||||
const visibleEvents = events.filter(e => selectionSet.has(e.id))
|
||||
|
||||
const eventsByWeekday = _.mapValues(
|
||||
_.groupBy(visibleEvents, e => e.start.getDay()),
|
||||
dailyEvents => _.groupBy(dailyEvents, e => e.start.getHours())
|
||||
)
|
||||
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 (
|
||||
<div class="schedule-view">
|
||||
{Object.entries(eventsByWeekday).map(([index, dailyEvents]) => (
|
||||
<Popup
|
||||
title={
|
||||
<>
|
||||
<div class="header giorno">
|
||||
<div class="inner">{WEEK_DAYS[index]}</div>
|
||||
</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">{prettyCourseName(event.name)}</div>
|
||||
<div class="orario">
|
||||
{format(event.start, 'H:mm')} –{' '}
|
||||
{format(event.end, 'H:mm')}
|
||||
</div>
|
||||
<div class="aula">{event.aule.join(', ')}</div>
|
||||
<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>
|
||||
|
||||
<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 NoCourseWarning = () => {
|
||||
return (
|
||||
<div class="no-courses-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 }) => {
|
||||
console.log(layout)
|
||||
return (
|
||||
<>
|
||||
{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="day-label" style={`--position: ${n + 1}`}>
|
||||
{WEEK_DAYS_SHORT[n]}
|
||||
</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, setSelection }) => {
|
||||
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,157 +0,0 @@
|
||||
import { useEffect, useRef, useState } from 'preact/hooks'
|
||||
|
||||
import _ from 'lodash'
|
||||
import { differenceInMinutes, startOfDay } from 'date-fns'
|
||||
|
||||
import { hashString, prettyCourseName, 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': 2 * (9 - 7),
|
||||
'--size': 4,
|
||||
}}
|
||||
>
|
||||
9:00 – 11:00
|
||||
</div>
|
||||
<div
|
||||
class="block"
|
||||
style={{
|
||||
'--start': 2 * (11 - 7),
|
||||
'--size': 4,
|
||||
}}
|
||||
>
|
||||
11:00 – 13:00
|
||||
</div>
|
||||
<div
|
||||
class="block skip-border"
|
||||
style={{
|
||||
'--start': 2 * (14 - 7),
|
||||
'--size': 4,
|
||||
}}
|
||||
>
|
||||
14:00 – 16:00
|
||||
</div>
|
||||
<div
|
||||
class="block"
|
||||
style={{
|
||||
'--start': 2 * (16 - 7),
|
||||
'--size': 4,
|
||||
}}
|
||||
>
|
||||
16:00 – 18:00
|
||||
</div>
|
||||
<div
|
||||
class="block"
|
||||
style={{
|
||||
'--start': 2 * (18 - 7),
|
||||
'--size': 4,
|
||||
}}
|
||||
>
|
||||
18:00 – 20: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 / 30 - 14,
|
||||
'--stack': stackIndex + 1,
|
||||
'--size': (event.end - event.start) / 30,
|
||||
'--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">{prettyCourseName(event.data.name)}</div>
|
||||
<div class="aula">{event.data.aule.join(', ')}</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
))}
|
||||
|
||||
{/* Grid Tracks */}
|
||||
{[4, 8, 12].map(i => (
|
||||
<div class="grid-line-h" style={{ '--track': i }}></div>
|
||||
))}
|
||||
{[14, 18, 22].map(i => (
|
||||
<div class="grid-line-h" style={{ '--track': i }}></div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,227 +0,0 @@
|
||||
import { useEffect, useRef, useState } from 'preact/hooks'
|
||||
|
||||
import _ from 'lodash'
|
||||
import { differenceInMinutes, startOfDay } from 'date-fns'
|
||||
|
||||
import { prettyCourseName } from '../../utils.jsx'
|
||||
import { layoutIntervals } from '../../interval-layout.js'
|
||||
|
||||
export const WorkWeekGrid = ({ timetables, selection, setSelection }) => {
|
||||
const events = timetables['tutti']
|
||||
const selectionSet = new Set(selection)
|
||||
|
||||
const colorList = ['red', 'purple', 'blue', 'yellow', 'green', 'orange', 'lightblue']
|
||||
|
||||
const courses = _.uniqBy(
|
||||
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(
|
||||
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.aule.join(', ')}
|
||||
</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">{prettyCourseName(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, prettyCourseName, 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}
|
||||
>
|
||||
{prettyCourseName(event.data.name)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return <Local />
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -34,6 +34,62 @@ 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...
|
||||
// //
|
||||
|
||||
+97
-71
@@ -1,43 +1,44 @@
|
||||
import _ from 'lodash'
|
||||
import { render } from 'preact'
|
||||
import { useEffect, useState } from 'preact/hooks'
|
||||
import { ToolOverlay } from './components/CourseVisibility.jsx'
|
||||
|
||||
import { EventsView, MODE_COURSES, MODE_SCHEDULE } 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 { prettyAulaName, prettyProfName, usePersistentState } from './utils.jsx'
|
||||
import {
|
||||
prettyAulaName,
|
||||
prettyProfName,
|
||||
clearOldPersistentStates,
|
||||
usePersistentState,
|
||||
} from './utils.jsx'
|
||||
|
||||
// Che fanno queste due righe?
|
||||
window._ = _
|
||||
window.dataBuffer = {}
|
||||
|
||||
const CALENDAR_IDS = {
|
||||
'anno-1': ['64a7c1c651f079001d52e9c8'],
|
||||
'anno-2': ['6308e2dc09352a0208fefdd9'],
|
||||
'anno-3': ['6308e42a1df5cb026699ced4'],
|
||||
'magistrale': ['64a7c7091ab813002c5d9ede'],
|
||||
'tutti': [
|
||||
'64a7c1c651f079001d52e9c8',
|
||||
'6308e2dc09352a0208fefdd9',
|
||||
'6308e42a1df5cb026699ced4',
|
||||
'64a7c7091ab813002c5d9ede',
|
||||
],
|
||||
}
|
||||
|
||||
const TIMETABLE_IDS = {
|
||||
'anno-1': '64a7c1c651f079001d52e9c8',
|
||||
'anno-2': '6308e2dc09352a0208fefdd9',
|
||||
'anno-3': '6308e42a1df5cb026699ced4',
|
||||
'anno-1': '64a7c1c651f079001d52e9c8',
|
||||
'anno-2': '6308e2dc09352a0208fefdd9',
|
||||
'anno-3': '6308e42a1df5cb026699ced4',
|
||||
'magistrale': '64a7c7091ab813002c5d9ede',
|
||||
}
|
||||
|
||||
const DEFAULT_DATE_RANGE = {
|
||||
from: '2023-10-02T00:00:00.000Z',
|
||||
to: '2023-10-07T00:00:00.000Z',
|
||||
from: '2023-10-09T00:00:00.000Z',
|
||||
to: '2023-10-14T00:00:00.000Z',
|
||||
}
|
||||
|
||||
const DATE_RANGES = {
|
||||
@@ -51,7 +52,10 @@ function specialEventPatches(eventi) {
|
||||
// Il laboratorio del primo anno in realtà è in due canali separati
|
||||
let i = 1
|
||||
eventi.forEach(evento => {
|
||||
if (evento.nome === 'LABORATORIO DI INTRODUZIONE ALLA MATEMATICA COMPUTAZIONALE') {
|
||||
if (
|
||||
evento.nome ===
|
||||
'LABORATORIO DI INTRODUZIONE ALLA MATEMATICA COMPUTAZIONALE'
|
||||
) {
|
||||
evento.nome += ` (${i})`
|
||||
i++
|
||||
}
|
||||
@@ -61,18 +65,18 @@ function specialEventPatches(eventi) {
|
||||
}
|
||||
|
||||
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)),
|
||||
}
|
||||
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() {
|
||||
@@ -112,31 +116,43 @@ async function loadCalendari() {
|
||||
req(TIMETABLE_IDS['magistrale']),
|
||||
]
|
||||
const results = await Promise.all(requests)
|
||||
const timetablesRaw = results.map(timetable => specialEventPatches(_.uniqBy(timetable, 'id')))
|
||||
const timetablesRaw = results.map(timetable =>
|
||||
specialEventPatches(_.uniqBy(timetable, 'id'))
|
||||
)
|
||||
const allRaw = specialEventPatches(_.uniqBy(_.concat(...results), 'id'))
|
||||
|
||||
return {
|
||||
'anno-1': formatEvents(timetablesRaw[0]),
|
||||
'anno-2': formatEvents(timetablesRaw[1]),
|
||||
'anno-3': formatEvents(timetablesRaw[2]),
|
||||
'anno-1': formatEvents(timetablesRaw[0]),
|
||||
'anno-2': formatEvents(timetablesRaw[1]),
|
||||
'anno-3': formatEvents(timetablesRaw[2]),
|
||||
'magistrale': formatEvents(timetablesRaw[3]),
|
||||
'tutti': formatEvents(allRaw)
|
||||
'tutti': formatEvents(allRaw),
|
||||
}
|
||||
}
|
||||
|
||||
const App = ({}) => {
|
||||
// Clear persistent states unless state_token corresponds to the one passed
|
||||
// as the argument. Useful with breaking updates. Change this token if your
|
||||
// (breaking) update needs a reset of persistent states to avoid crashes.
|
||||
//
|
||||
// Use any random string of your choice
|
||||
clearOldPersistentStates('e73cba02')
|
||||
|
||||
// Data Sources
|
||||
const [source, setSource] = usePersistentState('orario.source', 'magistrale')
|
||||
const [source, setSource] = usePersistentState('source', 'magistrale')
|
||||
const [timetables, setTimetables] = useState(null)
|
||||
useEffect(async () => {
|
||||
setTimetables(await loadCalendari())
|
||||
}, [])
|
||||
|
||||
// View Modes
|
||||
const [mode, setMode] = usePersistentState('orario.mode', MODE_COURSES)
|
||||
// const [mode, setMode] = usePersistentState('orario.mode', MODE_COURSES)
|
||||
|
||||
// Selection
|
||||
const [selectedCourses, setSelectedCourses] = usePersistentState('orario.selection', [])
|
||||
const [selectedCourses, setSelectedCourses] = usePersistentState(
|
||||
'selection',
|
||||
[]
|
||||
)
|
||||
|
||||
// Menus
|
||||
const [helpVisible, setHelpVisible] = useState(false)
|
||||
@@ -146,8 +162,11 @@ const App = ({}) => {
|
||||
const toolOverlayVisible = selectedCourses.length > 0
|
||||
|
||||
const [theme, setTheme] = usePersistentState(
|
||||
'orario.theme',
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
||||
'theme',
|
||||
'light'
|
||||
// window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
// ? 'dark'
|
||||
// : 'light'
|
||||
)
|
||||
|
||||
document.body.classList.toggle('dark-mode', theme === 'dark')
|
||||
@@ -156,8 +175,6 @@ const App = ({}) => {
|
||||
<>
|
||||
<Toolbar
|
||||
{...{
|
||||
mode,
|
||||
setMode,
|
||||
source,
|
||||
setSource,
|
||||
onShowMenu: () => setShowMobileMenu(true),
|
||||
@@ -166,44 +183,53 @@ const App = ({}) => {
|
||||
setTheme,
|
||||
}}
|
||||
/>
|
||||
{mode === MODE_COURSES && (
|
||||
<OptionBar
|
||||
{...{
|
||||
mode,
|
||||
setMode,
|
||||
source,
|
||||
setSource,
|
||||
onHelp: () => setHelpVisible(true),
|
||||
}}orizzontale
|
||||
/>
|
||||
)}
|
||||
<OptionBar
|
||||
{...{
|
||||
source,
|
||||
setSource,
|
||||
onHelp: () => setHelpVisible(true),
|
||||
}}
|
||||
orizzontale
|
||||
/>
|
||||
|
||||
{timetables && (
|
||||
<EventsView
|
||||
mode={mode}
|
||||
selection={selectedCourses}
|
||||
setSelection={setSelectedCourses}
|
||||
start={new Date(2022, 10, 3)}
|
||||
source={source}
|
||||
timetables={timetables}
|
||||
/>
|
||||
<div class="content">
|
||||
{source === 'orario' ? (
|
||||
<Schedule
|
||||
selection={selectedCourses}
|
||||
setSelection={setSelectedCourses}
|
||||
start={new Date(2022, 10, 3)}
|
||||
source={source}
|
||||
timetables={timetables}
|
||||
/>
|
||||
) : (
|
||||
<Courses
|
||||
selection={selectedCourses}
|
||||
setSelection={setSelectedCourses}
|
||||
start={new Date(2022, 10, 3)}
|
||||
source={source}
|
||||
timetables={timetables}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{toolOverlayVisible && (
|
||||
{/* toolOverlayVisible && (
|
||||
<ToolOverlay
|
||||
mode={mode}
|
||||
toggleMode={() => setMode(mode === MODE_COURSES ? MODE_SCHEDULE : MODE_COURSES)}
|
||||
toggleMode={() =>
|
||||
setMode(
|
||||
mode === MODE_COURSES ? MODE_SCHEDULE : MODE_COURSES
|
||||
)
|
||||
}
|
||||
onClose={() => {
|
||||
setSelectedCourses([])
|
||||
setMode(MODE_COURSES)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
) */}
|
||||
{showMobileMenu && (
|
||||
<HamburgerMenu
|
||||
{...{
|
||||
mode,
|
||||
setMode,
|
||||
source,
|
||||
setSource,
|
||||
theme,
|
||||
setTheme,
|
||||
onClose: () => {
|
||||
|
||||
+221
-578
@@ -28,32 +28,14 @@ html {
|
||||
--accent-500: #6cc16c;
|
||||
--accent-900: #244624;
|
||||
|
||||
--bubble-red: hsl(359, 100%, 92%);
|
||||
--bubble-purple: hsl(274, 100%, 92%);
|
||||
--bubble-blue: hsl(241, 100%, 92%);
|
||||
--bubble-yellow: hsl(50, 100%, 92%);
|
||||
--bubble-green: hsl(125, 100%, 92%);
|
||||
--bubble-orange: hsl(25, 100%, 92%);
|
||||
--bubble-lightlightblue: hsl(176, 100%, 92%);
|
||||
--bubble-lightblue: hsl(198, 100%, 92%);
|
||||
|
||||
--bubble-highlight-red: hsl(359, 100%, 85%);
|
||||
--bubble-highlight-purple: hsl(274, 100%, 85%);
|
||||
--bubble-highlight-blue: hsl(241, 100%, 85%);
|
||||
--bubble-highlight-yellow: hsl(50, 100%, 85%);
|
||||
--bubble-highlight-green: hsl(125, 100%, 85%);
|
||||
--bubble-highlight-orange: hsl(25, 100%, 85%);
|
||||
--bubble-highlight-lightlightblue: hsl(176, 100%, 85%);
|
||||
--bubble-highlight-lightblue: hsl(198, 100%, 85%);
|
||||
|
||||
--bubble-border-red: hsl(359, 75%, 51%);
|
||||
--bubble-border-purple: hsl(274, 75%, 51%);
|
||||
--bubble-border-blue: hsl(241, 75%, 51%);
|
||||
--bubble-border-yellow: hsl(50, 75%, 51%);
|
||||
--bubble-border-green: hsl(125, 75%, 51%);
|
||||
--bubble-border-orange: hsl(25, 75%, 51%);
|
||||
--bubble-border-lightlightblue: hsl(176, 75%, 51%);
|
||||
--bubble-border-lightblue: hsl(198, 75%, 51%);
|
||||
--event-red: hsl(359, 100%, 85%);
|
||||
--event-purple: hsl(274, 100%, 85%);
|
||||
--event-blue: hsl(241, 100%, 85%);
|
||||
--event-yellow: hsl(50, 100%, 85%);
|
||||
--event-green: hsl(125, 100%, 85%);
|
||||
--event-orange: hsl(25, 100%, 85%);
|
||||
--event-lightlightblue: hsl(176, 100%, 85%);
|
||||
--event-lightblue: hsl(198, 100%, 85%);
|
||||
|
||||
--bold-on-dark: 300;
|
||||
}
|
||||
@@ -76,29 +58,13 @@ body.dark-mode {
|
||||
--accent-500: hsl(269, 40%, 70%);
|
||||
--accent-900: hsl(269, 30%, 90%);
|
||||
|
||||
--bubble-red: hsl(359, 40%, 25%);
|
||||
--bubble-purple: hsl(274, 40%, 25%);
|
||||
--bubble-blue: hsl(241, 40%, 25%);
|
||||
--bubble-yellow: hsl(50, 40%, 25%);
|
||||
--bubble-green: hsl(125, 40%, 25%);
|
||||
--bubble-orange: hsl(25, 40%, 25%);
|
||||
--bubble-lightblue: hsl(176, 40%, 25%);
|
||||
|
||||
--bubble-border-red: hsl(359, 75%, 51%);
|
||||
--bubble-border-purple: hsl(274, 75%, 51%);
|
||||
--bubble-border-blue: hsl(241, 75%, 51%);
|
||||
--bubble-border-yellow: hsl(50, 75%, 51%);
|
||||
--bubble-border-green: hsl(125, 75%, 51%);
|
||||
--bubble-border-orange: hsl(25, 75%, 51%);
|
||||
--bubble-border-lightblue: hsl(176, 75%, 51%);
|
||||
|
||||
--bubble-highlight-red: hsl(359, 40%, 31%);
|
||||
--bubble-highlight-purple: hsl(274, 40%, 31%);
|
||||
--bubble-highlight-blue: hsl(241, 40%, 31%);
|
||||
--bubble-highlight-yellow: hsl(50, 40%, 31%);
|
||||
--bubble-highlight-green: hsl(125, 40%, 31%);
|
||||
--bubble-highlight-orange: hsl(25, 40%, 31%);
|
||||
--bubble-highlight-lightblue: hsl(176, 40%, 31%);
|
||||
--event-red: hsl(359, 40%, 31%);
|
||||
--event-purple: hsl(274, 40%, 31%);
|
||||
--event-blue: hsl(241, 40%, 31%);
|
||||
--event-yellow: hsl(50, 40%, 31%);
|
||||
--event-green: hsl(125, 40%, 31%);
|
||||
--event-orange: hsl(25, 40%, 31%);
|
||||
--event-lightblue: hsl(176, 40%, 31%);
|
||||
|
||||
--bold-on-dark: 500;
|
||||
}
|
||||
@@ -214,6 +180,10 @@ button,
|
||||
&.flat {
|
||||
border: none;
|
||||
}
|
||||
|
||||
&.small {
|
||||
padding: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.input-combo {
|
||||
@@ -294,49 +264,60 @@ button,
|
||||
|
||||
max-width: 100%;
|
||||
|
||||
.popup {
|
||||
@extend .panel;
|
||||
|
||||
.popup-wrapper {
|
||||
max-width: 100%;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
|
||||
box-shadow: 0 0 1.5rem #00000033;
|
||||
.popup {
|
||||
@extend .panel;
|
||||
|
||||
// position: absolute;
|
||||
// top: 50%;
|
||||
// left: 50%;
|
||||
// transform: translate(-50%, -50%);
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
|
||||
user-select: none;
|
||||
box-shadow: 0 0 1.5rem #00000033;
|
||||
|
||||
.title {
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
justify-content: space-between;
|
||||
|
||||
font-size: 24px;
|
||||
user-select: none;
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
@extend .text-block;
|
||||
.content {
|
||||
@extend .text-block;
|
||||
|
||||
width: 60ch;
|
||||
max-width: 100%;
|
||||
height: fit-content;
|
||||
max-height: 60vh;
|
||||
|
||||
overflow-y: auto;
|
||||
|
||||
padding-bottom: 3rem;
|
||||
}
|
||||
|
||||
width: 60ch;
|
||||
max-width: 100%;
|
||||
max-height: 60vh;
|
||||
|
||||
overflow-y: auto;
|
||||
|
||||
padding-bottom: 3rem;
|
||||
}
|
||||
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -496,7 +477,7 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
.events-view {
|
||||
.content {
|
||||
height: calc(100vh - 4rem);
|
||||
@media screen and (max-width: $device-s-width), (pointer: coarse) {
|
||||
height: calc(100vh - 8rem);
|
||||
@@ -568,564 +549,226 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
.work-week-h-view {
|
||||
--header-height: 3rem;
|
||||
--event-height: 2rem;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-template-rows: var(--header-height) 1fr;
|
||||
|
||||
overflow-y: scroll;
|
||||
|
||||
.schedule-view {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
max-width: 57rem;
|
||||
margin: auto;
|
||||
|
||||
.week {
|
||||
grid-column: 1 / 2;
|
||||
grid-row: 2 / 3;
|
||||
padding: 1rem 0.5rem;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
|
||||
.no-courses-warning {
|
||||
width: 100%;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
|
||||
.day {
|
||||
box-sizing: content-box;
|
||||
|
||||
// @media screen and (max-width: $device-s-width) {
|
||||
// writing-mode: vertical-lr;
|
||||
// }
|
||||
|
||||
height: calc(var(--size) * var(--event-height) + (var(--size) - 1) * 0.25rem);
|
||||
|
||||
border-top: 1px solid var(--border-500);
|
||||
border-right: 1px solid var(--border-500);
|
||||
padding: 0.5rem 1rem 0 0.5rem;
|
||||
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 1px solid var(--border-500);
|
||||
}
|
||||
p {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
.schedule-card {
|
||||
width: 100%;
|
||||
|
||||
.events {
|
||||
grid-column: 2 / 3;
|
||||
grid-row: 1 / 3;
|
||||
|
||||
overflow-y: hidden;
|
||||
overflow-x: scroll;
|
||||
|
||||
// scrollbar-gutter: stable both-edges;
|
||||
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr;
|
||||
|
||||
--hour-width: 7.5rem;
|
||||
|
||||
.header {
|
||||
.grid {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(12, var(--hour-width));
|
||||
grid-template-rows: var(--header-height);
|
||||
|
||||
gap: 0.25rem;
|
||||
border: 1px solid var(--border-600);
|
||||
border-radius: 10px 10px 0 0;
|
||||
|
||||
.label {
|
||||
display: grid;
|
||||
place-content: center;
|
||||
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
|
||||
border-left: 1px solid var(--border-500);
|
||||
|
||||
&:nth-child(2),
|
||||
&:nth-child(4) {
|
||||
border-right: 1px solid var(--border-500);
|
||||
}
|
||||
@media screen and (max-width: $device-s-width),
|
||||
(pointer: coarse) {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.days {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.transpose-button,
|
||||
.day-label,
|
||||
.time-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.day {
|
||||
display: grid;
|
||||
.transpose-button {
|
||||
width: 4rem;
|
||||
height: 3rem;
|
||||
}
|
||||
|
||||
// grid-template-columns: repeat(7, 0) repeat(12, 1fr) repeat(5, 0);
|
||||
grid-template-columns: repeat(12, var(--hour-width));
|
||||
grid-template-rows: repeat(var(--size), var(--event-height));
|
||||
|
||||
gap: 0.25rem;
|
||||
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.event {
|
||||
grid-row: var(--row) / span 1;
|
||||
grid-column: var(--start) / span var(--size);
|
||||
|
||||
$color: var(--bg-500);
|
||||
|
||||
background: $color;
|
||||
color: var(--text-500);
|
||||
|
||||
border-radius: 0.25rem;
|
||||
|
||||
border: 1px solid var(--border-600);
|
||||
|
||||
display: flex;
|
||||
padding: 0.4rem;
|
||||
|
||||
font-size: 15px;
|
||||
.event-block-wrapper {
|
||||
padding: 0.125rem;
|
||||
|
||||
.event-block {
|
||||
width: 100%;
|
||||
|
||||
min-height: var(--event-height);
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
|
||||
position: relative;
|
||||
|
||||
line-height: 1.1;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
.event-wrapper {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
top: 0%;
|
||||
left: 0%;
|
||||
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.125rem;
|
||||
|
||||
background: linear-gradient(to top, $color 0%, transparent 25%);
|
||||
}
|
||||
.event {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
&:hover {
|
||||
z-index: 3;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
position: absolute;
|
||||
overflow: visible;
|
||||
text-align: center;
|
||||
|
||||
&::after {
|
||||
background: none;
|
||||
background: var(--color);
|
||||
border-radius: 10px;
|
||||
font-weight: var(--bold-on-dark);
|
||||
}
|
||||
}
|
||||
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
border-top: 1px solid var(--border-500);
|
||||
padding: 0.25rem 0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 1px solid var(--border-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.work-week-v-view {
|
||||
position: relative;
|
||||
&.original {
|
||||
grid-template-columns: auto repeat(5, 1fr);
|
||||
grid-template-rows: min-content repeat(
|
||||
var(--time-slots),
|
||||
1fr
|
||||
);
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: max-content repeat(5, minmax(auto, 1fr));
|
||||
|
||||
height: 100%;
|
||||
|
||||
overflow: scroll;
|
||||
|
||||
// --event-height: 4.75rem;
|
||||
--event-height: 6rem;
|
||||
|
||||
.pivot {
|
||||
height: 3rem;
|
||||
|
||||
grid-column: 1 / 2;
|
||||
grid-row: 1 / 2;
|
||||
|
||||
position: sticky;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
z-index: 2;
|
||||
|
||||
background: var(--bg-500);
|
||||
|
||||
border-bottom: 1px solid var(--border-500);
|
||||
border-right: 1px solid var(--border-500);
|
||||
}
|
||||
|
||||
.left-header {
|
||||
grid-column: 1 / 2;
|
||||
grid-row: 1 / 2;
|
||||
|
||||
display: grid;
|
||||
|
||||
position: sticky;
|
||||
left: 0;
|
||||
|
||||
border-right: 1px solid var(--border-500);
|
||||
|
||||
background: var(--bg-500);
|
||||
|
||||
z-index: 1;
|
||||
|
||||
padding-top: 3rem;
|
||||
|
||||
.blocks {
|
||||
display: grid;
|
||||
|
||||
grid-template-rows: repeat(24, calc(var(--event-height) / 2));
|
||||
|
||||
.block {
|
||||
grid-row: var(--start) / span var(--size);
|
||||
|
||||
display: grid;
|
||||
place-content: center;
|
||||
|
||||
padding: 0.5rem;
|
||||
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
|
||||
border-top: 1px solid var(--border-500);
|
||||
|
||||
@media screen and (max-width: $device-s-width), (pointer: coarse) {
|
||||
writing-mode: vertical-lr;
|
||||
.transpose-button,
|
||||
.day-label {
|
||||
grid-column: var(--position);
|
||||
grid-row: 1;
|
||||
}
|
||||
.time-label {
|
||||
min-height: 5rem;
|
||||
grid-column: 1;
|
||||
grid-row: calc(var(--position) + 2) / span 4;
|
||||
}
|
||||
|
||||
&:not(.skip-border) {
|
||||
height: calc(2 * var(--event-height) + 0.5px);
|
||||
border-bottom: 1px solid var(--border-500);
|
||||
.day-line {
|
||||
grid-column: var(--position);
|
||||
grid-row: 1/-1;
|
||||
width: 0px;
|
||||
border-right: 1px solid var(--border-500);
|
||||
}
|
||||
.time-line {
|
||||
grid-row: calc(var(--position) + 2);
|
||||
grid-column: 1/-1;
|
||||
height: 0px;
|
||||
border-top: 1px dashed var(--border-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.day {
|
||||
position: relative;
|
||||
.event-block-wrapper {
|
||||
grid-row: calc(var(--time-start) + 2) /
|
||||
calc(var(--time-end) + 2);
|
||||
grid-column: calc(var(--day-position) + 1);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.top-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
height: 3rem;
|
||||
|
||||
flex: 0 0 auto;
|
||||
|
||||
z-index: 1;
|
||||
|
||||
grid-column: span var(--size);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
|
||||
background: var(--bg-500);
|
||||
border-bottom: 1px solid var(--border-500);
|
||||
}
|
||||
|
||||
// &:not(:last-child) {
|
||||
border-right: 1px solid var(--border-500);
|
||||
// }
|
||||
|
||||
.events {
|
||||
position: relative;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--size), auto);
|
||||
grid-template-rows: repeat(24, calc(var(--event-height) / 2));
|
||||
|
||||
padding: 0 0.25rem;
|
||||
|
||||
// gap: 1rem 0.5rem;
|
||||
|
||||
.event {
|
||||
position: relative;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
grid-row: var(--start) / span var(--size);
|
||||
grid-column: var(--stack) / span 1;
|
||||
|
||||
background: var(--bg-500);
|
||||
|
||||
border: 1px solid var(--border-600);
|
||||
// border: 4px solid hsl(var(--hue), 80%, 80%);
|
||||
|
||||
border-radius: 0.5rem;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
gap: 0.25rem;
|
||||
|
||||
padding: 0.5rem 1rem 0.5rem 0.5rem;
|
||||
margin: 0.5rem 0.25rem;
|
||||
|
||||
// box-sizing: content-box;
|
||||
|
||||
&.selected {
|
||||
background: var(--bg-selected-500);
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -1px;
|
||||
border-radius: 0.5rem;
|
||||
|
||||
border: 3px solid var(--border-selected-500);
|
||||
.event-block {
|
||||
.event-wrapper {
|
||||
width: calc(100% / var(--of));
|
||||
height: calc(
|
||||
100% * var(--size) / var(--block-size)
|
||||
);
|
||||
transform: translateX(calc(100% * var(--index)))
|
||||
translateY(
|
||||
calc(
|
||||
100% * var(--relative-start) /
|
||||
var(--size)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&.transposed {
|
||||
grid-template-rows: auto repeat(5, 1fr);
|
||||
grid-template-columns: min-content repeat(
|
||||
var(--time-slots),
|
||||
1fr
|
||||
);
|
||||
|
||||
&.highlight {
|
||||
background: var(--bg-highlight-500);
|
||||
border-color: var(--border-highlight-500);
|
||||
.transpose-button,
|
||||
.day-label {
|
||||
grid-row: var(--position);
|
||||
grid-column: 1;
|
||||
}
|
||||
.day-label {
|
||||
min-height: 5rem;
|
||||
}
|
||||
.time-label {
|
||||
grid-row: 1;
|
||||
grid-column: calc(var(--position) + 2) / span 4;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
.day-line {
|
||||
grid-row: var(--position);
|
||||
grid-column: 1/-1;
|
||||
height: 0px;
|
||||
border-top: 1px solid var(--border-500);
|
||||
}
|
||||
.time-line {
|
||||
grid-column: calc(var(--position) + 2);
|
||||
grid-row: 1/-1;
|
||||
width: 0px;
|
||||
border-right: 1px dashed var(--border-500);
|
||||
}
|
||||
|
||||
.aula {
|
||||
font-size: 16px;
|
||||
font-weight: 300;
|
||||
.event-block-wrapper {
|
||||
grid-column: calc(var(--time-start) + 2) /
|
||||
calc(var(--time-end) + 2);
|
||||
grid-row: calc(var(--day-position) + 1);
|
||||
|
||||
.event-block {
|
||||
.event-wrapper {
|
||||
height: calc(100% / var(--of));
|
||||
width: calc(
|
||||
100% * var(--size) / var(--block-size)
|
||||
);
|
||||
transform: translateY(calc(100% * var(--index)))
|
||||
translateX(
|
||||
calc(
|
||||
100% * var(--relative-start) /
|
||||
var(--size)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.grid-line-h {
|
||||
grid-column: 1 / span var(--size);
|
||||
grid-row: var(--track) / span 1;
|
||||
|
||||
height: 0px;
|
||||
border-bottom: 1px dashed var(--border-500);
|
||||
|
||||
transform: translate(0, -1px);
|
||||
|
||||
// align-self: center;
|
||||
|
||||
position: relative;
|
||||
top: 0.5px;
|
||||
left: -0.5rem;
|
||||
width: calc(100% + 1rem);
|
||||
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.work-week-grid-view {
|
||||
position: relative;
|
||||
padding: 1rem;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
|
||||
overflow-y: auto;
|
||||
|
||||
.grid {
|
||||
width: 100%;
|
||||
max-width: 55rem;
|
||||
|
||||
display: grid;
|
||||
|
||||
grid-template-columns: 3rem repeat(var(--time-length), 1fr);
|
||||
grid-template-rows: 2rem repeat(var(--days-length), 1fr);
|
||||
|
||||
border: 1px solid var(--border-600);
|
||||
border-radius: 10px 10px 0 0;
|
||||
|
||||
@media screen and (max-width: $device-s-width), (pointer: coarse) {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.time {
|
||||
display: flex;
|
||||
.legend {
|
||||
display: grid;
|
||||
grid-template-columns: min-content 1fr;
|
||||
border: 1px solid var(--border-600);
|
||||
border-radius: 0 0 10px 10px;
|
||||
border-top: none;
|
||||
padding: 1rem;
|
||||
gap: 0.5rem 1rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.5rem;
|
||||
grid-row: 1;
|
||||
grid-column: var(--offset) / span 2;
|
||||
}
|
||||
|
||||
.day-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.5rem;
|
||||
grid-column: 1;
|
||||
grid-row: var(--line);
|
||||
}
|
||||
|
||||
.event {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@media screen and (max-width: $device-s-width), (pointer: coarse) {
|
||||
flex-direction: column;
|
||||
}
|
||||
gap: 0.25rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0.25rem;
|
||||
padding: 0.5rem;
|
||||
grid-row: var(--line);
|
||||
grid-column: var(--offset) / span var(--length);
|
||||
background-color: var(--color);
|
||||
border: 3px solid var(--color);
|
||||
border-radius: 10px;
|
||||
font-weight: var(--bold-on-dark);
|
||||
text-align: center;
|
||||
|
||||
&.highlight {
|
||||
background-color: var(--highlight-color);
|
||||
border: 3px solid var(--highlight-color);
|
||||
}
|
||||
&.selected {
|
||||
border: 3px solid var(--border-color);
|
||||
}
|
||||
}
|
||||
|
||||
.hline {
|
||||
grid-column: 1 / 13;
|
||||
grid-row: var(--line);
|
||||
height: 0px;
|
||||
border-bottom: 1px solid var(--border-500);
|
||||
}
|
||||
|
||||
.vline {
|
||||
grid-column: var(--offset);
|
||||
grid-row: 1 / calc(2 + var(--days-length));
|
||||
width: 0px;
|
||||
border-right: 1px dashed var(--border-500);
|
||||
}
|
||||
}
|
||||
.legend {
|
||||
max-width: 55rem;
|
||||
display: grid;
|
||||
grid-template-columns: min-content 1fr;
|
||||
border: 1px solid var(--border-600);
|
||||
border-radius: 0 0 10px 10px;
|
||||
border-top: none;
|
||||
padding: 1rem;
|
||||
gap: 0.5rem 1rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
|
||||
.name {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.color {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-weight: 500;
|
||||
width: 2rem;
|
||||
height: 1.5rem;
|
||||
background-color: var(--color);
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 10px;
|
||||
font-weight: var(--bold-on-dark);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.schedule-view {
|
||||
padding: 1rem;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
gap: 1rem;
|
||||
|
||||
overflow-y: scroll;
|
||||
|
||||
height: 100%;
|
||||
|
||||
& > * {
|
||||
max-width: 50rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
position: relative;
|
||||
|
||||
width: 100%;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
|
||||
&.giorno {
|
||||
font-size: 24px;
|
||||
font-weight: 400;
|
||||
|
||||
.inner {
|
||||
background: var(--bg-500);
|
||||
padding: 0 0.5rem;
|
||||
.name {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
|
||||
content: '';
|
||||
top: 50%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background: var(--border-600);
|
||||
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
|
||||
&.orario {
|
||||
font-size: 18px;
|
||||
font-weight: 300;
|
||||
}
|
||||
}
|
||||
|
||||
.event {
|
||||
@extend .panel;
|
||||
|
||||
width: 100%;
|
||||
|
||||
.title {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
position: relative;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
@media screen and (min-width: $device-s-width), (pointer: fine) {
|
||||
&:hover {
|
||||
background: var(--bg-highlight-500);
|
||||
border-color: var(--border-highlight-500);
|
||||
}
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: var(--bg-selected-500);
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -1px;
|
||||
border-radius: 1rem;
|
||||
|
||||
border: 3px solid var(--border-selected-500);
|
||||
.color {
|
||||
width: 2rem;
|
||||
height: 1.5rem;
|
||||
background-color: var(--color);
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+32
-5
@@ -12,6 +12,16 @@ export const WEEK_DAYS = [
|
||||
'Sabato',
|
||||
]
|
||||
|
||||
export const WEEK_DAYS_SHORT = [
|
||||
'Dom',
|
||||
'Lun',
|
||||
'Mar',
|
||||
'Mer',
|
||||
'Gio',
|
||||
'Ven',
|
||||
'Sab'
|
||||
]
|
||||
|
||||
// Hashing
|
||||
|
||||
export function hashString(str, seed = 0) {
|
||||
@@ -22,8 +32,12 @@ export function hashString(str, seed = 0) {
|
||||
h1 = Math.imul(h1 ^ ch, 2654435761)
|
||||
h2 = Math.imul(h2 ^ ch, 1597334677)
|
||||
}
|
||||
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909)
|
||||
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909)
|
||||
h1 =
|
||||
Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^
|
||||
Math.imul(h2 ^ (h2 >>> 13), 3266489909)
|
||||
h2 =
|
||||
Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^
|
||||
Math.imul(h1 ^ (h1 >>> 13), 3266489909)
|
||||
return 4294967296 * (2097151 & h2) + (h1 >>> 0)
|
||||
}
|
||||
|
||||
@@ -36,7 +50,9 @@ export function prettyCourseName(name) {
|
||||
.map(word => {
|
||||
if (word.trim().length === 0) return word
|
||||
|
||||
return /(^del|^nel|^di$|^dei$|^con$|^alla$|^per$|^e$|^la$)/.test(word)
|
||||
return /(^del|^nel|^di$|^dei$|^con$|^alla$|^per$|^e$|^la$)/.test(
|
||||
word
|
||||
)
|
||||
? word
|
||||
: word[0].toUpperCase() + word.slice(1)
|
||||
})
|
||||
@@ -71,17 +87,28 @@ export const withClasses = classes =>
|
||||
.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(key)
|
||||
const previousValue = localStorage.getItem(`orario.${key}`)
|
||||
|
||||
const [value, setValue] = useState(
|
||||
previousValue !== null ? JSON.parse(previousValue) : initialValue
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(key, JSON.stringify(value))
|
||||
localStorage.setItem(`orario.${key}`, JSON.stringify(value))
|
||||
}, [value])
|
||||
|
||||
return [value, setValue]
|
||||
|
||||
Reference in New Issue
Block a user