feat(product-row-card): add reusable product row card component with styling and functionality
This commit is contained in:
@@ -631,6 +631,30 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="component-demo card shadow-sm mt-4">
|
||||
<div class="card-body">
|
||||
<span class="eyebrow">Reusable Component</span>
|
||||
<h2 class="mb-4">Tarjetas de Producto (Row Layout)</h2>
|
||||
|
||||
<p class="text-muted mb-4">
|
||||
Diseño horizontal que ocupa el 100% del ancho disponible.
|
||||
</p>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12 mb-4 d-flex align-items-stretch">
|
||||
<app-product-row-card
|
||||
[title]="testRowProduct.title"
|
||||
[description]="testRowProduct.description"
|
||||
[price]="testRowProduct.price"
|
||||
[variants]="testRowProduct.variants"
|
||||
(buy)="onProductBuy(testRowProduct.title)"
|
||||
(addToCart)="triggerToast('success')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<app-store-section
|
||||
title="PRODUCTOS"
|
||||
class="component-demo store-section-demo mt-5"
|
||||
|
||||
@@ -9,6 +9,7 @@ import { IconButtonComponent } from '../../../../shared/components/icon-button/i
|
||||
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';
|
||||
import { ProductRowCardComponent } from '../../../../shared/components/product-row-card/product-row-card.component';
|
||||
import { StoreSectionComponent } from '../../../../shared/components/store-section/store-section.component';
|
||||
import { ModalService } from '../../../../core/services/modal.service';
|
||||
import { TenantService } from '../../../../core/services/tenant.service';
|
||||
@@ -24,6 +25,7 @@ import { StepComponent } from '../../../../shared/components/stepper/step.compon
|
||||
InputComponent,
|
||||
PaginatorComponent,
|
||||
ProductCardComponent,
|
||||
ProductRowCardComponent,
|
||||
StoreSectionComponent,
|
||||
IconButtonComponent,
|
||||
CartIconComponent,
|
||||
@@ -86,6 +88,16 @@ export class ReutilizablesTestPageComponent {
|
||||
}
|
||||
];
|
||||
|
||||
protected readonly testRowProduct = {
|
||||
title: 'ENTRADA GENERAL',
|
||||
description: 'Acceso total al predio. No incluye acceso a estacionamiento. Niños menores de 5 años ingresan gratis.',
|
||||
price: 10000,
|
||||
variants: [
|
||||
{ label: '10 de Octubre', value: '10-oct' },
|
||||
{ label: '11 de Octubre', value: '11-oct' }
|
||||
]
|
||||
};
|
||||
|
||||
protected readonly cartMockItems: CartItemMock[] = [
|
||||
{
|
||||
imageUrl: null,
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
<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 text-black mb-1 m-0">
|
||||
{{ title() }}
|
||||
</h3>
|
||||
@if (description()) {
|
||||
<p class="product-row-card__description m-0 mt-1">
|
||||
{{ description() }}
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Right Side: Actions and Pricing -->
|
||||
<div class="product-row-card__actions d-flex flex-column gap-2 justify-content-center">
|
||||
<!-- Top Row: Price and Comprar button -->
|
||||
<div class="d-flex align-items-center justify-content-end gap-3">
|
||||
<span class="product-row-card__price text-primary">
|
||||
{{ formattedPrice() }}
|
||||
</span>
|
||||
<div class="product-row-card__btn-wrapper">
|
||||
<app-button variant="primary" (click)="buy.emit()">
|
||||
Comprar
|
||||
</app-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom Row: Select, Quantity, and Agregar al carrito button -->
|
||||
<div class="d-flex align-items-center justify-content-end gap-2">
|
||||
@if (variants().length > 0) {
|
||||
<select class="form-select product-row-card__select" [(ngModel)]="selectedVariant">
|
||||
@for (variant of variants(); track variant.value) {
|
||||
<option [value]="variant.value">{{ variant.label }}</option>
|
||||
}
|
||||
</select>
|
||||
}
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,63 @@
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.product-row-card {
|
||||
width: 100%;
|
||||
background-color: #f8f9fa; /* slightly light bg matching the screenshot if needed, but let's use white or #f8f9fa based on the border */
|
||||
border-color: #e9ecef !important;
|
||||
|
||||
&__title {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #555; /* dark grey matching screenshot */
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
&__description {
|
||||
font-size: 13px;
|
||||
color: #777; /* lighter grey for text */
|
||||
line-height: 1.4;
|
||||
max-width: 600px;
|
||||
|
||||
/* Simulate the bold text for 'Niños menores...' if it was HTML.
|
||||
Since it's passed as string we just let it be, unless we parse it. */
|
||||
}
|
||||
|
||||
&__price {
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
color: var(--tenant-primary, #009933) !important; /* Green matching screenshot */
|
||||
}
|
||||
|
||||
&__select {
|
||||
width: auto;
|
||||
min-width: 150px;
|
||||
font-size: 14px;
|
||||
height: 38px;
|
||||
color: #666;
|
||||
border-color: #ccc;
|
||||
cursor: pointer;
|
||||
|
||||
&:focus {
|
||||
border-color: var(--tenant-primary);
|
||||
box-shadow: 0 0 0 0.25rem rgba(0, 153, 51, 0.25);
|
||||
}
|
||||
}
|
||||
|
||||
&__btn-wrapper {
|
||||
min-width: 160px; /* To make both buttons equal width as in screenshot */
|
||||
|
||||
app-button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
|
||||
::ng-deep .btn {
|
||||
width: 100%;
|
||||
font-weight: 600;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { ChangeDetectionStrategy, Component, computed, 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';
|
||||
|
||||
export interface Variant {
|
||||
label: string;
|
||||
value: any;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-product-row-card',
|
||||
standalone: true,
|
||||
imports: [ButtonComponent, QuantitySelectorComponent, FormsModule],
|
||||
templateUrl: './product-row-card.component.html',
|
||||
styleUrl: './product-row-card.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ProductRowCardComponent {
|
||||
// Configurable inputs
|
||||
readonly title = input<string>('');
|
||||
readonly description = input<string>('');
|
||||
readonly price = input<number>(0);
|
||||
readonly variants = input<Variant[]>([]);
|
||||
|
||||
// Internal state models
|
||||
readonly quantity = model<number>(1);
|
||||
readonly selectedVariant = model<any>(null);
|
||||
|
||||
// Interactive events
|
||||
readonly buy = output<void>();
|
||||
readonly addToCart = output<{ quantity: number; variant: any }>();
|
||||
|
||||
// Formatted string for price: "$ 10.000"
|
||||
readonly formattedPrice = computed(() => {
|
||||
return this.formatCurrency(this.price());
|
||||
});
|
||||
|
||||
protected onAddToCart(): void {
|
||||
this.addToCart.emit({
|
||||
quantity: this.quantity(),
|
||||
variant: this.selectedVariant()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to format currency numbers to Argentine Pesos style "$ XX.XXX".
|
||||
*/
|
||||
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(',')}`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user