feat: apply new layout overhaul

This commit is contained in:
Andy Bastian
2022-08-08 11:01:00 +00:00
committed by Rainer Killinger
parent f16e5394cc
commit 7bbdba5c0b
228 changed files with 28387 additions and 1092 deletions

View File

@@ -0,0 +1,46 @@
<ion-header>
<ion-toolbar class="ion-hide-md-up">
<ion-label slot="start">{{
'dashboard.header.title' | daytimeKey | translate
}}</ion-label>
<ion-img src="assets/imgs/logo.png" class="logo"></ion-img>
</ion-toolbar>
</ion-header>
<ion-content>
<div class="schedule">
<a [routerLink]="['/schedule/recurring']">
<ion-icon name="layout-grid"></ion-icon>
<ion-label>{{ 'schedule.recurring' | translate }}</ion-label>
</a>
<a
*ngIf="nextEvent && nextEvent.event"
[routerLink]="['/data-detail', nextEvent.event.uid]"
class="schedule-item-button"
>
<ion-label>{{ 'dashboard.schedule.title' | translate }}</ion-label>
<ion-label>
{{ nextEvent.dates | nextDateInList | amDateFormat: 'll, HH:mm' }}
{{ 'timeSuffix' | translate }}
</ion-label>
<ion-label>{{ nextEvent.event.name }}</ion-label>
</a>
<a
*ngIf="!nextEvent || !nextEvent.event"
[routerLink]="['/schedule/recurring']"
class="schedule-item-button"
>
<ion-label>{{ 'dashboard.schedule.title' | translate }}</ion-label>
<ion-label>{{ 'dashboard.schedule.noEvent' | translate }}</ion-label>
<ion-label>{{ 'dashboard.schedule.noEventLink' | translate }}</ion-label>
</a>
</div>
<div class="scrollable-container">
<stapps-navigation-section></stapps-navigation-section>
<stapps-search-section></stapps-search-section>
<stapps-news-section></stapps-news-section>
<stapps-mensa-section></stapps-mensa-section>
<stapps-favorites-section></stapps-favorites-section>
</div>
</ion-content>

View File

@@ -0,0 +1,224 @@
@import '../../../theme/util/mixins';
:host ion-toolbar:last-of-type {
--padding-top: var(--spacing-md);
--padding-bottom: 0;
ion-icon {
margin-right: var(--spacing-sm);
width: var(--font-size-xl);
height: var(--font-size-xl);
}
ion-label {
font-family: var(--headline-font-family);
font-size: var(--font-size-md);
font-weight: 700;
}
.logo {
width: 27vw;
max-width: 150px;
max-height: 80px;
aspect-ratio: 1/1;
object-position: right;
margin-left: auto;
margin-right: var(--spacing-sm);
@include phoneLandscape {
width: auto;
height: 50px;
}
@include phonePortraitSmall {
width: auto;
height: 50px;
}
}
}
ion-content {
--background: var(--ion-color-light);
--overflow: hidden;
.scrollable-container {
overflow: hidden auto;
height: 100%;
padding-top: 160px;
@media (max-width: 440px) {
padding-top: 140px;
}
@include ion-md-up {
padding-top: 0;
padding-bottom: 160px;
}
}
@include phoneLandscape {
--overflow: hidden auto;
.scrollable-container {
overflow: initial;
height: initial;
padding-top: 0;
padding-bottom: 0;
}
}
}
.schedule {
position: fixed;
width: 100%;
z-index: 3;
background: var(--ion-color-primary);
display: flex;
justify-content: space-between;
gap: var(--spacing-md);
padding: var(--spacing-sm) var(--spacing-sm) var(--spacing-xl);
@include ion-md-up {
position: unset;
width: unset;
z-index: unset;
height: calc(var(--tablet-top-bar-height) + (2 * var(--spacing-xl)));
margin: 0;
padding: var(--spacing-xl);
}
a {
display: flex;
flex-direction: column;
color: var(--ion-color-primary-contrast);
text-decoration: none;
height: auto;
padding: var(--spacing-lg);
border-radius: var(--border-radius-default);
}
a:first-child {
border: 2px solid var(--ion-color-primary-tint);
text-align: center;
flex: 1 1 auto;
aspect-ratio: 1;
box-sizing: content-box;
max-height: 100px;
@include phoneLandscape {
height: auto;
}
ion-icon {
margin: auto auto var(--spacing-xs);
height: 40px;
width: 40px;
}
ion-label {
margin: 0 auto auto;
font-size: var(--font-size-xxs);
font-weight: var(--font-weight-semi-bold);
}
}
a:last-child {
flex: 1 1 65%;
background: var(--linear-gradient);
justify-content: center;
@include ion-md-up {
flex: 1 1 100%;
}
@include phoneLandscape {
flex: 1 1 85%;
}
ion-label {
font-size: var(--font-size-xxs);
font-weight: var(--font-weight-bold);
line-height: 1.4;
}
ion-label:first-child {
text-transform: uppercase;
color: var(--ion-color-secondary);
}
ion-label:nth-child(2n) {
font-size: var(--font-size-lg);
font-weight: var(--font-weight-semi-bold);
line-height: 1.2;
}
}
}
.section {
padding: var(--spacing-md);
&.section-extended {
padding-right: 0;
ion-icon[name='edit'] {
margin-right: var(--spacing-md);
}
}
&:first-of-type {
padding-top: var(--spacing-lg);
}
& > ion-label:first-child {
font-family: var(--headline-font-family);
font-size: var(--font-size-lg);
font-weight: var(--font-weight-semi-bold);
text-transform: uppercase;
margin-bottom: var(--spacing-md);
width: 100%;
display: flex;
flex-direction: revert;
justify-content: space-between;
ion-icon {
color: var(--ion-color-medium-shade);
width: 25px;
height: 25px;
}
}
}
.swiper {
background-color: var(--ion-color-primary-contrast);
border-radius: var(--border-radius-default);
padding: var(--spacing-lg);
width: 28%;
display: flex;
flex-direction: column;
font-size: var(--font-size-xs);
font-weight: var(--font-weight-bold);
ion-icon {
width: 40px;
height: 40px;
margin-bottom: var(--spacing-xs);
}
}
ion-searchbar {
padding: 0;
--background: var(--ion-color-primary-contrast);
::ng-deep .searchbar-input-container {
height: 100%;
input {
padding: var(--spacing-lg);
}
ion-icon {
left: auto;
right: var(--spacing-lg);
width: 30px;
}
}
}

View File

@@ -0,0 +1,107 @@
/*
* 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 {Component, OnInit, OnDestroy} from '@angular/core';
import {Router} from '@angular/router';
import {Location} from '@angular/common';
import {Subscription} from 'rxjs';
import moment from 'moment';
import {SCDateSeries, SCUuid} from '@openstapps/core';
import {SplashScreen} from '@capacitor/splash-screen';
import {DataRoutingService} from '../data/data-routing.service';
import {ScheduleProvider} from '../calendar/schedule.provider';
@Component({
selector: 'app-dashboard',
templateUrl: './dashboard.component.html',
styleUrls: ['./dashboard.component.scss'],
})
export class DashboardComponent implements OnInit, OnDestroy {
/**
* Array of all subscriptions to Observables
*/
subscriptions: Subscription[] = [];
/**
* UUID subscription
*/
private _uuidSubscription: Subscription;
/**
* The events to display
*/
private uuids: SCUuid[];
/**
* Next event in calendar
*/
nextEvent: SCDateSeries | undefined;
/**
* Slider options
*/
quickNavigationOptions = {
slidesPerView: 'auto',
spaceBetween: 12,
freeMode: {
enabled: true,
sticky: true,
},
};
constructor(
private readonly dataRoutingService: DataRoutingService,
private scheduleProvider: ScheduleProvider,
protected router: Router,
public location: Location,
) {
this.subscriptions.push(
this.dataRoutingService.itemSelectListener().subscribe(item => {
void this.router.navigate(['data-detail', item.uid]);
}),
);
}
async ngOnInit() {
this._uuidSubscription = this.scheduleProvider.uuids$.subscribe(
async result => {
this.uuids = result;
await this.loadNextEvent();
},
);
await SplashScreen.hide();
}
async loadNextEvent() {
const dataSeries = await this.scheduleProvider.getDateSeries(
this.uuids,
['P1W', 'P2W', 'P3W', 'P4W'],
moment(moment.now()).startOf('week').toISOString(),
);
this.nextEvent =
(dataSeries && dataSeries.dates && dataSeries.dates[0]) || undefined;
}
/**
* Remove subscriptions when the component is removed
*/
ngOnDestroy() {
for (const sub of this.subscriptions) {
sub.unsubscribe();
}
this._uuidSubscription.unsubscribe();
}
}

View File

@@ -0,0 +1,73 @@
/*
* 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 {CommonModule} from '@angular/common';
import {NgModule} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {RouterModule, Routes} from '@angular/router';
import {IonicModule} from '@ionic/angular';
import {SwiperModule} from 'swiper/angular';
import {TranslateModule} from '@ngx-translate/core';
import {MomentModule} from 'ngx-moment';
import {DataModule} from '../data/data.module';
import {SettingsProvider} from '../settings/settings.provider';
import {DashboardComponent} from './dashboard.component';
import {EditModalComponent} from './edit-modal/edit-modal.component';
import {SectionComponent} from './section/section.component';
import {NavigationSectionComponent} from './sections/navigation-section/navigation-section.component';
import {SearchSectionComponent} from './sections/search-section/search-section.component';
import {NewsSectionComponent} from './sections/news-section/news-section.component';
import {MensaSectionComponent} from './sections/mensa-section/mensa-section.component';
import {MensaSectionContentComponent} from './sections/mensa-section/mensa-section-content.component';
import {FavoritesSectionComponent} from './sections/favorites-section/favorites-section.component';
import {ThingTranslateModule} from '../../translation/thing-translate.module';
import {UtilModule} from '../../util/util.module';
const catalogRoutes: Routes = [
{
path: 'dashboard',
component: DashboardComponent,
},
];
/**
* Catalog Module
*/
@NgModule({
declarations: [
SectionComponent,
EditModalComponent,
NavigationSectionComponent,
SearchSectionComponent,
NewsSectionComponent,
MensaSectionComponent,
MensaSectionContentComponent,
FavoritesSectionComponent,
DashboardComponent,
],
imports: [
IonicModule.forRoot(),
FormsModule,
TranslateModule.forChild(),
RouterModule.forChild(catalogRoutes),
CommonModule,
MomentModule,
DataModule,
SwiperModule,
ThingTranslateModule.forChild(),
UtilModule,
],
providers: [SettingsProvider],
})
export class DashboardModule {}

View File

@@ -0,0 +1,87 @@
/*
* 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 {Injectable} from '@angular/core';
import {
SCBooleanFilterArguments,
SCMessage,
SCSearchBooleanFilter,
SCSearchFilter,
SCSearchQuery,
} from '@openstapps/core';
import {DataProvider} from '../data/data.provider';
/**
* Service for providing catalog and semester data
*/
@Injectable({
providedIn: 'root',
})
export class DashboardProvider {
constructor(private readonly dataProvider: DataProvider) {}
/**
* Get news messages
*
* @param size How many messages/news to fetch
* @param from From which (results) page to start
* @param filters Additional filters to apply
*/
async getNews(
size: number,
from: number,
filters?: SCSearchFilter[],
): Promise<SCMessage[]> {
const query: SCSearchQuery = {
filter: {
type: 'boolean',
arguments: {
filters: [
{
type: 'value',
arguments: {
field: 'type',
value: 'message',
},
},
],
operation: 'and',
},
},
sort: [
{
type: 'generic',
arguments: {
field: 'datePublished',
},
order: 'desc',
},
],
size: size,
from: from,
};
if (typeof filters !== 'undefined') {
for (const filter of filters) {
(
(query.filter as SCSearchBooleanFilter)
.arguments as SCBooleanFilterArguments
).filters.push(filter);
}
}
const result = await this.dataProvider.search(query);
return result.data as SCMessage[];
}
}

View File

@@ -0,0 +1,4 @@
export enum EditModalTypeEnum {
CHECKBOXES,
RADIOBOXES,
}

View File

@@ -0,0 +1,51 @@
<ion-header translucent>
<ion-toolbar mode="ios">
<ion-title>{{ 'modal.settings' | translate }}</ion-title>
<ion-button fill="clear" slot="end" (click)="dismissModal()">
<ion-icon name="x"></ion-icon>
</ion-button>
</ion-toolbar>
</ion-header>
<ion-content fullscreen>
<ng-container [ngSwitch]="true">
<ion-reorder-group
*ngSwitchCase="type === types.CHECKBOXES"
disabled="false"
(ionItemReorder)="doReorder($event)"
>
<!-- Default reorder icon, end aligned items -->
<ion-item *ngFor="let item of reorderedItems">
<ion-reorder slot="start"></ion-reorder>
<ion-label>{{ item.label | translate }}</ion-label>
<ion-toggle
slot="end"
[checked]="item.active"
[(ngModel)]="item.active"
></ion-toggle>
</ion-item>
</ion-reorder-group>
<ion-radio-group
*ngSwitchCase="type === types.RADIOBOXES"
[(ngModel)]="selectedValue"
>
<ion-list-header>
<ion-label>{{
'dashboard.canteens.choose_favorite' | translate
}}</ion-label>
</ion-list-header>
<ion-item *ngFor="let item of items">
<ion-label>{{ item.name }}</ion-label>
<ion-radio slot="end" [value]="item.uid"></ion-radio>
</ion-item>
</ion-radio-group>
</ng-container>
</ion-content>
<ion-footer class="ion-text-end">
<ion-button fill="clear" class="ion-margin-end" (click)="onSaveClick()">{{
'save' | translate
}}</ion-button>
<ion-button fill="clear" (click)="dismissModal()">{{
'abort' | translate
}}</ion-button>
</ion-footer>

View File

@@ -0,0 +1,3 @@
:host {
--width: 100vw;
}

View File

@@ -0,0 +1,67 @@
/*
* Copyright (C) 2022 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 {Component, Input, OnInit, ViewChild} from '@angular/core';
import {IonReorderGroup, ModalController} from '@ionic/angular';
import {ItemReorderEventDetail} from '@ionic/core';
import {SCThings} from '@openstapps/core';
import {MenuItemInterface} from '../sections/navigation-section/menu-item.interface';
import {EditModalTypeEnum} from './edit-modal-type.enum';
/**
* Shows a modal window to sort and enable/disable menu items
*/
@Component({
selector: 'stapps-edit-modal',
templateUrl: 'edit-modal.component.html',
styleUrls: ['edit-modal.component.scss'],
})
export class EditModalComponent implements OnInit {
@ViewChild(IonReorderGroup) reorderGroup: IonReorderGroup;
@Input() type: EditModalTypeEnum = EditModalTypeEnum.CHECKBOXES;
@Input() items: MenuItemInterface[] | SCThings[];
@Input() selectedValue: string;
reorderedItems: MenuItemInterface[] | SCThings[];
types = EditModalTypeEnum;
constructor(public modalController: ModalController) {}
ngOnInit() {
this.reorderedItems = this.items;
}
ionViewWillLeave() {
this.dismissModal();
}
doReorder(event: CustomEvent<ItemReorderEventDetail>) {
this.reorderedItems = event.detail.complete(this.reorderedItems);
}
onSaveClick() {
this.modalController.dismiss({
items: this.reorderedItems,
selectedValue: this.selectedValue,
});
}
dismissModal() {
this.modalController.dismiss();
}
}

View File

@@ -0,0 +1,10 @@
<ion-label class="section-headline"
>{{ title }}
<ion-icon name="edit" *ngIf="isEditable" (click)="onEditClick()"></ion-icon>
<ion-icon
[name]="customIcon"
*ngIf="customIcon"
(click)="onEditClick()"
></ion-icon>
</ion-label>
<ng-content></ng-content>

View File

@@ -0,0 +1,55 @@
@import '../../../../theme/util/mixins';
:host {
display: block;
padding: var(--spacing-md) var(--spacing-md) var(--spacing-sm);
@include ion-md-up {
padding: var(--spacing-lg) var(--spacing-xxl) var(--spacing-sm);
}
&.is-editable ::ng-deep {
.swiper-button-prev {
right: 65px;
}
.swiper-button-next {
right: 35px;
}
}
&.is-extended {
padding-right: 0;
ion-icon[name='edit'] {
margin-right: var(--spacing-md);
}
}
// TODO
&:first-of-type {
padding-top: var(--spacing-lg);
}
& > ion-label:first-child {
font-family: var(--headline-font-family);
font-size: var(--font-size-lg);
font-weight: var(--font-weight-bold);
font-stretch: condensed;
text-transform: uppercase;
margin-bottom: var(--spacing-xs);
width: 100%;
display: flex;
flex-direction: revert;
justify-content: space-between;
ion-icon {
color: var(--ion-color-medium-shade);
width: 25px;
height: 25px;
position: relative;
bottom: var(--spacing-sm);
cursor: pointer;
}
}
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright (C) 2022 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 {
Component,
EventEmitter,
HostBinding,
Input,
OnInit,
Output,
} from '@angular/core';
/**
* Shows a horizontal list of action chips
*/
@Component({
selector: 'stapps-section',
templateUrl: 'section.component.html',
styleUrls: ['section.component.scss'],
})
export class SectionComponent implements OnInit {
@HostBinding('class.is-extended') isExtendedClass = false;
@HostBinding('class.is-editable') isEditableClass = false;
@Input() title = '';
@Input() isSectionExtended = false;
@Input() isEditable = false;
@Input() customIcon?: string = undefined;
// eslint-disable-next-line @angular-eslint/no-output-on-prefix
@Output() onEdit = new EventEmitter<void>();
ngOnInit() {
this.isExtendedClass = this.isSectionExtended;
this.isEditableClass = this.isEditable;
}
/**
* Action when edit is clicked
*/
onEditClick() {
this.onEdit.emit();
}
}

View File

@@ -0,0 +1,31 @@
<stapps-section
[title]="'dashboard.favorites.title' | translate"
[isEditable]="true"
(onEdit)="onSectionEdit()"
>
<div *ngIf="(items | async)?.length" class="container">
<div
*ngFor="let item of items | async"
class="card clickable"
(click)="notifySelect(item)"
>
<ion-thumbnail class="ion-margin-end">
<ion-icon color="dark" [attr.name]="item.type | dataIcon"></ion-icon>
</ion-thumbnail>
<ion-label>
{{ 'name' | thingTranslate: item }}
</ion-label>
</div>
</div>
<ng-container *ngIf="!(items | async)?.length">
<div class="card">
<ion-label>
{{ 'dashboard.favorites.no_favorite_prefix' | translate }}
<a (click)="onSectionEdit()">{{
'dashboard.favorites.no_favorite_link' | translate
}}</a>
{{ 'dashboard.favorites.no_favorite_suffix' | translate }}
</ion-label>
</div>
</ng-container>
</stapps-section>

View File

@@ -0,0 +1,36 @@
.container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: var(--spacing-sm);
--size: 60px;
& > * {
display: inline-block;
position: relative;
overflow: hidden;
width: 100%;
min-height: var(--size);
margin-bottom: var(--spacing-sm);
ion-thumbnail {
position: absolute;
bottom: 0;
right: 0;
z-index: 1;
transform: translate(10%, 20%);
margin: 0 auto var(--spacing-xs);
--size: 60px;
ion-icon {
width: var(--size);
height: var(--size);
margin: auto;
--ion-color-base: var(--ion-color-light-icon) !important;
}
}
ion-label {
position: relative;
z-index: 2;
}
}
}

View File

@@ -0,0 +1,145 @@
/*
* Copyright (C) 2022 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 {Component, OnInit} from '@angular/core';
import {ActivatedRoute, Router} from '@angular/router';
import {AlertController} from '@ionic/angular';
import {combineLatest} from 'rxjs';
import {
debounceTime,
distinctUntilChanged,
startWith,
take,
} from 'rxjs/operators';
import {NGXLogger} from 'ngx-logger';
import {SCThings} from '@openstapps/core';
import {DataProvider} from '../../../data/data.provider';
import {DataRoutingService} from '../../../data/data-routing.service';
import {SearchPageComponent} from '../../../data/list/search-page.component';
import {PositionService} from '../../../map/position.service';
import {SettingsProvider} from '../../../settings/settings.provider';
import {FavoritesService} from '../../../favorites/favorites.service';
import {ContextMenuService} from '../../../menu/context/context-menu.service';
import {ConfigProvider} from '../../../config/config.provider';
/**
* Shows a section with meals of the chosen mensa
*/
@Component({
selector: 'stapps-favorites-section',
templateUrl: 'favorites-section.component.html',
styleUrls: ['favorites-section.component.scss'],
})
export class FavoritesSectionComponent
extends SearchPageComponent
implements OnInit
{
constructor(
protected readonly alertController: AlertController,
protected dataProvider: DataProvider,
protected readonly contextMenuService: ContextMenuService,
protected readonly settingsProvider: SettingsProvider,
protected readonly logger: NGXLogger,
protected dataRoutingService: DataRoutingService,
protected router: Router,
route: ActivatedRoute,
positionService: PositionService,
private favoritesService: FavoritesService,
configProvider: ConfigProvider,
) {
super(
alertController,
dataProvider,
contextMenuService,
settingsProvider,
logger,
dataRoutingService,
router,
route,
positionService,
configProvider,
);
}
async initialize() {
this.subscriptions.push(
combineLatest([
this.queryTextChanged.pipe(
debounceTime(this.searchQueryDueTime),
distinctUntilChanged(),
startWith(this.queryText),
),
this.favoritesService.favoritesChanged$,
]).subscribe(async () => {
await this.fetchAndUpdateItems();
this.queryChanged.next();
}),
);
}
/**
* Fetches/updates the favorites (search page component's method override)
*/
async fetchAndUpdateItems() {
this.favoritesService
.search(this.queryText, this.filterQuery, this.sortQuery)
.pipe(take(1))
.subscribe(result => {
this.items = new Promise(resolve => {
resolve(
result.data && result.data.filter(item => !this.isMensaThing(item)),
);
});
});
}
/**
* Helper function as 'typeof' is not accessible in HTML
*
* @param item TODO
*/
isMensaThing(item: SCThings): boolean {
return (
this.hasCategories(item) &&
((item.categories as string[]).includes('canteen') ||
(item.categories as string[]).includes('cafe') ||
(item.categories as string[]).includes('student canteen') ||
(item.categories as string[]).includes('restaurant'))
);
}
/**
* TODO
*
* @param item TODO
*/
hasCategories(item: SCThings): item is SCThings & {categories: string[]} {
return typeof (item as {categories: string[]}).categories !== 'undefined';
}
/**
* Emit event that an item was selected
*/
notifySelect(item: SCThings) {
this.dataRoutingService.emitChildEvent(item);
}
/**
* Action when user clicked edit to this section
*/
onSectionEdit() {
void this.router.navigate(['/search']);
}
}

View File

@@ -0,0 +1,17 @@
<swiper
[config]="sliderOptions"
[navigation]="true"
class="mensa-swiper card-swiper"
*ngIf="dishes && dishes.length > 0"
>
<ng-template swiperSlide *ngFor="let dish of dishes">
<a [routerLink]="'/data-detail/' + dish.uid" class="card">
<ion-label>{{ 'name' | thingTranslate: dish }}</ion-label>
</a>
</ng-template>
</swiper>
<div class="card" *ngIf="!dishes || dishes.length === 0">
<ion-label>
{{ 'dashboard.canteens.no_dishes_available' | translate }}
</ion-label>
</div>

View File

@@ -0,0 +1,77 @@
/*
* Copyright (C) 2022 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 {
Component,
Input,
OnInit,
OnChanges,
SimpleChanges,
} from '@angular/core';
import {SCDish, SCPlace, SCThings} from '@openstapps/core';
import {PlaceMensaService} from '../../../data/types/place/special/mensa/place-mensa-service';
/**
* Shows a section with meals of the chosen mensa
*/
@Component({
selector: 'stapps-mensa-section-content',
templateUrl: 'mensa-section-content.component.html',
styleUrls: ['mensa-section.component.scss'],
})
export class MensaSectionContentComponent implements OnInit, OnChanges {
/**
* Slider options
*/
sliderOptions = {
spaceBetween: 12,
freeMode: {
enabled: true,
sticky: true,
},
width: 120,
};
/**
* Map of dishes for each day
*/
// eslint-disable-next-line unicorn/no-null
dishes: SCDish[] | null = [];
@Input() items: SCThings[];
constructor(private readonly mensaService: PlaceMensaService) {}
async ngOnInit() {
await this.getDishes();
}
async ngOnChanges(changes: SimpleChanges) {
if (typeof changes.items !== 'undefined') {
await this.getDishes();
}
}
/**
* Request dishes
*/
async getDishes() {
if (this.items) {
for (const item of this.items) {
const dishes = await this.mensaService.getAllDishes(item as SCPlace, 1);
this.dishes?.push(...dishes[Object.keys(dishes)[0]]);
}
}
}
}

View File

@@ -0,0 +1,22 @@
<stapps-section
[title]="'dashboard.canteens.title' | translate"
[isEditable]="true"
(onEdit)="onSectionEdit()"
>
<ng-container *ngIf="(items | async)?.length">
<stapps-mensa-section-content
[items]="items | async"
></stapps-mensa-section-content>
</ng-container>
<ng-container *ngIf="!(items | async)?.length">
<div class="card">
<ion-label>
{{ 'dashboard.canteens.no_favorite_prefix' | translate }}
<a (click)="onSectionEdit()">{{
'dashboard.canteens.no_favorite_link' | translate
}}</a>
{{ 'dashboard.canteens.no_favorite_suffix' | translate }}
</ion-label>
</div>
</ng-container>
</stapps-section>

View File

@@ -0,0 +1,148 @@
/*
* Copyright (C) 2022 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 {Component} from '@angular/core';
import {ActivatedRoute, Router} from '@angular/router';
import {AlertController, ModalController} from '@ionic/angular';
import {combineLatest, Subscription} from 'rxjs';
import {
debounceTime,
distinctUntilChanged,
startWith,
take,
} from 'rxjs/operators';
import {NGXLogger} from 'ngx-logger';
import {SCThings} from '@openstapps/core';
import {DataProvider} from '../../../data/data.provider';
import {DataRoutingService} from '../../../data/data-routing.service';
import {FoodDataListComponent} from '../../../data/list/food-data-list.component';
import {PositionService} from '../../../map/position.service';
import {SettingsProvider} from '../../../settings/settings.provider';
import {FavoritesService} from '../../../favorites/favorites.service';
import {ContextMenuService} from '../../../menu/context/context-menu.service';
import {ConfigProvider} from '../../../config/config.provider';
/**
* Shows a section with meals of the chosen mensa
*/
@Component({
selector: 'stapps-mensa-section',
templateUrl: 'mensa-section.component.html',
styleUrls: ['mensa-section.component.scss'],
})
export class MensaSectionComponent extends FoodDataListComponent {
sub: Subscription;
constructor(
protected readonly alertController: AlertController,
protected dataProvider: DataProvider,
protected readonly contextMenuService: ContextMenuService,
protected readonly settingsProvider: SettingsProvider,
protected readonly logger: NGXLogger,
protected dataRoutingService: DataRoutingService,
protected router: Router,
route: ActivatedRoute,
protected positionService: PositionService,
public modalController: ModalController,
protected favoritesService: FavoritesService,
configProvider: ConfigProvider,
) {
super(
alertController,
dataProvider,
contextMenuService,
settingsProvider,
logger,
dataRoutingService,
router,
route,
positionService,
configProvider,
);
}
async initialize() {
super.initialize();
this.subscriptions.push(
combineLatest([
this.queryTextChanged.pipe(
debounceTime(this.searchQueryDueTime),
distinctUntilChanged(),
startWith(this.queryText),
),
this.favoritesService.favoritesChanged$,
]).subscribe(async query => {
this.queryText = query[0];
this.from = 0;
if (
typeof this.filterQuery !== 'undefined' ||
this.queryText?.length > 0 ||
this.showDefaultData
) {
await this.fetchAndUpdateItems();
this.queryChanged.next();
}
}),
);
}
/**
* Fetches/updates the favorites (search page component's method override)
*/
async fetchAndUpdateItems() {
this.favoritesService
.search(this.queryText, this.filterQuery, this.sortQuery)
.pipe(take(1))
.subscribe(result => {
this.items = new Promise(resolve => {
resolve(
result.data && result.data.filter(item => this.isMensaThing(item)),
);
});
});
}
/**
* Helper function as 'typeof' is not accessible in HTML
*
* @param item TODO
*/
isMensaThing(item: SCThings): boolean {
return (
this.hasCategories(item) &&
((item.categories as string[]).includes('canteen') ||
(item.categories as string[]).includes('cafe') ||
(item.categories as string[]).includes('student canteen') ||
(item.categories as string[]).includes('restaurant'))
);
}
/**
* TODO
*
* @param item TODO
*/
hasCategories(item: SCThings): item is SCThings & {categories: string[]} {
return typeof (item as {categories: string[]}).categories !== 'undefined';
}
/**
* Action when user clicked edit to this section
*/
onSectionEdit() {
void this.router.navigate(['/canteen']);
}
}

View File

@@ -0,0 +1,6 @@
export interface MenuItemInterface {
icon: string;
label: string;
link: string;
active: boolean;
}

View File

@@ -0,0 +1,34 @@
import {MenuItemInterface} from './menu-item.interface';
export const MenuItems: MenuItemInterface[] = [
{
icon: 'book',
label: 'dashboard.navigation.item.catalog',
link: '/catalog',
active: true,
},
{
icon: 'tools-kitchen',
label: 'dashboard.navigation.item.canteen',
link: '/canteen',
active: true,
},
{
icon: 'map',
label: 'dashboard.navigation.item.map',
link: '/map',
active: true,
},
{
icon: 'settings',
label: 'dashboard.navigation.item.settings',
link: '/settings',
active: true,
},
{
icon: 'search',
label: 'dashboard.navigation.item.search',
link: '/search',
active: false,
},
];

View File

@@ -0,0 +1,19 @@
<stapps-section
[title]="'dashboard.navigation.title' | translate"
[isEditable]="true"
[isSectionExtended]="true"
(onEdit)="onSectionEdit()"
>
<swiper
[config]="sliderOptions"
slidesPerView="auto"
class="navigation-swiper card-swiper"
>
<ng-template swiperSlide *ngFor="let menuItem of activeMenuItems">
<a [routerLink]="menuItem.link" class="card">
<ion-icon [name]="menuItem.icon"></ion-icon>
<ion-label>{{ menuItem.label | translate }}</ion-label>
</a>
</ng-template>
</swiper>
</stapps-section>

View File

@@ -0,0 +1,21 @@
.navigation-swiper.swiper {
.swiper-slide {
a {
font-family: var(--ion-font-family);
font-size: var(--font-size-xs);
font-weight: var(--font-weight-semi-bold);
text-align: center;
padding: var(--spacing-md) var(--spacing-lg);
justify-content: center;
}
ion-icon {
display: block;
width: 40px;
height: 40px;
margin: auto;
}
}
}

View File

@@ -0,0 +1,111 @@
/*
* Copyright (C) 2022 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 {Component, OnInit, ViewEncapsulation} from '@angular/core';
import {ModalController} from '@ionic/angular';
import {EditModalComponent} from '../../edit-modal/edit-modal.component';
import {MenuItems} from './menu-items.config';
import {MenuItemInterface} from './menu-item.interface';
import {EditModalTypeEnum} from '../../edit-modal/edit-modal-type.enum';
import {StorageProvider} from '../../../storage/storage.provider';
const DASHBOARD_NAVIGATION = 'stapps.dashboard.navigation';
/**
* Shows a horizontal list of navigation items
*/
@Component({
selector: 'stapps-navigation-section',
templateUrl: 'navigation-section.component.html',
styleUrls: ['navigation-section.component.scss'],
encapsulation: ViewEncapsulation.None,
})
export class NavigationSectionComponent implements OnInit {
/**
* Slider options
*/
sliderOptions = {
spaceBetween: 12,
freeMode: {
enabled: true,
sticky: true,
},
width: 120,
};
menuItems: MenuItemInterface[] = MenuItems;
activeMenuItems: MenuItemInterface[] = MenuItems;
constructor(
public modalController: ModalController,
private storageProvider: StorageProvider,
) {}
ngOnInit() {
void this.getItems();
}
/**
* Get current order of items
*/
async getItems() {
if (await this.storageProvider.has(DASHBOARD_NAVIGATION)) {
const storedMenuItems: string = await this.storageProvider.get(
DASHBOARD_NAVIGATION,
);
if (storedMenuItems) {
const parsedMenuItems = JSON.parse(storedMenuItems);
if (Array.isArray(parsedMenuItems)) {
this.menuItems = parsedMenuItems;
this.activeMenuItems = parsedMenuItems.filter(item => item.active);
}
}
}
}
/**
* Save updated order of items
*
* @param items List of items
*/
setItems(items: MenuItemInterface[]) {
this.menuItems = items;
this.activeMenuItems = items.filter(item => item.active);
void this.storageProvider.put<string>(
DASHBOARD_NAVIGATION,
JSON.stringify(items),
);
}
/**
* Action when user clicked edit to this section
*/
async onSectionEdit() {
const modal = await this.modalController.create({
component: EditModalComponent,
componentProps: {
items: this.menuItems,
type: EditModalTypeEnum.CHECKBOXES,
},
});
await modal.present();
modal.onDidDismiss().then(result => {
if (result.data?.items) {
this.setItems(result.data.items);
}
});
}
}

View File

@@ -0,0 +1,32 @@
<stapps-section
[title]="'dashboard.news.title' | translate"
[isEditable]="false"
[customIcon]="'news'"
class="is-editable"
(onEdit)="onMoreNewsClicked()"
>
<swiper
[config]="sliderOptions"
slidesPerView="auto"
[navigation]="true"
*ngIf="news.length > 0"
class="news-swiper card-swiper"
>
<ng-template swiperSlide *ngFor="let newsItem of news">
<a [routerLink]="['/data-detail', newsItem.uid]" class="card">
<ion-img [src]="newsItem.image"></ion-img>
<ion-label>{{ newsItem.name }}</ion-label>
</a>
</ng-template>
<ng-template swiperSlide>
<a [routerLink]="['/news']" class="card more-news">
<ion-label>{{
'dashboard.news.moreNews' | translate | titlecase
}}</ion-label>
<ion-thumbnail class="ion-margin-end">
<ion-icon color="dark" name="news"></ion-icon>
</ion-thumbnail>
</a>
</ng-template>
</swiper>
</stapps-section>

View File

@@ -0,0 +1,48 @@
.news-swiper.swiper {
.swiper-slide {
padding: 0;
.card {
padding: 0;
ion-img {
border-radius: var(--border-radius-default) var(--border-radius-default) 0 0;
overflow: hidden;
}
ion-label {
margin: var(--spacing-lg);
text-align: left;
font-size: var(--font-size-sm);
font-weight: var(--font-weight-bold);
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
&.more-news {
ion-label {
font-size: var(--font-size-lg);
}
ion-thumbnail {
position: absolute;
bottom: 0;
right: 0;
z-index: 1;
margin: 0 auto var(--spacing-xs);
--size: 160px;
ion-icon {
width: var(--size);
height: var(--size);
margin: auto;
--ion-color-base: var(--ion-color-light-icon) !important;
}
}
}
}
}
}

View File

@@ -0,0 +1,95 @@
/*
* Copyright (C) 2022 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 {Component, OnInit, ViewEncapsulation} from '@angular/core';
import {Router} from '@angular/router';
import {NewsPageComponent} from '../../../news/page/news-page.component';
import {NewsProvider} from '../../../news/news.provider';
import {SettingsProvider} from '../../../settings/settings.provider';
import {
newsFilterSettingsFieldsMapping,
NewsFilterSettingsNames,
} from '../../../news/news-filter-settings';
import {DataProvider} from '../../../data/data.provider';
import {SCSearchValueFilter} from '@openstapps/core';
/**
* Shows a section with news
*/
@Component({
selector: 'stapps-news-section',
templateUrl: 'news-section.component.html',
styleUrls: ['news-section.component.scss'],
encapsulation: ViewEncapsulation.None,
})
export class NewsSectionComponent extends NewsPageComponent implements OnInit {
/**
* Slider options
*/
sliderOptions = {
spaceBetween: 12,
freeMode: {
enabled: true,
sticky: true,
},
width: 240,
breakpoints: {
768: {
width: 280,
},
},
};
pageSize = 5;
/**
* A map of the filters where the keys are settings names
*/
filtersMap = new Map<NewsFilterSettingsNames, SCSearchValueFilter>();
constructor(
newsProvider: NewsProvider,
settingsProvider: SettingsProvider,
private router: Router,
) {
super(newsProvider, settingsProvider);
}
async ngOnInit() {
await super.ngOnInit();
for (const setting of this.settings) {
this.filtersMap.set(
setting.name as NewsFilterSettingsNames,
DataProvider.createValueFilter(
newsFilterSettingsFieldsMapping[
setting.name as NewsFilterSettingsNames
],
setting.value as string,
),
);
}
this.filters = [...this.filtersMap.values()];
try {
await this.fetchNews();
} catch {
this.news = [];
}
}
onMoreNewsClicked() {
void this.router.navigate(['/news']);
}
}

View File

@@ -0,0 +1,19 @@
<stapps-section
title="{{ 'dashboard.navigation.item.search' | translate }}"
[isEditable]="false"
>
<div class="searchbar">
<ion-input
type="search"
placeholder="{{ 'search.search_bar.placeholder' | translate }}"
(submit)="onSubmitSearch()"
(keyup.enter)="onSubmitSearch()"
[(ngModel)]="searchTerm"
></ion-input>
<ion-icon
name="search"
(click)="onSubmitSearch()"
class="clickable"
></ion-icon>
</div>
</stapps-section>

View File

@@ -0,0 +1,24 @@
.searchbar {
position: relative;
max-width: 700px;
ion-input {
background: var(--ion-color-field-bg);
border-radius: var(--border-radius-default);
--padding-start: var(--spacing-md);
--padding-end: var(--spacing-md);
--padding-top: var(--spacing-xl);
--padding-bottom: var(--spacing-xl);
font-size: var(--font-size-xs);
box-shadow: var(--shadow-default);
}
ion-icon {
position: absolute;
top: 50%;
right: var(--spacing-md);
transform: translateY(-50%);
z-index: 2;
width: 25px;
height: 25px;
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright (C) 2022 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 {Component} from '@angular/core';
import {Router} from '@angular/router';
/**
* Shows a search input field
*/
@Component({
selector: 'stapps-search-section',
templateUrl: 'search-section.component.html',
styleUrls: ['search-section.component.scss'],
})
export class SearchSectionComponent {
searchTerm = '';
constructor(private router: Router) {}
/**
* User submits search
*/
onSubmitSearch() {
this.router.navigate(['/search', this.searchTerm]);
}
}