From 04d96013871115858b34d0aadd83b3e1d1b19590 Mon Sep 17 00:00:00 2001 From: favor-star Date: Mon, 24 Jun 2024 10:54:04 +0200 Subject: [PATCH] Intergrated redux store for fetched products across the web --- src/App.tsx | 39 +- src/components/Products/ProductCard.tsx | 2 +- src/components/navbar/Navbar.tsx | 447 ++++++++++-------- src/components/search/SearchHeader.tsx | 49 ++ src/components/search/SearchResults.tsx | 54 +++ src/containers/Arrivals/NewArrivals.tsx | 17 +- .../FeaturedProducts/FeaturedProducts.tsx | 25 +- src/containers/searchResults/SearchPage.tsx | 22 + src/pages/LandingPage.tsx | 1 + src/redux/slices/productsSlice.ts | 36 ++ src/redux/store.ts | 2 + 11 files changed, 456 insertions(+), 238 deletions(-) create mode 100644 src/components/search/SearchHeader.tsx create mode 100644 src/components/search/SearchResults.tsx create mode 100644 src/containers/searchResults/SearchPage.tsx create mode 100644 src/redux/slices/productsSlice.ts diff --git a/src/App.tsx b/src/App.tsx index 35a92cf..e0db5d3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,8 +8,41 @@ import GoogleAuthSuccess from './components/authentication/GoogleAuthSucces'; import { ToastContainer } from 'react-toastify'; import AdminPage from './pages/Admin'; import Category from './pages/Admin/Category'; - +import Searchpage from './containers/searchResults/SearchPage'; +import { useDispatch } from 'react-redux'; +import { ProductResponse, Product } from './types/Types'; +import { useEffect, useRef } from 'react'; +import { useGetProductsQuery } from './services/productApi'; +import { setError, setIsLoading, setProductFetched, setProductsDataList } from './redux/slices/productsSlice'; const App = () => { + const { data, error, isLoading } = useGetProductsQuery(); + const dispatch = useDispatch(); + const firstRender = useRef(true); + + const productsData: ProductResponse = data as unknown as ProductResponse; + + useEffect(() => { + const fetchProducts = async () => { + if (firstRender.current) { + firstRender.current = false; + return; + } + if (error) { + dispatch(setError(error)); + dispatch(setIsLoading(false)); + dispatch(setProductFetched(false)); + return; + } + if (!isLoading && productsData) { + const productsList = productsData.data as Product[]; + dispatch(setProductsDataList([...productsList])); + dispatch(setIsLoading(false)); + dispatch(setProductFetched(true)); + } + }; + fetchProducts(); + }, [productsData, isLoading, dispatch]); + const router = createBrowserRouter([ { path: '/', @@ -53,6 +86,10 @@ const App = () => { }, ], }, + { + path: 'search', + element: , + }, ]); return ( <> diff --git a/src/components/Products/ProductCard.tsx b/src/components/Products/ProductCard.tsx index ecc4435..2c9aa7c 100644 --- a/src/components/Products/ProductCard.tsx +++ b/src/components/Products/ProductCard.tsx @@ -14,7 +14,7 @@ const ProductCard: React.FC = ({ product }) => { {product.name}
diff --git a/src/components/navbar/Navbar.tsx b/src/components/navbar/Navbar.tsx index 075b0c9..14e4a6d 100644 --- a/src/components/navbar/Navbar.tsx +++ b/src/components/navbar/Navbar.tsx @@ -4,214 +4,257 @@ import { DesktopNav, PopularCategory } from '../../containers/nav/NavbarComponen import { useEffect } from 'react'; import WishNav from './wishNav/WishNav'; import CartNav from './cartNav/CartNav'; + const Navbar: React.FC = () => { - const [cartOpen, SetCartOpen] = useState(false) - const [wish, setWish] = useState(false) - const navigate = useNavigate(); - useEffect(() => { - const humbergurBtn = document.getElementById('humbergurBtn'); - const closeBtn = document.getElementById('close'); - const overlay = document.getElementById('overlay'); - const container = document.getElementById('humbergerContainer'); - const hideScrollbar = () => { - document.body.style.overflow = 'hidden'; - }; - const showScrollbar = () => { - document.body.style.overflow = 'auto'; - }; - closeBtn?.addEventListener('click', () => { - overlay?.classList.add('-translate-x-full'); - container?.classList.add('-translate-x-full'); - showScrollbar(); - }); - humbergurBtn?.addEventListener('click', () => { - overlay?.classList.remove('-translate-x-full'); - container?.classList.remove('-translate-x-full'); - hideScrollbar(); - }); - overlay?.addEventListener('click', e => { - if (e.target !== e.currentTarget) { - return; - } - overlay?.classList.add('-translate-x-full'); - container?.classList.add('-translate-x-full'); - showScrollbar(); - }); - }, []); + const [cartOpen, SetCartOpen] = useState(false); + const [wish, setWish] = useState(false); + const [searchQuery, setSearchQuery] = useState(''); + const navigate = useNavigate(); + useEffect(() => { + const humbergurBtn = document.getElementById('humbergurBtn'); + const closeBtn = document.getElementById('close'); + const overlay = document.getElementById('overlay'); + const container = document.getElementById('humbergerContainer'); + const hideScrollbar = () => { + document.body.style.overflow = 'hidden'; + }; + const showScrollbar = () => { + document.body.style.overflow = 'auto'; + }; + closeBtn?.addEventListener('click', () => { + overlay?.classList.add('-translate-x-full'); + container?.classList.add('-translate-x-full'); + showScrollbar(); + }); + humbergurBtn?.addEventListener('click', () => { + overlay?.classList.remove('-translate-x-full'); + container?.classList.remove('-translate-x-full'); + hideScrollbar(); + }); + overlay?.addEventListener('click', e => { + if (e.target !== e.currentTarget) { + return; + } + overlay?.classList.add('-translate-x-full'); + container?.classList.add('-translate-x-full'); + showScrollbar(); + }); + }, []); - return ( - <> - { - (wish || cartOpen) &&
{ - if (e.target !== e.currentTarget) { - return - } - setWish(false) - SetCartOpen(false) - }} - className='absolute bg-[#00000000] top-0 w-full border h-full' style={{ zIndex: 10 }}>
+ const handleSearch = (e: any) => { + e.preventDefault(); + navigate(`/search?searchQuery=${searchQuery}`); + }; + return ( + <> + {(wish || cartOpen) && ( +
{ + if (e.target !== e.currentTarget) { + return; } -
-
-
-
- - - -
-
navigate('/')} - className='leading-none font-bold text-2xl hover:cursor-pointer md:text-3xl' - > -

MAVERICKSđź›’

-
-
-
- {/* Profile */} -
setWish(state => !state)} - className='rounded-full transition-all ease-in-out delay-100 hover:bg-grayColor active:bg-greenColor p-1 active:text-blackColor hover:text-blackColor'> - - - - {wish &&
{ - if (e.target !== e.currentTarget) { - setWish(false) - return - } - }} - className='absolute top-0 md:h-[115px] w-screen right-0 h-[100px] bg-[#0000000] z-40'> - - -
} -
- {/* Favorite */} - - - - - - - -
SetCartOpen(state => !state)} - className='rounded-full transition-all ease-in-out delay-100 hover:bg-grayColor active:bg-greenColor active:text-blackColor hover:text-blackColor p-1 select-none'> -
- - - - - 3 - -
- {/* Cart */} - {cartOpen &&
{ - if (e.target !== e.currentTarget) { - SetCartOpen(false) - return - } - }} - className='absolute top-0 md:h-[115px] w-screen right-0 h-[100px] bg-[#00000000] z-40'> - - -
} -
-
- {/* Searching box */} -
-
- - -
-
+ setWish(false); + SetCartOpen(false); + }} + className='absolute bg-[#00000000] top-0 w-full border h-full' + style={{ zIndex: 10 }} + >
+ )} +
+
+
+
+ + + +
+
navigate('/')} + className='leading-none font-bold text-2xl hover:cursor-pointer md:text-3xl' + > +

MAVERICKSđź›’

+
+
+
+ {/* Profile */} +
setWish(state => !state)} + className='rounded-full transition-all ease-in-out delay-100 hover:bg-grayColor active:bg-greenColor p-1 active:text-blackColor hover:text-blackColor' + > + + + + {wish && ( +
{ + if (e.target !== e.currentTarget) { + setWish(false); + return; + } + }} + className='absolute top-0 md:h-[115px] w-screen right-0 h-[100px] bg-[#0000000] z-40' + > +
- {/* Desktop Navs */} - - {/* Mobile sidebar Navs */} + )} +
+ {/* Favorite */} + + + + + + +
SetCartOpen(state => !state)} + className='rounded-full transition-all ease-in-out delay-100 hover:bg-grayColor active:bg-greenColor active:text-blackColor hover:text-blackColor p-1 select-none' + > +
+ + + + + 3 + +
+ {/* Cart */} + {cartOpen && (
{ + if (e.target !== e.currentTarget) { + SetCartOpen(false); + return; + } + }} + className='absolute top-0 md:h-[115px] w-screen right-0 h-[100px] bg-[#00000000] z-40' > -
- {/* close */} -
-
- - - -
-
-
- - - -
-
+
+ )} +
+
+ {/* Searching box */} +
+
+ setSearchQuery(e.target.value)} + /> + +
+
+
+ {/* Desktop Navs */} + + {/* Mobile sidebar Navs */} +
+
+ {/* close */} +
+
+ + + +
+
+
+ + +
- - ) -} +
+
+
+ + ); +}; -export default Navbar \ No newline at end of file +export default Navbar; diff --git a/src/components/search/SearchHeader.tsx b/src/components/search/SearchHeader.tsx new file mode 100644 index 0000000..60b9394 --- /dev/null +++ b/src/components/search/SearchHeader.tsx @@ -0,0 +1,49 @@ +import { useState } from 'react'; +import { LuChevronDown } from 'react-icons/lu'; +import { cn } from '../../utils'; + +interface SearchParamProps { + searchQuery: string; +} +const SearchHeader = ({ searchQuery }: SearchParamProps) => { + const [sortInfoHidden, setSortInfoHidden] = useState(true); + + return ( + <> +
+

+ + Search Results for + "{searchQuery}" + + +

+
+ + ); +}; + +export default SearchHeader; diff --git a/src/components/search/SearchResults.tsx b/src/components/search/SearchResults.tsx new file mode 100644 index 0000000..22f6645 --- /dev/null +++ b/src/components/search/SearchResults.tsx @@ -0,0 +1,54 @@ +// import { useSelector } from 'react-redux'; +import { useSelector } from 'react-redux'; +import ProductCard from '../Products/ProductCard'; +import Button from '../common/Button'; +import { Product } from '../../types/Types'; +import { useNavigate } from 'react-router-dom'; +import { useEffect } from 'react'; + +interface SearchQueryProps { + searchQuery: string; +} +const SearchResults = ({ searchQuery }: SearchQueryProps) => { + const { isLoading, productsDataList: productsList } = useSelector((state: any) => state.products); + const filteredProducts = [ + ...productsList.filter((product: Product) => product.name.toLowerCase().includes(searchQuery.toLowerCase())), + ...productsList.filter((product: Product) => product.description.toLowerCase().includes(searchQuery.toLowerCase())), + ]; + useEffect(() => { + if (searchQuery.length !== 0 && filteredProducts.length !== 0) { + const productNames = filteredProducts.map(product => product.name); + let frequentlySearched = JSON.parse(localStorage.getItem('frequentlySearched') as string) || []; + frequentlySearched = [...new Set(frequentlySearched)]; + frequentlySearched = [...new Set([...frequentlySearched, ...productNames])]; + localStorage.setItem('frequentlySearched', JSON.stringify(frequentlySearched)); + } + }, [searchQuery]); + + + const navigate = useNavigate(); + const handleNavigate = () => { + navigate('/'); + }; + return ( +
+ +
+ {isLoading ? ( + 'Loading....' + ) : filteredProducts.length === 0 ? ( +
+

+ No matching products were found for {searchQuery} +

+
+ ) : ( + filteredProducts.map((product: Product) => ) + )} +
+
+ ); +}; + +export default SearchResults; diff --git a/src/containers/Arrivals/NewArrivals.tsx b/src/containers/Arrivals/NewArrivals.tsx index c2faba6..13d0970 100644 --- a/src/containers/Arrivals/NewArrivals.tsx +++ b/src/containers/Arrivals/NewArrivals.tsx @@ -1,24 +1,15 @@ import ProductCard from '../../components/Products/ProductCard'; import ProductCardSkeleton from '../../components/Products/ProductCardSkeleton'; import { useState } from 'react'; -import { useGetProductsQuery } from '../../services/productApi'; +import { Product } from '../../types/Types'; import { BiSolidCircle } from 'react-icons/bi'; -import { ProductResponse, Product } from '../../types/Types'; +import { useSelector } from 'react-redux'; const perPage = 6; export default function NewArrivals() { - const { data, error, isLoading } = useGetProductsQuery(); const [currentPage, setCurrentPage] = useState(0); - - const productsData: ProductResponse = data as unknown as ProductResponse; - - if (error) { - return
Error loading products
; - } - - const productsList: Product[] = productsData ? productsData.data : []; - + const { isLoading, productsDataList: productsList } = useSelector((state: any) => state.products); const next = () => { setCurrentPage(prevPage => Math.min(prevPage + 1, Math.floor(productsList.length / perPage))); }; @@ -39,7 +30,7 @@ export default function NewArrivals() {
{isLoading ? Array.from({ length: perPage }).map((_, index) => ) - : allProductsOnPage.map(product => )} + : allProductsOnPage.map((product: Product) => )}
{}, [isLoading, error, data]); - - if (isLoading) { - // Display skeleton loaders + const { isLoading, productsDataList: productsList } = useSelector((state: any) => state.products); if (isLoading) { return (
@@ -25,24 +19,13 @@ export default function FeaturedProduct() { ); } - if (error) { - return
Error loading products
; - } - - const productsData: ProductResponse = data as unknown as ProductResponse; - - if (!productsData) { - return null; - } - - const productsList: Product[] = productsData.data; return (

Featured Products

- {productsList.map(product => ( + {productsList.map((product: Product) => ( ))}
diff --git a/src/containers/searchResults/SearchPage.tsx b/src/containers/searchResults/SearchPage.tsx new file mode 100644 index 0000000..a9104e4 --- /dev/null +++ b/src/containers/searchResults/SearchPage.tsx @@ -0,0 +1,22 @@ +import Footer from '../../components/footer/Footer'; +import Navbar from '../../components/navbar/Navbar'; +import SearchHeader from '../../components/search/SearchHeader'; +import SearchResults from '../../components/search/SearchResults'; +import { useSearchParams } from 'react-router-dom'; + +const Searchpage = () => { + const [searchParams] = useSearchParams(); + const searchParam = searchParams.get('searchQuery') as string; + return ( +
+ + + + + +
+
+ ); +}; + +export default Searchpage; diff --git a/src/pages/LandingPage.tsx b/src/pages/LandingPage.tsx index dac9e56..5e88b52 100644 --- a/src/pages/LandingPage.tsx +++ b/src/pages/LandingPage.tsx @@ -3,6 +3,7 @@ import Footer from '../components/footer/Footer'; import Navbar from '../components/navbar/Navbar'; const LandingPage = () => { + return (
diff --git a/src/redux/slices/productsSlice.ts b/src/redux/slices/productsSlice.ts new file mode 100644 index 0000000..9035452 --- /dev/null +++ b/src/redux/slices/productsSlice.ts @@ -0,0 +1,36 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { Product } from '../../types/Types'; + +interface ProductsStateProps { + error: any; + isLoading: boolean; + productsFetched: boolean; + productsDataList: Product[]; +} +const initialState: ProductsStateProps = { + isLoading: true, + error: null, + productsFetched: false, + productsDataList: [], +}; +const productSlice = createSlice({ + name: 'producs', + initialState, + reducers: { + setProductFetched: (state, action: PayloadAction) => { + state.productsFetched = action.payload; + }, + setProductsDataList: (state, action: PayloadAction) => { + state.productsDataList = action.payload; + }, + setIsLoading: (state, action: PayloadAction) => { + state.isLoading = action.payload; + }, + setError: (state, action: PayloadAction) => { + state.error = action.payload; + }, + }, +}); +export const { setProductFetched, setProductsDataList, setIsLoading,setError } = productSlice.actions; + +export default productSlice.reducer; diff --git a/src/redux/store.ts b/src/redux/store.ts index aa7d69d..82441f8 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -5,9 +5,11 @@ import { mavericksApi } from '../services'; import { setupListeners } from '@reduxjs/toolkit/query'; import registerReducer from './slices/registerSlice'; import userReducer from './slices/userSlice'; +import productReducer from './slices/productsSlice'; export const store = configureStore({ reducer: { + products: productReducer, user: userReducer, register: registerReducer, [mavericksApi.reducerPath]: mavericksApi.reducer,