forked from phc/orario
Compare commits
25
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b1b9c2aed | ||
|
|
ebb78c4aff | ||
|
|
8e3d0b0155
|
||
|
|
3b2fc34ef1
|
||
|
|
c0340c3407 | ||
|
|
0f0bc1c52a | ||
|
|
01fa4b16e2 | ||
|
|
23c1510455 | ||
|
|
b76213e75d | ||
|
|
fdeb17c90e | ||
|
|
d767615545
|
||
|
|
0a7331f63a | ||
|
|
85b24db785
|
||
|
|
46d8b5b878 | ||
|
|
c3810670b2 | ||
|
|
b593407915
|
||
|
|
d2526bcfa6
|
||
|
|
f90b60fbd9 | ||
|
|
c7c8ce9d48 | ||
|
|
09ebad224b
|
||
|
|
0e81edfa3b
|
||
|
|
360fa42606
|
||
|
|
2dda03dd8a
|
||
|
|
d5dd2a7824
|
||
|
|
03ca03debc
|
+2
-1
@@ -2,7 +2,8 @@ FROM node:18
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY . ./
|
COPY package.json ./
|
||||||
RUN npm install
|
RUN npm install
|
||||||
|
|
||||||
|
COPY . .
|
||||||
CMD ["npm", "run", "build"]
|
CMD ["npm", "run", "build"]
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# Orario
|
||||||
|
|
||||||
|
Sito per visualizzare l'orario delle lezioni per i vari anni di corso.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
You need to have installed `node` and `npm` (or `pnpm`). To setup the project just run `npm install`.
|
||||||
|
|
||||||
|
### Development
|
||||||
|
|
||||||
|
To start the development server run `npm run dev`.
|
||||||
|
|
||||||
|
### Production
|
||||||
|
|
||||||
|
To build the ViteJS project run `npm run build`, for deployment a `.env` files can be used to set the `BASE_URL` variable.
|
||||||
|
|
||||||
+10
-51
@@ -1,81 +1,40 @@
|
|||||||
[
|
[
|
||||||
|
"ALGEBRA 1",
|
||||||
"ANALISI ARMONICA - ANALISI ARMONICA/a",
|
"ANALISI ARMONICA - ANALISI ARMONICA/a",
|
||||||
|
"ANALISI MATEMATICA 1",
|
||||||
|
"ANALISI MATEMATICA 2",
|
||||||
"ANALISI MATEMATICA 3",
|
"ANALISI MATEMATICA 3",
|
||||||
|
"ANALISI NUMERICA CON LABORATORIO - ANALISI NUMERICA",
|
||||||
|
"ARITMETICA",
|
||||||
"ASPETTI MATEMATICI NELLA COMPUTAZIONE QUANTISTICA",
|
"ASPETTI MATEMATICI NELLA COMPUTAZIONE QUANTISTICA",
|
||||||
"CALCOLO SCIENTIFICO",
|
"CALCOLO SCIENTIFICO",
|
||||||
"COMBINATORIA ALGEBRICA",
|
"COMBINATORIA ALGEBRICA",
|
||||||
"DETERMINAZIONE ORBITALE",
|
"DETERMINAZIONE ORBITALE",
|
||||||
"DINAMICA DEL SISTEMA SOLARE",
|
"DINAMICA DEL SISTEMA SOLARE",
|
||||||
"DINAMICA DEL SISTEMA SOLARE",
|
|
||||||
"ELEMENTI DI GEOMETRIA ALGEBRICA",
|
|
||||||
"ELEMENTI DI GEOMETRIA ALGEBRICA",
|
|
||||||
"ELEMENTI DI GEOMETRIA ALGEBRICA",
|
|
||||||
"ELEMENTI DI GEOMETRIA ALGEBRICA",
|
"ELEMENTI DI GEOMETRIA ALGEBRICA",
|
||||||
"ELEMENTI DI TOPOLOGIA ALGEBRICA",
|
"ELEMENTI DI TOPOLOGIA ALGEBRICA",
|
||||||
"ELEMENTI DI TOPOLOGIA ALGEBRICA",
|
|
||||||
"ELEMENTI DI TOPOLOGIA ALGEBRICA",
|
|
||||||
"ELEMENTI DI TOPOLOGIA ALGEBRICA",
|
|
||||||
"FISICA II",
|
|
||||||
"FISICA II",
|
|
||||||
"FISICA II",
|
|
||||||
"FISICA II",
|
|
||||||
"FISICA II",
|
|
||||||
"FISICA II",
|
|
||||||
"FISICA II",
|
|
||||||
"FISICA II",
|
"FISICA II",
|
||||||
|
"FONDAMENTI DI PROGRAMMAZIONE CON LABORATORIO - FONDAMENTI DI PROGRAMMAZIONE ",
|
||||||
|
"GEOMETRIA 1",
|
||||||
|
"GEOMETRIA 2 - GEOMETRIA 2 A",
|
||||||
"GEOMETRIA E TOPOLOGIA DIFFERENZIALE",
|
"GEOMETRIA E TOPOLOGIA DIFFERENZIALE",
|
||||||
"GEOMETRIA E TOPOLOGIA DIFFERENZIALE",
|
"INGLESE SCIENTIFICO",
|
||||||
"GEOMETRIA E TOPOLOGIA DIFFERENZIALE",
|
|
||||||
"GEOMETRIA E TOPOLOGIA DIFFERENZIALE",
|
|
||||||
"GEOMETRIA E TOPOLOGIA DIFFERENZIALE",
|
|
||||||
"GEOMETRIA E TOPOLOGIA DIFFERENZIALE",
|
|
||||||
"ISTITUZIONI DI ALGEBRA",
|
|
||||||
"ISTITUZIONI DI ALGEBRA",
|
"ISTITUZIONI DI ALGEBRA",
|
||||||
"ISTITUZIONI DI ANALISI MATEMATICA",
|
"ISTITUZIONI DI ANALISI MATEMATICA",
|
||||||
"ISTITUZIONI DI ANALISI MATEMATICA",
|
|
||||||
"ISTITUZIONI DI ANALISI MATEMATICA",
|
|
||||||
"ISTITUZIONI DI DIDATTICA DELLA MATEMATICA",
|
"ISTITUZIONI DI DIDATTICA DELLA MATEMATICA",
|
||||||
"ISTITUZIONI DI DIDATTICA DELLA MATEMATICA",
|
|
||||||
"ISTITUZIONI DI DIDATTICA DELLA MATEMATICA",
|
|
||||||
"ISTITUZIONI DI FISICA MATEMATICA",
|
|
||||||
"ISTITUZIONI DI FISICA MATEMATICA",
|
"ISTITUZIONI DI FISICA MATEMATICA",
|
||||||
"LABORATORIO COMPUTAZIONALE",
|
"LABORATORIO COMPUTAZIONALE",
|
||||||
"LOGICA MATEMATICA",
|
"LABORATORIO DI INTRODUZIONE ALLA MATEMATICA COMPUTAZIONALE",
|
||||||
"LOGICA MATEMATICA",
|
|
||||||
"LOGICA MATEMATICA",
|
|
||||||
"LOGICA MATEMATICA",
|
"LOGICA MATEMATICA",
|
||||||
"MECCANICA SUPERIORE - MECCANICA SUPERIORE/a",
|
"MECCANICA SUPERIORE - MECCANICA SUPERIORE/a",
|
||||||
"MECCANICA SUPERIORE - MECCANICA SUPERIORE/a",
|
|
||||||
"METODI NUMERICI PER CATENE DI MARKOV - METODI NUMERICI PER CATENE DI MARKOV/a",
|
|
||||||
"METODI NUMERICI PER CATENE DI MARKOV - METODI NUMERICI PER CATENE DI MARKOV/a",
|
"METODI NUMERICI PER CATENE DI MARKOV - METODI NUMERICI PER CATENE DI MARKOV/a",
|
||||||
"METODI NUMERICI PER LA GRAFICA - METODI NUMERICI PER LA GRAFICA/a",
|
"METODI NUMERICI PER LA GRAFICA - METODI NUMERICI PER LA GRAFICA/a",
|
||||||
"METODI NUMERICI PER LA GRAFICA - METODI NUMERICI PER LA GRAFICA/a",
|
|
||||||
"PROBABILITÀ",
|
"PROBABILITÀ",
|
||||||
"PROBABILITÀ",
|
|
||||||
"PROBABILITÀ",
|
|
||||||
"PROBABILITÀ",
|
|
||||||
"PROBABILITÀ",
|
|
||||||
"PROBABILITÀ",
|
|
||||||
"RICERCA OPERATIVA",
|
|
||||||
"RICERCA OPERATIVA",
|
|
||||||
"RICERCA OPERATIVA",
|
|
||||||
"RICERCA OPERATIVA",
|
|
||||||
"RICERCA OPERATIVA",
|
|
||||||
"RICERCA OPERATIVA",
|
"RICERCA OPERATIVA",
|
||||||
"SISTEMI DINAMICI",
|
"SISTEMI DINAMICI",
|
||||||
"SISTEMI DINAMICI",
|
|
||||||
"SISTEMI DINAMICI",
|
|
||||||
"SISTEMI DINAMICI",
|
|
||||||
"STORIA DELLA MATEMATICA",
|
|
||||||
"STORIA DELLA MATEMATICA",
|
|
||||||
"STORIA DELLA MATEMATICA",
|
|
||||||
"STORIA DELLA MATEMATICA",
|
"STORIA DELLA MATEMATICA",
|
||||||
"TECNOLOGIE PER LA DIDATTICA",
|
"TECNOLOGIE PER LA DIDATTICA",
|
||||||
"TECNOLOGIE PER LA DIDATTICA",
|
|
||||||
"TEORIA ANALITICA DEI NUMERI A - TEORIA ANALITICA DEI NUMERI A/a",
|
|
||||||
"TEORIA ANALITICA DEI NUMERI A - TEORIA ANALITICA DEI NUMERI A/a",
|
"TEORIA ANALITICA DEI NUMERI A - TEORIA ANALITICA DEI NUMERI A/a",
|
||||||
"TEORIA E METODI DELL'OTTIMIZZAZIONE",
|
"TEORIA E METODI DELL'OTTIMIZZAZIONE",
|
||||||
"TEORIA E METODI DELL'OTTIMIZZAZIONE",
|
|
||||||
"TOPOLOGIA DIFFERENZIALE - TOPOLOGIA DIFFERENZIALE/a",
|
|
||||||
"TOPOLOGIA DIFFERENZIALE - TOPOLOGIA DIFFERENZIALE/a"
|
"TOPOLOGIA DIFFERENZIALE - TOPOLOGIA DIFFERENZIALE/a"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
// import { createContext } from 'preact'
|
|
||||||
// import { toChildArray } from 'preact'
|
|
||||||
// import { useContext, useState } from 'preact/hooks'
|
|
||||||
|
|
||||||
// const AnimationContext = createContext()
|
|
||||||
|
|
||||||
// export const useAnimation = animation => {
|
|
||||||
// const [styles, setStyles] = useState({})
|
|
||||||
|
|
||||||
// const start = () => {
|
|
||||||
// setStyles({ animation })
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return [
|
|
||||||
// start,
|
|
||||||
// {
|
|
||||||
// styles,
|
|
||||||
// onAnimationEnd: e => {
|
|
||||||
// console.log('animation end')
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// ]
|
|
||||||
// }
|
|
||||||
|
|
||||||
// export const Animation = ({ animation, children }) => {}
|
|
||||||
@@ -1,28 +1,18 @@
|
|||||||
import { useRef, useState } from 'preact/hooks'
|
|
||||||
import { Icon } from './Icon.jsx'
|
import { Icon } from './Icon.jsx'
|
||||||
|
|
||||||
let ids = 0
|
|
||||||
|
|
||||||
function generateId() {
|
|
||||||
return 'combo-id-' + ids++
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ComboBox = ({ options, value, setValue }) => {
|
export const ComboBox = ({ options, value, setValue }) => {
|
||||||
const [uid] = useState(() => generateId())
|
|
||||||
const selectRef = useRef()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="input-combo">
|
<div class="input-combo">
|
||||||
<select ref={selectRef} id={uid} onInput={e => setValue(e.target.value)}>
|
<select onInput={e => setValue(e.target.value)}>
|
||||||
{options.map(option => (
|
{options.map(option => (
|
||||||
<option value={option.value} selected={option.value === value}>
|
<option value={option.value} selected={option.value === value}>
|
||||||
{option.label}
|
{option.label}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
<label class="icon" for={uid}>
|
<div class="icon">
|
||||||
<Icon name="expand_more" />
|
<Icon name="expand_more" />
|
||||||
</label>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-362
@@ -1,368 +1,21 @@
|
|||||||
//
|
import { Course } from './view/Course.jsx'
|
||||||
// Modes
|
import { Schedule } from './view/Schedule.jsx'
|
||||||
//
|
import { WorkWeek } from './view/WorkWeek.jsx'
|
||||||
|
import { WorkWeekGrid } from './view/WorkWeekGrid.jsx'
|
||||||
|
import { WorkWeekTranspose } from './view/WorkWeekTranspose.jsx'
|
||||||
|
|
||||||
import { differenceInMinutes, format, startOfDay } from 'date-fns'
|
export const MODE_COURSE = 'course'
|
||||||
|
export const MODE_WORKWEEK = 'work-week'
|
||||||
import _ from 'lodash'
|
export const MODE_SCHEDULE = 'schedule'
|
||||||
import { useEffect, useRef, useState } from 'preact/hooks'
|
export const MODE_WORKWEEK_GRID = 'work-week-grid'
|
||||||
import { layoutIntervals } from '../interval-layout.js'
|
export const MODE_WORKWEEK_TRANSPOSE = 'work-week-transpose'
|
||||||
|
|
||||||
const WEEK_DAYS = ['Domenica', 'Lunedì', 'Martedì', 'Mercoledì', 'Giovedì', 'Venerdì', 'Sabato']
|
|
||||||
|
|
||||||
function hashString(str, seed = 0) {
|
|
||||||
let h1 = 0xdeadbeef ^ seed,
|
|
||||||
h2 = 0x41c6ce57 ^ seed
|
|
||||||
for (let i = 0, ch; i < str.length; i++) {
|
|
||||||
ch = str.charCodeAt(i)
|
|
||||||
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)
|
|
||||||
return 4294967296 * (2097151 & h2) + (h1 >>> 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeCourseName(name) {
|
|
||||||
return name
|
|
||||||
.split(' ')
|
|
||||||
.map(word => word.toLowerCase())
|
|
||||||
.map(word => {
|
|
||||||
if (word.trim().length === 0) return word
|
|
||||||
|
|
||||||
return /(^del|^nel|^di$|^dei$|^con$|^alla$|^per$|^e$|^la$)/.test(word)
|
|
||||||
? word
|
|
||||||
: word[0].toUpperCase() + word.slice(1)
|
|
||||||
})
|
|
||||||
.join(' ')
|
|
||||||
.replaceAll('Ii', 'II')
|
|
||||||
.replaceAll('Iii', 'III')
|
|
||||||
.replaceAll(/'(.)/g, ({}, letter) => "'" + letter.toUpperCase())
|
|
||||||
}
|
|
||||||
|
|
||||||
const WorkWeekView = ({ 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.name)
|
|
||||||
) %
|
|
||||||
360) +
|
|
||||||
'deg',
|
|
||||||
}}
|
|
||||||
ref={eventRef}
|
|
||||||
>
|
|
||||||
{normalizeCourseName(event.data.name)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Local />
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const WorkWeekVerticalView = ({ events, selection, setSelection, hideOtherCourses }) => {
|
|
||||||
const selectionSet = new Set(selection)
|
|
||||||
|
|
||||||
// const base = {
|
|
||||||
// 1: [],
|
|
||||||
// 2: [],
|
|
||||||
// 3: [],
|
|
||||||
// 4: [],
|
|
||||||
// 5: [],
|
|
||||||
// }
|
|
||||||
|
|
||||||
const eventsByWeekday = _.groupBy(
|
|
||||||
!hideOtherCourses ? events : events.filter(e => selectionSet.has(e.name)),
|
|
||||||
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.name
|
|
||||||
? ' highlight'
|
|
||||||
: '') +
|
|
||||||
(selectionSet.has(event.data.name) ? ' selected' : '')
|
|
||||||
}
|
|
||||||
data-event-id={event.data.name}
|
|
||||||
style={{
|
|
||||||
'--start': event.start / 60 - 7,
|
|
||||||
'--stack': stackIndex + 1,
|
|
||||||
'--size': (event.end - event.start) / 60,
|
|
||||||
'--hue':
|
|
||||||
(Math.abs(hashString('seed3' + event.data.name)) %
|
|
||||||
360) +
|
|
||||||
'deg',
|
|
||||||
}}
|
|
||||||
onClick={() => {
|
|
||||||
if (!selectionSet.has(event.data.name))
|
|
||||||
setSelection([...selection, event.data.name])
|
|
||||||
else
|
|
||||||
setSelection(
|
|
||||||
selection.filter(
|
|
||||||
name => event.data.name !== name
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const CourseView = ({ events, selection, setSelection, hideOtherCourses }) => {
|
|
||||||
const selectionSet = new Set(selection)
|
|
||||||
|
|
||||||
const visibleEvents = !hideOtherCourses ? events : events.filter(e => selectionSet.has(e.name))
|
|
||||||
|
|
||||||
const eventsByCourse = _.groupBy(_.sortBy(visibleEvents, 'name'), 'name')
|
|
||||||
|
|
||||||
const [currentlyHovered, setCurrentlyHovered] = useState(null)
|
|
||||||
const element = useRef()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (element.current) {
|
|
||||||
const l = e => {
|
|
||||||
const $course = e.target.closest('.course')
|
|
||||||
if ($course) {
|
|
||||||
setCurrentlyHovered($course.dataset.courseId)
|
|
||||||
} 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="course-view" ref={element}>
|
|
||||||
<div class="wrap-container">
|
|
||||||
{Object.entries(eventsByCourse).map(([name, courseEvents]) => (
|
|
||||||
<div
|
|
||||||
class={
|
|
||||||
'course' +
|
|
||||||
(currentlyHovered === name ? ' highlight' : '') +
|
|
||||||
(selectionSet.has(name) ? ' selected' : '')
|
|
||||||
}
|
|
||||||
data-course-id={name}
|
|
||||||
onClick={() => {
|
|
||||||
if (!selectionSet.has(name)) setSelection([...selection, name])
|
|
||||||
else setSelection(selection.filter(n => n !== name))
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div class="title">{normalizeCourseName(name)}</div>
|
|
||||||
<div class="docenti">{courseEvents[0].docenti.join(', ')}</div>
|
|
||||||
<div class="events">
|
|
||||||
{courseEvents.map(course => (
|
|
||||||
<div>
|
|
||||||
{WEEK_DAYS[course.start.getDay()]}{' '}
|
|
||||||
{format(course.start, 'H:mm')} –
|
|
||||||
{format(course.end, 'H:mm')} {course.aula}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// EventsView
|
|
||||||
//
|
|
||||||
|
|
||||||
const viewModeMap = {
|
const viewModeMap = {
|
||||||
'work-week-h': WorkWeekView,
|
[MODE_COURSE]: Course,
|
||||||
'work-week-v': WorkWeekVerticalView,
|
[MODE_WORKWEEK]: WorkWeek,
|
||||||
'course': CourseView,
|
[MODE_WORKWEEK_GRID]: WorkWeekGrid,
|
||||||
|
[MODE_WORKWEEK_TRANSPOSE]: WorkWeekTranspose,
|
||||||
|
[MODE_SCHEDULE]: Schedule,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EventsView = ({ mode, ...viewProps }) => {
|
export const EventsView = ({ mode, ...viewProps }) => {
|
||||||
|
|||||||
@@ -1,14 +1,23 @@
|
|||||||
import { ComboBox } from './ComboBox.jsx'
|
import { ComboBox } from './ComboBox.jsx'
|
||||||
|
import { MODE_COURSE, MODE_SCHEDULE, MODE_WORKWEEK, MODE_WORKWEEK_GRID } from './EventsView.jsx'
|
||||||
import { Help } from './Help.jsx'
|
import { Help } from './Help.jsx'
|
||||||
import { Icon } from './Icon.jsx'
|
import { Icon } from './Icon.jsx'
|
||||||
|
|
||||||
export const HamburgerMenu = ({ onClose, mode, setMode, source, setSource }) => {
|
export const HamburgerMenu = ({ onClose, mode, setMode, source, setSource, theme, setTheme }) => {
|
||||||
return (
|
return (
|
||||||
<div class="menu">
|
<div class="menu">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<button class="flat icon" onClick={onClose}>
|
<div class="option-group">
|
||||||
<Icon name="close" />
|
<button class="flat icon" onClick={onClose}>
|
||||||
</button>
|
<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">
|
<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>
|
||||||
@@ -31,12 +40,10 @@ export const HamburgerMenu = ({ onClose, mode, setMode, source, setSource }) =>
|
|||||||
value={mode}
|
value={mode}
|
||||||
setValue={setMode}
|
setValue={setMode}
|
||||||
options={[
|
options={[
|
||||||
{ value: 'course', label: 'Corsi' },
|
{ value: MODE_COURSE, label: 'Corsi' },
|
||||||
{ value: 'work-week-v', label: 'Settimana' },
|
{ value: MODE_SCHEDULE, label: 'Giornaliera' },
|
||||||
// {
|
{ value: MODE_WORKWEEK, label: 'Settimana' },
|
||||||
// value: 'work-week-h',
|
{ value: MODE_WORKWEEK_GRID, label: 'Schema' },
|
||||||
// label: 'Settimana (Trasposto)',
|
|
||||||
// },
|
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+23
-12
@@ -1,7 +1,17 @@
|
|||||||
import { CompoundButton } from './CompoundButton.jsx'
|
import { CompoundButton } from './CompoundButton.jsx'
|
||||||
|
import { MODE_COURSE, MODE_SCHEDULE, MODE_WORKWEEK, MODE_WORKWEEK_GRID } from './EventsView.jsx'
|
||||||
import { Icon } from './Icon.jsx'
|
import { Icon } from './Icon.jsx'
|
||||||
|
|
||||||
export const Toolbar = ({ mode, setMode, source, setSource, onShowMenu, onHelp }) => {
|
export const Toolbar = ({
|
||||||
|
mode,
|
||||||
|
setMode,
|
||||||
|
source,
|
||||||
|
setSource,
|
||||||
|
onShowMenu,
|
||||||
|
onHelp,
|
||||||
|
theme,
|
||||||
|
setTheme,
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<div class="mobile">
|
<div class="mobile">
|
||||||
@@ -31,17 +41,10 @@ export const Toolbar = ({ mode, setMode, source, setSource, onShowMenu, onHelp }
|
|||||||
<div class="item option">
|
<div class="item option">
|
||||||
<CompoundButton
|
<CompoundButton
|
||||||
options={[
|
options={[
|
||||||
{ value: 'course', label: 'Corsi' },
|
{ value: MODE_COURSE, label: 'Corsi' },
|
||||||
{ value: 'work-week-v', label: 'Settimana' },
|
{ value: MODE_WORKWEEK, label: 'Settimana' },
|
||||||
// {
|
{ value: MODE_WORKWEEK_GRID, label: 'Schema' },
|
||||||
// value: 'work-week-h',
|
{ value: MODE_SCHEDULE, label: 'Giorno' },
|
||||||
// label: (
|
|
||||||
// <>
|
|
||||||
// <Icon name="warning" />
|
|
||||||
// Settimana<sup>T</sup>
|
|
||||||
// </>
|
|
||||||
// ),
|
|
||||||
// },
|
|
||||||
]}
|
]}
|
||||||
value={mode}
|
value={mode}
|
||||||
setValue={setMode}
|
setValue={setMode}
|
||||||
@@ -52,6 +55,14 @@ export const Toolbar = ({ mode, setMode, source, setSource, onShowMenu, onHelp }
|
|||||||
<Icon name="print" />
|
<Icon name="print" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="item option">
|
||||||
|
<button
|
||||||
|
class="icon"
|
||||||
|
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
|
||||||
|
>
|
||||||
|
<Icon name={theme === 'dark' ? 'dark_mode' : 'light_mode'} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div class="item option">
|
<div class="item option">
|
||||||
<button class="icon" onClick={onHelp}>
|
<button class="icon" onClick={onHelp}>
|
||||||
<Icon name="question_mark" />
|
<Icon name="question_mark" />
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { format } from 'date-fns'
|
||||||
|
|
||||||
|
import _ from 'lodash'
|
||||||
|
import { useEffect, useRef, useState } from 'preact/hooks'
|
||||||
|
import { normalizeCourseName, WEEK_DAYS } from '../../utils.jsx'
|
||||||
|
|
||||||
|
export const Course = ({ events, selection, setSelection, hideOtherCourses }) => {
|
||||||
|
const selectionSet = new Set(selection)
|
||||||
|
|
||||||
|
const visibleEvents = !hideOtherCourses ? events : events.filter(e => selectionSet.has(e.id))
|
||||||
|
|
||||||
|
const eventsByCourse = _.groupBy(_.sortBy(visibleEvents, 'id'), 'id')
|
||||||
|
|
||||||
|
const [currentlyHovered, setCurrentlyHovered] = useState(null)
|
||||||
|
const element = useRef()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (element.current) {
|
||||||
|
const l = e => {
|
||||||
|
const $course = e.target.closest('.course')
|
||||||
|
if ($course) {
|
||||||
|
setCurrentlyHovered($course.dataset.courseId)
|
||||||
|
} 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="course-view" ref={element}>
|
||||||
|
<div class="wrap-container">
|
||||||
|
{Object.entries(eventsByCourse).map(([id, courseEvents]) => (
|
||||||
|
<div
|
||||||
|
class={
|
||||||
|
'course' +
|
||||||
|
(currentlyHovered === id ? ' highlight' : '') +
|
||||||
|
(selectionSet.has(id) ? ' selected' : '')
|
||||||
|
}
|
||||||
|
data-course-id={id}
|
||||||
|
onClick={() => {
|
||||||
|
if (!selectionSet.has(id)) setSelection([...selection, id])
|
||||||
|
else setSelection(selection.filter(selId => selId !== id))
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="title">{normalizeCourseName(courseEvents[0].name)}</div>
|
||||||
|
<div class="docenti">{courseEvents[0].docenti.join(', ')}</div>
|
||||||
|
<div class="events">
|
||||||
|
{courseEvents.map(course => (
|
||||||
|
<div>
|
||||||
|
{WEEK_DAYS[course.start.getDay()]}{' '}
|
||||||
|
{format(course.start, 'H:mm')} –{' '}
|
||||||
|
{format(course.end, 'H:mm')} {course.aula}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { format } from 'date-fns'
|
||||||
|
import _ from 'lodash'
|
||||||
|
import { normalizeCourseName, WEEK_DAYS, withClasses } from '../../utils.jsx'
|
||||||
|
|
||||||
|
export const Schedule = ({ events, selection, setSelection, hideOtherCourses }) => {
|
||||||
|
const selectionSet = new Set(selection)
|
||||||
|
|
||||||
|
const visibleEvents = !hideOtherCourses ? events : events.filter(e => selectionSet.has(e.id))
|
||||||
|
|
||||||
|
const eventsByWeekday = _.mapValues(
|
||||||
|
_.groupBy(visibleEvents, e => e.start.getDay()),
|
||||||
|
dailyEvents => _.groupBy(dailyEvents, e => e.start.getHours())
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="schedule-view">
|
||||||
|
{Object.entries(eventsByWeekday).map(([index, dailyEvents]) => (
|
||||||
|
<>
|
||||||
|
<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">{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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
+39
-15
@@ -3,7 +3,7 @@ import { render } from 'preact'
|
|||||||
import { useEffect, useState } from 'preact/hooks'
|
import { useEffect, useState } from 'preact/hooks'
|
||||||
import { ToolOverlay } from './components/CourseVisibility.jsx'
|
import { ToolOverlay } from './components/CourseVisibility.jsx'
|
||||||
|
|
||||||
import { EventsView } from './components/EventsView.jsx'
|
import { EventsView, MODE_WORKWEEK_GRID } from './components/EventsView.jsx'
|
||||||
import { HamburgerMenu } from './components/HamburgerMenu.jsx'
|
import { HamburgerMenu } from './components/HamburgerMenu.jsx'
|
||||||
import { Help } from './components/Help.jsx'
|
import { Help } from './components/Help.jsx'
|
||||||
import { Icon } from './components/Icon.jsx'
|
import { Icon } from './components/Icon.jsx'
|
||||||
@@ -68,15 +68,23 @@ async function loadEventi(ids) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const App = ({}) => {
|
const App = ({}) => {
|
||||||
const [source, setSource] = useState('magistrale')
|
// Data Sources
|
||||||
|
const [source, setSource] = useState('anno-1')
|
||||||
const [eventi, setEventi] = useState([])
|
const [eventi, setEventi] = useState([])
|
||||||
const [selectedCourses, setSelectedCourses] = useState([])
|
|
||||||
|
|
||||||
useEffect(() => {
|
// View Modes
|
||||||
setSelectedCourses([])
|
const [mode, setMode] = useState(MODE_WORKWEEK_GRID)
|
||||||
}, [source])
|
|
||||||
|
// Selection
|
||||||
|
const [selectedCourses, setSelectedCourses] = useState([])
|
||||||
|
const [hideOtherCourses, setHideOtherCourses] = useState(false)
|
||||||
|
|
||||||
|
// Menus
|
||||||
|
const [helpVisible, setHelpVisible] = useState(false)
|
||||||
|
const [showMobileMenu, setShowMobileMenu] = useState(false)
|
||||||
|
|
||||||
useEffect(async () => {
|
useEffect(async () => {
|
||||||
|
console.log('source changed')
|
||||||
const eventi = await loadEventi(CALENDAR_IDS[source])
|
const eventi = await loadEventi(CALENDAR_IDS[source])
|
||||||
|
|
||||||
window.dataBuffer[source] = eventi
|
window.dataBuffer[source] = eventi
|
||||||
@@ -84,18 +92,29 @@ const App = ({}) => {
|
|||||||
setEventi(eventi)
|
setEventi(eventi)
|
||||||
}, [source])
|
}, [source])
|
||||||
|
|
||||||
const [helpVisible, setHelpVisible] = useState(false)
|
const groupIds = new Set(eventi.map(e => e.nome))
|
||||||
|
const toolOverlayVisible =
|
||||||
|
selectedCourses.length > 0 && selectedCourses.filter(id => groupIds.has(id)).length > 0
|
||||||
|
|
||||||
const [mode, setMode] = useState('course')
|
useEffect(() => {
|
||||||
|
console.log('course length changed')
|
||||||
|
const groupIds = new Set(eventi.map(e => e.nome))
|
||||||
|
|
||||||
const [hideOtherCourses, setHideOtherCourses] = useState(false)
|
if (
|
||||||
|
selectedCourses.length === 0 ||
|
||||||
|
selectedCourses.filter(id => groupIds.has(id)).length === 0
|
||||||
|
) {
|
||||||
|
setHideOtherCourses(false)
|
||||||
|
}
|
||||||
|
}, [eventi, selectedCourses.length])
|
||||||
|
|
||||||
// TODO: Should wrap in "useEffect"?
|
const [theme, setTheme] = useState(
|
||||||
if (selectedCourses.length === 0) {
|
localStorage.getItem('theme') ??
|
||||||
setHideOtherCourses(false)
|
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
|
||||||
}
|
)
|
||||||
|
|
||||||
const [showMobileMenu, setShowMobileMenu] = useState(false)
|
document.body.classList.toggle('dark-mode', theme === 'dark')
|
||||||
|
localStorage.setItem('theme', theme)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -107,6 +126,8 @@ const App = ({}) => {
|
|||||||
setSource,
|
setSource,
|
||||||
onShowMenu: () => setShowMobileMenu(true),
|
onShowMenu: () => setShowMobileMenu(true),
|
||||||
onHelp: () => setHelpVisible(true),
|
onHelp: () => setHelpVisible(true),
|
||||||
|
theme,
|
||||||
|
setTheme,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<EventsView
|
<EventsView
|
||||||
@@ -116,6 +137,7 @@ const App = ({}) => {
|
|||||||
hideOtherCourses={hideOtherCourses}
|
hideOtherCourses={hideOtherCourses}
|
||||||
start={new Date(2022, 10, 3)}
|
start={new Date(2022, 10, 3)}
|
||||||
events={eventi.map(({ nome, dataInizio, dataFine, docenti, aule }) => ({
|
events={eventi.map(({ nome, dataInizio, dataFine, docenti, aule }) => ({
|
||||||
|
id: nome,
|
||||||
name: _.split(nome, '-', 1)[0].trim(),
|
name: _.split(nome, '-', 1)[0].trim(),
|
||||||
start: new Date(dataInizio),
|
start: new Date(dataInizio),
|
||||||
end: new Date(dataFine),
|
end: new Date(dataFine),
|
||||||
@@ -128,7 +150,7 @@ const App = ({}) => {
|
|||||||
),
|
),
|
||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
{selectedCourses.length > 0 && (
|
{toolOverlayVisible && (
|
||||||
<ToolOverlay
|
<ToolOverlay
|
||||||
visibility={hideOtherCourses}
|
visibility={hideOtherCourses}
|
||||||
toggleVisibility={() => setHideOtherCourses(s => !s)}
|
toggleVisibility={() => setHideOtherCourses(s => !s)}
|
||||||
@@ -145,6 +167,8 @@ const App = ({}) => {
|
|||||||
setMode,
|
setMode,
|
||||||
source,
|
source,
|
||||||
setSource,
|
setSource,
|
||||||
|
theme,
|
||||||
|
setTheme,
|
||||||
onClose: () => {
|
onClose: () => {
|
||||||
setShowMobileMenu(false)
|
setShowMobileMenu(false)
|
||||||
},
|
},
|
||||||
|
|||||||
+372
-97
@@ -14,34 +14,104 @@ html {
|
|||||||
// Theme
|
// Theme
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--gray-000: #ffffff;
|
--bg-500: #ffffff;
|
||||||
--gray-100: #f8f8f8;
|
--bg-selected-500: #f8f8f8;
|
||||||
--gray-300: #f0f0f0;
|
--bg-highlight-500: #f0f0f0;
|
||||||
--gray-350: #e8e8e8;
|
--border-400: #e8e8e8;
|
||||||
--gray-500: #d8d8d8;
|
--border-500: #d8d8d8;
|
||||||
--gray-600: #c0c0c0;
|
--border-600: #c0c0c0;
|
||||||
--gray-700: #b0b0b0;
|
--border-highlight-500: #b0b0b0;
|
||||||
--gray-800: #909090;
|
--border-selected-500: #909090;
|
||||||
--gray-900: #282828;
|
--text-500: #282828;
|
||||||
|
|
||||||
// --accent-100: #e6ceff;
|
|
||||||
// --accent-400: #d0b8ef;
|
|
||||||
// --accent-500: #aa82db;
|
|
||||||
|
|
||||||
// --accent-100: hsl(221, 100%, 88%);
|
|
||||||
// --accent-400: hsl(221, 75%, 78%);
|
|
||||||
// --accent-500: hsl(221, 70%, 75%);
|
|
||||||
|
|
||||||
--accent-100: #d6ffc2;
|
--accent-100: #d6ffc2;
|
||||||
--accent-400: #6cc16c;
|
--accent-500: #6cc16c;
|
||||||
--accent-500: #5aa05a;
|
|
||||||
--accent-900: #244624;
|
--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%);
|
||||||
|
|
||||||
|
--bold-on-dark: 300;
|
||||||
}
|
}
|
||||||
|
|
||||||
$device-s-width: 500px;
|
body.dark-mode {
|
||||||
|
--bg-500: hsl(269, 24%, 20%);
|
||||||
|
--bg-selected-500: #722da0;
|
||||||
|
--bg-highlight-500: #a64edb;
|
||||||
|
--border-400: hsl(269, 40%, 50%);
|
||||||
|
--border-500: hsl(269, 60%, 60%);
|
||||||
|
--border-600: hsl(269, 80%, 70%);
|
||||||
|
--border-highlight-500: hsl(269, 80%, 78%);
|
||||||
|
--border-selected-500: #b66af6;
|
||||||
|
--text-500: #e6cff6;
|
||||||
|
|
||||||
|
--accent-100: hsl(269, 50%, 50%);
|
||||||
|
--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%);
|
||||||
|
|
||||||
|
--bold-on-dark: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
$device-s-width: 600px;
|
||||||
|
|
||||||
// Elements
|
// Elements
|
||||||
|
|
||||||
|
a,
|
||||||
|
a:visited {
|
||||||
|
color: var(--text-500);
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
font-size: 90%;
|
font-size: 90%;
|
||||||
}
|
}
|
||||||
@@ -97,10 +167,10 @@ button,
|
|||||||
|
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
|
|
||||||
color: var(--gray-900);
|
color: var(--text-500);
|
||||||
|
|
||||||
background: var(--gray-000);
|
background: var(--bg-500);
|
||||||
border: 2px solid var(--gray-350);
|
border: 2px solid var(--border-400);
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
|
|
||||||
font: inherit;
|
font: inherit;
|
||||||
@@ -126,7 +196,7 @@ button,
|
|||||||
|
|
||||||
&.primary {
|
&.primary {
|
||||||
background: var(--accent-100);
|
background: var(--accent-100);
|
||||||
border-color: var(--accent-400);
|
border-color: var(--accent-500);
|
||||||
|
|
||||||
&:hover::after {
|
&:hover::after {
|
||||||
background: #00000020;
|
background: #00000020;
|
||||||
@@ -152,8 +222,8 @@ button,
|
|||||||
|
|
||||||
height: 3rem;
|
height: 3rem;
|
||||||
|
|
||||||
background: var(--gray-000);
|
background: var(--bg-500);
|
||||||
border: 2px solid var(--gray-350);
|
border: 2px solid var(--border-400);
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
|
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
@@ -168,6 +238,7 @@ button,
|
|||||||
select {
|
select {
|
||||||
border: none;
|
border: none;
|
||||||
background: none;
|
background: none;
|
||||||
|
color: var(--text-500);
|
||||||
appearance: none;
|
appearance: none;
|
||||||
|
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
@@ -197,6 +268,21 @@ button,
|
|||||||
|
|
||||||
// Components
|
// Components
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
& > img {
|
||||||
|
max-height: 2.5rem;
|
||||||
|
|
||||||
|
body.dark-mode & {
|
||||||
|
filter: hue-rotate(135deg) brightness(1.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.popup-container {
|
.popup-container {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
@@ -282,16 +368,16 @@ button,
|
|||||||
|
|
||||||
border-top-right-radius: 0.5rem;
|
border-top-right-radius: 0.5rem;
|
||||||
border-bottom-right-radius: 0.5rem;
|
border-bottom-right-radius: 0.5rem;
|
||||||
border: 2px solid var(--gray-350);
|
border: 2px solid var(--border-400);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.radio {
|
&.radio {
|
||||||
&.selected {
|
&.selected {
|
||||||
background: var(--accent-100);
|
background: var(--accent-100);
|
||||||
border-color: var(--accent-400);
|
border-color: var(--accent-500);
|
||||||
|
|
||||||
& + .radio {
|
& + .radio {
|
||||||
border-left-color: var(--accent-400);
|
border-left-color: var(--accent-500);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover::after {
|
&:hover::after {
|
||||||
@@ -305,8 +391,8 @@ button,
|
|||||||
// Extension Classes
|
// Extension Classes
|
||||||
|
|
||||||
.panel {
|
.panel {
|
||||||
background: var(--gray-000);
|
background: var(--bg-500);
|
||||||
border: 1px solid var(--gray-500);
|
border: 1px solid var(--border-500);
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
@@ -329,8 +415,8 @@ button,
|
|||||||
// Structure
|
// Structure
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background: var(--gray-000);
|
background: var(--bg-500);
|
||||||
color: var(--gray-900);
|
color: var(--text-500);
|
||||||
|
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: 'Inter', sans-serif;
|
||||||
@@ -345,7 +431,7 @@ body {
|
|||||||
|
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
border: none;
|
border: none;
|
||||||
border-bottom: 1px solid var(--gray-500);
|
border-bottom: 1px solid var(--border-500);
|
||||||
|
|
||||||
height: 4rem;
|
height: 4rem;
|
||||||
|
|
||||||
@@ -359,22 +445,10 @@ body {
|
|||||||
|
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
|
|
||||||
@media screen and (max-width: $device-s-width) and (orientation: portrait),
|
@media screen and (max-width: $device-s-width), (pointer: coarse) {
|
||||||
screen and (max-height: $device-s-width) and (orientation: landscape) {
|
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
img {
|
|
||||||
max-height: 2.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
gap: 0.25rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.events-view {
|
.events-view {
|
||||||
@@ -415,7 +489,7 @@ body {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&.selected {
|
&.selected {
|
||||||
background: var(--gray-100);
|
background: var(--bg-selected-500);
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
content: '';
|
content: '';
|
||||||
@@ -423,13 +497,13 @@ body {
|
|||||||
inset: -1px;
|
inset: -1px;
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
|
|
||||||
border: 3px solid var(--gray-800);
|
border: 3px solid var(--border-selected-500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.highlight {
|
&.highlight {
|
||||||
background: var(--gray-300);
|
background: var(--bg-highlight-500);
|
||||||
border-color: var(--gray-700);
|
border-color: var(--border-highlight-500);
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
@@ -474,15 +548,15 @@ body {
|
|||||||
|
|
||||||
height: calc(var(--size) * var(--event-height) + (var(--size) - 1) * 0.25rem);
|
height: calc(var(--size) * var(--event-height) + (var(--size) - 1) * 0.25rem);
|
||||||
|
|
||||||
border-top: 1px solid var(--gray-500);
|
border-top: 1px solid var(--border-500);
|
||||||
border-right: 1px solid var(--gray-500);
|
border-right: 1px solid var(--border-500);
|
||||||
padding: 0.5rem 1rem 0 0.5rem;
|
padding: 0.5rem 1rem 0 0.5rem;
|
||||||
|
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
border-bottom: 1px solid var(--gray-500);
|
border-bottom: 1px solid var(--border-500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -515,11 +589,11 @@ body {
|
|||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
|
||||||
border-left: 1px solid var(--gray-500);
|
border-left: 1px solid var(--border-500);
|
||||||
|
|
||||||
&:nth-child(2),
|
&:nth-child(2),
|
||||||
&:nth-child(4) {
|
&:nth-child(4) {
|
||||||
border-right: 1px solid var(--gray-500);
|
border-right: 1px solid var(--border-500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -547,15 +621,14 @@ body {
|
|||||||
grid-row: var(--row) / span 1;
|
grid-row: var(--row) / span 1;
|
||||||
grid-column: var(--start) / span var(--size);
|
grid-column: var(--start) / span var(--size);
|
||||||
|
|
||||||
// $color: hsl(var(--hue), 80%, 60%);
|
$color: var(--bg-500);
|
||||||
$color: var(--gray-000);
|
|
||||||
|
|
||||||
background: $color;
|
background: $color;
|
||||||
color: var(--gray-900);
|
color: var(--text-500);
|
||||||
|
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
|
|
||||||
border: 1px solid var(--gray-600);
|
border: 1px solid var(--border-600);
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 0.4rem;
|
padding: 0.4rem;
|
||||||
@@ -595,11 +668,11 @@ body {
|
|||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
border-top: 1px solid var(--gray-500);
|
border-top: 1px solid var(--border-500);
|
||||||
padding: 0.25rem 0;
|
padding: 0.25rem 0;
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
border-bottom: 1px solid var(--gray-500);
|
border-bottom: 1px solid var(--border-500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -630,10 +703,10 @@ body {
|
|||||||
|
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
|
||||||
background: var(--gray-000);
|
background: var(--bg-500);
|
||||||
|
|
||||||
border-bottom: 1px solid var(--gray-500);
|
border-bottom: 1px solid var(--border-500);
|
||||||
border-right: 1px solid var(--gray-500);
|
border-right: 1px solid var(--border-500);
|
||||||
}
|
}
|
||||||
|
|
||||||
.left-header {
|
.left-header {
|
||||||
@@ -645,9 +718,9 @@ body {
|
|||||||
position: sticky;
|
position: sticky;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|
||||||
border-right: 1px solid var(--gray-500);
|
border-right: 1px solid var(--border-500);
|
||||||
|
|
||||||
background: var(--gray-000);
|
background: var(--bg-500);
|
||||||
|
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|
||||||
@@ -669,16 +742,15 @@ body {
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
|
||||||
border-top: 1px solid var(--gray-500);
|
border-top: 1px solid var(--border-500);
|
||||||
|
|
||||||
@media screen and (max-width: $device-s-width) and (orientation: portrait),
|
@media screen and (max-width: $device-s-width), (pointer: coarse) {
|
||||||
screen and (max-height: $device-s-width) and (orientation: landscape) {
|
|
||||||
writing-mode: vertical-lr;
|
writing-mode: vertical-lr;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(.skip-border) {
|
&:not(.skip-border) {
|
||||||
height: calc(2 * (var(--event-height) + 1rem) + 1px);
|
height: calc(2 * (var(--event-height) + 1rem) + 1px);
|
||||||
border-bottom: 1px solid var(--gray-500);
|
border-bottom: 1px solid var(--border-500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -708,12 +780,12 @@ body {
|
|||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
|
||||||
background: var(--gray-000);
|
background: var(--bg-500);
|
||||||
border-bottom: 1px solid var(--gray-500);
|
border-bottom: 1px solid var(--border-500);
|
||||||
}
|
}
|
||||||
|
|
||||||
// &:not(:last-child) {
|
// &:not(:last-child) {
|
||||||
border-right: 1px solid var(--gray-500);
|
border-right: 1px solid var(--border-500);
|
||||||
// }
|
// }
|
||||||
|
|
||||||
.events {
|
.events {
|
||||||
@@ -730,12 +802,14 @@ body {
|
|||||||
.event {
|
.event {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
grid-row: var(--start) / span var(--size);
|
grid-row: var(--start) / span var(--size);
|
||||||
grid-column: var(--stack) / span 1;
|
grid-column: var(--stack) / span 1;
|
||||||
|
|
||||||
background: var(--gray-000);
|
background: var(--bg-500);
|
||||||
|
|
||||||
border: 1px solid var(--gray-600);
|
border: 1px solid var(--border-600);
|
||||||
// border: 4px solid hsl(var(--hue), 80%, 80%);
|
// border: 4px solid hsl(var(--hue), 80%, 80%);
|
||||||
|
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
@@ -748,7 +822,7 @@ body {
|
|||||||
padding: 0.5rem 1rem 0.5rem 0.5rem;
|
padding: 0.5rem 1rem 0.5rem 0.5rem;
|
||||||
|
|
||||||
&.selected {
|
&.selected {
|
||||||
background: var(--gray-100);
|
background: var(--bg-selected-500);
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
content: '';
|
content: '';
|
||||||
@@ -756,13 +830,13 @@ body {
|
|||||||
inset: -1px;
|
inset: -1px;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
|
|
||||||
border: 3px solid var(--gray-800);
|
border: 3px solid var(--border-selected-500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.highlight {
|
&.highlight {
|
||||||
background: var(--gray-300);
|
background: var(--bg-highlight-500);
|
||||||
border-color: var(--gray-700);
|
border-color: var(--border-highlight-500);
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
@@ -781,7 +855,7 @@ body {
|
|||||||
grid-row: var(--track) / span 2;
|
grid-row: var(--track) / span 2;
|
||||||
|
|
||||||
height: 0px;
|
height: 0px;
|
||||||
border-bottom: 1px dashed var(--gray-500);
|
border-bottom: 1px dashed var(--border-500);
|
||||||
|
|
||||||
align-self: center;
|
align-self: center;
|
||||||
|
|
||||||
@@ -795,6 +869,207 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.work-week-grid-view {
|
||||||
|
position: relative;
|
||||||
|
padding: 1rem;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
overflow: scroll;
|
||||||
|
|
||||||
|
.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: 2px solid var(--border-500);
|
||||||
|
border-radius: 10px 10px 0 0;
|
||||||
|
|
||||||
|
|
||||||
|
@media screen and (max-width: $device-s-width), (pointer: coarse) {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time {
|
||||||
|
display: flex;
|
||||||
|
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: 2px solid var(--border-500);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::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;
|
||||||
|
|
||||||
|
&: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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.overlay {
|
.overlay {
|
||||||
@@ -804,10 +1079,17 @@ body {
|
|||||||
bottom: 3rem;
|
bottom: 3rem;
|
||||||
right: 3rem;
|
right: 3rem;
|
||||||
|
|
||||||
|
// on mobile
|
||||||
|
@media screen and (max-width: $device-s-width), (pointer: coarse) {
|
||||||
|
bottom: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
|
||||||
button {
|
button {
|
||||||
padding: 0.8rem;
|
width: 3rem;
|
||||||
|
height: 3rem;
|
||||||
|
|
||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
|
|
||||||
@@ -830,7 +1112,7 @@ body {
|
|||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
|
|
||||||
background: var(--gray-000);
|
background: var(--bg-500);
|
||||||
|
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
|
||||||
@@ -843,16 +1125,11 @@ body {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
||||||
border-bottom: 1px solid var(--gray-500);
|
border-bottom: 1px solid var(--border-500);
|
||||||
.logo {
|
|
||||||
|
.option-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
gap: 0.5rem;
|
||||||
|
|
||||||
img {
|
|
||||||
max-height: 2.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
gap: 0.25rem;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -878,7 +1155,7 @@ body {
|
|||||||
height: 1px;
|
height: 1px;
|
||||||
left: 1rem;
|
left: 1rem;
|
||||||
|
|
||||||
background: var(--gray-500);
|
background: var(--border-500);
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -891,8 +1168,7 @@ body {
|
|||||||
|
|
||||||
// not on mobile
|
// not on mobile
|
||||||
|
|
||||||
@media screen and (min-width: $device-s-width) and (orientation: portrait),
|
@media screen and (min-width: $device-s-width) and (pointer: fine) {
|
||||||
screen and (min-height: $device-s-width) and (orientation: landscape) {
|
|
||||||
.mobile {
|
.mobile {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -904,8 +1180,7 @@ body {
|
|||||||
|
|
||||||
// on mobile
|
// on mobile
|
||||||
|
|
||||||
@media screen and (max-width: $device-s-width) and (orientation: portrait),
|
@media screen and (max-width: $device-s-width), (pointer: coarse) {
|
||||||
screen and (max-height: $device-s-width) and (orientation: landscape) {
|
|
||||||
.toolbar {
|
.toolbar {
|
||||||
padding: 0.75rem 1rem 0.75rem 0.75rem;
|
padding: 0.75rem 1rem 0.75rem 0.75rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,51 @@
|
|||||||
|
// Calendar
|
||||||
|
|
||||||
|
export const WEEK_DAYS = [
|
||||||
|
'Domenica',
|
||||||
|
'Lunedì',
|
||||||
|
'Martedì',
|
||||||
|
'Mercoledì',
|
||||||
|
'Giovedì',
|
||||||
|
'Venerdì',
|
||||||
|
'Sabato',
|
||||||
|
]
|
||||||
|
|
||||||
|
// Hashing
|
||||||
|
|
||||||
|
export function hashString(str, seed = 0) {
|
||||||
|
let h1 = 0xdeadbeef ^ seed,
|
||||||
|
h2 = 0x41c6ce57 ^ seed
|
||||||
|
for (let i = 0, ch; i < str.length; i++) {
|
||||||
|
ch = str.charCodeAt(i)
|
||||||
|
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)
|
||||||
|
return 4294967296 * (2097151 & h2) + (h1 >>> 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Courses
|
||||||
|
|
||||||
|
export function normalizeCourseName(name) {
|
||||||
|
return name
|
||||||
|
.split(' ')
|
||||||
|
.map(word => word.toLowerCase())
|
||||||
|
.map(word => {
|
||||||
|
if (word.trim().length === 0) return word
|
||||||
|
|
||||||
|
return /(^del|^nel|^di$|^dei$|^con$|^alla$|^per$|^e$|^la$)/.test(word)
|
||||||
|
? word
|
||||||
|
: word[0].toUpperCase() + word.slice(1)
|
||||||
|
})
|
||||||
|
.join(' ')
|
||||||
|
.replaceAll('Ii', 'II')
|
||||||
|
.replaceAll('Iii', 'III')
|
||||||
|
.replaceAll(/'(.)/g, ({}, letter) => "'" + letter.toUpperCase())
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSX
|
||||||
|
|
||||||
export const withClasses = classes =>
|
export const withClasses = classes =>
|
||||||
Array.isArray(classes)
|
Array.isArray(classes)
|
||||||
? classes.filter(e => !!e).join(' ')
|
? classes.filter(e => !!e).join(' ')
|
||||||
|
|||||||
Reference in New Issue
Block a user