Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ddf325c03 | ||
|
|
973acad17a | ||
|
|
c8172ad88a | ||
|
|
c0b2894c22 | ||
|
|
fd0440465c | ||
|
|
32dc9a6104 | ||
|
|
af41177561 |
+2
-1
@@ -16,7 +16,8 @@
|
||||
"lodash-es": "^4.17.21",
|
||||
"preact": "^10.10.6",
|
||||
"sass": "^1.54.8",
|
||||
"vite": "^3.0.9"
|
||||
"vite": "^3.0.9",
|
||||
"yaml": "^2.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.18.13",
|
||||
|
||||
Generated
+8
@@ -23,6 +23,9 @@ dependencies:
|
||||
vite:
|
||||
specifier: ^3.0.9
|
||||
version: 3.0.9(sass@1.54.8)
|
||||
yaml:
|
||||
specifier: ^2.3.2
|
||||
version: 2.3.2
|
||||
|
||||
devDependencies:
|
||||
'@babel/core':
|
||||
@@ -937,3 +940,8 @@ packages:
|
||||
sass: 1.54.8
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.2
|
||||
|
||||
/yaml@2.3.2:
|
||||
resolution: {integrity: sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==}
|
||||
engines: {node: '>= 14'}
|
||||
dev: false
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -1,27 +1,17 @@
|
||||
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 { Icon } from './Icon.jsx'
|
||||
|
||||
export const HamburgerMenu = ({ onClose, theme, setTheme }) => {
|
||||
export const HamburgerMenu = ({ date, setDate, onClose, theme, setTheme }) => {
|
||||
return (
|
||||
<div class="menu">
|
||||
<div class="header">
|
||||
<div class="option-group">
|
||||
<button class="flat icon" onClick={onClose}>
|
||||
<Icon name="close" />
|
||||
</button>
|
||||
<button
|
||||
class="icon"
|
||||
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
|
||||
>
|
||||
<Icon name={theme === 'dark' ? 'dark_mode' : 'light_mode'} />
|
||||
</button>
|
||||
</div>
|
||||
<div class="item logo">
|
||||
<img src="logo-circuit-board.svg" alt="logo" /> / <span>Orario</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="help">
|
||||
<h2>
|
||||
<Icon name="info" />
|
||||
|
||||
@@ -12,7 +12,8 @@ export const Help = ({}) => (
|
||||
</p>
|
||||
<p>
|
||||
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>
|
||||
Per via di eventuali preferenze personali, è possibile cambiare
|
||||
@@ -20,6 +21,11 @@ export const Help = ({}) => (
|
||||
pulsante Trasponi (
|
||||
<Icon name="switch_left" style="transform: rotate(-45deg)" />)
|
||||
</p>
|
||||
<p>
|
||||
È anche possibile visualizzare in uno specchietto riassuntivo
|
||||
soltanto i corsi selezionati, andando nella visualizzazione Lista (
|
||||
<Icon name="list" />)
|
||||
</p>
|
||||
<h3>Stampa</h3>
|
||||
<p>
|
||||
Da desktop puoi stampare l'orario attualmente visibile con il
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { CompoundButton } from './CompoundButton.jsx'
|
||||
import { Icon } from './Icon.jsx'
|
||||
|
||||
export const OptionBar = ({ source, setSource }) => {
|
||||
export const OptionBar = ({ view, setView }) => {
|
||||
return (
|
||||
<div class="option-bar">
|
||||
<div class="option-group">
|
||||
@@ -14,20 +14,25 @@ export const OptionBar = ({ source, setSource }) => {
|
||||
{ value: 'magistrale', label: 'M' },
|
||||
{ value: 'tutti', label: 'Tutti' },
|
||||
]}
|
||||
value={source}
|
||||
setValue={setSource}
|
||||
value={view}
|
||||
setValue={setView}
|
||||
/>
|
||||
</div>
|
||||
<CompoundButton
|
||||
options={[
|
||||
{
|
||||
value: 'orario',
|
||||
label: <Icon name="calendar_month" />,
|
||||
label: <Icon name="calendar_view_month" />,
|
||||
icon: true,
|
||||
},
|
||||
{
|
||||
value: 'lista',
|
||||
label: <Icon name="list" />,
|
||||
icon: true,
|
||||
},
|
||||
]}
|
||||
value={source}
|
||||
setValue={setSource}
|
||||
value={view}
|
||||
setValue={setView}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -1,10 +1,14 @@
|
||||
import { CompoundButton } from './CompoundButton.jsx'
|
||||
import { DatePicker } from './DatePicker.jsx'
|
||||
import { Icon } from './Icon.jsx'
|
||||
|
||||
export const Toolbar = ({
|
||||
source,
|
||||
setSource,
|
||||
onShowMenu,
|
||||
date,
|
||||
setDate,
|
||||
showMobileMenu,
|
||||
setShowMobileMenu,
|
||||
onHelp,
|
||||
theme,
|
||||
setTheme,
|
||||
@@ -12,8 +16,11 @@ export const Toolbar = ({
|
||||
return (
|
||||
<div class="toolbar">
|
||||
<div class="mobile">
|
||||
<button class="flat icon" onClick={onShowMenu}>
|
||||
<Icon name="menu" />
|
||||
<button
|
||||
class="flat icon"
|
||||
onClick={() => setShowMobileMenu(!showMobileMenu)}
|
||||
>
|
||||
<Icon name={`${showMobileMenu ? 'close' : 'menu'}`} />
|
||||
</button>
|
||||
</div>
|
||||
<div class="item logo">
|
||||
@@ -29,6 +36,7 @@ export const Toolbar = ({
|
||||
{ value: 'anno-3', label: 'III' },
|
||||
{ value: 'magistrale', label: 'Magistrale' },
|
||||
{ value: 'tutti', label: 'Tutti' },
|
||||
{ value: 'custom', label: 'Custom' },
|
||||
]}
|
||||
value={source}
|
||||
setValue={setSource}
|
||||
@@ -38,11 +46,16 @@ export const Toolbar = ({
|
||||
<CompoundButton
|
||||
options={[
|
||||
{ value: 'orario', label: 'Orario' },
|
||||
{ value: 'lista', label: 'Lista' },
|
||||
]}
|
||||
value={source}
|
||||
setValue={setSource}
|
||||
/>
|
||||
</div>
|
||||
<div class="empty"></div>
|
||||
<div class="item option">
|
||||
<DatePicker date={date} setDate={setDate} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="option-group">
|
||||
<div class="item option">
|
||||
|
||||
@@ -2,13 +2,29 @@ import { format } from 'date-fns'
|
||||
|
||||
import _ from 'lodash'
|
||||
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 }) => {
|
||||
const events = timetables[source]
|
||||
export const Courses = ({
|
||||
source,
|
||||
timetables,
|
||||
selection,
|
||||
setSelection,
|
||||
custom,
|
||||
isRestrictedList,
|
||||
}) => {
|
||||
const events = isRestrictedList ? timetables['tutti'] : timetables[source]
|
||||
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 =>
|
||||
_.uniq(events.flatMap(event => event.docenti))
|
||||
@@ -42,28 +58,53 @@ export const Courses = ({ source, timetables, selection, setSelection }) => {
|
||||
|
||||
return (
|
||||
<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">
|
||||
{Object.entries(eventsByCourse).map(([id, courseEvents]) => (
|
||||
<div
|
||||
class={
|
||||
'course' +
|
||||
(currentlyHovered === id ? ' highlight' : '') +
|
||||
(selectionSet.has(id) ? ' selected' : '')
|
||||
(currentlyHovered === id && !isRestrictedList
|
||||
? ' highlight'
|
||||
: '') +
|
||||
(selectionSet.has(id) && !isRestrictedList
|
||||
? ' selected'
|
||||
: '')
|
||||
}
|
||||
data-course-id={id}
|
||||
onClick={() => {
|
||||
if (!selectionSet.has(id)) setSelection([...selection, id])
|
||||
else setSelection(selection.filter(selId => selId !== id))
|
||||
if (isRestrictedList) return
|
||||
if (!selectionSet.has(id))
|
||||
setSelection([...selection, id])
|
||||
else
|
||||
setSelection(
|
||||
selection.filter(selId => selId !== id)
|
||||
)
|
||||
}}
|
||||
>
|
||||
<div class="title">{prettyCourseName(courseEvents[0].name)}</div>
|
||||
<div class="docenti">{profsPerCourse[id].join(', ')}</div>
|
||||
<div class="title">
|
||||
{prettyCourseName(courseEvents[0].name)}
|
||||
</div>
|
||||
<div class="docenti">
|
||||
{profsPerCourse[id].join(', ')}
|
||||
</div>
|
||||
<div class="events">
|
||||
{courseEvents.map(course => (
|
||||
<div>
|
||||
{WEEK_DAYS[course.start.getDay()]}{' '}
|
||||
{format(course.start, 'H:mm')}–
|
||||
{format(course.end, 'H:mm')} {course.aule.join(', ')}
|
||||
{WEEK_DAYS[course.day]} {course.start / 60}:
|
||||
{String(course.start % 60).padStart(2, '0')}
|
||||
–
|
||||
{course.end / 60}:
|
||||
{String(course.end % 60).padStart(2, '0')}{' '}
|
||||
{course.aule.join(', ')}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useEffect, useRef, useState } from 'preact/hooks'
|
||||
|
||||
import _ from 'lodash'
|
||||
import _, { parseInt } from 'lodash'
|
||||
import { differenceInMinutes, startOfDay } from 'date-fns'
|
||||
import { parse } from 'yaml'
|
||||
import { parseCustomEvents } from '../../utils.jsx'
|
||||
|
||||
import {
|
||||
WEEK_DAYS_SHORT,
|
||||
@@ -46,7 +48,7 @@ const TransposePopup = ({ onClose }) => {
|
||||
|
||||
const NoCourseWarning = () => {
|
||||
return (
|
||||
<div class="no-courses-warning">
|
||||
<div class="warning">
|
||||
<p>Non hai ancora selezionato nessun corso.</p>
|
||||
<p>
|
||||
Clicca sui corsi nelle altre visuali per selezionarli e
|
||||
@@ -57,7 +59,6 @@ const NoCourseWarning = () => {
|
||||
}
|
||||
|
||||
const Layout = ({ layout, day, colors }) => {
|
||||
console.log(layout)
|
||||
return (
|
||||
<>
|
||||
{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(
|
||||
'transpose_info',
|
||||
'false'
|
||||
@@ -233,14 +234,8 @@ export const Schedule = ({ timetables, selection, setSelection }) => {
|
||||
|
||||
const allEvents = timetables['tutti']
|
||||
const selectionSet = new Set(selection)
|
||||
const events = allEvents
|
||||
.filter(e => selectionSet.has(e.id))
|
||||
.map(e => ({
|
||||
...e,
|
||||
day: e.start.getDay(),
|
||||
start: differenceInMinutes(e.start, startOfDay(e.start)),
|
||||
end: differenceInMinutes(e.end, startOfDay(e.start)),
|
||||
}))
|
||||
const events = allEvents.filter(e => selectionSet.has(e.id))
|
||||
parseCustomEvents(custom).forEach(event => events.push(event))
|
||||
|
||||
const weekStart = Math.min(...events.map(e => e.start), 9 * 60) / 30
|
||||
const weekEnd = Math.max(...events.map(e => e.end), 18 * 60) / 30
|
||||
|
||||
+161
-80
@@ -1,6 +1,7 @@
|
||||
import _ from 'lodash'
|
||||
import { render } from 'preact'
|
||||
import { useEffect, useState } from 'preact/hooks'
|
||||
import { differenceInMinutes, startOfDay } from 'date-fns'
|
||||
|
||||
// import { ToolOverlay } from './components/ToolOverlay.jsx'
|
||||
//
|
||||
@@ -18,12 +19,8 @@ import { Icon } from './components/Icon.jsx'
|
||||
import { Popup } from './components/Popup.jsx'
|
||||
import { Toolbar } from './components/Toolbar.jsx'
|
||||
import { OptionBar } from './components/OptionBar.jsx'
|
||||
import {
|
||||
prettyAulaName,
|
||||
prettyProfName,
|
||||
clearOldPersistentStates,
|
||||
usePersistentState,
|
||||
} from './utils.jsx'
|
||||
import { prettyAulaName, prettyProfName, usePersistentState } from './utils.jsx'
|
||||
import { SettingsBar } from './components/SettingsBar.jsx'
|
||||
|
||||
// Che fanno queste due righe?
|
||||
window._ = _
|
||||
@@ -36,17 +33,17 @@ const TIMETABLE_IDS = {
|
||||
'magistrale': '64a7c7091ab813002c5d9ede',
|
||||
}
|
||||
|
||||
const DEFAULT_DATE_RANGE = {
|
||||
from: '2023-10-09T00:00:00.000Z',
|
||||
to: '2023-10-14T00:00:00.000Z',
|
||||
}
|
||||
// const DEFAULT_DATE_RANGE = {
|
||||
// from: '2023-10-09T00:00:00.000Z',
|
||||
// to: '2023-10-14T00:00:00.000Z',
|
||||
// }
|
||||
|
||||
const DATE_RANGES = {
|
||||
'64a7c1c651f079001d52e9c8': DEFAULT_DATE_RANGE,
|
||||
'6308e2dc09352a0208fefdd9': DEFAULT_DATE_RANGE,
|
||||
'6308e42a1df5cb026699ced4': DEFAULT_DATE_RANGE,
|
||||
'64a7c7091ab813002c5d9ede': DEFAULT_DATE_RANGE,
|
||||
}
|
||||
// const DATE_RANGES = {
|
||||
// '64a7c1c651f079001d52e9c8': DEFAULT_DATE_RANGE,
|
||||
// '6308e2dc09352a0208fefdd9': DEFAULT_DATE_RANGE,
|
||||
// '6308e42a1df5cb026699ced4': DEFAULT_DATE_RANGE,
|
||||
// '64a7c7091ab813002c5d9ede': DEFAULT_DATE_RANGE,
|
||||
// }
|
||||
|
||||
function specialEventPatches(eventi) {
|
||||
// Il laboratorio del primo anno in realtà è in due canali separati
|
||||
@@ -66,11 +63,14 @@ function specialEventPatches(eventi) {
|
||||
|
||||
function formatEvents(timetable) {
|
||||
return timetable.map(({ nome, dataInizio, dataFine, docenti, aule }) => {
|
||||
const start = new Date(dataInizio)
|
||||
const end = new Date(dataFine)
|
||||
return {
|
||||
id: nome,
|
||||
name: _.split(nome, '-', 1)[0].trim(),
|
||||
start: new Date(dataInizio),
|
||||
end: new Date(dataFine),
|
||||
day: start.getDay(),
|
||||
start: differenceInMinutes(start, startOfDay(start)),
|
||||
end: differenceInMinutes(end, startOfDay(start)),
|
||||
docenti: docenti.map(({ 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) {
|
||||
// Almost directly copy-pasted from Chrome Dev Tools
|
||||
const req = await fetch(
|
||||
@@ -97,8 +108,8 @@ async function loadCalendari() {
|
||||
linkCalendarioId: id,
|
||||
clienteId: '628de8b9b63679f193b87046',
|
||||
pianificazioneTemplate: false,
|
||||
dataInizio: DATE_RANGES[id].from,
|
||||
dataFine: DATE_RANGES[id].to,
|
||||
dataInizio: monday.toISOString(),
|
||||
dataFine: saturday.toISOString(),
|
||||
}),
|
||||
method: 'POST',
|
||||
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 = ({}) => {
|
||||
// Clear persistent states unless state_token corresponds to the one passed
|
||||
// as the argument. Useful with breaking updates. Change this token if your
|
||||
// (breaking) update needs a reset of persistent states to avoid crashes.
|
||||
//
|
||||
// Use any random string of your choice
|
||||
clearOldPersistentStates('e73cba02')
|
||||
// clearOldPersistentStates('e73cba02')
|
||||
|
||||
const [date, setDate] = useState(new Date().toISOString())
|
||||
|
||||
// Data Sources
|
||||
const [source, setSource] = usePersistentState('source', 'magistrale')
|
||||
const [view, setView] = usePersistentState('view', 'magistrale')
|
||||
const [timetables, setTimetables] = useState(null)
|
||||
useEffect(async () => {
|
||||
setTimetables(await loadCalendari())
|
||||
}, [])
|
||||
|
||||
// View Modes
|
||||
// const [mode, setMode] = usePersistentState('orario.mode', MODE_COURSES)
|
||||
setTimetables(await loadCalendari(new Date(date)))
|
||||
}, [date])
|
||||
|
||||
// Selection
|
||||
const [selectedCourses, setSelectedCourses] = usePersistentState(
|
||||
@@ -154,6 +220,9 @@ const App = ({}) => {
|
||||
[]
|
||||
)
|
||||
|
||||
// Custom Events
|
||||
const [custom, setCustom] = usePersistentState('custom', [])
|
||||
|
||||
// Menus
|
||||
const [helpVisible, setHelpVisible] = useState(false)
|
||||
const [showMobileMenu, setShowMobileMenu] = useState(false)
|
||||
@@ -175,69 +244,81 @@ const App = ({}) => {
|
||||
<>
|
||||
<Toolbar
|
||||
{...{
|
||||
source,
|
||||
setSource,
|
||||
onShowMenu: () => setShowMobileMenu(true),
|
||||
source: view,
|
||||
setSource: setView,
|
||||
date: date,
|
||||
setDate: setDate,
|
||||
showMobileMenu: showMobileMenu,
|
||||
setShowMobileMenu: setShowMobileMenu,
|
||||
onHelp: () => setHelpVisible(true),
|
||||
theme,
|
||||
setTheme,
|
||||
}}
|
||||
/>
|
||||
<OptionBar
|
||||
{...{
|
||||
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
|
||||
{showMobileMenu ? (
|
||||
<SettingsBar
|
||||
{...{
|
||||
theme,
|
||||
setTheme,
|
||||
onClose: () => {
|
||||
setShowMobileMenu(false)
|
||||
},
|
||||
date,
|
||||
setDate,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<OptionBar
|
||||
{...{
|
||||
view: view,
|
||||
setView: setView,
|
||||
onHelp: () => setHelpVisible(true),
|
||||
}}
|
||||
orizzontale
|
||||
/>
|
||||
)}
|
||||
|
||||
<div class="content">
|
||||
{timetables &&
|
||||
(showMobileMenu ? (
|
||||
<HamburgerMenu
|
||||
{...{
|
||||
date,
|
||||
setDate,
|
||||
theme,
|
||||
setTheme,
|
||||
onClose: () => {
|
||||
setShowMobileMenu(false)
|
||||
},
|
||||
}}
|
||||
/>
|
||||
) : timetables['tutti'].length === 0 ? (
|
||||
<div class="warning">
|
||||
<p>
|
||||
Non esistono corsi per la settimana selezionata:
|
||||
buone vacanze! 🎉
|
||||
</p>
|
||||
<p>
|
||||
Per cambiare settimana puoi usare il widget
|
||||
Calendario (
|
||||
<Icon name="calendar_month" />) in alto a destra
|
||||
<br />
|
||||
In versione mobile, il widget Calendario è
|
||||
situato dentro il Menu (
|
||||
<Icon name="menu" />)
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<View
|
||||
selection={selectedCourses}
|
||||
setSelection={setSelectedCourses}
|
||||
view={view}
|
||||
timetables={timetables}
|
||||
custom={custom}
|
||||
setCustom={setCustom}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* showMobileMenu && (
|
||||
) */}
|
||||
{helpVisible && (
|
||||
<Popup
|
||||
title={
|
||||
|
||||
+167
-83
@@ -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
|
||||
|
||||
.panel {
|
||||
@@ -444,7 +485,8 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
.option-bar {
|
||||
.option-bar,
|
||||
.settings-bar {
|
||||
@extend .panel;
|
||||
|
||||
padding: 0.5rem;
|
||||
@@ -469,30 +511,60 @@ body {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.option-group {
|
||||
.option-group,
|
||||
.settings-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
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 {
|
||||
height: calc(100vh - 4rem);
|
||||
height: calc(100dvh - 4rem);
|
||||
@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 {
|
||||
padding: 1rem;
|
||||
padding: 0rem 1rem;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
|
||||
overflow-y: scroll;
|
||||
text-align: center;
|
||||
|
||||
gap: 1rem;
|
||||
|
||||
.wrap-container {
|
||||
display: grid;
|
||||
@@ -550,30 +622,18 @@ body {
|
||||
}
|
||||
|
||||
.schedule-view {
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
width: 100%;
|
||||
max-width: 57rem;
|
||||
max-width: 55rem;
|
||||
margin: auto;
|
||||
|
||||
padding: 1rem 0.5rem;
|
||||
padding: 0rem 0.5rem;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
|
||||
.no-courses-warning {
|
||||
width: 100%;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
|
||||
p {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
.schedule-card {
|
||||
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 {
|
||||
@@ -808,67 +953,6 @@ body {
|
||||
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
|
||||
|
||||
@media screen and (min-width: $device-s-width) and (pointer: fine) {
|
||||
|
||||
+60
-11
@@ -1,6 +1,7 @@
|
||||
// Calendar
|
||||
|
||||
import { useEffect, useState } from 'preact/hooks'
|
||||
import { parse } from 'yaml'
|
||||
|
||||
// Calendar
|
||||
|
||||
export const WEEK_DAYS = [
|
||||
'Domenica',
|
||||
@@ -12,15 +13,7 @@ export const WEEK_DAYS = [
|
||||
'Sabato',
|
||||
]
|
||||
|
||||
export const WEEK_DAYS_SHORT = [
|
||||
'Dom',
|
||||
'Lun',
|
||||
'Mar',
|
||||
'Mer',
|
||||
'Gio',
|
||||
'Ven',
|
||||
'Sab'
|
||||
]
|
||||
export const WEEK_DAYS_SHORT = ['Dom', 'Lun', 'Mar', 'Mer', 'Gio', 'Ven', 'Sab']
|
||||
|
||||
// Hashing
|
||||
|
||||
@@ -77,6 +70,62 @@ export function prettyProfName(nome, cognome) {
|
||||
.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
|
||||
|
||||
export const withClasses = classes =>
|
||||
|
||||
Reference in New Issue
Block a user