Commit 538641c2 authored by ellav's avatar ellav Committed by Alexander Dietmüller

Frontend: Improve user interface

The course list is completely reworked and now sorts courses by department
and year. Furthermore the individual courses are now nicely formatted.

Additionally, timing overlap betweeen the courses and already selected
courses are now checked.
parent 6858f239
/* eslint-disable no-param-reassign */
import m from 'mithril';
import { courses, userCourses } from './backend';
import SidebarCard from './components/SidebarCard';
import { dateFormatter, isOverlappingTime } from './utils';
import expandableContent from './components/Expandable';
// choose itet oder mavt
const selectedDepartment = 'mavt';
function getCoursesByDepartment(coursesList, department) {
return coursesList
.filter(course => Boolean(course.lecture))
.filter(course => course.lecture.department === department);
}
function onlyUnique(value, index, self) {
// Trick: indexOf returns the index of the first occurence of `value`
// so the following returns true only for the first item
return self.indexOf(value) === index;
}
function getUniqueYears(courseList) {
return courseList
.map(course => course.lecture.year)
.filter(onlyUnique);
}
function isSelectedOrReserved(course) {
return userCourses.selected.some(sel => sel.course === course._id) ||
userCourses.signups.some(signup => signup.course === course._id);
function getUniqueLecturesByYear(courseList, year) {
return courseList
.filter(course => course.lecture.year === year)
.map(course => course.lecture.title)
.filter(onlyUnique);
}
function displayCard(course) {
// Get all currently selected courses
const selectedCourses = userCourses.all.map(selection =>
courses.items[selection.course]).filter(Boolean);
// Check if this course is already selected
const selected = selectedCourses.map(c => c._id).indexOf(course._id) !== -1;
// Find all other courses with time overlap (if any)
const overlappingCourses = selectedCourses.filter((otherCourse) => {
let overlap = false;
if (course.datetimes && otherCourse && otherCourse.datetimes) {
course.datetimes.forEach((datetime1) => {
otherCourse.datetimes.forEach((datetime2) => {
if (isOverlappingTime(datetime1, datetime2)) { overlap = true; }
});
});
}
return overlap;
});
// For prettier output: List of lectures for which courses overlap
const overlappingLectures = overlappingCourses
.map(c => c.lecture.title)
.filter(onlyUnique)
.join(', ');
return m(SidebarCard, {
// Title and content
title: `Assistant: ${course.assistant}`,
subtitle: `Room: ${course.room}`,
content: [
m('ul', course.datetimes.map(timeslot =>
m('li', dateFormatter(timeslot)))),
// If course is not selected already, show potential timing conflicts
!selected && overlappingCourses.length !== 0 ? [
m('p', [
'Time conflict with your courses for the following lectures: ',
overlappingLectures,
]),
] : [],
],
// Action
actionName: selected ? 'Already selected' : 'Select',
actionActive: !selected && (overlappingCourses.length === 0),
action() { userCourses.select(course._id); },
});
}
export default class CourseList {
// get courses on initiating webpage
static oninit() {
courses.getAll();
}
// draw the SidebarCard sorted year and lecture.name filtered by department
static view() {
return m('table', [
m('thead', [
m('tr', [
m('th', 'Course'),
m('th', 'Department'),
m('th', 'Name'),
m('th', 'Spots'),
m('th', 'Signup Start'),
m('th', 'Signup End'),
m('th', 'Starting time'),
m('th', 'Ending time'),
]),
]),
m('tbody', courses.list.map(course =>
m('tr', [
m('td', course.lecture.title),
m('td', course.lecture.department),
m('td', course.assistant),
m('td', course.spots),
m('td', course.signup.start),
m('td', course.signup.end),
course.datetimes.map(timeslot => [
m('td', timeslot.start),
m('td', timeslot.end),
]),
const coursesByDepartment = getCoursesByDepartment(
courses.list,
selectedDepartment,
);
return getUniqueYears(coursesByDepartment).map(year =>
m(expandableContent, { name: `Year ${year}`, level: 0 }, [
getUniqueLecturesByYear(
coursesByDepartment,
year,
).map(lecture =>
m(
'td',
m(
'button',
{
onclick() { userCourses.select(course._id); },
disabled: isSelectedOrReserved(course),
// ?
// true : false,
// return false;
// return !((UserCourses.selected.courses || []).some(sel =>
// sel === course._id));
// },
},
'add course',
),
),
]))),
]);
expandableContent,
{ name: lecture, level: 1 },
coursesByDepartment
.filter(course => course.lecture.year === year)
.filter(course => course.lecture.title === lecture)
.map(displayCard),
))]));
}
}
......@@ -5,16 +5,27 @@ import SidebarCard from './components/SidebarCard';
class CourseView {
static view({ attrs: { _id, courseId, remove } }) {
static view({ attrs: { /* _id, */ courseId, remove } }) {
// Get Lecture of Course
const course = courses.list.find(item => item._id === courseId);
const course = courses.items[courseId];
// Otherwise display loading
return m('li', [
if (course) {
return m(SidebarCard, {
title: course.lecture.title,
subtitle: [course.assistant, ' - ', course.room],
actionName: 'X',
action() { remove(); },
});
}
return m(SidebarCard, {
title: 'Loading ...',
});
/* m('li', [
m('span', course ? course.lecture.title : 'Loading...'),
// If there is no id, the element is not yet created,
// so a delete button does not make any sense
m('button', { onclick: remove, disabled: !_id }, 'X'),
]);
]); */
}
}
......
......@@ -63,16 +63,30 @@ class Resource {
this._items_deleted = [];
}
get _currentItems() {
// Hide items marked to be deletd and include unconfirmed patches
return Object.keys(this._items)
.filter(key => this._items_deleted.indexOf(key) === -1)
.map(key => this._items_updated[key] || this._items[key]);
}
isBusy() {
return Object.keys(this._items_updated).length !== 0 ||
this._items_deleted.length !== 0;
}
// Access items by id
get items() {
const obj = {};
this._currentItems.forEach((item) => {
obj[item._id] = item;
});
return obj;
}
// Access items as list
get list() {
const currentItems = Object.keys(this._items)
.filter(key => this._items_deleted.indexOf(key) === -1)
.map(key => this._items_updated[key] || this._items[key]);
return [...currentItems, ...this._items_new];
return [...this._currentItems, ...this._items_new];
}
get(page = 1) {
......@@ -209,6 +223,13 @@ export const userCourses = {
this.resources.signups.get();
},
get all() {
return [].concat(
this.resources.selections.list,
this.resources.signups.list,
);
},
get selected() {
return this.resources.selections.list;
},
......
/* eslint-disable no-param-reassign */
import m from 'mithril';
const icons = {
// html arrows for expandable content
ArrowRight: '<svg fill="#000000" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M8.59 16.34l4.58-4.59-4.58-4.59L10 5.75l6 6-6 6z"/><path d="M0-.25h24v24H0z" fill="none"/></svg>',
ArrowDown: '<svg fill="#000000" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M7.41 7.84L12 12.42l4.59-4.58L18 9.25l-6 6-6-6z"/><path d="M0-.75h24v24H0z" fill="none"/></svg>',
};
export default class expandableContent {
// return object with state.expanded, state.level and state.name
static view(vnode) {
return [
m(
'div',
{
onclick() {
vnode.state.expanded = !vnode.state.expanded;
},
},
m(
`h${vnode.attrs.level + 1}`,
[vnode.state.expanded ? m.trust(icons.ArrowDown) : m.trust(icons.ArrowRight),
vnode.attrs.name],
),
),
vnode.state.expanded ? vnode.children : [],
];
}
}
......@@ -21,7 +21,7 @@ body {
/* Grid power! */
display: grid;
grid-template-columns: [container-start] 5% [header-start sidebar-start] auto
grid-template-columns: [container-start] 5% [header-start sidebar-start] 30%
[sidebar-end content-start] auto
[header-end content-end] 5% [container-end];
grid-template-rows: [header-start] auto [header-end] auto
......
/* eslint-disable no-param-reassign */
export function dateFormatterStart(datestring) {
// converts an API datestring into the standard format Mon 30/01/1990, 10:21
if (!datestring) return '';
const date = new Date(datestring);
return date.toLocaleString('en-GB', {
weekday: 'short',
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
}
export function dateFormatterEnd(datestring) {
// converts an API datestring into the standard format 10:21
if (!datestring) return '';
const date = new Date(datestring);
return date.toLocaleString('en-GB', {
hour: '2-digit',
minute: '2-digit',
});
}
export function dateFormatter(timeslot) {
const start = dateFormatterStart(timeslot.start);
const end = dateFormatterEnd(timeslot.end);
return `${start} - ${end}`;
}
export function isOverlappingTime(dateTime1, dateTime2) {
// checks if there is a overlap between to timeslots
return (!(Date.parse(dateTime1.start) >= Date.parse(dateTime2.end)
|| Date.parse(dateTime2.start) >= Date.parse(dateTime1.end)));
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment