Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c464eadcf | ||
|
|
c9f3031c17 | ||
|
|
885b1d9744 |
@@ -1,47 +0,0 @@
|
||||
# React Shop
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## 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.
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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>
|
||||
Generated
-18257
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
-32
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -1,9 +0,0 @@
|
||||
const Footer = () => {
|
||||
const getCurrentYear = () => {
|
||||
return new Date().getFullYear();
|
||||
};
|
||||
|
||||
return <div className='footer'>Dan Dobos © {getCurrentYear()}</div>;
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
@@ -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
Reference in New Issue
Block a user