13 changed files with 559 additions and 228 deletions
+2 -1
View File
@@ -16,7 +16,8 @@
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"preact": "^10.10.6", "preact": "^10.10.6",
"sass": "^1.54.8", "sass": "^1.54.8",
"vite": "^3.0.9" "vite": "^3.0.9",
"yaml": "^2.3.2"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.18.13", "@babel/core": "^7.18.13",
+8
View File
@@ -23,6 +23,9 @@ dependencies:
vite: vite:
specifier: ^3.0.9 specifier: ^3.0.9
version: 3.0.9(sass@1.54.8) version: 3.0.9(sass@1.54.8)
yaml:
specifier: ^2.3.2
version: 2.3.2
devDependencies: devDependencies:
'@babel/core': '@babel/core':
@@ -937,3 +940,8 @@ packages:
sass: 1.54.8 sass: 1.54.8
optionalDependencies: optionalDependencies:
fsevents: 2.3.2 fsevents: 2.3.2
/yaml@2.3.2:
resolution: {integrity: sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==}
engines: {node: '>= 14'}
dev: false
+26
View File
@@ -0,0 +1,26 @@
import { useRef } from 'preact/hooks'
import { Icon } from './Icon.jsx'
export const DatePicker = ({ date, setDate }) => {
const input = useRef()
const [year, month, day] = date.split('T')[0].split('-')
return (
<div class="date-picker" onClick={() => input.current.showPicker()}>
<input
ref={input}
type="date"
value={`${year}-${month}-${day}`}
onChange={e => setDate(new Date(e.target.value).toISOString())}
/>
<div class="date-picker-render">
<div class="date">
{day}/{month}/{year}
</div>
<div class="calendar">
<Icon name="calendar_month" />
</div>
</div>
</div>
)
}
+8 -18
View File
@@ -1,27 +1,17 @@
import { ComboBox } from './ComboBox.jsx' import { ComboBox } from './ComboBox.jsx'
import { MODE_COURSES, MODE_SCHEDULE, MODE_WORKWEEK, MODE_WORKWEEK_GRID } from './EventsView.jsx' import {
MODE_COURSES,
MODE_SCHEDULE,
MODE_WORKWEEK,
MODE_WORKWEEK_GRID,
} from './EventsView.jsx'
import { DatePicker } from './DatePicker.jsx'
import { Help } from './Help.jsx' import { Help } from './Help.jsx'
import { Icon } from './Icon.jsx' import { Icon } from './Icon.jsx'
export const HamburgerMenu = ({ onClose, theme, setTheme }) => { export const HamburgerMenu = ({ date, setDate, onClose, theme, setTheme }) => {
return ( return (
<div class="menu"> <div class="menu">
<div class="header">
<div class="option-group">
<button class="flat icon" onClick={onClose}>
<Icon name="close" />
</button>
<button
class="icon"
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
>
<Icon name={theme === 'dark' ? 'dark_mode' : 'light_mode'} />
</button>
</div>
<div class="item logo">
<img src="logo-circuit-board.svg" alt="logo" /> / <span>Orario</span>
</div>
</div>
<div class="help"> <div class="help">
<h2> <h2>
<Icon name="info" /> <Icon name="info" />
+7 -1
View File
@@ -12,7 +12,8 @@ export const Help = ({}) => (
</p> </p>
<p> <p>
Una volta compiuta la selezione, è possibile vedere la tabella delle Una volta compiuta la selezione, è possibile vedere la tabella delle
lezioni andando nella visualizzazione Orario lezioni andando nella visualizzazione Orario (
<Icon name="calendar_view_month" />)
</p> </p>
<p> <p>
Per via di eventuali preferenze personali, è possibile cambiare Per via di eventuali preferenze personali, è possibile cambiare
@@ -20,6 +21,11 @@ export const Help = ({}) => (
pulsante Trasponi ( pulsante Trasponi (
<Icon name="switch_left" style="transform: rotate(-45deg)" />) <Icon name="switch_left" style="transform: rotate(-45deg)" />)
</p> </p>
<p>
È anche possibile visualizzare in uno specchietto riassuntivo
soltanto i corsi selezionati, andando nella visualizzazione Lista (
<Icon name="list" />)
</p>
<h3>Stampa</h3> <h3>Stampa</h3>
<p> <p>
Da desktop puoi stampare l'orario attualmente visibile con il Da desktop puoi stampare l'orario attualmente visibile con il
+11 -6
View File
@@ -1,7 +1,7 @@
import { CompoundButton } from './CompoundButton.jsx' import { CompoundButton } from './CompoundButton.jsx'
import { Icon } from './Icon.jsx' import { Icon } from './Icon.jsx'
export const OptionBar = ({ source, setSource }) => { export const OptionBar = ({ view, setView }) => {
return ( return (
<div class="option-bar"> <div class="option-bar">
<div class="option-group"> <div class="option-group">
@@ -14,20 +14,25 @@ export const OptionBar = ({ source, setSource }) => {
{ value: 'magistrale', label: 'M' }, { value: 'magistrale', label: 'M' },
{ value: 'tutti', label: 'Tutti' }, { value: 'tutti', label: 'Tutti' },
]} ]}
value={source} value={view}
setValue={setSource} setValue={setView}
/> />
</div> </div>
<CompoundButton <CompoundButton
options={[ options={[
{ {
value: 'orario', value: 'orario',
label: <Icon name="calendar_month" />, label: <Icon name="calendar_view_month" />,
icon: true,
},
{
value: 'lista',
label: <Icon name="list" />,
icon: true, icon: true,
}, },
]} ]}
value={source} value={view}
setValue={setSource} setValue={setView}
/> />
</div> </div>
</div> </div>
+32
View File
@@ -0,0 +1,32 @@
import { DatePicker } from './DatePicker.jsx'
import { Icon } from './Icon.jsx'
export const SettingsBar = ({ theme, setTheme, date, setDate }) => {
return (
<div class="settings-bar">
<div class="settings-group">
<button
class="icon"
onClick={() =>
setTheme(theme === 'dark' ? 'light' : 'dark')
}
>
<Icon
name={theme === 'dark' ? 'dark_mode' : 'light_mode'}
/>
</button>
<button
class="icon"
onClick={() =>
setTheme(theme === 'dark' ? 'light' : 'dark')
}
>
<Icon
name="add"
/>
</button>
<DatePicker date={date} setDate={setDate} />
</div>
</div>
)
}
+16 -3
View File
@@ -1,10 +1,14 @@
import { CompoundButton } from './CompoundButton.jsx' import { CompoundButton } from './CompoundButton.jsx'
import { DatePicker } from './DatePicker.jsx'
import { Icon } from './Icon.jsx' import { Icon } from './Icon.jsx'
export const Toolbar = ({ export const Toolbar = ({
source, source,
setSource, setSource,
onShowMenu, date,
setDate,
showMobileMenu,
setShowMobileMenu,
onHelp, onHelp,
theme, theme,
setTheme, setTheme,
@@ -12,8 +16,11 @@ export const Toolbar = ({
return ( return (
<div class="toolbar"> <div class="toolbar">
<div class="mobile"> <div class="mobile">
<button class="flat icon" onClick={onShowMenu}> <button
<Icon name="menu" /> class="flat icon"
onClick={() => setShowMobileMenu(!showMobileMenu)}
>
<Icon name={`${showMobileMenu ? 'close' : 'menu'}`} />
</button> </button>
</div> </div>
<div class="item logo"> <div class="item logo">
@@ -29,6 +36,7 @@ export const Toolbar = ({
{ value: 'anno-3', label: 'III' }, { value: 'anno-3', label: 'III' },
{ value: 'magistrale', label: 'Magistrale' }, { value: 'magistrale', label: 'Magistrale' },
{ value: 'tutti', label: 'Tutti' }, { value: 'tutti', label: 'Tutti' },
{ value: 'custom', label: 'Custom' },
]} ]}
value={source} value={source}
setValue={setSource} setValue={setSource}
@@ -38,11 +46,16 @@ export const Toolbar = ({
<CompoundButton <CompoundButton
options={[ options={[
{ value: 'orario', label: 'Orario' }, { value: 'orario', label: 'Orario' },
{ value: 'lista', label: 'Lista' },
]} ]}
value={source} value={source}
setValue={setSource} setValue={setSource}
/> />
</div> </div>
<div class="empty"></div>
<div class="item option">
<DatePicker date={date} setDate={setDate} />
</div>
</div> </div>
<div class="option-group"> <div class="option-group">
<div class="item option"> <div class="item option">
+54 -13
View File
@@ -2,13 +2,29 @@ import { format } from 'date-fns'
import _ from 'lodash' import _ from 'lodash'
import { useEffect, useRef, useState } from 'preact/hooks' import { useEffect, useRef, useState } from 'preact/hooks'
import { prettyCourseName, WEEK_DAYS } from '../../utils.jsx' import { parseCustomEvents, prettyCourseName, WEEK_DAYS } from '../../utils.jsx'
import { Icon } from '../Icon.jsx'
export const Courses = ({ source, timetables, selection, setSelection }) => { export const Courses = ({
const events = timetables[source] source,
timetables,
selection,
setSelection,
custom,
isRestrictedList,
}) => {
const events = isRestrictedList ? timetables['tutti'] : timetables[source]
const selectionSet = new Set(selection) const selectionSet = new Set(selection)
const eventsByCourse = _.groupBy(_.sortBy(events, 'id'), 'id') console.log(events)
const visibleEvents = isRestrictedList
? events
.filter(e => selectionSet.has(e.id))
.concat(parseCustomEvents(custom))
: events
const eventsByCourse = _.groupBy(_.sortBy(visibleEvents, 'id'), 'id')
const profsPerCourse = _.mapValues(eventsByCourse, events => const profsPerCourse = _.mapValues(eventsByCourse, events =>
_.uniq(events.flatMap(event => event.docenti)) _.uniq(events.flatMap(event => event.docenti))
@@ -42,28 +58,53 @@ export const Courses = ({ source, timetables, selection, setSelection }) => {
return ( return (
<div class="course-view" ref={element}> <div class="course-view" ref={element}>
{isRestrictedList && selection.length === 0 && (
<div class="warning">
<p>Non hai ancora selezionato nessun corso.</p>
<p>
Clicca sui corsi nelle altre visuali per selezionarli e
visualizzarli nella lista
</p>
</div>
)}
<div class="wrap-container"> <div class="wrap-container">
{Object.entries(eventsByCourse).map(([id, courseEvents]) => ( {Object.entries(eventsByCourse).map(([id, courseEvents]) => (
<div <div
class={ class={
'course' + 'course' +
(currentlyHovered === id ? ' highlight' : '') + (currentlyHovered === id && !isRestrictedList
(selectionSet.has(id) ? ' selected' : '') ? ' highlight'
: '') +
(selectionSet.has(id) && !isRestrictedList
? ' selected'
: '')
} }
data-course-id={id} data-course-id={id}
onClick={() => { onClick={() => {
if (!selectionSet.has(id)) setSelection([...selection, id]) if (isRestrictedList) return
else setSelection(selection.filter(selId => selId !== id)) if (!selectionSet.has(id))
setSelection([...selection, id])
else
setSelection(
selection.filter(selId => selId !== id)
)
}} }}
> >
<div class="title">{prettyCourseName(courseEvents[0].name)}</div> <div class="title">
<div class="docenti">{profsPerCourse[id].join(', ')}</div> {prettyCourseName(courseEvents[0].name)}
</div>
<div class="docenti">
{profsPerCourse[id].join(', ')}
</div>
<div class="events"> <div class="events">
{courseEvents.map(course => ( {courseEvents.map(course => (
<div> <div>
{WEEK_DAYS[course.start.getDay()]}{' '} {WEEK_DAYS[course.day]} {course.start / 60}:
{format(course.start, 'H:mm')}&ndash; {String(course.start % 60).padStart(2, '0')}
{format(course.end, 'H:mm')} {course.aule.join(', ')} &ndash;
{course.end / 60}:
{String(course.end % 60).padStart(2, '0')}{' '}
{course.aule.join(', ')}
</div> </div>
))} ))}
</div> </div>
+7 -12
View File
@@ -1,7 +1,9 @@
import { useEffect, useRef, useState } from 'preact/hooks' import { useEffect, useRef, useState } from 'preact/hooks'
import _ from 'lodash' import _, { parseInt } from 'lodash'
import { differenceInMinutes, startOfDay } from 'date-fns' import { differenceInMinutes, startOfDay } from 'date-fns'
import { parse } from 'yaml'
import { parseCustomEvents } from '../../utils.jsx'
import { import {
WEEK_DAYS_SHORT, WEEK_DAYS_SHORT,
@@ -46,7 +48,7 @@ const TransposePopup = ({ onClose }) => {
const NoCourseWarning = () => { const NoCourseWarning = () => {
return ( return (
<div class="no-courses-warning"> <div class="warning">
<p>Non hai ancora selezionato nessun corso.</p> <p>Non hai ancora selezionato nessun corso.</p>
<p> <p>
Clicca sui corsi nelle altre visuali per selezionarli e Clicca sui corsi nelle altre visuali per selezionarli e
@@ -57,7 +59,6 @@ const NoCourseWarning = () => {
} }
const Layout = ({ layout, day, colors }) => { const Layout = ({ layout, day, colors }) => {
console.log(layout)
return ( return (
<> <>
{layout.map(block => ( {layout.map(block => (
@@ -211,7 +212,7 @@ const ScheduleCard = ({
) )
} }
export const Schedule = ({ timetables, selection, setSelection }) => { export const Schedule = ({ timetables, selection, custom }) => {
const [hasSeenTranspose, setHasSeenTranspose] = usePersistentState( const [hasSeenTranspose, setHasSeenTranspose] = usePersistentState(
'transpose_info', 'transpose_info',
'false' 'false'
@@ -233,14 +234,8 @@ export const Schedule = ({ timetables, selection, setSelection }) => {
const allEvents = timetables['tutti'] const allEvents = timetables['tutti']
const selectionSet = new Set(selection) const selectionSet = new Set(selection)
const events = allEvents const events = allEvents.filter(e => selectionSet.has(e.id))
.filter(e => selectionSet.has(e.id)) parseCustomEvents(custom).forEach(event => events.push(event))
.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 weekStart = Math.min(...events.map(e => e.start), 9 * 60) / 30
const weekEnd = Math.max(...events.map(e => e.end), 18 * 60) / 30 const weekEnd = Math.max(...events.map(e => e.end), 18 * 60) / 30
+161 -80
View File
@@ -1,6 +1,7 @@
import _ from 'lodash' import _ from 'lodash'
import { render } from 'preact' import { render } from 'preact'
import { useEffect, useState } from 'preact/hooks' import { useEffect, useState } from 'preact/hooks'
import { differenceInMinutes, startOfDay } from 'date-fns'
// import { ToolOverlay } from './components/ToolOverlay.jsx' // import { ToolOverlay } from './components/ToolOverlay.jsx'
// //
@@ -18,12 +19,8 @@ import { Icon } from './components/Icon.jsx'
import { Popup } from './components/Popup.jsx' import { Popup } from './components/Popup.jsx'
import { Toolbar } from './components/Toolbar.jsx' import { Toolbar } from './components/Toolbar.jsx'
import { OptionBar } from './components/OptionBar.jsx' import { OptionBar } from './components/OptionBar.jsx'
import { import { prettyAulaName, prettyProfName, usePersistentState } from './utils.jsx'
prettyAulaName, import { SettingsBar } from './components/SettingsBar.jsx'
prettyProfName,
clearOldPersistentStates,
usePersistentState,
} from './utils.jsx'
// Che fanno queste due righe? // Che fanno queste due righe?
window._ = _ window._ = _
@@ -36,17 +33,17 @@ const TIMETABLE_IDS = {
'magistrale': '64a7c7091ab813002c5d9ede', 'magistrale': '64a7c7091ab813002c5d9ede',
} }
const DEFAULT_DATE_RANGE = { // const DEFAULT_DATE_RANGE = {
from: '2023-10-09T00:00:00.000Z', // from: '2023-10-09T00:00:00.000Z',
to: '2023-10-14T00:00:00.000Z', // to: '2023-10-14T00:00:00.000Z',
} // }
const DATE_RANGES = { // const DATE_RANGES = {
'64a7c1c651f079001d52e9c8': DEFAULT_DATE_RANGE, // '64a7c1c651f079001d52e9c8': DEFAULT_DATE_RANGE,
'6308e2dc09352a0208fefdd9': DEFAULT_DATE_RANGE, // '6308e2dc09352a0208fefdd9': DEFAULT_DATE_RANGE,
'6308e42a1df5cb026699ced4': DEFAULT_DATE_RANGE, // '6308e42a1df5cb026699ced4': DEFAULT_DATE_RANGE,
'64a7c7091ab813002c5d9ede': DEFAULT_DATE_RANGE, // '64a7c7091ab813002c5d9ede': DEFAULT_DATE_RANGE,
} // }
function specialEventPatches(eventi) { function specialEventPatches(eventi) {
// Il laboratorio del primo anno in realtà è in due canali separati // Il laboratorio del primo anno in realtà è in due canali separati
@@ -66,11 +63,14 @@ function specialEventPatches(eventi) {
function formatEvents(timetable) { function formatEvents(timetable) {
return timetable.map(({ nome, dataInizio, dataFine, docenti, aule }) => { return timetable.map(({ nome, dataInizio, dataFine, docenti, aule }) => {
const start = new Date(dataInizio)
const end = new Date(dataFine)
return { return {
id: nome, id: nome,
name: _.split(nome, '-', 1)[0].trim(), name: _.split(nome, '-', 1)[0].trim(),
start: new Date(dataInizio), day: start.getDay(),
end: new Date(dataFine), start: differenceInMinutes(start, startOfDay(start)),
end: differenceInMinutes(end, startOfDay(start)),
docenti: docenti.map(({ nome, cognome }) => docenti: docenti.map(({ nome, cognome }) =>
prettyProfName(nome, cognome) prettyProfName(nome, cognome)
), ),
@@ -79,7 +79,18 @@ function formatEvents(timetable) {
}) })
} }
async function loadCalendari() { async function loadCalendari(date) {
function getMonday(d) {
const day = d.getDay()
const diff = d.getDate() - day + (day === 0 ? -6 : 1)
const monday = new Date(d.setDate(diff))
monday.setUTCHours(0, 0, 0, 0)
return monday
}
const monday = getMonday(date)
const saturday = new Date(monday)
saturday.setDate(monday.getDate() + 5)
async function req(id) { async function req(id) {
// Almost directly copy-pasted from Chrome Dev Tools // Almost directly copy-pasted from Chrome Dev Tools
const req = await fetch( const req = await fetch(
@@ -97,8 +108,8 @@ async function loadCalendari() {
linkCalendarioId: id, linkCalendarioId: id,
clienteId: '628de8b9b63679f193b87046', clienteId: '628de8b9b63679f193b87046',
pianificazioneTemplate: false, pianificazioneTemplate: false,
dataInizio: DATE_RANGES[id].from, dataInizio: monday.toISOString(),
dataFine: DATE_RANGES[id].to, dataFine: saturday.toISOString(),
}), }),
method: 'POST', method: 'POST',
mode: 'cors', mode: 'cors',
@@ -130,23 +141,78 @@ async function loadCalendari() {
} }
} }
const View = ({
view,
selection,
setSelection,
timetables,
custom,
setCustom,
}) => {
if (view === 'orario') {
return (
<Schedule
selection={selection}
timetables={timetables}
custom={custom}
/>
)
} else if (view === 'lista') {
return (
<Courses
selection={selection}
setSelection={setSelection}
timetables={timetables}
custom={custom}
isRestrictedList={true}
/>
)
} else if (view === 'custom') {
return (
<div class="custom-events-view">
<textarea
value={custom}
onChange={e => setCustom(e.target.value)}
/>
<p>Esempio di evento personalizzato</p>
<p>
Nome evento [label globale]:
<br />
- Lun 9-11
<br />
- Mar 9:00-11:00
<br />- Gio 8:00-12:00 [label locale]
</p>
</div>
)
} else {
return (
<Courses
selection={selection}
setSelection={setSelection}
source={view}
timetables={timetables}
isRestrictedList={false}
/>
)
}
}
const App = ({}) => { const App = ({}) => {
// Clear persistent states unless state_token corresponds to the one passed // Clear persistent states unless state_token corresponds to the one passed
// as the argument. Useful with breaking updates. Change this token if your // as the argument. Useful with breaking updates. Change this token if your
// (breaking) update needs a reset of persistent states to avoid crashes. // (breaking) update needs a reset of persistent states to avoid crashes.
// //
// Use any random string of your choice // Use any random string of your choice
clearOldPersistentStates('e73cba02') // clearOldPersistentStates('e73cba02')
const [date, setDate] = useState(new Date().toISOString())
// Data Sources // Data Sources
const [source, setSource] = usePersistentState('source', 'magistrale') const [view, setView] = usePersistentState('view', 'magistrale')
const [timetables, setTimetables] = useState(null) const [timetables, setTimetables] = useState(null)
useEffect(async () => { useEffect(async () => {
setTimetables(await loadCalendari()) setTimetables(await loadCalendari(new Date(date)))
}, []) }, [date])
// View Modes
// const [mode, setMode] = usePersistentState('orario.mode', MODE_COURSES)
// Selection // Selection
const [selectedCourses, setSelectedCourses] = usePersistentState( const [selectedCourses, setSelectedCourses] = usePersistentState(
@@ -154,6 +220,9 @@ const App = ({}) => {
[] []
) )
// Custom Events
const [custom, setCustom] = usePersistentState('custom', [])
// Menus // Menus
const [helpVisible, setHelpVisible] = useState(false) const [helpVisible, setHelpVisible] = useState(false)
const [showMobileMenu, setShowMobileMenu] = useState(false) const [showMobileMenu, setShowMobileMenu] = useState(false)
@@ -175,69 +244,81 @@ const App = ({}) => {
<> <>
<Toolbar <Toolbar
{...{ {...{
source, source: view,
setSource, setSource: setView,
onShowMenu: () => setShowMobileMenu(true), date: date,
setDate: setDate,
showMobileMenu: showMobileMenu,
setShowMobileMenu: setShowMobileMenu,
onHelp: () => setHelpVisible(true), onHelp: () => setHelpVisible(true),
theme, theme,
setTheme, setTheme,
}} }}
/> />
<OptionBar {showMobileMenu ? (
{...{ <SettingsBar
source,
setSource,
onHelp: () => setHelpVisible(true),
}}
orizzontale
/>
{timetables && (
<div class="content">
{source === 'orario' ? (
<Schedule
selection={selectedCourses}
setSelection={setSelectedCourses}
start={new Date(2022, 10, 3)}
source={source}
timetables={timetables}
/>
) : (
<Courses
selection={selectedCourses}
setSelection={setSelectedCourses}
start={new Date(2022, 10, 3)}
source={source}
timetables={timetables}
/>
)}
</div>
)}
{/* toolOverlayVisible && (
<ToolOverlay
mode={mode}
toggleMode={() =>
setMode(
mode === MODE_COURSES ? MODE_SCHEDULE : MODE_COURSES
)
}
onClose={() => {
setSelectedCourses([])
setMode(MODE_COURSES)
}}
/>
) */}
{showMobileMenu && (
<HamburgerMenu
{...{ {...{
theme, theme,
setTheme, setTheme,
onClose: () => { date,
setShowMobileMenu(false) setDate,
},
}} }}
/> />
) : (
<OptionBar
{...{
view: view,
setView: setView,
onHelp: () => setHelpVisible(true),
}}
orizzontale
/>
)} )}
<div class="content">
{timetables &&
(showMobileMenu ? (
<HamburgerMenu
{...{
date,
setDate,
theme,
setTheme,
onClose: () => {
setShowMobileMenu(false)
},
}}
/>
) : timetables['tutti'].length === 0 ? (
<div class="warning">
<p>
Non esistono corsi per la settimana selezionata:
buone vacanze! 🎉
</p>
<p>
Per cambiare settimana puoi usare il widget
Calendario (
<Icon name="calendar_month" />) in alto a destra
<br />
In versione mobile, il widget Calendario è
situato dentro il Menu (
<Icon name="menu" />)
</p>
</div>
) : (
<View
selection={selectedCourses}
setSelection={setSelectedCourses}
view={view}
timetables={timetables}
custom={custom}
setCustom={setCustom}
/>
))}
</div>
{/* showMobileMenu && (
) */}
{helpVisible && ( {helpVisible && (
<Popup <Popup
title={ title={
+167 -83
View File
@@ -374,6 +374,47 @@ button,
} }
} }
.date-picker {
position: relative;
display: flex;
// align-items: center;
justify-content: center;
height: 2.5rem;
padding-left: 0.75rem;
padding-right: 0.75rem;
background: var(--bg-500);
border: 2px solid var(--border-400);
border-radius: 0.5rem;
font-weight: 400;
user-select: none;
cursor: pointer;
input {
z-index: -1;
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 0;
opacity: 0.2;
}
.date-picker-render {
display: flex;
align-items: center;
gap: 0.5rem;
.material-symbols-outlined {
font-size: 18px;
}
}
}
// Extension Classes // Extension Classes
.panel { .panel {
@@ -444,7 +485,8 @@ body {
} }
} }
.option-bar { .option-bar,
.settings-bar {
@extend .panel; @extend .panel;
padding: 0.5rem; padding: 0.5rem;
@@ -469,30 +511,60 @@ body {
display: flex; display: flex;
} }
.option-group { .option-group,
.settings-group {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.75rem; gap: 0.75rem;
width: 100%;
justify-content: space-evenly;
}
}
.settings-bar {
@media screen and (max-width: $device-s-width), (pointer: coarse) {
display: flex;
justify-content: space-between;
} }
} }
.content { .content {
height: calc(100vh - 4rem); height: calc(100dvh - 4rem);
@media screen and (max-width: $device-s-width), (pointer: coarse) { @media screen and (max-width: $device-s-width), (pointer: coarse) {
height: calc(100vh - 8rem); height: calc(100dvh - 8rem);
}
overflow-y: scroll;
padding: 1rem 0rem;
.warning {
@extend .text-block;
width: 100%;
display: flex;
flex-direction: column;
gap: 1rem;
align-items: center;
p {
text-align: center;
}
} }
.course-view { .course-view {
padding: 1rem; padding: 0rem 1rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
height: 100%; min-height: 100%;
overflow-y: scroll; text-align: center;
gap: 1rem;
.wrap-container { .wrap-container {
display: grid; display: grid;
@@ -550,30 +622,18 @@ body {
} }
.schedule-view { .schedule-view {
height: 100%; min-height: 100%;
width: 100%; width: 100%;
max-width: 57rem; max-width: 55rem;
margin: auto; margin: auto;
padding: 1rem 0.5rem; padding: 0rem 0.5rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: 1rem; gap: 1rem;
.no-courses-warning {
width: 100%;
display: flex;
flex-direction: column;
gap: 1rem;
align-items: center;
p {
text-align: center;
}
}
.schedule-card { .schedule-card {
width: 100%; width: 100%;
@@ -773,6 +833,91 @@ body {
} }
} }
} }
.menu {
// position: absolute;
// top: 0;
// left: 0;
width: 100%;
min-height: 100%;
background: var(--bg-500);
z-index: 10;
.header {
height: 4rem;
padding: 0.75rem 1rem 0.75rem 0.75rem;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--border-500);
.option-group {
display: flex;
gap: 0.5rem;
}
}
.options {
padding: 1rem;
display: grid;
grid-template-columns: auto 1fr;
gap: 1rem;
align-items: center;
.label {
font-weight: 400;
}
}
hr {
position: relative;
width: calc(100% - 2rem);
height: 1px;
left: 1rem;
background: var(--border-500);
border: none;
}
.help {
@extend .text-block;
padding: 1rem 1rem 2.5rem;
height: 100%;
overflow-y: scroll;
}
}
.custom-events-view {
min-height: 100%;
width: 100%;
max-width: 30rem;
margin: auto;
padding: 1rem 1rem;
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
textarea {
width: 100%;
min-height: 20rem;
font-size: 20px;
}
}
} }
.overlay { .overlay {
@@ -808,67 +953,6 @@ body {
animation: fade-in 150ms ease-in forwards; animation: fade-in 150ms ease-in forwards;
} }
.menu {
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: var(--bg-500);
z-index: 10;
.header {
height: 4rem;
padding: 0.75rem 1rem 0.75rem 0.75rem;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--border-500);
.option-group {
display: flex;
gap: 0.5rem;
}
}
.options {
padding: 1rem;
display: grid;
grid-template-columns: auto 1fr;
gap: 1rem;
align-items: center;
.label {
font-weight: 400;
}
}
hr {
position: relative;
width: calc(100% - 2rem);
height: 1px;
left: 1rem;
background: var(--border-500);
border: none;
}
.help {
@extend .text-block;
padding: 1rem 1rem 2.5rem;
}
}
// not on mobile // not on mobile
@media screen and (min-width: $device-s-width) and (pointer: fine) { @media screen and (min-width: $device-s-width) and (pointer: fine) {
+60 -11
View File
@@ -1,6 +1,7 @@
// Calendar
import { useEffect, useState } from 'preact/hooks' import { useEffect, useState } from 'preact/hooks'
import { parse } from 'yaml'
// Calendar
export const WEEK_DAYS = [ export const WEEK_DAYS = [
'Domenica', 'Domenica',
@@ -12,15 +13,7 @@ export const WEEK_DAYS = [
'Sabato', 'Sabato',
] ]
export const WEEK_DAYS_SHORT = [ export const WEEK_DAYS_SHORT = ['Dom', 'Lun', 'Mar', 'Mer', 'Gio', 'Ven', 'Sab']
'Dom',
'Lun',
'Mar',
'Mer',
'Gio',
'Ven',
'Sab'
]
// Hashing // Hashing
@@ -77,6 +70,62 @@ export function prettyProfName(nome, cognome) {
.join('') .join('')
} }
// Custom events
export function parseCustomEvents(customEventsString) {
try {
const customEvents = []
const parsedEvents = parse(customEventsString)
for (const customCourse in parsedEvents) {
const [_, name, teachers] = /([^\[]*)(?:\[(.*)\])?/.exec(
customCourse
)
const docenti = teachers ? teachers.split(',') : []
for (const customEvent of parsedEvents[customCourse]) {
const [_b, day, startH, startM, endH, endM, label] =
/(lun|lunedì|mar|martedì|mer|mercoledì|gio|giovedì|ven|venerdì)\s*(\d{1,2})(?:\:(\d\d))?\s*-\s*(\d{1,2})(?:\:(\d\d))?\s*(.*)?/i.exec(
customEvent
)
const dayNumber = {
lun: 1,
lunedì: 1,
mar: 2,
martedì: 2,
mer: 3,
mercoledì: 3,
gio: 4,
giovedì: 4,
ven: 5,
venerdì: 5,
}[day.toLowerCase()]
const start = startM
? parseInt(startH) * 60 + parseInt(startM)
: parseInt(startH) * 60
const end = endM
? parseInt(endH) * 60 + parseInt(endM)
: parseInt(endH) * 60
const aule = label ? label.split(',') : []
customEvents.push({
id: name,
name,
docenti,
start,
end,
day: dayNumber,
aule,
})
}
}
return customEvents
} catch (e) {
console.log('ehi', e)
return []
}
}
// JSX // JSX
export const withClasses = classes => export const withClasses = classes =>