feat: implement cart overlay and dropdown with styling for improved user experience
This commit is contained in:
@@ -38,6 +38,7 @@
|
|||||||
[quantity]="cartQuantity()"
|
[quantity]="cartQuantity()"
|
||||||
ariaLabel="Carrito de compras"
|
ariaLabel="Carrito de compras"
|
||||||
title="Carrito"
|
title="Carrito"
|
||||||
|
(click)="cartClick.emit()"
|
||||||
/>
|
/>
|
||||||
<app-icon-button
|
<app-icon-button
|
||||||
variant="user"
|
variant="user"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Component, input } from '@angular/core';
|
import { Component, input, output } from '@angular/core';
|
||||||
import { CartIconComponent } from '../../../../shared/components/cart-icon/cart-icon.component';
|
import { CartIconComponent } from '../../../../shared/components/cart-icon/cart-icon.component';
|
||||||
import { IconButtonComponent } from '../../../../shared/components/icon-button/icon-button.component';
|
import { IconButtonComponent } from '../../../../shared/components/icon-button/icon-button.component';
|
||||||
|
|
||||||
@@ -11,4 +11,5 @@ import { IconButtonComponent } from '../../../../shared/components/icon-button/i
|
|||||||
export class StoreHeaderComponent {
|
export class StoreHeaderComponent {
|
||||||
readonly logoUrl = input<string | null>(null);
|
readonly logoUrl = input<string | null>(null);
|
||||||
readonly cartQuantity = input<number>(0);
|
readonly cartQuantity = input<number>(0);
|
||||||
|
readonly cartClick = output<void>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,27 @@
|
|||||||
<div class="store-layout d-flex flex-column flex-grow-1">
|
<div class="store-layout d-flex flex-column flex-grow-1">
|
||||||
<app-store-header [cartQuantity]="cartQuantity()" [logoUrl]="tenant()?.header_logo ?? null" />
|
<app-store-header
|
||||||
|
[cartQuantity]="cartQuantity()"
|
||||||
|
[logoUrl]="tenant()?.header_logo ?? null"
|
||||||
|
(cartClick)="isCartOpen.set(!isCartOpen())"
|
||||||
|
/>
|
||||||
|
|
||||||
|
@if (isCartOpen()) {
|
||||||
|
<div class="store-layout__cart-overlay" (click)="isCartOpen.set(false)"></div>
|
||||||
|
<div class="store-layout__cart-dropdown card border-0 shadow-lg">
|
||||||
|
<app-cart
|
||||||
|
[showClose]="true"
|
||||||
|
[items]="mappedCartItems()"
|
||||||
|
[subtotal]="cartSubtotal()"
|
||||||
|
[discount]="cartDiscount()"
|
||||||
|
[total]="cartTotal()"
|
||||||
|
[backgroundColor]="'#ffffff'"
|
||||||
|
(closed)="isCartOpen.set(false)"
|
||||||
|
>
|
||||||
|
<app-button variant="secondary" class="flex-grow-1">Seguir comprando</app-button>
|
||||||
|
<app-button variant="primary" class="flex-grow-1">Comprar</app-button>
|
||||||
|
</app-cart>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<main class="flex-grow-1 d-block">
|
<main class="flex-grow-1 d-block">
|
||||||
<router-outlet />
|
<router-outlet />
|
||||||
|
|||||||
@@ -10,3 +10,53 @@
|
|||||||
#f6f6f6;
|
#f6f6f6;
|
||||||
color: #202020;
|
color: #202020;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.store-layout {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-layout__cart-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
z-index: 1040;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-layout__cart-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: 76px;
|
||||||
|
right: calc((100% - 1320px) / 2 + 1.5rem);
|
||||||
|
|
||||||
|
height: 520px;
|
||||||
|
max-height: calc(100vh - 120px);
|
||||||
|
z-index: 1050;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||||
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.08);
|
||||||
|
overflow: hidden;
|
||||||
|
animation: store-layout-slide-down 0.2s ease-out;
|
||||||
|
|
||||||
|
@media (max-width: 1400px) {
|
||||||
|
right: 1.5rem;
|
||||||
|
}
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
right: 1rem;
|
||||||
|
left: 1rem;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes store-layout-slide-down {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-8px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import { Component, computed, inject, OnInit } from '@angular/core';
|
import { Component, computed, inject, OnInit, signal } from '@angular/core';
|
||||||
import { RouterOutlet } from '@angular/router';
|
import { RouterOutlet } from '@angular/router';
|
||||||
import { TenantService } from '../../services/tenant.service';
|
import { TenantService } from '../../services/tenant.service';
|
||||||
import { CartService } from '../../services/cart/cart.service';
|
import { CartService } from '../../services/cart/cart.service';
|
||||||
import { StoreFooterComponent, StoreFooterSection, StoreSocialLink } from './store-footer/store-footer.component';
|
import { StoreFooterComponent, StoreFooterSection, StoreSocialLink } from './store-footer/store-footer.component';
|
||||||
import { StoreHeaderComponent } from './store-header/store-header.component';
|
import { StoreHeaderComponent } from './store-header/store-header.component';
|
||||||
|
import { CartComponent, CartItemMock } from '../../../shared/components/cart/cart.component';
|
||||||
|
import { ButtonComponent } from '../../../shared/components/button/button.component';
|
||||||
|
import { CartItem } from '../../services/cart/cart.interface';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-store-layout',
|
selector: 'app-store-layout',
|
||||||
imports: [RouterOutlet, StoreHeaderComponent, StoreFooterComponent],
|
imports: [RouterOutlet, StoreHeaderComponent, StoreFooterComponent, CartComponent, ButtonComponent],
|
||||||
templateUrl: './store-layout.component.html',
|
templateUrl: './store-layout.component.html',
|
||||||
styleUrl: './store-layout.component.scss'
|
styleUrl: './store-layout.component.scss'
|
||||||
})
|
})
|
||||||
@@ -15,6 +18,52 @@ export class StoreLayoutComponent implements OnInit {
|
|||||||
private readonly tenantService = inject(TenantService);
|
private readonly tenantService = inject(TenantService);
|
||||||
private readonly cartService = inject(CartService);
|
private readonly cartService = inject(CartService);
|
||||||
|
|
||||||
|
protected readonly isCartOpen = signal(false);
|
||||||
|
|
||||||
|
protected readonly cartSubtotal = computed(() => {
|
||||||
|
const cart = this.cartService.cart();
|
||||||
|
return cart ? parseFloat(cart.subtotal) : 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
protected readonly cartDiscount = computed(() => 0);
|
||||||
|
|
||||||
|
protected readonly cartTotal = computed(() => this.cartSubtotal() - this.cartDiscount());
|
||||||
|
|
||||||
|
protected readonly mappedCartItems = computed<CartItemMock[]>(() => {
|
||||||
|
const cart = this.cartService.cart();
|
||||||
|
if (!cart || !cart.items) return [];
|
||||||
|
return cart.items.map((item) => this.mapCartItemToMock(item));
|
||||||
|
});
|
||||||
|
|
||||||
|
private mapCartItemToMock(item: CartItem): CartItemMock {
|
||||||
|
const fullName = item.product?.nombre ?? '';
|
||||||
|
let product = fullName;
|
||||||
|
let attributes: { label: string; value: string }[] = [];
|
||||||
|
|
||||||
|
const match = fullName.match(/^(.*?)\s*\((.*?)\)$/);
|
||||||
|
if (match) {
|
||||||
|
product = match[1];
|
||||||
|
const attributesString = match[2];
|
||||||
|
attributes = attributesString.split(',').map((attr) => {
|
||||||
|
const parts = attr.split(':');
|
||||||
|
if (parts.length === 2) {
|
||||||
|
return { label: parts[0].trim(), value: parts[1].trim() };
|
||||||
|
}
|
||||||
|
return { label: '', value: attr.trim() };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
imageUrl: item.product?.imagen ?? null,
|
||||||
|
product,
|
||||||
|
originalPrice: null,
|
||||||
|
discountedPrice: parseFloat(item.precio_unitario),
|
||||||
|
discountPercentage: null,
|
||||||
|
attributes,
|
||||||
|
quantity: item.cantidad
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
protected readonly currentYear = new Date().getFullYear();
|
protected readonly currentYear = new Date().getFullYear();
|
||||||
protected readonly tenant = this.tenantService.tenant;
|
protected readonly tenant = this.tenantService.tenant;
|
||||||
|
|
||||||
|
|||||||
@@ -22,8 +22,16 @@
|
|||||||
.cart-item-media {
|
.cart-item-media {
|
||||||
width: var(--item-media-width);
|
width: var(--item-media-width);
|
||||||
height: var(--item-media-height);
|
height: var(--item-media-height);
|
||||||
border-radius: 6px 0 0 6px;
|
border-radius: 6px;
|
||||||
background-color: #d9d9d9;
|
background-color: #ffffff;
|
||||||
|
border: 1px solid #eaeaea;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.cart-item-discount-badge {
|
.cart-item-discount-badge {
|
||||||
|
|||||||
@@ -38,6 +38,10 @@
|
|||||||
<span class="cart-total-text">TOTAL:</span>
|
<span class="cart-total-text">TOTAL:</span>
|
||||||
<span class="cart-total-text">{{ formattedTotal() }}</span>
|
<span class="cart-total-text">{{ formattedTotal() }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="cart-actions mt-3">
|
||||||
|
<ng-content />
|
||||||
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 370px;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,3 +58,8 @@
|
|||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cart-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user