diff --git a/package.json b/package.json index 6f968124..28ed9084 100644 --- a/package.json +++ b/package.json @@ -9,19 +9,25 @@ "lint": "next lint" }, "dependencies": { + "@hookform/resolvers": "^5.2.1", + "lucide-react": "^0.541.0", + "next": "15.4.5", "react": "19.1.0", "react-dom": "19.1.0", - "next": "15.4.5" + "react-hook-form": "^7.62.0", + "react-toastify": "^11.0.5", + "zod": "^4.1.5", + "zustand": "^5.0.8" }, "devDependencies": { - "typescript": "^5", + "@eslint/eslintrc": "^3", + "@tailwindcss/postcss": "^4", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", - "@tailwindcss/postcss": "^4", - "tailwindcss": "^4", "eslint": "^9", "eslint-config-next": "15.4.5", - "@eslint/eslintrc": "^3" + "tailwindcss": "^4", + "typescript": "^5" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fbeef25e..be788fa5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,12 @@ importers: .: dependencies: + '@hookform/resolvers': + specifier: ^5.2.1 + version: 5.2.1(react-hook-form@7.62.0(react@19.1.0)) + lucide-react: + specifier: ^0.541.0 + version: 0.541.0(react@19.1.0) next: specifier: 15.4.5 version: 15.4.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -17,6 +23,18 @@ importers: react-dom: specifier: 19.1.0 version: 19.1.0(react@19.1.0) + react-hook-form: + specifier: ^7.62.0 + version: 7.62.0(react@19.1.0) + react-toastify: + specifier: ^11.0.5 + version: 11.0.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + zod: + specifier: ^4.1.5 + version: 4.1.5 + zustand: + specifier: ^5.0.8 + version: 5.0.8(@types/react@19.1.9)(react@19.1.0) devDependencies: '@eslint/eslintrc': specifier: ^3 @@ -103,6 +121,11 @@ packages: resolution: {integrity: sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@hookform/resolvers@5.2.1': + resolution: {integrity: sha512-u0+6X58gkjMcxur1wRWokA7XsiiBJ6aK17aPZxhkoYiK5J+HcTx0Vhu9ovXe6H+dVpO6cjrn2FkJTryXEMlryQ==} + peerDependencies: + react-hook-form: ^7.55.0 + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -341,6 +364,9 @@ packages: '@rushstack/eslint-patch@1.12.0': resolution: {integrity: sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw==} + '@standard-schema/utils@0.3.0': + resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} + '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} @@ -727,6 +753,10 @@ packages: client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1347,6 +1377,11 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true + lucide-react@0.541.0: + resolution: {integrity: sha512-s0Vircsu5WaGv2KoJZ5+SoxiAJ3UXV5KqEM3eIFDHaHkcLIFdIWgXtZ412+Gh02UsdS7Was+jvEpBvPCWQISlg==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} @@ -1527,9 +1562,21 @@ packages: peerDependencies: react: ^19.1.0 + react-hook-form@7.62.0: + resolution: {integrity: sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-toastify@11.0.5: + resolution: {integrity: sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA==} + peerDependencies: + react: ^18 || ^19 + react-dom: ^18 || ^19 + react@19.1.0: resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} engines: {node: '>=0.10.0'} @@ -1797,6 +1844,27 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + zod@4.1.5: + resolution: {integrity: sha512-rcUUZqlLJgBC33IT3PNMgsCq6TzLQEG/Ei/KTCU0PedSWRMAXoOUN+4t/0H+Q8bdnLPdqUYnvboJT0bn/229qg==} + + zustand@5.0.8: + resolution: {integrity: sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + snapshots: '@alloc/quick-lru@5.2.0': {} @@ -1866,6 +1934,11 @@ snapshots: '@eslint/core': 0.15.1 levn: 0.4.1 + '@hookform/resolvers@5.2.1(react-hook-form@7.62.0(react@19.1.0))': + dependencies: + '@standard-schema/utils': 0.3.0 + react-hook-form: 7.62.0(react@19.1.0) + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.6': @@ -2038,6 +2111,8 @@ snapshots: '@rushstack/eslint-patch@1.12.0': {} + '@standard-schema/utils@0.3.0': {} + '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 @@ -2434,6 +2509,8 @@ snapshots: client-only@0.0.1: {} + clsx@2.1.1: {} + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -3200,6 +3277,10 @@ snapshots: dependencies: js-tokens: 4.0.0 + lucide-react@0.541.0(react@19.1.0): + dependencies: + react: 19.1.0 + magic-string@0.30.17: dependencies: '@jridgewell/sourcemap-codec': 1.5.4 @@ -3374,8 +3455,18 @@ snapshots: react: 19.1.0 scheduler: 0.26.0 + react-hook-form@7.62.0(react@19.1.0): + dependencies: + react: 19.1.0 + react-is@16.13.1: {} + react-toastify@11.0.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + clsx: 2.1.1 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react@19.1.0: {} reflect.getprototypeof@1.0.10: @@ -3771,3 +3862,10 @@ snapshots: yallist@5.0.0: {} yocto-queue@0.1.0: {} + + zod@4.1.5: {} + + zustand@5.0.8(@types/react@19.1.9)(react@19.1.0): + optionalDependencies: + '@types/react': 19.1.9 + react: 19.1.0 diff --git a/src/app/cart/page.tsx b/src/app/cart/page.tsx new file mode 100644 index 00000000..26b7ee70 --- /dev/null +++ b/src/app/cart/page.tsx @@ -0,0 +1,193 @@ +"use client" + +import PaymentForm from "@/components/PaymentForm" +import ShippingForm from "@/components/ShippinForm" +import useCartStore from "@/stores/cartStore" +import { CartItemsType, shippingFormInputs } from "@/types" +import { ArrowRight, Trash2 } from "lucide-react" +import Image from "next/image" +import { useRouter, useSearchParams } from "next/navigation" +import { useState } from "react" + +const steps =[ + {id:1, + title:"Shipping Cart" + }, + {id:2, + title:"Shipping Address" + }, + {id:3, + title:"Shipping Method" + } +] + + +// const cartItems:CartItemsType =[ +// { +// id:1, +// name:"Product 1", +// shortDescription:"This is product 1", +// description:"lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", +// price:9.99, +// sizes:["S","M","L","XL"], +// colors:["gray","purple","green"], +// images:{ +// gray:"/products/1g.png", +// purple:"/products/1p.png", +// green:"/products/1gr.png" +// }, +// quantity:1, +// selectedSize:"M", +// selectedColor:"gray" +// }, + +// { +// id:2, +// name:"Product 2", +// shortDescription:"This is product 2", +// description:"lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", +// price:19.99, +// sizes:["S","M","L","XL"], +// colors:["gray","green"], +// images:{ +// gray:"/products/2g.png", +// green:"/products/2gr.png" +// }, +// quantity:3, +// selectedSize:"L", +// selectedColor:"green" +// }, + +// { +// id:3, +// name:"Product 3", +// shortDescription:"This is product 3", +// description:"lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", +// price:29.99, +// sizes:["S","M","L","XL"], +// colors:["green","black","blue"], +// images:{ +// green:"/products/3gr.png", +// black:"/products/3bl.png", +// blue:"/products/3b.png" +// }, +// quantity:1, +// selectedSize:"XL", +// selectedColor:"black" +// }, +// ] + +const CartPage =() =>{ + const searchParams = useSearchParams() + const router = useRouter() + const [shippingForm,setShippingForm] = useState() + + const activeStep = parseInt(searchParams.get("step")|| "1") + + const {cart, removeFromCart} = useCartStore(); + return( +
+ {/* title */} +

Your Shopping Cart

+ {/* steps */} +
+ {steps.map(step=>( +
+
{step.id} +
+

+ {step.title} +

+
+ ))} + +
+ {/* steps and details */} +
+ {/* steps */} +
+ {activeStep===1 ? ( + cart.map((item)=>( + //Single Cart Item +
+ {/* Image and details */} +
+ {/* Image */} +
+ {item.name} +
+ {/* item details */} +
+
+

{item.name}

+

Quantity: {" "}{item.quantity}

+

Size: {item.selectedSize}

+

Color: {item.selectedColor}

+ +
+

${item.price.toFixed(2)}

+ +
+ +
+ {/* delete button */} + +
+ )) + ): + activeStep===2 ?(): + activeStep===3 && shippingForm? (): + (

Please fill the shipping form to continue

)} +
+ {/* details */} +
+

Cart Details

+
+
+

Subtotal

+

+ ${cart.reduce((acc,item)=> acc + item.price*item.quantity,0).toFixed(2)} +

+
+
+

Discount (10%)

+

$10

+ +
+
+

Shipping Fee

+

$10

+ +
+
+
+

Total

+

${cart.reduce((acc,item)=> acc + item.price*item.quantity,0).toFixed(2) + } +

+ +
+
+ {activeStep===1 && + } +
+
+
+ ) +} + +export default CartPage; \ No newline at end of file diff --git a/src/app/layout.tsx b/src/app/layout.tsx index f7fa87eb..cdea476c 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,9 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; +import Navbar from "@/components/Navbar"; +import Footer from "@/components/Footer"; +import { ToastContainer } from "react-toastify"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -13,8 +16,8 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "Zeinab Store - the best clothes", + description: "the best place to find high quality clothes", }; export default function RootLayout({ @@ -27,7 +30,12 @@ export default function RootLayout({ - {children} +
+ + {children} +
+
+ ); diff --git a/src/app/page.tsx b/src/app/page.tsx index 3d365a49..72eb6271 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,6 +1,18 @@ -const Homepage = () => { +import ProductList from "@/components/ProductList" +import Image from "next/image" + +const Homepage = async({searchParams}:{searchParams:Promise<{category:string}>;}) => { + + const category = (await searchParams).category; return ( -
Homepage
+ +
+ +
+ featured +
+ +
) } diff --git a/src/app/products/[id]/page.tsx b/src/app/products/[id]/page.tsx new file mode 100644 index 00000000..26b8d072 --- /dev/null +++ b/src/app/products/[id]/page.tsx @@ -0,0 +1,72 @@ +import ProductInteraction from "@/components/ProductInteraction"; +import { ProductType } from "@/types"; +import Image from "next/image"; +import { title } from "process"; + + +//Temporary data +const product : ProductType ={ + id:1, + name:"Product 1", + shortDescription:"This is product 1", + description:"lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + price:9.99, + sizes:["S","M","L","XL"], + colors:["gray","purple","green"], + images:{ + gray:"/products/1g.png", + purple:"/products/1p.png", + green:"/products/1gr.png" + }, +} + +export const generateMetadata = async ({params}:{params: {id:string}})=>{ + //todo: fetch product data from database + //temporary + return{ + title: product.name, + description:product.description, + } +} + +const ProductPage = async ({param, searchParams}:{param: Promise<{id:string}> ;searchParams:Promise<{color:string ; size:string}>})=>{ + + const {size, color} = await searchParams + + const selectedSize = (size || product.sizes[0] as string) + const selectedColor = (color || product.colors[0] as string) + return ( +
+ {/* Image */} +
+ {product.name} +
+ {/* Details */} +
+

{product.name}

+

{product.description}

+

${product.price.toFixed(2)}

+ + + + {/* Card Info */} +
+ klarna + cards + stripe +
+ +

+ by completing your purchase you agree to our Terms of + Service and Privacy Policy + you authorize us to charge your card or other payment method + for this purchase and future purchases according to our terms. + You agree that you are responsible for any additional fees + imposed by your bank or card issuer. +

+
+
+ ) +} + +export default ProductPage; \ No newline at end of file diff --git a/src/app/products/page.tsx b/src/app/products/page.tsx new file mode 100644 index 00000000..486f83ee --- /dev/null +++ b/src/app/products/page.tsx @@ -0,0 +1,13 @@ +import ProductList from "@/components/ProductList" + +const ProductsPage = async({searchParams}:{searchParams:Promise<{category:string}>}) =>{ + + const category = (await searchParams).category; + return ( +
+ +
+ ) + +} +export default ProductsPage \ No newline at end of file diff --git a/src/components/Categories.tsx b/src/components/Categories.tsx new file mode 100644 index 00000000..27aebc1c --- /dev/null +++ b/src/components/Categories.tsx @@ -0,0 +1,78 @@ +"use client"; + +import { Briefcase, Footprints, Glasses, Hand, Shirt, ShoppingBasket, Venus } from "lucide-react"; +import { usePathname, useRouter, useSearchParams } from "next/navigation"; + +const categories = [ + { + name: "All", + icon: , + slug: "all", + }, + { + name: "T-shirts", + icon: , + slug: "t-shirts", + }, + { + name: "Shoes", + icon: , + slug: "shoes", + }, + { + name: "Accessories", + icon: , + slug: "accessories", + }, + { + name: "Bags", + icon: , + slug: "bags", + }, + { + name: "Dresses", + icon: , + slug: "dresses", + }, + { + name: "Jackets", + icon: , + slug: "jackets", + }, + { + name: "Gloves", + icon: , + slug: "gloves", + }, +]; + + + +const Categories = () => { + const searchParams = useSearchParams(); + const router = useRouter(); + const pathname = usePathname(); + + const selectedCategory = searchParams.get("category"); + const handleChange = (value:string |null)=>{ + const params = new URLSearchParams(searchParams); + params.set("category",value || "all"); + router.push(`${pathname}?${params.toString()}` , {scroll:false} ) + } + + + return
+ {categories.map(category=>( +
handleChange(category.slug)} + > + {category.icon} + {category.name} +
+ + + ))} +
+} +export default Categories \ No newline at end of file diff --git a/src/components/Filter.tsx b/src/components/Filter.tsx new file mode 100644 index 00000000..238c1f75 --- /dev/null +++ b/src/components/Filter.tsx @@ -0,0 +1,30 @@ +"use client" + +import { usePathname, useRouter, useSearchParams } from "next/navigation"; + +const Filter = () =>{ + + const searchParams = useSearchParams(); + const router = useRouter(); + const pathname = usePathname(); + + const handleFilter = (value:string)=>{ + const params = new URLSearchParams(searchParams); + params.set("sort",value); + router.push(`${pathname}?${params.toString()}` , {scroll:false} ) + } + + return( +
+ Sort by: + +
+ ) + +} +export default Filter \ No newline at end of file diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx new file mode 100644 index 00000000..c6b3af22 --- /dev/null +++ b/src/components/Footer.tsx @@ -0,0 +1,47 @@ +import Image from "next/image"; +import Link from "next/link"; + +const Footer =()=>{ + return( +
+
+ + TrendLama +

TRENDLAMA.

+ +

o 2025 Trendlama

+

All rights reserved

+
+ + +
+

Links

+ Homepage + Contact + Terms of service + Privacy Policy +
+ +
+

Products

+ All Products + New Arrival + Best Sellers + Sale +
+ + +
+

Support

+ FAQs + Shipping + Returns + Order Status +
+
+ ) +} +export default Footer; \ No newline at end of file diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx new file mode 100644 index 00000000..b2c97615 --- /dev/null +++ b/src/components/Navbar.tsx @@ -0,0 +1,33 @@ +import Image from "next/image"; +import Link from "next/link"; +import SearchBar from "./SearchBar"; +import { Bell, Home, ShoppingCart } from "lucide-react"; +import ShopingCartIcon from "./ShopingCartIcon"; + +const Navbar = () => { + return ( + + ) +} +export default Navbar; \ No newline at end of file diff --git a/src/components/PaymentForm.tsx b/src/components/PaymentForm.tsx new file mode 100644 index 00000000..f20f7ead --- /dev/null +++ b/src/components/PaymentForm.tsx @@ -0,0 +1,112 @@ +import { PaymentFormInputs, paymentFormSchema} from "@/types"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { ArrowRight, ShoppingCart } from "lucide-react"; +import Image from "next/image"; +import { useRouter } from "next/navigation"; +import { SubmitHandler, useForm } from "react-hook-form"; + + + +const PaymentForm = () => { + const { + register, + handleSubmit, + formState:{errors} + } = useForm({ + resolver: zodResolver(paymentFormSchema), + }); + const router = useRouter(); + const handlePaymentForm: SubmitHandler = (data)=>{ + + + } + return ( +
+
+ + + {errors.cardHolder &&( +

{errors.cardHolder?.message}

+ )} +
+ +
+ + + {errors.cardNumber &&( +

{errors.cardNumber?.message}

+ )} +
+ +
+ + + {errors.expirationDate &&( +

{errors.expirationDate?.message}

+ )} +
+ +
+ + + {errors.cvv &&( +

{errors.cvv?.message}

+ )} + + +
+ +
+ klarna + cards + stripe + + +
+ + + + + + +
+ ); +} +export default PaymentForm; \ No newline at end of file diff --git a/src/components/ProductCard.tsx b/src/components/ProductCard.tsx new file mode 100644 index 00000000..caedca65 --- /dev/null +++ b/src/components/ProductCard.tsx @@ -0,0 +1,100 @@ +"use client"; + +import useCartStore from "@/stores/cartStore"; +import { ProductType } from "@/types"; +import { ShoppingCart } from "lucide-react"; +import Image from "next/image"; +import Link from "next/link"; +import { useState } from "react"; +import { toast } from "react-toastify"; + +const ProductCard = ({product}:{product:ProductType}) => { + + const [productTypes, setProductTypes] =useState({ + size: product.sizes[0], + color: product.colors[0], + }); + + const {addToCart} = useCartStore(); + + const handleProductType = ({type,value}:{type:"size" | "color", value:string})=>{ + setProductTypes(prev=>({ + ...prev, + [type]:value + })) + }; + + const handleAddToCart = ()=>{ + addToCart({ + ...product, + quantity:1, + selectedSize:productTypes.size, + selectedColor:productTypes.color, + }); + toast.success("Product added to cart"); + } + + + return( +
+ {/* Image */} + +
+ {product.name} +
+ + + {/* product detail */} +
+

{product.name}

+

{product.shortDescription}

+ + {/* product types */} +
+ {/* Sizes */} +
+ Size + + +
+ + {/* Colors */} +
+ Colors +
+ {product.colors.map(color=>( +
handleProductType({type:"color", value:color})}> +
+
+ ))} +
+ +
+
+ + {/* prices and add to Carts */} +
+

${product.price.toFixed(2)}

+ + +
+ +
+ +
+ ) +} +export default ProductCard \ No newline at end of file diff --git a/src/components/ProductInteraction.tsx b/src/components/ProductInteraction.tsx new file mode 100644 index 00000000..6e3fc873 --- /dev/null +++ b/src/components/ProductInteraction.tsx @@ -0,0 +1,120 @@ +"use client" + +import useCartStore from "@/stores/cartStore"; +import { ProductType } from "@/types" +import { Minus, Plus, ShoppingCart } from "lucide-react"; +import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import { useState } from "react"; +import { toast } from "react-toastify"; + + +const ProductInteraction = ({ + product, + selectedSize, + selectedColor +}:{ + product:ProductType; + selectedSize:string; + selectedColor:string +}) => { + const router = useRouter(); + const pathname = usePathname(); + const searchparams = useSearchParams(); + + const [quantity, setQuantity] = useState(1) + + const {addToCart} = useCartStore() + + const handleTypeChange = (type:string, value:string) =>{ + const params = new URLSearchParams(searchparams.toString()); + params.set(type, value); + router.push(`${pathname}?${params.toString()}`, {scroll:false}); + }; + + const handleQuantityChange= (type: "increment" | "decrement")=>{ + if(type==="increment"){ + setQuantity((prev)=>prev+1); + }else{ + if(quantity>1){ + setQuantity((prev)=>prev-1); + } + } + }; + + const handleAddToCart = ()=>{ + addToCart({ + ...product, + quantity, + selectedColor, + selectedSize, + }); + toast.success("Product added to the cart") + } + + + return( +
+ {/* SIZE */} +
+ Size +
+ {product.sizes.map((size) => ( +
handleTypeChange("size",size)}> +
{size.toUpperCase()}
+
+ ))} +
+
+ + {/* COLOR */} +
+ Color +
+ {product.colors.map((color) => ( +
handleTypeChange("color",color)}> +
+
+ ))} +
+
+ + {/* QUANTITY */} +
+ Quantity +
+ + + {quantity} + + +
+
+ {/* BUTTONS */} + + + +
+ ) +} +export default ProductInteraction \ No newline at end of file diff --git a/src/components/ProductList.tsx b/src/components/ProductList.tsx new file mode 100644 index 00000000..b9151187 --- /dev/null +++ b/src/components/ProductList.tsx @@ -0,0 +1,144 @@ +import { ProductsType } from "@/types" +import Categories from "./Categories" +import ProductCard from "./ProductCard" +import Link from "next/link" +import Filter from "./Filter" + +const products:ProductsType = [ + { + id:1, + name:"Product 1", + shortDescription:"This is product 1", + description:"lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + price:9.99, + sizes:["S","M","L","XL"], + colors:["gray","purple","green"], + images:{ + gray:"/products/1g.png", + purple:"/products/1p.png", + green:"/products/1gr.png" + }, + }, + + { + id:2, + name:"Product 2", + shortDescription:"This is product 2", + description:"lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + price:19.99, + sizes:["S","M","L","XL"], + colors:["gray","green"], + images:{ + gray:"/products/2g.png", + green:"/products/2gr.png" + }, + }, + + { + id:3, + name:"Product 3", + shortDescription:"This is product 3", + description:"lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + price:29.99, + sizes:["S","M","L","XL"], + colors:["green","black","blue"], + images:{ + green:"/products/3gr.png", + black:"/products/3bl.png", + blue:"/products/3b.png" + }, + }, + + { + id:4, + name:"Product 4", + shortDescription:"This is product 4", + description:"lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + price:39.99, + sizes:["S","M","L","XL"], + colors:["white","purple"], + images:{ + purple:"/products/4p.png", + white:"/products/4w.png", + + }, + }, + + + { + id:5, + name:"Product 5", + shortDescription:"This is product 5", + description:"lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + price:49.99, + sizes:["S","M","L","XL"], + colors:["orange","red","balck"], + images:{ + orange:"/products/5o.png", + red:"/products/5r.png", + black:"/products/5bl.png", + + }, + }, + + { + id:6, + name:"Product 6", + shortDescription:"This is product 6", + description:"lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + price:59.99, + sizes:["S","M","L","XL"], + colors:["gray","white"], + images:{ + gray:"/products/6g.png", + white:"/products/6w.png", + }, + }, + + + { + id:7, + name:"Product 7", + shortDescription:"This is product 7", + description:"lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + price:69.99, + sizes:["S","M","L","XL"], + colors:["gray","purple"], + images:{ + gray:"/products/7g.png", + purple:"/products/7p.png", + }, + }, + + { + id:8, + name:"Product 8", + shortDescription:"This is product 8", + description:"lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + price:79.99, + sizes:["S","M","L","XL"], + colors:["black","green"], + images:{ + black:"/products/8b.png", + green:"/products/8gr.png", + }, + } +] + + +const ProductList = ({category, params}:{category:string ,params:"homePage" | "products"}) => { + return ( +
+ + {params === "products" && } +
+ {products.map(product=>( + + ))} +
+ View all products +
+ ) +} +export default ProductList \ No newline at end of file diff --git a/src/components/SearchBar.tsx b/src/components/SearchBar.tsx new file mode 100644 index 00000000..68708a1b --- /dev/null +++ b/src/components/SearchBar.tsx @@ -0,0 +1,11 @@ +import { Search } from "lucide-react"; + +const SearchBar =()=>{ + return( +
+ + +
+ ) +} +export default SearchBar; \ No newline at end of file diff --git a/src/components/ShippinForm.tsx b/src/components/ShippinForm.tsx new file mode 100644 index 00000000..1dbc9208 --- /dev/null +++ b/src/components/ShippinForm.tsx @@ -0,0 +1,119 @@ +import { shippingFormInputs, shippingFormSchema } from "@/types"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { ArrowRight } from "lucide-react"; +import { useRouter } from "next/navigation"; +import { SubmitHandler, useForm } from "react-hook-form"; + + + +const ShippingForm = ({setShippingForm}:{setShippingForm:(data:shippingFormInputs)=>void}) => { + const { + register, + handleSubmit, + formState:{errors} + } = useForm({ + resolver: zodResolver(shippingFormSchema), + }); + const router = useRouter(); + const handleShippingForm: SubmitHandler = (data)=>{ + setShippingForm(data); + router.push("/cart?step=3" , {scroll:false}); + + } + return ( +
+
+ + + {errors.name &&( +

{errors.name?.message}

+ )} +
+ +
+ + + {errors.email &&( +

{errors.email?.message}

+ )} +
+ +
+ + + {errors.phone &&( +

{errors.phone?.message}

+ )} +
+ +
+ + + {errors.address &&( +

{errors.address?.message}

+ )} + + +
+
+ + + {errors.city &&( +

{errors.city?.message}

+ )} +
+ + + + + + +
+ ); +} +export default ShippingForm; \ No newline at end of file diff --git a/src/components/ShopingCartIcon.tsx b/src/components/ShopingCartIcon.tsx new file mode 100644 index 00000000..ae273941 --- /dev/null +++ b/src/components/ShopingCartIcon.tsx @@ -0,0 +1,20 @@ + +"use client" +import useCartStore from "@/stores/cartStore" +import { ShoppingCart } from "lucide-react" +import Link from "next/link" + +const ShopingCartIcon = () => { + + const {cart, hasHydrated} = useCartStore() + if(!hasHydrated) return null; + return ( + + + + {cart.reduce((acc,item)=> acc +item.quantity,0)} + + + ) +} +export default ShopingCartIcon; \ No newline at end of file diff --git a/src/stores/cartStore.ts b/src/stores/cartStore.ts new file mode 100644 index 00000000..df05dfbe --- /dev/null +++ b/src/stores/cartStore.ts @@ -0,0 +1,55 @@ +import { CartStoreActionsType, CartStoreStateType } from '@/types' +import { create } from 'zustand' +import { persist, createJSONStorage } from 'zustand/middleware'; + +const useCartStore = create()( +persist( + (set) => ({ + cart: [], + hasHydrated: false, + addToCart: (product) => set((state) => { + const existingIndex = state.cart.findIndex((p) => + p.id === product.id && + p.selectedColor === product.selectedColor && + p.selectedSize === product.selectedSize + ); + + if (existingIndex !== -1) { + const updatedCart = [...state.cart]; + updatedCart[existingIndex].quantity += product.quantity || 1; + return { cart: updatedCart }; + } + + return { + cart: [ + ...state.cart, + { + ...product, + quantity:product.quantity|| 1, + selectedColor: product.selectedColor, + selectedSize: product.selectedSize + }, + ] + } + }), + removeFromCart: (product) => set((state) => ({ + cart: state.cart.filter((p) => !( + p.id === product.id && + p.selectedColor === product.selectedColor && + p.selectedSize === product.selectedSize + )) + })), + clearCart: () => set({ cart: [] }), + }), + { + name: "cart", + storage: createJSONStorage(() => localStorage), + onRehydrateStorage:()=>(state)=>{ + if(state) + state.hasHydrated = true; + } + }, +), + +); +export default useCartStore; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 00000000..f8fed036 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,55 @@ +import { Car, Phone } from "lucide-react"; +import { z } from "zod"; + +export type ProductType = { + id: number; + name: string; + shortDescription: string; + description: string; + price: number; + sizes: string[]; + colors: string[]; + images: Record; +}; + +export type ProductsType = ProductType[] + +export type CartItemType = ProductType & { + quantity: number; + selectedSize: string; + selectedColor: string; +}; +export type CartItemsType = CartItemType[] + + +export const shippingFormSchema = z.object({ + name: z.string().min(2,"Name must be at least 2 characters long"), + email: z.string().min(1,"Email is required"), + phone: z.string().min(7, "Phone number must be at least 10 digits").max(10,"Phone number must be between 7 and 10 digits").regex(/^\d+$/,"Phone number must contain only digits"), + address: z.string().min(5, "Address is required"), + city: z.string().min(1,"City is required"), + +}); +export type shippingFormInputs=z.infer; + +export const paymentFormSchema = z.object({ + cardHolder: z.string().min(2,"Card holder is required"), + cardNumber: z.string().length(16, "Card number must be 16 digits"), + expirationDate: z.string().regex(/^(0[1-9]|1[0-2])\/\d{2}$/, "Expiration date must be in MM/YY format"), + cvv: z.string().length(3, "CVV must be 3 digits"), +}); + +export type PaymentFormInputs = z.infer; + + +export type CartStoreStateType = { + cart:CartItemsType; + hasHydrated: boolean; +}; + +export type CartStoreActionsType = { + addToCart:(product:CartItemType) =>void; + removeFromCart:(product:CartItemType) =>void; + clearCart:() =>void; + +}; \ No newline at end of file