Refactor delle view-modes, readme and better dockerfile

This commit is contained in:
2022-09-11 16:18:45 +02:00
parent 23c1510455
commit 01fa4b16e2
14 changed files with 449 additions and 62303 deletions
-25
View File
@@ -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 }) => {}
+3 -13
View File
@@ -1,28 +1,18 @@
import { useRef, useState } from 'preact/hooks'
import { Icon } from './Icon.jsx'
let ids = 0
function generateId() {
return 'combo-id-' + ids++
}
export const ComboBox = ({ options, value, setValue }) => {
const [uid] = useState(() => generateId())
const selectRef = useRef()
return (
<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 => (
<option value={option.value} selected={option.value === value}>
{option.label}
</option>
))}
</select>
<label class="icon" for={uid}>
<div class="icon">
<Icon name="expand_more" />
</label>
</div>
</div>
)
}
+9 -362
View File
@@ -1,368 +1,15 @@
//
// Modes
//
import { Course } from './view/Course.jsx'
import { WorkWeek } from './view/WorkWeek.jsx'
import { WorkWeekTranspose } from './view/WorkWeekTranspose.jsx'
import { differenceInMinutes, format, startOfDay } from 'date-fns'
import _ from 'lodash'
import { useEffect, useRef, useState } from 'preact/hooks'
import { layoutIntervals } from '../interval-layout.js'
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 &ndash; 11:00
</div>
<div class="label" style={{ 'grid-column': '4 / span 2' }}>
11:00 &ndash; 13:00
</div>
<div class="label" style={{ 'grid-column': '7 / span 2' }}>
14:00 &ndash; 16:00
</div>
<div class="label" style={{ 'grid-column': '9 / span 2' }}>
16:00 &ndash; 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 &ndash; 11:00
</div>
<div
class="block"
style={{
'--start': 11 - 7,
'--size': 2,
}}
>
11:00 &ndash; 13:00
</div>
<div
class="block skip-border"
style={{
'--start': 14 - 7,
'--size': 2,
}}
>
14:00 &ndash; 16:00
</div>
<div
class="block"
style={{
'--start': 16 - 7,
'--size': 2,
}}
>
16:00 &ndash; 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')} &ndash;{' '}
{format(course.end, 'H:mm')} {course.aula}
</div>
))}
</div>
</div>
))}
</div>
</div>
)
}
//
// EventsView
//
export const MODE_COURSE = 'course'
export const MODE_WORKWEEK = 'work-week'
export const MODE_WORKWEEK_TRANSPOSE = 'work-week-transpose'
const viewModeMap = {
'work-week-h': WorkWeekView,
'work-week-v': WorkWeekVerticalView,
'course': CourseView,
[MODE_COURSE]: Course,
[MODE_WORKWEEK]: WorkWeek,
[MODE_WORKWEEK_TRANSPOSE]: WorkWeekTranspose,
}
export const EventsView = ({ mode, ...viewProps }) => {
+3 -6
View File
@@ -1,4 +1,5 @@
import { ComboBox } from './ComboBox.jsx'
import { MODE_COURSE, MODE_WORKWEEK } from './EventsView.jsx'
import { Help } from './Help.jsx'
import { Icon } from './Icon.jsx'
@@ -39,12 +40,8 @@ export const HamburgerMenu = ({ onClose, mode, setMode, source, setSource, theme
value={mode}
setValue={setMode}
options={[
{ value: 'course', label: 'Corsi' },
{ value: 'work-week-v', label: 'Settimana' },
// {
// value: 'work-week-h',
// label: 'Settimana (Trasposto)',
// },
{ value: MODE_COURSE, label: 'Corsi' },
{ value: MODE_WORKWEEK, label: 'Settimana' },
]}
/>
</div>
+3 -11
View File
@@ -1,4 +1,5 @@
import { CompoundButton } from './CompoundButton.jsx'
import { MODE_COURSE, MODE_WORKWEEK } from './EventsView.jsx'
import { Icon } from './Icon.jsx'
export const Toolbar = ({
@@ -40,17 +41,8 @@ export const Toolbar = ({
<div class="item option">
<CompoundButton
options={[
{ value: 'course', label: 'Corsi' },
{ value: 'work-week-v', label: 'Settimana' },
// {
// value: 'work-week-h',
// label: (
// <>
// <Icon name="warning" />
// Settimana<sup>T</sup>
// </>
// ),
// },
{ value: MODE_COURSE, label: 'Corsi' },
{ value: MODE_WORKWEEK, label: 'Settimana' },
]}
value={mode}
setValue={setMode}
+70
View File
@@ -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.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')} &ndash;{' '}
{format(course.end, 'H:mm')} {course.aula}
</div>
))}
</div>
</div>
))}
</div>
</div>
)
}
+160
View File
@@ -0,0 +1,160 @@
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 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 &ndash; 11:00
</div>
<div
class="block"
style={{
'--start': 11 - 7,
'--size': 2,
}}
>
11:00 &ndash; 13:00
</div>
<div
class="block skip-border"
style={{
'--start': 14 - 7,
'--size': 2,
}}
>
14:00 &ndash; 16:00
</div>
<div
class="block"
style={{
'--start': 16 - 7,
'--size': 2,
}}
>
16:00 &ndash; 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>
)
}
+104
View File
@@ -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 &ndash; 11:00
</div>
<div class="label" style={{ 'grid-column': '4 / span 2' }}>
11:00 &ndash; 13:00
</div>
<div class="label" style={{ 'grid-column': '7 / span 2' }}>
14:00 &ndash; 16:00
</div>
<div class="label" style={{ 'grid-column': '9 / span 2' }}>
16:00 &ndash; 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>
)
}