├── vite.config.js
├── src
├── store.js
├── main.jsx
├── features
│ ├── modal
│ │ └── modalSlice.js
│ └── cart
│ │ └── cartSlice.js
├── components
│ ├── NavBar.jsx
│ ├── Modal.jsx
│ ├── CartContainer.jsx
│ └── CartItem.jsx
├── cartItems.js
├── App.jsx
├── icons.jsx
├── assets
│ └── react.svg
└── index.css
├── .gitignore
├── index.html
├── package.json
└── public
└── vite.svg
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | })
8 |
--------------------------------------------------------------------------------
/src/store.js:
--------------------------------------------------------------------------------
1 | import {configureStore} from '@reduxjs/toolkit';
2 | import cartSlice from './features/cart/cartSlice';
3 | import modalSlice from './features/modal/modalSlice';
4 | export const store = configureStore({
5 | reducer:{
6 | cart:cartSlice,
7 | modal:modalSlice
8 | },
9 | })
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + React
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/main.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import App from './App';
4 | import './index.css';
5 | import { store } from './store';
6 | import {Provider} from 'react-redux';
7 |
8 | ReactDOM.createRoot(document.getElementById('root')).render(
9 |
10 |
11 |
12 |
13 | ,
14 | )
15 |
--------------------------------------------------------------------------------
/src/features/modal/modalSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 |
3 | const initialState = {
4 | isOpen: false,
5 | };
6 | const modalSlice = createSlice({
7 | name: "modal",
8 | initialState,
9 | reducers: {
10 | openModal: (state) => {
11 | state.isOpen = true;
12 | },
13 | closeModal: (state) => {
14 | state.isOpen = false;
15 | },
16 | },
17 | });
18 | export const { openModal, closeModal } = modalSlice.actions;
19 | export default modalSlice.reducer;
20 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ecommerce-redux-warm-up",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "preview": "vite preview"
10 | },
11 | "dependencies": {
12 | "@reduxjs/toolkit": "^1.9.3",
13 | "react": "^18.2.0",
14 | "react-dom": "^18.2.0",
15 | "react-redux": "^8.0.5"
16 | },
17 | "devDependencies": {
18 | "@types/react": "^18.0.28",
19 | "@types/react-dom": "^18.0.11",
20 | "@vitejs/plugin-react": "^3.1.0",
21 | "vite": "^4.2.0"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/NavBar.jsx:
--------------------------------------------------------------------------------
1 | import {CartIcon} from '../icons.jsx';
2 | import { useSelector } from 'react-redux';
3 |
4 | const NavBar = () => {
5 | const {amount,cartItem} = useSelector(store => store.cart);
6 | return(
7 |
18 | )
19 | }
20 | export default NavBar
--------------------------------------------------------------------------------
/src/cartItems.js:
--------------------------------------------------------------------------------
1 | const cartItems = [
2 | {
3 | id: 'rec1JZlfCIBOPdcT2',
4 | title: 'Samsung Galaxy S8',
5 | price: '399.99',
6 | img: 'https://images2.imgbox.com/c2/14/zedmXgs6_o.png',
7 | amount: 1,
8 | },
9 | {
10 | id: 'recB6qcHPxb62YJ75',
11 | title: 'google pixel',
12 | price: '499.99',
13 | img: 'https://images2.imgbox.com/fb/3d/O4TPmhlt_o.png',
14 | amount: 1,
15 | },
16 | {
17 | id: 'recdRxBsE14Rr2VuJ',
18 | title: 'Xiaomi Redmi Note 2',
19 | price: '699.99',
20 | img: 'https://images2.imgbox.com/4f/3d/WN3GvciF_o.png',
21 | amount: 1,
22 | },
23 | {
24 | id: 'recwTo160XST3PIoW',
25 | title: 'Samsung Galaxy S7',
26 | price: '599.99 ',
27 | img: 'https://images2.imgbox.com/2e/7c/yFsJ4Zkb_o.png',
28 | amount: 1,
29 | },
30 | ];
31 | export default cartItems;
32 |
--------------------------------------------------------------------------------
/src/App.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { useDispatch,useSelector } from 'react-redux';
3 | import NavBar from './components/NavBar.jsx'
4 | import CartContainer from './components/CartContainer.jsx'
5 | import Modal from './components/Modal.jsx';
6 | import { calculateTotals,getCartItem } from './features/cart/cartSlice.js';
7 | import { store } from './store.js';
8 |
9 | function App() {
10 | const {cartItems,isLoading} = useSelector(store=>store.cart)
11 | const {isOpen} = useSelector(store=>store.modal)
12 | const dispatch = useDispatch()
13 | useEffect(()=>{
14 | dispatch(calculateTotals())
15 | },[cartItems])
16 |
17 | useEffect(()=>{
18 | dispatch(getCartItem())
19 | },[])
20 |
21 | if (isLoading) {
22 | return (
23 |
24 |
Loading...
25 |
26 | )
27 | }
28 | return (
29 |
30 | {isOpen &&}
31 |
32 |
33 |
34 | )
35 | }
36 |
37 | export default App
38 |
--------------------------------------------------------------------------------
/src/components/Modal.jsx:
--------------------------------------------------------------------------------
1 | import { useDispatch } from "react-redux";
2 | import { closeModal } from "../features/modal/modalSlice";
3 | import { clearCart } from "../features/cart/cartSlice";
4 | const Modal = () => {
5 | const dispatch = useDispatch();
6 | return (
7 |
31 | );
32 | };
33 | export default Modal;
34 |
--------------------------------------------------------------------------------
/src/icons.jsx:
--------------------------------------------------------------------------------
1 | export const CartIcon = () => {
2 | return (
3 |
17 | );
18 | };
19 |
20 | export const ChevronDown = () => {
21 | return (
22 |
32 | );
33 | };
34 |
35 | export const ChevronUp = () => {
36 | return (
37 |
47 | );
48 | };
49 |
--------------------------------------------------------------------------------
/src/components/CartContainer.jsx:
--------------------------------------------------------------------------------
1 | import { openModal } from "../features/modal/modalSlice";
2 | import CartItem from "./CartItem";
3 | import { useSelector, useDispatch} from "react-redux";
4 | const CartContainer = () => {
5 | const { cartItems, total, amount } = useSelector((store) => store.cart);
6 | const dispatch = useDispatch()
7 | if (amount < 1) {
8 | return (
9 |
10 |
11 | your bag
12 | is currently empty
:
13 |
14 |
15 | );
16 | }
17 |
18 | return (
19 |
36 | );
37 | };
38 | export default CartContainer;
39 |
--------------------------------------------------------------------------------
/src/components/CartItem.jsx:
--------------------------------------------------------------------------------
1 | import { decreaseAmount, increaseAmount, removeItem } from "../features/cart/cartSlice";
2 | import { ChevronDown, ChevronUp } from "../icons";
3 | import { useDispatch } from "react-redux";
4 | const CartItem = ({id,img,title,price,amount}) => {
5 | const dispatch = useDispatch()
6 | return (
7 |
8 |
9 |
10 |
{title}
11 | ${price}
12 |
13 |
14 |
15 |
18 |
{amount}
19 |
27 |
28 |
29 | )
30 | }
31 | export default CartItem;
--------------------------------------------------------------------------------
/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/features/cart/cartSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice,createAsyncThunk } from "@reduxjs/toolkit";
2 | import cartItems from "../../cartItems";
3 | const url = 'https://course-api.com/react-useReducer-cart-project';
4 | const initialState = {
5 | cartItems: [],
6 | amount: 10,
7 | total: 0,
8 | isLoading: true,
9 | };
10 |
11 | export const getCartItem = createAsyncThunk('cart/getCartItems',async()=>{
12 | const res = await fetch(url)
13 | const data =await res.json()
14 | console.log(data)
15 | return data
16 | })
17 | const cartSlice = createSlice({
18 | name: "cart",
19 | initialState,
20 | reducers: {
21 | clearCart: (state) => {
22 | state.cartItems = [];
23 | },
24 | removeItem: (state, action) => {
25 | const itemId = action.payload;
26 | state.cartItems = state.cartItems.filter((item) => item.id !== itemId);
27 | },
28 | increaseAmount: (state, { payload }) => {
29 | const cartItem = state.cartItems.find((item) => item.id === payload);
30 | cartItem.amount = cartItem.amount + 1;
31 | },
32 | decreaseAmount: (state, { payload }) => {
33 | const cartItem = state.cartItems.find((item) => item.id === payload);
34 | cartItem.amount = cartItem.amount - 1;
35 | },
36 | calculateTotals: (state) => {
37 | let total = 0;
38 | let amount = 0;
39 | state.cartItems.forEach((item) => {
40 | total += item.price * item.amount;
41 | amount += item.amount;
42 | });
43 | state.total = total;
44 | state.amount = amount;
45 | },
46 | },
47 | extraReducers:{
48 | [getCartItem.pending] : (state)=>{
49 | state.isLoading = true
50 | },
51 | [getCartItem.fulfilled] : (state,action)=>{
52 | state.isLoading = false
53 | state.cartItems = action.payload
54 | },
55 | [getCartItem.rejected] : (state)=>{
56 | state.isLoading = false
57 | },
58 | }
59 | });
60 | export const {
61 | clearCart,
62 | removeItem,
63 | increaseAmount,
64 | decreaseAmount,
65 | calculateTotals,
66 | } = cartSlice.actions;
67 | export default cartSlice.reducer;
68 |
--------------------------------------------------------------------------------
/src/assets/react.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | /*
2 | ===============
3 | Variables
4 | ===============
5 | */
6 |
7 | :root {
8 | --clr-primary: #645cff;
9 | --clr-primary-dark: #282566;
10 | --clr-primary-light: #a29dff;
11 | --clr-grey-1: #102a42;
12 | --clr-grey-5: #617d98;
13 | --clr-grey-10: #f1f5f8;
14 | --clr-white: #fff;
15 | --clr-red-dark: hsl(360, 67%, 44%);
16 | --clr-red-light: hsl(360, 71%, 66%);
17 | --transition: all 0.3s linear;
18 | --spacing: 0.25rem;
19 | --radius: 0.25rem;
20 | --large-screen-width: 1170px;
21 | --small-screen-width: 90vw;
22 | --fixed-width: 50rem;
23 | }
24 | * {
25 | margin: 0;
26 | padding: 0;
27 | box-sizing: border-box;
28 | }
29 | body {
30 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
31 | Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
32 | background: var(--clr-grey-10);
33 | color: var(--clr-grey-1);
34 | line-height: 1.5;
35 | font-size: 0.875rem;
36 | }
37 | a {
38 | text-decoration: none;
39 | }
40 | img {
41 | width: 100%;
42 | display: block;
43 | }
44 | h1,
45 | h2,
46 | h3,
47 | h4 {
48 | letter-spacing: var(--spacing);
49 | text-transform: capitalize;
50 | line-height: 1.25;
51 | margin-bottom: 0.75rem;
52 | }
53 | h1 {
54 | font-size: 3rem;
55 | }
56 | h2 {
57 | font-size: 2rem;
58 | }
59 | h3 {
60 | font-size: 1.5rem;
61 | }
62 | h4 {
63 | font-size: 0.875rem;
64 | }
65 | p {
66 | margin-bottom: 1.25rem;
67 | }
68 | @media screen and (min-width: 800px) {
69 | h1 {
70 | font-size: 4rem;
71 | }
72 | h2 {
73 | font-size: 2.5rem;
74 | }
75 | h3 {
76 | font-size: 2rem;
77 | }
78 | h4 {
79 | font-size: 1rem;
80 | }
81 | body {
82 | font-size: 1rem;
83 | }
84 | h1,
85 | h2,
86 | h3,
87 | h4 {
88 | line-height: 1;
89 | }
90 | }
91 | /* more global css */
92 |
93 | .btn {
94 | text-transform: uppercase;
95 | background: var(--clr-primary);
96 | color: var(--clr-white);
97 | padding: 0.375rem 0.75rem;
98 | letter-spacing: var(--spacing);
99 | display: inline-block;
100 | font-weight: 700;
101 | transition: var(--transition);
102 | font-size: 0.875rem;
103 | border: none;
104 | cursor: pointer;
105 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
106 | }
107 | .btn:hover {
108 | color: var(--clr-primary);
109 | background: var(--clr-primary-light);
110 | }
111 |
112 | /*
113 | ===============
114 | Navbar
115 | ===============
116 | */
117 | .loading {
118 | text-align: center;
119 | margin-top: 5rem;
120 | }
121 | nav {
122 | background: var(--clr-primary);
123 | padding: 1.25rem 2rem;
124 | }
125 | .nav-center {
126 | max-width: var(--fixed-width);
127 | width: 100%;
128 | margin: 0 auto;
129 | display: flex;
130 | justify-content: space-between;
131 | align-items: center;
132 | }
133 | nav h3 {
134 | margin-bottom: 0;
135 | letter-spacing: 1px;
136 | color: var(--clr-white);
137 | }
138 | .nav-container {
139 | display: block;
140 | position: relative;
141 | }
142 | nav svg {
143 | width: 40px;
144 | color: var(--clr-white);
145 | }
146 | .amount-container {
147 | position: absolute;
148 | top: -0.6rem;
149 | right: -0.6rem;
150 | width: 1.75rem;
151 | height: 1.75rem;
152 | border-radius: 50%;
153 | background: var(--clr-primary-light);
154 | display: flex;
155 | align-items: center;
156 | justify-content: center;
157 | }
158 | .total-amount {
159 | color: var(--clr-white);
160 | margin-bottom: 0;
161 | font-size: 1.25rem;
162 | }
163 | /*
164 | ===============
165 | Cart
166 | ===============
167 | */
168 | .cart {
169 | min-height: calc(100vh - 120px);
170 | width: 90vw;
171 | margin: 0 auto;
172 | margin-top: 40px;
173 | padding: 2.5rem 0;
174 | max-width: var(--fixed-width);
175 | }
176 | .cart h2 {
177 | text-transform: uppercase;
178 | text-align: center;
179 | margin-bottom: 3rem;
180 | }
181 | .empty-cart {
182 | text-transform: lowercase;
183 | color: var(--clr-grey-5);
184 | margin-top: 1rem;
185 | text-align: center;
186 | }
187 | .cart footer {
188 | margin-top: 4rem;
189 | text-align: center;
190 | }
191 | .cart-total h4 {
192 | text-transform: capitalize;
193 | display: flex;
194 | justify-content: space-between;
195 | margin-top: 1rem;
196 | }
197 | .clear-btn,
198 | .confirm-btn {
199 | background: transparent;
200 | padding: 0.5rem 1rem;
201 | color: var(--clr-red-dark);
202 | border: 1px solid var(--clr-red-dark);
203 | margin-top: 2.25rem;
204 | border-radius: var(--radius);
205 | }
206 | .clear-btn:hover {
207 | background: var(--clr-red-light);
208 | color: var(--clr-red-dark);
209 | border-color: var(--clr-red-light);
210 | }
211 | .confirm-btn {
212 | border-color: var(--clr-primary);
213 | color: var(--clr-primary);
214 | }
215 | /*
216 | ===============
217 | Cart Item
218 | ===============
219 | */
220 | .cart-item {
221 | display: grid;
222 | align-items: center;
223 | grid-template-columns: auto 1fr auto;
224 | grid-column-gap: 1.5rem;
225 | margin: 1.5rem 0;
226 | }
227 | .cart-item img {
228 | width: 5rem;
229 | height: 5rem;
230 | object-fit: cover;
231 | }
232 | .cart-item h4 {
233 | margin-bottom: 0.5rem;
234 | font-weight: 500;
235 | letter-spacing: 2px;
236 | }
237 | .item-price {
238 | color: var(--clr-grey-5);
239 | }
240 | .remove-btn {
241 | color: var(--clr-primary);
242 | letter-spacing: var(--spacing);
243 | cursor: pointer;
244 | font-size: 0.85rem;
245 | background: transparent;
246 | border: none;
247 | margin-top: 0.375rem;
248 | transition: var(--transition);
249 | }
250 | .remove-btn:hover {
251 | color: var(--clr-primary-light);
252 | }
253 | .amount-btn {
254 | width: 24px;
255 | background: transparent;
256 | border: none;
257 | cursor: pointer;
258 | }
259 | .amount-btn svg {
260 | color: var(--clr-primary);
261 | }
262 | .amount-btn:hover svg {
263 | color: var(--clr-primary-light);
264 | }
265 | .amount {
266 | text-align: center;
267 | margin-bottom: 0;
268 | font-size: 1.25rem;
269 | line-height: 1;
270 | }
271 | hr {
272 | background: var(--clr-grey-5);
273 | border-color: transparent;
274 | border-width: 0.25px;
275 | }
276 |
277 | .modal-container {
278 | position: fixed;
279 | top: 0;
280 | left: 0;
281 | width: 100%;
282 | height: 100%;
283 | background: rgba(0, 0, 0, 0.7);
284 | z-index: 10;
285 | display: flex;
286 | align-items: center;
287 | justify-content: center;
288 | }
289 |
290 | .modal {
291 | background: var(--clr-white);
292 | width: 80vw;
293 | max-width: 400px;
294 | border-radius: var(--radius);
295 | padding: 2rem 1rem;
296 | text-align: center;
297 | }
298 | .modal h4 {
299 | margin-bottom: 0;
300 | line-height: 1.5;
301 | }
302 | .modal .clear-btn,
303 | .modal .confirm-btn {
304 | margin-top: 1rem;
305 | }
306 | .btn-container {
307 | display: flex;
308 | justify-content: space-around;
309 | }
310 |
--------------------------------------------------------------------------------