mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-01-21 00:52:55 +00:00
feat: timetable module - schedule and calendar
This commit is contained in:
243
src/app/modules/schedule/page/schedule-page.component.ts
Normal file
243
src/app/modules/schedule/page/schedule-page.component.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user