feat(ux): implement sprint 11 user experience module
This commit is contained in:
@@ -38,6 +38,24 @@
|
||||
|
||||
<!-- Right Actions -->
|
||||
<div class="dexar-actions">
|
||||
@if (userExperienceConfig.wishlist.enabled) {
|
||||
<button type="button" class="dexar-ux-btn" (click)="navigateToWishlist()" [attr.aria-label]="'header.wishlist' | translate">
|
||||
<span class="dexar-ux-icon">♥</span>
|
||||
@if (userExperienceConfig.wishlist.headerBadgeEnabled && wishlistCount() > 0) {
|
||||
<span class="dexar-ux-badge">{{ wishlistCount() }}</span>
|
||||
}
|
||||
</button>
|
||||
}
|
||||
|
||||
@if (userExperienceConfig.compare.enabled) {
|
||||
<button type="button" class="dexar-ux-btn" (click)="navigateToCompare()" [attr.aria-label]="'header.compare' | translate">
|
||||
<span class="dexar-ux-icon">⇄</span>
|
||||
@if (compareCount() > 0) {
|
||||
<span class="dexar-ux-badge">{{ compareCount() }}</span>
|
||||
}
|
||||
</button>
|
||||
}
|
||||
|
||||
<!-- Cart Button -->
|
||||
<a [routerLink]="'/cart' | langRoute" routerLinkActive="dexar-cart-active" class="dexar-cart-btn" (click)="closeMenu()">
|
||||
<span class="dexar-cart-icon">
|
||||
|
||||
@@ -632,6 +632,50 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.dexar-ux-btn {
|
||||
position: relative;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border: 1px solid #d3dad9;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.84);
|
||||
color: #1e3c38;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: transform 0.18s ease, border-color 0.2s ease, background 0.2s ease;
|
||||
}
|
||||
|
||||
.dexar-ux-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
border-color: #497671;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.dexar-ux-icon {
|
||||
font-size: 15px;
|
||||
line-height: 1;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.dexar-ux-badge {
|
||||
position: absolute;
|
||||
right: -6px;
|
||||
top: -6px;
|
||||
min-width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 999px;
|
||||
background: #497671;
|
||||
color: #ffffff;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.dexar-cart-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -8,6 +8,9 @@ import { RegionSelectorComponent } from '../region-selector/region-selector.comp
|
||||
import { LangRoutePipe } from '../../pipes/lang-route.pipe';
|
||||
import { TranslatePipe } from '../../i18n/translate.pipe';
|
||||
import { UiRuntimeFacade } from '../../facades/runtime/ui-runtime.facade';
|
||||
import { UserExperienceFacade } from '../../facades/platform/user-experience.facade';
|
||||
import { ConfigService } from '../../core/config/config.service';
|
||||
import { DEFAULT_USER_EXPERIENCE_CONFIG } from '../../shared/models/config';
|
||||
|
||||
@Component({
|
||||
selector: 'app-header',
|
||||
@@ -25,6 +28,12 @@ export class HeaderComponent {
|
||||
private document = inject(DOCUMENT);
|
||||
private langService = inject(LanguageService);
|
||||
private uiRuntime = inject(UiRuntimeFacade);
|
||||
private uxFacade = inject(UserExperienceFacade);
|
||||
private configService = inject(ConfigService);
|
||||
|
||||
readonly wishlistCount = this.uxFacade.wishlistCount;
|
||||
readonly compareCount = this.uxFacade.compareCount;
|
||||
readonly userExperienceConfig = this.resolveUserExperienceConfig();
|
||||
|
||||
constructor(private cartService: CartService, private router: Router) {
|
||||
this.cartItemCount = this.cartService.itemCount;
|
||||
@@ -89,6 +98,18 @@ export class HeaderComponent {
|
||||
});
|
||||
}
|
||||
|
||||
navigateToWishlist(): void {
|
||||
this.closeMenu();
|
||||
const lang = this.langService.currentLanguage();
|
||||
this.router.navigate([`/${lang}/wishlist`]);
|
||||
}
|
||||
|
||||
navigateToCompare(): void {
|
||||
this.closeMenu();
|
||||
const lang = this.langService.currentLanguage();
|
||||
this.router.navigate([`/${lang}/compare`]);
|
||||
}
|
||||
|
||||
formatCartTotal(total: number): string {
|
||||
const locale = this.langService.currentLanguage() === 'en'
|
||||
? 'en-US'
|
||||
@@ -104,4 +125,21 @@ export class HeaderComponent {
|
||||
|
||||
return `${amount} ${currencySymbol}`;
|
||||
}
|
||||
|
||||
private resolveUserExperienceConfig() {
|
||||
const raw = (this.configService.getBootstrapSnapshot() as any)?.userExperience ?? {};
|
||||
|
||||
return {
|
||||
...DEFAULT_USER_EXPERIENCE_CONFIG,
|
||||
...raw,
|
||||
wishlist: {
|
||||
...DEFAULT_USER_EXPERIENCE_CONFIG.wishlist,
|
||||
...(raw.wishlist ?? {})
|
||||
},
|
||||
compare: {
|
||||
...DEFAULT_USER_EXPERIENCE_CONFIG.compare,
|
||||
...(raw.compare ?? {})
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,16 +9,19 @@
|
||||
<span class="stock-badge" [class.out]="item.remainings === 'out'">{{ item.remainings }}</span>
|
||||
}
|
||||
|
||||
@if (showFavoritePlaceholder || showComparePlaceholder || showQuickViewPlaceholder) {
|
||||
@if (showFavoriteControl() || showCompareControl() || showShareAction || showQuickViewPlaceholder) {
|
||||
<div class="product-actions-overlay">
|
||||
@if (showFavoritePlaceholder) {
|
||||
<button type="button" (click)="onFavoritePlaceholder($event)">Fav</button>
|
||||
@if (showFavoriteControl()) {
|
||||
<button type="button" class="product-action-btn product-action-favorite" [class.active]="isFavorite" (click)="onFavoritePlaceholder($event)">❤</button>
|
||||
}
|
||||
@if (showComparePlaceholder) {
|
||||
<button type="button" (click)="onComparePlaceholder($event)">Compare</button>
|
||||
@if (showCompareControl()) {
|
||||
<button type="button" class="product-action-btn" [class.active]="isCompared" (click)="onComparePlaceholder($event)">⇄</button>
|
||||
}
|
||||
@if (showShareAction) {
|
||||
<button type="button" class="product-action-btn" (click)="onShare($event)">↗</button>
|
||||
}
|
||||
@if (showQuickViewPlaceholder) {
|
||||
<button type="button" (click)="onQuickViewPlaceholder($event)">Quick</button>
|
||||
<button type="button" class="product-action-btn" (click)="onQuickViewPlaceholder($event)">○</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -90,16 +90,46 @@
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.product-actions-overlay button {
|
||||
min-height: 24px;
|
||||
.product-action-btn {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border: 1px solid rgba(30, 60, 56, 0.15);
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
color: #1e3c38;
|
||||
font-size: 0.72rem;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 700;
|
||||
padding: 0 8px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: transform 0.18s ease, border-color 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
|
||||
.product-action-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
border-color: #497671;
|
||||
}
|
||||
|
||||
.product-action-btn.active {
|
||||
border-color: #497671;
|
||||
color: #497671;
|
||||
}
|
||||
|
||||
.product-action-favorite.active {
|
||||
animation: favorite-pulse 280ms ease-out;
|
||||
}
|
||||
|
||||
@keyframes favorite-pulse {
|
||||
0% {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
60% {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.product-badges-overlay {
|
||||
|
||||
@@ -28,6 +28,11 @@ export class ProductCardComponent {
|
||||
@Input() showFavoritePlaceholder = false;
|
||||
@Input() showComparePlaceholder = false;
|
||||
@Input() showQuickViewPlaceholder = false;
|
||||
@Input() showFavoriteAction = false;
|
||||
@Input() showCompareAction = false;
|
||||
@Input() showShareAction = false;
|
||||
@Input() isFavorite = false;
|
||||
@Input() isCompared = false;
|
||||
|
||||
@Output() addToCart = new EventEmitter<{ itemID: number; event: Event }>();
|
||||
@Output() preview = new EventEmitter<number>();
|
||||
@@ -35,6 +40,9 @@ export class ProductCardComponent {
|
||||
@Output() favoritePlaceholder = new EventEmitter<number>();
|
||||
@Output() comparePlaceholder = new EventEmitter<number>();
|
||||
@Output() quickViewPlaceholder = new EventEmitter<number>();
|
||||
@Output() favoriteToggled = new EventEmitter<number>();
|
||||
@Output() compareToggled = new EventEmitter<number>();
|
||||
@Output() shareRequested = new EventEmitter<number>();
|
||||
|
||||
readonly getMainImage = getMainImage;
|
||||
readonly getDiscountedPrice = getDiscountedPrice;
|
||||
@@ -52,12 +60,14 @@ export class ProductCardComponent {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.favoritePlaceholder.emit(this.item.itemID);
|
||||
this.favoriteToggled.emit(this.item.itemID);
|
||||
}
|
||||
|
||||
onComparePlaceholder(event: Event): void {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.comparePlaceholder.emit(this.item.itemID);
|
||||
this.compareToggled.emit(this.item.itemID);
|
||||
}
|
||||
|
||||
onQuickViewPlaceholder(event: Event): void {
|
||||
@@ -65,4 +75,18 @@ export class ProductCardComponent {
|
||||
event.stopPropagation();
|
||||
this.quickViewPlaceholder.emit(this.item.itemID);
|
||||
}
|
||||
|
||||
onShare(event: Event): void {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.shareRequested.emit(this.item.itemID);
|
||||
}
|
||||
|
||||
showFavoriteControl(): boolean {
|
||||
return this.showFavoriteAction || this.showFavoritePlaceholder;
|
||||
}
|
||||
|
||||
showCompareControl(): boolean {
|
||||
return this.showCompareAction || this.showComparePlaceholder;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user