Skip to content

Commit 819617e

Browse files
committed
refactor: product.service & UI and chatbot update
1 parent ef1925a commit 819617e

File tree

5 files changed

+296
-171
lines changed

5 files changed

+296
-171
lines changed

prisma/initial_data.ts

Lines changed: 52 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ export const categories = [
3030
];
3131

3232
export const variantAttributes = [
33-
{ name: "no aplica" },
3433
{ name: "talla" },
3534
{ name: "dimensiones" },
35+
{ name: "no aplica" },
3636
]
3737

3838
export const products = [
@@ -357,74 +357,74 @@ export const products = [
357357

358358
export const variantAttributeValues = [
359359
// --- POLOS (talla: S, M, L) ---
360-
{ attributeId: 1, productId: 1, value: "S", price: 20.0 },
361-
{ attributeId: 1, productId: 1, value: "M", price: 20.0 },
362-
{ attributeId: 1, productId: 1, value: "L", price: 20.0 },
360+
{ attributeId: 1, productId: 1, value: "Small", price: 20.0 },
361+
{ attributeId: 1, productId: 1, value: "Medium", price: 20.0 },
362+
{ attributeId: 1, productId: 1, value: "Large", price: 20.0 },
363363

364-
{ attributeId: 1, productId: 2, value: "S", price: 20.0 },
365-
{ attributeId: 1, productId: 2, value: "M", price: 20.0 },
366-
{ attributeId: 1, productId: 2, value: "L", price: 20.0 },
364+
{ attributeId: 1, productId: 2, value: "Small", price: 20.0 },
365+
{ attributeId: 1, productId: 2, value: "Medium", price: 20.0 },
366+
{ attributeId: 1, productId: 2, value: "Large", price: 20.0 },
367367

368-
{ attributeId: 1, productId: 3, value: "S", price: 20.0 },
369-
{ attributeId: 1, productId: 3, value: "M", price: 20.0 },
370-
{ attributeId: 1, productId: 3, value: "L", price: 20.0 },
368+
{ attributeId: 1, productId: 3, value: "Small", price: 20.0 },
369+
{ attributeId: 1, productId: 3, value: "Medium", price: 20.0 },
370+
{ attributeId: 1, productId: 3, value: "Large", price: 20.0 },
371371

372-
{ attributeId: 1, productId: 4, value: "S", price: 20.0 },
373-
{ attributeId: 1, productId: 4, value: "M", price: 20.0 },
374-
{ attributeId: 1, productId: 4, value: "L", price: 20.0 },
372+
{ attributeId: 1, productId: 4, value: "Small", price: 20.0 },
373+
{ attributeId: 1, productId: 4, value: "Medium", price: 20.0 },
374+
{ attributeId: 1, productId: 4, value: "Large", price: 20.0 },
375375

376-
{ attributeId: 1, productId: 5, value: "S", price: 25.0 },
377-
{ attributeId: 1, productId: 5, value: "M", price: 25.0 },
378-
{ attributeId: 1, productId: 5, value: "L", price: 25.0 },
376+
{ attributeId: 1, productId: 5, value: "Small", price: 25.0 },
377+
{ attributeId: 1, productId: 5, value: "Medium", price: 25.0 },
378+
{ attributeId: 1, productId: 5, value: "Large", price: 25.0 },
379379

380-
{ attributeId: 1, productId: 6, value: "S", price: 25.0 },
381-
{ attributeId: 1, productId: 6, value: "M", price: 25.0 },
382-
{ attributeId: 1, productId: 6, value: "L", price: 25.0 },
380+
{ attributeId: 1, productId: 6, value: "Small", price: 25.0 },
381+
{ attributeId: 1, productId: 6, value: "Medium", price: 25.0 },
382+
{ attributeId: 1, productId: 6, value: "Large", price: 25.0 },
383383

384-
{ attributeId: 1, productId: 7, value: "S", price: 25.0 },
385-
{ attributeId: 1, productId: 7, value: "M", price: 25.0 },
386-
{ attributeId: 1, productId: 7, value: "L", price: 25.0 },
384+
{ attributeId: 1, productId: 7, value: "Small", price: 25.0 },
385+
{ attributeId: 1, productId: 7, value: "Medium", price: 25.0 },
386+
{ attributeId: 1, productId: 7, value: "Large", price: 25.0 },
387387

388-
{ attributeId: 1, productId: 8, value: "S", price: 15.0 },
389-
{ attributeId: 1, productId: 8, value: "M", price: 15.0 },
390-
{ attributeId: 1, productId: 8, value: "L", price: 15.0 },
388+
{ attributeId: 1, productId: 8, value: "Small", price: 15.0 },
389+
{ attributeId: 1, productId: 8, value: "Medium", price: 15.0 },
390+
{ attributeId: 1, productId: 8, value: "Large", price: 15.0 },
391391

392-
{ attributeId: 1, productId: 9, value: "S", price: 15.0 },
393-
{ attributeId: 1, productId: 9, value: "M", price: 15.0 },
394-
{ attributeId: 1, productId: 9, value: "L", price: 15.0 },
392+
{ attributeId: 1, productId: 9, value: "Small", price: 15.0 },
393+
{ attributeId: 1, productId: 9, value: "Medium", price: 15.0 },
394+
{ attributeId: 1, productId: 9, value: "Large", price: 15.0 },
395395

396396
// --- STICKERS (dimensiones: 3x3, 6x6, 9x9) ---
397-
{ attributeId: 2, productId: 10, value: "3x3", price: 2.99 },
398-
{ attributeId: 2, productId: 10, value: "5x5", price: 3.99 },
399-
{ attributeId: 2, productId: 10, value: "10x10", price: 4.99 },
397+
{ attributeId: 2, productId: 10, value: "3x3 cm", price: 2.99 },
398+
{ attributeId: 2, productId: 10, value: "5x5 cm", price: 3.99 },
399+
{ attributeId: 2, productId: 10, value: "10x10 cm", price: 4.99 },
400400

401-
{ attributeId: 2, productId: 11, value: "3x3", price: 2.49 },
402-
{ attributeId: 2, productId: 11, value: "5x5", price: 3.49 },
403-
{ attributeId: 2, productId: 11, value: "10x10", price: 4.49 },
401+
{ attributeId: 2, productId: 11, value: "3x3 cm", price: 2.49 },
402+
{ attributeId: 2, productId: 11, value: "5x5 cm", price: 3.49 },
403+
{ attributeId: 2, productId: 11, value: "10x10 cm", price: 4.49 },
404404

405-
{ attributeId: 2, productId: 12, value: "3x3", price: 3.99 },
406-
{ attributeId: 2, productId: 12, value: "5x5", price: 4.99 },
407-
{ attributeId: 2, productId: 12, value: "10x10", price: 5.99 },
405+
{ attributeId: 2, productId: 12, value: "3x3 cm", price: 3.99 },
406+
{ attributeId: 2, productId: 12, value: "5x5 cm", price: 4.99 },
407+
{ attributeId: 2, productId: 12, value: "10x10 cm", price: 5.99 },
408408

409-
{ attributeId: 2, productId: 13, value: "3x3", price: 2.99 },
410-
{ attributeId: 2, productId: 13, value: "5x5", price: 3.99 },
411-
{ attributeId: 2, productId: 13, value: "10x10", price: 4.99 },
409+
{ attributeId: 2, productId: 13, value: "3x3 cm", price: 2.99 },
410+
{ attributeId: 2, productId: 13, value: "5x5 cm", price: 3.99 },
411+
{ attributeId: 2, productId: 13, value: "10x10 cm", price: 4.99 },
412412

413-
{ attributeId: 2, productId: 14, value: "3x3", price: 2.49 },
414-
{ attributeId: 2, productId: 14, value: "5x5", price: 3.49 },
415-
{ attributeId: 2, productId: 14, value: "10x10", price: 4.49 },
413+
{ attributeId: 2, productId: 14, value: "3x3 cm", price: 2.49 },
414+
{ attributeId: 2, productId: 14, value: "5x5 cm", price: 3.49 },
415+
{ attributeId: 2, productId: 14, value: "10x10 cm", price: 4.49 },
416416

417-
{ attributeId: 2, productId: 15, value: "3x3", price: 2.49 },
418-
{ attributeId: 2, productId: 15, value: "5x5", price: 3.49 },
419-
{ attributeId: 2, productId: 15, value: "10x10", price: 4.49 },
417+
{ attributeId: 2, productId: 15, value: "3x3 cm", price: 2.49 },
418+
{ attributeId: 2, productId: 15, value: "5x5 cm", price: 3.49 },
419+
{ attributeId: 2, productId: 15, value: "10x10 cm", price: 4.49 },
420420

421-
{ attributeId: 2, productId: 16, value: "3x3", price: 2.99 },
422-
{ attributeId: 2, productId: 16, value: "5x5", price: 3.99 },
423-
{ attributeId: 2, productId: 16, value: "10x10", price: 4.99 },
421+
{ attributeId: 2, productId: 16, value: "3x3 cm", price: 2.99 },
422+
{ attributeId: 2, productId: 16, value: "5x5 cm", price: 3.99 },
423+
{ attributeId: 2, productId: 16, value: "10x10 cm", price: 4.99 },
424424

425-
{ attributeId: 2, productId: 17, value: "3x3", price: 2.99 },
426-
{ attributeId: 2, productId: 17, value: "5x5", price: 3.99 },
427-
{ attributeId: 2, productId: 17, value: "10x10", price: .99 },
425+
{ attributeId: 2, productId: 17, value: "3x3 cm", price: 2.99 },
426+
{ attributeId: 2, productId: 17, value: "5x5 cm", price: 3.99 },
427+
{ attributeId: 2, productId: 17, value: "10x10 cm", price: 4.99 },
428428

429429
// --- TAZAS (no aplica: Único) ---
430430
{ attributeId: 3, productId: 18, value: "Único", price: 14.99 },

src/routes/category/components/product-card/index.tsx

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,25 @@ export function ProductCard({ product }: ProductCardProps) {
3030
<div className="flex grow flex-col gap-2 p-4">
3131
<h2 className="text-sm font-medium">{product.title}</h2>
3232
<p className="text-sm text-muted-foreground">{product.description}</p>
33-
{isSticker ? (
34-
<div className="text-xs text-muted-foreground">
35-
<p className="text-base font-semibold text-accent-foreground">
36-
Desde
37-
</p>
38-
<p className="font-medium text-foreground text-base">
39-
S/{product.minPrice} - S/{product.maxPrice}
40-
</p>
41-
</div>
33+
{product.categoryId === 3 ? (
34+
<div className="text-xs text-muted-foreground">
35+
<p className="text-base font-semibold text-accent-foreground">
36+
Desde
37+
</p>
38+
<p className="font-medium text-foreground text-base">
39+
S/{product.minPrice} - S/{product.maxPrice}
40+
</p>
41+
</div>
4242
) : (
43-
<p className="mt-auto text-base font-medium">S/{product.price}</p>
44-
)}
43+
<div className="text-xs text-muted-foreground">
44+
<p className="text-base font-semibold text-accent-foreground">
45+
Precio
46+
</p>
47+
<p className="font-medium text-foreground text-base">
48+
S/{product.price}
49+
</p>
50+
</div>
51+
)}
4552
</div>
4653
{product.isOnSale && (
4754
<span className="absolute top-0 right-0 rounded-bl-xl bg-primary px-2 py-1 text-sm font-medium text-primary-foreground">

src/routes/product/index.tsx

Lines changed: 84 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Form, useNavigation } from "react-router";
2-
import { useState } from "react";
2+
import { useState, useEffect } from "react";
33
import { Button, Container, Separator } from "@/components/ui";
44
import { type Product } from "@/models/product.model";
55
import { getProductById } from "@/services/product.service";
@@ -19,37 +19,54 @@ export default function Product({ loaderData }: Route.ComponentProps) {
1919
const { product } = loaderData;
2020
const navigation = useNavigation();
2121
const cartLoading = navigation.state === "submitting";
22-
const [selectedSize, setSelectedSize] = useState<string>("Medium");
22+
23+
// Estados para manejar variantes
24+
const [selectedVariant, setSelectedVariant] = useState<number | null>(null);
25+
const [currentPrice, setCurrentPrice] = useState<number>(0);
2326

2427
if (!product) {
2528
return <NotFound />;
2629
}
2730

28-
const showSizeSelector = product.categoryId === 1 || product.categoryId === 3;
31+
// Verificar si el producto tiene variantes
32+
const hasVariants = product.variantAttributeValues && product.variantAttributeValues.length > 0;
33+
34+
// Verificar si debe mostrar selectores (solo polos y stickers)
35+
const shouldShowVariants = hasVariants && (product.categoryId === 1 || product.categoryId === 3);
2936

30-
const getSizeOptions = () => {
31-
if (product.categoryId === 3) {
32-
return {
33-
label: "Dimensiones",
34-
options: [
35-
{ value: "Small", label: "3x3 cm" },
36-
{ value: "Medium", label: "5x5 cm" },
37-
{ value: "Large", label: "10x10 cm" }
38-
]
39-
};
37+
// Agrupar variantes por atributo (en caso de que un producto tenga múltiples tipos de atributos)
38+
const variantGroups = shouldShowVariants
39+
? product.variantAttributeValues.reduce((groups, variant) => {
40+
const attributeName = variant.variantAttribute.name;
41+
if (!groups[attributeName]) {
42+
groups[attributeName] = [];
43+
}
44+
groups[attributeName].push(variant);
45+
return groups;
46+
}, {} as Record<string, typeof product.variantAttributeValues>)
47+
: {};
48+
49+
// Inicializar precio y variante seleccionada
50+
useEffect(() => {
51+
if (hasVariants) {
52+
// Seleccionar la primera variante por defecto
53+
const firstVariant = product.variantAttributeValues[0];
54+
setSelectedVariant(firstVariant.id);
55+
setCurrentPrice(firstVariant.price);
4056
} else {
41-
return {
42-
label: "Talla",
43-
options: [
44-
{ value: "Small", label: "Small" },
45-
{ value: "Medium", label: "Medium" },
46-
{ value: "Large", label: "Large" }
47-
]
48-
};
57+
// Si no hay variantes, usar el precio base del producto (asumiendo que existe)
58+
setCurrentPrice(product.price || 0);
4959
}
50-
};
60+
}, [product]);
5161

52-
const sizeOptions = getSizeOptions();
62+
// Manejar cambio de variante
63+
const handleVariantChange = (variantId: number) => {
64+
setSelectedVariant(variantId);
65+
const variant = product.variantAttributeValues.find(v => v.id === variantId);
66+
if (variant) {
67+
setCurrentPrice(variant.price);
68+
}
69+
};
5370

5471
return (
5572
<>
@@ -65,48 +82,67 @@ export default function Product({ loaderData }: Route.ComponentProps) {
6582
<div className="flex-grow flex-basis-0">
6683
<h1 className="text-3xl leading-9 font-bold mb-3">
6784
{product.title}
68-
{showSizeSelector && (
69-
<span className="text-muted-foreground">
70-
{" "}({sizeOptions.options.find(option => option.value === selectedSize)?.label})
71-
</span>
72-
)}
7385
</h1>
74-
<p className="text-3xl leading-9 mb-6">S/{product.price}</p>
86+
87+
{/* Precio dinámico */}
88+
<p className="text-3xl leading-9 mb-6">
89+
S/{currentPrice.toFixed(2)}
90+
</p>
91+
7592
<p className="text-sm leading-5 text-muted-foreground mb-10">
7693
{product.description}
7794
</p>
7895

79-
{showSizeSelector && (
80-
<div className="mb-9">
81-
<p className="text-sm font-semibold text-accent-foreground mb-2">{sizeOptions.label}</p>
82-
<div className="flex gap-2">
83-
{sizeOptions.options.map((option) => (
84-
<Button
85-
key={option.value}
86-
variant={selectedSize === option.value ? "default" : "secondary"}
87-
size="lg"
88-
onClick={() => setSelectedSize(option.value)}
89-
>
90-
{option.label}
91-
</Button>
92-
))}
93-
</div>
94-
</div>
96+
{/* Selectores de variantes dinámicos - solo para polos y stickers */}
97+
{shouldShowVariants && (
98+
<>
99+
{Object.entries(variantGroups).map(([attributeName, variants]) => (
100+
<div key={attributeName} className="mb-9">
101+
<p className="text-sm font-semibold text-accent-foreground mb-2">
102+
{attributeName.charAt(0).toUpperCase() + attributeName.slice(1)}
103+
</p>
104+
<div className="flex gap-2">
105+
{variants.map((variant) => (
106+
<Button
107+
key={variant.id}
108+
variant={selectedVariant === variant.id ? "default" : "secondary"}
109+
size="lg"
110+
onClick={() => handleVariantChange(variant.id)}
111+
>
112+
{variant.value}
113+
</Button>
114+
))}
115+
</div>
116+
</div>
117+
))}
118+
</>
95119
)}
96120

121+
{/* Formulario actualizado para enviar variante seleccionada */}
97122
<Form method="post" action="/cart/add-item">
98123
<input
99124
type="hidden"
100125
name="redirectTo"
101126
value={`/products/${product.id}`}
102127
/>
128+
<input
129+
type="hidden"
130+
name="productId"
131+
value={product.id}
132+
/>
133+
{/* Enviar la variante seleccionada si existe y debe mostrar variantes */}
134+
{shouldShowVariants && selectedVariant && (
135+
<input
136+
type="hidden"
137+
name="variantId"
138+
value={selectedVariant}
139+
/>
140+
)}
103141
<Button
104142
size="xl"
105143
className="w-full md:w-80"
106144
type="submit"
107-
name="productId"
108-
value={product.id}
109-
disabled={cartLoading}
145+
disabled={cartLoading || (shouldShowVariants && !selectedVariant)}
110146
>
111147
{cartLoading ? "Agregando..." : "Agregar al Carrito"}
112148
</Button>

0 commit comments

Comments
 (0)