diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 7954b09..93d9ac5 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -21,6 +21,7 @@ module.exports = { plugins: ["react-refresh"], rules: { "react/react-in-jsx-scope": "off", + "import/no-named-as-default": "off", "react/jsx-props-no-spreading": "off", "implicit-arrow-linebreak":"off", "react/require-default-props": "off", @@ -64,14 +65,14 @@ module.exports = { "react/no-array-index-key": "off", "no-promise-executor-return": "off", "jsx-a11y/control-has-associated-label": "off", - "jsx-a11y/no-static-element-interactions":"off", + "jsx-a11y/no-static-element-interactions": "off", "max-len": "off", "react-hooks/exhaustive-deps": "off", "@typescript-eslint/semi": "off", "@typescript-eslint/ban-ts-comment": "off", "no-unsafe-optional-chaining": "off", "react/no-unescaped-entities": "off", - '@typescript-eslint/no-explicit-any': 0, + "@typescript-eslint/no-explicit-any": 0, "react/prop-types": "off", "jsx-a11y/control-has-associated-label": "off", "react-refresh/only-export-components": [ diff --git a/src/components/cards/ProductCard.tsx b/src/components/cards/ProductCard.tsx index a99bf92..496b281 100644 --- a/src/components/cards/ProductCard.tsx +++ b/src/components/cards/ProductCard.tsx @@ -27,7 +27,7 @@ const ProductCard: React.FC = ({ product }) => { const [isLoading, setIsLoading] = useState(false); const dispatch = useAppDispatch(); - const soleilFN = (price: number) => { + const formatPrice = (price: number) => { if (price < 1000) { return price.toString(); } @@ -87,8 +87,9 @@ const ProductCard: React.FC = ({ product }) => { return (
{diff < 2 && ( -
-

New

+
+ {/*

New

*/} + {diff}
)} @@ -155,7 +156,7 @@ const ProductCard: React.FC = ({ product }) => {

$ - {soleilFN(product.price)} + {formatPrice(product.price)}

= ({ showFilters, toggleFilter, }) => { + const [searchParams, setSearchParams] = useSearchParams({ + minPrice: "0", + maxPrice: "", + category: "", + name: "", + }); const location = useLocation(); let currentLink: string = ""; const handleClick = () => { toggleFilter(); + setSearchParams({}); }; const crums = location.pathname .split("/") @@ -41,8 +51,7 @@ const BreadCrums: React.FC = ({ - {" "} - Filters + Clear Filters
)}
diff --git a/src/components/common/filter/ProductFilter.tsx b/src/components/common/filter/ProductFilter.tsx index 4417294..9f3380a 100644 --- a/src/components/common/filter/ProductFilter.tsx +++ b/src/components/common/filter/ProductFilter.tsx @@ -1,19 +1,31 @@ import React, { useState, useEffect } from "react"; -import { TextField } from "@mui/material"; +import { + Button, + CircularProgress, + IconButton, + MenuItem, + Stack, + TextField, +} from "@mui/material"; +import { useSearchParams } from "react-router-dom"; +import { BiSearch } from "react-icons/bi"; +import { ToastContainer, toast } from "react-toastify"; import { ICategory, IProduct } from "../../../types"; +import { useAppDispatch, useAppSelector } from "../../../redux/hooks"; +import { fetchCategories } from "../../../redux/reducers/categoriesSlice"; +import { fetchProducts } from "../../../redux/reducers/productsSlice"; +import api from "../../../redux/api/api"; interface IProductFilterProps { products: IProduct[]; onFilter: (filteredProducts: IProduct[]) => void; } -const priceRanges = [ - { label: "All", value: [0, Infinity] }, - { label: "30000 RWF - 60000 RWF", value: [30000, 60000] }, - { label: "60000 RWF - 90000 RWF", value: [60000, 90000] }, - { label: "90000 RWF - 120000 RWF", value: [90000, 120000] }, -]; +interface IRanges { + min: number | null; + max: number | null; +} const ProductFilter: React.FC = ({ products, @@ -21,32 +33,36 @@ const ProductFilter: React.FC = ({ }) => { const [query, setQuery] = useState(""); const [sort, setSort] = useState(""); - const [priceRange, setPriceRange] = useState<[number, number]>([0, Infinity]); + const [priceRange, setPriceRange] = useState<[number, number]>([ + 0, 100000000000000, + ]); const [selectedCategories, setSelectedCategories] = useState([]); const [categories, setCategories] = useState([]); - const [isLoading, setIsLoading] = useState(true); const [isError, setIsError] = useState(false); + const [cat, setCat] = useState("All"); + const [searchParams, setSearchParams] = useSearchParams({ + minPrice: "0", + maxPrice: "", + category: "", + }); + const [ranges, setRanges] = useState({ + min: null, + max: null, + }); - useEffect(() => { - if (products) { - const categorySet = new Set(); - products.forEach((product) => { - categorySet.add(product.category); - }); - const filteredCategories: ICategory[] = Array.from(categorySet); - - const uniqueCategories = filteredCategories.filter( - (category, index, self) => self.findIndex( - (otherCategory) => otherCategory.id === category.id, - ) === index, - ); + const dispatch = useAppDispatch(); - setCategories(uniqueCategories); - setIsLoading(false); - } + const { data, error } = useAppSelector((state) => state.categories); + const { loading } = useAppSelector((state) => state.products); + + useEffect(() => { + dispatch(fetchCategories()); }, []); useEffect(() => { + if (!Array.isArray(products)) { + return; + } const filterProducts = () => { const filtered = products.filter( (product) => product.name.toLowerCase().includes(query.toLowerCase()) @@ -66,38 +82,67 @@ const ProductFilter: React.FC = ({ }; filterProducts(); - }, [query, sort, priceRange, selectedCategories, products]); - - const handleQueryChange = (event: React.ChangeEvent) => { - setQuery(event.target.value); - }; + }, [query, sort, priceRange, selectedCategories, products, onFilter]); const handleSortChange = (event: React.ChangeEvent) => { setSort(event.target.value); }; - const handlePriceRangeChange = (range: [number, number]) => { - setPriceRange(range); - }; + useEffect(() => { + if (data) { + setCategories(data); + } + }, [data]); - const handleCategoryChange = ( - event: React.ChangeEvent, - ) => { - const { options } = event.target; - const selected: string[] = []; - for (let i = 0; i < options.length; i++) { - if (options[i].selected) { - selected.push(options[i].value); + const handleCategoryChange = (event: React.ChangeEvent) => { + const { value } = event.target; + setCat(value); + setSearchParams((prev) => { + const newParams = new URLSearchParams(prev); + if (value === "All") { + const unique = localStorage.getItem("uniqueCat"); + const cats = unique && JSON.parse(unique); + setCategories(cats); + newParams.delete("category"); + dispatch(fetchProducts); + } else { + newParams.set("category", value); } + return newParams; + }); + }; + + const handleRangesChange = (event: React.ChangeEvent) => { + const { name, value } = event.target; + if (/^\d*$/.test(value)) { + const numericValue = value === "" ? null : Number(value); + setRanges((prev) => ({ ...prev, [name]: numericValue })); } - setSelectedCategories(selected); + }; + + const handleSetParams = () => { + const newParams: Record = {}; + + if (ranges.min !== null) { + newParams.minPrice = ranges.min.toString(); + } + if (ranges.max !== null) { + newParams.maxPrice = ranges.max.toString(); + } + + if (ranges.max !== null && ranges.min !== null && ranges.min > ranges.max) { + setIsError(true); + return; + } + + setSearchParams(newParams); }; return (
-
+
SORT BY
@@ -146,59 +191,99 @@ const ProductFilter: React.FC = ({
-
-
+
+
PRICE FILTER
-
- {priceRanges.map((range) => ( - - ))} +
+ + ranges.max + ? "Inavlid Query" + : "Min Price" + } + // @ts-ignore + color={ + ranges.max !== null + && ranges.min !== null + && ranges.min > ranges.max + ? "error" + : "" + } + name="min" + size="small" + variant="outlined" + value={ranges.min !== null ? ranges.min : ""} + onChange={handleRangesChange} + fullWidth + /> + + +
-
-
- Filter By Category +
+
+ FILTER BY CATEGORY
- {isLoading &&

Loading categories...

} + {loading &&

Loading categories...

} {isError &&

Error fetching categories

} - - {/* - - {categories.map((category,i) => ( - - ))} - */} + {loading ? "Loading..." : "All"} + {!error ? ( + categories.map((category, i) => ( + + {category.name} + + )) + ) : ( +
+ Failed to fetch category wait until backedn PR is merged +
+ )} +
+
); }; diff --git a/src/components/common/header/Header.tsx b/src/components/common/header/Header.tsx index d5360ed..f7a2cb6 100644 --- a/src/components/common/header/Header.tsx +++ b/src/components/common/header/Header.tsx @@ -3,7 +3,7 @@ import { } from "@mui/material"; import { CiUser } from "react-icons/ci"; import { FaSearch, FaShoppingCart } from "react-icons/fa"; -import React, { useEffect, useState } from "react"; +import React, { useEffect, useState, SetStateAction } from "react"; import { Link } from "react-router-dom"; import { useSelector } from "react-redux"; @@ -16,6 +16,7 @@ import { useAppDispatch } from "../../../redux/hooks"; interface ISerachProps { searchQuery: string; setSearchQuery: React.Dispatch>; + setRefetch: React.Dispatch>; } const Header: React.FC = ({ searchQuery, setSearchQuery }) => { @@ -23,12 +24,14 @@ const Header: React.FC = ({ searchQuery, setSearchQuery }) => { const handleSearch = (e: React.ChangeEvent) => { setSearchQuery(e.target.value); }; + let userInfo; const accessToken = localStorage.getItem("accessToken"); if (accessToken) { userInfo = JSON.parse(atob(accessToken.split(".")[1])); } const dispatch = useAppDispatch(); + useEffect(() => { if (accessToken) { try { @@ -41,6 +44,19 @@ const Header: React.FC = ({ searchQuery, setSearchQuery }) => { } } }, []); + + useEffect(() => { + const searchParams = new URLSearchParams(window.location.search); + if (searchQuery) { + searchParams.set("name", searchQuery); + } else { + searchParams.delete("name"); + } + const newUrl = `${window.location.pathname}?${searchParams.toString()}`; + + window.history.pushState(null, "", newUrl); + }, [searchQuery]); + const dispatches = useAppDispatch(); useEffect(() => { dispatch(cartManage()); diff --git a/src/components/common/related-products/RelatedProducts.tsx b/src/components/common/related-products/RelatedProducts.tsx new file mode 100644 index 0000000..9522335 --- /dev/null +++ b/src/components/common/related-products/RelatedProducts.tsx @@ -0,0 +1,103 @@ +import React, { useEffect, useState } from "react"; +import { Grid } from "@mui/material"; + +import api from "../../../redux/api/api"; +import { IProduct } from "../../../types"; +import ProductCard from "../../cards/ProductCard"; +import ProductCardSkelton from "../../cards/ProductCardSkeleton"; + +interface IProductCtegory { + category: string; + currentP: string; +} + +const RelatedProducts: React.FC = ({ category, currentP }) => { + const [products, setProducts] = useState([]); + const [isLoading, setIsloading] = useState(true); + const [error, setError] = useState(""); + const [relatedProd, setRelatedProd] = useState([]); + + useEffect(() => { + const fetch = async () => { + try { + const res = await api.get("/products"); + + const products = res.data.products as IProduct[]; + setRelatedProd( + products + .filter( + (p) => p.category.name.toLowerCase() === category.toLowerCase(), + ) + .filter((p) => p.name.toLowerCase() !== currentP.toLowerCase()), + ); + + if (relatedProd?.length < 5) { + const otherCat = products.filter( + (p) => p.category.name.toLowerCase() !== category.toLowerCase(), + ); + setRelatedProd([...relatedProd, ...otherCat]); + } + + setIsloading(false); + } catch (error: any) { + setError(error.message); + setIsloading(false); + } finally { + setIsloading(false); + } + }; + fetch(); + }, []); + + if (isLoading) { + return ( + + {Array.from({ length: 5 }).map((_, index) => ( + + + + ))} + + ); + } + return ( +
+

Related Products

+ + + {relatedProd.slice(0, 5).map((product) => ( + + + + ))} + +
+ ); +}; + +export default RelatedProducts; diff --git a/src/components/layouts/RootLayout.tsx b/src/components/layouts/RootLayout.tsx index e668a92..57aa6b2 100644 --- a/src/components/layouts/RootLayout.tsx +++ b/src/components/layouts/RootLayout.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import React, { SetStateAction, useEffect, useState } from "react"; import { Outlet, useLocation } from "react-router-dom"; import { Divider } from "@mui/material"; @@ -11,6 +11,8 @@ import BreadCrums from "../common/breadcrum/BreadCrum"; export interface IOutletProps { searchQuery: string; showFilters: boolean; + refetch: boolean; + setRefetch: React.Dispatch>; } const RootLayout = () => { @@ -18,6 +20,11 @@ const RootLayout = () => { const [isHeaderInfoVisible, setIsHeaderInfoVisible] = useState(true); const [searchQuery, setSearchQuery] = useState(""); const [showFilters, setShowFilters] = useState(true); + const [refetch, setRefetch] = useState(false); + + const handleRefect = () => { + setRefetch(true); + }; const location = useLocation(); @@ -52,14 +59,27 @@ const RootLayout = () => {
{isHeaderInfoVisible && } -
+
{location.pathname !== "/" && } - +
diff --git a/src/pages/CartManagement.tsx b/src/pages/CartManagement.tsx index 29ba1f6..ea182d8 100644 --- a/src/pages/CartManagement.tsx +++ b/src/pages/CartManagement.tsx @@ -70,7 +70,8 @@ const CartManagement: React.FC = () => { const handleUpdate = async () => { try { const updatePromises = Object.entries(updatedQuantities).map( - ([productId, quantity]) => dispatch(updateCarts({ productId: Number(productId), quantity })), + ([productId, quantity]) => + dispatch(updateCarts({ productId: Number(productId), quantity })), ); await Promise.all(updatePromises); diff --git a/src/pages/ProductDetails.tsx b/src/pages/ProductDetails.tsx index 4183eff..8a6f993 100644 --- a/src/pages/ProductDetails.tsx +++ b/src/pages/ProductDetails.tsx @@ -19,6 +19,7 @@ import { useFetchSingleProduct } from "../libs/queries"; import ProductDetailSkleton from "../components/skeletons/ProductDetailSkleton"; import { IProduct, prod } from "../types"; import api from "../redux/api/api"; +import RelatedProducts from "../components/common/related-products/RelatedProducts"; const ProductDetails: React.FC = () => { const [mainImage, setMainImage] = useState(null); @@ -86,177 +87,183 @@ const ProductDetails: React.FC = () => { return (
{product && ( - - + <> + - {product?.images.map((img, index) => ( + + {product?.images.map((img, index) => ( + some alt here handleImage(img, index)} + /> + ))} + + + some alt here handleImage(img, index)} + width="100%" + height={400} + className="rounded-md cursor-pointer mx-auto flex-1" + alt="main prod" + src={mainImage || product?.images[0]} + style={{ + maxWidth: "500px", + maxHeight: "500px", + objectFit: "contain", + minWidth: "100%", + }} /> - ))} - - - - main prod + - - - - - {product?.name} -
-
- -

(56 Reviews)

-

- ( - {product?.stockQuantity} - {' '} - In stock) -

+ + + + {product?.name} +
+
+ +

(56 Reviews)

+

+ ( + {product?.stockQuantity} + {' '} + In stock) +

+
-
- -
- {isDiscounted ? ( - <> + +
+ {isDiscounted ? ( + <> + + $ + {soleilFN(product?.price)} + + + $ + {soleilFN(priceAfterDiscount)} + + + ) : ( $ {soleilFN(product?.price)} - - $ - {soleilFN(priceAfterDiscount)} - - - ) : ( - - $ - {soleilFN(product?.price)} - - )} -
- + )} +
+ +
+ {isDiscounted && ( + + - + {' '} + {product?.discount} + % + + )} - {isDiscounted && ( - - - - {' '} - {product?.discount} - % - - )} - - + - -
- - -

{items}

- + +

{items}

+ +
+ -
- - -
+ +
-
-
- -
- - Free Delivery - -

Enter your postal code for Delivery Availability

+
+
+ +
+ + Free Delivery + +

Enter your postal code for Delivery Availability

+
-
- -
- -
- - Return Delivery - -

Free 30 Days Delivery Returns. Details

+ +
+ +
+ + Return Delivery + +

Free 30 Days Delivery Returns. Details

+
-
+ - + + )}
); diff --git a/src/pages/ProductPage.tsx b/src/pages/ProductPage.tsx index ca61970..4144080 100644 --- a/src/pages/ProductPage.tsx +++ b/src/pages/ProductPage.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useCallback } from "react"; import { Grid, FormControl, @@ -10,75 +10,87 @@ import { import { gsap } from "gsap"; import Pagination from "@mui/material/Pagination"; import Stack from "@mui/material/Stack"; -import { useOutletContext } from "react-router-dom"; -// import { toast } from "react-toastify"; +import { useOutletContext, useSearchParams } from "react-router-dom"; import ProductPageSkeleton from "../components/skeletons/ProductPageSkeleton"; import ProductCard from "../components/cards/ProductCard"; import { IProduct } from "../types"; import ProductFilter from "../components/common/filter/ProductFilter"; -// import { useFetchProducts } from "../libs/queries"; import { IOutletProps } from "../components/layouts/RootLayout"; -import api from "../redux/api/api"; +import { useAppDispatch, useAppSelector } from "../redux/hooks"; +import { handleSearchProduct } from "../redux/reducers/productsSlice"; const ProductPage = () => { - const [products, setProducts] = useState([]); const [filteredProducts, setFilteredProducts] = useState([]); const [currentPage, setCurrentPage] = useState(1); const [itemsPerPage, setItemsPerPage] = useState(15); - const [isLoading, setIsloading] = useState(true); - const [error, setError] = useState(""); - const { searchQuery, showFilters } = useOutletContext(); + const { + searchQuery, showFilters, refetch, setRefetch, + } = useOutletContext(); + + const [searchParams, setSearchParams] = useSearchParams(); + + const dispatch = useAppDispatch(); + const { data, loading, error } = useAppSelector((state) => state.products); + + useEffect(() => { + dispatch( + handleSearchProduct({ + name: "", + minPrice: "0", + maxPrice: "100000000000000000000", + category: "", + }), + ); + }, [dispatch]); useEffect(() => { - const fetch = async () => { + const fetchFilteredProducts = async () => { try { - const res = await api.get("/products"); - setProducts(res.data.products); - setIsloading(false); - gsap.fromTo( - ".soleil2", - { - opacity: 0, - y: 20, - }, - { - y: 0, - opacity: 1, - ease: "power1.inOut", - stagger: 0.2, - yoyo: true, - delay: 1, - }, + const name = searchParams.get("name") || ""; + const minPrice = searchParams.get("minPrice") || "0"; + const maxPrice = searchParams.get("maxPrice") || "100000000000000000000"; + const category = searchParams.get("category") || ""; + + await dispatch( + handleSearchProduct({ + name: searchQuery, + minPrice, + maxPrice, + category, + }), ); } catch (error: any) { - setError(error.message); - setIsloading(false); - } finally { - setIsloading(false); + alert("error"); } }; - fetch(); - }, []); + fetchFilteredProducts(); + }, [searchParams, searchQuery, dispatch]); useEffect(() => { - const filtered = products.filter( - (product) => product.name.toLowerCase().includes(searchQuery.toLowerCase()) - || product.category.name.toLowerCase().includes(searchQuery.toLowerCase()), + let filtered = data; + + const name = searchParams.get("name") || ""; + const minPrice = parseFloat(searchParams.get("minPrice") || "0"); + const maxPrice = parseFloat( + searchParams.get("maxPrice") || "100000000000000000000", ); - setFilteredProducts(filtered); - setCurrentPage(1); - }, [searchQuery]); + const category = searchParams.get("category") || ""; - if (isLoading) { - return ; - } + if (name) { + filtered = filtered.filter((product) => product.name.toLowerCase().includes(name.toLowerCase())); + } - if (error) { - return ( -
{error}
+ // if (category) { + // filtered = filtered.filter((product) => product.category === category); + // } + + filtered = filtered.filter( + (product) => product.price >= minPrice && product.price <= maxPrice, ); - } + + setFilteredProducts(filtered); + }, [data, searchParams]); const handleFilter = (filtered: IProduct[]) => { setFilteredProducts(filtered); @@ -106,11 +118,27 @@ const ProductPage = () => { setCurrentPage(1); }; + if (loading) { + return ( + <> + + + ; + + ); + } + if (error) { + return ( +
+ +

{error}

+
+ ); + } + return (
- {showFilters && ( - - )} + { ))} - {searchQuery && currentItems.length < 1 && ( + {searchQuery && currentItems.length < 1 && data.length < 1 && (

No product found based on your query

diff --git a/src/redux/hooks.ts b/src/redux/hooks.ts index f9f711c..87bb02d 100644 --- a/src/redux/hooks.ts +++ b/src/redux/hooks.ts @@ -1,5 +1,6 @@ -import { useDispatch } from "react-redux"; +import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux"; -import type { AppDispatch } from "./store"; +import type { AppDispatch, RootState } from "./store"; export const useAppDispatch = () => useDispatch(); +export const useAppSelector: TypedUseSelectorHook = useSelector; diff --git a/src/redux/reducers/categoriesSlice.ts b/src/redux/reducers/categoriesSlice.ts index d9828f8..2013888 100644 --- a/src/redux/reducers/categoriesSlice.ts +++ b/src/redux/reducers/categoriesSlice.ts @@ -1,7 +1,9 @@ -import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"; +import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit"; import { AxiosError } from "axios"; +import { toast } from "react-toastify"; import api from "../api/api"; +import { ICategory } from "../../types"; export const fetchCategories = createAsyncThunk( "categories", @@ -13,8 +15,9 @@ export const fetchCategories = createAsyncThunk( Authorization: `Bearer ${localStorage.getItem("accessToken")}`, }, }); + console.log("soleil kubeeting", response.data.categories); return response.data.categories; - } catch (err) { + } catch (err: any) { const error = err as AxiosError; return rejectWithValue(error.response?.data); } @@ -42,23 +45,34 @@ export const addCategory = createAsyncThunk( }, ); +interface ICategoryRoot { + loading: boolean; + data: ICategory[]; + error: string | null; +} + +const initialState: ICategoryRoot = { + loading: false, + data: [], + error: null, +}; + const categoriesSlice = createSlice({ name: "categories", - initialState: { - loading: false, - data: [], - error: null, - }, + initialState, reducers: {}, extraReducers: (builder) => { builder .addCase(fetchCategories.pending, (state) => { state.loading = true; }) - .addCase(fetchCategories.fulfilled, (state, action) => { - state.loading = false; - state.data = action.payload; - }) + .addCase( + fetchCategories.fulfilled, + (state, action: PayloadAction) => { + state.loading = false; + state.data = action.payload; + }, + ) .addCase(fetchCategories.rejected, (state, action) => { state.loading = false; // @ts-ignore diff --git a/src/redux/reducers/productsSlice.ts b/src/redux/reducers/productsSlice.ts index 5c128b1..bc30a27 100644 --- a/src/redux/reducers/productsSlice.ts +++ b/src/redux/reducers/productsSlice.ts @@ -2,8 +2,22 @@ import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"; import { AxiosError } from "axios"; import api from "../api/api"; +import { IProduct } from "../../types"; -export const fetchProducts = createAsyncThunk( +interface SearchParams { + name?: string; + minPrice?: string; + maxPrice?: string; + category?: string; +} + +interface ProductsState { + data: IProduct[]; + loading: boolean; + error: string | null; +} + +export const fetchProducts = createAsyncThunk( "products", async (_, { rejectWithValue }) => { try { @@ -35,14 +49,45 @@ export const deleteProduct = createAsyncThunk( } }, ); +export const handleSearchProduct = createAsyncThunk< +IProduct[] | { status: number; message: string }, +SearchParams +>( + "search/product", + async ({ + name, minPrice, maxPrice, category, + }, { rejectWithValue }) => { + try { + let queryString = "/products/search?"; + if (name) queryString += `name=${name}&`; + if (minPrice !== undefined) queryString += `minPrice=${minPrice}&`; + if (maxPrice !== undefined) queryString += `maxPrice=${maxPrice}&`; + if (category !== undefined) queryString += `category=${category}&`; + + const res = await api.get(queryString); + + if (!Array.isArray(res.data)) { + return rejectWithValue( + "No products found matching your search criteria.", + ); + } + + return res.data; + } catch (error: any) { + return rejectWithValue(error.message); + } + }, +); + +const initialState: ProductsState = { + loading: false, + data: [], + error: null, +}; const productsSlice = createSlice({ name: "products", - initialState: { - loading: false, - data: [], - error: null, - }, + initialState, reducers: { setProducts: (state, action) => { state.data = action.payload; @@ -61,6 +106,25 @@ const productsSlice = createSlice({ state.loading = false; // @ts-ignore state.error = action.payload || action.error.message; + }) + .addCase(handleSearchProduct.pending, (state) => { + state.loading = true; + state.error = null; + }) + .addCase(handleSearchProduct.fulfilled, (state, action) => { + if (Array.isArray(action.payload)) { + state.loading = false; + state.data = action.payload; + } else { + state.loading = false; + state.data = []; + } + }) + .addCase(handleSearchProduct.rejected, (state, action) => { + state.loading = false; + state.data = []; + // @ts-ignore + state.error = action.payload || "Failed to search products"; }); }, });