feat: timetable module - schedule and calendar

This commit is contained in:
Wieland Schöbl
2021-08-13 12:27:40 +00:00
parent e81b2e161d
commit d8ede006df
59 changed files with 3287 additions and 555 deletions

View File

@@ -0,0 +1,243 @@
/*
* Copyright (C) 2021 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {
AfterViewInit,
Component,
HostListener,
OnInit,
ViewChild,
} from '@angular/core';
import {ActivatedRoute} from '@angular/router';
import {AnimationController, ModalController} from '@ionic/angular';
import {last} from 'lodash-es';
import {SharedAxisChoreographer} from '../../../animation/animation-choreographer';
import {materialSharedAxisX} from '../../../animation/material-motion';
import {ModalEventCreatorComponent} from './modal/modal-event-creator.component';
import {ScheduleResponsiveBreakpoint} from './schema/schema';
import {animate, style, transition, trigger} from '@angular/animations';
/**
* This needs to be sorted by break point low -> high
*
* Last entry must have `until: Infinity`
*/
const responsiveConfig: ScheduleResponsiveBreakpoint[] = [
{
until: 768,
days: 1,
startOf: 'day',
},
{
until: 1700,
days: 3,
startOf: 'day',
},
{
until: Number.POSITIVE_INFINITY,
days: 7,
startOf: 'week',
},
];
const fabAnimations = trigger('fabAnimation', [
transition(':leave', [
style({opacity: 1, transform: 'translate(0, 0) scale(1)'}),
animate(
'100ms ease-in',
style({opacity: 0, transform: 'translate(-5vw, -5vh) scale(200%)'}),
),
]),
transition(':enter', [
style({opacity: 0, transform: 'translate(-5vw, -5vh) scale(200%)'}),
animate(
'200ms ease-out',
style({opacity: 1, transform: 'translate(0, 0) scale(1)'}),
),
]),
]);
/**
* Component that displays the schedule
*/
@Component({
selector: 'stapps-schedule-page',
templateUrl: 'schedule-page.html',
styleUrls: ['schedule-page.scss'],
animations: [materialSharedAxisX, fabAnimations],
})
export class SchedulePageComponent implements OnInit, AfterViewInit {
/**
* Current width of the window
*/
private currentWindowWidth: number = window.innerWidth;
/**
* Actual Segment Tab
*/
actualSegmentValue?: string | null;
fabVisible = true;
/**
* Layout
*/
layout: ScheduleResponsiveBreakpoint = SchedulePageComponent.getDaysToDisplay(
this.currentWindowWidth,
);
/**
* Vertical scale of the schedule (distance between hour lines)
*/
scale = 60;
@ViewChild('segment') segmentView!: HTMLIonSegmentElement;
/**
* Choreographer for the tab switching
*/
tabChoreographer: SharedAxisChoreographer<string | null | undefined>;
/**
* Weekly config for schedule
*/
weeklyConfig: ScheduleResponsiveBreakpoint = {
until: Number.POSITIVE_INFINITY,
days: 7,
startOf: 'week',
};
/**
* Amount of days that should be shown according to current display width
*/
static getDaysToDisplay(width: number): ScheduleResponsiveBreakpoint {
// the search could be optimized, but probably would have little
// actual effect with five entries.
// we can be sure we get an hit when the last value.until is infinity
// (unless someone has a display that reaches across the universe)
return (
responsiveConfig.find(value => width < value.until) ??
responsiveConfig[responsiveConfig.length - 1]
);
}
constructor(
private readonly modalController: ModalController,
private readonly activatedRoute: ActivatedRoute,
private readonly animationController: AnimationController,
) {}
ngOnInit() {
this.tabChoreographer = new SharedAxisChoreographer(
this.activatedRoute.snapshot.paramMap.get('mode'),
['calendar', 'recurring', 'single'],
);
}
ngAfterViewInit() {
this.segmentView.value = this.tabChoreographer.currentValue;
}
/**
* Resize callback
*
* Note: this may not fire when the browser transfers from full screen to windowed
* (Firefox & Chrome tested)
*/
@HostListener('window:resize', ['$event'])
onResize(_: UIEvent) {
const current = SchedulePageComponent.getDaysToDisplay(
this.currentWindowWidth,
);
const next = SchedulePageComponent.getDaysToDisplay(window.innerWidth);
this.currentWindowWidth = window.innerWidth;
if (current.days === next.days) {
this.layout = next;
}
}
/**
* When the segment changes
*/
onSegmentChange() {
window.history.replaceState(
{},
'',
`/#/schedule/${this.segmentView.value}/${last(
window.location.href.split('/'),
)}`,
);
this.tabChoreographer.changeViewForState(this.segmentView.value);
}
/**
* Add event modal sheet
*/
async showCreateEventModal() {
this.fabVisible = false;
// eslint-disable-next-line @typescript-eslint/no-explicit-any,unicorn/consistent-function-scoping
const enterAnimation = (baseElement: any) => {
const backdropAnimation = this.animationController
.create()
.addElement(baseElement.querySelector('.modal-wrapper'))
.fromTo('opacity', '0', 'var(--backdrop-opacity)');
const wrapperAnimation = this.animationController
.create()
.addElement(baseElement.querySelector('.modal-wrapper'))
.keyframes([
{
opacity: '0',
transform: 'translate(30vw, 30vh) scale(0.5)',
},
{
opacity: '1',
transform: 'translate(0, 0) scale(1)',
},
]);
return this.animationController
.create()
.addElement(baseElement)
.easing('ease-out')
.duration(150)
.addAnimation([backdropAnimation, wrapperAnimation]);
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any,unicorn/consistent-function-scoping
const leaveAnimation = (baseElement: any) => {
return enterAnimation(baseElement).direction('reverse');
};
const modal = await this.modalController.create({
component: ModalEventCreatorComponent,
swipeToClose: true,
cssClass: 'add-modal',
componentProps: {
dismissAction: () => {
modal.dismiss();
},
},
enterAnimation,
leaveAnimation,
});
await modal.present();
await modal.onWillDismiss();
this.fabVisible = true;
}
}