diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f06235c --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules +dist diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..abb0d7a --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,45 @@ +#npx lint-staged + + +echo ' +💅🛠️ Formatting and testing project before committing... +' + +# Check tsconfig standards +npm run check-types || +( + echo ' + TypeScript Check Failed. ❌ + Make the required changes listed above, add changes and try to commit again. + ' + false; +) + + +# Check ESLint Standards +npm run check-lint || +( + echo ' + ESLint Check Failed. ❌ + Make the required changes listed above, add changes and try to commit again. + ' + false; +) + + +# Check Prettier standards +npm run check-format-client || +( + echo ' + Prettier Check Failed. ❌ + Run npm run format, add changes and try commit again.'; + false; +) + +npm run check-format-server || +( + echo ' + Prettier Check Failed. ❌ + Run npm run format, add changes and try commit again.'; + false; +) \ No newline at end of file diff --git a/client/index.html b/client/index.html index 90aae2f..e132ed2 100644 --- a/client/index.html +++ b/client/index.html @@ -2,7 +2,7 @@ - + Online Shopping Site for Mobiles, Electronics, Furniture, Grocery, diff --git a/client/package-lock.json b/client/package-lock.json index f58b969..e47ce28 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -12,12 +12,12 @@ "@emotion/styled": "^11.11.0", "@mui/material": "^5.15.2", "@reduxjs/toolkit": "^2.0.1", - "@tanstack/react-query": "^5.17.10", "axios": "^1.6.4", - "classnames": "^2.5.1", "lodash.debounce": "^4.0.8", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-hot-toast": "^2.4.1", + "react-image-gallery": "^1.3.0", "react-redux": "^9.0.4", "react-router-dom": "^6.21.0" }, @@ -1641,30 +1641,6 @@ "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", "dev": true }, - "node_modules/@tanstack/query-core": { - "version": "5.17.10", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.17.10.tgz", - "integrity": "sha512-bJ2oQUDBftvHcEkLS3gyzzShSeZpJyzNNRu8oHK13iNdsofyaDXtNO/c1Zy/PZYVX+PhqOXwoT42gMiEMRSSfQ==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tanstack/react-query": { - "version": "5.17.10", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.17.10.tgz", - "integrity": "sha512-TNmJN7LkSLzmv01Jen3JbcvhXZyRhc/ETJNjssmmlyMB8IoNvicfgvDRX2gX3q1FTONq+mfsmWintwI+ejmEUw==", - "dependencies": { - "@tanstack/query-core": "5.17.10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^18.0.0" - } - }, "node_modules/@testing-library/dom": { "version": "9.3.3", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.3.tgz", @@ -2772,11 +2748,6 @@ "node": "*" } }, - "node_modules/classnames": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", - "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" - }, "node_modules/clsx": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", @@ -3790,6 +3761,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/goober": { + "version": "2.1.14", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.14.tgz", + "integrity": "sha512-4UpC0NdGyAFqLNPnhCT2iHpza2q+RAY3GV85a/mRPdzyPQMsj0KmMMuetdIkzWRbJ+Hgau1EZztq8ImmiMGhsg==", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -5181,6 +5160,29 @@ "react": "^18.2.0" } }, + "node_modules/react-hot-toast": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.4.1.tgz", + "integrity": "sha512-j8z+cQbWIM5LY37pR6uZR6D4LfseplqnuAO4co4u8917hBUvXlEqyP1ZzqVLcqoyUesZZv/ImreoCeHVDpE5pQ==", + "dependencies": { + "goober": "^2.1.10" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, + "node_modules/react-image-gallery": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/react-image-gallery/-/react-image-gallery-1.3.0.tgz", + "integrity": "sha512-lKnPaOzxqSdujPFyl+CkVw0j1aYoNCHk61cvr1h7aahf5aWqmPcR9YhUB4cYrt5Tn5KHDaPUzYm5/+cX9WxzaA==", + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", diff --git a/client/package.json b/client/package.json index d420521..163d749 100644 --- a/client/package.json +++ b/client/package.json @@ -8,20 +8,23 @@ "build": "vite build", "preview": "vite preview", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "check-lint": "eslint . --ext ts,tsx", + "check-types": "tsc --pretty --noEmit", "test": "vitest", - "format": "prettier --write ./src" + "format": "prettier --write ./src", + "check-format": "prettier --check ./src" }, "dependencies": { "@emotion/react": "^11.11.3", "@emotion/styled": "^11.11.0", "@mui/material": "^5.15.2", "@reduxjs/toolkit": "^2.0.1", - "@tanstack/react-query": "^5.17.10", "axios": "^1.6.4", - "classnames": "^2.5.1", "lodash.debounce": "^4.0.8", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-hot-toast": "^2.4.1", + "react-image-gallery": "^1.3.0", "react-redux": "^9.0.4", "react-router-dom": "^6.21.0" }, diff --git a/client/public/_redirects b/client/public/_redirects new file mode 100644 index 0000000..f824337 --- /dev/null +++ b/client/public/_redirects @@ -0,0 +1 @@ +/* /index.html 200 \ No newline at end of file diff --git a/client/src/App.tsx b/client/src/App.tsx index fa2d263..fbd1c08 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -4,12 +4,12 @@ import { Footer, Header } from './layouts' export default function App() { return ( - <div className="AppContainer"> + <main className="AppContainer"> <Header /> <Suspense fallback="Loading..."> <Outlet /> </Suspense> <Footer /> - </div> + </main> ) } diff --git a/client/src/assets/css/global.css b/client/src/assets/css/global.css new file mode 100644 index 0000000..c1f97b9 --- /dev/null +++ b/client/src/assets/css/global.css @@ -0,0 +1,22 @@ +.AppContainer { + background-color: #f1f3f6; + z-index: -1; + min-height: 100vh; +} + +hr { + border-top: 1px solid rgba(96, 96, 96, 0.217); + margin: 15px 0 0 0; +} + +.sidebar section { + margin: 10px 0; +} + +.sidebar section input { + margin: 8px 0; +} + +.sidebar section label { + padding: 0 10px; +} diff --git a/client/src/assets/css/reset.css b/client/src/assets/css/reset.css index a1ad506..d6edfe7 100644 --- a/client/src/assets/css/reset.css +++ b/client/src/assets/css/reset.css @@ -11,18 +11,15 @@ html { font-family: system-ui, sans-serif; } +html, body { - line-height: 1.5; - min-height: 100vh; - min-width: 100vw; - /* overflow-x: hidden; */ + height: 100%; } -.AppContainer { - background-color: #f1f3f6; - z-index: -1; +body { + line-height: 1.5; min-height: 100vh; - min-width: 100rem; + max-width: 100vw; } input, @@ -32,7 +29,23 @@ select { font: inherit; } -/* responsive images/videos */ +input[type='radio'], +label[for] { + cursor: pointer; +} + +input[type='text'], +input[type='number'], +select, +textarea { + font-size: 16px; +} + +button:disabled { + opacity: 0.5; + pointer-events: none; +} + img, picture, svg, diff --git a/client/src/assets/img/fa.png b/client/src/assets/img/fa.png new file mode 100644 index 0000000..3c578fd Binary files /dev/null and b/client/src/assets/img/fa.png differ diff --git a/client/public/flipkart.png b/client/src/assets/img/flipkart.png similarity index 100% rename from client/public/flipkart.png rename to client/src/assets/img/flipkart.png diff --git a/client/public/flipkartPlus.png b/client/src/assets/img/flipkartPlus.png similarity index 100% rename from client/public/flipkartPlus.png rename to client/src/assets/img/flipkartPlus.png diff --git a/client/src/assets/img/shield.svg b/client/src/assets/img/shield.svg new file mode 100644 index 0000000..02f9dd7 --- /dev/null +++ b/client/src/assets/img/shield.svg @@ -0,0 +1 @@ +<svg width="31" height="38" viewBox="0 0 31 38" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><title>shield (1) \ No newline at end of file diff --git a/client/src/components/Form.tsx b/client/src/components/Form.tsx deleted file mode 100644 index 0b72510..0000000 --- a/client/src/components/Form.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export default function Form() { - return ( - <> -
Form
- - ) -} diff --git a/client/src/pages/signup/SignUpPage.module.css b/client/src/components/form/Form.module.css similarity index 88% rename from client/src/pages/signup/SignUpPage.module.css rename to client/src/components/form/Form.module.css index 5993646..d1de9b4 100644 --- a/client/src/pages/signup/SignUpPage.module.css +++ b/client/src/components/form/Form.module.css @@ -1,4 +1,4 @@ -.login-container { +.form-container { margin: 1.4rem auto; width: 50rem; height: 31rem; @@ -15,13 +15,13 @@ height: 100%; } -.login-title { +.form-title { font-size: 1.7rem; font-weight: 500; margin: 0.5rem 0; } -.login-desc { +.form-desc { color: #dbdbdb; font-size: 1.1rem; } @@ -32,7 +32,7 @@ height: 100%; } -.right form { +.right .form { margin-top: 2rem; display: flex; align-items: center; @@ -40,7 +40,7 @@ gap: 0.8rem; } -form input { +.form input { width: 100%; padding: 0.4rem 1rem; margin: 0.6rem 0; @@ -49,11 +49,11 @@ form input { border: 0; border-bottom: 1px solid grey; } -input::placeholder { +.form input::placeholder { opacity: 0.7; } -form button { +.form button { border: 0; background-color: #fb641b; box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.2); @@ -64,7 +64,7 @@ form button { color: white; } -small { +.form-small { text-align: left; font-size: 0.7rem; color: #878787; diff --git a/client/src/components/form/Form.tsx b/client/src/components/form/Form.tsx new file mode 100644 index 0000000..143989a --- /dev/null +++ b/client/src/components/form/Form.tsx @@ -0,0 +1,49 @@ +import { Link } from 'react-router-dom' +import styles from './Form.module.css' + +type FormProps = { + mode: string + promptText: string + promptLink: string + promptLinkText: string +} + +export default function From({ + mode, + promptText, + promptLink, + promptLinkText, +}: FormProps) { + return ( +
+
+
+ {mode} +
+
+

Get access to your Orders,

+

Wishlist and Recommendations

+
+
+
+
+ + + + By continuing, you agree to Flipkart's + Terms of Use + and + Privacy Policy. + + +

+ {promptText}{' '} + + {promptLinkText} + +

+
+
+
+ ) +} diff --git a/client/src/components/loader/Loader.tsx b/client/src/components/loader/Loader.tsx new file mode 100644 index 0000000..0a79188 --- /dev/null +++ b/client/src/components/loader/Loader.tsx @@ -0,0 +1,17 @@ +import { CircularProgress } from '@mui/material' + +export default function Loader() { + return ( +
+ +
+ ) +} diff --git a/client/src/components/notification/Notification.tsx b/client/src/components/notification/Notification.tsx new file mode 100644 index 0000000..4b1d00c --- /dev/null +++ b/client/src/components/notification/Notification.tsx @@ -0,0 +1,18 @@ +import { Alert } from '@mui/material' + +type Type = 'success' | 'info' | 'warning' | 'error' + +type Props = { + type: Type + message: string +} + +export default function Notification({ type = 'info', message }: Props) { + return ( + <> + + {message} + + + ) +} diff --git a/client/src/components/ui/Button.tsx b/client/src/components/ui/Button.tsx deleted file mode 100644 index c72c12d..0000000 --- a/client/src/components/ui/Button.tsx +++ /dev/null @@ -1,5 +0,0 @@ -type ButtonType = React.ComponentPropsWithoutRef<'button'> - -export default function Button({ children, ...props }: ButtonType) { - return -} diff --git a/client/src/components/ui/Input.tsx b/client/src/components/ui/Input.tsx index 174bf95..af080ca 100644 --- a/client/src/components/ui/Input.tsx +++ b/client/src/components/ui/Input.tsx @@ -1,9 +1,43 @@ -/* eslint-disable react-refresh/only-export-components */ +type InputProps = { + type: string + name?: string + placeholder?: string + id?: string + value: string + onChange: React.ChangeEventHandler | undefined + checked?: boolean + label?: string + className?: string +} -// import { forwardRef } from 'react' +function Input({ + type, + name, + placeholder, + id, + value, + onChange, + checked, + label, + className = '', +}: InputProps) { + return ( + <> + + + + ) +} -// function Input({ label, type = 'text', className = '', ...props }, ref) { -// return <>{label && } -// } - -// export default forwardRef(Input) +export default Input diff --git a/client/src/conf/index.ts b/client/src/conf/index.ts index f8d9a49..c443916 100644 --- a/client/src/conf/index.ts +++ b/client/src/conf/index.ts @@ -1,4 +1,4 @@ export const ENV = { API_BASE_URL: String(import.meta.env.VITE_API_BASE_URL), - + SERVER_URL: String(import.meta.env.VITE_SERVER_URL), } diff --git a/client/src/constants/filterConstants.ts b/client/src/constants/filterConstants.ts index 8ec39d0..ef903fa 100644 --- a/client/src/constants/filterConstants.ts +++ b/client/src/constants/filterConstants.ts @@ -2,7 +2,6 @@ export const SORT_TYPE = { PRICE_HIGH_TO_LOW: 'priceHighToLow', PRICE_LOW_TO_HIGH: 'priceLowToHigh', RATING_HIGH_TO_LOW: 'ratingHighToLow', - RATING_LOW_TO_HIGH: 'ratingLowToHigh', } export const RATING_TYPE = { @@ -12,3 +11,121 @@ export const RATING_TYPE = { ONE_AND_UP: '1', } +export const RATING_ITEMS = [ + { value: RATING_TYPE.FOUR_AND_UP, label: '4★ & above' }, + { value: RATING_TYPE.THREE_AND_UP, label: '3★ & above' }, + { value: RATING_TYPE.TWO_AND_UP, label: '2★ & above' }, + { value: RATING_TYPE.ONE_AND_UP, label: '1★ & above' }, +] + +export const SORT_ITEMS = [ + { value: SORT_TYPE.PRICE_HIGH_TO_LOW, label: 'Price high to low' }, + { value: SORT_TYPE.PRICE_LOW_TO_HIGH, label: 'Price low to high' }, + { value: SORT_TYPE.RATING_HIGH_TO_LOW, label: 'Rating High To Low' }, +] + +type CategoryType = Record + +export const CATEGORIES: CategoryType = { + Smartphones: 'smartphones', + Laptops: 'laptops', + Fragrances: 'fragrances', + Skincare: 'skincare', + Groceries: 'groceries', + 'Home Decoration': 'home-decoration', + Furniture: 'furniture', + Tops: 'tops', + 'Womens Dresses': 'womens-dresses', + 'Womens Shoes': 'womens-shoes', + 'Mens Shirts': 'mens-shirts', + 'Mens Shoes': 'mens-shoes', + 'Mens Watches': 'mens-watches', + 'Womens Watches': 'womens-watches', + 'Womens Bags': 'womens-bags', + 'Womens Jewellery': 'womens-jewellery', + Sunglasses: 'sunglasses', + Automotive: 'automotive', + Motorcycle: 'motorcycle', + Lighting: 'lighting', +} + +export const BRAND = [ + 'Apple', + 'Samsung', + 'OPPO', + 'Huawei', + 'Microsoft Surface', + 'Infinix', + 'HP Pavilion', + 'Impression of Acqua Di Gio', + 'Royal_Mirage', + 'Fog Scent Xpressio', + 'Al Munakh', + 'Lord - Al-Rehab', + "L'Oreal Paris", + 'Hemani Tea', + 'Dermive', + 'ROREC White Rice', + 'Fair & Clear', + 'Saaf & Khaas', + 'Bake Parlor Big', + 'Baking Food Items', + 'fauji', + 'Dry Rose', + 'Boho Decor', + 'Flying Wooden', + 'LED Lights', + 'luxury palace', + 'Golden', + 'Furniture Bed Set', + 'Ratttan Outdoor', + 'Kitchen Shelf', + 'Multi Purpose', + 'AmnaMart', + 'Professional Wear', + 'Soft Cotton', + 'Top Sweater', + 'RED MICKY MOUSE..', + 'Digital Printed', + 'Ghazi Fabric', + 'IELGY', + 'IELGY fashion', + 'Synthetic Leather', + 'Sandals Flip Flops', + 'Maasai Sandals', + 'Arrivals Genuine', + 'Vintage Apparel', + 'FREE FIRE', + 'The Warehouse', + 'Sneakers', + 'Rubber', + 'Naviforce', + 'SKMEI 9117', + 'Strap Skeleton', + 'Stainless', + 'Eastern Watches', + 'Luxury Digital', + 'Watch Pearls', + 'Bracelet', + 'LouisWill', + 'Copenhagen Luxe', + 'Steal Frame', + 'Darojay', + 'Fashion Jewellery', + 'Cuff Butterfly', + 'Designer Sun Glasses', + 'mastar watch', + 'Car Aux', + 'W1209 DC12V', + 'TC Reusable', + 'Neon LED Light', + 'METRO 70cc Motorcycle - MR70', + 'BRAVE BULL', + 'shock absorber', + 'JIEPOLLY', + 'Xiangle', + 'lightingbrilliance', + 'Ifei Home', + 'DADAWU', + 'YIOSI', +] diff --git a/client/src/context/ServerStatusProvider.tsx b/client/src/context/ServerStatusProvider.tsx new file mode 100644 index 0000000..f4b28ae --- /dev/null +++ b/client/src/context/ServerStatusProvider.tsx @@ -0,0 +1,47 @@ +import axios from 'axios' +import { PropsWithChildren, createContext, useEffect, useState } from 'react' + +import { ENV } from '@/conf' + +type ServerStateType = { + status: boolean + message: string +} + +export const ServerStatusContext = createContext(null) + +export default function ServerStatusProvider({ children }: PropsWithChildren) { + const [serverStatus, setServerStatus] = useState({ + status: false, + message: '', + }) + + useEffect(() => { + const checkServerStatus = async () => { + try { + const { data } = await axios.get(`${ENV.SERVER_URL}/api/v1/status`) + + if (data.status) { + setServerStatus(data) + } else { + setServerStatus({ status: false, message: 'Server is not running' }) + } + } catch (error) { + setServerStatus({ status: false, message: 'Wait for server to start' }) + } + } + + checkServerStatus() + + const intervalId = setInterval(checkServerStatus, 10000) + + // Clear interval on component unmount + return () => clearInterval(intervalId) + }, []) + + return ( + + {children} + + ) +} diff --git a/client/src/hooks/useFilters.ts b/client/src/hooks/useFilters.ts index 65c8dbb..1444232 100644 --- a/client/src/hooks/useFilters.ts +++ b/client/src/hooks/useFilters.ts @@ -1,42 +1,43 @@ import { SORT_TYPE } from '@/constants/filterConstants' import { useGetAllProductsQuery } from '@/state/services/productApi' -import { useAppSelector } from '@/state/store' +import type { ProductType } from '@/types' +import useGetParams from './useGetParams' export default function useFilter() { - const stateData = useAppSelector((state) => state.filter) - const { data } = useGetAllProductsQuery() - const filteredData = data?.products - ?.filter((item) => { + const { data, isLoading } = useGetAllProductsQuery(null) + + const { q, category, brand, rating, price, sort } = useGetParams() + const products = data?.products + + const filteredData = products + ?.filter((item: ProductType) => { // Price filter - if ( - stateData.priceRange && - !( - item.price >= stateData.priceRange[0] && - item.price <= stateData.priceRange[1] - ) - ) + if (price && !(item.price >= price[0] && item.price <= price[1])) return false // Rating filter - if ( - stateData.stateRating && - !(item.rating >= Number(stateData.stateRating)) - ) - return false + if (rating && !(item.rating >= Number(rating))) return false // Search filter if ( - stateData.searchQuery && - !item.title.toLowerCase().includes(stateData.searchQuery) && - !item.category.toLowerCase().includes(stateData.searchQuery) && - !item.brand.toLowerCase().includes(stateData.searchQuery) + q && + !item.title.toLowerCase().includes(q.toLowerCase()) && + !item.category.toLowerCase().includes(q.toLowerCase()) && + !item.brand.toLowerCase().includes(q.toLowerCase()) ) return false + + // Category filter + if (category && !(item.category === category)) return false + + // Brand filter + if (brand && !(item.brand === brand)) return false + // Add more filters as needed return true }) - .sort((a, b) => { - switch (stateData.sort) { + .sort((a: ProductType, b: ProductType) => { + switch (sort) { case SORT_TYPE.PRICE_HIGH_TO_LOW: return b.price - a.price case SORT_TYPE.PRICE_LOW_TO_HIGH: @@ -49,5 +50,5 @@ export default function useFilter() { } }) - return { filteredData } + return { filteredData, isLoading } } diff --git a/client/src/hooks/useGetParams.ts b/client/src/hooks/useGetParams.ts new file mode 100644 index 0000000..01204f9 --- /dev/null +++ b/client/src/hooks/useGetParams.ts @@ -0,0 +1,14 @@ +import { useSearchParams } from 'react-router-dom' + +export default function useGetParams() { + const searchParams = useSearchParams()[0] + + const q = searchParams.get('q') || '' + const category = searchParams.get('category') || '' + const brand = searchParams.get('brand') || '' + const rating = searchParams.get('rating') || '' + const price = searchParams.get('price')?.split('-').map(Number) || [10, 2000] + const sort = searchParams.get('sort') + + return { category, brand, price, q, rating, sort } +} diff --git a/client/src/hooks/useHandleDispatch.ts b/client/src/hooks/useHandleDispatch.ts index 2f8e76d..2dbcfbe 100644 --- a/client/src/hooks/useHandleDispatch.ts +++ b/client/src/hooks/useHandleDispatch.ts @@ -1,38 +1,47 @@ +import { useSearchParams } from 'react-router-dom' + import { addToCart, decrementCartItem, removeFromCart, } from '@/state/slices/cartSlice' -import { - clearFilters, - filterRating, - filterSearch, - priceRange, - sort, -} from '@/state/slices/filtersSlice' import { useAppDispatch } from '@/state/store' -import { CartType } from '@/types' +import type { CartType, ProductType } from '@/types' export default function useHandleDispatch() { const dispatch = useAppDispatch() + const setSearchParams = useSearchParams()[1] const handlePriceRange = (event: Event, newValue: number | number[]) => { - dispatch(priceRange(newValue)) + setSearchParams((prev) => { + // @ts-expect-error - MUI's fault? + prev.set('price', `${newValue[0]}-${newValue[1]}`) + return prev + }) } const handleFilterRating = (filterValue: string) => { - dispatch(filterRating(filterValue)) + setSearchParams((prev) => { + prev.set('rating', filterValue) + return prev + }) } const handleSort = (sortType: string) => { - dispatch(sort(sortType)) + setSearchParams((prev) => { + prev.set('sort', sortType) + return prev + }) } const handleSearchQuery = (query: string) => { - dispatch(filterSearch(query)) + setSearchParams((prev) => { + prev.set('q', query) + return prev + }) } - const handleAddToCart = (data: CartType) => { + const handleAddToCart = (data: ProductType) => { dispatch(addToCart(data)) } @@ -44,8 +53,32 @@ export default function useHandleDispatch() { dispatch(removeFromCart(cartItemId)) } + const handleCategoryFilter = (category: string) => { + setSearchParams((prev) => { + prev.set('category', category) + prev.delete('q') + return prev + }) + } + + const handleBrandFilter = (brand: string) => { + setSearchParams((prev) => { + prev.set('brand', brand) + prev.delete('q') + return prev + }) + } + const handleClearFilter = () => { - dispatch(clearFilters()) + setSearchParams((prev) => { + prev.delete('q') + prev.delete('category') + prev.delete('rating') + prev.delete('price') + prev.delete('sort') + prev.delete('brand') + return prev + }) } return { @@ -56,6 +89,8 @@ export default function useHandleDispatch() { handleRemoveFromCart, handlePriceRange, handleSearchQuery, + handleCategoryFilter, + handleBrandFilter, handleClearFilter, } } diff --git a/client/src/hooks/useServerStatus.ts b/client/src/hooks/useServerStatus.ts new file mode 100644 index 0000000..4438f1b --- /dev/null +++ b/client/src/hooks/useServerStatus.ts @@ -0,0 +1,12 @@ +import { ServerStatusContext } from '@/context/ServerStatusProvider' +import { useContext } from 'react' + +export default function useServerStatus() { + const context = useContext(ServerStatusContext) + if (context === undefined) { + throw new Error( + 'useServerStatus must be used within an ServerStatusProvider' + ) + } + return context +} diff --git a/client/src/layouts/filters/SidebarFilters.tsx b/client/src/layouts/filters/SidebarFilters.tsx index d414506..c73a3fa 100644 --- a/client/src/layouts/filters/SidebarFilters.tsx +++ b/client/src/layouts/filters/SidebarFilters.tsx @@ -1,51 +1,48 @@ -import { Box, Button, FormControl, Typography } from '@mui/material' - -import useHandleDispatch from '@/hooks/useHandleDispatch' -import { useAppSelector } from '@/state/store' -import PriceSliderFilter from './components/PriceSliderFilter' -import SortProductsFilter from './components/SortProductsFilter' -import RatingFilter from './components/RatingFilter' -import CategoryFilter from './components/CategoryFilter' - -export default function SidebarFilters() { - const stateData = useAppSelector((state) => state.filter) - const { - handleFilterRating, - handleSort, - handlePriceRange, - handleClearFilter, - } = useHandleDispatch() +import { memo } from 'react' +import { + AppliedFilters, + BrandFilter, + CategoryFilter, + ClearFilter, + PriceSliderFilter, + RatingFilter, + SortProductsFilter, +} from './components' +function SidebarFilters() { return ( - <> - - Filters - - - {/* Clear Filters */} - -
- - - - {/* Sort products */} - - - {/* Ratings filter */} - - - - {/* Category Filters */} +
+

Filters

+
+ + + + + + + + + + - - + + +
+
) } + +const MemoizedSidebarFilters = memo(SidebarFilters) +export default MemoizedSidebarFilters diff --git a/client/src/layouts/filters/components/CategoryFilter.tsx b/client/src/layouts/filters/components/CategoryFilter.tsx index 2f3f041..c509308 100644 --- a/client/src/layouts/filters/components/CategoryFilter.tsx +++ b/client/src/layouts/filters/components/CategoryFilter.tsx @@ -1,42 +1,31 @@ -import { Checkbox, FormControlLabel, FormGroup } from '@mui/material' +import { memo } from 'react' -const categories = [ - 'smartphones', - 'laptops', - 'fragrances', - 'skincare', - 'groceries', - 'home-decoration', - 'furniture', - 'tops', - 'womens-dresses', - 'womens-shoes', - 'mens-shirts', - 'mens-shoes', - 'mens-watches', - 'womens-watches', - 'womens-bags', - 'womens-jewellery', - 'sunglasses', - 'automotive', - 'motorcycle', - 'lighting', -] +import Input from '@/components/ui/Input' +import { CATEGORIES } from '@/constants/filterConstants' +import useGetParams from '@/hooks/useGetParams' +import useHandleDispatch from '@/hooks/useHandleDispatch' -export default function CategoryFilter() { +function CategoryFilter() { + const { handleCategoryFilter } = useHandleDispatch() + const { category: categoryParam } = useGetParams() return ( - <> - {/* @ts-expect-error - mui component issue */} - console.log(e.target.value)}> - {categories.map((category, index) => ( - } +
+

Categories

+ {Object.keys(CATEGORIES).map((category) => ( +
+ handleCategoryFilter(e.target.value)} + checked={categoryParam === CATEGORIES[category]} /> - ))} - - +
+ ))} +
) } + +const MemoizedCategoryFilter = memo(CategoryFilter) +export default MemoizedCategoryFilter diff --git a/client/src/layouts/filters/components/PriceSliderFilter.tsx b/client/src/layouts/filters/components/PriceSliderFilter.tsx index f27b841..2f74e98 100644 --- a/client/src/layouts/filters/components/PriceSliderFilter.tsx +++ b/client/src/layouts/filters/components/PriceSliderFilter.tsx @@ -1,22 +1,30 @@ -import { Box, Slider, Typography } from '@mui/material' +import Slider from '@mui/material/Slider' +import { memo } from 'react' -export default function PriceSliderFilter({ stateData, handlePriceRange }) { +import useGetParams from '@/hooks/useGetParams' +import useHandleDispatch from '@/hooks/useHandleDispatch' + +function PriceSliderFilter() { + const { handlePriceRange } = useHandleDispatch() + const { price } = useGetParams() return ( - <> - Price - - ${stateData.priceRange[0]} - ${stateData.priceRange[1]} - - +
+

Price

+

{`$${price[0]} - $${price[1]}`}

+
- - +
+
) } + +const MemoizedPriceSliderFilter = memo(PriceSliderFilter) +export default MemoizedPriceSliderFilter diff --git a/client/src/layouts/filters/components/RatingFilter.tsx b/client/src/layouts/filters/components/RatingFilter.tsx index 4bbba6d..da0c852 100644 --- a/client/src/layouts/filters/components/RatingFilter.tsx +++ b/client/src/layouts/filters/components/RatingFilter.tsx @@ -1,39 +1,33 @@ -import { FormControlLabel, Radio, RadioGroup, Typography } from '@mui/material' -import { RATING_TYPE } from '@/constants/filterConstants' +import { memo } from 'react' -export default function RatingFilter({ stateData, handleFilterRating }) { +import Input from '@/components/ui/Input' +import { RATING_ITEMS } from '@/constants/filterConstants' +import useGetParams from '@/hooks/useGetParams' +import useHandleDispatch from '@/hooks/useHandleDispatch' + +function RatingFilter() { + const { rating } = useGetParams() + const { handleFilterRating } = useHandleDispatch() return ( - <> - - Customer Review - - handleFilterRating(e.target.value)} - > - } - label="4 & Up" - /> - } - label="3 & Up" - /> - } - label="2 & Up" - /> - } - label="1 & Up" - /> - - +
+

Customer Review

+ {RATING_ITEMS.map((item) => ( +
+ handleFilterRating(event.target.value)} + name="rating" + label={item.label} + value={item.value} + /> +
+ ))} +
+
) } + +const MemoizedRatingFilter = memo(RatingFilter) +export default MemoizedRatingFilter diff --git a/client/src/layouts/filters/components/SortProductsFilter.tsx b/client/src/layouts/filters/components/SortProductsFilter.tsx index aaa07d5..c69883d 100644 --- a/client/src/layouts/filters/components/SortProductsFilter.tsx +++ b/client/src/layouts/filters/components/SortProductsFilter.tsx @@ -1,34 +1,36 @@ -import { FormControlLabel, Radio, RadioGroup, Typography } from '@mui/material' -import { SORT_TYPE } from '@/constants/filterConstants' +import { memo } from 'react' -export default function SortProductsFilter({ stateData, handleSort }) { +import Input from '@/components/ui/Input' +import { SORT_ITEMS } from '@/constants/filterConstants' +import useGetParams from '@/hooks/useGetParams' +import useHandleDispatch from '@/hooks/useHandleDispatch' + +function SortProductsFilter() { + const { sort } = useGetParams() + const { handleSort } = useHandleDispatch() return ( - <> - - Sort - - handleSort(e.target.value)} - > - } - label="Sort by high to low price" - /> - } - label="Sort by low to high price" - /> - } - label="Rating High To Low" - /> - - +
+

Sort

+
+ {SORT_ITEMS.map((item) => ( +
+ handleSort(event.target.value)} + name="sort" + label={item.label} + value={item.value} + /> +
+
+ ))} +
+
+
) } + +const MemoizedSortProductsFilter = memo(SortProductsFilter) +export default MemoizedSortProductsFilter diff --git a/client/src/layouts/filters/components/applied-filters/AppliedFilters.module.css b/client/src/layouts/filters/components/applied-filters/AppliedFilters.module.css new file mode 100644 index 0000000..d874a86 --- /dev/null +++ b/client/src/layouts/filters/components/applied-filters/AppliedFilters.module.css @@ -0,0 +1,22 @@ +.filter-list { + display: flex; + align-items: center; + flex-wrap: wrap; +} + +.filter-list .filter-item { + margin: 2px 4px; + padding: 0.4rem; + background-color: #e0e0e0; + font-size: 14px; + cursor: pointer; +} + +.filter-item:hover { + text-decoration: line-through; +} + +.no-filter { + font-style: italic; + color: grey; +} diff --git a/client/src/layouts/filters/components/applied-filters/AppliedFilters.tsx b/client/src/layouts/filters/components/applied-filters/AppliedFilters.tsx new file mode 100644 index 0000000..a5edab3 --- /dev/null +++ b/client/src/layouts/filters/components/applied-filters/AppliedFilters.tsx @@ -0,0 +1,54 @@ +import { memo } from 'react' +import { useSearchParams } from 'react-router-dom' + +import { RATING_ITEMS, SORT_ITEMS } from '@/constants/filterConstants' +import styles from './AppliedFilters.module.css' + +function AppliedFilters() { + const [searchParams, setSearchParams] = useSearchParams() + const params = [] + + for (const entry of searchParams.entries()) { + params.push(entry) + } + + const handleRemoveFilter = (key: string) => { + searchParams.delete(key) + setSearchParams(searchParams) + } + + const getLabel = (key: string, value: string) => { + let items: { value: string; label: string }[] = [] + + if (key === 'sort') { + items = SORT_ITEMS + } else if (key === 'rating') { + items = RATING_ITEMS + } + const item = items.find((item) => item.value === value) + return item ? item.label : value + } + + return ( +
+
    + {params.length > 0 ? ( + params.map(([key, value]) => ( +
  • handleRemoveFilter(key)} + > + ✕ {getLabel(key, value)} +
  • + )) + ) : ( +
  • No filters applied
  • + )} +
+
+ ) +} + +const MemoizedAppliedFilters = memo(AppliedFilters) +export default MemoizedAppliedFilters diff --git a/client/src/layouts/filters/components/brand-filter/BrandFilter.tsx b/client/src/layouts/filters/components/brand-filter/BrandFilter.tsx new file mode 100644 index 0000000..98ca589 --- /dev/null +++ b/client/src/layouts/filters/components/brand-filter/BrandFilter.tsx @@ -0,0 +1,31 @@ +import { memo } from 'react' + +import Input from '@/components/ui/Input' +import { BRAND } from '@/constants/filterConstants' +import useGetParams from '@/hooks/useGetParams' +import useHandleDispatch from '@/hooks/useHandleDispatch' + +function BrandFilter() { + const { handleBrandFilter } = useHandleDispatch() + const { brand: brandParam } = useGetParams() + return ( +
+

Brands

+ {BRAND.map((brand) => ( +
+ handleBrandFilter(e.target.value)} + checked={brandParam === brand} + /> +
+ ))} +
+ ) +} + +const MemoizedBrandFilter = memo(BrandFilter) +export default MemoizedBrandFilter diff --git a/client/src/layouts/filters/components/clear-filter/ClearFilter.module.css b/client/src/layouts/filters/components/clear-filter/ClearFilter.module.css new file mode 100644 index 0000000..f2a71a1 --- /dev/null +++ b/client/src/layouts/filters/components/clear-filter/ClearFilter.module.css @@ -0,0 +1,6 @@ +.clear-btn { + all: unset; + cursor: pointer; + color: #2874f0; + font-size: 14px; +} diff --git a/client/src/layouts/filters/components/clear-filter/ClearFilter.tsx b/client/src/layouts/filters/components/clear-filter/ClearFilter.tsx new file mode 100644 index 0000000..3787277 --- /dev/null +++ b/client/src/layouts/filters/components/clear-filter/ClearFilter.tsx @@ -0,0 +1,13 @@ +import useHandleDispatch from '@/hooks/useHandleDispatch' +import styles from './ClearFilter.module.css' + +export default function ClearFilter() { + const { handleClearFilter } = useHandleDispatch() + return ( +
+ +
+ ) +} diff --git a/client/src/layouts/filters/components/index.tsx b/client/src/layouts/filters/components/index.tsx new file mode 100644 index 0000000..bc5ef20 --- /dev/null +++ b/client/src/layouts/filters/components/index.tsx @@ -0,0 +1,17 @@ +import CategoryFilter from './CategoryFilter' +import PriceSliderFilter from './PriceSliderFilter' +import RatingFilter from './RatingFilter' +import SortProductsFilter from './SortProductsFilter' +import AppliedFilters from './applied-filters/AppliedFilters' +import BrandFilter from './brand-filter/BrandFilter' +import ClearFilter from './clear-filter/ClearFilter' + +export { + AppliedFilters, + BrandFilter, + CategoryFilter, + ClearFilter, + PriceSliderFilter, + RatingFilter, + SortProductsFilter, +} diff --git a/client/src/layouts/footer/Footer.tsx b/client/src/layouts/footer/Footer.tsx index a305d14..aad4c84 100644 --- a/client/src/layouts/footer/Footer.tsx +++ b/client/src/layouts/footer/Footer.tsx @@ -1,6 +1,7 @@ import { Divider } from '@mui/material' -import classNames from 'classnames/bind' +import { memo } from 'react' import { Link } from 'react-router-dom' + import gift from '@/assets/img/gift.svg' import paymentMethods from '@/assets/img/payment-method.svg' import question from '@/assets/img/question.svg' @@ -8,9 +9,7 @@ import seller from '@/assets/img/seller.svg' import star from '@/assets/img/star.svg' import styles from './Footer.module.css' -const cx = classNames.bind(styles) - -export default function Footer() { +function Footer() { const about = [ 'Contact Us', 'About Us', @@ -41,9 +40,9 @@ export default function Footer() { ] const social = ['Facebook', 'Twitter', 'YouTube'] return ( -