├── .dockerignore
├── .env.example
├── .gitignore
├── README.md
├── docker-compose.prod.yaml
├── docker-compose.yaml
├── docker
├── Dockerfile
└── Dockerfile.prod
├── nginx
└── nginx.conf
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── index.html
└── manifest.json
└── src
├── App.css
├── App.js
├── App.test.js
├── Layouts
├── MainLayout.jsx
└── SecondaryLayout.jsx
├── Utility
├── currency.js
└── formValidation.js
├── assets
├── icons
│ ├── cart.svg
│ ├── check-circle.png
│ ├── facebook-f.svg
│ ├── instagram.svg
│ ├── linkedin.svg
│ ├── money.png
│ ├── truck.png
│ └── twitter.svg
└── images
│ ├── baby_dress.jpg
│ ├── fila_black.jpg
│ ├── flare_dress.png
│ ├── shirt.jpg
│ └── shop_images
│ ├── analog-quartz-watch.jpg
│ ├── belt.jpg
│ ├── bodycon-dress.jpg
│ ├── boy_boxers.jpg
│ ├── boys-t-shirt.jpg
│ ├── cotton-dress.jpg
│ ├── crew-neck-tshirt.jpg
│ ├── floral-dress.jpg
│ ├── gemch_shoes.jpg
│ ├── gladiator-flat-flip.jpg
│ ├── gsoft-khaki.jpg
│ ├── leather-shoes.jpg
│ ├── princes-dress.jpg
│ ├── quartz-wrist-watch.jpg
│ ├── singedani-handbag.jpg
│ ├── slim-fit-suit.jpg
│ ├── sneakers.jpg
│ ├── vest.jpg
│ └── vintage-flare-dress.jpg
├── components
├── AddToWishlist
│ └── AddToWishlist.jsx
├── Cart
│ ├── CartProductTotals.jsx
│ └── CartProducts.jsx
├── Checkout
│ ├── CheckoutCartProduct.jsx
│ ├── CheckoutCartTotals.jsx
│ ├── Forms
│ │ ├── CustomerInputs.jsx
│ │ ├── DeliveryOptions.jsx
│ │ └── Payments
│ │ │ ├── CreditCardInputs.jsx
│ │ │ └── PaymentOptions.jsx
│ ├── PromoCodeForm.jsx
│ └── PromoCodeValue.jsx
├── CurrencyConverter.jsx
├── EmptyCategoryPageContent.jsx
├── Footer
│ ├── Footer.css
│ ├── Index.jsx
│ └── components
│ │ ├── FooterLinks.jsx
│ │ ├── GitLink.jsx
│ │ └── NewsLetter.jsx
├── LeftColumn.jsx
├── Loader
│ ├── Index.css
│ └── Index.jsx
├── Menus
│ ├── MainMenu.jsx
│ ├── MenuComponent.jsx
│ └── SideMenu.jsx
├── OrderSuccess.jsx
├── ProductCard
│ ├── Index.jsx
│ ├── ProductCard.css
│ └── ProductFeatures.jsx
├── ProductFilter
│ ├── Index.jsx
│ └── index.css
├── ProductsDisplay
│ └── Index.jsx
├── PromoCodes.jsx
├── Ratings
│ └── Ratings.jsx
└── UI
│ ├── Alert
│ └── Alert.jsx
│ ├── Backdrop
│ └── Backdrop.jsx
│ ├── BreadCrumbs
│ └── BreadCrumbs.jsx
│ ├── Icons
│ └── Icons.jsx
│ ├── Input
│ └── InputField.js
│ ├── Menu
│ └── Menu.jsx
│ ├── MenuItem
│ └── MenuItem.jsx
│ ├── Modal
│ └── Modal.jsx
│ └── Wrappers
│ ├── MainPageWrapper.jsx
│ ├── PageContentWrapper.jsx
│ └── SideMenuWrapper.jsx
├── index.css
├── index.js
├── static
├── constants.js
└── data.js
├── store
├── actions
│ ├── actionTypes.js
│ └── index.js
├── reducers
│ └── index.js
├── selectors.js
└── store.js
└── views
├── All.jsx
├── Cart.jsx
├── Checkout.jsx
├── Home
├── Home.css
├── Home.jsx
└── components
│ ├── Banner.jsx
│ ├── Deal.jsx
│ ├── HomeSale.jsx
│ ├── ItemBanners.jsx
│ └── SelloutCards.jsx
├── ProductCategories.jsx
├── ProductDetails
├── ProductDetails.css
└── ProductDetails.jsx
├── Sale.jsx
└── index.js
/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | npm-debug.log
3 | build
4 | .dockerignore
5 | docker
6 | **/.git
7 | **/.DS_Store
8 | **/node_modules
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | REACT_APP_STRIPE_KEY=somekey
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | .env
4 |
5 | # dependencies
6 | /node_modules
7 | /.pnp
8 | .pnp.js
9 |
10 | # testing
11 | /coverage
12 |
13 | # production
14 | /build
15 |
16 | # misc
17 | .DS_Store
18 | .env.local
19 | .env.development.local
20 | .env.test.local
21 | .env.production.local
22 |
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | #ide
28 | .idea
29 |
30 | #build
31 | /build
32 |
33 |
34 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## React-Redux Ecommerce web application
2 |
3 | this is a sample ecommerce web app made using react, redux and react router.
4 |
5 | Live demo can be found at [https://blissful-ramanujan-3c108d.netlify.app/](https://blissful-ramanujan-3c108d.netlify.app/).
6 |
7 | ## Build Setup
8 |
9 | 1. Run `npm install` in root directory, to install all required dependencies.
10 | 2. Rename `.env.example` file to `.env`
11 | 3. Add your **Stripe Api Key** to the `.env` file
12 | 4. Use `npm start` to start the application on your local machine
13 | 5. Use `4242 4242 4242 4242` credit card number when checking out!
14 |
--------------------------------------------------------------------------------
/docker-compose.prod.yaml:
--------------------------------------------------------------------------------
1 | version: "3.8"
2 |
3 | services:
4 | react-duka-prod:
5 | container_name: react-duka-prod
6 | build:
7 | context: .
8 | dockerfile: docker/Dockerfile.prod
9 | ports:
10 | - "8877:80"
11 | stdin_open: true
12 |
--------------------------------------------------------------------------------
/docker-compose.yaml:
--------------------------------------------------------------------------------
1 | version: "3.8"
2 |
3 | services:
4 | react-duka:
5 | container_name: react-duka
6 | build:
7 | context: .
8 | dockerfile: docker/Dockerfile
9 | volumes:
10 | - ".:/var/www/"
11 | - "/var/www/node_nodules"
12 | ports:
13 | - 3001:3000
14 | environment:
15 | - CHOKIDAR_USEPOLLING=true
16 | stdin_open: true
17 |
--------------------------------------------------------------------------------
/docker/Dockerfile:
--------------------------------------------------------------------------------
1 | # Set the base image to node:14.9-stretch
2 | FROM node:14.9-stretch
3 |
4 | # Specify where our app will live in the container
5 | WORKDIR /var/www/
6 |
7 | # add `/var/www/node_modules/.bin` to $PATH
8 | ENV PATH /var/www/node_modules/.bin:$PATH
9 |
10 | # install app dependencies
11 | COPY package.json ./
12 | COPY package-lock.json ./
13 | RUN npm install
14 | RUN npm install react-scripts@3.4.3
15 |
16 | # add app
17 | COPY . ./
18 |
19 | # start app
20 | CMD ["npm", "start"]
--------------------------------------------------------------------------------
/docker/Dockerfile.prod:
--------------------------------------------------------------------------------
1 | # Set the base image to node:14.9-stretch
2 | FROM node:14.9-stretch as builder
3 |
4 | # Specify where our app will live in the container
5 | WORKDIR /var/www/
6 |
7 | # add `/var/www/node_modules/.bin` to $PATH
8 | ENV PATH /var/www/node_modules/.bin:$PATH
9 |
10 | # install app dependencies
11 | COPY package.json ./
12 | COPY package-lock.json ./
13 | RUN npm install
14 | RUN npm install react-scripts@3.4.3
15 |
16 | # add app
17 | COPY . ./
18 |
19 | # start app
20 | RUN npm run build
21 |
22 |
23 | FROM nginx:1.19.2-perl
24 | COPY --from=builder /var/www/build /usr/share/nginx/html
25 | COPY nginx/nginx.conf /etc/nginx/conf.d/default.conf
26 | EXPOSE 80
27 | CMD ["nginx","-g","deamon off;"]
--------------------------------------------------------------------------------
/nginx/nginx.conf:
--------------------------------------------------------------------------------
1 | server {
2 |
3 | listen 80;
4 |
5 | location / {
6 | root /usr/share/nginx/html;
7 | index index.html index.htm;
8 | try_files $uri $uri/ /index.html;
9 | }
10 |
11 | error_page 500 502 503 504 /50x.html;
12 |
13 | location = /50x.html {
14 | root /usr/share/nginx/html;
15 | }
16 |
17 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-shop",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "bootstrap": "^4.3.1",
7 | "prop-types": "^15.6.2",
8 | "react": "16.7.0",
9 | "react-dom": "16.7.0",
10 | "react-icons": "^3.9.0",
11 | "react-redux": "6.0.0",
12 | "react-router": "4.3.1",
13 | "react-router-dom": "4.3.1",
14 | "react-scripts": "^3.4.3",
15 | "react-stripe-elements": "^2.0.2",
16 | "redux": "4.0.1",
17 | "redux-thunk": "2.3.0"
18 | },
19 | "scripts": {
20 | "start": "react-scripts start",
21 | "build": "react-scripts build",
22 | "test": "react-scripts test",
23 | "eject": "react-scripts eject",
24 | "docker-run-dev" : "docker-compose up -d --build",
25 | "docker-run-prod" : "docker-compose -f docker-compose.prod.yaml up -d --build",
26 | "docker-stop" : "docker-compose stop"
27 | },
28 | "eslintConfig": {
29 | "extends": "react-app"
30 | },
31 | "browserslist": [
32 | ">0.2%",
33 | "not dead",
34 | "not ie <= 11",
35 | "not op_mini all"
36 | ],
37 | "devDependencies": {
38 | "redux-devtools-extension": "^2.13.8"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/collinsNick/React-redux-shopping-web-app/d3defcc6fcca8665b9b0ef5fa4a120bf3d92a018/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 |
11 |
12 |
13 |
14 |
15 | React-shop
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | /* ------------- general --------------*/
2 | .container-fluid {
3 | padding-right: 0;
4 | padding-left: 0;
5 | }
6 |
7 | .shop-container {
8 | width: 75%;
9 | }
10 |
11 | .shop-bg-white {
12 | background-color: #fff;
13 | }
14 |
15 | .text-success {
16 | color: #f17e0a !important;
17 | }
18 |
19 | input.shop-input-error {
20 | color: #b93338;
21 | background-color: #f2dede;
22 | border: 1px solid #eed3d7;
23 | }
24 |
25 | [type="text"]:focus.shop-input-error,
26 | [type="email"]:focus.shop-input-error {
27 | color: #b93338;
28 | background-color: #f2dede;
29 | border: 1px solid #eed3d7;
30 | }
31 |
32 | input.success {
33 | color: #398843;
34 | background-color: #dff0d8;
35 | border: 1px solid #d6e9c6;
36 | }
37 |
38 | .page-results {
39 | font-size: 16px;
40 | font-weight: 400;
41 | margin-bottom: 10px;
42 | }
43 |
44 | /* ------------- breadCrumbs --------------*/
45 |
46 | .breadcrumb {
47 | padding: 0.5rem 0rem;
48 | margin-bottom: 0.5rem;
49 | background-color: transparent;
50 | }
51 |
52 | .breadcrumb-item + .breadcrumb-item::before {
53 | display: inline-block;
54 | padding-right: 0.5rem;
55 | color: #6c757d;
56 | content: "/";
57 | }
58 |
59 | .breadcrumb-item > a {
60 | text-transform: capitalize !important;
61 | text-decoration: none;
62 | color: #000;
63 | }
64 |
65 | .breadcrumb-item > a:hover {
66 | text-decoration: none;
67 | color: #000;
68 | }
69 |
70 | .breadcrumb-item.active {
71 | text-transform: capitalize !important;
72 | }
73 |
74 | /* ------------- shop menu --------------*/
75 |
76 | .navbar {
77 | padding: 0.8rem 1rem;
78 | }
79 |
80 | .navbar-brand > a {
81 | color: #e40046;
82 | text-decoration: none;
83 | font-weight: bold;
84 | }
85 |
86 | .badge-light {
87 | color: #fff;
88 | background-color: #e40046;
89 | }
90 |
91 | .nav-link.active .badge-light {
92 | color: #e40046;
93 | background-color: #fff;
94 | }
95 |
96 | .navbar-light .navbar-nav .nav-link {
97 | color: #6e6e6e;
98 | }
99 |
100 | .navbar-light .navbar-nav .nav-link:hover {
101 | color: #e40046;
102 | }
103 |
104 | .navbar-light .navbar-nav .nav-link.active,
105 | .navbar-light .navbar-nav .nav-link.show,
106 | .navbar-light .navbar-nav .show > .nav-link {
107 | background-color: #e40046;
108 | color: #fff;
109 | }
110 |
111 | .bg-light {
112 | background-color: #fff !important;
113 | }
114 |
115 | /* ------------- shop left column --------------*/
116 | .shop-left-column {
117 | background: #fff;
118 | box-shadow: 2px 2px 2px -2px gray;
119 | }
120 |
121 | .shop-left-column h5 {
122 | color: #f17e0a;
123 | font-weight: 600;
124 | }
125 |
126 | .shop-left-column p {
127 | font-size: 0.92rem;
128 | text-align: justify;
129 | }
130 |
131 | .shop-text-red {
132 | color: #e40046;
133 | }
134 |
135 | /* ------------- modal --------------*/
136 | .shop-modal {
137 | position: fixed;
138 | top: 0;
139 | left: 0;
140 | z-index: 1050;
141 | display: block;
142 | width: 100%;
143 | overflow: hidden;
144 | outline: 0;
145 | }
146 |
147 | /* ------------- shop cart --------------*/
148 | .shop-div {
149 | background-color: #fff;
150 | }
151 |
152 | .shop-cart-b-container {
153 | text-align: right !important;
154 | }
155 |
156 | .shop-empty-cart a,
157 | .shop-empty-cart a .hover {
158 | color: #f17e0a;
159 | text-decoration: none;
160 | }
161 |
162 | .shop-cart-image {
163 | width: 100px;
164 | height: 100px;
165 | }
166 |
167 | .shop-btn-warning,
168 | .shop-btn-warning:focus,
169 | .shop-btn-warning.focus,
170 | .shop-btn-warning:hover,
171 | .shop-btn-warning:active,
172 | .shop-btn-warning:active:focus,
173 | .shop-btn-warning.active {
174 | background-color: transparent;
175 | border-color: #e40046;
176 | color: #e40046;
177 | }
178 |
179 | .shop-btn-outline,
180 | .shop-btn-outline:focus,
181 | .shop-btn-outline.focus,
182 | .shop-btn-outline:hover,
183 | .shop-btn-outline:active,
184 | .shop-btn-outline:active:focus,
185 | .shop-btn-outline.active {
186 | background-color: transparent;
187 | border-color: #6c757d;
188 | color: #6c757d;
189 | margin-right: 1rem;
190 | }
191 |
192 | .shop-cart-total {
193 | font-weight: 500;
194 | }
195 |
196 | .shop-cart-amounts {
197 | font-size: 0.95rem;
198 | }
199 |
200 | .shop-cart-item-price,
201 | .shop-cart-item-total {
202 | font-size: 0.95rem;
203 | }
204 |
205 | .shop-cart-item-total span {
206 | font-weight: 600;
207 | }
208 |
209 | .shop-cart-category {
210 | color: #777;
211 | font-size: 0.95rem;
212 | }
213 |
214 | .wishlist-container > .product-wishlist {
215 | position: absolute;
216 | right: 1.5rem;
217 | top: -0.3rem;
218 | }
219 |
220 | .shop-cart-name {
221 | font-size: 1rem;
222 | font-weight: 500;
223 | }
224 |
225 | .shop-cart-product-details .badge-warning {
226 | color: #fff;
227 | background-color: #e40046;
228 | }
229 |
230 | .shop-cart-product-details .badge-success {
231 | color: #fff;
232 | background-color: #f17e0a;
233 | }
234 |
235 | /* ------------- shop footer --------------*/
236 | .shop-footer {
237 | background: #1e1e1e;
238 | color: #fff;
239 | }
240 |
241 | .shop-footer a {
242 | color: #fff;
243 | font-weight: bold;
244 | }
245 |
246 | /* ------------- modal --------------*/
247 | .modal .shop-display {
248 | display: block !important;
249 | }
250 |
251 | /* ------------- side bar --------------*/
252 |
253 | .side-menu-wrapper {
254 | width: 200px;
255 | min-height: 100vh;
256 | background: #fff;
257 | color: #fff;
258 | position: fixed;
259 | margin-left: -200px;
260 | box-shadow: 2px 2px 2px -2px gray;
261 | transition: all 0.3s;
262 | z-index: 2050;
263 | }
264 |
265 | .page-wrapper {
266 | width: 100%;
267 | transition: all 0.3s;
268 | position: absolute;
269 | top: 0;
270 | right: 0;
271 | }
272 |
273 | .side-menu-wrapper.show {
274 | margin-left: 0px;
275 | }
276 |
277 | .nav {
278 | list-style-type: none;
279 | }
280 |
281 | .nav .nav-item .nav-link {
282 | padding: 15px 20px;
283 | font-size: 1em;
284 | display: block;
285 | color: #6e6e6e;
286 | }
287 |
288 | .nav .nav-item .nav-link:hover {
289 | color: #e40046;
290 | }
291 |
292 | .nav .nav-item .nav-link.active {
293 | background-color: #e40046;
294 | color: #ffffff;
295 | }
296 |
297 | /* ------------- backdrop --------------*/
298 | .shop-backdrop {
299 | position: fixed;
300 | top: 0;
301 | left: 0;
302 | z-index: 1040;
303 | width: 100vw;
304 | height: 100vh;
305 | background-color: rgba(0, 0, 0, 0.5);
306 | }
307 |
308 | /* ------------- checkout page --------------*/
309 | .badge-secondary {
310 | color: #fff;
311 | background-color: #f17e0a;
312 | }
313 |
314 | .shop-checkout-image {
315 | width: 60px;
316 | margin-top: 5px;
317 | }
318 |
319 | .list-group-item {
320 | padding: 0.75rem 0.75rem;
321 | }
322 |
323 | .checkout-product-info h6 {
324 | font-size: 0.95rem;
325 | color: #000;
326 | }
327 |
328 | .checkout-product-info p {
329 | margin-top: 0.2rem;
330 | margin-bottom: 0;
331 | font-size: 0.9rem;
332 | color: #f17e0a;
333 | font-weight: 600;
334 | }
335 |
336 | .checkout-product-info small {
337 | color: #000;
338 | font-weight: 600;
339 | }
340 |
341 | .shop-checkout-prices {
342 | color: #000;
343 | font-size: 0.9rem;
344 | }
345 |
346 | .shop-checkout-prices span {
347 | font-weight: 600;
348 | }
349 |
350 | .shop-checkout-total {
351 | color: #000;
352 | font-weight: 600;
353 | font-size: 1.2rem;
354 | }
355 |
356 | .shop-checkout-total .shop-total {
357 | color: #f17e0a;
358 | }
359 |
360 | .shop-form ul {
361 | list-style-type: none;
362 | font-size: 1rem;
363 | padding-left: 2rem;
364 | }
365 |
366 | input[type="radio"] {
367 | margin-right: 1rem;
368 | }
369 |
370 | .shop-card-field {
371 | border: 1px solid #ced4da;
372 | border-radius: 3px;
373 | }
374 |
375 | .shop-input-errors {
376 | width: 100%;
377 | margin-top: 0.25rem;
378 | font-size: 80%;
379 | color: #dc3545;
380 | }
381 |
382 | .shop-delivery-options {
383 | background-color: #fff;
384 | border-radius: 5px;
385 | box-shadow: 2px 2px 2px -2px gray;
386 | font-size: 0.95rem;
387 | }
388 |
389 | /* ------------- side bar --------------*/
390 | @media (max-width: 767.98px) {
391 | /* ------------- general --------------*/
392 | .shop-container {
393 | width: 100%;
394 | }
395 |
396 | .shop-hide {
397 | display: none;
398 | }
399 |
400 | /* ------------- footer --------------*/
401 | .shop-footer {
402 | font-size: 0.9rem;
403 | }
404 |
405 | /* ------------- product card --------------*/
406 | .shop-card .shop-card-discount,
407 | .shop-card .shop-card-sale {
408 | font-size: 10px;
409 | font-weight: 600;
410 | }
411 |
412 | .shop-card .shop-card-image img {
413 | width: 50%;
414 | }
415 |
416 | .shop-card .shop-card-title {
417 | font-size: 14px;
418 | }
419 |
420 | /* ------------- cart page --------------*/
421 | .shop-cart-image-div {
422 | display: none;
423 | }
424 |
425 | .shop-empty-cart {
426 | font-size: 1rem !important;
427 | }
428 |
429 | .shop-cart-item-price {
430 | margin-top: 0.5rem;
431 | }
432 |
433 | .shop-cart-b-container {
434 | text-align: left !important;
435 | margin-top: 0.5rem;
436 | }
437 |
438 | .btn-outline-secondary {
439 | margin-right: 0.5rem;
440 | margin-bottom: 0.9rem;
441 | }
442 |
443 | .checkout {
444 | margin-right: 0.5rem;
445 | }
446 |
447 | .shop-cart-total {
448 | font-size: 1.2rem;
449 | }
450 |
451 | .shop-btn-outline {
452 | margin-bottom: 0.5rem;
453 | }
454 | }
455 |
456 | @media (max-width: 991.98px) {
457 | .shop-cart-name {
458 | font-size: 0.9rem;
459 | font-weight: 500;
460 | margin-top: 0.5rem;
461 | }
462 |
463 | .shop-cart-category {
464 | font-size: 0.9rem;
465 | }
466 |
467 | .shop-cart-item-price,
468 | .shop-cart-item-total {
469 | font-size: 0.9rem;
470 | }
471 |
472 | /* ------------- left column --------------*/
473 | .shop-left-column h5 {
474 | font-size: 1rem;
475 | }
476 |
477 | .shop-left-column p {
478 | font-size: 0.8rem;
479 | }
480 |
481 | .shop-btn-outline {
482 | margin-bottom: 0.5rem;
483 | }
484 |
485 | .shop-div h5 {
486 | font-size: 1rem;
487 | }
488 | }
489 |
490 | @media (min-width: 767.99px) {
491 | .side-menu-wrapper {
492 | margin-left: -200px;
493 | }
494 |
495 | .side-menu-wrapper.show {
496 | margin-left: -200px;
497 | }
498 | }
499 |
500 | @media (min-width: 767.99px) and (max-width: 991.98px) {
501 | .shop-container {
502 | width: 100%;
503 | }
504 |
505 | /* ------------- product card --------------*/
506 | .shop-card .shop-card-discount,
507 | .shop-card .shop-card-sale {
508 | font-size: 10px;
509 | font-weight: 600;
510 | }
511 |
512 | .shop-card .shop-card-title {
513 | font-size: 14px;
514 | }
515 | }
516 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { Route, Switch, withRouter, Redirect } from "react-router-dom";
3 | import { connect } from "react-redux";
4 | import { closeMaxProductModal, toogleSideBar } from "./store/actions";
5 | import MainLayout from "./Layouts/MainLayout";
6 | import * as Maincontainers from "./views";
7 | import "./App.css";
8 |
9 | class App extends Component {
10 | render() {
11 | return (
12 |
13 |
21 |
22 |
23 |
24 |
28 |
29 |
30 |
31 | (
34 |
38 | )}
39 | />
40 | {/*always redirect to index*/}
41 |
42 |
43 |
44 |
45 | );
46 | }
47 | }
48 |
49 | const mapStateToProps = (state) => {
50 | return {
51 | storeCartItemsCount: state.cartTotal,
52 | showModalProp: state.productMaxShowModal,
53 | modalMessageProp: state.modalMessage,
54 | showSideNavigationProp: state.showSideNavigation,
55 | };
56 | };
57 |
58 | const mapDispatchToProps = (dispatch) => {
59 | return {
60 | closeModalProp: () => dispatch(closeMaxProductModal()),
61 | toggleSideBarProp: () => dispatch(toogleSideBar()),
62 | };
63 | };
64 |
65 | export default withRouter(connect(mapStateToProps, mapDispatchToProps)(App));
66 |
--------------------------------------------------------------------------------
/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div');
7 | ReactDOM.render(, div);
8 | ReactDOM.unmountComponentAtNode(div);
9 | });
10 |
--------------------------------------------------------------------------------
/src/Layouts/MainLayout.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import MainWrapper from "../components/UI/Wrappers/MainPageWrapper";
3 | import SideMenuWrapper from "../components/UI/Wrappers/SideMenuWrapper";
4 | import ContentWrapper from "../components/UI/Wrappers/PageContentWrapper";
5 | import MainMenu from "../components/Menus/MainMenu";
6 | import SideMenu from "../components/Menus/SideMenu";
7 | import Footer from "../components/Footer/Index";
8 | import Modal from "../components/UI/Modal/Modal";
9 | import PropTypes from "prop-types";
10 |
11 | const MainLayout = (props) => {
12 | return (
13 |
14 |
15 |
19 |
23 |
24 |
25 |
31 |
32 | {props.children}
33 | {props.showModal ? (
34 |
38 | {props.modalMessage}
39 |
40 | ) : null}
41 |
42 |
45 |
46 |
47 |
48 | );
49 | };
50 |
51 | MainLayout.propTpes = {
52 | storeCartCount: PropTypes.number.isRequired,
53 | showModal: PropTypes.bool,
54 | closeModalClick: PropTypes.func,
55 | modalMessage: PropTypes.string,
56 | showSideBar: PropTypes.bool,
57 | toggleSideBar: PropTypes.func.isRequired,
58 | };
59 |
60 | export default MainLayout;
61 |
--------------------------------------------------------------------------------
/src/Layouts/SecondaryLayout.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import LeftColumn from "../components/LeftColumn";
4 | import BreadCrumbs from "../components/UI/BreadCrumbs/BreadCrumbs";
5 |
6 | const SecondaryLayout = (props) => {
7 | return (
8 |
9 | {props.breadCrumbs ? (
10 |
15 | ) : null}
16 |
17 |
18 |
19 |
20 |
21 | {props.results ? (
22 |
23 |
{props.results}
24 |
25 | ) : null}
26 |
{props.children}
27 |
28 |
29 |
30 | );
31 | };
32 |
33 | SecondaryLayout.prototypes = {
34 | results: PropTypes.string,
35 | breadCrumbs: PropTypes.array,
36 | };
37 |
38 | export default SecondaryLayout;
39 |
--------------------------------------------------------------------------------
/src/Utility/currency.js:
--------------------------------------------------------------------------------
1 | export const currencyToUse = (usedCurrency) => {
2 | let currencyKeys = Object.keys(usedCurrency);
3 | return {
4 | name: usedCurrency[currencyKeys[1]],
5 | value: usedCurrency[currencyKeys[0]],
6 | };
7 | };
8 |
9 | export const productPrice = (price, value) => {
10 | return Math.round(price * value).toLocaleString();
11 | };
12 |
13 | export const productDiscountPrice = (price, discountPrice) => {
14 | return `-${Math.round(((discountPrice - price) * 100) / discountPrice)}%`;
15 | };
16 |
--------------------------------------------------------------------------------
/src/Utility/formValidation.js:
--------------------------------------------------------------------------------
1 | export default function (identifier, value) {
2 |
3 | let isValid = true;
4 | let errMsg = '';
5 |
6 | switch (identifier) {
7 |
8 | case('firstName'):
9 | case('secondName'):
10 | if (value.trim() === '' && isValid) {
11 | isValid = false;
12 | errMsg = 'Name must have a value';
13 | } else if (!value.match(/^[a-zA-Z]*$/) && isValid) {
14 | isValid = false;
15 | errMsg = 'Name must be letters only';
16 | } else if (value.length < 2 && isValid) {
17 | isValid = false;
18 | errMsg = 'Name must be more than one characters long';
19 | }
20 | break;
21 |
22 | case('email') :
23 | if (value.trim() === '' && isValid) {
24 | isValid = false;
25 | errMsg = 'Email Address Must have a value';
26 | } else {
27 | const pattern = new RegExp(/^(("[\w-\s]+")|([\w-]+(?:\.[\w-]+)*)|("[\w-\s]+")([\w-]+(?:\.[\w-]+)*))(@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$)|(@\[?((25[0-5]\.|2[0-4][0-9]\.|1[0-9]{2}\.|[0-9]{1,2}\.))((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[0-9]{1,2})\.){2}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[0-9]{1,2})\]?$)/i);
28 | if (!pattern.test(value)) {
29 | isValid = false;
30 | errMsg = "Please enter a valid email address"
31 | }
32 | }
33 | break;
34 |
35 | default:
36 |
37 | }
38 |
39 | return {isValid: isValid, errorsMsg: errMsg};
40 | }
--------------------------------------------------------------------------------
/src/assets/icons/cart.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/check-circle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/collinsNick/React-redux-shopping-web-app/d3defcc6fcca8665b9b0ef5fa4a120bf3d92a018/src/assets/icons/check-circle.png
--------------------------------------------------------------------------------
/src/assets/icons/facebook-f.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/icons/instagram.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/icons/linkedin.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/icons/money.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/collinsNick/React-redux-shopping-web-app/d3defcc6fcca8665b9b0ef5fa4a120bf3d92a018/src/assets/icons/money.png
--------------------------------------------------------------------------------
/src/assets/icons/truck.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/collinsNick/React-redux-shopping-web-app/d3defcc6fcca8665b9b0ef5fa4a120bf3d92a018/src/assets/icons/truck.png
--------------------------------------------------------------------------------
/src/assets/icons/twitter.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/images/baby_dress.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/collinsNick/React-redux-shopping-web-app/d3defcc6fcca8665b9b0ef5fa4a120bf3d92a018/src/assets/images/baby_dress.jpg
--------------------------------------------------------------------------------
/src/assets/images/fila_black.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/collinsNick/React-redux-shopping-web-app/d3defcc6fcca8665b9b0ef5fa4a120bf3d92a018/src/assets/images/fila_black.jpg
--------------------------------------------------------------------------------
/src/assets/images/flare_dress.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/collinsNick/React-redux-shopping-web-app/d3defcc6fcca8665b9b0ef5fa4a120bf3d92a018/src/assets/images/flare_dress.png
--------------------------------------------------------------------------------
/src/assets/images/shirt.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/collinsNick/React-redux-shopping-web-app/d3defcc6fcca8665b9b0ef5fa4a120bf3d92a018/src/assets/images/shirt.jpg
--------------------------------------------------------------------------------
/src/assets/images/shop_images/analog-quartz-watch.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/collinsNick/React-redux-shopping-web-app/d3defcc6fcca8665b9b0ef5fa4a120bf3d92a018/src/assets/images/shop_images/analog-quartz-watch.jpg
--------------------------------------------------------------------------------
/src/assets/images/shop_images/belt.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/collinsNick/React-redux-shopping-web-app/d3defcc6fcca8665b9b0ef5fa4a120bf3d92a018/src/assets/images/shop_images/belt.jpg
--------------------------------------------------------------------------------
/src/assets/images/shop_images/bodycon-dress.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/collinsNick/React-redux-shopping-web-app/d3defcc6fcca8665b9b0ef5fa4a120bf3d92a018/src/assets/images/shop_images/bodycon-dress.jpg
--------------------------------------------------------------------------------
/src/assets/images/shop_images/boy_boxers.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/collinsNick/React-redux-shopping-web-app/d3defcc6fcca8665b9b0ef5fa4a120bf3d92a018/src/assets/images/shop_images/boy_boxers.jpg
--------------------------------------------------------------------------------
/src/assets/images/shop_images/boys-t-shirt.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/collinsNick/React-redux-shopping-web-app/d3defcc6fcca8665b9b0ef5fa4a120bf3d92a018/src/assets/images/shop_images/boys-t-shirt.jpg
--------------------------------------------------------------------------------
/src/assets/images/shop_images/cotton-dress.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/collinsNick/React-redux-shopping-web-app/d3defcc6fcca8665b9b0ef5fa4a120bf3d92a018/src/assets/images/shop_images/cotton-dress.jpg
--------------------------------------------------------------------------------
/src/assets/images/shop_images/crew-neck-tshirt.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/collinsNick/React-redux-shopping-web-app/d3defcc6fcca8665b9b0ef5fa4a120bf3d92a018/src/assets/images/shop_images/crew-neck-tshirt.jpg
--------------------------------------------------------------------------------
/src/assets/images/shop_images/floral-dress.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/collinsNick/React-redux-shopping-web-app/d3defcc6fcca8665b9b0ef5fa4a120bf3d92a018/src/assets/images/shop_images/floral-dress.jpg
--------------------------------------------------------------------------------
/src/assets/images/shop_images/gemch_shoes.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/collinsNick/React-redux-shopping-web-app/d3defcc6fcca8665b9b0ef5fa4a120bf3d92a018/src/assets/images/shop_images/gemch_shoes.jpg
--------------------------------------------------------------------------------
/src/assets/images/shop_images/gladiator-flat-flip.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/collinsNick/React-redux-shopping-web-app/d3defcc6fcca8665b9b0ef5fa4a120bf3d92a018/src/assets/images/shop_images/gladiator-flat-flip.jpg
--------------------------------------------------------------------------------
/src/assets/images/shop_images/gsoft-khaki.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/collinsNick/React-redux-shopping-web-app/d3defcc6fcca8665b9b0ef5fa4a120bf3d92a018/src/assets/images/shop_images/gsoft-khaki.jpg
--------------------------------------------------------------------------------
/src/assets/images/shop_images/leather-shoes.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/collinsNick/React-redux-shopping-web-app/d3defcc6fcca8665b9b0ef5fa4a120bf3d92a018/src/assets/images/shop_images/leather-shoes.jpg
--------------------------------------------------------------------------------
/src/assets/images/shop_images/princes-dress.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/collinsNick/React-redux-shopping-web-app/d3defcc6fcca8665b9b0ef5fa4a120bf3d92a018/src/assets/images/shop_images/princes-dress.jpg
--------------------------------------------------------------------------------
/src/assets/images/shop_images/quartz-wrist-watch.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/collinsNick/React-redux-shopping-web-app/d3defcc6fcca8665b9b0ef5fa4a120bf3d92a018/src/assets/images/shop_images/quartz-wrist-watch.jpg
--------------------------------------------------------------------------------
/src/assets/images/shop_images/singedani-handbag.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/collinsNick/React-redux-shopping-web-app/d3defcc6fcca8665b9b0ef5fa4a120bf3d92a018/src/assets/images/shop_images/singedani-handbag.jpg
--------------------------------------------------------------------------------
/src/assets/images/shop_images/slim-fit-suit.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/collinsNick/React-redux-shopping-web-app/d3defcc6fcca8665b9b0ef5fa4a120bf3d92a018/src/assets/images/shop_images/slim-fit-suit.jpg
--------------------------------------------------------------------------------
/src/assets/images/shop_images/sneakers.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/collinsNick/React-redux-shopping-web-app/d3defcc6fcca8665b9b0ef5fa4a120bf3d92a018/src/assets/images/shop_images/sneakers.jpg
--------------------------------------------------------------------------------
/src/assets/images/shop_images/vest.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/collinsNick/React-redux-shopping-web-app/d3defcc6fcca8665b9b0ef5fa4a120bf3d92a018/src/assets/images/shop_images/vest.jpg
--------------------------------------------------------------------------------
/src/assets/images/shop_images/vintage-flare-dress.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/collinsNick/React-redux-shopping-web-app/d3defcc6fcca8665b9b0ef5fa4a120bf3d92a018/src/assets/images/shop_images/vintage-flare-dress.jpg
--------------------------------------------------------------------------------
/src/components/AddToWishlist/AddToWishlist.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { connect } from "react-redux";
3 | import PropTypes from "prop-types";
4 | import { toogleItemInWishList } from "../../store/actions";
5 | import { Heart } from "../UI/Icons/Icons.jsx";
6 | import { getWishlist } from "../../store/selectors";
7 |
8 | const AddToWishlist = (props) => {
9 | function toogleWishlistItem() {
10 | props.addRemoveItemInWishlist(props.productId);
11 | }
12 |
13 | function wished() {
14 | return props.wishlistItems.find(
15 | (productId) => productId === props.productId
16 | )
17 | ? "wished"
18 | : null;
19 | }
20 | return (
21 |
26 |
27 |
28 | );
29 | };
30 |
31 | const mapStateToProps = (state) => {
32 | return {
33 | wishlistItems: getWishlist(state),
34 | };
35 | };
36 |
37 | const mapDispatchToProps = (dispatch) => {
38 | return {
39 | addRemoveItemInWishlist: (productId) =>
40 | dispatch(toogleItemInWishList(productId)),
41 | };
42 | };
43 |
44 | AddToWishlist.prototypes = {
45 | productId: PropTypes.number.isRequired,
46 | classStyleName: PropTypes.string,
47 | title: PropTypes.string,
48 | };
49 |
50 | export default connect(mapStateToProps, mapDispatchToProps)(AddToWishlist);
51 |
--------------------------------------------------------------------------------
/src/components/Cart/CartProductTotals.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Link } from "react-router-dom";
3 | import PropTypes from "prop-types";
4 | import { currencyToUse } from "../../Utility/currency";
5 |
6 | const cartProductTotals = (props) => {
7 | let currencyKeys = currencyToUse(props.currency);
8 | let currencyName = currencyKeys.name;
9 |
10 | let subtotal = props.subtotal;
11 | let vatPercentage = props.vat > 0 ? props.vat / 100 : 0;
12 | let vat = subtotal > 0 ? Math.round(subtotal * vatPercentage) : 0;
13 | let totalCost = subtotal > 0 ? subtotal + vat : 0;
14 |
15 | return (
16 |
17 |
18 |
19 | Subtotal
20 |
21 |
22 | {currencyName}
23 | {subtotal.toLocaleString()}
24 |
25 |
26 |
27 |
28 |
29 | VAT
30 |
31 |
32 | {currencyName}
33 | {vat.toLocaleString()}
34 |
35 |
36 |
37 |
38 |
39 |
Total
40 |
41 |
42 |
43 | {currencyName}
44 | {totalCost.toLocaleString()}
45 |
46 |
47 |
48 |
49 |
50 |
51 |
54 |
55 | Continue shopping
56 |
57 |
61 | Checkout
62 |
63 |
64 |
65 |
66 | );
67 | };
68 |
69 | cartProductTotals.propTypes = {
70 | subtotal: PropTypes.number.isRequired,
71 | clearCart: PropTypes.func.isRequired,
72 | vat: PropTypes.number,
73 | currency: PropTypes.object.isRequired,
74 | };
75 |
76 | cartProductTotals.defaultProps = {
77 | shippingPrice: 0,
78 | };
79 |
80 | export default cartProductTotals;
81 |
--------------------------------------------------------------------------------
/src/components/Cart/CartProducts.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import AddToWishList from "../../components/AddToWishlist/AddToWishlist";
4 | import { currencyToUse } from "../../Utility/currency";
5 |
6 | const cartProducts = (props) => {
7 | let currencyKeys = currencyToUse(props.currency);
8 | let currencyName = currencyKeys.name;
9 |
10 | return (
11 |
12 |
13 |
14 |
15 |
16 |
![{props.productPhoto.split(".")[0]}]({require(`../../assets/images/shop_images/${props.productPhoto}`)})
21 |
22 |
23 |
24 |
31 | {props.productVendor ? (
32 |
33 | {`Sold by : `}
34 |
35 | {props.productVendor.name}
36 |
37 |
38 | ) : null}
39 |
40 |
41 | {props.productName}
42 |
43 |
44 | {props.productSize ? (
45 |
46 | {`Size : `}
47 | {props.productSize}
48 |
49 | ) : null}
50 |
51 |
52 | 0
56 | ? "badge-success"
57 | : "badge-danger")
58 | }
59 | >
60 | {props.productQuantity > 0 ? "In Stock" : "Out of Stock"}
61 |
62 |
63 |
64 |
65 |
66 |
67 | {props.productQuantity > 0 ? (
68 |
69 |
70 |
71 | {currencyName}
72 |
73 | {props.productPrice.toLocaleString()}
74 |
75 |
89 |
90 | {`Total `}
91 |
92 | {currencyName}
93 |
94 |
95 | {(
96 | props.productPrice * props.productCount
97 | ).toLocaleString()}
98 |
99 |
100 |
101 | ) : null}
102 |
103 |
104 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 | );
120 | };
121 |
122 | cartProducts.propTypes = {
123 | productId: PropTypes.number.isRequired,
124 | productPhoto: PropTypes.string.isRequired,
125 | productName: PropTypes.string.isRequired,
126 | productVendor: PropTypes.object,
127 | productCategory: PropTypes.string.isRequired,
128 | productPrice: PropTypes.number.isRequired,
129 | updateProductCount: PropTypes.func.isRequired,
130 | productQuantity: PropTypes.number.isRequired,
131 | removeCartProduct: PropTypes.func.isRequired,
132 | currency: PropTypes.object.isRequired,
133 | };
134 |
135 | export default cartProducts;
136 |
--------------------------------------------------------------------------------
/src/components/Checkout/CheckoutCartProduct.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import { currencyToUse } from "../../Utility/currency";
4 |
5 | const checkoutCartProduct = (props) => {
6 | let currencyKeys = currencyToUse(props.currency);
7 | let currencyName = currencyKeys.name;
8 |
9 | return (
10 |
11 |
12 |
13 |
14 |
![{props.checkoutProductImage.split(".")[0]}]({require(`../../assets/images/shop_images/${props.checkoutProductImage}`)})
19 |
20 |
21 |
{props.checkoutProductName}
22 |
23 | {currencyName}
24 | {props.checkoutProductPrice}
25 |
26 |
27 |
28 | {props.checkoutCartSize ? (
29 |
30 | Size:
31 | {props.checkoutCartSize}
32 |
33 | ) : null}
34 |
35 | Qty:
36 | {props.checkoutCartCount}
37 |
38 |
39 |
40 |
41 |
42 |
43 | );
44 | };
45 |
46 | checkoutCartProduct.propTypes = {
47 | checkoutProductName: PropTypes.string.isRequired,
48 | checkoutCartCount: PropTypes.number.isRequired,
49 | checkoutProductPrice: PropTypes.number.isRequired,
50 | checkoutProductImage: PropTypes.string.isRequired,
51 | currency: PropTypes.object.isRequired,
52 | };
53 |
54 | export default checkoutCartProduct;
55 |
--------------------------------------------------------------------------------
/src/components/Checkout/CheckoutCartTotals.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import { currencyToUse } from "../../Utility/currency";
4 |
5 | const checkoutCartTotals = (props) => {
6 | let currencyKeys = currencyToUse(props.currency);
7 | let currencyName = currencyKeys.name;
8 |
9 | return (
10 |
11 |
12 |
13 | Sub Total
14 |
15 | {" "}
16 | {currencyName}
17 | {props.productTotals.toLocaleString()}
18 |
19 |
20 |
23 | VAT
24 |
25 | {currencyName}
26 | {props.vat.toLocaleString()}
27 |
28 |
29 |
30 | Shipping amount
31 |
32 | {currencyName}
33 | {props.shippingPrice.toLocaleString()}
34 |
35 |
36 |
37 |
38 |
39 | Total
40 |
41 | {currencyName}
42 | {props.shoppingTotal.toLocaleString()}
43 |
44 |
45 |
46 | );
47 | };
48 |
49 | checkoutCartTotals.propTypes = {
50 | productTotals: PropTypes.number.isRequired,
51 | vat: PropTypes.number.isRequired,
52 | shippingPrice: PropTypes.number.isRequired,
53 | shoppingTotal: PropTypes.number.isRequired,
54 | currency: PropTypes.object.isRequired,
55 | };
56 |
57 | export default checkoutCartTotals;
58 |
--------------------------------------------------------------------------------
/src/components/Checkout/Forms/CustomerInputs.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import InputField from '../../UI/Input/InputField';
4 |
5 | const customerInputs = (props) => {
6 | return (
7 |
8 |
9 |
10 | props.inputChanged(event, 'firstName')}/>
16 |
17 |
18 |
19 | props.inputChanged(event, 'secondName')}/>
25 |
26 |
27 |
28 |
29 | props.inputChanged(event, 'email')}/>
35 |
36 |
37 |
38 | )
39 | };
40 |
41 | customerInputs.propTypes = {
42 | inputChanged: PropTypes.func.isRequired,
43 | customerInfo: PropTypes.object.isRequired,
44 | };
45 |
46 | export default customerInputs;
--------------------------------------------------------------------------------
/src/components/Checkout/Forms/DeliveryOptions.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import { currencyToUse } from "../../../Utility/currency";
4 |
5 | const deliveryOptions = (props) => {
6 | let currencyKeys = currencyToUse(props.currency);
7 | let currencyName = currencyKeys.name;
8 | let currencyValue = currencyKeys.value;
9 |
10 | return (
11 |
45 | );
46 | };
47 |
48 | deliveryOptions.propTypes = {
49 | deliveryOptions: PropTypes.array.isRequired,
50 | usedDeliveryOption: PropTypes.number,
51 | deliveryOptionChanged: PropTypes.func.isRequired,
52 | currency: PropTypes.object.isRequired,
53 | };
54 |
55 | export default deliveryOptions;
56 |
--------------------------------------------------------------------------------
/src/components/Checkout/Forms/Payments/CreditCardInputs.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | const creditCardInputs = (props) => {
5 | return (
6 |
7 |
8 |
9 |
10 |
props.inputChanged(event,'creditCardName')}/>
16 |
17 | Name on card is required
18 |
19 |
20 |
21 |
22 |
props.inputChanged(event,'creditCardNumber')}/>
28 |
29 | Credit card number is required
30 |
31 |
32 |
33 |
34 |
35 |
36 |
props.inputChanged(event,'creditCardExpiration')}/>
42 |
43 | Expiration date required
44 |
45 |
46 |
47 |
48 |
props.inputChanged(event,'creditCardCvv')}/>
54 |
55 | Security code required
56 |
57 |
58 |
59 |
60 | )
61 | };
62 |
63 | creditCardInputs.propTypes = {
64 | inputChanged: PropTypes.func.isRequired,
65 | creditCardInfo: PropTypes.object.isRequired,
66 | };
67 |
68 | export default creditCardInputs;
--------------------------------------------------------------------------------
/src/components/Checkout/Forms/Payments/PaymentOptions.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | const paymentOptions = props => {
5 |
6 | return (
7 |
32 | )
33 | };
34 |
35 | paymentOptions.propTypes = {
36 | paymentOptionChanged: PropTypes.func.isRequired,
37 | paymentMethod: PropTypes.string.isRequired
38 | };
39 |
40 | export default paymentOptions;
--------------------------------------------------------------------------------
/src/components/Checkout/PromoCodeForm.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | const promoCodeForm = props => {
5 |
6 | return (
7 |
25 | )
26 | };
27 |
28 | promoCodeForm.propTypes = {
29 | setPromoCode: PropTypes.func.isRequired,
30 | promoCodeChangeHandler: PropTypes.func.isRequired,
31 | promoCode:PropTypes.string.isRequired
32 | };
33 |
34 | export default promoCodeForm;
--------------------------------------------------------------------------------
/src/components/Checkout/PromoCodeValue.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import { currencyToUse } from "../../Utility/currency";
4 |
5 | const promoCodeFormValue = (props) => {
6 | let currencyKeys = currencyToUse(props.currency);
7 | let currencyName = currencyKeys.name;
8 |
9 | return (
10 |
11 |
12 | Applied Promo code
13 |
14 |
15 |
16 | {props.usedPromoCode.code}
17 |
18 |
19 |
20 | -{currencyName}
21 | {props.discountAmount.toLocaleString()}
22 |
23 |
24 |
25 |
26 | );
27 | };
28 |
29 | promoCodeFormValue.propTypes = {
30 | usedPromoCode: PropTypes.object.isRequired,
31 | discountAmount: PropTypes.number.isRequired,
32 | currency: PropTypes.object.isRequired,
33 | };
34 |
35 | export default promoCodeFormValue;
36 |
--------------------------------------------------------------------------------
/src/components/CurrencyConverter.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { connect } from "react-redux";
3 | import { changeCurrency } from "../store/actions";
4 | import PropTypes from "prop-types";
5 |
6 | const CurrencyConverter = (props) => {
7 | const currencyChangeHandler = (event) => {
8 | props.changeCurrencyProp(event.target.value);
9 | };
10 |
11 | return (
12 |
13 | {props.showLabel ? (
14 |
17 | ) : null}
18 |
29 |
30 | );
31 | };
32 |
33 | CurrencyConverter.propType = {
34 | usedCurrencyProp: PropTypes.object.isRequired,
35 | exchangeRatesProps: PropTypes.object.isRequired,
36 | showLabel: PropTypes.bool,
37 | };
38 |
39 | const mapStateToProps = (state) => {
40 | return {
41 | exchangeRatesProps: state.exchangeRates,
42 | usedCurrencyProp: state.usedCurrency,
43 | };
44 | };
45 |
46 | const mapDispatchToProps = (dispatch) => {
47 | return {
48 | changeCurrencyProp: (currencyName) =>
49 | dispatch(changeCurrency(currencyName)),
50 | };
51 | };
52 |
53 | export default connect(mapStateToProps, mapDispatchToProps)(CurrencyConverter);
54 |
--------------------------------------------------------------------------------
/src/components/EmptyCategoryPageContent.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const EmptyCategoryPageContent = () => {
4 | return (
5 |
6 |
There are currently no products. Please check back later
7 |
8 | )
9 | };
10 |
11 | export default EmptyCategoryPageContent;
12 |
--------------------------------------------------------------------------------
/src/components/Footer/Footer.css:
--------------------------------------------------------------------------------
1 | /* footer section */
2 | .footer-contaner {
3 | color: #ffffff;
4 | background: #111111;
5 | padding: 40px 20px;
6 | display: grid;
7 | grid-template-columns: repeat(4, 1fr);
8 | justify-items: center;
9 | }
10 |
11 | .footer-contaner a {
12 | color: #6c757d;
13 | text-decoration: none;
14 | }
15 | .footer-contaner a:hover {
16 | color: #ffffff;
17 | }
18 |
19 | @media (max-width: 576px) {
20 | /* footer section */
21 | .footer-contaner {
22 | grid-template-columns: repeat(2, 1fr);
23 | grid-template-rows: repeat(2, 1fr);
24 | justify-items: left;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/components/Footer/Index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import NewsLetter from "./components/NewsLetter";
3 | import FooterLinks from "./components/FooterLinks";
4 | import GitLinks from "./components/GitLink";
5 | import "./Footer.css";
6 |
7 | const Index = () => {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 | );
15 | };
16 |
17 | export default Index;
18 |
--------------------------------------------------------------------------------
/src/components/Footer/components/FooterLinks.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { NavLink } from "react-router-dom";
3 |
4 | const FooterLinks = () => {
5 | return (
6 |
7 |
8 |
About
9 |
10 | -
11 | Company
12 |
13 | -
14 | Location
15 |
16 | -
17 | Contacts
18 |
19 | -
20 | Opening Hours
21 |
22 |
23 |
24 |
25 |
Useful links
26 |
27 | -
28 | Help
29 |
30 | -
31 | Privacy Ploicy
32 |
33 | -
34 | Terms and Conditions
35 |
36 | -
37 | FAQ
38 |
39 |
40 |
41 |
42 |
Customer Servie
43 |
44 | -
45 | Payment Methods
46 |
47 | -
48 | Money-back
49 |
50 | -
51 | Returns
52 |
53 | -
54 | Shipping
55 |
56 |
57 |
58 |
59 |
Join Us
60 |
61 | -
62 | Twitter
63 |
64 | -
65 | Facebook
66 |
67 | -
68 | Instagram
69 |
70 | -
71 | Linkedin
72 |
73 |
74 |
75 |
76 | );
77 | };
78 |
79 | export default FooterLinks;
80 |
--------------------------------------------------------------------------------
/src/components/Footer/components/GitLink.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const GitLink = () => {
4 | return (
5 |
24 | );
25 | };
26 |
27 | export default GitLink;
28 |
--------------------------------------------------------------------------------
/src/components/Footer/components/NewsLetter.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const NewsLetter = () => {
4 | return (
5 |
6 |
New To Duka?
7 |
8 | Subscribe to our newsletter to get updates on our lates offers!
9 |
10 |
11 |
12 |
17 |
18 |
25 |
26 |
27 |
28 |
29 | );
30 | };
31 |
32 | export default NewsLetter;
33 |
--------------------------------------------------------------------------------
/src/components/LeftColumn.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PromoCodes from "./PromoCodes";
3 | import CurrencyConverter from "./CurrencyConverter";
4 | import ProductFilter from "../components/ProductFilter/Index";
5 |
6 | const LeftColumn = () => {
7 | return (
8 |
9 |
10 |
11 | {/*shown only if there are no promocodes set*/}
12 |
13 |
New Stock!
14 |
15 | We have just restocked. Shop now fot the latest trends in fashion.
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | );
27 | };
28 |
29 | export default LeftColumn;
30 |
--------------------------------------------------------------------------------
/src/components/Loader/Index.css:
--------------------------------------------------------------------------------
1 | /*loader from https://projects.lukehaas.me/css-loaders/ */
2 |
3 | .loader-container {
4 | display: flex;
5 | width: 100vw;
6 | height: 100vh;
7 | background: #EEE8AA;
8 | align-items: center;
9 | justify-items: center;
10 | }
11 | .loader,
12 | .loader:before,
13 | .loader:after {
14 | background: #f17e0a;
15 | -webkit-animation: load1 1s infinite ease-in-out;
16 | animation: load1 1s infinite ease-in-out;
17 | width: 1em;
18 | height: 4em;
19 | }
20 | .loader {
21 | color: #f17e0a;
22 | text-indent: -9999em;
23 | margin: 88px auto;
24 | position: relative;
25 | font-size: 11px;
26 | -webkit-transform: translateZ(0);
27 | -ms-transform: translateZ(0);
28 | transform: translateZ(0);
29 | -webkit-animation-delay: -0.16s;
30 | animation-delay: -0.16s;
31 | }
32 | .loader:before,
33 | .loader:after {
34 | position: absolute;
35 | top: 0;
36 | content: "";
37 | }
38 | .loader:before {
39 | left: -1.5em;
40 | -webkit-animation-delay: -0.32s;
41 | animation-delay: -0.32s;
42 | }
43 | .loader:after {
44 | left: 1.5em;
45 | }
46 | @-webkit-keyframes load1 {
47 | 0%,
48 | 80%,
49 | 100% {
50 | box-shadow: 0 0;
51 | height: 4em;
52 | }
53 | 40% {
54 | box-shadow: 0 -2em;
55 | height: 5em;
56 | }
57 | }
58 | @keyframes load1 {
59 | 0%,
60 | 80%,
61 | 100% {
62 | box-shadow: 0 0;
63 | height: 4em;
64 | }
65 | 40% {
66 | box-shadow: 0 -2em;
67 | height: 5em;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/components/Loader/Index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import "./Index.css";
3 |
4 | const Index = () => {
5 | return (
6 |
9 | );
10 | };
11 |
12 | export default Index;
13 |
--------------------------------------------------------------------------------
/src/components/Menus/MainMenu.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Menu from '../UI/Menu/Menu';
3 | import MenuComponent from '../Menus/MenuComponent';
4 | import PropTypes from 'prop-types';
5 | import { NavLink } from 'react-router-dom';
6 |
7 | const MainMenu = (props) => {
8 | return (
9 |
27 | )
28 | };
29 |
30 | MainMenu.propTypes = {
31 | toggleSideBar: PropTypes.func.isRequired,
32 | cartItemNumber: PropTypes.number.isRequired
33 | };
34 |
35 | export default MainMenu;
--------------------------------------------------------------------------------
/src/components/Menus/MenuComponent.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import MenuItem from "../UI/MenuItem/MenuItem";
3 | import PropTypes from 'prop-types';
4 |
5 | const MenuComponent = (props) => {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 |
13 |
16 |
17 | )
18 | };
19 |
20 | MenuComponent.propTypes = {
21 | cartCount: PropTypes.number
22 | };
23 |
24 | export default MenuComponent;
25 |
--------------------------------------------------------------------------------
/src/components/Menus/SideMenu.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Menu from '../UI/Menu/Menu';
3 | import MenuComponent from '../Menus/MenuComponent';
4 | import PropTypes from 'prop-types';
5 | import mainMenu from "./MainMenu";
6 |
7 | const SideMenu = (props) => {
8 | return (
9 |
10 |
13 | {/**/}
14 |
15 | )
16 | };
17 |
18 | mainMenu.propTypes = {
19 | cartItemNumber: PropTypes.number.isRequired
20 | };
21 |
22 | export default SideMenu;
--------------------------------------------------------------------------------
/src/components/OrderSuccess.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Link } from "react-router-dom";
3 |
4 | const OrderSuccess = () => {
5 | return (
6 |
7 |
8 |
Success!
9 |
10 | Your order is successful. Thank you for shopping with us.
11 |
12 |
13 | Continue Shopping
14 |
15 |
16 | check your order object in your console
17 |
18 |
19 |
20 | );
21 | };
22 |
23 | export default OrderSuccess;
24 |
--------------------------------------------------------------------------------
/src/components/ProductCard/Index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import { NavLink } from "react-router-dom";
4 | import AddToWishList from "../AddToWishlist/AddToWishlist";
5 | import ProductFeatures from "../../components/ProductCard/ProductFeatures";
6 | import Ratings from "../Ratings/Ratings";
7 | import {
8 | currencyToUse,
9 | productPrice,
10 | productDiscountPrice,
11 | } from "../../Utility/currency";
12 | import "./ProductCard.css";
13 |
14 | const Index = (props) => {
15 | let currencyKeys = currencyToUse(props.currency);
16 | let currencyValue = currencyKeys.value;
17 | let currencyName = currencyKeys.name;
18 | let item = props.product;
19 |
20 | return (
21 |
22 |
23 |
24 |
25 |
26 |
30 |
31 | {item.sale ?
Sale : null}
32 |
37 | {item.discount_price ? (
38 |
39 | {productDiscountPrice(item.price, item.discount_price)}
40 |
41 | ) : null}
42 |
43 |
44 |
45 |
46 | {item.vendor ? item.vendor.name : null}
47 |
48 |
{item.name}
49 |
56 |
57 |
58 | {currencyName}
59 | {productPrice(item.price, currencyValue)}
60 |
61 | {item.discount_price ? (
62 |
63 |
64 | {currencyName}
65 |
66 | {productPrice(item.discount_price, currencyValue)}
67 |
68 | ) : null}
69 |
70 |
71 |
74 |
79 | View Item
80 |
81 |
82 |
83 |
84 |
85 | );
86 | };
87 |
88 | Index.propTypes = {
89 | product: PropTypes.object.isRequired,
90 | currency: PropTypes.object.isRequired,
91 | };
92 |
93 | export default Index;
94 |
--------------------------------------------------------------------------------
/src/components/ProductCard/ProductCard.css:
--------------------------------------------------------------------------------
1 | /* ------------- shop product cards --------------*/
2 |
3 | .shop-card {
4 | background-color: #fff;
5 | padding: 3px;
6 | }
7 |
8 | .shop-card .shop-card-image {
9 | text-align: center;
10 | position: relative;
11 | padding-top: .5rem;
12 | }
13 |
14 | .shop-card .shop-card-image a {
15 | display: block
16 | }
17 |
18 | .shop-card .shop-card-image img {
19 | width: 70%;
20 | height: auto
21 | }
22 |
23 | .shop-card .shop-card-sale {
24 | color: #fff;
25 | background-color: #E40046;
26 | font-size: 12px;
27 | font-weight: 700;
28 | text-transform: uppercase;
29 | padding: 2px 7px;
30 | display: block;
31 | position: absolute;
32 | top: 10px;
33 | left: 0
34 | }
35 |
36 | .shop-card .shop-card-wishlist {
37 | position: absolute;
38 | padding: 2px 7px;
39 | background-color: transparent;
40 | color: #e5e5e5 !important;
41 | font-size: 20px !important;
42 | left: auto;
43 | cursor: pointer;
44 | top: 3px;
45 | right: 0
46 | }
47 |
48 | .shop-card .wished {
49 | color: #E40046 !important;
50 | }
51 |
52 | .shop-card .shop-card-content {
53 | /* text-align: center; */
54 | padding: 15px 5px 5px;
55 | margin: 0 auto;
56 | }
57 |
58 | .shop-card .shop-card-vendor {
59 | color: #878787;
60 | font-size: 11px;
61 | font-weight: 500;
62 | text-transform: uppercase;
63 | }
64 |
65 | .shop-card .shop-card-title {
66 | white-space: nowrap;
67 | width: 100%; /* IE6 needs any width */
68 | overflow: hidden; /* "overflow" value must be different from visible"*/
69 | -o-text-overflow: ellipsis; /* Opera < 11*/
70 | text-overflow: ellipsis;
71 | font-size: 14px;
72 | font-weight: 400;
73 | color: #212121;
74 | letter-spacing: .5px;
75 | text-transform: capitalize;
76 | margin: 5px 0 5px;
77 | }
78 |
79 | .shop-card .shop-card-ratings-container {
80 | margin-bottom:5px;
81 | font-size: 12px;
82 | position : relative;
83 | }
84 |
85 | .shop-card .shop-card-ratings-container > span {
86 | margin-right:5px;
87 | }
88 |
89 | .full-star-icon,.half-star-icon{
90 | color:#fbb023;
91 | }
92 |
93 | .empty-star-icon{
94 | color:#999;
95 | }
96 |
97 | .total-rating-votes{
98 | position:relative;
99 | bottom: -2px;
100 | color:#999;
101 | margin-left: 5px !important;
102 | }
103 |
104 | .shop-card .shop-card-title a:hover,
105 | .shop-card:hover .shop-card-title a {
106 | color: #E40046;
107 | text-decoration: none;
108 | }
109 |
110 | .shop-card .shop-card-price {
111 | text-transform: capitalize;
112 | font-size: 16px;
113 | font-weight: 700;
114 | color: #212121;
115 | }
116 |
117 | .shop-card .shop-card-discount {
118 | position: absolute;
119 | bottom: -10px;
120 | right: 5px;
121 | font-size: 12px;
122 | background-color: #feefde;
123 | width: 40px;
124 | height: 40px;
125 | border-radius: 50%;
126 | color: #f68b1e;
127 | font-weight: bold;
128 | text-align: center;
129 | line-height: 40px;
130 | box-sizing: content-box;
131 | }
132 |
133 | .shop-card .shop-card-discount-price {
134 | color: #999;
135 | font-size: 12px;
136 | font-weight: 400;
137 | text-decoration: line-through;
138 | margin-left: 3px;
139 | display: inline-block;
140 | padding-left: 5px;
141 | }
142 |
143 | .shop-card .shop-card-features-container {
144 | margin-top: 2px;
145 | margin-bottom: 10px;
146 | }
147 |
148 | .shop-card .shop-card-product-features {
149 | font-size: 18px;
150 | color: #e40044bc;
151 | padding-right: 25px;
152 | }
153 |
154 | .shop-btn-primary{
155 | color: #F17E0A;
156 | font-size: 13px;
157 | background-color: transparent;
158 | font-weight: 600;
159 | border-color: #F17E0A;
160 | border-radius: 0;
161 | }
162 |
163 |
164 | .shop-btn-primary:focus,
165 | .shop-btn-primary.focus,
166 | .shop-btn-primary:hover,
167 | .shop-btn-primary:active,
168 | .shop-btn-primary:active:focus,
169 | .shop-btn-primary.active {
170 | color: #ffffff;
171 | background-color: #F17E0A;
172 | }
173 |
174 | /*cart and checkout buttons*/
175 |
176 | .shop-btn-secondary,
177 | .shop-btn-secondary:focus,
178 | .shop-btn-secondary.focus,
179 | .shop-btn-secondary:hover,
180 | .shop-btn-secondary:active,
181 | .shop-btn-secondary:active:focus,
182 | .shop-btn-secondary.active {
183 | color: #FFF;
184 | font-size: 1rem;
185 | background-color: #F17E0A;
186 | font-weight: 600;
187 | border-color: #F17E0A;
188 | }
--------------------------------------------------------------------------------
/src/components/ProductCard/ProductFeatures.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {
3 | LocalShipping,
4 | International,
5 | Warehouse,
6 | DukaApproved,
7 | } from "../UI/Icons/Icons";
8 |
9 | const ProductFeatures = (props) => {
10 | return (
11 |
12 | {props.product.duka_approved ? (
13 |
14 |
15 | {props.showText ? (
16 | Duka Aproved
17 | ) : null}
18 |
19 | ) : null}
20 | {props.product.fulfilled_by_duka ? (
21 |
22 |
23 | {props.showText ? (
24 | Fullfiled By Duka
25 | ) : null}
26 |
27 | ) : null}
28 | {props.product.shipped_from_abroad ? (
29 |
33 |
34 | {props.showText ? (
35 | Shipped From Abroad
36 | ) : null}
37 |
38 | ) : (
39 |
40 |
41 | {props.showText ? (
42 | Local Shippping
43 | ) : null}
44 |
45 | )}
46 |
47 | );
48 | };
49 |
50 | export default ProductFeatures;
51 |
--------------------------------------------------------------------------------
/src/components/ProductFilter/Index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { connect } from "react-redux";
3 | import { getUsedCurrency } from "../../store/selectors";
4 | import { currencyToUse, productPrice } from "../../Utility/currency";
5 | import { setProductPriceFilter } from "../../store/actions/index";
6 | import { getProductPriceFilter } from "../../store/selectors";
7 | import "./index.css";
8 |
9 | const Index = (props) => {
10 | let currencyKeys = currencyToUse(props.usedCurrencyProp);
11 | let currencyValue = currencyKeys.value;
12 | let currencyName = currencyKeys.name;
13 | let { min, max, pricerange } = props.productPricefilter;
14 | const rangeChanged = (event) => {
15 | props.updateProductFilterPrice(event.target.value);
16 | };
17 |
18 | return (
19 |
20 |
Filter by Price
21 |
22 | {`Max Price : `}
23 |
24 | {currencyName}
25 | {productPrice(pricerange, currencyValue)}
26 |
27 |
28 |
38 |
39 |
40 | {currencyName}
41 | {productPrice(min, currencyValue)}
42 |
43 |
44 | {currencyName}
45 | {productPrice(max, currencyValue)}
46 |
47 |
48 |
49 | );
50 | };
51 |
52 | const mapStateToProps = (state) => {
53 | return {
54 | usedCurrencyProp: getUsedCurrency(state),
55 | productPricefilter: getProductPriceFilter(state),
56 | };
57 | };
58 |
59 | const mapDispatchToProps = (dispatch) => {
60 | return {
61 | updateProductFilterPrice: (filterPrice) =>
62 | dispatch(setProductPriceFilter(filterPrice)),
63 | };
64 | };
65 |
66 | export default connect(mapStateToProps, mapDispatchToProps)(Index);
67 |
--------------------------------------------------------------------------------
/src/components/ProductFilter/index.css:
--------------------------------------------------------------------------------
1 | .price-filter {
2 | background: #fff;
3 | }
4 |
5 | .price-filter div {
6 | margin-bottom: 0.5rem;
7 | }
8 |
9 | .price-filter div span {
10 | font-weight: bold;
11 | }
12 |
13 | input[type="range"] {
14 | width: 100%; /* Specific width is required for Firefox. */
15 | }
16 |
17 | .price-ranges {
18 | font-size: .9rem;
19 | font-weight: normal;
20 | display: flex;
21 | justify-content: space-between;
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/ProductsDisplay/Index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import ProductCard from "../ProductCard/Index";
4 | import SecondaryLayout from "../../Layouts/SecondaryLayout";
5 | import EmptyCategoryPageContent from "../EmptyCategoryPageContent";
6 |
7 | const Index = (props) => {
8 | let products = ;
9 | let productsCount = props.products.length;
10 | if (productsCount > 0) {
11 | products = props.products.map((product) => {
12 | return (
13 |
18 | );
19 | });
20 | }
21 | return (
22 |
26 | {products}
27 |
28 | );
29 | };
30 |
31 | Index.propTypes = {
32 | products: PropTypes.array.isRequired,
33 | usedCurrency: PropTypes.object.isRequired,
34 | breadCrumbs: PropTypes.array.isRequired,
35 | };
36 |
37 | export default Index;
38 |
--------------------------------------------------------------------------------
/src/components/PromoCodes.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { connect } from "react-redux";
3 | import PropTypes from "prop-types";
4 |
5 | const PromoCodes = (props) => {
6 | return props.promoCodeProps.length > 0 ? (
7 |
8 | {props.showText ? (
9 |
10 | Great Discounts!
11 | Use the following promo codes to get amazing discounts
12 |
13 | ) : null}
14 |
15 | {props.promoCodeProps.map((promoCode, index) => (
16 | -
20 | {promoCode.code}
21 |
22 | {promoCode.percentage}%
23 |
24 |
25 | ))}
26 |
27 |
28 | ) : (
29 | props.children
30 | );
31 | };
32 |
33 | PromoCodes.propTypes = {
34 | promoCodeProps: PropTypes.array,
35 | showText: PropTypes.bool.isRequired,
36 | };
37 |
38 | const mapStateToProps = (state) => {
39 | return {
40 | promoCodeProps: state.promoCode,
41 | };
42 | };
43 |
44 | export default connect(mapStateToProps)(PromoCodes);
45 |
--------------------------------------------------------------------------------
/src/components/Ratings/Ratings.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from 'prop-types';
3 | import { EmptyStar, FullStar,HalfStar } from '../UI/Icons/Icons.jsx';
4 |
5 | const Ratings = (props) => {
6 |
7 | const createStars = () =>{
8 |
9 | let stars = [];
10 | let ratingsValue = props.ratings.star_ratings;
11 | const flooredRatingsValue = Math.floor(ratingsValue);
12 | const remainingRatingsValue = (ratingsValue-flooredRatingsValue).toFixed(1);
13 | const defaultCount = remainingRatingsValue > 0 ? 4 : 5;
14 | const remainingRatings = defaultCount - flooredRatingsValue;
15 |
16 | for(let i =0; i < flooredRatingsValue ; i++){
17 | stars.push();
18 | }
19 | if(remainingRatingsValue > 0){
20 | stars.push();
21 | }
22 | if(remainingRatings){
23 | for(let k =0; k < remainingRatings; k++){
24 | stars.push();
25 | }
26 | }
27 | return stars;
28 | }
29 |
30 | return (
31 |
32 | { createStars() }
33 | ({props.ratings.votes})
34 |
35 | );
36 |
37 | }
38 |
39 | Ratings.prototypes = {
40 | ratings: PropTypes.object.isRequired,
41 | containerClassName: PropTypes.string,
42 | fullStarIcon: PropTypes.string,
43 | halfStarIcon: PropTypes.string,
44 | emptyStarIcon: PropTypes.string,
45 | }
46 |
47 | export default Ratings;
--------------------------------------------------------------------------------
/src/components/UI/Alert/Alert.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | const Alert = (props) => {
5 | return (
6 |
10 | {props.children}
11 |
14 |
15 | );
16 | };
17 |
18 | Alert.propTypes = {
19 | closeAlert: PropTypes.func.isRequired,
20 | alertType: PropTypes.string.isRequired,
21 | };
22 |
23 | export default Alert;
24 |
--------------------------------------------------------------------------------
/src/components/UI/Backdrop/Backdrop.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | const BackDrop = (props) => {
5 | return props.showB ? (
6 |
7 | ) : null;
8 | };
9 |
10 | BackDrop.propTypes = {
11 | showBackDrop: PropTypes.bool.isRequired,
12 | closeSomething: PropTypes.func,
13 | };
14 |
15 | export default BackDrop;
16 |
--------------------------------------------------------------------------------
/src/components/UI/BreadCrumbs/BreadCrumbs.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import { NavLink } from "react-router-dom";
4 |
5 | const BreadCrumbs = (props) => {
6 | let links = props.breadCrumbLinks;
7 |
8 | function generateBreadCrumb() {
9 | let crumbs = [];
10 | if (links.length) {
11 | links.forEach((link, index) => {
12 | if (index === links.length - 1) {
13 | crumbs.push(
14 |
15 | {link.label}
16 |
17 | );
18 | } else {
19 | crumbs.push(
20 |
21 |
22 | {link.label}
23 |
24 |
25 | );
26 | }
27 | });
28 | }
29 | return crumbs;
30 | }
31 |
32 | return (
33 |
43 | );
44 | };
45 |
46 | BreadCrumbs.prototypes = {
47 | breadCrumbLinks: PropTypes.array,
48 | };
49 |
50 | export default BreadCrumbs;
51 |
--------------------------------------------------------------------------------
/src/components/UI/Icons/Icons.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | FaHeart,
4 | FaShippingFast,
5 | FaPlane,
6 | FaWarehouse,
7 | FaRegStar,
8 | FaStar,
9 | FaStarHalfAlt,
10 | FaAward
11 | } from "react-icons/fa";
12 |
13 | export const Heart = () => ;
14 | export const LocalShipping = () =>
15 | export const International = () =>
16 | export const Warehouse = () =>
17 | export const EmptyStar = () =>
18 | export const HalfStar = () =>
19 | export const FullStar = () =>
20 | export const DukaApproved = () =>
--------------------------------------------------------------------------------
/src/components/UI/Input/InputField.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropType from "prop-types";
3 |
4 | const InputField = (props) => {
5 | return (
6 |
7 |
8 | props.changed(event, "inputname")}
19 | />
20 | {!props.identifier.valid ? (
21 | {props.identifier.errorsMsg}
22 | ) : null}
23 |
24 | );
25 | };
26 |
27 | InputField.propTypes = {
28 | label: PropType.string,
29 | type: PropType.string,
30 | identifier: PropType.object.isRequired,
31 | changed: PropType.func.isRequired,
32 | };
33 |
34 | export default InputField;
35 |
--------------------------------------------------------------------------------
/src/components/UI/Menu/Menu.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | const Menu = (props) => {
5 | return ;
6 | };
7 |
8 | Menu.propTypes = {
9 | menuClasses: PropTypes.string.isRequired,
10 | };
11 |
12 | export default Menu;
13 |
--------------------------------------------------------------------------------
/src/components/UI/MenuItem/MenuItem.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import { NavLink } from "react-router-dom";
4 |
5 | const MenuItem = (props) => {
6 | return (
7 |
8 |
9 | {props.children}
10 |
11 |
12 | );
13 | };
14 |
15 | MenuItem.propTypes = {
16 | navItemClasses: PropTypes.string,
17 | navLinkClasses: PropTypes.string,
18 | linkTo: PropTypes.string,
19 | };
20 |
21 | export default MenuItem;
22 |
--------------------------------------------------------------------------------
/src/components/UI/Modal/Modal.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Backdrop from "../Backdrop/Backdrop";
3 | import PropTypes from "prop-types";
4 |
5 | const Modal = (props) => {
6 | return (
7 |
8 |
12 |
13 |
14 |
15 |
16 |
{props.children}
17 |
18 |
19 |
26 |
27 |
28 |
29 |
30 |
31 | );
32 | };
33 |
34 | Modal.propTypes = {
35 | closeModalClick: PropTypes.func.isRequired,
36 | showModal: PropTypes.bool.isRequired,
37 | };
38 |
39 | export default Modal;
40 |
--------------------------------------------------------------------------------
/src/components/UI/Wrappers/MainPageWrapper.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const MainPageWrapper = (props) => {
4 | return {props.children}
;
5 | };
6 |
7 | export default MainPageWrapper;
8 |
--------------------------------------------------------------------------------
/src/components/UI/Wrappers/PageContentWrapper.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const PageContentWrapper = (props) => {
4 | return {props.children}
;
5 | };
6 |
7 | export default PageContentWrapper;
8 |
--------------------------------------------------------------------------------
/src/components/UI/Wrappers/SideMenuWrapper.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | const SideMenuWrapper = (props) => {
5 | return (
6 |
10 | {props.children}
11 |
12 | );
13 | };
14 |
15 | SideMenuWrapper.propTypes = {
16 | showSideBar: PropTypes.bool.isRequired,
17 | toggleSideMenu: PropTypes.func.isRequired,
18 | };
19 |
20 | export default SideMenuWrapper;
21 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | background: #E6EEFB;
5 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
6 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
7 | sans-serif;
8 | -webkit-font-smoothing: antialiased;
9 | -moz-osx-font-smoothing: grayscale;
10 | }
11 |
12 | code {
13 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
14 | monospace;
15 | }
16 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import { BrowserRouter } from "react-router-dom";
4 | import { Provider } from "react-redux";
5 | import store from "./store/store";
6 | import "../node_modules/bootstrap/dist/css/bootstrap.min.css";
7 | import "./index.css";
8 | import { Elements, StripeProvider } from "react-stripe-elements";
9 | import App from "./App";
10 |
11 | const app = (
12 |
13 |
14 | {/* StripeProvider initializes the stripe and passes in the publishable key */}
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | );
23 |
24 | ReactDOM.render(app, document.getElementById("root"));
25 |
--------------------------------------------------------------------------------
/src/static/constants.js:
--------------------------------------------------------------------------------
1 | export const VISIBILITY_FILTERS = {
2 | ALL: "all",
3 | MEN: "men",
4 | WOMEN: "women",
5 | KIDS: "kids",
6 | SALE: "sale",
7 | };
8 |
--------------------------------------------------------------------------------
/src/static/data.js:
--------------------------------------------------------------------------------
1 | let data = {
2 | cart: [],
3 | wishlist: [],
4 | vat: 16, //vat in percentage
5 | cartTotal: 0,
6 | orderSuccess: false,
7 | promoCode: [
8 | {
9 | code: "TENPERCENT",
10 | percentage: 10,
11 | },
12 | {
13 | code: "FIVEPERCENT",
14 | percentage: 5,
15 | },
16 | ],
17 | usedPromoCode: null,
18 | deliveryOptions: [
19 | {
20 | id: 1,
21 | name: "standard",
22 | duration: "24 - 72 hours",
23 | cost: 300,
24 | },
25 | {
26 | id: 2,
27 | name: "fastest",
28 | duration: "1 - 24 hours",
29 | cost: 1000,
30 | },
31 | ],
32 | productMaxShowModal: false,
33 | modalMessage: null,
34 | showSideNavigation: false,
35 | // used currency should load with the default currency name and rate
36 | usedCurrency: { KES: 1, symbol: "Ksh " },
37 | // exchange rates can be got from any api source
38 | exchangeRates: {
39 | base: "KES",
40 | date: "2019-01-29",
41 | rates: {
42 | KES: 1,
43 | USD: 0.0099,
44 | GBP: 0.0075,
45 | EUR: 0.0087,
46 | TZS: 22.92,
47 | UGX: 36.33,
48 | NGN: 3.59,
49 | INR: 0.71,
50 | },
51 | },
52 | // overkill but doing it for fun
53 | currencySymbols: {
54 | KES: "Ksh ",
55 | USD: "$",
56 | GBP: "£",
57 | EUR: "€",
58 | TZS: "TSh ",
59 | UGX: "USh ",
60 | NGN: "₦",
61 | INR: "₹",
62 | },
63 | priceFilter: {
64 | min: 0,
65 | max: 3700,
66 | pricerange: 3700,
67 | },
68 | products: [
69 | {
70 | id: 1,
71 | name: "men's analog quartz watch",
72 | slug: "mens-analog-quartz-watch-547383",
73 | price: 500,
74 | discount_price: 2800,
75 | category: "men",
76 | color: "black",
77 | subcategory: "",
78 | sale: true,
79 | article: "watch",
80 | quantity: 5,
81 | img: "analog-quartz-watch.jpg",
82 | options: [1, 2, 3],
83 | fulfilled_by_duka: true,
84 | shipped_from_abroad: false,
85 | duka_approved: true,
86 | vendor: {
87 | id: 1,
88 | name: "duka",
89 | },
90 | ratings: {
91 | star_ratings: 4.8,
92 | votes: 350,
93 | },
94 | },
95 | {
96 | id: 2,
97 | name: "singedani four set handbag",
98 | slug: "singedani-four-set-handbag-647483",
99 | price: 1160,
100 | discount_price: 2320,
101 | category: "women",
102 | color: "gray",
103 | subcategory: "",
104 | sale: false,
105 | article: "handbag",
106 | quantity: 8,
107 | img: "singedani-handbag.jpg",
108 | options: [],
109 | fulfilled_by_duka: false,
110 | shipped_from_abroad: true,
111 | duka_approved: false,
112 | vendor: {
113 | id: 2,
114 | name: "vendor two",
115 | },
116 | ratings: {
117 | star_ratings: 3.6,
118 | votes: 200,
119 | },
120 | },
121 | {
122 | id: 3,
123 | name: "Boys gray boxer set",
124 | slug: "boys-gray-boxer-set-546488",
125 | price: 900,
126 | discount_price: 1200,
127 | category: "kids",
128 | color: "blue",
129 | sizes: ["S", "L", "M"],
130 | subcategory: "boys",
131 | sale: true,
132 | article: "boxer",
133 | quantity: 3,
134 | img: "boy_boxers.jpg",
135 | options: [2],
136 | fulfilled_by_duka: false,
137 | shipped_from_abroad: false,
138 | duka_approved: true,
139 | vendor: {
140 | id: 3,
141 | name: "vendor three",
142 | },
143 | ratings: {
144 | star_ratings: 2.5,
145 | votes: 150,
146 | },
147 | },
148 | {
149 | id: 4,
150 | name: "Hiamok men leather belt",
151 | slug: "hiamok-men-leather-belt-238192",
152 | price: 392,
153 | discount_price: 1098,
154 | category: "men",
155 | color: "brown",
156 | subcategory: "",
157 | sale: false,
158 | article: "belt",
159 | quantity: 10,
160 | img: "belt.jpg",
161 | options: [],
162 | fulfilled_by_duka: false,
163 | shipped_from_abroad: true,
164 | duka_approved: false,
165 | vendor: {
166 | id: 4,
167 | name: "vendor four",
168 | },
169 | ratings: {
170 | star_ratings: 3.8,
171 | votes: 20,
172 | },
173 | },
174 | {
175 | id: 5,
176 | name: "vintage print flare dress",
177 | slug: "vintage-print-flare-dress-987426",
178 | price: 1720,
179 | discount_price: 5160,
180 | category: "women",
181 | color: "White",
182 | sizes: ["S", "M", "L"],
183 | subcategory: "",
184 | sale: true,
185 | article: "dress",
186 | quantity: 0,
187 | img: "vintage-flare-dress.jpg",
188 | options: [],
189 | fulfilled_by_duka: false,
190 | shipped_from_abroad: false,
191 | duka_approved: true,
192 | vendor: {
193 | id: 5,
194 | name: "vendor five",
195 | },
196 | ratings: {
197 | star_ratings: 4.0,
198 | votes: 130,
199 | },
200 | },
201 | {
202 | id: 6,
203 | name: "capped sleeves red cotton dress",
204 | slug: "capped-sleeves-red-cotton-dress-349824",
205 | price: 1100,
206 | discount_price: 1650,
207 | category: "kids",
208 | color: "Red",
209 | sizes: ["S", "M"],
210 | subcategory: "girls",
211 | sale: true,
212 | article: "dress",
213 | quantity: 2,
214 | img: "cotton-dress.jpg",
215 | options: [],
216 | fulfilled_by_duka: true,
217 | shipped_from_abroad: true,
218 | duka_approved: true,
219 | vendor: {
220 | id: 1,
221 | name: "duka",
222 | },
223 | ratings: {
224 | star_ratings: 2.1,
225 | votes: 268,
226 | },
227 | },
228 | {
229 | id: 7,
230 | name: "gemch men casual running shoes",
231 | slug: "gemch-men-casual-running-shoes-459123",
232 | price: 3020,
233 | discount_price: 3580,
234 | category: "men",
235 | color: "black",
236 | sizes: ["39", "40", "42"],
237 | subcategory: "",
238 | sale: false,
239 | article: "shoes",
240 | quantity: 6,
241 | img: "gemch_shoes.jpg",
242 | options: [],
243 | fulfilled_by_duka: true,
244 | shipped_from_abroad: true,
245 | duka_approved: true,
246 | vendor: {
247 | id: 2,
248 | name: "vendor one",
249 | },
250 | ratings: {
251 | star_ratings: 4,
252 | votes: 250,
253 | },
254 | },
255 | {
256 | id: 8,
257 | name: "Boho printed floral dress",
258 | slug: "boho-printed-floral-dress-656623",
259 | price: 1999,
260 | discount_price: 2199,
261 | category: "women",
262 | color: "skyblue",
263 | sizes: ["M", "L", "XL"],
264 | subcategory: "",
265 | sale: true,
266 | article: "dress",
267 | quantity: 10,
268 | img: "floral-dress.jpg",
269 | options: [],
270 | fulfilled_by_duka: false,
271 | shipped_from_abroad: false,
272 | duka_approved: false,
273 | vendor: {
274 | id: 3,
275 | name: "vendor three",
276 | },
277 | ratings: {
278 | star_ratings: 3.6,
279 | votes: 129,
280 | },
281 | },
282 | {
283 | id: 9,
284 | name: "Baby girl bowknot leather shoes",
285 | slug: "baby-girl-bowknot-leather-shoes-312947",
286 | price: 493,
287 | discount_price: 502,
288 | category: "kids",
289 | color: "Silver",
290 | sizes: ["S"],
291 | subcategory: "girls",
292 | sale: false,
293 | article: "dress",
294 | quantity: 9,
295 | img: "leather-shoes.jpg",
296 | options: [],
297 | fulfilled_by_duka: false,
298 | shipped_from_abroad: true,
299 | duka_approved: false,
300 | vendor: {
301 | id: 4,
302 | name: "vendor four",
303 | },
304 | ratings: {
305 | star_ratings: 4.1,
306 | votes: 50,
307 | },
308 | },
309 | {
310 | id: 10,
311 | name: "men khaki trouser - navy blue",
312 | slug: "men-khaki-trouser-navy-blue-537329",
313 | price: 1346,
314 | discount_price: 1347,
315 | category: "men",
316 | color: "Navy Blue",
317 | sizes: ["M", "L"],
318 | subcategory: "",
319 | sale: false,
320 | article: "shoes",
321 | quantity: 0,
322 | img: "gsoft-khaki.jpg",
323 | options: [],
324 | fulfilled_by_duka: true,
325 | shipped_from_abroad: true,
326 | duka_approved: false,
327 | vendor: {
328 | id: 5,
329 | name: "vendor five",
330 | },
331 | ratings: {
332 | star_ratings: 2.0,
333 | votes: 35,
334 | },
335 | },
336 | {
337 | id: 11,
338 | name: "Women printed bodycon dress",
339 | slug: "women-printed-bodycon-dress-439618",
340 | price: 1554,
341 | discount_price: 1640,
342 | category: "women",
343 | sizes: ["M", "L", "XL"],
344 | subcategory: "",
345 | sale: false,
346 | article: "dress",
347 | quantity: 7,
348 | img: "bodycon-dress.jpg",
349 | options: [],
350 | fulfilled_by_duka: false,
351 | shipped_from_abroad: false,
352 | duka_approved: true,
353 | vendor: {
354 | id: 1,
355 | name: "duka",
356 | },
357 | ratings: {
358 | star_ratings: 3.2,
359 | votes: 240,
360 | },
361 | },
362 | {
363 | id: 12,
364 | name: "girl princess lace dress",
365 | slug: "girl-princess-lace-dress-123567",
366 | price: 1808,
367 | discount_price: 2350,
368 | category: "kids",
369 | color: "White",
370 | sizes: ["S", "M", "L"],
371 | subcategory: "girls",
372 | sale: true,
373 | article: "dress",
374 | quantity: 4,
375 | img: "princes-dress.jpg",
376 | options: [],
377 | fulfilled_by_duka: true,
378 | shipped_from_abroad: true,
379 | duka_approved: false,
380 | vendor: {
381 | id: 2,
382 | name: "vendor one",
383 | },
384 | ratings: {
385 | star_ratings: 3.6,
386 | votes: 70,
387 | },
388 | },
389 | {
390 | id: 13,
391 | name: "men's formal slim fit suit",
392 | slug: "mens-formal-slim-fit-suit-345987",
393 | price: 3627,
394 | discount_price: 6045,
395 | category: "men",
396 | color: "Dark Blue",
397 | sizes: ["M", "L", "XL"],
398 | subcategory: "",
399 | sale: true,
400 | article: "suit",
401 | quantity: 3,
402 | img: "slim-fit-suit.jpg",
403 | options: [],
404 | fulfilled_by_duka: true,
405 | shipped_from_abroad: false,
406 | duka_approved: false,
407 | vendor: {
408 | id: 3,
409 | name: "vendor three",
410 | },
411 | ratings: {
412 | star_ratings: 5.0,
413 | votes: 210,
414 | },
415 | },
416 | {
417 | id: 14,
418 | name: "Women's rome strappy gladiator loe flat flip",
419 | slug: "womens-rome-strappy-gladiator-loe-flat-flip-230978",
420 | price: 876,
421 | discount_price: 987,
422 | category: "women",
423 | sizes: ["25", "35", "40"],
424 | subcategory: "",
425 | sale: true,
426 | article: "sandals",
427 | quantity: 2,
428 | img: "gladiator-flat-flip.jpg",
429 | options: [],
430 | fulfilled_by_duka: true,
431 | shipped_from_abroad: false,
432 | duka_approved: true,
433 | vendor: {
434 | id: 4,
435 | name: "vendor four",
436 | },
437 | ratings: {
438 | star_ratings: 1.5,
439 | votes: 3,
440 | },
441 | },
442 | {
443 | id: 15,
444 | name: "navy long sleeved boys t-shirt",
445 | slug: "navy-long-sleeved-boys-tshirt-786534",
446 | price: 960,
447 | discount_price: 1200,
448 | category: "kids",
449 | color: "black",
450 | sizes: ["M", "L"],
451 | subcategory: "boys",
452 | sale: false,
453 | article: "dress",
454 | quantity: 0,
455 | img: "boys-t-shirt.jpg",
456 | options: [],
457 | fulfilled_by_duka: false,
458 | shipped_from_abroad: true,
459 | duka_approved: false,
460 | vendor: {
461 | id: 5,
462 | name: "vendor five",
463 | },
464 | ratings: {
465 | star_ratings: 2.9,
466 | votes: 65,
467 | },
468 | },
469 | {
470 | id: 16,
471 | name: "3 piece men's vest - white",
472 | slug: "3-piece-mens-vest-white-891267",
473 | price: 899,
474 | discount_price: 1800,
475 | category: "men",
476 | color: "White",
477 | sizes: ["M", "L"],
478 | subcategory: "",
479 | sale: true,
480 | article: "suit",
481 | quantity: 8,
482 | img: "vest.jpg",
483 | options: [],
484 | fulfilled_by_duka: true,
485 | shipped_from_abroad: true,
486 | duka_approved: false,
487 | vendor: {
488 | id: 1,
489 | name: "duka",
490 | },
491 | ratings: {
492 | star_ratings: 3.0,
493 | votes: 289,
494 | },
495 | },
496 | {
497 | id: 17,
498 | name: "checkers faix leather wrist watch",
499 | slug: "checkers-faix-leather-wrist-watch-120934",
500 | price: 341,
501 | discount_price: 443,
502 | category: "women",
503 | color: "Gold",
504 | subcategory: "",
505 | sale: true,
506 | article: "watch",
507 | quantity: 4,
508 | img: "quartz-wrist-watch.jpg",
509 | options: [],
510 | fulfilled_by_duka: false,
511 | shipped_from_abroad: true,
512 | duka_approved: true,
513 | vendor: {
514 | id: 2,
515 | name: "vendor two",
516 | },
517 | ratings: {
518 | star_ratings: 4.0,
519 | votes: 200,
520 | },
521 | },
522 | {
523 | id: 18,
524 | name: "boys black crew neck t-shirt",
525 | slug: "boys-black-crew-neck-tshirt-784301",
526 | price: 890,
527 | discount_price: 1200,
528 | category: "kids",
529 | color: "Black",
530 | sizes: ["S", "M"],
531 | subcategory: "boys",
532 | sale: true,
533 | article: "dress",
534 | quantity: 7,
535 | img: "crew-neck-tshirt.jpg",
536 | options: [],
537 | fulfilled_by_duka: false,
538 | shipped_from_abroad: false,
539 | duka_approved: false,
540 | vendor: {
541 | id: 3,
542 | name: "vendor three",
543 | },
544 | ratings: {
545 | star_ratings: 4.7,
546 | votes: 130,
547 | },
548 | },
549 | ],
550 | };
551 |
552 | export default data;
553 |
--------------------------------------------------------------------------------
/src/store/actions/actionTypes.js:
--------------------------------------------------------------------------------
1 | export const ADD_TO_CART = "ADD_TO_CART";
2 | export const REMOVE_FROM_CART = "REMOVE_FROM_CART";
3 | export const CLEAR_CART = "CLEAR_CART";
4 | export const UPDATE_CART_PRODUCT_COUNT = "UPDATE_CART_PRODUCT_COUNT";
5 | export const CONFIRM_ORDER_SUCCESS = "CONFIRM_ORDER_SUCCESS";
6 | export const CONFIRM_ORDER_FAILURE = "CONFIRM_ORDER_FAILURE";
7 | export const RESET_ORDER_SUCCESS = "RESET_ORDER_SUCCESS";
8 | export const CLOSE_MAX_PRODUCT_MODAL = "CLOSE_MAX_PRODUCT_MODAL";
9 | export const TOGGLE_SIDE_BAR = "TOGGLE_SIDE_BAR";
10 | export const SET_PROMO_CODE = "SET_PROMO_CODE";
11 | export const CHANGE_CURRENCY = "CHANGE_CURRENCY";
12 | export const TOOLE_ITEM_IN_WISHLIST = "TOOLE_ITEM_IN_WISHLIST";
13 | export const SET_PRODUCT_PRICE_FILTER = "SET_PRODUCT_PRICE_FILTER";
14 |
--------------------------------------------------------------------------------
/src/store/actions/index.js:
--------------------------------------------------------------------------------
1 | import * as actionTypes from "./actionTypes";
2 |
3 | export const setProductPriceFilter = (price) => {
4 | return { type: actionTypes.SET_PRODUCT_PRICE_FILTER, price: price };
5 | };
6 |
7 | export const addToCart = (productDetails) => {
8 | return {
9 | type: actionTypes.ADD_TO_CART,
10 | productDetails: productDetails,
11 | };
12 | };
13 |
14 | export const removeFromCart = (productDetails) => {
15 | return {
16 | type: actionTypes.REMOVE_FROM_CART,
17 | productDetails: productDetails,
18 | };
19 | };
20 |
21 | export const clearCart = () => {
22 | return {
23 | type: actionTypes.CLEAR_CART,
24 | };
25 | };
26 |
27 | export const updateCartProductCount = (value, productDetails) => {
28 | return {
29 | type: actionTypes.UPDATE_CART_PRODUCT_COUNT,
30 | newCountValue: value,
31 | productDetails: productDetails,
32 | };
33 | };
34 |
35 | export const confirmOrder = (order, ownProps) => {
36 | return (dispatch) => {
37 | // send order object to an end point of choice
38 | console.log(order);
39 | // todo
40 | //token to be used with stripe
41 | dispatch(confirmOrderSuccess());
42 | ownProps.history.push("/cart");
43 | setTimeout(() => {
44 | dispatch(resetOrderSuccess());
45 | }, 5000);
46 | };
47 | };
48 |
49 | export const closeMaxProductModal = () => {
50 | return {
51 | type: actionTypes.CLOSE_MAX_PRODUCT_MODAL,
52 | };
53 | };
54 |
55 | export const confirmOrderSuccess = () => {
56 | return {
57 | type: actionTypes.CONFIRM_ORDER_SUCCESS,
58 | };
59 | };
60 |
61 | export const resetOrderSuccess = () => {
62 | return {
63 | type: actionTypes.RESET_ORDER_SUCCESS,
64 | };
65 | };
66 |
67 | export const confirmOrderFailure = () => {
68 | // todo
69 | return {
70 | type: actionTypes.CONFIRM_ORDER_FAILURE,
71 | };
72 | };
73 |
74 | export const toogleSideBar = () => {
75 | return {
76 | type: actionTypes.TOGGLE_SIDE_BAR,
77 | };
78 | };
79 |
80 | export const setPromoCode = (promoCodeObject) => {
81 | return {
82 | type: actionTypes.SET_PROMO_CODE,
83 | promoCode: promoCodeObject,
84 | };
85 | };
86 |
87 | export const changeCurrency = (currencyName) => {
88 | // currency value can be fetched here from an external api and then passes to the store
89 | return {
90 | type: actionTypes.CHANGE_CURRENCY,
91 | currencyName: currencyName,
92 | };
93 | };
94 |
95 | export const toogleItemInWishList = (productId) => {
96 | return {
97 | type: actionTypes.TOOLE_ITEM_IN_WISHLIST,
98 | productId: productId,
99 | };
100 | };
101 |
--------------------------------------------------------------------------------
/src/store/reducers/index.js:
--------------------------------------------------------------------------------
1 | import * as actionTypes from "../actions/actionTypes";
2 | import Data from "../../static/data";
3 |
4 | const initialState = Data;
5 |
6 | const appReducer = (state = initialState, action) => {
7 | switch (action.type) {
8 | case actionTypes.SET_PRODUCT_PRICE_FILTER:
9 | let productPriceFilter = state.priceFilter;
10 | productPriceFilter = {
11 | ...productPriceFilter,
12 | pricerange: parseInt(action.price),
13 | };
14 | return {
15 | ...state,
16 | priceFilter: productPriceFilter,
17 | };
18 | case actionTypes.ADD_TO_CART:
19 | let newCart = [...state.cart];
20 | let newCartTotal = state.cartTotal;
21 | let productMaxShowModal = state.productMaxShowModal;
22 | let modalMessage = null;
23 |
24 | let toAddProduct = action.productDetails;
25 | let cart = state.cart;
26 |
27 | var filteredCartItems = cart.filter(
28 | (product) => product.id === toAddProduct.id
29 | );
30 |
31 | let quantityToAdd = parseInt(toAddProduct.quantity);
32 | if (filteredCartItems.length) {
33 | if (toAddProduct.size) {
34 | let prodIndex = cart.findIndex(
35 | (product) =>
36 | product.id === toAddProduct.id &&
37 | product.size === toAddProduct.size
38 | );
39 | if (prodIndex > -1) {
40 | let itemToModify = newCart[prodIndex];
41 | newCart[prodIndex] = {
42 | ...itemToModify,
43 | quantity: parseInt(itemToModify.quantity) + quantityToAdd,
44 | };
45 | } else {
46 | newCart = cart.concat(toAddProduct);
47 | }
48 | } else {
49 | let prodIndex = cart.findIndex(
50 | (product) => product.id === toAddProduct.id
51 | );
52 | let itemToModify = newCart[prodIndex];
53 | newCart[prodIndex] = {
54 | ...itemToModify,
55 | quantity: parseInt(itemToModify.quantity) + quantityToAdd,
56 | };
57 | }
58 | newCartTotal = state.cartTotal + quantityToAdd;
59 | } else {
60 | newCart = cart.concat(toAddProduct);
61 | newCartTotal = state.cartTotal + quantityToAdd;
62 | }
63 |
64 | return {
65 | ...state,
66 | cartTotal: newCartTotal,
67 | cart: newCart,
68 | productMaxShowModal: productMaxShowModal,
69 | modalMessage: modalMessage,
70 | };
71 |
72 | case actionTypes.REMOVE_FROM_CART:
73 | let toRemoveProduct = action.productDetails;
74 | let removeIndex = null;
75 | let cartToRemove = [...state.cart];
76 |
77 | if (toRemoveProduct.size) {
78 | removeIndex = cartToRemove.findIndex(
79 | (product) =>
80 | product.id === toRemoveProduct.id &&
81 | product.size === toRemoveProduct.size
82 | );
83 | } else {
84 | removeIndex = cartToRemove.findIndex(
85 | (product) => product.id === toRemoveProduct.id
86 | );
87 | }
88 |
89 | cartToRemove.splice(removeIndex, 1);
90 |
91 | return {
92 | ...state,
93 | cart: cartToRemove,
94 | cartTotal: state.cartTotal - toRemoveProduct.quantity,
95 | };
96 |
97 | case actionTypes.CLEAR_CART:
98 | return {
99 | ...state,
100 | cartTotal: 0,
101 | cart: [],
102 | };
103 |
104 | case actionTypes.UPDATE_CART_PRODUCT_COUNT:
105 | let cartToUpdate = [...state.cart];
106 | let prodToUpdate = action.productDetails;
107 | let updateIndex = null;
108 | if (prodToUpdate.size) {
109 | updateIndex = state.cart.findIndex(
110 | (product) =>
111 | product.id === prodToUpdate.id && product.size === prodToUpdate.size
112 | );
113 | } else {
114 | updateIndex = state.cart.findIndex(
115 | (product) => product.id === prodToUpdate.id
116 | );
117 | }
118 |
119 | let cartTotal = state.cartTotal;
120 | if (updateIndex > -1) {
121 | let itemToModify = cartToUpdate[updateIndex];
122 | cartToUpdate[updateIndex] = {
123 | ...itemToModify,
124 | quantity: parseInt(action.newCountValue),
125 | };
126 | cartTotal -= itemToModify.quantity - action.newCountValue;
127 | }
128 |
129 | return {
130 | ...state,
131 | cart: cartToUpdate,
132 | cartTotal: cartTotal,
133 | };
134 |
135 | case actionTypes.CONFIRM_ORDER_SUCCESS:
136 | return {
137 | ...state,
138 | cart: [],
139 | cartTotal: 0,
140 | orderSuccess: true,
141 | };
142 |
143 | case actionTypes.RESET_ORDER_SUCCESS:
144 | return {
145 | ...state,
146 | orderSuccess: false,
147 | };
148 |
149 | case actionTypes.CONFIRM_ORDER_FAILURE:
150 | return {
151 | ...state,
152 | };
153 |
154 | case actionTypes.CLOSE_MAX_PRODUCT_MODAL:
155 | return {
156 | ...state,
157 | productMaxShowModal: !state.productMaxShowModal,
158 | };
159 |
160 | case actionTypes.TOGGLE_SIDE_BAR:
161 | return {
162 | ...state,
163 | showSideNavigation: !state.showSideNavigation,
164 | };
165 |
166 | case actionTypes.SET_PROMO_CODE:
167 | return {
168 | ...state,
169 | usedPromoCode: action.promoCode,
170 | };
171 |
172 | case actionTypes.CHANGE_CURRENCY: {
173 | let currencyName = null;
174 | let currencyValue = null;
175 | let currencyObj = {};
176 |
177 | let currencyNameSearch = Object.keys(state.exchangeRates.rates).filter(
178 | (rate) => action.currencyName === rate
179 | );
180 | if (currencyNameSearch) {
181 | currencyName = action.currencyName;
182 | currencyValue = state.exchangeRates.rates[currencyName];
183 |
184 | currencyObj[currencyName] = currencyValue;
185 | currencyObj["symbol"] = state.currencySymbols[currencyName];
186 | }
187 |
188 | return {
189 | ...state,
190 | // just in case the currency is not found
191 | usedCurrency: currencyNameSearch
192 | ? currencyObj
193 | : this.state.usedCurrency,
194 | };
195 | }
196 |
197 | case actionTypes.TOOLE_ITEM_IN_WISHLIST:
198 | let wisList = state.wishlist;
199 | let chkProductInWishList = state.wishlist.find(
200 | (id) => id === action.productId
201 | );
202 | if (chkProductInWishList) {
203 | // remove from wish list
204 | wisList = state.wishlist.filter((id) => id !== action.productId);
205 | } else {
206 | // addd to wish list
207 | wisList = state.wishlist.concat(action.productId);
208 | }
209 |
210 | return {
211 | ...state,
212 | wishlist: wisList,
213 | };
214 |
215 | default:
216 | return {
217 | ...state,
218 | };
219 | }
220 | };
221 |
222 | export default appReducer;
223 |
--------------------------------------------------------------------------------
/src/store/selectors.js:
--------------------------------------------------------------------------------
1 | import { VISIBILITY_FILTERS } from "../static/constants";
2 |
3 | export const getProducts = (store) => store.products;
4 | export const getProductPriceFilter = (store) => store.priceFilter;
5 | export const getWishlist = (store) => store.wishlist;
6 |
7 | export const getProductsByFilter = (store, visibilityFilter, count = null) => {
8 | const allProducts = getProducts(store);
9 | const filterPrices = getProductPriceFilter(store);
10 | switch (visibilityFilter) {
11 | case VISIBILITY_FILTERS.MEN:
12 | case VISIBILITY_FILTERS.WOMEN:
13 | case VISIBILITY_FILTERS.KIDS:
14 | return allProducts.filter(
15 | (product) =>
16 | product.category === visibilityFilter &&
17 | product.price < filterPrices.pricerange
18 | );
19 | case VISIBILITY_FILTERS.SALE:
20 | if (count) {
21 | return allProducts.filter((product, index) => {
22 | if (product.sale === true && index < 6) {
23 | return true;
24 | }
25 | return false;
26 | });
27 | } else {
28 | return allProducts.filter(
29 | (product) =>
30 | product.sale === true && product.price < filterPrices.pricerange
31 | );
32 | }
33 | case VISIBILITY_FILTERS.ALL:
34 | default:
35 | return allProducts.filter((product) => {
36 | return product.price < filterPrices.pricerange;
37 | });
38 | }
39 | };
40 |
41 | export const getUsedCurrency = (store) => store.usedCurrency;
42 |
--------------------------------------------------------------------------------
/src/store/store.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware } from "redux";
2 | import thunk from "redux-thunk";
3 | import { composeWithDevTools } from "redux-devtools-extension";
4 | import shopReducer from "./reducers";
5 |
6 | // use this to show redux dev tool
7 | const store = createStore(
8 | shopReducer,
9 | composeWithDevTools(applyMiddleware(thunk))
10 | );
11 |
12 | // const store = createStore(shopReducer, applyMiddleware(thunk));
13 |
14 | export default store;
15 |
--------------------------------------------------------------------------------
/src/views/All.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { connect } from "react-redux";
3 | import PropTypes from "prop-types";
4 | import { VISIBILITY_FILTERS } from "../static/constants";
5 | import { getProductsByFilter, getUsedCurrency } from "../store/selectors";
6 | import ProductsDisplay from "../components/ProductsDisplay/Index";
7 |
8 | const All = (props) => {
9 | return (
10 |
20 | );
21 | };
22 |
23 | const mapStateToProps = (state) => {
24 | return {
25 | productsProps: getProductsByFilter(state, VISIBILITY_FILTERS.ALL),
26 | usedCurrencyProp: getUsedCurrency(state),
27 | };
28 | };
29 |
30 | All.propTypes = {
31 | productsProps: PropTypes.array.isRequired,
32 | usedCurrencyProp: PropTypes.object.isRequired,
33 | };
34 |
35 | export default connect(mapStateToProps)(All);
36 |
--------------------------------------------------------------------------------
/src/views/Cart.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { connect } from "react-redux";
3 | import { Link } from "react-router-dom";
4 | import {
5 | removeFromCart,
6 | clearCart,
7 | updateCartProductCount,
8 | } from "../store/actions";
9 | import CartProduct from "../components/Cart/CartProducts";
10 | import CartProductTotals from "../components/Cart/CartProductTotals";
11 | import OrderSuccess from "../components/OrderSuccess";
12 | import PropTypes from "prop-types";
13 | import { currencyToUse } from "../Utility/currency";
14 |
15 | const Cart = (props) => {
16 | const productCountHandler = (field_value, product_details) => {
17 | props.updateCartProductCountProp(field_value, product_details);
18 | };
19 |
20 | let cartContent = null;
21 | let currencyKeys = currencyToUse(props.usedCurrencyProp);
22 | let currencyValue = currencyKeys.value;
23 |
24 | if (props.cartTotalProp > 0) {
25 | let cartPriceCountArray = [];
26 | let cartProducts = props.cartProductsProp.map((productInCart, index) => {
27 | // fetch product information from source based on id
28 | // product information can also be stored in state
29 | let productFromStore = props.productProps.find(
30 | (product) => product.id === productInCart.id
31 | );
32 | cartPriceCountArray.push({
33 | price:
34 | productFromStore.quantity > 0
35 | ? Math.round(productFromStore.price * currencyValue)
36 | : 0,
37 | count: productInCart.quantity,
38 | });
39 | return (
40 |
52 | productCountHandler(event.target.value, productInCart)
53 | }
54 | removeCartProduct={() =>
55 | props.removeProductFromCartProp(productInCart)
56 | }
57 | currency={props.usedCurrencyProp}
58 | />
59 | );
60 | });
61 |
62 | let cartTotals = (
63 | acc + el.price * el.count,
66 | 0
67 | )}
68 | vat={props.vatProp}
69 | clearCart={() => props.clearProductsFromCartProp()}
70 | currency={props.usedCurrencyProp}
71 | />
72 | );
73 |
74 | cartContent = (
75 |
76 | {cartProducts}
77 | {cartTotals}
78 |
79 | );
80 | } else if (props.cartTotalProp === 0 && props.orderSuccessProp) {
81 | cartContent = ;
82 | } else {
83 | cartContent = (
84 |
85 | Your cart is empty. Please fill it up.
86 |
87 | );
88 | }
89 |
90 | return (
91 |
94 | );
95 | };
96 |
97 | const mapStateToProps = (state) => {
98 | return {
99 | productProps: state.products,
100 | cartTotalProp: state.cartTotal,
101 | cartProductsProp: state.cart,
102 | vatProp: state.vat,
103 | orderSuccessProp: state.orderSuccess,
104 | usedCurrencyProp: state.usedCurrency,
105 | };
106 | };
107 |
108 | const mapDispatchToProps = (dispatch) => {
109 | return {
110 | removeProductFromCartProp: (productDetails) =>
111 | dispatch(removeFromCart(productDetails)),
112 | clearProductsFromCartProp: () => dispatch(clearCart()),
113 | updateCartProductCountProp: (value, productDetails) =>
114 | dispatch(updateCartProductCount(Number(value), productDetails)),
115 | };
116 | };
117 |
118 | Cart.propTypes = {
119 | cartTotalProp: PropTypes.number.isRequired,
120 | cartProductsProp: PropTypes.array.isRequired,
121 | productProps: PropTypes.array.isRequired,
122 | orderSuccessProp: PropTypes.bool.isRequired,
123 | vatProp: PropTypes.number,
124 | usedCurrencyProp: PropTypes.object.isRequired,
125 | };
126 |
127 | export default connect(mapStateToProps, mapDispatchToProps)(Cart);
128 |
--------------------------------------------------------------------------------
/src/views/Checkout.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { Redirect } from "react-router-dom";
3 | import { connect } from "react-redux";
4 | import { confirmOrder, setPromoCode } from "../store/actions";
5 | import CheckoutCartProduct from "../components/Checkout/CheckoutCartProduct";
6 | import PromoCodeForm from "../components/Checkout/PromoCodeForm";
7 | import PromoCodeValue from "../components/Checkout/PromoCodeValue";
8 | import CheckoutCartTotals from "../components/Checkout/CheckoutCartTotals";
9 | import CustomerInputs from "../components/Checkout/Forms/CustomerInputs";
10 | import DeliveryOptions from "../components/Checkout/Forms/DeliveryOptions";
11 | import PaymentOptions from "../components/Checkout/Forms/Payments/PaymentOptions";
12 | import Alert from "../components/UI/Alert/Alert";
13 | import PropTypes from "prop-types";
14 | import formValidator from "../Utility/formValidation";
15 | import { CardElement, injectStripe } from "react-stripe-elements";
16 | import { currencyToUse } from "../Utility/currency";
17 |
18 | class Checkout extends Component {
19 | state = {
20 | promoCode: "",
21 | showAlert: false,
22 | alertType: "",
23 | alertMessage: "",
24 | paymentMethod: "creditCard",
25 | shippingPrice: 300,
26 | usedDeliveryOption: 1,
27 | makeOrder: false,
28 | correctCardInfo: false,
29 | customerInfo: {
30 | firstName: {
31 | value: "",
32 | valid: false,
33 | touched: false,
34 | errorsMsg: "",
35 | },
36 | secondName: {
37 | value: "",
38 | valid: false,
39 | touched: false,
40 | errorsMsg: "",
41 | },
42 | email: {
43 | value: "",
44 | valid: false,
45 | touched: false,
46 | errorsMsg: "",
47 | },
48 | },
49 | };
50 |
51 | customerInfoChangeHandler = (event, identifier) => {
52 | // use deep cloning to be able to get the values of nested objects
53 | const customerInfo = { ...this.state.customerInfo };
54 | const customerInfoField = { ...customerInfo[identifier] };
55 | customerInfoField.value = event.target.value;
56 | const validationResults = formValidator(
57 | identifier,
58 | customerInfoField.value
59 | );
60 | customerInfoField.valid = validationResults.isValid;
61 | customerInfoField.errorsMsg = validationResults.errorsMsg;
62 | customerInfoField.touched = true;
63 | customerInfo[identifier] = customerInfoField;
64 |
65 | let makeOrder = true;
66 | for (let identifier in customerInfo) {
67 | makeOrder = customerInfo[identifier].valid && makeOrder;
68 | }
69 | this.setState({ customerInfo: customerInfo, makeOrder: makeOrder });
70 | };
71 |
72 | promoCodeChangeHandler = (event) => {
73 | this.setState({ promoCode: event.target.value });
74 | };
75 |
76 | paymentOptionChangeHandler = (event) => {
77 | if (event.target.value === "creditCard") {
78 | this.setState({ correctCardInfo: false });
79 | } else {
80 | this.setState({ correctCardInfo: true });
81 | }
82 | this.setState({ paymentMethod: event.target.value });
83 | };
84 |
85 | confirmOrderHandler = (event) => {
86 | event.preventDefault();
87 | let order = {};
88 | order["cart"] = this.props.cartProductsProps;
89 | order["user"] = {
90 | firstName: this.state.customerInfo.firstName.value,
91 | secondName: this.state.customerInfo.secondName.value,
92 | email: this.state.customerInfo.email.value,
93 | };
94 | order["usedPromoCode"] = this.state.promoCode;
95 | order["currency"] = this.props.usedCurrencyProp;
96 | order["paymentMethod"] = this.state.paymentMethod;
97 | order["deliveryOption"] = this.state.usedDeliveryOption;
98 |
99 | // todo
100 | // create stripe token for payments
101 | this.props.confirmOrderProp(order);
102 | };
103 |
104 | setPromoCode = (event) => {
105 | event.preventDefault();
106 | // check promo code in state
107 | let getPromoCode = this.props.promoCodeProp.filter(
108 | (codeName) => codeName.code === this.state.promoCode
109 | );
110 |
111 | if (getPromoCode.length > 0) {
112 | this.props.setPromoCodeProp(getPromoCode[0]);
113 | this.setState({
114 | showAlert: true,
115 | alertType: "alert-success",
116 | alertMessage: `The promo code you entered has given you a ${getPromoCode[0].percentage}% discount on the total price.`,
117 | });
118 | } else {
119 | this.setState({
120 | showAlert: true,
121 | alertType: "alert alert-danger",
122 | alertMessage: "The Promo code you entered does not have discounts",
123 | });
124 | }
125 | };
126 |
127 | closeAlertHandler = () => {
128 | this.setState({
129 | showAlert: !this.state.showAlert,
130 | alertType: "",
131 | alertMessage: "",
132 | });
133 | };
134 |
135 | deliveryOptionChangeHandler = (event) => {
136 | //get used delivery option from the state
137 | let deliveryOption = this.props.deliveryOptions.find(
138 | (option) => option.id === parseInt(event.target.value)
139 | );
140 | if (deliveryOption) {
141 | this.setState({
142 | usedDeliveryOption: parseInt(event.target.value),
143 | shippingPrice: deliveryOption.cost,
144 | });
145 | }
146 | };
147 |
148 | creditCardHandler = (element) => {
149 | if (element.complete) {
150 | this.setState({ correctCardInfo: true });
151 | }
152 | };
153 |
154 | render() {
155 | let productsPrices = [];
156 | let chosenPaymentMethod = null;
157 | let currencyKeys = currencyToUse(this.props.usedCurrencyProp);
158 | let currencyValue = currencyKeys.value;
159 |
160 | const cartProducts = this.props.cartProductsProps.map(
161 | (cartProduct, index) => {
162 | // fetch product information from source based on id
163 | let productFromStore = this.props.productsProps.find(
164 | (product) => product.id === cartProduct.id
165 | );
166 | productsPrices.push({
167 | price:
168 | productFromStore.quantity > 0
169 | ? Math.round(productFromStore.price * currencyValue)
170 | : 0,
171 | count: cartProduct.quantity,
172 | });
173 | return (
174 |
186 | );
187 | }
188 | );
189 |
190 | let shippingPrice = this.state.shippingPrice
191 | ? Math.round(this.state.shippingPrice * currencyValue)
192 | : 0;
193 | let productTotals = productsPrices.reduce(
194 | (acc, el) => acc + el.price * el.count,
195 | 0
196 | );
197 | let vatPercentage = this.props.vatProps > 0 ? this.props.vatProps / 100 : 0;
198 | let vat = productTotals > 0 ? Math.round(productTotals * vatPercentage) : 0;
199 | let percentageDiscount = this.props.usedPromoCodeProp
200 | ? this.props.usedPromoCodeProp.percentage / 100
201 | : 0;
202 | let discountAmount = productTotals * percentageDiscount;
203 | let shoppingTotal =
204 | productTotals > 0
205 | ? productTotals + vat + shippingPrice - discountAmount
206 | : 0;
207 |
208 | if (this.state.paymentMethod === "creditCard") {
209 | chosenPaymentMethod = (
210 |
211 | this.creditCardHandler(element)}
213 | />
214 |
215 | );
216 | } else if (this.state.paymentMethod === "onDelivery") {
217 | chosenPaymentMethod = (
218 |
219 | You will pay when the product is delivered to you.
220 |
221 | );
222 | }
223 |
224 | return (
225 |
226 | {this.props.cartTotalProps <= 0 ?
: null}
227 |
228 | {this.state.showAlert ? (
229 |
233 | {this.state.alertMessage}
234 |
235 | ) : null}
236 |
237 |
238 |
239 |
240 | Order Review
241 |
242 | {this.props.cartTotalProps}
243 |
244 |
245 |
246 |
247 | {/* items in cart */}
248 | {cartProducts}
249 |
250 | {/* used promo codes */}
251 | {this.props.usedPromoCodeProp ? (
252 |
257 | ) : null}
258 |
259 | {/* checkout totals */}
260 |
267 |
268 |
269 | {/*promo code form */}
270 |
273 | this.promoCodeChangeHandler(event)
274 | }
275 | promoCode={this.state.promoCode}
276 | />
277 |
278 |
279 |
Billing Information
280 |
315 |
316 |
317 |
318 | );
319 | }
320 | }
321 |
322 | Checkout.propTypes = {
323 | productsProps: PropTypes.array.isRequired,
324 | cartProductsProps: PropTypes.array.isRequired,
325 | cartTotalProps: PropTypes.number.isRequired,
326 | promoCodeProp: PropTypes.array,
327 | usedPromoCodeProp: PropTypes.object,
328 | deliveryOptions: PropTypes.array.isRequired,
329 | usedCurrencyProp: PropTypes.object.isRequired,
330 | vatProps: PropTypes.number,
331 | };
332 |
333 | Checkout.defaultProps = {
334 | shippingPriceProp: 0,
335 | };
336 |
337 | const mapStateToProps = (state) => {
338 | return {
339 | productsProps: state.products,
340 | cartProductsProps: state.cart,
341 | cartTotalProps: state.cartTotal,
342 | vatProps: state.vat,
343 | promoCodeProp: state.promoCode,
344 | usedPromoCodeProp: state.usedPromoCode,
345 | deliveryOptions: state.deliveryOptions,
346 | usedCurrencyProp: state.usedCurrency,
347 | };
348 | };
349 |
350 | const mapDispatchToProps = (dispatch, ownProps) => {
351 | return {
352 | confirmOrderProp: (order) => dispatch(confirmOrder(order, ownProps)),
353 | setPromoCodeProp: (promoCode, percentage) =>
354 | dispatch(setPromoCode(promoCode, percentage)),
355 | };
356 | };
357 |
358 | // inject stripe prop into the component
359 | export default connect(
360 | mapStateToProps,
361 | mapDispatchToProps
362 | )(injectStripe(Checkout));
363 |
--------------------------------------------------------------------------------
/src/views/Home/Home.css:
--------------------------------------------------------------------------------
1 | /* general */
2 |
3 | .card {
4 | border: 0px;
5 | border-radius: 0;
6 | }
7 |
8 | .card-img-top {
9 | border-radius: 0;
10 | }
11 |
12 | .shadow {
13 | box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.1) !important;
14 | }
15 |
16 | .btn-primary,
17 | .btn-primary:disabled {
18 | background-color: #333333;
19 | border-color: #333333;
20 | color: #ffffff;
21 | border-radius: 0;
22 | }
23 |
24 | .btn-primary:disabled {
25 | cursor: not-allowed;
26 | }
27 |
28 | .btn-primary:focus,
29 | .btn-primary.focus,
30 | .btn-primary:hover,
31 | .btn-primary:active,
32 | .btn-primary.active,
33 | .btn-primary:active:focus {
34 | background-color: #f17e0a !important;
35 | border-color: #f17e0a !important;
36 | color: #ffffff !important;
37 | }
38 |
39 | .form-control {
40 | border-radius: 0rem;
41 | }
42 |
43 | /* banner section */
44 |
45 | .main-banner-container {
46 | max-height: 500px;
47 | }
48 |
49 | .main-banner-content {
50 | padding: 2rem;
51 | background: #ffffff;
52 | display: grid;
53 | grid-template-columns: repeat(2, 1fr);
54 | }
55 |
56 | .main-banner-text {
57 | align-self: center;
58 | }
59 |
60 | .main-banner-title {
61 | font-weight: 500;
62 | color: #f17e0a !important;
63 | }
64 |
65 | .main-banner-image {
66 | height: inherit;
67 | justify-self: end;
68 | }
69 |
70 | /* sellout section */
71 | .sellout-section {
72 | display: grid;
73 | grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
74 | grid-gap: 1rem;
75 | }
76 |
77 | .sellout-card {
78 | align-items: center;
79 | padding: 1.25rem 2rem;
80 | }
81 |
82 | .sellout-icon {
83 | height: 60px;
84 | }
85 | .sellout-title {
86 | font-weight: bold;
87 | }
88 |
89 | /* item banners section*/
90 | .item-banners {
91 | display: grid;
92 | grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
93 | grid-gap: 1rem;
94 | }
95 |
96 | .item-container {
97 | display: grid;
98 | grid-template-columns: repeat(2, 1fr);
99 | background-color: #ffffff;
100 | padding: 20px;
101 | }
102 |
103 | .item-content {
104 | align-self: center;
105 | }
106 |
107 | .item-banner-title {
108 | color: #f17e0a;
109 | text-transform: uppercase;
110 | font-weight: 500;
111 | margin-bottom: 0;
112 | }
113 |
114 | .banner-image {
115 | height: 300px;
116 | justify-self: end;
117 | }
118 |
119 | /* products section */
120 | .products-section-title {
121 | display: flex;
122 | justify-content: space-between;
123 | }
124 |
125 | .products-section-link {
126 | color: #333333;
127 | text-decoration: none;
128 | text-transform: uppercase;
129 | font-weight: 500;
130 | }
131 |
132 | .products-section-link:hover {
133 | color: #f17e0a;
134 | text-decoration: none;
135 | }
136 |
137 | .products-container {
138 | display: grid;
139 | grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
140 | grid-gap: 1rem;
141 | }
142 |
143 | .products-container .card-title {
144 | text-align: center;
145 | white-space: nowrap;
146 | width: 100%;
147 | overflow: hidden;
148 | text-overflow: ellipsis;
149 | font-size: 1rem;
150 | text-transform: capitalize;
151 | margin-top: 0.5rem;
152 | margin-bottom: 0.5rem;
153 | }
154 |
155 | .products-container .card-text {
156 | text-align: center;
157 | color: #f17e0a;
158 | font-weight: 600;
159 | }
160 |
161 | /* deals section */
162 | .deals-content {
163 | height: 450px;
164 | display: grid;
165 | grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
166 | background: #ffffff;
167 | }
168 |
169 | .deals-image {
170 | max-height: 400px;
171 | align-self: center;
172 | justify-self: center;
173 | }
174 |
175 | .deals-text {
176 | padding: 0 75px;
177 | /* color: #ffffff !important; */
178 | height: 100%;
179 | display: grid;
180 | align-content: center;
181 | justify-content: center;
182 | background: rgba(241, 126, 10, 0.5);
183 | background: #eee8aa;
184 | }
185 |
186 | /* newsletter section */
187 | .newsletter-contaner {
188 | display: flex;
189 | flex-direction: column;
190 | align-items: center;
191 | background: #f6f7fb;
192 | padding: 40px 20px;
193 | }
194 |
195 | .input-contaner {
196 | width: 500px;
197 | }
198 |
199 | @media (max-width: 576px) {
200 | /* deals section */
201 | .deals-image {
202 | height: 150px;
203 | }
204 |
205 | .deals-text {
206 | padding: 1rem 2rem;
207 | }
208 |
209 | /* newsletter section */
210 | .newsletter-contaner {
211 | padding: 30px;
212 | }
213 | .input-contaner {
214 | width: 90%;
215 | }
216 |
217 | .newsletter-contaner .text-muted {
218 | text-align: justify;
219 | }
220 | }
221 |
222 | @media (max-width: 768px) and (min-width: 567px) {
223 | /* sellout section */
224 | .sellout-section {
225 | grid-template-columns: 1fr;
226 | grid-template-rows: repeat(3, 1fr);
227 | }
228 |
229 | /* deals section */
230 | .deals-image {
231 | height: 250px;
232 | }
233 |
234 | .deals-text {
235 | padding: 1rem 1rem;
236 | text-align: center;
237 | }
238 |
239 | .deals-text .text {
240 | text-align: justify;
241 | }
242 |
243 | /* products section */
244 | .products-container {
245 | grid-template-columns: repeat(2, 1fr);
246 | grid-template-rows: repeat(2, 1fr);
247 | }
248 | }
249 |
250 | @media (max-width: 768px) {
251 | /* banner section */
252 | .main-banner-container {
253 | max-height: auto;
254 | }
255 | .main-banner-content {
256 | display: block;
257 | }
258 |
259 | .main-banner-title {
260 | font-size: 3rem;
261 | }
262 |
263 | .lead {
264 | font-size: 1rem;
265 | }
266 |
267 | .main-banner-image {
268 | display: none;
269 | }
270 |
271 | /* item banners section */
272 | .item-banner-title {
273 | font-size: 0.8rem;
274 | }
275 |
276 | .item-container h4 {
277 | font-size: 1rem;
278 | }
279 |
280 | .banner-image {
281 | height: 150px;
282 | }
283 |
284 | /* deals section */
285 | .deals-content {
286 | height: auto;
287 | }
288 |
289 | .deals-title {
290 | margin-top: 1rem;
291 | font-size: 1.5rem;
292 | }
293 | }
294 |
295 | @media (max-width: 992px) and (min-width: 769px) {
296 | /* banner section */
297 | .main-banner-image {
298 | height: 250px;
299 | justify-self: center;
300 | align-self: center;
301 | }
302 | .main-banner-title {
303 | font-size: 2rem;
304 | }
305 | /* sellout section */
306 | .sellout-section {
307 | grid-template-columns: 1fr;
308 | grid-template-rows: repeat(3, 1fr);
309 | }
310 |
311 | /* item banners section*/
312 | .banner-image {
313 | height: 150px;
314 | }
315 |
316 | .item-banner-title {
317 | font-size: 0.8rem;
318 | }
319 |
320 | .item-container h4 {
321 | font-size: 0.9rem;
322 | }
323 |
324 | /* deals section */
325 | .deals-text {
326 | padding: 0 1rem;
327 | }
328 | .deals-title {
329 | font-size: 2rem;
330 | }
331 | }
332 |
333 | @media (max-width: 1200px) and (min-width: 993px) {
334 | /* item banners section*/
335 | .banner-image {
336 | height: 250px;
337 | }
338 |
339 | /* products section */
340 | .products-container {
341 | grid-template-columns: repeat(2, 1fr);
342 | grid-template-rows: repeat(2, 1fr);
343 | }
344 | }
345 |
--------------------------------------------------------------------------------
/src/views/Home/Home.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import SelloutCards from "./components/SelloutCards";
3 | import ItemBanners from "./components/ItemBanners";
4 | import Deal from "./components/Deal";
5 | import Banner from "./components/Banner";
6 | import HomeSale from "./components/HomeSale";
7 | import Loader from "../../components/Loader/Index";
8 | import "./Home.css";
9 |
10 | class Home extends Component {
11 | constructor() {
12 | super();
13 | this.state = {
14 | loading: true,
15 | };
16 | }
17 |
18 | componentDidMount() {
19 | this.setState({ loading: false });
20 | }
21 | render() {
22 | return this.state.loading ? (
23 |
24 | ) : (
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | );
33 | }
34 | }
35 |
36 | export default Home;
37 |
--------------------------------------------------------------------------------
/src/views/Home/components/Banner.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { NavLink } from "react-router-dom";
3 |
4 | const Banner = () => {
5 | const banner = {
6 | image: "fila_black.jpg",
7 | title: "Awsome Collection",
8 | text:
9 | " Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ipsa,ipsam, eligendi, in quo sunt possimus non incidunt odit veroaliquid similique quaerat.",
10 | link: "/all",
11 | };
12 | return (
13 |
14 |
15 |
16 |
{banner.title}
17 |
{banner.text}
18 |
19 |
20 | Shop Now
21 |
22 |
23 |
24 |
})
29 |
30 |
31 | );
32 | };
33 |
34 | export default Banner;
35 |
--------------------------------------------------------------------------------
/src/views/Home/components/Deal.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { NavLink } from "react-router-dom";
3 |
4 | const Deal = () => {
5 | const deal = {
6 | image: "flare_dress.png",
7 | title: "Latest In Ladies Fashion",
8 | text:
9 | "Suspendisse massa leo, vesti cursus nulla sit amet, placeratlorem.vestibulum cursus nulla sit amet, placerat lorem",
10 | link: "/category/women",
11 | };
12 | return (
13 |
14 |
15 |
})
20 |
21 |
22 |
{deal.title}
23 |
{deal.text}
24 |
25 | Shop Now
26 |
27 |
28 |
29 |
30 |
31 | );
32 | };
33 |
34 | export default Deal;
35 |
--------------------------------------------------------------------------------
/src/views/Home/components/HomeSale.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { connect } from "react-redux";
3 | import { NavLink } from "react-router-dom";
4 | import { currencyToUse } from "../../../Utility/currency";
5 | import { VISIBILITY_FILTERS } from "../../../static/constants";
6 | import { getProductsByFilter } from "../../../store/selectors";
7 |
8 | const HomeSale = (props) => {
9 | let currencyKeys = currencyToUse(props.usedCurrencyProp);
10 |
11 | let products = props.productsProps.map((product, index) => {
12 | return (
13 |
14 |
})
19 |
{product.name}
20 |
21 | {currencyKeys.name}
22 | {Math.round(product.price * currencyKeys.value).toLocaleString()}
23 |
24 |
29 | View Item
30 |
31 |
32 | );
33 | });
34 | return (
35 |
36 |
37 |
ON SALE
38 |
43 | See All
44 |
45 |
46 |
{products}
47 |
48 | );
49 | };
50 |
51 | const mapStateToProps = (state) => {
52 | return {
53 | productsProps: getProductsByFilter(state, VISIBILITY_FILTERS.SALE, 6),
54 | usedCurrencyProp: state.usedCurrency,
55 | };
56 | };
57 |
58 | export default connect(mapStateToProps)(HomeSale);
59 |
--------------------------------------------------------------------------------
/src/views/Home/components/ItemBanners.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { NavLink } from "react-router-dom";
3 |
4 | const ItemBanners = () => {
5 | let itemData = [
6 | {
7 | image: "baby_dress.jpg",
8 | title: "Kids Fashion",
9 | text: `Limited Offer`,
10 | link: "/category/kids",
11 | percentage: "30",
12 | },
13 | {
14 | image: "shirt.jpg",
15 | title: "Men's Collectons",
16 | text: `New Arrivals`,
17 | link: "/category/men",
18 | percentage: "50",
19 | },
20 | ];
21 | function generateItemBanners() {
22 | return itemData.map((item, index) => {
23 | return (
24 |
25 |
26 |
{item.title}
27 |
28 | {item.text}
Up to {item.percentage}%
29 |
30 |
31 | Shop Now
32 |
33 |
34 |
})
39 |
40 | );
41 | });
42 | }
43 | return (
44 | {generateItemBanners()}
45 | );
46 | };
47 |
48 | export default ItemBanners;
49 |
--------------------------------------------------------------------------------
/src/views/Home/components/SelloutCards.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const SelloutCards = () => {
4 | let cardsData = [
5 | {
6 | image: "money.png",
7 | title: "Best Prices",
8 | description:
9 | "Dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor.",
10 | },
11 | {
12 | image: "truck.png",
13 | title: "Fast delivery",
14 | description:
15 | "Dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor.",
16 | },
17 | {
18 | image: "check-circle.png",
19 | title: "Free Returns",
20 | description:
21 | "Dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor.",
22 | },
23 | ];
24 |
25 | function generateSelloutCards() {
26 | return cardsData.map((card, index) => {
27 | return (
28 |
29 |
})
34 |
35 |
{card.title}
36 |
{card.description}
37 |
38 |
39 | );
40 | });
41 | }
42 | return (
43 |
44 | {generateSelloutCards()}
45 |
46 | );
47 | };
48 |
49 | export default SelloutCards;
50 |
--------------------------------------------------------------------------------
/src/views/ProductCategories.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { connect } from "react-redux";
3 | import PropTypes from "prop-types";
4 | import { getProductsByFilter, getUsedCurrency } from "../store/selectors";
5 | import ProductsDisplay from "../components/ProductsDisplay/Index";
6 |
7 | const ProductCategories = (props) => {
8 | const {
9 | match: { params },
10 | } = props;
11 | return (
12 |
22 | );
23 | };
24 |
25 | const mapStateToProps = (state, ownProps) => {
26 | return {
27 | productsProps: getProductsByFilter(state, ownProps.match.params.category),
28 | usedCurrencyProp: getUsedCurrency(state),
29 | };
30 | };
31 |
32 | ProductCategories.propTypes = {
33 | productsProps: PropTypes.array.isRequired,
34 | usedCurrencyProp: PropTypes.object.isRequired,
35 | };
36 |
37 | export default connect(mapStateToProps)(ProductCategories);
38 |
--------------------------------------------------------------------------------
/src/views/ProductDetails/ProductDetails.css:
--------------------------------------------------------------------------------
1 | .product-card {
2 | padding: 1rem;
3 | display: grid;
4 | grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
5 | }
6 |
7 | .product-card-image {
8 | justify-self: center;
9 | align-self: center;
10 | height: 300px;
11 | }
12 |
13 | .product-title-container {
14 | display: flex;
15 | justify-content: space-between;
16 | }
17 |
18 | .product-title {
19 | text-transform: capitalize;
20 | }
21 |
22 | .product-wishlist {
23 | color: #e5e5e5 !important;
24 | font-size: 20px !important;
25 | cursor: pointer;
26 | }
27 |
28 | .wished {
29 | color: #e40046 !important;
30 | }
31 |
32 | .product-vendor {
33 | text-transform: capitalize;
34 | color: #e40046;
35 | font-weight: 600;
36 | }
37 |
38 | .product-price-container {
39 | background: #f5f7fa;
40 | padding: 0.5rem 0.5rem;
41 | margin-top: 1rem;
42 | margin-bottom: 1rem;
43 | }
44 |
45 | .product-price {
46 | color: #f17e0a;
47 | font-weight: 700;
48 | font-size: 1.5rem;
49 | }
50 |
51 | .product-discount-price {
52 | text-transform: lowercase;
53 | margin-left: 0.5rem;
54 | text-decoration: line-through;
55 | color: #909399;
56 | }
57 |
58 | .product-percentage-discount {
59 | margin-left: 0.5rem;
60 | font-size: 0.8rem;
61 | padding: 0.2rem 0.7rem;
62 | background: #f17e0a;
63 | color: #ffffff;
64 | border-radius: 0.2rem;
65 | }
66 |
67 | .product-features {
68 | display: flex;
69 | margin-bottom: 0.5rem;
70 | }
71 |
72 | .product-features-title {
73 | text-transform: capitalize;
74 | color: #333333;
75 | width: 150px;
76 | }
77 |
78 | .feature-fulfillmemt {
79 | display: flex;
80 | flex-direction: column;
81 | }
82 |
83 | .feature-fulfillmemt .shop-card-product-features {
84 | margin-bottom: 0.5rem;
85 | }
86 |
87 | .feature-text {
88 | color: #6c757d;
89 | font-weight: 400;
90 | }
91 |
92 | .product-features svg {
93 | font-size: 1.2rem;
94 | color: #e40046;
95 | margin-right: 0.5rem;
96 | }
97 |
98 | .feature-color {
99 | padding: 0.2rem 0.5rem;
100 | height: 25px;
101 | color: #ffffff;
102 | font-size: 0.9rem;
103 | font-weight: 600;
104 | }
105 |
106 | .product-quantity {
107 | display: flex;
108 | }
109 |
110 | .product-quantity .btn-secondary {
111 | border-radius: 0;
112 | width: 40px;
113 | height: 30px !important;
114 | padding: 0rem;
115 | border: none;
116 | }
117 |
118 | .product-quantity .form-control {
119 | border-radius: 0;
120 | width: 80px;
121 | height: 30px !important;
122 | }
123 |
--------------------------------------------------------------------------------
/src/views/ProductDetails/ProductDetails.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { connect } from "react-redux";
3 | import { addToCart } from "../../store/actions";
4 | import BreadCrumbs from "../../components/UI/BreadCrumbs/BreadCrumbs";
5 | import Ratings from "../../components/Ratings/Ratings";
6 | import AddToWishList from "../../components/AddToWishlist/AddToWishlist";
7 | import ProductFeatures from "../../components/ProductCard/ProductFeatures";
8 | import {
9 | currencyToUse,
10 | productPrice,
11 | productDiscountPrice,
12 | } from "../../Utility/currency";
13 | import HomeSale from "../../views/Home/components/HomeSale";
14 | import "./ProductDetails.css";
15 |
16 | class ProductDetails extends Component {
17 | state = {
18 | productDetails: {
19 | id: null,
20 | size: "",
21 | quantity: 1,
22 | },
23 | };
24 |
25 | componentDidMount() {
26 | if (this.props.productProp) {
27 | this.setState((prevState) => ({
28 | productDetails: {
29 | ...prevState.productDetails,
30 | id: this.props.productProp.id,
31 | },
32 | }));
33 | }
34 | }
35 |
36 | product = this.props.productProp;
37 | currencyKeys = currencyToUse(this.props.usedCurrencyProp);
38 |
39 | truncateProductName() {
40 | const productName = this.product.name;
41 | return productName.length > 45
42 | ? `${productName.substring(0, 45)}...`
43 | : productName;
44 | }
45 |
46 | disableAddToCartButton() {
47 | let prodDetails = this.state.productDetails;
48 | let generalValidations =
49 | !prodDetails.id ||
50 | !prodDetails.quantity ||
51 | prodDetails.quantity < 1 ||
52 | prodDetails.quantity > this.product.quantity;
53 | if (!this.product.sizes) {
54 | return generalValidations;
55 | }
56 | return generalValidations || !prodDetails.size;
57 | }
58 |
59 | handleAdditionSubtraction(action) {
60 | let stateData = this.state.productDetails;
61 | if (action === "subtract" && stateData.quantity < 1) {
62 | return;
63 | }
64 |
65 | let quantity = parseInt(stateData.quantity);
66 | let newValue = action === "subtract" ? quantity - 1 : quantity + 1;
67 |
68 | this.setState((prevState) => ({
69 | productDetails: {
70 | ...prevState.productDetails,
71 | quantity: newValue,
72 | },
73 | }));
74 | }
75 |
76 | handleInputChange = (event) => {
77 | const target = event.target;
78 | let value = target.value;
79 | const name = target.name;
80 |
81 | if (name === "quantity") {
82 | if (!value.match(/^[0-9]+$/)) {
83 | value = 1;
84 | }
85 | }
86 |
87 | this.setState({
88 | productDetails: { ...this.state.productDetails, [name]: value },
89 | });
90 | };
91 |
92 | handleAddToCart = () => {
93 | this.props.addProductToCartProp(this.state.productDetails);
94 | };
95 |
96 | render() {
97 | return (
98 |
99 | {!this.product ? (
100 |
101 |
102 |
Product Not Found
103 |
104 |
105 | ) : (
106 |
107 |
108 |
120 |
121 |
})
126 |
127 |
128 |
{this.product.name}
129 |
134 |
135 |
136 | {this.product.vendor.name ? (
137 |
138 | Sold By :
139 |
140 | {this.product.vendor.name}
141 |
142 |
143 | ) : null}
144 |
145 |
146 |
147 |
154 |
155 |
156 |
157 |
158 | {this.currencyKeys.name}
159 | {productPrice(
160 | this.product.price,
161 | this.currencyKeys.value
162 | )}
163 |
164 | {this.product.discount_price ? (
165 |
166 | {this.currencyKeys.name}
167 | {productPrice(
168 | this.product.discount_price,
169 | this.currencyKeys.value
170 | )}
171 |
172 | ) : null}
173 | {this.product.discount_price ? (
174 |
175 | {productDiscountPrice(
176 | this.product.price,
177 | this.product.discount_price
178 | )}
179 |
180 | ) : null}
181 |
182 |
183 |
184 |
185 |
186 | Features:
187 |
188 |
191 |
192 | {this.product.color ? (
193 |
194 |
195 | colors:
196 |
197 |
206 | {this.product.color}
207 |
208 |
209 | ) : null}
210 | {this.product.sizes ? (
211 |
212 |
213 | Size:
214 |
215 |
216 |
231 |
232 |
233 | ) : null}
234 |
235 | {this.product.quantity ? (
236 |
237 |
238 | quantity:
239 |
240 |
241 |
250 | this.handleInputChange(event)}
257 | >
258 |
271 |
272 |
273 | ) : null}
274 |
275 |
276 |
277 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 | )}
293 |
294 | );
295 | }
296 | }
297 | const mapStateToProps = (state, ownProps) => {
298 | return {
299 | productProp: state.products.find(
300 | (product) => product.slug === ownProps.match.params.productSlug
301 | ),
302 | usedCurrencyProp: state.usedCurrency,
303 | showModal: state.productMaxShowModal,
304 | modalmessage: state.showModal,
305 | };
306 | };
307 |
308 | const mapDispatchToProps = (dispatch) => {
309 | return {
310 | addProductToCartProp: (productDetails) =>
311 | dispatch(addToCart(productDetails)),
312 | };
313 | };
314 |
315 | export default connect(mapStateToProps, mapDispatchToProps)(ProductDetails);
316 |
--------------------------------------------------------------------------------
/src/views/Sale.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { connect } from "react-redux";
3 | import PropTypes from "prop-types";
4 | import { VISIBILITY_FILTERS } from "../static/constants";
5 | import { getProductsByFilter, getUsedCurrency } from "../store/selectors";
6 | import ProductsDisplay from "../components/ProductsDisplay/Index";
7 |
8 | const Sale = (props) => {
9 | return (
10 |
20 | );
21 | };
22 |
23 | const mapStateToProps = (state) => {
24 | return {
25 | productsProps: getProductsByFilter(state, VISIBILITY_FILTERS.SALE),
26 | usedCurrencyProp: getUsedCurrency(state),
27 | };
28 | };
29 |
30 | Sale.propTypes = {
31 | productsProps: PropTypes.array.isRequired,
32 | usedCurrencyProp: PropTypes.object.isRequired,
33 | results: PropTypes.object,
34 | };
35 |
36 | export default connect(mapStateToProps)(Sale);
37 |
--------------------------------------------------------------------------------
/src/views/index.js:
--------------------------------------------------------------------------------
1 | import Home from "./Home/Home";
2 | import All from "./All";
3 | import ProductCategories from "./ProductCategories";
4 | import ProductDetails from "./ProductDetails/ProductDetails";
5 | import Sale from "./Sale";
6 | import Cart from "./Cart";
7 | import Checkout from "./Checkout";
8 |
9 | export const HomePage = Home;
10 | export const AllPage = All;
11 | export const ProductCategoriesPage = ProductCategories;
12 | export const ProductDetailsPage = ProductDetails;
13 | export const SalesPage = Sale;
14 | export const CartPage = Cart;
15 | export const CheckoutPage = Checkout;
16 |
--------------------------------------------------------------------------------