online shop app

This commit is contained in:
2023-01-06 16:30:17 +01:00
parent ea5924bf48
commit 018ee29abd
21 changed files with 30351 additions and 1 deletions
+24
View File
@@ -0,0 +1,24 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
node_modules
+36 -1
View File
@@ -1 +1,36 @@
# online-shop # Online Shop
![React](https://img.shields.io/badge/react-%2320232a.svg?style=for-the-badge&logo=react&logoColor=%2361DAFB)
![React Router](https://img.shields.io/badge/React_Router-CA4245?style=for-the-badge&logo=react-router&logoColor=white)
![Redux](https://img.shields.io/badge/redux-%23593d88.svg?style=for-the-badge&logo=redux&logoColor=white)
![SASS](https://img.shields.io/badge/SASS-hotpink.svg?style=for-the-badge&logo=SASS&logoColor=white)
---
## Description
---
The Online Shop is build with React. It has custom responsive layout styled with SCSS. It makes an API call to fetch mock products from [Dummy JSON API](https://dummyjson.com/products) with Redux [RTK Query](https://redux-toolkit.js.org/rtk-query/overview). Added also Toastify for notifications.
The application shows a product home page and a cart page, the routing is done with React Router.
## Installation
---
### Under the repository name, click on clone or download. In the terminal paste this command:
```git clone https://dadobos.github.io/online-shop.git```
### Open the project in VSCode or move inside the folder with:
```cd online-shop```
### Install the npm packages:
```npm install```
### Run the application with:
```npm start```
### In the browser open the "3000" port:
```http://localhost:3000/```
+29220
View File
File diff suppressed because it is too large Load Diff
+49
View File
@@ -0,0 +1,49 @@
{
"name": "online-shop",
"homepage": "https://dadobos.github.io/online-shop",
"version": "0.1.0",
"private": true,
"dependencies": {
"@reduxjs/toolkit": "^1.9.1",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-redux": "^8.0.5",
"react-router-dom": "^6.4.4",
"react-scripts": "^5.0.1",
"react-toastify": "^9.1.1",
"sass": "^1.56.1",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"predeploy": "npm run build",
"deploy": "gh-pages -d build"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"gh-pages": "^4.0.0"
}
}
+47
View File
@@ -0,0 +1,47 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link
rel="icon"
href="https://res.cloudinary.com/dadobos/image/upload/v1642766520/web/favicon_ugvc88.ico"
type="image/x-icon"
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#335c67" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Online Shop</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
+11
View File
@@ -0,0 +1,11 @@
{
"short_name": "React Online Shop",
"name": "React Online Shop",
"icons": [
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
+3
View File
@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
+32
View File
@@ -0,0 +1,32 @@
import React from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import './App.scss';
import Navbar from './components/Navbar';
import Footer from './components/Footer';
import Home from './components/Home';
import Cart from './components/Cart';
import NotFound from './components/NotFound';
function App() {
return (
<div className='App'>
<BrowserRouter>
<ToastContainer />
<Navbar />
<Routes>
<Route path='cart' exact element={<Cart />} />
<Route path='/' exact element={<Home />} />
<Route path='*' element={<NotFound />} />
</Routes>
<Footer/>
</BrowserRouter>
</div>
);
}
export default App;
+476
View File
@@ -0,0 +1,476 @@
@import url("https://fonts.googleapis.com/css2?family=Nanum+Gothic+Coding:wght@400;700&display=swap");
// Variables
// colors
$OuterSpaceCrayola: #22363c;
$DeepSpaceSparkle: #335c67;
$Cultured: #eceff1;
$PersianGreen: #009688;
$SolidPink: #92374d;
//size
$NavbarHeight: 64px;
$FooterHeight: 22px;
$xs: 21rem; // 336px;
$sm: 48rem; // 768px;
$md: 64rem; // 1024px;
$lg: 90rem; // 1440px;
$xl: 120rem; // 1920px;
$breakpoints: $xs, $sm, $md, $lg, $xl;
// Mixins
@mixin layout($breakpoint, $direction: min) {
@if $direction == min {
@media (min-width: $breakpoint) {
@content;
}
} @else {
@media (max-width: $breakpoint) {
@content;
}
}
// use the custom value if the breakpoint is not part of the pre-defined list
}
@mixin flex-center {
display: flex;
align-items: center;
justify-content: center;
}
@mixin hover-animation {
&:hover {
transform: scale(1.03);
}
}
// reset
* {
padding: 0;
margin: 0;
box-sizing: border-box;
font-family: "Nanum Gothic Coding", monospace;
}
a {
text-decoration: none;
color: $PersianGreen;
@include hover-animation();
}
// layout
.responsive-layout {
display: block;
margin-left: auto;
margin-right: auto;
padding: 1rem;
@include layout($xl, min) {
max-width: $lg;
}
@include layout($xl, max) {
max-width: $lg;
}
@include layout($lg, max) {
max-width: $md;
}
@include layout($md, max) {
max-width: $sm;
}
@include layout($sm, max) {
max-width: $xs;
}
}
// spinner
.spinner {
width: 50px;
height: 50px;
padding: 8px;
// aspect-ratio: 1;
border-radius: 50%;
background: $PersianGreen;
--_m: conic-gradient(#0000 10%, #000), linear-gradient(#000 0 0) content-box;
-webkit-mask: var(--_m);
mask: var(--_m);
-webkit-mask-composite: source-out;
mask-composite: subtract;
animation: spin 1s infinite linear;
}
@keyframes spin {
to {
transform: rotate(1turn);
}
}
// navbar
.navbar-background {
background-color: $OuterSpaceCrayola;
}
.navbar {
height: $NavbarHeight;
align-items: center;
@extend .responsive-layout;
.title {
@include hover-animation();
font-size: 2rem;
float: left;
color: $Cultured;
@include layout($md, max) {
font-size: 1.8rem;
}
@include layout($sm, max) {
font-size: 1.5rem;
}
}
.nav-bag {
float: right;
display: flex;
align-items: center;
color: $Cultured;
svg {
color: $Cultured;
width: 20px;
height: 20px;
}
.nav-quantity {
@include flex-center();
height: 25px;
width: 25px;
background-color: $PersianGreen;
margin-left: 5px;
border-radius: 50%;
font-size: 14px;
}
}
}
// footer
.footer {
@include flex-center();
height: $FooterHeight;
color: $Cultured;
background-color: $OuterSpaceCrayola;
}
// home
.home-container {
display: flex;
flex-wrap: wrap;
justify-content: space-evenly;
min-height: calc(100vh - $NavbarHeight - $FooterHeight - 2rem);
h2 {
width: 100%;
}
}
// card
.card-list {
margin: 1rem auto;
padding: 1rem;
border: 1px solid $DeepSpaceSparkle;
border-radius: 4px;
width: 250px;
max-width: 100%;
min-height: 400px;
display: flex;
flex-direction: column;
justify-content: space-between;
@include hover-animation();
button {
border: 1px solid $PersianGreen;
border-radius: 20px;
outline: none;
// background-color: $Cultured;
// background-color: $PersianGreen;
// color: $Cultured;
color: $PersianGreen;
font-size: 1.2rem;
font-weight: 700;
cursor: pointer;
height: 1.5rem;
margin-top: 0.5rem;
letter-spacing: 1.1px;
width: 100%;
}
img {
max-width: 100%;
max-height: 10rem;
min-height: 8rem;
margin-left: auto;
margin-right: auto;
}
.image-container {
text-align: center;
}
.title {
font-size: 1.2rem;
max-width: 100%;
}
.description {
font-size: 12px;
line-height: 1.5;
}
.category,
.price,
.discount,
.stock {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
font-size: 14px;
margin: 0.2rem 0;
}
.category {
span {
background-color: $OuterSpaceCrayola;
color: $Cultured;
font-size: 10px;
border-radius: 20px;
padding: 0.2rem 0.5rem;
// border-bottom: 1px solid #335c67;
}
}
}
// cart
.cart-container {
min-height: calc(100vh - $NavbarHeight - $FooterHeight - 2rem);
.titles {
margin: 2rem 0 1rem 0;
border-bottom: 1px solid $OuterSpaceCrayola;
.product-title {
padding-left: 0.5rem;
}
.total-title {
padding-right: 0.5rem;
justify-self: right;
}
}
.cart-item {
margin-top: 2rem;
border-bottom: 0.5px solid $OuterSpaceCrayola;
.cart-product {
display: flex;
img {
width: 100px;
max-width: 100%;
max-height: 100px;
margin-right: 1rem;
}
h3 {
font-weight: 400;
margin-bottom: .3rem;
}
button {
border: none;
outline: none;
cursor: pointer;
margin: 1rem;
background: none;
color: $SolidPink;
&:hover {
color: $PersianGreen;
}
}
}
.quantity {
display: flex;
align-items: flex-start;
justify-content: center;
width: 130px;
max-width: 100%;
border: 0.5px solid $OuterSpaceCrayola;
border-radius: 5px;
button {
border: none;
outline: none;
background: none;
padding: 0.7rem 1.5rem;
cursor: pointer;
// font-size: 2rem;
}
.count {
padding: 0.7rem 0;
}
}
.total-price {
justify-self: right;
font-weight: 700;
}
}
.cart-item,
.titles {
display: grid;
align-items: center;
grid-template-columns: 3fr 1fr 1fr 1fr;
column-gap: 0.5rem;
max-width: 100%;
}
.cart-summary {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding-top: 2rem;
.clear-cart {
@include hover-animation();
width: 130px;
max-width: 100%;
height: 40px;
border-radius: 5px;
font-weight: 400;
letter-spacing: 1.1px;
border: 0.5px solid $SolidPink;
color: $SolidPink;
background: none;
outline: none;
cursor: pointer;
font-size: 1.2rem;
}
.cart-checkout {
width: 270px;
max-width: 100%;
.subtotal {
display: flex;
justify-content: space-between;
font-size: 1.2rem;
font-weight: 700;
.amount {
font-weight: 700;
}
}
button {
@include hover-animation();
border: none;
border-radius: 5px;
outline: none;
background-color: $OuterSpaceCrayola;
color: $Cultured;
font-size: 1.2rem;
font-weight: 700;
cursor: pointer;
height: 1.5rem;
margin-top: 0.5rem;
letter-spacing: 1.1px;
width: 100%;
}
}
}
.continue-shopping {
@include flex-center();
}
.no-cart-items {
@include flex-center();
flex-direction: column;
min-height: calc(100vh - $NavbarHeight - $FooterHeight - 4rem);
p {
font-size: 1.4rem;
}
}
@include layout($sm, max) {
.titles {
display: none;
}
.cart-item {
display: flex;
flex-direction: column;
justify-content: space-between;
min-height: 20rem;
border: 0.5px solid $OuterSpaceCrayola;
border-radius: 4px;
padding: 1rem;
.cart-product {
flex-direction: column;
justify-content: space-between;
img {
margin-left: auto;
margin-right: auto;
width: auto;
max-height: 100%;
}
& > div > p {
margin-top: 1rem;
}
button{
font-size: 1rem;
margin-left: auto;
margin-right: auto;
}
}
.price{
margin-bottom: 1rem;
&::before{
content: "Price:";
}
}
.total-price{
margin-top: 1rem;
&::before{
content: "Total Price:";
}
}
}
.cart-summary {
flex-direction: column;
.clear-cart {
margin-bottom: 2rem;
align-self: center;
}
}
}
}
// not found
.not-found {
@include flex-center();
flex-direction: column;
color: $OuterSpaceCrayola;
min-height: calc(100vh - $NavbarHeight - $FooterHeight);
h2 {
height: 48px;
}
p {
font-size: 1.4rem;
}
}
// back to home button
.back-to-home {
font-size: 1.4rem;
margin-top: 3rem;
}
.go-to-cart {
font-size: 1rem;
}
+26
View File
@@ -0,0 +1,26 @@
import { Link } from 'react-router-dom';
const BackHomeButton = (props) => {
const { text } = props;
return (
<Link to='/' className='back-to-home'>
<span>
<svg
xmlns='http://www.w3.org/2000/svg'
width='16'
height='16'
fill='currentColor'
className='bi bi-arrow-left'
viewBox='0 0 16 16'>
<path
fillRule='evenodd'
d='M15 8a.5.5 0 0 0-.5-.5H2.707l3.147-3.146a.5.5 0 1 0-.708-.708l-4 4a.5.5 0 0 0 0 .708l4 4a.5.5 0 0 0 .708-.708L2.707 8.5H14.5A.5.5 0 0 0 15 8z'
/>
</svg>
</span>
<span> {text}</span>
</Link>
);
};
export default BackHomeButton;
+49
View File
@@ -0,0 +1,49 @@
import { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { addToCart, getTotals } from '../features/cartSlice';
import { Capitalize } from '../features/Utils';
const Card = ({ product }) => {
const dispatch = useDispatch();
const cart = useSelector((state) => state.cart);
useEffect(() => {
dispatch(getTotals());
}, [cart, dispatch]);
const handleAddToCart = (product) => {
dispatch(addToCart(product));
};
return (
<>
<h3 className='title'>{Capitalize(product.title)}</h3>
<div className='image-container'>
<img src={product.images[0]} alt={Capitalize(product.title)} />
</div>
<div className='category'>
<span>{Capitalize(product.category)}</span>
<span>{Capitalize(product.brand)}</span>
</div>
<p className='description'>{Capitalize(product.description)}</p>
<div>
<p className='discount'>
<span>Discount: </span>
<span>{product.discountPercentage}%</span>
</p>
<p className='price'>
<span>Price:</span>
<span>{product.price}</span>
</p>
<p className='stock'>
<span>Stoc:</span>
<span>{product.stock}</span>
</p>
</div>
<button onClick={() => handleAddToCart(product)}>Add to cart</button>
</>
);
};
export default Card;
+103
View File
@@ -0,0 +1,103 @@
import { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import {
addToCart,
decreaseCart,
removeFromCart,
clearCart,
getTotals,
} from '../features/cartSlice';
import { Capitalize } from '../features/Utils';
import BackHomeButton from './BackHomeButton';
const Cart = () => {
const cart = useSelector((state) => state.cart);
const dispatch = useDispatch();
useEffect(() => {
dispatch(getTotals());
}, [cart, dispatch]);
const handleRemoveFromCart = (cartItem) => {
dispatch(removeFromCart(cartItem));
};
const handleDecreaseCart = (cartItem) => {
dispatch(decreaseCart(cartItem));
};
const handleIncreaseCart = (cartItem) => {
dispatch(addToCart(cartItem));
};
const handleClearCart = () => {
dispatch(clearCart());
};
return (
<div className='responsive-layout'>
<div className='cart-container'>
<h2>Cart</h2>
{cart.cartItems.length === 0 ? (
<div className='no-cart-items'>
<p>Your cart is empty</p>
<BackHomeButton text={'Start Shopping'} />
</div>
) : (
<div>
<div className='titles'>
<h3 className='product-title'>Product</h3>
<h3>Price</h3>
<h3>Quantity</h3>
<h3 className='total-title'>Total</h3>
</div>
<>
{cart.cartItems?.map((cartItem) => (
<div className='cart-item' key={cartItem.id}>
<div className='cart-product'>
<img src={cartItem.images[0]} alt={cartItem.title} />
<div>
<h3>{Capitalize(cartItem.title)}</h3>
<p>{cartItem.description}</p>
<button onClick={() => handleRemoveFromCart(cartItem)}>
Remove
</button>
</div>
</div>
<div className='price'>{cartItem.price}</div>
<div className='quantity'>
<button onClick={() => handleDecreaseCart(cartItem)}>
-
</button>
<div className='count'>{cartItem.cartQuantity}</div>
<button onClick={() => handleIncreaseCart(cartItem)}>
+
</button>
</div>
<div className='total-price'>
{cartItem.price * cartItem.cartQuantity}
</div>
</div>
))}
</>
<div className='cart-summary'>
<button className='clear-cart' onClick={() => handleClearCart()}>
Clear Cart
</button>
<div className='cart-checkout'>
<div className='subtotal'>
<span>Subtotal</span>
<span className='amount'>{cart.cartTotalAmount}</span>
</div>
<button>Checkout</button>
</div>
</div>
<div className='continue-shopping'>
<BackHomeButton text={'Continue Shopping'} />
</div>
</div>
)}
</div>
</div>
);
};
export default Cart;
+9
View File
@@ -0,0 +1,9 @@
const Footer = () => {
const getCurrentYear = () => {
return new Date().getFullYear();
};
return <div className='footer'>Dan Dobos &copy; {getCurrentYear()}</div>;
};
export default Footer;
+30
View File
@@ -0,0 +1,30 @@
import { Link } from 'react-router-dom';
const GoToCartButton = (props) => {
const { text } = props;
return (
<div>
{text}
<br />
<Link to='/cart' className='go-to-cart'>
<span>Go To Cart </span>
<span>
<svg
xmlns='http://www.w3.org/2000/svg'
width='14'
height='8'
fill='currentColor'
className='bi bi-arrow-right'
viewBox='0 0 16 16'>
<path
fillRule='evenodd'
d='M1 8a.5.5 0 0 1 .5-.5h11.793l-3.147-3.146a.5.5 0 0 1 .708-.708l4 4a.5.5 0 0 1 0 .708l-4 4a.5.5 0 0 1-.708-.708L13.293 8.5H1.5A.5.5 0 0 1 1 8z'
/>
</svg>
</span>
</Link>
</div>
);
};
export default GoToCartButton;
+29
View File
@@ -0,0 +1,29 @@
import { useGetProductsQuery } from '../features/productsApi';
import Card from './Card';
const Home = () => {
const { data, error, isLoading } = useGetProductsQuery();
return (
<div className='responsive-layout'>
<section className='home-container'>
<h2>Products</h2>
{isLoading ? (
<p className='spinner'></p>
) : error ? (
<p>An error occoured: {error} </p>
) : (
<>
{data.products?.map((product) => (
<article className='card-list' key={product.id}>
<Card product={product} />
</article>
))}
</>
)}
</section>
</div>
);
};
export default Home;
+32
View File
@@ -0,0 +1,32 @@
import { Link } from 'react-router-dom';
import { useSelector } from 'react-redux';
const Navbar = () => {
const { cartTotalQuantity } = useSelector((state) => state.cart);
return (
<div className='navbar-background'>
<nav className='navbar'>
<Link to='/' className='title'>
React Online Shop
</Link>
<Link to='/cart'>
<div className='nav-bag'>
<svg
xmlns='http://www.w3.org/2000/svg'
fill='currentColor'
className='bi bi-bag'
viewBox='0 0 16 16'>
<path d='M8 1a2.5 2.5 0 0 1 2.5 2.5V4h-5v-.5A2.5 2.5 0 0 1 8 1zm3.5 3v-.5a3.5 3.5 0 1 0-7 0V4H1v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V4h-3.5zM2 5h12v9a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V5z' />
</svg>
<span className='nav-quantity'>
<span>{cartTotalQuantity}</span>
</span>
</div>
</Link>
</nav>
</div>
);
};
export default Navbar;
+13
View File
@@ -0,0 +1,13 @@
import BackHomeButton from './BackHomeButton'
const NotFound = () => {
return (
<div className='not-found'>
<h2>404</h2>
<p>Page not found</p>
<BackHomeButton text={"Home"}/>
</div>
);
};
export default NotFound;
+3
View File
@@ -0,0 +1,3 @@
export const Capitalize = (text) => {
return text.replace(/[_-]/g, " ").replace(/(^\w|\s\w)(\S*)/g, (_,m1,m2) => m1.toUpperCase()+m2.toLowerCase())
};
+116
View File
@@ -0,0 +1,116 @@
import { createSlice } from '@reduxjs/toolkit';
import { toast } from 'react-toastify';
import { Capitalize } from './Utils';
import GoToCartButton from '../components/GoToCartButton';
const initialState = {
cartItems: localStorage.getItem('cartItems')
? JSON.parse(localStorage.getItem('cartItems'))
: [],
cartTotalQuantity: 0,
cartTotalAmount: 0,
};
const toastifyOptions = {
position: 'bottom-left',
icon: false,
};
const cartSlice = createSlice({
name: 'cart',
initialState,
reducers: {
addToCart(state, action) {
const itemIndex = state.cartItems.findIndex(
(item) => item.id === action.payload.id
);
if (itemIndex >= 0) {
state.cartItems[itemIndex].cartQuantity += 1;
let multipleItemsText = `Increased ${Capitalize(
state.cartItems[itemIndex].title
)} quantity to ${state.cartItems[itemIndex].cartQuantity}`;
toast.info(
window.location.href.indexOf('cart') !== -1 ? (
multipleItemsText
) : (
<GoToCartButton text={multipleItemsText} />
),
toastifyOptions
);
} else {
const tempProduct = { ...action.payload, cartQuantity: 1 };
state.cartItems.push(tempProduct);
let firstItemText = `${Capitalize(action.payload.title)} added to cart`
toast.success(
<GoToCartButton text={firstItemText} />,
toastifyOptions
);
}
localStorage.setItem('cartItems', JSON.stringify(state.cartItems));
},
removeFromCart(state, action) {
const nextCartItems = state.cartItems.filter(
(cartItem) => cartItem.id !== action.payload.id
);
state.cartItems = nextCartItems;
localStorage.setItem('cartItems', JSON.stringify(state.cartItems));
toast.error(`${Capitalize(action.payload.title)} removed from cart`, toastifyOptions);
},
decreaseCart(state, action) {
const itemIndex = state.cartItems.findIndex(
(cartItem) => cartItem.id === action.payload.id
);
if (state.cartItems[itemIndex].cartQuantity > 1) {
state.cartItems[itemIndex].cartQuantity -= 1;
toast.info(
`Decreased ${Capitalize(action.payload.title)} cart quantity`,
toastifyOptions
);
} else if (state.cartItems[itemIndex].cartQuantity === 1) {
const nextCartItems = state.cartItems.filter(
(cartItem) => cartItem.id !== action.payload.id
);
state.cartItems = nextCartItems;
toast.error(
`${Capitalize(action.payload.title)} removed from cart`,
toastifyOptions
);
}
localStorage.setItem('cartItems', JSON.stringify(state.cartItems));
},
clearCart(state, action) {
state.cartItems = [];
toast.error(`Cart cleared`, toastifyOptions);
localStorage.setItem('cartItems', JSON.stringify(state.cartItems));
},
getTotals(state, action) {
let { total, quantity } = state.cartItems.reduce(
(cartTotal, cartItem) => {
const { price, cartQuantity } = cartItem;
const itemTotal = price * cartQuantity;
cartTotal.total += itemTotal;
cartTotal.quantity += cartQuantity;
return cartTotal;
},
{
total: 0,
quantity: 0,
}
);
state.cartTotalQuantity = quantity;
state.cartTotalAmount = total;
},
},
});
export const { addToCart, removeFromCart, decreaseCart, clearCart, getTotals } =
cartSlice.actions;
export default cartSlice.reducer;
+13
View File
@@ -0,0 +1,13 @@
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
export const productsApi = createApi({
reducerPath: 'productsApi',
baseQuery: fetchBaseQuery({ baseUrl: 'https://dummyjson.com/' }),
endpoints: (builder) => ({
getProducts: builder.query({
query: () => 'products',
}),
}),
});
export const { useGetProductsQuery } = productsApi;
+30
View File
@@ -0,0 +1,30 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { configureStore } from '@reduxjs/toolkit';
import { setupListeners } from '@reduxjs/toolkit/query';
import { Provider } from 'react-redux';
import { productsApi } from './features/productsApi';
import cartReducer from './features/cartSlice';
export const store = configureStore({
reducer: {
cart: cartReducer,
[productsApi.reducerPath]: productsApi.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(productsApi.middleware),
});
setupListeners(store.dispatch);
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);