Skip to content

Commit 8a245d2

Browse files
committed
feat(cart item): agregamos logica para mostrar la variante, precio con la variante en el carrito, el total tambien refleje la variante del producto, se mantiene todo el flujo funcional de eliminar cart item, eliminar producto, agregar producto de manera consistente, para usuarios visitantes y usuarios logeados
1 parent 640cd78 commit 8a245d2

File tree

9 files changed

+303
-144
lines changed

9 files changed

+303
-144
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/*
2+
Warnings:
3+
4+
- Added the required column `final_price` to the `cart_items` table without a default value. This is not possible if the table is not empty.
5+
6+
*/
7+
-- AlterTable
8+
ALTER TABLE "cart_items" ADD COLUMN "final_price" DECIMAL(10,2) NOT NULL;

prisma/schema.prisma

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ model CartItem {
111111
productId Int @map("product_id")
112112
categoryVariantId Int? @map("category_variant_id") // Variante seleccionada
113113
quantity Int
114+
finalPrice Decimal @map("final_price") @db.Decimal(10, 2)// Price final
114115
createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(0)
115116
updatedAt DateTime @default(now()) @map("updated_at") @db.Timestamp(0)
116117

src/lib/cart.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,16 @@ export async function addToCart(
1919
userId: number | undefined,
2020
sessionCartId: string | undefined,
2121
productId: Product["id"],
22-
quantity: number = 1
22+
quantity: number = 1,
23+
categoryVariantId: number | null = null
2324
) {
2425
try {
2526
const updatedCart = await alterQuantityCartItem(
2627
userId,
2728
sessionCartId,
2829
productId,
29-
quantity
30+
quantity,
31+
categoryVariantId
3032
);
3133
return updatedCart;
3234
} catch (error) {
@@ -60,9 +62,9 @@ export function calculateTotal(items: CartItemInput[]): number;
6062
export function calculateTotal(items: CartItem[] | CartItemInput[]): number {
6163
return items.reduce((total, item) => {
6264
// Type guard to determine which type we're working with
63-
if ("product" in item) {
65+
if ("finalPrice" in item) {
6466
// CartItem - has a product property
65-
return total + item.product.price * item.quantity;
67+
return total + item.finalPrice * item.quantity;
6668
} else {
6769
// CartItemInput - has price directly
6870
return total + item.price * item.quantity;

src/models/cart.model.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,30 @@ import type {
55
CartItem as PrismaCartItem,
66
} from "@/../generated/prisma/client";
77

8-
export type CartItem = PrismaCartItem & {
8+
export type Cart = PrismaCart;
9+
10+
export type CartItem = {
11+
id: number;
12+
cartId: number;
13+
productId: number;
14+
categoryVariantId: number | null;
15+
quantity: number;
16+
finalPrice: number; // ← number, no Decimal
17+
createdAt: Date;
18+
updatedAt: Date;
19+
// Campos adicionales transformados
920
product: Pick<
1021
Product,
1122
"id" | "title" | "imgSrc" | "alt" | "price" | "isOnSale"
1223
>;
24+
categoryVariant?: {
25+
id: number;
26+
label: string;
27+
value: string;
28+
priceModifier: number;
29+
} | null;
1330
};
1431

15-
export type Cart = PrismaCart;
16-
1732
export interface CartItemInput {
1833
productId: Product["id"];
1934
quantity: number;
@@ -33,6 +48,8 @@ export type CartProductInfo = Pick<
3348
export type CartItemWithProduct = {
3449
product: CartProductInfo;
3550
quantity: number;
51+
categoryVariantId: number | null;
52+
finalPrice: number;
3653
};
3754

3855
// Tipo para el carrito con items y productos incluidos

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,22 @@ import type { Route } from "../+types";
88
export async function action({ request }: Route.ActionArgs) {
99
const formData = await request.formData();
1010
const productId = Number(formData.get("productId"));
11+
const categoryVariantId = formData.get("categoryVariantId")
12+
? Number(formData.get("categoryVariantId"))
13+
: null;
1114
const quantity = Number(formData.get("quantity")) || 1;
1215
const redirectTo = formData.get("redirectTo") as string | null;
1316
const session = await getSession(request.headers.get("Cookie"));
1417
const sessionCartId = session.get("sessionCartId");
1518
const userId = session.get("userId");
1619

17-
await addToCart(userId, sessionCartId, productId, quantity);
20+
await addToCart(
21+
userId,
22+
sessionCartId,
23+
productId,
24+
quantity,
25+
categoryVariantId
26+
);
1827

1928
return redirect(redirectTo || "/cart");
2029
}

src/routes/cart/index.tsx

Lines changed: 98 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -23,82 +23,127 @@ export async function loader({ request }: Route.LoaderArgs) {
2323
export default function Cart({ loaderData }: Route.ComponentProps) {
2424
const { cart, total } = loaderData;
2525

26+
// ✅ AGREGAR: Verificación para carrito vacío
27+
if (!cart || !cart.items || cart.items.length === 0) {
28+
return (
29+
<Section>
30+
<Container className="max-w-xl">
31+
<h1 className="text-3xl leading-8 font-bold text-center mb-12">
32+
Carrito de compras
33+
</h1>
34+
<div className="border-solid border rounded-xl flex flex-col p-6 text-center">
35+
<p className="text-muted-foreground mb-4">Tu carrito está vacío</p>
36+
<Button size="lg" className="w-full" asChild>
37+
<Link to="/">Ir a la tienda</Link>
38+
</Button>
39+
</div>
40+
</Container>
41+
</Section>
42+
);
43+
}
44+
2645
return (
2746
<Section>
2847
<Container className="max-w-xl">
2948
<h1 className="text-3xl leading-8 font-bold text-center mb-12">
3049
Carrito de compras
3150
</h1>
3251
<div className="border-solid border rounded-xl flex flex-col">
33-
{cart?.items?.map(({ product, quantity, id }) => (
34-
<div key={product.id} className="flex gap-7 p-6 border-b">
35-
<div className="w-20 rounded-xl bg-muted">
36-
<img
37-
src={product.imgSrc}
38-
alt={product.alt || product.title}
39-
className="w-full aspect-[2/3] object-contain"
40-
/>
41-
</div>
42-
<div className="flex grow flex-col justify-between">
43-
<div className="flex gap-4 justify-between items-center">
44-
<h2 className="text-sm">{product.title}</h2>
45-
<Form method="post" action="/cart/remove-item">
46-
<Button
47-
size="sm-icon"
48-
variant="outline"
49-
name="itemId"
50-
value={id}
51-
>
52-
<Trash2 />
53-
</Button>
54-
</Form>
52+
{cart.items.map(
53+
({ product, quantity, id, finalPrice, categoryVariant }) => (
54+
<div
55+
key={`${product.id}-${categoryVariant?.id || "no-variant"}`}
56+
className="flex gap-7 p-6 border-b"
57+
>
58+
<div className="w-20 rounded-xl bg-muted">
59+
<img
60+
src={product.imgSrc}
61+
alt={product.alt || product.title}
62+
className="w-full aspect-[2/3] object-contain"
63+
/>
5564
</div>
56-
<div className="flex flex-col gap-2 md:flex-row md:justify-between md:items-center">
57-
<p className="text-sm font-medium">
58-
${product.price.toFixed(2)}
59-
</p>
60-
<div className="flex gap-4 items-center">
61-
<Form method="post" action="/cart/add-item">
62-
<input type="hidden" name="quantity" value="-1" />
65+
<div className="flex grow flex-col justify-between">
66+
<div className="flex gap-4 justify-between items-center">
67+
<div className="flex items-center gap-2">
68+
<h2 className="text-sm">{product.title}</h2>
69+
{/* ✅ CORREGIDO: Mejor layout para la variante */}
70+
{categoryVariant && (
71+
<p className="text-sm">({categoryVariant.label})</p>
72+
)}
73+
</div>
74+
<Form method="post" action="/cart/remove-item">
6375
<Button
64-
name="productId"
65-
value={product.id}
66-
variant="outline"
6776
size="sm-icon"
68-
disabled={quantity === 1}
69-
>
70-
<Minus />
71-
</Button>
72-
</Form>
73-
<span className="h-8 w-8 flex justify-center items-center border rounded-md py-2 px-4">
74-
{quantity}
75-
</span>
76-
<Form method="post" action="/cart/add-item">
77-
<Button
7877
variant="outline"
79-
size="sm-icon"
80-
name="productId"
81-
value={product.id}
78+
name="itemId"
79+
value={id}
80+
type="submit"
8281
>
83-
<Plus />
82+
<Trash2 />
8483
</Button>
8584
</Form>
8685
</div>
86+
<div className="flex flex-col gap-2 md:flex-row md:justify-between md:items-center">
87+
<p className="text-sm font-medium">
88+
S/{finalPrice.toFixed(2)}
89+
</p>
90+
<div className="flex gap-4 items-center">
91+
<Form method="post" action="/cart/add-item">
92+
<input type="hidden" name="quantity" value="-1" />
93+
<input
94+
type="hidden"
95+
name="productId"
96+
value={product.id}
97+
/>
98+
{categoryVariant && (
99+
<input
100+
type="hidden"
101+
name="categoryVariantId"
102+
value={categoryVariant.id}
103+
/>
104+
)}
105+
<Button
106+
type="submit"
107+
variant="outline"
108+
size="sm-icon"
109+
disabled={quantity === 1}
110+
>
111+
<Minus />
112+
</Button>
113+
</Form>
114+
<span className="h-8 w-8 flex justify-center items-center border rounded-md py-2 px-4">
115+
{quantity}
116+
</span>
117+
<Form method="post" action="/cart/add-item">
118+
<input
119+
type="hidden"
120+
name="productId"
121+
value={product.id}
122+
/>
123+
{categoryVariant && (
124+
<input
125+
type="hidden"
126+
name="categoryVariantId"
127+
value={categoryVariant.id}
128+
/>
129+
)}
130+
<Button type="submit" variant="outline" size="sm-icon">
131+
<Plus />
132+
</Button>
133+
</Form>
134+
</div>
135+
</div>
87136
</div>
88137
</div>
89-
</div>
90-
))}
138+
)
139+
)}
91140
<div className="flex justify-between p-6 text-base font-medium border-b">
92141
<p>Total</p>
93142
<p>S/{total.toFixed(2)}</p>
94143
</div>
95144
<div className="p-6">
96145
<Button size="lg" className="w-full" asChild>
97-
{cart?.items && cart.items.length > 0 ? (
98-
<Link to="/checkout">Continuar Compra</Link>
99-
) : (
100-
<Link to="/">Ir a la tienda</Link>
101-
)}
146+
<Link to="/checkout">Continuar Compra</Link>
102147
</Button>
103148
</div>
104149
</div>

src/routes/product/index.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,6 @@ export default function Product({ loaderData }: Route.ComponentProps) {
134134
name="redirectTo"
135135
value={`/products/${product.id}`}
136136
/>
137-
<input type="hidden" name="productId" value={product.id} />
138137
{selectedVariant && (
139138
<input
140139
type="hidden"

src/routes/root/index.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,10 @@ export async function loader({ request }: Route.LoaderArgs) {
5454
if (!user && !sessionCartId) {
5555
try {
5656
cart = await createRemoteItems(undefined, undefined, []);
57-
const cartId = cart.sessionCartId;
58-
if (cartId) {
59-
session.set("sessionCartId", cartId);
57+
58+
if (cart) {
59+
session.set("sessionCartId", cart.sessionCartId);
60+
session.set("userId", cart.userId ?? undefined);
6061
}
6162
} catch (error) {
6263
console.error("Error al crear carrito de invitado:", error);

0 commit comments

Comments
 (0)