Browse Source

Feature/persist the selected date range on the device

pull/8237/head
mister-ideas 5 days ago
parent
commit
8a5379912e
  1. 1
      CHANGELOG.md
  2. 39
      apps/client/src/app/services/user/user.service.ts
  3. 55
      libs/common/src/lib/date-range.helper.spec.ts
  4. 32
      libs/common/src/lib/date-range.helper.ts
  5. 11
      libs/ui/src/lib/services/data.service.ts

1
CHANGELOG.md

@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Added a date range selector to the home and the portfolio pages - Added a date range selector to the home and the portfolio pages
- Persisted the selected date range on the device and restored it on page load
### Changed ### Changed

39
apps/client/src/app/services/user/user.service.ts

@ -1,4 +1,5 @@
import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service'; import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service';
import { getStoredDateRange } from '@ghostfolio/common/date-range.helper';
import { Filter, User } from '@ghostfolio/common/interfaces'; import { Filter, User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
@ -9,7 +10,7 @@ import { MatDialog } from '@angular/material/dialog';
import { ObservableStore } from '@codewithdan/observable-store'; import { ObservableStore } from '@codewithdan/observable-store';
import { parseISO } from 'date-fns'; import { parseISO } from 'date-fns';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
import { Observable, of } from 'rxjs'; import { EMPTY, Observable, of } from 'rxjs';
import { throwError } from 'rxjs'; import { throwError } from 'rxjs';
import { catchError, map } from 'rxjs/operators'; import { catchError, map } from 'rxjs/operators';
@ -24,6 +25,8 @@ export class UserService extends ObservableStore<UserStoreState> {
() => this.deviceDetectorService.deviceInfo().deviceType () => this.deviceDetectorService.deviceInfo().deviceType
); );
private hasRestoredDateRange = false;
private readonly destroyRef = inject(DestroyRef); private readonly destroyRef = inject(DestroyRef);
private readonly deviceDetectorService = inject(DeviceDetectorService); private readonly deviceDetectorService = inject(DeviceDetectorService);
private readonly dialog = inject(MatDialog); private readonly dialog = inject(MatDialog);
@ -161,6 +164,8 @@ export class UserService extends ObservableStore<UserStoreState> {
); );
} }
this.restoreStoredDateRange(user);
this.setState({ user }, UserStoreActions.GetUser); this.setState({ user }, UserStoreActions.GetUser);
if ( if (
@ -194,6 +199,38 @@ export class UserService extends ObservableStore<UserStoreState> {
); );
} }
/**
* Restores the date range selected last on this device once per page load,
* e.g. if the stored user setting has been changed from another device.
*/
private restoreStoredDateRange(user: User) {
if (this.hasRestoredDateRange || !user.settings) {
return;
}
this.hasRestoredDateRange = true;
const storedDateRange = getStoredDateRange();
if (
!storedDateRange ||
storedDateRange === user.settings.dateRange ||
user.settings.viewMode === 'ZEN'
) {
return;
}
user.settings.dateRange = storedDateRange;
this.http
.put('/api/v1/user/setting', { dateRange: storedDateRange })
.pipe(
catchError(() => EMPTY),
takeUntilDestroyed(this.destroyRef)
)
.subscribe();
}
private handleError(error: any) { private handleError(error: any) {
if (error.error instanceof Error) { if (error.error instanceof Error) {
const errMessage = error.error.message; const errMessage = error.error.message;

55
libs/common/src/lib/date-range.helper.spec.ts

@ -1,4 +1,9 @@
import { getAvailableDateRanges } from './date-range.helper'; import {
getAvailableDateRanges,
getStoredDateRange,
saveStoredDateRange,
STORED_DATE_RANGE_KEY
} from './date-range.helper';
describe('getAvailableDateRanges', () => { describe('getAvailableDateRanges', () => {
const referenceDate = new Date('2026-10-19T12:00:00.000Z'); const referenceDate = new Date('2026-10-19T12:00:00.000Z');
@ -40,3 +45,51 @@ describe('getAvailableDateRanges', () => {
).toEqual(['1d', 'wtd', 'mtd', 'ytd', '1y', '5y', 'max']); ).toEqual(['1d', 'wtd', 'mtd', 'ytd', '1y', '5y', 'max']);
}); });
}); });
describe('stored date range', () => {
const createStorage = (initial?: string) => {
const items = new Map<string, string>();
if (initial !== undefined) {
items.set(STORED_DATE_RANGE_KEY, initial);
}
return {
getItem: (key: string) => items.get(key) ?? null,
setItem: (key: string, value: string) => {
items.set(key, value);
}
};
};
it('returns undefined without a stored value', () => {
expect(getStoredDateRange(createStorage())).toBeUndefined();
});
it('returns a valid stored value', () => {
expect(getStoredDateRange(createStorage('ytd'))).toBe('ytd');
expect(getStoredDateRange(createStorage('2025'))).toBe('2025');
});
it('ignores an invalid stored value', () => {
expect(getStoredDateRange(createStorage('forever'))).toBeUndefined();
});
it('round-trips a saved value', () => {
const storage = createStorage();
saveStoredDateRange('mtd', storage);
expect(getStoredDateRange(storage)).toBe('mtd');
});
it('does not throw if the storage is not available', () => {
expect(
getStoredDateRange({
getItem: () => {
throw new Error('denied');
}
})
).toBeUndefined();
});
});

32
libs/common/src/lib/date-range.helper.ts

@ -31,3 +31,35 @@ export function getAvailableDateRanges({
return dateRanges; return dateRanges;
} }
export const STORED_DATE_RANGE_KEY = 'gf-date-range';
/**
* Returns the date range which has been selected last on this device
* (e.g. `ytd` or a year like `2025`), if any.
*/
export function getStoredDateRange(storage?: Pick<Storage, 'getItem'>) {
try {
const value = (storage ?? globalThis.localStorage)?.getItem(
STORED_DATE_RANGE_KEY
);
return value && /^(1d|wtd|mtd|ytd|1y|5y|max|\d{4})$/.test(value)
? (value as DateRange)
: undefined;
} catch {
return undefined;
}
}
export function saveStoredDateRange(
dateRange: DateRange,
storage?: Pick<Storage, 'setItem'>
) {
try {
(storage ?? globalThis.localStorage)?.setItem(
STORED_DATE_RANGE_KEY,
dateRange
);
} catch {}
}

11
libs/ui/src/lib/services/data.service.ts

@ -1,3 +1,4 @@
import { saveStoredDateRange } from '@ghostfolio/common/date-range.helper';
import { import {
CreateAccessDto, CreateAccessDto,
CreateAccountBalanceDto, CreateAccountBalanceDto,
@ -86,7 +87,7 @@ import { utc } from '@date-fns/utc';
import { format, parseISO } from 'date-fns'; import { format, parseISO } from 'date-fns';
import { cloneDeep, groupBy, isNumber } from 'lodash-es'; import { cloneDeep, groupBy, isNumber } from 'lodash-es';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map } from 'rxjs/operators'; import { map, tap } from 'rxjs/operators';
@Service() @Service()
export class DataService { export class DataService {
@ -958,7 +959,13 @@ export class DataService {
} }
public putUserSetting(aData: UpdateUserSettingDto) { public putUserSetting(aData: UpdateUserSettingDto) {
return this.http.put<User>('/api/v1/user/setting', aData); return this.http.put<User>('/api/v1/user/setting', aData).pipe(
tap(() => {
if (aData.dateRange) {
saveStoredDateRange(aData.dateRange);
}
})
);
} }
public redeemCoupon(couponCode: string) { public redeemCoupon(couponCode: string) {

Loading…
Cancel
Save