diff --git a/prisma/initial_data.ts b/prisma/initial_data.ts index 6b45206..1e8b763 100644 --- a/prisma/initial_data.ts +++ b/prisma/initial_data.ts @@ -42,6 +42,17 @@ export const productVariant = [ { productTitle: "Polo It Works On My Machine", sizes: ["small", "medium", "large"] }, ]; +export const stickersVariant = [ + { productTitle: "Sticker JavaScript", measure: ["3*3", "5*5", "10*10"] }, + { productTitle: "Sticker React", measure: ["3*3", "5*5", "10*10"] }, + { productTitle: "Sticker Git", measure: ["3*3", "5*5", "10*10"] }, + { productTitle: "Sticker Docker", measure: ["3*3", "5*5", "10*10"] }, + { productTitle: "Sticker Linux", measure: ["3*3", "5*5", "10*10"] }, + { productTitle: "Sticker VS Code", measure: ["3*3", "5*5", "10*10"] }, + { productTitle: "Sticker GitHub", measure: ["3*3", "5*5", "10*10"] }, + { productTitle: "Sticker HTML", measure: ["3*3", "5*5", "10*10"] }, + ]; + export const products = [ { title: "Polo React", diff --git a/prisma/migrations/20250822231633_stickers/migration.sql b/prisma/migrations/20250822231633_stickers/migration.sql new file mode 100644 index 0000000..572e32a --- /dev/null +++ b/prisma/migrations/20250822231633_stickers/migration.sql @@ -0,0 +1,29 @@ +/* + Warnings: + + - A unique constraint covering the columns `[cartId,productId,productVariantId,stickersVariantId]` on the table `cart_items` will be added. If there are existing duplicate values, this will fail. + +*/ +-- DropIndex +DROP INDEX "cart_items_cartId_productId_productVariantId_key"; + +-- AlterTable +ALTER TABLE "cart_items" ADD COLUMN "stickersVariantId" INTEGER; + +-- CreateTable +CREATE TABLE "stickersVariant" ( + "id" SERIAL NOT NULL, + "productId" INTEGER NOT NULL, + "measure" TEXT NOT NULL, + + CONSTRAINT "stickersVariant_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "cart_items_cartId_productId_productVariantId_stickersVarian_key" ON "cart_items"("cartId", "productId", "productVariantId", "stickersVariantId"); + +-- AddForeignKey +ALTER TABLE "stickersVariant" ADD CONSTRAINT "stickersVariant_productId_fkey" FOREIGN KEY ("productId") REFERENCES "products"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "cart_items" ADD CONSTRAINT "cart_items_stickersVariantId_fkey" FOREIGN KEY ("stickersVariantId") REFERENCES "stickersVariant"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/migrations/20250825232113_add_price/migration.sql b/prisma/migrations/20250825232113_add_price/migration.sql new file mode 100644 index 0000000..8c92e7e --- /dev/null +++ b/prisma/migrations/20250825232113_add_price/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - Added the required column `price` to the `cart_items` table without a default value. This is not possible if the table is not empty. + - Added the required column `price` to the `stickersVariant` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "cart_items" ADD COLUMN "price" DECIMAL(10,2) NOT NULL; + +-- AlterTable +ALTER TABLE "stickersVariant" ADD COLUMN "price" DECIMAL(10,2) NOT NULL; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 207b342..541249e 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -63,6 +63,7 @@ model Product { createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(0) updatedAt DateTime @default(now()) @map("updated_at") @db.Timestamp(0) variants ProductVariant[] + stickersVariants stickersVariant[] category Category? @relation(fields: [categoryId], references: [id], onDelete: SetNull) cartItems CartItem[] @@ -80,6 +81,16 @@ model ProductVariant { cartItems CartItem[] @relation("CartItemToProductVariant") } +model stickersVariant { + id Int @id @default(autoincrement()) + product Product @relation(fields: [productId], references: [id]) + productId Int + measure String // '3*3', '5*5', '10*10' + price Decimal @db.Decimal(10, 2) + + cartItems CartItem[] @relation("CartItemTostickersVariant") +} + model Cart { id Int @id @default(autoincrement()) sessionCartId String @unique @default(dbgenerated("gen_random_uuid()")) @map("session_cart_id") @db.Uuid @@ -98,6 +109,8 @@ model CartItem { cartId Int productId Int productVariantId Int? + stickersVariantId Int? + price Decimal @db.Decimal(10, 2) quantity Int createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(0) updatedAt DateTime @default(now()) @map("updated_at") @db.Timestamp(0) @@ -105,8 +118,9 @@ model CartItem { cart Cart @relation(fields: [cartId], references: [id], onDelete: Cascade) product Product @relation(fields: [productId], references: [id], onDelete: Cascade) productVariant ProductVariant? @relation("CartItemToProductVariant", fields: [productVariantId], references: [id]) + stickersVariant stickersVariant? @relation("CartItemTostickersVariant", fields: [stickersVariantId], references: [id]) - @@unique([cartId, productId, productVariantId], name: "unique_cart_item") + @@unique([cartId, productId, productVariantId, stickersVariantId], name: "unique_cart_item") @@map("cart_items") } diff --git a/prisma/seed.ts b/prisma/seed.ts index c40dd8c..414c9f5 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -5,10 +5,13 @@ const prisma = new PrismaClient(); // Define las tallas para los productos tipo "Polo" const poloSizes = ["small", "medium", "large"] as const; +// Define el tamaño para los productos tipo "Stickers" +const stickerMeasures = ["3*3", "5*5", "10*10"] as const; async function seedDb() { // Limpia las tablas para evitar duplicados await prisma.productVariant.deleteMany(); + await prisma.stickersVariant.deleteMany(); await prisma.product.deleteMany(); await prisma.category.deleteMany(); @@ -46,6 +49,35 @@ async function seedDb() { } console.log("3. Polo variants successfully inserted"); } + + // Obtiene los productos tipo "Stickers" para agregar variantes + const stickersCategory = await prisma.category.findUnique({ + where: { slug: "stickers" }, + }); + if (stickersCategory) { + const stickers = await prisma.product.findMany({ + where: { categoryId: stickersCategory.id }, + }); + + const stickerPrices: Record = { + "3*3": 2.99, + "5*5": 5.99, + "10*10": 8.99, + }; + + for (const sticker of stickers) { + for (const measure of stickerMeasures) { + await prisma.stickersVariant.create({ + data: { + productId: sticker.id, + measure, + price: stickerPrices[measure], // Asigna el precio según la medida + }, + }); + } + } + console.log("4. Stickers variants successfully inserted"); + } } seedDb() diff --git a/src/components/product/VariantSelector.tsx b/src/components/product/VariantSelector.tsx new file mode 100644 index 0000000..880c39e --- /dev/null +++ b/src/components/product/VariantSelector.tsx @@ -0,0 +1,46 @@ +type VariantOption = { + id: number | string; + label: string; + value: string; +}; + +type VariantSelectorProps = { + label: string; + name: string; + options: VariantOption[]; + selectedValue: string; + onSelect: (value: string) => void; +}; + +export function VariantSelector({ + label, + name, + options, + selectedValue, + onSelect, +}: VariantSelectorProps) { + return ( +
+ +
+ {options.map(option => ( + + ))} +
+ {/* input oculto para enviar la opción seleccionada */} + +
+ ); +} + diff --git a/src/lib/cart.ts b/src/lib/cart.ts index d8cb443..60828db 100644 --- a/src/lib/cart.ts +++ b/src/lib/cart.ts @@ -20,7 +20,8 @@ export async function addToCart( sessionCartId: string | undefined, productId: Product["id"], quantity: number = 1, - productVariantId?: number + productVariantId?: number, + stickersVariantId?: number ) { try { const updatedCart = await alterQuantityCartItem( @@ -28,7 +29,8 @@ export async function addToCart( sessionCartId, productId, quantity, - productVariantId + productVariantId, + stickersVariantId ); return updatedCart; } catch (error) { diff --git a/src/models/cart.model.ts b/src/models/cart.model.ts index 5905251..0a01af7 100644 --- a/src/models/cart.model.ts +++ b/src/models/cart.model.ts @@ -11,6 +11,10 @@ export type CartItem = PrismaCartItem & { id: number; size: "small" | "medium" | "large"; } | null; + stickersVariant?: { + id: number; + measure: "3*3" | "5*5" | "10*10"; + } | null; }; export type Cart = PrismaCart; @@ -35,6 +39,8 @@ export type CartItemWithProduct = { product: CartProductInfo; quantity: number; productVariantId: number | null; + stickersVariantId: number | null; + price: number; }; // Tipo para el carrito con items y productos incluidos diff --git a/src/models/product.model.ts b/src/models/product.model.ts index ca78f1a..0b9ee87 100644 --- a/src/models/product.model.ts +++ b/src/models/product.model.ts @@ -3,6 +3,13 @@ import type { Product as PrismaProduct } from "@/../generated/prisma/client"; export type Product = Omit & { price: number; variants?: ProductVariant[]; + stickersVariants?: StickersVariant[]; +}; + +export type StickersVariant = { + id: number; + measure: "3*3" | "5*5" | "10*10"; + price: number; }; export interface ProductVariant { id: number; diff --git a/src/routes/cart/add-item/index.tsx b/src/routes/cart/add-item/index.tsx index 4504a4e..139dd09 100644 --- a/src/routes/cart/add-item/index.tsx +++ b/src/routes/cart/add-item/index.tsx @@ -10,12 +10,14 @@ export async function action({ request }: Route.ActionArgs) { const productId = Number(formData.get("productId")); const quantity = Number(formData.get("quantity")) || 1; const size = formData.get("size") as string | undefined; + const measure = formData.get("measure") as string | undefined; const redirectTo = formData.get("redirectTo") as string | null; const session = await getSession(request.headers.get("Cookie")); const sessionCartId = session.get("sessionCartId"); const userId = session.get("userId"); let productVariantId: number | undefined = undefined; + let stickersVariantId: number | undefined = undefined; // Si hay talla, busca el variant correspondiente if (size) { @@ -34,7 +36,24 @@ export async function action({ request }: Route.ActionArgs) { productVariantId = variant.id; } - await addToCart(userId, sessionCartId, productId, quantity, productVariantId); + // Si hay medida, busca el variant correspondiente + if (measure) { + const variantMeasure = await prisma.stickersVariant.findFirst({ + where: { + productId, + measure, + }, + }); + if (!variantMeasure) { + return new Response( + JSON.stringify({ error: "No se encontró la variante de stickers seleccionada." }), + { status: 400, headers: { "Content-Type": "application/json" } } + ); + } + stickersVariantId = variantMeasure.id; + } + + await addToCart(userId, sessionCartId, productId, quantity, productVariantId, stickersVariantId); return redirect(redirectTo || "/cart"); } \ No newline at end of file diff --git a/src/routes/cart/index.tsx b/src/routes/cart/index.tsx index a54f8f4..cdc2c7c 100644 --- a/src/routes/cart/index.tsx +++ b/src/routes/cart/index.tsx @@ -31,7 +31,8 @@ export default function Cart({ loaderData }: Route.ComponentProps) { Carrito de compras
- {cart?.items?.map(({ product, quantity, id, productVariant }) => ( + {cart?.items?.map( + ({ product, quantity, id, productVariant, stickersVariant }) => (
)} + {stickersVariant && ( + + ({stickersVariant.measure}) + + )}
- ))} + ) + )}

Total

S/{total.toFixed(2)}

diff --git a/src/routes/checkout/index.tsx b/src/routes/checkout/index.tsx index b4e1fff..25ad4bd 100644 --- a/src/routes/checkout/index.tsx +++ b/src/routes/checkout/index.tsx @@ -250,7 +250,8 @@ export default function Checkout({

Resumen de la orden

- {cart?.items?.map(({ product, quantity, id, productVariant }) => ( + {cart?.items?.map( + ({ product, quantity, id, productVariant, stickersVariant }) => (
({productVariant.size}) - )} - + )} + {stickersVariant && ( + + ({stickersVariant.measure}) + + )} +

{quantity}

@@ -278,7 +284,8 @@ export default function Checkout({
- ))} + ) + )}

Total

S/{total.toFixed(2)}

diff --git a/src/routes/product/index.tsx b/src/routes/product/index.tsx index 7ed5c53..88bbffd 100644 --- a/src/routes/product/index.tsx +++ b/src/routes/product/index.tsx @@ -1,8 +1,9 @@ import { useState } from "react"; import { Form, useNavigation } from "react-router"; +import { VariantSelector } from "@/components/product/VariantSelector"; import { Button, Container, Separator } from "@/components/ui"; -import { type Product } from "@/models/product.model"; +import { capitalize } from "@/lib/utils"; import { getProductById } from "@/services/product.service"; import NotFound from "../not-found"; @@ -28,10 +29,21 @@ export default function Product({ loaderData }: Route.ComponentProps) { product?.variants?.[0]?.size ?? "" ); + // Si el producto tiene variantes de stickers, selecciona la primera por defecto + const [selectedMeasure, setSelectedMeasure] = useState( + product?.stickersVariants?.[0]?.measure ?? "" + ); + if (!product) { return ; } + let displayedPrice = product.price; + + if (selectedMeasure) { + displayedPrice = product.stickersVariants?.find(v => v.measure === selectedMeasure)?.price || product.price; + } + return ( <>
@@ -47,7 +59,7 @@ export default function Product({ loaderData }: Route.ComponentProps) {

{product.title}

-

S/{product.price}

+

S/{displayedPrice}

{product.description}

@@ -59,28 +71,33 @@ export default function Product({ loaderData }: Route.ComponentProps) { /> {/* Botones de talla si hay variantes */} {product.variants && product.variants.length > 0 && ( -
- -
- {product.variants.map(variant => ( - - ))} -
- {/* input oculto para enviar la talla seleccionada */} - -
+ ({ + id: variant.id, + label: capitalize(variant.size), + value: variant.size , + }))} + selectedValue={selectedSize} + onSelect={setSelectedSize} + /> + )} + {/* Botones de medida si hay variantes de stickers */} + {product.stickersVariants && product.stickersVariants.length > 0 && ( + ({ + id: variant.id, + label: variant.measure, + value: variant.measure , + }))} + selectedValue={selectedMeasure} + onSelect={setSelectedMeasure} + /> )} + {/* Botón de agregar al carrito */}