mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-04-14 10:19:15 +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:
11
.changeset/cold-squids-remain.md
Normal file
11
.changeset/cold-squids-remain.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
'@openstapps/app': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Refactored Opening Hours
|
||||||
|
|
||||||
|
- Migrated Opening Hours to use OnPush change detection
|
||||||
|
- Fixed a bug where opening hours would not update correctly
|
||||||
|
- Lazy-load opening hours module to keep it out of the main bundle
|
||||||
|
- Added e2e tests to verify functionality
|
||||||
|
- Changed live update status to show exact minutes starting one hour before the next change
|
||||||
@@ -15,8 +15,12 @@
|
|||||||
buildToolsVersions = [ "${buildToolsVersion}" ];
|
buildToolsVersions = [ "${buildToolsVersion}" ];
|
||||||
platformVersions = [ "32" ];
|
platformVersions = [ "32" ];
|
||||||
};
|
};
|
||||||
cypress = prev.cypress.overrideAttrs(prev: {
|
cypress = prev.cypress.overrideAttrs(cyPrev: rec {
|
||||||
version = "12.17.1";
|
version = "13.2.0";
|
||||||
|
src = prev.fetchzip {
|
||||||
|
url = "https://cdn.cypress.io/desktop/${version}/linux-x64/cypress.zip";
|
||||||
|
hash = "sha256-9o0nprGcJhudS1LNm+T7Vf0Dwd1RBauYKI+w1FBQ3ZM=";
|
||||||
|
};
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -49,10 +49,16 @@ The command `ionic cordova run ios` runs into the error `/platforms/ios/build/em
|
|||||||
|
|
||||||
The browser doesn't open or the tests don't connect to a browser
|
The browser doesn't open or the tests don't connect to a browser
|
||||||
|
|
||||||
|
#### Cause
|
||||||
|
|
||||||
|
Cypress was installed to a read-only location, see
|
||||||
|
[this issue](https://github.com/cypress-io/cypress/issues/18893).
|
||||||
|
This can be the case if you use NixOS.
|
||||||
|
|
||||||
#### Solution
|
#### Solution
|
||||||
|
|
||||||
Delete the Cypress config file
|
Make sure the cypress folder is writable before each launch
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
rm -rf ~/.config/Cypress
|
chmod -R +rw ~/.config/Cypress
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -182,6 +182,7 @@
|
|||||||
"builder": "@cypress/schematic:cypress",
|
"builder": "@cypress/schematic:cypress",
|
||||||
"options": {
|
"options": {
|
||||||
"devServerTarget": "app:serve",
|
"devServerTarget": "app:serve",
|
||||||
|
"liveReload": false,
|
||||||
"watch": true,
|
"watch": true,
|
||||||
"headless": false
|
"headless": false
|
||||||
},
|
},
|
||||||
|
|||||||
141
frontend/app/cypress/integration/opening-hours.spec.ts
Normal file
141
frontend/app/cypress/integration/opening-hours.spec.ts
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
describe('opening hours', () => {
|
||||||
|
beforeEach(function () {
|
||||||
|
cy.intercept('POST', 'https://mobile.server.uni-frankfurt.de/search', {
|
||||||
|
fixture: 'search/types/canteen/canteen-search-result.json',
|
||||||
|
}).as('search');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should specify relative closing time', () => {
|
||||||
|
cy.clock(new Date(2023, 9, 16, 15, 29), ['Date']);
|
||||||
|
cy.visit('/canteen');
|
||||||
|
|
||||||
|
cy.get('stapps-opening-hours')
|
||||||
|
.first()
|
||||||
|
.should('contain', 'Geöffnet')
|
||||||
|
.should('contain', 'Schließt heute um 22:00');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should specify relative opening time', () => {
|
||||||
|
cy.clock(new Date(2023, 9, 16, 6, 29), ['Date']);
|
||||||
|
cy.visit('/canteen');
|
||||||
|
|
||||||
|
cy.get('stapps-opening-hours')
|
||||||
|
.first()
|
||||||
|
.should('contain', 'Geschlossen')
|
||||||
|
.should('contain', 'Öffnet heute um 08:30');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should specify soon opening time', () => {
|
||||||
|
cy.clock(new Date(2023, 9, 16, 8, 0), ['Date']);
|
||||||
|
cy.visit('/canteen');
|
||||||
|
|
||||||
|
cy.get('stapps-opening-hours')
|
||||||
|
.first()
|
||||||
|
.should('contain', 'Geschlossen')
|
||||||
|
.should('contain', 'Öffnet in 30 Minuten');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should specify soon closing time', () => {
|
||||||
|
cy.clock(new Date(2023, 9, 16, 21, 30), ['Date']);
|
||||||
|
cy.visit('/canteen');
|
||||||
|
|
||||||
|
cy.get('stapps-opening-hours')
|
||||||
|
.first()
|
||||||
|
.should('contain', 'Geöffnet')
|
||||||
|
.should('contain', 'Schließt in 30 Minuten');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update the soon closing time every minute', () => {
|
||||||
|
cy.clock(new Date(2023, 9, 16, 21, 30));
|
||||||
|
cy.visit('/canteen');
|
||||||
|
cy.tick(500);
|
||||||
|
|
||||||
|
cy.get('stapps-opening-hours')
|
||||||
|
.first()
|
||||||
|
.should('contain', 'Geöffnet')
|
||||||
|
.should('contain', 'Schließt in 30 Minuten');
|
||||||
|
|
||||||
|
cy.tick(60_000);
|
||||||
|
cy.tick(50);
|
||||||
|
|
||||||
|
cy.get('stapps-opening-hours')
|
||||||
|
.first()
|
||||||
|
.should('contain', 'Geöffnet')
|
||||||
|
.should('contain', 'Schließt in 29 Minuten');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update the status when it changes', () => {
|
||||||
|
cy.clock(new Date(2023, 9, 16, 21, 59));
|
||||||
|
cy.visit('/canteen');
|
||||||
|
cy.tick(500);
|
||||||
|
|
||||||
|
cy.get('stapps-opening-hours')
|
||||||
|
.first()
|
||||||
|
.should('contain', 'Geöffnet')
|
||||||
|
.should('contain', 'Schließt in 1 Minute');
|
||||||
|
|
||||||
|
cy.tick(60_000);
|
||||||
|
|
||||||
|
cy.get('stapps-opening-hours')
|
||||||
|
.first()
|
||||||
|
.should('contain', 'Geschlossen')
|
||||||
|
.should('contain', 'Öffnet morgen um 08:30');
|
||||||
|
});
|
||||||
|
|
||||||
|
// This one takes long to execute!
|
||||||
|
it('should update as expected', () => {
|
||||||
|
cy.clock(new Date(2023, 9, 16, 20, 59));
|
||||||
|
cy.visit('/canteen');
|
||||||
|
cy.tick(500);
|
||||||
|
|
||||||
|
cy.get('stapps-opening-hours')
|
||||||
|
.first()
|
||||||
|
.should('contain', 'Geöffnet')
|
||||||
|
.should('contain', 'Schließt heute um 22:00');
|
||||||
|
|
||||||
|
cy.tick(60_000);
|
||||||
|
|
||||||
|
cy.get('stapps-opening-hours')
|
||||||
|
.first()
|
||||||
|
.should('contain', 'Geöffnet')
|
||||||
|
.should('contain', 'Schließt in 60 Minuten');
|
||||||
|
|
||||||
|
cy.tick(30 * 60_000);
|
||||||
|
|
||||||
|
cy.get('stapps-opening-hours')
|
||||||
|
.first()
|
||||||
|
.should('contain', 'Geöffnet')
|
||||||
|
.should('contain', 'Schließt in 30 Minuten');
|
||||||
|
|
||||||
|
cy.tick(30 * 60_000);
|
||||||
|
|
||||||
|
cy.get('stapps-opening-hours')
|
||||||
|
.first()
|
||||||
|
.should('contain', 'Geschlossen')
|
||||||
|
.should('contain', 'Öffnet morgen um 08:30');
|
||||||
|
|
||||||
|
cy.tick(9.5 * 60 * 60_000);
|
||||||
|
|
||||||
|
cy.get('stapps-opening-hours')
|
||||||
|
.first()
|
||||||
|
.should('contain', 'Geschlossen')
|
||||||
|
.should('contain', 'Öffnet in 60 Minuten');
|
||||||
|
|
||||||
|
cy.tick(30 * 60_000);
|
||||||
|
|
||||||
|
cy.get('stapps-opening-hours')
|
||||||
|
.first()
|
||||||
|
.should('contain', 'Geschlossen')
|
||||||
|
.should('contain', 'Öffnet in 30 Minuten');
|
||||||
|
|
||||||
|
cy.tick(30 * 60_000);
|
||||||
|
|
||||||
|
cy.get('stapps-opening-hours')
|
||||||
|
.first()
|
||||||
|
.should('contain', 'Geöffnet')
|
||||||
|
.should('contain', 'Schließt heute um 22:00');
|
||||||
|
|
||||||
|
// Long tick warps will cause network requests to time out
|
||||||
|
cy.get('@consoleError').invoke('resetHistory');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -31,36 +31,24 @@
|
|||||||
// When a command from ./commands is ready to use, import with `import './commands'` syntax
|
// When a command from ./commands is ready to use, import with `import './commands'` syntax
|
||||||
// import './commands';
|
// import './commands';
|
||||||
|
|
||||||
beforeEach(async function () {
|
beforeEach(function () {
|
||||||
let databases: string[];
|
cy.wrap(
|
||||||
if (window.indexedDB.databases) {
|
new Promise(resolve => {
|
||||||
databases = (await window.indexedDB.databases()).map(it => it.name);
|
window.indexedDB.deleteDatabase('_ionicstorage').onsuccess = resolve;
|
||||||
console.log('Trying to clear all databases');
|
}),
|
||||||
} else {
|
);
|
||||||
console.log("Browser doesn't support database enumeration, deleting just ionic storage");
|
|
||||||
databases = ['_ionicstorage'];
|
|
||||||
}
|
|
||||||
for (const database of databases) {
|
|
||||||
if (database) {
|
|
||||||
console.log(`Deleting database ${database}`);
|
|
||||||
await new Promise(resolve => (window.indexedDB.deleteDatabase(database).onsuccess = resolve));
|
|
||||||
console.log(`Deleted database ${database}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Cypress.on('window:before:load', window => {
|
Cypress.on('window:before:load', window => {
|
||||||
// Fake that user is using its browser in german language
|
// Fake that user is using its browser in German
|
||||||
Object.defineProperty(window.navigator, 'language', {value: 'de-DE'});
|
Object.defineProperty(window.navigator, 'language', {value: 'de-DE'});
|
||||||
Object.defineProperty(window.navigator, 'languages', [{value: 'de-DE'}]);
|
Object.defineProperty(window.navigator, 'languages', [{value: 'de-DE'}]);
|
||||||
|
|
||||||
// Fail tests on console error
|
cy.spy(window.console, 'error').as('consoleError');
|
||||||
cy.stub(window.console, 'error').callsFake(message => {
|
});
|
||||||
// log out to the terminal
|
|
||||||
cy.now('task', 'error', message);
|
afterEach(function () {
|
||||||
// log to Command Log and fail the test
|
cy.get('@consoleError').should('not.have.been.called');
|
||||||
throw new Error(message);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Cypress.on('uncaught:exception', error => {
|
Cypress.on('uncaught:exception', error => {
|
||||||
|
|||||||
@@ -91,6 +91,8 @@
|
|||||||
"@types/dom-view-transitions": "1.0.1",
|
"@types/dom-view-transitions": "1.0.1",
|
||||||
"capacitor-secure-storage-plugin": "0.8.1",
|
"capacitor-secure-storage-plugin": "0.8.1",
|
||||||
"cordova-plugin-calendar": "5.1.6",
|
"cordova-plugin-calendar": "5.1.6",
|
||||||
|
"date-fns": "2.30.0",
|
||||||
|
"ngx-date-fns": "10.0.1",
|
||||||
"deepmerge": "4.3.1",
|
"deepmerge": "4.3.1",
|
||||||
"form-data": "4.0.0",
|
"form-data": "4.0.0",
|
||||||
"geojson": "0.5.0",
|
"geojson": "0.5.0",
|
||||||
@@ -148,7 +150,7 @@
|
|||||||
"@typescript-eslint/eslint-plugin": "5.60.1",
|
"@typescript-eslint/eslint-plugin": "5.60.1",
|
||||||
"@typescript-eslint/parser": "5.60.1",
|
"@typescript-eslint/parser": "5.60.1",
|
||||||
"cordova-res": "0.15.4",
|
"cordova-res": "0.15.4",
|
||||||
"cypress": "12.17.1",
|
"cypress": "13.2.0",
|
||||||
"eslint": "8.43.0",
|
"eslint": "8.43.0",
|
||||||
"eslint-plugin-jsdoc": "46.4.2",
|
"eslint-plugin-jsdoc": "46.4.2",
|
||||||
"eslint-plugin-prettier": "4.2.1",
|
"eslint-plugin-prettier": "4.2.1",
|
||||||
|
|||||||
@@ -58,12 +58,15 @@ import {StorageProvider} from './modules/storage/storage.provider';
|
|||||||
import {AssessmentsModule} from './modules/assessments/assessments.module';
|
import {AssessmentsModule} from './modules/assessments/assessments.module';
|
||||||
import {ServiceHandlerInterceptor} from './_helpers/service-handler.interceptor';
|
import {ServiceHandlerInterceptor} from './_helpers/service-handler.interceptor';
|
||||||
import {RoutingStackService} from './util/routing-stack.service';
|
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 {DefaultAuthService} from './modules/auth/default-auth.service';
|
||||||
import {PAIAAuthService} from './modules/auth/paia/paia-auth.service';
|
import {PAIAAuthService} from './modules/auth/paia/paia-auth.service';
|
||||||
import {IonIconModule} from './util/ion-icon/ion-icon.module';
|
import {IonIconModule} from './util/ion-icon/ion-icon.module';
|
||||||
import {NavigationModule} from './modules/menu/navigation/navigation.module';
|
import {NavigationModule} from './modules/menu/navigation/navigation.module';
|
||||||
import {browserFactory, SimpleBrowser} from './util/browser.factory';
|
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);
|
registerLocaleData(localeDe);
|
||||||
|
|
||||||
@@ -71,12 +74,6 @@ SwiperCore.use([FreeMode, Navigation]);
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes data needed on startup
|
* 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(
|
export function initializerFactory(
|
||||||
storageProvider: StorageProvider,
|
storageProvider: StorageProvider,
|
||||||
@@ -87,6 +84,7 @@ export function initializerFactory(
|
|||||||
_routingStackService: RoutingStackService,
|
_routingStackService: RoutingStackService,
|
||||||
defaultAuthService: DefaultAuthService,
|
defaultAuthService: DefaultAuthService,
|
||||||
paiaAuthService: PAIAAuthService,
|
paiaAuthService: PAIAAuthService,
|
||||||
|
dateFnsConfigurationService: DateFnsConfigurationService,
|
||||||
) {
|
) {
|
||||||
return async () => {
|
return async () => {
|
||||||
initLogger(logger);
|
initLogger(logger);
|
||||||
@@ -107,6 +105,10 @@ export function initializerFactory(
|
|||||||
translateService.setDefaultLang('en');
|
translateService.setDefaultLang('en');
|
||||||
translateService.use(languageCode);
|
translateService.use(languageCode);
|
||||||
moment.locale(languageCode);
|
moment.locale(languageCode);
|
||||||
|
const dateFnsLocale = await getDateFnsLocale(languageCode as SCLanguageCode);
|
||||||
|
setDefaultOptions({locale: dateFnsLocale});
|
||||||
|
dateFnsConfigurationService.setLocale(dateFnsLocale);
|
||||||
|
|
||||||
await defaultAuthService.init();
|
await defaultAuthService.init();
|
||||||
await paiaAuthService.init();
|
await paiaAuthService.init();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -198,6 +200,7 @@ export function createTranslateLoader(http: HttpClient) {
|
|||||||
RoutingStackService,
|
RoutingStackService,
|
||||||
DefaultAuthService,
|
DefaultAuthService,
|
||||||
PAIAAuthService,
|
PAIAAuthService,
|
||||||
|
DateFnsConfigurationService,
|
||||||
],
|
],
|
||||||
useFactory: initializerFactory,
|
useFactory: initializerFactory,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {MapPosition} from '../../map/position.service';
|
|||||||
import {SearchPageComponent} from './search-page.component';
|
import {SearchPageComponent} from './search-page.component';
|
||||||
import {Geolocation} from '@capacitor/geolocation';
|
import {Geolocation} from '@capacitor/geolocation';
|
||||||
import {BehaviorSubject} from 'rxjs';
|
import {BehaviorSubject} from 'rxjs';
|
||||||
import {pauseWhen} from '../../../util/pause-when';
|
import {pauseWhen} from '../../../util/rxjs/pause-when';
|
||||||
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import {MapProvider} from '../map.provider';
|
|||||||
import {MapPosition, PositionService} from '../position.service';
|
import {MapPosition, PositionService} from '../position.service';
|
||||||
import {Geolocation, PermissionStatus} from '@capacitor/geolocation';
|
import {Geolocation, PermissionStatus} from '@capacitor/geolocation';
|
||||||
import {Capacitor} from '@capacitor/core';
|
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 {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
||||||
import {startViewTransition} from '../../../util/view-transition';
|
import {startViewTransition} from '../../../util/view-transition';
|
||||||
|
|
||||||
|
|||||||
@@ -12,13 +12,11 @@
|
|||||||
* You should have received a copy of the GNU General Public License along with
|
* You should have received a copy of the GNU General Public License along with
|
||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {Injectable, OnDestroy, Pipe, PipeTransform} from '@angular/core';
|
import {Injectable, OnDestroy, Pipe, PipeTransform} from '@angular/core';
|
||||||
import {LangChangeEvent, TranslateService} from '@ngx-translate/core';
|
import {LangChangeEvent, TranslateService} from '@ngx-translate/core';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import {Subscription} from 'rxjs';
|
import {Subscription} from 'rxjs';
|
||||||
import {logger} from '../_helpers/ts-logger';
|
import {logger} from '../_helpers/ts-logger';
|
||||||
import opening_hours from 'opening_hours';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@Pipe({
|
@Pipe({
|
||||||
@@ -110,141 +108,6 @@ export class StringSplitPipe implements PipeTransform {
|
|||||||
return this.value as never;
|
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()
|
@Injectable()
|
||||||
@Pipe({
|
@Pipe({
|
||||||
name: 'durationLocalized',
|
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
|
* You should have received a copy of the GNU General Public License along with
|
||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {ModuleWithProviders, NgModule, Provider} from '@angular/core';
|
import {ModuleWithProviders, NgModule, Provider} from '@angular/core';
|
||||||
import {
|
import {
|
||||||
ArrayJoinPipe,
|
ArrayJoinPipe,
|
||||||
@@ -23,7 +22,6 @@ import {
|
|||||||
IsNumericPipe,
|
IsNumericPipe,
|
||||||
MetersLocalizedPipe,
|
MetersLocalizedPipe,
|
||||||
NumberLocalizedPipe,
|
NumberLocalizedPipe,
|
||||||
OpeningHoursPipe,
|
|
||||||
SentenceCasePipe,
|
SentenceCasePipe,
|
||||||
StringSplitPipe,
|
StringSplitPipe,
|
||||||
ToUnixPipe,
|
ToUnixPipe,
|
||||||
@@ -51,7 +49,6 @@ export interface ThingTranslateModuleConfig {
|
|||||||
ThingTranslatePipe,
|
ThingTranslatePipe,
|
||||||
TranslateSimplePipe,
|
TranslateSimplePipe,
|
||||||
DateLocalizedFormatPipe,
|
DateLocalizedFormatPipe,
|
||||||
OpeningHoursPipe,
|
|
||||||
SentenceCasePipe,
|
SentenceCasePipe,
|
||||||
ToUnixPipe,
|
ToUnixPipe,
|
||||||
EntriesPipe,
|
EntriesPipe,
|
||||||
@@ -69,7 +66,6 @@ export interface ThingTranslateModuleConfig {
|
|||||||
ThingTranslatePipe,
|
ThingTranslatePipe,
|
||||||
TranslateSimplePipe,
|
TranslateSimplePipe,
|
||||||
DateLocalizedFormatPipe,
|
DateLocalizedFormatPipe,
|
||||||
OpeningHoursPipe,
|
|
||||||
SentenceCasePipe,
|
SentenceCasePipe,
|
||||||
ToUnixPipe,
|
ToUnixPipe,
|
||||||
EntriesPipe,
|
EntriesPipe,
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ import {
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import {isDefined, ThingTranslateParser} from './thing-translate.parser';
|
import {isDefined, ThingTranslateParser} from './thing-translate.parser';
|
||||||
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
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');
|
// export const DEFAULT_LANGUAGE = new InjectionToken<string>('DEFAULT_LANGUAGE');
|
||||||
|
|
||||||
@@ -40,8 +43,13 @@ export class ThingTranslateService {
|
|||||||
*
|
*
|
||||||
* @param translateService Instance of Angular TranslateService
|
* @param translateService Instance of Angular TranslateService
|
||||||
* @param parser An instance of the parser currently used
|
* @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(
|
this.translator = new SCThingTranslator(
|
||||||
(translateService.currentLang ?? translateService.defaultLang) as SCLanguageCode,
|
(translateService.currentLang ?? translateService.defaultLang) as SCLanguageCode,
|
||||||
);
|
);
|
||||||
@@ -49,6 +57,10 @@ export class ThingTranslateService {
|
|||||||
this.translateService.onLangChange.pipe(takeUntilDestroyed()).subscribe((event: LangChangeEvent) => {
|
this.translateService.onLangChange.pipe(takeUntilDestroyed()).subscribe((event: LangChangeEvent) => {
|
||||||
this.translator.language = event.lang as keyof SCTranslations<SCLanguage>;
|
this.translator.language = event.lang as keyof SCTranslations<SCLanguage>;
|
||||||
moment.locale(event.lang);
|
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
|
* You should have received a copy of the GNU General Public License along with
|
||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
import {ChangeDetectionStrategy, Component, ContentChild, Input, TemplateRef} from '@angular/core';
|
||||||
import {Component, ContentChild, Input, OnDestroy, OnInit, TemplateRef} from '@angular/core';
|
import {interval, Observable} from 'rxjs';
|
||||||
import opening_hours from 'opening_hours';
|
import {fromOpeningHours} from './opening-hours';
|
||||||
|
import {map, startWith} from 'rxjs/operators';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'stapps-opening-hours',
|
selector: 'stapps-opening-hours',
|
||||||
templateUrl: 'opening-hours.html',
|
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>;
|
@ContentChild(TemplateRef) content: TemplateRef<unknown>;
|
||||||
|
|
||||||
@Input() openingHours?: string;
|
|
||||||
|
|
||||||
@Input() colorize = true;
|
@Input() colorize = true;
|
||||||
|
|
||||||
@Input() showNextChange = 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() {
|
@Input() set openingHours(value: string | undefined) {
|
||||||
if (typeof this.openingHours !== 'string') {
|
if (!value) return;
|
||||||
return;
|
this.openingHours$ = fromOpeningHours(value).pipe(
|
||||||
}
|
map(({isUnknown, isOpen, changesSoon, nextChange, comment}) => ({
|
||||||
clearTimeout(this.timer);
|
color: isUnknown ? 'light' : changesSoon ? 'warning' : isOpen ? 'success' : 'danger',
|
||||||
|
statusName: `common.openingHours.state_${isUnknown ? 'maybe' : isOpen ? 'open' : 'closed'}`,
|
||||||
const ohObject = new opening_hours(this.openingHours, {
|
statusText: comment,
|
||||||
address: {
|
nextChangeAction: isOpen ? 'closing' : 'opening',
|
||||||
country_code: 'de',
|
nextChangeSoon: changesSoon
|
||||||
state: 'Hessen',
|
? interval(60_000).pipe(
|
||||||
},
|
startWith(nextChange),
|
||||||
lon: 8.667_97,
|
map(() => nextChange),
|
||||||
lat: 50.129_16,
|
)
|
||||||
});
|
: undefined,
|
||||||
|
nextChange,
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,18 +13,20 @@
|
|||||||
~ this program. If not, see <https://www.gnu.org/licenses/>.
|
~ this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<ng-container *ngIf="openingHours">
|
<div *ngIf="openingHours$ | async as openingHours">
|
||||||
<div>
|
<ion-badge *ngIf="colorize; else blank" [color]="openingHours.color" slot="start">
|
||||||
<ng-template [ngIf]="colorize" [ngIfElse]="blank">
|
{{ openingHours.statusName | translate }}
|
||||||
<ion-badge
|
|
||||||
[color]="openingHours | openingHours | slice : 0 : 1 | join : ' '"
|
|
||||||
slot="start"
|
|
||||||
style="vertical-align: bottom"
|
|
||||||
>
|
|
||||||
{{ openingHours | openingHours | slice : 1 : 2 }}
|
|
||||||
</ion-badge>
|
</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>
|
</ng-template>
|
||||||
<ng-template #blank> {{ openingHours | openingHours | slice : 1 : 2 }} </ng-template>
|
</div>
|
||||||
<ng-container *ngIf="showNextChange"> {{ openingHours | openingHours | slice : 2 : 3 }} </ng-container>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
|
|||||||
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 {SectionComponent} from './section.component';
|
||||||
import {RouterModule} from '@angular/router';
|
import {RouterModule} from '@angular/router';
|
||||||
import {IonContentParallaxDirective} from './ion-content-parallax.directive';
|
import {IonContentParallaxDirective} from './ion-content-parallax.directive';
|
||||||
|
import {FormatDistanceToNowStrictPipeModule, FormatRelativeToNowPipeModule} from 'ngx-date-fns';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [BrowserModule, IonicModule, TranslateModule, ThingTranslateModule.forChild(), RouterModule],
|
imports: [
|
||||||
|
BrowserModule,
|
||||||
|
IonicModule,
|
||||||
|
TranslateModule,
|
||||||
|
ThingTranslateModule.forChild(),
|
||||||
|
RouterModule,
|
||||||
|
FormatRelativeToNowPipeModule,
|
||||||
|
FormatDistanceToNowStrictPipeModule,
|
||||||
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
IonContentParallaxDirective,
|
IonContentParallaxDirective,
|
||||||
ElementSizeChangeDirective,
|
ElementSizeChangeDirective,
|
||||||
|
|||||||
@@ -69,15 +69,13 @@
|
|||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"openingHours": {
|
"openingHours": {
|
||||||
"closing": "Schließt {{relativeDateTime}}",
|
"closing": "Schließt {{date}}",
|
||||||
"closing_soon_warning": "Schließt bald! Um {{time}} Uhr",
|
"closing_soon": "Schließt in {{duration}}",
|
||||||
"closing_today": "Schließt um {{time}} Uhr",
|
|
||||||
"state_closed": "Geschlossen",
|
"state_closed": "Geschlossen",
|
||||||
"state_maybe": "Vielleicht Geöffnet",
|
"state_maybe": "Vielleicht Geöffnet",
|
||||||
"state_open": "Geöffnet",
|
"state_open": "Geöffnet",
|
||||||
"opening": "Öffnet {{relativeDateTime}}",
|
"opening": "Öffnet {{date}}",
|
||||||
"opening_today": "Öffnet um {{time}} Uhr",
|
"opening_soon": "Öffnet in {{duration}}"
|
||||||
"opening_soon_warning": "Öffnet bald! Um {{time}} Uhr"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
|
|||||||
@@ -69,15 +69,13 @@
|
|||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"openingHours": {
|
"openingHours": {
|
||||||
"closing": "Closing {{relativeDateTime}}",
|
"closing": "Closing {{date}}",
|
||||||
"closing_soon_warning": "Closing soon! At {{time}}",
|
"closing_soon": "Closing in {{duration}}",
|
||||||
"closing_today": "Closing at {{time}}",
|
|
||||||
"state_closed": "Closed",
|
"state_closed": "Closed",
|
||||||
"state_maybe": "Maybe open",
|
"state_maybe": "Maybe open",
|
||||||
"state_open": "Open",
|
"state_open": "Open",
|
||||||
"opening": "Opens {{relativeDateTime}}",
|
"opening": "Opens {{date}}",
|
||||||
"opening_today": "Opens at {{time}}",
|
"opening_soon": "Opens in {{duration}}"
|
||||||
"opening_soon_warning": "Opens soon! At {{time}}"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
|
|||||||
93
pnpm-lock.yaml
generated
93
pnpm-lock.yaml
generated
@@ -836,6 +836,9 @@ importers:
|
|||||||
cordova-plugin-calendar:
|
cordova-plugin-calendar:
|
||||||
specifier: 5.1.6
|
specifier: 5.1.6
|
||||||
version: 5.1.6
|
version: 5.1.6
|
||||||
|
date-fns:
|
||||||
|
specifier: 2.30.0
|
||||||
|
version: 2.30.0
|
||||||
deepmerge:
|
deepmerge:
|
||||||
specifier: 4.3.1
|
specifier: 4.3.1
|
||||||
version: 4.3.1
|
version: 4.3.1
|
||||||
@@ -863,6 +866,9 @@ importers:
|
|||||||
moment:
|
moment:
|
||||||
specifier: 2.29.4
|
specifier: 2.29.4
|
||||||
version: 2.29.4
|
version: 2.29.4
|
||||||
|
ngx-date-fns:
|
||||||
|
specifier: 10.0.1
|
||||||
|
version: 10.0.1(@angular/common@16.1.4)(@angular/core@16.1.4)(date-fns@2.30.0)
|
||||||
ngx-logger:
|
ngx-logger:
|
||||||
specifier: 5.0.12
|
specifier: 5.0.12
|
||||||
version: 5.0.12(rxjs@7.8.1)
|
version: 5.0.12(rxjs@7.8.1)
|
||||||
@@ -1003,8 +1009,8 @@ importers:
|
|||||||
specifier: 0.15.4
|
specifier: 0.15.4
|
||||||
version: 0.15.4
|
version: 0.15.4
|
||||||
cypress:
|
cypress:
|
||||||
specifier: 12.17.1
|
specifier: 13.2.0
|
||||||
version: 12.17.1
|
version: 13.2.0
|
||||||
eslint:
|
eslint:
|
||||||
specifier: 8.43.0
|
specifier: 8.43.0
|
||||||
version: 8.43.0
|
version: 8.43.0
|
||||||
@@ -2656,7 +2662,7 @@ packages:
|
|||||||
rxjs: ^5.5.0 || ^6.5.0 || ^7.3.0
|
rxjs: ^5.5.0 || ^6.5.0 || ^7.3.0
|
||||||
dependencies:
|
dependencies:
|
||||||
'@awesome-cordova-plugins/core': 5.45.0(rxjs@7.8.1)
|
'@awesome-cordova-plugins/core': 5.45.0(rxjs@7.8.1)
|
||||||
'@types/cordova': 11.0.0
|
'@types/cordova': 11.0.1
|
||||||
rxjs: 7.8.1
|
rxjs: 7.8.1
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
@@ -2665,7 +2671,7 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
rxjs: ^5.5.0 || ^6.5.0 || ^7.3.0
|
rxjs: ^5.5.0 || ^6.5.0 || ^7.3.0
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/cordova': 11.0.0
|
'@types/cordova': 11.0.1
|
||||||
rxjs: 7.8.1
|
rxjs: 7.8.1
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
@@ -5352,8 +5358,8 @@ packages:
|
|||||||
postcss-selector-parser: 6.0.13
|
postcss-selector-parser: 6.0.13
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@cypress/request@2.88.11:
|
/@cypress/request@3.0.1:
|
||||||
resolution: {integrity: sha512-M83/wfQ1EkspjkE2lNWNV5ui2Cv7UCv1swW1DqljahbzLVWltcsexQh8jYtuS/vzFXP+HySntGM83ZXA9fn17w==}
|
resolution: {integrity: sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
dependencies:
|
dependencies:
|
||||||
aws-sign2: 0.7.0
|
aws-sign2: 0.7.0
|
||||||
@@ -5371,7 +5377,7 @@ packages:
|
|||||||
performance-now: 2.1.0
|
performance-now: 2.1.0
|
||||||
qs: 6.10.4
|
qs: 6.10.4
|
||||||
safe-buffer: 5.2.1
|
safe-buffer: 5.2.1
|
||||||
tough-cookie: 2.5.0
|
tough-cookie: 4.1.3
|
||||||
tunnel-agent: 0.6.0
|
tunnel-agent: 0.6.0
|
||||||
uuid: 8.3.2
|
uuid: 8.3.2
|
||||||
dev: true
|
dev: true
|
||||||
@@ -5755,7 +5761,7 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
rxjs: ^5.5.0 || ^6.5.0
|
rxjs: ^5.5.0 || ^6.5.0
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/cordova': 11.0.0
|
'@types/cordova': 11.0.1
|
||||||
rxjs: 7.8.1
|
rxjs: 7.8.1
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
@@ -5767,7 +5773,7 @@ packages:
|
|||||||
rxjs: ^5.5.0 || ^6.5.0
|
rxjs: ^5.5.0 || ^6.5.0
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ionic-native/core': 5.36.0(rxjs@7.8.1)
|
'@ionic-native/core': 5.36.0(rxjs@7.8.1)
|
||||||
'@types/cordova': 11.0.0
|
'@types/cordova': 11.0.1
|
||||||
rxjs: 7.8.1
|
rxjs: 7.8.1
|
||||||
dev: false
|
dev: false
|
||||||
optional: true
|
optional: true
|
||||||
@@ -5780,7 +5786,7 @@ packages:
|
|||||||
rxjs: ^5.5.0 || ^6.5.0
|
rxjs: ^5.5.0 || ^6.5.0
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ionic-native/core': 5.36.0(rxjs@7.8.1)
|
'@ionic-native/core': 5.36.0(rxjs@7.8.1)
|
||||||
'@types/cordova': 11.0.0
|
'@types/cordova': 11.0.1
|
||||||
rxjs: 7.8.1
|
rxjs: 7.8.1
|
||||||
dev: false
|
dev: false
|
||||||
optional: true
|
optional: true
|
||||||
@@ -5793,7 +5799,7 @@ packages:
|
|||||||
rxjs: ^5.5.0 || ^6.5.0
|
rxjs: ^5.5.0 || ^6.5.0
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ionic-native/core': 5.36.0(rxjs@7.8.1)
|
'@ionic-native/core': 5.36.0(rxjs@7.8.1)
|
||||||
'@types/cordova': 11.0.0
|
'@types/cordova': 11.0.1
|
||||||
rxjs: 7.8.1
|
rxjs: 7.8.1
|
||||||
dev: false
|
dev: false
|
||||||
optional: true
|
optional: true
|
||||||
@@ -5806,7 +5812,7 @@ packages:
|
|||||||
rxjs: ^5.5.0 || ^6.5.0
|
rxjs: ^5.5.0 || ^6.5.0
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ionic-native/core': 5.36.0(rxjs@7.8.1)
|
'@ionic-native/core': 5.36.0(rxjs@7.8.1)
|
||||||
'@types/cordova': 11.0.0
|
'@types/cordova': 11.0.1
|
||||||
rxjs: 7.8.1
|
rxjs: 7.8.1
|
||||||
dev: false
|
dev: false
|
||||||
optional: true
|
optional: true
|
||||||
@@ -6728,8 +6734,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==}
|
resolution: {integrity: sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@types/cordova@11.0.0:
|
/@types/cordova@11.0.1:
|
||||||
resolution: {integrity: sha512-AtBm1IAqqXsXszJe6XxuA2iXLhraNCj25p/FHRyikPeW0Z3YfgM6qzWb+VJglJTmZc5lqRNy84cYM/sQI5v6Vw==}
|
resolution: {integrity: sha512-Zd6LAhYUAdn0mL0SbxHeF4fO/3uzkcW3fzE0ZIK1wDlTRCWlI4/0i+Phb+otP9ryziyeW2LKofRNSP5yil85hA==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@types/cors@2.8.13:
|
/@types/cors@2.8.13:
|
||||||
@@ -6964,6 +6970,10 @@ packages:
|
|||||||
/@types/node@18.15.3:
|
/@types/node@18.15.3:
|
||||||
resolution: {integrity: sha512-p6ua9zBxz5otCmbpb5D3U4B5Nanw6Pk3PPyX05xnxbB/fRv71N7CPmORg7uAD5P70T0xmx1pzAx/FUfa5X+3cw==}
|
resolution: {integrity: sha512-p6ua9zBxz5otCmbpb5D3U4B5Nanw6Pk3PPyX05xnxbB/fRv71N7CPmORg7uAD5P70T0xmx1pzAx/FUfa5X+3cw==}
|
||||||
|
|
||||||
|
/@types/node@18.17.17:
|
||||||
|
resolution: {integrity: sha512-cOxcXsQ2sxiwkykdJqvyFS+MLQPLvIdwh5l6gNg8qF6s+C7XSkEWOZjK+XhUZd+mYvHV/180g2cnCcIl4l06Pw==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/nodemailer@6.4.7:
|
/@types/nodemailer@6.4.7:
|
||||||
resolution: {integrity: sha512-f5qCBGAn/f0qtRcd4SEn88c8Fp3Swct1731X4ryPKqS61/A3LmmzN8zaEz7hneJvpjFbUUgY7lru/B/7ODTazg==}
|
resolution: {integrity: sha512-f5qCBGAn/f0qtRcd4SEn88c8Fp3Swct1731X4ryPKqS61/A3LmmzN8zaEz7hneJvpjFbUUgY7lru/B/7ODTazg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -9471,15 +9481,15 @@ packages:
|
|||||||
resolution: {integrity: sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==}
|
resolution: {integrity: sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/cypress@12.17.1:
|
/cypress@13.2.0:
|
||||||
resolution: {integrity: sha512-eKfBgO6t8waEyhegL4gxD7tcI6uTCGttu+ZU7y9Hq8BlpMztd7iLeIF4AJFAnbZH1xjX+wwgg4cRKFNSvv3VWQ==}
|
resolution: {integrity: sha512-AvDQxBydE771GTq0TR4ZUBvv9m9ffXuB/ueEtpDF/6gOcvFR96amgwSJP16Yhqw6VhmwqspT5nAGzoxxB+D89g==}
|
||||||
engines: {node: ^14.0.0 || ^16.0.0 || >=18.0.0}
|
engines: {node: ^16.0.0 || ^18.0.0 || >=20.0.0}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dependencies:
|
dependencies:
|
||||||
'@cypress/request': 2.88.11
|
'@cypress/request': 3.0.1
|
||||||
'@cypress/xvfb': 1.2.4(supports-color@8.1.1)
|
'@cypress/xvfb': 1.2.4(supports-color@8.1.1)
|
||||||
'@types/node': 14.18.38
|
'@types/node': 18.17.17
|
||||||
'@types/sinonjs__fake-timers': 8.1.1
|
'@types/sinonjs__fake-timers': 8.1.1
|
||||||
'@types/sizzle': 2.3.3
|
'@types/sizzle': 2.3.3
|
||||||
arch: 2.2.0
|
arch: 2.2.0
|
||||||
@@ -9512,6 +9522,7 @@ packages:
|
|||||||
minimist: 1.2.8
|
minimist: 1.2.8
|
||||||
ospath: 1.2.2
|
ospath: 1.2.2
|
||||||
pretty-bytes: 5.6.0
|
pretty-bytes: 5.6.0
|
||||||
|
process: 0.11.10
|
||||||
proxy-from-env: 1.0.0
|
proxy-from-env: 1.0.0
|
||||||
request-progress: 3.0.0
|
request-progress: 3.0.0
|
||||||
semver: 7.5.4
|
semver: 7.5.4
|
||||||
@@ -14272,6 +14283,19 @@ packages:
|
|||||||
resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==}
|
resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/ngx-date-fns@10.0.1(@angular/common@16.1.4)(@angular/core@16.1.4)(date-fns@2.30.0):
|
||||||
|
resolution: {integrity: sha512-8IdwrblaMULNQgqwA0Yo8HPpUMQDLKC1pvkoFTToElf7gZDPWroOva2cogUrrNXXWLkwA9YDpc1skZVfI+mmwA==}
|
||||||
|
peerDependencies:
|
||||||
|
'@angular/common': '>=14'
|
||||||
|
'@angular/core': '>=14'
|
||||||
|
date-fns: '>=2'
|
||||||
|
dependencies:
|
||||||
|
'@angular/common': 16.1.4(@angular/core@16.1.4)(rxjs@7.8.1)
|
||||||
|
'@angular/core': 16.1.4(rxjs@7.8.1)(zone.js@0.13.1)
|
||||||
|
date-fns: 2.30.0
|
||||||
|
tslib: 2.4.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/ngx-logger@5.0.12(rxjs@7.8.1):
|
/ngx-logger@5.0.12(rxjs@7.8.1):
|
||||||
resolution: {integrity: sha512-4kTtPvxQoV2ka6pigtvkbtaLKpMYWqZm7Slu0YQVcwzBKoVR2K+oLmMVcA50S6kCxkZXq7iKcrXUKR2vhMXPqQ==}
|
resolution: {integrity: sha512-4kTtPvxQoV2ka6pigtvkbtaLKpMYWqZm7Slu0YQVcwzBKoVR2K+oLmMVcA50S6kCxkZXq7iKcrXUKR2vhMXPqQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -15419,6 +15443,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/process@0.11.10:
|
||||||
|
resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
|
||||||
|
engines: {node: '>= 0.6.0'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/progress@1.1.8:
|
/progress@1.1.8:
|
||||||
resolution: {integrity: sha512-UdA8mJ4weIkUBO224tIarHzuHs4HuYiJvsuGT7j/SPQiUJVjYvNDBIPa0hAorduOfjGohB/qHWRa/lrrWX/mXw==}
|
resolution: {integrity: sha512-UdA8mJ4weIkUBO224tIarHzuHs4HuYiJvsuGT7j/SPQiUJVjYvNDBIPa0hAorduOfjGohB/qHWRa/lrrWX/mXw==}
|
||||||
engines: {node: '>=0.4.0'}
|
engines: {node: '>=0.4.0'}
|
||||||
@@ -15563,6 +15592,10 @@ packages:
|
|||||||
engines: {node: '>=0.6'}
|
engines: {node: '>=0.6'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/querystringify@2.2.0:
|
||||||
|
resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/queue-microtask@1.2.3:
|
/queue-microtask@1.2.3:
|
||||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||||
|
|
||||||
@@ -17574,6 +17607,16 @@ packages:
|
|||||||
punycode: 2.3.0
|
punycode: 2.3.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/tough-cookie@4.1.3:
|
||||||
|
resolution: {integrity: sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
dependencies:
|
||||||
|
psl: 1.9.0
|
||||||
|
punycode: 2.3.0
|
||||||
|
universalify: 0.2.0
|
||||||
|
url-parse: 1.5.10
|
||||||
|
dev: true
|
||||||
|
|
||||||
/tr46@1.0.1:
|
/tr46@1.0.1:
|
||||||
resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==}
|
resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -18116,6 +18159,11 @@ packages:
|
|||||||
engines: {node: '>= 4.0.0'}
|
engines: {node: '>= 4.0.0'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/universalify@0.2.0:
|
||||||
|
resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==}
|
||||||
|
engines: {node: '>= 4.0.0'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/universalify@2.0.0:
|
/universalify@2.0.0:
|
||||||
resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==}
|
resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==}
|
||||||
engines: {node: '>= 10.0.0'}
|
engines: {node: '>= 10.0.0'}
|
||||||
@@ -18157,6 +18205,13 @@ packages:
|
|||||||
resolution: {integrity: sha512-1WJ8YX1Kcec9wgxy8d/ATzGP1ayO6BRnd3iB6NlM+7cOnn6U8p5PKppRTCPLobh3CSdJ4d0TdPjopzyU2KcVFw==}
|
resolution: {integrity: sha512-1WJ8YX1Kcec9wgxy8d/ATzGP1ayO6BRnd3iB6NlM+7cOnn6U8p5PKppRTCPLobh3CSdJ4d0TdPjopzyU2KcVFw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/url-parse@1.5.10:
|
||||||
|
resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==}
|
||||||
|
dependencies:
|
||||||
|
querystringify: 2.2.0
|
||||||
|
requires-port: 1.0.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/url-value-parser@2.2.0:
|
/url-value-parser@2.2.0:
|
||||||
resolution: {integrity: sha512-yIQdxJpgkPamPPAPuGdS7Q548rLhny42tg8d4vyTNzFqvOnwqrgHXvgehT09U7fwrzxi3RxCiXjoNUNnNOlQ8A==}
|
resolution: {integrity: sha512-yIQdxJpgkPamPPAPuGdS7Q548rLhny42tg8d4vyTNzFqvOnwqrgHXvgehT09U7fwrzxi3RxCiXjoNUNnNOlQ8A==}
|
||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
|
|||||||
Reference in New Issue
Block a user