feat(ux): implement sprint 11 user experience module

This commit is contained in:
sdarbinyan
2026-07-09 01:13:54 +04:00
parent 1a8f916942
commit 92e1bdaff8
59 changed files with 2062 additions and 25 deletions

View 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;
}

View File

@@ -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';
}
}

View File

@@ -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;
}

View File

@@ -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;
}
});