mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-01-18 15:42:54 +00:00
refactor: change opening hours handling
fix: opening hours not updating feat: lazy-load opening hours module feat: add e2e tests for opening hours refactor: migrate opening hours to on-push change detection feat: show exact minutes in opening hours starting one hour before next change
This commit is contained in:
@@ -58,12 +58,15 @@ import {StorageProvider} from './modules/storage/storage.provider';
|
||||
import {AssessmentsModule} from './modules/assessments/assessments.module';
|
||||
import {ServiceHandlerInterceptor} from './_helpers/service-handler.interceptor';
|
||||
import {RoutingStackService} from './util/routing-stack.service';
|
||||
import {SCSettingValue} from '@openstapps/core';
|
||||
import {SCLanguageCode, SCSettingValue} from '@openstapps/core';
|
||||
import {DefaultAuthService} from './modules/auth/default-auth.service';
|
||||
import {PAIAAuthService} from './modules/auth/paia/paia-auth.service';
|
||||
import {IonIconModule} from './util/ion-icon/ion-icon.module';
|
||||
import {NavigationModule} from './modules/menu/navigation/navigation.module';
|
||||
import {browserFactory, SimpleBrowser} from './util/browser.factory';
|
||||
import {getDateFnsLocale} from './translation/dfns-locale';
|
||||
import {setDefaultOptions} from 'date-fns';
|
||||
import {DateFnsConfigurationService} from 'ngx-date-fns';
|
||||
|
||||
registerLocaleData(localeDe);
|
||||
|
||||
@@ -71,12 +74,6 @@ SwiperCore.use([FreeMode, Navigation]);
|
||||
|
||||
/**
|
||||
* Initializes data needed on startup
|
||||
* @param storageProvider provider of the saved data (using framework's storage)
|
||||
* @param logger TODO
|
||||
* @param settingsProvider provider of settings (e.g. language that has been set)
|
||||
* @param configProvider TODO
|
||||
* @param translateService TODO
|
||||
* @param _routingStackService Just for init and to track the stack from the get go
|
||||
*/
|
||||
export function initializerFactory(
|
||||
storageProvider: StorageProvider,
|
||||
@@ -87,6 +84,7 @@ export function initializerFactory(
|
||||
_routingStackService: RoutingStackService,
|
||||
defaultAuthService: DefaultAuthService,
|
||||
paiaAuthService: PAIAAuthService,
|
||||
dateFnsConfigurationService: DateFnsConfigurationService,
|
||||
) {
|
||||
return async () => {
|
||||
initLogger(logger);
|
||||
@@ -107,6 +105,10 @@ export function initializerFactory(
|
||||
translateService.setDefaultLang('en');
|
||||
translateService.use(languageCode);
|
||||
moment.locale(languageCode);
|
||||
const dateFnsLocale = await getDateFnsLocale(languageCode as SCLanguageCode);
|
||||
setDefaultOptions({locale: dateFnsLocale});
|
||||
dateFnsConfigurationService.setLocale(dateFnsLocale);
|
||||
|
||||
await defaultAuthService.init();
|
||||
await paiaAuthService.init();
|
||||
} catch (error) {
|
||||
@@ -198,6 +200,7 @@ export function createTranslateLoader(http: HttpClient) {
|
||||
RoutingStackService,
|
||||
DefaultAuthService,
|
||||
PAIAAuthService,
|
||||
DateFnsConfigurationService,
|
||||
],
|
||||
useFactory: initializerFactory,
|
||||
},
|
||||
|
||||
@@ -17,7 +17,7 @@ import {MapPosition} from '../../map/position.service';
|
||||
import {SearchPageComponent} from './search-page.component';
|
||||
import {Geolocation} from '@capacitor/geolocation';
|
||||
import {BehaviorSubject} from 'rxjs';
|
||||
import {pauseWhen} from '../../../util/pause-when';
|
||||
import {pauseWhen} from '../../../util/rxjs/pause-when';
|
||||
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
||||
|
||||
/**
|
||||
|
||||
@@ -27,7 +27,7 @@ import {MapProvider} from '../map.provider';
|
||||
import {MapPosition, PositionService} from '../position.service';
|
||||
import {Geolocation, PermissionStatus} from '@capacitor/geolocation';
|
||||
import {Capacitor} from '@capacitor/core';
|
||||
import {pauseWhen} from '../../../util/pause-when';
|
||||
import {pauseWhen} from '../../../util/rxjs/pause-when';
|
||||
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
||||
import {startViewTransition} from '../../../util/view-transition';
|
||||
|
||||
|
||||
@@ -12,13 +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 {Injectable, OnDestroy, Pipe, PipeTransform} from '@angular/core';
|
||||
import {LangChangeEvent, TranslateService} from '@ngx-translate/core';
|
||||
import moment from 'moment';
|
||||
import {Subscription} from 'rxjs';
|
||||
import {logger} from '../_helpers/ts-logger';
|
||||
import opening_hours from 'opening_hours';
|
||||
|
||||
@Injectable()
|
||||
@Pipe({
|
||||
@@ -110,141 +108,6 @@ export class StringSplitPipe implements PipeTransform {
|
||||
return this.value as never;
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
@Pipe({
|
||||
name: 'openingHours',
|
||||
pure: true,
|
||||
})
|
||||
export class OpeningHoursPipe implements PipeTransform, OnDestroy {
|
||||
locale: string;
|
||||
|
||||
onLangChange?: Subscription;
|
||||
|
||||
value: string[] = [];
|
||||
|
||||
constructor(private readonly translate: TranslateService) {
|
||||
this.locale = translate.currentLang;
|
||||
}
|
||||
|
||||
private _dispose(): void {
|
||||
if (this.onLangChange?.closed === false) {
|
||||
this.onLangChange?.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this._dispose();
|
||||
}
|
||||
|
||||
transform(aString: string | unknown): string[] {
|
||||
this.updateValue(aString);
|
||||
this._dispose();
|
||||
if (this.onLangChange?.closed === true) {
|
||||
this.onLangChange = this.translate.onLangChange.subscribe((event: LangChangeEvent) => {
|
||||
this.locale = event.lang;
|
||||
this.updateValue(aString);
|
||||
});
|
||||
}
|
||||
|
||||
return this.value;
|
||||
}
|
||||
|
||||
updateValue(aString: string | unknown) {
|
||||
if (typeof aString !== 'string') {
|
||||
logger.warn(`openingHours pipe unable to parse input: ${aString}`);
|
||||
|
||||
return;
|
||||
}
|
||||
let openingHours;
|
||||
|
||||
try {
|
||||
openingHours = new opening_hours(aString, {
|
||||
address: {
|
||||
country_code: 'de',
|
||||
state: 'Hessen',
|
||||
},
|
||||
lon: 8.667_97,
|
||||
lat: 50.129_16,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.warn(error);
|
||||
this.value = [];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const isOpen: boolean = openingHours.getState();
|
||||
const isUnknown: boolean = openingHours.getUnknown();
|
||||
|
||||
const nextChange = openingHours.getNextChange();
|
||||
const nextChangeIsOpen: boolean = openingHours.getState(nextChange);
|
||||
const nextChangeUnknown: boolean = openingHours.getUnknown(nextChange);
|
||||
const nextChangeIsToday: boolean = moment().isSame(nextChange, 'day');
|
||||
|
||||
let stateKey = isOpen ? 'common.openingHours.state_open' : 'common.openingHours.state_closed';
|
||||
|
||||
stateKey = isUnknown ? 'common.openingHours.state_maybe' : stateKey;
|
||||
|
||||
this.value = [isOpen ? 'success' : 'danger', `${this.translate.instant(stateKey)}`];
|
||||
|
||||
if (isUnknown) {
|
||||
const comment = openingHours.getComment();
|
||||
this.value = ['light', `${this.translate.instant(stateKey)}`];
|
||||
if (typeof comment === 'string') {
|
||||
this.value.push(comment);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (nextChangeUnknown) {
|
||||
return;
|
||||
}
|
||||
|
||||
let nextChangeKey: string | undefined;
|
||||
|
||||
let formattedCalender = moment(nextChange).calendar();
|
||||
|
||||
if (moment(nextChange).isBefore(moment().add(1, 'hours'))) {
|
||||
this.value[0] = 'warning';
|
||||
nextChangeKey = nextChangeIsOpen
|
||||
? 'common.openingHours.opening_soon_warning'
|
||||
: 'common.openingHours.closing_soon_warning';
|
||||
this.value.push(
|
||||
`${this.translate.instant(nextChangeKey, {
|
||||
time: new Intl.DateTimeFormat(this.locale, {
|
||||
timeStyle: 'short',
|
||||
}).format(nextChange),
|
||||
})}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (nextChangeIsToday) {
|
||||
nextChangeKey = nextChangeIsOpen
|
||||
? 'common.openingHours.opening_today'
|
||||
: 'common.openingHours.closing_today';
|
||||
this.value.push(
|
||||
`${this.translate.instant(nextChangeKey, {
|
||||
time: new Intl.DateTimeFormat(this.locale, {
|
||||
timeStyle: 'short',
|
||||
}).format(nextChange),
|
||||
})}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
nextChangeKey = nextChangeIsOpen ? 'common.openingHours.opening' : 'common.openingHours.closing';
|
||||
formattedCalender = formattedCalender.slice(0, 1).toUpperCase() + formattedCalender.slice(1);
|
||||
this.value.push(
|
||||
`${this.translate.instant(nextChangeKey, {
|
||||
relativeDateTime: formattedCalender,
|
||||
})}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
@Pipe({
|
||||
name: 'durationLocalized',
|
||||
|
||||
35
frontend/app/src/app/translation/dfns-locale.ts
Normal file
35
frontend/app/src/app/translation/dfns-locale.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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 {SCLanguageCode} from '@openstapps/core';
|
||||
import type {Locale} from 'date-fns';
|
||||
|
||||
type LocalesMap = Record<SCLanguageCode, () => Promise<{default: Locale}>>;
|
||||
|
||||
const LOCALES = {
|
||||
en: () => import('date-fns/locale/en-GB'),
|
||||
de: () => import('date-fns/locale/de'),
|
||||
} satisfies Partial<LocalesMap>;
|
||||
|
||||
/**
|
||||
* Get a Date Fns Locale
|
||||
*/
|
||||
export async function getDateFnsLocale(code: SCLanguageCode): Promise<Locale> {
|
||||
if (code in LOCALES) {
|
||||
return LOCALES[code as keyof typeof LOCALES]().then(it => it.default);
|
||||
} else {
|
||||
console.warn(`Unknown Locale "${code}" for Date Fns. Falling back to English.`);
|
||||
return LOCALES.en().then(it => it.default);
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,6 @@
|
||||
* 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 {ModuleWithProviders, NgModule, Provider} from '@angular/core';
|
||||
import {
|
||||
ArrayJoinPipe,
|
||||
@@ -23,7 +22,6 @@ import {
|
||||
IsNumericPipe,
|
||||
MetersLocalizedPipe,
|
||||
NumberLocalizedPipe,
|
||||
OpeningHoursPipe,
|
||||
SentenceCasePipe,
|
||||
StringSplitPipe,
|
||||
ToUnixPipe,
|
||||
@@ -51,7 +49,6 @@ export interface ThingTranslateModuleConfig {
|
||||
ThingTranslatePipe,
|
||||
TranslateSimplePipe,
|
||||
DateLocalizedFormatPipe,
|
||||
OpeningHoursPipe,
|
||||
SentenceCasePipe,
|
||||
ToUnixPipe,
|
||||
EntriesPipe,
|
||||
@@ -69,7 +66,6 @@ export interface ThingTranslateModuleConfig {
|
||||
ThingTranslatePipe,
|
||||
TranslateSimplePipe,
|
||||
DateLocalizedFormatPipe,
|
||||
OpeningHoursPipe,
|
||||
SentenceCasePipe,
|
||||
ToUnixPipe,
|
||||
EntriesPipe,
|
||||
|
||||
@@ -25,6 +25,9 @@ import {
|
||||
import moment from 'moment';
|
||||
import {isDefined, ThingTranslateParser} from './thing-translate.parser';
|
||||
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
||||
import {setDefaultOptions} from 'date-fns';
|
||||
import {DateFnsConfigurationService} from 'ngx-date-fns';
|
||||
import {getDateFnsLocale} from './dfns-locale';
|
||||
|
||||
// export const DEFAULT_LANGUAGE = new InjectionToken<string>('DEFAULT_LANGUAGE');
|
||||
|
||||
@@ -40,8 +43,13 @@ export class ThingTranslateService {
|
||||
*
|
||||
* @param translateService Instance of Angular TranslateService
|
||||
* @param parser An instance of the parser currently used
|
||||
* @param dfnsConfiguration the date fns configuration
|
||||
*/
|
||||
constructor(private readonly translateService: TranslateService, public parser: ThingTranslateParser) {
|
||||
constructor(
|
||||
private readonly translateService: TranslateService,
|
||||
public parser: ThingTranslateParser,
|
||||
private dfnsConfiguration: DateFnsConfigurationService,
|
||||
) {
|
||||
this.translator = new SCThingTranslator(
|
||||
(translateService.currentLang ?? translateService.defaultLang) as SCLanguageCode,
|
||||
);
|
||||
@@ -49,6 +57,10 @@ export class ThingTranslateService {
|
||||
this.translateService.onLangChange.pipe(takeUntilDestroyed()).subscribe((event: LangChangeEvent) => {
|
||||
this.translator.language = event.lang as keyof SCTranslations<SCLanguage>;
|
||||
moment.locale(event.lang);
|
||||
getDateFnsLocale(event.lang as SCLanguageCode).then(locale => {
|
||||
setDefaultOptions({locale});
|
||||
this.dfnsConfiguration.setLocale(locale);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -12,64 +12,49 @@
|
||||
* 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, ContentChild, Input, OnDestroy, OnInit, TemplateRef} from '@angular/core';
|
||||
import opening_hours from 'opening_hours';
|
||||
import {ChangeDetectionStrategy, Component, ContentChild, Input, TemplateRef} from '@angular/core';
|
||||
import {interval, Observable} from 'rxjs';
|
||||
import {fromOpeningHours} from './opening-hours';
|
||||
import {map, startWith} from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'stapps-opening-hours',
|
||||
templateUrl: 'opening-hours.html',
|
||||
styleUrls: ['opening-hours.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class OpeningHoursComponent implements OnDestroy, OnInit {
|
||||
export class OpeningHoursComponent {
|
||||
@ContentChild(TemplateRef) content: TemplateRef<unknown>;
|
||||
|
||||
@Input() openingHours?: string;
|
||||
|
||||
@Input() colorize = true;
|
||||
|
||||
@Input() showNextChange = true;
|
||||
|
||||
timer: NodeJS.Timeout;
|
||||
openingHours$?: Observable<{
|
||||
color: 'light' | 'warning' | 'success' | 'danger';
|
||||
statusName: string;
|
||||
statusText?: string;
|
||||
nextChangeAction: 'closing' | 'opening';
|
||||
nextChangeSoon?: Observable<Date | undefined>;
|
||||
nextChange?: Date;
|
||||
}>;
|
||||
|
||||
updateTimer() {
|
||||
if (typeof this.openingHours !== 'string') {
|
||||
return;
|
||||
}
|
||||
clearTimeout(this.timer);
|
||||
|
||||
const ohObject = new opening_hours(this.openingHours, {
|
||||
address: {
|
||||
country_code: 'de',
|
||||
state: 'Hessen',
|
||||
},
|
||||
lon: 8.667_97,
|
||||
lat: 50.129_16,
|
||||
});
|
||||
|
||||
const millisecondsRemaining =
|
||||
// eslint-disable-next-line unicorn/prefer-date-now
|
||||
(ohObject.getNextChange()?.getTime() ?? 0) - new Date().getTime() + 1000;
|
||||
|
||||
if (millisecondsRemaining > 1_209_600_000) {
|
||||
// setTimeout has upper bound of 0x7FFFFFFF
|
||||
// ignore everything over a week
|
||||
return;
|
||||
}
|
||||
|
||||
if (millisecondsRemaining > 0) {
|
||||
this.timer = setTimeout(() => {
|
||||
// pseudo update value to tigger openingHours pipe
|
||||
this.openingHours = `${this.openingHours}`;
|
||||
this.updateTimer();
|
||||
}, millisecondsRemaining);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.updateTimer();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
clearTimeout(this.timer);
|
||||
@Input() set openingHours(value: string | undefined) {
|
||||
if (!value) return;
|
||||
this.openingHours$ = fromOpeningHours(value).pipe(
|
||||
map(({isUnknown, isOpen, changesSoon, nextChange, comment}) => ({
|
||||
color: isUnknown ? 'light' : changesSoon ? 'warning' : isOpen ? 'success' : 'danger',
|
||||
statusName: `common.openingHours.state_${isUnknown ? 'maybe' : isOpen ? 'open' : 'closed'}`,
|
||||
statusText: comment,
|
||||
nextChangeAction: isOpen ? 'closing' : 'opening',
|
||||
nextChangeSoon: changesSoon
|
||||
? interval(60_000).pipe(
|
||||
startWith(nextChange),
|
||||
map(() => nextChange),
|
||||
)
|
||||
: undefined,
|
||||
nextChange,
|
||||
})),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,18 +13,20 @@
|
||||
~ this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<ng-container *ngIf="openingHours">
|
||||
<div>
|
||||
<ng-template [ngIf]="colorize" [ngIfElse]="blank">
|
||||
<ion-badge
|
||||
[color]="openingHours | openingHours | slice : 0 : 1 | join : ' '"
|
||||
slot="start"
|
||||
style="vertical-align: bottom"
|
||||
>
|
||||
{{ openingHours | openingHours | slice : 1 : 2 }}
|
||||
</ion-badge>
|
||||
</ng-template>
|
||||
<ng-template #blank> {{ openingHours | openingHours | slice : 1 : 2 }} </ng-template>
|
||||
<ng-container *ngIf="showNextChange"> {{ openingHours | openingHours | slice : 2 : 3 }} </ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
<div *ngIf="openingHours$ | async as openingHours">
|
||||
<ion-badge *ngIf="colorize; else blank" [color]="openingHours.color" slot="start">
|
||||
{{ openingHours.statusName | translate }}
|
||||
</ion-badge>
|
||||
<ng-template #blank>{{ openingHours.statusName | translate }}</ng-template>
|
||||
<ng-container *ngIf="openingHours.statusText; else nextChange"> {{openingHours.statusText}} </ng-container>
|
||||
<ng-template #nextChange>
|
||||
<ng-container *ngIf="showNextChange && openingHours.nextChangeSoon">
|
||||
{{ ('common.openingHours.' + openingHours.nextChangeAction + '_soon') | translate: {duration:
|
||||
(openingHours.nextChangeSoon | async | dfnsFormatDistanceToNowStrict: {unit: 'minute'})} }}
|
||||
</ng-container>
|
||||
<ng-container *ngIf="showNextChange && !openingHours.nextChangeSoon">
|
||||
{{ ('common.openingHours.' + openingHours.nextChangeAction) | translate: {date: openingHours.nextChange
|
||||
| dfnsFormatRelativeToNow} }}
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
3
frontend/app/src/app/util/opening-hours.scss
Normal file
3
frontend/app/src/app/util/opening-hours.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
ion-badge {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
72
frontend/app/src/app/util/opening-hours.ts
Normal file
72
frontend/app/src/app/util/opening-hours.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* 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 type {nominatim_object} from 'opening_hours';
|
||||
import {from, Observable, map, expand, of, delay} from 'rxjs';
|
||||
import {lazy} from './rxjs/lazy';
|
||||
import {isAfter, subHours} from 'date-fns';
|
||||
|
||||
export const OPENING_HOURS_REFERENCE = {
|
||||
address: {
|
||||
country_code: 'de',
|
||||
state: 'Hessen',
|
||||
},
|
||||
lon: 8.667_97,
|
||||
lat: 50.129_16,
|
||||
} satisfies nominatim_object;
|
||||
|
||||
export interface OpeningHoursInfo {
|
||||
isOpen: boolean;
|
||||
changesSoon: boolean;
|
||||
isUnknown: boolean;
|
||||
nextChange?: Date;
|
||||
comment?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opening Hours is a pretty huge CommonJS module,
|
||||
* so we lazy-load it
|
||||
*/
|
||||
const OpeningHours = lazy(() => import('opening_hours').then(it => it.default));
|
||||
|
||||
/**
|
||||
* Create an observable from opening hours
|
||||
* @param openingHours The opening hours string
|
||||
* @param soonThresholdHours The number of hours before which a change is marked as "soon"
|
||||
*/
|
||||
export function fromOpeningHours(openingHours: string, soonThresholdHours = 1): Observable<OpeningHoursInfo> {
|
||||
return from(OpeningHours).pipe(
|
||||
map(OpeningHours => new OpeningHours(openingHours, OPENING_HOURS_REFERENCE)),
|
||||
expand(it => {
|
||||
const now = new Date();
|
||||
const nextChange = it.getNextChange(now);
|
||||
const changesSoon = nextChange ? isAfter(now, subHours(nextChange, soonThresholdHours)) : false;
|
||||
|
||||
const changeTime = nextChange && !changesSoon ? subHours(nextChange, soonThresholdHours) : nextChange;
|
||||
return changeTime ? of(it).pipe(delay(changeTime)) : of();
|
||||
}),
|
||||
map(it => {
|
||||
const now = new Date();
|
||||
const nextChange = it.getNextChange(now);
|
||||
|
||||
return {
|
||||
isOpen: it.getState(now),
|
||||
isUnknown: it.getUnknown(now),
|
||||
changesSoon: nextChange ? isAfter(now, subHours(nextChange, soonThresholdHours)) : false,
|
||||
comment: it.getComment(now),
|
||||
nextChange,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
17
frontend/app/src/app/util/rxjs/lazy.ts
Normal file
17
frontend/app/src/app/util/rxjs/lazy.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import {Observable} from 'rxjs';
|
||||
|
||||
/**
|
||||
* Lazy-load something
|
||||
* @param construct the constructing function
|
||||
* @example lazy(() => import('module').then(it => it.default))
|
||||
*/
|
||||
export function lazy<T>(construct: () => Promise<T>): Observable<T> {
|
||||
let value: Promise<T>;
|
||||
return new Observable<T>(subscriber => {
|
||||
value ??= construct();
|
||||
value.then(it => {
|
||||
subscriber.next(it);
|
||||
subscriber.complete();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -32,9 +32,18 @@ import {SearchbarAutofocusDirective} from './searchbar-autofocus.directive';
|
||||
import {SectionComponent} from './section.component';
|
||||
import {RouterModule} from '@angular/router';
|
||||
import {IonContentParallaxDirective} from './ion-content-parallax.directive';
|
||||
import {FormatDistanceToNowStrictPipeModule, FormatRelativeToNowPipeModule} from 'ngx-date-fns';
|
||||
|
||||
@NgModule({
|
||||
imports: [BrowserModule, IonicModule, TranslateModule, ThingTranslateModule.forChild(), RouterModule],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
IonicModule,
|
||||
TranslateModule,
|
||||
ThingTranslateModule.forChild(),
|
||||
RouterModule,
|
||||
FormatRelativeToNowPipeModule,
|
||||
FormatDistanceToNowStrictPipeModule,
|
||||
],
|
||||
declarations: [
|
||||
IonContentParallaxDirective,
|
||||
ElementSizeChangeDirective,
|
||||
|
||||
@@ -69,15 +69,13 @@
|
||||
},
|
||||
"common": {
|
||||
"openingHours": {
|
||||
"closing": "Schließt {{relativeDateTime}}",
|
||||
"closing_soon_warning": "Schließt bald! Um {{time}} Uhr",
|
||||
"closing_today": "Schließt um {{time}} Uhr",
|
||||
"closing": "Schließt {{date}}",
|
||||
"closing_soon": "Schließt in {{duration}}",
|
||||
"state_closed": "Geschlossen",
|
||||
"state_maybe": "Vielleicht Geöffnet",
|
||||
"state_open": "Geöffnet",
|
||||
"opening": "Öffnet {{relativeDateTime}}",
|
||||
"opening_today": "Öffnet um {{time}} Uhr",
|
||||
"opening_soon_warning": "Öffnet bald! Um {{time}} Uhr"
|
||||
"opening": "Öffnet {{date}}",
|
||||
"opening_soon": "Öffnet in {{duration}}"
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
|
||||
@@ -69,15 +69,13 @@
|
||||
},
|
||||
"common": {
|
||||
"openingHours": {
|
||||
"closing": "Closing {{relativeDateTime}}",
|
||||
"closing_soon_warning": "Closing soon! At {{time}}",
|
||||
"closing_today": "Closing at {{time}}",
|
||||
"closing": "Closing {{date}}",
|
||||
"closing_soon": "Closing in {{duration}}",
|
||||
"state_closed": "Closed",
|
||||
"state_maybe": "Maybe open",
|
||||
"state_open": "Open",
|
||||
"opening": "Opens {{relativeDateTime}}",
|
||||
"opening_today": "Opens at {{time}}",
|
||||
"opening_soon_warning": "Opens soon! At {{time}}"
|
||||
"opening": "Opens {{date}}",
|
||||
"opening_soon": "Opens in {{duration}}"
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
|
||||
Reference in New Issue
Block a user