From a4dce56d29162f6a54da069b5b71644d73414fe9 Mon Sep 17 00:00:00 2001 From: Nazar Kyselov Date: Mon, 13 Apr 2026 16:05:04 +0200 Subject: [PATCH 01/15] refactor(stepper) Remove logic outside the component. It must receive, display, and emit eventual changes. --- components/stepper/stepper.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/components/stepper/stepper.tsx b/components/stepper/stepper.tsx index 0fcdb49..060cfdc 100644 --- a/components/stepper/stepper.tsx +++ b/components/stepper/stepper.tsx @@ -8,22 +8,23 @@ export interface StepperProps { step?: number; disabled?: boolean; size?: "md" | "sm"; - onChange: (newValue: number) => void; + onChange: (delta: number) => void; } export default function Stepper({ value, min = 1, max = 99, + step = 1, disabled = false, size = "md", onChange, }: StepperProps) { const handleDecrement = (): void => { - if (value > min) onChange(value - 1); + onChange(-step); }; const handleIncrement = (): void => { - if (value < max) onChange(value + 1); + onChange(step); }; const sizeClass = { From 21aa1cfea2b7cc4ceb7d25261662c6812888ea9d Mon Sep 17 00:00:00 2001 From: Nazar Kyselov Date: Mon, 13 Apr 2026 16:05:25 +0200 Subject: [PATCH 02/15] feat(types): add CartItemType --- lib/types.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/types.ts b/lib/types.ts index 2e7f189..bef1a88 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,6 +1,6 @@ export interface ProductType { id: string; - code: number; + code: number; //! convert to a string name: string; imageUrls: string[]; rating: number; @@ -70,4 +70,13 @@ export interface BrandType { export interface BrandGroupType { char: string; brands: BrandType[]; +} + +export interface CartItemType { + imageUrl: string; + name: string; + variant: VariantType; + productId: string; + productCode: string; + quantity: number; } \ No newline at end of file From 1372dd2a93c6d6d936f6bc91dd153c6ad56d91c9 Mon Sep 17 00:00:00 2001 From: Nazar Kyselov Date: Mon, 13 Apr 2026 16:09:08 +0200 Subject: [PATCH 03/15] refactor(cartItem): move logic outside - Move the handling delete, quantity change to the parent components. only displays and emit eventual changes. - add onQuantityChange event - rename prop title -> name --- components/cartItem.tsx | 187 +++++++++++++++++++--------------------- 1 file changed, 91 insertions(+), 96 deletions(-) diff --git a/components/cartItem.tsx b/components/cartItem.tsx index 42d36e9..9cd133b 100644 --- a/components/cartItem.tsx +++ b/components/cartItem.tsx @@ -1,126 +1,121 @@ "use client"; -import { AvailabilityType } from "@/lib/types"; +import {AvailabilityType} from "@/lib/types"; import { - getAvailability, - getAvailabilityClass, - getEuro, + getAvailability, + getAvailabilityClass, + getEuro, } from "@/lib/utils"; -import { Trash2 } from "lucide-react"; +import {Trash2} from "lucide-react"; import Image from "next/image"; -import { useState } from "react"; +import {useState} from "react"; import Stepper from "./stepper/stepper"; import Link from "next/link"; export interface CartItemProps { - imageUrl: string; - title: string; - volume: number; - productId: string; - productCode: string; - quantity: number; - quantityInStock: number; - pricePerItem: number; // cents - onDelete: () => void; + imageUrl: string; + name: string; + volume: number; + productId: string; + productCode: string; + quantity: number; + quantityInStock: number; + pricePerItem: number; // cents + onDelete: (id: string) => void; + onQuantityChange: (id: string, delta: number) => void; } export default function CartItem({ - imageUrl, - title, - volume, - productId, - productCode, - quantity, - quantityInStock, - pricePerItem, - onDelete, -}: CartItemProps) { - const [isDeleted, setIsDeleted] = useState(false); - const [availability] = useState( - getAvailability(quantityInStock) - ); - const availabilityClass: string = getAvailabilityClass(availability); - const productLink: string = `/shop/product/${productId}`; + imageUrl, + name, + volume, + productId, + productCode, + quantity, + quantityInStock, + pricePerItem, + onDelete, + onQuantityChange, + }: CartItemProps) { + const availability = getAvailability(quantityInStock); + const availabilityClass: string = getAvailabilityClass(availability); + const productLink: string = `/shop/product/${productId}`; - const [newQuantity, setNewQuantity] = useState(quantity); - const [totalPrice, setTotalPrice] = useState( - pricePerItem * quantity - ); + const totalPrice = pricePerItem * quantity; - const handleQuantity = (e: number) => { - setNewQuantity(e); - setTotalPrice(e * pricePerItem); - }; + const handleQuantityChange = (delta: number) => { + console.log("Changed value by:", delta); + onQuantityChange(productId, delta); + }; - const handleDelete = () => { - setIsDeleted(true); - onDelete(); - }; + const handleDelete = () => { + onDelete(productId); + }; - return ( - <> - {!isDeleted && ( -
- -
- {`${title} -
-
-
-

- {title} -

-
+ return ( + <> +
+ +
+ {`${name} +
+
+
+

+ {name} +

+
{volume} ml - + Product Code -  - + {productCode} -
-
+
+
- + {availability} -
- - handleQuantity(e)} - min={1} - max={quantityInStock} - disabled={quantityInStock == 0} - > -
+
+ + handleQuantityChange(delta)} + min={1} + max={quantityInStock} + disabled={quantityInStock == 0} + > +
{getEuro(totalPrice)} - -
-
- )} - - ); + +
+
+ + ); } From b8049a2d82661d250b56b5e07a5d812742f67b5e Mon Sep 17 00:00:00 2001 From: Nazar Kyselov Date: Mon, 13 Apr 2026 16:10:42 +0200 Subject: [PATCH 04/15] feat(lib): add mock data for cart - add CART_ITEMS data array to lib/data.ts - create a mock cartItems API endpoint --- lib/api/cartItems.ts | 9 + lib/data.ts | 610 +++++++++++++++++++++++-------------------- 2 files changed, 330 insertions(+), 289 deletions(-) create mode 100644 lib/api/cartItems.ts diff --git a/lib/api/cartItems.ts b/lib/api/cartItems.ts new file mode 100644 index 0000000..94d1f3e --- /dev/null +++ b/lib/api/cartItems.ts @@ -0,0 +1,9 @@ +import {CART_ITEMS} from "../data"; + +export default async function getCartItems(userId: string) { + // Simulatel DB network delay + await new Promise((resolve) => setTimeout(resolve, 500)); + console.log("Cart items fetched successfully."); + + return CART_ITEMS; +} \ No newline at end of file diff --git a/lib/data.ts b/lib/data.ts index 1276318..d109e39 100644 --- a/lib/data.ts +++ b/lib/data.ts @@ -1,308 +1,340 @@ -import { CommentCardProps } from "@/components/commentCard/commentCard"; -import { BrandType, FilterState, ProductType } from "./types"; +import {CommentCardProps} from "@/components/commentCard/commentCard"; +import {BrandType, CartItemType, FilterState, ProductType} from "./types"; +import {CartItemProps} from "@/components/cartItem"; const PRODUCTS: ProductType[] = [ - { - id: "asdf-werg-cfad", - code: 123456, - name: "Jean Paul Gaultier Le Beau", - imageUrls: [ - "https://i.makeup.it/9/9i/9iajbg7jxhit.jpg", - "https://i.makeup.it/o/oc/oct1za9lqofn.jpg", - "https://i.makeup.it/w/wz/wzyoa9i8eafq.jpg", - "https://i.makeup.it/7/7u/7ukogdy4r4na.jpg", - ], - rating: 4.4, - reviewsAmount: 100, - type: "Eau de Toilette", - tags: [], - gender: "men", - brand: "Jean Paul Gaultier", - variants: [ - { - volume: 30, - price: 4999, - discountedPrice: undefined, - wishlist: true, - quantityInStock: 1, - }, - { - volume: 50, - price: 6999, - discountedPrice: 5794, - wishlist: true, - quantityInStock: 0, - }, - { - volume: 75, - price: 7999, - wishlist: true, - discountedPrice: undefined, - quantityInStock: 12, - }, - ], - description: - "L'eau de toilette Jean Paul Gaultier Le Beau è un’originale fragranza maschile legnosa-fougère rilasciata nel 2019. È un vero e proprio elisir perfetto per gli uomini seducenti e sexy. L'individualità e la rara esclusività del carattere della composizione sono evidenziate anche dal design del flacone, creato dai migliori designer del marchio. La fragranza è presentata in un'elegante bottiglia di vetro verde scuro, seguendo le linee di un torso maestoso e coraggioso, simile ai dipinti raffiguranti il ​​dio greco Apollo. I creatori hanno deciso di non aggiungere alcun tapo per non distrarre l'attenzione dal design del flacone. L'insolita fragranza si apre con note di bergamotto, che incanta con il suono verde, floreale e leggermente fruttato. Quando le note di testa si dissolvono, il cuore del profumo si rivela con la piacevole nota esotica di cocco. La scia finale avvolge a lungo con note di fava tonka, che esalta la profondità del suono, conferendo alla composizione un suono incredibilmente persistente.", - details: { - "Lanciato sul mercato": 2019, - Marchio: "Jean Paul Gaultier", - Serie: "Le Beau", - "Gruppo di prodotti": "Eau de Toilette", - Classificazione: "Di lusso", - Volume: "75 ml", - "Paese TM": "Francia", - Produttore: - "PUIG, Plaza Europa, 46-48, 08902 – L’Hospitalet de Llobregat, Barcellona, Spagna, consumercare@puig.com", - "Precauzioni d'uso": - "Evitare il contatto con gli occhi, Facilmente infiammabile, Non utilizzare vicino al fuoco o a sostanze infiammabili, Tenere fuori dalla portata dei bambini", - Profumiere: "Quentin Bisch", - "Made in": "Francia, Spania", - Sesso: "Uomo", - "Tipo di aroma": "Aromatico, Legnoso", - "Note di testa": "Bergamotto", - "Note di cuore": "Noce di cocco", - "Note di base": "Fava tonka", - }, - ingredients: - "Alcohol Denat., Aqua (Water), Parfum (Fragrance), Coumarin, Linalool, Alpha-Isomethyl Ionone, Butyl Methoxydibenzoylmethane, Limonene, Anise Alcohol, Cinnamal, Benzyl Alcohol, Hydroxycitronellal, Citral, Citronellol, Eugenol, Geraniol.", - markers: ["hit"], - }, - { - id: "asdf-werg-cfad", - code: 529683, - name: "Yves Saint Laurent Libre Intense", - rating: 4.9, - reviewsAmount: 142, - type: "Eau de Parfum", - tags: [], - gender: "women", - brand: "Yves Saint Laurent", - imageUrls: [ - "https://i.makeup.it/l/l7/l7yfzcx8yetn.png", - "https://i.makeup.it/v/vw/vwrmjmzohgco.jpg", - "https://i.makeup.it/6/6u/6uhlhhy8uaxj.jpg", - "https://i.makeup.it/2/2q/2qokirdaqgby.jpg", - "https://i.makeup.it/q/qh/qhfr0bfoyr0h.jpg", - "https://i.makeup.it/u/uz/uziryzj6tohs.jpg", - ], - variants: [ - { - volume: 30, - price: 10900, - discountedPrice: 6539, - wishlist: true, - quantityInStock: 1, - }, - { - volume: 50, - price: 15600, - discountedPrice: 9388, - wishlist: true, - quantityInStock: 34, - }, - { - volume: 60, - price: 19900, - discountedPrice: undefined, - wishlist: true, - quantityInStock: 12, - }, - ], - description: - "L'eau de parfum Yves Saint Laurent Libre Intense è un'intensa fragranza floreale per tutte le donne libere di esprimersi così come sono e di vivere secondo il proprio istinto. La donna \"libera\" scatena i suoi istinti più ardenti. Audace, attira nel profondo di sé una forza sovversiva e potente per gridare la sua libertà illimitata e senza compromessi. L'incantevole fragranza femminile si rivela con note di lavanda, mandarino e bergamotto. \ - Il cuore dell'aroma rivela una combinazione floreale senza pari di lavanda, fiore d'arancio tunisino, gelsomino Sambac e orchidea. Questo bouquet unico trova il suo completamento in un sillage persistente, in cui si intrecciano armoniosamente note di vaniglia del Madagascar, fava tonka, ambra grigia e vetiver. La fragranza è racchiusa in un flacone attorcigliato da un accessorio lussuoso e di grandi dimensioni. Il logo iconico del brand è incastonato nel vetro come un gioiello. Catene dorate e un tappo asimmetrico laccato nero adornano questo flacone couture, facendolo diventare anche un bellissimo accessorio oltre a un'incantevole fragranza.", - details: { - "Lanciato sul mercato": 2020, - Marchio: "Yves Saint Laurent", - Serie: "Libre Intense", - "Gruppo di prodotti": "Eau de Parfum", - Classificazione: "Di lusso", - Volume: "30 ml, 50 ml, 90 ml", - "Paese TM": "Francia", - Tipo: "intensivo", - Collezione: "Libre", - Produttore: - "YSL BEAUTÉ, 14, rue Royale, 75008 Parigi, Francia, contact@loreal.com", - "Precauzioni d'uso": - "Evitare il contatto con gli occhi, Facilmente infiammabile, Non utilizzare vicino al fuoco o a sostanze infiammabili, Tenere fuori dalla portata dei bambini", - Profumiere: "Anne Flipo, Carlos Benaim", - "Made in": "Francia", - Sesso: "Donna", - "Tipo di aroma": "Fougere, Orientale", - "Note di testa": "Bergamotto, Lavanda, Mandarino", - "Note di cuore": - "Fiore di arancio, Gelsomino di sambac, Lavanda, Orchidea", - "Note di base": - "Ambra grigia, Fava tonka, Vaniglia di Madagascar, Vetiver", - }, - ingredients: undefined, - markers: ["hit"], - }, - { - id: "asdf-wedg-cfad", - code: 422454, - name: "Montblanc Explorer", - rating: 4.9, - reviewsAmount: 352, - type: "Eau de Parfum", - tags: [], - gender: "men", - brand: "Montblanc", - imageUrls: [ - "https://i.makeup.it/2/2h/2h0tbxmkoqlr.jpg", - "https://i.makeup.it/k/kg/kgdgremnqrkg.jpg", - "https://i.makeup.it/p/pm/pme2fz4v2ocd.jpg", - "https://i.makeup.it/q/qt/qta5xslqbzqv.jpg", - "https://i.makeup.it/b/bz/bzyzdgg98hcd.jpg", - "https://i.makeup.it/y/yr/yr4d1ifdw0rc.jpg", - "https://i.makeup.it/n/nx/nxwoowtpvrkqf.jpg", - "https://i.makeup.it/i/is/isypg80tyziu.jpg", - ], - variants: [ - { - volume: 60, - price: 5100, - discountedPrice: 4550, - wishlist: true, - quantityInStock: 1, - }, - { - volume: 100, - price: 7084, - discountedPrice: undefined, - wishlist: true, - quantityInStock: 34, - }, - { - volume: 200, - price: 9894, - discountedPrice: undefined, - wishlist: true, - quantityInStock: 12, - }, - ], - description: - "Mont Blanc Explorer è una straordinaria novità creata dai profumieri francesi Oliver Peschaux e Antonio Masondiou che impressionerà qualsiasi uomo moderno. Un aroma speziato, contrastante con note di dolcezza, spezie e freschezza insite nei profumi orientali, fin dai primi secondi eccita i sensi ed evoca vivide emozioni. Questo profumo è adatto per uomini rigorosi, sicuri di sé e rilassati che non nascondono il loro vero carattere. La composizione della fragranza si apre con le note iniziali di bergamotto aspro, pepe rosa, foglie di alloro speziate e salvia sclarea, poi note di cuoio ruvido e vetiver entrano nell'insieme, conferendo alla composizione cioccolato morbido e sfumature legnose. Satura l'aroma con una nobile e persistente sillage di ambroxan, cacao e patchouli, aggiungendo caratteristiche note orientali. Mont Blanc Explorer è la versatilità e la dinamica delle sfumature che enfatizzano mascolinità ed eleganza. Il design austero e minimalista del flacone è pienamente coerente con il carattere della fragranza, consentendo di vivere appieno la sua estetica. L'aroma caldo e ricco di Mont Blanc Explorer sarà un ottimo compagno della tua serata e suonerà particolarmente organico nella stagione fredda.", - details: { - "Lanciato sul mercato": 2019, - Marchio: "Montblanc", - Serie: "Explorer", - "Gruppo di prodotti": "Eau de Parfum", - Colore: "Nero", - Classificazione: "Di lusso", - Volume: "60 ml, 100 ml, 200 ml", - "Paese TM": "Francia", - "Precauzioni d'uso": - "Evitare il contatto con gli occhi, Facilmente infiammabile, Non utilizzare vicino al fuoco o a sostanze infiammabili, Tenere fuori dalla portata dei bambini", - Profumiere: "Antoine Maisondieu, Olivier Pescheux", - "Made in": "Francia", - Sesso: "Uomo", - "Tipo di aroma": "Fougere, Legnoso", - "Note di testa": "Bergamotto, Pepe rosa, Salvia", - "Note di cuore": "Cuoio, Vetiver Tahitian", - "Note di base": - "Akigalawood, Ambroxan, Cacao, Patchouli dall'Indonesia", - }, - ingredients: - "INGREDIENTS: ALCOHOL DENAT. (SD ALCOHOL 39-C), PARFUM (FRAGRANCE), AQUA (WATER), ETHYLHEXYL METHOXYCINNAMATE, BUTYL METHOXYDIBENZOYLMETHANE, ETHYLHEXYL SALICYLATE, BHT, CITRAL, LIMONENE, GERANIOL, LINALOOL, CI 14700 (RED 4), CI 42090 (BLUE 1), CI 19140 (YELLOW 5).", - markers: ["hit"], - }, + { + id: "asdf-werg-cfad", + code: 123456, + name: "Jean Paul Gaultier Le Beau", + imageUrls: [ + "https://i.makeup.it/9/9i/9iajbg7jxhit.jpg", + "https://i.makeup.it/o/oc/oct1za9lqofn.jpg", + "https://i.makeup.it/w/wz/wzyoa9i8eafq.jpg", + "https://i.makeup.it/7/7u/7ukogdy4r4na.jpg", + ], + rating: 4.4, + reviewsAmount: 100, + type: "Eau de Toilette", + tags: [], + gender: "men", + brand: "Jean Paul Gaultier", + variants: [ + { + volume: 30, + price: 4999, + discountedPrice: undefined, + wishlist: true, + quantityInStock: 1, + }, + { + volume: 50, + price: 6999, + discountedPrice: 5794, + wishlist: true, + quantityInStock: 0, + }, + { + volume: 75, + price: 7999, + wishlist: true, + discountedPrice: undefined, + quantityInStock: 12, + }, + ], + description: + "L'eau de toilette Jean Paul Gaultier Le Beau è un’originale fragranza maschile legnosa-fougère rilasciata nel 2019. È un vero e proprio elisir perfetto per gli uomini seducenti e sexy. L'individualità e la rara esclusività del carattere della composizione sono evidenziate anche dal design del flacone, creato dai migliori designer del marchio. La fragranza è presentata in un'elegante bottiglia di vetro verde scuro, seguendo le linee di un torso maestoso e coraggioso, simile ai dipinti raffiguranti il ​​dio greco Apollo. I creatori hanno deciso di non aggiungere alcun tapo per non distrarre l'attenzione dal design del flacone. L'insolita fragranza si apre con note di bergamotto, che incanta con il suono verde, floreale e leggermente fruttato. Quando le note di testa si dissolvono, il cuore del profumo si rivela con la piacevole nota esotica di cocco. La scia finale avvolge a lungo con note di fava tonka, che esalta la profondità del suono, conferendo alla composizione un suono incredibilmente persistente.", + details: { + "Lanciato sul mercato": 2019, + Marchio: "Jean Paul Gaultier", + Serie: "Le Beau", + "Gruppo di prodotti": "Eau de Toilette", + Classificazione: "Di lusso", + Volume: "75 ml", + "Paese TM": "Francia", + Produttore: + "PUIG, Plaza Europa, 46-48, 08902 – L’Hospitalet de Llobregat, Barcellona, Spagna, consumercare@puig.com", + "Precauzioni d'uso": + "Evitare il contatto con gli occhi, Facilmente infiammabile, Non utilizzare vicino al fuoco o a sostanze infiammabili, Tenere fuori dalla portata dei bambini", + Profumiere: "Quentin Bisch", + "Made in": "Francia, Spania", + Sesso: "Uomo", + "Tipo di aroma": "Aromatico, Legnoso", + "Note di testa": "Bergamotto", + "Note di cuore": "Noce di cocco", + "Note di base": "Fava tonka", + }, + ingredients: + "Alcohol Denat., Aqua (Water), Parfum (Fragrance), Coumarin, Linalool, Alpha-Isomethyl Ionone, Butyl Methoxydibenzoylmethane, Limonene, Anise Alcohol, Cinnamal, Benzyl Alcohol, Hydroxycitronellal, Citral, Citronellol, Eugenol, Geraniol.", + markers: ["hit"], + }, + { + id: "asdf-werg-cfad", + code: 529683, + name: "Yves Saint Laurent Libre Intense", + rating: 4.9, + reviewsAmount: 142, + type: "Eau de Parfum", + tags: [], + gender: "women", + brand: "Yves Saint Laurent", + imageUrls: [ + "https://i.makeup.it/l/l7/l7yfzcx8yetn.png", + "https://i.makeup.it/v/vw/vwrmjmzohgco.jpg", + "https://i.makeup.it/6/6u/6uhlhhy8uaxj.jpg", + "https://i.makeup.it/2/2q/2qokirdaqgby.jpg", + "https://i.makeup.it/q/qh/qhfr0bfoyr0h.jpg", + "https://i.makeup.it/u/uz/uziryzj6tohs.jpg", + ], + variants: [ + { + volume: 30, + price: 10900, + discountedPrice: 6539, + wishlist: true, + quantityInStock: 1, + }, + { + volume: 50, + price: 15600, + discountedPrice: 9388, + wishlist: true, + quantityInStock: 34, + }, + { + volume: 60, + price: 19900, + discountedPrice: undefined, + wishlist: true, + quantityInStock: 12, + }, + ], + description: + "L'eau de parfum Yves Saint Laurent Libre Intense è un'intensa fragranza floreale per tutte le donne libere di esprimersi così come sono e di vivere secondo il proprio istinto. La donna \"libera\" scatena i suoi istinti più ardenti. Audace, attira nel profondo di sé una forza sovversiva e potente per gridare la sua libertà illimitata e senza compromessi. L'incantevole fragranza femminile si rivela con note di lavanda, mandarino e bergamotto. \ + Il cuore dell'aroma rivela una combinazione floreale senza pari di lavanda, fiore d'arancio tunisino, gelsomino Sambac e orchidea. Questo bouquet unico trova il suo completamento in un sillage persistente, in cui si intrecciano armoniosamente note di vaniglia del Madagascar, fava tonka, ambra grigia e vetiver. La fragranza è racchiusa in un flacone attorcigliato da un accessorio lussuoso e di grandi dimensioni. Il logo iconico del brand è incastonato nel vetro come un gioiello. Catene dorate e un tappo asimmetrico laccato nero adornano questo flacone couture, facendolo diventare anche un bellissimo accessorio oltre a un'incantevole fragranza.", + details: { + "Lanciato sul mercato": 2020, + Marchio: "Yves Saint Laurent", + Serie: "Libre Intense", + "Gruppo di prodotti": "Eau de Parfum", + Classificazione: "Di lusso", + Volume: "30 ml, 50 ml, 90 ml", + "Paese TM": "Francia", + Tipo: "intensivo", + Collezione: "Libre", + Produttore: + "YSL BEAUTÉ, 14, rue Royale, 75008 Parigi, Francia, contact@loreal.com", + "Precauzioni d'uso": + "Evitare il contatto con gli occhi, Facilmente infiammabile, Non utilizzare vicino al fuoco o a sostanze infiammabili, Tenere fuori dalla portata dei bambini", + Profumiere: "Anne Flipo, Carlos Benaim", + "Made in": "Francia", + Sesso: "Donna", + "Tipo di aroma": "Fougere, Orientale", + "Note di testa": "Bergamotto, Lavanda, Mandarino", + "Note di cuore": + "Fiore di arancio, Gelsomino di sambac, Lavanda, Orchidea", + "Note di base": + "Ambra grigia, Fava tonka, Vaniglia di Madagascar, Vetiver", + }, + ingredients: undefined, + markers: ["hit"], + }, + { + id: "asdf-wedg-cfad", + code: 422454, + name: "Montblanc Explorer", + rating: 4.9, + reviewsAmount: 352, + type: "Eau de Parfum", + tags: [], + gender: "men", + brand: "Montblanc", + imageUrls: [ + "https://i.makeup.it/2/2h/2h0tbxmkoqlr.jpg", + "https://i.makeup.it/k/kg/kgdgremnqrkg.jpg", + "https://i.makeup.it/p/pm/pme2fz4v2ocd.jpg", + "https://i.makeup.it/q/qt/qta5xslqbzqv.jpg", + "https://i.makeup.it/b/bz/bzyzdgg98hcd.jpg", + "https://i.makeup.it/y/yr/yr4d1ifdw0rc.jpg", + "https://i.makeup.it/n/nx/nxwoowtpvrkqf.jpg", + "https://i.makeup.it/i/is/isypg80tyziu.jpg", + ], + variants: [ + { + volume: 60, + price: 5100, + discountedPrice: 4550, + wishlist: true, + quantityInStock: 1, + }, + { + volume: 100, + price: 7084, + discountedPrice: undefined, + wishlist: true, + quantityInStock: 34, + }, + { + volume: 200, + price: 9894, + discountedPrice: undefined, + wishlist: true, + quantityInStock: 12, + }, + ], + description: + "Mont Blanc Explorer è una straordinaria novità creata dai profumieri francesi Oliver Peschaux e Antonio Masondiou che impressionerà qualsiasi uomo moderno. Un aroma speziato, contrastante con note di dolcezza, spezie e freschezza insite nei profumi orientali, fin dai primi secondi eccita i sensi ed evoca vivide emozioni. Questo profumo è adatto per uomini rigorosi, sicuri di sé e rilassati che non nascondono il loro vero carattere. La composizione della fragranza si apre con le note iniziali di bergamotto aspro, pepe rosa, foglie di alloro speziate e salvia sclarea, poi note di cuoio ruvido e vetiver entrano nell'insieme, conferendo alla composizione cioccolato morbido e sfumature legnose. Satura l'aroma con una nobile e persistente sillage di ambroxan, cacao e patchouli, aggiungendo caratteristiche note orientali. Mont Blanc Explorer è la versatilità e la dinamica delle sfumature che enfatizzano mascolinità ed eleganza. Il design austero e minimalista del flacone è pienamente coerente con il carattere della fragranza, consentendo di vivere appieno la sua estetica. L'aroma caldo e ricco di Mont Blanc Explorer sarà un ottimo compagno della tua serata e suonerà particolarmente organico nella stagione fredda.", + details: { + "Lanciato sul mercato": 2019, + Marchio: "Montblanc", + Serie: "Explorer", + "Gruppo di prodotti": "Eau de Parfum", + Colore: "Nero", + Classificazione: "Di lusso", + Volume: "60 ml, 100 ml, 200 ml", + "Paese TM": "Francia", + "Precauzioni d'uso": + "Evitare il contatto con gli occhi, Facilmente infiammabile, Non utilizzare vicino al fuoco o a sostanze infiammabili, Tenere fuori dalla portata dei bambini", + Profumiere: "Antoine Maisondieu, Olivier Pescheux", + "Made in": "Francia", + Sesso: "Uomo", + "Tipo di aroma": "Fougere, Legnoso", + "Note di testa": "Bergamotto, Pepe rosa, Salvia", + "Note di cuore": "Cuoio, Vetiver Tahitian", + "Note di base": + "Akigalawood, Ambroxan, Cacao, Patchouli dall'Indonesia", + }, + ingredients: + "INGREDIENTS: ALCOHOL DENAT. (SD ALCOHOL 39-C), PARFUM (FRAGRANCE), AQUA (WATER), ETHYLHEXYL METHOXYCINNAMATE, BUTYL METHOXYDIBENZOYLMETHANE, ETHYLHEXYL SALICYLATE, BHT, CITRAL, LIMONENE, GERANIOL, LINALOOL, CI 14700 (RED 4), CI 42090 (BLUE 1), CI 19140 (YELLOW 5).", + markers: ["hit"], + }, ]; const MOCK_SHOP_FILTERS: FilterState = { - searchQuery: "", + searchQuery: "", - // Min/Max for the slider - priceRange: [0, 600], + // Min/Max for the slider + priceRange: [0, 600], - // Checkbox Options (Strings as they appear in DB) - brands: [ - "Versace", - "Jean Paul Gaultier", - "Giorgio Armani", - "Dior", - "Yves Saint Laurent", - "Montblanc", - "Paco Rabanne", - "Chanel", - "Tom Ford", - "Creed", - "Hermès", - "Dolce & Gabbana", - "Hugo Boss", - "Gucci", - "Prada", - "Valentino", - "Givenchy", - ], + // Checkbox Options (Strings as they appear in DB) + brands: [ + "Versace", + "Jean Paul Gaultier", + "Giorgio Armani", + "Dior", + "Yves Saint Laurent", + "Montblanc", + "Paco Rabanne", + "Chanel", + "Tom Ford", + "Creed", + "Hermès", + "Dolce & Gabbana", + "Hugo Boss", + "Gucci", + "Prada", + "Valentino", + "Givenchy", + ], - genders: ["men", "women", "unisex"], + genders: ["men", "women", "unisex"], - volumes: ["30", "50", "75", "90", "100", "125", "150", "200"], + volumes: ["30", "50", "75", "90", "100", "125", "150", "200"], - markers: [ - "hot", - "Bestseller", - "Discounted", - "Limited Edition", - "Staff Pick", - ], + markers: [ + "hot", + "Bestseller", + "Discounted", + "Limited Edition", + "Staff Pick", + ], - // Bonus: If you want to filter by concentration (EDT/EDP) - types: [ - "Eau de Toilette", - "Eau de Parfum", - "Parfum", - "Extrait de Parfum", - "Eau de Cologne", - ], + // Bonus: If you want to filter by concentration (EDT/EDP) + types: [ + "Eau de Toilette", + "Eau de Parfum", + "Parfum", + "Extrait de Parfum", + "Eau de Cologne", + ], - page: 1, + page: 1, }; const HOME_COMMENTS: CommentCardProps[] = Array.from( - { length: 12 }, - (v, key) => { - return key % 2 == 0 - ? { - userAvatarUrl: "https://github.com/shadcn.png", - userName: "Nazar", - dateCommentLeft: "11.11", - rating: 4.4, - text: "I am pleasantly surprised by the service and quality of the fragrances! I ordered some perfume, and it arrived very quickly.", - } - : { - userAvatarUrl: "", - userName: "Misha", - dateCommentLeft: "02.01", - rating: 4.9, - text: "Satisfied.", - }; - }, + {length: 12}, + (v, key) => { + return key % 2 == 0 + ? { + userAvatarUrl: "https://github.com/shadcn.png", + userName: "Nazar", + dateCommentLeft: "11.11", + rating: 4.4, + text: "I am pleasantly surprised by the service and quality of the fragrances! I ordered some perfume, and it arrived very quickly.", + } + : { + userAvatarUrl: "", + userName: "Misha", + dateCommentLeft: "02.01", + rating: 4.9, + text: "Satisfied.", + }; + }, ); const BRANDS: BrandType[] = [ - "Versace", - "Jean Paul Gaultier", - "Giorgio Armani", - "Dior", - "Yves Saint Laurent", - "Montblanc", - "Paco Rabanne", - "Chanel", - "Tom Ford", - "Creed", - "Hermès", - "Dolce & Gabbana", - "Hugo Boss", - "Gucci", - "Prada", - "Valentino", - "Givenchy", - "1million", - "2million", - "3million", - "1hundred" - ].map((b) => ({id: b, name: b})); + "Versace", + "Jean Paul Gaultier", + "Giorgio Armani", + "Dior", + "Yves Saint Laurent", + "Montblanc", + "Paco Rabanne", + "Chanel", + "Tom Ford", + "Creed", + "Hermès", + "Dolce & Gabbana", + "Hugo Boss", + "Gucci", + "Prada", + "Valentino", + "Givenchy", + "1million", + "2million", + "3million", + "1hundred" +].map((b) => ({id: b, name: b})); -export { PRODUCTS, MOCK_SHOP_FILTERS, HOME_COMMENTS, BRANDS }; +const CART_ITEMS: CartItemType[] = [ + { + imageUrl: "https://i.makeup.it/2/2h/2h0tbxmkoqlr.jpg", + name: "Montblanc Explorer", + variant: { + volume: 100, + price: 7084, + discountedPrice: undefined, + wishlist: true, + quantityInStock: 34, + }, + productId: "asdf-wedg-cfad", + productCode: "422454", + quantity: 2, + }, + { + imageUrl: "https://i.makeup.it/9/9i/9iajbg7jxhit.jpg", + name: "Jean Paul Gaultier Le Beau", + variant: { + volume: 75, + price: 7999, + discountedPrice: undefined, + wishlist: true, + quantityInStock: 12, + }, + productId: "feqd-asdv-edwq", + productCode: "123456", + quantity: 1, + } +] + +export {PRODUCTS, MOCK_SHOP_FILTERS, HOME_COMMENTS, BRANDS, CART_ITEMS}; From 7063cd06767ec5862ee82b05afd5926401baabd9 Mon Sep 17 00:00:00 2001 From: Nazar Kyselov Date: Mon, 13 Apr 2026 16:12:18 +0200 Subject: [PATCH 05/15] feat(cart): create the page - page.tsx is a server wrapper. Gets data and passes it to the client component - the component displays the list of cart items and the total price. --- app/account/cart/page.tsx | 39 +++++++++ .../pages/account/cart/cartPageClient.tsx | 83 +++++++++++++++++++ 2 files changed, 122 insertions(+) create mode 100644 app/account/cart/page.tsx create mode 100644 components/pages/account/cart/cartPageClient.tsx diff --git a/app/account/cart/page.tsx b/app/account/cart/page.tsx new file mode 100644 index 0000000..b3944b1 --- /dev/null +++ b/app/account/cart/page.tsx @@ -0,0 +1,39 @@ +"use server"; + +/* Components */ +import Breadcrumbs, {BreadcrumbItem} from "@/components/ui/breadcrumbs"; +import CartPageClient from "@/components/pages/account/cart/cartPageClient"; +import Nav from "@/components/layout/nav"; + +/* Lib */ +import getCartItems from "@/lib/api/cartItems"; + +export default async function CartPage() { + const cartItems = await getCartItems("12344321"); + + const breadcrumbsItems: BreadcrumbItem[] = [ + { label: "Scent", href: "/" }, + { label: "Cart", href: "/account/cart" }, + ]; + + return ( + <> +
+
+
+ {/* Header */} +
+

+ Cart +

+ +
+ + {/* Interactive Client Part */} + {/* Cart items list & total*/} + +
+ + ); +} \ No newline at end of file diff --git a/components/pages/account/cart/cartPageClient.tsx b/components/pages/account/cart/cartPageClient.tsx new file mode 100644 index 0000000..dd485c8 --- /dev/null +++ b/components/pages/account/cart/cartPageClient.tsx @@ -0,0 +1,83 @@ +"use client"; + +/* React */ +import {useMemo, useState} from "react"; + +/* Next.js */ +import Link from "next/link"; + +/* Components */ +import CartItem from "@/components/cartItem"; +import {Button} from "@/components/ui/button"; + +/* Lib */ +import {CartItemType} from "@/lib/types"; +import {getEuro} from "@/lib/utils"; + +interface CartPageClientProps { + cartItems: CartItemType[]; +} + +const DELIVERY_COST = 0; + +export default function CartPageClient({cartItems}: CartPageClientProps) { + const [items, setItems] = useState(cartItems); + const orderPrice = useMemo(() => items.reduce((sum, item) => sum + item.variant.price * item.quantity, 0), [items]); + const totalPrice = useMemo(() => orderPrice + DELIVERY_COST, [orderPrice, DELIVERY_COST]) + + const updateQuantity = (id: string, delta: number) => { + setItems((prev) => + prev.map((item) => { + if (item.productId === id) { + const newQuantity = Math.max(1, item.quantity + delta); + return {...item, quantity: newQuantity}; + } + return item; + }) + ); + }; + + const removeItem = (id: string) => { + setItems((prev) => prev.filter((item) => item.productId !== id)); + }; + + return ( + <> +
+ {items.length > 0 && items.map((item, index) => ( + + ))} +
+ +
+
+ Order price + {getEuro(orderPrice)} +
+
+ Estimated delivery price + {getEuro(DELIVERY_COST)} +
+
+ Total + {getEuro(totalPrice)} +
+ +
+ + ); +} \ No newline at end of file From ebe575b344cf1b587f168d346e8b5a8204ddd2aa Mon Sep 17 00:00:00 2001 From: Nazar Kyselov Date: Mon, 13 Apr 2026 16:25:21 +0200 Subject: [PATCH 06/15] fix(demo): prop title->name --- app/demo/page.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/demo/page.tsx b/app/demo/page.tsx index 6ba015d..4c3f511 100644 --- a/app/demo/page.tsx +++ b/app/demo/page.tsx @@ -180,7 +180,7 @@ export default function Page() {
console.log("Deleted")} + onQuantityChange={() => console.log("QuantityChange")} /> console.log("Deleted")} + onQuantityChange={() => console.log("QuantityChange")} />
From f4dda0ce6b1b5c184841c42a1d60b4443aa61a71 Mon Sep 17 00:00:00 2001 From: Nazar Kyselov Date: Mon, 13 Apr 2026 16:26:01 +0200 Subject: [PATCH 07/15] chore(stepper): handle increment, decr --- components/stepper/stepper.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/components/stepper/stepper.tsx b/components/stepper/stepper.tsx index 060cfdc..0a68b91 100644 --- a/components/stepper/stepper.tsx +++ b/components/stepper/stepper.tsx @@ -8,7 +8,7 @@ export interface StepperProps { step?: number; disabled?: boolean; size?: "md" | "sm"; - onChange: (delta: number) => void; + onChange: (newValue: number) => void; } export default function Stepper({ @@ -21,10 +21,10 @@ export default function Stepper({ onChange, }: StepperProps) { const handleDecrement = (): void => { - onChange(-step); + onChange(Math.max(min, value - step)); }; const handleIncrement = (): void => { - onChange(step); + onChange(Math.min(max, value + step)); }; const sizeClass = { From e7fad9e3c436ad28cd7b86728d5b3ddbf27ebbde Mon Sep 17 00:00:00 2001 From: Nazar Kyselov Date: Mon, 13 Apr 2026 16:26:38 +0200 Subject: [PATCH 08/15] chore(cartItems): onQuantityChange emit a newValue instead of a delta --- components/cartItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cartItem.tsx b/components/cartItem.tsx index 9cd133b..f702063 100644 --- a/components/cartItem.tsx +++ b/components/cartItem.tsx @@ -22,7 +22,7 @@ export interface CartItemProps { quantityInStock: number; pricePerItem: number; // cents onDelete: (id: string) => void; - onQuantityChange: (id: string, delta: number) => void; + onQuantityChange: (id: string, newValue: number) => void; } export default function CartItem({ From edca75467b68e577eb6a53cae3703ec8eccd229c Mon Sep 17 00:00:00 2001 From: Nazar Kyselov Date: Mon, 13 Apr 2026 16:27:07 +0200 Subject: [PATCH 09/15] chore(cartPageClient): updateQuantity - handle newValue instead of delta --- components/pages/account/cart/cartPageClient.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/components/pages/account/cart/cartPageClient.tsx b/components/pages/account/cart/cartPageClient.tsx index dd485c8..9b3cd89 100644 --- a/components/pages/account/cart/cartPageClient.tsx +++ b/components/pages/account/cart/cartPageClient.tsx @@ -25,13 +25,10 @@ export default function CartPageClient({cartItems}: CartPageClientProps) { const orderPrice = useMemo(() => items.reduce((sum, item) => sum + item.variant.price * item.quantity, 0), [items]); const totalPrice = useMemo(() => orderPrice + DELIVERY_COST, [orderPrice, DELIVERY_COST]) - const updateQuantity = (id: string, delta: number) => { + const updateQuantity = (id: string, newValue: number) => { setItems((prev) => prev.map((item) => { - if (item.productId === id) { - const newQuantity = Math.max(1, item.quantity + delta); - return {...item, quantity: newQuantity}; - } + if (item.productId === id) return {...item, quantity: newValue}; return item; }) ); @@ -61,7 +58,8 @@ export default function CartPageClient({cartItems}: CartPageClientProps) { ))} -
+
Order price {getEuro(orderPrice)} From 1dd72859526cd74e716cf50a95cc43f169e3e442 Mon Sep 17 00:00:00 2001 From: Nazar Kyselov Date: Mon, 13 Apr 2026 16:28:03 +0200 Subject: [PATCH 10/15] chore(cartItem): remove unused import --- components/cartItem.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/components/cartItem.tsx b/components/cartItem.tsx index f702063..028e1b3 100644 --- a/components/cartItem.tsx +++ b/components/cartItem.tsx @@ -8,7 +8,6 @@ import { } from "@/lib/utils"; import {Trash2} from "lucide-react"; import Image from "next/image"; -import {useState} from "react"; import Stepper from "./stepper/stepper"; import Link from "next/link"; @@ -37,7 +36,7 @@ export default function CartItem({ onDelete, onQuantityChange, }: CartItemProps) { - const availability = getAvailability(quantityInStock); + const availability: AvailabilityType = getAvailability(quantityInStock); const availabilityClass: string = getAvailabilityClass(availability); const productLink: string = `/shop/product/${productId}`; From 793f010b6c9ad8fa0a9b4b14aff3e78293cf94be Mon Sep 17 00:00:00 2001 From: Nazar Kyselov Date: Mon, 13 Apr 2026 16:28:34 +0200 Subject: [PATCH 11/15] Update cartItems.ts --- lib/api/cartItems.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/api/cartItems.ts b/lib/api/cartItems.ts index 94d1f3e..e6a9a53 100644 --- a/lib/api/cartItems.ts +++ b/lib/api/cartItems.ts @@ -3,7 +3,7 @@ import {CART_ITEMS} from "../data"; export default async function getCartItems(userId: string) { // Simulatel DB network delay await new Promise((resolve) => setTimeout(resolve, 500)); - console.log("Cart items fetched successfully."); + console.log(`Cart items of the user ${userId} fetched successfully.`); return CART_ITEMS; } \ No newline at end of file From 301718fb005fc40ef4803f8f972ad7a692d07ad8 Mon Sep 17 00:00:00 2001 From: Nazar Kyselov Date: Mon, 13 Apr 2026 16:29:06 +0200 Subject: [PATCH 12/15] chore(cartPageClient): totalPrice deps --- components/pages/account/cart/cartPageClient.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/pages/account/cart/cartPageClient.tsx b/components/pages/account/cart/cartPageClient.tsx index 9b3cd89..2647ed2 100644 --- a/components/pages/account/cart/cartPageClient.tsx +++ b/components/pages/account/cart/cartPageClient.tsx @@ -23,7 +23,7 @@ const DELIVERY_COST = 0; export default function CartPageClient({cartItems}: CartPageClientProps) { const [items, setItems] = useState(cartItems); const orderPrice = useMemo(() => items.reduce((sum, item) => sum + item.variant.price * item.quantity, 0), [items]); - const totalPrice = useMemo(() => orderPrice + DELIVERY_COST, [orderPrice, DELIVERY_COST]) + const totalPrice = useMemo(() => orderPrice + DELIVERY_COST, [items]) const updateQuantity = (id: string, newValue: number) => { setItems((prev) => From a8370cad12d36f15e5a0e4c436d074c2e0071f7a Mon Sep 17 00:00:00 2001 From: Nazar Kyselov Date: Mon, 13 Apr 2026 16:37:51 +0200 Subject: [PATCH 13/15] feat(cartPageClient): handle empty cart case --- .../pages/account/cart/cartPageClient.tsx | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/components/pages/account/cart/cartPageClient.tsx b/components/pages/account/cart/cartPageClient.tsx index 2647ed2..8455867 100644 --- a/components/pages/account/cart/cartPageClient.tsx +++ b/components/pages/account/cart/cartPageClient.tsx @@ -13,6 +13,7 @@ import {Button} from "@/components/ui/button"; /* Lib */ import {CartItemType} from "@/lib/types"; import {getEuro} from "@/lib/utils"; +import {Search} from "lucide-react"; interface CartPageClientProps { cartItems: CartItemType[]; @@ -38,6 +39,27 @@ export default function CartPageClient({cartItems}: CartPageClientProps) { setItems((prev) => prev.filter((item) => item.productId !== id)); }; + if (items.length === 0) return ( +
+
+

+ Your cart is empty{" "} +

{" "} + {" "} +
+

+ You will find something you like in the shop! +

+ +
+ ); + return ( <>
From 8db002a53af0fc7b60479dd0ab0147f90d2b554a Mon Sep 17 00:00:00 2001 From: Nazar Kyselov Date: Mon, 13 Apr 2026 16:38:53 +0200 Subject: [PATCH 14/15] chore(header): update cart link URL --- components/layout/header.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/layout/header.tsx b/components/layout/header.tsx index 820657d..15a7672 100644 --- a/components/layout/header.tsx +++ b/components/layout/header.tsx @@ -280,7 +280,7 @@ function SearchDropdown({ isOpen, query }: { isOpen: boolean; query: string }) { function CartLink({ items = 0 }: { items?: number }) { return ( From 8156acd0995f4ffe42015f35353c0619316e8ec6 Mon Sep 17 00:00:00 2001 From: Nazar Kyselov Date: Mon, 13 Apr 2026 16:41:00 +0200 Subject: [PATCH 15/15] fix(eslint) - (cartPageClient): set correct dependencies - (lib/data): remove unused import --- components/pages/account/cart/cartPageClient.tsx | 2 +- lib/data.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/components/pages/account/cart/cartPageClient.tsx b/components/pages/account/cart/cartPageClient.tsx index 8455867..3d23eb7 100644 --- a/components/pages/account/cart/cartPageClient.tsx +++ b/components/pages/account/cart/cartPageClient.tsx @@ -24,7 +24,7 @@ const DELIVERY_COST = 0; export default function CartPageClient({cartItems}: CartPageClientProps) { const [items, setItems] = useState(cartItems); const orderPrice = useMemo(() => items.reduce((sum, item) => sum + item.variant.price * item.quantity, 0), [items]); - const totalPrice = useMemo(() => orderPrice + DELIVERY_COST, [items]) + const totalPrice = useMemo(() => orderPrice + DELIVERY_COST, [orderPrice]); const updateQuantity = (id: string, newValue: number) => { setItems((prev) => diff --git a/lib/data.ts b/lib/data.ts index d109e39..97a8d7c 100644 --- a/lib/data.ts +++ b/lib/data.ts @@ -1,6 +1,5 @@ import {CommentCardProps} from "@/components/commentCard/commentCard"; import {BrandType, CartItemType, FilterState, ProductType} from "./types"; -import {CartItemProps} from "@/components/cartItem"; const PRODUCTS: ProductType[] = [ {