feat(cart): implement add to cart functionality and display empty cart message
This commit is contained in:
@@ -1,15 +1,16 @@
|
||||
<article class="w-100 p-3 rounded-0 cart-item">
|
||||
<div class="position-relative overflow-hidden cart-item-media">
|
||||
@if (discountPercentage() && discountPercentage()! > 0) {
|
||||
<span class="position-absolute top-0 start-0 z-1 rounded-0 cart-item-discount-badge">-{{ discountPercentage() }}%</span>
|
||||
}
|
||||
<article
|
||||
class="w-100 p-3 rounded-0 cart-item"
|
||||
[class.cart-item-with-media]="imageUrl()"
|
||||
>
|
||||
@if (imageUrl()) {
|
||||
<div class="position-relative overflow-hidden cart-item-media">
|
||||
@if (discountPercentage() && discountPercentage()! > 0) {
|
||||
<span class="position-absolute top-0 start-0 z-1 rounded-0 cart-item-discount-badge">-{{ discountPercentage() }}%</span>
|
||||
}
|
||||
|
||||
@if (imageUrl()) {
|
||||
<img class="w-100 h-100 object-fit-cover d-block" [src]="imageUrl()" [alt]="product()" />
|
||||
} @else {
|
||||
<div class="w-100 h-100 cart-item-placeholder" aria-hidden="true"></div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="d-grid min-w-0 cart-item-content">
|
||||
<div class="d-grid align-items-start cart-item-top-row">
|
||||
|
||||
@@ -29,11 +29,15 @@
|
||||
|
||||
.cart-item {
|
||||
display: grid;
|
||||
grid-template-columns: var(--item-media-size) minmax(0, 1fr);
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 0.75rem;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.cart-item-with-media {
|
||||
grid-template-columns: var(--item-media-size) minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.cart-item-media {
|
||||
width: var(--item-media-size);
|
||||
aspect-ratio: 1 / 1;
|
||||
@@ -63,12 +67,6 @@
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.cart-item-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, #d2d2d2, #ececec);
|
||||
}
|
||||
|
||||
.cart-item-content {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
@@ -34,27 +34,31 @@
|
||||
(quantityChange)="onItemQuantityChange(idx, $event)"
|
||||
(remove)="onItemRemove(idx)"
|
||||
/>
|
||||
} @empty {
|
||||
<p class="cart-empty-message" role="status">El carrito está vacío</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
<footer class="d-grid gap-1 bg-transparent cart-footer">
|
||||
<div class="d-flex align-items-baseline justify-content-between gap-3">
|
||||
<span class="cart-summary-text-muted">Subtotal:</span>
|
||||
<span class="cart-summary-text-muted-bold">{{ formattedSubtotal() }}</span>
|
||||
</div>
|
||||
@if (items().length > 0) {
|
||||
<footer class="d-grid gap-1 bg-transparent cart-footer">
|
||||
<div class="d-flex align-items-baseline justify-content-between gap-3">
|
||||
<span class="cart-summary-text-muted">Subtotal:</span>
|
||||
<span class="cart-summary-text-muted-bold">{{ formattedSubtotal() }}</span>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-baseline justify-content-between gap-3">
|
||||
<span class="cart-summary-text-muted">Descuento:</span>
|
||||
<span class="cart-summary-text-muted-bold">{{ formattedDiscount() }}</span>
|
||||
</div>
|
||||
<div class="d-flex align-items-baseline justify-content-between gap-3">
|
||||
<span class="cart-summary-text-muted">Descuento:</span>
|
||||
<span class="cart-summary-text-muted-bold">{{ formattedDiscount() }}</span>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-baseline justify-content-between gap-3 mt-1">
|
||||
<span class="cart-total-text">TOTAL:</span>
|
||||
<span class="cart-total-text">{{ formattedTotal() }}</span>
|
||||
</div>
|
||||
<div class="d-flex align-items-baseline justify-content-between gap-3 mt-1">
|
||||
<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>
|
||||
<div class="cart-actions mt-3">
|
||||
<ng-content />
|
||||
</div>
|
||||
</footer>
|
||||
}
|
||||
</section>
|
||||
|
||||
@@ -31,8 +31,6 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
@@ -43,6 +41,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
.cart-empty-message {
|
||||
align-self: center;
|
||||
margin: 0;
|
||||
padding: 24px;
|
||||
color: #a0a0a0;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.cart-footer {
|
||||
padding: 8px 22px 14px 24px;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,44 @@ describe('CartComponent', () => {
|
||||
TestBed.resetTestingModule();
|
||||
});
|
||||
|
||||
it('shows an empty cart message when there are no items', async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [CartComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: CartService,
|
||||
useValue: {
|
||||
cart: signal(null).asReadonly(),
|
||||
updateItemQuantity: vi.fn(),
|
||||
removeItem: vi.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: ModalService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: ToastService,
|
||||
useValue: {
|
||||
success: vi.fn(),
|
||||
info: vi.fn(),
|
||||
danger: vi.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
const fixture = TestBed.createComponent(CartComponent);
|
||||
fixture.detectChanges();
|
||||
|
||||
const emptyMessage = fixture.debugElement.query(By.css('.cart-empty-message'));
|
||||
|
||||
expect(emptyMessage).not.toBeNull();
|
||||
expect(emptyMessage.nativeElement.textContent.trim()).toBe('El carrito está vacío');
|
||||
expect(fixture.debugElement.query(By.css('app-cart-item'))).toBeNull();
|
||||
expect(fixture.debugElement.query(By.css('.cart-footer'))).toBeNull();
|
||||
});
|
||||
|
||||
it('opens a confirm delete modal before removing an item', async () => {
|
||||
const removeItem = vi.fn().mockReturnValue(of({ message: 'Producto eliminado.' }));
|
||||
const openConfirmDelete = vi.fn().mockReturnValue(of(true));
|
||||
@@ -71,6 +109,8 @@ describe('CartComponent', () => {
|
||||
]);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.debugElement.query(By.css('.cart-item-media'))).toBeNull();
|
||||
|
||||
fixture.debugElement.query(By.css('app-cart-item')).triggerEventHandler('remove');
|
||||
|
||||
expect(openConfirmDelete).toHaveBeenCalledWith({
|
||||
|
||||
Reference in New Issue
Block a user