Compare commits
3 Commits
902f65d8d0
...
163444c91f
| Author | SHA1 | Date | |
|---|---|---|---|
| 163444c91f | |||
| 4f1405f6c1 | |||
| f063bac527 |
@@ -36,6 +36,10 @@ export type InventoryPolicy = 'tracked' | 'unlimited';
|
||||
export interface CatalogItemVariant {
|
||||
id: number;
|
||||
stock_tecnico: number | null;
|
||||
minimum_use_date?: string | null;
|
||||
maximum_use_date?: string | null;
|
||||
effective_minimum_use_date?: string | null;
|
||||
effective_maximum_use_date?: string | null;
|
||||
values: Record<string, string>;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,8 +27,31 @@
|
||||
}
|
||||
|
||||
@if (isLoading()) {
|
||||
<header class="tickets-list__skeleton-header" aria-hidden="true">
|
||||
<div class="tickets-list__skeleton-selection">
|
||||
<span class="tickets-list__skeleton-check"></span>
|
||||
<span class="tickets-list__skeleton-line tickets-list__skeleton-line--section"></span>
|
||||
</div>
|
||||
<div class="tickets-list__skeleton-bulk-actions">
|
||||
<span class="tickets-list__skeleton-icon"></span>
|
||||
<span class="tickets-list__skeleton-icon"></span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@for (item of [1, 2, 3, 4]; track item) {
|
||||
<div class="tickets-list__skeleton" aria-hidden="true"></div>
|
||||
<div class="tickets-list__skeleton" aria-hidden="true">
|
||||
<span class="tickets-list__skeleton-check"></span>
|
||||
<div class="tickets-list__skeleton-details">
|
||||
<span class="tickets-list__skeleton-line tickets-list__skeleton-line--title"></span>
|
||||
<span class="tickets-list__skeleton-line tickets-list__skeleton-line--id"></span>
|
||||
</div>
|
||||
<span class="tickets-list__skeleton-line tickets-list__skeleton-line--date"></span>
|
||||
<div class="tickets-list__skeleton-actions">
|
||||
<span class="tickets-list__skeleton-button"></span>
|
||||
<span class="tickets-list__skeleton-icon"></span>
|
||||
<span class="tickets-list__skeleton-icon"></span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
} @else if (tickets().length) {
|
||||
@for (ticket of activeTickets(); track ticket.id) {
|
||||
|
||||
@@ -75,15 +75,96 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tickets-list__skeleton {
|
||||
height: 77px;
|
||||
.tickets-list__skeleton-header {
|
||||
display: flex;
|
||||
min-height: 58px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #dddddd;
|
||||
background: linear-gradient(90deg, transparent, #f3f3f3, transparent);
|
||||
background-size: 200% 100%;
|
||||
animation: loading 1.4s infinite;
|
||||
}
|
||||
|
||||
@keyframes loading {
|
||||
.tickets-list__skeleton-selection,
|
||||
.tickets-list__skeleton-bulk-actions,
|
||||
.tickets-list__skeleton-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tickets-list__skeleton-selection {
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.tickets-list__skeleton-bulk-actions,
|
||||
.tickets-list__skeleton-actions {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tickets-list__skeleton {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) 78px auto;
|
||||
min-height: 76px;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 16px 0;
|
||||
border-bottom: 1px solid #dddddd;
|
||||
}
|
||||
|
||||
.tickets-list__skeleton-check,
|
||||
.tickets-list__skeleton-line,
|
||||
.tickets-list__skeleton-icon,
|
||||
.tickets-list__skeleton-button {
|
||||
display: block;
|
||||
border-radius: 4px;
|
||||
background: linear-gradient(90deg, #eeeeee 20%, #f7f7f7 50%, #eeeeee 80%);
|
||||
background-size: 200% 100%;
|
||||
animation: tickets-loading 1.4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.tickets-list__skeleton-check {
|
||||
width: 21px;
|
||||
height: 21px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.tickets-list__skeleton-details {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tickets-list__skeleton-line--section {
|
||||
width: 120px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.tickets-list__skeleton-line--title {
|
||||
width: min(240px, 75%);
|
||||
height: 13px;
|
||||
}
|
||||
|
||||
.tickets-list__skeleton-line--id {
|
||||
width: 86px;
|
||||
height: 11px;
|
||||
}
|
||||
|
||||
.tickets-list__skeleton-line--date {
|
||||
width: 64px;
|
||||
height: 11px;
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
.tickets-list__skeleton-button {
|
||||
width: 115px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.tickets-list__skeleton-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
@keyframes tickets-loading {
|
||||
to {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
@@ -93,4 +174,23 @@
|
||||
.tickets-page__title {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.tickets-list__skeleton {
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.tickets-list__skeleton-line--date {
|
||||
grid-column: 2;
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
.tickets-list__skeleton-actions {
|
||||
grid-column: 3;
|
||||
grid-row: 1 / span 2;
|
||||
}
|
||||
|
||||
.tickets-list__skeleton-actions .tickets-list__skeleton-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
DestroyRef,
|
||||
Injector,
|
||||
computed,
|
||||
inject,
|
||||
signal,
|
||||
@@ -12,6 +13,8 @@ import { catchError, distinctUntilChanged, map, of, switchMap, tap } from 'rxjs'
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
|
||||
import { CartService } from '../../../../core/services/cart/cart.service';
|
||||
import { AuthService } from '../../../../core/services/auth/auth.service';
|
||||
import { CheckoutService } from '../../../../core/services/checkout.service';
|
||||
import {
|
||||
CatalogFeaturedItem,
|
||||
CatalogFeaturedItems,
|
||||
@@ -24,8 +27,8 @@ import { TenantService } from '../../../../core/services/tenant.service';
|
||||
import { ToastService } from '../../../../core/services/toast.service';
|
||||
import {
|
||||
ProductListCartEvent,
|
||||
ProductListBuyEvent,
|
||||
ProductListComponent,
|
||||
ProductListItem,
|
||||
} from '../../../../shared/components/product-list/product-list.component';
|
||||
|
||||
interface SearchRouteState {
|
||||
@@ -44,6 +47,7 @@ export class SearchPageComponent {
|
||||
private readonly minSearchLength = 3;
|
||||
private readonly cartService = inject(CartService);
|
||||
private readonly catalogService = inject(CatalogService);
|
||||
private readonly injector = inject(Injector);
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
private readonly router = inject(Router);
|
||||
@@ -54,6 +58,7 @@ export class SearchPageComponent {
|
||||
protected readonly results = signal<ApiPaginatedResponse<CatalogFeaturedItem[]> | null>(null);
|
||||
protected readonly loading = signal(false);
|
||||
protected readonly error = signal<string | null>(null);
|
||||
protected readonly creatingDirectPurchase = signal(false);
|
||||
|
||||
protected readonly productLayout = computed<CatalogProductLayout>(
|
||||
() => this.tenantService.tenant()?.search_product_layout ?? 'column_with_image',
|
||||
@@ -131,8 +136,45 @@ export class SearchPageComponent {
|
||||
});
|
||||
}
|
||||
|
||||
protected onBuyProduct(product: ProductListItem): void {
|
||||
void this.router.navigate(['/producto', product.id]);
|
||||
protected async onBuyProduct(event: ProductListBuyEvent): Promise<void> {
|
||||
if (!event.directPurchase) {
|
||||
await this.router.navigate(['/producto', event.product.id]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.creatingDirectPurchase()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.injector.get(AuthService).user()) {
|
||||
await this.router.navigate(['/login'], {
|
||||
queryParams: { returnUrl: `/producto/${event.product.id}` },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const tenant = this.tenantService.tenant();
|
||||
if (!tenant) {
|
||||
this.toastService.danger('No se pudo identificar la tienda.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.creatingDirectPurchase.set(true);
|
||||
try {
|
||||
const purchase = await this.injector.get(CheckoutService).startCheckout(tenant.codigo, {
|
||||
direct_item: {
|
||||
catalog_item_id: event.product.id,
|
||||
variant_id: event.variant ?? null,
|
||||
cantidad: event.quantity,
|
||||
},
|
||||
});
|
||||
await this.router.navigate(['/checkout'], { queryParams: { purchase: purchase.id } });
|
||||
} catch (error) {
|
||||
console.error('Failed to create direct purchase:', error);
|
||||
this.toastService.danger('No se pudo iniciar la compra directa.');
|
||||
} finally {
|
||||
this.creatingDirectPurchase.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
protected onAddToCart(event: ProductListCartEvent): void {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
Injector,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
computed,
|
||||
@@ -12,14 +13,16 @@ import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
import { CartService } from '../../../../core/services/cart/cart.service';
|
||||
import { AuthService } from '../../../../core/services/auth/auth.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 { CheckoutService } from '../../../../core/services/checkout.service';
|
||||
import {
|
||||
ProductListComponent,
|
||||
ProductListCartEvent,
|
||||
ProductListItem,
|
||||
ProductListBuyEvent,
|
||||
} from '../../../../shared/components/product-list/product-list.component';
|
||||
import { HeroBannerComponent } from '../../../../shared/components/hero-banner/hero-banner.component';
|
||||
import { MainCarouselComponent } from '../../../../shared/components/main-carousel/main-carousel.component';
|
||||
@@ -44,6 +47,7 @@ import {
|
||||
export class StoreHomePageComponent implements OnInit, OnDestroy {
|
||||
private readonly cartService = inject(CartService);
|
||||
private readonly catalogService = inject(CatalogService);
|
||||
private readonly injector = inject(Injector);
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
private readonly router = inject(Router);
|
||||
private readonly tenantService = inject(TenantService);
|
||||
@@ -55,6 +59,7 @@ export class StoreHomePageComponent implements OnInit, OnDestroy {
|
||||
protected readonly loadingGroupIds = signal<ReadonlySet<number>>(new Set());
|
||||
protected readonly error = signal<string | null>(null);
|
||||
protected readonly mainCarouselReady = signal(false);
|
||||
protected readonly creatingDirectPurchase = signal(false);
|
||||
protected readonly hasMainCarouselImages = computed(
|
||||
() => (this.tenant()?.main_carousel_images?.length ?? 0) > 0,
|
||||
);
|
||||
@@ -64,7 +69,8 @@ export class StoreHomePageComponent implements OnInit, OnDestroy {
|
||||
|
||||
ngOnInit(): void {
|
||||
const resolvedData = this.route.snapshot.data['catalogData'] as
|
||||
StoreHomeCatalogResolvedData | undefined;
|
||||
| StoreHomeCatalogResolvedData
|
||||
| undefined;
|
||||
|
||||
if (resolvedData) {
|
||||
this.applyResolvedData(resolvedData);
|
||||
@@ -118,8 +124,45 @@ export class StoreHomePageComponent implements OnInit, OnDestroy {
|
||||
this.groupRequestSubscriptions.set(groupId, subscription);
|
||||
}
|
||||
|
||||
protected onBuyProduct(product: ProductListItem): void {
|
||||
this.router.navigate(['/producto', product.id]);
|
||||
protected async onBuyProduct(event: ProductListBuyEvent): Promise<void> {
|
||||
if (!event.directPurchase) {
|
||||
await this.router.navigate(['/producto', event.product.id]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.creatingDirectPurchase()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.injector.get(AuthService).user()) {
|
||||
await this.router.navigate(['/login'], {
|
||||
queryParams: { returnUrl: `/producto/${event.product.id}` },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const tenant = this.tenantService.tenant();
|
||||
if (!tenant) {
|
||||
this.toastService.danger('No se pudo identificar la tienda.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.creatingDirectPurchase.set(true);
|
||||
try {
|
||||
const purchase = await this.injector.get(CheckoutService).startCheckout(tenant.codigo, {
|
||||
direct_item: {
|
||||
catalog_item_id: event.product.id,
|
||||
variant_id: event.variant ?? null,
|
||||
cantidad: event.quantity,
|
||||
},
|
||||
});
|
||||
await this.router.navigate(['/checkout'], { queryParams: { purchase: purchase.id } });
|
||||
} catch (error) {
|
||||
console.error('Failed to create direct purchase:', error);
|
||||
this.toastService.danger('No se pudo iniciar la compra directa.');
|
||||
} finally {
|
||||
this.creatingDirectPurchase.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
protected onAddToCart(event: ProductListCartEvent): void {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
[description]="item.descripcion ?? ''"
|
||||
[price]="price(item)"
|
||||
[variants]="variantsFor(item)"
|
||||
(buy)="buy.emit(item)"
|
||||
(buy)="emitRowBuy(item, $event)"
|
||||
(addToCart)="emitRowCart(item, $event)"
|
||||
/>
|
||||
}
|
||||
@@ -22,7 +22,7 @@
|
||||
[title]="item.nombre"
|
||||
[description]="item.descripcion ?? ''"
|
||||
[price]="price(item)"
|
||||
(buy)="buy.emit(item)"
|
||||
(buy)="emitColumnBuy(item, $event)"
|
||||
(addToCart)="emitColumnCart(item, $event)"
|
||||
/>
|
||||
}
|
||||
@@ -32,7 +32,7 @@
|
||||
[title]="item.nombre"
|
||||
[originalPrice]="price(item)"
|
||||
[imagePriority]="loadImages() && index < 4"
|
||||
(buy)="buy.emit(item)"
|
||||
(buy)="emitProductDetailBuy(item)"
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,6 +126,40 @@ describe('ProductListComponent', () => {
|
||||
).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('emits a direct-purchase event with the row quantity and selected variant', async () => {
|
||||
const fixture = await render('row');
|
||||
const buySpy = vi.fn();
|
||||
fixture.componentInstance.buy.subscribe(buySpy);
|
||||
const rowCard = fixture.nativeElement.querySelector('app-product-row-card') as HTMLElement;
|
||||
|
||||
(rowCard.querySelector('.btn-primary') as HTMLButtonElement).click();
|
||||
|
||||
expect(buySpy).toHaveBeenCalledWith({
|
||||
product: items[0],
|
||||
quantity: 1,
|
||||
variant: null,
|
||||
directPurchase: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('emits a direct-purchase event with the column quantity', async () => {
|
||||
const fixture = await render('column_with_cart');
|
||||
const buySpy = vi.fn();
|
||||
fixture.componentInstance.buy.subscribe(buySpy);
|
||||
const card = fixture.nativeElement.querySelector(
|
||||
'app-product-vertical-with-cart-card',
|
||||
) as HTMLElement;
|
||||
|
||||
(card.querySelector('.btn-primary') as HTMLButtonElement).click();
|
||||
|
||||
expect(buySpy).toHaveBeenCalledWith({
|
||||
product: items[0],
|
||||
quantity: 1,
|
||||
variant: null,
|
||||
directPurchase: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('renders pagination and emits the requested page', async () => {
|
||||
const fixture = await render('row');
|
||||
const pageChangeSpy = vi.fn();
|
||||
|
||||
@@ -37,6 +37,13 @@ export interface ProductListCartEvent {
|
||||
variant?: number | null;
|
||||
}
|
||||
|
||||
export interface ProductListBuyEvent {
|
||||
product: ProductListItem;
|
||||
quantity: number;
|
||||
variant?: number | null;
|
||||
directPurchase: boolean;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-product-list',
|
||||
imports: [
|
||||
@@ -61,7 +68,7 @@ export class ProductListComponent {
|
||||
readonly loading = input(false);
|
||||
readonly loadImages = input(true);
|
||||
|
||||
readonly buy = output<ProductListItem>();
|
||||
readonly buy = output<ProductListBuyEvent>();
|
||||
readonly addToCart = output<ProductListCartEvent>();
|
||||
readonly pageChange = output<number>();
|
||||
|
||||
@@ -119,4 +126,24 @@ export class ProductListComponent {
|
||||
protected emitColumnCart(product: ProductListItem, event: { quantity: number }): void {
|
||||
this.addToCart.emit({ product, quantity: event.quantity });
|
||||
}
|
||||
|
||||
protected emitRowBuy(
|
||||
product: ProductListItem,
|
||||
event: { quantity: number; variant: unknown },
|
||||
): void {
|
||||
this.buy.emit({
|
||||
product,
|
||||
quantity: event.quantity,
|
||||
variant: typeof event.variant === 'number' ? event.variant : null,
|
||||
directPurchase: true,
|
||||
});
|
||||
}
|
||||
|
||||
protected emitColumnBuy(product: ProductListItem, event: { quantity: number }): void {
|
||||
this.buy.emit({ product, quantity: event.quantity, variant: null, directPurchase: true });
|
||||
}
|
||||
|
||||
protected emitProductDetailBuy(product: ProductListItem): void {
|
||||
this.buy.emit({ product, quantity: 1, variant: null, directPurchase: false });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<div class="product-row-card border rounded bg-white shadow-sm d-flex justify-content-between p-3 gap-3">
|
||||
<div
|
||||
class="product-row-card border rounded bg-white shadow-sm d-flex justify-content-between p-3 gap-3"
|
||||
>
|
||||
<!-- Left Side: Title and Description -->
|
||||
<div class="product-row-card__info d-flex flex-column justify-content-center flex-grow-1">
|
||||
<h3 class="product-row-card__title text-uppercase mb-1 m-0">
|
||||
@@ -19,9 +21,7 @@
|
||||
{{ formattedPrice() }}
|
||||
</span>
|
||||
<div class="product-row-card__btn-wrapper">
|
||||
<app-button variant="primary" (click)="buy.emit()">
|
||||
Comprar
|
||||
</app-button>
|
||||
<app-button variant="primary" (click)="onBuy()"> Comprar </app-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -35,12 +35,10 @@
|
||||
</select>
|
||||
}
|
||||
|
||||
<app-quantity-selector [(quantity)]="quantity" ></app-quantity-selector>
|
||||
<app-quantity-selector [(quantity)]="quantity"></app-quantity-selector>
|
||||
|
||||
<div class="product-row-card__btn-wrapper">
|
||||
<app-button variant="secondary" (click)="onAddToCart()">
|
||||
Agregar al carrito
|
||||
</app-button>
|
||||
<app-button variant="secondary" (click)="onAddToCart()"> Agregar al carrito </app-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import { ChangeDetectionStrategy, Component, computed, effect, input, output, model } from '@angular/core';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
effect,
|
||||
input,
|
||||
output,
|
||||
model,
|
||||
} from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ButtonComponent } from '../button/button.component';
|
||||
import { QuantitySelectorComponent } from '../quantity-selector/quantity-selector.component';
|
||||
@@ -28,7 +36,7 @@ export class ProductRowCardComponent {
|
||||
readonly selectedVariant = model<any>(null);
|
||||
|
||||
// Interactive events
|
||||
readonly buy = output<void>();
|
||||
readonly buy = output<{ quantity: number; variant: any }>();
|
||||
readonly addToCart = output<{ quantity: number; variant: any }>();
|
||||
|
||||
constructor() {
|
||||
@@ -53,7 +61,14 @@ export class ProductRowCardComponent {
|
||||
protected onAddToCart(): void {
|
||||
this.addToCart.emit({
|
||||
quantity: this.quantity(),
|
||||
variant: this.selectedVariant()
|
||||
variant: this.selectedVariant(),
|
||||
});
|
||||
}
|
||||
|
||||
protected onBuy(): void {
|
||||
this.buy.emit({
|
||||
quantity: this.quantity(),
|
||||
variant: this.selectedVariant(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
</div>
|
||||
|
||||
<div class="product-vertical-with-cart-card__actions">
|
||||
<app-button variant="primary" (click)="buy.emit()">Comprar</app-button>
|
||||
<app-button variant="primary" (click)="onBuy()">Comprar</app-button>
|
||||
<app-button variant="secondary" (click)="onAddToCart()"> Agregar al carrito </app-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -83,7 +83,7 @@ describe('ProductVerticalWithCartCardComponent', () => {
|
||||
) as HTMLButtonElement;
|
||||
button.click();
|
||||
|
||||
expect(buySpy).toHaveBeenCalledOnce();
|
||||
expect(buySpy).toHaveBeenCalledWith({ quantity: 1 });
|
||||
});
|
||||
|
||||
it('emits addToCart with the current quantity', async () => {
|
||||
|
||||
@@ -17,7 +17,7 @@ export class ProductVerticalWithCartCardComponent {
|
||||
|
||||
readonly quantity = model<number>(1);
|
||||
|
||||
readonly buy = output<void>();
|
||||
readonly buy = output<{ quantity: number }>();
|
||||
readonly addToCart = output<{ quantity: number }>();
|
||||
|
||||
protected readonly formattedPrice = computed(() => this.formatCurrency(this.price()));
|
||||
@@ -26,6 +26,10 @@ export class ProductVerticalWithCartCardComponent {
|
||||
this.addToCart.emit({ quantity: this.quantity() });
|
||||
}
|
||||
|
||||
protected onBuy(): void {
|
||||
this.buy.emit({ quantity: this.quantity() });
|
||||
}
|
||||
|
||||
private formatCurrency(value: number): string {
|
||||
const rounded = Math.round(value);
|
||||
const parts = rounded.toString().split('.');
|
||||
|
||||
Reference in New Issue
Block a user