feat: implement cart overlay and dropdown with styling for improved user experience

This commit is contained in:
2026-07-02 09:59:24 -03:00
parent 5ac6704300
commit f2cc42e0cc
8 changed files with 146 additions and 7 deletions

View File

@@ -38,6 +38,7 @@
[quantity]="cartQuantity()"
ariaLabel="Carrito de compras"
title="Carrito"
(click)="cartClick.emit()"
/>
<app-icon-button
variant="user"

View File

@@ -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 { IconButtonComponent } from '../../../../shared/components/icon-button/icon-button.component';
@@ -11,4 +11,5 @@ import { IconButtonComponent } from '../../../../shared/components/icon-button/i
export class StoreHeaderComponent {
readonly logoUrl = input<string | null>(null);
readonly cartQuantity = input<number>(0);
readonly cartClick = output<void>();
}

View File

@@ -1,5 +1,27 @@
<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">
<router-outlet />

View File

@@ -10,3 +10,53 @@
#f6f6f6;
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);
}
}

View File

@@ -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 { TenantService } from '../../services/tenant.service';
import { CartService } from '../../services/cart/cart.service';
import { StoreFooterComponent, StoreFooterSection, StoreSocialLink } from './store-footer/store-footer.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({
selector: 'app-store-layout',
imports: [RouterOutlet, StoreHeaderComponent, StoreFooterComponent],
imports: [RouterOutlet, StoreHeaderComponent, StoreFooterComponent, CartComponent, ButtonComponent],
templateUrl: './store-layout.component.html',
styleUrl: './store-layout.component.scss'
})
@@ -15,6 +18,52 @@ export class StoreLayoutComponent implements OnInit {
private readonly tenantService = inject(TenantService);
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 tenant = this.tenantService.tenant;

View File

@@ -22,8 +22,16 @@
.cart-item-media {
width: var(--item-media-width);
height: var(--item-media-height);
border-radius: 6px 0 0 6px;
background-color: #d9d9d9;
border-radius: 6px;
background-color: #ffffff;
border: 1px solid #eaeaea;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: contain;
}
}
.cart-item-discount-badge {

View File

@@ -38,6 +38,10 @@
<span class="cart-total-text">TOTAL:</span>
<span class="cart-total-text">{{ formattedTotal() }}</span>
</div>
<div class="cart-actions mt-3">
<ng-content />
</div>
</footer>
</section>

View File

@@ -1,7 +1,6 @@
:host {
display: block;
width: 100%;
max-width: 370px;
height: 100%;
}
@@ -59,3 +58,8 @@
font-size: 15px;
font-weight: 700;
}
.cart-actions {
display: flex;
gap: 12px;
}