mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-01-07 22:12:53 +00:00
refactor: simplify favorites and mensa dashboard sections
This commit is contained in:
committed by
Rainer Killinger
parent
47565e51b0
commit
23bd5a431c
34
src/app/_helpers/rxjs/mutation-observer.ts
Normal file
34
src/app/_helpers/rxjs/mutation-observer.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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 {Observable} from 'rxjs';
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export function fromMutationObserver(
|
||||
target: Node,
|
||||
options?: MutationObserverInit,
|
||||
): Observable<MutationRecord[]> {
|
||||
return new Observable(subscriber => {
|
||||
const observer = new MutationObserver(mutations => {
|
||||
subscriber.next(mutations);
|
||||
});
|
||||
observer.observe(target, options);
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -50,6 +50,9 @@
|
||||
ion-content {
|
||||
--background: var(--ion-color-light);
|
||||
--padding-bottom: var(--spacing-xl);
|
||||
&::part(inner-scroll) {
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
}
|
||||
|
||||
.schedule {
|
||||
|
||||
@@ -23,7 +23,6 @@ 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 {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';
|
||||
@@ -46,7 +45,6 @@ const catalogRoutes: Routes = [
|
||||
*/
|
||||
@NgModule({
|
||||
declarations: [
|
||||
EditModalComponent,
|
||||
SearchSectionComponent,
|
||||
NewsSectionComponent,
|
||||
MensaSectionComponent,
|
||||
@@ -69,6 +67,5 @@ const catalogRoutes: Routes = [
|
||||
NewsModule,
|
||||
],
|
||||
providers: [SettingsProvider, TranslatePipe],
|
||||
exports: [EditModalComponent],
|
||||
})
|
||||
export class DashboardModule {}
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
<!--
|
||||
~ 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/>.
|
||||
-->
|
||||
|
||||
<ion-header>
|
||||
<ion-toolbar mode="ios">
|
||||
<ion-title>{{ 'modal.settings' | translate | titlecase }}</ion-title>
|
||||
<ion-button fill="clear" slot="start" (click)="dismissModal()">
|
||||
{{ 'modal.DISMISS_CANCEL' | translate }}
|
||||
</ion-button>
|
||||
<ion-button fill="clear" slot="end" (click)="onSaveClick()">
|
||||
<ion-label>{{ 'modal.DISMISS_CONFIRM' | translate }}</ion-label>
|
||||
</ion-button>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<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 items">
|
||||
<ion-reorder slot="start"></ion-reorder>
|
||||
<ion-label>{{ item.labelLocalized }}</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.labelLocalized }}</ion-label>
|
||||
<ion-radio slot="end" [value]="item.id"></ion-radio>
|
||||
</ion-item>
|
||||
</ion-radio-group>
|
||||
</ng-container>
|
||||
</ion-content>
|
||||
@@ -1,3 +0,0 @@
|
||||
:host {
|
||||
--width: 100vw;
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
/*
|
||||
* 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 {EditModalItem, EditModalTypeEnum} from './edit-modal-type.enum';
|
||||
|
||||
/**
|
||||
* Shows a modal window to sort and enable/disable menu items
|
||||
*/
|
||||
@Component({
|
||||
selector: 'stapps-dashboard-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: EditModalItem[];
|
||||
|
||||
@Input() selectedValue: string;
|
||||
|
||||
reorderedItems: EditModalItem[];
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2022 StApps
|
||||
* Copyright (C) 2023 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.
|
||||
@@ -12,14 +12,8 @@
|
||||
* 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 {animate, style, transition, trigger} from '@angular/animations';
|
||||
|
||||
export enum EditModalTypeEnum {
|
||||
CHECKBOXES,
|
||||
RADIOBOXES,
|
||||
}
|
||||
|
||||
export interface EditModalItem {
|
||||
id: unknown;
|
||||
labelLocalized: string;
|
||||
active: boolean;
|
||||
}
|
||||
export const fadeAnimation = trigger('fade', [
|
||||
transition(':enter', [style({opacity: '0'}), animate(250, style({opacity: '1'}))]),
|
||||
]);
|
||||
28
src/app/modules/dashboard/mensa-filters.ts
Normal file
28
src/app/modules/dashboard/mensa-filters.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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 {SCBuildingCategories, SCThings, SCThingWithCategories} from '@openstapps/core';
|
||||
|
||||
const mensaCategories = new Set<SCBuildingCategories>(['canteen', 'cafe', 'student canteen', 'restaurant']);
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export function isMensaThing(item: SCThings): boolean {
|
||||
return (
|
||||
(item as SCThingWithCategories<string, never>).categories?.some(category =>
|
||||
mensaCategories.has(category as never),
|
||||
) || false
|
||||
);
|
||||
}
|
||||
@@ -17,9 +17,9 @@
|
||||
<ion-button slot="button-end" fill="clear" color="medium" [routerLink]="['/favorites']">
|
||||
<ion-icon slot="icon-only" name="search" size="24"></ion-icon>
|
||||
</ion-button>
|
||||
<simple-swiper *ngIf="(items | async)?.length; else noItems">
|
||||
<simple-swiper *ngIf="items | async as items; else noItems" @fade>
|
||||
<stapps-data-list-item
|
||||
*ngFor="let item of items | async"
|
||||
*ngFor="let item of items"
|
||||
[hideThumbnail]="true"
|
||||
[favoriteButton]="false"
|
||||
[item]="item"
|
||||
|
||||
@@ -12,22 +12,11 @@
|
||||
* 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, AnimationController} 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 {ChangeDetectionStrategy, Component} from '@angular/core';
|
||||
import {filter, map} from 'rxjs/operators';
|
||||
import {FavoritesService} from '../../../favorites/favorites.service';
|
||||
import {ContextMenuService} from '../../../menu/context/context-menu.service';
|
||||
import {ConfigProvider} from '../../../config/config.provider';
|
||||
import {fadeAnimation} from '../../fade.animation';
|
||||
import {isMensaThing} from '../../mensa-filters';
|
||||
|
||||
/**
|
||||
* Shows a section with meals of the chosen mensa
|
||||
@@ -36,95 +25,14 @@ import {ConfigProvider} from '../../../config/config.provider';
|
||||
selector: 'stapps-favorites-section',
|
||||
templateUrl: 'favorites-section.component.html',
|
||||
styleUrls: ['favorites-section.component.scss'],
|
||||
animations: [fadeAnimation],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
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,
|
||||
animationController: AnimationController,
|
||||
) {
|
||||
super(
|
||||
alertController,
|
||||
dataProvider,
|
||||
contextMenuService,
|
||||
settingsProvider,
|
||||
logger,
|
||||
dataRoutingService,
|
||||
router,
|
||||
route,
|
||||
positionService,
|
||||
configProvider,
|
||||
animationController,
|
||||
);
|
||||
}
|
||||
export class FavoritesSectionComponent {
|
||||
items = this.favoritesService.favoriteThings$.pipe(
|
||||
map(favorites => favorites.filter(it => !isMensaThing(it))),
|
||||
filter(favorites => favorites.length > 0),
|
||||
);
|
||||
|
||||
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);
|
||||
}
|
||||
constructor(private favoritesService: FavoritesService) {}
|
||||
}
|
||||
|
||||
@@ -12,11 +12,11 @@
|
||||
* 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} from '@angular/core';
|
||||
import {ChangeDetectionStrategy, Component, Input} from '@angular/core';
|
||||
import {SCDish, SCPlace, SCThings} from '@openstapps/core';
|
||||
import {PlaceMensaService} from '../../../data/types/place/special/mensa/place-mensa-service';
|
||||
import {animate, style, transition, trigger} from '@angular/animations';
|
||||
import moment from 'moment';
|
||||
import {fadeAnimation} from '../../fade.animation';
|
||||
|
||||
/**
|
||||
* Shows a section with meals of the chosen mensa
|
||||
@@ -25,11 +25,8 @@ import moment from 'moment';
|
||||
selector: 'stapps-mensa-section-content',
|
||||
templateUrl: 'mensa-section-content.component.html',
|
||||
styleUrls: ['mensa-section-content.component.scss'],
|
||||
animations: [
|
||||
trigger('fade', [
|
||||
transition(':enter', [style({opacity: '0'}), animate('500ms ease', style({opacity: '1'}))]),
|
||||
]),
|
||||
],
|
||||
animations: [fadeAnimation],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class MensaSectionContentComponent {
|
||||
/**
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<ion-item class="nothing-selected" lines="none">
|
||||
<ion-label class="ion-text-wrap">
|
||||
{{ 'dashboard.canteens.no_favorite_prefix' | translate }}
|
||||
<a (click)="onSectionEdit()">{{ 'dashboard.canteens.no_favorite_link' | translate }}</a>
|
||||
<a [routerLink]="['/canteen']">{{ 'dashboard.canteens.no_favorite_link' | translate }}</a>
|
||||
{{ 'dashboard.canteens.no_favorite_suffix' | translate }}
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
@@ -12,23 +12,11 @@
|
||||
* 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, AnimationController, 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 {ChangeDetectionStrategy, Component} from '@angular/core';
|
||||
import {map} from 'rxjs/operators';
|
||||
import {FavoritesService} from '../../../favorites/favorites.service';
|
||||
import {ContextMenuService} from '../../../menu/context/context-menu.service';
|
||||
import {ConfigProvider} from '../../../config/config.provider';
|
||||
import {animate, style, transition, trigger} from '@angular/animations';
|
||||
import {fadeAnimation} from '../../fade.animation';
|
||||
import {isMensaThing} from '../../mensa-filters';
|
||||
|
||||
/**
|
||||
* Shows a section with meals of the chosen mensa
|
||||
@@ -37,107 +25,11 @@ import {animate, style, transition, trigger} from '@angular/animations';
|
||||
selector: 'stapps-mensa-section',
|
||||
templateUrl: 'mensa-section.component.html',
|
||||
styleUrls: ['mensa-section.component.scss'],
|
||||
animations: [
|
||||
trigger('fade', [transition(':enter', [style({opacity: '0'}), animate(250, style({opacity: '1'}))])]),
|
||||
],
|
||||
animations: [fadeAnimation],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class MensaSectionComponent extends FoodDataListComponent {
|
||||
sub: Subscription;
|
||||
export class MensaSectionComponent {
|
||||
items = this.favoritesService.favoriteThings$.pipe(map(favorites => favorites.filter(isMensaThing)));
|
||||
|
||||
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,
|
||||
animationController: AnimationController,
|
||||
) {
|
||||
super(
|
||||
alertController,
|
||||
dataProvider,
|
||||
contextMenuService,
|
||||
settingsProvider,
|
||||
logger,
|
||||
dataRoutingService,
|
||||
router,
|
||||
route,
|
||||
positionService,
|
||||
configProvider,
|
||||
animationController,
|
||||
);
|
||||
}
|
||||
|
||||
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']);
|
||||
}
|
||||
constructor(protected favoritesService: FavoritesService) {}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ simple-swiper {
|
||||
}
|
||||
|
||||
.more-news {
|
||||
width: 128px;
|
||||
font-size: var(--font-size-xl);
|
||||
--color: var(--ion-color-medium-tint);
|
||||
|
||||
|
||||
@@ -12,11 +12,9 @@
|
||||
* 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 {ChangeDetectionStrategy, Component} from '@angular/core';
|
||||
import {NewsProvider} from '../../../news/news.provider';
|
||||
import {SCMessage} from '@openstapps/core';
|
||||
import {animate, style, transition, trigger} from '@angular/animations';
|
||||
import {Router} from '@angular/router';
|
||||
import {fadeAnimation} from '../../fade.animation';
|
||||
|
||||
/**
|
||||
* Shows a section with news
|
||||
@@ -25,21 +23,14 @@ import {Router} from '@angular/router';
|
||||
selector: 'stapps-news-section',
|
||||
templateUrl: 'news-section.component.html',
|
||||
styleUrls: ['news-section.component.scss'],
|
||||
animations: [
|
||||
trigger('fade', [
|
||||
transition(':enter', [
|
||||
style({opacity: '0', transform: 'translateX(100px)'}),
|
||||
animate('250ms ease', style({opacity: '1', transform: 'translateX(0)'})),
|
||||
]),
|
||||
]),
|
||||
],
|
||||
animations: [fadeAnimation],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class NewsSectionComponent {
|
||||
news: Promise<SCMessage[]>;
|
||||
news = this.newsProvider
|
||||
.getCurrentFilters()
|
||||
// eslint-disable-next-line unicorn/prefer-top-level-await,unicorn/consistent-function-scoping
|
||||
.then(filters => this.newsProvider.getList(5, 0, filters));
|
||||
|
||||
constructor(readonly newsProvider: NewsProvider, readonly router: Router) {
|
||||
this.news = this.newsProvider
|
||||
.getCurrentFilters()
|
||||
.then(filters => this.newsProvider.getList(5, 0, filters));
|
||||
}
|
||||
constructor(readonly newsProvider: NewsProvider) {}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
<stapps-section title="{{ 'dashboard.navigation.item.search' | translate }}">
|
||||
<ion-searchbar
|
||||
[routerLink]="'/search'"
|
||||
[routerLink]="['/search']"
|
||||
[routerAnimation]="routeTransition"
|
||||
class="stapps-searchbar ion-activatable ripple-parent"
|
||||
>
|
||||
|
||||
@@ -13,10 +13,7 @@
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {Component} from '@angular/core';
|
||||
import {Router} from '@angular/router';
|
||||
import {Capacitor} from '@capacitor/core';
|
||||
import {Keyboard} from '@capacitor/keyboard';
|
||||
import {AnimationBuilder, AnimationController} from '@ionic/angular';
|
||||
import {AnimationController} from '@ionic/angular';
|
||||
import {homePageSearchTransition} from './search-route-transition';
|
||||
|
||||
/**
|
||||
@@ -28,29 +25,7 @@ import {homePageSearchTransition} from './search-route-transition';
|
||||
styleUrls: ['search-section.component.scss'],
|
||||
})
|
||||
export class SearchSectionComponent {
|
||||
searchTerm = '';
|
||||
routeTransition = homePageSearchTransition(this.animationController);
|
||||
|
||||
routeTransition: AnimationBuilder;
|
||||
|
||||
constructor(private router: Router, private animationController: AnimationController) {
|
||||
this.routeTransition = homePageSearchTransition(this.animationController);
|
||||
}
|
||||
|
||||
/**
|
||||
* User submits search
|
||||
*/
|
||||
onSubmitSearch() {
|
||||
this.router
|
||||
.navigate(['/search'], {queryParams: {query: this.searchTerm}})
|
||||
.then(() => this.hideKeyboard());
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides keyboard in native app environments
|
||||
*/
|
||||
hideKeyboard() {
|
||||
if (Capacitor.isNativePlatform()) {
|
||||
Keyboard.hide();
|
||||
}
|
||||
}
|
||||
constructor(private animationController: AnimationController) {}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,8 @@ export class FavoritesService {
|
||||
// using debounce time 0 allows change detection to run through async suspension
|
||||
favoritesChanged$ = this.favorites.pipe(debounceTime(0));
|
||||
|
||||
favoriteThings$ = this.favoritesChanged$.pipe(map(favorite => [...favorite.values()].map(it => it.data)));
|
||||
|
||||
static getDataFromFavorites(items: SCFavorite[]) {
|
||||
return items.map(item => item.data);
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
*ngFor="let link of item.links"
|
||||
[routerLink]="link.link"
|
||||
[disabled]="link.needsAuth && !isLoggedIn"
|
||||
[detail]="false"
|
||||
>
|
||||
<div>
|
||||
<ion-icon [name]="link.icon" size="36" color="dark"></ion-icon>
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
@use 'sass:math';
|
||||
|
||||
$width: 108px;
|
||||
|
||||
simple-swiper {
|
||||
@@ -22,7 +24,7 @@ simple-swiper {
|
||||
@each $i in 7, 6, 5, 4, 3, 2, 1 {
|
||||
$max: #{($width + 8px) * $i};
|
||||
@container (inline-size < #{$max}) {
|
||||
--swiper-slide-width: #{100cqi / $i};
|
||||
--swiper-slide-width: #{math.div(100cqi, $i)};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
/*
|
||||
* 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.
|
||||
* Copyright (C) 2023 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.
|
||||
* 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/>.
|
||||
* 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, HostBinding, Input} from '@angular/core';
|
||||
import {ChangeDetectionStrategy, Component, HostBinding, Input} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'stapps-icon',
|
||||
templateUrl: 'icon.html',
|
||||
styleUrls: ['icon.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class IconComponent {
|
||||
@HostBinding('style.--size')
|
||||
|
||||
@@ -25,14 +25,24 @@
|
||||
</ng-template>
|
||||
</ion-col>
|
||||
|
||||
<ng-container *ngIf="swiper">
|
||||
<ng-container *ngIf="swiper | async as swiper">
|
||||
<ion-col size="auto" class="swiper-button">
|
||||
<ion-button fill="clear" color="medium" (click)="slidePrev()" [disabled]="false">
|
||||
<ion-button
|
||||
fill="clear"
|
||||
color="medium"
|
||||
(click)="swiper.scrollBy({left: -swiper.offsetWidth, behavior: 'smooth'})"
|
||||
[disabled]="false"
|
||||
>
|
||||
<ion-icon size="24" slot="icon-only" name="chevron_left"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-col>
|
||||
<ion-col size="auto" class="swiper-button">
|
||||
<ion-button fill="clear" color="medium" (click)="slideNext()" [disabled]="false">
|
||||
<ion-button
|
||||
fill="clear"
|
||||
color="medium"
|
||||
(click)="swiper.scrollBy({left: swiper.offsetWidth, behavior: 'smooth'})"
|
||||
[disabled]="false"
|
||||
>
|
||||
<ion-icon size="24" slot="icon-only" name="chevron_right"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-col>
|
||||
|
||||
@@ -55,6 +55,7 @@ ion-col {
|
||||
}
|
||||
|
||||
:host {
|
||||
transition: height 250ms ease;
|
||||
display: block;
|
||||
padding: var(--spacing-sm) var(--spacing-md) var(--spacing-sm);
|
||||
--swiper-scroll-padding: var(--spacing-md);
|
||||
|
||||
@@ -12,8 +12,11 @@
|
||||
* 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 {AfterContentInit, Component, Input, OnDestroy, ViewContainerRef} from '@angular/core';
|
||||
import {AfterContentInit, ChangeDetectionStrategy, Component, Input, ViewContainerRef} from '@angular/core';
|
||||
import {SCThings} from '@openstapps/core';
|
||||
import {fromMutationObserver} from '../_helpers/rxjs/mutation-observer';
|
||||
import {mergeMap, ReplaySubject, takeLast} from 'rxjs';
|
||||
import {distinctUntilChanged, filter, map, startWith} from 'rxjs/operators';
|
||||
|
||||
/**
|
||||
* Shows a horizontal list of action chips
|
||||
@@ -22,50 +25,34 @@ import {SCThings} from '@openstapps/core';
|
||||
selector: 'stapps-section',
|
||||
templateUrl: 'section.component.html',
|
||||
styleUrls: ['section.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class SectionComponent implements AfterContentInit, OnDestroy {
|
||||
export class SectionComponent implements AfterContentInit {
|
||||
@Input() title = '';
|
||||
|
||||
@Input() item?: SCThings;
|
||||
|
||||
mutationObserver: MutationObserver;
|
||||
nativeElement = new ReplaySubject<HTMLElement>(1);
|
||||
|
||||
swiper?: HTMLElement;
|
||||
swiper = this.nativeElement.pipe(
|
||||
takeLast(1),
|
||||
mergeMap(element =>
|
||||
fromMutationObserver(element, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
}).pipe(
|
||||
startWith([]),
|
||||
map(() => element.querySelector('simple-swiper') as HTMLElement),
|
||||
distinctUntilChanged(),
|
||||
filter(element => !!element),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
constructor(readonly viewContainerRef: ViewContainerRef) {}
|
||||
|
||||
ngAfterContentInit() {
|
||||
this.mutationObserver = new MutationObserver(() => {
|
||||
const simpleSwiper = this.viewContainerRef.element.nativeElement.querySelector('simple-swiper');
|
||||
if (!simpleSwiper) return;
|
||||
|
||||
this.swiper = simpleSwiper;
|
||||
});
|
||||
this.mutationObserver.observe(this.viewContainerRef.element.nativeElement, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
}
|
||||
|
||||
slideNext() {
|
||||
if (this.swiper) {
|
||||
this.swiper.scrollBy({
|
||||
left: this.swiper.offsetWidth,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
slidePrev() {
|
||||
if (this.swiper) {
|
||||
this.swiper.scrollBy({
|
||||
left: -this.swiper.offsetWidth,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.mutationObserver.disconnect();
|
||||
this.nativeElement.next(this.viewContainerRef.element.nativeElement);
|
||||
this.nativeElement.complete();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,15 +13,12 @@
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Component, ContentChildren, ElementRef, ViewContainerRef} from '@angular/core';
|
||||
import {ChangeDetectionStrategy, Component} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'simple-swiper',
|
||||
templateUrl: 'simple-swiper.html',
|
||||
styleUrls: ['simple-swiper.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class SimpleSwiperComponent {
|
||||
constructor(readonly viewContainerRef: ViewContainerRef) {}
|
||||
|
||||
@ContentChildren('*') children: ElementRef<unknown>;
|
||||
}
|
||||
export class SimpleSwiperComponent {}
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
gap: var(--swiper-gap, 0);
|
||||
|
||||
&::ng-deep > *:not(ion-button) {
|
||||
&::ng-deep > * {
|
||||
contain: layout;
|
||||
scroll-snap-align: start;
|
||||
scroll-margin-inline: var(--swiper-scroll-padding, 0);
|
||||
|
||||
@@ -30,9 +30,10 @@ import {ThingTranslateModule} from '../translation/thing-translate.module';
|
||||
import {SimpleSwiperComponent} from './simple-swiper.component';
|
||||
import {SearchbarAutofocusDirective} from './searchbar-autofocus.directive';
|
||||
import {SectionComponent} from './section.component';
|
||||
import {RouterModule} from '@angular/router';
|
||||
|
||||
@NgModule({
|
||||
imports: [BrowserModule, IonicModule, TranslateModule, ThingTranslateModule.forChild()],
|
||||
imports: [BrowserModule, IonicModule, TranslateModule, ThingTranslateModule.forChild(), RouterModule],
|
||||
declarations: [
|
||||
ElementSizeChangeDirective,
|
||||
ArrayLastPipe,
|
||||
|
||||
Reference in New Issue
Block a user