feat(ux): implement sprint 11 user experience module
This commit is contained in:
48
src/app/core/user-experience/models/user-experience.model.ts
Normal file
48
src/app/core/user-experience/models/user-experience.model.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Product } from '../../products/models/product-domain.model';
|
||||
|
||||
export interface FavoriteItem {
|
||||
product: Product;
|
||||
addedAt: string;
|
||||
ownerType: 'guest' | 'user';
|
||||
}
|
||||
|
||||
export interface ComparedProduct {
|
||||
product: Product;
|
||||
addedAt: string;
|
||||
}
|
||||
|
||||
export interface RecentlyViewedItem {
|
||||
product: Product;
|
||||
viewedAt: string;
|
||||
}
|
||||
|
||||
export interface SavedSearch {
|
||||
id: string;
|
||||
name: string;
|
||||
query: string;
|
||||
sort: string;
|
||||
categoryId?: number;
|
||||
filters: {
|
||||
values: Record<string, string[]>;
|
||||
ranges: Record<string, { min?: number; max?: number }>;
|
||||
toggles: Record<string, boolean>;
|
||||
};
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface ContinueBrowsingState {
|
||||
routeKey: string;
|
||||
query: string;
|
||||
sort: string;
|
||||
layout: string;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
categoryId?: number;
|
||||
filterState: {
|
||||
values: Record<string, string[]>;
|
||||
ranges: Record<string, { min?: number; max?: number }>;
|
||||
toggles: Record<string, boolean>;
|
||||
};
|
||||
scrollY: number;
|
||||
updatedAt: string;
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ComparedProduct, ContinueBrowsingState, FavoriteItem, RecentlyViewedItem, SavedSearch } from '../models/user-experience.model';
|
||||
import { UserExperienceRepository } from './user-experience.repository';
|
||||
|
||||
const KEYS = {
|
||||
wishlist: 'marketplace.ux.wishlist',
|
||||
compare: 'marketplace.ux.compare',
|
||||
recentlyViewed: 'marketplace.ux.recently-viewed',
|
||||
savedSearches: 'marketplace.ux.saved-searches',
|
||||
continueBrowsingPrefix: 'marketplace.ux.continue-browsing:'
|
||||
} as const;
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class LocalUserExperienceRepository implements UserExperienceRepository {
|
||||
getWishlist(): FavoriteItem[] {
|
||||
return this.readArray<FavoriteItem>(KEYS.wishlist);
|
||||
}
|
||||
|
||||
saveWishlist(items: FavoriteItem[]): void {
|
||||
this.write(KEYS.wishlist, items);
|
||||
}
|
||||
|
||||
getComparedProducts(): ComparedProduct[] {
|
||||
return this.readArray<ComparedProduct>(KEYS.compare);
|
||||
}
|
||||
|
||||
saveComparedProducts(items: ComparedProduct[]): void {
|
||||
this.write(KEYS.compare, items);
|
||||
}
|
||||
|
||||
getRecentlyViewed(): RecentlyViewedItem[] {
|
||||
return this.readArray<RecentlyViewedItem>(KEYS.recentlyViewed);
|
||||
}
|
||||
|
||||
saveRecentlyViewed(items: RecentlyViewedItem[]): void {
|
||||
this.write(KEYS.recentlyViewed, items);
|
||||
}
|
||||
|
||||
getSavedSearches(): SavedSearch[] {
|
||||
return this.readArray<SavedSearch>(KEYS.savedSearches);
|
||||
}
|
||||
|
||||
saveSavedSearches(items: SavedSearch[]): void {
|
||||
this.write(KEYS.savedSearches, items);
|
||||
}
|
||||
|
||||
getContinueBrowsing(routeKey: string): ContinueBrowsingState | null {
|
||||
return this.readSingle<ContinueBrowsingState>(this.continueKey(routeKey));
|
||||
}
|
||||
|
||||
saveContinueBrowsing(state: ContinueBrowsingState): void {
|
||||
this.write(this.continueKey(state.routeKey), state);
|
||||
}
|
||||
|
||||
clearContinueBrowsing(routeKey: string): void {
|
||||
if (!this.isBrowser()) {
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.removeItem(this.continueKey(routeKey));
|
||||
}
|
||||
|
||||
private readArray<T>(key: string): T[] {
|
||||
if (!this.isBrowser()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const raw = localStorage.getItem(key);
|
||||
if (!raw) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(raw);
|
||||
return Array.isArray(parsed) ? (parsed as T[]) : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private readSingle<T>(key: string): T | null {
|
||||
if (!this.isBrowser()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const raw = localStorage.getItem(key);
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return JSON.parse(raw) as T;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private write<T>(key: string, value: T): void {
|
||||
if (!this.isBrowser()) {
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.setItem(key, JSON.stringify(value));
|
||||
}
|
||||
|
||||
private continueKey(routeKey: string): string {
|
||||
return `${KEYS.continueBrowsingPrefix}${routeKey}`;
|
||||
}
|
||||
|
||||
private isBrowser(): boolean {
|
||||
return typeof window !== 'undefined';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { ComparedProduct, ContinueBrowsingState, FavoriteItem, RecentlyViewedItem, SavedSearch } from '../models/user-experience.model';
|
||||
|
||||
export interface UserExperienceRepository {
|
||||
getWishlist(): FavoriteItem[];
|
||||
saveWishlist(items: FavoriteItem[]): void;
|
||||
|
||||
getComparedProducts(): ComparedProduct[];
|
||||
saveComparedProducts(items: ComparedProduct[]): void;
|
||||
|
||||
getRecentlyViewed(): RecentlyViewedItem[];
|
||||
saveRecentlyViewed(items: RecentlyViewedItem[]): void;
|
||||
|
||||
getSavedSearches(): SavedSearch[];
|
||||
saveSavedSearches(items: SavedSearch[]): void;
|
||||
|
||||
getContinueBrowsing(routeKey: string): ContinueBrowsingState | null;
|
||||
saveContinueBrowsing(state: ContinueBrowsingState): void;
|
||||
clearContinueBrowsing(routeKey: string): void;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { InjectionToken, inject } from '@angular/core';
|
||||
import { LocalUserExperienceRepository } from './repositories/local-user-experience.repository';
|
||||
import { UserExperienceRepository } from './repositories/user-experience.repository';
|
||||
|
||||
export const USER_EXPERIENCE_REPOSITORY = new InjectionToken<UserExperienceRepository>('USER_EXPERIENCE_REPOSITORY', {
|
||||
providedIn: 'root',
|
||||
factory: () => {
|
||||
// Sprint 11: guest-first local storage; can be switched to authenticated repository later.
|
||||
const localRepository = inject(LocalUserExperienceRepository);
|
||||
return localRepository;
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user