├── .gitignore
├── .idea
├── ecommerce.iml
├── inspectionProfiles
│ └── Project_Default.xml
├── misc.xml
├── modules.xml
├── vcs.xml
└── workspace.xml
├── README.md
├── ecm1-1.png
├── ecm2-2.png
├── ecm3.png
├── ecm4.png
├── ecm5.png
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── index.html
└── manifest.json
├── src
├── App.js
├── App.scss
├── App.test.js
├── actions
│ └── index.js
├── components
│ ├── BrandFilter
│ │ ├── BrandFilter.js
│ │ └── BrandFilter.scss
│ ├── CartItem
│ │ ├── CartItem.js
│ │ └── CartItem.scss
│ ├── Footer
│ │ └── Footer.js
│ ├── Header
│ │ └── Header.js
│ ├── LayoutMode
│ │ ├── LayoutMode.js
│ │ └── LayoutMode.scss
│ ├── OrderFilter
│ │ ├── OrderFilter.js
│ │ └── OrderFilter.scss
│ ├── Pagination
│ │ ├── Pagination.js
│ │ └── Pagination.scss
│ ├── Product
│ │ ├── Product.js
│ │ └── Product.scss
│ ├── ProductDetail
│ │ └── ProductDetail.js
│ ├── ProductSlider
│ │ ├── ProductSlider.js
│ │ └── ProductSlider.scss
│ └── SlideDots
│ │ ├── SlideDots.js
│ │ └── SlideDots.scss
├── containers
│ ├── FilterBar
│ │ └── FilterBar.js
│ └── ProductList
│ │ ├── ProductList.js
│ │ └── ProductList.scss
├── data
│ ├── brands.js
│ ├── getData.js
│ └── phones.js
├── index.js
├── index.scss
├── logo.svg
├── pages
│ ├── Home
│ │ └── Home.js
│ ├── ProductDetail
│ │ └── ProductDetail.js
│ └── ShopingCart
│ │ └── ShoppingCart.js
├── pipes
│ ├── brandFilter.js
│ ├── orderByFilter.js
│ ├── paginationFilter.js
│ ├── priceFormatter.js
│ └── shortenTitle.js
├── reducers
│ ├── brand.filter.reducer.js
│ ├── index.js
│ ├── orderByPrice.filter.reducer.js
│ ├── pagination.reducer.js
│ └── shop.reducer.js
├── serviceWorker.js
└── utilities
│ └── cumulativeOffset.js
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/.idea/ecommerce.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/workspace.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 | true
241 | DEFINITION_ORDER
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 |
335 |
336 |
337 |
338 |
339 |
340 | 1551523495441
341 |
342 |
343 | 1551523495441
344 |
345 |
346 |
347 |
348 |
349 |
350 |
351 |
352 |
353 |
354 |
355 |
356 |
357 |
358 | 1552232148707
359 |
360 |
361 |
362 | 1552232148707
363 |
364 |
365 |
366 |
367 |
368 |
369 |
370 |
371 |
372 |
373 |
374 |
375 |
376 |
377 |
378 |
379 |
380 |
381 |
382 |
383 |
384 |
385 |
386 |
387 |
388 |
389 |
390 |
391 |
392 |
393 |
394 |
395 |
396 |
397 |
398 |
399 |
400 |
401 |
402 |
403 |
404 |
405 |
406 |
407 |
408 |
409 |
410 |
411 |
412 |
413 |
414 |
415 |
416 |
417 |
418 |
419 |
420 |
421 |
422 |
423 |
424 |
425 |
426 |
427 |
428 | file://$PROJECT_DIR$/src/pages/ShopingCart/ShoppingCart.js
429 | 30
430 |
431 |
432 |
433 | file://$PROJECT_DIR$/src/containers/ProductList/ProductList.js
434 |
435 |
436 |
437 |
438 |
439 |
440 |
441 |
442 |
443 |
444 |
445 |
446 |
447 |
448 |
449 |
450 |
451 |
452 |
453 |
454 |
455 |
456 |
457 |
458 |
459 |
460 |
461 |
462 |
463 |
464 |
465 |
466 |
467 |
468 |
469 |
470 |
471 |
472 |
473 |
474 |
475 |
476 |
477 |
478 |
479 |
480 |
481 |
482 |
483 |
484 |
485 |
486 |
487 |
488 |
489 |
490 |
491 |
492 |
493 |
494 |
495 |
496 |
497 |
498 |
499 |
500 |
501 |
502 |
503 |
504 |
505 |
506 |
507 |
508 |
509 |
510 |
511 |
512 |
513 |
514 |
515 |
516 |
517 |
518 |
519 |
520 |
521 |
522 |
523 |
524 |
525 |
526 |
527 |
528 |
529 |
530 |
531 |
532 |
533 |
534 |
535 |
536 |
537 |
538 |
539 |
540 |
541 |
542 |
543 |
544 |
545 |
546 |
547 |
548 |
549 |
550 |
551 |
552 |
553 |
554 |
555 |
556 |
557 |
558 |
559 |
560 |
561 |
562 |
563 |
564 |
565 |
566 |
567 |
568 |
569 |
570 |
571 |
572 |
573 |
574 |
575 |
576 |
577 |
578 |
579 |
580 |
581 |
582 |
583 |
584 |
585 |
586 |
587 |
588 |
589 |
590 |
591 |
592 |
593 |
594 |
595 |
596 |
597 |
598 |
599 |
600 |
601 |
602 |
603 |
604 |
605 |
606 |
607 |
608 |
609 |
610 |
611 |
612 |
613 |
614 |
615 |
616 |
617 |
618 |
619 |
620 |
621 |
622 |
623 |
624 |
625 |
626 |
627 |
628 |
629 |
630 |
631 |
632 |
633 |
634 |
635 |
636 |
637 |
638 |
639 |
640 |
641 |
642 |
643 |
644 |
645 |
646 |
647 |
648 |
649 |
650 |
651 |
652 |
653 |
654 |
655 |
656 |
657 |
658 |
659 |
660 |
661 |
662 |
663 |
664 |
665 |
666 |
667 |
668 |
669 |
670 |
671 |
672 |
673 |
674 |
675 |
676 |
677 |
678 |
679 |
680 |
681 |
682 |
683 |
684 |
685 |
686 |
687 |
688 |
689 |
690 |
691 |
692 |
693 |
694 |
695 |
696 |
697 |
698 |
699 |
700 |
701 |
702 |
703 |
704 |
705 |
706 |
707 |
708 |
709 |
710 |
711 |
712 |
713 |
714 |
715 |
716 |
717 |
718 |
719 |
720 |
721 |
722 |
723 |
724 |
725 |
726 |
727 |
728 |
729 |
730 |
731 |
732 |
733 |
734 |
735 |
736 |
737 |
738 |
739 |
740 |
741 |
742 |
743 |
744 |
745 |
746 |
747 |
748 |
749 |
750 |
751 |
752 |
753 |
754 |
755 |
756 |
757 |
758 |
759 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Here is the live demo CLICK TO SEE DEMO
2 |
3 | 
4 | 
5 | 
6 | 
7 | 
8 |
9 |
10 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
11 |
12 | ## Available Scripts
13 |
14 | In the project directory, you can run:
15 |
16 | ### `npm start`
17 |
18 | Runs the app in the development mode.
19 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
20 |
21 | The page will reload if you make edits.
22 | You will also see any lint errors in the console.
23 |
24 | ### `npm test`
25 |
26 | Launches the test runner in the interactive watch mode.
27 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
28 |
29 | ### `npm run build`
30 |
31 | Builds the app for production to the `build` folder.
32 | It correctly bundles React in production mode and optimizes the build for the best performance.
33 |
34 | The build is minified and the filenames include the hashes.
35 | Your app is ready to be deployed!
36 |
37 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
38 |
39 | ### `npm run eject`
40 |
41 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
42 |
43 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
44 |
45 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
46 |
47 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
48 |
49 | ## Learn More
50 |
51 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
52 |
53 | To learn React, check out the [React documentation](https://reactjs.org/).
54 |
55 | ### Code Splitting
56 |
57 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting
58 |
59 | ### Analyzing the Bundle Size
60 |
61 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size
62 |
63 | ### Making a Progressive Web App
64 |
65 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app
66 |
67 | ### Advanced Configuration
68 |
69 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration
70 |
71 | ### Deployment
72 |
73 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment
74 |
75 | ### `npm run build` fails to minify
76 |
77 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify
78 |
--------------------------------------------------------------------------------
/ecm1-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheCoderDream/React-Ecommerce-App-with-Redux/a83dde725a9256c784853718edcbc65b4ec8222f/ecm1-1.png
--------------------------------------------------------------------------------
/ecm2-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheCoderDream/React-Ecommerce-App-with-Redux/a83dde725a9256c784853718edcbc65b4ec8222f/ecm2-2.png
--------------------------------------------------------------------------------
/ecm3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheCoderDream/React-Ecommerce-App-with-Redux/a83dde725a9256c784853718edcbc65b4ec8222f/ecm3.png
--------------------------------------------------------------------------------
/ecm4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheCoderDream/React-Ecommerce-App-with-Redux/a83dde725a9256c784853718edcbc65b4ec8222f/ecm4.png
--------------------------------------------------------------------------------
/ecm5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheCoderDream/React-Ecommerce-App-with-Redux/a83dde725a9256c784853718edcbc65b4ec8222f/ecm5.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ecommerce",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "bootstrap": "^4.3.1",
7 | "node-sass": "^4.11.0",
8 | "react": "^16.8.3",
9 | "react-dom": "^16.8.3",
10 | "react-medium-image-zoom": "^3.0.15",
11 | "react-redux": "^6.0.1",
12 | "react-router-dom": "^4.3.1",
13 | "react-scripts": "2.1.5",
14 | "redux": "^4.0.1"
15 | },
16 | "scripts": {
17 | "start": "react-scripts start",
18 | "build": "react-scripts build",
19 | "test": "react-scripts test",
20 | "eject": "react-scripts eject"
21 | },
22 | "eslintConfig": {
23 | "extends": "react-app"
24 | },
25 | "browserslist": [
26 | ">0.2%",
27 | "not dead",
28 | "not ie <= 11",
29 | "not op_mini all"
30 | ]
31 | }
32 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheCoderDream/React-Ecommerce-App-with-Redux/a83dde725a9256c784853718edcbc65b4ec8222f/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 |
11 |
12 | React App
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/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.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import {Provider} from 'react-redux';
3 | import {createStore } from 'redux';
4 | import rootReducer from './reducers';
5 |
6 | import {BrowserRouter, Switch, Route, Redirect} from 'react-router-dom';
7 |
8 | import './App.scss';
9 | import Home from "./pages/Home/Home";
10 | import Header from "./components/Header/Header";
11 | import Footer from "./components/Footer/Footer";
12 | import ProductDetail from "./pages/ProductDetail/ProductDetail";
13 | import ShoppingCart from "./pages/ShopingCart/ShoppingCart";
14 |
15 |
16 |
17 | export const store = createStore(rootReducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__());
18 |
19 | class App extends Component {
20 | render() {
21 | return (
22 |
23 |
24 |
25 |
26 |
27 | {
28 | return
29 | }}/>
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | );
39 | }
40 | }
41 |
42 | export default App;
43 |
--------------------------------------------------------------------------------
/src/App.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheCoderDream/React-Ecommerce-App-with-Redux/a83dde725a9256c784853718edcbc65b4ec8222f/src/App.scss
--------------------------------------------------------------------------------
/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/actions/index.js:
--------------------------------------------------------------------------------
1 | export const ADD_PRODUCT_TO_CART = 'ADD_PRODUCT_TO_CART';
2 | export const REMOVE_PRODUCT_FROM_CART = 'REMOVE_PRODUCT_FROM_CART';
3 | export const INCREMENT_CART_ITEM_QUANTITY = 'INCREMENT_CART_ITEM_QUANTITY';
4 | export const DECREMENT_CART_ITEM_QUANTITY = 'DECREMENT_CART_ITEM_QUANTITY';
5 |
6 | export const addProductToCart = product => {
7 | return {
8 | type: ADD_PRODUCT_TO_CART,
9 | payload: product
10 | }
11 | };
12 |
13 | export const removeProductToCart = productId => {
14 | return {
15 | type: REMOVE_PRODUCT_FROM_CART,
16 | payload: productId
17 | }
18 | };
19 |
20 | export const incrementCartQuantity = productId => {
21 | return{
22 | type: INCREMENT_CART_ITEM_QUANTITY,
23 | payload: productId
24 | }
25 | };
26 |
27 | export const decrementCartQuantity = productId => {
28 | return {
29 | type: DECREMENT_CART_ITEM_QUANTITY,
30 | payload: productId
31 | }
32 | };
33 |
34 |
35 | export const ADD_BRAND_TO_FILTER = 'ADD_BRAND_TO_FILTER';
36 | export const REMOVE_BRAND_FROM_FILTER = 'REMOVE_BRAND_FROM_FILTER';
37 |
38 | export const addBrandToFilter = brand => {
39 | return {
40 | type: ADD_BRAND_TO_FILTER,
41 | brand
42 | }
43 | };
44 |
45 |
46 | export const removeBrandFromFilter = brand => {
47 | return {
48 | type: REMOVE_BRAND_FROM_FILTER,
49 | brand
50 | }
51 | };
52 |
53 | export const ORDER_BY_ASC = 'ORDER_BY_ASC';
54 | export const ORDER_BY_DESC = 'ORDER_BY_DESC';
55 | export const CLEAR_ORDER_BY_PRICE = 'CLEAR_ORDER_BY_PRICE';
56 |
57 | export const orderByAsc = () => {
58 | return {
59 | type: ORDER_BY_ASC
60 | }
61 | };
62 |
63 | export const orderByDesc = () => {
64 | return {
65 | type: ORDER_BY_DESC
66 | }
67 | };
68 |
69 | export const clearOrderBy = () => {
70 | return {
71 | type: CLEAR_ORDER_BY_PRICE
72 | }
73 | };
74 |
75 |
76 | export const PREV_PAGE = 'PREV_PAGE';
77 | export const NEXT_PAGE = 'NEXT_PAGE';
78 | export const GO_PAGE = 'GO_PAGE';
79 | export const COUNT_ITEM = 'COUNT_ITEM';
80 |
81 |
82 | export const nextPage = () => {
83 | return {
84 | type: NEXT_PAGE
85 | }
86 | };
87 |
88 | export const prevPage = () => {
89 | return {
90 | type: PREV_PAGE
91 | }
92 | };
93 |
94 | export const goPage = (n) => {
95 | return {
96 | type: GO_PAGE,
97 | currentPage: n
98 | }
99 | };
100 |
101 | export const countItem = (n) => {
102 | return {
103 | type: COUNT_ITEM,
104 | totalItemsCount: n
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/src/components/BrandFilter/BrandFilter.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import {connect} from 'react-redux';
3 | import './BrandFilter.scss';
4 | import {brands} from "../../data/brands";
5 | import {addBrandToFilter, removeBrandFromFilter} from "../../actions";
6 |
7 |
8 | const BrandFilter = (props) => {
9 |
10 | const {dispatch, brandItemsCount} = props;
11 | const handleSelectBox = (e) => {
12 | const name = e.target.name;
13 | const value = e.target.checked;
14 |
15 | if(e.target.checked) {
16 | dispatch(addBrandToFilter(name));
17 | } else {
18 | dispatch(removeBrandFromFilter(name));
19 | }
20 | };
21 |
22 |
23 | return (
24 |
25 |
26 |
Brands
27 |
28 |
40 |
41 | );
42 |
43 | };
44 |
45 | const mapStateToProps = (state) => {
46 |
47 | const brandItemsCount = {};
48 |
49 | state.shop.products.forEach(p => {
50 | brandItemsCount[p.brand] = brandItemsCount[p.brand] + 1 || 1;
51 | });
52 |
53 |
54 | return {
55 | brandItemsCount
56 | }
57 |
58 | };
59 |
60 | export default connect(mapStateToProps)(BrandFilter);
--------------------------------------------------------------------------------
/src/components/BrandFilter/BrandFilter.scss:
--------------------------------------------------------------------------------
1 | .custom-checkbox {
2 | display: block;
3 | position: relative;
4 | padding-left: 2rem;
5 | cursor: pointer;
6 | user-select: none;
7 |
8 | &__input {
9 | position: absolute;
10 | opacity: 0;
11 | height: 0;
12 | width: 0;
13 |
14 |
15 | }
16 |
17 | &__span {
18 | position: absolute;
19 | top: 50%;
20 | left: 0;
21 | transform: translateY(-50%);
22 | height: 1.2rem;
23 | width: 1.2rem;
24 | background-color: white;
25 | border: 2px solid gray;
26 |
27 | &::after {
28 | content: "";
29 | opacity: 0;
30 | position: absolute;
31 | top: 50%;
32 | left: 50%;
33 | width: .6rem;
34 | height: .6rem;
35 | background-color: dodgerblue;
36 | transform: translate(-50%, -50%);
37 | }
38 | }
39 |
40 | &:hover &__span {
41 | background-color: #eeeeee;
42 | }
43 |
44 | &__input:checked + &__span::after {
45 | opacity: 1;
46 | }
47 | }
48 |
49 | .flex-50 {
50 | flex: 0 0 100%;
51 | }
52 |
53 | @media only screen and (max-width: 768px) {
54 |
55 | .flex-50 {
56 | flex: 0 0 50%;
57 | }
58 | }
--------------------------------------------------------------------------------
/src/components/CartItem/CartItem.js:
--------------------------------------------------------------------------------
1 | import React, {useState} from 'react';
2 | import {connect} from 'react-redux';
3 | import {shortenTitle} from "../../pipes/shortenTitle";
4 | import {formatMoney} from "../../pipes/priceFormatter";
5 | import './CartItem.scss';
6 | import {addProductToCart, decrementCartQuantity, incrementCartQuantity, removeProductToCart} from "../../actions";
7 |
8 | const CartItem = (
9 | {
10 | title,
11 | price,
12 | description,
13 | quantity,
14 | id,
15 | img,
16 | dispatch
17 | }
18 | ) => {
19 |
20 | console.log(id);
21 | const [itemQuantity, setItemQuantity] = useState(quantity);
22 | const removeItem = () => {
23 | dispatch(removeProductToCart(id));
24 | };
25 |
26 | const handleQuantityChange = (e) => {
27 | /* const value = e.target.value;
28 | console.log(value)
29 |
30 | if(value > 0 && value <= 10) {
31 | setItemQuantity(value);
32 | dispatch(addProductToCart(id));
33 | } */
34 | };
35 |
36 | const incrementOrDecrement = (e, type) => {
37 | const value = itemQuantity;
38 | console.log(type, value);
39 |
40 | if(type === 'inc' && value < 10) {
41 | setItemQuantity(itemQuantity + 1);
42 | dispatch(incrementCartQuantity(id));
43 | }
44 |
45 |
46 | if(type === 'desc' && value > 1) {
47 | setItemQuantity(itemQuantity - 1);
48 | dispatch(decrementCartQuantity(id));
49 | }
50 |
51 | };
52 |
53 |
54 | return (
55 |
56 |
57 |

59 |
60 |
61 |
{shortenTitle(title)}
62 |
63 | {description}
64 |
65 |
66 |
67 |
68 |
{formatMoney(price)}$ x
69 |
70 |
85 |
86 |
91 |
92 |
93 |
94 | );
95 | };
96 |
97 | export default connect()(CartItem);
98 |
--------------------------------------------------------------------------------
/src/components/CartItem/CartItem.scss:
--------------------------------------------------------------------------------
1 |
2 |
3 | .quantity {
4 | float: left;
5 | margin-right: 15px;
6 | background-color: #eee;
7 | position: relative;
8 | width: 80px;
9 | overflow: hidden
10 | }
11 |
12 | .quantity input {
13 | margin: 0;
14 | text-align: center;
15 | width: 15px;
16 | height: 15px;
17 | padding: 0;
18 | float: right;
19 | color: #000;
20 | font-size: 20px;
21 | border: 0;
22 | outline: 0;
23 | background-color: #F6F6F6
24 | }
25 |
26 | .quantity input.qty {
27 | position: relative;
28 | border: 0;
29 | width: 100%;
30 | height: 40px;
31 | padding: 10px 25px 10px 10px;
32 | text-align: center;
33 | font-weight: 400;
34 | font-size: 15px;
35 | border-radius: 0;
36 | background-clip: padding-box
37 | }
38 |
39 | .quantity .minus, .quantity .plus {
40 | line-height: 0;
41 | background-clip: padding-box;
42 | -webkit-border-radius: 0;
43 | -moz-border-radius: 0;
44 | border-radius: 0;
45 | -webkit-background-size: 6px 30px;
46 | -moz-background-size: 6px 30px;
47 | color: #bbb;
48 | font-size: 20px;
49 | position: absolute;
50 | height: 50%;
51 | border: 0;
52 | right: 0;
53 | padding: 0;
54 | width: 25px;
55 | z-index: 3
56 | }
57 |
58 | .quantity .minus:hover, .quantity .plus:hover {
59 | background-color: #dad8da
60 | }
61 |
62 | .quantity .minus {
63 | bottom: 0
64 | }
65 | .shopping-cart {
66 | margin-top: 20px;
67 | }
68 |
69 | @media only screen and (max-width: 768px) {
70 | .product-name {
71 | font-size: 1rem;
72 | margin-top: .5rem;
73 | }
74 |
75 | .product-description {
76 | font-size: 1rem;
77 | }
78 |
79 | .product-quantity-container {
80 | margin-top: .5rem;
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/components/Footer/Footer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Footer = () => {
4 | return (
5 |
10 | );
11 | };
12 |
13 | export default Footer;
--------------------------------------------------------------------------------
/src/components/Header/Header.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {connect} from 'react-redux';
3 | import {NavLink} from 'react-router-dom';
4 |
5 | const Header = ({cartLength}) => {
6 |
7 | return (
8 |
21 | );
22 | };
23 |
24 |
25 | const mapStateToProps = (state) => {
26 | return {
27 | cartLength: state.shop.cart.length
28 | }
29 | };
30 |
31 | export default connect(mapStateToProps, null)(Header);
32 |
33 |
34 | /*
35 | *
36 | Home
37 | (current)
38 |
39 |
40 |
41 | About
42 |
43 |
44 | Services
45 |
46 |
47 | Contact
48 |
49 | * */
--------------------------------------------------------------------------------
/src/components/LayoutMode/LayoutMode.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './LayoutMode.scss';
3 |
4 | const LayoutMode = (
5 | {
6 | len,
7 | click,
8 | isActive
9 | }
10 | ) => {
11 |
12 | let items = [];
13 |
14 | let classess = 'layout-mode__item';
15 |
16 | if(isActive) {
17 | classess += ' layout-mode__item--active'
18 | }
19 |
20 | for(let i = 0; i < len; i ++) {
21 | items.push();
22 | };
23 |
24 |
25 | return (
26 | {click(len)}}>
27 | {items}
28 |
29 | );
30 | };
31 |
32 | export default LayoutMode;
33 |
--------------------------------------------------------------------------------
/src/components/LayoutMode/LayoutMode.scss:
--------------------------------------------------------------------------------
1 | .layout-mode {
2 |
3 | display: flex;
4 | flex-direction: row;
5 | padding: .3rem;
6 | border: 2px solid gray;
7 | cursor: pointer;
8 |
9 | &__item {
10 | width: .7rem;
11 | height: .7rem;
12 | background-color: gray;
13 |
14 | &--active {
15 | background-color: dodgerblue;
16 | }
17 |
18 | }
19 |
20 | &__item:not(:last-child) {
21 | margin-right: .3rem;
22 | }
23 |
24 | &:not(:last-child) {
25 | margin-right: .8rem;
26 | }
27 |
28 |
29 |
30 |
31 | &:hover {
32 | border: 2px solid #646464;
33 | }
34 |
35 | &--active {
36 | border: 2px solid dodgerblue;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/components/OrderFilter/OrderFilter.js:
--------------------------------------------------------------------------------
1 | import React, {useState} from 'react';
2 | import {connect} from 'react-redux';
3 | import './OrderFilter.scss';
4 | import {clearOrderBy, ORDER_BY_ASC, ORDER_BY_DESC, orderByAsc, orderByDesc} from "../../actions";
5 |
6 | const OrderFilter = ({dispatch}) => {
7 |
8 | let removeSelected;
9 | const [selected, setSelected] = useState('');
10 |
11 | const handleRadioChange = (e) => {
12 | const value = e.target.value;
13 | setSelected(value);
14 | if(value === ORDER_BY_ASC) {
15 | dispatch(orderByAsc());
16 | } else {
17 | dispatch(orderByDesc());
18 | }
19 | };
20 |
21 | const removeFilter = (e) => {
22 |
23 | const buttons = document.getElementsByName('orderByPrice');
24 |
25 | buttons.forEach(el => {
26 | el.checked = false;
27 | });
28 |
29 | dispatch(clearOrderBy());
30 | setSelected('');
31 | };
32 |
33 | if(selected) {
34 | removeSelected = Remove filter
35 | }
36 |
37 |
38 |
39 | return (
40 |
41 |
42 |
Price {removeSelected}
43 |
44 |
65 |
66 | );
67 | };
68 |
69 | export default connect()(OrderFilter);
--------------------------------------------------------------------------------
/src/components/OrderFilter/OrderFilter.scss:
--------------------------------------------------------------------------------
1 | .custom-radio-btn {
2 | display: block;
3 | position: relative;
4 | cursor: pointer;
5 | padding-left: 2rem;
6 | user-select: none;
7 |
8 | &__input {
9 | position: absolute;
10 | opacity: 0;
11 | cursor: pointer;
12 | }
13 |
14 | &__span {
15 | position: absolute;
16 | top: 50%;
17 | left: 0;
18 | height: 1.3rem;
19 | width: 1.3rem;
20 | background-color: #eeeeee;
21 | border-radius: 12px;
22 | transform: translateY(-50%);
23 |
24 | &::after {
25 | position: absolute;
26 | top: 50%;
27 | left: 50%;
28 | transform: translate(-50%, -50%);
29 | width: .7rem;
30 | height: .7rem;
31 | border-radius: 50%;
32 | background-color: dodgerblue;
33 | content: "";
34 | display: none;
35 | }
36 | }
37 |
38 | &__input:checked ~ &__span::after {
39 | display: block;
40 | }
41 |
42 | &:hover &__span {
43 | background-color: #dfdfdf;
44 | }
45 |
46 |
47 | }
48 |
49 | .text-remove-selected {
50 | font-size: 1rem;
51 | font-weight: normal;
52 | cursor: pointer;
53 | padding-left: 2rem;
54 | }
--------------------------------------------------------------------------------
/src/components/Pagination/Pagination.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import {connect} from "react-redux";
3 | import {goPage, nextPage, prevPage} from "../../actions";
4 |
5 | class Pagination extends Component {
6 |
7 |
8 | onPage(n){
9 | this.props.onGoPage(n);
10 | }
11 |
12 | isOnLastPage(){
13 | // console.log(this.props.perPage * this.props.currentPage, this.props.totalItemsCount);
14 | return this.props.perPage * this.props.currentPage >= this.props.totalItemsCount;
15 | }
16 |
17 | totalPages() {
18 | return Math.ceil(this.props.totalItemsCount / this.props.perPage) || 0;
19 | }
20 |
21 | getMin(){
22 | return ((this.props.perPage * this.props.currentPage) - this.props.perPage) + 1;
23 | }
24 |
25 | getMax() {
26 | let max = this.props.perPage * this.props.currentPage;
27 | if (max > this.props.totalItemsCount) {
28 | max = this.props.totalItemsCount;
29 | }
30 | return max;
31 | }
32 | onPrev = () => {
33 | this.props.onPrevPage();
34 | }
35 |
36 | onNext = () => {
37 | this.props.onNextPage();
38 | }
39 |
40 |
41 | getPages = () => {
42 | const c = Math.ceil(this.props.totalItemsCount / this.props.perPage);
43 | const p = this.props.currentPage || 1;
44 | const pagesToShow = this.props.pagesToShow || 9;
45 | const pages = [];
46 | pages.push(p);
47 | const times = pagesToShow - 1;
48 | for (let i = 0; i < times; i++) {
49 | if (pages.length < pagesToShow) {
50 | if (Math.min.apply(null, pages) > 1) {
51 | pages.push(Math.min.apply(null, pages) - 1);
52 | }
53 | }
54 | if (pages.length < pagesToShow) {
55 | if (Math.max.apply(null, pages) < c) {
56 | pages.push(Math.max.apply(null, pages) + 1);
57 | }
58 | }
59 | }
60 | pages.sort((a, b) => a - b);
61 | return pages;
62 | }
63 |
64 |
65 |
66 |
67 | render() {
68 |
69 | console.log(this.props);
70 |
71 | const pages =this.getPages().map(pageNum => {
72 |
73 | let buttonClass = 'page-item';
74 |
75 | if(pageNum === this.props.currentPage) {
76 | buttonClass += ' active';
77 | }
78 |
79 | return ( {this.onPage(pageNum)}}>);
80 | });
81 |
82 | let prevButtonClass = 'page-item';
83 |
84 | if (this.props.currentPage === 1) {
85 | prevButtonClass += ' disabled';
86 | }
87 |
88 | const prevButton = (
89 |
91 | );
92 |
93 | let nextButtonClass = 'page-item';
94 |
95 | if(this.isOnLastPage()) {
96 | nextButtonClass += ' disabled';
97 | }
98 |
99 | const nextButton = (
100 |
101 |
104 |
105 | );
106 |
107 |
108 |
109 | return (
110 |
117 | );
118 | }
119 | }
120 |
121 |
122 | const mapStateToProps = (state) => {
123 | return {
124 | ...state.pagination,
125 | totalItemsCount: state.shop.products.length,
126 | }
127 | };
128 |
129 | export default Pagination;
130 |
--------------------------------------------------------------------------------
/src/components/Pagination/Pagination.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheCoderDream/React-Ecommerce-App-with-Redux/a83dde725a9256c784853718edcbc65b4ec8222f/src/components/Pagination/Pagination.scss
--------------------------------------------------------------------------------
/src/components/Product/Product.js:
--------------------------------------------------------------------------------
1 | import React, {useState} from 'react';
2 | import {connect} from 'react-redux';
3 | import {Link} from 'react-router-dom';
4 | import {formatMoney} from "../../pipes/priceFormatter";
5 | import {cumulativeOffSet} from "../../utilities/cumulativeOffset";
6 |
7 | import './Product.scss';
8 | import SlideDots from "../SlideDots/SlideDots";
9 | import {addProductToCart} from "../../actions";
10 |
11 |
12 | const Product = (props) => {
13 |
14 | const {
15 | title,
16 | price,
17 | images,
18 | description,
19 | id,
20 | } = props.product;
21 |
22 | const imageRef = React.createRef();
23 | const [img, setImg] = useState(images[0]);
24 | const [aItem, setAItem] = useState(0);
25 |
26 |
27 | const handleImageChange = (e) => {
28 |
29 | let clientX;
30 |
31 | if(e.type === 'touchmove') {
32 | clientX = e.touches[0].clientX;
33 | } else {
34 | clientX = e.clientX;
35 | }
36 |
37 | const currentX = clientX - cumulativeOffSet(imageRef.current).left;
38 |
39 | // console.dir(imageRef.current);
40 |
41 | const part = imageRef.current.clientWidth / images.length;
42 | // console.log(Math.ceil(currentX / part) - 1);
43 |
44 | let imgIndex = Math.ceil(currentX / part) - 1;
45 | if (imgIndex < 0) {
46 | imgIndex = 0;
47 | }
48 |
49 | if (imgIndex >= images.length) {
50 | imgIndex = images.length - 1;
51 | }
52 | setAItem(imgIndex);
53 | setImg(images[imgIndex]);
54 | };
55 |
56 | const handleMouseOut = (e) => {
57 | setImg(images[0]);
58 | setAItem(0);
59 | };
60 |
61 | const changeImage = (i) => {
62 | setImg(images[i]);
63 | setAItem(i);
64 | }
65 |
66 | return (
67 |
68 |

74 |
75 |
76 |
77 |
78 | {title}
79 |
80 |
${formatMoney(price)}
81 |
{description}
82 |
88 |
89 |
90 | );
91 | };
92 |
93 | export default connect()(Product);
94 |
95 |
--------------------------------------------------------------------------------
/src/components/Product/Product.scss:
--------------------------------------------------------------------------------
1 | .product {
2 | padding-bottom: 2rem;
3 | &__img {
4 | width: 100%;
5 | height: 100%;
6 | }
7 |
8 | &__text {
9 | flex: 0 0 40%;
10 | }
11 |
12 | &__link {
13 | flex: 0 0 60%;
14 | height: 60%;
15 | padding: 1rem;
16 | position: relative;
17 | }
18 |
19 | &__title {
20 | font-size: .8rem;
21 | }
22 |
23 | &__price {
24 |
25 | }
26 |
27 | &__description {
28 | font-size: 0.7rem;
29 | }
30 |
31 | &:hover {
32 | box-shadow: 0 1rem 2rem rgba(0,0,0, .2);
33 | }
34 |
35 | &__add-to-cart {
36 | position: absolute;
37 | bottom: .8rem;
38 | width: 60%;
39 | left: 50%;
40 | opacity: 0;
41 | visibility: hidden;
42 | transform: translateX(-50%);
43 | transition: all .2s;
44 | padding: 3px 12px;
45 |
46 | }
47 |
48 | &:hover &__add-to-cart {
49 | visibility: visible;
50 | opacity: 1;
51 | }
52 |
53 | &:hover .owl-dots {
54 | display: inline-block;
55 | }
56 |
57 | }
58 |
59 | @media only screen and (max-width: 768px) {
60 | .product {
61 | height: auto !important;
62 | &__add-to-cart {
63 | visibility: visible;
64 | opacity: 1;
65 | }
66 |
67 | & .owl-dots {
68 | display: inline-block;
69 | }
70 | }
71 | }
--------------------------------------------------------------------------------
/src/components/ProductDetail/ProductDetail.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {connect} from 'react-redux';
3 | import {formatMoney} from "../../pipes/priceFormatter";
4 | import {addProductToCart} from "../../actions";
5 |
6 | const ProductDetail = (props) => {
7 |
8 | const {
9 | title,
10 | images,
11 | brand,
12 | price,
13 | cpu,
14 | camera,
15 | size,
16 | weight,
17 | display,
18 | battery,
19 | memory,
20 | description,
21 | id
22 | } = props.product;
23 |
24 |
25 | const onCart = () => {
26 | props.dispatch(addProductToCart(props.product));
27 | };
28 |
29 | return (
30 |
80 | );
81 | };
82 |
83 | export default connect()(ProductDetail);
84 |
--------------------------------------------------------------------------------
/src/components/ProductSlider/ProductSlider.js:
--------------------------------------------------------------------------------
1 | import React, {useState} from 'react';
2 | import {cumulativeOffSet} from "../../utilities/cumulativeOffset"
3 | import './ProductSlider.scss';
4 |
5 | const ProductSlider = (
6 | {
7 | images
8 | }
9 | ) => {
10 | const imageRef = React.createRef();
11 | const [img, setImg] = useState(images[0]);
12 | const [aItem, setAItem] = useState(0);
13 |
14 |
15 | const handleImageChange = (e) => {
16 | const currentX = e.clientX - cumulativeOffSet(imageRef.current).left;
17 |
18 | console.dir(imageRef.current);
19 |
20 | const part = imageRef.current.clientWidth / images.length;
21 | console.log(Math.ceil(currentX / part) - 1);
22 |
23 | let imgIndex = Math.ceil(currentX / part) - 1;
24 | if (imgIndex < 0) {
25 | imgIndex = 0;
26 | }
27 |
28 | if (imgIndex >= images.length) {
29 | imgIndex = images.length - 1;
30 | }
31 | setAItem(imgIndex);
32 | setImg(images[imgIndex]);
33 | };
34 |
35 | const handleMouseOut = (e) => {
36 | setImg(images[0]);
37 | setAItem(0);
38 | };
39 |
40 | const changeImage = (i) => {
41 | setImg(images[i]);
42 | setAItem(i);
43 | }
44 |
45 |
46 | return (
47 |
66 | );
67 | };
68 |
69 | export default ProductSlider;
--------------------------------------------------------------------------------
/src/components/ProductSlider/ProductSlider.scss:
--------------------------------------------------------------------------------
1 | .gallery-wrap .img-big-wrap img {
2 | height: 450px;
3 | width: auto;
4 | display: inline-block;
5 | }
6 |
7 |
8 |
9 | .gallery-wrap .img-small-wrap .item-gallery {
10 | width: 60px;
11 | height: 60px;
12 | border: 1px solid #ddd;
13 | margin: 7px 2px;
14 | display: inline-block;
15 | overflow: hidden;
16 | }
17 |
18 | .gallery-wrap .img-small-wrap {
19 | text-align: center;
20 | }
21 | .gallery-wrap .img-small-wrap img {
22 | max-width: 100%;
23 | max-height: 100%;
24 | object-fit: cover;
25 | border-radius: 4px;
26 | cursor: pointer;
27 | }
--------------------------------------------------------------------------------
/src/components/SlideDots/SlideDots.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './SlideDots.scss';
3 |
4 | const SlideDots = (
5 | {
6 | len,
7 | activeItem,
8 | changeItem
9 | }
10 | ) => {
11 |
12 | const dots = [];
13 | for(let i = 0; i < len; i++) {
14 | let dotClass = 'owl-dot';
15 |
16 | if(activeItem === i) {
17 | dotClass += ' active';
18 | }
19 |
20 | dots.push()
24 | }
25 |
26 | return (
27 |
28 | {dots}
29 |
30 | );
31 | };
32 |
33 | export default SlideDots;
--------------------------------------------------------------------------------
/src/components/SlideDots/SlideDots.scss:
--------------------------------------------------------------------------------
1 | .owl-dots {
2 | display: none;
3 | position: absolute;
4 | bottom: -1rem;
5 | left: 0;
6 |
7 | }
8 |
9 | .owl-dots {
10 | width: 100%;
11 | text-align: center;
12 | }
13 |
14 | .owl-dots .active.owl-dot {
15 | background-color: #f28b00;
16 | border-color: #f28b00;
17 | }
18 |
19 | .owl-dots .owl-dot {
20 | display: inline-block;
21 | zoom: 1;
22 | margin: 8px 4px;
23 | border-radius: 10px;
24 | -moz-border-radius: 10px;
25 | -o-border-radius: 10px;
26 | -webkit-border-radius: 10px;
27 | -ms-webkit-radius: 10px;
28 | zoom: 1;
29 | background-color: #fff;
30 | padding: 3px;
31 | border: solid 1px #e9e9e9;
32 | cursor: pointer;
33 | }
--------------------------------------------------------------------------------
/src/containers/FilterBar/FilterBar.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import BrandFilter from "../../components/BrandFilter/BrandFilter";
3 | import OrderFilter from "../../components/OrderFilter/OrderFilter";
4 |
5 | class FilterBar extends Component {
6 | render() {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | );
19 | }
20 | }
21 |
22 | export default FilterBar;
--------------------------------------------------------------------------------
/src/containers/ProductList/ProductList.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import {connect} from 'react-redux';
3 | import Product from "../../components/Product/Product";
4 |
5 | import {brandFilter} from "../../pipes/brandFilter";
6 | import {orderByFilter} from "../../pipes/orderByFilter";
7 | import LayoutMode from "../../components/LayoutMode/LayoutMode";
8 | import {paginationPipe} from "../../pipes/paginationFilter";
9 | import Pagination from "../../components/Pagination/Pagination";
10 |
11 | class ProductList extends Component {
12 |
13 | state = {
14 | colValue : 'col-lg-4',
15 | perPage: 12,
16 | currentPage: 1,
17 | pagesToShow: 3,
18 | gridValue: 3
19 | };
20 |
21 | changeLayout = (n) => {
22 | this.setState({gridValue: n});
23 |
24 | let realGridValue;
25 |
26 | if(n === 4) {
27 | realGridValue = 3
28 | } else {
29 | realGridValue = 4;
30 | }
31 |
32 | this.setState({
33 | colValue: `col-lg-${realGridValue}`
34 | });
35 | };
36 |
37 |
38 | onPrev = () => {
39 | const updatedState = {...this.state};
40 | updatedState.currentPage = this.state.currentPage - 1;
41 | this.setState(updatedState);
42 | };
43 |
44 |
45 | onNext = () => {
46 | this.setState({
47 | ...this.state,
48 | currentPage: this.state.currentPage + 1
49 | });
50 | };
51 |
52 | goPage = (n) => {
53 | this.setState({
54 | ...this.state,
55 | currentPage: n
56 | });
57 | };
58 |
59 |
60 | render() {
61 |
62 | let isActive = this.state.colValue[this.state.colValue.length -1];
63 |
64 | return (
65 |
66 |
67 |
68 |
69 |
70 | Change Layout:
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 | {paginationPipe(this.props.products, this.state).map(product =>{
79 | let classes = `${this.state.colValue} col-md-6 mb-4`;
80 | return (
)
83 | })}
84 |
85 |
96 |
97 | );
98 | }
99 | }
100 |
101 | const mapStateToProps = state => {
102 | const brands = state.brandFilter;
103 | const orderBy = state.orderBy;
104 |
105 | const filterByBrandArr = brandFilter(state.shop.products, brands);
106 | const filterByOrderArr = orderByFilter(filterByBrandArr, orderBy);
107 |
108 |
109 | return {products: filterByOrderArr }
110 | };
111 |
112 | export default connect(mapStateToProps, null)(ProductList);
113 |
--------------------------------------------------------------------------------
/src/containers/ProductList/ProductList.scss:
--------------------------------------------------------------------------------
1 |
2 | $body-bg: #000;
3 |
4 | @import '~bootstrap/scss/bootstrap.scss';
--------------------------------------------------------------------------------
/src/data/brands.js:
--------------------------------------------------------------------------------
1 | export const brands = ["apple","huawei","meizu","samsung","vestel","xiaomi","asus"];
--------------------------------------------------------------------------------
/src/data/getData.js:
--------------------------------------------------------------------------------
1 |
2 | var imagesMap = {};
3 |
4 | var images = document.querySelectorAll('.product-image');
5 |
6 | console.log(images);
7 |
8 | images.forEach(img => {
9 |
10 | if(!imagesMap[img.title]) {
11 | imagesMap[img.title] = [];
12 | imagesMap[img.title].push(img.src);
13 | } else {
14 | imagesMap[img.title].push(img.src);
15 | }
16 |
17 | });
18 |
19 | for (let key in imagesMap) {
20 |
21 | if(imagesMap[key].length === 0) {
22 | delete imagesMap[key];
23 | }
24 |
25 | if(!imagesMap[key][0]) {
26 | imagesMap[key].splice(0,1);
27 | }
28 |
29 |
30 | }
31 |
32 | function getRandomArbitrary(min, max) {
33 | return Math.random() * (max - min) + min;
34 | }
35 |
36 | brand
37 | var phones = Object.keys(imagesMap).reduce((acc, key , i) => {
38 |
39 | if(imagesMap[key].length !== 0) {
40 | var newPhone = {
41 | price: getRandomArbitrary(1500, 10000),
42 | cpu: "1.3GHz Apple A6",
43 | camera: "8mp (3264x2448)",
44 | size: "124.4mm x 59.2mm x 8.97mm (4.9 x 2.33 x 0.35)",
45 | weight: "132 grams (4.7 ounces) with battery",
46 | display: "4.0 326 pixel density",
47 | battery: "1480 mAh",
48 | memory: "16GB, 32GB and RAM 1 GB"
49 | };
50 | newPhone.title = key;
51 | newPhone.brand = key.split(' ')[0].toLowerCase();
52 | newPhone.category = 'phone';
53 | newPhone.images = imagesMap[key];
54 |
55 | acc.push(newPhone);
56 | }
57 |
58 | return acc;
59 | }, []);
60 |
61 | console.log(JSON.stringify(phones));
--------------------------------------------------------------------------------
/src/data/phones.js:
--------------------------------------------------------------------------------
1 | export const phones = [{
2 | "title": "Apple iPhone 7 Plus 32 GB (Apple Türkiye Garantili)",
3 | "category": "phone",
4 | "images": ["https://productimages.hepsiburada.net/s/18/280-413/9801258663986.jpg?v1", "https://productimages.hepsiburada.net/s/18/280-413/9801258696754.jpg?v1", "https://productimages.hepsiburada.net/s/18/280-413/9801258729522.jpg?v1", "https://productimages.hepsiburada.net/s/18/280-413/9801258762290.jpg?v1"],
5 | "brand": "apple",
6 | "price": 4241.499828399639,
7 | "cpu": "1.3GHz Apple A6",
8 | "camera": "8mp (3264x2448)",
9 | "size": "124.4mm x 59.2mm x 8.97mm (4.9 x 2.33 x 0.35)",
10 | "weight": "132 grams (4.7 ounces) with battery",
11 | "display": "4.0 326 pixel density",
12 | "battery": "1480 mAh",
13 | "memory": "16GB, 32GB and RAM 1 GB",
14 | "id": 0,
15 | "description": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet numquam aspernatur!"
16 | }, {
17 | "title": "Huawei Mate 20 Lite 64 GB (Huawei Türkiye Garantili)",
18 | "category": "phone",
19 | "images": ["https://productimages.hepsiburada.net/s/21/280-413/9933217792050.jpg?v1", "https://productimages.hepsiburada.net/s/21/280-413/9933217628210.jpg?v1", "https://productimages.hepsiburada.net/s/21/280-413/9933217660978.jpg?v1", "https://productimages.hepsiburada.net/s/21/280-413/9933217693746.jpg?v1"],
20 | "brand": "huawei",
21 | "price": 1823.6625483451157,
22 | "cpu": "1.3GHz Apple A6",
23 | "camera": "8mp (3264x2448)",
24 | "size": "124.4mm x 59.2mm x 8.97mm (4.9 x 2.33 x 0.35)",
25 | "weight": "132 grams (4.7 ounces) with battery",
26 | "display": "4.0 326 pixel density",
27 | "battery": "1480 mAh",
28 | "memory": "16GB, 32GB and RAM 1 GB",
29 | "id": 1,
30 | "description": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet numquam aspernatur!"
31 | }, {
32 | "title": "Huawei P20 Lite 64 GB (Huawei Türkiye Garantili)",
33 | "category": "phone",
34 | "images": ["https://productimages.hepsiburada.net/s/19/280-413/9826975907890.jpg?v1", "https://productimages.hepsiburada.net/s/19/280-413/9826975940658.jpg?v1", "https://productimages.hepsiburada.net/s/19/280-413/9826975973426.jpg?v1", "https://productimages.hepsiburada.net/s/19/280-413/9826976006194.jpg?v1"],
35 | "brand": "huawei",
36 | "price": 7429.467511354926,
37 | "cpu": "1.3GHz Apple A6",
38 | "camera": "8mp (3264x2448)",
39 | "size": "124.4mm x 59.2mm x 8.97mm (4.9 x 2.33 x 0.35)",
40 | "weight": "132 grams (4.7 ounces) with battery",
41 | "display": "4.0 326 pixel density",
42 | "battery": "1480 mAh",
43 | "memory": "16GB, 32GB and RAM 1 GB",
44 | "id": 2,
45 | "description": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet numquam aspernatur!"
46 | }, {
47 | "title": "Meizu 16TH 64 GB (Meizu Türkiye Garantili)",
48 | "category": "phone",
49 | "images": ["https://productimages.hepsiburada.net/s/24/280-413/10094991409202.jpg?v1", "https://productimages.hepsiburada.net/s/24/280-413/10094991441970.jpg?v1", "https://productimages.hepsiburada.net/s/24/280-413/10094991474738.jpg?v1", "https://productimages.hepsiburada.net/s/24/280-413/10094991507506.jpg?v1"],
50 | "brand": "meizu",
51 | "price": 5664.265944453384,
52 | "cpu": "1.3GHz Apple A6",
53 | "camera": "8mp (3264x2448)",
54 | "size": "124.4mm x 59.2mm x 8.97mm (4.9 x 2.33 x 0.35)",
55 | "weight": "132 grams (4.7 ounces) with battery",
56 | "display": "4.0 326 pixel density",
57 | "battery": "1480 mAh",
58 | "memory": "16GB, 32GB and RAM 1 GB",
59 | "id": 3,
60 | "description": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet numquam aspernatur!"
61 | }, {
62 | "title": "Meizu X8 64 GB (Meizu Türkiye Garantili)",
63 | "category": "phone",
64 | "images": ["https://productimages.hepsiburada.net/s/25/280-413/10108030091314.jpg?v1", "https://productimages.hepsiburada.net/s/24/280-413/10082391818290.jpg?v1", "https://productimages.hepsiburada.net/s/24/280-413/10082391851058.jpg?v1", "https://productimages.hepsiburada.net/s/24/280-413/10082391883826.jpg?v1"],
65 | "brand": "meizu",
66 | "price": 4596.99884783711,
67 | "cpu": "1.3GHz Apple A6",
68 | "camera": "8mp (3264x2448)",
69 | "size": "124.4mm x 59.2mm x 8.97mm (4.9 x 2.33 x 0.35)",
70 | "weight": "132 grams (4.7 ounces) with battery",
71 | "display": "4.0 326 pixel density",
72 | "battery": "1480 mAh",
73 | "memory": "16GB, 32GB and RAM 1 GB",
74 | "id": 4,
75 | "description": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet numquam aspernatur!"
76 | }, {
77 | "title": "Samsung Galaxy A7 2018 64 GB (Samsung Türkiye Garantili)",
78 | "category": "phone",
79 | "images": ["https://productimages.hepsiburada.net/s/22/280-413/9946187399218.jpg?v1", "https://productimages.hepsiburada.net/s/22/280-413/9946187431986.jpg?v1", "https://productimages.hepsiburada.net/s/22/280-413/9946187464754.jpg?v1", "https://productimages.hepsiburada.net/s/22/280-413/9946187497522.jpg?v1"],
80 | "brand": "samsung",
81 | "price": 4108.082941215698,
82 | "cpu": "1.3GHz Apple A6",
83 | "camera": "8mp (3264x2448)",
84 | "size": "124.4mm x 59.2mm x 8.97mm (4.9 x 2.33 x 0.35)",
85 | "weight": "132 grams (4.7 ounces) with battery",
86 | "display": "4.0 326 pixel density",
87 | "battery": "1480 mAh",
88 | "memory": "16GB, 32GB and RAM 1 GB",
89 | "id": 5,
90 | "description": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet numquam aspernatur!"
91 | }, {
92 | "title": "Samsung Galaxy J6 Plus 32 GB (Samsung Türkiye Garantili)",
93 | "category": "phone",
94 | "images": ["https://productimages.hepsiburada.net/s/22/280-413/9941129494578.jpg?v1", "https://productimages.hepsiburada.net/s/22/280-413/9941129527346.jpg?v1", "https://productimages.hepsiburada.net/s/22/280-413/9941129560114.jpg?v1", "https://productimages.hepsiburada.net/s/22/280-413/9941129592882.jpg?v1"],
95 | "brand": "samsung",
96 | "price": 4260.9529075338505,
97 | "cpu": "1.3GHz Apple A6",
98 | "camera": "8mp (3264x2448)",
99 | "size": "124.4mm x 59.2mm x 8.97mm (4.9 x 2.33 x 0.35)",
100 | "weight": "132 grams (4.7 ounces) with battery",
101 | "display": "4.0 326 pixel density",
102 | "battery": "1480 mAh",
103 | "memory": "16GB, 32GB and RAM 1 GB",
104 | "id": 6,
105 | "description": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet numquam aspernatur!"
106 | }, {
107 | "title": "Vestel Venus Z20 (Vestel Garantili)",
108 | "category": "phone",
109 | "images": ["https://productimages.hepsiburada.net/s/19/280-413/9841497047090.jpg?v1", "https://productimages.hepsiburada.net/s/19/280-413/9841497079858.jpg?v1", "https://productimages.hepsiburada.net/s/19/280-413/9841497112626.jpg?v1", "https://productimages.hepsiburada.net/s/19/280-413/9841497145394.jpg?v1"],
110 | "brand": "vestel",
111 | "price": 4730.962860489047,
112 | "cpu": "1.3GHz Apple A6",
113 | "camera": "8mp (3264x2448)",
114 | "size": "124.4mm x 59.2mm x 8.97mm (4.9 x 2.33 x 0.35)",
115 | "weight": "132 grams (4.7 ounces) with battery",
116 | "display": "4.0 326 pixel density",
117 | "battery": "1480 mAh",
118 | "memory": "16GB, 32GB and RAM 1 GB",
119 | "id": 7,
120 | "description": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet numquam aspernatur!"
121 | }, {
122 | "title": "Xiaomi Mi 8 Lite 128 GB (İthalatçı Garantili)",
123 | "category": "phone",
124 | "images": ["https://productimages.hepsiburada.net/s/22/280-413/9957349523506.jpg?v1", "https://productimages.hepsiburada.net/s/22/280-413/9957349556274.jpg?v1", "https://productimages.hepsiburada.net/s/22/280-413/9957349589042.jpg?v1", "https://productimages.hepsiburada.net/s/22/280-413/9957349621810.jpg?v1"],
125 | "brand": "xiaomi",
126 | "price": 5565.737301732921,
127 | "cpu": "1.3GHz Apple A6",
128 | "camera": "8mp (3264x2448)",
129 | "size": "124.4mm x 59.2mm x 8.97mm (4.9 x 2.33 x 0.35)",
130 | "weight": "132 grams (4.7 ounces) with battery",
131 | "display": "4.0 326 pixel density",
132 | "battery": "1480 mAh",
133 | "memory": "16GB, 32GB and RAM 1 GB",
134 | "id": 8,
135 | "description": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet numquam aspernatur!"
136 | }, {
137 | "title": "Xiaomi Mi 8 Lite 64 GB (İthalatçı Garantili)",
138 | "category": "phone",
139 | "images": ["https://productimages.hepsiburada.net/s/23/280-413/10051147202610.jpg?v1", "https://productimages.hepsiburada.net/s/23/280-413/10051147235378.jpg?v1", "https://productimages.hepsiburada.net/s/23/280-413/10051147268146.jpg?v1", "https://productimages.hepsiburada.net/s/23/280-413/10051147300914.jpg?v1"],
140 | "brand": "xiaomi",
141 | "price": 5830.067673371856,
142 | "cpu": "1.3GHz Apple A6",
143 | "camera": "8mp (3264x2448)",
144 | "size": "124.4mm x 59.2mm x 8.97mm (4.9 x 2.33 x 0.35)",
145 | "weight": "132 grams (4.7 ounces) with battery",
146 | "display": "4.0 326 pixel density",
147 | "battery": "1480 mAh",
148 | "memory": "16GB, 32GB and RAM 1 GB",
149 | "id": 9,
150 | "description": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet numquam aspernatur!"
151 | }, {
152 | "title": "Apple iPhone 7 32 GB (Apple Türkiye Garantili)",
153 | "category": "phone",
154 | "images": ["https://productimages.hepsiburada.net/s/1/280-413/9502147641394.jpg?v1", "https://productimages.hepsiburada.net/s/1/280-413/9502147674162.jpg?v1", "https://productimages.hepsiburada.net/s/1/280-413/9502147706930.jpg?v1", "https://productimages.hepsiburada.net/s/1/280-413/9502147739698.jpg?v1"],
155 | "brand": "apple",
156 | "price": 1525.6236967422828,
157 | "cpu": "1.3GHz Apple A6",
158 | "camera": "8mp (3264x2448)",
159 | "size": "124.4mm x 59.2mm x 8.97mm (4.9 x 2.33 x 0.35)",
160 | "weight": "132 grams (4.7 ounces) with battery",
161 | "display": "4.0 326 pixel density",
162 | "battery": "1480 mAh",
163 | "memory": "16GB, 32GB and RAM 1 GB",
164 | "id": 10,
165 | "description": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet numquam aspernatur!"
166 | }, {
167 | "title": "Samsung Galaxy S10 Plus 128 GB (Samsung Türkiye Garantili)",
168 | "category": "phone",
169 | "images": ["https://productimages.hepsiburada.net/s/25/280-413/10107992703026.jpg?v1", "https://productimages.hepsiburada.net/s/25/280-413/10107992735794.jpg?v1", "https://productimages.hepsiburada.net/s/25/280-413/10107992768562.jpg?v1", "https://productimages.hepsiburada.net/s/25/280-413/10107992801330.jpg?v1"],
170 | "brand": "samsung",
171 | "price": 3429.3471420603028,
172 | "cpu": "1.3GHz Apple A6",
173 | "camera": "8mp (3264x2448)",
174 | "size": "124.4mm x 59.2mm x 8.97mm (4.9 x 2.33 x 0.35)",
175 | "weight": "132 grams (4.7 ounces) with battery",
176 | "display": "4.0 326 pixel density",
177 | "battery": "1480 mAh",
178 | "memory": "16GB, 32GB and RAM 1 GB",
179 | "id": 11,
180 | "description": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet numquam aspernatur!"
181 | }, {
182 | "title": "Samsung Galaxy S10 128 GB (Samsung Türkiye Garantili)",
183 | "category": "phone",
184 | "images": ["https://productimages.hepsiburada.net/s/25/280-413/10107307425842.jpg?v1", "https://productimages.hepsiburada.net/s/25/280-413/10107307458610.jpg?v1", "https://productimages.hepsiburada.net/s/25/280-413/10107307491378.jpg?v1", "https://productimages.hepsiburada.net/s/25/280-413/10107307524146.jpg?v1"],
185 | "brand": "samsung",
186 | "price": 1017.7877018963508,
187 | "cpu": "1.3GHz Apple A6",
188 | "camera": "8mp (3264x2448)",
189 | "size": "124.4mm x 59.2mm x 8.97mm (4.9 x 2.33 x 0.35)",
190 | "weight": "132 grams (4.7 ounces) with battery",
191 | "display": "4.0 326 pixel density",
192 | "battery": "1480 mAh",
193 | "memory": "16GB, 32GB and RAM 1 GB",
194 | "id": 12,
195 | "description": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet numquam aspernatur!"
196 | }, {
197 | "title": "Samsung Galaxy S10e 128 GB (Samsung Türkiye Garantili)",
198 | "category": "phone",
199 | "images": ["https://productimages.hepsiburada.net/s/25/280-413/10107307032626.jpg?v1", "https://productimages.hepsiburada.net/s/25/280-413/10107307065394.jpg?v1", "https://productimages.hepsiburada.net/s/25/280-413/10107307098162.jpg?v1", "https://productimages.hepsiburada.net/s/25/280-413/10107307130930.jpg?v1"],
200 | "brand": "samsung",
201 | "price": 3235.202225041739,
202 | "cpu": "1.3GHz Apple A6",
203 | "camera": "8mp (3264x2448)",
204 | "size": "124.4mm x 59.2mm x 8.97mm (4.9 x 2.33 x 0.35)",
205 | "weight": "132 grams (4.7 ounces) with battery",
206 | "display": "4.0 326 pixel density",
207 | "battery": "1480 mAh",
208 | "memory": "16GB, 32GB and RAM 1 GB",
209 | "id": 13,
210 | "description": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet numquam aspernatur!"
211 | }, {
212 | "title": "Samsung Galaxy M20 32 GB (Samsung Türkiye Garantili)",
213 | "category": "phone",
214 | "images": ["https://productimages.hepsiburada.net/s/25/280-413/10094999240754.jpg?v1", "https://productimages.hepsiburada.net/s/25/280-413/10094999273522.jpg?v1", "https://productimages.hepsiburada.net/s/25/280-413/10094999306290.jpg?v1", "https://productimages.hepsiburada.net/s/25/280-413/10094999339058.jpg?v1"],
215 | "brand": "samsung",
216 | "price": 5850.5748675199875,
217 | "cpu": "1.3GHz Apple A6",
218 | "camera": "8mp (3264x2448)",
219 | "size": "124.4mm x 59.2mm x 8.97mm (4.9 x 2.33 x 0.35)",
220 | "weight": "132 grams (4.7 ounces) with battery",
221 | "display": "4.0 326 pixel density",
222 | "battery": "1480 mAh",
223 | "memory": "16GB, 32GB and RAM 1 GB",
224 | "id": 14,
225 | "description": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet numquam aspernatur!"
226 | }, {
227 | "title": "Samsung Galaxy S8 (Samsung Türkiye Garantili)",
228 | "category": "phone",
229 | "images": ["https://productimages.hepsiburada.net/s/3/280-413/9604775739442.jpg?v1", "https://productimages.hepsiburada.net/s/4/280-413/9665566703666.jpg?v1", "https://productimages.hepsiburada.net/s/4/280-413/9665566736434.jpg?v1", "https://productimages.hepsiburada.net/s/4/280-413/9665566769202.jpg?v1"],
230 | "brand": "samsung",
231 | "price": 3207.2840718201587,
232 | "cpu": "1.3GHz Apple A6",
233 | "camera": "8mp (3264x2448)",
234 | "size": "124.4mm x 59.2mm x 8.97mm (4.9 x 2.33 x 0.35)",
235 | "weight": "132 grams (4.7 ounces) with battery",
236 | "display": "4.0 326 pixel density",
237 | "battery": "1480 mAh",
238 | "memory": "16GB, 32GB and RAM 1 GB",
239 | "id": 15,
240 | "description": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet numquam aspernatur!"
241 | }, {
242 | "title": "Huawei P Smart 2019 64 GB (Huawei Türkiye Garantili)",
243 | "category": "phone",
244 | "images": ["https://productimages.hepsiburada.net/s/23/280-413/10059934859314.jpg?v1", "https://productimages.hepsiburada.net/s/23/280-413/10059934892082.jpg?v1", "https://productimages.hepsiburada.net/s/23/280-413/10059934924850.jpg?v1", "https://productimages.hepsiburada.net/s/23/280-413/10059934957618.jpg?v1"],
245 | "brand": "huawei",
246 | "price": 5288.552334214134,
247 | "cpu": "1.3GHz Apple A6",
248 | "camera": "8mp (3264x2448)",
249 | "size": "124.4mm x 59.2mm x 8.97mm (4.9 x 2.33 x 0.35)",
250 | "weight": "132 grams (4.7 ounces) with battery",
251 | "display": "4.0 326 pixel density",
252 | "battery": "1480 mAh",
253 | "memory": "16GB, 32GB and RAM 1 GB",
254 | "id": 16,
255 | "description": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet numquam aspernatur!"
256 | }, {
257 | "title": "Xiaomi Mi 8 128 GB (İthalatçı Garantili)",
258 | "category": "phone",
259 | "images": ["https://productimages.hepsiburada.net/s/20/280-413/9873948540978.jpg?v1", "https://productimages.hepsiburada.net/s/20/280-413/9873948573746.jpg?v1", "https://productimages.hepsiburada.net/s/20/280-413/9873948606514.jpg?v1", "https://productimages.hepsiburada.net/s/20/280-413/9873948639282.jpg?v1"],
260 | "brand": "xiaomi",
261 | "price": 1354.662450097338,
262 | "cpu": "1.3GHz Apple A6",
263 | "camera": "8mp (3264x2448)",
264 | "size": "124.4mm x 59.2mm x 8.97mm (4.9 x 2.33 x 0.35)",
265 | "weight": "132 grams (4.7 ounces) with battery",
266 | "display": "4.0 326 pixel density",
267 | "battery": "1480 mAh",
268 | "memory": "16GB, 32GB and RAM 1 GB",
269 | "id": 17,
270 | "description": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet numquam aspernatur!"
271 | }, {
272 | "title": "Apple iPhone 6S Plus 32 GB (Apple Türkiye Garantili)",
273 | "category": "phone",
274 | "images": ["https://productimages.hepsiburada.net/s/1/280-413/9489589993522.jpg?v1", "https://productimages.hepsiburada.net/s/1/280-413/9489589927986.jpg?v1", "https://productimages.hepsiburada.net/s/1/280-413/9489589960754.jpg?v1"],
275 | "brand": "apple",
276 | "price": 5467.082548922358,
277 | "cpu": "1.3GHz Apple A6",
278 | "camera": "8mp (3264x2448)",
279 | "size": "124.4mm x 59.2mm x 8.97mm (4.9 x 2.33 x 0.35)",
280 | "weight": "132 grams (4.7 ounces) with battery",
281 | "display": "4.0 326 pixel density",
282 | "battery": "1480 mAh",
283 | "memory": "16GB, 32GB and RAM 1 GB",
284 | "id": 18,
285 | "description": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet numquam aspernatur!"
286 | }, {
287 | "title": "Meizu 16 64 GB (Meizu Türkiye Garantili)",
288 | "category": "phone",
289 | "images": ["https://productimages.hepsiburada.net/s/24/280-413/10082399387698.jpg?v1", "https://productimages.hepsiburada.net/s/24/280-413/10082399420466.jpg?v1", "https://productimages.hepsiburada.net/s/24/280-413/10082399453234.jpg?v1", "https://productimages.hepsiburada.net/s/24/280-413/10082399486002.jpg?v1"],
290 | "brand": "meizu",
291 | "price": 1173.3514841571446,
292 | "cpu": "1.3GHz Apple A6",
293 | "camera": "8mp (3264x2448)",
294 | "size": "124.4mm x 59.2mm x 8.97mm (4.9 x 2.33 x 0.35)",
295 | "weight": "132 grams (4.7 ounces) with battery",
296 | "display": "4.0 326 pixel density",
297 | "battery": "1480 mAh",
298 | "memory": "16GB, 32GB and RAM 1 GB",
299 | "id": 19,
300 | "description": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet numquam aspernatur!"
301 | }, {
302 | "title": "Vestel Venüs E3 (Vestel Türkiye Garantili)",
303 | "category": "phone",
304 | "images": ["https://productimages.hepsiburada.net/s/20/280-413/9885707108402.jpg?v1", "https://productimages.hepsiburada.net/s/20/280-413/9885707141170.jpg?v1", "https://productimages.hepsiburada.net/s/20/280-413/9885707173938.jpg?v1"],
305 | "brand": "vestel",
306 | "price": 2371.8351283872053,
307 | "cpu": "1.3GHz Apple A6",
308 | "camera": "8mp (3264x2448)",
309 | "size": "124.4mm x 59.2mm x 8.97mm (4.9 x 2.33 x 0.35)",
310 | "weight": "132 grams (4.7 ounces) with battery",
311 | "display": "4.0 326 pixel density",
312 | "battery": "1480 mAh",
313 | "memory": "16GB, 32GB and RAM 1 GB",
314 | "id": 20,
315 | "description": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet numquam aspernatur!"
316 | }, {
317 | "title": "Asus Zenfone Max Pro ZB602KL 64 GB (Asus Türkiye Garantili)",
318 | "category": "phone",
319 | "images": ["https://productimages.hepsiburada.net/s/21/280-413/9924255121458.jpg?v1", "https://productimages.hepsiburada.net/s/21/280-413/9924255154226.jpg?v1", "https://productimages.hepsiburada.net/s/21/280-413/9924255186994.jpg?v1"],
320 | "brand": "asus",
321 | "price": 2603.504706144322,
322 | "cpu": "1.3GHz Apple A6",
323 | "camera": "8mp (3264x2448)",
324 | "size": "124.4mm x 59.2mm x 8.97mm (4.9 x 2.33 x 0.35)",
325 | "weight": "132 grams (4.7 ounces) with battery",
326 | "display": "4.0 326 pixel density",
327 | "battery": "1480 mAh",
328 | "memory": "16GB, 32GB and RAM 1 GB",
329 | "id": 21,
330 | "description": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet numquam aspernatur!"
331 | }, {
332 | "title": "Samsung Galaxy Note 9 128 GB (Samsung Türkiye Garantili)",
333 | "category": "phone",
334 | "images": ["https://productimages.hepsiburada.net/s/20/280-413/9902572142642.jpg?v1", "https://productimages.hepsiburada.net/s/20/280-413/9902572175410.jpg?v1", "https://productimages.hepsiburada.net/s/20/280-413/9902572208178.jpg?v1", "https://productimages.hepsiburada.net/s/20/280-413/9902572240946.jpg?v1"],
335 | "brand": "samsung",
336 | "price": 1165.0255199945123,
337 | "cpu": "1.3GHz Apple A6",
338 | "camera": "8mp (3264x2448)",
339 | "size": "124.4mm x 59.2mm x 8.97mm (4.9 x 2.33 x 0.35)",
340 | "weight": "132 grams (4.7 ounces) with battery",
341 | "display": "4.0 326 pixel density",
342 | "battery": "1480 mAh",
343 | "memory": "16GB, 32GB and RAM 1 GB",
344 | "id": 22,
345 | "description": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet numquam aspernatur!"
346 | }, {
347 | "title": "Huawei Mate 20 Lite Dual Sim 64 GB (İthalatçı Garantili)",
348 | "category": "phone",
349 | "images": ["https://productimages.hepsiburada.net/s/21/280-413/9933217792050.jpg?v1", "https://productimages.hepsiburada.net/s/21/280-413/9933217660978.jpg?v1", "https://productimages.hepsiburada.net/s/21/280-413/9933217693746.jpg?v1", "https://productimages.hepsiburada.net/s/21/280-413/9933217726514.jpg?v1"],
350 | "brand": "huawei",
351 | "price": 2693.4407990587074,
352 | "cpu": "1.3GHz Apple A6",
353 | "camera": "8mp (3264x2448)",
354 | "size": "124.4mm x 59.2mm x 8.97mm (4.9 x 2.33 x 0.35)",
355 | "weight": "132 grams (4.7 ounces) with battery",
356 | "display": "4.0 326 pixel density",
357 | "battery": "1480 mAh",
358 | "memory": "16GB, 32GB and RAM 1 GB",
359 | "id": 23,
360 | "description": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet numquam aspernatur!"
361 | }]
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.scss';
4 | import App from './App';
5 | import * as serviceWorker from './serviceWorker';
6 |
7 | ReactDOM.render(, document.getElementById('root'));
8 |
9 | serviceWorker.unregister();
10 |
--------------------------------------------------------------------------------
/src/index.scss:
--------------------------------------------------------------------------------
1 | $body-bg: #fff;
2 |
3 |
4 |
5 |
6 | @import '~bootstrap/scss/bootstrap.scss';
7 |
8 | html{ height:100%; }
9 | body{ min-height:100%; padding:0; margin:0; position:relative; }
10 |
11 | body::after{ content:''; display:block; height:100px; }
12 |
13 | footer{
14 | position:absolute;
15 | bottom:0;
16 | width:100%;
17 | height:100px;
18 | }
19 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/src/pages/Home/Home.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import FilterBar from "../../containers/FilterBar/FilterBar";
3 | import ProductList from "../../containers/ProductList/ProductList";
4 | import Pagination from "../../components/Pagination/Pagination";
5 |
6 | const Home = () => {
7 | return (
8 |
9 |
15 |
16 | );
17 | };
18 |
19 |
20 | export default Home;
21 |
--------------------------------------------------------------------------------
/src/pages/ProductDetail/ProductDetail.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {connect} from 'react-redux';
3 | import ProductDetailComponent from '../../components/ProductDetail/ProductDetail';
4 | import ProductSlider from "../../components/ProductSlider/ProductSlider";
5 |
6 | const ProductDetail = (props) => {
7 |
8 | console.log(props);
9 |
10 | return (
11 |
19 | );
20 | };
21 |
22 | const mapStateToProps = (state, props) => {
23 |
24 | const product = state.shop.products.find(product => product.id === +props.match.params.id);
25 |
26 | return {
27 | product
28 | }
29 | };
30 |
31 |
32 |
33 | export default connect(mapStateToProps, null)(ProductDetail);
34 |
--------------------------------------------------------------------------------
/src/pages/ShopingCart/ShoppingCart.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {connect} from 'react-redux';
3 | import {formatMoney} from "../../pipes/priceFormatter";
4 | import CartItem from "../../components/CartItem/CartItem";
5 |
6 | const ShoppingCart = (props) => {
7 | return (
8 | <>
9 |
10 |
11 |
12 |
13 | Shipping cart
14 |
15 |
16 |
17 | {props.cartItemCount ? props.cartItems.map(cart => (
18 |
19 | )) :
There is no product in your cart
}
20 |
21 |
22 |
23 |
24 | Total price: {formatMoney(props.totalPrice)}€
25 |
26 |
27 |
28 |
29 |
30 | >
31 | );
32 | };
33 |
34 |
35 | const mapStateToProps = state => {
36 |
37 | console.log(state, 'state has changed');
38 |
39 | return {
40 | cartItems: state.shop.cart,
41 | cartItemCount: state.shop.cart.reduce((count, curItem) => {
42 | return count + curItem.quantity;
43 | }, 0),
44 | totalPrice: state.shop.cart.reduce((count, curItem) => {
45 | return count + (curItem.price * curItem.quantity);
46 | }, 0)
47 | }
48 | }
49 |
50 | export default connect(mapStateToProps, null)(ShoppingCart);
51 |
--------------------------------------------------------------------------------
/src/pipes/brandFilter.js:
--------------------------------------------------------------------------------
1 | export const brandFilter = (arr, brand) => {
2 | if(!brand) return arr;
3 |
4 | return arr.filter(product => brand.includes(product.brand));
5 | };
--------------------------------------------------------------------------------
/src/pipes/orderByFilter.js:
--------------------------------------------------------------------------------
1 | import {ORDER_BY_ASC} from "../actions";
2 |
3 | export const orderByFilter = (arr, type ) => {
4 | if(!type) return arr;
5 | console.log('orderbYmethod', type);
6 | if(type === 'asc') {
7 | return arr.slice().sort((el1, el2) => el1.price - el2.price);
8 | } else {
9 | return arr.slice().sort((el1, el2) => el2.price - el1.price);
10 | }
11 | };
--------------------------------------------------------------------------------
/src/pipes/paginationFilter.js:
--------------------------------------------------------------------------------
1 | import {store} from "../App";
2 | import {countItem} from "../actions";
3 |
4 | export const paginationPipe = (state,args) => {
5 | if (!args || !args.perPage || !args.currentPage) {
6 | return state;
7 | }
8 | const location = (args.perPage * (args.currentPage - 1)) || 0 ;
9 |
10 | return state.slice(location, location + args.perPage);
11 | };
12 |
13 |
--------------------------------------------------------------------------------
/src/pipes/priceFormatter.js:
--------------------------------------------------------------------------------
1 | export const formatMoney = (price) => {
2 | return price.toFixed(2).replace(/\d(?=(\d{3})+\.)/g, '$&,');
3 | };
--------------------------------------------------------------------------------
/src/pipes/shortenTitle.js:
--------------------------------------------------------------------------------
1 | export const shortenTitle = (title) => {
2 | return title.split(' (')[0];
3 | };
--------------------------------------------------------------------------------
/src/reducers/brand.filter.reducer.js:
--------------------------------------------------------------------------------
1 | import {ADD_BRAND_TO_FILTER, REMOVE_BRAND_FROM_FILTER} from "../actions";
2 |
3 | export const brandFilterReducer = (state = '', action) => {
4 | switch (action.type) {
5 | case ADD_BRAND_TO_FILTER:
6 | if(state.includes(action.brand)) return state;
7 | return state += action.brand;
8 | case REMOVE_BRAND_FROM_FILTER:
9 | console.log('remove brand', action);
10 | const reg = new RegExp(action.brand, 'gi');
11 | return state.replace(reg, '');
12 | default:
13 | return state;
14 | }
15 | };
16 |
17 |
--------------------------------------------------------------------------------
/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import {combineReducers} from 'redux';
2 | import shop from './shop.reducer';
3 | import {brandFilterReducer} from "./brand.filter.reducer";
4 | import {orderByPriceReducer} from "./orderByPrice.filter.reducer";
5 | import {paginationReducer} from "./pagination.reducer";
6 |
7 | export default combineReducers({
8 | shop,
9 | brandFilter: brandFilterReducer,
10 | orderBy: orderByPriceReducer,
11 | pagination: paginationReducer
12 | });
13 |
--------------------------------------------------------------------------------
/src/reducers/orderByPrice.filter.reducer.js:
--------------------------------------------------------------------------------
1 | import {CLEAR_ORDER_BY_PRICE, ORDER_BY_ASC, ORDER_BY_DESC} from "../actions";
2 |
3 | export const orderByPriceReducer = (state = '', action) => {
4 | switch (action.type) {
5 | case ORDER_BY_ASC:
6 | return 'asc';
7 | case ORDER_BY_DESC:
8 | return 'desc';
9 | case CLEAR_ORDER_BY_PRICE:
10 | return '';
11 | default:
12 | return state;
13 | }
14 | }
--------------------------------------------------------------------------------
/src/reducers/pagination.reducer.js:
--------------------------------------------------------------------------------
1 | import {COUNT_ITEM, GO_PAGE, NEXT_PAGE, PREV_PAGE} from "../actions";
2 |
3 | const initialState = {
4 | perPage: 12,
5 | currentPage: 1,
6 | pagesToShow: 3,
7 | totalItemsCount: 0
8 | };
9 |
10 |
11 | export const paginationReducer = (state = initialState, action) => {
12 | switch (action.type) {
13 | case PREV_PAGE:
14 | if(state.currentPage === 1) return state;
15 |
16 | return {
17 | ...state,
18 | currentPage: state.currentPage - 1
19 | };
20 | case NEXT_PAGE:
21 | return {
22 | ...state,
23 | currentPage: state.currentPage + 1
24 | };
25 | case GO_PAGE:
26 | return {
27 | ...state,
28 | currentPage: action.currentPage
29 | };
30 | case COUNT_ITEM:
31 | return {
32 | ...state,
33 | totalItemsCount: action.totalItemsCount
34 | };
35 | default:
36 | return state;
37 | }
38 | };
39 |
--------------------------------------------------------------------------------
/src/reducers/shop.reducer.js:
--------------------------------------------------------------------------------
1 | import {
2 | ADD_PRODUCT_TO_CART,
3 | DECREMENT_CART_ITEM_QUANTITY,
4 | INCREMENT_CART_ITEM_QUANTITY,
5 | REMOVE_PRODUCT_FROM_CART
6 | } from '../actions';
7 | import {phones} from "../data/phones";
8 |
9 | const initialState = {
10 | products: phones,
11 | cart: []
12 | };
13 |
14 |
15 | const shopReducer = (state = initialState, action ) => {
16 | let updatedCart;
17 | let updatedItemIndex;
18 |
19 | switch (action.type) {
20 | case INCREMENT_CART_ITEM_QUANTITY:
21 | updatedCart = [...state.cart];
22 | updatedItemIndex = updatedCart.findIndex(
23 | item => item.id === action.payload
24 | );
25 |
26 | const incrementedItem = {
27 | ...updatedCart[updatedItemIndex]
28 | };
29 |
30 | incrementedItem.quantity++;
31 |
32 | updatedCart[updatedItemIndex] = incrementedItem;
33 |
34 |
35 | return {...state, cart: updatedCart};
36 |
37 | case DECREMENT_CART_ITEM_QUANTITY:
38 | updatedCart = [...state.cart];
39 | updatedItemIndex = updatedCart.findIndex(
40 | item => item.id === action.payload
41 | );
42 |
43 | const decrementedItem = {
44 | ...updatedCart[updatedItemIndex]
45 | };
46 |
47 | decrementedItem.quantity--;
48 |
49 | updatedCart[updatedItemIndex] = decrementedItem;
50 |
51 | return {...state, cart: updatedCart};
52 |
53 | case ADD_PRODUCT_TO_CART:
54 | updatedCart = [...state.cart];
55 | updatedItemIndex = updatedCart.findIndex(item => item.id === action.payload.id);
56 |
57 | if(updatedItemIndex < 0) {
58 | updatedCart.push({...action.payload, quantity: 1});
59 | } else {
60 | const updatedItem = {
61 | ...updatedCart[updatedItemIndex]
62 | };
63 |
64 | updatedItem.quantity++;
65 | updatedCart[updatedItemIndex] = updatedItem;
66 | }
67 |
68 | return {...state, cart: updatedCart};
69 | case REMOVE_PRODUCT_FROM_CART:
70 | updatedCart = [...state.cart];
71 | updatedItemIndex = updatedCart.findIndex(
72 | item => item.id === action.payload
73 | );
74 |
75 | updatedCart.splice(updatedItemIndex, 1);
76 |
77 | return {...state, cart: updatedCart};
78 | default:
79 | return state;
80 |
81 | }
82 | };
83 |
84 | export default shopReducer;
85 |
--------------------------------------------------------------------------------
/src/serviceWorker.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read http://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.1/8 is considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export function register(config) {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit http://bit.ly/CRA-PWA'
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | if (installingWorker == null) {
64 | return;
65 | }
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === 'installed') {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the updated precached content has been fetched,
70 | // but the previous service worker will still serve the older
71 | // content until all client tabs are closed.
72 | console.log(
73 | 'New content is available and will be used when all ' +
74 | 'tabs for this page are closed. See http://bit.ly/CRA-PWA.'
75 | );
76 |
77 | // Execute callback
78 | if (config && config.onUpdate) {
79 | config.onUpdate(registration);
80 | }
81 | } else {
82 | // At this point, everything has been precached.
83 | // It's the perfect time to display a
84 | // "Content is cached for offline use." message.
85 | console.log('Content is cached for offline use.');
86 |
87 | // Execute callback
88 | if (config && config.onSuccess) {
89 | config.onSuccess(registration);
90 | }
91 | }
92 | }
93 | };
94 | };
95 | })
96 | .catch(error => {
97 | console.error('Error during service worker registration:', error);
98 | });
99 | }
100 |
101 | function checkValidServiceWorker(swUrl, config) {
102 | // Check if the service worker can be found. If it can't reload the page.
103 | fetch(swUrl)
104 | .then(response => {
105 | // Ensure service worker exists, and that we really are getting a JS file.
106 | const contentType = response.headers.get('content-type');
107 | if (
108 | response.status === 404 ||
109 | (contentType != null && contentType.indexOf('javascript') === -1)
110 | ) {
111 | // No service worker found. Probably a different app. Reload the page.
112 | navigator.serviceWorker.ready.then(registration => {
113 | registration.unregister().then(() => {
114 | window.location.reload();
115 | });
116 | });
117 | } else {
118 | // Service worker found. Proceed as normal.
119 | registerValidSW(swUrl, config);
120 | }
121 | })
122 | .catch(() => {
123 | console.log(
124 | 'No internet connection found. App is running in offline mode.'
125 | );
126 | });
127 | }
128 |
129 | export function unregister() {
130 | if ('serviceWorker' in navigator) {
131 | navigator.serviceWorker.ready.then(registration => {
132 | registration.unregister();
133 | });
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/src/utilities/cumulativeOffset.js:
--------------------------------------------------------------------------------
1 | export const cumulativeOffSet = (element) => {
2 | let top = 0, left = 0;
3 | do {
4 | top += element.offsetTop || 0;
5 | left += element.offsetLeft || 0;
6 | element = element.offsetParent;
7 | } while(element);
8 |
9 | return {
10 | top: top,
11 | left: left
12 | };
13 | };
--------------------------------------------------------------------------------