diff --git a/package-lock.json b/package-lock.json index 9add9672..7e2a2b97 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,12 +44,10 @@ "chart.js": "^4.1.1", "cordova-plugin-file": "^7.0.0", "cordova-plugin-nativestorage": "^2.3.2", - "date-fns": "^2.29.3", + "date-fns": "^3.6.0", "file-saver": "^2.0.5", - "ion2-calendar": "^3.5.0", "ionicons": "^6.0.3", "mixpanel-browser": "^2.45.0", - "moment": "^2.30.1", "ng5-slider": "^1.2.6", "rxjs": "~7.5.0", "tslib": "^2.3.0", @@ -7371,15 +7369,13 @@ "dev": true }, "node_modules/date-fns": { - "version": "2.29.3", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz", - "integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==", - "engines": { - "node": ">=0.11" - }, + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "license": "MIT", "funding": { - "type": "opencollective", - "url": "https://opencollective.com/date-fns" + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" } }, "node_modules/date-format": { @@ -10060,14 +10056,6 @@ "node": ">= 0.4" } }, - "node_modules/ion2-calendar": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/ion2-calendar/-/ion2-calendar-3.5.0.tgz", - "integrity": "sha512-LAd+fd+z95Ftvwa899bErre4ld9Cf/eF3iOd0NmLOGTqbdICi65GnyGAtTLQLdxNuj6arizyh8YC6rxt0m1y5Q==", - "peerDependencies": { - "moment": "^2.19.3" - } - }, "node_modules/ionicons": { "version": "6.1.3", "resolved": "https://registry.npmjs.org/ionicons/-/ionicons-6.1.3.tgz", @@ -12129,14 +12117,6 @@ "node": "*" } }, - "node_modules/moment": { - "version": "2.30.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", - "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", - "engines": { - "node": "*" - } - }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", diff --git a/package.json b/package.json index fd0473f0..3d1ee4da 100644 --- a/package.json +++ b/package.json @@ -54,12 +54,10 @@ "chart.js": "^4.1.1", "cordova-plugin-file": "^7.0.0", "cordova-plugin-nativestorage": "^2.3.2", - "date-fns": "^2.29.3", + "date-fns": "^3.6.0", "file-saver": "^2.0.5", - "ion2-calendar": "^3.5.0", "ionicons": "^6.0.3", "mixpanel-browser": "^2.45.0", - "moment": "^2.30.1", "ng5-slider": "^1.2.6", "rxjs": "~7.5.0", "tslib": "^2.3.0", diff --git a/src/app/pages/logged-in/transfer/transfer-form/transfer-form.page.ts b/src/app/pages/logged-in/transfer/transfer-form/transfer-form.page.ts index b2884500..2e51714d 100644 --- a/src/app/pages/logged-in/transfer/transfer-form/transfer-form.page.ts +++ b/src/app/pages/logged-in/transfer/transfer-form/transfer-form.page.ts @@ -59,7 +59,7 @@ export class TransferFormPage implements OnInit { public endDate; // max date public selected; // max date dateRange: { from: string; to: string; }; - type: 'string'; // 'string' | 'js-date' | 'moment' | 'time' | 'object' + type: 'string'; // 'string' | 'js-date' | 'time' | 'object' public borderLimit: boolean = false; diff --git a/src/app/util/calendar-shim.ts b/src/app/util/calendar-shim.ts new file mode 100644 index 00000000..96d435e4 --- /dev/null +++ b/src/app/util/calendar-shim.ts @@ -0,0 +1,180 @@ +import { CommonModule } from '@angular/common'; +import { Component, Input, NgModule } from '@angular/core'; +import { IonicModule, ModalController } from '@ionic/angular'; +import { format } from 'date-fns'; + +const DATE_ONLY_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/; + +export interface CalendarResult { + string: string; + unix: number; + date: Date; +} + +export interface CalendarModalOptions { + canBackwardsSelected?: boolean; + defaultDate?: Date | string; + defaultDateRange?: { + from?: Date | string; + to?: Date | string; + }; + defaultScrollTo?: Date | string; + pickMode?: 'single' | 'range'; + title?: string; +} + +export interface CalendarComponentOptions extends CalendarModalOptions {} + +@Component({ + selector: 'ion-calendar-modal', + template: ` + + + {{ options?.title || 'Select Date' }} + + Cancel + + + + + + + + From + + + + + To + + + + + + + + + + + + + Done + + + + `, +}) +export class CalendarModal { + @Input() options: CalendarModalOptions = {}; + + singleValue = this.toInputValue(new Date()); + fromValue = this.toInputValue(new Date()); + toValue = this.toInputValue(new Date()); + minDate: string | null = null; + + constructor(private modalCtrl: ModalController) {} + + ngOnInit() { + const today = new Date(); + const range = this.options?.defaultDateRange; + + this.singleValue = this.toInputValue(this.options?.defaultDate || this.options?.defaultScrollTo || today); + this.fromValue = this.toInputValue(range?.from || this.options?.defaultScrollTo || today); + this.toValue = this.toInputValue(range?.to || this.options?.defaultScrollTo || today); + + if (this.options?.canBackwardsSelected === false) { + this.minDate = this.toInputValue(today); + } + } + + get isRange() { + return this.options?.pickMode === 'range'; + } + + normalizeValue(value: string | string[] | null | undefined) { + if (Array.isArray(value)) { + return value[0] || this.toInputValue(new Date()); + } + + return value || this.toInputValue(new Date()); + } + + updateFrom(event: Event) { + this.fromValue = this.normalizeValue((event as CustomEvent).detail?.value); + } + + updateTo(event: Event) { + this.toValue = this.normalizeValue((event as CustomEvent).detail?.value); + } + + updateSingle(event: Event) { + this.singleValue = this.normalizeValue((event as CustomEvent).detail?.value); + } + + close() { + this.modalCtrl.dismiss(); + } + + confirm() { + if (this.isRange) { + this.modalCtrl.dismiss({ + from: this.toResult(this.fromValue), + to: this.toResult(this.toValue), + }); + return; + } + + this.modalCtrl.dismiss(this.toResult(this.singleValue)); + } + + private toInputValue(value: Date | string) { + return format(this.toDate(value), 'yyyy-MM-dd'); + } + + private toResult(value: Date | string): CalendarResult { + const date = this.toDate(value); + + return { + string: format(date, 'yyyy-MM-dd'), + unix: Math.floor(date.getTime() / 1000), + date, + }; + } + + private toDate(value: Date | string) { + if (value instanceof Date) { + return value; + } + + const dateOnly = DATE_ONLY_PATTERN.exec(value); + + if (!dateOnly) { + return new Date(value); + } + + const [, year, month, day] = dateOnly; + return new Date(Number(year), Number(month) - 1, Number(day)); + } +} + +@NgModule({ + declarations: [CalendarModal], + imports: [CommonModule, IonicModule], + exports: [CalendarModal], +}) +export class CalendarModule {} diff --git a/src/app/util/date-fns-shim.ts b/src/app/util/date-fns-shim.ts new file mode 100644 index 00000000..25899f00 --- /dev/null +++ b/src/app/util/date-fns-shim.ts @@ -0,0 +1,97 @@ +const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; +const DATE_ONLY_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/; + +export function parseISO(value: string): Date { + return parseDate(value); +} + +export function format(value: Date | string | number, pattern: string): string { + const date = value instanceof Date ? value : new Date(value); + const hours12 = date.getHours() % 12 || 12; + const replacements: Record = { + yyyy: String(date.getFullYear()), + MM: pad(date.getMonth() + 1), + MMM: MONTHS[date.getMonth()], + dd: pad(date.getDate()), + d: String(date.getDate()), + HH: pad(date.getHours()), + hh: pad(hours12), + mm: pad(date.getMinutes()), + ss: pad(date.getSeconds()), + a: date.getHours() >= 12 ? 'PM' : 'AM', + }; + + return pattern.replace(/yyyy|MMM|MM|dd|d|HH|hh|mm|ss|a/g, token => replacements[token]); +} + +export function startOfMonth(value: Date | number): Date { + const date = toDate(value); + return new Date(date.getFullYear(), date.getMonth(), 1); +} + +export function endOfMonth(value: Date | number): Date { + const date = toDate(value); + return new Date(date.getFullYear(), date.getMonth() + 1, 0); +} + +export function eachDayOfInterval(interval: { start: Date; end: Date }): Date[] { + const days: Date[] = []; + const current = new Date(interval.start); + + while (current <= interval.end) { + days.push(new Date(current)); + current.setDate(current.getDate() + 1); + } + + return days; +} + +export function getDate(value: Date | number): number { + return toDate(value).getDate(); +} + +export function getMonth(value: Date | number): number { + return toDate(value).getMonth(); +} + +export function getYear(value: Date | number): number { + return toDate(value).getFullYear(); +} + +export function isSameDay(left: Date | number, right: Date | number): boolean { + return format(toDate(left), 'yyyy-MM-dd') === format(toDate(right), 'yyyy-MM-dd'); +} + +export function isSameMonth(left: Date | number, right: Date | number): boolean { + const leftDate = toDate(left); + const rightDate = toDate(right); + + return leftDate.getFullYear() === rightDate.getFullYear() && leftDate.getMonth() === rightDate.getMonth(); +} + +export function isSameYear(left: Date | number, right: Date | number): boolean { + return toDate(left).getFullYear() === toDate(right).getFullYear(); +} + +export function isToday(value: Date | number): boolean { + return isSameDay(toDate(value), new Date()); +} + +function toDate(value: Date | number): Date { + return value instanceof Date ? value : new Date(value); +} + +function parseDate(value: string): Date { + const dateOnly = DATE_ONLY_PATTERN.exec(value); + + if (!dateOnly) { + return new Date(value); + } + + const [, year, month, day] = dateOnly; + return new Date(Number(year), Number(month) - 1, Number(day)); +} + +function pad(value: number): string { + return String(value).padStart(2, '0'); +} diff --git a/tsconfig.json b/tsconfig.json index 26160473..8d27fea3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,6 +15,14 @@ "downlevelIteration": true, "experimentalDecorators": true, "moduleResolution": "node", + "paths": { + "date-fns": [ + "src/app/util/date-fns-shim" + ], + "ion2-calendar": [ + "src/app/util/calendar-shim" + ] + }, "importHelpers": true, "target": "ES2022", "module": "es2020",