Minimal working prototype, no mobile, no printing

dev
Antonio De Lucreziis 2 years ago
parent 5d4c303b4a
commit c42ccc58dc

@ -0,0 +1,81 @@
[
"ANALISI ARMONICA - ANALISI ARMONICA/a",
"ANALISI MATEMATICA 3",
"ASPETTI MATEMATICI NELLA COMPUTAZIONE QUANTISTICA",
"CALCOLO SCIENTIFICO",
"COMBINATORIA ALGEBRICA",
"DETERMINAZIONE ORBITALE",
"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 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",
"GEOMETRIA E TOPOLOGIA DIFFERENZIALE",
"GEOMETRIA E TOPOLOGIA DIFFERENZIALE",
"GEOMETRIA E TOPOLOGIA DIFFERENZIALE",
"GEOMETRIA E TOPOLOGIA DIFFERENZIALE",
"GEOMETRIA E TOPOLOGIA DIFFERENZIALE",
"GEOMETRIA E TOPOLOGIA DIFFERENZIALE",
"ISTITUZIONI DI ALGEBRA",
"ISTITUZIONI DI ALGEBRA",
"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 FISICA MATEMATICA",
"ISTITUZIONI DI FISICA MATEMATICA",
"LABORATORIO COMPUTAZIONALE",
"LOGICA MATEMATICA",
"LOGICA MATEMATICA",
"LOGICA MATEMATICA",
"LOGICA MATEMATICA",
"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 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À",
"RICERCA OPERATIVA",
"RICERCA OPERATIVA",
"RICERCA OPERATIVA",
"RICERCA OPERATIVA",
"RICERCA OPERATIVA",
"RICERCA OPERATIVA",
"SISTEMI DINAMICI",
"SISTEMI DINAMICI",
"SISTEMI DINAMICI",
"SISTEMI DINAMICI",
"STORIA DELLA MATEMATICA",
"STORIA DELLA MATEMATICA",
"STORIA DELLA MATEMATICA",
"STORIA DELLA MATEMATICA",
"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 E METODI DELL'OTTIMIZZAZIONE",
"TEORIA E METODI DELL'OTTIMIZZAZIONE",
"TOPOLOGIA DIFFERENZIALE - TOPOLOGIA DIFFERENZIALE/a",
"TOPOLOGIA DIFFERENZIALE - TOPOLOGIA DIFFERENZIALE/a"
]

@ -0,0 +1,25 @@
// 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 }) => {}

@ -0,0 +1,19 @@
import { useRef } from 'preact/hooks'
import { Icon } from './Icon.jsx'
export const ComboBox = ({ selected, options }) => {
const selectRef = useRef()
return (
<div class="input-combo" onClick={() => selectRef.current?.click()}>
<select ref={selectRef}>
{options.map(({ label, value }) => (
<option value={value} selected={value === selected}>
{label}
</option>
))}
</select>
<Icon name="expand_more" />
</div>
)
}

@ -3,7 +3,7 @@ import { Icon } from './Icon.jsx'
export const ToolOverlay = ({ visibility, toggleVisibility, onClose }) => ( export const ToolOverlay = ({ visibility, toggleVisibility, onClose }) => (
<div class="overlay"> <div class="overlay">
<button class="icon primary" onClick={toggleVisibility}> <button class="icon primary" onClick={toggleVisibility}>
<Icon name={visibility ? 'visibility' : 'visibility_off'} /> <Icon name={visibility ? 'visibility_off' : 'visibility'} />
</button> </button>
<button class="icon" onClick={onClose}> <button class="icon" onClick={onClose}>
<Icon name="close" /> <Icon name="close" />

@ -2,7 +2,7 @@
// Modes // Modes
// //
import { differenceInMinutes, format, getWeek, startOfDay } from 'date-fns' import { differenceInMinutes, format, startOfDay } from 'date-fns'
import { it } from 'date-fns/locale' import { it } from 'date-fns/locale'
import _ from 'lodash' import _ from 'lodash'
@ -22,6 +22,23 @@ function hashString(str, seed = 0) {
return 4294967296 * (2097151 & h2) + (h1 >>> 0) 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 WorkWeekView = ({ events }) => {
const eventsByWeekday = _.groupBy(events, event => event.start.getDay()) const eventsByWeekday = _.groupBy(events, event => event.start.getDay())
@ -105,10 +122,7 @@ const WorkWeekView = ({ events }) => {
}} }}
ref={eventRef} ref={eventRef}
> >
{_.startCase(_.lowerCase(event.data.name)) {normalizeCourseName(event.data.name)}
.replaceAll('Ii', 'II')
.replaceAll('Iii', 'III')
.replaceAll(/\bE\b/g, 'e')}
</div> </div>
) )
} }
@ -159,9 +173,11 @@ const WorkWeekVerticalView = ({ events, selection, setSelection, hideOtherCourse
} }
element.current.addEventListener('mousemove', l) element.current.addEventListener('mousemove', l)
element.current.addEventListener('mouseleave', l)
return () => { return () => {
element.current.removeEventListener('mousemove', l) element.current.removeEventListener('mousemove', l)
element.current.removeEventListener('mouseleave', l)
} }
} }
}, [element.current]) }, [element.current])
@ -246,10 +262,7 @@ const WorkWeekVerticalView = ({ events, selection, setSelection, hideOtherCourse
}} }}
> >
<div class="title"> <div class="title">
{_.startCase(_.lowerCase(event.data.name)) {normalizeCourseName(event.data.name)}
.replaceAll('Ii', 'II')
.replaceAll('Iii', 'III')
.replaceAll(/\bE\b/g, 'e')}
</div> </div>
<div class="aula">{event.data.aula}</div> <div class="aula">{event.data.aula}</div>
</div> </div>
@ -274,10 +287,9 @@ const WorkWeekVerticalView = ({ events, selection, setSelection, hideOtherCourse
const CourseView = ({ events, selection, setSelection, hideOtherCourses }) => { const CourseView = ({ events, selection, setSelection, hideOtherCourses }) => {
const selectionSet = new Set(selection) const selectionSet = new Set(selection)
const eventsByCourse = _.groupBy( const visibleEvents = !hideOtherCourses ? events : events.filter(e => selectionSet.has(e.name))
!hideOtherCourses ? events : events.filter(e => selectionSet.has(e.name)),
'name' const eventsByCourse = _.groupBy(_.sortBy(visibleEvents, 'name'), 'name')
)
const [currentlyHovered, setCurrentlyHovered] = useState(null) const [currentlyHovered, setCurrentlyHovered] = useState(null)
const element = useRef() const element = useRef()
@ -294,9 +306,11 @@ const CourseView = ({ events, selection, setSelection, hideOtherCourses }) => {
} }
element.current.addEventListener('mousemove', l) element.current.addEventListener('mousemove', l)
element.current.addEventListener('mouseleave', l)
return () => { return () => {
element.current.removeEventListener('mousemove', l) element.current.removeEventListener('mousemove', l)
element.current.removeEventListener('mouseleave', l)
} }
} }
}, [element.current]) }, [element.current])
@ -317,12 +331,7 @@ const CourseView = ({ events, selection, setSelection, hideOtherCourses }) => {
else setSelection(selection.filter(n => n !== name)) else setSelection(selection.filter(n => n !== name))
}} }}
> >
<div class="title"> <div class="title">{normalizeCourseName(name)}</div>
{_.startCase(_.lowerCase(name))
.replaceAll('Ii', 'II')
.replaceAll('Iii', 'III')
.replaceAll(/\bE\b/g, 'e')}
</div>
<div class="docenti">{courseEvents[0].docenti.join(', ')}</div> <div class="docenti">{courseEvents[0].docenti.join(', ')}</div>
<div class="events"> <div class="events">
{courseEvents.map(course => ( {courseEvents.map(course => (

@ -0,0 +1,25 @@
import { ComboBox } from './ComboBox.jsx'
import { Icon } from './Icon.jsx'
export const HamburgerMenu = ({ onClose }) => {
return (
<div class="menu">
<div class="header">
<button class="flat icon" onClick={onClose}>
<Icon name="close" />
</button>
<div class="item logo">PHC / Orari</div>
</div>
<div class="options">
<ComboBox
selected={1}
options={[
{ label: 'Prova 1', value: 1 },
{ label: 'Prova 2', value: 2 },
{ label: 'Prova 3', value: 3 },
]}
/>
</div>
</div>
)
}

@ -1,10 +1,50 @@
import { Icon } from './Icon.jsx' import { Icon } from './Icon.jsx'
export const Toolbar = ({ mode, setMode }) => { export const Toolbar = ({ mode, setMode, source, setSource, onShowMenu }) => {
return ( return (
<div class="toolbar"> <div class="toolbar">
<div class="mobile">
<button class="flat icon" onClick={onShowMenu}>
<Icon name="menu" />
</button>
</div>
<div class="item logo">PHC / Orari</div> <div class="item logo">PHC / Orari</div>
<div class="options"> <div class="option-group">
<div class="item option">
<div class="compound">
<button
class={'radio' + (source === 'anno-1' ? ' selected' : '')}
onClick={() => setSource('anno-1')}
>
I
</button>
<button
class={'radio' + (source === 'anno-2' ? ' selected' : '')}
onClick={() => setSource('anno-2')}
>
II
</button>
<button
class={'radio' + (source === 'anno-3' ? ' selected' : '')}
onClick={() => setSource('anno-3')}
>
III
</button>
<button
class={'radio' + (source === 'magistrale' ? ' selected' : '')}
onClick={() => setSource('magistrale')}
>
Magistrale
</button>
{/* TODO: Aggiungere il pulsante per "tutti" */}
{/* <button
class={'radio' + (source === 'tutti' ? ' selected' : '')}
onClick={() => setSource('tutti')}
>
Tutti
</button> */}
</div>
</div>
<div class="item option"> <div class="item option">
<div class="compound"> <div class="compound">
<button <button
@ -27,6 +67,8 @@ export const Toolbar = ({ mode, setMode }) => {
</button> </button>
</div> </div>
</div> </div>
</div>
<div class="option-group">
<div class="item option"> <div class="item option">
<button class="icon"> <button class="icon">
<Icon name="print" /> <Icon name="print" />

@ -4,29 +4,57 @@ 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 } from './components/EventsView.jsx'
import { HamburgerMenu } from './components/HamburgerMenu.jsx'
import { Toolbar } from './components/Toolbar.jsx' import { Toolbar } from './components/Toolbar.jsx'
window._ = _
window.dataBuffer = {}
const CALENDAR_IDS = {
'anno-1': '6308cfcb1df5cb026699ce32',
'anno-2': '6308e2dc09352a0208fefdd9',
'anno-3': '6308e42a1df5cb026699ced4',
'magistrale': '6308e8ea0c34e703bb1f7e85',
}
const App = ({}) => { const App = ({}) => {
const [source, setSource] = useState('magistrale')
const [eventi, setEventi] = useState([]) const [eventi, setEventi] = useState([])
const [selectedCourses, setSelectedCourses] = useState([])
useEffect(() => {
setSelectedCourses([])
}, [source])
useEffect(() => { useEffect(() => {
// Directly copy-pasted from chrome Dev Tools // Directly copy-pasted from chrome Dev Tools
// const request = fetch( const request = fetch(
// 'https://apache.prod.up.cineca.it/api/Impegni/getImpegniCalendarioPubblico', 'https://apache.prod.up.cineca.it/api/Impegni/getImpegniCalendarioPubblico',
// { {
// headers: { headers: {
// 'content-type': 'application/json;charset=UTF-8', 'content-type': 'application/json;charset=UTF-8',
// 'sec-fetch-dest': 'empty', 'sec-fetch-dest': 'empty',
// 'sec-fetch-mode': 'cors', 'sec-fetch-mode': 'cors',
// 'sec-fetch-site': 'same-site', 'sec-fetch-site': 'same-site',
// }, },
// body: '{"mostraImpegniAnnullati":true,"mostraIndisponibilitaTotali":false,"linkCalendarioId":"6308e8ea0c34e703bb1f7e85","clienteId":"628de8b9b63679f193b87046","pianificazioneTemplate":false,"dataInizio":"2022-10-02T22:00:00.000Z","dataFine":"2022-10-07T22:00:00.000Z"}', body: JSON.stringify({
// method: 'POST', mostraImpegniAnnullati: true,
// mode: 'cors', mostraIndisponibilitaTotali: false,
// credentials: 'omit', linkCalendarioId: CALENDAR_IDS[source],
// } clienteId: '628de8b9b63679f193b87046',
// ) pianificazioneTemplate: false,
dataInizio: '2022-10-02T22:00:00.000Z',
const request = fetch('/data.json') dataFine: '2022-10-07T22:00:00.000Z',
}),
method: 'POST',
mode: 'cors',
credentials: 'omit',
}
)
// const request = fetch('/data.json')
console.time('loading eventi') console.time('loading eventi')
@ -34,28 +62,33 @@ const App = ({}) => {
.then(res => res.json()) .then(res => res.json())
.then(data => { .then(data => {
console.timeEnd('loading eventi') console.timeEnd('loading eventi')
window.data = data window.dataBuffer[source] = data
setEventi(data) setEventi(data)
}) })
}, []) }, [source])
const [mode, setMode] = useState('course') const [mode, setMode] = useState('course')
const [hideOtherCourses, setHideOtherCourses] = useState(false) const [hideOtherCourses, setHideOtherCourses] = useState(false)
console.log(hideOtherCourses) // TODO: Should wrap in "useEffect"?
const [selectedCourses, setSelectedCourses] = useState([])
useEffect(() => {
if (selectedCourses.length === 0) { if (selectedCourses.length === 0) {
setHideOtherCourses(false) setHideOtherCourses(false)
} }
}, [selectedCourses])
const [showMobileMenu, setShowMobileMenu] = useState(true)
return ( return (
<> <>
<Toolbar {...{ mode, setMode }} /> <Toolbar
{...{
mode,
setMode,
source,
setSource,
onShowMenu: () => setShowMobileMenu(true),
}}
/>
<EventsView <EventsView
mode={mode} mode={mode}
selection={selectedCourses} selection={selectedCourses}
@ -85,6 +118,13 @@ const App = ({}) => {
}} }}
/> />
)} )}
{showMobileMenu && (
<HamburgerMenu
onClose={() => {
setShowMobileMenu(false)
}}
/>
)}
</> </>
) )
} }

@ -47,7 +47,8 @@ code {
} }
sup { sup {
padding-bottom: 0.5rem; display: inline-block;
transform: translate(0, -0.25rem);
} }
button, button,
@ -95,6 +96,38 @@ button,
} }
} }
.input-combo {
display: flex;
align-items: center;
justify-content: center;
height: 3rem;
background: var(--gray-000);
border: 2px solid var(--gray-350);
border-radius: 0.5rem;
font-weight: 400;
padding: 0.5rem 0.5rem 0.5rem 1rem;
user-select: none;
cursor: pointer;
position: relative;
gap: 0.5rem;
select {
border: none;
background: none;
appearance: none;
font-family: inherit;
font-size: inherit;
}
}
// Components // Components
.compound { .compound {
@ -108,6 +141,8 @@ button,
padding-left: 0.75rem; padding-left: 0.75rem;
padding-right: 0.75rem; padding-right: 0.75rem;
height: 2.5;
&:first-child { &:first-child {
padding-left: 1rem; padding-left: 1rem;
@ -160,6 +195,8 @@ body {
.toolbar { .toolbar {
@extend .panel; @extend .panel;
padding: 1rem 0.5rem 1rem 1rem;
border-radius: 0; border-radius: 0;
border: none; border: none;
border-bottom: 1px solid var(--gray-500); border-bottom: 1px solid var(--gray-500);
@ -170,11 +207,11 @@ body {
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
.options { .option-group {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 1rem;
@media screen and (max-width: $device-s-width) { @media screen and (max-width: $device-s-width) {
display: none; display: none;
@ -197,11 +234,12 @@ body {
overflow-y: scroll; overflow-y: scroll;
.wrap-container { .wrap-container {
display: flex; display: grid;
flex-wrap: wrap; grid-template-columns: repeat(auto-fill, minmax(30ch, 1fr));
gap: 1rem; gap: 1rem;
width: 100%;
max-width: 1200px; max-width: 1200px;
.course { .course {
@ -414,6 +452,8 @@ body {
overflow: scroll; overflow: scroll;
--event-height: 4.25rem;
.pivot { .pivot {
height: 3rem; height: 3rem;
@ -452,7 +492,7 @@ body {
.blocks { .blocks {
display: grid; display: grid;
grid-template-rows: repeat(12, 6rem); grid-template-rows: repeat(12, calc(var(--event-height) + 1rem));
.block { .block {
grid-row: var(--start) / span var(--size); grid-row: var(--start) / span var(--size);
@ -472,7 +512,7 @@ body {
} }
&:not(.skip-border) { &:not(.skip-border) {
height: calc(2 * 6rem + 1px); height: calc(2 * (var(--event-height) + 1rem) + 1px);
border-bottom: 1px solid var(--gray-500); border-bottom: 1px solid var(--gray-500);
} }
} }
@ -516,7 +556,7 @@ body {
display: grid; display: grid;
grid-template-columns: repeat(var(--size), auto); grid-template-columns: repeat(var(--size), auto);
grid-template-rows: repeat(12, 5rem); grid-template-rows: repeat(12, var(--event-height));
padding: 0.5rem; padding: 0.5rem;
@ -613,6 +653,83 @@ body {
box-shadow: 0 0.25rem 0.75rem #00000033; box-shadow: 0 0.25rem 0.75rem #00000033;
} }
animation: fade-in 150ms ease-in forwards;
}
// not on mobile
@media screen and (min-width: $device-s-width) {
.mobile {
display: none;
}
.menu {
display: none;
}
}
// on mobile
@media screen and (max-width: $device-s-width) {
.toolbar {
padding: 0.75rem 1rem 0.75rem 0.75rem;
}
button {
&.flat {
border: none;
}
}
.menu {
position: absolute;
inset: 0;
background: var(--gray-000);
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(--gray-500);
}
.options {
padding: 1rem;
display: flex;
flex-direction: column;
align-items: center;
}
}
}
@keyframes fade-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.fade-out {
animation: fade-out 150ms ease-in forwards;
}
@keyframes fade-out {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
} }
// Utilities // Utilities

Loading…
Cancel
Save