├── .gitignore
├── README.md
├── package.json
├── public
├── favicon.ico
├── index.html
└── manifest.json
├── src
├── Calculator.css
├── Calculator.js
├── index.css
├── index.js
└── registerServiceWorker.js
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # IDE
4 | .idea
5 |
6 | # dependencies
7 | /node_modules
8 |
9 | # testing
10 | /coverage
11 |
12 | # production
13 | /build
14 |
15 | # misc
16 | .DS_Store
17 | .env.local
18 | .env.development.local
19 | .env.test.local
20 | .env.production.local
21 | *.swp
22 | *~
23 | Thumbs.db
24 | .project
25 | .nvm-version
26 | /tags
27 | /atom-shell/
28 | /out/
29 | docs/output
30 | docs/includes
31 | out/
32 | /electron/
33 |
34 | debug.log
35 | npm-debug.log*
36 | yarn-debug.log*
37 | yarn-error.log*
38 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Learning React and Redux: decoupling with stateless components
2 |
3 | Welcome to official repository for Udemy course
4 | [Learning React and Redux: decoupling with stateless components](https://www.udemy.com/course/1326760/)
5 | by [Mateusz Grzesiukiewicz](https://www.linkedin.com/in/mateusz-grzesiukiewicz-8556a030/)
6 |
7 | ## Install
8 | ```
9 | yarn install
10 | yarn start
11 | ```
12 |
13 | ## Who is this course for?
14 | - Those with **Javascript** skills who want to learn React library and start with good practises
15 | - Experienced **React** developers who struggle to maintain their projects
16 | - Anyone who strives to write **reusable code** using modern Javascript libraries
17 | - Redux users who embrace Flux architecture but use other library for Views than React
18 | - Those who struggle to write easily **testable React or Redux code**
19 |
20 | ## You will learn how to...
21 | - Create reusable **stateless** and easily testable components
22 | - Create pure & easily testable action handlers (**reducers**)
23 | - Connect stateless views with stateless reducers through **React containers**
24 | - Refactor applications to be more testable and reusable (**decoupled**)
25 | - Understand React **Flux architecture** and how to connect all the bits
26 |
27 | ## Take the course
28 | [Learning React and Redux: decoupling with stateless components](https://www.udemy.com/course/1326760/)
29 |
30 | ## Create react app boilerplate
31 |
32 | This project was bootstrapped with [Create React App](https://github.com/facebookincubator/create-react-app).
33 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "decouple-react-redux",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "react": "^15.6.1",
7 | "react-dom": "^15.6.1",
8 | "react-scripts": "1.0.11",
9 | "wolfy87-eventemitter": "^5.2.2"
10 | },
11 | "scripts": {
12 | "start": "react-scripts start",
13 | "build": "react-scripts build",
14 | "test": "react-scripts test --env=jsdom",
15 | "eject": "react-scripts eject"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ajdija/Learning-React-Redux-decoupling-with-stateless-components/ba40a3b04c421bb4eeba771bdf2e5eedc5af5a65/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
22 | React App
23 |
24 |
25 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Learning React & Redux: decoupling with stateless components",
3 | "name": "Repository for Udemy course Learning React & Redux: decoupling with stateless components by Mateusz Grzesiukiewicz",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/src/Calculator.css:
--------------------------------------------------------------------------------
1 | /**
2 | * This css was inspired by https://codepen.io/tbremer/pen/wKpaWe
3 | */
4 |
5 | .react-calculator {
6 | position: relative;
7 | margin: 0 auto;
8 | width: 440px;
9 |
10 | box-shadow: 0 15px 45px rgba(19, 19, 19, .24), 0 15px 45px rgba(19, 19, 19, .12);
11 | }
12 |
13 | button {
14 | outline: none;
15 | border: 0;
16 | padding: 1rem;
17 | background-color: #292f36;
18 | font-size: 1.5rem;
19 | line-height: 1;
20 | color: #FFFDF7;
21 |
22 | -webkit-transition: all 300ms cubic-bezier(1, 1, 1, 1);
23 | transition: all 300ms cubic-bezier(1, 1, 1, 1);
24 | }
25 |
26 | button:hover {
27 | color: #FFE66D;
28 | }
29 |
30 | button .block {
31 | width: 100%;
32 | }
33 |
34 | button .transparent {
35 | background-color: transparent;
36 | }
37 |
38 | button .no-padding {
39 | padding: 0;
40 | }
41 |
42 | button .control {
43 | font-size: 1rem;
44 | }
45 |
46 | -webkit-scrollbar, .display::-webkit-scrollbar, .history::-webkit-scrollbar {
47 | width: .5rem;
48 | }
49 |
50 | -webkit-scrollbar:horizontal,
51 | .display::-webkit-scrollbar:horizontal,
52 | .history::-webkit-scrollbar:horizontal {
53 | height: .5rem;
54 | }
55 |
56 | -webkit-scrollbar-track,
57 | -webkit-scrollbar:horizontal,
58 | .display::-webkit-scrollbar-track,
59 | .display::-webkit-scrollbar:horizontal,
60 | .history::-webkit-scrollbar-track,
61 | .history::-webkit-scrollbar:horizontal {
62 | background-color: #131313;
63 | }
64 |
65 | -webkit-scrollbar-thumb,
66 | -webkit-scrollbar:horizontal,
67 | .display::-webkit-scrollbar-thumb,
68 | .display::-webkit-scrollbar:horizontal,
69 | .history::-webkit-scrollbar-thumb,
70 | .history::-webkit-scrollbar:horizontal {
71 | background-color: #FFE66D;
72 | }
73 |
74 | hover::-webkit-scrollbar-thumb,
75 | hover::-webkit-scrollbar:horizontal,
76 | .display:hover::-webkit-scrollbar-thumb,
77 | .display:hover::-webkit-scrollbar:horizontal,
78 | .history:hover::-webkit-scrollbar-thumb,
79 | .history:hover::-webkit-scrollbar:horizontal {
80 | background-color: #FFE66D;
81 | }
82 |
83 | .display {
84 | /*Positioning*/
85 | position: relative;
86 | width: 440px;
87 | height: 120px;
88 | z-index: 10;
89 |
90 | /*Formatting*/
91 | outline: none;
92 | box-shadow: 0 4px 2px -2px rgba(19, 19, 19, .64);
93 | padding: .5rem;
94 | overflow-y: hidden;
95 | overflow-x: scroll;
96 | font-size: 3rem;
97 | line-height: 2;
98 | text-align: right;
99 | direction: rtl;
100 | white-space: nowrap;
101 |
102 | /*Colors*/
103 | background-color: rgba(19, 19, 19, .64);
104 | color: #FFE66D;
105 | }
106 |
107 | .history {
108 | /*Positioning*/
109 | position: absolute;
110 | top: 120px;
111 | left: 0;
112 | z-index: 10;
113 |
114 | /*Formatting*/
115 | width: 100%;
116 | height: 0;
117 | overflow: hidden;
118 | padding: 0;
119 |
120 | /*Colors*/
121 | background-color: rgba(19, 19, 19, .64);
122 | color: #00a3f5;
123 |
124 | /*Animation*/
125 | -webkit-transition: height 150ms cubic-bezier(1, 1, 1, 1),
126 | overflow 1ms cubic-bezier(1, 1, 1, 1) 200ms,
127 | padding 1ms cubic-bezier(1, 1, 1, 1) 200ms;
128 |
129 | transition: height 150ms cubic-bezier(1, 1, 1, 1),
130 | overflow 1ms cubic-bezier(1, 1, 1, 1) 200ms,
131 | padding 1ms cubic-bezier(1, 1, 1, 1) 200ms;
132 | }
133 |
134 | .history .toggle-close {
135 | position: absolute;
136 | top: 5px;
137 | right: 5px;
138 | padding: 2px 5px;
139 | }
140 |
141 | .history .toggle-close .title {
142 | display: inline-block;
143 |
144 | /*Rotation - plus sign*/
145 | -webkit-transform: rotate(135deg);
146 | transform: rotate(135deg);
147 | }
148 |
149 | .history.visible {
150 | /*Formatting*/
151 | height: calc(100% - 120px);
152 | padding: 10px;
153 |
154 | /*Overflow*/
155 | overflow-y: auto;
156 |
157 | /*Animation*/
158 | -webkit-transition: height 200ms cubic-bezier(1, 1, 1, 1), padding 1ms cubic-bezier(1, 1, 1, 1);
159 | transition: height 200ms cubic-bezier(1, 1, 1, 1), padding 1ms cubic-bezier(1, 1, 1, 1);
160 | }
161 |
162 | .buttons--controls,
163 | .buttons--operators {
164 | background-color: #292f36;
165 | }
166 |
167 | .buttons--controls button, .buttons--operators button {
168 | /*Formatting*/
169 | display: inline-block;
170 | width: 110px;
171 | height: 110px;
172 | vertical-align: top;
173 |
174 | /*Color*/
175 | color: #FFE66D;
176 |
177 | /*Uppercase transform*/
178 | text-transform: uppercase;
179 | -webkit-font-feature-settings: "c2sc", "c2sc", "c2sc";
180 | -moz-font-feature-settings: "c2sc", "c2sc", "c2sc";
181 | font-feature-settings: "c2sc", "c2sc", "c2sc";
182 | font-variant: small-caps;
183 | }
184 |
185 | .buttons--controls button:hover, .buttons--operators button:hover {
186 | color: #c41c4f;
187 | }
188 |
189 | .buttons--digits {
190 | /*Formatting*/
191 | width: 330px;
192 | float: left;
193 |
194 | /*Color*/
195 | background-color: #292f36;
196 | }
197 |
198 | .buttons--digits button {
199 | /*Formatting*/
200 | display: block;
201 | position: relative;
202 | width: 110px;
203 | height: 110px;
204 | float: left;
205 |
206 | /*Color*/
207 | background-color: #292f36;
208 |
209 | /*Transforms*/
210 | -webkit-transition: box-shadow 300ms cubic-bezier(1, 1, 1, 1);
211 | transition: box-shadow 300ms cubic-bezier(1, 1, 1, 1);
212 | }
213 |
214 | .buttons--digits button:last-child {
215 | width: 100%;
216 | }
217 |
218 | .buttons--controls {
219 | clear: left;
220 | float: left;
221 | width: 330px;
222 | height: 110px;
223 | }
224 |
--------------------------------------------------------------------------------
/src/Calculator.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import './Calculator.css';
3 | import EventEmitter from 'wolfy87-eventemitter';
4 |
5 | class Calculator extends Component {
6 | render() {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | )
16 | }
17 | }
18 |
19 | const ee = new EventEmitter();
20 | let store = {
21 | displayedExpression: 0,
22 | get curExpression() {
23 | return this.displayedExpression;
24 | },
25 | set newExpression(curExpression) {
26 | this.displayedExpression = curExpression;
27 | ee.emitEvent('displayUpdate', [this.curExpression]);
28 | ee.emitEvent('historyUpdate', [this.curExpression]);
29 | }
30 | };
31 |
32 |
33 | class Display extends Component {
34 | constructor(props) {
35 | super(props);
36 |
37 | this.state = { text: this.props.text || '0' };
38 | this.updateDisplay = this.updateDisplay.bind(this);
39 | this.onClickHandler = this.onClickHandler.bind(this);
40 | }
41 |
42 | updateDisplay(newStr) {
43 | return this.setState({ text: newStr.toString().split(' ').reverse().join(' ') });
44 | }
45 |
46 | componentWillMount() {
47 | ee.addListener('displayUpdate', this.updateDisplay);
48 | }
49 |
50 | onClickHandler() {
51 | if (this.props.clickHandler) {
52 | this.props.clickHandler.call(this);
53 | }
54 | }
55 |
56 | render() {
57 | return {this.state.text}
58 | }
59 | }
60 |
61 | class ControlPanel extends Component {
62 | showHistory() {
63 | ee.emitEvent('toggle-history');
64 | }
65 |
66 | clearDisplay() {
67 | store.newExpression = 0;
68 | }
69 |
70 | removeOneChar() {
71 | const curExpression = String(store.curExpression);
72 | const newExpWithRemovedChar = curExpression.toString().trim().substring(0, (curExpression.length - 1));
73 |
74 | return store.newExpression = newExpWithRemovedChar === '' ? 0 : newExpWithRemovedChar;
75 | }
76 |
77 | render() {
78 | return (
79 |
84 | )
85 | }
86 | }
87 |
88 | class Operators extends Component {
89 | opHandler(type) {
90 | store.newExpression = `${store.curExpression} ${type} `;
91 | }
92 |
93 | calculateExpression() {
94 | /* eslint-disable */
95 | // This rule is important in production apps!
96 | // Read more: https://eslint.org/docs/rules/no-eval
97 | // To simplify the functionality in this course we use eval
98 | const calcFunc = eval;
99 | /* eslint-enable */
100 | try {
101 | store.newExpression = calcFunc(store.curExpression);
102 | } catch (e) {
103 | console.error("Error: Incorrect Expression of digits & operators :(")
104 | }
105 | }
106 |
107 | render() {
108 | return (
109 |
110 | {["+", "-", "*", "/"]
111 | .map((op, i) => (
112 |
116 | )
117 | }
118 | }
119 |
120 | class Digits extends Component {
121 | digitClickHandler(num) {
122 | if (!store.curExpression) {
123 | return store.newExpression = num;
124 | }
125 |
126 | return store.newExpression = `${store.curExpression}${num}`;
127 | }
128 |
129 | render() {
130 | return
131 | {[1, 2, 3, 4, 5, 6, 7, 8, 9, 0]
132 | .map(nr => )}
133 |
134 | }
135 | }
136 |
137 | class History extends Component {
138 | constructor(props) {
139 | super(props);
140 |
141 | this.state = { show: false, history: [] };
142 | this.toggleHistory = this.toggleHistory.bind(this);
143 | this.addHistoryItem = this.addHistoryItem.bind(this);
144 | this.getHistoryItems = this.getHistoryItems.bind(this);
145 | }
146 |
147 | componentWillMount() {
148 | ee.addListener('historyUpdate', this.addHistoryItem);
149 | ee.addListener('toggle-history', this.toggleHistory);
150 | }
151 |
152 | addHistoryItem(historyItem) {
153 | const trimmedItem = historyItem.toString().trim();
154 | if (this.getHistoryItems().filter(i => i === trimmedItem).length === 0) {
155 | this.setState({
156 | ...this.state,
157 | history: [
158 | ...this.state.history,
159 | trimmedItem
160 | ]
161 | });
162 | }
163 | }
164 |
165 | getHistoryItems() {
166 | return this.state.history.filter(h => !!h);
167 | }
168 |
169 | toggleHistory() {
170 | this.setState({ ...this.state, show: !this.state.show });
171 | }
172 |
173 | historyItemClickHandler(history) {
174 | store.newExpression = history;
175 | ee.emitEvent('toggle-history');
176 | }
177 |
178 | render() {
179 | return (
180 |
181 |
182 | {this.getHistoryItems().map((mem, i) => (
183 |
185 | ))}
186 |
187 | )
188 | }
189 | }
190 |
191 | class Button extends Component {
192 | constructor(props) {
193 | super(props);
194 | this.onClickHandler = this.onClickHandler.bind(this);
195 | }
196 |
197 | onClickHandler() {
198 | if (this.props.clickHandler) {
199 | this.props.clickHandler.call(null, this.props.text);
200 | }
201 | }
202 |
203 | render() {
204 | return (
205 |
206 | {this.props.text}
207 |
208 | );
209 | }
210 | }
211 |
212 | export default Calculator;
213 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | *,
2 | *:before,
3 | *:after {
4 | box-sizing: border-box;
5 | }
6 |
7 | html,
8 | body {
9 | position: relative;
10 |
11 | /*Formatting*/
12 | padding-top: 20px;
13 | font-size: 16px;
14 | width: 100%;
15 | height: 100%;
16 |
17 | /*Font*/
18 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
19 |
20 | /*Select clear*/
21 | -webkit-user-select: none;
22 | -moz-user-select: none;
23 | -ms-user-select: none;
24 | user-select: none;
25 | }
26 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import Calculator from './Calculator';
5 | import registerServiceWorker from './registerServiceWorker';
6 |
7 | ReactDOM.render(, document.getElementById('root'));
8 | registerServiceWorker();
9 |
--------------------------------------------------------------------------------
/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | // In production, we register a service worker to serve assets from local cache.
2 |
3 | // This lets the app load faster on subsequent visits in production, and gives
4 | // it offline capabilities. However, it also means that developers (and users)
5 | // will only see deployed updates on the "N+1" visit to a page, since previously
6 | // cached resources are updated in the background.
7 |
8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
9 | // This link also includes instructions on opting out of this behavior.
10 |
11 | const isLocalhost = Boolean(
12 | window.location.hostname === 'localhost' ||
13 | // [::1] is the IPv6 localhost address.
14 | window.location.hostname === '[::1]' ||
15 | // 127.0.0.1/8 is considered localhost for IPv4.
16 | window.location.hostname.match(
17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
18 | )
19 | );
20 |
21 | export default function register() {
22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
23 | // The URL constructor is available in all browsers that support SW.
24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
25 | if (publicUrl.origin !== window.location.origin) {
26 | // Our service worker won't work if PUBLIC_URL is on a different origin
27 | // from what our page is served on. This might happen if a CDN is used to
28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
29 | return;
30 | }
31 |
32 | window.addEventListener('load', () => {
33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
34 |
35 | if (!isLocalhost) {
36 | // Is not local host. Just register service worker
37 | registerValidSW(swUrl);
38 | } else {
39 | // This is running on localhost. Lets check if a service worker still exists or not.
40 | checkValidServiceWorker(swUrl);
41 | }
42 | });
43 | }
44 | }
45 |
46 | function registerValidSW(swUrl) {
47 | navigator.serviceWorker
48 | .register(swUrl)
49 | .then(registration => {
50 | registration.onupdatefound = () => {
51 | const installingWorker = registration.installing;
52 | installingWorker.onstatechange = () => {
53 | if (installingWorker.state === 'installed') {
54 | if (navigator.serviceWorker.controller) {
55 | // At this point, the old content will have been purged and
56 | // the fresh content will have been added to the cache.
57 | // It's the perfect time to display a "New content is
58 | // available; please refresh." message in your web app.
59 | console.log('New content is available; please refresh.');
60 | } else {
61 | // At this point, everything has been precached.
62 | // It's the perfect time to display a
63 | // "Content is cached for offline use." message.
64 | console.log('Content is cached for offline use.');
65 | }
66 | }
67 | };
68 | };
69 | })
70 | .catch(error => {
71 | console.error('Error during service worker registration:', error);
72 | });
73 | }
74 |
75 | function checkValidServiceWorker(swUrl) {
76 | // Check if the service worker can be found. If it can't reload the page.
77 | fetch(swUrl)
78 | .then(response => {
79 | // Ensure service worker exists, and that we really are getting a JS file.
80 | if (
81 | response.status === 404 ||
82 | response.headers.get('content-type').indexOf('javascript') === -1
83 | ) {
84 | // No service worker found. Probably a different app. Reload the page.
85 | navigator.serviceWorker.ready.then(registration => {
86 | registration.unregister().then(() => {
87 | window.location.reload();
88 | });
89 | });
90 | } else {
91 | // Service worker found. Proceed as normal.
92 | registerValidSW(swUrl);
93 | }
94 | })
95 | .catch(() => {
96 | console.log(
97 | 'No internet connection found. App is running in offline mode.'
98 | );
99 | });
100 | }
101 |
102 | export function unregister() {
103 | if ('serviceWorker' in navigator) {
104 | navigator.serviceWorker.ready.then(registration => {
105 | registration.unregister();
106 | });
107 | }
108 | }
109 |
--------------------------------------------------------------------------------