feat(F-070): completed feature
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
||||
calcGrossPrice,
|
||||
} from '@/lib/api';
|
||||
import ProductAddToCart from '@/components/cart/ProductAddToCart';
|
||||
import ProductAttributes from '@/components/product/ProductAttributes';
|
||||
|
||||
interface Props {
|
||||
params: Promise<{ slug: string }>;
|
||||
@@ -134,6 +135,8 @@ export default async function ProductPage({ params }: Props) {
|
||||
{product.name}
|
||||
</h1>
|
||||
|
||||
<ProductAttributes attributes={product.attributes} />
|
||||
|
||||
{/* Price */}
|
||||
{price ? (
|
||||
<div className="mt-6 bg-gray-50 rounded-xl p-6">
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { ProductAttribute } from '@/types/api';
|
||||
import { ATTRIBUTE_LABELS } from '@/lib/product-attributes';
|
||||
|
||||
interface Props {
|
||||
attributes: ProductAttribute[] | undefined;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function ProductAttributes({ attributes, className }: Props) {
|
||||
if (!attributes || attributes.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className={`flex flex-wrap gap-2 ${className ?? ''}`}>
|
||||
{attributes.map((attr) => (
|
||||
<span
|
||||
key={attr}
|
||||
className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-emerald-100 text-emerald-800"
|
||||
title={ATTRIBUTE_LABELS[attr]}
|
||||
>
|
||||
{ATTRIBUTE_LABELS[attr]}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
25
project/frontend/src/lib/product-attributes.ts
Normal file
25
project/frontend/src/lib/product-attributes.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { ProductAttribute } from '@/types/api';
|
||||
|
||||
/** Human-readable labels for product attribute tags. */
|
||||
export const ATTRIBUTE_LABELS: Record<ProductAttribute, string> = {
|
||||
bio: 'Eco',
|
||||
'comercio-justo': 'Comercio justo',
|
||||
congelado: 'Congelado',
|
||||
'cruelty-free': 'Cruelty-free',
|
||||
'de-temporada': 'De temporada',
|
||||
demeter: 'Demeter',
|
||||
'fruta-verdura': 'Fruta y verdura',
|
||||
keto: 'Keto',
|
||||
kosher: 'Kosher',
|
||||
'low-carb': 'Low-carb',
|
||||
'raw-food': 'Raw food',
|
||||
'sin-azucar': 'Sin azúcar',
|
||||
'sin-gluten': 'Sin gluten',
|
||||
'sin-lactosa': 'Sin lactosa',
|
||||
vegano: 'Vegano',
|
||||
'zero-waste': 'Zero waste',
|
||||
};
|
||||
|
||||
export function getAttributeLabel(attr: ProductAttribute): string {
|
||||
return ATTRIBUTE_LABELS[attr] ?? attr;
|
||||
}
|
||||
@@ -1,3 +1,25 @@
|
||||
// Product attribute tags (matching backend ProductAttribute domain enum)
|
||||
export const PRODUCT_ATTRIBUTES = [
|
||||
'bio',
|
||||
'comercio-justo',
|
||||
'congelado',
|
||||
'cruelty-free',
|
||||
'de-temporada',
|
||||
'demeter',
|
||||
'fruta-verdura',
|
||||
'keto',
|
||||
'kosher',
|
||||
'low-carb',
|
||||
'raw-food',
|
||||
'sin-azucar',
|
||||
'sin-gluten',
|
||||
'sin-lactosa',
|
||||
'vegano',
|
||||
'zero-waste',
|
||||
] as const;
|
||||
|
||||
export type ProductAttribute = (typeof PRODUCT_ATTRIBUTES)[number];
|
||||
|
||||
export interface Product {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -5,6 +27,7 @@ export interface Product {
|
||||
url: string;
|
||||
description?: string;
|
||||
state: string;
|
||||
attributes?: ProductAttribute[];
|
||||
seoTitle?: string;
|
||||
seoDescription?: string;
|
||||
images: Array<{ id: string; url: string; altText?: string }>;
|
||||
|
||||
Reference in New Issue
Block a user