From dafaa85480de89df429430c2fee277f301d71f08 Mon Sep 17 00:00:00 2001 From: ncoronel Date: Mon, 20 Jul 2026 17:11:06 -0300 Subject: [PATCH] feat(cart): implement add to cart functionality and display empty cart message --- .../store-home-page.component.html | 1 + .../store-home-page.component.spec.ts | 34 ++++++++++++++++ .../store-home-page.component.ts | 21 ++++++++++ .../cart-item/cart-item.component.html | 21 +++++----- .../cart-item/cart-item.component.scss | 12 +++--- .../components/cart/cart.component.html | 38 ++++++++++-------- .../components/cart/cart.component.scss | 11 ++++- .../components/cart/cart.component.spec.ts | 40 +++++++++++++++++++ 8 files changed, 142 insertions(+), 36 deletions(-) diff --git a/src/app/features/store/pages/store-home-page/store-home-page.component.html b/src/app/features/store/pages/store-home-page/store-home-page.component.html index 6ecd0f1..f3e0759 100644 --- a/src/app/features/store/pages/store-home-page/store-home-page.component.html +++ b/src/app/features/store/pages/store-home-page/store-home-page.component.html @@ -21,6 +21,7 @@ [items]="group.items" [loading]="isGroupLoading(group.id)" (buy)="onBuyProduct($event)" + (addToCart)="onAddToCart($event)" (pageChange)="onPageChange(group.id, $event)" /> diff --git a/src/app/features/store/pages/store-home-page/store-home-page.component.spec.ts b/src/app/features/store/pages/store-home-page/store-home-page.component.spec.ts index cf37162..968320f 100644 --- a/src/app/features/store/pages/store-home-page/store-home-page.component.spec.ts +++ b/src/app/features/store/pages/store-home-page/store-home-page.component.spec.ts @@ -1,14 +1,18 @@ import { TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; import { ActivatedRoute } from '@angular/router'; import { Subject, of } from 'rxjs'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { ApiPaginatedResponse } from '../../../../core/services/api-paginated-response.interface'; +import { CartService } from '../../../../core/services/cart/cart.service'; import { CatalogFeaturedGroup, CatalogFeaturedItem, } from '../../../../core/services/catalog/catalog.interface'; import { CatalogService } from '../../../../core/services/catalog/catalog.service'; +import { ToastService } from '../../../../core/services/toast.service'; +import { ProductListComponent } from '../../../../shared/components/product-list/product-list.component'; import { StoreHomePageComponent } from './store-home-page.component'; import { STORE_HOME_PRODUCTS_ERROR_MESSAGE, @@ -213,4 +217,34 @@ describe('StoreHomePageComponent', () => { 'No pudimos cargar los productos en este momento.', ); }); + + it('adds a product-list cart event to the cart', async () => { + const catalogServiceStub = { getCatalog: vi.fn(), getFeaturedGroupItems: vi.fn() }; + const cartServiceStub = { + addItem: vi.fn().mockReturnValue( + of({ data: {}, message: 'Producto agregado correctamente' }), + ), + }; + const toastServiceStub = { success: vi.fn(), danger: vi.fn() }; + + await TestBed.configureTestingModule({ + imports: [StoreHomePageComponent], + providers: [ + provideActivatedRoute({ response: createCatalog(), error: null }), + { provide: CatalogService, useValue: catalogServiceStub }, + { provide: CartService, useValue: cartServiceStub }, + { provide: ToastService, useValue: toastServiceStub }, + ], + }).compileComponents(); + + const fixture = TestBed.createComponent(StoreHomePageComponent); + fixture.detectChanges(); + const productList = fixture.debugElement.query(By.directive(ProductListComponent)) + .componentInstance as ProductListComponent; + + productList.addToCart.emit({ product: pageOneItems[0], quantity: 3, variant: 12 }); + + expect(cartServiceStub.addItem).toHaveBeenCalledWith(1, 12, 3); + expect(toastServiceStub.success).toHaveBeenCalledWith('Producto agregado correctamente'); + }); }); diff --git a/src/app/features/store/pages/store-home-page/store-home-page.component.ts b/src/app/features/store/pages/store-home-page/store-home-page.component.ts index 61b0e4e..fa2a15f 100644 --- a/src/app/features/store/pages/store-home-page/store-home-page.component.ts +++ b/src/app/features/store/pages/store-home-page/store-home-page.component.ts @@ -6,14 +6,18 @@ import { inject, signal, } from '@angular/core'; +import { HttpErrorResponse } from '@angular/common/http'; import { ActivatedRoute, Router } from '@angular/router'; import { Subscription } from 'rxjs'; +import { CartService } from '../../../../core/services/cart/cart.service'; import { CatalogFeaturedGroup } from '../../../../core/services/catalog/catalog.interface'; import { CatalogService } from '../../../../core/services/catalog/catalog.service'; import { TenantService } from '../../../../core/services/tenant.service'; +import { ToastService } from '../../../../core/services/toast.service'; import { ProductListComponent, + ProductListCartEvent, ProductListItem, } from '../../../../shared/components/product-list/product-list.component'; import { HeroBannerComponent } from '../../../../shared/components/hero-banner/hero-banner.component'; @@ -31,10 +35,12 @@ import { changeDetection: ChangeDetectionStrategy.OnPush, }) export class StoreHomePageComponent implements OnInit, OnDestroy { + private readonly cartService = inject(CartService); private readonly catalogService = inject(CatalogService); private readonly route = inject(ActivatedRoute); private readonly router = inject(Router); private readonly tenantService = inject(TenantService); + private readonly toastService = inject(ToastService); protected readonly tenant = this.tenantService.tenant; protected readonly catalog = signal([]); @@ -105,6 +111,21 @@ export class StoreHomePageComponent implements OnInit, OnDestroy { this.router.navigate(['/producto', product.id]); } + protected onAddToCart(event: ProductListCartEvent): void { + this.cartService + .addItem(event.product.id, event.variant ?? null, event.quantity) + .subscribe({ + next: (response) => { + this.toastService.success(response.message || 'Producto agregado al carrito'); + }, + error: (error: HttpErrorResponse) => { + const message = + error.error?.message || 'No se pudo agregar el producto al carrito.'; + this.toastService.danger(message); + }, + }); + } + private loadCatalog(): void { this.loading.set(true); this.error.set(null); diff --git a/src/app/shared/components/cart-item/cart-item.component.html b/src/app/shared/components/cart-item/cart-item.component.html index e33ff77..78aadca 100644 --- a/src/app/shared/components/cart-item/cart-item.component.html +++ b/src/app/shared/components/cart-item/cart-item.component.html @@ -1,15 +1,16 @@ -
-
- @if (discountPercentage() && discountPercentage()! > 0) { - -{{ discountPercentage() }}% - } +
+ @if (imageUrl()) { +
+ @if (discountPercentage() && discountPercentage()! > 0) { + -{{ discountPercentage() }}% + } - @if (imageUrl()) { - } @else { - - } -
+
+ }
diff --git a/src/app/shared/components/cart-item/cart-item.component.scss b/src/app/shared/components/cart-item/cart-item.component.scss index 2a35637..2a73769 100644 --- a/src/app/shared/components/cart-item/cart-item.component.scss +++ b/src/app/shared/components/cart-item/cart-item.component.scss @@ -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; } diff --git a/src/app/shared/components/cart/cart.component.html b/src/app/shared/components/cart/cart.component.html index 2998087..468cd11 100644 --- a/src/app/shared/components/cart/cart.component.html +++ b/src/app/shared/components/cart/cart.component.html @@ -34,27 +34,31 @@ (quantityChange)="onItemQuantityChange(idx, $event)" (remove)="onItemRemove(idx)" /> + } @empty { +

El carrito está vacío

}
- + } diff --git a/src/app/shared/components/cart/cart.component.scss b/src/app/shared/components/cart/cart.component.scss index e3231cb..b80a2c0 100644 --- a/src/app/shared/components/cart/cart.component.scss +++ b/src/app/shared/components/cart/cart.component.scss @@ -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; } diff --git a/src/app/shared/components/cart/cart.component.spec.ts b/src/app/shared/components/cart/cart.component.spec.ts index 519fdcd..fe8ec93 100644 --- a/src/app/shared/components/cart/cart.component.spec.ts +++ b/src/app/shared/components/cart/cart.component.spec.ts @@ -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({