Compare commits

3 Commits
Author SHA1 Message Date
Dan Dobos 8c464eadcf Updates 2025-10-21 12:08:19 +02:00
Dan Dobos c9f3031c17 Updates 2025-10-21 09:29:59 +02:00
Dan Dobos 885b1d9744 Updates 2025-10-21 09:11:20 +02:00
27 changed files with 81 additions and 19366 deletions
-47
View File
@@ -1,47 +0,0 @@
# React 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.
## Quick overview
- Browse products retrieved from a mock API
- View product details and add items to a cart
- Update item quantities or remove items from the cart
- Toast notifications for user actions
- Responsive layout using SCSS
## Tech stack
- React (functional components + hooks)
- Redux Toolkit + RTK Query for async data
- React Router for navigation
- SCSS for styling
- react-toastify (notifications)
## Getting started (local)
1. Clone the repository :
```git clone https://dadobos.github.io/react-shop.git```
2. Go into the project folder:
```cd react-shop```
3. Install dependencies:
```npm install```
4. Start the dev server:
```npm start```
5. Open the app in your browser:
```http://localhost:3000/```
## Common scripts
- npm start — start dev server (hot reload)
- npm run build — create a production build
- npm test — run tests (if present)
## Notes for development
- API calls are handled with RTK Query; service files are located in src (look for a services or api directory).
- Styles are in SCSS — components can import their module styles or use shared partials.
- Routing lives under src (check App or routes-related files) — add pages or adjust routes there.
- For mock API changes, update the base URL or endpoints in the RTK Query service file.
+13
View File
@@ -0,0 +1,13 @@
{
"files": {
"main.css": "/react-shop/static/css/main.00152e63.css",
"main.js": "/react-shop/static/js/main.9f3d8fbe.js",
"index.html": "/react-shop/index.html",
"main.00152e63.css.map": "/react-shop/static/css/main.00152e63.css.map",
"main.9f3d8fbe.js.map": "/react-shop/static/js/main.9f3d8fbe.js.map"
},
"entrypoints": [
"static/css/main.00152e63.css",
"static/js/main.9f3d8fbe.js"
]
}
+1
View File
@@ -0,0 +1 @@
<!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"/><link rel="manifest" href="/react-shop/manifest.json"/><title>Online Shop</title><script defer="defer" src="/react-shop/static/js/main.9f3d8fbe.js"></script><link href="/react-shop/static/css/main.00152e63.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
-18257
View File
File diff suppressed because it is too large Load Diff
-46
View File
@@ -1,46 +0,0 @@
{
"name": "react-shop",
"homepage": "https://dadobos.github.io/react-shop/",
"version": "0.1.0",
"private": true,
"dependencies": {
"@reduxjs/toolkit": "^2.9.0",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"gh-pages": "^6.3.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-redux": "^9.2.0",
"react-router-dom": "^7.9.4",
"react-scripts": "^5.0.1",
"react-toastify": "^11.0.5",
"sass": "^1.93.2"
},
"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"
]
}
}
-47
View File
@@ -1,47 +0,0 @@
<!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>
View File
-32
View File
@@ -1,32 +0,0 @@
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 basename={process.env.PUBLIC_URL}>
<ToastContainer />
<Navbar />
<Routes>
<Route path='cart' element={<Cart />} />
<Route path='/' element={<Home />} />
<Route path='*' element={<NotFound />} />
</Routes>
<Footer />
</BrowserRouter>
</div>
);
}
export default App;
-476
View File
@@ -1,476 +0,0 @@
@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
@@ -1,26 +0,0 @@
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;
-11
View File
@@ -1,11 +0,0 @@
const Capitalize = ({ children, text })=> {
// ensure we always work with a string
const value = (children ?? "").toString();
if (!value) return null;
const capitalized = value.replace(/\b\w/g, (c) => c.toUpperCase());
return <>{capitalized}</>;
}
export default Capitalize
-49
View File
@@ -1,49 +0,0 @@
import { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { addToCart, getTotals } from '../features/cartSlice';
import Capitalize from './Capitalize';
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
@@ -1,103 +0,0 @@
import { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import {
addToCart,
decreaseCart,
removeFromCart,
clearCart,
getTotals,
} from '../features/cartSlice';
import Capitalize from './Capitalize';
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
@@ -1,9 +0,0 @@
const Footer = () => {
const getCurrentYear = () => {
return new Date().getFullYear();
};
return <div className='footer'>Dan Dobos &copy; {getCurrentYear()}</div>;
};
export default Footer;
-30
View File
@@ -1,30 +0,0 @@
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
@@ -1,29 +0,0 @@
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
@@ -1,32 +0,0 @@
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 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
@@ -1,13 +0,0 @@
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;
-116
View File
@@ -1,116 +0,0 @@
import { createSlice } from '@reduxjs/toolkit';
import { toast } from 'react-toastify';
import Capitalize from '../components/Capitalize';
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
@@ -1,13 +0,0 @@
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
@@ -1,30 +0,0 @@
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>
);
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+60
View File
@@ -0,0 +1,60 @@
/**
* @license React
* react-dom.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react-jsx-runtime.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* scheduler.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* use-sync-external-store-with-selector.production.js
*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* react-router v7.9.4
*
* Copyright (c) Remix Software Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE.md file in the root directory of this source tree.
*
* @license MIT
*/
File diff suppressed because one or more lines are too long