From af406c215a116f79e878f818b5a01e87943b2cd2 Mon Sep 17 00:00:00 2001 From: ncoronel Date: Wed, 1 Jul 2026 16:51:23 -0300 Subject: [PATCH] feat: implement cart component with mock data and styling for improved cart functionality --- .../reutilizables-test-page.component.html | 29 ++++ .../reutilizables-test-page.component.scss | 19 +++ .../reutilizables-test-page.component.ts | 65 ++++++- .../cart-item/cart-item.component.html | 45 +++++ .../cart-item/cart-item.component.scss | 160 ++++++++++++++++++ .../cart-item/cart-item.component.ts | 37 ++++ .../components/cart/cart.component.html | 42 +++++ .../components/cart/cart.component.scss | 104 ++++++++++++ .../shared/components/cart/cart.component.ts | 43 +++++ 9 files changed, 543 insertions(+), 1 deletion(-) create mode 100644 src/app/shared/components/cart-item/cart-item.component.html create mode 100644 src/app/shared/components/cart-item/cart-item.component.scss create mode 100644 src/app/shared/components/cart-item/cart-item.component.ts create mode 100644 src/app/shared/components/cart/cart.component.html create mode 100644 src/app/shared/components/cart/cart.component.scss create mode 100644 src/app/shared/components/cart/cart.component.ts diff --git a/src/app/features/componentes-test/pages/reutilizables-test-page/reutilizables-test-page.component.html b/src/app/features/componentes-test/pages/reutilizables-test-page/reutilizables-test-page.component.html index 1ea62ee..0947756 100644 --- a/src/app/features/componentes-test/pages/reutilizables-test-page/reutilizables-test-page.component.html +++ b/src/app/features/componentes-test/pages/reutilizables-test-page/reutilizables-test-page.component.html @@ -207,6 +207,35 @@ (pageChange)="currentPage = $event" /> + +
+ +
+
+
+

Carrito mock

+

+ Demo visual aislada del carrito con scroll a partir del cuarto item. +

+
+ + @if (!cartVisible) { + Volver a mostrar carrito + } +
+ + @if (cartVisible) { + + } +
diff --git a/src/app/features/componentes-test/pages/reutilizables-test-page/reutilizables-test-page.component.scss b/src/app/features/componentes-test/pages/reutilizables-test-page/reutilizables-test-page.component.scss index 680a36e..97ed775 100644 --- a/src/app/features/componentes-test/pages/reutilizables-test-page/reutilizables-test-page.component.scss +++ b/src/app/features/componentes-test/pages/reutilizables-test-page/reutilizables-test-page.component.scss @@ -62,6 +62,20 @@ h1 { text-align: center; } +.cart-showcase { + display: grid; + gap: 1rem; + justify-items: start; +} + +.cart-showcase__header { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; +} + .button-grid--single-char app-button { min-width: 3rem; } @@ -108,6 +122,11 @@ h1 { .input-grid { grid-template-columns: minmax(0, 1fr); } + + .cart-showcase__header { + align-items: start; + flex-direction: column; + } } .configuracion-tennant { diff --git a/src/app/features/componentes-test/pages/reutilizables-test-page/reutilizables-test-page.component.ts b/src/app/features/componentes-test/pages/reutilizables-test-page/reutilizables-test-page.component.ts index 42c5e3a..767e865 100644 --- a/src/app/features/componentes-test/pages/reutilizables-test-page/reutilizables-test-page.component.ts +++ b/src/app/features/componentes-test/pages/reutilizables-test-page/reutilizables-test-page.component.ts @@ -1,5 +1,6 @@ import { Component, inject } from '@angular/core'; import { ButtonComponent } from '../../../../shared/components/button/button.component'; +import { CartComponent, CartItemMock } from '../../../../shared/components/cart/cart.component'; import { InputComponent } from '../../../../shared/components/input/input.component'; import { PaginatorComponent } from '../../../../shared/components/paginator/paginator.component'; import { ProductCardComponent } from '../../../../shared/components/product-card/product-card.component'; @@ -9,7 +10,7 @@ import { ToastService } from '../../../../core/services/toast.service'; @Component({ selector: 'app-reutilizables-test-page', - imports: [ButtonComponent, InputComponent, PaginatorComponent, ProductCardComponent, StoreSectionComponent], + imports: [ButtonComponent, CartComponent, InputComponent, PaginatorComponent, ProductCardComponent, StoreSectionComponent], templateUrl: './reutilizables-test-page.component.html', styleUrl: './reutilizables-test-page.component.scss' }) @@ -33,7 +34,9 @@ export class ReutilizablesTestPageComponent { protected clearTrigger = 0; protected editableDisabled = true; protected currentPage = 1; + protected cartVisible = true; protected readonly paginatorTotalPages = 8; + protected readonly cartBackgroundColor = '#ffffff'; protected readonly testProducts = [ { @@ -59,6 +62,57 @@ export class ReutilizablesTestPageComponent { } ]; + protected readonly cartMockItems: CartItemMock[] = [ + { + imageUrl: null, + product: 'Calza térmica unisex con proceso sense y cintura con mayor agarre', + originalPrice: 95000, + discountedPrice: 76000, + discountPercentage: 20, + quantity: 1, + attributes: [ + { label: 'Color', value: 'Beige' }, + { label: 'Talle', value: 'S' } + ] + }, + { + imageUrl: null, + product: 'Pantalón recto de scuba negro', + originalPrice: null, + discountedPrice: 89000, + discountPercentage: null, + quantity: 1, + attributes: [ + { label: 'Color', value: 'Beige' }, + { label: 'Talle', value: 'S' } + ] + }, + { + imageUrl: null, + product: 'Top de supplex color habano', + originalPrice: 98000, + discountedPrice: 85000, + discountPercentage: 20, + quantity: 1, + attributes: [ + { label: 'Color', value: 'Negro' }, + { label: 'Talle', value: 'XL' } + ] + }, + { + imageUrl: null, + product: 'Buzo oversize canguro gris', + originalPrice: 72000, + discountedPrice: 69000, + discountPercentage: 5, + quantity: 2, + attributes: [ + { label: 'Color', value: 'Gris' }, + { label: 'Talle', value: 'M' } + ] + } + ]; + protected onProductBuy(productTitle: string): void { console.log(`Comprando: ${productTitle}`); alert(`Comprando: ${productTitle}`); @@ -73,6 +127,15 @@ export class ReutilizablesTestPageComponent { this.clearTrigger += 1; } + protected showCart(): void { + this.cartVisible = true; + } + + protected hideCart(): void { + this.cartVisible = false; + this.toastService.info('Se disparó el evento de cerrado del carrito mock.'); + } + protected triggerToast(type: 'success' | 'danger' | 'info'): void { if (type === 'success') { this.toastService.success('¡Operación exitosa! El producto se ha guardado correctamente.'); diff --git a/src/app/shared/components/cart-item/cart-item.component.html b/src/app/shared/components/cart-item/cart-item.component.html new file mode 100644 index 0000000..6827310 --- /dev/null +++ b/src/app/shared/components/cart-item/cart-item.component.html @@ -0,0 +1,45 @@ +
+
+ @if (discountPercentage() && discountPercentage()! > 0) { + -{{ discountPercentage() }}% + } + + @if (imageUrl()) { + + } @else { + + } +
+ +
+
+

{{ product() }}

+ +
+ @if (formattedOriginalPrice(); as originalPrice) { + {{ originalPrice }} + } + + {{ formattedDiscountedPrice() }} +
+
+ +
+ @for (attribute of attributes(); track attribute.label + attribute.value) { +
+ {{ attribute.label }}: + {{ attribute.value }} +
+ } +
+ + +
+
diff --git a/src/app/shared/components/cart-item/cart-item.component.scss b/src/app/shared/components/cart-item/cart-item.component.scss new file mode 100644 index 0000000..80edcbe --- /dev/null +++ b/src/app/shared/components/cart-item/cart-item.component.scss @@ -0,0 +1,160 @@ +:host { + display: block; +} + +.cart-item { + display: grid; + grid-template-columns: 64px minmax(0, 1fr); + gap: 0.75rem; + padding: 10px 12px; + border-radius: 0; + background-color: #f5f5f5; + + &__media { + position: relative; + width: 64px; + height: 72px; + overflow: hidden; + border-radius: 6px 0 0 6px; + background-color: #d9d9d9; + } + + &__image { + width: 100%; + height: 100%; + object-fit: cover; + display: block; + } + + &__image--placeholder { + background: linear-gradient(135deg, #d2d2d2, #ececec); + } + + &__discount-badge { + position: absolute; + top: 0; + left: 0; + z-index: 1; + padding: 2px 5px; + border-bottom-right-radius: 4px; + background-color: #6173ff; + color: #ffffff; + font-size: 8px; + font-weight: 500; + line-height: 1; + } + + &__content { + display: grid; + gap: 0.5rem; + min-width: 0; + } + + &__top-row { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 0.75rem; + align-items: start; + } + + &__product { + margin: 0; + color: #666666; + font-size: 12px; + font-weight: 325; + line-height: 1.25; + text-transform: uppercase; + } + + &__prices { + display: grid; + justify-items: end; + white-space: nowrap; + line-height: 1.1; + } + + &__price { + color: #a0a0a0; + } + + &__price--original { + font-size: 10px; + font-weight: 325; + text-decoration: line-through; + } + + &__price--discounted { + font-size: 13px; + font-weight: 425; + } + + &__attributes { + display: grid; + gap: 0.125rem; + } + + &__attribute { + color: #666666; + font-size: 11px; + line-height: 1.2; + } + + &__attribute-label { + font-weight: 325; + } + + &__attribute-value { + font-weight: 700; + } + + &__actions { + display: flex; + justify-content: flex-end; + align-items: center; + gap: 0.125rem; + margin-top: auto; + } + + &__quantity-control, + &__quantity { + display: inline-flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + border: 1px solid #cfcfcf; + background-color: #ffffff; + color: #666666; + font-size: 10px; + line-height: 1; + } + + &__quantity-control { + padding: 0; + } + + &__remove { + display: inline-flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + margin-left: 0.35rem; + padding: 0; + border: 0; + background: transparent; + color: #b3b3b3; + font-size: 12px; + } +} + +@media (max-width: 420px) { + .cart-item { + grid-template-columns: 56px minmax(0, 1fr); + + &__media { + width: 56px; + height: 64px; + } + } +} diff --git a/src/app/shared/components/cart-item/cart-item.component.ts b/src/app/shared/components/cart-item/cart-item.component.ts new file mode 100644 index 0000000..aa1134e --- /dev/null +++ b/src/app/shared/components/cart-item/cart-item.component.ts @@ -0,0 +1,37 @@ +import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core'; + +export interface CartItemAttribute { + label: string; + value: string; +} + +@Component({ + selector: 'app-cart-item', + standalone: true, + templateUrl: './cart-item.component.html', + styleUrl: './cart-item.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class CartItemComponent { + readonly imageUrl = input(null); + readonly product = input(''); + readonly originalPrice = input(null); + readonly discountedPrice = input(0); + readonly discountPercentage = input(null); + readonly attributes = input([]); + readonly quantity = input(1); + + protected readonly formattedOriginalPrice = computed(() => { + const price = this.originalPrice(); + return price === null ? null : this.formatCurrency(price); + }); + + protected readonly formattedDiscountedPrice = computed(() => this.formatCurrency(this.discountedPrice())); + + private formatCurrency(value: number): string { + const rounded = Math.round(value); + const parts = rounded.toString().split('.'); + parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, '.'); + return `$ ${parts.join(',')}`; + } +} diff --git a/src/app/shared/components/cart/cart.component.html b/src/app/shared/components/cart/cart.component.html new file mode 100644 index 0000000..0a9c595 --- /dev/null +++ b/src/app/shared/components/cart/cart.component.html @@ -0,0 +1,42 @@ +
+
+

{{ title() }}

+ + @if (showClose()) { + + } +
+ +
+ @for (item of items(); track item.product + item.discountedPrice) { + + } +
+ +
+
+ Subtotal: + {{ formattedSubtotal() }} +
+ +
+ Descuento: + {{ formattedDiscount() }} +
+ +
+ TOTAL: + {{ formattedTotal() }} +
+
+
diff --git a/src/app/shared/components/cart/cart.component.scss b/src/app/shared/components/cart/cart.component.scss new file mode 100644 index 0000000..249b2a6 --- /dev/null +++ b/src/app/shared/components/cart/cart.component.scss @@ -0,0 +1,104 @@ +:host { + display: block; + width: 100%; + max-width: 370px; +} + +.cart { + display: grid; + gap: 0.75rem; + width: 100%; + color: #666666; + border-radius: 0; + overflow: hidden; + + &__header, + &__summary { + background-color: inherit; + } + + &__header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 8px 0 24px; + } + + &__title { + margin: 0; + color: #a0a0a0; + font-size: 13px; + font-weight: 700; + letter-spacing: 0.04em; + } + + &__close { + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + padding: 0; + border: 0; + background: transparent; + color: #a0a0a0; + font-size: 14px; + } + + &__items { + display: grid; + gap: 8px; + max-height: 254px; + padding: 0 0 0 24px; + overflow-y: auto; + scrollbar-color: #d5d5d5 transparent; + scrollbar-width: thin; + } + + &__items::-webkit-scrollbar { + width: 6px; + } + + &__items::-webkit-scrollbar-thumb { + border-radius: 999px; + background-color: #d5d5d5; + } + + &__summary { + display: grid; + gap: 0.35rem; + padding: 8px 22px 14px 24px; + } + + &__summary-row { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 1rem; + } + + &__summary-label, + &__summary-value { + color: #a0a0a0; + font-size: 13px; + } + + &__summary-label { + font-weight: 325; + } + + &__summary-value { + font-weight: 700; + } + + &__summary-row--total { + margin-top: 0.2rem; + } + + &__summary-row--total .cart__summary-label, + &__summary-row--total .cart__summary-value { + color: #666666; + font-size: 15px; + font-weight: 700; + } +} diff --git a/src/app/shared/components/cart/cart.component.ts b/src/app/shared/components/cart/cart.component.ts new file mode 100644 index 0000000..d3b29ee --- /dev/null +++ b/src/app/shared/components/cart/cart.component.ts @@ -0,0 +1,43 @@ +import { ChangeDetectionStrategy, Component, computed, input, output } from '@angular/core'; +import { CartItemAttribute, CartItemComponent } from '../cart-item/cart-item.component'; + +export interface CartItemMock { + imageUrl: string | null; + product: string; + originalPrice: number | null; + discountedPrice: number; + discountPercentage: number | null; + attributes: CartItemAttribute[]; + quantity: number; +} + +@Component({ + selector: 'app-cart', + standalone: true, + imports: [CartItemComponent], + templateUrl: './cart.component.html', + styleUrl: './cart.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class CartComponent { + readonly title = input('CARRITO'); + readonly showClose = input(false); + readonly items = input([]); + readonly subtotal = input(0); + readonly discount = input(0); + readonly total = input(0); + readonly backgroundColor = input('#ffffff'); + + readonly closed = output(); + + protected readonly formattedSubtotal = computed(() => this.formatCurrency(this.subtotal())); + protected readonly formattedDiscount = computed(() => this.formatCurrency(this.discount())); + protected readonly formattedTotal = computed(() => this.formatCurrency(this.total())); + + private formatCurrency(value: number): string { + const rounded = Math.round(value); + const parts = rounded.toString().split('.'); + parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, '.'); + return `$ ${parts.join(',')}`; + } +}