feat(ux): implement sprint 11 user experience module
This commit is contained in:
136
src/app/facades/platform/user-experience.facade.ts
Normal file
136
src/app/facades/platform/user-experience.facade.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import { Injectable, computed, inject, signal } from '@angular/core';
|
||||
import { Product } from '../../core/products/models/product-domain.model';
|
||||
import { ComparedProduct, ContinueBrowsingState, FavoriteItem, RecentlyViewedItem, SavedSearch } from '../../core/user-experience/models/user-experience.model';
|
||||
import { USER_EXPERIENCE_REPOSITORY } from '../../core/user-experience/user-experience-repository.token';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class UserExperienceFacade {
|
||||
private readonly repository = inject(USER_EXPERIENCE_REPOSITORY);
|
||||
|
||||
private readonly wishlistState = signal<FavoriteItem[]>(this.repository.getWishlist());
|
||||
private readonly compareState = signal<ComparedProduct[]>(this.repository.getComparedProducts());
|
||||
private readonly recentlyViewedState = signal<RecentlyViewedItem[]>(this.repository.getRecentlyViewed());
|
||||
private readonly savedSearchesState = signal<SavedSearch[]>(this.repository.getSavedSearches());
|
||||
|
||||
readonly wishlist = this.wishlistState.asReadonly();
|
||||
readonly comparedProducts = this.compareState.asReadonly();
|
||||
readonly recentlyViewed = this.recentlyViewedState.asReadonly();
|
||||
readonly savedSearches = this.savedSearchesState.asReadonly();
|
||||
|
||||
readonly wishlistCount = computed(() => this.wishlistState().length);
|
||||
readonly compareCount = computed(() => this.compareState().length);
|
||||
|
||||
isInWishlist(productId: number): boolean {
|
||||
return this.wishlistState().some(item => item.product.itemID === productId);
|
||||
}
|
||||
|
||||
isInCompare(productId: number): boolean {
|
||||
return this.compareState().some(item => item.product.itemID === productId);
|
||||
}
|
||||
|
||||
toggleWishlist(product: Product, ownerType: FavoriteItem['ownerType'] = 'guest'): { added: boolean; count: number } {
|
||||
const existing = this.wishlistState();
|
||||
const present = existing.some(item => item.product.itemID === product.itemID);
|
||||
|
||||
const next = present
|
||||
? existing.filter(item => item.product.itemID !== product.itemID)
|
||||
: [{ product, addedAt: new Date().toISOString(), ownerType }, ...existing];
|
||||
|
||||
this.wishlistState.set(next);
|
||||
this.repository.saveWishlist(next);
|
||||
|
||||
return { added: !present, count: next.length };
|
||||
}
|
||||
|
||||
clearWishlist(): void {
|
||||
this.wishlistState.set([]);
|
||||
this.repository.saveWishlist([]);
|
||||
}
|
||||
|
||||
addToCompare(product: Product, maxItems: number): { added: boolean; reason?: 'limit'; count: number } {
|
||||
const existing = this.compareState();
|
||||
|
||||
if (existing.some(item => item.product.itemID === product.itemID)) {
|
||||
return { added: false, count: existing.length };
|
||||
}
|
||||
|
||||
if (existing.length >= maxItems) {
|
||||
return { added: false, reason: 'limit', count: existing.length };
|
||||
}
|
||||
|
||||
const next = [{ product, addedAt: new Date().toISOString() }, ...existing];
|
||||
this.compareState.set(next);
|
||||
this.repository.saveComparedProducts(next);
|
||||
|
||||
return { added: true, count: next.length };
|
||||
}
|
||||
|
||||
removeFromCompare(productId: number): void {
|
||||
const next = this.compareState().filter(item => item.product.itemID !== productId);
|
||||
this.compareState.set(next);
|
||||
this.repository.saveComparedProducts(next);
|
||||
}
|
||||
|
||||
clearCompare(): void {
|
||||
this.compareState.set([]);
|
||||
this.repository.saveComparedProducts([]);
|
||||
}
|
||||
|
||||
trackRecentlyViewed(product: Product, maxItems: number): void {
|
||||
const existing = this.recentlyViewedState().filter(item => item.product.itemID !== product.itemID);
|
||||
const next = [{ product, viewedAt: new Date().toISOString() }, ...existing].slice(0, Math.max(1, maxItems));
|
||||
|
||||
this.recentlyViewedState.set(next);
|
||||
this.repository.saveRecentlyViewed(next);
|
||||
}
|
||||
|
||||
clearRecentlyViewed(): void {
|
||||
this.recentlyViewedState.set([]);
|
||||
this.repository.saveRecentlyViewed([]);
|
||||
}
|
||||
|
||||
saveSearch(input: Omit<SavedSearch, 'id' | 'createdAt'>, maxItems: number): SavedSearch {
|
||||
const normalizedQuery = input.query.trim();
|
||||
const existing = this.savedSearchesState();
|
||||
const duplicateIndex = existing.findIndex(entry =>
|
||||
entry.query.toLowerCase() === normalizedQuery.toLowerCase() &&
|
||||
entry.sort === input.sort &&
|
||||
(entry.categoryId ?? null) === (input.categoryId ?? null)
|
||||
);
|
||||
|
||||
if (duplicateIndex >= 0) {
|
||||
return existing[duplicateIndex];
|
||||
}
|
||||
|
||||
const nextEntry: SavedSearch = {
|
||||
...input,
|
||||
query: normalizedQuery,
|
||||
id: `saved-search-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||
createdAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
const next = [nextEntry, ...existing].slice(0, Math.max(1, maxItems));
|
||||
this.savedSearchesState.set(next);
|
||||
this.repository.saveSavedSearches(next);
|
||||
|
||||
return nextEntry;
|
||||
}
|
||||
|
||||
removeSavedSearch(id: string): void {
|
||||
const next = this.savedSearchesState().filter(item => item.id !== id);
|
||||
this.savedSearchesState.set(next);
|
||||
this.repository.saveSavedSearches(next);
|
||||
}
|
||||
|
||||
saveContinueBrowsing(state: ContinueBrowsingState): void {
|
||||
this.repository.saveContinueBrowsing(state);
|
||||
}
|
||||
|
||||
getContinueBrowsing(routeKey: string): ContinueBrowsingState | null {
|
||||
return this.repository.getContinueBrowsing(routeKey);
|
||||
}
|
||||
|
||||
clearContinueBrowsing(routeKey: string): void {
|
||||
this.repository.clearContinueBrowsing(routeKey);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user