Skip to content

Commit d86c6c9

Browse files
author
Kellyarias02
committed
Improve variant handling for sizes M and L in both frontend and backend
1 parent d8699c5 commit d86c6c9

File tree

8 files changed

+170
-44
lines changed

8 files changed

+170
-44
lines changed

api/gemini-chat.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import express from "express";
2+
import { GoogleGenAI } from "@google/genai";
3+
4+
const router = express.Router();
5+
const ai = new GoogleGenAI({ apiKey: process.env.GOOGLE_API_KEY || "" });
6+
7+
router.post("/", async (req, res) => {
8+
const { message } = req.body;
9+
try {
10+
const chat = ai.chats.create({ model: "gemini-2.5-flash", history: [] });
11+
const response = await chat.sendMessage({ message });
12+
res.json({ text: response.text });
13+
} catch (err) {
14+
res.status(500).json({ error: "Error al contactar a Gemini" });
15+
}
16+
});
17+
18+
export default router;

api/index.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import express from "express";
2+
import cors from "cors";
3+
import geminiChatRouter from "./gemini-chat.js";
4+
5+
const app = express();
6+
app.use(express.json());
7+
app.use("/api/gemini-chat", geminiChatRouter);
8+
app.use(cors());
9+
10+
app.listen(3001, () => {
11+
console.log("API server listening on http://localhost:3001");
12+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/*
2+
Warnings:
3+
4+
- A unique constraint covering the columns `[cartId,productId,productVariantId]` on the table `cart_items` will be added. If there are existing duplicate values, this will fail.
5+
6+
*/
7+
-- DropIndex
8+
DROP INDEX "cart_items_cartId_productId_key";
9+
10+
-- CreateIndex
11+
CREATE UNIQUE INDEX "cart_items_cartId_productId_productVariantId_key" ON "cart_items"("cartId", "productId", "productVariantId");

prisma/schema.prisma

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ model CartItem {
106106
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
107107
productVariant ProductVariant? @relation("CartItemToProductVariant", fields: [productVariantId], references: [id])
108108
109-
@@unique([cartId, productId], name: "unique_cart_item")
109+
@@unique([cartId, productId, productVariantId], name: "unique_cart_item")
110110
@@map("cart_items")
111111
}
112112

src/routes/cart/add-item/index.tsx

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,30 +9,18 @@ export async function action({ request }: Route.ActionArgs) {
99
const formData = await request.formData();
1010
const productId = Number(formData.get("productId"));
1111
const quantity = Number(formData.get("quantity")) || 1;
12-
const size = formData.get("size") as string | undefined;
12+
//const size = formData.get("size") as string | undefined;
1313
const redirectTo = formData.get("redirectTo") as string | null;
1414
const session = await getSession(request.headers.get("Cookie"));
1515
const sessionCartId = session.get("sessionCartId");
1616
const userId = session.get("userId");
1717

18-
let productVariantId: number | undefined = undefined;
18+
const variantId = formData.get("variantId");
19+
let productVariantId: number | undefined = undefined;
1920

20-
// Si hay talla, busca el variant correspondiente
21-
if (size) {
22-
const variant = await prisma.productVariant.findFirst({
23-
where: {
24-
productId,
25-
size,
26-
},
27-
});
28-
if (!variant) {
29-
return new Response(
30-
JSON.stringify({ error: "No se encontró la variante seleccionada." }),
31-
{ status: 400, headers: { "Content-Type": "application/json" } }
32-
);
33-
}
34-
productVariantId = variant.id;
35-
}
21+
if (variantId) {
22+
productVariantId = Number(variantId);
23+
}
3624

3725
await addToCart(userId, sessionCartId, productId, quantity, productVariantId);
3826

src/routes/home/HomeGemini.tsx

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { useEffect, useState } from "react";
2+
import { sendGeminiMessage } from "../../services/gemini.service";
3+
4+
export const HomeGemini = () => {
5+
const [messages, setMessages] = useState<{ from: "user" | "ai"; text: string }[]>([
6+
{
7+
from: "ai",
8+
text: "¡Bienvenido a Fullstock! ¿En qué puedo ayudarte hoy? Puedes preguntarme sobre productos, envíos, o cualquier duda que tengas.",
9+
},
10+
]);
11+
const [input, setInput] = useState("");
12+
const [loading, setLoading] = useState(false);
13+
14+
15+
16+
const handleSubmit = async (e: React.FormEvent) => {
17+
e.preventDefault();
18+
if (!input.trim()) return;
19+
const userMsg = input.trim();
20+
setMessages((msgs) => [...msgs, { from: "user", text: userMsg }]);
21+
setInput("");
22+
setLoading(true);
23+
try {
24+
const aiText = await sendGeminiMessage(userMsg);
25+
setMessages((msgs) => [...msgs, { from: "ai", text: aiText }]);
26+
} catch (err) {
27+
setMessages((msgs) => [
28+
...msgs,
29+
{ from: "ai", text: "Ocurrió un error al contactar a Gemini." },
30+
]);
31+
} finally {
32+
setLoading(false);
33+
}
34+
};
35+
36+
return (
37+
<div className="fixed bottom-6 right-6 z-50 border border-gray-300 rounded-lg p-4 max-w-[350px] w-full bg-background shadow-xl">
38+
<h1 className="text-lg font-bold mb-2">Gemini AI Chat</h1>
39+
<div className="min-h-[120px] mb-2 overflow-y-auto bg-gray-50 p-2 rounded">
40+
{messages.map((msg, i) => (
41+
<div key={i} className={msg.from === "user" ? "text-right" : "text-left"}>
42+
<b>{msg.from === "user" ? "Tú" : "Gemini"}:</b> {msg.text}
43+
</div>
44+
))}
45+
{loading && (
46+
<div className="text-left text-gray-400">
47+
<b>Gemini:</b> ...
48+
</div>
49+
)}
50+
</div>
51+
<form onSubmit={handleSubmit} className="flex gap-2">
52+
<input
53+
value={input}
54+
onChange={(e) => setInput(e.target.value)}
55+
placeholder="Escribe tu mensaje..."
56+
className="flex-1 px-2 py-1 rounded border border-gray-300"
57+
disabled={loading}
58+
/>
59+
<button
60+
type="submit"
61+
disabled={loading || !input.trim()}
62+
className="px-3 py-1 rounded bg-blue-600 text-white disabled:bg-gray-400"
63+
>
64+
Enviar
65+
</button>
66+
</form>
67+
</div>
68+
);
69+
};

src/routes/product/index.tsx

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ export default function Product({ loaderData }: Route.ComponentProps) {
2424
const cartLoading = navigation.state === "submitting";
2525

2626
// Si el producto tiene variantes, selecciona la primera por defecto
27-
const [selectedSize, setSelectedSize] = useState(
28-
product?.variants?.[0]?.size ?? ""
29-
);
27+
const [selectedVariantId, setSelectedVariantId] = useState(
28+
product?.variants?.[0]?.id ?? ""
29+
);
3030

3131
if (!product) {
3232
return <NotFound />;
@@ -59,28 +59,28 @@ export default function Product({ loaderData }: Route.ComponentProps) {
5959
/>
6060
{/* Botones de talla si hay variantes */}
6161
{product.variants && product.variants.length > 0 && (
62-
<div className="mb-4">
63-
<label className="block mb-2 font-medium">Talla</label>
64-
<div className="flex gap-2">
65-
{product.variants.map(variant => (
66-
<button
67-
type="button"
68-
key={variant.id}
69-
className={`px-4 py-2 rounded border ${
70-
selectedSize === variant.size
71-
? "bg-primary text-white border-primary"
72-
: "bg-white text-black border-gray-300"
73-
}`}
74-
onClick={() => setSelectedSize(variant.size)}
75-
>
76-
{variant.size.charAt(0).toUpperCase() + variant.size.slice(1)}
77-
</button>
78-
))}
79-
</div>
80-
{/* input oculto para enviar la talla seleccionada */}
81-
<input type="hidden" name="size" value={selectedSize} />
82-
</div>
83-
)}
62+
<div className="mb-4">
63+
<label className="block mb-2 font-medium">Talla</label>
64+
<div className="flex gap-2">
65+
{product.variants.map(variant => (
66+
<button
67+
type="button"
68+
key={variant.id}
69+
className={`px-4 py-2 rounded border ${
70+
selectedVariantId === variant.id
71+
? "bg-primary text-white border-primary"
72+
: "bg-white text-black border-gray-300"
73+
}`}
74+
onClick={() => setSelectedVariantId(variant.id)}
75+
>
76+
{variant.size.charAt(0).toUpperCase() + variant.size.slice(1)}
77+
</button>
78+
))}
79+
</div>
80+
{/* input oculto para enviar el id del variant seleccionado */}
81+
<input type="hidden" name="variantId" value={selectedVariantId} />
82+
</div>
83+
)}
8484
<Button
8585
size="xl"
8686
className="w-full md:w-80"

src/services/gemini.service.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { GoogleGenAI } from "@google/genai";
2+
3+
const ai = new GoogleGenAI({
4+
apiKey: import.meta.env.VITE_GOOGLE_API_KEY || "",
5+
});
6+
7+
let chatInstance: any = null;
8+
9+
export function getChatInstance() {
10+
if (!chatInstance) {
11+
chatInstance = ai.chats.create({
12+
model: "gemini-2.5-flash",
13+
history: [],
14+
});
15+
}
16+
return chatInstance;
17+
}
18+
19+
export async function sendGeminiMessage(message: string) {
20+
const res = await fetch("/api/gemini-chat", {
21+
method: "POST",
22+
headers: { "Content-Type": "application/json" },
23+
body: JSON.stringify({ message }),
24+
});
25+
if (!res.ok) throw new Error("Error al contactar a Gemini");
26+
const data = await res.json();
27+
return data.text;
28+
}

0 commit comments

Comments
 (0)