├── .eslintrc.js ├── .gitignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── products.json ├── shopcart.json ├── src ├── actions │ ├── index.js │ ├── product.js │ └── shopcart.js ├── app.js ├── components │ ├── loading_view.js │ ├── modal_view.js │ ├── product_list.js │ └── shop_cart.js ├── constants │ └── actionTypes.js ├── containers │ └── app.js ├── index.html ├── pages │ ├── home.js │ └── intro.js └── reducers │ ├── index.js │ ├── product.js │ └── shopcart.js └── webpack.config.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": [ 3 | "google", 4 | "plugin:react/recommended" 5 | ], 6 | "parser": "babel-eslint", 7 | "parserOptions": { 8 | "sourceType": "module", 9 | "allowImportExportEverywhere": true 10 | }, 11 | "rules": { 12 | "max-len": [1, 120, 2, { 13 | "ignorePattern": "^import\\s.+\\sfrom\\s.+;$", 14 | "ignoreUrls": true 15 | }], 16 | } 17 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/node,webstorm,reactnative,visualstudiocode 3 | 4 | ### Node ### 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | 24 | # nyc test coverage 25 | .nyc_output 26 | 27 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 28 | .grunt 29 | 30 | # Bower dependency directory (https://bower.io/) 31 | bower_components 32 | 33 | # node-waf configuration 34 | .lock-wscript 35 | 36 | # Compiled binary addons (http://nodejs.org/api/addons.html) 37 | build/Release 38 | 39 | # Dependency directories 40 | node_modules/ 41 | jspm_packages/ 42 | 43 | # Typescript v1 declaration files 44 | typings/ 45 | 46 | # Optional npm cache directory 47 | .npm 48 | 49 | # Optional eslint cache 50 | .eslintcache 51 | 52 | # Optional REPL history 53 | .node_repl_history 54 | 55 | # Output of 'npm pack' 56 | *.tgz 57 | 58 | # Yarn Integrity file 59 | .yarn-integrity 60 | 61 | # dotenv environment variables file 62 | .env 63 | 64 | 65 | ### ReactNative ### 66 | # React Native Stack Base 67 | ### ReactNative.macOS Stack ### 68 | *.DS_Store 69 | .AppleDouble 70 | .LSOverride 71 | 72 | # Icon must end with two \r 73 | Icon 74 | 75 | 76 | # Thumbnails 77 | ._* 78 | 79 | # Files that might appear in the root of a volume 80 | .DocumentRevisions-V100 81 | .fseventsd 82 | .Spotlight-V100 83 | .TemporaryItems 84 | .Trashes 85 | .VolumeIcon.icns 86 | .com.apple.timemachine.donotpresent 87 | 88 | # Directories potentially created on remote AFP share 89 | .AppleDB 90 | .AppleDesktop 91 | Network Trash Folder 92 | Temporary Items 93 | .apdisk 94 | 95 | ### ReactNative.Linux Stack ### 96 | *~ 97 | 98 | # temporary files which can be created if a process still has a handle open of a deleted file 99 | .fuse_hidden* 100 | 101 | # KDE directory preferences 102 | .directory 103 | 104 | # Linux trash folder which might appear on any partition or disk 105 | .Trash-* 106 | 107 | # .nfs files are created when an open file is removed but is still being accessed 108 | .nfs* 109 | 110 | ### ReactNative.Android Stack ### 111 | # Built application files 112 | *.apk 113 | *.ap_ 114 | 115 | # Files for the ART/Dalvik VM 116 | *.dex 117 | 118 | # Java class files 119 | *.class 120 | 121 | # Generated files 122 | bin/ 123 | gen/ 124 | out/ 125 | 126 | # Gradle files 127 | .gradle/ 128 | build/ 129 | 130 | # Local configuration file (sdk path, etc) 131 | local.properties 132 | 133 | # Proguard folder generated by Eclipse 134 | proguard/ 135 | 136 | # Log Files 137 | 138 | # Android Studio Navigation editor temp files 139 | .navigation/ 140 | 141 | # Android Studio captures folder 142 | captures/ 143 | 144 | # Intellij 145 | *.iml 146 | .idea/workspace.xml 147 | .idea/tasks.xml 148 | .idea/gradle.xml 149 | .idea/dictionaries 150 | .idea/libraries 151 | 152 | # Keystore files 153 | *.jks 154 | 155 | # External native build folder generated in Android Studio 2.2 and later 156 | .externalNativeBuild 157 | 158 | # Google Services (e.g. APIs or Firebase) 159 | google-services.json 160 | 161 | # Freeline 162 | freeline.py 163 | freeline/ 164 | freeline_project_description.json 165 | 166 | ### ReactNative.Gradle Stack ### 167 | .gradle 168 | /build/ 169 | 170 | # Ignore Gradle GUI config 171 | gradle-app.setting 172 | 173 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 174 | !gradle-wrapper.jar 175 | 176 | # Cache of project 177 | .gradletasknamecache 178 | 179 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 180 | # gradle/wrapper/gradle-wrapper.properties 181 | 182 | ### ReactNative.Node Stack ### 183 | # Logs 184 | 185 | # Runtime data 186 | 187 | # Directory for instrumented libs generated by jscoverage/JSCover 188 | 189 | # Coverage directory used by tools like istanbul 190 | 191 | # nyc test coverage 192 | 193 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 194 | 195 | # Bower dependency directory (https://bower.io/) 196 | 197 | # node-waf configuration 198 | 199 | # Compiled binary addons (http://nodejs.org/api/addons.html) 200 | 201 | # Dependency directories 202 | 203 | # Typescript v1 declaration files 204 | 205 | # Optional npm cache directory 206 | 207 | # Optional eslint cache 208 | 209 | # Optional REPL history 210 | 211 | # Output of 'npm pack' 212 | 213 | # Yarn Integrity file 214 | 215 | # dotenv environment variables file 216 | 217 | 218 | ### ReactNative.Xcode Stack ### 219 | # Xcode 220 | # 221 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 222 | 223 | ## Build generated 224 | DerivedData/ 225 | 226 | ## Various settings 227 | *.pbxuser 228 | !default.pbxuser 229 | *.mode1v3 230 | !default.mode1v3 231 | *.mode2v3 232 | !default.mode2v3 233 | *.perspectivev3 234 | !default.perspectivev3 235 | xcuserdata/ 236 | 237 | ## Other 238 | *.moved-aside 239 | *.xccheckout 240 | *.xcscmblueprint 241 | 242 | ### ReactNative.Buck Stack ### 243 | buck-out/ 244 | .buckconfig.local 245 | .buckd/ 246 | .buckversion 247 | .fakebuckversion 248 | 249 | ### VisualStudioCode ### 250 | .vscode/* 251 | !.vscode/settings.json 252 | !.vscode/tasks.json 253 | !.vscode/launch.json 254 | !.vscode/extensions.json 255 | 256 | ### WebStorm ### 257 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 258 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 259 | 260 | # User-specific stuff: 261 | .idea/**/workspace.xml 262 | .idea/**/tasks.xml 263 | 264 | # Sensitive or high-churn files: 265 | .idea/**/dataSources/ 266 | .idea/**/dataSources.ids 267 | .idea/**/dataSources.xml 268 | .idea/**/dataSources.local.xml 269 | .idea/**/sqlDataSources.xml 270 | .idea/**/dynamic.xml 271 | .idea/**/uiDesigner.xml 272 | 273 | # Gradle: 274 | .idea/**/gradle.xml 275 | .idea/**/libraries 276 | 277 | # CMake 278 | cmake-build-debug/ 279 | 280 | # Mongo Explorer plugin: 281 | .idea/**/mongoSettings.xml 282 | 283 | ## File-based project format: 284 | *.iws 285 | 286 | ## Plugin-specific files: 287 | 288 | # IntelliJ 289 | /out/ 290 | 291 | # mpeltonen/sbt-idea plugin 292 | .idea_modules/ 293 | 294 | # JIRA plugin 295 | atlassian-ide-plugin.xml 296 | 297 | # Cursive Clojure plugin 298 | .idea/replstate.xml 299 | 300 | # Crashlytics plugin (for Android Studio and IntelliJ) 301 | com_crashlytics_export_strings.xml 302 | crashlytics.properties 303 | crashlytics-build.properties 304 | fabric.properties 305 | 306 | ### WebStorm Patch ### 307 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 308 | 309 | # *.iml 310 | # modules.xml 311 | # .idea/misc.xml 312 | # *.ipr 313 | 314 | # Sonarlint plugin 315 | .idea/sonarlint 316 | # End of https://www.gitignore.io/api/node,webstorm,reactnative,visualstudiocode 317 | 318 | dist/ 319 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Eric Ping 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## React Redux Shopcart Example 2 | This is an example for beginners to learn redux. 3 | ## Prerequisites 4 | Latest Node.js and NPM 5 | ### Installation 6 | ```bash 7 | npm install 8 | ``` 9 | ### Usage 10 | ```bash 11 | npm start 12 | ``` 13 | and open http://localhost:8080 in the browser -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-redux-shopcart", 3 | "version": "1.0.0", 4 | "description": "React + Redux + Shopcart Example", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "webpack-dev-server", 8 | "build": "NODE_ENV=production webpack", 9 | "test": "echo \"Error: no test specified\" && exit 1", 10 | "eslint": "./node_modules/.bin/eslint ./src/*.js* ./src/**/*.js* --fix" 11 | }, 12 | "keywords": [ 13 | "react", 14 | "redux", 15 | "shopcart" 16 | ], 17 | "author": "Eric Ping", 18 | "license": "MIT", 19 | "devDependencies": { 20 | "babel-cli": "^6.24.1", 21 | "babel-core": "^6.25.0", 22 | "babel-eslint": "^7.2.3", 23 | "babel-loader": "^7.0.0", 24 | "babel-preset-env": "^1.5.2", 25 | "babel-preset-react": "^6.24.1", 26 | "babel-preset-stage-3": "^6.24.1", 27 | "css-loader": "^0.28.4", 28 | "eslint": "^4.0.0", 29 | "eslint-config-google": "^0.8.0", 30 | "eslint-plugin-react": "^7.0.1", 31 | "file-loader": "^0.11.2", 32 | "html-loader": "^0.5.1", 33 | "html-webpack-plugin": "^2.30.1", 34 | "node-sass": "^4.5.3", 35 | "sass-loader": "^6.0.5", 36 | "style-loader": "^0.18.2", 37 | "webpack": "^2.6.1", 38 | "webpack-dev-server": "^2.4.5" 39 | }, 40 | "dependencies": { 41 | "bootstrap": "^4.0.0", 42 | "classnames": "^2.2.5", 43 | "colors": "^1.1.2", 44 | "font-awesome": "^4.7.0", 45 | "history": "^4.7.2", 46 | "jquery": "^3.2.1", 47 | "popper.js": "^1.12.9", 48 | "prop-types": "^15.5.10", 49 | "react": "^16.0.0", 50 | "react-dom": "^16.0.0", 51 | "react-redux": "^5.0.5", 52 | "react-router": "^4.2.0", 53 | "react-router-dom": "^4.2.2", 54 | "react-router-redux": "^5.0.0-alpha.8", 55 | "redux": "^3.7.0", 56 | "redux-thunk": "^2.2.0", 57 | "styled-jsx": "^2.1.2" 58 | }, 59 | "repository": { 60 | "type": "git", 61 | "url": "https://github.com/EricPing/react-redux-shopcart.git" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /products.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 1, 4 | "title": "哈利波特 - 神秘的魔法石", 5 | "img": "https://unsplash.it/400/300/?random&1", 6 | "price": 15, 7 | "discount": 5 8 | }, 9 | { 10 | "id": 2, 11 | "title": "波西傑克森 - 神火之賊", 12 | "img": "https://unsplash.it/400/300/?random&2", 13 | "price": 20, 14 | "discount": 5 15 | }, 16 | { 17 | "id": 3, 18 | "title": "刀劍神域 1", 19 | "img": "https://unsplash.it/400/300/?random&3", 20 | "price": 25, 21 | "discount": 5 22 | }, 23 | { 24 | "id": 4, 25 | "title": "加速世界 1", 26 | "img": "https://unsplash.it/400/300/?random&1", 27 | "price": 15, 28 | "discount": 5 29 | }, 30 | { 31 | "id": 5, 32 | "title": "零之使魔 1", 33 | "img": "https://unsplash.it/400/300/?random&2", 34 | "price": 20, 35 | "discount": 5 36 | }, 37 | { 38 | "id": 6, 39 | "title": "歷史守護者 1", 40 | "img": "https://unsplash.it/400/300/?random&3", 41 | "price": 25, 42 | "discount": 5 43 | } 44 | ] -------------------------------------------------------------------------------- /shopcart.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /src/actions/index.js: -------------------------------------------------------------------------------- 1 | import * as productActions from './product'; 2 | import * as shopcartActions from './shopcart'; 3 | 4 | export default {...productActions, ...shopcartActions}; 5 | -------------------------------------------------------------------------------- /src/actions/product.js: -------------------------------------------------------------------------------- 1 | import {GETTING_PRODUCT_LIST, 2 | GET_PRODUCT_LIST_SUCCESS} from '../constants/actionTypes'; 3 | 4 | /** 5 | * 這邊使用setTimeout是因為localhost載入資料很快,為了做到Loading的效果,所以這邊延長時間 6 | * @return {function} async function 7 | */ 8 | export function getProducts() { 9 | return (dispatch) => { 10 | dispatch({type: GETTING_PRODUCT_LIST}); 11 | setTimeout(() => { 12 | fetch('/products.json') 13 | .then((response) => response.json()) 14 | .then((productList) => { 15 | dispatch({type: GET_PRODUCT_LIST_SUCCESS, product_list: productList}); 16 | }); 17 | }, 3000); 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /src/actions/shopcart.js: -------------------------------------------------------------------------------- 1 | import {GETTING_SHOPCART_LIST, 2 | GET_SHOPCART_LIST_SUCCESS, 3 | ADDING_TO_SHOPCART, 4 | ADDED_TO_SHOPCART, 5 | DELETE_FROM_SHOPCART, 6 | UPDATE_AMOUNT_TO_SHOPCART, 7 | ENABLE_ADD_TO_SHOPCART, 8 | DISABLE_ADD_TO_SHOPCART} from '../constants/actionTypes'; 9 | 10 | /** 11 | * 這邊使用setTimeout是因為localhost載入資料很快,為了做到Loading的效果,所以這邊延長時間 12 | * @return {function} async function 13 | */ 14 | export function getShopList() { 15 | return (dispatch) => { 16 | dispatch({type: GETTING_SHOPCART_LIST}); 17 | setTimeout(() => { 18 | fetch('/shopcart.json') 19 | .then((response) => response.json()) 20 | .then((shopcartList) => { 21 | dispatch({type: GET_SHOPCART_LIST_SUCCESS, shopcart_list: shopcartList}); 22 | }); 23 | }, 3000); 24 | }; 25 | }; 26 | 27 | /** 28 | * @param {int} id 29 | * @param {object} product 30 | * @param {int} amount 31 | * @return {function} async function 32 | */ 33 | export function addToShopcart(id, product, amount = 1) { 34 | return (dispatch) => { 35 | dispatch({type: ADDING_TO_SHOPCART}); 36 | dispatch({type: DISABLE_ADD_TO_SHOPCART, id: id}); 37 | setTimeout(() => { 38 | dispatch({type: ADDED_TO_SHOPCART, product: product, amount: amount}); 39 | }, 1500); 40 | }; 41 | } 42 | 43 | /** 44 | * 45 | * @param {int} index 46 | * @param {int} amount 47 | * @return {function} function 48 | */ 49 | export function updateAmountToShopcart(index, amount) { 50 | return (dispatch) => { 51 | dispatch({type: UPDATE_AMOUNT_TO_SHOPCART, index: index, amount: amount}); 52 | }; 53 | } 54 | 55 | /** 56 | * @param {int} id 57 | * @param {int} index 58 | * @return {function} function 59 | */ 60 | export function deleteFromShopcart(id, index) { 61 | return (dispatch) => { 62 | dispatch({type: ENABLE_ADD_TO_SHOPCART, id: id}); 63 | dispatch({type: DELETE_FROM_SHOPCART, index: index}); 64 | }; 65 | } 66 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import 'bootstrap/dist/css/bootstrap.css'; 2 | // import 'font-awesome/css/font-awesome.css'; 3 | import 'bootstrap/dist/js/bootstrap'; 4 | import React from 'react'; 5 | import ReactDOM from 'react-dom'; 6 | import AppContainer from './containers/app'; 7 | import {createStore, applyMiddleware, combineReducers} from 'redux'; 8 | import {Provider} from 'react-redux'; 9 | import createHistory from 'history/createBrowserHistory'; 10 | import {ConnectedRouter, routerReducer, routerMiddleware} from 'react-router-redux'; 11 | import thunk from 'redux-thunk'; 12 | import reducers from './reducers'; 13 | 14 | const history = createHistory(); 15 | 16 | const store = createStore( 17 | combineReducers({routerReducer, ...reducers}), 18 | applyMiddleware(routerMiddleware(history), thunk), 19 | ); 20 | 21 | ReactDOM.render( 22 | 23 | 24 | 25 | 26 | 27 | , document.getElementById('app')); 28 | -------------------------------------------------------------------------------- /src/components/loading_view.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | /** 3 | * @return {component} The component of product list 4 | */ 5 | class LoadingView extends React.Component { 6 | /** 7 | * @return {component} 8 | */ 9 | render() { 10 | return ( 11 |
12 | 18 |

Loading...

19 | 20 |
21 | ); 22 | } 23 | } 24 | 25 | export default LoadingView; 26 | -------------------------------------------------------------------------------- /src/components/modal_view.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | /** 5 | * 一個Bootstrap Modal的呼叫 6 | */ 7 | class ModalView extends React.Component { 8 | /** 9 | */ 10 | componentDidMount() { 11 | $(this.modal).modal('show'); 12 | } 13 | 14 | /** 15 | * 16 | * @param {boolean} result 17 | */ 18 | hideModal(result) { 19 | let {callback} = this.props; 20 | $(this.modal).on('hidden.bs.modal', () => callback(result)); 21 | $(this.modal).modal('hide'); 22 | } 23 | 24 | /** 25 | * @return {component} 26 | */ 27 | render() { 28 | let {title, content} = this.props; 29 | return ( 30 |
this.modal = modal}> 31 |
32 |
33 |
34 |

{title}

35 | 39 |
40 |
41 |

{content}

42 |
43 |
44 | 46 | 48 |
49 |
50 |
51 |
52 | ); 53 | }; 54 | } 55 | 56 | ModalView.propTypes = { 57 | title: PropTypes.string.isRequired, 58 | content: PropTypes.string.isRequired, 59 | callback: PropTypes.func.isRequired, 60 | }; 61 | 62 | export default ModalView; 63 | -------------------------------------------------------------------------------- /src/components/product_list.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import LoadingView from './loading_view'; 4 | import classnames from 'classnames'; 5 | import {connect} from 'react-redux'; 6 | import {bindActionCreators} from 'redux'; 7 | import {getProducts} from '../actions/product'; 8 | import {addToShopcart} from '../actions/shopcart'; 9 | /** 10 | * @return {component} The component of product list 11 | */ 12 | class ProductList extends React.Component { 13 | /** 14 | * 15 | */ 16 | componentDidMount() { 17 | if (this.props.store.product_list.length == 0) { 18 | this.props.getProducts(); 19 | } 20 | } 21 | 22 | /** 23 | * @return {component} 24 | * @param {object} product 25 | */ 26 | productDetail(product) { 27 | let {id, title, img, price, discount} = product; 28 | let isDisabled = product.is_disabled; 29 | let options = {}; 30 | if (isDisabled) { 31 | options['disabled'] = 'disabled'; 32 | } 33 | 34 | return ( 35 |
36 |
37 | 38 |
39 |
{title}
40 |
41 | 45 |
46 | 50 |
51 |
52 |
53 | ); 54 | } 55 | 56 | /** 57 | * @return {component} 58 | */ 59 | render() { 60 | let productList = this.props.store.product_list; 61 | let products = productList.map((product) => { 62 | return this.productDetail(product); 63 | }); 64 | 65 | return ( 66 |
67 |

商品

68 |
69 | 75 | {(() => { 76 | if (this.props.store.is_loading) { 77 | return ( 78 | 79 | ); 80 | } 81 | 82 | return products; 83 | })()} 84 |
85 |
86 | ); 87 | } 88 | } 89 | 90 | ProductList.propTypes = { 91 | addToShopcart: PropTypes.func.isRequired, 92 | getProducts: PropTypes.func.isRequired, 93 | store: PropTypes.shape({ 94 | product_list: PropTypes.array.isRequired, 95 | is_loading: PropTypes.bool.isRequired, 96 | }), 97 | }; 98 | 99 | const mapStateToProps = ({productStore}) => ({store: productStore}); 100 | 101 | const mapDispatchToProps = (dispatch) => { 102 | return { 103 | addToShopcart: bindActionCreators(addToShopcart, dispatch), 104 | getProducts: bindActionCreators(getProducts, dispatch), 105 | }; 106 | }; 107 | 108 | 109 | export default connect(mapStateToProps, mapDispatchToProps)(ProductList); 110 | -------------------------------------------------------------------------------- /src/components/shop_cart.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import LoadingView from './loading_view'; 4 | import ModalView from './modal_view'; 5 | import {connect} from 'react-redux'; 6 | import {bindActionCreators} from 'redux'; 7 | import {deleteFromShopcart, updateAmountToShopcart, getShopList} from '../actions/shopcart'; 8 | /** 9 | * @return {component} The component of shopcart 10 | */ 11 | class ShopCart extends React.Component { 12 | /** 13 | * 14 | */ 15 | componentDidMount() { 16 | if (this.props.store.shopcart_list.length == 0) { 17 | this.props.getShopList(); 18 | } 19 | } 20 | /** 21 | * 結帳事件,尚未實作 22 | */ 23 | handleCheckout() { 24 | alert('It\'s just a demo project!'); 25 | } 26 | 27 | /** 28 | * 處理刪除事件的程式碼,會先跳出Modal來讓使用者確認是否需要刪除 29 | * @param {int} id 30 | * @param {int} index 31 | */ 32 | handleDelete(id, index) { 33 | this.setState({id: id, index: index, show_delete_check: true}); 34 | } 35 | 36 | /** 37 | * 處理刪除事件的回傳程式碼 38 | * @param {bool} result 39 | */ 40 | handleDeleteCallback(result) { 41 | this.setState({show_delete_check: false}); 42 | if (result) { 43 | this.props.deleteFromShopcart(this.state.id, this.state.index); 44 | } 45 | } 46 | 47 | /** 48 | * 跳出提示視窗詢問是否要刪除 49 | * @return {null} 50 | */ 51 | showDeleteModal() { 52 | if (this.state && this.state.show_delete_check == true) { 53 | return ( 54 | this.handleDeleteCallback(result)} /> 56 | ); 57 | } 58 | } 59 | /** 60 | * @return {component} 61 | * @param {int} index 62 | * @param {object} product 63 | * @param {int} amount 64 | * @param {int} total 65 | */ 66 | itemDetail(index, product, amount, total) { 67 | const fontWeight = 'bolder'; 68 | return ( 69 | 70 | {index + 1} 71 | {product.title} 72 | 73 | 81 | 82 | 83 | 88 |

{product.price - product.discount}

89 | 90 |

{total}

91 | 92 | 95 | 96 | 97 | ); 98 | } 99 | 100 | /** 101 | * @return {component} 102 | */ 103 | render() { 104 | let shopcartList = this.props.store.shopcart_list 105 | .map((item, index) => this.itemDetail(index, item.product, item.amount, item.total)); 106 | 107 | let renderChild = null; 108 | if (this.props.store.is_loading) { 109 | renderChild= ; 110 | } else { 111 | renderChild = ( 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 135 | 136 | 137 | 138 | {shopcartList} 139 | 140 |
#品項數量價錢小計動作
總計{this.props.store.sum} 131 | 134 |
141 | ); 142 | } 143 | 144 | return ( 145 |
146 |

購物車

147 | {this.showDeleteModal()} 148 | {renderChild} 149 |
150 | ); 151 | } 152 | } 153 | 154 | ShopCart.propTypes = { 155 | getShopList: PropTypes.func.isRequired, 156 | deleteFromShopcart: PropTypes.func.isRequired, 157 | updateAmountToShopcart: PropTypes.func.isRequired, 158 | store: PropTypes.shape({ 159 | sum: PropTypes.number.isRequired, 160 | shopcart_list: PropTypes.array.isRequired, 161 | is_loading: PropTypes.bool.isRequired, 162 | }), 163 | }; 164 | 165 | 166 | const mapStateToProps = ({shopcartStore}) => ({store: shopcartStore}); 167 | 168 | const mapDispatchToProps = (dispatch) => { 169 | return { 170 | deleteFromShopcart: bindActionCreators(deleteFromShopcart, dispatch), 171 | updateAmountToShopcart: bindActionCreators(updateAmountToShopcart, dispatch), 172 | getShopList: bindActionCreators(getShopList, dispatch), 173 | }; 174 | }; 175 | 176 | 177 | export default connect(mapStateToProps, mapDispatchToProps)(ShopCart); 178 | -------------------------------------------------------------------------------- /src/constants/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const GET_PRODUCT_LIST_SUCCESS = 'GET_PRODUCT_LIST_SUCCESS'; 2 | export const GETTING_PRODUCT_LIST = 'GETTING_PRODUCT_LIST'; 3 | export const GETTING_SHOPCART_LIST = 'GETTING_SHOPCART_LIST'; 4 | export const GET_SHOPCART_LIST_SUCCESS = 'GET_SHOPCART_LIST_SUCCESS'; 5 | export const ENABLE_ADD_TO_SHOPCART = 'ENABLE_ADD_TO_SHOPCART'; 6 | export const DISABLE_ADD_TO_SHOPCART = 'DISABLE_ADD_TO_SHOPCART'; 7 | export const ADDED_TO_SHOPCART = 'ADDED_TO_SHOPCART'; 8 | export const ADDING_TO_SHOPCART = 'ADDING_TO_SHOPCART'; 9 | export const UPDATE_AMOUNT_TO_SHOPCART = 'UPDATE_AMOUNT_TO_SHOPCART'; 10 | export const DELETE_FROM_SHOPCART = 'DELETE_FROM_SHOPCART'; 11 | -------------------------------------------------------------------------------- /src/containers/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import HomePage from '../pages/home'; 3 | import IntroPage from '../pages/intro'; 4 | import {bindActionCreators} from 'redux'; 5 | import {connect} from 'react-redux'; 6 | import {Route, Switch} from 'react-router'; 7 | import actions from '../actions'; 8 | 9 | const ConnectedSwitch = connect((state) => ({ 10 | location: state.location, 11 | }))(Switch); 12 | 13 | /** 14 | * @return {component} connect with actions & store; 15 | * @param {component} item 16 | */ 17 | function connectDispatch(item) { 18 | return connect((state) =>{ 19 | return state; 20 | }, (dispatch) => { 21 | return { 22 | actions: bindActionCreators(actions, dispatch), 23 | dispatch, 24 | }; 25 | })(item); 26 | } 27 | 28 | const Home = connectDispatch(HomePage); 29 | const Intro = connectDispatch(IntroPage); 30 | 31 | /** 32 | * @return {compoment} The main compoment of AppContainer. 33 | */ 34 | class AppContainer extends React.Component { 35 | /** 36 | * 37 | */ 38 | componentDidMount() { 39 | } 40 | 41 | /** 42 | * @return {component} 43 | */ 44 | render() { 45 | return ( 46 |
47 | 48 | 49 | 50 | 51 |
52 | ); 53 | } 54 | } 55 | 56 | export default AppContainer; 57 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Document 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /src/pages/home.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ProductList from '../components/product_list'; 3 | import ShopCart from '../components/shop_cart'; 4 | import {Link} from 'react-router-dom'; 5 | /** 6 | * @return {compoment} The main compoment of HomePage. 7 | */ 8 | class HomePage extends React.Component { 9 | /** 10 | * @return {component} 11 | */ 12 | render() { 13 | return ( 14 |
15 |

購物車

16 | 說明 17 |
18 |
19 | 20 |
21 |
22 | 23 |
24 |
25 |
26 | ); 27 | } 28 | } 29 | 30 | export default HomePage; 31 | -------------------------------------------------------------------------------- /src/pages/intro.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {Link} from 'react-router-dom'; 4 | 5 | /** 6 | * @return {compoment} The main compoment of IntroPage. 7 | */ 8 | class IntroPage extends React.Component { 9 | /** 10 | * 11 | */ 12 | componentDidMount() { 13 | 14 | } 15 | 16 | /** 17 | * @return {component} 18 | */ 19 | render() { 20 | return ( 21 |
22 |

說明

23 |

這只是個練習用購物車

24 |

購物車金額: {this.props.shopcartStore.sum}

25 | 返回購物車 26 |
27 | ); 28 | } 29 | } 30 | 31 | IntroPage.propTypes = { 32 | shopcartStore: PropTypes.object.isRequired, 33 | productStore: PropTypes.object.isRequired, 34 | actions: PropTypes.object.isRequired, 35 | }; 36 | 37 | export default IntroPage; 38 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import product from './product'; 2 | import shopcart from './shopcart'; 3 | 4 | export default { 5 | productStore: product, 6 | shopcartStore: shopcart, 7 | }; 8 | -------------------------------------------------------------------------------- /src/reducers/product.js: -------------------------------------------------------------------------------- 1 | import {GET_PRODUCT_LIST_SUCCESS, 2 | GETTING_PRODUCT_LIST, 3 | ENABLE_ADD_TO_SHOPCART, 4 | DISABLE_ADD_TO_SHOPCART} from '../constants/actionTypes'; 5 | 6 | let initialState = { 7 | product_list: [], 8 | is_loading: true, 9 | }; 10 | 11 | /** 12 | * @param {object} state 13 | * @param {string} action 14 | * @return {function} The reducer of product 15 | */ 16 | export default function product(state = initialState, action) { 17 | switch (action.type) { 18 | case GET_PRODUCT_LIST_SUCCESS: 19 | state = { 20 | ...action, 21 | is_loading: false, 22 | }; 23 | break; 24 | case GETTING_PRODUCT_LIST: 25 | state = initialState; 26 | break; 27 | case ENABLE_ADD_TO_SHOPCART: 28 | { 29 | state = { 30 | ...state, 31 | is_loading: false, 32 | }; 33 | 34 | let product = state.product_list.filter(({id}) => id == action.id)[0]; 35 | delete(product.is_disabled); 36 | break; 37 | } 38 | case DISABLE_ADD_TO_SHOPCART: 39 | { 40 | state = { 41 | ...state, 42 | is_loading: false, 43 | }; 44 | 45 | let product = state.product_list.filter(({id}) => id == action.id)[0]; 46 | product.is_disabled = true; 47 | break; 48 | } 49 | } 50 | 51 | return state; 52 | } 53 | -------------------------------------------------------------------------------- /src/reducers/shopcart.js: -------------------------------------------------------------------------------- 1 | import {GET_SHOPCART_LIST_SUCCESS, 2 | GETTING_SHOPCART_LIST, 3 | ADDING_TO_SHOPCART, 4 | ADDED_TO_SHOPCART, 5 | UPDATE_AMOUNT_TO_SHOPCART, 6 | DELETE_FROM_SHOPCART} from '../constants/actionTypes'; 7 | 8 | let initialState = { 9 | shopcart_list: [], 10 | is_loading: true, 11 | }; 12 | 13 | /** 14 | * @param {object} state 15 | * @param {string} action 16 | * @return {function} The reducer of product 17 | */ 18 | export default function shopcart(state = initialState, action) { 19 | switch (action.type) { 20 | case GETTING_SHOPCART_LIST: 21 | state = initialState; 22 | break; 23 | case GET_SHOPCART_LIST_SUCCESS: 24 | state = { 25 | shopcart_list: action.shopcart_list, 26 | is_loading: false, 27 | }; 28 | break; 29 | case ADDING_TO_SHOPCART: 30 | state = {...state}; 31 | state.is_loading = true; 32 | break; 33 | case ADDED_TO_SHOPCART: 34 | state = {...state}; 35 | state.shopcart_list.push({ 36 | product: action.product, 37 | amount: action.amount, 38 | }); 39 | 40 | state.is_loading = false; 41 | break; 42 | case UPDATE_AMOUNT_TO_SHOPCART: 43 | state = {...state}; 44 | let item = state.shopcart_list[action.index]; 45 | state.shopcart_list[action.index] = { 46 | ...item, 47 | amount: action.amount, 48 | }; 49 | break; 50 | case DELETE_FROM_SHOPCART: 51 | state = {...state}; 52 | state.shopcart_list.splice(action.index, 1); 53 | break; 54 | } 55 | 56 | state.sum = 0; 57 | for (let i = 0; i < state.shopcart_list.length; i++) { 58 | let product = state.shopcart_list[i].product; 59 | state.shopcart_list[i].total = (product.price - product.discount) * state.shopcart_list[i].amount; 60 | state.sum += state.shopcart_list[i].total; 61 | } 62 | 63 | return state; 64 | } 65 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | let webpack = require('webpack'); 2 | let colors = require('colors/safe'); 3 | let HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | 5 | const PRODUCTION = 'production'; 6 | const DEVELOPMENT = 'development' 7 | 8 | let mode = process.env.NODE_ENV ? process.env.NODE_ENV : DEVELOPMENT; 9 | console.log(colors.red(`CURRENT MODE: ${mode.toUpperCase()}`)); 10 | 11 | var plugins = [ 12 | new webpack.ProvidePlugin({ 13 | $: "jquery", 14 | jQuery: "jquery" 15 | }), 16 | new HtmlWebpackPlugin({ 17 | template: 'src/index.html' 18 | }) 19 | ]; 20 | 21 | if (mode == PRODUCTION) { 22 | plugins.push( 23 | new webpack.optimize.UglifyJsPlugin({ 24 | minimize: true 25 | }) 26 | ); 27 | } 28 | 29 | module.exports = { 30 | entry: './src/app.js', 31 | output: { 32 | path: __dirname + '/dist', 33 | filename: './bundle.js', 34 | }, 35 | devServer: { 36 | historyApiFallback: true 37 | }, 38 | resolve: { 39 | extensions: ['.js', '.jsx'], 40 | }, 41 | plugins: plugins, 42 | module: { 43 | loaders: [ 44 | { 45 | loader: 'babel-loader', 46 | query: { 47 | presets: ['env', 'stage-3', 'react'], 48 | plugins: [ 49 | "styled-jsx/babel" 50 | ] 51 | }, 52 | test: /\.(js|jsx)?$/, 53 | exclude: /(node_modules|bower_components)/, 54 | }, 55 | { 56 | test: /\.css$/, 57 | loaders: ['style-loader', 'css-loader'], 58 | }, 59 | { 60 | test: /\.(scss|sass)$/, 61 | loaders: ['style-loader', 'css-loader', 'sass-loader'], 62 | }, 63 | { 64 | test: /\.(eot|svg|ttf|woff|woff2)$/, 65 | loaders: ['file-loader'], 66 | } 67 | ], 68 | }, 69 | }; 70 | --------------------------------------------------------------------------------