feat: dashboard search rework

This commit is contained in:
Thea Schöbl
2023-03-10 15:46:59 +00:00
committed by Rainer Killinger
parent dff4a95acc
commit 8c30a47706
40 changed files with 375 additions and 781 deletions

View File

@@ -43,7 +43,7 @@
</div>
<ion-content fullscreen="true" #ionContent>
<stapps-search-section #search (focusin)="onSearchBarFocus($event)"></stapps-search-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>

View File

@@ -43,8 +43,6 @@ export class DashboardComponent implements OnInit, OnDestroy {
@ViewChild('schedule', {read: ElementRef}) scheduleRef: ElementRef;
@ViewChild('search', {read: ElementRef}) searchRef: ElementRef;
@ViewChild('ionContent') ionContentRef: IonContent;
collapseAnimation: DashboardCollapse;
@@ -145,8 +143,4 @@ export class DashboardComponent implements OnInit, OnDestroy {
this._eventUuidSubscription.unsubscribe();
this.collapseAnimation.destroy();
}
async onSearchBarFocus(_event: Event) {
this.ionContentRef.scrollToTop(100);
}
}

View File

@@ -24,8 +24,6 @@ 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';
@@ -48,9 +46,7 @@ const catalogRoutes: Routes = [
*/
@NgModule({
declarations: [
SectionComponent,
EditModalComponent,
NavigationSectionComponent,
SearchSectionComponent,
NewsSectionComponent,
MensaSectionComponent,

View File

@@ -13,11 +13,10 @@
~ this program. If not, see <https://www.gnu.org/licenses/>.
-->
<stapps-section
[title]="'dashboard.favorites.title' | translate"
[isEditable]="true"
(onEdit)="onSectionEdit()"
>
<stapps-section [title]="'dashboard.favorites.title' | translate">
<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">
<stapps-data-list-item
*ngFor="let item of items | async"
@@ -31,7 +30,7 @@
<ion-item class="nothing-selected" lines="none">
<ion-label class="ion-text-wrap">
{{ 'dashboard.favorites.no_favorite_prefix' | translate }}
<a (click)="onSectionEdit()">{{ 'dashboard.favorites.no_favorite_link' | translate }}</a>
<a [routerLink]="'/search'">{{ 'dashboard.favorites.no_favorite_link' | translate }}</a>
{{ 'dashboard.favorites.no_favorite_suffix' | translate }}
</ion-label>
</ion-item>

View File

@@ -127,11 +127,4 @@ export class FavoritesSectionComponent extends SearchPageComponent implements On
notifySelect(item: SCThings) {
this.dataRoutingService.emitChildEvent(item);
}
/**
* Action when user clicked edit to this section
*/
onSectionEdit() {
void this.router.navigate(['/search']);
}
}

View File

@@ -16,24 +16,17 @@
<ng-container *ngIf="items | async as items">
<ng-container *ngIf="items.length !== 0; else nothingSelected">
<ng-container *ngFor="let item of items">
<stapps-section
@fade
[item]="item"
[title]="'name' | thingTranslate: item"
[isEditable]="true"
(onEdit)="onSectionEdit()"
>
<stapps-section @fade [item]="item" [title]="'name' | thingTranslate: item">
<ion-button slot="button-end" fill="clear" color="medium" (click)="favoritesService.delete(item)">
<ion-icon slot="icon-only" name="delete" size="24"></ion-icon>
</ion-button>
<stapps-opening-hours slot="subtitle" [openingHours]="item.openingHours"></stapps-opening-hours>
<stapps-mensa-section-content [item]="item"></stapps-mensa-section-content>
</stapps-section>
</ng-container>
</ng-container>
<ng-template #nothingSelected>
<stapps-section
[title]="'dashboard.canteens.title' | translate"
[isEditable]="true"
(onEdit)="onSectionEdit()"
>
<stapps-section [title]="'dashboard.canteens.title' | translate">
<ion-item class="nothing-selected" lines="none">
<ion-label class="ion-text-wrap">
{{ 'dashboard.canteens.no_favorite_prefix' | translate }}

View File

@@ -1,30 +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/>.
*/
export interface MenuItemInterface {
icon: string;
label: string;
link: string;
}
export enum MenuItemKey {
CATALOG = 'catalog',
CANTEEN = 'canteen',
MAP = 'map',
SETTINGS = 'settings',
SEARCH = 'search',
}
export type MenuItemConfig = Record<MenuItemKey, boolean>;

View File

@@ -1,53 +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 {MenuItemInterface, MenuItemKey} from './menu-item.interface';
import {SCIcon} from '../../../../util/ion-icon/icon';
export const MENU_ITEMS: Record<MenuItemKey, MenuItemInterface> = {
catalog: {
icon: SCIcon`book`,
label: 'dashboard.navigation.item.catalog',
link: '/catalog',
},
canteen: {
icon: SCIcon`local_cafe`,
label: 'dashboard.navigation.item.canteen',
link: '/canteen',
},
map: {
icon: SCIcon`map`,
label: 'dashboard.navigation.item.map',
link: '/map',
},
settings: {
icon: SCIcon`settings`,
label: 'dashboard.navigation.item.settings',
link: '/settings',
},
search: {
icon: SCIcon`search`,
label: 'dashboard.navigation.item.search',
link: '/search',
},
};
export const DEFAULT_ACTIVE_MENU_ITEMS: MenuItemKey[] = [
MenuItemKey.CATALOG,
MenuItemKey.CANTEEN,
MenuItemKey.MAP,
MenuItemKey.SETTINGS,
MenuItemKey.SEARCH,
];

View File

@@ -1,30 +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/>.
-->
<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 item of activeMenuItems">
<a [routerLink]="menuItems[item].link" class="card">
<ion-icon size="40" [name]="menuItems[item].icon"></ion-icon>
<ion-label>{{ menuItems[item].label | translate }}</ion-label>
</a>
</ng-template>
</swiper>
</stapps-section>

View File

@@ -1,32 +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/>.
*/
.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;
margin: auto;
}
}
}

View File

@@ -1,111 +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, OnInit, ViewEncapsulation} from '@angular/core';
import {ModalController} from '@ionic/angular';
import {EditModalComponent} from '../../edit-modal/edit-modal.component';
import {DEFAULT_ACTIVE_MENU_ITEMS, MENU_ITEMS} from './menu-items.config';
import {MenuItemKey} from './menu-item.interface';
import {EditModalItem, EditModalTypeEnum} from '../../edit-modal/edit-modal-type.enum';
import {StorageProvider} from '../../../storage/storage.provider';
import {TranslatePipe} from '@ngx-translate/core';
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 = MENU_ITEMS;
activeMenuItems: MenuItemKey[] = DEFAULT_ACTIVE_MENU_ITEMS;
constructor(
public modalController: ModalController,
private storageProvider: StorageProvider,
private translatePipe: TranslatePipe,
) {}
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) && parsedMenuItems.every(it => typeof it === 'string')) {
this.activeMenuItems = parsedMenuItems;
}
}
}
}
/**
* Save updated order of items
*/
updateActiveItems(items: MenuItemKey[]) {
this.activeMenuItems = items;
void this.storageProvider.put<string>(DASHBOARD_NAVIGATION, JSON.stringify(this.activeMenuItems));
}
/**
* Action when user clicked edit to this section
*/
async onSectionEdit() {
const modal = await this.modalController.create({
component: EditModalComponent,
canDismiss: true,
componentProps: {
items: Object.entries(this.menuItems).map(([id, item]) => ({
id,
active: this.activeMenuItems.includes(id as MenuItemKey),
labelLocalized: this.translatePipe.transform(item.label),
})),
type: EditModalTypeEnum.CHECKBOXES,
},
});
await modal.present();
modal.onDidDismiss().then(result => {
if (result.data?.items) {
this.updateActiveItems(
result.data.items.filter((it: EditModalItem) => it.active).map((it: EditModalItem) => it.id),
);
}
});
}
}

View File

@@ -13,14 +13,11 @@
~ this program. If not, see <https://www.gnu.org/licenses/>.
-->
<stapps-section
[title]="'dashboard.news.title' | translate"
[isEditable]="false"
[customIcon]="'read_more'"
class="is-editable"
(onEdit)="onMoreNewsClicked()"
>
<simple-swiper class="news-swiper card-swiper" *ngIf="news" @fade>
<stapps-section [title]="'dashboard.news.title' | translate">
<ion-button size="small" slot="button-end" fill="clear" color="medium" [routerLink]="['/news']">
<ion-icon slot="icon-only" name="read_more"></ion-icon>
</ion-button>
<simple-swiper class="news-swiper card-swiper" *ngIf="news | async as news" @fade>
<stapps-news-item *ngFor="let newsItem of news" [item]="newsItem"> </stapps-news-item>
<ion-item [routerLink]="['/news']" class="more-news" lines="none">
<ion-label>{{ 'dashboard.news.moreNews' | translate | titlecase }}</ion-label>

View File

@@ -12,15 +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 {Router} from '@angular/router';
import {NewsPageComponent} from '../../../news/page/news-page.component';
import {Component} from '@angular/core';
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';
import {SCMessage} from '@openstapps/core';
import {animate, style, transition, trigger} from '@angular/animations';
import {Router} from '@angular/router';
/**
* Shows a section with news
@@ -38,40 +34,12 @@ import {animate, style, transition, trigger} from '@angular/animations';
]),
],
})
export class NewsSectionComponent extends NewsPageComponent implements OnInit {
pageSize = 5;
export class NewsSectionComponent {
news: Promise<SCMessage[]>;
/**
* 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']);
constructor(readonly newsProvider: NewsProvider, readonly router: Router) {
this.news = this.newsProvider
.getCurrentFilters()
.then(filters => this.newsProvider.getList(5, 0, filters));
}
}

View File

@@ -0,0 +1,102 @@
/*
* 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 {AnimationController} from '@ionic/angular';
import {AnimationOptions} from '@ionic/angular/providers/nav-controller';
/**
*
*/
export function homePageSearchTransition(animationController: AnimationController) {
let scheduleTransform: WebKitCSSMatrix;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (_baseElement: HTMLElement, options: AnimationOptions | any) => {
const back = options.direction === 'back';
const searchPage = back ? options.leavingEl : options.enteringEl;
const homePage = back ? options.enteringEl : options.leavingEl;
const rootTransition = animationController
.create()
.duration(options.duration ?? 350)
.easing(
// quintic in / out
back ? 'cubic-bezier(0.64, 0, 0.78, 0)' : 'cubic-bezier(0.22, 1, 0.36, 1)',
);
const homePageContent = homePage.querySelector('ion-content').shadowRoot.querySelector('.inner-scroll');
const leavingSearchbar = homePage.querySelector('ion-searchbar');
const enteringSearchbar = searchPage.querySelector('ion-searchbar');
const searchPageHeader = searchPage.querySelector('ion-header');
const homePageSchedule = homePage.querySelector('.schedule');
if (!back) {
scheduleTransform = new WebKitCSSMatrix(window.getComputedStyle(homePageSchedule).transform);
}
const enteringSearchbarTop = enteringSearchbar.getBoundingClientRect().top;
const leavingSearchbarTop = leavingSearchbar.getBoundingClientRect().top;
const searchbarDelta = leavingSearchbarTop - enteringSearchbarTop;
const searchHeaderHeight = searchPageHeader.getBoundingClientRect().bottom;
const homeHeaderHeight = homePageSchedule.getBoundingClientRect().bottom;
const homePageSlideAmount = -50;
const headerDelta = homeHeaderHeight - searchHeaderHeight;
const enterTransition = animationController.create().fromTo('opacity', '0', '1').addElement(searchPage);
const exitTransition = animationController.create().fromTo('opacity', '1', '1').addElement(homePage);
const homePageSlide = animationController
.create()
.fromTo('transform', `translateY(0px)`, `translateY(${homePageSlideAmount}px)`)
.addElement(homePageContent);
const toolbarExit = animationController
.create()
.fromTo(
'transform',
scheduleTransform.toString(),
scheduleTransform.translate(undefined, -headerDelta).toString(),
)
.addElement(homePageSchedule);
const headerSlide = animationController
.create()
.fromTo('transform', `translateY(${headerDelta}px)`, 'translateY(0px)')
.addElement(searchPageHeader);
const searchbarSlideIn = animationController
.create()
.fromTo('transform', `translateY(${searchbarDelta - headerDelta}px)`, 'translateY(0px)')
.beforeStyles({
'z-index': 1000,
})
.afterClearStyles(['z-index'])
.addElement(searchPage.querySelector('.toolbar-searchbar'));
const searchbarSlideOut = animationController
.create()
.fromTo('transform', 'translateY(0px)', `translateY(-${searchbarDelta + homePageSlideAmount}px)`)
.addElement(homePage.querySelector('stapps-search-section > stapps-section'));
rootTransition
.addAnimation([
enterTransition,
exitTransition,
toolbarExit,
homePageSlide,
headerSlide,
searchbarSlideIn,
searchbarSlideOut,
])
.direction(back ? 'reverse' : 'normal');
return rootTransition;
};
}

View File

@@ -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.
@@ -13,17 +13,12 @@
~ this program. If not, see <https://www.gnu.org/licenses/>.
-->
<stapps-section title="{{ 'dashboard.navigation.item.search' | translate }}" [isEditable]="false">
<div class="searchbar">
<ion-input
type="search"
enterkeyhint="search"
placeholder="{{ 'search.search_bar.placeholder' | translate }}"
(submit)="onSubmitSearch()"
(keyup.enter)="onSubmitSearch()"
(search)="onSubmitSearch()"
[(ngModel)]="searchTerm"
></ion-input>
<ion-icon size="35" weight="300" name="search" (click)="onSubmitSearch()" class="clickable"></ion-icon>
</div>
<stapps-section title="{{ 'dashboard.navigation.item.search' | translate }}">
<ion-searchbar
[routerLink]="'/search'"
[routerAnimation]="routeTransition"
class="stapps-searchbar ion-activatable ripple-parent"
>
<ion-ripple-effect></ion-ripple-effect>
</ion-searchbar>
</stapps-section>

View File

@@ -1,36 +1,43 @@
/*!
* 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/>.
*/
.searchbar {
position: relative;
max-width: 700px;
ion-ripple-effect {
z-index: 1000;
border-radius: var(--spacing-sm);
}
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-md);
--padding-bottom: var(--spacing-md);
box-shadow: var(--shadow-default);
ion-searchbar {
cursor: text;
}
ion-searchbar ::ng-deep .searchbar-input-container {
pointer-events: none;
}
ion-searchbar.ios {
ion-ripple-effect {
display: none;
}
ion-icon {
position: absolute;
top: 50%;
right: var(--spacing-md);
transform: translateY(-50%);
z-index: 2;
transition: opacity 150ms ease;
&:active {
opacity: 0.6;
}
@media (hover: hover) {
&:hover {
opacity: 0.6;
}
}
}

View File

@@ -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.
@@ -16,6 +16,8 @@ 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 {homePageSearchTransition} from './search-route-transition';
/**
* Shows a search input field
@@ -28,7 +30,11 @@ import {Keyboard} from '@capacitor/keyboard';
export class SearchSectionComponent {
searchTerm = '';
constructor(private router: Router) {}
routeTransition: AnimationBuilder;
constructor(private router: Router, private animationController: AnimationController) {
this.routeTransition = homePageSearchTransition(this.animationController);
}
/**
* User submits search

View File

@@ -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.
@@ -33,6 +33,7 @@
type="search"
enterkeyhint="search"
class="filterable"
autofocus
>
<ion-menu-button menu="context" auto-hide="false">
<ion-icon name="tune"></ion-icon>

View File

@@ -1,3 +1,18 @@
/*!
* 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/>.
*/
:host {
display: flex;
flex-direction: column;
@@ -35,3 +50,11 @@ ion-content {
margin-bottom: 0;
}
}
ion-header {
background: var(--ion-color-primary);
}
ion-toolbar {
--ion-color-base: none !important;
}

View File

@@ -39,6 +39,7 @@ ion-card-title {
overflow: hidden;
font-size: var(--font-size-xl);
--color: var(--ion-color-dark-contrast);
max-lines: 3;
}
ion-card-subtitle {

View File

@@ -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.
@@ -19,8 +19,17 @@ import {
SCSearchBooleanFilter,
SCSearchFilter,
SCSearchQuery,
SCSearchValueFilter,
SCSetting,
} from '@openstapps/core';
import {DataProvider} from '../data/data.provider';
import {
newsFilterSettingsCategory,
newsFilterSettingsFieldsMapping,
NewsFilterSettingsNames,
} from './news-filter-settings';
import {SettingsProvider} from '../settings/settings.provider';
/**
* Service for providing news messages
*/
@@ -28,7 +37,31 @@ import {DataProvider} from '../data/data.provider';
providedIn: 'root',
})
export class NewsProvider {
constructor(private dataProvider: DataProvider) {}
constructor(private dataProvider: DataProvider, private settingsProvider: SettingsProvider) {}
async getCurrentSettings(): Promise<SCSetting[]> {
const settings: SCSetting[] = [];
for (const settingName of Object.keys(newsFilterSettingsFieldsMapping) as NewsFilterSettingsNames[]) {
settings.push(await this.settingsProvider.getSetting(newsFilterSettingsCategory, settingName));
}
return settings;
}
async getCurrentFilters(): Promise<SCSearchFilter[]> {
const settings = await this.getCurrentSettings();
const filtersMap = new Map<NewsFilterSettingsNames, SCSearchValueFilter>();
for (const setting of settings) {
filtersMap.set(
setting.name as NewsFilterSettingsNames,
DataProvider.createValueFilter(
newsFilterSettingsFieldsMapping[setting.name as NewsFilterSettingsNames],
setting.value as string,
),
);
}
return [...filtersMap.values()];
}
/**
* Get news messages

View File

@@ -15,12 +15,6 @@
import {Component, OnInit} from '@angular/core';
import {IonRefresher} from '@ionic/angular';
import {SCMessage, SCSearchFilter, SCSearchValueFilter, SCSetting} from '@openstapps/core';
import {SettingsProvider} from '../../settings/settings.provider';
import {
newsFilterSettingsCategory,
newsFilterSettingsFieldsMapping,
NewsFilterSettingsNames,
} from '../news-filter-settings';
import {NewsProvider} from '../news.provider';
import {SplashScreen} from '@capacitor/splash-screen';
@@ -68,7 +62,7 @@ export class NewsPageComponent implements OnInit {
*/
filters: SCSearchFilter[];
constructor(private newsProvider: NewsProvider, private settingsProvider: SettingsProvider) {}
constructor(private newsProvider: NewsProvider) {}
/**
* Fetch news from the backend
@@ -109,18 +103,7 @@ export class NewsPageComponent implements OnInit {
* Initialize the local variables on component initialization
*/
async ngOnInit() {
// Helper method to provide the relevant settings
const getNewsSettings = async (settingNames: NewsFilterSettingsNames[]) => {
const settings = [];
for (const settingName of settingNames) {
settings.push(await this.settingsProvider.getSetting(newsFilterSettingsCategory, settingName));
}
return settings;
};
this.settings = await getNewsSettings(
Object.keys(newsFilterSettingsFieldsMapping) as NewsFilterSettingsNames[],
);
this.settings = await this.newsProvider.getCurrentSettings();
}
/**

View File

@@ -13,58 +13,26 @@
~ this program. If not, see <https://www.gnu.org/licenses/>.
-->
<ion-label class="section-headline">
<!-- TODO: move this to thing translate -->
{{ 'name' | translateSimple: item }}
</ion-label>
<ion-button
*ngIf="item.authProvider"
fill="clear"
color="dark"
(click)="toggleLogIn()"
style="grid-area: login"
>
<ion-icon *ngIf="isLoggedIn; else loginIcon" slot="end" name="logout"></ion-icon>
<ng-template #loginIcon>
<ion-icon slot="end" name="login"></ion-icon>
</ng-template>
<ion-label>{{ 'profile.buttons.default.log_' + (isLoggedIn ? 'out' : 'in') | translate }}</ion-label>
</ion-button>
<ion-button
[disabled]="isBeginning"
(click)="swiper.swiperRef.slidePrev()"
fill="clear"
color="dark"
class="navigation"
[class.hidden]="slidesFillScreen"
style="grid-area: prev"
><ion-icon slot="icon-only" name="chevron_left"></ion-icon
></ion-button>
<ion-button
[disabled]="isEnd"
(click)="swiper.swiperRef.slideNext()"
fill="clear"
color="dark"
class="navigation"
[class.hidden]="slidesFillScreen"
style="grid-area: next"
><ion-icon slot="icon-only" name="chevron_right"></ion-icon
></ion-button>
<swiper
#swiper
(elementSizeChange)="resizeSwiper($event, swiper.swiperRef)"
[elementSizeChangeDebounce]="100"
(toEdge)="activeIndexChange($event[0])"
(fromEdge)="activeIndexChange($event[0])"
[cssMode]="true"
[spaceBetween]="0"
[slidesPerView]="slidesPerView"
class="card-swiper"
>
<ng-template swiperSlide *ngFor="let link of item.links">
<stapps-section-link-card
<stapps-section [title]="'name' | translateSimple: item">
<ion-button slot="button-end" *ngIf="item.authProvider" fill="clear" color="dark" (click)="toggleLogIn()">
<ion-icon *ngIf="isLoggedIn; else loginIcon" slot="end" name="logout"></ion-icon>
<ng-template #loginIcon>
<ion-icon slot="end" name="login"></ion-icon>
</ng-template>
<ion-label>{{ 'profile.buttons.default.log_' + (isLoggedIn ? 'out' : 'in') | translate }}</ion-label>
</ion-button>
<simple-swiper>
<ion-item
lines="none"
*ngFor="let link of item.links"
[routerLink]="link.link"
[disabled]="link.needsAuth && !isLoggedIn"
[item]="link"
></stapps-section-link-card>
</ng-template>
</swiper>
>
<div>
<ion-icon [name]="link.icon" size="36" color="dark"></ion-icon>
<ion-label>{{ 'name' | translateSimple: link }}</ion-label>
</div>
</ion-item>
</simple-swiper>
</stapps-section>

View File

@@ -12,9 +12,50 @@
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
$width: 108px;
.section-headline {
padding-inline-start: var(--spacing-md);
simple-swiper {
container-type: inline-size;
--swiper-slide-width: #{$width};
ion-item {
@each $i in 7, 6, 5, 4, 3, 2, 1 {
$max: #{($width + 8px) * $i};
@container (inline-size < #{$max}) {
--swiper-slide-width: #{100cqi / $i};
}
}
}
}
ion-item {
height: 96px;
--border-radius: var(--border-radius-default);
--inner-padding-start: unset;
--inner-padding-end: unset;
--padding-start: unset;
--padding-end: unset;
> div {
width: 100%;
height: 100%;
}
div,
ion-label {
white-space: normal;
text-align: center;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
font-size: var(--font-size-sm);
}
&::part(native) {
height: 100%;
width: 100%;
}
}
.log-in-hint {
@@ -23,47 +64,3 @@
box-shadow: none;
background: var(--ion-color-light-tint);
}
.navigation::part(native) {
padding-inline: 0;
}
swiper {
//noinspection CssInvalidFunction
transform: translateY(calc(-1 * var(--spacing-sm)));
}
stapps-section-link-card {
// required spacing for box shadow
margin-block: var(--spacing-sm);
}
:host {
display: grid;
grid-template-columns: 1fr auto auto auto;
grid-template-rows: 42px 1fr;
grid-template-areas:
'title prev next login'
'swiper swiper swiper swiper';
align-items: center;
> ion-label {
margin-block-end: 0;
}
> swiper {
grid-area: swiper;
width: 100%;
padding-right: 0;
}
}
ion-button.hidden {
display: none;
}
@media (hover: none) {
ion-button.navigation {
display: none;
}
}

View File

@@ -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.
@@ -25,7 +25,6 @@ import {UtilModule} from '../../util/util.module';
import {IonIconModule} from '../../util/ion-icon/ion-icon.module';
import {ProfilePageSectionComponent} from './page/profile-page-section.component';
import {ThingTranslateModule} from '../../translation/thing-translate.module';
import {SectionModule} from '../../util/section/section.module';
const routes: Routes = [
{
@@ -46,7 +45,6 @@ const routes: Routes = [
SwiperModule,
UtilModule,
ThingTranslateModule,
SectionModule,
],
})
export class ProfilePageModule {}

View File

@@ -0,0 +1,39 @@
/*
* 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 {AfterViewInit, Directive, ElementRef} from '@angular/core';
import {IonSearchbar} from '@ionic/angular';
@Directive({
selector: 'ion-searchbar[autofocus]',
})
export class SearchbarAutofocusDirective implements AfterViewInit {
constructor(private element: ElementRef) {}
ngAfterViewInit() {
const label = `focus`;
console.time(label);
const interval = setInterval(() => {
const searchbar = this.element.nativeElement as IonSearchbar;
searchbar.setFocus();
});
const onFocus = () => {
console.timeEnd(label);
clearInterval(interval);
this.element.nativeElement.removeEventListener('ionFocus', onFocus);
};
this.element.nativeElement.addEventListener('ionFocus', onFocus);
}
}

View File

@@ -37,20 +37,14 @@
</ion-button>
</ion-col>
</ng-container>
<ion-col size="auto" *ngIf="isEditable">
<ion-button fill="clear" color="medium" (click)="onEditClick()">
<ion-icon size="24" slot="icon-only" name="edit_square"></ion-icon>
</ion-button>
</ion-col>
<ion-col size="auto" *ngIf="customIcon">
<ion-button fill="clear" color="medium" (click)="onEditClick()">
<ion-icon slot="icon-only" size="24" [name]="customIcon"></ion-icon>
</ion-button>
<ion-col size="auto">
<div>
<ng-content select="[slot=button-end]"></ng-content>
</div>
</ion-col>
</ion-row>
<ion-row>
<ion-col #content>
<ion-col>
<ng-content></ng-content>
</ion-col>
</ion-row>

View File

@@ -13,7 +13,7 @@
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
@import '../../../../theme/util/mixins';
@import 'src/theme/util/mixins';
a {
display: contents;
@@ -44,8 +44,8 @@ ion-col {
padding: 0;
}
ion-button::part(native) {
padding-inline: var(--spacing-sm);
:host ::ng-deep ion-button::part(native) {
padding-inline: var(--spacing-xs);
}
@media (hover: none) {

View File

@@ -12,17 +12,7 @@
* 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,
EventEmitter,
HostBinding,
Input,
OnDestroy,
OnInit,
Output,
ViewContainerRef,
} from '@angular/core';
import {AfterContentInit, Component, Input, OnDestroy, ViewContainerRef} from '@angular/core';
import {SCThings} from '@openstapps/core';
/**
@@ -33,35 +23,17 @@ import {SCThings} from '@openstapps/core';
templateUrl: 'section.component.html',
styleUrls: ['section.component.scss'],
})
export class SectionComponent implements OnInit, AfterContentInit, OnDestroy {
@HostBinding('class.is-extended') isExtendedClass = false;
@HostBinding('class.is-editable') isEditableClass = false;
export class SectionComponent implements AfterContentInit, OnDestroy {
@Input() title = '';
@Input() isSectionExtended = false;
@Input() isEditable = false;
@Input() customIcon?: string = undefined;
@Input() item?: SCThings;
// eslint-disable-next-line @angular-eslint/no-output-on-prefix
@Output() onEdit = new EventEmitter<void>();
mutationObserver: MutationObserver;
swiper?: HTMLElement;
constructor(readonly viewContainerRef: ViewContainerRef) {}
ngOnInit() {
this.isExtendedClass = this.isSectionExtended;
this.isEditableClass = this.isEditable;
}
ngAfterContentInit() {
this.mutationObserver = new MutationObserver(() => {
const simpleSwiper = this.viewContainerRef.element.nativeElement.querySelector('simple-swiper');
@@ -93,13 +65,6 @@ export class SectionComponent implements OnInit, AfterContentInit, OnDestroy {
}
}
/**
* Action when edit is clicked
*/
onEditClick() {
this.onEdit.emit();
}
ngOnDestroy() {
this.mutationObserver.disconnect();
}

View File

@@ -1,27 +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} from '@angular/core';
import {SCSectionLink} from '../../modules/profile/page/sections';
@Component({
selector: 'stapps-section-link-card',
templateUrl: 'section-link-card.html',
styleUrls: ['section-link-card.scss'],
})
export class SectionLinkCardComponent {
@Input() item: SCSectionLink;
@Input() disabled: boolean;
}

View File

@@ -1,23 +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-card [routerLink]="item.link" class="card" [class.disabled]="disabled">
<ion-card-header mode="md">
<ion-card-title>
<ion-icon [name]="item.icon" size="40"></ion-icon>
<!-- TODO: move this to thing translate -->
{{ 'name' | translateSimple: item }}
</ion-card-title>
</ion-card-header>
</ion-card>

View File

@@ -1,49 +0,0 @@
/*!
* 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/>.
*/
:host,
ion-card,
ion-card-header,
ion-card::part(native) {
height: 100%;
}
ion-card {
padding: 0;
margin-block: 0;
margin-inline: var(--spacing-sm);
}
ion-card-header {
padding: var(--spacing-md) var(--spacing-lg);
}
ion-card-title {
padding: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
font-family: var(--ion-font-family);
font-size: var(--font-size-xs);
font-weight: var(--font-weight-semi-bold);
}
.disabled {
opacity: 0.3;
}

View File

@@ -1,23 +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} from '@angular/core';
@Component({
selector: 'stapps-section-tail-prompt-card',
templateUrl: 'section-tail-prompt-card.html',
styleUrls: ['section-tail-prompt-card.scss', 'section-link-card.scss'],
})
export class SectionTailPromptCardComponent {}

View File

@@ -1,22 +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-card [button]="true" class="card">
<ion-card-header>
<ion-card-title>
<ng-content></ng-content>
</ion-card-title>
</ion-card-header>
</ion-card>

View File

@@ -1,19 +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/>.
*/
.card {
box-shadow: none;
background: none;
}

View File

@@ -1,30 +0,0 @@
/*
* 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 {NgModule} from '@angular/core';
import {SectionLinkCardComponent} from './section-link-card.component';
import {IonicModule} from '@ionic/angular';
import {BrowserModule} from '@angular/platform-browser';
import {TranslateModule} from '@ngx-translate/core';
import {IonIconModule} from '../ion-icon/ion-icon.module';
import {ThingTranslateModule} from '../../translation/thing-translate.module';
import {RouterModule} from '@angular/router';
import {SectionTailPromptCardComponent} from './section-tail-prompt-card.component';
@NgModule({
imports: [BrowserModule, IonicModule, TranslateModule, IonIconModule, ThingTranslateModule, RouterModule],
declarations: [SectionLinkCardComponent, SectionTailPromptCardComponent],
exports: [SectionLinkCardComponent, SectionTailPromptCardComponent],
})
export class SectionModule {}

View File

@@ -28,6 +28,8 @@ import {ElementSizeChangeDirective} from './element-size-change.directive';
import {OpeningHoursComponent} from './opening-hours.component';
import {ThingTranslateModule} from '../translation/thing-translate.module';
import {SimpleSwiperComponent} from './simple-swiper.component';
import {SearchbarAutofocusDirective} from './searchbar-autofocus.directive';
import {SectionComponent} from './section.component';
@NgModule({
imports: [BrowserModule, IonicModule, TranslateModule, ThingTranslateModule.forChild()],
@@ -37,12 +39,14 @@ import {SimpleSwiperComponent} from './simple-swiper.component';
DateIsThisPipe,
NullishCoalescingPipe,
LazyPipe,
SectionComponent,
DateFromIndexPipe,
DaytimeKeyPipe,
NextDateInListPipe,
EditModalComponent,
OpeningHoursComponent,
SimpleSwiperComponent,
SearchbarAutofocusDirective,
],
exports: [
ElementSizeChangeDirective,
@@ -52,10 +56,12 @@ import {SimpleSwiperComponent} from './simple-swiper.component';
LazyPipe,
DateFromIndexPipe,
DaytimeKeyPipe,
SectionComponent,
NextDateInListPipe,
EditModalComponent,
OpeningHoursComponent,
SimpleSwiperComponent,
SearchbarAutofocusDirective,
],
})
export class UtilModule {}