Added compact grid view for the week

pull/1/head
Francesco Baldino 2 years ago
parent c0340c3407
commit ebb78c4aff

@ -1,16 +1,19 @@
import { Course } from './view/Course.jsx' import { Course } from './view/Course.jsx'
import { Schedule } from './view/Schedule.jsx' import { Schedule } from './view/Schedule.jsx'
import { WorkWeek } from './view/WorkWeek.jsx' import { WorkWeek } from './view/WorkWeek.jsx'
import { WorkWeekGrid } from './view/WorkWeekGrid.jsx'
import { WorkWeekTranspose } from './view/WorkWeekTranspose.jsx' import { WorkWeekTranspose } from './view/WorkWeekTranspose.jsx'
export const MODE_COURSE = 'course' export const MODE_COURSE = 'course'
export const MODE_WORKWEEK = 'work-week' export const MODE_WORKWEEK = 'work-week'
export const MODE_SCHEDULE = 'schedule' export const MODE_SCHEDULE = 'schedule'
export const MODE_WORKWEEK_GRID = 'work-week-grid'
export const MODE_WORKWEEK_TRANSPOSE = 'work-week-transpose' export const MODE_WORKWEEK_TRANSPOSE = 'work-week-transpose'
const viewModeMap = { const viewModeMap = {
[MODE_COURSE]: Course, [MODE_COURSE]: Course,
[MODE_WORKWEEK]: WorkWeek, [MODE_WORKWEEK]: WorkWeek,
[MODE_WORKWEEK_GRID]: WorkWeekGrid,
[MODE_WORKWEEK_TRANSPOSE]: WorkWeekTranspose, [MODE_WORKWEEK_TRANSPOSE]: WorkWeekTranspose,
[MODE_SCHEDULE]: Schedule, [MODE_SCHEDULE]: Schedule,
} }

@ -1,5 +1,5 @@
import { ComboBox } from './ComboBox.jsx' import { ComboBox } from './ComboBox.jsx'
import { MODE_COURSE, MODE_SCHEDULE, MODE_WORKWEEK } from './EventsView.jsx' import { MODE_COURSE, MODE_SCHEDULE, MODE_WORKWEEK, MODE_WORKWEEK_GRID } from './EventsView.jsx'
import { Help } from './Help.jsx' import { Help } from './Help.jsx'
import { Icon } from './Icon.jsx' import { Icon } from './Icon.jsx'
@ -43,6 +43,7 @@ export const HamburgerMenu = ({ onClose, mode, setMode, source, setSource, theme
{ value: MODE_COURSE, label: 'Corsi' }, { value: MODE_COURSE, label: 'Corsi' },
{ value: MODE_SCHEDULE, label: 'Giornaliera' }, { value: MODE_SCHEDULE, label: 'Giornaliera' },
{ value: MODE_WORKWEEK, label: 'Settimana' }, { value: MODE_WORKWEEK, label: 'Settimana' },
{ value: MODE_WORKWEEK_GRID, label: 'Schema' },
]} ]}
/> />
</div> </div>

@ -1,5 +1,5 @@
import { CompoundButton } from './CompoundButton.jsx' import { CompoundButton } from './CompoundButton.jsx'
import { MODE_COURSE, MODE_SCHEDULE, MODE_WORKWEEK } from './EventsView.jsx' import { MODE_COURSE, MODE_SCHEDULE, MODE_WORKWEEK, MODE_WORKWEEK_GRID } from './EventsView.jsx'
import { Icon } from './Icon.jsx' import { Icon } from './Icon.jsx'
export const Toolbar = ({ export const Toolbar = ({
@ -43,6 +43,7 @@ export const Toolbar = ({
options={[ options={[
{ value: MODE_COURSE, label: 'Corsi' }, { value: MODE_COURSE, label: 'Corsi' },
{ value: MODE_WORKWEEK, label: 'Settimana' }, { value: MODE_WORKWEEK, label: 'Settimana' },
{ value: MODE_WORKWEEK_GRID, label: 'Schema' },
{ value: MODE_SCHEDULE, label: 'Giorno' }, { value: MODE_SCHEDULE, label: 'Giorno' },
]} ]}
value={mode} value={mode}

@ -0,0 +1,200 @@
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 WorkWeekGrid = ({ events, selection, setSelection, hideOtherCourses }) => {
const selectionSet = new Set(selection)
const colorList = [
'red',
'purple',
'blue',
'yellow',
'green',
'orange',
'lightblue',
];
const courses = _.uniqBy(!hideOtherCourses ? events : events.filter(e => selectionSet.has(e.id)), event => event.id);
const colors = Object.fromEntries(courses.map((course, index) => {
return [
course.id,
[
colorList[index % colorList.length],
courses.length <= colorList.length ? '' : String.fromCharCode(65 + Math.floor(index / colorList.length))
]
];
}));
const base = {
1: [],
2: [],
3: [],
4: [],
5: [],
}
const eventsByWeekday = {
... base,
... _.groupBy(
!hideOtherCourses ? events : events.filter(e => selectionSet.has(e.id)),
event => event.start.getDay()
)
}
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 startsAndEnds = Object.entries(dayIntervalLayout).flatMap(([index, layout]) => layout.flatMap(events => events.map(event => [event.start, event.end])));
const minStart = Math.min(...startsAndEnds.map(([s, e]) => s)) / 60;
const maxEnd = Math.max(...startsAndEnds.map(([s, e]) => e)) / 60;
// const timeStart = minStart < 9 ? 7 :
// minStart < 11 ? 9 :
// minStart < 13 ? 11 :
// minStart < 14 ? 13 :
// minStart < 16 ? 14 :
// minStart < 18 ? 16 :
// 18;
// const timeEnd = maxEnd > 18 ? 20 :
// maxEnd > 16 ? 18 :
// maxEnd > 14 ? 16 :
// maxEnd > 13 ? 14 :
// maxEnd > 11 ? 13 :
// maxEnd > 9 ? 11 :
// 9;
const timeStart = minStart < 9 ? 7 : 9;
const timeEnd = maxEnd > 18 ? 20 : 18;
const daySizes = Object.entries(dayIntervalLayout).map(([index, layout]) => Math.max(1, layout.length));
const dayOffsets = daySizes.map((v, i) => {
let sum = 0;
for(let j = 0; j < i; j++) {
sum += daySizes[j];
}
return sum;
});
const daysLength = daySizes[4] + dayOffsets[4];
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-grid-view" ref={element}>
<div class="grid" style={{'--days-length': daysLength, '--time-length': timeEnd - timeStart}}>
{[7, 9, 11, 14, 16, 18].map(n =>
<>
{n + 2 > timeStart && n < timeEnd && (
<div class="time" style={{'--offset': n - timeStart + 2}}>{n}-{n+2}</div>
)}
</>
)}
<div class="vline" style="--offset: 2"></div>
{[9, 11, 13, 14, 16, 18].map(n =>
<>
{n > timeStart && n < timeEnd && (
<div class="vline" style={{'--offset': n - timeStart + 2}}></div>
)}
</>
)}
<div class="day-name" style={{'--line': 2 + dayOffsets[0]}}>Lun</div>
<div class="day-name" style={{'--line': 2 + dayOffsets[1]}}>Mar</div>
<div class="day-name" style={{'--line': 2 + dayOffsets[2]}}>Mer</div>
<div class="day-name" style={{'--line': 2 + dayOffsets[3]}}>Gio</div>
<div class="day-name" style={{'--line': 2 + dayOffsets[4]}}>Ven</div>
<div class="hline" style={{'--line': 2 + dayOffsets[0]}}></div>
<div class="hline" style={{'--line': 2 + dayOffsets[1]}}></div>
<div class="hline" style={{'--line': 2 + dayOffsets[2]}}></div>
<div class="hline" style={{'--line': 2 + dayOffsets[3]}}></div>
<div class="hline" style={{'--line': 2 + dayOffsets[4]}}></div>
{Object.entries(dayIntervalLayout).map(([index, layout]) => (
<>
{layout.map((events, stackIndex) => (
<>
{events.map(event => (
<div class={
'event' +
(currentlyHovered === event.data.id
? ' highlight'
: '') +
(selectionSet.has(event.data.id) ? ' selected' : '')
}
data-event-id={event.data.id}
style={{
'--line': 2 + dayOffsets[index-1] + stackIndex,
'--offset': event.start / 60 - timeStart + 2,
'--length': (event.end - event.start) / 60,
'--color': `var(--bubble-${colors[event.data.id][0]})`,
'--border-color': `var(--bubble-border-${colors[event.data.id][0]})`,
'--highlight-color': `var(--bubble-highlight-${colors[event.data.id][0]})`
}}
onClick={() => {
if (!selectionSet.has(event.data.id))
setSelection([...selection, event.data.id])
else
setSelection(
selection.filter(id => event.data.id !== id)
)
}}
>
{colors[event.data.id][1] !== '' && (
<div className="distinguisher">{`(${colors[event.data.id][1]})`}</div>
)}
{event.data.aula}</div>
))}
</>
))}
</>
))}
</div>
<div class="legend">
{courses.map(course => (
<>
<div class="color"
style={{
'--color': `var(--bubble-${colors[course.id][0]})`,
'--border-color': `var(--bubble-border-${colors[course.id][0]})`
}}
>
{colors[course.id][1]}
</div>
<div class="name">{normalizeCourseName(course.name)}</div>
</>
))}
</div>
</div>
)
}

@ -3,7 +3,7 @@ import { render } from 'preact'
import { useEffect, useState } from 'preact/hooks' import { useEffect, useState } from 'preact/hooks'
import { ToolOverlay } from './components/CourseVisibility.jsx' import { ToolOverlay } from './components/CourseVisibility.jsx'
import { EventsView, MODE_SCHEDULE } from './components/EventsView.jsx' import { EventsView, MODE_WORKWEEK_GRID } from './components/EventsView.jsx'
import { HamburgerMenu } from './components/HamburgerMenu.jsx' import { HamburgerMenu } from './components/HamburgerMenu.jsx'
import { Help } from './components/Help.jsx' import { Help } from './components/Help.jsx'
import { Icon } from './components/Icon.jsx' import { Icon } from './components/Icon.jsx'
@ -69,11 +69,11 @@ async function loadEventi(ids) {
const App = ({}) => { const App = ({}) => {
// Data Sources // Data Sources
const [source, setSource] = useState('magistrale') const [source, setSource] = useState('anno-1')
const [eventi, setEventi] = useState([]) const [eventi, setEventi] = useState([])
// View Modes // View Modes
const [mode, setMode] = useState(MODE_SCHEDULE) const [mode, setMode] = useState(MODE_WORKWEEK_GRID)
// Selection // Selection
const [selectedCourses, setSelectedCourses] = useState([]) const [selectedCourses, setSelectedCourses] = useState([])

@ -27,6 +27,36 @@ html {
--accent-100: #d6ffc2; --accent-100: #d6ffc2;
--accent-500: #6cc16c; --accent-500: #6cc16c;
--accent-900: #244624; --accent-900: #244624;
--bubble-red: hsl(359, 100%, 92%);
--bubble-purple: hsl(274, 100%, 92%);
--bubble-blue: hsl(241, 100%, 92%);
--bubble-yellow: hsl(50, 100%, 92%);
--bubble-green: hsl(125, 100%, 92%);
--bubble-orange: hsl(25, 100%, 92%);
--bubble-lightlightblue: hsl(176, 100%, 92%);
--bubble-lightblue: hsl(198, 100%, 92%);
--bubble-highlight-red: hsl(359, 100%, 85%);
--bubble-highlight-purple: hsl(274, 100%, 85%);
--bubble-highlight-blue: hsl(241, 100%, 85%);
--bubble-highlight-yellow: hsl(50, 100%, 85%);
--bubble-highlight-green: hsl(125, 100%, 85%);
--bubble-highlight-orange: hsl(25, 100%, 85%);
--bubble-highlight-lightlightblue: hsl(176, 100%, 85%);
--bubble-highlight-lightblue: hsl(198, 100%, 85%);
--bubble-border-red: hsl(359, 75%, 51%);
--bubble-border-purple: hsl(274, 75%, 51%);
--bubble-border-blue: hsl(241, 75%, 51%);
--bubble-border-yellow: hsl(50, 75%, 51%);
--bubble-border-green: hsl(125, 75%, 51%);
--bubble-border-orange: hsl(25, 75%, 51%);
--bubble-border-lightlightblue: hsl(176, 75%, 51%);
--bubble-border-lightblue: hsl(198, 75%, 51%);
--bold-on-dark: 300;
} }
body.dark-mode { body.dark-mode {
@ -43,6 +73,33 @@ body.dark-mode {
--accent-100: hsl(269, 50%, 50%); --accent-100: hsl(269, 50%, 50%);
--accent-500: hsl(269, 40%, 70%); --accent-500: hsl(269, 40%, 70%);
--accent-900: hsl(269, 30%, 90%); --accent-900: hsl(269, 30%, 90%);
--bubble-red: hsl(359, 40%, 25%);
--bubble-purple: hsl(274, 40%, 25%);
--bubble-blue: hsl(241, 40%, 25%);
--bubble-yellow: hsl(50, 40%, 25%);
--bubble-green: hsl(125, 40%, 25%);
--bubble-orange: hsl(25, 40%, 25%);
--bubble-lightblue: hsl(176, 40%, 25%);
--bubble-border-red: hsl(359, 75%, 51%);
--bubble-border-purple: hsl(274, 75%, 51%);
--bubble-border-blue: hsl(241, 75%, 51%);
--bubble-border-yellow: hsl(50, 75%, 51%);
--bubble-border-green: hsl(125, 75%, 51%);
--bubble-border-orange: hsl(25, 75%, 51%);
--bubble-border-lightblue: hsl(176, 75%, 51%);
--bubble-highlight-red: hsl(359, 40%, 31%);
--bubble-highlight-purple: hsl(274, 40%, 31%);
--bubble-highlight-blue: hsl(241, 40%, 31%);
--bubble-highlight-yellow: hsl(50, 40%, 31%);
--bubble-highlight-green: hsl(125, 40%, 31%);
--bubble-highlight-orange: hsl(25, 40%, 31%);
--bubble-highlight-lightblue: hsl(176, 40%, 31%);
--bold-on-dark: 500;
} }
$device-s-width: 600px; $device-s-width: 600px;
@ -811,6 +868,121 @@ body {
} }
} }
.work-week-grid-view {
position: relative;
padding: 1rem;
display: flex;
flex-direction: column;
align-items: center;
height: 100%;
overflow: scroll;
.grid {
width: 100%;
max-width: 55rem;
display: grid;
grid-template-columns: 3rem repeat(var(--time-length), 1fr);
grid-template-rows: 2rem repeat(var(--days-length), 1fr);
border: 2px solid var(--border-500);
border-radius: 10px 10px 0 0;
@media screen and (max-width: $device-s-width), (pointer: coarse) {
font-size: 12px;
}
.time {
display: flex;
align-items: center;
justify-content: center;
padding: 0.5rem;
grid-row: 1;
grid-column: var(--offset) / span 2;
}
.day-name {
display: flex;
align-items: center;
justify-content: center;
padding: 0.5rem;
grid-column: 1;
grid-row: var(--line);
}
.event {
display: flex;
flex-direction: row;
@media screen and (max-width: $device-s-width), (pointer: coarse) {
flex-direction: column;
}
gap: 0.25rem;
align-items: center;
justify-content: center;
margin: 0.25rem;
padding: 0.5rem;
grid-row: var(--line);
grid-column: var(--offset) / span var(--length);
background-color: var(--color);
border: 3px solid var(--color);
border-radius: 10px;
font-weight: var(--bold-on-dark);
text-align: center;
&.highlight {
background-color: var(--highlight-color);
border: 3px solid var(--highlight-color);
}
&.selected {
border: 3px solid var(--border-color);
}
}
.hline {
grid-column: 1 / 13;
grid-row: var(--line);
height: 0px;
border-bottom: 1px solid var(--border-500);
}
.vline {
grid-column: var(--offset);
grid-row: 1 / calc(2 + var(--days-length));
width: 0px;
border-right: 1px dashed var(--border-500);
}
}
.legend {
max-width: 55rem;
display: grid;
grid-template-columns: min-content 1fr;
border: 2px solid var(--border-500);
border-radius: 0 0 10px 10px;
border-top: none;
padding: 1rem;
gap: 0.5rem 1rem;
align-items: center;
justify-content: center;
width: 100%;
.name {
width: 100%;
}
.color {
display: flex;
justify-content: center;
align-items: center;
font-weight: 500;
width: 2rem;
height: 1.5rem;
background-color: var(--color);
border: 2px solid var(--border-color);
border-radius: 10px;
font-weight: var(--bold-on-dark);
}
}
}
.schedule-view { .schedule-view {
padding: 1rem; padding: 1rem;

Loading…
Cancel
Save