diff --git a/package-lock.json b/package-lock.json
index 0593704..bcb23de 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,11 +8,15 @@
"name": "ecommerce-training-app",
"version": "0.0.0",
"dependencies": {
+ "@hookform/resolvers": "^5.2.2",
"@tailwindcss/vite": "^4.2.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
+ "react-hook-form": "^7.72.0",
+ "react-hot-toast": "^2.6.0",
"react-router-dom": "^7.13.0",
"tailwindcss": "^4.2.0",
+ "yup": "^1.7.1",
"zustand": "^5.0.11"
},
"devDependencies": {
@@ -60,7 +64,6 @@
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
@@ -885,6 +888,18 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
+ "node_modules/@hookform/resolvers": {
+ "version": "5.2.2",
+ "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz",
+ "integrity": "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==",
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/utils": "^0.3.0"
+ },
+ "peerDependencies": {
+ "react-hook-form": "^7.55.0"
+ }
+ },
"node_modules/@humanfs/core": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@@ -1321,6 +1336,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@standard-schema/utils": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
+ "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
+ "license": "MIT"
+ },
"node_modules/@tailwindcss/node": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.0.tgz",
@@ -1660,7 +1681,6 @@
"integrity": "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==",
"devOptional": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"undici-types": "~7.18.0"
}
@@ -1671,7 +1691,6 @@
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
"devOptional": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -1841,7 +1860,6 @@
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"dev": true,
"license": "MIT",
- "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -1960,7 +1978,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -2099,7 +2116,6 @@
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
- "devOptional": true,
"license": "MIT"
},
"node_modules/debug": {
@@ -2246,7 +2262,6 @@
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -2584,6 +2599,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/goober": {
+ "version": "2.1.18",
+ "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.18.tgz",
+ "integrity": "sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "csstype": "^3.0.10"
+ }
+ },
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@@ -2596,7 +2620,6 @@
"integrity": "sha512-hR/uLYQdngTyEfxnOoa+e6KTcfBFyc1hgFj/Cc144A5JJUuHFYqIEBDcD4FeGqUeKLRZqJ9eN9u7/GDjYEgS1g==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@types/node": ">=20.0.0",
"@types/whatwg-mimetype": "^3.0.2",
@@ -3258,7 +3281,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -3304,6 +3326,12 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/property-expr": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz",
+ "integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==",
+ "license": "MIT"
+ },
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -3319,7 +3347,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -3329,7 +3356,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -3337,6 +3363,39 @@
"react": "^19.2.4"
}
},
+ "node_modules/react-hook-form": {
+ "version": "7.72.0",
+ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.72.0.tgz",
+ "integrity": "sha512-V4v6jubaf6JAurEaVnT9aUPKFbNtDgohj5CIgVGyPHvT9wRx5OZHVjz31GsxnPNI278XMu+ruFz+wGOscHaLKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/react-hook-form"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17 || ^18 || ^19"
+ }
+ },
+ "node_modules/react-hot-toast": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz",
+ "integrity": "sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==",
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.1.3",
+ "goober": "^2.1.16"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "react": ">=16",
+ "react-dom": ">=16"
+ }
+ },
"node_modules/react-refresh": {
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
@@ -3559,6 +3618,12 @@
"url": "https://opencollective.com/webpack"
}
},
+ "node_modules/tiny-case": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz",
+ "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==",
+ "license": "MIT"
+ },
"node_modules/tinybench": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
@@ -3602,6 +3667,12 @@
"node": ">=14.0.0"
}
},
+ "node_modules/toposort": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz",
+ "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==",
+ "license": "MIT"
+ },
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -3615,6 +3686,18 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/type-fest": {
+ "version": "2.19.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
+ "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==",
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=12.20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/undici-types": {
"version": "7.18.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
@@ -3668,7 +3751,6 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"esbuild": "^0.27.0",
"fdir": "^6.5.0",
@@ -3911,13 +3993,24 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/yup": {
+ "version": "1.7.1",
+ "resolved": "https://registry.npmjs.org/yup/-/yup-1.7.1.tgz",
+ "integrity": "sha512-GKHFX2nXul2/4Dtfxhozv701jLQHdf6J34YDh2cEkpqoo8le5Mg6/LrdseVLrFarmFygZTlfIhHx/QKfb/QWXw==",
+ "license": "MIT",
+ "dependencies": {
+ "property-expr": "^2.0.5",
+ "tiny-case": "^1.0.3",
+ "toposort": "^2.0.2",
+ "type-fest": "^2.19.0"
+ }
+ },
"node_modules/zod": {
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"dev": true,
"license": "MIT",
- "peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
diff --git a/package.json b/package.json
index 2f25bba..f1bb0aa 100644
--- a/package.json
+++ b/package.json
@@ -12,11 +12,15 @@
"test:watch": "vitest"
},
"dependencies": {
+ "@hookform/resolvers": "^5.2.2",
"@tailwindcss/vite": "^4.2.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
+ "react-hook-form": "^7.72.0",
+ "react-hot-toast": "^2.6.0",
"react-router-dom": "^7.13.0",
"tailwindcss": "^4.2.0",
+ "yup": "^1.7.1",
"zustand": "^5.0.11"
},
"devDependencies": {
diff --git a/src/App.jsx b/src/App.jsx
index 2bd0ff4..85efb3b 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -7,21 +7,36 @@ import CartPage from "./pages/CartPage";
import WishlistPage from "./pages/WishlistPage";
import ComparePage from "./pages/ComparePage";
import CheckoutPage from "./pages/CheckoutPage";
+import { Toaster } from "react-hot-toast";
+
+// I forgot and use Context insted of zustand 😂 but edit it again.
export default function App() {
return (
-
-
- }>
- } />
- } />
- } />
- } />
- } />
- } />
- } />
-
-
-
+ <>
+
+
+ }>
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+
+ {/* used Toaster inside these files
+ 1-useCartStore.js 📁 Add to & Remove from wishList , toast.success('Added to Wishlist!') , toast.success('Removed from Wishlist!')
+ 2-useWishlistStore.js 📁 Add to & Remove from Cart , toast.success('Added to Cart!') , toast.success('Removed from Cart!')
+ 3-useCompareStore.js 📁 Add to & Remove from Compare , toast.success('Added to Comare!') , toast.success('Removed from Comare!')
+ 4- 📁 Place order , toast.success('Successfull place order !')
+
+
+
+ */}
+
+ >
);
}
diff --git a/src/components/Atoms/PriceRangeSlider.jsx b/src/components/Atoms/PriceRangeSlider.jsx
new file mode 100644
index 0000000..d7ab8f4
--- /dev/null
+++ b/src/components/Atoms/PriceRangeSlider.jsx
@@ -0,0 +1,98 @@
+import { useRef, useEffect, memo } from "react";
+
+function PriceRangeSlider({ filterParams, updateURL }) {
+
+ // reade max and min prices from url to keep ui updated when happen any refresh
+ const urlMin = filterParams.get("minPrice") || "0";
+ const urlMax = filterParams.get("maxPrice") || "400";
+
+ const minRangeRef = useRef(null);
+ const maxRangeRef = useRef(null);
+ const rangeTrackRef = useRef(null);
+ const minValueRef = useRef(null);
+ const maxValueRef = useRef(null);
+
+ const minGap = 10;
+ const maxValue = 400;
+
+ const updateRange = (e) => {
+ let min = parseInt(minRangeRef.current.value);
+ let max = parseInt(maxRangeRef.current.value);
+
+ if (max - min < minGap) {
+ if (e?.target === minRangeRef.current) {
+ minRangeRef.current.value = max - minGap;
+ min = max - minGap;
+ } else {
+ maxRangeRef.current.value = min + minGap;
+ max = min + minGap;
+ }
+ }
+
+ // تحديث الأرقام والخط الملون
+ minValueRef.current.textContent = min;
+ maxValueRef.current.textContent = max;
+
+ let minPercent = (min / maxValue) * 100;
+ let maxPercent = (max / maxValue) * 100;
+
+ rangeTrackRef.current.style.left = `${minPercent}%`;
+ rangeTrackRef.current.style.right = `${100 - maxPercent}%`;
+ };
+
+ // 2. تأكد إن الـ UI يتحدث أول ما الصفحة تفتح بناءً على قيم الـ URL
+ useEffect(() => {
+ updateRange();
+ }, [urlMin, urlMax]); // لو اللينك اتغير من بره، الـ Slider يتحرك
+
+ return (
+ <>
+
+
+
+
+ Maximum Price: {urlMax}
+ $
+
+
+ Minimum Price: {urlMin}
+ $
+
+
+ >
+ );
+}
+
+export default memo(PriceRangeSlider);
\ No newline at end of file
diff --git a/src/components/FiltersDrawer.jsx b/src/components/FiltersDrawer.jsx
new file mode 100644
index 0000000..400b965
--- /dev/null
+++ b/src/components/FiltersDrawer.jsx
@@ -0,0 +1,121 @@
+import PriceRangeSlider from "./Atoms/PriceRangeSlider";
+import { useFilterActions } from "../hooks/useFilterActions";
+import { getCategories } from '../features/products/services/productService'
+import { useEffect, useState } from "react";
+export default function FiltersDrawer() {
+ const [categories , setCategories] = useState([]);
+ const { filterParams, updateURL } = useFilterActions();
+ const [searchTerm, setSearchTerm] = useState(
+ filterParams.get("search") || "",
+ );
+
+ {/* test persisting zustand store */}
+ // const {count , incCount}=useProductsStore()
+
+ // debounce pattern
+ useEffect(() => {
+ if (searchTerm !== (filterParams.get("search") || "")) {
+ const timer = setTimeout(() => {
+ updateURL({ search: searchTerm });
+ }, 400);
+
+ return () => {
+ clearTimeout(timer);
+ };
+ }
+ }, [searchTerm]);
+
+
+ useEffect(()=>{
+
+ const fetchCats= async ()=>{
+
+ const cats=await getCategories();
+ setCategories(cats)
+ }
+ fetchCats()
+ },[])
+
+ return (
+
+
+ {/* Search */}
+
+
+
setSearchTerm(e.target.value)}
+ className="w-full h-full pl-10 pr-4 py-2.5 border border-gray-200 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent"
+ />
+
+
+ {/* Category Filter */}
+
+
+ {/* Sort */}
+
+
+ {/* products per page */}
+
+
+
+ {/* memoized component */}
+
+
+
+
+ {/* test persisting zustand store */}
+ {/*
+
count : {count}
*/}
+
+ );
+}
diff --git a/src/components/Navbar.jsx b/src/components/Navbar.jsx
index c411958..30d7ed5 100644
--- a/src/components/Navbar.jsx
+++ b/src/components/Navbar.jsx
@@ -1,11 +1,19 @@
import { Link } from "react-router-dom";
import { useState } from "react";
import useCartStore from "../features/cart/hooks/useCartStore";
+import useWishlistStore from "../features/wishlist/hooks/useWishlistStore";
+import useCompareStore from "../features/compare/hooks/useCompareStore";
export default function Navbar() {
const [mobileOpen, setMobileOpen] = useState(false);
- const items = useCartStore((s) => s.items);
- const totalItems = items.reduce((sum, item) => sum + item.quantity, 0);
+ const cartItems = useCartStore((s) => s.items);
+ const wishItems = useWishlistStore((s)=>s.items)
+ const compareItems = useCompareStore((s)=>s.items)
+
+ const totalCartItems = cartItems.reduce((sum, item) => sum + item.quantity, 0);
+ const totalWishItems = wishItems.length;
+ const totalCompareItems = compareItems.length;
+
const links = [
{ to: "/", label: "Home" },
@@ -34,13 +42,41 @@ export default function Navbar() {
{link.label}
+ { link.label=="Wishlist" ?
+
+ <>
+
+ {totalWishItems > 0 && (
+
+ {totalWishItems}
+
+ )}
+
+ >
+ :link.label=="Compare"?
+
+ <>
+
+ {totalCompareItems > 0 && (
+
+ {totalCompareItems}
+
+ )}
+
+ >
+
+
+ :"" }
))}
+
+
+
{/* Cart + Mobile Toggle */}
- {totalItems > 0 && (
+ {totalCartItems > 0 && (
- {totalItems}
+ {totalCartItems}
)}
@@ -60,8 +96,7 @@ export default function Navbar() {
{/* Mobile menu button */}