diff --git a/prisma/initial_data.ts b/prisma/initial_data.ts index 0520e9e..3ab5380 100644 --- a/prisma/initial_data.ts +++ b/prisma/initial_data.ts @@ -29,11 +29,16 @@ export const categories = [ }, ]; +export const variantAttributes = [ + { name: "no aplica" }, + { name: "talla" }, + { name: "dimensiones" }, +] + export const products = [ { title: "Polo React", imgSrc: `${imagesBaseUrl}/polos/polo-react.png`, - price: 20.0, description: "Viste tu pasión por React con estilo y comodidad en cada línea de código.", categoryId: 1, @@ -48,7 +53,6 @@ export const products = [ { title: "Polo JavaScript", imgSrc: `${imagesBaseUrl}/polos/polo-js.png`, - price: 20.0, description: "Deja que tu amor por JavaScript hable a través de cada hilo de este polo.", categoryId: 1, @@ -63,7 +67,6 @@ export const products = [ { title: "Polo Node.js", imgSrc: `${imagesBaseUrl}/polos/polo-node.png`, - price: 20.0, description: "Conéctate al estilo con este polo de Node.js, tan robusto como tu código.", categoryId: 1, @@ -78,7 +81,6 @@ export const products = [ { title: "Polo TypeScript", imgSrc: `${imagesBaseUrl}/polos/polo-ts.png`, - price: 20.0, description: "Tipa tu estilo con precisión: lleva tu pasión por TypeScript en cada hilo.", categoryId: 1, @@ -93,7 +95,6 @@ export const products = [ { title: "Polo Backend Developer", imgSrc: `${imagesBaseUrl}/polos/polo-backend.png`, - price: 25.0, description: "Domina el servidor con estilo: viste con orgullo tu título de Backend Developer.", categoryId: 1, @@ -108,7 +109,6 @@ export const products = [ { title: "Polo Frontend Developer", imgSrc: `${imagesBaseUrl}/polos/polo-frontend.png`, - price: 25.0, description: "Construye experiencias con estilo: luce con orgullo tu polo de Frontend Developer.", categoryId: 1, @@ -123,7 +123,6 @@ export const products = [ { title: "Polo Full-Stack Developer", imgSrc: `${imagesBaseUrl}/polos/polo-fullstack.png`, - price: 25.0, description: "Domina ambos mundos con estilo: lleva tu título de FullStack Developer en cada línea de tu look.", categoryId: 1, @@ -138,7 +137,6 @@ export const products = [ { title: "Polo It's A Feature", imgSrc: `${imagesBaseUrl}/polos/polo-feature.png`, - price: 15.0, description: "Cuando el bug se convierte en arte: lleva con orgullo tu polo 'It's a feature'.", categoryId: 1, @@ -153,7 +151,6 @@ export const products = [ { title: "Polo It Works On My Machine", imgSrc: `${imagesBaseUrl}/polos/polo-works.png`, - price: 15.0, description: "El clásico del desarrollador: presume tu confianza con 'It works on my machine'.", categoryId: 1, @@ -168,7 +165,6 @@ export const products = [ { title: "Sticker JavaScript", imgSrc: `${imagesBaseUrl}/stickers/sticker-js.png`, - price: 2.99, description: "Muestra tu amor por JavaScript con este elegante sticker clásico.", categoryId: 3, @@ -183,7 +179,6 @@ export const products = [ { title: "Sticker React", imgSrc: `${imagesBaseUrl}/stickers/sticker-react.png`, - price: 2.49, description: "Decora tus dispositivos con el icónico átomo giratorio de React.", categoryId: 3, @@ -198,7 +193,6 @@ export const products = [ { title: "Sticker Git", imgSrc: `${imagesBaseUrl}/stickers/sticker-git.png`, - price: 3.99, description: "Visualiza el poder del control de versiones con este sticker de Git.", categoryId: 3, @@ -213,7 +207,6 @@ export const products = [ { title: "Sticker Docker", imgSrc: `${imagesBaseUrl}/stickers/sticker-docker.png`, - price: 2.99, description: "La adorable ballena de Docker llevando contenedores en un sticker único.", categoryId: 3, @@ -228,7 +221,6 @@ export const products = [ { title: "Sticker Linux", imgSrc: `${imagesBaseUrl}/stickers/sticker-linux.png`, - price: 2.49, description: "El querido pingüino Tux, mascota oficial de Linux, en formato sticker.", categoryId: 3, @@ -243,7 +235,6 @@ export const products = [ { title: "Sticker VS Code", imgSrc: `${imagesBaseUrl}/stickers/sticker-vscode.png`, - price: 2.49, description: "El elegante logo del editor favorito de los desarrolladores.", categoryId: 3, isOnSale: false, @@ -257,7 +248,6 @@ export const products = [ { title: "Sticker GitHub", imgSrc: `${imagesBaseUrl}/stickers/sticker-github.png`, - price: 2.99, description: "El alojamiento de repositorios más popular en un sticker de alta calidad.", categoryId: 3, @@ -272,7 +262,6 @@ export const products = [ { title: "Sticker HTML", imgSrc: `${imagesBaseUrl}/stickers/sticker-html.png`, - price: 2.99, description: "El escudo naranja de HTML5, el lenguaje que estructura la web.", categoryId: 3, @@ -287,7 +276,6 @@ export const products = [ { title: "Taza JavaScript", imgSrc: `${imagesBaseUrl}/tazas/taza-js.png`, - price: 14.99, description: "Disfruta tu café mientras programas con el logo de JavaScript.", categoryId: 2, @@ -302,7 +290,6 @@ export const products = [ { title: "Taza React", imgSrc: `${imagesBaseUrl}/tazas/taza-react.png`, - price: 13.99, description: "Una taza que hace render de tu bebida favorita con estilo React.", categoryId: 2, @@ -317,7 +304,6 @@ export const products = [ { title: "Taza Git", imgSrc: `${imagesBaseUrl}/tazas/taza-git.png`, - price: 12.99, description: "Commit a tu rutina diaria de café con esta taza de Git.", categoryId: 2, isOnSale: false, @@ -331,7 +317,6 @@ export const products = [ { title: "Taza SQL", imgSrc: `${imagesBaseUrl}/tazas/taza-sql.png`, - price: 15.99, description: "Tu amor por los lenguajes estructurados en una taza de SQL.", categoryId: 2, isOnSale: false, @@ -345,7 +330,6 @@ export const products = [ { title: "Taza Linux", imgSrc: `${imagesBaseUrl}/tazas/taza-linux.png`, - price: 13.99, description: "Toma tu café con la libertad que solo Linux puede ofrecer.", categoryId: 2, isOnSale: false, @@ -359,7 +343,6 @@ export const products = [ { title: "Taza GitHub", imgSrc: `${imagesBaseUrl}/tazas/taza-github.png`, - price: 14.99, description: "Colabora con tu café en esta taza con el logo de GitHub.", categoryId: 2, isOnSale: false, @@ -371,3 +354,83 @@ export const products = [ ], }, ]; + +export const variantAttributeValues = [ + // --- POLOS (talla: S, M, L) --- + { attributeId: 1, productId: 1, value: "S", price: 20.0 }, + { attributeId: 1, productId: 1, value: "M", price: 20.0 }, + { attributeId: 1, productId: 1, value: "L", price: 20.0 }, + + { attributeId: 1, productId: 2, value: "S", price: 20.0 }, + { attributeId: 1, productId: 2, value: "M", price: 20.0 }, + { attributeId: 1, productId: 2, value: "L", price: 20.0 }, + + { attributeId: 1, productId: 3, value: "S", price: 20.0 }, + { attributeId: 1, productId: 3, value: "M", price: 20.0 }, + { attributeId: 1, productId: 3, value: "L", price: 20.0 }, + + { attributeId: 1, productId: 4, value: "S", price: 20.0 }, + { attributeId: 1, productId: 4, value: "M", price: 20.0 }, + { attributeId: 1, productId: 4, value: "L", price: 20.0 }, + + { attributeId: 1, productId: 5, value: "S", price: 25.0 }, + { attributeId: 1, productId: 5, value: "M", price: 25.0 }, + { attributeId: 1, productId: 5, value: "L", price: 25.0 }, + + { attributeId: 1, productId: 6, value: "S", price: 25.0 }, + { attributeId: 1, productId: 6, value: "M", price: 25.0 }, + { attributeId: 1, productId: 6, value: "L", price: 25.0 }, + + { attributeId: 1, productId: 7, value: "S", price: 25.0 }, + { attributeId: 1, productId: 7, value: "M", price: 25.0 }, + { attributeId: 1, productId: 7, value: "L", price: 25.0 }, + + { attributeId: 1, productId: 8, value: "S", price: 15.0 }, + { attributeId: 1, productId: 8, value: "M", price: 15.0 }, + { attributeId: 1, productId: 8, value: "L", price: 15.0 }, + + { attributeId: 1, productId: 9, value: "S", price: 15.0 }, + { attributeId: 1, productId: 9, value: "M", price: 15.0 }, + { attributeId: 1, productId: 9, value: "L", price: 15.0 }, + + // --- STICKERS (dimensiones: 3x3, 6x6, 9x9) --- + { attributeId: 2, productId: 10, value: "3x3", price: 2.99 }, + { attributeId: 2, productId: 10, value: "5x5", price: 3.99 }, + { attributeId: 2, productId: 10, value: "10x10", price: 4.99 }, + + { attributeId: 2, productId: 11, value: "3x3", price: 2.49 }, + { attributeId: 2, productId: 11, value: "5x5", price: 3.49 }, + { attributeId: 2, productId: 11, value: "10x10", price: 4.49 }, + + { attributeId: 2, productId: 12, value: "3x3", price: 3.99 }, + { attributeId: 2, productId: 12, value: "5x5", price: 4.99 }, + { attributeId: 2, productId: 12, value: "10x10", price: 5.99 }, + + { attributeId: 2, productId: 13, value: "3x3", price: 2.99 }, + { attributeId: 2, productId: 13, value: "5x5", price: 3.99 }, + { attributeId: 2, productId: 13, value: "10x10", price: 4.99 }, + + { attributeId: 2, productId: 14, value: "3x3", price: 2.49 }, + { attributeId: 2, productId: 14, value: "5x5", price: 3.49 }, + { attributeId: 2, productId: 14, value: "10x10", price: 4.49 }, + + { attributeId: 2, productId: 15, value: "3x3", price: 2.49 }, + { attributeId: 2, productId: 15, value: "5x5", price: 3.49 }, + { attributeId: 2, productId: 15, value: "10x10", price: 4.49 }, + + { attributeId: 2, productId: 16, value: "3x3", price: 2.99 }, + { attributeId: 2, productId: 16, value: "5x5", price: 3.99 }, + { attributeId: 2, productId: 16, value: "10x10", price: 4.99 }, + + { attributeId: 2, productId: 17, value: "3x3", price: 2.99 }, + { attributeId: 2, productId: 17, value: "5x5", price: 3.99 }, + { attributeId: 2, productId: 17, value: "10x10", price: .99 }, + + // --- TAZAS (no aplica: Único) --- + { attributeId: 3, productId: 18, value: "Único", price: 14.99 }, + { attributeId: 3, productId: 19, value: "Único", price: 13.99 }, + { attributeId: 3, productId: 20, value: "Único", price: 12.99 }, + { attributeId: 3, productId: 21, value: "Único", price: 15.99 }, + { attributeId: 3, productId: 22, value: "Único", price: 13.99 }, + { attributeId: 3, productId: 23, value: "Único", price: 14.99 }, +]; \ No newline at end of file diff --git a/prisma/migrations/20250820183901_add_tables_variants_products/migration.sql b/prisma/migrations/20250820183901_add_tables_variants_products/migration.sql new file mode 100644 index 0000000..f64a02c --- /dev/null +++ b/prisma/migrations/20250820183901_add_tables_variants_products/migration.sql @@ -0,0 +1,43 @@ +/* + Warnings: + + - You are about to drop the column `price` on the `products` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "products" DROP COLUMN "price"; + +-- CreateTable +CREATE TABLE "variants_attributes" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "created_at" TIMESTAMP(0) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(0) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "variants_attributes_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "variants_attributes_values" ( + "id" SERIAL NOT NULL, + "attribute_id" INTEGER NOT NULL, + "product_id" INTEGER NOT NULL, + "value" TEXT NOT NULL, + "price" DECIMAL(10,2) NOT NULL, + "created_at" TIMESTAMP(0) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(0) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "variants_attributes_values_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "variants_attributes_name_key" ON "variants_attributes"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "variants_attributes_values_attribute_id_product_id_value_key" ON "variants_attributes_values"("attribute_id", "product_id", "value"); + +-- AddForeignKey +ALTER TABLE "variants_attributes_values" ADD CONSTRAINT "variants_attributes_values_attribute_id_fkey" FOREIGN KEY ("attribute_id") REFERENCES "variants_attributes"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "variants_attributes_values" ADD CONSTRAINT "variants_attributes_values_product_id_fkey" FOREIGN KEY ("product_id") REFERENCES "products"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e0f992b..d251f4a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -55,7 +55,6 @@ model Product { title String imgSrc String @map("img_src") alt String? - price Decimal @db.Decimal(10, 2) description String? categoryId Int? @map("category_id") isOnSale Boolean @default(false) @map("is_on_sale") @@ -63,13 +62,41 @@ model Product { createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(0) updatedAt DateTime @default(now()) @map("updated_at") @db.Timestamp(0) - category Category? @relation(fields: [categoryId], references: [id], onDelete: SetNull) - cartItems CartItem[] - orderItems OrderItem[] + category Category? @relation(fields: [categoryId], references: [id], onDelete: SetNull) + cartItems CartItem[] + orderItems OrderItem[] + variantAttributeValues VariantAttributeValue[] @@map("products") } +model VariantAttribute { + id Int @id @default(autoincrement()) + name String @unique + createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(0) + updatedAt DateTime @default(now()) @map("updated_at") @db.Timestamp(0) + + variantsAttributeValue VariantAttributeValue[] + + @@map("variants_attributes") +} + +model VariantAttributeValue { + id Int @id @default(autoincrement()) + attributeId Int @map("attribute_id") + productId Int @map("product_id") + value String + price Decimal @db.Decimal(10, 2) + createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(0) + updatedAt DateTime @default(now()) @map("updated_at") @db.Timestamp(0) + + variantAttribute VariantAttribute @relation(fields: [attributeId], references: [id]) + product Product @relation(fields: [productId], references: [id]) + + @@unique([attributeId, productId, value], name: "unique_attribute_product_value") + @@map("variants_attributes_values") +} + model Cart { id Int @id @default(autoincrement()) sessionCartId String @unique @default(dbgenerated("gen_random_uuid()")) @map("session_cart_id") @db.Uuid diff --git a/prisma/seed.ts b/prisma/seed.ts index 106da46..4a2b3e5 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -1,4 +1,4 @@ -import { categories, products } from "./initial_data"; +import { categories, products, variantAttributes, variantAttributeValues } from "./initial_data"; import { PrismaClient } from "../generated/prisma/client"; const prisma = new PrismaClient(); @@ -9,10 +9,23 @@ async function seedDb() { }); console.log("1. Categories successfully inserted"); + await prisma.variantAttribute.createMany({ + data: variantAttributes, + }) + console.log("2. Variant Attributes successfully inserted"); + await prisma.product.createMany({ data: products, + }); - console.log("2. Products successfully inserted"); + console.log("3. Products successfully inserted"); + + await prisma.variantAttributeValue.createMany({ + data: variantAttributeValues, + }) + + console.log("4. Variant Attribute Values successfully inserted"); + } seedDb() diff --git a/src/models/product.model.ts b/src/models/product.model.ts index 96ba043..489125f 100644 --- a/src/models/product.model.ts +++ b/src/models/product.model.ts @@ -1,5 +1,12 @@ +import type { VariantAttributeValue } from "./variant-attribute.model"; import type { Product as PrismaProduct } from "@/../generated/prisma/client"; -export type Product = Omit & { - price: number; +export type Product = PrismaProduct & { + price?: number | null; + minPrice?: number | null; + maxPrice?: number | null; }; + +export type ProductVariantValue = PrismaProduct & { + variantAttributeValues: VariantAttributeValue[]; +} \ No newline at end of file diff --git a/src/models/variant-attribute.model.ts b/src/models/variant-attribute.model.ts new file mode 100644 index 0000000..2c91027 --- /dev/null +++ b/src/models/variant-attribute.model.ts @@ -0,0 +1,2 @@ +import type { VariantAttributeValue as PrismaVariantAttributeValue } from "@/../generated/prisma/client"; +export type VariantAttributeValue= PrismaVariantAttributeValue \ No newline at end of file diff --git a/src/routes/category/components/product-card/index.tsx b/src/routes/category/components/product-card/index.tsx index a6abe33..7805793 100644 --- a/src/routes/category/components/product-card/index.tsx +++ b/src/routes/category/components/product-card/index.tsx @@ -8,6 +8,7 @@ interface ProductCardProps { export function ProductCard({ product }: ProductCardProps) { return ( + <>

{product.title}

{product.description}

+ { + product?.price &&

S/{product.price}

+ } + { + product?.minPrice && +

Entre S/{product.minPrice} - {product.maxPrice}

+ } {product.isOnSale && ( @@ -34,5 +42,6 @@ export function ProductCard({ product }: ProductCardProps) { )} + ); } diff --git a/src/routes/category/index.tsx b/src/routes/category/index.tsx index 7c0aef5..e4f83a0 100644 --- a/src/routes/category/index.tsx +++ b/src/routes/category/index.tsx @@ -36,8 +36,24 @@ export async function loader({ params, request }: Route.LoaderArgs) { const min = minPrice ? parseFloat(minPrice) : 0; const max = maxPrice ? parseFloat(maxPrice) : Infinity; return products.filter( - (product) => product.price >= min && product.price <= max - ); + (product) => { + const minProductPrice = product.minPrice||0 + const maxProductPrice = product.maxPrice ||0 + const productPrice = product.price || 0 + + if (min && max) { + return ((productPrice||minProductPrice) >= min) && ((productPrice||maxProductPrice) <= max) + } + + if (min) { + return (productPrice||minProductPrice) >= min + } + if (max) { + return (productPrice||maxProductPrice) <= max + + } + return true + }); }; const filteredProducts = filterProductsByPrice( diff --git a/src/routes/root/index.tsx b/src/routes/root/index.tsx index f3197d1..b76631d 100644 --- a/src/routes/root/index.tsx +++ b/src/routes/root/index.tsx @@ -68,7 +68,7 @@ export async function loader({ request }: Route.LoaderArgs) { } const totalItems = - cart?.items.reduce((total, item) => total + item.quantity, 0) || 0; + cart?.items?.reduce((total, item) => total + item.quantity, 0) || 0; // Preparar datos de respuesta según estado de autenticación const responseData = user ? { user, totalItems } : { totalItems }; diff --git a/src/services/cart.service.ts b/src/services/cart.service.ts index f742706..74cbdfc 100644 --- a/src/services/cart.service.ts +++ b/src/services/cart.service.ts @@ -18,42 +18,52 @@ async function getCart( : undefined; if (!whereCondition) return null; + try { - const data = await prisma.cart.findFirst({ - where: whereCondition, - include: { - items: { - include: { - product: { - select: { - id: true, - title: true, - imgSrc: true, - alt: true, - price: true, - isOnSale: true, + const data = await prisma.cart.findFirst({ + where: whereCondition, + include: { + items: { + include: { + product: { + select: { + id: true, + title: true, + imgSrc: true, + alt: true, + price: true, + isOnSale: true, + }, }, }, - }, - orderBy: { - id: "asc", + orderBy: { + id: "asc", + }, }, }, - }, - }); - - if (!data) return null; + }); + + if (!data) return null; + + return { + ...data, + items: data.items.map((item) => ({ + ...item, + product: { + ...item.product, + price: item.product.price.toNumber(), + }, + })), + }; + }catch(e) { + console.log(e) + return { + error: true, + status: 500, + message: "Error al obtener el carrito. Verifica el modelo Product.", + }; + } - return { - ...data, - items: data.items.map((item) => ({ - ...item, - product: { - ...item.product, - price: item.product.price.toNumber(), - }, - })), - }; } export async function getRemoteCart( diff --git a/src/services/product.service.ts b/src/services/product.service.ts index 3406570..add02b7 100644 --- a/src/services/product.service.ts +++ b/src/services/product.service.ts @@ -1,38 +1,66 @@ import { prisma } from "@/db/prisma"; import type { Category } from "@/models/category.model"; -import type { Product } from "@/models/product.model"; +import type { Product, ProductVariantValue } from "@/models/product.model"; +import type { VariantAttributeValue } from "@/models/variant-attribute.model"; import { getCategoryBySlug } from "./category.service"; +const formattedProduct = (product: ProductVariantValue) => { + const {variantAttributeValues, ...rest} = product + const prices = variantAttributeValues.map((v: VariantAttributeValue) => Number(v.price)) + const minPrice = Math.min(...prices) + const maxPrice = Math.max(...prices) + if (minPrice === maxPrice) { + return { + ...rest, + price: minPrice + } + } + return { + ...rest, + minPrice, + maxPrice + } +} + export async function getProductsByCategorySlug( categorySlug: Category["slug"] ): Promise { const category = await getCategoryBySlug(categorySlug); const products = await prisma.product.findMany({ where: { categoryId: category.id }, + include: { + variantAttributeValues: true + } }); - return products.map((product) => ({ - ...product, - price: product.price.toNumber(), - })); + return products.map(formattedProduct) } export async function getProductById(id: number): Promise { const product = await prisma.product.findUnique({ where: { id }, + include: { + variantAttributeValues: true + } }); if (!product) { throw new Error("Product not found"); } + const variants = product.variantAttributeValues.map((variant)=> ({ + ...variant, + price: Number(variant.price) + })) - return { ...product, price: product.price.toNumber() }; +return {...product, variantAttributeValues: variants } as Product } export async function getAllProducts(): Promise { - return (await prisma.product.findMany()).map((p) => ({ - ...p, - price: p.price.toNumber(), - })); + const products = await prisma.product.findMany({ + include: { + variantAttributeValues: true + } + }); + return products.map(formattedProduct) }