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