diff --git a/src/components/EventsView.jsx b/src/components/EventsView.jsx
index 9a83bbf..507d210 100644
--- a/src/components/EventsView.jsx
+++ b/src/components/EventsView.jsx
@@ -1,16 +1,19 @@
import { Course } from './view/Course.jsx'
import { Schedule } from './view/Schedule.jsx'
import { WorkWeek } from './view/WorkWeek.jsx'
+import { WorkWeekGrid } from './view/WorkWeekGrid.jsx'
import { WorkWeekTranspose } from './view/WorkWeekTranspose.jsx'
export const MODE_COURSE = 'course'
export const MODE_WORKWEEK = 'work-week'
export const MODE_SCHEDULE = 'schedule'
+export const MODE_WORKWEEK_GRID = 'work-week-grid'
export const MODE_WORKWEEK_TRANSPOSE = 'work-week-transpose'
const viewModeMap = {
[MODE_COURSE]: Course,
[MODE_WORKWEEK]: WorkWeek,
+ [MODE_WORKWEEK_GRID]: WorkWeekGrid,
[MODE_WORKWEEK_TRANSPOSE]: WorkWeekTranspose,
[MODE_SCHEDULE]: Schedule,
}
diff --git a/src/components/HamburgerMenu.jsx b/src/components/HamburgerMenu.jsx
index 19ef2eb..5f9ff71 100644
--- a/src/components/HamburgerMenu.jsx
+++ b/src/components/HamburgerMenu.jsx
@@ -1,5 +1,5 @@
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 { Icon } from './Icon.jsx'
@@ -43,6 +43,7 @@ export const HamburgerMenu = ({ onClose, mode, setMode, source, setSource, theme
{ value: MODE_COURSE, label: 'Corsi' },
{ value: MODE_SCHEDULE, label: 'Giornaliera' },
{ value: MODE_WORKWEEK, label: 'Settimana' },
+ { value: MODE_WORKWEEK_GRID, label: 'Schema' },
]}
/>
diff --git a/src/components/Toolbar.jsx b/src/components/Toolbar.jsx
index 25e8568..c4ba083 100644
--- a/src/components/Toolbar.jsx
+++ b/src/components/Toolbar.jsx
@@ -1,5 +1,5 @@
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'
export const Toolbar = ({
@@ -43,6 +43,7 @@ export const Toolbar = ({
options={[
{ value: MODE_COURSE, label: 'Corsi' },
{ value: MODE_WORKWEEK, label: 'Settimana' },
+ { value: MODE_WORKWEEK_GRID, label: 'Schema' },
{ value: MODE_SCHEDULE, label: 'Giorno' },
]}
value={mode}
diff --git a/src/components/view/WorkWeekGrid.jsx b/src/components/view/WorkWeekGrid.jsx
new file mode 100644
index 0000000..2d6167b
--- /dev/null
+++ b/src/components/view/WorkWeekGrid.jsx
@@ -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 (
+
+
+ {[7, 9, 11, 14, 16, 18].map(n =>
+ <>
+ {n + 2 > timeStart && n < timeEnd && (
+
{n}-{n+2}
+ )}
+ >
+ )}
+
+ {[9, 11, 13, 14, 16, 18].map(n =>
+ <>
+ {n > timeStart && n < timeEnd && (
+
+ )}
+ >
+ )}
+
+
Lun
+
Mar
+
Mer
+
Gio
+
Ven
+
+
+
+
+
+
+
+ {Object.entries(dayIntervalLayout).map(([index, layout]) => (
+ <>
+ {layout.map((events, stackIndex) => (
+ <>
+ {events.map(event => (
+
{
+ 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] !== '' && (
+
{`(${colors[event.data.id][1]})`}
+ )}
+ {event.data.aula}
+ ))}
+ >
+ ))}
+ >
+ ))}
+
+
+ {courses.map(course => (
+ <>
+
+ {colors[course.id][1]}
+
+
{normalizeCourseName(course.name)}
+ >
+ ))}
+
+
+ )
+}
diff --git a/src/main.jsx b/src/main.jsx
index be40c47..cd033fe 100644
--- a/src/main.jsx
+++ b/src/main.jsx
@@ -3,7 +3,7 @@ import { render } from 'preact'
import { useEffect, useState } from 'preact/hooks'
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 { Help } from './components/Help.jsx'
import { Icon } from './components/Icon.jsx'
@@ -69,11 +69,11 @@ async function loadEventi(ids) {
const App = ({}) => {
// Data Sources
- const [source, setSource] = useState('magistrale')
+ const [source, setSource] = useState('anno-1')
const [eventi, setEventi] = useState([])
// View Modes
- const [mode, setMode] = useState(MODE_SCHEDULE)
+ const [mode, setMode] = useState(MODE_WORKWEEK_GRID)
// Selection
const [selectedCourses, setSelectedCourses] = useState([])
diff --git a/src/styles/main.scss b/src/styles/main.scss
index 83a66f9..5d44a3d 100644
--- a/src/styles/main.scss
+++ b/src/styles/main.scss
@@ -27,6 +27,36 @@ html {
--accent-100: #d6ffc2;
--accent-500: #6cc16c;
--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 {
@@ -43,6 +73,33 @@ body.dark-mode {
--accent-100: hsl(269, 50%, 50%);
--accent-500: hsl(269, 40%, 70%);
--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;
@@ -813,6 +870,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 {
padding: 1rem;