Compare commits

2 Commits
Author SHA1 Message Date
Dan Dobos e0d5039d07 feat: remove Capitalize component and update Card and Cart components to use direct string manipulation for titles and descriptions
feat: add launch configuration for debugging in VSCode
fix: update Home component to use Tilt with improved speed and gyroscope options
2025-10-21 12:07:26 +02:00
Dan Dobos 013f01bc90 feat: add Tilt component for enhanced product display and integrate with Home component
fix: correct spelling of 'Stock' in Card component
fix: update package.json and package-lock.json to include vanilla-tilt dependency
2025-10-21 09:29:07 +02:00
28 changed files with 19402 additions and 81 deletions
+15
View File
@@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://localhost:3000/react-shop",
"webRoot": "${workspaceFolder}/src"
}
]
}
+47
View File
@@ -0,0 +1,47 @@
# 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
@@ -1,13 +0,0 @@
{
"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
@@ -1 +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"/><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>
+18265
View File
File diff suppressed because it is too large Load Diff
+47
View File
@@ -0,0 +1,47 @@
{
"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",
"vanilla-tilt": "^1.8.1",
"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
@@ -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>
View File
+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 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;
+480
View File
@@ -0,0 +1,480 @@
// @import url("https://fonts.googleapis.com/css2?family=Sansation:ital,wght@0,300;0,400;0,700;1,300;1,400;1,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: "Sansation", sans-serif;
}
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: 36px;
width: 36px;
background-color: $OuterSpaceCrayola;
border: 1px solid $PersianGreen;
margin-left: 5px;
border-radius: 50%;
font-size: 16px;
}
}
}
// 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;
.card-container {
display: flex;
flex-direction: column;
gap: 0.5rem;
min-height: 480px;
}
button {
border-radius: 20px;
outline: none;
background-color: $OuterSpaceCrayola;
color: $Cultured;
font-size: 1rem;
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;
flex-grow: 1;
}
.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: 0.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;
background-color: $SolidPink;
border: none;
color: $Cultured;
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: 50px;
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;
color: $PersianGreen;
}
.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;
+47
View File
@@ -0,0 +1,47 @@
import { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { addToCart, getTotals } from "../features/cartSlice";
const Card = ({ product }) => {
const dispatch = useDispatch();
const cart = useSelector((state) => state.cart);
useEffect(() => {
dispatch(getTotals());
}, [cart, dispatch]);
const handleAddToCart = (product) => {
dispatch(addToCart(product));
};
return (
<div className="card-container">
<h3 className="title">{product.title}</h3>
<div className="image-container">
<img src={product.images[0]} alt={product.title} />
</div>
<div className="category">
<span>{product.category}</span>
<span>{product.availabilityStatus}</span>
</div>
<p className="description">{product?.description}</p>
<div>
<p className="discount">
<span>Discount:</span>
<span>{Math.round(product.discountPercentage)} %</span>
</p>
<p className="stock">
<span>Stock:</span>
<span>{product.stock}</span>
</p>
<p className="price">
<span>Price:</span>
<span>{product.price}</span>
</p>
</div>
<button onClick={() => handleAddToCart(product)}>Add to cart</button>
</div>
);
};
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 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>{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'>
{Math.round((cartItem.price * cartItem.cartQuantity) * 100)/100}
</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'>{Math.round(cart.cartTotalAmount * 100)/100}</span>
</div>
<button className='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;
+41
View File
@@ -0,0 +1,41 @@
import { useGetProductsQuery } from "../features/productsApi";
import Tilt from "./Tilt";
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) => (
<Tilt
key={product.id}
options={{
reverse: true,
speed: 300,
easing: "cubic-bezier(.09,.67,.54,.76)",
gyroscope: true,
}}>
<article className="card-list" >
<Card product={product} />
</article>
</Tilt>
))}
</>
)}
</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 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;
+16
View File
@@ -0,0 +1,16 @@
import React, { useEffect, useRef } from "react";
import VanillaTilt from "vanilla-tilt";
const Tilt = (props) => {
const { options, children } = props;
const tilt = useRef(null);
useEffect(() => {
VanillaTilt.init(tilt.current, options);
}, [options]);
return <div ref={tilt}>{children}</div>;
};
export default Tilt;
+109
View File
@@ -0,0 +1,109 @@
import { createSlice } from '@reduxjs/toolkit';
import { toast } from 'react-toastify';
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 ${
state.cartItems[itemIndex].title
} quantity to ${state.cartItems[itemIndex].cartQuantity}`;
// Show a toast when quantity is increased
if (window.location.href.indexOf('cart') !== -1) {
toast.info(multipleItemsText, toastifyOptions);
} else {
toast.info(multipleItemsText, toastifyOptions);
// Optionally, trigger a UI update to show GoToCartButton elsewhere
}
} else {
const tempProduct = { ...action.payload, cartQuantity: 1 };
state.cartItems.push(tempProduct);
let firstItemText = `${action.payload.title} added to cart`
toast.success(firstItemText, toastifyOptions);
// Optionally, trigger a UI update to show GoToCartButton elsewhere
}
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(`${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 ${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(
`${action.payload.title} removed from cart`,
toastifyOptions
);
}
localStorage.setItem('cartItems', JSON.stringify(state.cartItems));
},
clearCart(state) {
state.cartItems = [];
toast.error(`Cart cleared`, toastifyOptions);
localStorage.setItem('cartItems', JSON.stringify(state.cartItems));
},
getTotals(state, action) {
let { total: cartTotalAmount, quantity: cartTotalQuantity } = 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 = cartTotalQuantity;
state.cartTotalAmount = cartTotalAmount;
},
},
});
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>
);
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
@@ -1,60 +0,0 @@
/**
* @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