├── src ├── App.css ├── app │ ├── constants.js │ └── store.js ├── pages │ ├── CartPage.js │ ├── LoginPage.js │ ├── SignupPage.js │ ├── ForgotPasswordPage.js │ ├── AdminOrdersPage.js │ ├── AdminProductFormPage.js │ ├── AdminHome.js │ ├── UserOrdersPage.js │ ├── UserProfilePage.js │ ├── AdminProductDetailPage.js │ ├── Home.js │ ├── ProductDetailPage.js │ ├── 404.js │ ├── OrderSuccessPage.js │ └── Checkout.js ├── features │ ├── counter │ │ ├── counterAPI.js │ │ ├── Counter.js │ │ └── counterSlice.js │ ├── order │ │ ├── Order.js │ │ ├── orderAPI.js │ │ └── orderSlice.js │ ├── auth │ │ ├── components │ │ │ ├── Protected.js │ │ │ ├── ProtectedAdmin.js │ │ │ ├── Logout.js │ │ │ ├── ForgotPassword.js │ │ │ ├── Login.js │ │ │ └── Signup.js │ │ ├── authAPI.js │ │ └── authSlice.js │ ├── user │ │ ├── userAPI.js │ │ ├── userSlice.js │ │ └── components │ │ │ └── UserOrders.js │ ├── cart │ │ ├── CartAPI.js │ │ ├── CartSlice.js │ │ └── Cart.js │ ├── common │ │ ├── Footer.js │ │ ├── Pagination.js │ │ └── Modal.js │ ├── product │ │ ├── ProductAPI.js │ │ ├── ProductSlice.js │ │ └── components │ │ │ ├── ProductDetail.js │ │ │ └── ProductList.js │ ├── admin │ │ └── components │ │ │ ├── AdminOrders.js │ │ │ ├── AdminProductDetail.js │ │ │ └── ProductForm.js │ └── navbar │ │ └── Navbar.js ├── setupTests.js ├── reportWebVitals.js ├── App.test.js ├── index.css ├── index.js ├── logo.svg └── App.js ├── public ├── robots.txt ├── favicon.ico ├── logo192.png ├── logo512.png ├── space-craft.png ├── manifest.json └── index.html ├── tailwind.config.js ├── .gitignore ├── package.json └── README.md /src/App.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PB2204/space-craft/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PB2204/space-craft/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PB2204/space-craft/HEAD/public/logo512.png -------------------------------------------------------------------------------- /public/space-craft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PB2204/space-craft/HEAD/public/space-craft.png -------------------------------------------------------------------------------- /src/app/constants.js: -------------------------------------------------------------------------------- 1 | export const ITEMS_PER_PAGE = 10; 2 | export function discountedPrice(item){ 3 | return Math.round(item.price*(1-item.discountPercentage/100),2) 4 | } 5 | -------------------------------------------------------------------------------- /src/pages/CartPage.js: -------------------------------------------------------------------------------- 1 | import Cart from "../features/cart/Cart"; 2 | 3 | function CartPage() { 4 | return
5 | 6 |
; 7 | } 8 | 9 | export default CartPage; -------------------------------------------------------------------------------- /src/pages/LoginPage.js: -------------------------------------------------------------------------------- 1 | import Login from "../features/auth/components/Login"; 2 | function LoginPage() { 3 | return (
4 | 5 |
); 6 | } 7 | 8 | export default LoginPage; -------------------------------------------------------------------------------- /src/pages/SignupPage.js: -------------------------------------------------------------------------------- 1 | import Signup from "../features/auth/components/Signup"; 2 | 3 | function SignupPage() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | 11 | export default SignupPage; -------------------------------------------------------------------------------- /src/features/counter/counterAPI.js: -------------------------------------------------------------------------------- 1 | export function fetchCount(amount = 1) { 2 | return new Promise(async (resolve) =>{ 3 | const response = await fetch('http://localhost:8080') 4 | const data = await response.json() 5 | resolve({data}) 6 | } 7 | ); 8 | } 9 | -------------------------------------------------------------------------------- /src/pages/ForgotPasswordPage.js: -------------------------------------------------------------------------------- 1 | import ForgotPassword from "../features/auth/components/ForgotPassword"; 2 | function ForgotPasswordPage() { 3 | return (
4 | 5 |
); 6 | } 7 | 8 | export default ForgotPasswordPage; -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /src/features/order/Order.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useSelector, useDispatch } from 'react-redux'; 3 | import { } from './orderSlice'; 4 | 5 | export default function Order() { 6 | const dispatch = useDispatch(); 7 | 8 | return ( 9 |
10 |
{/* We will use to show orders on Admin Page */}
11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/pages/AdminOrdersPage.js: -------------------------------------------------------------------------------- 1 | import AdminOrders from "../features/admin/components/AdminOrders"; 2 | import NavBar from "../features/navbar/Navbar"; 3 | 4 | function AdminOrdersPage() { 5 | return ( 6 |
7 | 8 | 9 | 10 |
11 | ); 12 | } 13 | 14 | export default AdminOrdersPage; -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./src/**/*.{js,jsx,ts,tsx}", 5 | ], 6 | theme: { 7 | extend: { 8 | gridTemplateRows: { 9 | '[auto,auto,1fr]': 'auto auto 1fr', 10 | }, 11 | }, 12 | }, 13 | plugins: [require('@tailwindcss/aspect-ratio'),require('@tailwindcss/forms')], 14 | } 15 | 16 | -------------------------------------------------------------------------------- /src/pages/AdminProductFormPage.js: -------------------------------------------------------------------------------- 1 | import ProductForm from "../features/admin/components/ProductForm"; 2 | import NavBar from "../features/navbar/Navbar"; 3 | function AdminProductFormPage() { 4 | return ( 5 |
6 | 7 | 8 | 9 |
10 | ); 11 | } 12 | 13 | export default AdminProductFormPage; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /src/pages/AdminHome.js: -------------------------------------------------------------------------------- 1 | import AdminProductList from "../features/admin/components/AdminProductList"; 2 | import NavBar from "../features/navbar/Navbar"; 3 | 4 | function AdminHome() { 5 | return ( 6 |
7 | 8 | 9 | 10 | Foot 11 |
12 | ); 13 | } 14 | 15 | export default AdminHome; -------------------------------------------------------------------------------- /src/pages/UserOrdersPage.js: -------------------------------------------------------------------------------- 1 | import NavBar from '../features/navbar/Navbar'; 2 | import UserOrders from '../features/user/components/UserOrders'; 3 | 4 | function UserOrdersPage() { 5 | return ( 6 |
7 | 8 |

My Orders

9 | 10 |
11 |
12 | ); 13 | } 14 | 15 | export default UserOrdersPage; 16 | -------------------------------------------------------------------------------- /src/pages/UserProfilePage.js: -------------------------------------------------------------------------------- 1 | import NavBar from '../features/navbar/Navbar'; 2 | import UserProfile from '../features/user/components/UserProfile'; 3 | 4 | function UserProfilePage() { 5 | return ( 6 |
7 | 8 |

My Profile

9 | 10 |
11 |
12 | ); 13 | } 14 | 15 | export default UserProfilePage; -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /src/pages/AdminProductDetailPage.js: -------------------------------------------------------------------------------- 1 | import AdminProductDetail from "../features/admin/components/AdminProductDetail"; 2 | import NavBar from "../features/navbar/Navbar"; 3 | function AdminProductDetailPage() { 4 | return ( 5 |
6 | 7 | 8 | 9 |
10 | ); 11 | } 12 | 13 | export default AdminProductDetailPage; -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import { Provider } from 'react-redux'; 4 | import { store } from './app/store'; 5 | import App from './App'; 6 | 7 | test('renders learn react link', () => { 8 | const { getByText } = render( 9 | 10 | 11 | 12 | ); 13 | 14 | expect(getByText(/learn/i)).toBeInTheDocument(); 15 | }); 16 | -------------------------------------------------------------------------------- /src/features/auth/components/Protected.js: -------------------------------------------------------------------------------- 1 | import { useSelector } from 'react-redux'; 2 | import { Navigate } from 'react-router-dom'; 3 | import { selectLoggedInUser } from '../authSlice'; 4 | 5 | function Protected({ children }) { 6 | const user = useSelector(selectLoggedInUser); 7 | 8 | if (!user) { 9 | return ; 10 | } 11 | return children; 12 | } 13 | 14 | export default Protected; 15 | -------------------------------------------------------------------------------- /src/features/counter/Counter.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useSelector, useDispatch } from 'react-redux'; 3 | import { 4 | increment, 5 | incrementAsync, 6 | selectCount, 7 | } from './counterSlice'; 8 | 9 | export default function Counter() { 10 | const count = useSelector(selectCount); 11 | const dispatch = useDispatch(); 12 | 13 | 14 | return ( 15 |
16 |
17 | 18 | 19 |
20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/pages/Home.js: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | import NavBar from "../features/navbar/Navbar"; 3 | import ProductList from "../features/product/components/ProductList"; 4 | import Footer from "../features/common/Footer"; 5 | 6 | function Home() { 7 | return ( 8 |
9 | 10 | 11 | 12 | 13 |
14 | ); 15 | } 16 | 17 | export default Home; -------------------------------------------------------------------------------- /src/pages/ProductDetailPage.js: -------------------------------------------------------------------------------- 1 | import NavBar from "../features/navbar/Navbar"; 2 | import ProductDetail from "../features/product/components/ProductDetail"; 3 | import Footer from "../features/common/Footer"; 4 | 5 | function ProductDetailPage() { 6 | return ( 7 |
8 | 9 | 10 | 11 | 12 |
13 | ); 14 | } 15 | 16 | export default ProductDetailPage; -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | margin: 0; 7 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 8 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 9 | sans-serif; 10 | -webkit-font-smoothing: antialiased; 11 | -moz-osx-font-smoothing: grayscale; 12 | } 13 | 14 | code { 15 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 16 | monospace; 17 | } 18 | -------------------------------------------------------------------------------- /src/features/auth/components/ProtectedAdmin.js: -------------------------------------------------------------------------------- 1 | import { useSelector } from 'react-redux'; 2 | import { Navigate } from 'react-router-dom'; 3 | import { selectLoggedInUser } from '../authSlice'; 4 | 5 | function ProtectedAdmin({ children }) { 6 | const user = useSelector(selectLoggedInUser); 7 | 8 | if (!user) { 9 | return ; 10 | } 11 | if (user && user.role!=='admin') { 12 | return ; 13 | } 14 | return children; 15 | } 16 | 17 | export default ProtectedAdmin; 18 | -------------------------------------------------------------------------------- /src/app/store.js: -------------------------------------------------------------------------------- 1 | import { configureStore, createReducer } from '@reduxjs/toolkit'; 2 | import productReducer from '../features/product/productSlice'; 3 | import authReducer from '../features/auth/authSlice'; 4 | import cartReducer from '../features/cart/cartSlice'; 5 | import orderReducer from '../features/order/orderSlice'; 6 | import userReducer from '../features/user/userSlice'; 7 | 8 | export const store = configureStore({ 9 | reducer: { 10 | product: productReducer, 11 | auth: authReducer, 12 | cart: cartReducer, 13 | order: orderReducer, 14 | user: userReducer, 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/features/auth/components/Logout.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { selectLoggedInUser, signOutAsync } from '../authSlice'; 3 | import { useDispatch, useSelector } from 'react-redux'; 4 | import { Navigate } from 'react-router-dom'; 5 | 6 | function Logout() { 7 | const dispatch = useDispatch(); 8 | const user = useSelector(selectLoggedInUser); 9 | 10 | useEffect(() => { 11 | dispatch(signOutAsync()); 12 | }); 13 | 14 | // but useEffect runs after render, so we have to delay navigate part 15 | return <>{!user && }; 16 | } 17 | 18 | export default Logout; 19 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import { Provider } from 'react-redux'; 4 | import { store } from './app/store'; 5 | import App from './App'; 6 | import reportWebVitals from './reportWebVitals'; 7 | import './index.css'; 8 | 9 | const container = document.getElementById('root'); 10 | const root = createRoot(container); 11 | 12 | root.render( 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | 20 | // If you want to start measuring performance in your app, pass a function 21 | // to log results (for example: reportWebVitals(console.log)) 22 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 23 | reportWebVitals(); 24 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | 15 | Space Craft 16 | 17 | 18 | 19 |
20 | 21 | -------------------------------------------------------------------------------- /src/features/user/userAPI.js: -------------------------------------------------------------------------------- 1 | export function fetchLoggedInUserOrders(userId) { 2 | return new Promise(async (resolve) =>{ 3 | const response = await fetch('http://localhost:8080/orders/?user.id='+userId) 4 | const data = await response.json() 5 | resolve({data}) 6 | } 7 | ); 8 | } 9 | 10 | 11 | export function fetchLoggedInUser(userId) { 12 | return new Promise(async (resolve) =>{ 13 | const response = await fetch('http://localhost:8080/users/'+userId) 14 | const data = await response.json() 15 | resolve({data}) 16 | } 17 | ); 18 | } 19 | 20 | export function updateUser(update) { 21 | return new Promise(async (resolve) => { 22 | const response = await fetch('http://localhost:8080/users/'+update.id, { 23 | method: 'PATCH', 24 | body: JSON.stringify(update), 25 | headers: { 'content-type': 'application/json' }, 26 | }); 27 | const data = await response.json(); 28 | // TODO: on server it will only return some info of user (not password) 29 | resolve({ data }); 30 | }); 31 | } 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/pages/404.js: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | 3 | function PageNotFound() { 4 | return ( 5 |
6 |
7 |

404

8 |

9 | Page not found 10 |

11 |

12 | Sorry, we couldn't find the page you're looking for. 13 |

14 |
15 | 19 | Go back home 20 | 21 |
22 |
23 |
24 | ); 25 | } 26 | 27 | export default PageNotFound; 28 | -------------------------------------------------------------------------------- /src/features/counter/counterSlice.js: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; 2 | import { fetchCount } from './counterAPI'; 3 | 4 | const initialState = { 5 | value: 0, 6 | status: 'idle', 7 | }; 8 | 9 | export const incrementAsync = createAsyncThunk( 10 | 'counter/fetchCount', 11 | async (amount) => { 12 | const response = await fetchCount(amount); 13 | // The value we return becomes the `fulfilled` action payload 14 | return response.data; 15 | } 16 | ); 17 | 18 | export const counterSlice = createSlice({ 19 | name: 'counter', 20 | initialState, 21 | reducers: { 22 | increment: (state) => { 23 | state.value += 1; 24 | }, 25 | }, 26 | extraReducers: (builder) => { 27 | builder 28 | .addCase(incrementAsync.pending, (state) => { 29 | state.status = 'loading'; 30 | }) 31 | .addCase(incrementAsync.fulfilled, (state, action) => { 32 | state.status = 'idle'; 33 | state.value += action.payload; 34 | }); 35 | }, 36 | }); 37 | 38 | export const { increment } = counterSlice.actions; 39 | 40 | export const selectCount = (state) => state.counter.value; 41 | 42 | export default counterSlice.reducer; 43 | -------------------------------------------------------------------------------- /src/features/auth/authAPI.js: -------------------------------------------------------------------------------- 1 | export function createUser(userData) { 2 | return new Promise(async (resolve) => { 3 | const response = await fetch('http://localhost:8080/users', { 4 | method: 'POST', 5 | body: JSON.stringify(userData), 6 | headers: { 'content-type': 'application/json' }, 7 | }); 8 | const data = await response.json(); 9 | // TODO: on server it will only return some info of user (not password) 10 | resolve({ data }); 11 | }); 12 | } 13 | 14 | export function checkUser(loginInfo) { 15 | return new Promise(async (resolve, reject) => { 16 | const email = loginInfo.email; 17 | const password = loginInfo.password; 18 | const response = await fetch('http://localhost:8080/users?email=' + email); 19 | const data = await response.json(); 20 | console.log({ data }); 21 | if (data.length) { 22 | if (password === data[0].password) { 23 | resolve({ data: data[0] }); 24 | } else { 25 | reject({ message: 'wrong credentials' }); 26 | } 27 | } else { 28 | reject({ message: 'user not found' }); 29 | } 30 | // TODO: on server it will only return some info of user (not password) 31 | }); 32 | } 33 | 34 | export function signOut(userId) { 35 | return new Promise(async (resolve) => { 36 | // TODO: on server we will remove user session info 37 | resolve({ data: 'success' }); 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /src/features/order/orderAPI.js: -------------------------------------------------------------------------------- 1 | export function createOrder(order) { 2 | return new Promise(async (resolve) => { 3 | const response = await fetch('http://localhost:8080/orders', { 4 | method: 'POST', 5 | body: JSON.stringify(order), 6 | headers: { 'content-type': 'application/json' }, 7 | }); 8 | const data = await response.json(); 9 | resolve({ data }); 10 | }); 11 | } 12 | 13 | export function updateOrder(order) { 14 | return new Promise(async (resolve) => { 15 | const response = await fetch('http://localhost:8080/orders/'+order.id, { 16 | method: 'PATCH', 17 | body: JSON.stringify(order), 18 | headers: { 'content-type': 'application/json' }, 19 | }); 20 | const data = await response.json(); 21 | resolve({ data }); 22 | }); 23 | } 24 | 25 | export function fetchAllOrders(sort, pagination) { 26 | let queryString = ''; 27 | 28 | for (let key in sort) { 29 | queryString += `${key}=${sort[key]}&`; 30 | } 31 | for (let key in pagination) { 32 | queryString += `${key}=${pagination[key]}&`; 33 | } 34 | 35 | return new Promise(async (resolve) => { 36 | //TODO: we will not hard-code server URL here 37 | const response = await fetch( 38 | 'http://localhost:8080/orders?' + queryString 39 | ); 40 | const data = await response.json(); 41 | const totalOrders = await response.headers.get('X-Total-Count'); 42 | resolve({ data: { orders: data, totalOrders: +totalOrders } }); 43 | }); 44 | } 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "space-craft", 3 | "version": "1.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "@headlessui/react": "^1.7.14", 7 | "@heroicons/react": "^2.0.17", 8 | "@reduxjs/toolkit": "^1.9.5", 9 | "@tailwindcss/aspect-ratio": "^0.4.2", 10 | "@tailwindcss/forms": "^0.5.3", 11 | "@testing-library/jest-dom": "^5.16.5", 12 | "@testing-library/react": "^13.4.0", 13 | "@testing-library/user-event": "^14.4.3", 14 | "html-webpack-plugin": "^5.5.3", 15 | "react": "^18.2.0", 16 | "react-alert": "^7.0.3", 17 | "react-alert-template-basic": "^1.0.2", 18 | "react-dom": "^18.2.0", 19 | "react-hook-form": "^7.43.9", 20 | "react-loader-spinner": "^5.3.4", 21 | "react-redux": "^8.1.0", 22 | "react-router-dom": "^6.10.0", 23 | "react-scripts": "5.0.1", 24 | "tailwindcss": "^3.3.2", 25 | "web-vitals": "^2.1.4" 26 | }, 27 | "scripts": { 28 | "start": "react-scripts start", 29 | "build": "react-scripts build", 30 | "test": "react-scripts test", 31 | "eject": "react-scripts eject" 32 | }, 33 | "eslintConfig": { 34 | "extends": [ 35 | "react-app", 36 | "react-app/jest" 37 | ] 38 | }, 39 | "browserslist": { 40 | "production": [ 41 | ">0.2%", 42 | "not dead", 43 | "not op_mini all" 44 | ], 45 | "development": [ 46 | "last 1 chrome version", 47 | "last 1 firefox version", 48 | "last 1 safari version" 49 | ] 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/pages/OrderSuccessPage.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { Link, Navigate, useParams } from "react-router-dom"; 3 | import { resetCartAsync } from "../features/cart/cartSlice"; 4 | import { useDispatch, useSelector } from "react-redux"; 5 | import { selectLoggedInUser } from "../features/auth/authSlice"; 6 | import { resetOrder } from "../features/order/orderSlice"; 7 | 8 | function OrderSuccessPage() { 9 | const params = useParams() 10 | const dispatch = useDispatch(); 11 | const user = useSelector(selectLoggedInUser); 12 | 13 | useEffect(()=>{ 14 | // reset cart 15 | dispatch(resetCartAsync(user.id)) 16 | // reset currentOrder 17 | dispatch(resetOrder()) 18 | },[dispatch,user]) 19 | 20 | return ( 21 | <> 22 | {!params.id && } 23 |
24 |
25 |

Order Successfully Placed

26 |

27 | Order Number #{params?.id} 28 |

29 |

30 | You can check your order in My Account > My Orders 31 |

32 |
33 | 37 | Go back home 38 | 39 |
40 |
41 |
42 | 43 | ); 44 | } 45 | 46 | export default OrderSuccessPage; 47 | -------------------------------------------------------------------------------- /src/features/cart/CartAPI.js: -------------------------------------------------------------------------------- 1 | export function addToCart(item) { 2 | return new Promise(async (resolve) => { 3 | const response = await fetch('http://localhost:8080/cart', { 4 | method: 'POST', 5 | body: JSON.stringify(item), 6 | headers: { 'content-type': 'application/json' }, 7 | }); 8 | const data = await response.json(); 9 | // TODO: on server it will only return some info of user (not password) 10 | resolve({ data }); 11 | }); 12 | } 13 | 14 | export function fetchItemsByUserId(userId) { 15 | return new Promise(async (resolve) => { 16 | //TODO: we will not hard-code server URL here 17 | const response = await fetch('http://localhost:8080/cart?user=' + userId); 18 | const data = await response.json(); 19 | resolve({ data }); 20 | }); 21 | } 22 | 23 | export function updateCart(update) { 24 | return new Promise(async (resolve) => { 25 | const response = await fetch('http://localhost:8080/cart/' + update.id, { 26 | method: 'PATCH', 27 | body: JSON.stringify(update), 28 | headers: { 'content-type': 'application/json' }, 29 | }); 30 | const data = await response.json(); 31 | // TODO: on server it will only return some info of user (not password) 32 | resolve({ data }); 33 | }); 34 | } 35 | 36 | export function deleteItemFromCart(itemId) { 37 | return new Promise(async (resolve) => { 38 | const response = await fetch('http://localhost:8080/cart/' + itemId, { 39 | method: 'DELETE', 40 | headers: { 'content-type': 'application/json' }, 41 | }); 42 | const data = await response.json(); 43 | // TODO: on server it will only return some info of user (not password) 44 | resolve({ data: { id: itemId } }); 45 | }); 46 | } 47 | 48 | export function resetCart(userId) { 49 | // get all items of user's cart - and then delete each 50 | return new Promise(async (resolve) => { 51 | const response = await fetchItemsByUserId(userId); 52 | const items = response.data; 53 | for (let item of items) { 54 | await deleteItemFromCart(item.id); 55 | } 56 | resolve({ status: 'success' }); 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /src/features/common/Footer.js: -------------------------------------------------------------------------------- 1 | function Footer() { 2 | return ( 3 | <> 4 |
5 |
6 |
7 |

Download Our Space Craft App

8 |

Buy What You Want.

9 |
10 |
11 | 15 |
16 |

Download On

17 |

Google Play Store

18 |
19 |
20 |
21 | 25 |
26 |

Download On

27 |

Apple Store

28 |
29 |
30 |
31 |
32 |
33 |

34 | {' '} 35 | © Space Craft , 2023{' '} 36 |

37 |
38 | About us 39 | Contact us 40 | Privacy Policy 41 |
42 |
43 |
44 |
45 | 46 | ); 47 | } 48 | 49 | export default Footer; 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App and Redux 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app), using the [Redux](https://redux.js.org/) and [Redux Toolkit](https://redux-toolkit.js.org/) template. 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser. 13 | 14 | The page will reload when you make changes.\ 15 | You may also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can't go back!** 35 | 36 | If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. 39 | 40 | You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | -------------------------------------------------------------------------------- /src/features/user/userSlice.js: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; 2 | import { 3 | fetchLoggedInUserOrders, 4 | updateUser, 5 | fetchLoggedInUser, 6 | } from './userAPI'; 7 | 8 | const initialState = { 9 | userOrders: [], 10 | status: 'idle', 11 | userInfo: null, // this info will be used in case of detailed user info, while auth will 12 | // only be used for loggedInUser id etc checks 13 | }; 14 | 15 | export const fetchLoggedInUserOrderAsync = createAsyncThunk( 16 | 'user/fetchLoggedInUserOrders', 17 | async (id) => { 18 | const response = await fetchLoggedInUserOrders(id); 19 | // The value we return becomes the `fulfilled` action payload 20 | return response.data; 21 | } 22 | ); 23 | 24 | export const fetchLoggedInUserAsync = createAsyncThunk( 25 | 'user/fetchLoggedInUser', 26 | async (id) => { 27 | const response = await fetchLoggedInUser(id); 28 | // The value we return becomes the `fulfilled` action payload 29 | return response.data; 30 | } 31 | ); 32 | 33 | export const updateUserAsync = createAsyncThunk( 34 | 'user/updateUser', 35 | async (id) => { 36 | const response = await updateUser(id); 37 | // The value we return becomes the `fulfilled` action payload 38 | return response.data; 39 | } 40 | ); 41 | 42 | export const userSlice = createSlice({ 43 | name: 'user', 44 | initialState, 45 | reducers: { 46 | 47 | }, 48 | extraReducers: (builder) => { 49 | builder 50 | .addCase(fetchLoggedInUserOrderAsync.pending, (state) => { 51 | state.status = 'loading'; 52 | }) 53 | .addCase(fetchLoggedInUserOrderAsync.fulfilled, (state, action) => { 54 | state.status = 'idle'; 55 | state.userOrders = action.payload; 56 | }) 57 | .addCase(updateUserAsync.pending, (state) => { 58 | state.status = 'loading'; 59 | }) 60 | .addCase(updateUserAsync.fulfilled, (state, action) => { 61 | state.status = 'idle'; 62 | state.userOrders = action.payload; 63 | }) 64 | .addCase(fetchLoggedInUserAsync.pending, (state) => { 65 | state.status = 'loading'; 66 | }) 67 | .addCase(fetchLoggedInUserAsync.fulfilled, (state, action) => { 68 | state.status = 'idle'; 69 | // this info can be different or more from logged-in User info 70 | state.userInfo = action.payload; 71 | }); 72 | }, 73 | }); 74 | 75 | export const selectUserOrders = (state) => state.user.userOrders; 76 | export const selectUserInfo = (state) => state.user.userInfo; 77 | 78 | // export const { increment } = userSlice.actions; 79 | 80 | export default userSlice.reducer; 81 | -------------------------------------------------------------------------------- /src/features/order/orderSlice.js: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; 2 | import { createOrder, fetchAllOrders,updateOrder } from './orderAPI'; 3 | 4 | const initialState = { 5 | orders: [], 6 | status: 'idle', 7 | currentOrder: null, 8 | totalOrders: 0 9 | }; 10 | //we may need more info of current order 11 | 12 | export const createOrderAsync = createAsyncThunk( 13 | 'order/createOrder', 14 | async (order) => { 15 | const response = await createOrder(order); 16 | // The value we return becomes the `fulfilled` action payload 17 | return response.data; 18 | } 19 | ); 20 | export const updateOrderAsync = createAsyncThunk( 21 | 'order/updateOrder', 22 | async (order) => { 23 | const response = await updateOrder(order); 24 | // The value we return becomes the `fulfilled` action payload 25 | return response.data; 26 | } 27 | ); 28 | 29 | export const fetchAllOrdersAsync = createAsyncThunk( 30 | 'order/fetchAllOrders', 31 | async ({sort, pagination}) => { 32 | const response = await fetchAllOrders(sort,pagination); 33 | // The value we return becomes the `fulfilled` action payload 34 | return response.data; 35 | } 36 | ); 37 | 38 | export const orderSlice = createSlice({ 39 | name: 'order', 40 | initialState, 41 | reducers: { 42 | resetOrder: (state) => { 43 | state.currentOrder = null; 44 | }, 45 | }, 46 | extraReducers: (builder) => { 47 | builder 48 | .addCase(createOrderAsync.pending, (state) => { 49 | state.status = 'loading'; 50 | }) 51 | .addCase(createOrderAsync.fulfilled, (state, action) => { 52 | state.status = 'idle'; 53 | state.orders.push(action.payload); 54 | state.currentOrder = action.payload; 55 | }) 56 | .addCase(fetchAllOrdersAsync.pending, (state) => { 57 | state.status = 'loading'; 58 | }) 59 | .addCase(fetchAllOrdersAsync.fulfilled, (state, action) => { 60 | state.status = 'idle'; 61 | state.orders = action.payload.orders; 62 | state.totalOrders = action.payload.totalOrders; 63 | }) 64 | .addCase(updateOrderAsync.pending, (state) => { 65 | state.status = 'loading'; 66 | }) 67 | .addCase(updateOrderAsync.fulfilled, (state, action) => { 68 | state.status = 'idle'; 69 | const index = state.orders.findIndex(order=>order.id===action.payload.id) 70 | state.orders[index] = action.payload; 71 | }) 72 | }, 73 | }); 74 | 75 | export const { resetOrder } = orderSlice.actions; 76 | 77 | export const selectCurrentOrder = (state) => state.order.currentOrder; 78 | export const selectOrders = (state) => state.order.orders; 79 | export const selectTotalOrders = (state) => state.order.totalOrders; 80 | 81 | export default orderSlice.reducer; 82 | -------------------------------------------------------------------------------- /src/features/auth/authSlice.js: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; 2 | import { checkUser, createUser, signOut } from './authAPI'; 3 | import { updateUser } from '../user/userAPI'; 4 | 5 | const initialState = { 6 | loggedInUser: null, 7 | status: 'idle', 8 | error: null, 9 | }; 10 | 11 | export const createUserAsync = createAsyncThunk( 12 | 'user/createUser', 13 | async (userData) => { 14 | const response = await createUser(userData); 15 | // The value we return becomes the `fulfilled` action payload 16 | return response.data; 17 | } 18 | ); 19 | 20 | export const updateUserAsync = createAsyncThunk( 21 | 'user/updateUser', 22 | async (update) => { 23 | const response = await updateUser(update); 24 | // The value we return becomes the `fulfilled` action payload 25 | return response.data; 26 | } 27 | ); 28 | 29 | export const checkUserAsync = createAsyncThunk( 30 | 'user/checkUser', 31 | async (loginInfo) => { 32 | const response = await checkUser(loginInfo); 33 | // The value we return becomes the `fulfilled` action payload 34 | return response.data; 35 | } 36 | ); 37 | 38 | export const signOutAsync = createAsyncThunk( 39 | 'user/signOut', 40 | async (loginInfo) => { 41 | const response = await signOut(loginInfo); 42 | // The value we return becomes the `fulfilled` action payload 43 | return response.data; 44 | } 45 | ); 46 | 47 | export const authSlice = createSlice({ 48 | name: 'user', 49 | initialState, 50 | reducers: { 51 | }, 52 | extraReducers: (builder) => { 53 | builder 54 | .addCase(createUserAsync.pending, (state) => { 55 | state.status = 'loading'; 56 | }) 57 | .addCase(createUserAsync.fulfilled, (state, action) => { 58 | state.status = 'idle'; 59 | state.loggedInUser = action.payload; 60 | }) 61 | .addCase(checkUserAsync.pending, (state) => { 62 | state.status = 'loading'; 63 | }) 64 | .addCase(checkUserAsync.fulfilled, (state, action) => { 65 | state.status = 'idle'; 66 | state.loggedInUser = action.payload; 67 | }) 68 | .addCase(checkUserAsync.rejected, (state, action) => { 69 | state.status = 'idle'; 70 | state.error = action.error; 71 | }) 72 | .addCase(updateUserAsync.pending, (state) => { 73 | state.status = 'loading'; 74 | }) 75 | .addCase(updateUserAsync.fulfilled, (state, action) => { 76 | state.status = 'idle'; 77 | state.loggedInUser = action.payload; 78 | }) 79 | .addCase(signOutAsync.pending, (state) => { 80 | state.status = 'loading'; 81 | }) 82 | .addCase(signOutAsync.fulfilled, (state, action) => { 83 | state.status = 'idle'; 84 | state.loggedInUser = null; 85 | }); 86 | }, 87 | }); 88 | 89 | export const selectLoggedInUser = (state) => state.auth.loggedInUser; 90 | export const selectError = (state) => state.auth.error; 91 | 92 | // export const { } = authSlice.actions; 93 | 94 | export default authSlice.reducer; 95 | -------------------------------------------------------------------------------- /src/features/auth/components/ForgotPassword.js: -------------------------------------------------------------------------------- 1 | import { Link } from 'react-router-dom'; 2 | import { useForm } from 'react-hook-form'; 3 | 4 | export default function ForgotPassword() { 5 | const { 6 | register, 7 | handleSubmit, 8 | formState: { errors }, 9 | } = useForm(); 10 | 11 | console.log(errors); 12 | 13 | return ( 14 | <> 15 |
16 |
17 | Your Company 22 |

23 | Enter email to reset password 24 |

25 |
26 | 27 |
28 |
{ 31 | console.log(data); 32 | // TODO : implementation on backend with email 33 | })} 34 | className="space-y-6" 35 | > 36 |
37 | 43 |
44 | 56 | {errors.email && ( 57 |

{errors.email.message}

58 | )} 59 |
60 |
61 | 62 |
63 | 69 |
70 |
71 | 72 |

73 | Send me back to{' '} 74 | 78 | Login 79 | 80 |

81 |
82 |
83 | 84 | ); 85 | } 86 | -------------------------------------------------------------------------------- /src/features/product/ProductAPI.js: -------------------------------------------------------------------------------- 1 | export function fetchAllProducts() { 2 | return new Promise(async (resolve) => { 3 | //TODO: we will not hard-code server URL here 4 | const response = await fetch('http://localhost:8080/products'); 5 | 6 | const data = await response.json(); 7 | resolve({ data }); 8 | }); 9 | } 10 | 11 | export function fetchProductById(id) { 12 | return new Promise(async (resolve) => { 13 | //TODO: we will not hard-code server URL here 14 | const response = await fetch('http://localhost:8080/products/' + id); 15 | const data = await response.json(); 16 | resolve({ data }); 17 | }); 18 | } 19 | 20 | export function createProduct(product) { 21 | return new Promise(async (resolve) => { 22 | const response = await fetch('http://localhost:8080/products/', { 23 | method: 'POST', 24 | body: JSON.stringify(product), 25 | headers: { 'content-type': 'application/json' }, 26 | }); 27 | const data = await response.json(); 28 | resolve({ data }); 29 | }); 30 | } 31 | 32 | export function updateProduct(update) { 33 | return new Promise(async (resolve) => { 34 | const response = await fetch( 35 | 'http://localhost:8080/products/' + update.id, 36 | { 37 | method: 'PATCH', 38 | body: JSON.stringify(update), 39 | headers: { 'content-type': 'application/json' }, 40 | } 41 | ); 42 | const data = await response.json(); 43 | // TODO: on server it will only return some info of user (not password) 44 | resolve({ data }); 45 | }); 46 | } 47 | 48 | export function fetchProductsByFilters(filter, sort, pagination) { 49 | // filter = {"category":["smartphone","laptops"]} 50 | // sort = {_sort:"price",_order="desc"} 51 | // pagination = {_page:1,_limit=10} 52 | // TODO : on server we will support multi values in filter 53 | // TODO : Server will filter deleted products in case of non-admin 54 | 55 | let queryString = ''; 56 | for (let key in filter) { 57 | const categoryValues = filter[key]; 58 | if (categoryValues.length) { 59 | const lastCategoryValue = categoryValues[categoryValues.length - 1]; 60 | queryString += `${key}=${lastCategoryValue}&`; 61 | } 62 | } 63 | for (let key in sort) { 64 | queryString += `${key}=${sort[key]}&`; 65 | } 66 | console.log(pagination); 67 | for (let key in pagination) { 68 | queryString += `${key}=${pagination[key]}&`; 69 | } 70 | 71 | return new Promise(async (resolve) => { 72 | //TODO: we will not hard-code server URL here 73 | const response = await fetch( 74 | 'http://localhost:8080/products?' + queryString 75 | ); 76 | const data = await response.json(); 77 | const totalItems = await response.headers.get('X-Total-Count'); 78 | resolve({ data: { products: data, totalItems: +totalItems } }); 79 | }); 80 | } 81 | 82 | export function fetchCategories() { 83 | return new Promise(async (resolve) => { 84 | const response = await fetch('http://localhost:8080/categories'); 85 | const data = await response.json(); 86 | resolve({ data }); 87 | }); 88 | } 89 | 90 | export function fetchBrands() { 91 | return new Promise(async (resolve) => { 92 | const response = await fetch('http://localhost:8080/brands'); 93 | const data = await response.json(); 94 | resolve({ data }); 95 | }); 96 | } 97 | -------------------------------------------------------------------------------- /src/features/cart/CartSlice.js: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; 2 | import { addToCart, deleteItemFromCart, fetchItemsByUserId, resetCart, updateCart } from './cartAPI'; 3 | 4 | const initialState = { 5 | status: 'idle', 6 | items: [], 7 | }; 8 | 9 | export const addToCartAsync = createAsyncThunk( 10 | 'cart/addToCart', 11 | async (item) => { 12 | const response = await addToCart(item); 13 | // The value we return becomes the `fulfilled` action payload 14 | return response.data; 15 | } 16 | ); 17 | 18 | export const fetchItemsByUserIdAsync = createAsyncThunk( 19 | 'cart/fetchItemsByUserId', 20 | async (userId) => { 21 | const response = await fetchItemsByUserId(userId); 22 | // The value we return becomes the `fulfilled` action payload 23 | return response.data; 24 | } 25 | ); 26 | 27 | export const updateCartAsync = createAsyncThunk( 28 | 'cart/updateCart', 29 | async (update) => { 30 | const response = await updateCart(update); 31 | // The value we return becomes the `fulfilled` action payload 32 | return response.data; 33 | } 34 | ); 35 | 36 | export const deleteItemFromCartAsync = createAsyncThunk( 37 | 'cart/deleteItemFromCart', 38 | async (itemId) => { 39 | const response = await deleteItemFromCart(itemId); 40 | // The value we return becomes the `fulfilled` action payload 41 | return response.data; 42 | } 43 | ); 44 | 45 | export const resetCartAsync = createAsyncThunk( 46 | 'cart/resetCart', 47 | async (userId) => { 48 | const response = await resetCart(userId); 49 | // The value we return becomes the `fulfilled` action payload 50 | return response.data; 51 | } 52 | ); 53 | 54 | export const cartSlice = createSlice({ 55 | name: 'cart', 56 | initialState, 57 | reducers: { 58 | }, 59 | extraReducers: (builder) => { 60 | builder 61 | .addCase(addToCartAsync.pending, (state) => { 62 | state.status = 'loading'; 63 | }) 64 | .addCase(addToCartAsync.fulfilled, (state, action) => { 65 | state.status = 'idle'; 66 | state.items.push(action.payload); 67 | }) 68 | .addCase(fetchItemsByUserIdAsync.pending, (state) => { 69 | state.status = 'loading'; 70 | }) 71 | .addCase(fetchItemsByUserIdAsync.fulfilled, (state, action) => { 72 | state.status = 'idle'; 73 | state.items = action.payload; 74 | }) 75 | .addCase(updateCartAsync.pending, (state) => { 76 | state.status = 'loading'; 77 | }) 78 | .addCase(updateCartAsync.fulfilled, (state, action) => { 79 | state.status = 'idle'; 80 | const index = state.items.findIndex(item=>item.id===action.payload.id) 81 | state.items[index] = action.payload; 82 | }) 83 | .addCase(deleteItemFromCartAsync.pending, (state) => { 84 | state.status = 'loading'; 85 | }) 86 | .addCase(deleteItemFromCartAsync.fulfilled, (state, action) => { 87 | state.status = 'idle'; 88 | const index = state.items.findIndex(item=>item.id===action.payload.id) 89 | state.items.splice(index,1); 90 | }) 91 | .addCase(resetCartAsync.pending, (state) => { 92 | state.status = 'loading'; 93 | }) 94 | .addCase(resetCartAsync.fulfilled, (state, action) => { 95 | state.status = 'idle'; 96 | state.items = []; 97 | }) 98 | }, 99 | }); 100 | 101 | // export const { increment } = cartSlice.actions; 102 | 103 | export const selectItems = (state) => state.cart.items; 104 | export const selectCartStatus = (state) => state.cart.status; 105 | 106 | export default cartSlice.reducer; 107 | -------------------------------------------------------------------------------- /src/features/common/Pagination.js: -------------------------------------------------------------------------------- 1 | import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/24/outline"; 2 | import { ITEMS_PER_PAGE } from "../../app/constants"; 3 | 4 | export default function Pagination({ page, setPage, handlePage, totalItems }) { 5 | const totalPages = Math.ceil(totalItems / ITEMS_PER_PAGE); 6 | return ( 7 |
8 |
9 |
handlePage(page > 1 ? page - 1 : page)} 11 | className="relative inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50" 12 | > 13 | Previous 14 |
15 |
handlePage(page < totalPages ? page + 1 : page)} 17 | className="relative ml-3 inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50" 18 | > 19 | Next 20 |
21 |
22 |
23 |
24 |

25 | Showing{' '} 26 | 27 | {(page - 1) * ITEMS_PER_PAGE + 1} 28 | {' '} 29 | to{' '} 30 | 31 | {page * ITEMS_PER_PAGE > totalItems 32 | ? totalItems 33 | : page * ITEMS_PER_PAGE} 34 | {' '} 35 | of {totalItems} results 36 |

37 |
38 |
39 | 74 |
75 |
76 |
77 | ); 78 | } -------------------------------------------------------------------------------- /src/features/common/Modal.js: -------------------------------------------------------------------------------- 1 | import { Fragment, useEffect, useRef, useState } from 'react'; 2 | import { Dialog, Transition } from '@headlessui/react'; 3 | import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'; 4 | 5 | export default function Modal({title,message,dangerOption,cancelOption, dangerAction, cancelAction, showModal }) { 6 | const [open, setOpen] = useState(false); 7 | 8 | const cancelButtonRef = useRef(null); 9 | 10 | const handleDanger = ()=>{ 11 | setOpen(false) 12 | dangerAction() 13 | } 14 | 15 | const handleCancel = ()=>{ 16 | setOpen(false) 17 | cancelAction() 18 | } 19 | 20 | useEffect(()=>{ 21 | if(showModal){ 22 | setOpen(true) 23 | } else{ 24 | setOpen(false) 25 | } 26 | },[showModal]) 27 | 28 | return ( 29 | 30 | 36 | 45 |
46 | 47 | 48 |
49 |
50 | 59 | 60 |
61 |
62 |
63 |
68 |
69 | 73 | {title} 74 | 75 |
76 |

77 | {message} 78 |

79 |
80 |
81 |
82 |
83 |
84 | 91 | 99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 | ); 107 | } 108 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import { Counter } from './features/counter/Counter'; 2 | import './App.css'; 3 | import Home from './pages/Home'; 4 | import LoginPage from './pages/LoginPage'; 5 | import SignupPage from './pages/SignupPage'; 6 | 7 | import { createBrowserRouter, Link, RouterProvider } from 'react-router-dom'; 8 | import CartPage from './pages/CartPage'; 9 | import Checkout from './pages/Checkout'; 10 | import ProductDetailPage from './pages/ProductDetailPage'; 11 | import Protected from './features/auth/components/Protected'; 12 | import { useEffect } from 'react'; 13 | // import { useDispatch, useSelector } from 'react-redux'; 14 | import { useDispatch, useSelector } from 'react-redux'; 15 | import { selectLoggedInUser } from './features/auth/authSlice'; 16 | import { fetchItemsByUserIdAsync } from './features/cart/cartSlice'; 17 | import PageNotFound from './pages/404'; 18 | import OrderSuccessPage from './pages/OrderSuccessPage'; 19 | import UserOrdersPage from './pages/UserOrdersPage'; 20 | import UserProfilePage from './pages/UserProfilePage'; 21 | import { fetchLoggedInUserAsync } from './features/user/userSlice'; 22 | import Logout from './features/auth/components/Logout'; 23 | import ForgotPasswordPage from './pages/ForgotPasswordPage'; 24 | import ProtectedAdmin from './features/auth/components/ProtectedAdmin'; 25 | import AdminHome from './pages/AdminHome'; 26 | import AdminProductDetailPage from './pages/AdminProductDetailPage'; 27 | import AdminProductFormPage from './pages/AdminProductFormPage'; 28 | import AdminOrdersPage from './pages/AdminOrdersPage'; 29 | import { positions, Provider } from 'react-alert'; 30 | import AlertTemplate from 'react-alert-template-basic'; 31 | 32 | const options = { 33 | timeout: 5000, 34 | position: positions.BOTTOM_LEFT, 35 | }; 36 | 37 | const router = createBrowserRouter([ 38 | { 39 | path: '/', 40 | element: ( 41 | 42 | 43 | 44 | ), 45 | }, 46 | { 47 | path: '/admin', 48 | element: ( 49 | 50 | 51 | 52 | ), 53 | }, 54 | { 55 | path: '/login', 56 | element: , 57 | }, 58 | { 59 | path: '/signup', 60 | element: , 61 | }, 62 | { 63 | path: '/cart', 64 | element: ( 65 | 66 | 67 | 68 | ), 69 | }, 70 | { 71 | path: '/checkout', 72 | element: ( 73 | 74 | 75 | 76 | ), 77 | }, 78 | { 79 | path: '/product-detail/:id', 80 | element: ( 81 | 82 | 83 | 84 | ), 85 | }, 86 | { 87 | path: '/admin/product-detail/:id', 88 | element: ( 89 | 90 | 91 | 92 | ), 93 | }, 94 | { 95 | path: '/admin/product-form', 96 | element: ( 97 | 98 | 99 | 100 | ), 101 | }, 102 | { 103 | path: '/admin/orders', 104 | element: ( 105 | 106 | 107 | 108 | ), 109 | }, 110 | { 111 | path: '/admin/product-form/edit/:id', 112 | element: ( 113 | 114 | 115 | 116 | ), 117 | }, 118 | { 119 | path: '/order-success/:id', 120 | element: , 121 | }, 122 | { 123 | path: '/orders', 124 | element: , 125 | }, 126 | { 127 | path: '/profile', 128 | element: , 129 | }, 130 | { 131 | path: '/logout', 132 | element: , 133 | }, 134 | { 135 | path: '/forgot-password', 136 | element: , 137 | }, 138 | { 139 | path: '*', 140 | element: , 141 | }, 142 | ]); 143 | 144 | function App() { 145 | const dispatch = useDispatch(); 146 | const user = useSelector(selectLoggedInUser); 147 | 148 | useEffect(() => { 149 | if (user) { 150 | dispatch(fetchItemsByUserIdAsync(user.id)); 151 | dispatch(fetchLoggedInUserAsync(user.id)); 152 | } 153 | }, [dispatch, user]); 154 | 155 | return ( 156 | <> 157 |
158 | 159 | 160 | 161 | {/* Link must be inside the Provider */} 162 |
163 | 164 | ); 165 | } 166 | 167 | export default App; 168 | -------------------------------------------------------------------------------- /src/features/auth/components/Login.js: -------------------------------------------------------------------------------- 1 | import { useSelector, useDispatch } from 'react-redux'; 2 | import { selectError, selectLoggedInUser } from '../authSlice'; 3 | import { Link, Navigate } from 'react-router-dom'; 4 | import { checkUserAsync } from '../authSlice'; 5 | import { useForm } from 'react-hook-form'; 6 | 7 | export default function Login() { 8 | const dispatch = useDispatch(); 9 | const error = useSelector(selectError); 10 | const user = useSelector(selectLoggedInUser); 11 | const { 12 | register, 13 | handleSubmit, 14 | formState: { errors }, 15 | } = useForm(); 16 | 17 | console.log(errors); 18 | 19 | return ( 20 | <> 21 | {user && } 22 |
23 |
24 | Space Craft 29 |

30 | Log In To Your Account 31 |

32 |
33 | 34 |
35 |
{ 38 | dispatch( 39 | checkUserAsync({ email: data.email, password: data.password }) 40 | ); 41 | })} 42 | className="space-y-6" 43 | > 44 |
45 | 51 |
52 | 64 | {errors.email && ( 65 |

{errors.email.message}

66 | )} 67 |
68 |
69 | 70 |
71 |
72 | 78 |
79 | 83 | Forgot Password? 84 | 85 |
86 |
87 |
88 | 96 | {errors.password && ( 97 |

{errors.password.message}

98 | )} 99 |
100 | {error &&

{error.message}

} 101 |
102 | 103 |
104 | 110 |
111 |
112 | 113 |

114 | Not A Member ?{' '} 115 | 119 | Create An Account 120 | 121 |

122 |
123 |
124 | 125 | ); 126 | } 127 | -------------------------------------------------------------------------------- /src/features/user/components/UserOrders.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { useSelector, useDispatch } from 'react-redux'; 3 | import { 4 | fetchLoggedInUserOrderAsync, 5 | selectUserInfo, 6 | selectUserOrders, 7 | } from '../userSlice'; 8 | import { discountedPrice } from '../../../app/constants'; 9 | 10 | export default function UserOrders() { 11 | const dispatch = useDispatch(); 12 | const user = useSelector(selectUserInfo); 13 | const orders = useSelector(selectUserOrders); 14 | 15 | useEffect(() => { 16 | dispatch(fetchLoggedInUserOrderAsync(user.id)); 17 | }, [dispatch, user]); 18 | 19 | return ( 20 |
21 | {orders.map((order) => ( 22 |
23 |
24 |
25 |
26 |

27 | Order # {order.id} 28 |

29 |

30 | Order Status : {order.status} 31 |

32 |
33 |
    34 | {order.items.map((item) => ( 35 |
  • 36 |
    37 | {item.title} 42 |
    43 | 44 |
    45 |
    46 |
    47 |

    48 | {item.title} 49 |

    50 |

    ${discountedPrice(item)}

    51 |
    52 |

    53 | {item.brand} 54 |

    55 |
    56 |
    57 |
    58 | 64 |
    65 | 66 |
    67 |
    68 |
    69 |
  • 70 | ))} 71 |
72 |
73 |
74 | 75 |
76 |
77 |

Subtotal

78 |

$ {order.totalAmount}

79 |
80 |
81 |

Total Items In Cart

82 |

{order.totalItems} items

83 |
84 |

85 | Shipping Address : 86 |

87 |
88 |
89 |
90 |

91 | {order.selectedAddress.name} 92 |

93 |

94 | {order.selectedAddress.street} 95 |

96 |

97 | {order.selectedAddress.pinCode} 98 |

99 |
100 |
101 |
102 |

103 | Phone: {order.selectedAddress.phone} 104 |

105 |

106 | {order.selectedAddress.city} 107 |

108 |
109 |
110 |
111 |
112 |
113 |
114 | ))} 115 |
116 | ); 117 | } 118 | -------------------------------------------------------------------------------- /src/features/product/ProductSlice.js: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; 2 | import { 3 | fetchAllProducts, 4 | fetchProductsByFilters, 5 | fetchBrands, 6 | fetchCategories, 7 | fetchProductById, 8 | createProduct, 9 | updateProduct, 10 | } from './productAPI'; 11 | 12 | const initialState = { 13 | products: [], 14 | brands: [], 15 | categories: [], 16 | status: 'idle', 17 | totalItems: 0, 18 | selectedProduct: null, 19 | }; 20 | 21 | export const fetchAllProductsAsync = createAsyncThunk( 22 | 'product/fetchAllProducts', 23 | async () => { 24 | const response = await fetchAllProducts(); 25 | // The value we return becomes the `fulfilled` action payload 26 | return response.data; 27 | } 28 | ); 29 | 30 | export const fetchProductByIdAsync = createAsyncThunk( 31 | 'product/fetchProductById', 32 | async (id) => { 33 | const response = await fetchProductById(id); 34 | // The value we return becomes the `fulfilled` action payload 35 | return response.data; 36 | } 37 | ); 38 | 39 | export const fetchProductsByFiltersAsync = createAsyncThunk( 40 | 'product/fetchProductsByFilters', 41 | async ({ filter, sort, pagination }) => { 42 | const response = await fetchProductsByFilters(filter, sort, pagination); 43 | // The value we return becomes the `fulfilled` action payload 44 | return response.data; 45 | } 46 | ); 47 | 48 | export const fetchBrandsAsync = createAsyncThunk( 49 | 'product/fetchBrands', 50 | async () => { 51 | const response = await fetchBrands(); 52 | // The value we return becomes the `fulfilled` action payload 53 | return response.data; 54 | } 55 | ); 56 | export const fetchCategoriesAsync = createAsyncThunk( 57 | 'product/fetchCategories', 58 | async () => { 59 | const response = await fetchCategories(); 60 | // The value we return becomes the `fulfilled` action payload 61 | return response.data; 62 | } 63 | ); 64 | 65 | export const createProductAsync = createAsyncThunk( 66 | 'product/create', 67 | async (product) => { 68 | const response = await createProduct(product); 69 | return response.data; 70 | } 71 | ); 72 | 73 | export const updateProductAsync = createAsyncThunk( 74 | 'product/update', 75 | async (update) => { 76 | const response = await updateProduct(update); 77 | return response.data; 78 | } 79 | ); 80 | 81 | export const productSlice = createSlice({ 82 | name: 'product', 83 | initialState, 84 | reducers: { 85 | clearSelectedProduct:(state)=>{ 86 | state.selectedProduct = null 87 | } 88 | }, 89 | extraReducers: (builder) => { 90 | builder 91 | .addCase(fetchAllProductsAsync.pending, (state) => { 92 | state.status = 'loading'; 93 | }) 94 | .addCase(fetchAllProductsAsync.fulfilled, (state, action) => { 95 | state.status = 'idle'; 96 | state.products = action.payload; 97 | }) 98 | .addCase(fetchProductsByFiltersAsync.pending, (state) => { 99 | state.status = 'loading'; 100 | }) 101 | .addCase(fetchProductsByFiltersAsync.fulfilled, (state, action) => { 102 | state.status = 'idle'; 103 | state.products = action.payload.products; 104 | state.totalItems = action.payload.totalItems; 105 | }) 106 | .addCase(fetchBrandsAsync.pending, (state) => { 107 | state.status = 'loading'; 108 | }) 109 | .addCase(fetchBrandsAsync.fulfilled, (state, action) => { 110 | state.status = 'idle'; 111 | state.brands = action.payload; 112 | }) 113 | .addCase(fetchCategoriesAsync.pending, (state) => { 114 | state.status = 'loading'; 115 | }) 116 | .addCase(fetchCategoriesAsync.fulfilled, (state, action) => { 117 | state.status = 'idle'; 118 | state.categories = action.payload; 119 | }) 120 | .addCase(fetchProductByIdAsync.pending, (state) => { 121 | state.status = 'loading'; 122 | }) 123 | .addCase(fetchProductByIdAsync.fulfilled, (state, action) => { 124 | state.status = 'idle'; 125 | state.selectedProduct = action.payload; 126 | }) 127 | .addCase(createProductAsync.pending, (state) => { 128 | state.status = 'loading'; 129 | }) 130 | .addCase(createProductAsync.fulfilled, (state, action) => { 131 | state.status = 'idle'; 132 | state.products.push(action.payload); 133 | }) 134 | .addCase(updateProductAsync.pending, (state) => { 135 | state.status = 'loading'; 136 | }) 137 | .addCase(updateProductAsync.fulfilled, (state, action) => { 138 | state.status = 'idle'; 139 | const index = state.products.findIndex( 140 | (product) => product.id === action.payload.id 141 | ); 142 | state.products[index] = action.payload; 143 | state.selectedProduct = action.payload; 144 | 145 | }); 146 | }, 147 | }); 148 | 149 | export const { clearSelectedProduct } = productSlice.actions; 150 | 151 | export const selectAllProducts = (state) => state.product.products; 152 | export const selectBrands = (state) => state.product.brands; 153 | export const selectCategories = (state) => state.product.categories; 154 | export const selectProductById = (state) => state.product.selectedProduct; 155 | export const selectProductListStatus = (state) => state.product.status; 156 | 157 | export const selectTotalItems = (state) => state.product.totalItems; 158 | 159 | export default productSlice.reducer; 160 | -------------------------------------------------------------------------------- /src/features/auth/components/Signup.js: -------------------------------------------------------------------------------- 1 | import { useSelector, useDispatch } from 'react-redux'; 2 | import { useForm } from 'react-hook-form'; 3 | 4 | import { selectLoggedInUser, createUserAsync } from '../authSlice'; 5 | import { Link } from 'react-router-dom'; 6 | import { Navigate } from 'react-router-dom'; 7 | 8 | export default function Signup() { 9 | const dispatch = useDispatch(); 10 | const user = useSelector(selectLoggedInUser); 11 | 12 | const { 13 | register, 14 | handleSubmit, 15 | formState: { errors }, 16 | } = useForm(); 17 | 18 | console.log(errors); 19 | 20 | return ( 21 | <> 22 | {user && } 23 |
24 |
25 | Space Craft 30 |

31 | Create A New Account 32 |

33 |
34 | 35 |
36 |
{ 40 | dispatch( 41 | createUserAsync({ 42 | email: data.email, 43 | password: data.password, 44 | addresses: [], 45 | role:'user' 46 | //TODO: this role can be directly given on backend 47 | }) 48 | ); 49 | console.log(data); 50 | })} 51 | > 52 |
53 | 59 |
60 | 72 | {errors.email && ( 73 |

{errors.email.message}

74 | )} 75 |
76 |
77 | 78 |
79 |
80 | 86 |
87 |
88 | 103 | {errors.password && ( 104 |

{errors.password.message}

105 | )} 106 |
107 |
108 | 109 |
110 |
111 | 117 |
118 |
119 | 124 | value === formValues.password || 'password not matching', 125 | })} 126 | type="password" 127 | className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" 128 | /> 129 | {errors.confirmPassword && ( 130 |

131 | {errors.confirmPassword.message} 132 |

133 | )} 134 |
135 |
136 | 137 |
138 | 144 |
145 |
146 | 147 |

148 | Already A Member ?{' '} 149 | 153 | Log In 154 | 155 |

156 |
157 |
158 | 159 | ); 160 | } 161 | -------------------------------------------------------------------------------- /src/features/cart/Cart.js: -------------------------------------------------------------------------------- 1 | import { Fragment, useState } from 'react'; 2 | import { useSelector, useDispatch } from 'react-redux'; 3 | import { 4 | deleteItemFromCartAsync, 5 | selectCartStatus, 6 | selectItems, 7 | updateCartAsync, 8 | } from './cartSlice'; 9 | import { Link } from 'react-router-dom'; 10 | import { Navigate } from 'react-router-dom'; 11 | import { discountedPrice } from '../../app/constants'; 12 | import { Grid } from 'react-loader-spinner'; 13 | import Modal from '../common/Modal'; 14 | 15 | export default function Cart() { 16 | const dispatch = useDispatch(); 17 | 18 | const items = useSelector(selectItems); 19 | const status = useSelector(selectCartStatus); 20 | const [openModal, setOpenModal] = useState(null); 21 | 22 | const totalAmount = items.reduce( 23 | (amount, item) => discountedPrice(item) * item.quantity + amount, 24 | 0 25 | ); 26 | const totalItems = items.reduce((total, item) => item.quantity + total, 0); 27 | 28 | const handleQuantity = (e, item) => { 29 | dispatch(updateCartAsync({ ...item, quantity: +e.target.value })); 30 | }; 31 | 32 | const handleRemove = (e, id) => { 33 | dispatch(deleteItemFromCartAsync(id)); 34 | }; 35 | 36 | return ( 37 | <> 38 | {!items.length && } 39 | 40 |
41 |
42 |
43 |

44 | Cart 45 |

46 |
47 | {status === 'loading' ? ( 48 | 58 | ) : null} 59 |
    60 | {items.map((item) => ( 61 |
  • 62 |
    63 | {item.title} 68 |
    69 | 70 |
    71 |
    72 |
    73 |

    74 | {item.title} 75 |

    76 |

    ${discountedPrice(item)}

    77 |
    78 |

    79 | {item.brand} 80 |

    81 |
    82 |
    83 |
    84 | 90 | 100 |
    101 | 102 |
    103 | handleRemove(e, item.id)} 109 | cancelAction={()=>setOpenModal(null)} 110 | showModal={openModal === item.id} 111 | > 112 | 119 |
    120 |
    121 |
    122 |
  • 123 | ))} 124 |
125 |
126 |
127 | 128 |
129 |
130 |

Subtotal

131 |

$ {totalAmount}

132 |
133 |
134 |

Total Items In Cart

135 |

{totalItems} items

136 |
137 |

138 | Shipping And Taxes Are Calculated At Checkout. 139 |

140 |
141 | 145 | Checkout 146 | 147 |
148 |
149 |

150 | Or {'\u00A0'} 151 | 152 | 159 | 160 |

161 |
162 |
163 |
164 |
165 | 166 | ); 167 | } 168 | -------------------------------------------------------------------------------- /src/features/admin/components/AdminOrders.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { ITEMS_PER_PAGE, discountedPrice } from '../../../app/constants'; 3 | import { useDispatch, useSelector } from 'react-redux'; 4 | import { 5 | fetchAllOrdersAsync, 6 | selectOrders, 7 | selectTotalOrders, 8 | updateOrderAsync, 9 | } from '../../order/orderSlice'; 10 | import { 11 | PencilIcon, 12 | EyeIcon, 13 | ArrowUpIcon, 14 | ArrowDownIcon, 15 | } from '@heroicons/react/24/outline'; 16 | import Pagination from '../../common/Pagination'; 17 | 18 | function AdminOrders() { 19 | const [page, setPage] = useState(1); 20 | const dispatch = useDispatch(); 21 | const orders = useSelector(selectOrders); 22 | const totalOrders = useSelector(selectTotalOrders); 23 | const [editableOrderId, setEditableOrderId] = useState(-1); 24 | const [sort, setSort] = useState({}); 25 | 26 | const handleEdit = (order) => { 27 | setEditableOrderId(order.id); 28 | }; 29 | const handleShow = () => { 30 | console.log('handleShow'); 31 | }; 32 | 33 | const handleUpdate = (e, order) => { 34 | const updatedOrder = { ...order, status: e.target.value }; 35 | dispatch(updateOrderAsync(updatedOrder)); 36 | setEditableOrderId(-1); 37 | }; 38 | 39 | const handlePage = (page) => { 40 | setPage(page); 41 | }; 42 | 43 | const handleSort = (sortOption) => { 44 | const sort = { _sort: sortOption.sort, _order: sortOption.order }; 45 | console.log({ sort }); 46 | setSort(sort); 47 | }; 48 | 49 | const chooseColor = (status) => { 50 | switch (status) { 51 | case 'pending': 52 | return 'bg-purple-200 text-purple-600'; 53 | case 'dispatched': 54 | return 'bg-yellow-200 text-yellow-600'; 55 | case 'delivered': 56 | return 'bg-green-200 text-green-600'; 57 | case 'cancelled': 58 | return 'bg-red-200 text-red-600'; 59 | default: 60 | return 'bg-purple-200 text-purple-600'; 61 | } 62 | }; 63 | 64 | useEffect(() => { 65 | const pagination = { _page: page, _limit: ITEMS_PER_PAGE }; 66 | dispatch(fetchAllOrdersAsync({ sort, pagination })); 67 | }, [dispatch, page, sort]); 68 | 69 | return ( 70 |
71 |
72 |
73 |
74 | 75 | 76 | 77 | 94 | 95 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | {orders.map((order) => ( 119 | 120 | 126 | 142 | 147 | 159 | 177 | 193 | 194 | ))} 195 | 196 |
80 | handleSort({ 81 | sort: 'id', 82 | order: sort?._order === 'asc' ? 'desc' : 'asc', 83 | }) 84 | } 85 | > 86 | Order# {' '} 87 | {sort._sort === 'id' && 88 | (sort._order === 'asc' ? ( 89 | 90 | ) : ( 91 | 92 | ))} 93 | Items 98 | handleSort({ 99 | sort: 'totalAmount', 100 | order: sort?._order === 'asc' ? 'desc' : 'asc', 101 | }) 102 | } 103 | > 104 | Total Amount {' '} 105 | {sort._sort === 'totalAmount' && 106 | (sort._order === 'asc' ? ( 107 | 108 | ) : ( 109 | 110 | ))} 111 | Shipping AddressStatusActions
121 |
122 |
123 | {order.id} 124 |
125 |
127 | {order.items.map((item) => ( 128 |
129 |
130 | 134 |
135 | 136 | {item.title} - #{item.quantity} - $ 137 | {discountedPrice(item)} 138 | 139 |
140 | ))} 141 |
143 |
144 | ${order.totalAmount} 145 |
146 |
148 |
149 |
150 | {order.selectedAddress.name}, 151 |
152 |
{order.selectedAddress.street},
153 |
{order.selectedAddress.city},
154 |
{order.selectedAddress.state},
155 |
{order.selectedAddress.pinCode},
156 |
{order.selectedAddress.phone},
157 |
158 |
160 | {order.id === editableOrderId ? ( 161 | 167 | ) : ( 168 | 173 | {order.status} 174 | 175 | )} 176 | 178 |
179 |
180 | handleShow(order)} 183 | > 184 |
185 |
186 | handleEdit(order)} 189 | > 190 |
191 |
192 |
197 |
198 |
199 |
200 | 206 |
207 | ); 208 | } 209 | 210 | export default AdminOrders; -------------------------------------------------------------------------------- /src/features/navbar/Navbar.js: -------------------------------------------------------------------------------- 1 | import { Fragment } from 'react'; 2 | import { Disclosure, Menu, Transition } from '@headlessui/react'; 3 | import { 4 | Bars3Icon, 5 | ShoppingCartIcon, 6 | XMarkIcon, 7 | } from '@heroicons/react/24/outline'; 8 | import { Link } from 'react-router-dom'; 9 | import { useSelector } from 'react-redux'; 10 | import { selectItems } from '../cart/cartSlice'; 11 | import { selectLoggedInUser } from '../auth/authSlice'; 12 | 13 | 14 | const navigation = [ 15 | { name: 'Products', link: '/', user: true }, 16 | { name: 'Join As A Seller', link: '/seller', user: true }, 17 | { name: 'Products', link: '/admin', admin: true }, 18 | { name: 'Orders', link: '/admin/orders', admin: true }, 19 | ]; 20 | 21 | const userNavigation = [ 22 | { name: 'My Profile', link: '/profile' }, 23 | { name: 'My Orders', link: '/orders' }, 24 | { name: 'Sign out', link: '/logout' }, 25 | ]; 26 | 27 | function classNames(...classes) { 28 | return classes.filter(Boolean).join(' '); 29 | } 30 | 31 | function NavBar({ children }) { 32 | const items = useSelector(selectItems); 33 | const user = useSelector(selectLoggedInUser); 34 | 35 | return ( 36 | <> 37 |
38 | 39 | {({ open }) => ( 40 | <> 41 |
42 |
43 |
44 |
45 | 46 | Space Craft 51 | 52 |
53 |
54 |
55 | {navigation.map((item) => 56 | item[user.role] ? ( 57 | 68 | {item.name} 69 | 70 | ) : null 71 | )} 72 |
73 |
74 |
75 |
76 |
77 | 78 | 88 | 89 | {items.length > 0 && ( 90 | 91 | {items.length} 92 | 93 | )} 94 | 95 | {/* Profile dropdown */} 96 | 97 |
98 | 99 | Open User Menu 100 | 105 | 106 |
107 | 116 | 117 | {userNavigation.map((item) => ( 118 | 119 | {({ active }) => ( 120 | 127 | {item.name} 128 | 129 | )} 130 | 131 | ))} 132 | 133 | 134 |
135 |
136 |
137 |
138 | {/* Mobile menu button */} 139 | 140 | Open Main Menu 141 | {open ? ( 142 | 153 |
154 |
155 |
156 | 157 | 158 |
159 | {navigation.map((item) => ( 160 | 172 | {item.name} 173 | 174 | ))} 175 |
176 |
177 |
178 |
179 | 184 |
185 |
186 |
187 | {user.name} 188 |
189 |
190 | {user.email} 191 |
192 |
193 | 194 | 203 | 204 | {items.length > 0 && ( 205 | 206 | {items.length} 207 | 208 | )} 209 |
210 |
211 | {userNavigation.map((item) => ( 212 | 218 | {item.name} 219 | 220 | ))} 221 |
222 |
223 |
224 | 225 | )} 226 |
227 | 228 |
229 |
230 |

231 | Space Craft 232 |

233 |
234 |
235 |
236 |
237 | {children} 238 |
239 |
240 |
241 | 242 | ); 243 | } 244 | 245 | export default NavBar; 246 | -------------------------------------------------------------------------------- /src/features/admin/components/AdminProductDetail.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { StarIcon } from '@heroicons/react/20/solid'; 3 | import { RadioGroup } from '@headlessui/react'; 4 | import { useDispatch, useSelector } from 'react-redux'; 5 | import { fetchProductByIdAsync, selectProductById } from '../../product/productSlice'; 6 | import { useParams } from 'react-router-dom'; 7 | import { addToCartAsync } from '../../cart/cartSlice'; 8 | import { selectLoggedInUser } from '../../auth/authSlice'; 9 | import { discountedPrice } from '../../../app/constants'; 10 | 11 | // TODO: In server data we will add colors, sizes , highlights. to each product 12 | 13 | const colors = [ 14 | { name: 'White', class: 'bg-white', selectedClass: 'ring-gray-400' }, 15 | { name: 'Gray', class: 'bg-gray-200', selectedClass: 'ring-gray-400' }, 16 | { name: 'Black', class: 'bg-gray-900', selectedClass: 'ring-gray-900' }, 17 | ]; 18 | const sizes = [ 19 | { name: 'XXS', inStock: false }, 20 | { name: 'XS', inStock: true }, 21 | { name: 'S', inStock: true }, 22 | { name: 'M', inStock: true }, 23 | { name: 'L', inStock: true }, 24 | { name: 'XL', inStock: true }, 25 | { name: '2XL', inStock: true }, 26 | { name: '3XL', inStock: true }, 27 | ]; 28 | 29 | const highlights = [ 30 | 'Hand cut and sewn locally', 31 | 'Dyed with our proprietary colors', 32 | 'Pre-washed & pre-shrunk', 33 | 'Ultra-soft 100% cotton', 34 | ]; 35 | 36 | function classNames(...classes) { 37 | return classes.filter(Boolean).join(' '); 38 | } 39 | 40 | // TODO : Loading UI 41 | 42 | export default function AdminProductDetail() { 43 | const [selectedColor, setSelectedColor] = useState(colors[0]); 44 | const [selectedSize, setSelectedSize] = useState(sizes[2]); 45 | const user = useSelector(selectLoggedInUser); 46 | const product = useSelector(selectProductById); 47 | const dispatch = useDispatch(); 48 | const params = useParams(); 49 | 50 | const handleCart = (e) => { 51 | e.preventDefault(); 52 | const newItem = { ...product, quantity: 1, user: user.id }; 53 | delete newItem['id']; 54 | dispatch(addToCartAsync(newItem)); 55 | }; 56 | 57 | useEffect(() => { 58 | dispatch(fetchProductByIdAsync(params.id)); 59 | }, [dispatch, params.id]); 60 | 61 | return ( 62 |
63 | {product && ( 64 |
65 | 103 | 104 | {/* Image gallery */} 105 |
106 |
107 | {product.title} 112 |
113 |
114 |
115 | {product.title} 120 |
121 |
122 | {product.title} 127 |
128 |
129 |
130 | {product.title} 135 |
136 |
137 | 138 | {/* Product info */} 139 |
140 |
141 |

142 | {product.title} 143 |

144 |
145 | 146 | {/* Options */} 147 |
148 |

Product information

149 |

150 | ${product.price} 151 |

152 |

153 | ${discountedPrice(product)} 154 |

155 | 156 | {/* Reviews */} 157 |
158 |

Reviews

159 |
160 |
161 | {[0, 1, 2, 3, 4].map((rating) => ( 162 | rating 166 | ? 'text-gray-900' 167 | : 'text-gray-200', 168 | 'h-5 w-5 flex-shrink-0' 169 | )} 170 | aria-hidden="true" 171 | /> 172 | ))} 173 |
174 |

{product.rating} out of 5 stars

175 |
176 |
177 | 178 |
179 | {/* Colors */} 180 |
181 |

Color

182 | 183 | 188 | 189 | Choose a color 190 | 191 |
192 | {colors.map((color) => ( 193 | 197 | classNames( 198 | color.selectedClass, 199 | active && checked ? 'ring ring-offset-1' : '', 200 | !active && checked ? 'ring-2' : '', 201 | 'relative -m-0.5 flex cursor-pointer items-center justify-center rounded-full p-0.5 focus:outline-none' 202 | ) 203 | } 204 | > 205 | 206 | {color.name} 207 | 208 | 216 | ))} 217 |
218 |
219 |
220 | 221 | {/* Sizes */} 222 |
223 |
224 |

Size

225 | 229 | Size guide 230 | 231 |
232 | 233 | 238 | 239 | Choose a size 240 | 241 |
242 | {sizes.map((size) => ( 243 | 248 | classNames( 249 | size.inStock 250 | ? 'cursor-pointer bg-white text-gray-900 shadow-sm' 251 | : 'cursor-not-allowed bg-gray-50 text-gray-200', 252 | active ? 'ring-2 ring-indigo-500' : '', 253 | 'group relative flex items-center justify-center rounded-md border py-3 px-4 text-sm font-medium uppercase hover:bg-gray-50 focus:outline-none sm:flex-1 sm:py-6' 254 | ) 255 | } 256 | > 257 | {({ active, checked }) => ( 258 | <> 259 | 260 | {size.name} 261 | 262 | {size.inStock ? ( 263 | 297 | ))} 298 |
299 |
300 |
301 | 302 | 309 |
310 |
311 | 312 |
313 | {/* Description and details */} 314 |
315 |

Description

316 | 317 |
318 |

319 | {product.description} 320 |

321 |
322 |
323 | 324 |
325 |

326 | Highlights 327 |

328 | 329 |
330 |
    331 | {highlights.map((highlight) => ( 332 |
  • 333 | {highlight} 334 |
  • 335 | ))} 336 |
337 |
338 |
339 | 340 |
341 |

Details

342 | 343 |
344 |

{product.description}

345 |
346 |
347 |
348 |
349 |
350 | )} 351 |
352 | ); 353 | } 354 | -------------------------------------------------------------------------------- /src/features/product/components/ProductDetail.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { StarIcon } from '@heroicons/react/20/solid'; 3 | import { RadioGroup } from '@headlessui/react'; 4 | import { useDispatch, useSelector } from 'react-redux'; 5 | import { fetchProductByIdAsync, selectProductById, selectProductListStatus } from '../productSlice'; 6 | import { useParams } from 'react-router-dom'; 7 | import { addToCartAsync, selectItems } from '../../cart/cartSlice'; 8 | import { selectLoggedInUser } from '../../auth/authSlice'; 9 | import { discountedPrice } from '../../../app/constants'; 10 | import { useAlert } from 'react-alert'; 11 | import { Grid } from 'react-loader-spinner'; 12 | 13 | // TODO: In server data we will add colors, sizes , highlights. to each product 14 | 15 | const colors = [ 16 | { name: 'White', class: 'bg-white', selectedClass: 'ring-gray-400' }, 17 | { name: 'Gray', class: 'bg-gray-200', selectedClass: 'ring-gray-400' }, 18 | { name: 'Black', class: 'bg-gray-900', selectedClass: 'ring-gray-900' }, 19 | ]; 20 | const sizes = [ 21 | { name: 'XXS', inStock: false }, 22 | { name: 'XS', inStock: true }, 23 | { name: 'S', inStock: true }, 24 | { name: 'M', inStock: true }, 25 | { name: 'L', inStock: true }, 26 | { name: 'XL', inStock: true }, 27 | { name: '2XL', inStock: true }, 28 | { name: '3XL', inStock: true }, 29 | ]; 30 | 31 | const highlights = [ 32 | 'Hand cut and sewn locally', 33 | 'Dyed with our proprietary colors', 34 | 'Pre-washed & pre-shrunk', 35 | 'Ultra-soft 100% cotton', 36 | ]; 37 | 38 | function classNames(...classes) { 39 | return classes.filter(Boolean).join(' '); 40 | } 41 | 42 | // TODO : Loading UI 43 | 44 | export default function ProductDetail() { 45 | const [selectedColor, setSelectedColor] = useState(colors[0]); 46 | const [selectedSize, setSelectedSize] = useState(sizes[2]); 47 | const user = useSelector(selectLoggedInUser); 48 | const items = useSelector(selectItems); 49 | const product = useSelector(selectProductById); 50 | const dispatch = useDispatch(); 51 | const params = useParams(); 52 | const alert = useAlert(); 53 | const status = useSelector(selectProductListStatus); 54 | 55 | const handleCart = (e) => { 56 | e.preventDefault(); 57 | if (items.findIndex((item) => item.productId === product.id) < 0) { 58 | console.log({ items, product }); 59 | const newItem = { 60 | ...product, 61 | productId: product.id, 62 | quantity: 1, 63 | user: user.id, 64 | }; 65 | delete newItem['id']; 66 | dispatch(addToCartAsync(newItem)); 67 | // TODO: it will be based on server response of backend 68 | alert.error('Item added to Cart'); 69 | } else { 70 | alert.error('Item Already added'); 71 | } 72 | }; 73 | 74 | useEffect(() => { 75 | dispatch(fetchProductByIdAsync(params.id)); 76 | }, [dispatch, params.id]); 77 | 78 | return ( 79 |
80 | {status === 'loading' ? ( 81 | 91 | ) : null} 92 | {product && ( 93 |
94 | 130 | 131 | {/* Image gallery */} 132 |
133 |
134 | {product.title} 139 |
140 |
141 |
142 | {product.title} 147 |
148 |
149 | {product.title} 154 |
155 |
156 |
157 | {product.title} 162 |
163 |
164 | 165 | {/* Product info */} 166 |
167 |
168 |

169 | {product.title} 170 |

171 |
172 | 173 | {/* Options */} 174 |
175 |

Product information

176 |

177 | ${product.price} 178 |

179 |

180 | ${discountedPrice(product)} 181 |

182 | 183 | {/* Reviews */} 184 |
185 |

Reviews

186 |
187 |
188 | {[0, 1, 2, 3, 4].map((rating) => ( 189 | rating 193 | ? 'text-gray-900' 194 | : 'text-gray-200', 195 | 'h-5 w-5 flex-shrink-0' 196 | )} 197 | aria-hidden="true" 198 | /> 199 | ))} 200 |
201 |

{product.rating} out of 5 stars

202 |
203 |
204 | 205 |
206 | {/* Colors */} 207 |
208 |

Color

209 | 210 | 215 | 216 | Choose a color 217 | 218 |
219 | {colors.map((color) => ( 220 | 224 | classNames( 225 | color.selectedClass, 226 | active && checked ? 'ring ring-offset-1' : '', 227 | !active && checked ? 'ring-2' : '', 228 | 'relative -m-0.5 flex cursor-pointer items-center justify-center rounded-full p-0.5 focus:outline-none' 229 | ) 230 | } 231 | > 232 | 233 | {color.name} 234 | 235 | 243 | ))} 244 |
245 |
246 |
247 | 248 | {/* Sizes */} 249 |
250 |
251 |

Size

252 | 256 | Size guide 257 | 258 |
259 | 260 | 265 | 266 | Choose a size 267 | 268 |
269 | {sizes.map((size) => ( 270 | 275 | classNames( 276 | size.inStock 277 | ? 'cursor-pointer bg-white text-gray-900 shadow-sm' 278 | : 'cursor-not-allowed bg-gray-50 text-gray-200', 279 | active ? 'ring-2 ring-indigo-500' : '', 280 | 'group relative flex items-center justify-center rounded-md border py-3 px-4 text-sm font-medium uppercase hover:bg-gray-50 focus:outline-none sm:flex-1 sm:py-6' 281 | ) 282 | } 283 | > 284 | {({ active, checked }) => ( 285 | <> 286 | 287 | {size.name} 288 | 289 | {size.inStock ? ( 290 | 324 | ))} 325 |
326 |
327 |
328 | 329 | 336 |
337 |
338 | 339 |
340 | {/* Description and details */} 341 |
342 |

Description

343 | 344 |
345 |

346 | {product.description} 347 |

348 |
349 |
350 | 351 |
352 |

353 | Highlights 354 |

355 | 356 |
357 |
    358 | {highlights.map((highlight) => ( 359 |
  • 360 | {highlight} 361 |
  • 362 | ))} 363 |
364 |
365 |
366 | 367 |
368 |

Details

369 | 370 |
371 |

{product.description}

372 |
373 |
374 |
375 |
376 |
377 | )} 378 |
379 | ); 380 | } 381 | -------------------------------------------------------------------------------- /src/features/product/components/ProductList.js: -------------------------------------------------------------------------------- 1 | import React, { useState, Fragment, useEffect } from 'react'; 2 | import { useSelector, useDispatch } from 'react-redux'; 3 | import { 4 | fetchBrandsAsync, 5 | fetchCategoriesAsync, 6 | fetchProductsByFiltersAsync, 7 | selectAllProducts, 8 | selectBrands, 9 | selectCategories, 10 | selectProductListStatus, 11 | selectTotalItems, 12 | } from '../productSlice'; 13 | import { Dialog, Disclosure, Menu, Transition } from '@headlessui/react'; 14 | import { XMarkIcon } from '@heroicons/react/24/outline'; 15 | import { 16 | ChevronLeftIcon, 17 | ChevronRightIcon, 18 | StarIcon, 19 | } from '@heroicons/react/20/solid'; 20 | import { Link } from 'react-router-dom'; 21 | import { 22 | ChevronDownIcon, 23 | FunnelIcon, 24 | MinusIcon, 25 | PlusIcon, 26 | Squares2X2Icon, 27 | } from '@heroicons/react/20/solid'; 28 | import { ITEMS_PER_PAGE, discountedPrice } from '../../../app/constants'; 29 | import Pagination from '../../common/Pagination'; 30 | import { Grid } from 'react-loader-spinner'; 31 | 32 | const sortOptions = [ 33 | { name: 'Best Rating', sort: 'rating', order: 'desc', current: false }, 34 | { name: 'Price: Low to High', sort: 'price', order: 'asc', current: false }, 35 | { name: 'Price: High to Low', sort: 'price', order: 'desc', current: false }, 36 | ]; 37 | 38 | function classNames(...classes) { 39 | return classes.filter(Boolean).join(' '); 40 | } 41 | 42 | export default function ProductList() { 43 | const dispatch = useDispatch(); 44 | const products = useSelector(selectAllProducts); 45 | const brands = useSelector(selectBrands); 46 | const categories = useSelector(selectCategories); 47 | const totalItems = useSelector(selectTotalItems); 48 | const status = useSelector(selectProductListStatus); 49 | const filters = [ 50 | { 51 | id: 'category', 52 | name: 'Category', 53 | options: categories, 54 | }, 55 | { 56 | id: 'brand', 57 | name: 'Brands', 58 | options: brands, 59 | }, 60 | ]; 61 | 62 | const [filter, setFilter] = useState({}); 63 | const [sort, setSort] = useState({}); 64 | const [mobileFiltersOpen, setMobileFiltersOpen] = useState(false); 65 | const [page, setPage] = useState(1); 66 | 67 | const handleFilter = (e, section, option) => { 68 | console.log(e.target.checked); 69 | const newFilter = { ...filter }; 70 | // TODO : on server it will support multiple categories 71 | if (e.target.checked) { 72 | if (newFilter[section.id]) { 73 | newFilter[section.id].push(option.value); 74 | } else { 75 | newFilter[section.id] = [option.value]; 76 | } 77 | } else { 78 | const index = newFilter[section.id].findIndex( 79 | (el) => el === option.value 80 | ); 81 | newFilter[section.id].splice(index, 1); 82 | } 83 | console.log({ newFilter }); 84 | 85 | setFilter(newFilter); 86 | }; 87 | 88 | const handleSort = (e, option) => { 89 | const sort = { _sort: option.sort, _order: option.order }; 90 | console.log({ sort }); 91 | setSort(sort); 92 | }; 93 | 94 | const handlePage = (page) => { 95 | console.log({ page }); 96 | setPage(page); 97 | }; 98 | 99 | useEffect(() => { 100 | const pagination = { _page: page, _limit: ITEMS_PER_PAGE }; 101 | dispatch(fetchProductsByFiltersAsync({ filter, sort, pagination })); 102 | // TODO : Server will filter deleted products 103 | }, [dispatch, filter, sort, page]); 104 | 105 | useEffect(() => { 106 | setPage(1); 107 | }, [totalItems, sort]); 108 | 109 | useEffect(() => { 110 | dispatch(fetchBrandsAsync()); 111 | dispatch(fetchCategoriesAsync()); 112 | }, []); 113 | 114 | return ( 115 |
116 |
117 | 123 | 124 |
125 |
126 |

127 | All Products 128 |

129 | 130 |
131 | 132 |
133 | 134 | Sort 135 | 140 |
141 | 142 | 151 | 152 |
153 | {sortOptions.map((option) => ( 154 | 155 | {({ active }) => ( 156 |

handleSort(e, option)} 158 | className={classNames( 159 | option.current 160 | ? 'font-medium text-gray-900' 161 | : 'text-gray-500', 162 | active ? 'bg-gray-100' : '', 163 | 'block px-4 py-2 text-sm' 164 | )} 165 | > 166 | {option.name} 167 |

168 | )} 169 |
170 | ))} 171 |
172 |
173 |
174 |
175 | 176 | 183 | 191 |
192 |
193 | 194 |
195 |

196 | Products 197 |

198 | 199 |
200 | 204 | {/* Product grid */} 205 |
206 | 207 |
208 | {/* Product grid end */} 209 |
210 |
211 | 212 | {/* section of product and filters ends */} 213 | 219 |
220 |
221 |
222 | ); 223 | } 224 | 225 | function MobileFilter({ 226 | mobileFiltersOpen, 227 | setMobileFiltersOpen, 228 | handleFilter, 229 | filters, 230 | }) { 231 | return ( 232 | 233 | 238 | 247 |
248 | 249 | 250 |
251 | 260 | 261 |
262 |

Filters

263 | 271 |
272 | 273 | {/* Filters */} 274 |
275 | {filters.map((section) => ( 276 | 281 | {({ open }) => ( 282 | <> 283 |

284 | 285 | 286 | {section.name} 287 | 288 | 289 | {open ? ( 290 | 301 | 302 |

303 | 304 |
305 | {section.options.map((option, optionIdx) => ( 306 |
310 | 317 | handleFilter(e, section, option) 318 | } 319 | className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500" 320 | /> 321 | 327 |
328 | ))} 329 |
330 |
331 | 332 | )} 333 |
334 | ))} 335 |
336 |
337 |
338 |
339 |
340 |
341 | ); 342 | } 343 | 344 | function DesktopFilter({ handleFilter, filters }) { 345 | return ( 346 |
347 | {filters.map((section) => ( 348 | 353 | {({ open }) => ( 354 | <> 355 |

356 | 357 | 358 | {section.name} 359 | 360 | 361 | {open ? ( 362 | 367 | 368 |

369 | 370 |
371 | {section.options.map((option, optionIdx) => ( 372 |
373 | handleFilter(e, section, option)} 380 | className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500" 381 | /> 382 | 388 |
389 | ))} 390 |
391 |
392 | 393 | )} 394 |
395 | ))} 396 |
397 | ); 398 | } 399 | 400 | function ProductGrid({ products, status }) { 401 | return ( 402 |
403 |
404 |
405 | {status === 'loading' ? ( 406 | 416 | ) : null} 417 | {products.map((product) => ( 418 | 419 |
420 |
421 | {product.title} 426 |
427 |
428 |
429 |

430 |
431 |
434 |

435 |

436 | 437 | {product.rating} 438 |

439 |
440 |
441 |

442 | ${discountedPrice(product)} 443 |

444 |

445 | ${product.price} 446 |

447 |
448 |
449 | {product.deleted && ( 450 |
451 |

product deleted

452 |
453 | )} 454 | {product.stock <= 0 && ( 455 |
456 |

out of stock

457 |
458 | )} 459 | {/* TODO: will not be needed when backend is implemented */} 460 |
461 | 462 | ))} 463 |
464 |
465 |
466 | ); 467 | } 468 | -------------------------------------------------------------------------------- /src/features/admin/components/ProductForm.js: -------------------------------------------------------------------------------- 1 | import { useDispatch, useSelector } from 'react-redux'; 2 | import { 3 | clearSelectedProduct, 4 | createProductAsync, 5 | fetchProductByIdAsync, 6 | selectBrands, 7 | selectCategories, 8 | selectProductById, 9 | updateProductAsync, 10 | } from '../../product/productSlice'; 11 | import { useForm } from 'react-hook-form'; 12 | import { useParams } from 'react-router-dom'; 13 | import { useEffect, useState } from 'react'; 14 | import Modal from '../../common/Modal'; 15 | 16 | function ProductForm() { 17 | const { 18 | register, 19 | handleSubmit, 20 | setValue, 21 | reset, 22 | formState: { errors }, 23 | } = useForm(); 24 | const brands = useSelector(selectBrands); 25 | const categories = useSelector(selectCategories); 26 | const dispatch = useDispatch(); 27 | const params = useParams(); 28 | const selectedProduct = useSelector(selectProductById); 29 | const [openModal, setOpenModal] = useState(null); 30 | 31 | useEffect(() => { 32 | if (params.id) { 33 | dispatch(fetchProductByIdAsync(params.id)); 34 | } else { 35 | dispatch(clearSelectedProduct()); 36 | } 37 | }, [params.id, dispatch]); 38 | 39 | useEffect(() => { 40 | if (selectedProduct && params.id) { 41 | setValue('title', selectedProduct.title); 42 | setValue('description', selectedProduct.description); 43 | setValue('price', selectedProduct.price); 44 | setValue('discountPercentage', selectedProduct.discountPercentage); 45 | setValue('thumbnail', selectedProduct.thumbnail); 46 | setValue('stock', selectedProduct.stock); 47 | setValue('image1', selectedProduct.images[0]); 48 | setValue('image2', selectedProduct.images[1]); 49 | setValue('image3', selectedProduct.images[2]); 50 | setValue('brand', selectedProduct.brand); 51 | setValue('category', selectedProduct.category); 52 | } 53 | }, [selectedProduct, params.id, setValue]); 54 | 55 | const handleDelete = () => { 56 | const product = { ...selectedProduct }; 57 | product.deleted = true; 58 | dispatch(updateProductAsync(product)); 59 | }; 60 | 61 | return ( 62 | <> 63 |
{ 66 | console.log(data); 67 | const product = { ...data }; 68 | product.images = [ 69 | product.image1, 70 | product.image2, 71 | product.image3, 72 | product.thumbnail, 73 | ]; 74 | product.rating = 0; 75 | delete product['image1']; 76 | delete product['image2']; 77 | delete product['image3']; 78 | product.price = +product.price; 79 | product.stock = +product.stock; 80 | product.discountPercentage = +product.discountPercentage; 81 | console.log(product); 82 | 83 | if (params.id) { 84 | product.id = params.id; 85 | product.rating = selectedProduct.rating || 0; 86 | dispatch(updateProductAsync(product)); 87 | reset(); 88 | } else { 89 | dispatch(createProductAsync(product)); 90 | reset(); 91 | //TODO: on product successfully added clear fields and show a message 92 | } 93 | })} 94 | > 95 |
96 |
97 |

98 | Add Product 99 |

100 | 101 |
102 | {selectedProduct.deleted &&

This product is deleted

} 103 | 104 |
105 | 111 |
112 |
113 | 121 |
122 |
123 |
124 | 125 |
126 | 132 |
133 |