From 1645cc5f06104c19725275a2c35c93cce553b1e6 Mon Sep 17 00:00:00 2001 From: Andrew Motko Date: Fri, 1 Mar 2024 15:49:51 +0200 Subject: [PATCH] add: search, lazyLoad, backBtn --- package-lock.json | 15 ++++++++++ package.json | 1 + src/components/App/App.jsx | 22 +++++++++------ src/components/BackLink/BackLink.jsx | 29 ++++++++++++++++++++ src/components/ProductList/ProductList.jsx | 10 +++++-- src/components/SearchBox/SearchBox.jsx | 16 +++++++++++ src/components/SearchBox/SearchBox.styled.js | 23 ++++++++++++++++ src/components/SharedLayout/SharedLayout.jsx | 5 +++- src/fakeAPI.js | 20 +++++++------- src/index.js | 2 +- src/pages/About.jsx | 6 ++-- src/pages/ProductDetails.jsx | 7 ++++- src/pages/Products.jsx | 19 +++++++++++-- 13 files changed, 146 insertions(+), 29 deletions(-) create mode 100644 src/components/BackLink/BackLink.jsx create mode 100644 src/components/SearchBox/SearchBox.jsx create mode 100644 src/components/SearchBox/SearchBox.styled.js diff --git a/package-lock.json b/package-lock.json index eb29611..fdf1f13 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "axios": "^0.27.2", "react": "^18.1.0", "react-dom": "^18.1.0", + "react-icons": "^5.0.1", "react-router-dom": "^6.22.1", "react-scripts": "5.0.1", "react-tabs": "^5.1.0", @@ -11885,6 +11886,14 @@ "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==" }, + "node_modules/react-icons": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.0.1.tgz", + "integrity": "sha512-WqLZJ4bLzlhmsvme6iFdgO8gfZP17rfjYEJ2m9RsZjZ+cc4k1hTzknEz63YS1MeT50kVzoa1Nz36f4BEx+Wigw==", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -23178,6 +23187,12 @@ "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==" }, + "react-icons": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.0.1.tgz", + "integrity": "sha512-WqLZJ4bLzlhmsvme6iFdgO8gfZP17rfjYEJ2m9RsZjZ+cc4k1hTzknEz63YS1MeT50kVzoa1Nz36f4BEx+Wigw==", + "requires": {} + }, "react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", diff --git a/package.json b/package.json index 1bd5aaf..e6fc41d 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "axios": "^0.27.2", "react": "^18.1.0", "react-dom": "^18.1.0", + "react-icons": "^5.0.1", "react-router-dom": "^6.22.1", "react-scripts": "5.0.1", "react-tabs": "^5.1.0", diff --git a/src/components/App/App.jsx b/src/components/App/App.jsx index 9643cfe..f7d8b12 100644 --- a/src/components/App/App.jsx +++ b/src/components/App/App.jsx @@ -1,15 +1,17 @@ import { Routes, Route } from 'react-router-dom'; -import About from 'pages/About'; -import Products from 'pages/Products'; -import ProductDetails from 'pages/ProductDetails'; -import NotFound from 'pages/NotFound'; -import Mission from 'pages/Mission'; -import Team from 'pages/Team'; -import Reviews from 'pages/Reviews'; -import Home from 'pages/Home'; +import { lazy } from 'react'; import Sharedlayout from 'components/SharedLayout/SharedLayout'; +import NotFound from 'pages/NotFound'; -export const App = () => { +const About = lazy(() => import('pages/About')); +const Home = lazy(() => import('pages/Home')); +const ProductDetails = lazy(() => import('pages/ProductDetails')); +const Products = lazy(() => import('pages/Products')); +const Mission = lazy(() => import('pages/Mission')); +const Team = lazy(() => import('pages/Team')); +const Reviews = lazy(() => import('pages/Reviews')); + +const App = () => { return ( }> @@ -26,3 +28,5 @@ export const App = () => { ); }; + +export default App; diff --git a/src/components/BackLink/BackLink.jsx b/src/components/BackLink/BackLink.jsx new file mode 100644 index 0000000..f443919 --- /dev/null +++ b/src/components/BackLink/BackLink.jsx @@ -0,0 +1,29 @@ +import { HiArrowLeft } from 'react-icons/hi'; +import { Link } from 'react-router-dom'; +import styled from 'styled-components'; + +const StyledLink = styled(Link)` + display: inline-flex; + align-items: center; + gap: 4px; + padding: 8px 0; + color: black; + text-decoration: none; + font-weight: 500; + text-transform: uppercase; + + &:hover { + color: orangered; + } +`; + +const BackLink = ({ to, children }) => { + return ( + + + {children} + + ); +}; + +export default BackLink; diff --git a/src/components/ProductList/ProductList.jsx b/src/components/ProductList/ProductList.jsx index f42600a..5db70cf 100644 --- a/src/components/ProductList/ProductList.jsx +++ b/src/components/ProductList/ProductList.jsx @@ -1,12 +1,14 @@ -import { Link } from 'react-router-dom'; +import { Link, useLocation } from 'react-router-dom'; import { Container, CardWrapper, ProductName } from './ProductList.styled'; -export const ProductList = ({ products }) => { +const ProductList = ({ products }) => { + const location = useLocation(); + return ( {products.map(product => ( - + {product.name} @@ -15,3 +17,5 @@ export const ProductList = ({ products }) => { ); }; + +export default ProductList; diff --git a/src/components/SearchBox/SearchBox.jsx b/src/components/SearchBox/SearchBox.jsx new file mode 100644 index 0000000..fc03e5d --- /dev/null +++ b/src/components/SearchBox/SearchBox.jsx @@ -0,0 +1,16 @@ +import { Icon, Input, Wrapper } from './SearchBox.styled'; + +const SearchBox = ({ value, onChange }) => { + return ( + + + onChange(e.target.value)} + /> + + ); +}; + +export default SearchBox; diff --git a/src/components/SearchBox/SearchBox.styled.js b/src/components/SearchBox/SearchBox.styled.js new file mode 100644 index 0000000..885bca1 --- /dev/null +++ b/src/components/SearchBox/SearchBox.styled.js @@ -0,0 +1,23 @@ +import styled from 'styled-components'; +import { HiSearch } from 'react-icons/hi'; + +export const Wrapper = styled.div` + display: inline-flex; + align-items: center; + position: relative; + margin-bottom: 16px; + text-transform: uppercase; +`; + +export const Input = styled.input` + padding: 8px 32px 8px 8px; + border-radius: 4px; + font: inherit; +`; + +export const Icon = styled(HiSearch)` + width: 20px; + height: 20px; + position: absolute; + right: 6px; +`; diff --git a/src/components/SharedLayout/SharedLayout.jsx b/src/components/SharedLayout/SharedLayout.jsx index 14fd792..fdeb661 100644 --- a/src/components/SharedLayout/SharedLayout.jsx +++ b/src/components/SharedLayout/SharedLayout.jsx @@ -1,5 +1,6 @@ import { Container, Header, Logo, Link } from 'components/App/App.styled'; import { Outlet } from 'react-router-dom'; +import { Suspense } from 'react'; const Sharedlayout = () => { return ( @@ -17,7 +18,9 @@ const Sharedlayout = () => { Products - + Loading page ...}> + + ); }; diff --git a/src/fakeAPI.js b/src/fakeAPI.js index e350ede..c03601f 100644 --- a/src/fakeAPI.js +++ b/src/fakeAPI.js @@ -1,14 +1,14 @@ const products = [ - { id: 'h-1', name: 'Hoodie 1' }, - { id: 'h-2', name: 'Hoodie 2' }, - { id: 'h-3', name: 'Hoodie 3' }, - { id: 's-1', name: 'Sneakers 1' }, - { id: 's-2', name: 'Sneakers 2' }, - { id: 's-3', name: 'Sneakers 3' }, - { id: 's-4', name: 'Sneakers 4' }, - { id: 'p-1', name: 'Pants 1' }, - { id: 'p-2', name: 'Pants 2' }, - { id: 'p-3', name: 'Pants 3' }, + { id: 'h-1', name: 'Hoodie 1', color: 'red', maxPrice: 100 }, + { id: 'h-2', name: 'Hoodie 2', color: 'green', maxPrice: 300 }, + { id: 'h-3', name: 'Hoodie 3', color: 'blue', maxPrice: 500 }, + { id: 's-1', name: 'Sneakers 1', color: 'red', maxPrice: 100 }, + { id: 's-2', name: 'Sneakers 2', color: 'green', maxPrice: 300 }, + { id: 's-3', name: 'Sneakers 3', color: 'blue', maxPrice: 500 }, + { id: 's-4', name: 'Sneakers 4', color: 'red', maxPrice: 100 }, + { id: 'p-1', name: 'Pants 1', color: 'red', maxPrice: 100 }, + { id: 'p-2', name: 'Pants 2', color: 'green', maxPrice: 300 }, + { id: 'p-3', name: 'Pants 3', color: 'blue', maxPrice: 500 }, ]; export const getProducts = () => { diff --git a/src/index.js b/src/index.js index 97c9153..cd23161 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,6 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; -import { App } from 'components/App/App'; +import App from 'components/App/App'; import './index.css'; import { BrowserRouter } from 'react-router-dom'; diff --git a/src/pages/About.jsx b/src/pages/About.jsx index 6002ec4..d615c06 100644 --- a/src/pages/About.jsx +++ b/src/pages/About.jsx @@ -1,5 +1,5 @@ import { Link, Outlet } from 'react-router-dom'; - +import { Suspense } from 'react'; const About = () => { return (
@@ -28,7 +28,9 @@ const About = () => { Go through the reviews - + Loading subpage...}> + +
); }; diff --git a/src/pages/ProductDetails.jsx b/src/pages/ProductDetails.jsx index aff10c8..d540edf 100644 --- a/src/pages/ProductDetails.jsx +++ b/src/pages/ProductDetails.jsx @@ -1,11 +1,16 @@ -import { useParams } from 'react-router-dom'; +import { useParams, useLocation } from 'react-router-dom'; +import BackLink from 'components/BackLink/BackLink'; import { getProductById } from '../fakeAPI'; const ProductDetails = () => { const { id } = useParams(); const product = getProductById(id); + const location = useLocation(); + const backLinkHref = location.state?.from ?? '/products'; + return (
+ Back to products

diff --git a/src/pages/Products.jsx b/src/pages/Products.jsx index 752290c..70bd4d6 100644 --- a/src/pages/Products.jsx +++ b/src/pages/Products.jsx @@ -1,11 +1,26 @@ -import { ProductList } from '../components/ProductList/ProductList'; +import { useSearchParams } from 'react-router-dom'; +import SearchBox from 'components/SearchBox/SearchBox'; +import ProductList from '../components/ProductList/ProductList'; import { getProducts } from '../fakeAPI'; const Products = () => { const products = getProducts(); + const [searchParams, setSearchParams] = useSearchParams(); + const productName = searchParams.get('name') ?? ''; + + const visibleProducts = products.filter(product => + product.name.toLowerCase().includes(productName.toLowerCase()) + ); + + const updateQueryString = name => { + const nextParams = name !== '' ? { name } : {}; + setSearchParams(nextParams); + }; + return (
- + +
); };