From 9e26fa7a1a311f9ead265926f8e97ae5981eabdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thea=20Sch=C3=B6bl?= Date: Wed, 26 Jul 2023 16:20:03 +0200 Subject: [PATCH] refactor: replace moment.js with date-fns --- .changeset/wicked-cheetahs-prove.md | 5 + frontend/app/cypress/integration/ical.spec.ts | 4 +- .../app/cypress/integration/schedule.spec.ts | 6 +- frontend/app/package.json | 7 +- .../_helpers/data/resources/test-resources.ts | 18 +- frontend/app/src/app/app.module.ts | 7 +- .../modules/assessments/assessments.module.ts | 6 +- .../assessment/assessment-list-item.html | 4 +- .../modules/background/background.module.ts | 11 +- .../schedule/schedule-sync.service.ts | 9 +- .../add-event-review-modal.component.ts | 3 - .../calendar/add-event-review-modal.html | 2 +- .../app/modules/calendar/calendar.module.ts | 6 +- .../app/modules/calendar/calendar.service.ts | 19 +- .../app/modules/calendar/ical/ical.spec.ts | 50 +++--- .../app/src/app/modules/calendar/ical/ical.ts | 38 ++-- .../app/modules/calendar/schedule.provider.ts | 17 +- .../app/modules/catalog/catalog.component.ts | 4 +- .../src/app/modules/catalog/catalog.module.ts | 2 - .../dashboard/dashboard.component.html | 2 +- .../modules/dashboard/dashboard.component.ts | 10 +- .../app/modules/dashboard/dashboard.module.ts | 5 +- .../mensa-section-content.component.ts | 10 +- .../data/chips/edit-event-selection.html | 15 +- .../app/src/app/modules/data/data.module.ts | 24 ++- .../data/detail/data-detail.component.spec.ts | 2 +- .../modules/data/elements/offers-detail.html | 2 +- .../modules/data/elements/origin-detail.html | 20 +-- .../data/list/food-data-list.component.ts | 165 ++++++++---------- .../app/modules/data/list/food-data-list.html | 7 + .../data/list/search-page-switch-animation.ts | 4 +- .../data/list/search-page.component.ts | 26 +-- .../date-series-detail-content.html | 8 +- .../date-series/date-series-list-item.html | 7 +- .../types/message/message-detail-content.html | 4 +- .../types/place/place-list-item.component.ts | 20 ++- .../data/types/place/place-list-item.html | 4 +- .../mensa/place-mensa-detail.component.ts | 10 +- .../special/mensa/place-mensa-service.ts | 4 +- .../place/special/mensa/place-mensa.html | 4 +- .../semester/semester-detail-content.html | 2 +- .../types/semester/semester-list-item.html | 5 +- .../types/video/video-detail-content.html | 2 +- .../data/types/video/video-list-item.html | 4 +- .../favorites/favorites-page.component.ts | 7 +- .../daia-availability.component.spec.ts | 2 +- .../hebis/daia-availability/daia-holding.html | 2 +- .../hebis-detail.component.spec.ts | 2 +- .../app/src/app/modules/hebis/hebis.module.ts | 9 +- .../hebis/list/hebis-search-page.component.ts | 2 +- .../account/elements/fee-item/fee-item.html | 2 +- .../account/elements/paia-item/paiaitem.html | 2 +- .../library/account/profile/profile-page.html | 4 +- .../src/app/modules/library/library.module.ts | 6 +- .../src/app/modules/map/position.service.ts | 2 - .../src/app/modules/news/item/news-item.html | 2 +- .../app/src/app/modules/news/news.module.ts | 5 +- .../profile/page/my-courses.component.ts | 11 +- .../app/modules/profile/page/my-courses.html | 10 +- .../src/app/modules/profile/profile.module.ts | 7 +- .../schedule/page/calendar-view.component.ts | 10 +- .../modules/schedule/page/calendar-view.html | 45 ++--- .../page/components/calendar.component.ts | 72 +++----- .../page/grid/infinite-swiper.component.ts | 32 +--- .../page/grid/schedule-card.component.ts | 4 +- .../schedule/page/grid/schedule-card.html | 3 +- .../page/grid/schedule-cursor.component.ts | 11 +- .../page/grid/schedule-day.component.ts | 8 +- .../schedule/page/grid/schedule-day.html | 4 +- .../schedule/page/schedule-page.component.ts | 4 +- .../page/schedule-single-events.component.ts | 31 ++-- .../schedule/page/schedule-single-events.html | 2 +- .../page/schedule-single-events.spec.ts | 12 +- .../schedule/page/schedule-view.component.ts | 31 +--- .../modules/schedule/page/schedule-view.html | 29 +-- .../modules/schedule/page/schema/schema.ts | 4 +- .../app/modules/schedule/schedule.module.ts | 10 +- .../app/translation/common-string-pipes.ts | 87 --------- .../date-time/format-frequency.pipe.ts | 68 ++++++++ .../date-time/format-relative-date.pipe.ts | 67 +++++++ .../date-time/format-relative-live.pipe.ts | 57 ++++++ .../date-time/opening-hours.pipe.ts | 141 +++++++++++++++ .../date-time/parse-duration.pipe.ts | 14 ++ .../app/translation/thing-translate.module.ts | 9 - .../translation/thing-translate.service.ts | 3 - frontend/app/src/app/util/array-last.pipe.ts | 34 ---- .../app/src/app/util/date-from-index.pipe.ts | 27 --- .../app/src/app/util/date-is-today.pipe.ts | 38 ---- frontend/app/src/app/util/daytime-key.pipe.ts | 5 +- .../src/app/util/next-date-in-list.pipe.ts | 41 ----- .../src/app/util/nullish-coalecing.pipe.ts | 34 ---- frontend/app/src/app/util/util.module.ts | 15 -- 92 files changed, 765 insertions(+), 846 deletions(-) create mode 100644 .changeset/wicked-cheetahs-prove.md create mode 100644 frontend/app/src/app/modules/data/list/food-data-list.html create mode 100644 frontend/app/src/app/translation/date-time/format-frequency.pipe.ts create mode 100644 frontend/app/src/app/translation/date-time/format-relative-date.pipe.ts create mode 100644 frontend/app/src/app/translation/date-time/format-relative-live.pipe.ts create mode 100644 frontend/app/src/app/translation/date-time/opening-hours.pipe.ts create mode 100644 frontend/app/src/app/translation/date-time/parse-duration.pipe.ts delete mode 100644 frontend/app/src/app/util/array-last.pipe.ts delete mode 100644 frontend/app/src/app/util/date-from-index.pipe.ts delete mode 100644 frontend/app/src/app/util/date-is-today.pipe.ts delete mode 100644 frontend/app/src/app/util/next-date-in-list.pipe.ts delete mode 100644 frontend/app/src/app/util/nullish-coalecing.pipe.ts diff --git a/.changeset/wicked-cheetahs-prove.md b/.changeset/wicked-cheetahs-prove.md new file mode 100644 index 00000000..453bdcb0 --- /dev/null +++ b/.changeset/wicked-cheetahs-prove.md @@ -0,0 +1,5 @@ +--- +'@openstapps/app': minor +--- + +Replace moment.js with date-fns diff --git a/frontend/app/cypress/integration/ical.spec.ts b/frontend/app/cypress/integration/ical.spec.ts index fd0f62f6..b5f1dba5 100644 --- a/frontend/app/cypress/integration/ical.spec.ts +++ b/frontend/app/cypress/integration/ical.spec.ts @@ -30,14 +30,14 @@ describe('ical', function () { cy.get('ion-app > ion-modal').within(() => { cy.get('ion-footer > ion-toolbar > ion-button').should('have.attr', 'disabled'); - cy.contains('ion-item', /19\.\s+Januar\s+2059,\s+\d{2}:00\s+-\s+\d{2}:00/).click(); + cy.contains('ion-item', /1\s+Stunde\s+Sonntag,\s+19\.\s+Januar\s+2059\s+um\s+\d{2}:00/).click(); cy.get('ion-footer > ion-toolbar > ion-button').should('not.have.attr', 'disabled'); cy.get('ion-footer > ion-toolbar > ion-button').click(); }); cy.get('add-event-review-modal').within(() => { cy.get('ion-item-group').should('contain', 'UNIcert (Test)'); - cy.contains('ion-item-group', /19\.\s+Jan\.\s+2059,\s+\d{2}:00/); + cy.contains('ion-item-group', /19\.\s+Januar\s+2059\s+um\s+\d{2}:00/); }); }); }); diff --git a/frontend/app/cypress/integration/schedule.spec.ts b/frontend/app/cypress/integration/schedule.spec.ts index f55c3f45..e5244c25 100644 --- a/frontend/app/cypress/integration/schedule.spec.ts +++ b/frontend/app/cypress/integration/schedule.spec.ts @@ -25,7 +25,7 @@ describe('schedule', function () { it('should respect the url', function () { cy.visit('/schedule/calendar/2022-01-19'); - cy.get('#date-select-button0').should('contain', '19.01.22'); + cy.get('#date-select-button0').should('contain', '19.01.2022'); }); it('should navigate a full page', function () { @@ -66,13 +66,13 @@ describe('schedule', function () { it('should navigate to a specific date', function () { cy.visit('/schedule/calendar/2059-01-19'); - cy.contains('#date-select-button0', '19.01.59').click(); + cy.contains('#date-select-button0', '19.01.2059').click(); cy.wait(2000); cy.get('button[data-day=1][data-month=1][data-year=2059]', { includeShadowDom: true, }).click(); cy.wait(2000); - cy.contains('#date-select-button0', '01.01.59').click(); + cy.contains('#date-select-button0', '01.01.2059').click(); }); // TODO: Reenable and stabilize tests diff --git a/frontend/app/package.json b/frontend/app/package.json index f0a8c01c..5c52e032 100644 --- a/frontend/app/package.json +++ b/frontend/app/package.json @@ -15,7 +15,7 @@ ], "scripts": { "analyze": "webpack-bundle-analyzer www/stats.json", - "build": "pnpm check-icons && ng build --configuration=production --stats-json && webpack-bundle-analyzer www/stats.json --mode static --report www/bundle-info.html", + "build": "pnpm check-icons && ng build --configuration=production --stats-json && webpack-bundle-analyzer --no-open www/stats.json --mode static --report www/bundle-info.html", "build:analyze": "npm run build:stats && npm run analyze", "build:android": "ionic capacitor build android --no-open && cd android && ./gradlew clean assembleDebug && cd ..", "build:prod": "ng build --configuration=production", @@ -92,7 +92,7 @@ "capacitor-secure-storage-plugin": "0.8.1", "cordova-plugin-calendar": "5.1.6", "date-fns": "2.30.0", - "ngx-date-fns": "10.0.1", + "duration-fns": "3.0.2", "deepmerge": "4.3.1", "form-data": "4.0.0", "geojson": "0.5.0", @@ -101,10 +101,9 @@ "leaflet": "1.9.3", "leaflet.markercluster": "1.5.3", "material-symbols": "0.10.0", - "moment": "2.29.4", + "ngx-date-fns": "10.0.1", "ngx-logger": "5.0.12", "ngx-markdown": "16.0.0", - "ngx-moment": "6.0.2", "opening_hours": "3.8.0", "rxjs": "7.8.1", "swiper": "8.4.5", diff --git a/frontend/app/src/app/_helpers/data/resources/test-resources.ts b/frontend/app/src/app/_helpers/data/resources/test-resources.ts index ca5abada..6cdab952 100644 --- a/frontend/app/src/app/_helpers/data/resources/test-resources.ts +++ b/frontend/app/src/app/_helpers/data/resources/test-resources.ts @@ -14,7 +14,7 @@ */ /* eslint-disable */ -import moment from 'moment'; +import {addDays, endOfToday, formatISO, startOfToday} from 'date-fns'; export const sampleResources = [ { @@ -793,8 +793,8 @@ export const sampleResources = [ offers: [ { availability: 'in stock', - availabilityStarts: moment().startOf('day').add(2, 'days').toISOString(), - availabilityEnds: moment().endOf('day').add(2, 'days').toISOString(), + availabilityStarts: formatISO(addDays(startOfToday(), 2)), + availabilityEnds: formatISO(addDays(endOfToday(), 2)), prices: { default: 6.5, student: 5, @@ -904,8 +904,8 @@ export const sampleResources = [ offers: [ { availability: 'in stock', - availabilityStarts: moment().startOf('day').toISOString(), - availabilityEnds: moment().endOf('day').add(2, 'days').toISOString(), + availabilityStarts: formatISO(startOfToday()), + availabilityEnds: formatISO(addDays(endOfToday(), 2)), prices: { default: 4.85, student: 2.85, @@ -984,8 +984,8 @@ export const sampleResources = [ uid: '3b9b3df6-3a7a-58cc-922f-c7335c002634', }, availability: 'in stock', - availabilityStarts: moment().startOf('day').add(2, 'days').toISOString(), - availabilityEnds: moment().endOf('day').add(2, 'days').toISOString(), + availabilityStarts: formatISO(addDays(startOfToday(), 2)), + availabilityEnds: formatISO(addDays(endOfToday(), 2)), inPlace: { geo: { point: { @@ -1046,8 +1046,8 @@ export const sampleResources = [ ], offers: [ { - availabilityEnds: moment().endOf('day').toISOString(), - availabilityStarts: moment().startOf('day').toISOString(), + availabilityEnds: formatISO(endOfToday()), + availabilityStarts: formatISO(startOfToday()), availability: 'in stock', inPlace: { type: 'room', diff --git a/frontend/app/src/app/app.module.ts b/frontend/app/src/app/app.module.ts index 7f4859b7..e1b3c561 100644 --- a/frontend/app/src/app/app.module.ts +++ b/frontend/app/src/app/app.module.ts @@ -21,11 +21,8 @@ import {RouteReuseStrategy} from '@angular/router'; import {IonicModule, IonicRouteStrategy, Platform} from '@ionic/angular'; import {TranslateLoader, TranslateModule, TranslateService} from '@ngx-translate/core'; import {TranslateHttpLoader} from '@ngx-translate/http-loader'; -import moment from 'moment'; -import 'moment/min/locales'; import {LoggerModule, NGXLogger, NgxLoggerLevel} from 'ngx-logger'; import SwiperCore, {FreeMode, Navigation} from 'swiper'; - import {environment} from '../environments/environment'; import {AppRoutingModule} from './app-routing.module'; import {AppComponent} from './app.component'; @@ -66,7 +63,7 @@ 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'; +import {DateFnsConfigurationService, DateFnsModule} from 'ngx-date-fns'; registerLocaleData(localeDe); @@ -104,7 +101,6 @@ export function initializerFactory( // this language will be used as a fallback when a translation isn't found in the current language translateService.setDefaultLang('en'); translateService.use(languageCode); - moment.locale(languageCode); const dateFnsLocale = await getDateFnsLocale(languageCode as SCLanguageCode); setDefaultOptions({locale: dateFnsLocale}); dateFnsConfigurationService.setLocale(dateFnsLocale); @@ -144,6 +140,7 @@ export function createTranslateLoader(http: HttpClient) { ConfigModule, DashboardModule, DataModule, + DateFnsModule.forRoot(), HebisModule, IonicModule.forRoot(), IonIconModule, diff --git a/frontend/app/src/app/modules/assessments/assessments.module.ts b/frontend/app/src/app/modules/assessments/assessments.module.ts index a6eb1373..8412ead9 100644 --- a/frontend/app/src/app/modules/assessments/assessments.module.ts +++ b/frontend/app/src/app/modules/assessments/assessments.module.ts @@ -12,7 +12,6 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ - import {NgModule} from '@angular/core'; import {AssessmentListItemComponent} from './types/assessment/assessment-list-item.component'; import {AssessmentBaseInfoComponent} from './types/assessment/assessment-base-info.component'; @@ -27,7 +26,6 @@ import {CourseOfStudyAssessmentComponent} from './types/course-of-study/course-o import {AssessmentsPageComponent} from './page/assessments-page.component'; import {RouterModule} from '@angular/router'; import {AuthGuardService} from '../auth/auth-guard.service'; -import {MomentModule} from 'ngx-moment'; import {AssessmentsListItemComponent} from './list/assessments-list-item.component'; import {AssessmentsDataListComponent} from './list/assessments-data-list.component'; import {AssessmentsDetailComponent} from './detail/assessments-detail.component'; @@ -37,6 +35,7 @@ import {ProtectedRoutes} from '../auth/protected.routes'; import {AssessmentsTreeListComponent} from './list/assessments-tree-list.component'; import {IonIconModule} from '../../util/ion-icon/ion-icon.module'; import {UtilModule} from '../../util/util.module'; +import {FormatPipeModule, ParseIsoPipeModule} from 'ngx-date-fns'; const routes: ProtectedRoutes = [ { @@ -75,8 +74,9 @@ const routes: ProtectedRoutes = [ TranslateModule, DataModule, ThingTranslateModule, - MomentModule, UtilModule, + ParseIsoPipeModule, + FormatPipeModule, ], providers: [AssessmentsProvider], exports: [], diff --git a/frontend/app/src/app/modules/assessments/types/assessment/assessment-list-item.html b/frontend/app/src/app/modules/assessments/types/assessment/assessment-list-item.html index cb76aee5..8d5f8e37 100644 --- a/frontend/app/src/app/modules/assessments/types/assessment/assessment-list-item.html +++ b/frontend/app/src/app/modules/assessments/types/assessment/assessment-list-item.html @@ -14,6 +14,8 @@ -->
-

{{ 'name' | thingTranslate : item }} {{ item.date ? (item.date | amDateFormat) : '' }}

+

+ {{ 'name' | thingTranslate : item }} {{ item.date ? (item.date | dfnsParseIso | dfnsFormat : 'Pp') : '' }} +

diff --git a/frontend/app/src/app/modules/background/background.module.ts b/frontend/app/src/app/modules/background/background.module.ts index 96887c33..063a0833 100644 --- a/frontend/app/src/app/modules/background/background.module.ts +++ b/frontend/app/src/app/modules/background/background.module.ts @@ -12,10 +12,8 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ - import {NgModule} from '@angular/core'; import {ScheduleSyncService} from './schedule/schedule-sync.service'; -import {DateFormatPipe, DurationPipe} from 'ngx-moment'; import {CalendarModule} from '../calendar/calendar.module'; import {ScheduleProvider} from '../calendar/schedule.provider'; import {StorageProvider} from '../storage/storage.provider'; @@ -27,13 +25,6 @@ import {CalendarService} from '../calendar/calendar.service'; @NgModule({ declarations: [], imports: [CalendarModule], - providers: [ - DurationPipe, - DateFormatPipe, - ScheduleProvider, - StorageProvider, - CalendarService, - ScheduleSyncService, - ], + providers: [ScheduleProvider, StorageProvider, CalendarService, ScheduleSyncService], }) export class BackgroundModule {} diff --git a/frontend/app/src/app/modules/background/schedule/schedule-sync.service.ts b/frontend/app/src/app/modules/background/schedule/schedule-sync.service.ts index 3ceaa38b..0ffaf22a 100644 --- a/frontend/app/src/app/modules/background/schedule/schedule-sync.service.ts +++ b/frontend/app/src/app/modules/background/schedule/schedule-sync.service.ts @@ -22,7 +22,6 @@ import { import {SCDateSeries, SCThingType, SCUuid} from '@openstapps/core'; import {LocalNotifications} from '@capacitor/local-notifications'; import {ThingTranslateService} from '../../../translation/thing-translate.service'; -import {DateFormatPipe, DurationPipe} from 'ngx-moment'; import {BackgroundFetch} from '@transistorsoft/capacitor-background-fetch'; import {StorageProvider} from '../../storage/storage.provider'; import {CalendarService} from '../../calendar/calendar.service'; @@ -46,8 +45,6 @@ export class ScheduleSyncService { private scheduleProvider: ScheduleProvider, private storageProvider: StorageProvider, private translator: ThingTranslateService, - private dateFormatPipe: DateFormatPipe, - private durationFormatPipe: DurationPipe, private calendar: CalendarService, ) {} @@ -136,11 +133,7 @@ export class ScheduleSyncService { change => `${ this.translator.translator.translatedPropertyNames(SCThingType.DateSeries)?.[change] - }: ${formatRelevantKeys[change]( - changes.new[change] as never, - this.dateFormatPipe, - this.durationFormatPipe, - )}`, + }: ${formatRelevantKeys[change](changes.new[change] as never)}`, ); } diff --git a/frontend/app/src/app/modules/calendar/add-event-review-modal.component.ts b/frontend/app/src/app/modules/calendar/add-event-review-modal.component.ts index 23361c19..d8a19507 100644 --- a/frontend/app/src/app/modules/calendar/add-event-review-modal.component.ts +++ b/frontend/app/src/app/modules/calendar/add-event-review-modal.component.ts @@ -21,7 +21,6 @@ import { toICal, toICalUpdates, } from './ical/ical'; -import moment from 'moment'; import {Share} from '@capacitor/share'; import {Directory, Encoding, Filesystem} from '@capacitor/filesystem'; import {Device} from '@capacitor/device'; @@ -44,8 +43,6 @@ interface ICalInfo { styleUrls: ['add-event-review-modal.scss'], }) export class AddEventReviewModalComponent implements OnInit { - moment = moment; - @Input() dismissAction: () => void; @Input() dateSeries: SCDateSeries[]; diff --git a/frontend/app/src/app/modules/calendar/add-event-review-modal.html b/frontend/app/src/app/modules/calendar/add-event-review-modal.html index 82a2d674..1e4e6560 100644 --- a/frontend/app/src/app/modules/calendar/add-event-review-modal.html +++ b/frontend/app/src/app/modules/calendar/add-event-review-modal.html @@ -34,7 +34,7 @@ - {{ moment(iCalEvent.start) | amDateFormat : 'll, HH:mm' }} + {{ iCalEvent.start | dfnsParseIso | dfnsFormat : 'PPPp' }} {{ iCalEvent.rrule.interval }} {{ iCalEvent.rrule.freq | sentencecase }} diff --git a/frontend/app/src/app/modules/calendar/calendar.module.ts b/frontend/app/src/app/modules/calendar/calendar.module.ts index b28658fe..e033304b 100644 --- a/frontend/app/src/app/modules/calendar/calendar.module.ts +++ b/frontend/app/src/app/modules/calendar/calendar.module.ts @@ -12,7 +12,6 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ - import {NgModule} from '@angular/core'; import {AddEventReviewModalComponent} from './add-event-review-modal.component'; import {Calendar} from '@awesome-cordova-plugins/calendar/ngx'; @@ -23,9 +22,9 @@ import {TranslateModule} from '@ngx-translate/core'; import {ThingTranslateModule} from '../../translation/thing-translate.module'; import {FormsModule} from '@angular/forms'; import {CommonModule} from '@angular/common'; -import {MomentModule} from 'ngx-moment'; import {UtilModule} from '../../util/util.module'; import {IonIconModule} from '../../util/ion-icon/ion-icon.module'; +import {FormatPipeModule, ParseIsoPipeModule} from 'ngx-date-fns'; @NgModule({ declarations: [AddEventReviewModalComponent], @@ -36,8 +35,9 @@ import {IonIconModule} from '../../util/ion-icon/ion-icon.module'; IonIconModule, FormsModule, CommonModule, - MomentModule, UtilModule, + ParseIsoPipeModule, + FormatPipeModule, ], exports: [], providers: [Calendar, CalendarService, ScheduleProvider], diff --git a/frontend/app/src/app/modules/calendar/calendar.service.ts b/frontend/app/src/app/modules/calendar/calendar.service.ts index 7627d5bb..c587d183 100644 --- a/frontend/app/src/app/modules/calendar/calendar.service.ts +++ b/frontend/app/src/app/modules/calendar/calendar.service.ts @@ -16,17 +16,18 @@ import {Calendar} from '@awesome-cordova-plugins/calendar/ngx'; import {Injectable} from '@angular/core'; import {ICalEvent} from './ical/ical'; -import moment, {duration, Moment, unitOfTime} from 'moment'; import {Dialog} from '@capacitor/dialog'; import {CalendarInfo} from './calendar-info'; import {Subject} from 'rxjs'; import {ConfigProvider} from '../config/config.provider'; +import {add, differenceInDays, parseISO, startOfToday} from 'date-fns'; +import {parse as parseISODuration} from 'duration-fns'; -const RECURRENCE_PATTERNS: Partial> = { - year: 'yearly', - month: 'monthly', - week: 'weekly', - day: 'daily', +const RECURRENCE_PATTERNS: Partial> = { + years: 'yearly', + months: 'monthly', + weeks: 'weekly', + days: 'daily', }; @Injectable() @@ -85,7 +86,7 @@ export class CalendarService { iCalEvent.geo, iCalEvent.description, new Date(start), - moment(start).add(duration(iCalEvent.duration)).toDate(), + add(parseISO(start), parseISODuration(iCalEvent.duration!)), { id: `${iCalEvent.uuid}-${start}`, url: iCalEvent.url, @@ -107,8 +108,8 @@ export class CalendarService { * Emit the calendar index corresponding to the input date. * @param date Moment - date the calendar should go to */ - emitGoToDate(date: Moment) { - const index = date.diff(moment().startOf('day'), 'days'); + emitGoToDate(date: Date) { + const index = differenceInDays(date, startOfToday()); this.goToDate.next(index); } } diff --git a/frontend/app/src/app/modules/calendar/ical/ical.spec.ts b/frontend/app/src/app/modules/calendar/ical/ical.spec.ts index 158372ad..34067666 100644 --- a/frontend/app/src/app/modules/calendar/ical/ical.spec.ts +++ b/frontend/app/src/app/modules/calendar/ical/ical.spec.ts @@ -13,61 +13,59 @@ * this program. If not, see . */ import {findRRules, RRule} from './ical'; -import moment, {unitOfTime} from 'moment'; import {SCISO8601Date} from '@openstapps/core'; import {shuffle} from '@openstapps/collection-utils'; +import {add, addWeeks, formatISO, isEqual, parseISO} from 'date-fns'; +import {normalize} from 'duration-fns'; /** * */ function expandRRule(rule: RRule): SCISO8601Date[] { - const initial = moment(rule.from); + const initial = parseISO(rule.from); const interval = rule.interval ?? 1; + const dates = [initial]; + while (!isEqual(dates.at(-1)!, parseISO(rule.until))) { + dates.push(add(dates.at(-1)!, normalize({[rule.freq ?? 'days']: interval}, dates.at(-1)))); + } - return shuffle( - Array.from({ - length: Math.floor(moment(rule.until).diff(initial, rule.freq, true) / interval) + 1, - }).map((_, i) => - initial - .clone() - .add(interval * i, rule.freq ?? 'day') - .toISOString(), - ), - ); + return shuffle(dates.map(date => formatISO(date))); } describe('iCal', () => { - it('should find simple recurrence patterns', () => { - for (const freq of ['day', 'week', 'month', 'year'] as unitOfTime.Diff[]) { - for (const interval of [1, 2, 3]) { + for (const freq of ['days', 'weeks', 'months', 'years'] as const) { + for (const interval of [1, 2, 3]) { + it(`should find ${interval} ${freq} recurrence patterns`, () => { const pattern: RRule = { freq: freq, interval: interval, - from: moment('2021-09-01T10:00').toISOString(), - until: moment('2021-09-01T10:00') - .add(4 * interval, freq) - .toISOString(), + from: formatISO(parseISO('2021-09-01T10:00Z')), + until: formatISO( + add(parseISO('2021-09-01T10:00Z'), normalize({[freq]: 4 * interval}, '2021-09-01')), + ), }; + console.log(expandRRule(pattern)); + expect(findRRules(expandRRule(pattern))).toEqual([pattern]); - } + }); } - }); + } it('should find missing recurrence patterns', () => { - const pattern: SCISO8601Date = moment('2021-09-01T10:00').toISOString(); + const pattern: SCISO8601Date = formatISO(parseISO('2021-09-01T10:00')); expect(findRRules([pattern])).toEqual([pattern]); }); it('should find mixed recurrence patterns', () => { - const singlePattern: SCISO8601Date = moment('2021-09-01T09:00').toISOString(); + const singlePattern: SCISO8601Date = formatISO(parseISO('2021-09-01T09:00')); const weeklyPattern: RRule = { - freq: 'week', + freq: 'weeks', interval: 1, - from: moment('2021-09-03T10:00').toISOString(), - until: moment('2021-09-03T10:00').add(4, 'weeks').toISOString(), + from: formatISO(parseISO('2021-09-03T10:00')), + until: formatISO(addWeeks(parseISO('2021-09-03T10:00'), 4)), }; expect(findRRules(shuffle([singlePattern, ...expandRRule(weeklyPattern)]))).toEqual([ diff --git a/frontend/app/src/app/modules/calendar/ical/ical.ts b/frontend/app/src/app/modules/calendar/ical/ical.ts index f580cd8c..dd680958 100644 --- a/frontend/app/src/app/modules/calendar/ical/ical.ts +++ b/frontend/app/src/app/modules/calendar/ical/ical.ts @@ -20,8 +20,10 @@ import { SCThingWithCategories, SCUuid, } from '@openstapps/core'; -import moment, {unitOfTime} from 'moment'; import {minBy, mapValues} from '@openstapps/collection-utils'; +import type {Duration} from 'date-fns'; +import {formatISO, intervalToDuration, parseISO} from 'date-fns'; +import {toUnit} from 'duration-fns'; export interface ICalEvent { name?: string; @@ -55,19 +57,25 @@ export type ICalLike = ICalKeyValuePair[]; function timeDistance( current: SCISO8601Date, next: SCISO8601Date | undefined, - recurrence: unitOfTime.Diff, + recurrence: keyof Duration, ): number | undefined { if (!next) { return undefined; } - const diff = moment(next).diff(moment(current), recurrence, true); + const diff = toUnit( + intervalToDuration({ + start: parseISO(next), + end: parseISO(current), + }), + recurrence, + ); return Math.floor(diff) === diff ? diff : undefined; } export interface RRule { - freq: unitOfTime.Diff; // 'SECONDLY' | 'HOURLY' | 'DAILY' | 'WEEKLY' | 'MONTHLY' | 'YEARLY'; + freq: keyof Duration; interval: number; from: SCISO8601Date; until: SCISO8601Date; @@ -97,7 +105,7 @@ export function mergeRRules(rules: Array, allowExceptions * Find RRules in a list of dates */ export function findRRules(dates: SCISO8601Date[]): Array { - const sorted = dates.sort((a, b) => moment(a).unix() - moment(b).unix()); + const sorted = dates.sort(); const output: Optional[] = [ { @@ -112,7 +120,9 @@ export function findRRules(dates: SCISO8601Date[]): Array const next = sorted[i + 1] as SCISO8601Date | undefined; const element = output.at(-1); - const units: unitOfTime.Diff[] = element?.freq ? [element.freq] : ['day', 'week', 'month', 'year']; + const units: Array = element?.freq + ? [element.freq] + : ['days', 'weeks', 'months', 'years']; const freq = minBy( units.map(recurrence => ({ recurrence: recurrence, @@ -226,14 +236,14 @@ export function toICalUpdates(dateSeries: SCDateSeries, translator: SCThingTrans export function iso8601ToICalDateTime( date: T, ): T extends SCISO8601Date ? string : undefined { - return (date ? `${moment(date).utc().format('YYYYMMDDTHHmmss')}Z` : undefined) as never; + return (date ? formatISO(parseISO(date), {format: 'basic'}) : undefined) as never; } /** * Convert an ISO8601 date to a string in the format YYYYMMDD */ export function iso8601ToICalDate(date: SCISO8601Date): string { - return `${moment(date).utc().format('YYYYMMDD')}`; + return formatISO(parseISO(date), {format: 'basic', representation: 'date'}); } /** @@ -266,11 +276,11 @@ export function normalizeICalDates(iCal: ICalEvent): ICalEvent { }; } -const REPEAT_FREQUENCIES: Partial> = { - day: 'DAILY', - week: 'WEEKLY', - month: 'MONTHLY', - year: 'YEARLY', +const REPEAT_FREQUENCIES: Partial> = { + days: 'DAILY', + weeks: 'WEEKLY', + months: 'MONTHLY', + years: 'YEARLY', }; /** @@ -308,7 +318,7 @@ export function serializeICalEvent(iCal: ICalEvent): ICalLike { 'BEGIN:VEVENT', `DTSTART:${normalized.start}`, `DURATION:${normalized.duration}`, - `DTSTAMP:${moment().utc().format('YYYYMMDDTHHmmss')}Z`, + `DTSTAMP:${formatISO(Date.now(), {format: 'basic'})}`, `UID:${normalized.uuid}`, `RECURRENCE-ID:${normalized.recurrenceId}`, `CATEGORIES:${normalized.categories?.join(',')}`, diff --git a/frontend/app/src/app/modules/calendar/schedule.provider.ts b/frontend/app/src/app/modules/calendar/schedule.provider.ts index c94eef2c..958555b0 100644 --- a/frontend/app/src/app/modules/calendar/schedule.provider.ts +++ b/frontend/app/src/app/modules/calendar/schedule.provider.ts @@ -26,9 +26,10 @@ import { import {BehaviorSubject, Observable} from 'rxjs'; import {DataProvider} from '../data/data.provider'; import {map} from 'rxjs/operators'; -import {DateFormatPipe, DurationPipe} from 'ngx-moment'; import {pick} from '@openstapps/collection-utils'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; +import {format, formatDuration, parseISO} from 'date-fns'; +import {parse as parseISODuration} from 'duration-fns'; /** * @@ -48,17 +49,13 @@ export const dateSeriesRelevantKeys: Array = [ ]; export const formatRelevantKeys: { - [key in DateSeriesRelevantKeys]: ( - value: SCDateSeries[key], - dateFormatter: DateFormatPipe, - durationFormatter: DurationPipe, - ) => string; + [key in DateSeriesRelevantKeys]: (value: SCDateSeries[key]) => string; } = { uid: value => value, - dates: (value, dateFormatter) => `[${value.map(it => dateFormatter.transform(it)).join(', ')}]`, - exceptions: (value, dateFormatter) => `[${value?.map(it => dateFormatter.transform(it)).join(', ') ?? ''}]`, - repeatFrequency: (value, _, durationFormatter) => durationFormatter.transform(value), - duration: (value, _, durationFormatter) => durationFormatter.transform(value), + dates: value => `[${value.map(it => format(parseISO(it), 'PPp')).join(', ')}]`, + exceptions: value => `[${value?.map(it => format(parseISO(it), 'PPp')).join(', ') ?? ''}]`, + repeatFrequency: value => (value ? formatDuration(parseISODuration(value)) : ''), + duration: value => formatDuration(parseISODuration(value)), }; export type DateSeriesRelevantData = Pick; diff --git a/frontend/app/src/app/modules/catalog/catalog.component.ts b/frontend/app/src/app/modules/catalog/catalog.component.ts index 5b76dfa3..9a065711 100644 --- a/frontend/app/src/app/modules/catalog/catalog.component.ts +++ b/frontend/app/src/app/modules/catalog/catalog.component.ts @@ -15,11 +15,11 @@ import {Component, OnInit} from '@angular/core'; import {Router, ActivatedRoute} from '@angular/router'; import {SCCatalog, SCSemester} from '@openstapps/core'; -import moment from 'moment'; import {CatalogProvider} from './catalog.provider'; import {NGXLogger} from 'ngx-logger'; import {Location} from '@angular/common'; import {DataRoutingService} from '../data/data-routing.service'; +import {formatISO, startOfToday} from 'date-fns'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; @Component({ @@ -98,7 +98,7 @@ export class CatalogComponent implements OnInit { } async fetchSemesters(): Promise { - const today = moment().startOf('day').toISOString(); + const today = formatISO(startOfToday()); const semesters = await this.catalogProvider.getRelevantSemesters(); const currentSemester = semesters.find( semester => semester.startDate <= today && semester.endDate > today, diff --git a/frontend/app/src/app/modules/catalog/catalog.module.ts b/frontend/app/src/app/modules/catalog/catalog.module.ts index 88644e69..75225000 100644 --- a/frontend/app/src/app/modules/catalog/catalog.module.ts +++ b/frontend/app/src/app/modules/catalog/catalog.module.ts @@ -18,7 +18,6 @@ import {FormsModule} from '@angular/forms'; import {RouterModule, Routes} from '@angular/router'; import {IonicModule} from '@ionic/angular'; import {TranslateModule} from '@ngx-translate/core'; -import {MomentModule} from 'ngx-moment'; import {DataModule} from '../data/data.module'; import {SettingsProvider} from '../settings/settings.provider'; import {CatalogComponent} from './catalog.component'; @@ -42,7 +41,6 @@ const catalogRoutes: Routes = [ RouterModule.forChild(catalogRoutes), IonIconModule, CommonModule, - MomentModule, DataModule, UtilModule, ], diff --git a/frontend/app/src/app/modules/dashboard/dashboard.component.html b/frontend/app/src/app/modules/dashboard/dashboard.component.html index b3e9df5d..df4ef463 100644 --- a/frontend/app/src/app/modules/dashboard/dashboard.component.html +++ b/frontend/app/src/app/modules/dashboard/dashboard.component.html @@ -33,7 +33,7 @@ {{ nextEvent - ? (nextEvent!.dates | nextDateInList | amDateFormat : 'll, HH:mm') + ? (nextEvent!.dates.sort().at(-1) | dfnsParseIso | dfnsFormatRelative : (now | async)) : ('dashboard.schedule.noEvent' | translate) }} diff --git a/frontend/app/src/app/modules/dashboard/dashboard.component.ts b/frontend/app/src/app/modules/dashboard/dashboard.component.ts index 08b01c68..da476b50 100644 --- a/frontend/app/src/app/modules/dashboard/dashboard.component.ts +++ b/frontend/app/src/app/modules/dashboard/dashboard.component.ts @@ -15,7 +15,7 @@ import {Component, DestroyRef, ElementRef, inject, NgZone, OnDestroy, OnInit, ViewChild} from '@angular/core'; import {Router} from '@angular/router'; import {Location} from '@angular/common'; -import moment from 'moment'; +import {timer} from 'rxjs'; import {SCDateSeries, SCUuid} from '@openstapps/core'; import {SplashScreen} from '@capacitor/splash-screen'; import {DataRoutingService} from '../data/data-routing.service'; @@ -24,6 +24,10 @@ import {AnimationController, IonContent} from '@ionic/angular'; import {DashboardCollapse} from './dashboard-collapse'; import {BreakpointObserver} from '@angular/cdk/layout'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; +import {formatISO, minutesToMilliseconds, startOfWeek} from 'date-fns'; +import {map} from 'rxjs/operators'; + +// const scrollTimeline = new ScrollTimeline(); @Component({ selector: 'app-dashboard', @@ -63,6 +67,8 @@ export class DashboardComponent implements OnInit, OnDestroy { destroy$ = inject(DestroyRef); + now = timer(0, minutesToMilliseconds(1)).pipe(map(() => Date.now())); + constructor( private readonly dataRoutingService: DataRoutingService, private scheduleProvider: ScheduleProvider, @@ -108,7 +114,7 @@ export class DashboardComponent implements OnInit, OnDestroy { const dataSeries = await this.scheduleProvider.getDateSeries( this.eventUuids, undefined, - moment(moment.now()).startOf('week').toISOString(), + formatISO(startOfWeek(Date.now())), ); this.nextEvent = dataSeries.dates diff --git a/frontend/app/src/app/modules/dashboard/dashboard.module.ts b/frontend/app/src/app/modules/dashboard/dashboard.module.ts index e2d8d9a8..8dd7981c 100644 --- a/frontend/app/src/app/modules/dashboard/dashboard.module.ts +++ b/frontend/app/src/app/modules/dashboard/dashboard.module.ts @@ -19,7 +19,6 @@ import {RouterModule, Routes} from '@angular/router'; import {IonicModule} from '@ionic/angular'; import {SwiperModule} from 'swiper/angular'; import {TranslateModule, TranslatePipe} from '@ngx-translate/core'; -import {MomentModule} from 'ngx-moment'; import {DataModule} from '../data/data.module'; import {SettingsProvider} from '../settings/settings.provider'; import {DashboardComponent} from './dashboard.component'; @@ -32,6 +31,7 @@ import {ThingTranslateModule} from '../../translation/thing-translate.module'; import {UtilModule} from '../../util/util.module'; import {IonIconModule} from '../../util/ion-icon/ion-icon.module'; import {NewsModule} from '../news/news.module'; +import {FormatRelativePipeModule, ParseIsoPipeModule} from 'ngx-date-fns'; const catalogRoutes: Routes = [ { @@ -59,12 +59,13 @@ const catalogRoutes: Routes = [ TranslateModule.forChild(), RouterModule.forChild(catalogRoutes), CommonModule, - MomentModule, DataModule, SwiperModule, ThingTranslateModule.forChild(), UtilModule, NewsModule, + ParseIsoPipeModule, + FormatRelativePipeModule, ], providers: [SettingsProvider, TranslatePipe], }) diff --git a/frontend/app/src/app/modules/dashboard/sections/mensa-section/mensa-section-content.component.ts b/frontend/app/src/app/modules/dashboard/sections/mensa-section/mensa-section-content.component.ts index 6dfcbcbc..9d43276b 100644 --- a/frontend/app/src/app/modules/dashboard/sections/mensa-section/mensa-section-content.component.ts +++ b/frontend/app/src/app/modules/dashboard/sections/mensa-section/mensa-section-content.component.ts @@ -15,8 +15,8 @@ import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; import {SCDish, SCPlace, SCThings} from '@openstapps/core'; import {PlaceMensaService} from '../../../data/types/place/special/mensa/place-mensa-service'; -import moment from 'moment'; import {fadeAnimation} from '../../fade.animation'; +import {isToday, parseISO} from 'date-fns'; /** * Shows a section with meals of the chosen mensa @@ -38,10 +38,10 @@ export class MensaSectionContentComponent { @Input() set item(value: SCThings) { if (!value) return; this.dishes = this.mensaService.getAllDishes(value as SCPlace, 1).then(it => { - const closestDayWithDishes = Object.keys(it) - .filter(key => it[key].length > 0) - .find(key => moment(key).isSame(moment(), 'day')); - return closestDayWithDishes ? it[closestDayWithDishes] : []; + const days = Object.entries(it); + console.assert(days.length <= 1); + console.assert(!days[0] || isToday(parseISO(days[0][0]))); + return days[0]?.[1] ?? []; }); } diff --git a/frontend/app/src/app/modules/data/chips/edit-event-selection.html b/frontend/app/src/app/modules/data/chips/edit-event-selection.html index 22349804..bac998d2 100644 --- a/frontend/app/src/app/modules/data/chips/edit-event-selection.html +++ b/frontend/app/src/app/modules/data/chips/edit-event-selection.html @@ -32,8 +32,8 @@ > {{ frequency.children[0].item.repeatFrequency ? (frequency.children[0].item.repeatFrequency | - durationLocalized: true | sentencecase) : ('data.chips.add_events.popover.SINGLE' | translate | - titlecase) }} + dfnsParseDuration | dfnsFormatFrequency | sentencecase) : ('data.chips.add_events.popover.SINGLE' | + translate | titlecase) }} @@ -44,18 +44,19 @@ > - {{ date.item.dates[0] | amDateFormat: 'dddd, LT' }} - {{ date.item.dates[0] | amAdd: - date.item.duration | amDateFormat: 'LT' }} + {{ date.item.duration | dfnsParseDuration | dfnsFormatDuration }} + {{ date.item.dates[0] | dfnsParseIso | dfnsFormat: 'EEEE, p' }}
- {{ date.item.dates[0] | amDateFormat: 'LL' }} - {{ date.item.dates[date.item.dates.length - 1] | - amDateFormat: 'LL' }} + {{ date.item.dates[0] | dfnsParseIso | dfnsFormat: 'PPP' }} - {{ + date.item.dates[date.item.dates.length - 1] | dfnsParseIso | dfnsFormat: 'PPP' }}
- {{ time | amDateFormat: 'LL, LT' }} - {{ time | amAdd: date.item.duration | amDateFormat: 'LT' }} + {{ date.item.duration |dfnsParseDuration | dfnsFormatDuration}} + {{ time | dfnsParseIso | dfnsFormat: 'PPPPp' }} {{ 'data.chips.add_events.popover.DATA_ERROR' | translate }} diff --git a/frontend/app/src/app/modules/data/data.module.ts b/frontend/app/src/app/modules/data/data.module.ts index bdfc0b23..cb993fc7 100644 --- a/frontend/app/src/app/modules/data/data.module.ts +++ b/frontend/app/src/app/modules/data/data.module.ts @@ -20,7 +20,6 @@ import {FormsModule} from '@angular/forms'; import {IonicModule, Platform} from '@ionic/angular'; import {TranslateModule} from '@ngx-translate/core'; import {MarkdownModule} from 'ngx-markdown'; -import {MomentModule} from 'ngx-moment'; import {ThingTranslateModule} from '../../translation/thing-translate.module'; import {MenuModule} from '../menu/menu.module'; import {ScheduleProvider} from '../calendar/schedule.provider'; @@ -104,6 +103,16 @@ import {SkeletonListComponent} from './list/skeleton-list.component'; import {CertificationsInDetailComponent} from './elements/certifications-in-detail.component'; import {GeoNavigationDirective} from '../map/geo-navigation.directive'; import {NavigateActionChipComponent} from './chips/data/navigate-action-chip.component'; +import { + FormatDistanceToNowPipeModule, + FormatDurationPipeModule, + FormatPipeModule, + FormatRelativeToNowPipeModule, + ParseIsoPipeModule, +} from 'ngx-date-fns'; +import {ParseDurationPipe} from '../../translation/date-time/parse-duration.pipe'; +import {DfnsFormatFrequencyPipe} from '../../translation/date-time/format-frequency.pipe'; +import {FormatRelativeDatePipe} from '../../translation/date-time/format-relative-date.pipe'; /** * Module for handling data @@ -187,17 +196,20 @@ import {NavigateActionChipComponent} from './chips/data/navigate-action-chip.com MarkdownModule.forRoot(), MenuModule, IonIconModule, - MomentModule.forRoot({ - relativeTimeThresholdOptions: { - m: 59, - }, - }), ScrollingModule, StorageModule, TranslateModule.forChild(), ThingTranslateModule.forChild(), UtilModule, GeoNavigationDirective, + ParseIsoPipeModule, + FormatPipeModule, + ParseDurationPipe, + FormatDurationPipeModule, + FormatRelativeToNowPipeModule, + DfnsFormatFrequencyPipe, + FormatDistanceToNowPipeModule, + FormatRelativeDatePipe, ], providers: [ CoordinatedSearchProvider, diff --git a/frontend/app/src/app/modules/data/detail/data-detail.component.spec.ts b/frontend/app/src/app/modules/data/detail/data-detail.component.spec.ts index c75b6dfc..f0f846a0 100644 --- a/frontend/app/src/app/modules/data/detail/data-detail.component.spec.ts +++ b/frontend/app/src/app/modules/data/detail/data-detail.component.spec.ts @@ -95,7 +95,7 @@ describe('DataDetailComponent', () => { fixture = TestBed.createComponent(DataDetailComponent); comp = fixture.componentInstance; detailPage = fixture.debugElement; - translateService.use('foo'); + translateService.use('en'); fixture.detectChanges(); }); diff --git a/frontend/app/src/app/modules/data/elements/offers-detail.html b/frontend/app/src/app/modules/data/elements/offers-detail.html index 42620693..5a854b19 100644 --- a/frontend/app/src/app/modules/data/elements/offers-detail.html +++ b/frontend/app/src/app/modules/data/elements/offers-detail.html @@ -30,7 +30,7 @@ *ngIf="offer.availabilityRange.gt ? offer.availabilityRange.gt : offer.availabilityRange.gte" > {{ (offer.availabilityRange.gt ? offer.availabilityRange.gt : offer.availabilityRange.gte) | - amDateFormat : 'll' }} + dfnsParseIso | dfnsFormat : 'PPP' }} diff --git a/frontend/app/src/app/modules/data/elements/origin-detail.html b/frontend/app/src/app/modules/data/elements/origin-detail.html index 1e4e67d5..73614a17 100644 --- a/frontend/app/src/app/modules/data/elements/origin-detail.html +++ b/frontend/app/src/app/modules/data/elements/origin-detail.html @@ -20,16 +20,16 @@ >

- {{ 'data.types.origin.detail.CREATED' | translate | titlecase }}: {{ origin.created | amDateFormat : - 'll' }} + {{ 'data.types.origin.detail.CREATED' | translate | titlecase }}: {{ origin.created | dfnsParseIso | + dfnsFormat : 'PPP' }}

- {{ 'data.types.origin.detail.UPDATED' | translate | titlecase }}: {{ origin.updated | amDateFormat : - 'll' }} + {{ 'data.types.origin.detail.UPDATED' | translate | titlecase }}: {{ origin.updated | dfnsParseIso | + dfnsFormat : 'PPP' }}

- {{ 'data.types.origin.detail.MODIFIED' | translate | titlecase }}: {{ origin.modified | amDateFormat : - 'll' }} + {{ 'data.types.origin.detail.MODIFIED' | translate | titlecase }}: {{ origin.modified | dfnsParseIso | + dfnsFormat : 'PPP' }}

{{ 'data.types.origin.detail.MAINTAINER' | translate }}: {{ origin.name }}

@@ -46,12 +46,12 @@ >

- {{ 'data.types.origin.detail.INDEXED' | translate | titlecase }}: {{ origin.indexed | amDateFormat : - 'll' }} + {{ 'data.types.origin.detail.INDEXED' | translate | titlecase }}: {{ origin.indexed | dfnsParseIso | + dfnsFormat : 'PPP'}}

- {{ 'data.types.origin.detail.MODIFIED' | translate | titlecase }}: {{ origin.modified | amDateFormat : - 'll' }} + {{ 'data.types.origin.detail.MODIFIED' | translate | titlecase }}: {{ origin.modified | dfnsParseIso | + dfnsFormat : 'PPP' }}

{{ 'data.types.origin.detail.MAINTAINER' | translate }}: {{ origin.name }}

diff --git a/frontend/app/src/app/modules/data/list/food-data-list.component.ts b/frontend/app/src/app/modules/data/list/food-data-list.component.ts index f8564f2d..5fb5ddd5 100644 --- a/frontend/app/src/app/modules/data/list/food-data-list.component.ts +++ b/frontend/app/src/app/modules/data/list/food-data-list.component.ts @@ -12,113 +12,90 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ -import {Component, OnInit} from '@angular/core'; -import {MapPosition} from '../../map/position.service'; +import {AfterViewInit, Component, ViewChild} from '@angular/core'; +import {MapPosition, PositionService} from '../../map/position.service'; +import {SCSearchFilter, SCSearchSort} from '@openstapps/core'; import {SearchPageComponent} from './search-page.component'; import {Geolocation} from '@capacitor/geolocation'; -import {BehaviorSubject} from 'rxjs'; -import {pauseWhen} from '../../../util/rxjs/pause-when'; -import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; + +/** + * Converts a position into a sort query + */ +function asSortQuery(position: MapPosition): SCSearchSort[] { + return [ + { + type: 'distance', + order: 'asc', + arguments: { + field: 'geo', + position: [position.longitude, position.latitude], + }, + }, + ]; +} /** * Presents a list of places for eating/drinking */ @Component({ - templateUrl: 'search-page.html', - styleUrls: ['../../data/list/search-page.scss'], + selector: 'stapps-food-data-list', + templateUrl: 'food-data-list.html', + styleUrls: ['food-data-list.scss'], }) -export class FoodDataListComponent extends SearchPageComponent implements OnInit { - title = 'canteens.title'; +export class FoodDataListComponent implements AfterViewInit { + @ViewChild(SearchPageComponent) searchPage: SearchPageComponent; - showNavigation = false; - - isNotInView$ = new BehaviorSubject(true); - - /** - * Sets the forced filter to present only places for eating/drinking - */ - ngOnInit() { - this.positionService - .watchCurrentLocation({enableHighAccuracy: false, maximumAge: 1000}) - .pipe(pauseWhen(this.isNotInView$), takeUntilDestroyed(this.destroy$)) - .subscribe({ - next: (position: MapPosition) => { - this.positionService.position = position; - }, - error: async _error => { - this.positionService.position = undefined; - await Geolocation.checkPermissions(); - }, - }); - this.showDefaultData = true; - - this.sortQuery = [ - { - arguments: {field: 'name'}, - order: 'asc', - type: 'ducet', - }, - ]; - - this.forcedFilter = { - arguments: { - filters: [ - { - arguments: { - field: 'categories', - value: 'canteen', - }, - type: 'value', - }, - { - arguments: { - field: 'categories', - value: 'student canteen', - }, - type: 'value', - }, - { - arguments: { - field: 'categories', - value: 'cafe', - }, - type: 'value', - }, - { - arguments: { - field: 'categories', - value: 'restaurant', - }, - type: 'value', - }, - ], - operation: 'or', - }, - type: 'boolean', - }; - - if (this.positionService.position) { - this.sortQuery = [ + forcedFilter: SCSearchFilter = { + arguments: { + filters: [ { - type: 'distance', - order: 'asc', arguments: { - field: 'geo', - position: [this.positionService.position.longitude, this.positionService.position.latitude], + field: 'categories', + value: 'canteen', }, + type: 'value', }, - ]; + { + arguments: { + field: 'categories', + value: 'student canteen', + }, + type: 'value', + }, + { + arguments: { + field: 'categories', + value: 'cafe', + }, + type: 'value', + }, + { + arguments: { + field: 'categories', + value: 'restaurant', + }, + type: 'value', + }, + ], + operation: 'or', + }, + type: 'boolean', + }; + + sortQuery = this.positionService.getCurrentLocation({enableHighAccuracy: false}).then(asSortQuery); + + constructor(private readonly positionService: PositionService) {} + + async ngAfterViewInit() { + const canAccessLocation = await Geolocation.checkPermissions() + .then(it => it.coarseLocation === 'granted' || it.location === 'granted') + .catch(() => false); + this.searchPage.showDefaultData = true; + this.searchPage.loading = true; + if (!canAccessLocation) { + await this.searchPage.fetchAndUpdateItems(); } - - super.ngOnInit(); - } - - async ionViewWillEnter() { - await super.ionViewWillEnter(); - this.isNotInView$.next(false); - } - - ionViewWillLeave() { - this.isNotInView$.next(true); + this.searchPage.sortQuery = await this.sortQuery; + await this.searchPage.fetchAndUpdateItems(); } } diff --git a/frontend/app/src/app/modules/data/list/food-data-list.html b/frontend/app/src/app/modules/data/list/food-data-list.html new file mode 100644 index 00000000..4dd08fb9 --- /dev/null +++ b/frontend/app/src/app/modules/data/list/food-data-list.html @@ -0,0 +1,7 @@ + + diff --git a/frontend/app/src/app/modules/data/list/search-page-switch-animation.ts b/frontend/app/src/app/modules/data/list/search-page-switch-animation.ts index 860103c3..e81580f9 100644 --- a/frontend/app/src/app/modules/data/list/search-page-switch-animation.ts +++ b/frontend/app/src/app/modules/data/list/search-page-switch-animation.ts @@ -16,11 +16,13 @@ import type {AnimationBuilder} from '@ionic/angular'; import {AnimationController} from '@ionic/angular'; import type {AnimationOptions} from '@ionic/angular/providers/nav-controller'; +import {inject} from '@angular/core'; /** * */ -export function searchPageSwitchAnimation(animationController: AnimationController): AnimationBuilder { +export function searchPageSwitchAnimation(): AnimationBuilder { + const animationController = inject(AnimationController); // eslint-disable-next-line @typescript-eslint/no-explicit-any return (_baseElement: HTMLElement, options: AnimationOptions | any) => { const rootTransition = animationController diff --git a/frontend/app/src/app/modules/data/list/search-page.component.ts b/frontend/app/src/app/modules/data/list/search-page.component.ts index adfa8bab..7ffddaf8 100644 --- a/frontend/app/src/app/modules/data/list/search-page.component.ts +++ b/frontend/app/src/app/modules/data/list/search-page.component.ts @@ -15,7 +15,7 @@ import {Component, DestroyRef, inject, Input, OnInit} from '@angular/core'; import {ActivatedRoute, Router} from '@angular/router'; import {Keyboard} from '@capacitor/keyboard'; -import {AlertController, AnimationBuilder, AnimationController} from '@ionic/angular'; +import {AlertController} from '@ionic/angular'; import {Capacitor} from '@capacitor/core'; import { SCFacet, @@ -32,7 +32,6 @@ import {ContextMenuService} from '../../menu/context/context-menu.service'; import {SettingsProvider} from '../../settings/settings.provider'; import {DataRoutingService} from '../data-routing.service'; import {DataProvider} from '../data.provider'; -import {PositionService} from '../../map/position.service'; import {ConfigProvider} from '../../config/config.provider'; import {searchPageSwitchAnimation} from './search-page-switch-animation'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; @@ -144,21 +143,8 @@ export class SearchPageComponent implements OnInit { destroy$ = inject(DestroyRef); - routeAnimation: AnimationBuilder; + routeAnimation = searchPageSwitchAnimation(); - /** - * Injects the providers and creates subscriptions - * @param alertController AlertController - * @param dataProvider DataProvider - * @param contextMenuService ContextMenuService - * @param settingsProvider SettingsProvider - * @param logger An angular logger - * @param dataRoutingService DataRoutingService - * @param router Router - * @param route ActivatedRoute - * @param positionService PositionService - * @param configProvider ConfigProvider - */ constructor( protected readonly alertController: AlertController, protected dataProvider: DataProvider, @@ -168,18 +154,14 @@ export class SearchPageComponent implements OnInit { protected dataRoutingService: DataRoutingService, protected router: Router, private readonly route: ActivatedRoute, - protected positionService: PositionService, private readonly configProvider: ConfigProvider, - animationController: AnimationController, - ) { - this.routeAnimation = searchPageSwitchAnimation(animationController); - } + ) {} /** * Fetches items with set query configuration * @param append If true fetched data gets appended to existing, override otherwise (default false) */ - protected async fetchAndUpdateItems(append = false): Promise { + async fetchAndUpdateItems(append = false): Promise { // build query search options const searchOptions: SCSearchQuery = { from: this.from, diff --git a/frontend/app/src/app/modules/data/types/date-series/date-series-detail-content.html b/frontend/app/src/app/modules/data/types/date-series/date-series-detail-content.html index a9b31a4b..11749bb8 100644 --- a/frontend/app/src/app/modules/data/types/date-series/date-series-detail-content.html +++ b/frontend/app/src/app/modules/data/types/date-series/date-series-detail-content.html @@ -26,22 +26,22 @@ diff --git a/frontend/app/src/app/modules/data/types/date-series/date-series-list-item.html b/frontend/app/src/app/modules/data/types/date-series/date-series-list-item.html index 6824333e..be6ddd9c 100644 --- a/frontend/app/src/app/modules/data/types/date-series/date-series-list-item.html +++ b/frontend/app/src/app/modules/data/types/date-series/date-series-list-item.html @@ -22,11 +22,12 @@ - {{ item.repeatFrequency | durationLocalized : true | sentencecase }}, {{ item.dates[0] | - dateFormat : 'weekday:long' }} + {{ item.repeatFrequency | dfnsParseDuration | dfnsFormatDuration | sentencecase }}, {{ + item.dates[0] | dfnsParseIso | dfnsFormat : 'PPPP' }} - ({{ item.dates[0] | dateFormat }} - {{ item.dates[item.dates.length - 1] | dateFormat }}) + ({{ item.dates[0] | dfnsParseIso | dfnsFormat: 'PPP' }} - {{ item.dates[item.dates.length - 1] | + dfnsParseIso | dfnsFormat: 'PPP' }})

diff --git a/frontend/app/src/app/modules/data/types/message/message-detail-content.html b/frontend/app/src/app/modules/data/types/message/message-detail-content.html index 991be619..334699f3 100644 --- a/frontend/app/src/app/modules/data/types/message/message-detail-content.html +++ b/frontend/app/src/app/modules/data/types/message/message-detail-content.html @@ -25,7 +25,7 @@ diff --git a/frontend/app/src/app/modules/data/types/place/place-list-item.component.ts b/frontend/app/src/app/modules/data/types/place/place-list-item.component.ts index 54f004d3..fdedd7f5 100644 --- a/frontend/app/src/app/modules/data/types/place/place-list-item.component.ts +++ b/frontend/app/src/app/modules/data/types/place/place-list-item.component.ts @@ -12,10 +12,11 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ -import {Component, Input} from '@angular/core'; +import {ChangeDetectionStrategy, Component, DestroyRef, inject, Input} from '@angular/core'; import {PositionService} from '../../../map/position.service'; -import {interval, Subscription} from 'rxjs'; +import {BehaviorSubject, interval} from 'rxjs'; import {hasValidLocation, isSCFloor, PlaceTypes, PlaceTypesWithDistance} from './place-types'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; /** * Shows a place as a list item @@ -24,6 +25,7 @@ import {hasValidLocation, isSCFloor, PlaceTypes, PlaceTypesWithDistance} from '. selector: 'stapps-place-list-item', templateUrl: 'place-list-item.html', styleUrls: ['place-list-item.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class PlaceListItemComponent { /** @@ -39,10 +41,12 @@ export class PlaceListItemComponent { @Input() set item(item: PlaceTypes) { this._item = item; if (!isSCFloor(item) && hasValidLocation(item)) { - this.distance = this.positionService.getDistance(item.geo.point); - this.distanceSubscription = interval(10_000).subscribe(_ => { - this.distance = this.positionService.getDistance(item.geo.point); - }); + this.distance.next(this.positionService.getDistance(item.geo.point)); + interval(10_000) + .pipe(takeUntilDestroyed(this.destroy$)) + .subscribe(_ => { + this.distance.next(this.positionService.getDistance(item.geo.point)); + }); } } @@ -54,9 +58,9 @@ export class PlaceListItemComponent { /** * Distance in meters */ - distance?: number; + distance = new BehaviorSubject(undefined); - distanceSubscription?: Subscription; + private destroy$ = inject(DestroyRef); constructor(private positionService: PositionService) {} } diff --git a/frontend/app/src/app/modules/data/types/place/place-list-item.html b/frontend/app/src/app/modules/data/types/place/place-list-item.html index cf530c13..326a2b4b 100644 --- a/frontend/app/src/app/modules/data/types/place/place-list-item.html +++ b/frontend/app/src/app/modules/data/types/place/place-list-item.html @@ -27,7 +27,7 @@

{{ 'categories' | thingTranslate: item | join: ', ' | titlecase }} - + {{ distance | metersLocalized }} @@ -36,7 +36,7 @@ {{ 'type' | thingTranslate: item | titlecase }} - + {{ distance | metersLocalized }} diff --git a/frontend/app/src/app/modules/data/types/place/special/mensa/place-mensa-detail.component.ts b/frontend/app/src/app/modules/data/types/place/special/mensa/place-mensa-detail.component.ts index 15bd2bd0..4a739f5d 100644 --- a/frontend/app/src/app/modules/data/types/place/special/mensa/place-mensa-detail.component.ts +++ b/frontend/app/src/app/modules/data/types/place/special/mensa/place-mensa-detail.component.ts @@ -12,7 +12,6 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ -import moment, {Moment} from 'moment'; import {AfterViewInit, Component, DestroyRef, inject, Input} from '@angular/core'; import {SCDish, SCISO8601Date, SCPlace} from '@openstapps/core'; import {PlaceMensaService} from './place-mensa-service'; @@ -53,11 +52,6 @@ export class PlaceMensaDetailComponent implements AfterViewInit { */ selectedDay: string; - /** - * First day to display menu items for - */ - startingDay: Moment; - destroy$ = inject(DestroyRef); constructor( @@ -65,9 +59,7 @@ export class PlaceMensaDetailComponent implements AfterViewInit { protected router: Router, readonly routerOutlet: IonRouterOutlet, private readonly dataRoutingService: DataRoutingService, - ) { - this.startingDay = moment().startOf('day'); - } + ) {} ngAfterViewInit() { if (!this.openAsModal) { diff --git a/frontend/app/src/app/modules/data/types/place/special/mensa/place-mensa-service.ts b/frontend/app/src/app/modules/data/types/place/special/mensa/place-mensa-service.ts index e711da47..d30a9196 100644 --- a/frontend/app/src/app/modules/data/types/place/special/mensa/place-mensa-service.ts +++ b/frontend/app/src/app/modules/data/types/place/special/mensa/place-mensa-service.ts @@ -14,10 +14,10 @@ */ import {Injectable} from '@angular/core'; import {SCDish, SCISO8601Date, SCPlace, SCSearchQuery, SCThingType} from '@openstapps/core'; -import moment from 'moment'; import {DataProvider} from '../../../../data.provider'; import {mapValues} from '@openstapps/collection-utils'; import {SettingsProvider} from '../../../../../settings/settings.provider'; +import {addDays, formatISO} from 'date-fns'; /** * TODO @@ -38,7 +38,7 @@ export class PlaceMensaService { const request = mapValues, SCSearchQuery>( Array.from({length: days}) .map((_, i) => i) - .map(i => moment().add(i, 'days').toISOString()) + .map(i => formatISO(addDays(Date.now(), i))) .reduce((accumulator, item) => { accumulator[item] = item; return accumulator; diff --git a/frontend/app/src/app/modules/data/types/place/special/mensa/place-mensa.html b/frontend/app/src/app/modules/data/types/place/special/mensa/place-mensa.html index 04ff95bc..31beccb8 100644 --- a/frontend/app/src/app/modules/data/types/place/special/mensa/place-mensa.html +++ b/frontend/app/src/app/modules/data/types/place/special/mensa/place-mensa.html @@ -19,10 +19,10 @@ {{ day.key | dateFormat : 'weekday:long,month:numeric,day:numeric' }}{{ day.key | dfnsParseIso | dfnsFormatRelativeDate | sentencecase }} {{ day.key | dateFormat : 'weekday:short,month:numeric,day:numeric' }}{{ day.key | dfnsParseIso | dfnsFormatRelativeDate | sentencecase }} diff --git a/frontend/app/src/app/modules/data/types/semester/semester-detail-content.html b/frontend/app/src/app/modules/data/types/semester/semester-detail-content.html index 6f01a42c..d6c28c07 100644 --- a/frontend/app/src/app/modules/data/types/semester/semester-detail-content.html +++ b/frontend/app/src/app/modules/data/types/semester/semester-detail-content.html @@ -21,6 +21,6 @@ ('eventsEndDate' | propertyNameTranslate : item | titlecase) " [content]=" - (item.eventsStartDate | amDateFormat : 'll') + ' - ' + (item.eventsEndDate | amDateFormat : 'll') + (item.eventsStartDate | dfnsParseIso | dfnsFormat : 'PPP') + ' - ' + (item.eventsEndDate | dfnsParseIso | dfnsFormat : 'PPP') " > diff --git a/frontend/app/src/app/modules/data/types/semester/semester-list-item.html b/frontend/app/src/app/modules/data/types/semester/semester-list-item.html index bcae688a..403c5834 100644 --- a/frontend/app/src/app/modules/data/types/semester/semester-list-item.html +++ b/frontend/app/src/app/modules/data/types/semester/semester-list-item.html @@ -20,7 +20,10 @@ {{ 'name' | thingTranslate : item }}

- {{ item.startDate | dateFormat }} - {{ item.endDate | dateFormat }} + {{ item.startDate | dfnsParseIso | dfnsFormat: 'PPP' }} - {{ item.endDate | dfnsParseIso | + dfnsFormat: 'PPP' }}

{{ 'type' | thingTranslate : item }} diff --git a/frontend/app/src/app/modules/data/types/video/video-detail-content.html b/frontend/app/src/app/modules/data/types/video/video-detail-content.html index 3802bd30..4aaec145 100644 --- a/frontend/app/src/app/modules/data/types/video/video-detail-content.html +++ b/frontend/app/src/app/modules/data/types/video/video-detail-content.html @@ -26,7 +26,7 @@ diff --git a/frontend/app/src/app/modules/data/types/video/video-list-item.html b/frontend/app/src/app/modules/data/types/video/video-list-item.html index f2053e86..4328582e 100644 --- a/frontend/app/src/app/modules/data/types/video/video-list-item.html +++ b/frontend/app/src/app/modules/data/types/video/video-list-item.html @@ -10,8 +10,8 @@ >

- {{ 'duration' | propertyNameTranslate : item | titlecase }}: {{ item.duration | amDuration : - 'seconds' }} + {{ 'duration' | propertyNameTranslate : item | titlecase }}: {{ item.duration | dfnsParseDuration | + dfnsFormatDuration : {format: ['seconds']} }}

{{ 'type' | thingTranslate : item }} diff --git a/frontend/app/src/app/modules/favorites/favorites-page.component.ts b/frontend/app/src/app/modules/favorites/favorites-page.component.ts index 167f863a..76546f19 100644 --- a/frontend/app/src/app/modules/favorites/favorites-page.component.ts +++ b/frontend/app/src/app/modules/favorites/favorites-page.component.ts @@ -13,7 +13,7 @@ * this program. If not, see . */ import {Component, OnInit} from '@angular/core'; -import {AlertController, AnimationController} from '@ionic/angular'; +import {AlertController} from '@ionic/angular'; import {ActivatedRoute, Router} from '@angular/router'; import {NGXLogger} from 'ngx-logger'; import {debounceTime, distinctUntilChanged, startWith, take} from 'rxjs/operators'; @@ -25,7 +25,6 @@ import {ContextMenuService} from '../menu/context/context-menu.service'; import {SearchPageComponent} from '../data/list/search-page.component'; import {DataProvider} from '../data/data.provider'; import {SettingsProvider} from '../settings/settings.provider'; -import {PositionService} from '../map/position.service'; import {ConfigProvider} from '../config/config.provider'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; @@ -51,10 +50,8 @@ export class FavoritesPageComponent extends SearchPageComponent implements OnIni dataRoutingService: DataRoutingService, router: Router, route: ActivatedRoute, - positionService: PositionService, private favoritesService: FavoritesService, configProvider: ConfigProvider, - animationController: AnimationController, ) { super( alertController, @@ -65,9 +62,7 @@ export class FavoritesPageComponent extends SearchPageComponent implements OnIni dataRoutingService, router, route, - positionService, configProvider, - animationController, ); } diff --git a/frontend/app/src/app/modules/hebis/daia-availability/daia-availability.component.spec.ts b/frontend/app/src/app/modules/hebis/daia-availability/daia-availability.component.spec.ts index a1fa70a4..1c68100e 100644 --- a/frontend/app/src/app/modules/hebis/daia-availability/daia-availability.component.spec.ts +++ b/frontend/app/src/app/modules/hebis/daia-availability/daia-availability.component.spec.ts @@ -100,7 +100,7 @@ describe('DaiaAvailabilityComponent', () => { spyOn(DaiaAvailabilityComponent.prototype, 'getAvailability').and.callThrough(); fixture = await TestBed.createComponent(DaiaAvailabilityComponent); comp = fixture.componentInstance; - translateService.use('foo'); + translateService.use('en'); fixture.detectChanges(); }); diff --git a/frontend/app/src/app/modules/hebis/daia-availability/daia-holding.html b/frontend/app/src/app/modules/hebis/daia-availability/daia-holding.html index 6d21aabe..c45c137e 100644 --- a/frontend/app/src/app/modules/hebis/daia-availability/daia-holding.html +++ b/frontend/app/src/app/modules/hebis/daia-availability/daia-holding.html @@ -67,6 +67,6 @@ {{ 'hebisSearch.daia.dueDate' | translate }} - {{ holding.dueDate | amDateFormat : 'll' }} + {{ holding.dueDate | dfnsParseIso | dfnsFormat : 'PPP' }} diff --git a/frontend/app/src/app/modules/hebis/hebis-detail/hebis-detail.component.spec.ts b/frontend/app/src/app/modules/hebis/hebis-detail/hebis-detail.component.spec.ts index 05845805..ee5a765d 100644 --- a/frontend/app/src/app/modules/hebis/hebis-detail/hebis-detail.component.spec.ts +++ b/frontend/app/src/app/modules/hebis/hebis-detail/hebis-detail.component.spec.ts @@ -96,7 +96,7 @@ describe('HebisDetailComponent', () => { spyOn(HebisDetailComponent.prototype, 'getItem').and.callThrough(); fixture = TestBed.createComponent(HebisDetailComponent); comp = fixture.componentInstance; - translateService.use('foo'); + translateService.use('en'); fixture.detectChanges(); }); diff --git a/frontend/app/src/app/modules/hebis/hebis.module.ts b/frontend/app/src/app/modules/hebis/hebis.module.ts index 863bdd9b..2344c47a 100644 --- a/frontend/app/src/app/modules/hebis/hebis.module.ts +++ b/frontend/app/src/app/modules/hebis/hebis.module.ts @@ -20,7 +20,6 @@ import {FormsModule} from '@angular/forms'; import {IonicModule} from '@ionic/angular'; import {TranslateModule} from '@ngx-translate/core'; import {MarkdownModule} from 'ngx-markdown'; -import {MomentModule} from 'ngx-moment'; import {ThingTranslateModule} from '../../translation/thing-translate.module'; import {MenuModule} from '../menu/menu.module'; import {StorageModule} from '../storage/storage.module'; @@ -36,6 +35,7 @@ import {DaiaAvailabilityComponent} from './daia-availability/daia-availability.c import {UtilModule} from '../../util/util.module'; import {IonIconModule} from '../../util/ion-icon/ion-icon.module'; import {DaiaHoldingComponent} from './daia-availability/daia-holding.component'; +import {FormatPipeModule, ParseIsoPipeModule} from 'ngx-date-fns'; /** * Module for handling data @@ -58,16 +58,13 @@ import {DaiaHoldingComponent} from './daia-availability/daia-holding.component'; IonicModule.forRoot(), MarkdownModule.forRoot(), MenuModule, - MomentModule.forRoot({ - relativeTimeThresholdOptions: { - m: 59, - }, - }), ScrollingModule, StorageModule, TranslateModule.forChild(), ThingTranslateModule.forChild(), UtilModule, + ParseIsoPipeModule, + FormatPipeModule, ], providers: [HebisDataProvider, DaiaDataProvider, StAppsWebHttpClient], }) diff --git a/frontend/app/src/app/modules/hebis/list/hebis-search-page.component.ts b/frontend/app/src/app/modules/hebis/list/hebis-search-page.component.ts index fcdfd233..6acd8add 100644 --- a/frontend/app/src/app/modules/hebis/list/hebis-search-page.component.ts +++ b/frontend/app/src/app/modules/hebis/list/hebis-search-page.component.ts @@ -44,7 +44,7 @@ export class HebisSearchPageComponent extends SearchPageComponent implements OnI * Fetches items with set query configuration * @param append If true fetched data gets appended to existing, override otherwise (default false) */ - protected async fetchAndUpdateItems(append = false): Promise { + async fetchAndUpdateItems(append = false): Promise { // build query search options const searchOptions: {page: number; query: string} = { page: this.page, diff --git a/frontend/app/src/app/modules/library/account/elements/fee-item/fee-item.html b/frontend/app/src/app/modules/library/account/elements/fee-item/fee-item.html index 24f19c50..f317dd4c 100644 --- a/frontend/app/src/app/modules/library/account/elements/fee-item/fee-item.html +++ b/frontend/app/src/app/modules/library/account/elements/fee-item/fee-item.html @@ -28,7 +28,7 @@

{{ 'library.account.pages.fines.labels' + '.' + property | translate }}: {{ fee[property] }} - {{ fee[property] | amDateFormat : 'll' }} + {{ $any(fee[property]) | dfnsParseIso | dfnsFormat : 'PPP' }}

diff --git a/frontend/app/src/app/modules/library/account/elements/paia-item/paiaitem.html b/frontend/app/src/app/modules/library/account/elements/paia-item/paiaitem.html index c359ec6e..8bc7e5af 100644 --- a/frontend/app/src/app/modules/library/account/elements/paia-item/paiaitem.html +++ b/frontend/app/src/app/modules/library/account/elements/paia-item/paiaitem.html @@ -23,7 +23,7 @@ {{ item[property] }} - {{ item[property] | amDateFormat : 'll' }} + {{ $any(item[property]) | dfnsParseIso | dfnsFormat : 'PPP' }}

diff --git a/frontend/app/src/app/modules/library/account/profile/profile-page.html b/frontend/app/src/app/modules/library/account/profile/profile-page.html index 890f7623..dc847546 100644 --- a/frontend/app/src/app/modules/library/account/profile/profile-page.html +++ b/frontend/app/src/app/modules/library/account/profile/profile-page.html @@ -37,8 +37,8 @@ {{ 'library.account.pages.profile.values.unlimited' | translate }} - {{ 'library.account.pages.profile.values.expires' | translate }}: {{ patron[property] | - amDateFormat : 'll' }} + {{ 'library.account.pages.profile.values.expires' | translate }}: {{ + $any(patron[property]) | dfnsParseIso | dfnsFormat : 'PPP' }}
diff --git a/frontend/app/src/app/modules/library/library.module.ts b/frontend/app/src/app/modules/library/library.module.ts index 8cef1d92..e0c7fff8 100644 --- a/frontend/app/src/app/modules/library/library.module.ts +++ b/frontend/app/src/app/modules/library/library.module.ts @@ -12,7 +12,6 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ - import {NgModule} from '@angular/core'; import {CommonModule} from '@angular/common'; import {FormsModule} from '@angular/forms'; @@ -28,11 +27,11 @@ import {PAIAItemComponent} from './account/elements/paia-item/paiaitem.component import {FirstLastNamePipe} from './account/first-last-name.pipe'; import {AuthGuardService} from '../auth/auth-guard.service'; import {ProtectedRoutes} from '../auth/protected.routes'; -import {MomentModule} from 'ngx-moment'; import {FeeItemComponent} from './account/elements/fee-item/fee-item.component'; import {DataModule} from '../data/data.module'; import {UtilModule} from '../../util/util.module'; import {IonIconModule} from '../../util/ion-icon/ion-icon.module'; +import {FormatPipeModule, ParseIsoPipeModule} from 'ngx-date-fns'; const routes: ProtectedRoutes | Routes = [ { @@ -75,9 +74,10 @@ const routes: ProtectedRoutes | Routes = [ IonIconModule, RouterModule.forChild(routes), TranslateModule, - MomentModule, DataModule, UtilModule, + ParseIsoPipeModule, + FormatPipeModule, ], declarations: [ LibraryAccountPageComponent, diff --git a/frontend/app/src/app/modules/map/position.service.ts b/frontend/app/src/app/modules/map/position.service.ts index f29c96cd..53d36b1a 100644 --- a/frontend/app/src/app/modules/map/position.service.ts +++ b/frontend/app/src/app/modules/map/position.service.ts @@ -105,11 +105,9 @@ export class PositionService { subscriber.next(this.position); } }); - watcherID.then(console.log); return { unsubscribe() { watcherID.then(id => { - console.log(id); void Geolocation.clearWatch({id}); }); }, diff --git a/frontend/app/src/app/modules/news/item/news-item.html b/frontend/app/src/app/modules/news/item/news-item.html index 9f333998..16e0cc26 100644 --- a/frontend/app/src/app/modules/news/item/news-item.html +++ b/frontend/app/src/app/modules/news/item/news-item.html @@ -21,7 +21,7 @@ > {{ item.datePublished | amCalendar | sentencecase }}{{ item.datePublished | dfnsParseIso | dfnsFormatRelativeToNow | sentencecase }} {{ item.name }} diff --git a/frontend/app/src/app/modules/news/news.module.ts b/frontend/app/src/app/modules/news/news.module.ts index 96419e4a..ef603827 100644 --- a/frontend/app/src/app/modules/news/news.module.ts +++ b/frontend/app/src/app/modules/news/news.module.ts @@ -17,7 +17,6 @@ import {NgModule} from '@angular/core'; import {RouterModule, Routes} from '@angular/router'; import {IonicModule} from '@ionic/angular'; import {TranslateModule} from '@ngx-translate/core'; -import {MomentModule} from 'ngx-moment'; import {ThingTranslateModule} from '../../translation/thing-translate.module'; import {DataModule} from '../data/data.module'; import {SettingsProvider} from '../settings/settings.provider'; @@ -29,6 +28,7 @@ import {SettingsModule} from '../settings/settings.module'; import {NewsSettingsFilterComponent} from './elements/news-filter-settings/news-settings-filter.component'; import {UtilModule} from '../../util/util.module'; import {IonIconModule} from '../../util/ion-icon/ion-icon.module'; +import {FormatRelativeToNowPipeModule, ParseIsoPipeModule} from 'ngx-date-fns'; const newsRoutes: Routes = [{path: 'news', component: NewsPageComponent}]; @@ -50,11 +50,12 @@ const newsRoutes: Routes = [{path: 'news', component: NewsPageComponent}]; RouterModule.forChild(newsRoutes), IonIconModule, CommonModule, - MomentModule, DataModule, ThingTranslateModule, SettingsModule, UtilModule, + ParseIsoPipeModule, + FormatRelativeToNowPipeModule, ], providers: [SettingsProvider], exports: [NewsItemComponent], diff --git a/frontend/app/src/app/modules/profile/page/my-courses.component.ts b/frontend/app/src/app/modules/profile/page/my-courses.component.ts index 4aec8d72..493a8fd2 100644 --- a/frontend/app/src/app/modules/profile/page/my-courses.component.ts +++ b/frontend/app/src/app/modules/profile/page/my-courses.component.ts @@ -2,8 +2,9 @@ import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; import {mergeMap, ReplaySubject} from 'rxjs'; import {map} from 'rxjs/operators'; import {SCDateSeries, SCISO8601Date} from '@openstapps/core'; -import moment from 'moment/moment'; import {ScheduleProvider} from '../../calendar/schedule.provider'; +import {add, addDays, formatISO, isSameDay, startOfToday} from 'date-fns'; +import {parseISODuration} from 'duration-fns/dist/lib/parseISODuration'; interface MyCoursesTodayInterface { startTime: string; @@ -20,16 +21,16 @@ type MyCoursesGroup = [SCISO8601Date, MyCoursesTodayInterface[]][]; */ function groupDays(dateSeries: SCDateSeries[], visibleDays: number): MyCoursesGroup { const courses: [SCISO8601Date, MyCoursesTodayInterface[]][] = []; - const dates = Array.from({length: visibleDays}, (_, i) => moment().startOf('day').add(i, 'days')); + const dates = Array.from({length: visibleDays}, (_, i) => addDays(startOfToday(), i)); for (const day of dates) { const dayCourses: MyCoursesTodayInterface[] = []; for (const course of dateSeries) { for (const date of course.dates) { - if (moment(date).isSame(day, 'day')) { + if (isSameDay(new Date(date), day)) { dayCourses.push({ - startTime: moment(date).toISOString(), - endTime: moment(date).add(course.duration).toISOString(), + startTime: formatISO(new Date(date)), + endTime: formatISO(add(new Date(date), parseISODuration(course.duration))), course, }); } diff --git a/frontend/app/src/app/modules/profile/page/my-courses.html b/frontend/app/src/app/modules/profile/page/my-courses.html index 1766b631..0daa793a 100644 --- a/frontend/app/src/app/modules/profile/page/my-courses.html +++ b/frontend/app/src/app/modules/profile/page/my-courses.html @@ -7,9 +7,9 @@ {{ myCoursesDay[0] | amDateFormat: 'dddd, ll' }} - {{ ('profile.courses.' + (myCoursesDay[1].length - === 0 ? 'NO' : myCoursesDay[1].length === 1 ? 'ONE' : 'MANY' ) + '_EVENT') | translate: {count: - myCoursesDay[1].length} }}{{ myCoursesDay[0] | dfnsParseIso | dfnsFormatRelativeDate | titlecase }} - {{ ('profile.courses.' + + (myCoursesDay[1].length === 0 ? 'NO' : myCoursesDay[1].length === 1 ? 'ONE' : 'MANY' ) + '_EVENT') | + translate: {count: myCoursesDay[1].length} }} @@ -20,8 +20,8 @@ {{myCourse.startTime | amDateFormat: 'LT'}} - {{myCourse.endTime | amDateFormat: - 'LT'}}{{myCourse.startTime | dfnsParseIso | dfnsFormat: 'p'}} - {{myCourse.endTime | dfnsParseIso | + dfnsFormat: 'p'}} i); } /** diff --git a/frontend/app/src/app/modules/schedule/page/calendar-view.html b/frontend/app/src/app/modules/schedule/page/calendar-view.html index 8a97f9f4..fe950e70 100644 --- a/frontend/app/src/app/modules/schedule/page/calendar-view.html +++ b/frontend/app/src/app/modules/schedule/page/calendar-view.html @@ -19,32 +19,23 @@ - - -
- - {{ dateRange.startDate }} - {{ dateRange.endDate }} - +
+ + {{ dateRange.startDate | dfnsFormat : 'P' }} - {{ dateRange.endDate | dfnsFormat : 'P' }} + - - - - - - -
- - + + + + + + +
@@ -64,13 +55,13 @@ > diff --git a/frontend/app/src/app/modules/schedule/page/components/calendar.component.ts b/frontend/app/src/app/modules/schedule/page/components/calendar.component.ts index ae6ce37e..5c4177f4 100644 --- a/frontend/app/src/app/modules/schedule/page/components/calendar.component.ts +++ b/frontend/app/src/app/modules/schedule/page/components/calendar.component.ts @@ -15,16 +15,15 @@ import {Component, DestroyRef, inject, Input, OnInit} from '@angular/core'; import {ActivatedRoute} from '@angular/router'; import {SCISO8601Date, SCUuid} from '@openstapps/core'; -import moment, {Moment} from 'moment'; import {materialFade, materialManualFade, materialSharedAxisX} from '../../../../animation/material-motion'; import {ScheduleProvider} from '../../../calendar/schedule.provider'; import {ScheduleEvent, ScheduleResponsiveBreakpoint} from '../schema/schema'; -import {SwiperComponent} from 'swiper/angular'; import {InfiniteSwiperComponent} from '../grid/infinite-swiper.component'; import {IonContent, IonDatetime} from '@ionic/angular'; import {CalendarService} from '../../../calendar/calendar.service'; import {getScheduleCursorOffset} from '../grid/schedule-cursor-offset'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; +import {addDays, differenceInDays, formatISO, getHours, isValid, parseISO, startOfDay} from 'date-fns'; /** * Component that displays the schedule @@ -39,17 +38,14 @@ export class CalendarComponent implements OnInit { /** * The day that the schedule started out on */ - @Input() baselineDate: Moment; + @Input() baselineDate: Date; /** * Range of date of the slides shown on screen. */ - dateRange: { - startDate: string; - endDate: string; - } = { - startDate: '', - endDate: '', + dateRange = { + startDate: new Date(), + endDate: new Date(), }; prevHeaderIndex = 0; @@ -98,7 +94,7 @@ export class CalendarComponent implements OnInit { @Input() useInfiniteSwiper = true; - @Input() weekDates: Array; + @Input() weekDates: Array; destroy$ = inject(DestroyRef); @@ -109,15 +105,9 @@ export class CalendarComponent implements OnInit { ) {} ngOnInit() { - let dayString: string | number | null = this.activatedRoute.snapshot.paramMap.get('date'); - if (dayString == undefined || dayString === 'now') { - const fragments = window.location.href.split('/'); - const urlFragment: string = fragments.at(-1) ?? ''; - - dayString = /^\d{4}-\d{2}-\d{2}$/.test(urlFragment) ? urlFragment : moment.now(); - } - - this.baselineDate = moment(dayString).startOf('day'); + this.baselineDate = parseISO(this.activatedRoute.snapshot.paramMap.get('date') ?? ''); + if (!isValid(this.baselineDate)) this.baselineDate = new Date(); + this.baselineDate = startOfDay(this.baselineDate); this.initialSlideIndex = new Promise(resolve => { this.scheduleProvider.uuids$.pipe(takeUntilDestroyed(this.destroy$)).subscribe(async result => { @@ -126,8 +116,8 @@ export class CalendarComponent implements OnInit { }); }); - this.dateRange.startDate = this.calculateDateFromIndex(0, 0, 'DD.MM.YY'); - this.dateRange.endDate = this.calculateDateFromIndex(0, this.layout.days - 1, 'DD.MM.YY'); + this.dateRange.startDate = addDays(this.baselineDate, 0); + this.dateRange.endDate = addDays(this.baselineDate, this.layout.days - 1); } async scrollCursorIntoView(content: IonContent) { @@ -137,30 +127,22 @@ export class CalendarComponent implements OnInit { }); } - /** - * Get date from baseline date and index of current slide. - * @param index number - * @param delta number - is added to index - * @param dateFormat string - */ - calculateDateFromIndex(index: number, delta = 0, dateFormat = 'YYYY-MM-DD') { - return moment(this.baselineDate) - .add(index + delta, 'days') - .format(dateFormat); - } - /** * Change page */ onPageChange(index: number) { this.setDateRange(index); - window.history.replaceState({}, '', `${this.routeFragment}/${this.calculateDateFromIndex(index)}`); + window.history.replaceState( + {}, + '', + `${this.routeFragment}/${formatISO(addDays(this.baselineDate, index), {representation: 'date'})}`, + ); } setDateRange(index: number) { - this.dateRange.startDate = this.calculateDateFromIndex(index, 0, 'DD.MM.YY'); - this.dateRange.endDate = this.calculateDateFromIndex(index, this.layout.days - 1, 'DD.MM.YY'); + this.dateRange.startDate = addDays(this.baselineDate, index); + this.dateRange.endDate = addDays(this.baselineDate, index + (this.layout.days - 1)); } onHeaderSwipe(index: number, infiniteController: InfiniteSwiperComponent) { @@ -173,18 +155,8 @@ export class CalendarComponent implements OnInit { this.prevHeaderIndex = index; } - syncSwiper(self: SwiperComponent, other: SwiperComponent) { - other.swiperRef.slideTo(self.swiperRef.activeIndex); - } - - presentDatePopover( - mainSwiper: InfiniteSwiperComponent, - headerSwiper: InfiniteSwiperComponent, - index: number, - popoverDateTime: IonDatetime, - ) { - const nextIndex = - moment(popoverDateTime.value).diff(this.baselineDate, 'days') - headerSwiper.virtualIndex - index; + presentDatePopover(mainSwiper: InfiniteSwiperComponent, popoverDateTime: IonDatetime) { + const nextIndex = differenceInDays(parseISO(popoverDateTime.value as string), this.baselineDate); mainSwiper.goToIndex(nextIndex).then(() => { this.setDateRange(nextIndex); @@ -203,13 +175,13 @@ export class CalendarComponent implements OnInit { for (const series of dateSeries.dates) { for (const date of series.dates) { - const index = moment(date).startOf('day').diff(this.baselineDate, 'days'); + const index = differenceInDays(startOfDay(parseISO(date)), this.baselineDate); // fall back to default (this.testSchedule[index] ?? (this.testSchedule[index] = {}))[series.uid] = { dateSeries: series, time: { - start: moment(date).hours(), + start: getHours(parseISO(date)), duration: series.duration, }, }; diff --git a/frontend/app/src/app/modules/schedule/page/grid/infinite-swiper.component.ts b/frontend/app/src/app/modules/schedule/page/grid/infinite-swiper.component.ts index c9f439cf..1bd14544 100644 --- a/frontend/app/src/app/modules/schedule/page/grid/infinite-swiper.component.ts +++ b/frontend/app/src/app/modules/schedule/page/grid/infinite-swiper.component.ts @@ -22,7 +22,6 @@ import { Input, OnChanges, OnDestroy, - OnInit, Output, QueryList, SimpleChanges, @@ -59,7 +58,7 @@ async function wait(ms?: number) { styleUrls: ['infinite-swiper.scss'], animations: [materialManualFade], }) -export class InfiniteSwiperComponent implements OnInit, AfterViewInit, OnDestroy, OnChanges { +export class InfiniteSwiperComponent implements AfterViewInit, OnDestroy, OnChanges { @Input() controller?: InfiniteSwiperComponent; @Input() slidesPerView = 5; @@ -86,11 +85,10 @@ export class InfiniteSwiperComponent implements OnInit, AfterViewInit, OnDestroy private preventControllerCallback = false; - ngOnInit() { - this.createSwiper(); - } - - ngAfterViewInit() { + async ngAfterViewInit() { + const swiper = this.createSwiper(); + await wait(); + this.swiper = swiper; this.initSwiper(); } @@ -102,7 +100,7 @@ export class InfiniteSwiperComponent implements OnInit, AfterViewInit, OnDestroy async ngOnChanges(changes: SimpleChanges) { if ('slidesPerView' in changes) { const change = changes.slidesPerView; - if (change.isFirstChange()) return; + if (change.isFirstChange() || !this.swiper) return; // little bit of a cheesy trick just to reinitialize // everything... But you know, it works just fine. @@ -120,30 +118,18 @@ export class InfiniteSwiperComponent implements OnInit, AfterViewInit, OnDestroy } } - createSwiper() { + createSwiper(): Swiper { this.resetSlides(); - // I have absolutely no clue why two results are returned here. - // Probably a bug, so be on the lookout if you get odd errors - const [swiper] = new Swiper('.swiper', { - // TODO: evaluate if the controller has decent performance, some time in the future - // modules: [Controller], + return new Swiper(this.swiperElement.nativeElement, { slidesPerView: this.slidesPerView, initialSlide: this.slidesPerView, init: false, - }) as unknown as [Swiper, Swiper]; - this.swiper = swiper; + }); } initSwiper() { this.swiper.init(this.swiperElement.nativeElement); - // SwiperJS controller still has some performance issues unfortunately... - // So unfortunately we are kind of forced to use a workaround :/ - // TODO: evaluate if the controller has decent performance, some time in the future - /*setTimeout(() => { - this.swiper.controller.control = this.controller?.swiper; - });*/ - this.shiftSlides(); this.swiper.on('activeIndexChange', () => { diff --git a/frontend/app/src/app/modules/schedule/page/grid/schedule-card.component.ts b/frontend/app/src/app/modules/schedule/page/grid/schedule-card.component.ts index 41ae4ef6..dd535229 100644 --- a/frontend/app/src/app/modules/schedule/page/grid/schedule-card.component.ts +++ b/frontend/app/src/app/modules/schedule/page/grid/schedule-card.component.ts @@ -13,9 +13,9 @@ * this program. If not, see . */ import {Component, Input, OnInit} from '@angular/core'; -import moment from 'moment'; import {ScheduleProvider} from '../../../calendar/schedule.provider'; import {ScheduleEvent} from '../schema/schema'; +import {parse as parseISODuration, toHours} from 'duration-fns'; /** * Component that can display a schedule event @@ -86,7 +86,7 @@ export class ScheduleCardComponent implements OnInit { */ ngOnInit() { this.fromY = this.noOffset ? 0 : this.scheduleEvent.time.start; - this.height = moment.duration(this.scheduleEvent.time.duration).asHours(); + this.height = toHours(parseISODuration(this.scheduleEvent.time.duration)); this.title = this.scheduleEvent.dateSeries.event.name; diff --git a/frontend/app/src/app/modules/schedule/page/grid/schedule-card.html b/frontend/app/src/app/modules/schedule/page/grid/schedule-card.html index bf219c2e..8bc43d7f 100644 --- a/frontend/app/src/app/modules/schedule/page/grid/schedule-card.html +++ b/frontend/app/src/app/modules/schedule/page/grid/schedule-card.html @@ -25,8 +25,7 @@ > - {{ this.scheduleEvent?.dateSeries?.event?.name | nullishCoalesce : this.scheduleEvent?.dateSeries?.name - }} + {{ this.scheduleEvent?.dateSeries?.event?.name ?? this.scheduleEvent?.dateSeries?.name }} diff --git a/frontend/app/src/app/modules/schedule/page/grid/schedule-cursor.component.ts b/frontend/app/src/app/modules/schedule/page/grid/schedule-cursor.component.ts index dab8764e..afadf46c 100644 --- a/frontend/app/src/app/modules/schedule/page/grid/schedule-cursor.component.ts +++ b/frontend/app/src/app/modules/schedule/page/grid/schedule-cursor.component.ts @@ -14,8 +14,8 @@ */ import {Component, Input, OnInit} from '@angular/core'; import {HoursRange} from '../schema/schema'; -import moment from 'moment'; import {getScheduleCursorOffset} from './schedule-cursor-offset'; +import {getHours, getMinutes} from 'date-fns'; /** * Component that displays the schedule @@ -54,13 +54,8 @@ export class ScheduleCursorComponent implements OnInit { * Get a floating point time 0..24 */ static getCursorTime(): number { - const mnt = moment(moment.now()); - - const hh = mnt.hours(); - const mm = mnt.minutes(); - - // tslint:disable-next-line:no-magic-numbers - return hh + mm / 60; + const now = new Date(); + return getHours(now) + getMinutes(now) / 60; } /** diff --git a/frontend/app/src/app/modules/schedule/page/grid/schedule-day.component.ts b/frontend/app/src/app/modules/schedule/page/grid/schedule-day.component.ts index f6e2d867..11cb13b0 100644 --- a/frontend/app/src/app/modules/schedule/page/grid/schedule-day.component.ts +++ b/frontend/app/src/app/modules/schedule/page/grid/schedule-day.component.ts @@ -13,12 +13,12 @@ * this program. If not, see . */ import {Component, HostListener, Input, OnInit} from '@angular/core'; -import moment from 'moment'; import {Range, ScheduleEvent, ScheduleResponsiveBreakpoint} from '../schema/schema'; import {ScheduleProvider} from '../../../calendar/schedule.provider'; import {SCISO8601Duration, SCUuid} from '@openstapps/core'; import {materialFade} from '../../../../animation/material-motion'; import {groupRangeOverlaps} from './range-overlap'; +import {parse as parseISODuration, toHours} from 'duration-fns'; @Component({ selector: 'schedule-day', @@ -27,7 +27,7 @@ import {groupRangeOverlaps} from './range-overlap'; animations: [materialFade], }) export class ScheduleDayComponent implements OnInit { - @Input() day: moment.Moment; + @Input() day: Date; @Input() hoursRange: Range; @@ -52,7 +52,7 @@ export class ScheduleDayComponent implements OnInit { this.dateSeriesGroups = groupRangeOverlaps( Object.values(value), it => it.time.start, - it => it.time.start + moment.duration(it.time.duration).asHours(), + it => it.time.start + toHours(parseISODuration(it.time.duration)), ).map(it => it.elements); } @@ -71,7 +71,7 @@ export class ScheduleDayComponent implements OnInit { private determineDateFormat() { this.dateFormat = - this.layout && window.innerWidth > 1024 && window.innerWidth <= this.layout?.until ? 'dddd' : 'dd'; + this.layout && window.innerWidth > 1024 && window.innerWidth <= this.layout?.until ? 'EEEE' : 'EE'; } // TODO: backend bug results in the wrong date series being returned diff --git a/frontend/app/src/app/modules/schedule/page/grid/schedule-day.html b/frontend/app/src/app/modules/schedule/page/grid/schedule-day.html index 4ffe18e6..c58d0166 100644 --- a/frontend/app/src/app/modules/schedule/page/grid/schedule-day.html +++ b/frontend/app/src/app/modules/schedule/page/grid/schedule-day.html @@ -13,7 +13,7 @@ ~ this program. If not, see . -->
-
{{ day | amDateFormat : dateFormat }}
+
{{ day | dfnsFormat : dateFormat }}
- +
diff --git a/frontend/app/src/app/modules/schedule/page/schedule-page.component.ts b/frontend/app/src/app/modules/schedule/page/schedule-page.component.ts index 60121e6c..eab062e5 100644 --- a/frontend/app/src/app/modules/schedule/page/schedule-page.component.ts +++ b/frontend/app/src/app/modules/schedule/page/schedule-page.component.ts @@ -20,8 +20,8 @@ import {SharedAxisChoreographer} from '../../../animation/animation-choreographe import {materialSharedAxisX} from '../../../animation/material-motion'; import {ScheduleResponsiveBreakpoint} from './schema/schema'; import {CalendarService} from '../../calendar/calendar.service'; -import moment from 'moment'; import {fabExpand} from '../../../animation/fab-expand'; +import {startOfToday} from 'date-fns'; /** * This needs to be sorted by break point low -> high @@ -168,6 +168,6 @@ export class SchedulePageComponent implements OnInit, AfterViewInit { } onTodayClick() { - this.calendarService.emitGoToDate(moment().startOf('day')); + this.calendarService.emitGoToDate(startOfToday()); } } diff --git a/frontend/app/src/app/modules/schedule/page/schedule-single-events.component.ts b/frontend/app/src/app/modules/schedule/page/schedule-single-events.component.ts index f4f8a6f4..c28fbdeb 100644 --- a/frontend/app/src/app/modules/schedule/page/schedule-single-events.component.ts +++ b/frontend/app/src/app/modules/schedule/page/schedule-single-events.component.ts @@ -14,12 +14,12 @@ */ import {Component, DestroyRef, inject, Input, OnInit} from '@angular/core'; import {SCDateSeries, SCUuid} from '@openstapps/core'; -import moment from 'moment'; import {materialFade} from '../../../animation/material-motion'; import {ScheduleProvider} from '../../calendar/schedule.provider'; import {ScheduleEvent} from './schema/schema'; -import {groupBy, omit, stringSortBy} from '@openstapps/collection-utils'; +import {groupBy, stringSortBy} from '@openstapps/collection-utils'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; +import {compareAsc, formatISO, getHours, getMinutes, parseISO, startOfDay, startOfWeek} from 'date-fns'; /** * A single event @@ -28,7 +28,12 @@ export interface ScheduleSingleEvent { /** * Day the event is on */ - day: string; + day: Date; + + /** + * The full date the event is on, including time + */ + date: Date; /** * The event the date is referring to @@ -72,27 +77,19 @@ export class ScheduleSingleEventsComponent implements OnInit { dateSeries .flatMap(event => event.dates.map(date => ({ - dateUnix: moment(date).unix(), - day: moment(date).startOf('day').toISOString(), + day: startOfDay(parseISO(date)), + date: parseISO(date), event: { dateSeries: event, time: { - start: - moment(date).hour() + - moment(date) - // tslint:disable-next-line:no-magic-numbers - .minute() / - 60, - startAsString: moment(date).format('LT'), + start: getHours(parseISO(date)) + getMinutes(parseISO(date)) / 60, duration: event.duration, - endAsString: moment(date).add(event.duration).format('LT'), }, }, })), ) - .sort((a, b) => a.dateUnix - b.dateUnix) - .map(event => omit(event, 'dateUnix')), - it => it.day, + .sort((a, b) => compareAsc(a.date, b.date)), + it => formatISO(it.day), ), ) .sort(stringSortBy(([key]) => key)) @@ -109,7 +106,7 @@ export class ScheduleSingleEventsComponent implements OnInit { const dateSeries = await this.scheduleProvider.getDateSeries( this.uuids, undefined /*TODO*/, - moment(moment.now()).startOf('week').toISOString(), + formatISO(startOfWeek(Date.now())), ); // TODO: replace with filter diff --git a/frontend/app/src/app/modules/schedule/page/schedule-single-events.html b/frontend/app/src/app/modules/schedule/page/schedule-single-events.html index 8ea2ccf0..4ee8590e 100644 --- a/frontend/app/src/app/modules/schedule/page/schedule-single-events.html +++ b/frontend/app/src/app/modules/schedule/page/schedule-single-events.html @@ -16,7 +16,7 @@ - {{ day[0].day | amDateFormat : 'LL' }} + {{ day[0].day | dfnsFormat : 'PPP' }}
{{ event.event.time.startAsString }} diff --git a/frontend/app/src/app/modules/schedule/page/schedule-single-events.spec.ts b/frontend/app/src/app/modules/schedule/page/schedule-single-events.spec.ts index 05bd42e2..ec2c9926 100644 --- a/frontend/app/src/app/modules/schedule/page/schedule-single-events.spec.ts +++ b/frontend/app/src/app/modules/schedule/page/schedule-single-events.spec.ts @@ -12,10 +12,9 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ - import {SCDateSeries} from '@openstapps/core'; import {ScheduleSingleEventsComponent} from './schedule-single-events.component'; -import moment from 'moment'; +import {getHours, getMinutes, parseISO, startOfDay} from 'date-fns'; describe('ScheduleSingleEvents', () => { it('should group date series to days', () => { @@ -36,17 +35,16 @@ describe('ScheduleSingleEvents', () => { const grouped = ScheduleSingleEventsComponent.groupDateSeriesToDays(events as SCDateSeries[]); const seriesToDate = (series: Partial, index: number) => { - const time = moment(series.dates?.[index]); + const time = parseISO(series.dates![index]); return { - day: time.clone().startOf('day').toISOString(), + day: startOfDay(time), + date: time, event: { dateSeries: series as SCDateSeries, time: { - start: time.hour() + time.minute() / 60, - startAsString: moment(time).format('LT'), + start: getHours(time) + getMinutes(time) / 60, duration: series.duration as string, - endAsString: moment(time).add(series.duration?.[index]).format('LT'), }, }, }; diff --git a/frontend/app/src/app/modules/schedule/page/schedule-view.component.ts b/frontend/app/src/app/modules/schedule/page/schedule-view.component.ts index 235814cf..727c611a 100644 --- a/frontend/app/src/app/modules/schedule/page/schedule-view.component.ts +++ b/frontend/app/src/app/modules/schedule/page/schedule-view.component.ts @@ -14,15 +14,15 @@ */ import {AfterViewInit, Component, Input, OnInit, ViewChild} from '@angular/core'; import {ActivatedRoute} from '@angular/router'; -import moment, {Moment} from 'moment'; import {materialFade, materialManualFade, materialSharedAxisX} from '../../../animation/material-motion'; import {ScheduleProvider} from '../../calendar/schedule.provider'; import {SCISO8601Date, SCUuid} from '@openstapps/core'; import {ScheduleEvent, ScheduleResponsiveBreakpoint} from './schema/schema'; import {CalendarService} from '../../calendar/calendar.service'; import {CalendarComponent} from './components/calendar.component'; -import {IonContent, IonDatetime} from '@ionic/angular'; +import {IonContent} from '@ionic/angular'; import {SwiperComponent} from 'swiper/angular'; +import {addDays, differenceInDays, formatISO, getDay, getHours, parseISO, startOfWeek} from 'date-fns'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; /** @@ -37,14 +37,12 @@ import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; export class ScheduleViewComponent extends CalendarComponent implements OnInit, AfterViewInit { @ViewChild('mainSwiper') mainSwiper: SwiperComponent; - @ViewChild('headerSwiper') headerSwiper: SwiperComponent; - @ViewChild('content') content: IonContent; /** * The day that the schedule started out on */ - baselineDate: Moment; + baselineDate = new Date(); /** * Hours for grid @@ -80,7 +78,7 @@ export class ScheduleViewComponent extends CalendarComponent implements OnInit, // start at fist weekday depending on locale weekDates = Array.from({length: 7}).map( // eslint-disable-next-line unicorn/consistent-function-scoping - (_, i) => moment().startOf('week').add(i, 'days'), + (_, i) => addDays(startOfWeek(Date.now()), i), ); constructor( @@ -90,7 +88,7 @@ export class ScheduleViewComponent extends CalendarComponent implements OnInit, ) { super(activatedRoute, calendarService, scheduleProvider); const hoursAmount = this.hoursRange.to - this.hoursRange.from + 1; - this.hours = [...Array.from({length: hoursAmount}).keys()]; + this.hours = Array.from({length: hoursAmount}).map((_, i) => i); } /** @@ -111,7 +109,7 @@ export class ScheduleViewComponent extends CalendarComponent implements OnInit, * Slide today into view. */ slideToToday() { - const todayIndex = Number(moment().startOf('week').format('d')) + 1; + const todayIndex = differenceInDays(startOfWeek(Date.now()), this.baselineDate); this.mainSwiper?.swiperRef.slideTo(todayIndex); this.setDateRange(todayIndex); @@ -126,7 +124,7 @@ export class ScheduleViewComponent extends CalendarComponent implements OnInit, const dateSeries = await this.scheduleProvider.getDateSeries( this.uuids, ['P1W', 'P2W', 'P3W', 'P4W'], - moment(moment.now()).startOf('week').toISOString(), + formatISO(startOfWeek(Date.now())), ); this.testSchedule = {}; @@ -134,7 +132,7 @@ export class ScheduleViewComponent extends CalendarComponent implements OnInit, for (const series of dateSeries.dates) { const weekDays = Object.keys( series.dates.reduce((accumulator, date) => { - accumulator[moment(date).weekday()] = true; + accumulator[getDay(parseISO(date))] = true; return accumulator; }, {} as Record), ); @@ -144,7 +142,7 @@ export class ScheduleViewComponent extends CalendarComponent implements OnInit, (this.testSchedule[day] ?? (this.testSchedule[day] = {}))[series.uid] = { dateSeries: series, time: { - start: moment(series.dates[0]).hours(), + start: getHours(parseISO(series.dates[0])), duration: series.duration, }, }; @@ -153,15 +151,4 @@ export class ScheduleViewComponent extends CalendarComponent implements OnInit, return this.todaySlideIndex; } - - presentScheduleDatePopover(index: number, popoverDateTime: IonDatetime) { - const nextIndex = - moment(popoverDateTime.value).diff(this.baselineDate, 'days') - - this.headerSwiper.swiperRef.realIndex - - index; - - this.mainSwiper.swiperRef.slideTo(nextIndex); - this.setDateRange(nextIndex); - popoverDateTime.confirm(true); - } } diff --git a/frontend/app/src/app/modules/schedule/page/schedule-view.html b/frontend/app/src/app/modules/schedule/page/schedule-view.html index 8fd1bbf7..91b99a03 100644 --- a/frontend/app/src/app/modules/schedule/page/schedule-view.html +++ b/frontend/app/src/app/modules/schedule/page/schedule-view.html @@ -19,32 +19,6 @@ - - -
- - {{ dateRange.startDate }} - {{ dateRange.endDate }} - - - - - - - - -
-
-
@@ -60,7 +34,6 @@ #mainSwiper [slidesPerView]="layout.days" (indexChange)="onPageChange($event)" - (activeIndexChange)="syncSwiper(mainSwiper, headerSwiper)" class="full-height" > @@ -71,7 +44,7 @@ [uuids]="uuids" [dateSeries]="testSchedule[i]" [layout]="layout" - [isLeftmost]="dateRange.startDate === (i | dateFromIndex : baselineDate).format('DD.MM.YY')" + [isLeftmost]="dateRange.startDate | dfnsIsSameDay : (baselineDate | dfnsAddDays : i)" > diff --git a/frontend/app/src/app/modules/schedule/page/schema/schema.ts b/frontend/app/src/app/modules/schedule/page/schema/schema.ts index faafb114..e4d0fd4f 100644 --- a/frontend/app/src/app/modules/schedule/page/schema/schema.ts +++ b/frontend/app/src/app/modules/schedule/page/schema/schema.ts @@ -12,9 +12,7 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ - import {SCDateSeries, SCISO8601Duration} from '@openstapps/core'; -import {unitOfTime} from 'moment'; interface DateRange { duration: SCISO8601Duration; @@ -67,7 +65,7 @@ export interface ScheduleResponsiveBreakpoint { /** * When the first day should start */ - startOf: unitOfTime.StartOf; + startOf: 'day' | 'week'; /** * Width until next breakpoint is hit diff --git a/frontend/app/src/app/modules/schedule/schedule.module.ts b/frontend/app/src/app/modules/schedule/schedule.module.ts index 23415fe3..e5812292 100644 --- a/frontend/app/src/app/modules/schedule/schedule.module.ts +++ b/frontend/app/src/app/modules/schedule/schedule.module.ts @@ -19,8 +19,6 @@ import {RouterModule, Routes} from '@angular/router'; import {IonicModule} from '@ionic/angular'; import {TranslateModule} from '@ngx-translate/core'; import {ScheduleCardComponent} from './page/grid/schedule-card.component'; - -import {DateFormatPipe, MomentModule} from 'ngx-moment'; import {UtilModule} from '../../util/util.module'; import {DataModule} from '../data/data.module'; import {DataProvider} from '../data/data.provider'; @@ -37,6 +35,7 @@ import {InfiniteSwiperComponent} from './page/grid/infinite-swiper.component'; import {CalendarComponent} from './page/components/calendar.component'; import {IonIconModule} from '../../util/ion-icon/ion-icon.module'; import {ChooseEventsPageComponent} from './page/choose-events-page.component'; +import {AddDaysPipeModule, DateFnsModule, FormatPipeModule, IsSameDayPipeModule} from 'ngx-date-fns'; const settingsRoutes: Routes = [ {path: 'schedule', redirectTo: 'schedule/calendar/now'}, @@ -71,13 +70,16 @@ const settingsRoutes: Routes = [ FormsModule, IonicModule.forRoot(), IonIconModule, - MomentModule, RouterModule.forChild(settingsRoutes), SwiperModule, TranslateModule.forChild(), UtilModule, ThingTranslateModule, + AddDaysPipeModule, + FormatPipeModule, + IsSameDayPipeModule, + DateFnsModule, ], - providers: [ScheduleProvider, DataProvider, DateFormatPipe], + providers: [ScheduleProvider, DataProvider], }) export class ScheduleModule {} diff --git a/frontend/app/src/app/translation/common-string-pipes.ts b/frontend/app/src/app/translation/common-string-pipes.ts index 49a1f346..4ef5dcd5 100644 --- a/frontend/app/src/app/translation/common-string-pipes.ts +++ b/frontend/app/src/app/translation/common-string-pipes.ts @@ -14,7 +14,6 @@ */ 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'; @@ -52,17 +51,6 @@ export class EntriesPipe implements PipeTransform { } } -@Injectable() -@Pipe({ - name: 'toUnix', - pure: true, -}) -export class ToUnixPipe implements PipeTransform { - transform(value: string | number | Date | null | undefined): number { - return (value instanceof Date ? value : new Date(value ?? 0)).valueOf(); - } -} - @Injectable() @Pipe({ name: 'sentencecase', @@ -339,78 +327,3 @@ export class NumberLocalizedPipe implements PipeTransform, OnDestroy { this.value = new Intl.NumberFormat(this.locale, options).format(float); } } - -@Injectable() -@Pipe({ - name: 'dateFormat', - pure: true, -}) -export class DateLocalizedFormatPipe 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(); - } - - /** - * @param value The date to be formatted - * @param formatOptions Dateformat options to include. - * As specified by Intl.DateTimeFormatOptions as comma seperated key:value pairs - * Default is year,month,day,hour and minute in numeric representation e.g. (en-US) "8/6/2021, 10:35" - */ - transform(value: string | unknown, formatOptions?: string): string { - this.updateValue(value, formatOptions); - this._dispose(); - if (this.onLangChange?.closed === true) { - this.onLangChange = this.translate.onLangChange.subscribe((event: LangChangeEvent) => { - this.locale = event.lang; - this.updateValue(value, formatOptions); - }); - } - - return this.value; - } - - updateValue(value: string | Date | unknown, formatOptions?: string): void { - if (typeof value !== 'string' && Object.prototype.toString.call(value) !== '[object Date]') { - logger.warn(`dateFormat pipe unable to parse input: ${value}`); - - return; - } - const options = formatOptions - ?.split(',') - .map(element => element.split(':')) - .reduce( - (accumulator, [key, value_]) => ({ - ...accumulator, - [key.trim()]: value_.trim(), - }), - {}, - ) as Intl.DateTimeFormatOptions; - const date = typeof value === 'string' ? Date.parse(value) : (value as Date); - this.value = new Intl.DateTimeFormat( - this.locale, - options ?? { - day: 'numeric', - month: 'numeric', - year: 'numeric', - hour: 'numeric', - minute: 'numeric', - }, - ).format(date); - } -} diff --git a/frontend/app/src/app/translation/date-time/format-frequency.pipe.ts b/frontend/app/src/app/translation/date-time/format-frequency.pipe.ts new file mode 100644 index 00000000..471cee0c --- /dev/null +++ b/frontend/app/src/app/translation/date-time/format-frequency.pipe.ts @@ -0,0 +1,68 @@ +import {Injectable, OnDestroy, Pipe, PipeTransform} from '@angular/core'; +import {Subscription} from 'rxjs'; +import {LangChangeEvent, TranslateService} from '@ngx-translate/core'; +import {formatDuration} from 'date-fns'; + +@Injectable() +@Pipe({ + name: 'dfnsFormatFrequency', + standalone: true, + pure: false, +}) +export class DfnsFormatFrequencyPipe implements PipeTransform, OnDestroy { + locale: string; + + onLangChange?: Subscription; + + value: string; + + frequencyPrefixes: {[iso6391Code: string]: string} = { + de: 'alle', + en: 'every', + es: 'cada', + pt: 'a cada', + fr: 'tous les', + cn: '每', + ru: 'kаждые', + }; + + constructor(private readonly translate: TranslateService) { + this.locale = translate.currentLang; + } + + private _dispose(): void { + if (this.onLangChange?.closed === false) { + this.onLangChange?.unsubscribe(); + } + } + + ngOnDestroy(): void { + this._dispose(); + } + + /** + * @param value An ISO 8601 duration string + */ + transform(value: Duration): string { + this.updateValue(value); + this._dispose(); + if (this.onLangChange?.closed === true) { + this.onLangChange = this.translate.onLangChange.subscribe((event: LangChangeEvent) => { + this.locale = event.lang; + this.updateValue(value); + }); + } + + return this.value; + } + + updateValue(value: Duration): void { + const fequencyPrefix = Object.keys(this.frequencyPrefixes).filter(element => + this.locale.includes(element), + ); + this.value = [ + fequencyPrefix.length > 0 ? this.frequencyPrefixes[fequencyPrefix[0]] : this.frequencyPrefixes.en, + formatDuration(value), + ].join(' '); + } +} diff --git a/frontend/app/src/app/translation/date-time/format-relative-date.pipe.ts b/frontend/app/src/app/translation/date-time/format-relative-date.pipe.ts new file mode 100644 index 00000000..71118e07 --- /dev/null +++ b/frontend/app/src/app/translation/date-time/format-relative-date.pipe.ts @@ -0,0 +1,67 @@ +import {Pipe, PipeTransform} from '@angular/core'; +import {DateFnsConfigurationService} from 'ngx-date-fns'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; +import {formatRelative} from 'date-fns'; + +const formatRelativeLocaleEn = { + lastWeek: "'last' eeee", + yesterday: "'yesterday'", + today: "'today'", + tomorrow: "'tomorrow'", + nextWeek: "'next' eeee", + other: 'pp', +}; + +const formatRelativeLocaleDe = { + lastWeek: "'letzten' eeee", + yesterday: "'gestern'", + today: "'heute'", + tomorrow: "'morgen'", + nextWeek: 'eeee', + other: 'pp', +}; + +@Pipe({ + name: 'dfnsFormatRelativeDate', + pure: false, + standalone: true, +}) +export class FormatRelativeDatePipe implements PipeTransform { + locale: Locale; + + value: string; + + input: Date | number; + + base: Date | number; + + constructor(private dfnsConfig: DateFnsConfigurationService) { + this.updateLocale(); + dfnsConfig.localeChanged.pipe(takeUntilDestroyed()).subscribe(this.updateLocale.bind(this)); + } + + private updateLocale() { + this.locale = {...this.dfnsConfig.locale()!}; + const format = { + ['de']: formatRelativeLocaleDe, + ['en-US']: formatRelativeLocaleEn, + }[this.locale.code!]; + this.locale.formatRelative = function (token) { + return format![token as keyof typeof format] as string; + }; + this.updateValue(); + } + + transform(value: Date | number, other: Date | number = Date.now()): string { + this.input = value; + this.base = other; + this.updateValue(); + + return this.value; + } + + updateValue() { + if (!this.input || !this.base) return; + this.value = formatRelative(this.input, this.base, {locale: this.locale}); + } +} diff --git a/frontend/app/src/app/translation/date-time/format-relative-live.pipe.ts b/frontend/app/src/app/translation/date-time/format-relative-live.pipe.ts new file mode 100644 index 00000000..d4924f49 --- /dev/null +++ b/frontend/app/src/app/translation/date-time/format-relative-live.pipe.ts @@ -0,0 +1,57 @@ +import {OnDestroy, Pipe, PipeTransform} from '@angular/core'; +import {Subscription} from 'rxjs'; +import {LangChangeEvent, TranslateService} from '@ngx-translate/core'; +import {formatDuration} from 'date-fns'; + +@Pipe({ + name: 'dfnsFormatRelativeLive', + pure: false, + standalone: true, +}) +export class FormatRelativeLivePipe 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(); + } + + /** + * @param value An ISO 8601 duration string + */ + transform(value: Duration): string { + this.updateValue(value); + this._dispose(); + if (this.onLangChange?.closed === true) { + this.onLangChange = this.translate.onLangChange.subscribe((event: LangChangeEvent) => { + this.locale = event.lang; + this.updateValue(value); + }); + } + + return this.value; + } + + updateValue(value: Duration): void { + const fequencyPrefix = Object.keys(this.frequencyPrefixes).filter(element => + this.locale.includes(element), + ); + this.value = [ + fequencyPrefix.length > 0 ? this.frequencyPrefixes[fequencyPrefix[0]] : this.frequencyPrefixes.en, + formatDuration(value), + ].join(' '); + } +} diff --git a/frontend/app/src/app/translation/date-time/opening-hours.pipe.ts b/frontend/app/src/app/translation/date-time/opening-hours.pipe.ts new file mode 100644 index 00000000..4f946ea0 --- /dev/null +++ b/frontend/app/src/app/translation/date-time/opening-hours.pipe.ts @@ -0,0 +1,141 @@ +import {Injectable, OnDestroy, Pipe, PipeTransform} from '@angular/core'; +import {Subscription} from 'rxjs'; +import {LangChangeEvent, TranslateService} from '@ngx-translate/core'; +import {logger} from '../../_helpers/ts-logger'; +import opening_hours from 'opening_hours'; +import {addHours, formatRelative, isBefore, isToday} from 'date-fns'; + +@Injectable() +@Pipe({ + name: 'openingHours', + standalone: true, + pure: false, +}) +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 = isToday(nextChange!); + + 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 = formatRelative(nextChange!, Date.now()); + + if (isBefore(nextChange!, addHours(Date.now(), 1))) { + 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; + } +} diff --git a/frontend/app/src/app/translation/date-time/parse-duration.pipe.ts b/frontend/app/src/app/translation/date-time/parse-duration.pipe.ts new file mode 100644 index 00000000..99b3c6d0 --- /dev/null +++ b/frontend/app/src/app/translation/date-time/parse-duration.pipe.ts @@ -0,0 +1,14 @@ +import {Pipe, PipeTransform} from '@angular/core'; +import type {Duration} from 'date-fns'; +import {DurationInput, parse as parseISODuration} from 'duration-fns'; + +@Pipe({ + name: 'dfnsParseDuration', + standalone: true, + pure: true, +}) +export class ParseDurationPipe implements PipeTransform { + transform(duration: DurationInput): Duration { + return parseISODuration(duration); + } +} diff --git a/frontend/app/src/app/translation/thing-translate.module.ts b/frontend/app/src/app/translation/thing-translate.module.ts index d4421972..2387d4fd 100644 --- a/frontend/app/src/app/translation/thing-translate.module.ts +++ b/frontend/app/src/app/translation/thing-translate.module.ts @@ -15,8 +15,6 @@ import {ModuleWithProviders, NgModule, Provider} from '@angular/core'; import { ArrayJoinPipe, - DateLocalizedFormatPipe, - DurationLocalizedPipe, EntriesPipe, IsNaNPipe, IsNumericPipe, @@ -24,7 +22,6 @@ import { NumberLocalizedPipe, SentenceCasePipe, StringSplitPipe, - ToUnixPipe, } from './common-string-pipes'; import {ThingTranslateDefaultParser, ThingTranslateParser} from './thing-translate.parser'; import {ThingTranslatePipe} from './thing-translate.pipe'; @@ -41,16 +38,13 @@ export interface ThingTranslateModuleConfig { imports: [IonIconModule], declarations: [ ArrayJoinPipe, - DurationLocalizedPipe, NumberLocalizedPipe, MetersLocalizedPipe, StringSplitPipe, PropertyNameTranslatePipe, ThingTranslatePipe, TranslateSimplePipe, - DateLocalizedFormatPipe, SentenceCasePipe, - ToUnixPipe, EntriesPipe, IsNaNPipe, IsNumericPipe, @@ -58,16 +52,13 @@ export interface ThingTranslateModuleConfig { exports: [ IonIconModule, ArrayJoinPipe, - DurationLocalizedPipe, NumberLocalizedPipe, MetersLocalizedPipe, StringSplitPipe, PropertyNameTranslatePipe, ThingTranslatePipe, TranslateSimplePipe, - DateLocalizedFormatPipe, SentenceCasePipe, - ToUnixPipe, EntriesPipe, IsNaNPipe, IsNumericPipe, diff --git a/frontend/app/src/app/translation/thing-translate.service.ts b/frontend/app/src/app/translation/thing-translate.service.ts index cf1122c9..77d5d89c 100644 --- a/frontend/app/src/app/translation/thing-translate.service.ts +++ b/frontend/app/src/app/translation/thing-translate.service.ts @@ -22,7 +22,6 @@ import { SCThingType, SCTranslations, } from '@openstapps/core'; -import moment from 'moment'; import {isDefined, ThingTranslateParser} from './thing-translate.parser'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; import {setDefaultOptions} from 'date-fns'; @@ -40,7 +39,6 @@ export class ThingTranslateService { translator: SCThingTranslator; /** - * * @param translateService Instance of Angular TranslateService * @param parser An instance of the parser currently used * @param dfnsConfiguration the date fns configuration @@ -56,7 +54,6 @@ export class ThingTranslateService { /** set the default language from configuration */ this.translateService.onLangChange.pipe(takeUntilDestroyed()).subscribe((event: LangChangeEvent) => { this.translator.language = event.lang as keyof SCTranslations; - moment.locale(event.lang); getDateFnsLocale(event.lang as SCLanguageCode).then(locale => { setDefaultOptions({locale}); this.dfnsConfiguration.setLocale(locale); diff --git a/frontend/app/src/app/util/array-last.pipe.ts b/frontend/app/src/app/util/array-last.pipe.ts deleted file mode 100644 index b897052f..00000000 --- a/frontend/app/src/app/util/array-last.pipe.ts +++ /dev/null @@ -1,34 +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 . - */ - -import {Injectable, Pipe, PipeTransform} from '@angular/core'; - -/** - * Get the last value of an array - */ -@Injectable() -@Pipe({ - name: 'last', - pure: true, -}) -export class ArrayLastPipe implements PipeTransform { - /** - * Transform - */ - // tslint:disable-next-line:prefer-function-over-method - transform(value: T[]): T | undefined { - return value.at(-1); - } -} diff --git a/frontend/app/src/app/util/date-from-index.pipe.ts b/frontend/app/src/app/util/date-from-index.pipe.ts deleted file mode 100644 index 7cd58f22..00000000 --- a/frontend/app/src/app/util/date-from-index.pipe.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (C) 2021 StApps - * This program is free software: you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the Free - * Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along with - * this program. If not, see . - */ - -import {Pipe, PipeTransform} from '@angular/core'; -import {Moment} from 'moment'; - -@Pipe({ - name: 'dateFromIndex', - pure: true, -}) -export class DateFromIndexPipe implements PipeTransform { - transform(index: number, baseline: Moment): Moment { - return baseline.clone().add(index, 'days'); - } -} diff --git a/frontend/app/src/app/util/date-is-today.pipe.ts b/frontend/app/src/app/util/date-is-today.pipe.ts deleted file mode 100644 index b2457f6b..00000000 --- a/frontend/app/src/app/util/date-is-today.pipe.ts +++ /dev/null @@ -1,38 +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 . - */ - -import {Injectable, Pipe, PipeTransform} from '@angular/core'; -import moment, {Moment, unitOfTime} from 'moment'; - -/** - * Get the last value of an array - */ -@Injectable() -@Pipe({ - name: 'dateIsThis', - pure: false, // pure pipe can break in some change detection scenarios, - // specifically, on the calendar view it causes it to stay true even when you navigate -}) -export class DateIsThisPipe implements PipeTransform { - /** - * Transform - */ - // tslint:disable-next-line:prefer-function-over-method - transform(value: Moment | string | number, granularity: unitOfTime.StartOf): boolean { - return ( - typeof value === 'string' ? moment(value) : typeof value === 'number' ? moment.unix(value) : value - ).isSame(moment(moment.now()), granularity); - } -} diff --git a/frontend/app/src/app/util/daytime-key.pipe.ts b/frontend/app/src/app/util/daytime-key.pipe.ts index 5aa8f6e6..78a85981 100644 --- a/frontend/app/src/app/util/daytime-key.pipe.ts +++ b/frontend/app/src/app/util/daytime-key.pipe.ts @@ -12,9 +12,8 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ - import {Injectable, Pipe, PipeTransform} from '@angular/core'; -import moment from 'moment'; +import {getHours} from 'date-fns'; /** * Return the extended translation key by the current daytime key @@ -28,7 +27,7 @@ export class DaytimeKeyPipe implements PipeTransform { * Transform */ transform(translationKey: string): string { - const hour = Number.parseInt(moment().format('HH'), 10); + const hour = getHours(Date.now()); let key = ''; if (hour >= 5 && hour <= 10) { key = 'morning'; diff --git a/frontend/app/src/app/util/next-date-in-list.pipe.ts b/frontend/app/src/app/util/next-date-in-list.pipe.ts deleted file mode 100644 index 17efc92b..00000000 --- a/frontend/app/src/app/util/next-date-in-list.pipe.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (C) 2021 StApps - * This program is free software: you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the Free - * Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along with - * this program. If not, see . - */ - -import {Injectable, Pipe, PipeTransform} from '@angular/core'; -import moment from 'moment'; - -/** - * Get the last value of an array - */ -@Injectable() -@Pipe({ - name: 'nextDateInList', - pure: false, // pure pipe can break in some change detection scenarios, - // specifically, on the calendar view it causes it to stay true even when you navigate -}) -export class NextDateInListPipe implements PipeTransform { - /** - * Transform - */ - // tslint:disable-next-line:prefer-function-over-method - transform(dates: string[]): string { - const nextDate = dates - .sort((a, b) => moment(a).unix() - moment(b).unix()) - .find(date => { - return moment(date).unix() > moment().unix(); - }); - return nextDate || ''; - } -} diff --git a/frontend/app/src/app/util/nullish-coalecing.pipe.ts b/frontend/app/src/app/util/nullish-coalecing.pipe.ts deleted file mode 100644 index 064ae803..00000000 --- a/frontend/app/src/app/util/nullish-coalecing.pipe.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (C) 2021 StApps - * This program is free software: you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the Free - * Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along with - * this program. If not, see . - */ - -import {Injectable, Pipe, PipeTransform} from '@angular/core'; - -/** - * Get the last value of an array - */ -@Injectable() -@Pipe({ - name: 'nullishCoalesce', - pure: true, -}) -export class NullishCoalescingPipe implements PipeTransform { - /** - * Transform - */ - // tslint:disable-next-line:prefer-function-over-method - transform(value: T, fallback: G): T | G { - return value ?? fallback; - } -} diff --git a/frontend/app/src/app/util/util.module.ts b/frontend/app/src/app/util/util.module.ts index 60b91b37..9d5c5692 100644 --- a/frontend/app/src/app/util/util.module.ts +++ b/frontend/app/src/app/util/util.module.ts @@ -13,13 +13,8 @@ * this program. If not, see . */ import {NgModule} from '@angular/core'; -import {ArrayLastPipe} from './array-last.pipe'; -import {DateIsThisPipe} from './date-is-today.pipe'; -import {NullishCoalescingPipe} from './nullish-coalecing.pipe'; -import {DateFromIndexPipe} from './date-from-index.pipe'; import {DaytimeKeyPipe} from './daytime-key.pipe'; import {LazyPipe} from './lazy.pipe'; -import {NextDateInListPipe} from './next-date-in-list.pipe'; import {EditModalComponent} from './edit-modal.component'; import {BrowserModule} from '@angular/platform-browser'; import {IonicModule} from '@ionic/angular'; @@ -47,14 +42,9 @@ import {FormatDistanceToNowStrictPipeModule, FormatRelativeToNowPipeModule} from declarations: [ IonContentParallaxDirective, ElementSizeChangeDirective, - ArrayLastPipe, - DateIsThisPipe, - NullishCoalescingPipe, LazyPipe, SectionComponent, - DateFromIndexPipe, DaytimeKeyPipe, - NextDateInListPipe, EditModalComponent, OpeningHoursComponent, SimpleSwiperComponent, @@ -63,14 +53,9 @@ import {FormatDistanceToNowStrictPipeModule, FormatRelativeToNowPipeModule} from exports: [ IonContentParallaxDirective, ElementSizeChangeDirective, - ArrayLastPipe, - DateIsThisPipe, - NullishCoalescingPipe, LazyPipe, - DateFromIndexPipe, DaytimeKeyPipe, SectionComponent, - NextDateInListPipe, EditModalComponent, OpeningHoursComponent, SimpleSwiperComponent,