Minimal working prototype, no mobile, no printing

This commit is contained in:
2022-09-08 18:57:48 +02:00
parent 5d4c303b4a
commit c42ccc58dc
9 changed files with 415 additions and 57 deletions
+25
View File
@@ -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 }) => {}
+19
View File
@@ -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>
)
}
+1 -1
View File
@@ -3,7 +3,7 @@ import { Icon } from './Icon.jsx'
export const ToolOverlay = ({ visibility, toggleVisibility, onClose }) => (
<div class="overlay">
<button class="icon primary" onClick={toggleVisibility}>
<Icon name={visibility ? 'visibility' : 'visibility_off'} />
<Icon name={visibility ? 'visibility_off' : 'visibility'} />
</button>
<button class="icon" onClick={onClose}>
<Icon name="close" />
+28 -19
View File
@@ -2,7 +2,7 @@
// Modes
//
import { differenceInMinutes, format, getWeek, startOfDay } from 'date-fns'
import { differenceInMinutes, format, startOfDay } from 'date-fns'
import { it } from 'date-fns/locale'
import _ from 'lodash'
@@ -22,6 +22,23 @@ function hashString(str, seed = 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 eventsByWeekday = _.groupBy(events, event => event.start.getDay())
@@ -105,10 +122,7 @@ const WorkWeekView = ({ events }) => {
}}
ref={eventRef}
>
{_.startCase(_.lowerCase(event.data.name))
.replaceAll('Ii', 'II')
.replaceAll('Iii', 'III')
.replaceAll(/\bE\b/g, 'e')}
{normalizeCourseName(event.data.name)}
</div>
)
}
@@ -159,9 +173,11 @@ const WorkWeekVerticalView = ({ events, selection, setSelection, hideOtherCourse
}
element.current.addEventListener('mousemove', l)
element.current.addEventListener('mouseleave', l)
return () => {
element.current.removeEventListener('mousemove', l)
element.current.removeEventListener('mouseleave', l)
}
}
}, [element.current])
@@ -246,10 +262,7 @@ const WorkWeekVerticalView = ({ events, selection, setSelection, hideOtherCourse
}}
>
<div class="title">
{_.startCase(_.lowerCase(event.data.name))
.replaceAll('Ii', 'II')
.replaceAll('Iii', 'III')
.replaceAll(/\bE\b/g, 'e')}
{normalizeCourseName(event.data.name)}
</div>
<div class="aula">{event.data.aula}</div>
</div>
@@ -274,10 +287,9 @@ const WorkWeekVerticalView = ({ events, selection, setSelection, hideOtherCourse
const CourseView = ({ events, selection, setSelection, hideOtherCourses }) => {
const selectionSet = new Set(selection)
const eventsByCourse = _.groupBy(
!hideOtherCourses ? events : events.filter(e => selectionSet.has(e.name)),
'name'
)
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()
@@ -294,9 +306,11 @@ const CourseView = ({ events, selection, setSelection, hideOtherCourses }) => {
}
element.current.addEventListener('mousemove', l)
element.current.addEventListener('mouseleave', l)
return () => {
element.current.removeEventListener('mousemove', l)
element.current.removeEventListener('mouseleave', l)
}
}
}, [element.current])
@@ -317,12 +331,7 @@ const CourseView = ({ events, selection, setSelection, hideOtherCourses }) => {
else setSelection(selection.filter(n => n !== name))
}}
>
<div class="title">
{_.startCase(_.lowerCase(name))
.replaceAll('Ii', 'II')
.replaceAll('Iii', 'III')
.replaceAll(/\bE\b/g, 'e')}
</div>
<div class="title">{normalizeCourseName(name)}</div>
<div class="docenti">{courseEvents[0].docenti.join(', ')}</div>
<div class="events">
{courseEvents.map(course => (
+25
View File
@@ -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>
)
}
+44 -2
View File
@@ -1,10 +1,50 @@
import { Icon } from './Icon.jsx'
export const Toolbar = ({ mode, setMode }) => {
export const Toolbar = ({ mode, setMode, source, setSource, onShowMenu }) => {
return (
<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="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="compound">
<button
@@ -27,6 +67,8 @@ export const Toolbar = ({ mode, setMode }) => {
</button>
</div>
</div>
</div>
<div class="option-group">
<div class="item option">
<button class="icon">
<Icon name="print" />
+67 -27
View File
@@ -4,29 +4,57 @@ import { useEffect, useState } from 'preact/hooks'
import { ToolOverlay } from './components/CourseVisibility.jsx'
import { EventsView } from './components/EventsView.jsx'
import { HamburgerMenu } from './components/HamburgerMenu.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 [source, setSource] = useState('magistrale')
const [eventi, setEventi] = useState([])
const [selectedCourses, setSelectedCourses] = useState([])
useEffect(() => {
setSelectedCourses([])
}, [source])
useEffect(() => {
// Directly copy-pasted from chrome Dev Tools
// const request = fetch(
// 'https://apache.prod.up.cineca.it/api/Impegni/getImpegniCalendarioPubblico',
// {
// headers: {
// 'content-type': 'application/json;charset=UTF-8',
// 'sec-fetch-dest': 'empty',
// 'sec-fetch-mode': 'cors',
// '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"}',
// method: 'POST',
// mode: 'cors',
// credentials: 'omit',
// }
// )
const request = fetch(
'https://apache.prod.up.cineca.it/api/Impegni/getImpegniCalendarioPubblico',
{
headers: {
'content-type': 'application/json;charset=UTF-8',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-site',
},
body: JSON.stringify({
mostraImpegniAnnullati: true,
mostraIndisponibilitaTotali: false,
linkCalendarioId: CALENDAR_IDS[source],
clienteId: '628de8b9b63679f193b87046',
pianificazioneTemplate: false,
dataInizio: '2022-10-02T22:00:00.000Z',
dataFine: '2022-10-07T22:00:00.000Z',
}),
method: 'POST',
mode: 'cors',
credentials: 'omit',
}
)
const request = fetch('/data.json')
// const request = fetch('/data.json')
console.time('loading eventi')
@@ -34,28 +62,33 @@ const App = ({}) => {
.then(res => res.json())
.then(data => {
console.timeEnd('loading eventi')
window.data = data
window.dataBuffer[source] = data
setEventi(data)
})
}, [])
}, [source])
const [mode, setMode] = useState('course')
const [hideOtherCourses, setHideOtherCourses] = useState(false)
console.log(hideOtherCourses)
// TODO: Should wrap in "useEffect"?
if (selectedCourses.length === 0) {
setHideOtherCourses(false)
}
const [selectedCourses, setSelectedCourses] = useState([])
useEffect(() => {
if (selectedCourses.length === 0) {
setHideOtherCourses(false)
}
}, [selectedCourses])
const [showMobileMenu, setShowMobileMenu] = useState(true)
return (
<>
<Toolbar {...{ mode, setMode }} />
<Toolbar
{...{
mode,
setMode,
source,
setSource,
onShowMenu: () => setShowMobileMenu(true),
}}
/>
<EventsView
mode={mode}
selection={selectedCourses}
@@ -85,6 +118,13 @@ const App = ({}) => {
}}
/>
)}
{showMobileMenu && (
<HamburgerMenu
onClose={() => {
setShowMobileMenu(false)
}}
/>
)}
</>
)
}
+125 -8
View File
@@ -47,7 +47,8 @@ code {
}
sup {
padding-bottom: 0.5rem;
display: inline-block;
transform: translate(0, -0.25rem);
}
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
.compound {
@@ -108,6 +141,8 @@ button,
padding-left: 0.75rem;
padding-right: 0.75rem;
height: 2.5;
&:first-child {
padding-left: 1rem;
@@ -160,6 +195,8 @@ body {
.toolbar {
@extend .panel;
padding: 1rem 0.5rem 1rem 1rem;
border-radius: 0;
border: none;
border-bottom: 1px solid var(--gray-500);
@@ -170,11 +207,11 @@ body {
align-items: center;
justify-content: space-between;
.options {
.option-group {
display: flex;
align-items: center;
gap: 0.5rem;
gap: 1rem;
@media screen and (max-width: $device-s-width) {
display: none;
@@ -197,11 +234,12 @@ body {
overflow-y: scroll;
.wrap-container {
display: flex;
flex-wrap: wrap;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(30ch, 1fr));
gap: 1rem;
width: 100%;
max-width: 1200px;
.course {
@@ -414,6 +452,8 @@ body {
overflow: scroll;
--event-height: 4.25rem;
.pivot {
height: 3rem;
@@ -452,7 +492,7 @@ body {
.blocks {
display: grid;
grid-template-rows: repeat(12, 6rem);
grid-template-rows: repeat(12, calc(var(--event-height) + 1rem));
.block {
grid-row: var(--start) / span var(--size);
@@ -472,7 +512,7 @@ body {
}
&:not(.skip-border) {
height: calc(2 * 6rem + 1px);
height: calc(2 * (var(--event-height) + 1rem) + 1px);
border-bottom: 1px solid var(--gray-500);
}
}
@@ -516,7 +556,7 @@ body {
display: grid;
grid-template-columns: repeat(var(--size), auto);
grid-template-rows: repeat(12, 5rem);
grid-template-rows: repeat(12, var(--event-height));
padding: 0.5rem;
@@ -613,6 +653,83 @@ body {
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