├── reactjs-flex-order-practice ├── .babelrc ├── .eslintrc ├── .gitignore ├── db │ ├── category.json │ ├── item.json │ └── tables.json ├── index.html ├── index_bundle.js ├── package.json ├── readme.md ├── src │ ├── actions │ │ └── OrderActions.js │ ├── components │ │ ├── OrderContent.jsx │ │ ├── OrderContentDetail.jsx │ │ ├── OrderContentOperation.jsx │ │ ├── OrderContentSubmit.jsx │ │ ├── OrderContentTotal.jsx │ │ ├── OrderMenu.jsx │ │ ├── OrderMenuCategoryList.jsx │ │ ├── OrderMenuItemList.jsx │ │ └── OrderMenuPager.jsx │ ├── constants │ │ ├── ActionTypes.js │ │ └── OrderConstants.js │ ├── dispatcher │ │ └── AppDispatcher.js │ ├── index.html │ ├── index.jsx │ ├── stores │ │ └── OrderStore.js │ └── styles │ │ ├── components │ │ ├── datetime.scss │ │ ├── input.scss │ │ └── loading.scss │ │ ├── entry.scss │ │ ├── layout │ │ ├── app.scss │ │ └── grid.scss │ │ ├── pages │ │ └── Order.scss │ │ └── variables │ │ ├── button.scss │ │ └── font.scss └── webpack.config.js ├── reactjs-order-practice ├── .babelrc ├── .eslintrc ├── .gitignore ├── db │ ├── category.json │ ├── item.json │ └── tables.json ├── index.html ├── index_bundle.js ├── package.json ├── readme.md ├── src │ ├── actions │ │ └── OrderActions.js │ ├── components │ │ ├── OrderContent.jsx │ │ ├── OrderContentDetail.jsx │ │ ├── OrderContentOperation.jsx │ │ ├── OrderContentSubmit.jsx │ │ ├── OrderContentTotal.jsx │ │ ├── OrderMenu.jsx │ │ ├── OrderMenuCategoryList.jsx │ │ ├── OrderMenuItemList.jsx │ │ └── OrderMenuPager.jsx │ ├── constants │ │ └── OrderConstants.js │ ├── index.html │ ├── index.jsx │ └── styles │ │ ├── components │ │ ├── datetime.scss │ │ ├── input.scss │ │ └── loading.scss │ │ ├── entry.scss │ │ ├── layout │ │ ├── app.scss │ │ └── grid.scss │ │ ├── pages │ │ └── Order.scss │ │ └── variables │ │ ├── button.scss │ │ └── font.scss └── webpack.config.js ├── reactjs-php-isomorphic-example ├── .gitignore ├── README.md ├── build │ └── .gitignore ├── index.php ├── package.json ├── src │ ├── App.js │ ├── Heading.js │ └── Table.js └── webpack.config.js ├── reactjs-redux-order-practice ├── .babelrc ├── .eslintrc ├── .gitignore ├── db │ ├── category.json │ ├── item.json │ └── tables.json ├── index.html ├── index_bundle.js ├── package.json ├── readme.md ├── src │ ├── actions │ │ ├── data.js │ │ └── ui │ │ │ └── Order │ │ │ ├── categories.js │ │ │ ├── items.js │ │ │ ├── order.js │ │ │ └── pages.js │ ├── components │ │ └── Order │ │ │ ├── OrderContent.jsx │ │ │ ├── OrderContentDetail.jsx │ │ │ ├── OrderContentSubmit.jsx │ │ │ ├── OrderMenu.jsx │ │ │ ├── OrderMenuCategory.jsx │ │ │ ├── OrderMenuItem.jsx │ │ │ └── OrderMenuPager.jsx │ ├── constants │ │ ├── ActionTypes.js │ │ └── OrderConstants.js │ ├── containers │ │ ├── App.jsx │ │ └── Order │ │ │ ├── OrderContent.js │ │ │ ├── OrderContentDetail.js │ │ │ ├── OrderContentSubmit.js │ │ │ ├── OrderMenuCategory.js │ │ │ ├── OrderMenuItem.js │ │ │ └── OrderMenuPager.js │ ├── helpers │ │ └── index.js │ ├── index.html │ ├── index.jsx │ ├── reducers │ │ ├── Order │ │ │ ├── categories.js │ │ │ ├── items.js │ │ │ ├── order.js │ │ │ ├── pages.js │ │ │ └── tables.js │ │ └── index.js │ ├── store │ │ ├── configureStore.js │ │ └── index.js │ └── styles │ │ ├── components │ │ ├── datetime.scss │ │ ├── input.scss │ │ └── loading.scss │ │ ├── entry.scss │ │ ├── layout │ │ ├── app.scss │ │ └── grid.scss │ │ ├── pages │ │ └── Order.scss │ │ └── variables │ │ ├── button.scss │ │ └── font.scss └── webpack.config.js ├── reactjs-redux-todos-practice ├── .babelrc ├── .eslintrc ├── .gitignore ├── index.html ├── index_bundle.js ├── package.json ├── readme.md ├── src │ ├── actions │ │ └── index.js │ ├── components │ │ ├── AddTodo.jsx │ │ ├── App.jsx │ │ ├── Footer.jsx │ │ ├── Link.jsx │ │ ├── Todo.jsx │ │ └── TodoList.jsx │ ├── containers │ │ ├── AddTodo.js │ │ ├── FilterLink.js │ │ └── VisibleTodoList.js │ ├── index.html │ ├── index.jsx │ └── reducers │ │ ├── index.js │ │ ├── todos.js │ │ └── visibilityFilter.js └── webpack.config.js ├── readme.md └── wPOS ├── .gitignore ├── bundle.js ├── db ├── categories.json ├── items.json ├── payments.json └── tables.json ├── index.html ├── main.css ├── package.json ├── readme.md └── src ├── App.jsx ├── components ├── ChoiceButton.jsx ├── InputField.jsx ├── Loading.jsx ├── MenuCategory.jsx ├── MenuCategoryItem.jsx ├── MenuItem.jsx ├── MenuItemNav.jsx ├── MenuRow.jsx ├── MenuRowItem.jsx ├── OrderDetails.jsx ├── OrderList.jsx └── OrderSubmit.jsx ├── configs └── API.js ├── entry.jsx ├── helpers ├── ArrayHelper.js └── PaymentHelper.js ├── pages ├── Home.jsx ├── NotFound.jsx └── Order.jsx └── styles ├── components └── loading.scss ├── entry.scss ├── layout ├── app.scss └── grid.scss ├── pages ├── Home.scss └── Order.scss └── variables ├── button.scss └── font.scss /reactjs-flex-order-practice/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "react", 5 | ], 6 | "plugins": [] 7 | } 8 | -------------------------------------------------------------------------------- /reactjs-flex-order-practice/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "rules": { 4 | "no-multiple-empty-lines": [2, {"max": 2, "maxEOF": 2}] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /reactjs-flex-order-practice/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /reactjs-flex-order-practice/db/category.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "id": 1, 3 | "name": "\u4e3b\u98df", 4 | "user": "rz", 5 | "is_active": 1, 6 | "created_at": "2016-06-09 20:51:39", 7 | "updated_at": "2016-06-09 20:51:39" 8 | }, { 9 | "id": 2, 10 | "name": "\u6e6f\u54c1", 11 | "user": "rz", 12 | "is_active": 1, 13 | "created_at": "2016-06-09 20:51:39", 14 | "updated_at": "2016-06-09 20:51:39" 15 | }, { 16 | "id": 3, 17 | "name": "\u5c0f\u9ede", 18 | "user": "rz", 19 | "is_active": 1, 20 | "created_at": "2016-06-09 20:51:39", 21 | "updated_at": "2016-06-09 20:51:39" 22 | }] 23 | -------------------------------------------------------------------------------- /reactjs-flex-order-practice/db/tables.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "id": 1, 3 | "name": "一號桌" 4 | }, { 5 | "id": 2, 6 | "name": "二號桌" 7 | }, { 8 | "id": 3, 9 | "name": "三號桌" 10 | }, { 11 | "id": 4, 12 | "name": "四號桌" 13 | }, { 14 | "id": 5, 15 | "name": "五號桌" 16 | }, { 17 | "id": 6, 18 | "name": "六號桌" 19 | }] 20 | -------------------------------------------------------------------------------- /reactjs-flex-order-practice/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | POS Order - ReactJS + Flux 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /reactjs-flex-order-practice/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reactjs-flex-order-practice", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "webpack-dev-server --colors --inline --content-base .", 8 | "prod": "webpack -p" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "events": "^1.1.1", 14 | "flux": "^3.0.0", 15 | "react": "^15.3.2", 16 | "react-dom": "^15.3.2" 17 | }, 18 | "devDependencies": { 19 | "babel-core": "^6.16.0", 20 | "babel-eslint": "^7.0.0", 21 | "babel-loader": "^6.2.5", 22 | "babel-preset-es2015": "^6.16.0", 23 | "babel-preset-react": "^6.16.0", 24 | "css-loader": "^0.25.0", 25 | "eslint": "^3.6.1", 26 | "eslint-config-airbnb": "^12.0.0", 27 | "eslint-loader": "^1.5.0", 28 | "eslint-plugin-import": "^1.16.0", 29 | "eslint-plugin-jsx-a11y": "^2.2.2", 30 | "eslint-plugin-react": "^6.3.0", 31 | "html-webpack-plugin": "^2.22.0", 32 | "node-sass": "^3.10.1", 33 | "sass-loader": "^4.0.2", 34 | "style-loader": "^0.13.1", 35 | "webpack": "^1.13.2", 36 | "webpack-dev-server": "^1.16.1" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /reactjs-flex-order-practice/readme.md: -------------------------------------------------------------------------------- 1 | # POS Order 2 | 3 | ## 概觀 4 | 練習以 ReactJS + Flux 實作 POS 點單介面 5 | 6 | 7 | ## 開發 8 | 1. 套件相依安裝 `npm install` 9 | 2. 運行服務 `npm start` 10 | 3. 開啟瀏覽器 11 | 12 | ``` 13 | http://localhost:8008 14 | ``` 15 | 16 | ## 產出 js & html 17 | 運行 `npm run prod` 18 | 19 | > 產出的 js & html file 會在根目錄下 20 | 21 | 22 | ## 參考 23 | * [從零開始學 ReactJS(ReactJS 101)](https://github.com/kdchang/reactjs101) 24 | * [24 小時,React 快速入門](https://github.com/shiningjason1989/react-quick-tutorial/) -------------------------------------------------------------------------------- /reactjs-flex-order-practice/src/actions/OrderActions.js: -------------------------------------------------------------------------------- 1 | import AppDispatcher from '../dispatcher/AppDispatcher'; 2 | import ActionTypes from '../constants/ActionTypes'; 3 | 4 | const OrderActions = { 5 | /* Order 左側 Actions */ 6 | loadMenuCategories() { 7 | const url = './db/category.json'; 8 | fetch(url, { 9 | method: 'GET' 10 | }).then((response) => response.json()).then((records) => { 11 | AppDispatcher.dispatch({ 12 | type: ActionTypes.LOAD_CATEGORIES_SUCCESS, 13 | data: records 14 | }); 15 | }); 16 | }, 17 | loadMenuItems() { 18 | const url = './db/item.json'; 19 | fetch(url, { 20 | method: 'GET' 21 | }).then((response) => response.json()).then((records) => { 22 | AppDispatcher.dispatch({ 23 | type: ActionTypes.LOAD_ITEMS_SUCCESS, 24 | data: records 25 | }); 26 | }); 27 | }, 28 | loadTables() { 29 | const url = './db/tables.json'; 30 | 31 | fetch(url, { 32 | method: 'GET' 33 | }).then((response) => response.json()).then((records) => { 34 | AppDispatcher.dispatch({ 35 | type: ActionTypes.LOAD_TABLES_SUCCESS, 36 | data: records 37 | }); 38 | }); 39 | }, 40 | changeSelectedCategory(category_id) { 41 | AppDispatcher.dispatch({ 42 | type: ActionTypes.CHANGE_SELECTED_CATEGORY, 43 | data: category_id 44 | }); 45 | }, 46 | getMenuPagesCount(items, itemsPerPageAmount) { 47 | AppDispatcher.dispatch({ 48 | type: ActionTypes.GET_MENU_PAGES_COUNT, 49 | data: Math.ceil(items.length / itemsPerPageAmount) 50 | }); 51 | }, 52 | changeCurrentMenuPage(pageIdx) { 53 | AppDispatcher.dispatch({ 54 | type: ActionTypes.CHANGE_CURRENT_MENU_PAGE, 55 | data: pageIdx 56 | }); 57 | }, 58 | addItemToOrder(item) { 59 | AppDispatcher.dispatch({ 60 | type: ActionTypes.ADD_ITEM_TO_ORDER, 61 | data: item 62 | }); 63 | }, 64 | removeItemFromOder(item) { 65 | AppDispatcher.dispatch({ 66 | type: ActionTypes.REMOVE_ITEM_FROM_ORDER, 67 | data: item 68 | }); 69 | }, 70 | addAdditionToOrder() { 71 | AppDispatcher.dispatch({ 72 | type: ActionTypes.ADD_ADDITION_TO_ORDER, 73 | }); 74 | }, 75 | removeItemFromAddition(item) { 76 | AppDispatcher.dispatch({ 77 | type: ActionTypes.REMOVE_ITEM_FROM_ADDITION, 78 | data: item 79 | }); 80 | }, 81 | updateAdditionItem(id, field, value) { 82 | AppDispatcher.dispatch({ 83 | type: ActionTypes.UPADTE_ADDITION_ITEM, 84 | id: id, 85 | field: field, 86 | value: value, 87 | }); 88 | }, 89 | toggleToGoStatus() { 90 | AppDispatcher.dispatch({ 91 | type: ActionTypes.TOGGLE_TOGO_STATUS, 92 | }); 93 | }, 94 | toggleWhenStatus() { 95 | AppDispatcher.dispatch({ 96 | type: ActionTypes.TOGGLE_WHEN_STATUS, 97 | }); 98 | }, 99 | clearOrderAndAdditon() { 100 | AppDispatcher.dispatch({ 101 | type: ActionTypes.CLEAR_ORDER_AND_ADDITION, 102 | }); 103 | }, 104 | }; 105 | 106 | export default OrderActions; 107 | -------------------------------------------------------------------------------- /reactjs-flex-order-practice/src/components/OrderContent.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import OrderConstants from '../constants/OrderConstants'; 3 | import OrderContentDetail from './OrderContentDetail.jsx'; 4 | import OrderContentOperation from './OrderContentOperation.jsx'; 5 | import OrderContentSubmit from './OrderContentSubmit.jsx'; 6 | import OrderContentTotal from './OrderContentTotal.jsx'; 7 | 8 | class OrderContent extends Component { 9 | getOrderContentRowHeight() { 10 | const bodyHeight = document.body.clientHeight; 11 | const orderContentHeight = (bodyHeight - OrderConstants.contentDetailHeadingHeight - OrderConstants.contentOperationHeight); 12 | this.setState({ 13 | orderContentStyle: { 14 | height: orderContentHeight + 'px' 15 | } 16 | }); 17 | } 18 | 19 | constructor(props) { 20 | super(props); 21 | this.state = { 22 | orderContentStyle: { 23 | height: 'inherit' 24 | } 25 | }; 26 | this.getOrderContentRowHeight = this.getOrderContentRowHeight.bind(this); 27 | } 28 | 29 | componentDidMount() { 30 | // item 動態列高 31 | this.getOrderContentRowHeight(); 32 | window.addEventListener('resize', () => this.getOrderContentRowHeight()); 33 | } 34 | 35 | componentWillUnmount() { 36 | window.removeEventListener('resize', () => this.getOrderContentRowHeight()); 37 | } 38 | 39 | render() { 40 | return ( 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 | export default OrderContent; 71 | -------------------------------------------------------------------------------- /reactjs-flex-order-practice/src/components/OrderContentDetail.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import OrderActions from './../actions/OrderActions'; 3 | import OrderStore from '../stores/OrderStore'; 4 | 5 | class OrderItemFromMenu extends Component { 6 | handleClickItem() { 7 | OrderActions.removeItemFromOder(this.props.item); 8 | } 9 | 10 | constructor(props) { 11 | super(props); 12 | this.handleClickItem = this.handleClickItem.bind(this); 13 | } 14 | 15 | render() { 16 | return ( 17 | 18 | {this.props.item.name} 19 | {this.props.item.amount} 20 | {this.props.item.price} 21 | {this.props.item.subtotal} 22 | 23 | 26 | 27 | 28 | ); 29 | } 30 | 31 | } 32 | 33 | class OrderItemFromAddition extends Component { 34 | handleChange(e) { 35 | // 取值 36 | const id = e.target.dataset.id; 37 | const field = e.target.dataset.field; 38 | const value = e.target.value; 39 | 40 | // 改變 state 41 | let _item = this.state.item; 42 | _item[field] = value; 43 | _item.subtotal = _item.amount * _item.price; 44 | this.setState({item: _item}); 45 | 46 | // 改變 store 的 addition 47 | OrderActions.updateAdditionItem(id, field, value); 48 | } 49 | 50 | handleClickAddition(item) { 51 | OrderActions.removeItemFromAddition(item); 52 | } 53 | 54 | constructor(props) { 55 | super(props); 56 | this.state = { 57 | item: props.item 58 | }; 59 | this.handleChange = this.handleChange.bind(this); 60 | } 61 | 62 | render() { 63 | return ( 64 | 65 | 66 | 67 | 68 | {this.state.item.subtotal} 69 | 70 | 73 | 74 | 75 | ); 76 | } 77 | } 78 | 79 | class OrderContentDetail extends Component { 80 | constructor(props) { 81 | super(props); 82 | this.state = { 83 | order: [], 84 | addition: [] 85 | }; 86 | } 87 | 88 | componentDidMount() { 89 | OrderStore.addChangeListener(() => this.setState({order: OrderStore.getCurrentOrder(), addition: OrderStore.getCurrenrAddition()})); 90 | } 91 | 92 | componentWillUnmount() { 93 | OrderStore.removeChangeListener(); 94 | } 95 | 96 | render() { 97 | return ( 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | {this.state.order.map((record) => ())} 110 | {this.state.addition.map((record) => ())} 111 | 112 |
品項數量單價小計
113 | ); 114 | } 115 | } 116 | 117 | export default OrderContentDetail; 118 | -------------------------------------------------------------------------------- /reactjs-flex-order-practice/src/components/OrderContentOperation.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import OrderActions from './../actions/OrderActions'; 3 | import OrderStore from '../stores/OrderStore'; 4 | 5 | class OrderAddAddition extends Component { 6 | handleClick() { 7 | OrderActions.addAdditionToOrder(); 8 | } 9 | 10 | render() { 11 | return ( 12 |
13 | 18 |
19 | ); 20 | } 21 | } 22 | 23 | class OrderToGo extends Component { 24 | handleClick() { 25 | OrderActions.toggleToGoStatus(); 26 | } 27 | 28 | constructor(props) { 29 | super(props); 30 | this.state = { 31 | isToGo: null 32 | }; 33 | } 34 | 35 | componentDidMount() { 36 | OrderStore.addChangeListener(() => this.setState({isToGo: OrderStore.getToGoStatus()})); 37 | } 38 | 39 | componentWillUnmount() { 40 | OrderStore.removeChangeListener(); 41 | } 42 | 43 | render() { 44 | const toGoIconClass = (this.state.isToGo) 45 | ? 'fa fa-hand-o-right' 46 | : 'fa fa-cutlery'; // 外帶狀態 icon 47 | const toGoButtonClass = ((this.state.isToGo) 48 | ? 'btn btn-danger' 49 | : 'btn btn-success') + ' btn-lg btn-block'; 50 | return ( 51 |
52 | 59 |
60 | ); 61 | } 62 | } 63 | 64 | class OrderWhen extends Component { 65 | handleClick() { 66 | OrderActions.toggleWhenStatus(); 67 | } 68 | 69 | constructor(props) { 70 | super(props); 71 | this.state = { 72 | when: null 73 | }; 74 | } 75 | 76 | componentDidMount() { 77 | OrderStore.addChangeListener(() => this.setState({when: OrderStore.getwhenStatus()})); 78 | } 79 | 80 | componentWillUnmount() { 81 | OrderStore.removeChangeListener(); 82 | } 83 | 84 | render() { 85 | const whenIconClass = (this.state.when === '中午') 86 | ? 'fa fa-sun-o' 87 | : 'fa fa-moon-o'; // 中午/晚上狀態 icon 88 | const whenButtonClass = ((this.state.when === '中午') 89 | ? 'btn btn-warning' 90 | : 'btn btn-primary') + ' btn-lg btn-block'; // 中午/晚上狀態 icon 91 | return ( 92 |
93 | 98 |
99 | ); 100 | } 101 | } 102 | 103 | class OrderTables extends Component { 104 | constructor(props) { 105 | super(props); 106 | this.state = { 107 | tables: [], 108 | isToGo: null 109 | }; 110 | } 111 | 112 | componentDidMount() { 113 | OrderStore.addChangeListener(() => { 114 | this.setState({tables: OrderStore.getTables(), isToGo: OrderStore.getToGoStatus()}) 115 | }); 116 | } 117 | 118 | componentWillUnmount() { 119 | OrderStore.removeChangeListener(); 120 | } 121 | 122 | render() { 123 | if (this.state.isToGo == true) { 124 | return ( 125 |
126 | 129 |
130 | ); 131 | } 132 | return ( 133 |
134 | 141 |
142 | ); 143 | } 144 | } 145 | 146 | const OrderContentOperation = () => ( 147 |
148 | 149 | 150 | 151 | 152 |
153 | ); 154 | 155 | export default OrderContentOperation; 156 | -------------------------------------------------------------------------------- /reactjs-flex-order-practice/src/components/OrderContentSubmit.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import OrderActions from './../actions/OrderActions'; 3 | import OrderStore from '../stores/OrderStore'; 4 | 5 | class OrderSubmit extends Component { 6 | handleClick() { 7 | // dump order & addition records 8 | const data = { 9 | order: OrderStore.getCurrentOrder(), 10 | addition: OrderStore.getCurrenrAddition() 11 | }; 12 | console.log('--- dump order & addition records ---'); 13 | console.log(data); 14 | 15 | // clear order & addition records 16 | console.log('--- clear order & addition records ---'); 17 | OrderActions.clearOrderAndAdditon(); 18 | 19 | } 20 | 21 | constructor(props) { 22 | super(props); 23 | this.state = { 24 | order: [] 25 | }; 26 | } 27 | 28 | componentDidMount() { 29 | OrderStore.addChangeListener(() => this.setState({order: OrderStore.getCurrentOrder()})); 30 | } 31 | 32 | componentWillUnmount() { 33 | OrderStore.removeChangeListener(); 34 | } 35 | 36 | render() { 37 | return ( 38 | 43 | ); 44 | } 45 | } 46 | 47 | export default OrderSubmit; 48 | -------------------------------------------------------------------------------- /reactjs-flex-order-practice/src/components/OrderContentTotal.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import OrderStore from '../stores/OrderStore'; 3 | 4 | class OrderContentTotal extends Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { 8 | total: null 9 | }; 10 | } 11 | 12 | componentDidMount() { 13 | OrderStore.addChangeListener(() => this.setState({total: OrderStore.getTotal()})); 14 | } 15 | 16 | componentWillUnmount() { 17 | OrderStore.removeChangeListener(); 18 | } 19 | 20 | render() { 21 | return ( 22 |
23 |
24 | 總計 25 |
26 |
27 | {this.state.total} 28 |
29 |
30 | ); 31 | } 32 | } 33 | 34 | export default OrderContentTotal; 35 | -------------------------------------------------------------------------------- /reactjs-flex-order-practice/src/components/OrderMenu.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import OrderMenuCategoryList from './OrderMenuCategoryList.jsx' 3 | import OrderMenuItemList from './OrderMenuItemList.jsx' 4 | import OrderMenuPager from './OrderMenuPager.jsx' 5 | 6 | // stateless component 7 | const OrderMenu = () => ( 8 |
9 | 10 | 11 | 12 |
13 | ); 14 | 15 | export default OrderMenu; 16 | -------------------------------------------------------------------------------- /reactjs-flex-order-practice/src/components/OrderMenuCategoryList.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import OrderActions from './../actions/OrderActions'; 3 | import OrderStore from './../stores/OrderStore'; 4 | 5 | // stateless component 6 | const OrderMenyCategoryListItem = (props) => { 7 | const itemClass = props.isSelected 8 | ? "active" 9 | : ""; 10 | return ( 11 |
  • 12 | 13 |

    {props.name}

    14 |
    15 |
  • 16 | ) 17 | }; 18 | 19 | class OrderMenuCategoryList extends Component { 20 | handleClick(i) { 21 | // 讓 click 的 category 變成 active 22 | this.setState({selectedCategory: i}); 23 | 24 | // OrderStore 的 selectedCategory 也要一併更新 25 | OrderActions.changeSelectedCategory(i); 26 | 27 | // OrderStore 的 菜單分頁數 也要一併更新 28 | OrderActions.getMenuPagesCount(OrderStore.getItems(), 12); 29 | } 30 | 31 | constructor(props) { 32 | super(props); 33 | this.state = { 34 | categories: [], // 分類 35 | selectedCategory: null // 被選擇的分類 36 | }; 37 | this.handleClick = this.handleClick.bind(this); 38 | } 39 | 40 | componentDidMount() { 41 | OrderStore.addChangeListener(() => { 42 | this.setState({categories: OrderStore.getCategories(), selectedCategory: OrderStore.getSelectCategoryId()}) 43 | }); 44 | } 45 | 46 | componentWillUnmount() { 47 | OrderStore.removeChangeListener(); 48 | } 49 | 50 | render() { 51 | return ( 52 |
    53 | 56 |
    57 | ); 58 | } 59 | } 60 | 61 | export default OrderMenuCategoryList; 62 | -------------------------------------------------------------------------------- /reactjs-flex-order-practice/src/components/OrderMenuItemList.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import OrderConstants from '../constants/OrderConstants'; 3 | import OrderActions from './../actions/OrderActions'; 4 | import OrderStore from '../stores/OrderStore'; 5 | 6 | // stateless component 7 | const OrderMenuItem = (props) => { 8 | return ( 9 |
    10 |
    11 |
    {props.item.name}
    12 |
    13 |
    14 | ); 15 | }; 16 | 17 | class OrderMenuItemList extends Component { 18 | handleClickItem(item) { 19 | OrderActions.addItemToOrder(item); 20 | } 21 | 22 | getItemRowHeight() { 23 | const bodyHeight = document.body.clientHeight; 24 | const rowHeight = (bodyHeight - OrderConstants.menuItemRowMarginHeight - OrderConstants.menuCategoryListHeight - OrderConstants.menuPagerHeight) / OrderConstants.menuItemListRows; 25 | this.setState({ 26 | itemRowStyle: { 27 | height: rowHeight + 'px' 28 | } 29 | }); 30 | } 31 | 32 | constructor(props) { 33 | super(props); 34 | this.state = { 35 | items: [], 36 | itemRowStyle: { 37 | height: 'inherit' 38 | } 39 | }; 40 | this.getItemRowHeight = this.getItemRowHeight.bind(this); 41 | } 42 | 43 | componentDidMount() { 44 | OrderStore.addChangeListener(() => this.setState({items: OrderStore.getItems()})); 45 | 46 | // item 動態列高 47 | this.getItemRowHeight(); 48 | window.addEventListener('resize', () => this.getItemRowHeight()); 49 | } 50 | 51 | componentWillUnmount() { 52 | OrderStore.removeChangeListener(); 53 | window.removeEventListener('resize', () => this.getItemRowHeight()); 54 | } 55 | 56 | render() { 57 | return ( 58 |
    59 |
    60 | {this.state.items.map((record) => ())} 61 |
    62 |
    63 | ); 64 | } 65 | } 66 | 67 | export default OrderMenuItemList; 68 | -------------------------------------------------------------------------------- /reactjs-flex-order-practice/src/components/OrderMenuPager.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import OrderActions from '../actions/OrderActions'; 3 | import OrderStore from '../stores/OrderStore'; 4 | // import OrderConstants from '../constants/OrderConstants'; 5 | 6 | class OrderMenuPager extends Component { 7 | handleClick(i) { 8 | // 更新當前頁面 idx 9 | this.setState({currentMenuPage: i}); 10 | 11 | // OrderStore 的 當前頁面 idx 也要一併更新 12 | OrderActions.changeCurrentMenuPage(i); 13 | } 14 | 15 | constructor(props) { 16 | super(props); 17 | this.state = { 18 | menuPagesCount: null, 19 | currentMenuPage: null 20 | }; 21 | this.handleClick = this.handleClick.bind(this); 22 | } 23 | 24 | componentDidMount() { 25 | OrderStore.addChangeListener(() => { 26 | this.setState({menuPagesCount: OrderStore.getMenuPagesCount(), currentMenuPage: OrderStore.getCurrentMenuPage()}) 27 | }); 28 | } 29 | 30 | componentWillUnmount() { 31 | OrderStore.removeChangeListener(); 32 | } 33 | 34 | render() { 35 | const hasNextPage = this.state.menuPagesCount - this.state.currentMenuPage > 1; // 是否還有下一頁 36 | const hasPrevPage = this.state.currentMenuPage - 1 > -1; // 是否還有上一頁 37 | const nextPage = hasNextPage 38 | ? this.state.currentMenuPage + 1 39 | : this.state.currentMenuPage; 40 | const prevPage = hasPrevPage 41 | ? this.state.currentMenuPage - 1 42 | : this.state.currentMenuPage; 43 | 44 | return ( 45 |
    46 |
    47 |
    48 | 53 |
    54 |
    55 | 60 |
    61 |
    62 |
    63 | ); 64 | } 65 | } 66 | 67 | export default OrderMenuPager; 68 | -------------------------------------------------------------------------------- /reactjs-flex-order-practice/src/constants/ActionTypes.js: -------------------------------------------------------------------------------- 1 | const ActionTypes = { 2 | // Order 左側 Action 3 | CHANGE_EVENT: 'CHANGE_EVENT', 4 | LOAD_CATEGORIES_SUCCESS: 'LOAD_CATEGORIES_SUCCESS', 5 | LOAD_ITEMS_SUCCESS: 'LOAD_ITEMS_SUCCESS', 6 | LOAD_TABLES_SUCCESS: 'LOAD_TABLES_SUCCESS', 7 | CHANGE_SELECTED_CATEGORY: 'CHANGE_SELECTED_CATEGORY', 8 | GET_MENU_PAGES_COUNT: 'GET_MENU_PAGES_COUNT', 9 | CHANGE_CURRENT_MENU_PAGE: 'CHANGE_CURRENT_MENU_PAGE', 10 | ADD_ITEM_TO_ORDER: 'ADD_ITEM_TO_ORDER', 11 | // Order 右側 Action 12 | REMOVE_ITEM_FROM_ORDER: 'REMOVE_ITEM_FROM_ORDER', 13 | ADD_ADDITION_TO_ORDER: 'ADD_ADDITION_TO_ORDER', 14 | REMOVE_ITEM_FROM_ADDITION: 'REMOVE_ITEM_FROM_ADDITION', 15 | UPADTE_ADDITION_ITEM: 'UPADTE_ADDITION_ITEM', 16 | TOGGLE_TOGO_STATUS: 'TOGGLE_TOGO_STATUS', 17 | TOGGLE_WHEN_STATUS: 'TOGGLE_WHEN_STATUS', 18 | CLEAR_ORDER_AND_ADDITION: 'CLEAR_ORDER_AND_ADDITION', 19 | }; 20 | 21 | export default ActionTypes; 22 | -------------------------------------------------------------------------------- /reactjs-flex-order-practice/src/constants/OrderConstants.js: -------------------------------------------------------------------------------- 1 | const now = new Date(); 2 | const nightStart = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 17, 30, 0, 0); 3 | const whenStatus = (now.getTime() - nightStart.getTime() < 0) ? '中午' : '晚上'; 4 | 5 | const OrderConstants = { 6 | // parameters 7 | itemsPerPageAmount: 12, // 每頁菜單項目數 8 | menuItemListRows: 3, // 菜單列數 9 | menuItemRowMarginHeight: 30, // 菜單列與列之間 margin 高 10 | menuCategoryListHeight: 87, // 菜單分類 DOM 高 11 | menuPagerHeight: 62, // 菜單分頁 DOM 高 12 | contentDetailHeadingHeight: 72, // "點單明細" 高 13 | contentOperationHeight: 133, // order 操作區域高 14 | // store default value 15 | whenStatus: whenStatus // 菜單 中午/晚上的狀態 16 | }; 17 | 18 | export default OrderConstants; 19 | -------------------------------------------------------------------------------- /reactjs-flex-order-practice/src/dispatcher/AppDispatcher.js: -------------------------------------------------------------------------------- 1 | import { 2 | Dispatcher 3 | } from 'flux'; 4 | 5 | class DispatcherClass extends Dispatcher { 6 | // handleAction(action) { 7 | // this.dispatch({ 8 | // type: action.type, 9 | // payload: action.payload 10 | // }); 11 | // } 12 | } 13 | 14 | const AppDispatcher = new DispatcherClass(); 15 | 16 | export default AppDispatcher; 17 | -------------------------------------------------------------------------------- /reactjs-flex-order-practice/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | POS Order - ReactJS + Flux 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
    19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /reactjs-flex-order-practice/src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import OrderMenu from './components/OrderMenu.jsx'; 4 | import OrderContent from './components/OrderContent.jsx'; 5 | import OrderActions from './actions/OrderActions'; 6 | 7 | require("!style!css!sass!./styles/entry.scss"); 8 | 9 | class App extends React.Component { 10 | constructor(props) { 11 | super(props); 12 | this.state = {}; 13 | } 14 | 15 | componentDidMount() { 16 | OrderActions.loadMenuCategories(); 17 | OrderActions.loadMenuItems(); 18 | OrderActions.loadTables(); 19 | } 20 | 21 | render() { 22 | return ( 23 |
    24 |
    25 |
    26 |
    27 | {/* 左側欄 */} 28 | {/* 右側欄 */} 29 |
    30 |
    31 |
    32 |
    33 | ); 34 | } 35 | } 36 | 37 | ReactDOM.render( 38 | , document.getElementById('app')); 39 | -------------------------------------------------------------------------------- /reactjs-flex-order-practice/src/styles/components/datetime.scss: -------------------------------------------------------------------------------- 1 | .datetime { 2 | .time { 3 | font-family: 'Roboto', sans-serif; 4 | font-size: 24px; 5 | vertical-align: middle; 6 | display: inline-block; 7 | } 8 | .date { 9 | font-family: 'Roboto', sans-serif; 10 | font-size: 16px; 11 | margin-top: 5px; 12 | vertical-align: middle; 13 | display: inline-block; 14 | } 15 | height: 50px; // nav menu bar 的高度 16 | text-align: center; 17 | font-size: 0; 18 | &::before { 19 | content: ''; 20 | height: 100%; 21 | vertical-align: middle; 22 | display: inline-block; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /reactjs-flex-order-practice/src/styles/components/input.scss: -------------------------------------------------------------------------------- 1 | //input[type="number"] { 2 | // width:3em; 3 | //} 4 | -------------------------------------------------------------------------------- /reactjs-flex-order-practice/src/styles/components/loading.scss: -------------------------------------------------------------------------------- 1 | .sk-circle { 2 | margin: 100px auto; 3 | width: 40px; 4 | height: 40px; 5 | position: relative; 6 | } 7 | .sk-circle .sk-child { 8 | width: 100%; 9 | height: 100%; 10 | position: absolute; 11 | left: 0; 12 | top: 0; 13 | } 14 | .sk-circle .sk-child:before { 15 | content: ''; 16 | display: block; 17 | margin: 0 auto; 18 | width: 15%; 19 | height: 15%; 20 | background-color: #333; 21 | border-radius: 100%; 22 | -webkit-animation: sk-circleBounceDelay 1.2s infinite ease-in-out both; 23 | animation: sk-circleBounceDelay 1.2s infinite ease-in-out both; 24 | } 25 | .sk-circle .sk-circle2 { 26 | -webkit-transform: rotate(30deg); 27 | -ms-transform: rotate(30deg); 28 | transform: rotate(30deg); } 29 | .sk-circle .sk-circle3 { 30 | -webkit-transform: rotate(60deg); 31 | -ms-transform: rotate(60deg); 32 | transform: rotate(60deg); } 33 | .sk-circle .sk-circle4 { 34 | -webkit-transform: rotate(90deg); 35 | -ms-transform: rotate(90deg); 36 | transform: rotate(90deg); } 37 | .sk-circle .sk-circle5 { 38 | -webkit-transform: rotate(120deg); 39 | -ms-transform: rotate(120deg); 40 | transform: rotate(120deg); } 41 | .sk-circle .sk-circle6 { 42 | -webkit-transform: rotate(150deg); 43 | -ms-transform: rotate(150deg); 44 | transform: rotate(150deg); } 45 | .sk-circle .sk-circle7 { 46 | -webkit-transform: rotate(180deg); 47 | -ms-transform: rotate(180deg); 48 | transform: rotate(180deg); } 49 | .sk-circle .sk-circle8 { 50 | -webkit-transform: rotate(210deg); 51 | -ms-transform: rotate(210deg); 52 | transform: rotate(210deg); } 53 | .sk-circle .sk-circle9 { 54 | -webkit-transform: rotate(240deg); 55 | -ms-transform: rotate(240deg); 56 | transform: rotate(240deg); } 57 | .sk-circle .sk-circle10 { 58 | -webkit-transform: rotate(270deg); 59 | -ms-transform: rotate(270deg); 60 | transform: rotate(270deg); } 61 | .sk-circle .sk-circle11 { 62 | -webkit-transform: rotate(300deg); 63 | -ms-transform: rotate(300deg); 64 | transform: rotate(300deg); } 65 | .sk-circle .sk-circle12 { 66 | -webkit-transform: rotate(330deg); 67 | -ms-transform: rotate(330deg); 68 | transform: rotate(330deg); } 69 | .sk-circle .sk-circle2:before { 70 | -webkit-animation-delay: -1.1s; 71 | animation-delay: -1.1s; } 72 | .sk-circle .sk-circle3:before { 73 | -webkit-animation-delay: -1s; 74 | animation-delay: -1s; } 75 | .sk-circle .sk-circle4:before { 76 | -webkit-animation-delay: -0.9s; 77 | animation-delay: -0.9s; } 78 | .sk-circle .sk-circle5:before { 79 | -webkit-animation-delay: -0.8s; 80 | animation-delay: -0.8s; } 81 | .sk-circle .sk-circle6:before { 82 | -webkit-animation-delay: -0.7s; 83 | animation-delay: -0.7s; } 84 | .sk-circle .sk-circle7:before { 85 | -webkit-animation-delay: -0.6s; 86 | animation-delay: -0.6s; } 87 | .sk-circle .sk-circle8:before { 88 | -webkit-animation-delay: -0.5s; 89 | animation-delay: -0.5s; } 90 | .sk-circle .sk-circle9:before { 91 | -webkit-animation-delay: -0.4s; 92 | animation-delay: -0.4s; } 93 | .sk-circle .sk-circle10:before { 94 | -webkit-animation-delay: -0.3s; 95 | animation-delay: -0.3s; } 96 | .sk-circle .sk-circle11:before { 97 | -webkit-animation-delay: -0.2s; 98 | animation-delay: -0.2s; } 99 | .sk-circle .sk-circle12:before { 100 | -webkit-animation-delay: -0.1s; 101 | animation-delay: -0.1s; } 102 | 103 | @-webkit-keyframes sk-circleBounceDelay { 104 | 0%, 80%, 100% { 105 | -webkit-transform: scale(0); 106 | transform: scale(0); 107 | } 40% { 108 | -webkit-transform: scale(1); 109 | transform: scale(1); 110 | } 111 | } 112 | 113 | @keyframes sk-circleBounceDelay { 114 | 0%, 80%, 100% { 115 | -webkit-transform: scale(0); 116 | transform: scale(0); 117 | } 40% { 118 | -webkit-transform: scale(1); 119 | transform: scale(1); 120 | } 121 | } -------------------------------------------------------------------------------- /reactjs-flex-order-practice/src/styles/entry.scss: -------------------------------------------------------------------------------- 1 | // variables 2 | @import 'variables/font.scss'; 3 | @import 'variables/button.scss'; 4 | 5 | // layout 6 | @import 'layout/grid.scss'; 7 | @import 'layout/app.scss'; 8 | 9 | // Payment style 10 | @import 'pages/Order.scss'; 11 | 12 | // input style 13 | @import 'components/input.scss'; 14 | 15 | // loading style 16 | // ref:http://tobiasahlin.com/spinkit/ 17 | @import 'components/loading'; 18 | 19 | @import 'components/datetime.scss'; 20 | -------------------------------------------------------------------------------- /reactjs-flex-order-practice/src/styles/layout/app.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | /* The html and body elements cannot have any padding or margin. */ 5 | } 6 | 7 | 8 | // 滿版 window 高度, ref:http://www.bootply.com/97103 9 | .center-container { 10 | height:100%; 11 | } 12 | 13 | .center-row { 14 | height:100%; 15 | background-color: #9ac7d5; 16 | background-size:cover; 17 | overflow-y: hidden; 18 | } 19 | 20 | 21 | #app{ 22 | height: 100%; 23 | font-family: "微軟正黑體", "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; 24 | } 25 | 26 | /* Wrapper for page content to push down footer */ 27 | #wrap { 28 | min-height: 100%; 29 | height: 100%; 30 | /* Negative indent footer by its height */ 31 | margin: 0 auto 0 0; 32 | /* Pad bottom by footer height */ 33 | padding: 0 0 0 0; 34 | 35 | > .center-container { 36 | padding: 0 0 0 0; 37 | margin:0; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /reactjs-flex-order-practice/src/styles/layout/grid.scss: -------------------------------------------------------------------------------- 1 | // 複寫 col-* 的 padding & margin 2 | .nopadding { 3 | padding: 0 !important; 4 | margin: 0 !important; 5 | } -------------------------------------------------------------------------------- /reactjs-flex-order-practice/src/styles/pages/Order.scss: -------------------------------------------------------------------------------- 1 | .order { 2 | .menu { 3 | position: relative; 4 | .item { 5 | margin: 0 15px; 6 | .box { 7 | .text { 8 | margin: 10px; 9 | font-size: $fontValue; 10 | vertical-align: middle; 11 | display: inline-block; 12 | } 13 | margin: 5px; 14 | cursor: pointer; 15 | box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); 16 | background-color: #d9edf7; 17 | &:hover { 18 | background-color: #dff0d8; 19 | } 20 | &:active { 21 | box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.6), 0 6px 20px 0 rgba(0, 0, 0, 0.59); 22 | } 23 | // 區塊內容置中, height 已預先定義在 DOM 上 24 | text-align: center; 25 | font-size: 0; 26 | &::before { 27 | content: ''; 28 | height: 100%; 29 | vertical-align: middle; 30 | display: inline-block; 31 | } 32 | } 33 | } 34 | .item-nav { 35 | position: absolute; 36 | bottom: 0; 37 | .btn { 38 | span { 39 | font-size: $fontTitle; 40 | } 41 | padding: 10px 0; 42 | } 43 | } 44 | } 45 | .content { 46 | // position: relative; 47 | .panel.center-container { 48 | position: relative; 49 | .panel-heading { 50 | font-size: $fontTitle; 51 | } 52 | .panel-body { 53 | // 該 div height 由 js 動態決定 54 | overflow-y: auto; // 依據 div height 作用自動滾輪 55 | .order-details { 56 | table { 57 | font-size: $tableFont; 58 | } 59 | table { 60 | tbody tr td, 61 | thead tr th { 62 | &:nth-child(1) { 63 | width: 45%; 64 | } 65 | &:nth-child(2), 66 | &:nth-child(3) { 67 | width: 20%; 68 | } 69 | &:nth-child(4) { 70 | width: 10%; 71 | } 72 | &:nth-child(5) { 73 | width: 5%; 74 | } 75 | } 76 | } 77 | } 78 | .order-submit { 79 | position: absolute; 80 | bottom: 0; 81 | width: 100%; 82 | padding: 0 15px; 83 | margin-left: -15px; 84 | margin-bottom: -20px; 85 | button[type=submit] { 86 | height: $orderButtonHeight * 2; 87 | font-size: $fontValue; 88 | } 89 | select[name=table] { 90 | height: $orderButtonHeight; 91 | } 92 | .total { 93 | > div { 94 | height: $orderButtonHeight; 95 | } 96 | .title { 97 | font-size: $fontValue; 98 | padding: 6px; 99 | color: #333; 100 | background-color: #f5f5f5; 101 | border-color: #ddd; 102 | border-bottom: 1px solid transparent; 103 | border-top-left-radius: 3px; 104 | border-top-right-radius: 3px; 105 | } 106 | .value { 107 | font-size: $fontValue; 108 | padding: 6px; 109 | } 110 | } 111 | } 112 | } 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /reactjs-flex-order-practice/src/styles/variables/button.scss: -------------------------------------------------------------------------------- 1 | $orderButtonHeight: 55px; 2 | -------------------------------------------------------------------------------- /reactjs-flex-order-practice/src/styles/variables/font.scss: -------------------------------------------------------------------------------- 1 | $fontTitle: 36px; 2 | $fontValue: 24px; 3 | $tableFont: 20px; -------------------------------------------------------------------------------- /reactjs-flex-order-practice/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | const HTMLWebpackPluginConfig = new HtmlWebpackPlugin({ 5 | template: `${__dirname}/src/index.html`, 6 | filename: 'index.html', 7 | inject: 'body', 8 | }); 9 | 10 | module.exports = { 11 | entry: [ 12 | './src/index.jsx', 13 | ], 14 | output: { 15 | path: `${__dirname}`, 16 | filename: 'index_bundle.js', 17 | }, 18 | module: { 19 | preLoaders: [{ 20 | test: /\.jsx$|\.js$/, 21 | loader: 'eslint-loader', 22 | include: `${__dirname}/src`, 23 | exclude: /bundle\.js$/ 24 | }], 25 | loaders: [{ 26 | test: /\.js$|\.jsx$/, 27 | exclude: /node_modules/, 28 | loader: 'babel-loader', 29 | query: { 30 | presets: ['es2015', 'react'], 31 | }, 32 | }, { 33 | test: /\.scss$/, 34 | loaders: ["style", "css", "sass"] 35 | }], 36 | }, 37 | devServer: { 38 | inline: true, 39 | port: 8008, 40 | }, 41 | plugins: [HTMLWebpackPluginConfig], 42 | }; 43 | -------------------------------------------------------------------------------- /reactjs-order-practice/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "react", 5 | ], 6 | "plugins": [] 7 | } 8 | -------------------------------------------------------------------------------- /reactjs-order-practice/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "rules": { 4 | "no-multiple-empty-lines": [2, {"max": 2, "maxEOF": 2}] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /reactjs-order-practice/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /reactjs-order-practice/db/category.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "id": 1, 3 | "name": "\u4e3b\u98df", 4 | "user": "rz", 5 | "is_active": 1, 6 | "created_at": "2016-06-09 20:51:39", 7 | "updated_at": "2016-06-09 20:51:39" 8 | }, { 9 | "id": 2, 10 | "name": "\u6e6f\u54c1", 11 | "user": "rz", 12 | "is_active": 1, 13 | "created_at": "2016-06-09 20:51:39", 14 | "updated_at": "2016-06-09 20:51:39" 15 | }, { 16 | "id": 3, 17 | "name": "\u5c0f\u9ede", 18 | "user": "rz", 19 | "is_active": 1, 20 | "created_at": "2016-06-09 20:51:39", 21 | "updated_at": "2016-06-09 20:51:39" 22 | }] 23 | -------------------------------------------------------------------------------- /reactjs-order-practice/db/tables.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "id": 1, 3 | "name": "一號桌" 4 | }, { 5 | "id": 2, 6 | "name": "二號桌" 7 | }, { 8 | "id": 3, 9 | "name": "三號桌" 10 | }, { 11 | "id": 4, 12 | "name": "四號桌" 13 | }, { 14 | "id": 5, 15 | "name": "五號桌" 16 | }, { 17 | "id": 6, 18 | "name": "六號桌" 19 | }] 20 | -------------------------------------------------------------------------------- /reactjs-order-practice/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | POS Order - ReactJS + Flux 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
    19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /reactjs-order-practice/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reactjs-order-practice", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "webpack-dev-server --colors --inline --content-base .", 8 | "prod": "webpack -p" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "react": "^15.3.2", 14 | "react-dom": "^15.3.2" 15 | }, 16 | "devDependencies": { 17 | "babel-core": "^6.16.0", 18 | "babel-eslint": "^7.0.0", 19 | "babel-loader": "^6.2.5", 20 | "babel-preset-es2015": "^6.16.0", 21 | "babel-preset-react": "^6.16.0", 22 | "css-loader": "^0.25.0", 23 | "eslint": "^3.6.1", 24 | "eslint-config-airbnb": "^12.0.0", 25 | "eslint-loader": "^1.5.0", 26 | "eslint-plugin-import": "^1.16.0", 27 | "eslint-plugin-jsx-a11y": "^2.2.2", 28 | "eslint-plugin-react": "^6.3.0", 29 | "html-webpack-plugin": "^2.22.0", 30 | "node-sass": "^3.10.1", 31 | "sass-loader": "^4.0.2", 32 | "style-loader": "^0.13.1", 33 | "webpack": "^1.13.2", 34 | "webpack-dev-server": "^1.16.1" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /reactjs-order-practice/readme.md: -------------------------------------------------------------------------------- 1 | # POS Order 2 | 3 | ## 概觀 4 | 練習以 ReactJS 實作 POS 點單介面 5 | 6 | 7 | ## 開發 8 | 1. 套件相依安裝 `npm install` 9 | 2. 運行服務 `npm start` 10 | 3. 開啟瀏覽器 11 | 12 | ``` 13 | http://localhost:8008 14 | ``` 15 | 16 | ## 產出 js & html 17 | 運行 `npm run prod` 18 | 19 | > 產出的 js & html file 會在根目錄下 20 | 21 | 22 | ## 參考 23 | * [從零開始學 ReactJS(ReactJS 101)](https://github.com/kdchang/reactjs101) 24 | * [24 小時,React 快速入門](https://github.com/shiningjason1989/react-quick-tutorial/) 25 | -------------------------------------------------------------------------------- /reactjs-order-practice/src/actions/OrderActions.js: -------------------------------------------------------------------------------- 1 | const OrderActions = { 2 | addItemToOrder(order, item) { 3 | let target = order.find((record) => record.id === item.id); 4 | if (target) { 5 | // 已在 order 內, 更新 obj 的 amount & subtotal 6 | target.amount += 1; 7 | target.subtotal = target.amount * target.price; 8 | } else { 9 | // 若不在 order 內, 產生新 obj 10 | order.push({ 11 | id: item.id, 12 | name: item.name, 13 | amount: 1, 14 | price: item.price, 15 | subtotal: 1 * item.price 16 | }); 17 | } 18 | return order; 19 | }, 20 | removeItemFromOder(order, item) { 21 | let target = order.find((record) => record.id === item.id); 22 | if (target) { 23 | target.amount -= 1; 24 | target.subtotal = target.amount * target.price; 25 | } 26 | order = order.filter((record) => record.amount > 0); 27 | return order; 28 | }, 29 | addAdditionToOrder(addition) { 30 | addition.push({ 31 | id: new Date().getTime(), 32 | name: '', 33 | amount: 1, 34 | price: 0, 35 | subtotal: 0 36 | }); 37 | return addition; 38 | }, 39 | removeItemFromAddition(addition, item) { 40 | const additionIdx = addition.findIndex((record) => record.id === item.id); 41 | if (additionIdx !== -1) { 42 | addition.splice(additionIdx, 1); 43 | } 44 | return addition; 45 | }, 46 | updateAdditionItem(addition, item) { 47 | let target = addition.find((record) => record.id === item.id); 48 | if (target) { 49 | target.name = item.name; 50 | target.amount = item.amount; 51 | target.price = item.price; 52 | target.subtotal = item.amount * item.price; 53 | } 54 | return addition; 55 | }, 56 | updateTotal(order, addition) { 57 | let total = 0; 58 | order.map((el) => total += el.subtotal); 59 | addition.map((el) => total += el.subtotal); 60 | return total; 61 | }, 62 | toggleWhenStatus(when) { 63 | return (when === '中午') ? '晚上' : '中午'; 64 | }, 65 | toggleIsToGoStatus(isToGo) { 66 | return !isToGo; 67 | } 68 | }; 69 | 70 | export default OrderActions; 71 | -------------------------------------------------------------------------------- /reactjs-order-practice/src/components/OrderContent.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import OrderConstants from '../constants/OrderConstants'; 3 | import OrderContentDetail from './OrderContentDetail.jsx'; 4 | import OrderContentOperation from './OrderContentOperation.jsx'; 5 | import OrderContentSubmit from './OrderContentSubmit.jsx'; 6 | import OrderContentTotal from './OrderContentTotal.jsx'; 7 | 8 | class OrderContent extends Component { 9 | getOrderContentRowHeight() { 10 | const bodyHeight = document.body.clientHeight; 11 | const orderContentHeight = (bodyHeight - OrderConstants.contentDetailHeadingHeight - OrderConstants.contentOperationHeight); 12 | this.setState({ 13 | orderContentStyle: { 14 | height: orderContentHeight + 'px' 15 | } 16 | }); 17 | } 18 | 19 | constructor(props) { 20 | super(props); 21 | this.state = { 22 | orderContentStyle: { 23 | height: 'inherit' 24 | } 25 | }; 26 | this.getOrderContentRowHeight = this.getOrderContentRowHeight.bind(this); 27 | } 28 | 29 | componentDidMount() { 30 | // item 動態列高 31 | this.getOrderContentRowHeight(); 32 | window.addEventListener('resize', () => this.getOrderContentRowHeight()); 33 | } 34 | 35 | componentWillUnmount() { 36 | window.removeEventListener('resize', () => this.getOrderContentRowHeight()); 37 | } 38 | 39 | render() { 40 | return ( 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 | export default OrderContent; 69 | -------------------------------------------------------------------------------- /reactjs-order-practice/src/components/OrderContentDetail.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import OrderActions from './../actions/OrderActions'; 3 | 4 | class OrderItemFromMenu extends Component { 5 | handleClickItem() { 6 | this.props.updateOrder(OrderActions.removeItemFromOder, this.props.item); 7 | } 8 | 9 | constructor(props) { 10 | super(props); 11 | this.handleClickItem = this.handleClickItem.bind(this); 12 | } 13 | 14 | render() { 15 | return ( 16 | 17 | {this.props.item.name} 18 | {this.props.item.amount} 19 | {this.props.item.price} 20 | {this.props.item.subtotal} 21 | 22 | 25 | 26 | 27 | ); 28 | } 29 | 30 | } 31 | 32 | class OrderItemFromAddition extends Component { 33 | handleChange(e) { 34 | // 取值 35 | const id = e.target.dataset.id; 36 | const field = e.target.dataset.field; 37 | const value = e.target.value; 38 | 39 | // 改變 state 40 | let _item = this.state.item; 41 | _item[field] = value; 42 | _item.subtotal = _item.amount * _item.price; 43 | this.setState({item: _item}); 44 | 45 | // 改變 store 的 addition 46 | this.props.updateAddition(OrderActions.updateAdditionItem, _item); 47 | } 48 | 49 | handleClickAddition(item) { 50 | this.props.updateAddition(OrderActions.removeItemFromAddition, item); 51 | } 52 | 53 | constructor(props) { 54 | super(props); 55 | this.state = { 56 | item: props.item 57 | }; 58 | this.handleChange = this.handleChange.bind(this); 59 | this.handleClickAddition = this.handleClickAddition.bind(this); 60 | } 61 | 62 | render() { 63 | return ( 64 | 65 | 66 | 67 | 68 | {this.state.item.subtotal} 69 | 70 | 73 | 74 | 75 | ); 76 | } 77 | } 78 | 79 | class OrderContentDetail extends Component { 80 | render() { 81 | return ( 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | {this.props.order.map((record) => ())} 94 | {this.props.addition.map((record) => ())} 95 | 96 |
    品項數量單價小計
    97 | ); 98 | } 99 | } 100 | 101 | export default OrderContentDetail; 102 | -------------------------------------------------------------------------------- /reactjs-order-practice/src/components/OrderContentOperation.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import OrderActions from './../actions/OrderActions'; 3 | 4 | class OrderAddAddition extends Component { 5 | handleClick() { 6 | this.props.updateAddition(OrderActions.addAdditionToOrder); 7 | } 8 | 9 | constructor(props) { 10 | super(props); 11 | this.handleClick = this.handleClick.bind(this); 12 | } 13 | 14 | render() { 15 | return ( 16 |
    17 | 22 |
    23 | ); 24 | } 25 | } 26 | 27 | class OrderToGo extends Component { 28 | handleClick() { 29 | this.props.updateIsToGoStatus(); 30 | } 31 | 32 | constructor(props) { 33 | super(props); 34 | this.state = {}; 35 | this.handleClick = this.handleClick.bind(this); 36 | } 37 | 38 | render() { 39 | const toGoIconClass = (this.props.isToGo) 40 | ? 'fa fa-hand-o-right' 41 | : 'fa fa-cutlery'; // 外帶狀態 icon 42 | const toGoButtonClass = ((this.props.isToGo) 43 | ? 'btn btn-danger' 44 | : 'btn btn-success') + ' btn-lg btn-block'; 45 | return ( 46 |
    47 | 54 |
    55 | ); 56 | } 57 | } 58 | 59 | class OrderWhen extends Component { 60 | handleClick() { 61 | this.props.updateWhenStatus(); 62 | } 63 | 64 | constructor(props) { 65 | super(props); 66 | this.state = {}; 67 | this.handleClick = this.handleClick.bind(this); 68 | } 69 | 70 | render() { 71 | const whenIconClass = (this.props.when === '中午') 72 | ? 'fa fa-sun-o' 73 | : 'fa fa-moon-o'; // 中午/晚上狀態 icon 74 | const whenButtonClass = ((this.props.when === '中午') 75 | ? 'btn btn-warning' 76 | : 'btn btn-primary') + ' btn-lg btn-block'; // 中午/晚上狀態 icon 77 | return ( 78 |
    79 | 84 |
    85 | ); 86 | } 87 | } 88 | 89 | class OrderTables extends Component { 90 | render() { 91 | if (this.props.isToGo == true) { 92 | return ( 93 |
    94 | 97 |
    98 | ); 99 | } 100 | return ( 101 |
    102 | 109 |
    110 | ); 111 | } 112 | } 113 | 114 | const OrderContentOperation = (props) => ( 115 |
    116 | 117 | 118 | 119 | 120 |
    121 | ); 122 | 123 | export default OrderContentOperation; 124 | -------------------------------------------------------------------------------- /reactjs-order-practice/src/components/OrderContentSubmit.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | 3 | class OrderSubmit extends Component { 4 | handleClick() { 5 | // dump order & addition records 6 | const data = { 7 | order: this.props.order, 8 | addition: this.props.addition 9 | }; 10 | console.log('--- dump order & addition records ---'); 11 | console.log(data); 12 | 13 | // clear order & addition records 14 | console.log('--- clear order & addition records ---'); 15 | this.props.clearOrderAndAdditon(); 16 | 17 | } 18 | 19 | constructor(props) { 20 | super(props); 21 | this.handleClick = this.handleClick.bind(this); 22 | } 23 | 24 | render() { 25 | return ( 26 | 31 | ); 32 | } 33 | } 34 | 35 | export default OrderSubmit; 36 | -------------------------------------------------------------------------------- /reactjs-order-practice/src/components/OrderContentTotal.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | 3 | const OrderContentTotal = (props) => ( 4 |
    5 |
    6 | 總計 7 |
    8 |
    9 | {props.total} 10 |
    11 |
    12 | ); 13 | 14 | export default OrderContentTotal; 15 | -------------------------------------------------------------------------------- /reactjs-order-practice/src/components/OrderMenu.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import OrderConstants from '../constants/OrderConstants'; 3 | import OrderMenuCategoryList from './OrderMenuCategoryList.jsx' 4 | import OrderMenuItemList from './OrderMenuItemList.jsx' 5 | import OrderMenuPager from './OrderMenuPager.jsx' 6 | 7 | class OrderMenu extends Component { 8 | updateMenu(categoryId, pageId) { 9 | const items = this.props.items.filter((record) => { 10 | return record.category_id == categoryId; 11 | }).filter((record, i) => { 12 | return OrderConstants.itemsPerPageAmount * pageId <= i && i < OrderConstants.itemsPerPageAmount * (1 + pageId); 13 | }); 14 | this.setState({items: items, categoryId: categoryId}); 15 | } 16 | 17 | constructor(props) { 18 | super(props); 19 | this.state = { 20 | items: [], 21 | categoryId: null, 22 | currentPage: null 23 | }; 24 | this.updateMenu = this.updateMenu.bind(this); 25 | } 26 | 27 | componentWillReceiveProps(nextProps) { 28 | if (nextProps.categories.length > 0 && this.state.categoryId === null) { 29 | // set default category && menu page 30 | const categoryId = nextProps.categories[0].id 31 | const currentPage = 0; 32 | this.setState({selectedCategory: categoryId, currentPage: currentPage}); 33 | 34 | // refresh menu item 35 | this.updateMenu(categoryId, currentPage); 36 | } 37 | } 38 | 39 | render() { 40 | return ( 41 |
    42 | 43 | 44 | 45 |
    46 | ); 47 | } 48 | } 49 | 50 | export default OrderMenu; 51 | -------------------------------------------------------------------------------- /reactjs-order-practice/src/components/OrderMenuCategoryList.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | 3 | // stateless component 4 | const OrderMenyCategoryListItem = (props) => { 5 | const itemClass = props.isSelected 6 | ? "active" 7 | : ""; 8 | return ( 9 |
  • 10 | 11 |

    {props.name}

    12 |
    13 |
  • 14 | ) 15 | }; 16 | 17 | class OrderMenuCategoryList extends Component { 18 | handleClick(categoryId) { 19 | // refresh menu item 20 | const currentPage = 0; 21 | this.props.updateMenu(categoryId, currentPage); 22 | } 23 | 24 | constructor(props) { 25 | super(props); 26 | this.handleClick = this.handleClick.bind(this); 27 | } 28 | 29 | render() { 30 | return ( 31 |
    32 | 35 |
    36 | ); 37 | } 38 | } 39 | 40 | export default OrderMenuCategoryList; 41 | -------------------------------------------------------------------------------- /reactjs-order-practice/src/components/OrderMenuItemList.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import OrderConstants from '../constants/OrderConstants'; 3 | import OrderActions from '../actions/OrderActions'; 4 | 5 | class OrderMenuItem extends Component { 6 | handleClickItem(item) { 7 | this.props.updateOrder(OrderActions.addItemToOrder, item); 8 | } 9 | 10 | constructor(props) { 11 | super(props); 12 | this.handleClickItem = this.handleClickItem.bind(this); 13 | } 14 | 15 | render() { 16 | return ( 17 |
    18 |
    19 |
    {this.props.item.name}
    20 |
    21 |
    22 | ); 23 | } 24 | } 25 | 26 | class OrderMenuItemList extends Component { 27 | getItemRowHeight() { 28 | const bodyHeight = document.body.clientHeight; 29 | const rowHeight = (bodyHeight - OrderConstants.menuItemRowMarginHeight - OrderConstants.menuCategoryListHeight - OrderConstants.menuPagerHeight) / OrderConstants.menuItemListRows; 30 | this.setState({ 31 | itemRowStyle: { 32 | height: rowHeight + 'px' 33 | } 34 | }); 35 | } 36 | 37 | constructor(props) { 38 | super(props); 39 | this.state = { 40 | itemRowStyle: { 41 | height: 'inherit' 42 | } 43 | }; 44 | this.getItemRowHeight = this.getItemRowHeight.bind(this); 45 | } 46 | 47 | componentDidMount() { 48 | // item 動態列高 49 | this.getItemRowHeight(); 50 | window.addEventListener('resize', () => this.getItemRowHeight()); 51 | } 52 | 53 | componentWillUnmount() { 54 | window.removeEventListener('resize', () => this.getItemRowHeight()); 55 | } 56 | 57 | render() { 58 | return ( 59 |
    60 |
    61 | {this.props.items.map((record) => ())} 62 |
    63 |
    64 | ); 65 | } 66 | } 67 | 68 | export default OrderMenuItemList; 69 | -------------------------------------------------------------------------------- /reactjs-order-practice/src/components/OrderMenuPager.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import OrderConstants from '../constants/OrderConstants'; 3 | 4 | class OrderMenuPager extends Component { 5 | handleClick(pageId) { 6 | // 更新當前頁面 idx 7 | this.setState({currentMenuPage: pageId}); 8 | 9 | // refresh menu item 10 | this.props.updateMenu(this.props.categoryId, pageId); 11 | } 12 | 13 | constructor(props) { 14 | super(props); 15 | this.state = { 16 | menuPagesCount: null, 17 | currentMenuPage: 0 18 | }; 19 | this.handleClick = this.handleClick.bind(this); 20 | } 21 | 22 | componentWillReceiveProps(nextProps) { 23 | if (this.props.categoryId !== nextProps.categoryId && nextProps.categoryId !== null) { 24 | const menuPagesCount = Math.ceil(nextProps.items.filter((record) => { 25 | return record.category_id == nextProps.categoryId; 26 | }).length / OrderConstants.itemsPerPageAmount); 27 | this.setState({menuPagesCount: menuPagesCount}); 28 | } 29 | } 30 | 31 | render() { 32 | const hasNextPage = this.state.menuPagesCount - this.state.currentMenuPage > 1; // 是否還有下一頁 33 | const hasPrevPage = this.state.currentMenuPage - 1 > -1; // 是否還有上一頁 34 | const nextPage = hasNextPage 35 | ? this.state.currentMenuPage + 1 36 | : this.state.currentMenuPage; 37 | const prevPage = hasPrevPage 38 | ? this.state.currentMenuPage - 1 39 | : this.state.currentMenuPage; 40 | 41 | return ( 42 |
    43 |
    44 |
    45 | 50 |
    51 |
    52 | 57 |
    58 |
    59 |
    60 | ); 61 | } 62 | } 63 | 64 | export default OrderMenuPager; 65 | -------------------------------------------------------------------------------- /reactjs-order-practice/src/constants/OrderConstants.js: -------------------------------------------------------------------------------- 1 | const now = new Date(); 2 | const nightStart = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 17, 30, 0, 0); 3 | const whenStatus = (now.getTime() - nightStart.getTime() < 0) ? '中午' : '晚上'; 4 | 5 | const OrderConstants = { 6 | // parameters 7 | itemsPerPageAmount: 12, // 每頁菜單項目數 8 | menuItemListRows: 3, // 菜單列數 9 | menuItemRowMarginHeight: 30, // 菜單列與列之間 margin 高 10 | menuCategoryListHeight: 87, // 菜單分類 DOM 高 11 | menuPagerHeight: 62, // 菜單分頁 DOM 高 12 | contentDetailHeadingHeight: 72, // "點單明細" 高 13 | contentOperationHeight: 133, // order 操作區域高 14 | // store default value 15 | whenStatus: whenStatus // 菜單 中午/晚上的狀態 16 | }; 17 | 18 | export default OrderConstants; 19 | -------------------------------------------------------------------------------- /reactjs-order-practice/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | POS Order - ReactJS + Flux 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
    19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /reactjs-order-practice/src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import OrderMenu from './components/OrderMenu.jsx'; 4 | import OrderContent from './components/OrderContent.jsx'; 5 | import OrderActions from './actions/OrderActions'; 6 | import OrderConstants from './constants/OrderConstants'; 7 | 8 | require("!style!css!sass!./styles/entry.scss"); 9 | 10 | class App extends React.Component { 11 | updateOrder(cb, item) { 12 | const order = cb(this.state.order, item); 13 | const total = OrderActions.updateTotal(order, this.state.addition); 14 | this.setState({order: order, total: total}); 15 | } 16 | 17 | updateAddition(cb, item) { 18 | const addition = cb(this.state.addition, item); 19 | const total = OrderActions.updateTotal(this.state.order, addition); 20 | this.setState({addition: addition, total: total}); 21 | } 22 | 23 | updateIsToGoStatus() { 24 | this.setState({ 25 | isToGo: OrderActions.toggleIsToGoStatus(this.state.isToGo) 26 | }); 27 | } 28 | 29 | updateWhenStatus() { 30 | this.setState({ 31 | when: OrderActions.toggleWhenStatus(this.state.when) 32 | }); 33 | } 34 | 35 | clearOrderAndAdditon() { 36 | this.setState({ 37 | order: [], 38 | addition: [], 39 | total: 0, 40 | }); 41 | } 42 | 43 | constructor(props) { 44 | super(props); 45 | this.state = { 46 | categories: [], // for 47 | items: [], // for 48 | order: [], // for 49 | addition: [], // for 50 | tables: [], // for 51 | when: OrderConstants.whenStatus, // for 52 | isToGo: false, // for 53 | total: 0, // for 54 | }; 55 | this.updateOrder = this.updateOrder.bind(this); 56 | this.updateAddition = this.updateAddition.bind(this); 57 | this.updateIsToGoStatus = this.updateIsToGoStatus.bind(this); 58 | this.updateWhenStatus = this.updateWhenStatus.bind(this); 59 | this.clearOrderAndAdditon = this.clearOrderAndAdditon.bind(this); 60 | } 61 | 62 | componentDidMount() { 63 | // load items 64 | fetch('./db/item.json', {method: 'GET'}).then((response) => response.json()).then((records) => { 65 | this.setState({items: records}); 66 | }); 67 | 68 | // load categories && set default category 69 | fetch('./db/category.json', {method: 'GET'}).then((response) => response.json()).then((records) => { 70 | this.setState({categories: records, selectedCategory: records[0].id}); 71 | }); 72 | 73 | // load tables 74 | fetch('./db/tables.json', {method: 'GET'}).then((response) => response.json()).then((records) => { 75 | this.setState({tables: records}); 76 | }); 77 | } 78 | 79 | render() { 80 | return ( 81 |
    82 |
    83 |
    84 |
    85 | {/* 左側欄 */} 86 | {/* 右側欄 */} 87 |
    88 |
    89 |
    90 |
    91 | ); 92 | } 93 | } 94 | 95 | ReactDOM.render( 96 | , document.getElementById('app')); 97 | -------------------------------------------------------------------------------- /reactjs-order-practice/src/styles/components/datetime.scss: -------------------------------------------------------------------------------- 1 | .datetime { 2 | .time { 3 | font-family: 'Roboto', sans-serif; 4 | font-size: 24px; 5 | vertical-align: middle; 6 | display: inline-block; 7 | } 8 | .date { 9 | font-family: 'Roboto', sans-serif; 10 | font-size: 16px; 11 | margin-top: 5px; 12 | vertical-align: middle; 13 | display: inline-block; 14 | } 15 | height: 50px; // nav menu bar 的高度 16 | text-align: center; 17 | font-size: 0; 18 | &::before { 19 | content: ''; 20 | height: 100%; 21 | vertical-align: middle; 22 | display: inline-block; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /reactjs-order-practice/src/styles/components/input.scss: -------------------------------------------------------------------------------- 1 | //input[type="number"] { 2 | // width:3em; 3 | //} 4 | -------------------------------------------------------------------------------- /reactjs-order-practice/src/styles/components/loading.scss: -------------------------------------------------------------------------------- 1 | .sk-circle { 2 | margin: 100px auto; 3 | width: 40px; 4 | height: 40px; 5 | position: relative; 6 | } 7 | .sk-circle .sk-child { 8 | width: 100%; 9 | height: 100%; 10 | position: absolute; 11 | left: 0; 12 | top: 0; 13 | } 14 | .sk-circle .sk-child:before { 15 | content: ''; 16 | display: block; 17 | margin: 0 auto; 18 | width: 15%; 19 | height: 15%; 20 | background-color: #333; 21 | border-radius: 100%; 22 | -webkit-animation: sk-circleBounceDelay 1.2s infinite ease-in-out both; 23 | animation: sk-circleBounceDelay 1.2s infinite ease-in-out both; 24 | } 25 | .sk-circle .sk-circle2 { 26 | -webkit-transform: rotate(30deg); 27 | -ms-transform: rotate(30deg); 28 | transform: rotate(30deg); } 29 | .sk-circle .sk-circle3 { 30 | -webkit-transform: rotate(60deg); 31 | -ms-transform: rotate(60deg); 32 | transform: rotate(60deg); } 33 | .sk-circle .sk-circle4 { 34 | -webkit-transform: rotate(90deg); 35 | -ms-transform: rotate(90deg); 36 | transform: rotate(90deg); } 37 | .sk-circle .sk-circle5 { 38 | -webkit-transform: rotate(120deg); 39 | -ms-transform: rotate(120deg); 40 | transform: rotate(120deg); } 41 | .sk-circle .sk-circle6 { 42 | -webkit-transform: rotate(150deg); 43 | -ms-transform: rotate(150deg); 44 | transform: rotate(150deg); } 45 | .sk-circle .sk-circle7 { 46 | -webkit-transform: rotate(180deg); 47 | -ms-transform: rotate(180deg); 48 | transform: rotate(180deg); } 49 | .sk-circle .sk-circle8 { 50 | -webkit-transform: rotate(210deg); 51 | -ms-transform: rotate(210deg); 52 | transform: rotate(210deg); } 53 | .sk-circle .sk-circle9 { 54 | -webkit-transform: rotate(240deg); 55 | -ms-transform: rotate(240deg); 56 | transform: rotate(240deg); } 57 | .sk-circle .sk-circle10 { 58 | -webkit-transform: rotate(270deg); 59 | -ms-transform: rotate(270deg); 60 | transform: rotate(270deg); } 61 | .sk-circle .sk-circle11 { 62 | -webkit-transform: rotate(300deg); 63 | -ms-transform: rotate(300deg); 64 | transform: rotate(300deg); } 65 | .sk-circle .sk-circle12 { 66 | -webkit-transform: rotate(330deg); 67 | -ms-transform: rotate(330deg); 68 | transform: rotate(330deg); } 69 | .sk-circle .sk-circle2:before { 70 | -webkit-animation-delay: -1.1s; 71 | animation-delay: -1.1s; } 72 | .sk-circle .sk-circle3:before { 73 | -webkit-animation-delay: -1s; 74 | animation-delay: -1s; } 75 | .sk-circle .sk-circle4:before { 76 | -webkit-animation-delay: -0.9s; 77 | animation-delay: -0.9s; } 78 | .sk-circle .sk-circle5:before { 79 | -webkit-animation-delay: -0.8s; 80 | animation-delay: -0.8s; } 81 | .sk-circle .sk-circle6:before { 82 | -webkit-animation-delay: -0.7s; 83 | animation-delay: -0.7s; } 84 | .sk-circle .sk-circle7:before { 85 | -webkit-animation-delay: -0.6s; 86 | animation-delay: -0.6s; } 87 | .sk-circle .sk-circle8:before { 88 | -webkit-animation-delay: -0.5s; 89 | animation-delay: -0.5s; } 90 | .sk-circle .sk-circle9:before { 91 | -webkit-animation-delay: -0.4s; 92 | animation-delay: -0.4s; } 93 | .sk-circle .sk-circle10:before { 94 | -webkit-animation-delay: -0.3s; 95 | animation-delay: -0.3s; } 96 | .sk-circle .sk-circle11:before { 97 | -webkit-animation-delay: -0.2s; 98 | animation-delay: -0.2s; } 99 | .sk-circle .sk-circle12:before { 100 | -webkit-animation-delay: -0.1s; 101 | animation-delay: -0.1s; } 102 | 103 | @-webkit-keyframes sk-circleBounceDelay { 104 | 0%, 80%, 100% { 105 | -webkit-transform: scale(0); 106 | transform: scale(0); 107 | } 40% { 108 | -webkit-transform: scale(1); 109 | transform: scale(1); 110 | } 111 | } 112 | 113 | @keyframes sk-circleBounceDelay { 114 | 0%, 80%, 100% { 115 | -webkit-transform: scale(0); 116 | transform: scale(0); 117 | } 40% { 118 | -webkit-transform: scale(1); 119 | transform: scale(1); 120 | } 121 | } -------------------------------------------------------------------------------- /reactjs-order-practice/src/styles/entry.scss: -------------------------------------------------------------------------------- 1 | // variables 2 | @import 'variables/font.scss'; 3 | @import 'variables/button.scss'; 4 | 5 | // layout 6 | @import 'layout/grid.scss'; 7 | @import 'layout/app.scss'; 8 | 9 | // Payment style 10 | @import 'pages/Order.scss'; 11 | 12 | // input style 13 | @import 'components/input.scss'; 14 | 15 | // loading style 16 | // ref:http://tobiasahlin.com/spinkit/ 17 | @import 'components/loading'; 18 | 19 | @import 'components/datetime.scss'; 20 | -------------------------------------------------------------------------------- /reactjs-order-practice/src/styles/layout/app.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | /* The html and body elements cannot have any padding or margin. */ 5 | } 6 | 7 | 8 | // 滿版 window 高度, ref:http://www.bootply.com/97103 9 | .center-container { 10 | height:100%; 11 | } 12 | 13 | .center-row { 14 | height:100%; 15 | background-color: #9ac7d5; 16 | background-size:cover; 17 | overflow-y: hidden; 18 | } 19 | 20 | 21 | #app{ 22 | height: 100%; 23 | font-family: "微軟正黑體", "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; 24 | } 25 | 26 | /* Wrapper for page content to push down footer */ 27 | #wrap { 28 | min-height: 100%; 29 | height: 100%; 30 | /* Negative indent footer by its height */ 31 | margin: 0 auto 0 0; 32 | /* Pad bottom by footer height */ 33 | padding: 0 0 0 0; 34 | 35 | > .center-container { 36 | padding: 0 0 0 0; 37 | margin:0; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /reactjs-order-practice/src/styles/layout/grid.scss: -------------------------------------------------------------------------------- 1 | // 複寫 col-* 的 padding & margin 2 | .nopadding { 3 | padding: 0 !important; 4 | margin: 0 !important; 5 | } -------------------------------------------------------------------------------- /reactjs-order-practice/src/styles/pages/Order.scss: -------------------------------------------------------------------------------- 1 | .order { 2 | .menu { 3 | position: relative; 4 | .item { 5 | margin: 0 15px; 6 | .box { 7 | .text { 8 | margin: 10px; 9 | font-size: $fontValue; 10 | vertical-align: middle; 11 | display: inline-block; 12 | } 13 | margin: 5px; 14 | cursor: pointer; 15 | box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); 16 | background-color: #d9edf7; 17 | &:hover { 18 | background-color: #dff0d8; 19 | } 20 | &:active { 21 | box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.6), 0 6px 20px 0 rgba(0, 0, 0, 0.59); 22 | } 23 | // 區塊內容置中, height 已預先定義在 DOM 上 24 | text-align: center; 25 | font-size: 0; 26 | &::before { 27 | content: ''; 28 | height: 100%; 29 | vertical-align: middle; 30 | display: inline-block; 31 | } 32 | } 33 | } 34 | .item-nav { 35 | position: absolute; 36 | bottom: 0; 37 | .btn { 38 | span { 39 | font-size: $fontTitle; 40 | } 41 | padding: 10px 0; 42 | } 43 | } 44 | } 45 | .content { 46 | // position: relative; 47 | .panel.center-container { 48 | position: relative; 49 | .panel-heading { 50 | font-size: $fontTitle; 51 | } 52 | .panel-body { 53 | // 該 div height 由 js 動態決定 54 | overflow-y: auto; // 依據 div height 作用自動滾輪 55 | .order-details { 56 | table { 57 | font-size: $tableFont; 58 | } 59 | table { 60 | tbody tr td, 61 | thead tr th { 62 | &:nth-child(1) { 63 | width: 45%; 64 | } 65 | &:nth-child(2), 66 | &:nth-child(3) { 67 | width: 20%; 68 | } 69 | &:nth-child(4) { 70 | width: 10%; 71 | } 72 | &:nth-child(5) { 73 | width: 5%; 74 | } 75 | } 76 | } 77 | } 78 | .order-submit { 79 | position: absolute; 80 | bottom: 0; 81 | width: 100%; 82 | padding: 0 15px; 83 | margin-left: -15px; 84 | margin-bottom: -20px; 85 | button[type=submit] { 86 | height: $orderButtonHeight * 2; 87 | font-size: $fontValue; 88 | } 89 | select[name=table] { 90 | height: $orderButtonHeight; 91 | } 92 | .total { 93 | > div { 94 | height: $orderButtonHeight; 95 | } 96 | .title { 97 | font-size: $fontValue; 98 | padding: 6px; 99 | color: #333; 100 | background-color: #f5f5f5; 101 | border-color: #ddd; 102 | border-bottom: 1px solid transparent; 103 | border-top-left-radius: 3px; 104 | border-top-right-radius: 3px; 105 | } 106 | .value { 107 | font-size: $fontValue; 108 | padding: 6px; 109 | } 110 | } 111 | } 112 | } 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /reactjs-order-practice/src/styles/variables/button.scss: -------------------------------------------------------------------------------- 1 | $orderButtonHeight: 55px; 2 | -------------------------------------------------------------------------------- /reactjs-order-practice/src/styles/variables/font.scss: -------------------------------------------------------------------------------- 1 | $fontTitle: 36px; 2 | $fontValue: 24px; 3 | $tableFont: 20px; -------------------------------------------------------------------------------- /reactjs-order-practice/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | const HTMLWebpackPluginConfig = new HtmlWebpackPlugin({ 5 | template: `${__dirname}/src/index.html`, 6 | filename: 'index.html', 7 | inject: 'body', 8 | }); 9 | 10 | module.exports = { 11 | entry: [ 12 | './src/index.jsx', 13 | ], 14 | output: { 15 | path: `${__dirname}`, 16 | filename: 'index_bundle.js', 17 | }, 18 | module: { 19 | preLoaders: [{ 20 | test: /\.jsx$|\.js$/, 21 | loader: 'eslint-loader', 22 | include: `${__dirname}/src`, 23 | exclude: /bundle\.js$/ 24 | }], 25 | loaders: [{ 26 | test: /\.js$|\.jsx$/, 27 | exclude: /node_modules/, 28 | loader: 'babel-loader', 29 | query: { 30 | presets: ['es2015', 'react'], 31 | }, 32 | }, { 33 | test: /\.scss$/, 34 | loaders: ["style", "css", "sass"] 35 | }], 36 | }, 37 | devServer: { 38 | inline: true, 39 | port: 8008, 40 | }, 41 | plugins: [HTMLWebpackPluginConfig], 42 | }; 43 | -------------------------------------------------------------------------------- /reactjs-php-isomorphic-example/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /reactjs-php-isomorphic-example/README.md: -------------------------------------------------------------------------------- 1 | # react-php-isomorphic-example 2 | 3 | 部屬 4 | --- 5 | **安裝相依套件 & 打包 components** 6 | ``` 7 | npm install 8 | npm run watch 9 | ``` 10 | **運行 http server (via PHP)** 11 | ``` 12 | php -S 0.0.0.0:8080 -t . 13 | ``` 14 | `http://0.0.0.0:8080/` 15 | 16 | 環境 17 | --- 18 | * PHP 5.5.9 19 | * V8Js 0.1.3 (PHP extension) 20 | 21 | 參考 22 | --- 23 | * [v8js](https://github.com/phpv8/v8js) 24 | * [How To Install PHP-V8JS in Ubuntu 16.04](https://www.infinitastech.com/blog/php/how-to-install-php-v8js-in-ubuntu-16-04/) 25 | * Install in Windows (Download) 26 | * [PHP 5.5.9](http://windows.php.net/downloads/releases/archives/php-5.5.9-nts-Win32-VC11-x86.zip) 27 | * [V8Js 0.1.3](http://windows.php.net/downloads/pecl/snaps/v8js/0.1.3/php_v8js-0.1.3-5.5-nts-vc11-x86.zip) 28 | * Example: [easylearntutorial/react-js/server-rendering-php](https://github.com/formigone/easylearntutorial/tree/master/react-js/server-rendering-php) -------------------------------------------------------------------------------- /reactjs-php-isomorphic-example/build/.gitignore: -------------------------------------------------------------------------------- 1 | *.js -------------------------------------------------------------------------------- /reactjs-php-isomorphic-example/index.php: -------------------------------------------------------------------------------- 1 | [ 5 | [1,2,3], 6 | [4,5,6], 7 | [7,8,9] 8 | ] 9 | ]; 10 | $propsJson = json_encode($props); 11 | $react = [ 12 | file_get_contents(__DIR__.'/node_modules/react/dist/react.min.js'), 13 | file_get_contents(__DIR__.'/node_modules/react-dom/dist/react-dom-server.min.js'), 14 | file_get_contents(__DIR__.'/build/app.js'), 15 | 'ReactDOMServer.renderToString(React.createElement(App, ' . $propsJson . '))' 16 | ]; 17 | 18 | try { 19 | $reactStr = $v8->executeString(implode(PHP_EOL, $react)); 20 | } catch (Exception $e) { 21 | echo '

    ', $e->getMessage(), '

    '; 22 | echo '
    ', $e->getTraceAsString(), '
    '; 23 | exit; 24 | } 25 | 26 | ?> 27 | 28 | 29 | 30 | React page 31 | 32 | 33 | 34 |
    35 | 36 | 37 | 38 | 39 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /reactjs-php-isomorphic-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reactjs-php-isomorphic-example", 3 | "version": "0.0.1", 4 | "scripts": { 5 | "watch": "webpack --progress --colors --watch" 6 | }, 7 | "dependencies": { 8 | "react": "^15.3.2", 9 | "react-dom": "^15.3.2" 10 | }, 11 | "devDependencies": { 12 | "babel-cli": "~6.18.0", 13 | "babel-core": "^6.17.0", 14 | "babel-loader": "~6.2.9", 15 | "babel-preset-es2015": "^6.16.0", 16 | "babel-preset-react": "^6.16.0", 17 | "webpack": "~1.14.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /reactjs-php-isomorphic-example/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Table from './Table' 3 | import Heading from './Heading' 4 | 5 | const App = ({data}) => ( 6 |
    7 | 8 | 9 | 10 | ) 11 | 12 | global.App = App 13 | -------------------------------------------------------------------------------- /reactjs-php-isomorphic-example/src/Heading.js: -------------------------------------------------------------------------------- 1 | const Heading = () => (

    Hello world!!

    ) 2 | 3 | export default Heading 4 | -------------------------------------------------------------------------------- /reactjs-php-isomorphic-example/src/Table.js: -------------------------------------------------------------------------------- 1 | const Table = ({data}) => ( 2 |
    3 | {data.map((row, i) => ( 4 | 5 | {row.map((cell, j) => ( 6 | 7 | ))} 8 | 9 | ))} 10 |
    {cell}
    11 | ) 12 | 13 | export default Table 14 | -------------------------------------------------------------------------------- /reactjs-php-isomorphic-example/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: "./src/App.js", 3 | output: { 4 | path: "./build", 5 | filename: "app.js" 6 | }, 7 | module: { 8 | loaders: [{ 9 | test: /\.js$|\.jsx$/, 10 | exclude: /node_modules/, 11 | loader: 'babel-loader', 12 | query: { 13 | presets: ['es2015', 'react'], 14 | }, 15 | }] 16 | }, 17 | externals: { 18 | react: 'React', 19 | 'react-dom': 'ReactDOM' 20 | }, 21 | resolve: { 22 | extensions: ['', '.js', '.jsx'] 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /reactjs-redux-order-practice/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "react", 5 | ], 6 | "plugins": [] 7 | } 8 | -------------------------------------------------------------------------------- /reactjs-redux-order-practice/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "rules": { 4 | "no-multiple-empty-lines": [2, {"max": 2, "maxEOF": 2}] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /reactjs-redux-order-practice/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /reactjs-redux-order-practice/db/category.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "id": 1, 3 | "name": "\u4e3b\u98df", 4 | "user": "rz", 5 | "is_active": 1, 6 | "created_at": "2016-06-09 20:51:39", 7 | "updated_at": "2016-06-09 20:51:39" 8 | }, { 9 | "id": 2, 10 | "name": "\u6e6f\u54c1", 11 | "user": "rz", 12 | "is_active": 1, 13 | "created_at": "2016-06-09 20:51:39", 14 | "updated_at": "2016-06-09 20:51:39" 15 | }, { 16 | "id": 3, 17 | "name": "\u5c0f\u9ede", 18 | "user": "rz", 19 | "is_active": 1, 20 | "created_at": "2016-06-09 20:51:39", 21 | "updated_at": "2016-06-09 20:51:39" 22 | }] 23 | -------------------------------------------------------------------------------- /reactjs-redux-order-practice/db/tables.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "id": 1, 3 | "name": "一號桌" 4 | }, { 5 | "id": 2, 6 | "name": "二號桌" 7 | }, { 8 | "id": 3, 9 | "name": "三號桌" 10 | }, { 11 | "id": 4, 12 | "name": "四號桌" 13 | }, { 14 | "id": 5, 15 | "name": "五號桌" 16 | }, { 17 | "id": 6, 18 | "name": "六號桌" 19 | }] 20 | -------------------------------------------------------------------------------- /reactjs-redux-order-practice/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | POS Order - ReactJS + Redux 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
    19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /reactjs-redux-order-practice/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reactjs-redux-practice", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "webpack-dev-server --colors --inline --content-base .", 8 | "prod": "webpack -p" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "babel-polyfill": "^6.16.0", 14 | "isomorphic-fetch": "^2.2.1", 15 | "react": "^15.3.2", 16 | "react-dom": "^15.3.2", 17 | "react-redux": "^4.4.5", 18 | "redux": "^3.6.0", 19 | "redux-actions": "^0.12.0", 20 | "redux-immutable": "^3.0.8", 21 | "redux-logger": "^2.6.1", 22 | "redux-thunk": "^2.1.0" 23 | }, 24 | "devDependencies": { 25 | "babel-core": "^6.17.0", 26 | "babel-eslint": "^7.0.0", 27 | "babel-loader": "^6.2.5", 28 | "babel-preset-es2015": "^6.16.0", 29 | "babel-preset-react": "^6.16.0", 30 | "css-loader": "^0.25.0", 31 | "eslint": "^3.7.1", 32 | "eslint-config-airbnb": "^12.0.0", 33 | "eslint-loader": "^1.5.0", 34 | "eslint-plugin-import": "^1.16.0", 35 | "eslint-plugin-jsx-a11y": "^2.2.3", 36 | "eslint-plugin-react": "^6.4.1", 37 | "html-webpack-plugin": "^2.22.0", 38 | "node-sass": "^3.10.1", 39 | "sass-loader": "^4.0.2", 40 | "style-loader": "^0.13.1", 41 | "webpack": "^1.13.2", 42 | "webpack-dev-server": "^1.16.2" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /reactjs-redux-order-practice/readme.md: -------------------------------------------------------------------------------- 1 | # POS Order 2 | 3 | ## 概觀 4 | 練習以 ReactJS + Redux 實作 POS 點單介面 5 | 6 | 7 | ## 開發 8 | 1. 套件相依安裝 `npm install` 9 | 2. 運行服務 `npm start` 10 | 3. 開啟瀏覽器 11 | 12 | ``` 13 | http://localhost:8008 14 | ``` 15 | 16 | ## 產出 js & html 17 | 運行 `npm run prod` 18 | 19 | > 產出的 js & html file 會在根目錄下 20 | 21 | 22 | ## 參考 23 | * https://chentsulin.github.io/redux/index.html -------------------------------------------------------------------------------- /reactjs-redux-order-practice/src/actions/data.js: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-fetch' 2 | import { 3 | FETCH_CATEGORY_DATA, 4 | FETCH_ITEM_DATA, 5 | FETCH_TABLE_DATA 6 | } from '../constants/ActionTypes' 7 | 8 | export function fetchCategoryData() { 9 | return dispatch => { 10 | return fetch('./db/category.json') 11 | .then(response => response.json()) 12 | .then(json => dispatch({ 13 | type: FETCH_CATEGORY_DATA, 14 | payload: json 15 | })) 16 | } 17 | } 18 | 19 | export function fetchItemData() { 20 | return dispatch => { 21 | return fetch('./db/item.json') 22 | .then(response => response.json()) 23 | .then(json => dispatch({ 24 | type: FETCH_ITEM_DATA, 25 | payload: json 26 | })) 27 | } 28 | } 29 | 30 | export function fetchTableData() { 31 | return dispatch => { 32 | return fetch('./db/tables.json') 33 | .then(response => response.json()) 34 | .then(json => dispatch({ 35 | type: FETCH_TABLE_DATA, 36 | payload: json 37 | })) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /reactjs-redux-order-practice/src/actions/ui/Order/categories.js: -------------------------------------------------------------------------------- 1 | import { 2 | createAction 3 | } from 'redux-actions' 4 | import { 5 | CHANGE_CATEGORY 6 | } from '../../../constants/ActionTypes' 7 | 8 | export const changeCategoryBy = createAction(CHANGE_CATEGORY) 9 | -------------------------------------------------------------------------------- /reactjs-redux-order-practice/src/actions/ui/Order/items.js: -------------------------------------------------------------------------------- 1 | import { 2 | createAction 3 | } from 'redux-actions' 4 | import { 5 | UPDATE_MENU_STYLE 6 | } from '../../../constants/ActionTypes' 7 | 8 | export const updateMenuStyle = createAction(UPDATE_MENU_STYLE) 9 | -------------------------------------------------------------------------------- /reactjs-redux-order-practice/src/actions/ui/Order/order.js: -------------------------------------------------------------------------------- 1 | import { 2 | createAction 3 | } from 'redux-actions' 4 | import { 5 | ADD_ITEM_TO_ORDER, 6 | REMOVE_ITEM_FROM_ORDER, 7 | ADD_ADDITION_TO_ORDER, 8 | UPADTE_ADDITION, 9 | REMOVE_ADDITION_FROM_ORDER, 10 | UPDATE_ORDER_STYLE, 11 | TOGGLE_TOGO_STATUS, 12 | TOGGLE_WHEN_STATUS, 13 | SUBMIT_ORDER 14 | } from '../../../constants/ActionTypes' 15 | 16 | export const addItemToOrder = createAction(ADD_ITEM_TO_ORDER) 17 | export const removeItemFromOrder = createAction(REMOVE_ITEM_FROM_ORDER) 18 | export const addAdditionToOrder = createAction(ADD_ADDITION_TO_ORDER) 19 | export const updateAddition = createAction(UPADTE_ADDITION) 20 | export const removeAdditionFromOrder = createAction(REMOVE_ADDITION_FROM_ORDER) 21 | export const updateOrderStyle = createAction(UPDATE_ORDER_STYLE) 22 | export const toggleToGoStatus = createAction(TOGGLE_TOGO_STATUS) 23 | export const toggleWhenStatus = createAction(TOGGLE_WHEN_STATUS) 24 | export const sumbitOrder = createAction(SUBMIT_ORDER) 25 | -------------------------------------------------------------------------------- /reactjs-redux-order-practice/src/actions/ui/Order/pages.js: -------------------------------------------------------------------------------- 1 | import { 2 | createAction 3 | } from 'redux-actions' 4 | import { 5 | CHANGE_MENU_PAGE, 6 | RESET_MENU_PAGE 7 | } from '../../../constants/ActionTypes' 8 | 9 | export const changeMenuPageBy = createAction(CHANGE_MENU_PAGE) 10 | export const resetMenuPage = createAction(RESET_MENU_PAGE) 11 | -------------------------------------------------------------------------------- /reactjs-redux-order-practice/src/components/Order/OrderContent.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react' 2 | import {getOrderContentStyle} from '../../helpers' 3 | import OrderContentDetail from '../../containers/Order/OrderContentDetail' 4 | import OrderContentSubmit from '../../containers/Order/OrderContentSubmit' 5 | 6 | class OrderContent extends Component { 7 | componentDidMount() { 8 | // item 動態列高 9 | const {updateOrderStyle} = this.props 10 | updateOrderStyle(getOrderContentStyle()) 11 | window.addEventListener('resize', () => updateOrderStyle(getOrderContentStyle())) 12 | } 13 | 14 | componentWillUnmount() { 15 | const {updateOrderStyle} = this.props 16 | window.removeEventListener('resize', () => updateOrderStyle(getOrderContentStyle())) 17 | } 18 | 19 | render() { 20 | const {orderContentStyle} = this.props 21 | return ( 22 |
    23 |
    24 |
    點單明細
    25 |
    26 |
    27 | 28 |
    29 |
    30 | 31 |
    32 |
    33 |
    34 |
    35 | ) 36 | } 37 | } 38 | OrderContent.propTypes = { 39 | orderContentStyle: PropTypes.object.isRequired, 40 | updateOrderStyle: PropTypes.func.isRequired 41 | } 42 | 43 | export default OrderContent 44 | -------------------------------------------------------------------------------- /reactjs-redux-order-practice/src/components/Order/OrderContentDetail.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react' 2 | 3 | const OrderContentDetail = ({items, additions, removeItemFromOrder, removeAdditionFromOrder, updateAddition}) => ( 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | {items.map((item) => ( 16 | 17 | 18 | 19 | 20 | 21 | 26 | 27 | ))} 28 | {additions.map((addition) => ( 29 | 30 | 31 | 32 | 33 | 34 | 39 | 40 | ))} 41 | 42 |
    品項數量單價小計
    {item.name}{item.amount}{item.price}{item.subtotal} 22 | 25 |
    updateAddition(Object.assign({}, addition, {name: e.target.value}))}/> updateAddition(Object.assign({}, addition, {amount: e.target.value}))}/> updateAddition(Object.assign({}, addition, {price: e.target.value}))}/>{addition.subtotal} 35 | 38 |
    43 | ) 44 | OrderContentDetail.propTypes = { 45 | items: PropTypes.array.isRequired, 46 | additions: PropTypes.array.isRequired, 47 | removeItemFromOrder: PropTypes.func.isRequired, 48 | removeAdditionFromOrder: PropTypes.func.isRequired, 49 | updateAddition: PropTypes.func.isRequired 50 | } 51 | 52 | export default OrderContentDetail 53 | -------------------------------------------------------------------------------- /reactjs-redux-order-practice/src/components/Order/OrderContentSubmit.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react' 2 | 3 | const OrderContentSubmit = ({ 4 | order, 5 | isToGo, 6 | when, 7 | tables, 8 | total, 9 | addAdditionToOrder, 10 | toggleToGoStatus, 11 | toggleWhenStatus, 12 | sumbitOrder 13 | }) => ( 14 |
    15 |
    16 |
    17 | {/* 加點 */} 18 |
    19 | 24 |
    25 | {/* 外帶 */} 26 |
    27 | 38 |
    39 | {/* 時段 */} 40 |
    41 | 50 |
    51 | {/* 桌號 */} 52 |
    53 | 66 |
    67 |
    68 |
    69 |
    70 | {/* 送單 */} 71 | 76 |
    77 |
    78 | {/* 總計 */} 79 |
    80 |
    81 | 總計 82 |
    83 |
    84 | {total} 85 |
    86 |
    87 |
    88 |
    89 | ) 90 | OrderContentSubmit.propTypes = { 91 | order: PropTypes.array.isRequired, 92 | isToGo: PropTypes.bool.isRequired, 93 | when: PropTypes.string.isRequired, 94 | tables: PropTypes.array.isRequired, 95 | total: PropTypes.number.isRequired, 96 | addAdditionToOrder: PropTypes.func.isRequired, 97 | toggleToGoStatus: PropTypes.func.isRequired, 98 | toggleWhenStatus: PropTypes.func.isRequired, 99 | sumbitOrder: PropTypes.func.isRequired 100 | } 101 | 102 | export default OrderContentSubmit 103 | -------------------------------------------------------------------------------- /reactjs-redux-order-practice/src/components/Order/OrderMenu.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import OrderMenuCategory from '../../containers/Order/OrderMenuCategory' 3 | import OrderMenuItem from '../../containers/Order/OrderMenuItem' 4 | import OrderMenuPager from '../../containers/Order/OrderMenuPager' 5 | 6 | const OrderMenu = () => ( 7 |
    8 | 9 | 10 | 11 |
    12 | ) 13 | 14 | export default OrderMenu 15 | -------------------------------------------------------------------------------- /reactjs-redux-order-practice/src/components/Order/OrderMenuCategory.jsx: -------------------------------------------------------------------------------- 1 | import React, {PropTypes} from 'react' 2 | 3 | const OrderMenuCategory = ({categories, selectedId, changeCategoryBy, resetMenuPage}) => ( 4 |
    5 |
      6 | {categories.map((category) => ( 7 |
    • changeCategoryBy(category.id) && resetMenuPage()}> 10 | 11 |

      {category.name}

      12 |
      13 |
    • 14 | ))} 15 |
    16 |
    17 | ) 18 | OrderMenuCategory.propTypes = { 19 | categories: PropTypes.array.isRequired, 20 | selectedId: PropTypes.number.isRequired, 21 | changeCategoryBy: PropTypes.func.isRequired, 22 | resetMenuPage: PropTypes.func.isRequired 23 | } 24 | 25 | export default OrderMenuCategory 26 | -------------------------------------------------------------------------------- /reactjs-redux-order-practice/src/components/Order/OrderMenuItem.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react' 2 | import {getItemRowStyle} from '../../helpers' 3 | 4 | class OrderMenuItem extends Component { 5 | componentDidMount() { 6 | // item 動態列高 7 | const {updateMenuStyle} = this.props 8 | updateMenuStyle(getItemRowStyle()) 9 | window.addEventListener('resize', () => updateMenuStyle(getItemRowStyle())) 10 | } 11 | 12 | componentWillUnmount() { 13 | const {updateMenuStyle} = this.props 14 | window.removeEventListener('resize', () => updateMenuStyle(getItemRowStyle())) 15 | } 16 | 17 | render() { 18 | const {items, itemRowStyle, addItemToOrder} = this.props 19 | return ( 20 |
    21 |
    22 | {items.map((item) => ( 23 |
    24 |
    addItemToOrder(item)}> 25 |
    {item.name}
    26 |
    27 |
    28 | ))} 29 |
    30 |
    31 | ) 32 | } 33 | } 34 | OrderMenuItem.propTypes = { 35 | items: PropTypes.array.isRequired, 36 | itemRowStyle: PropTypes.object.isRequired, 37 | updateMenuStyle: PropTypes.func.isRequired 38 | } 39 | 40 | export default OrderMenuItem 41 | -------------------------------------------------------------------------------- /reactjs-redux-order-practice/src/components/Order/OrderMenuPager.jsx: -------------------------------------------------------------------------------- 1 | import React, {PropTypes} from 'react' 2 | 3 | const OrderMenuPager = ({pagesCount, currentPage, changeMenuPageBy}) => { 4 | const hasNextPage = pagesCount - currentPage > 1 // 是否還有下一頁 5 | const hasPrevPage = currentPage - 1 > -1 // 是否還有上一頁 6 | const nextPage = hasNextPage 7 | ? currentPage + 1 8 | : currentPage 9 | const prevPage = hasPrevPage 10 | ? currentPage - 1 11 | : currentPage 12 | 13 | return ( 14 |
    15 |
    16 |
    17 | 22 |
    23 |
    24 | 29 |
    30 |
    31 |
    32 | ) 33 | } 34 | OrderMenuPager.propTypes = { 35 | pagesCount: PropTypes.number.isRequired, 36 | currentPage: PropTypes.number.isRequired, 37 | changeMenuPageBy: PropTypes.func.isRequired 38 | } 39 | 40 | export default OrderMenuPager 41 | -------------------------------------------------------------------------------- /reactjs-redux-order-practice/src/constants/ActionTypes.js: -------------------------------------------------------------------------------- 1 | /* categories */ 2 | export const FETCH_CATEGORY_DATA = 'FETCH_CATEGORY_DATA' 3 | export const CHANGE_CATEGORY = 'CHANGE_CATEGORY' 4 | 5 | /* items */ 6 | export const FETCH_ITEM_DATA = 'FETCH_ITEM_DATA' 7 | export const UPDATE_MENU_STYLE = 'UPDATE_MENU_STYLE' 8 | 9 | /* order */ 10 | export const ADD_ITEM_TO_ORDER = 'ADD_ITEM_TO_ORDER' 11 | export const REMOVE_ITEM_FROM_ORDER = 'REMOVE_ITEM_FROM_ORDER' 12 | 13 | export const ADD_ADDITION_TO_ORDER = 'ADD_ADDITION_TO_ORDER' 14 | export const REMOVE_ADDITION_FROM_ORDER = 'REMOVE_ADDITION_FROM_ORDER' 15 | export const UPADTE_ADDITION = 'UPADTE_ADDITION' 16 | 17 | export const UPDATE_ORDER_STYLE = 'UPDATE_ORDER_STYLE' 18 | export const TOGGLE_TOGO_STATUS = 'TOGGLE_TOGO_STATUS' 19 | export const TOGGLE_WHEN_STATUS = 'TOGGLE_WHEN_STATUS' 20 | export const SUBMIT_ORDER = 'SUBMIT_ORDER' 21 | 22 | /* pages */ 23 | export const CHANGE_MENU_PAGE = 'CHANGE_MENU_PAGE' 24 | export const RESET_MENU_PAGE = 'RESET_MENU_PAGE' 25 | 26 | /* tables */ 27 | export const FETCH_TABLE_DATA = 'FETCH_TABLE_DATA' 28 | -------------------------------------------------------------------------------- /reactjs-redux-order-practice/src/constants/OrderConstants.js: -------------------------------------------------------------------------------- 1 | const now = new Date(); 2 | const nightStart = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 17, 30, 0, 0); 3 | const whenStatus = (now.getTime() - nightStart.getTime() < 0) ? '中午' : '晚上'; 4 | 5 | const OrderConstants = { 6 | // parameters 7 | itemsPerPageAmount: 12, // 每頁菜單項目數 8 | menuItemListRows: 3, // 菜單列數 9 | menuItemRowMarginHeight: 30, // 菜單列與列之間 margin 高 10 | menuCategoryListHeight: 87, // 菜單分類 DOM 高 11 | menuPagerHeight: 62, // 菜單分頁 DOM 高 12 | contentDetailHeadingHeight: 72, // "點單明細" 高 13 | contentOperationHeight: 133, // order 操作區域高 14 | // store default value 15 | whenStatus: whenStatus // 菜單 中午/晚上的狀態 16 | }; 17 | 18 | export default OrderConstants; 19 | -------------------------------------------------------------------------------- /reactjs-redux-order-practice/src/containers/App.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react' 2 | import {connect} from 'react-redux' 3 | import {fetchCategoryData, fetchItemData, fetchTableData} from '../actions/data' 4 | import OrderMenu from '../components/Order/OrderMenu.jsx' 5 | import OrderContent from '../containers/Order/OrderContent' 6 | 7 | class App extends Component { 8 | componentDidMount() { 9 | const {fetchCategoryData, fetchItemData, fetchTableData} = this.props 10 | fetchCategoryData() 11 | fetchItemData() 12 | fetchTableData() 13 | } 14 | 15 | render() { 16 | return ( 17 |
    18 |
    19 |
    20 |
    21 | {/* 左側欄 */} 22 | {/* 右側欄 */} 23 |
    24 |
    25 |
    26 |
    27 | 28 | ) 29 | } 30 | } 31 | App.propTypes = { 32 | fetchCategoryData: PropTypes.func.isRequired, 33 | fetchItemData: PropTypes.func.isRequired, 34 | fetchTableData: PropTypes.func.isRequired 35 | } 36 | 37 | function mapDispatchToProps(dispatch) { 38 | return { 39 | fetchCategoryData: () => dispatch(fetchCategoryData()), 40 | fetchItemData: () => dispatch(fetchItemData()), 41 | fetchTableData: () => dispatch(fetchTableData()) 42 | } 43 | } 44 | 45 | export default connect(undefined, mapDispatchToProps)(App) 46 | -------------------------------------------------------------------------------- /reactjs-redux-order-practice/src/containers/Order/OrderContent.js: -------------------------------------------------------------------------------- 1 | import { 2 | connect 3 | } from 'react-redux' 4 | import {updateOrderStyle} from '../../actions/ui/Order/order' 5 | import OrderContent from '../../components/Order/OrderContent.jsx' 6 | 7 | function mapStateToProps(state) { 8 | return { 9 | orderContentStyle: state.order.orderContentStyle 10 | } 11 | } 12 | 13 | function mapDispatchToProps(dispatch) { 14 | return { 15 | updateOrderStyle: (style) => dispatch(updateOrderStyle(style)) 16 | } 17 | } 18 | 19 | export default connect(mapStateToProps,mapDispatchToProps)(OrderContent) 20 | -------------------------------------------------------------------------------- /reactjs-redux-order-practice/src/containers/Order/OrderContentDetail.js: -------------------------------------------------------------------------------- 1 | import { 2 | connect 3 | } from 'react-redux' 4 | import { 5 | removeItemFromOrder, 6 | removeAdditionFromOrder, 7 | updateAddition 8 | } from '../../actions/ui/Order/order' 9 | import OrderContentDetail from '../../components/Order/OrderContentDetail.jsx' 10 | 11 | function mapStateToProps(state) { 12 | return { 13 | items: state.order.items, 14 | additions: state.order.additions 15 | } 16 | } 17 | 18 | function mapDispatchToProps(dispatch) { 19 | return { 20 | removeItemFromOrder: (item) => dispatch(removeItemFromOrder(item)), 21 | removeAdditionFromOrder: (addition) => dispatch(removeAdditionFromOrder(addition)), 22 | updateAddition: (addition) => dispatch(updateAddition(addition)) 23 | } 24 | } 25 | 26 | export default connect(mapStateToProps, mapDispatchToProps)(OrderContentDetail) 27 | -------------------------------------------------------------------------------- /reactjs-redux-order-practice/src/containers/Order/OrderContentSubmit.js: -------------------------------------------------------------------------------- 1 | import { 2 | connect 3 | } from 'react-redux' 4 | import { 5 | addAdditionToOrder, 6 | toggleToGoStatus, 7 | toggleWhenStatus, 8 | sumbitOrder 9 | } from '../../actions/ui/Order/order' 10 | import OrderContentSubmit from '../../components/Order/OrderContentSubmit.jsx' 11 | 12 | function mapStateToProps(state) { 13 | return { 14 | order: state.order.items, 15 | isToGo: state.order.isToGo, 16 | when: state.order.when, 17 | tables: state.tables.records, 18 | total: state.order.total 19 | } 20 | } 21 | 22 | function mapDispatchToProps(dispatch) { 23 | return { 24 | addAdditionToOrder: () => dispatch(addAdditionToOrder()), 25 | toggleToGoStatus: () => dispatch(toggleToGoStatus()), 26 | toggleWhenStatus: () => dispatch(toggleWhenStatus()), 27 | sumbitOrder: () => dispatch(sumbitOrder()) 28 | } 29 | } 30 | 31 | export default connect(mapStateToProps,mapDispatchToProps)(OrderContentSubmit) 32 | -------------------------------------------------------------------------------- /reactjs-redux-order-practice/src/containers/Order/OrderMenuCategory.js: -------------------------------------------------------------------------------- 1 | import {connect} from 'react-redux' 2 | import {changeCategoryBy} from '../../actions/ui/Order/categories' 3 | import {resetMenuPage} from '../../actions/ui/Order/pages' 4 | import OrderMenuCategory from '../../components/Order/OrderMenuCategory.jsx' 5 | 6 | function mapStateToProps(state) { 7 | return {categories: state.categories.records, selectedId: state.categories.selectedId} 8 | } 9 | 10 | function mapDispatchToProps(dispatch) { 11 | return { 12 | changeCategoryBy: (index) => dispatch(changeCategoryBy(index)), 13 | resetMenuPage: () => dispatch(resetMenuPage()) 14 | } 15 | } 16 | 17 | export default connect(mapStateToProps, mapDispatchToProps)(OrderMenuCategory) 18 | -------------------------------------------------------------------------------- /reactjs-redux-order-practice/src/containers/Order/OrderMenuItem.js: -------------------------------------------------------------------------------- 1 | import {connect} from 'react-redux' 2 | import {updateMenuStyle} from '../../actions/ui/Order/items' 3 | import {addItemToOrder} from '../../actions/ui/Order/order' 4 | import {filterItemsBy} from '../../helpers' 5 | import OrderMenuItem from '../../components/Order/OrderMenuItem.jsx' 6 | 7 | function mapStateToProps(state) { 8 | return { 9 | items: filterItemsBy(state.items.records, state.categories.selectedId, state.pages.currentPage), 10 | itemRowStyle: state.items.itemRowStyle 11 | } 12 | } 13 | 14 | function mapDispatchToProps(dispatch) { 15 | return { 16 | updateMenuStyle: (style) => dispatch(updateMenuStyle(style)), 17 | addItemToOrder: (item) => dispatch(addItemToOrder(item)) 18 | } 19 | } 20 | 21 | export default connect(mapStateToProps, mapDispatchToProps)(OrderMenuItem) 22 | -------------------------------------------------------------------------------- /reactjs-redux-order-practice/src/containers/Order/OrderMenuPager.js: -------------------------------------------------------------------------------- 1 | import {connect} from 'react-redux' 2 | import {changeMenuPageBy} from '../../actions/ui/Order/pages' 3 | import {countMenuPage} from '../../helpers' 4 | import OrderMenuPager from '../../components/Order/OrderMenuPager.jsx' 5 | 6 | function mapStateToProps(state) { 7 | return { 8 | pagesCount: countMenuPage(state.items.records, state.categories.selectedId), 9 | currentPage: state.pages.currentPage 10 | } 11 | } 12 | 13 | function mapDispatchToProps(dispatch) { 14 | return { 15 | changeMenuPageBy: (index) => dispatch(changeMenuPageBy(index)) 16 | } 17 | } 18 | 19 | export default connect(mapStateToProps, mapDispatchToProps)(OrderMenuPager) 20 | -------------------------------------------------------------------------------- /reactjs-redux-order-practice/src/helpers/index.js: -------------------------------------------------------------------------------- 1 | import OrderConstants from '../constants/OrderConstants' 2 | 3 | export function countMenuPage(items, categoryId) { 4 | const _items = items.filter((item) => item.category_id == categoryId) 5 | return Math.ceil(_items.length / OrderConstants.itemsPerPageAmount) 6 | } 7 | 8 | export function filterItemsBy(items, categoryId, pageId) { 9 | const _items = items.filter(item => item.category_id == categoryId) 10 | return _items.filter((record, i) => { 11 | return OrderConstants.itemsPerPageAmount * (pageId) <= i && i < OrderConstants.itemsPerPageAmount * (1 + pageId) 12 | }) 13 | } 14 | 15 | export function getItemRowStyle() { 16 | const bodyHeight = document.body.clientHeight 17 | const rowHeight = (bodyHeight - OrderConstants.menuItemRowMarginHeight - OrderConstants.menuCategoryListHeight - OrderConstants.menuPagerHeight) / OrderConstants.menuItemListRows 18 | return { 19 | height: rowHeight + 'px' 20 | } 21 | } 22 | 23 | export function getOrderContentStyle() { 24 | const bodyHeight = document.body.clientHeight; 25 | const contentHeight = (bodyHeight - OrderConstants.contentDetailHeadingHeight - OrderConstants.contentOperationHeight); 26 | return { 27 | height: contentHeight + 'px' 28 | } 29 | } 30 | 31 | export function calculateTotalBy(items, additions) { 32 | let total = 0; 33 | items.map((record) => total += record.subtotal) 34 | additions.map((record) => total += record.subtotal) 35 | return total 36 | } 37 | -------------------------------------------------------------------------------- /reactjs-redux-order-practice/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | POS Order - ReactJS + Redux 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
    19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /reactjs-redux-order-practice/src/index.jsx: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill' 2 | 3 | import React from 'react' 4 | import {render} from 'react-dom' 5 | import {Provider} from 'react-redux' 6 | import configureStore from './store/configureStore' 7 | import App from './containers/App.jsx' 8 | 9 | require("!style!css!sass!./styles/entry.scss") 10 | 11 | const store = configureStore() 12 | 13 | render( 14 | 15 | 16 | , document.getElementById('app')) 17 | -------------------------------------------------------------------------------- /reactjs-redux-order-practice/src/reducers/Order/categories.js: -------------------------------------------------------------------------------- 1 | import { 2 | handleActions 3 | } from 'redux-actions' 4 | import { 5 | FETCH_CATEGORY_DATA, 6 | CHANGE_CATEGORY 7 | } from '../../constants/ActionTypes' 8 | 9 | export default handleActions({ 10 | FETCH_CATEGORY_DATA: (state, { 11 | payload 12 | }) => { 13 | return Object.assign({}, state, { 14 | isFetched: true, 15 | selectedId: payload[0].id, 16 | records: payload, 17 | receivedAt: Date.now() 18 | }) 19 | }, 20 | CHANGE_CATEGORY: (state, { 21 | payload 22 | }) => { 23 | return Object.assign({}, state, { 24 | selectedId: payload 25 | }) 26 | } 27 | }, { 28 | isFetched: false, 29 | selectedId: -1, 30 | records: [] 31 | }) 32 | -------------------------------------------------------------------------------- /reactjs-redux-order-practice/src/reducers/Order/items.js: -------------------------------------------------------------------------------- 1 | import { 2 | handleActions 3 | } from 'redux-actions' 4 | import { 5 | FETCH_ITEM_DATA, 6 | UPDATE_MENU_STYLE 7 | } from '../../constants/ActionTypes' 8 | 9 | export default handleActions({ 10 | FETCH_ITEM_DATA: (state, { 11 | payload 12 | }) => { 13 | return Object.assign({}, state, { 14 | isFetched: true, 15 | records: payload, 16 | receivedAt: Date.now() 17 | }) 18 | }, 19 | UPDATE_MENU_STYLE: (state, { 20 | payload 21 | }) => { 22 | return Object.assign({}, state, { 23 | itemRowStyle: payload 24 | }) 25 | } 26 | }, { 27 | isFetched: false, 28 | itemRowStyle: { 29 | height: 'inherit' 30 | }, 31 | records: [] 32 | }) 33 | -------------------------------------------------------------------------------- /reactjs-redux-order-practice/src/reducers/Order/order.js: -------------------------------------------------------------------------------- 1 | import { 2 | handleActions 3 | } from 'redux-actions' 4 | import { 5 | ADD_ITEM_TO_ORDER, 6 | REMOVE_ITEM_FROM_ORDER, 7 | ADD_ADDITION_TO_ORDER, 8 | REMOVE_ADDITION_FROM_ORDER, 9 | UPADTE_ADDITION, 10 | UPDATE_ORDER_STYLE, 11 | TOGGLE_TOGO_STATUS, 12 | TOGGLE_WHEN_STATUS, 13 | SUBMIT_ORDER 14 | } from '../../constants/ActionTypes' 15 | import { 16 | calculateTotalBy 17 | } from '../../helpers' 18 | import OrderConstants from '../../constants/OrderConstants' 19 | 20 | export default handleActions({ 21 | ADD_ITEM_TO_ORDER: (state, { 22 | payload 23 | }) => { 24 | if (state.items.find((item) => item.id == payload.id)) { 25 | let _items = state.items.map(item => { 26 | if (item.id == payload.id) { 27 | return Object.assign({}, item, { 28 | amount: item.amount + 1, 29 | subtotal: (item.amount + 1) * item.price 30 | }) 31 | } 32 | return item 33 | }) 34 | return Object.assign({}, state, { 35 | items: _items, 36 | total: calculateTotalBy(_items, state.additions) 37 | }) 38 | } 39 | let _items = [...state.items, { 40 | id: payload.id, 41 | name: payload.name, 42 | amount: 1, 43 | price: payload.price, 44 | subtotal: payload.price 45 | }] 46 | return Object.assign({}, state, { 47 | items: _items, 48 | total: calculateTotalBy(_items, state.additions) 49 | }) 50 | }, 51 | REMOVE_ITEM_FROM_ORDER: (state, { 52 | payload 53 | }) => { 54 | let _items = state.items.map(item => { 55 | if (item.id == payload.id) { 56 | return Object.assign({}, item, { 57 | amount: item.amount - 1, 58 | subtotal: (item.amount - 1) * item.price 59 | }) 60 | } 61 | return item 62 | }).filter((item) => item.amount > 0) 63 | return Object.assign({}, state, { 64 | items: _items, 65 | total: calculateTotalBy(_items, state.additions) 66 | }) 67 | }, 68 | ADD_ADDITION_TO_ORDER: (state) => { 69 | return Object.assign({}, state, { 70 | additions: [...state.additions, { 71 | id: Date.now(), 72 | name: '', 73 | amount: 1, 74 | price: 0, 75 | subtotal: 0 76 | }] 77 | }) 78 | }, 79 | REMOVE_ADDITION_FROM_ORDER: (state, { 80 | payload 81 | }) => { 82 | let _additions = state.additions.filter((addition) => addition.id !== payload.id) 83 | return Object.assign({}, state, { 84 | additions: _additions, 85 | total: calculateTotalBy(state.items, _additions) 86 | }) 87 | }, 88 | UPADTE_ADDITION: (state, { 89 | payload 90 | }) => { 91 | let _additions = state.additions.map(addition => { 92 | if (addition.id == payload.id) { 93 | return Object.assign({}, addition, { 94 | id: payload.id, 95 | name: payload.name, 96 | amount: payload.amount, 97 | price: payload.price, 98 | subtotal: payload.amount * payload.price 99 | }) 100 | } 101 | return addition 102 | }) 103 | return Object.assign({}, state, { 104 | additions: _additions, 105 | total: calculateTotalBy(state.items, _additions) 106 | }) 107 | }, 108 | TOGGLE_TOGO_STATUS: (state) => { 109 | return Object.assign({}, state, { 110 | isToGo: !state.isToGo, 111 | }) 112 | }, 113 | TOGGLE_WHEN_STATUS: (state) => { 114 | return Object.assign({}, state, { 115 | when: (state.when === '中午') ? '晚上' : '中午', 116 | }) 117 | }, 118 | SUBMIT_ORDER: (state) => { 119 | console.log('--- clear order ---') 120 | return Object.assign({}, state, { 121 | items: [], 122 | additions: [], 123 | total: 0 124 | }) 125 | }, 126 | UPDATE_ORDER_STYLE: (state, { 127 | payload 128 | }) => { 129 | return Object.assign({}, state, { 130 | orderContentStyle: payload 131 | }) 132 | }, 133 | TOGGLE_WHEN_STATUS: (state) => { 134 | return Object.assign({}, state, { 135 | when: (state.when === '中午') ? '晚上' : '中午', 136 | }) 137 | }, 138 | TOGGLE_WHEN_STATUS: (state) => { 139 | return Object.assign({}, state, { 140 | when: (state.when === '中午') ? '晚上' : '中午', 141 | }) 142 | }, 143 | }, { 144 | orderContentStyle: { 145 | height: 'inherit' 146 | }, 147 | items: [], 148 | additions: [], 149 | isToGo: false, 150 | when: OrderConstants.whenStatus, 151 | total: 0 152 | }) 153 | -------------------------------------------------------------------------------- /reactjs-redux-order-practice/src/reducers/Order/pages.js: -------------------------------------------------------------------------------- 1 | import { 2 | handleActions 3 | } from 'redux-actions' 4 | import { 5 | CHANGE_MENU_PAGE, 6 | RESET_MENU_PAGE 7 | } from '../../constants/ActionTypes' 8 | 9 | export default handleActions({ 10 | CHANGE_MENU_PAGE: (state, { 11 | payload 12 | }) => { 13 | return Object.assign({}, state, { 14 | currentPage: payload, 15 | }) 16 | }, 17 | RESET_MENU_PAGE: (state) => { 18 | return Object.assign({}, state, { 19 | currentPage: 0, 20 | }) 21 | } 22 | }, { 23 | currentPage: 0 24 | }) 25 | -------------------------------------------------------------------------------- /reactjs-redux-order-practice/src/reducers/Order/tables.js: -------------------------------------------------------------------------------- 1 | import { 2 | handleActions 3 | } from 'redux-actions' 4 | import { 5 | FETCH_TABLE_DATA 6 | } from '../../constants/ActionTypes' 7 | 8 | export default handleActions({ 9 | FETCH_TABLE_DATA: (state, { 10 | payload 11 | }) => { 12 | return Object.assign({}, state, { 13 | isFetched: true, 14 | records: payload, 15 | receivedAt: Date.now() 16 | }) 17 | } 18 | }, { 19 | isFetched: false, 20 | records: [] 21 | }) 22 | -------------------------------------------------------------------------------- /reactjs-redux-order-practice/src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | combineReducers 3 | } from 'redux' 4 | import categories from './Order/categories' 5 | import items from './Order/items' 6 | import tables from './Order/tables' 7 | import pages from './Order/pages' 8 | import order from './Order/order' 9 | 10 | const rootReducer = combineReducers({ 11 | categories, 12 | items, 13 | tables, 14 | pages, 15 | order 16 | }) 17 | 18 | export default rootReducer 19 | -------------------------------------------------------------------------------- /reactjs-redux-order-practice/src/store/configureStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import thunkMiddleware from 'redux-thunk' 3 | import createLogger from 'redux-logger' 4 | import rootReducer from '../reducers' 5 | 6 | const loggerMiddleware = createLogger() 7 | 8 | export default function configureStore(preloadedState) { 9 | return createStore( 10 | rootReducer, 11 | preloadedState, 12 | applyMiddleware( 13 | thunkMiddleware, 14 | loggerMiddleware 15 | ) 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /reactjs-redux-order-practice/src/store/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './configureStore'; -------------------------------------------------------------------------------- /reactjs-redux-order-practice/src/styles/components/datetime.scss: -------------------------------------------------------------------------------- 1 | .datetime { 2 | .time { 3 | font-family: 'Roboto', sans-serif; 4 | font-size: 24px; 5 | vertical-align: middle; 6 | display: inline-block; 7 | } 8 | .date { 9 | font-family: 'Roboto', sans-serif; 10 | font-size: 16px; 11 | margin-top: 5px; 12 | vertical-align: middle; 13 | display: inline-block; 14 | } 15 | height: 50px; // nav menu bar 的高度 16 | text-align: center; 17 | font-size: 0; 18 | &::before { 19 | content: ''; 20 | height: 100%; 21 | vertical-align: middle; 22 | display: inline-block; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /reactjs-redux-order-practice/src/styles/components/input.scss: -------------------------------------------------------------------------------- 1 | //input[type="number"] { 2 | // width:3em; 3 | //} 4 | -------------------------------------------------------------------------------- /reactjs-redux-order-practice/src/styles/components/loading.scss: -------------------------------------------------------------------------------- 1 | .sk-circle { 2 | margin: 100px auto; 3 | width: 40px; 4 | height: 40px; 5 | position: relative; 6 | } 7 | .sk-circle .sk-child { 8 | width: 100%; 9 | height: 100%; 10 | position: absolute; 11 | left: 0; 12 | top: 0; 13 | } 14 | .sk-circle .sk-child:before { 15 | content: ''; 16 | display: block; 17 | margin: 0 auto; 18 | width: 15%; 19 | height: 15%; 20 | background-color: #333; 21 | border-radius: 100%; 22 | -webkit-animation: sk-circleBounceDelay 1.2s infinite ease-in-out both; 23 | animation: sk-circleBounceDelay 1.2s infinite ease-in-out both; 24 | } 25 | .sk-circle .sk-circle2 { 26 | -webkit-transform: rotate(30deg); 27 | -ms-transform: rotate(30deg); 28 | transform: rotate(30deg); } 29 | .sk-circle .sk-circle3 { 30 | -webkit-transform: rotate(60deg); 31 | -ms-transform: rotate(60deg); 32 | transform: rotate(60deg); } 33 | .sk-circle .sk-circle4 { 34 | -webkit-transform: rotate(90deg); 35 | -ms-transform: rotate(90deg); 36 | transform: rotate(90deg); } 37 | .sk-circle .sk-circle5 { 38 | -webkit-transform: rotate(120deg); 39 | -ms-transform: rotate(120deg); 40 | transform: rotate(120deg); } 41 | .sk-circle .sk-circle6 { 42 | -webkit-transform: rotate(150deg); 43 | -ms-transform: rotate(150deg); 44 | transform: rotate(150deg); } 45 | .sk-circle .sk-circle7 { 46 | -webkit-transform: rotate(180deg); 47 | -ms-transform: rotate(180deg); 48 | transform: rotate(180deg); } 49 | .sk-circle .sk-circle8 { 50 | -webkit-transform: rotate(210deg); 51 | -ms-transform: rotate(210deg); 52 | transform: rotate(210deg); } 53 | .sk-circle .sk-circle9 { 54 | -webkit-transform: rotate(240deg); 55 | -ms-transform: rotate(240deg); 56 | transform: rotate(240deg); } 57 | .sk-circle .sk-circle10 { 58 | -webkit-transform: rotate(270deg); 59 | -ms-transform: rotate(270deg); 60 | transform: rotate(270deg); } 61 | .sk-circle .sk-circle11 { 62 | -webkit-transform: rotate(300deg); 63 | -ms-transform: rotate(300deg); 64 | transform: rotate(300deg); } 65 | .sk-circle .sk-circle12 { 66 | -webkit-transform: rotate(330deg); 67 | -ms-transform: rotate(330deg); 68 | transform: rotate(330deg); } 69 | .sk-circle .sk-circle2:before { 70 | -webkit-animation-delay: -1.1s; 71 | animation-delay: -1.1s; } 72 | .sk-circle .sk-circle3:before { 73 | -webkit-animation-delay: -1s; 74 | animation-delay: -1s; } 75 | .sk-circle .sk-circle4:before { 76 | -webkit-animation-delay: -0.9s; 77 | animation-delay: -0.9s; } 78 | .sk-circle .sk-circle5:before { 79 | -webkit-animation-delay: -0.8s; 80 | animation-delay: -0.8s; } 81 | .sk-circle .sk-circle6:before { 82 | -webkit-animation-delay: -0.7s; 83 | animation-delay: -0.7s; } 84 | .sk-circle .sk-circle7:before { 85 | -webkit-animation-delay: -0.6s; 86 | animation-delay: -0.6s; } 87 | .sk-circle .sk-circle8:before { 88 | -webkit-animation-delay: -0.5s; 89 | animation-delay: -0.5s; } 90 | .sk-circle .sk-circle9:before { 91 | -webkit-animation-delay: -0.4s; 92 | animation-delay: -0.4s; } 93 | .sk-circle .sk-circle10:before { 94 | -webkit-animation-delay: -0.3s; 95 | animation-delay: -0.3s; } 96 | .sk-circle .sk-circle11:before { 97 | -webkit-animation-delay: -0.2s; 98 | animation-delay: -0.2s; } 99 | .sk-circle .sk-circle12:before { 100 | -webkit-animation-delay: -0.1s; 101 | animation-delay: -0.1s; } 102 | 103 | @-webkit-keyframes sk-circleBounceDelay { 104 | 0%, 80%, 100% { 105 | -webkit-transform: scale(0); 106 | transform: scale(0); 107 | } 40% { 108 | -webkit-transform: scale(1); 109 | transform: scale(1); 110 | } 111 | } 112 | 113 | @keyframes sk-circleBounceDelay { 114 | 0%, 80%, 100% { 115 | -webkit-transform: scale(0); 116 | transform: scale(0); 117 | } 40% { 118 | -webkit-transform: scale(1); 119 | transform: scale(1); 120 | } 121 | } -------------------------------------------------------------------------------- /reactjs-redux-order-practice/src/styles/entry.scss: -------------------------------------------------------------------------------- 1 | // variables 2 | @import 'variables/font.scss'; 3 | @import 'variables/button.scss'; 4 | 5 | // layout 6 | @import 'layout/grid.scss'; 7 | @import 'layout/app.scss'; 8 | 9 | // Payment style 10 | @import 'pages/Order.scss'; 11 | 12 | // input style 13 | @import 'components/input.scss'; 14 | 15 | // loading style 16 | // ref:http://tobiasahlin.com/spinkit/ 17 | @import 'components/loading'; 18 | 19 | @import 'components/datetime.scss'; 20 | -------------------------------------------------------------------------------- /reactjs-redux-order-practice/src/styles/layout/app.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | /* The html and body elements cannot have any padding or margin. */ 5 | } 6 | 7 | 8 | // 滿版 window 高度, ref:http://www.bootply.com/97103 9 | .center-container { 10 | height:100%; 11 | } 12 | 13 | .center-row { 14 | height:100%; 15 | background-color: #9ac7d5; 16 | background-size:cover; 17 | overflow-y: hidden; 18 | } 19 | 20 | 21 | #app{ 22 | height: 100%; 23 | font-family: "微軟正黑體", "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; 24 | } 25 | 26 | /* Wrapper for page content to push down footer */ 27 | #wrap { 28 | min-height: 100%; 29 | height: 100%; 30 | /* Negative indent footer by its height */ 31 | margin: 0 auto 0 0; 32 | /* Pad bottom by footer height */ 33 | padding: 0 0 0 0; 34 | 35 | > .center-container { 36 | padding: 0 0 0 0; 37 | margin:0; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /reactjs-redux-order-practice/src/styles/layout/grid.scss: -------------------------------------------------------------------------------- 1 | // 複寫 col-* 的 padding & margin 2 | .nopadding { 3 | padding: 0 !important; 4 | margin: 0 !important; 5 | } -------------------------------------------------------------------------------- /reactjs-redux-order-practice/src/styles/pages/Order.scss: -------------------------------------------------------------------------------- 1 | .order { 2 | .menu { 3 | position: relative; 4 | .item { 5 | margin: 0 15px; 6 | .box { 7 | .text { 8 | margin: 10px; 9 | font-size: $fontValue; 10 | vertical-align: middle; 11 | display: inline-block; 12 | } 13 | margin: 5px; 14 | cursor: pointer; 15 | box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); 16 | background-color: #d9edf7; 17 | &:hover { 18 | background-color: #dff0d8; 19 | } 20 | &:active { 21 | box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.6), 0 6px 20px 0 rgba(0, 0, 0, 0.59); 22 | } 23 | // 區塊內容置中, height 已預先定義在 DOM 上 24 | text-align: center; 25 | font-size: 0; 26 | &::before { 27 | content: ''; 28 | height: 100%; 29 | vertical-align: middle; 30 | display: inline-block; 31 | } 32 | } 33 | } 34 | .item-nav { 35 | position: absolute; 36 | bottom: 0; 37 | .btn { 38 | span { 39 | font-size: $fontTitle; 40 | } 41 | padding: 10px 0; 42 | } 43 | } 44 | } 45 | .content { 46 | // position: relative; 47 | .panel.center-container { 48 | position: relative; 49 | .panel-heading { 50 | font-size: $fontTitle; 51 | } 52 | .panel-body { 53 | // 該 div height 由 js 動態決定 54 | overflow-y: auto; // 依據 div height 作用自動滾輪 55 | .order-details { 56 | table { 57 | font-size: $tableFont; 58 | } 59 | table { 60 | tbody tr td, 61 | thead tr th { 62 | &:nth-child(1) { 63 | width: 45%; 64 | } 65 | &:nth-child(2), 66 | &:nth-child(3) { 67 | width: 20%; 68 | } 69 | &:nth-child(4) { 70 | width: 10%; 71 | } 72 | &:nth-child(5) { 73 | width: 5%; 74 | } 75 | } 76 | } 77 | } 78 | .order-submit { 79 | position: absolute; 80 | bottom: 0; 81 | width: 100%; 82 | padding: 0 15px; 83 | margin-left: -15px; 84 | margin-bottom: -20px; 85 | button[type=submit] { 86 | height: $orderButtonHeight * 2; 87 | font-size: $fontValue; 88 | } 89 | select[name=table] { 90 | height: $orderButtonHeight; 91 | } 92 | .total { 93 | > div { 94 | height: $orderButtonHeight; 95 | } 96 | .title { 97 | font-size: $fontValue; 98 | padding: 6px; 99 | color: #333; 100 | background-color: #f5f5f5; 101 | border-color: #ddd; 102 | border-bottom: 1px solid transparent; 103 | border-top-left-radius: 3px; 104 | border-top-right-radius: 3px; 105 | } 106 | .value { 107 | font-size: $fontValue; 108 | padding: 6px; 109 | } 110 | } 111 | } 112 | } 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /reactjs-redux-order-practice/src/styles/variables/button.scss: -------------------------------------------------------------------------------- 1 | $orderButtonHeight: 55px; 2 | -------------------------------------------------------------------------------- /reactjs-redux-order-practice/src/styles/variables/font.scss: -------------------------------------------------------------------------------- 1 | $fontTitle: 36px; 2 | $fontValue: 24px; 3 | $tableFont: 20px; -------------------------------------------------------------------------------- /reactjs-redux-order-practice/webpack.config.js: -------------------------------------------------------------------------------- 1 | // import 'babel-polyfill'; 2 | 3 | const path = require('path'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | 6 | const HTMLWebpackPluginConfig = new HtmlWebpackPlugin({ 7 | template: `${__dirname}/src/index.html`, 8 | filename: 'index.html', 9 | inject: 'body', 10 | }); 11 | 12 | module.exports = { 13 | entry: [ 14 | './src/index.jsx', 15 | ], 16 | output: { 17 | path: `${__dirname}`, 18 | filename: 'index_bundle.js', 19 | }, 20 | module: { 21 | preLoaders: [{ 22 | test: /\.jsx$|\.js$/, 23 | loader: 'eslint-loader', 24 | include: `${__dirname}/src`, 25 | exclude: /bundle\.js$/ 26 | }], 27 | loaders: [{ 28 | test: /\.js$|\.jsx$/, 29 | exclude: /node_modules/, 30 | loader: 'babel-loader', 31 | query: { 32 | presets: ['es2015', 'react'], 33 | }, 34 | }, { 35 | test: /\.scss$/, 36 | loaders: ["style", "css", "sass"] 37 | }], 38 | }, 39 | devServer: { 40 | inline: true, 41 | port: 8008, 42 | }, 43 | plugins: [HTMLWebpackPluginConfig], 44 | }; 45 | -------------------------------------------------------------------------------- /reactjs-redux-todos-practice/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "react", 5 | ], 6 | "plugins": [] 7 | } 8 | -------------------------------------------------------------------------------- /reactjs-redux-todos-practice/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "rules": { 4 | "no-multiple-empty-lines": [2, {"max": 2, "maxEOF": 2}] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /reactjs-redux-todos-practice/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /reactjs-redux-todos-practice/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Todo 8 | 9 | 10 | 11 |
    12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /reactjs-redux-todos-practice/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reactjs-redux-todos-practice", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "webpack-dev-server --colors --inline --content-base .", 8 | "prod": "webpack -p" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "react": "^15.3.2", 14 | "react-dom": "^15.3.2", 15 | "react-redux": "^4.4.5", 16 | "redux": "^3.6.0" 17 | }, 18 | "devDependencies": { 19 | "babel-core": "^6.17.0", 20 | "babel-eslint": "^7.0.0", 21 | "babel-loader": "^6.2.5", 22 | "babel-preset-es2015": "^6.16.0", 23 | "babel-preset-react": "^6.16.0", 24 | "eslint": "^3.7.1", 25 | "eslint-config-airbnb": "^12.0.0", 26 | "eslint-loader": "^1.5.0", 27 | "eslint-plugin-import": "^1.16.0", 28 | "eslint-plugin-jsx-a11y": "^2.2.3", 29 | "eslint-plugin-react": "^6.4.1", 30 | "html-webpack-plugin": "^2.22.0", 31 | "webpack": "^1.13.2", 32 | "webpack-dev-server": "^1.16.2" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /reactjs-redux-todos-practice/readme.md: -------------------------------------------------------------------------------- 1 | # Todos 2 | 3 | ## 概觀 4 | 練習以 ReactJS + Redux 實作 todos list 5 | 6 | 7 | ## 開發 8 | 1. 套件相依安裝 `npm install` 9 | 2. 運行服務 `npm start` 10 | 3. 開啟瀏覽器 11 | 12 | ``` 13 | http://localhost:8008 14 | ``` 15 | 16 | ## 產出 js & html 17 | 運行 `npm run prod` 18 | 19 | > 產出的 js & html file 會在根目錄下 20 | 21 | 22 | ## 參考 23 | * https://chentsulin.github.io/redux/index.html -------------------------------------------------------------------------------- /reactjs-redux-todos-practice/src/actions/index.js: -------------------------------------------------------------------------------- 1 | let nextTodoId = 0 2 | export const addTodo = (text) => { 3 | return { 4 | type: 'ADD_TODO', 5 | id: nextTodoId++, 6 | text 7 | } 8 | } 9 | 10 | export const setVisibilityFilter = (filter) => { 11 | return { 12 | type: 'SET_VISIBILITY_FILTER', 13 | filter 14 | } 15 | } 16 | 17 | export const toggleTodo = (id) => { 18 | return { 19 | type: 'TOGGLE_TODO', 20 | id 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /reactjs-redux-todos-practice/src/components/AddTodo.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const AddTodo = ({onAddTodo}) => { 4 | let input 5 | 6 | return ( 7 |
    8 |
    { 9 | e.preventDefault(); 10 | if (!input.value.trim()) { 11 | return 12 | } 13 | onAddTodo(input.value); 14 | input.value = ''; 15 | }}> 16 | { 17 | input = node 18 | }}/> 19 | 22 |
    23 |
    24 | ) 25 | } 26 | 27 | export default AddTodo 28 | -------------------------------------------------------------------------------- /reactjs-redux-todos-practice/src/components/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Footer from './Footer.jsx' 3 | import AddTodo from '../containers/AddTodo' 4 | import VisibleTodoList from '../containers/VisibleTodoList' 5 | 6 | const App = () => ( 7 |
    8 | 9 | 10 |
    11 |
    12 | ) 13 | 14 | export default App 15 | -------------------------------------------------------------------------------- /reactjs-redux-todos-practice/src/components/Footer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import FilterLink from '../containers/FilterLink' 3 | 4 | const Footer = () => ( 5 |

    6 | Show: 7 | {" "} 8 | 9 | All 10 | 11 | {", "} 12 | 13 | Active 14 | 15 | {", "} 16 | 17 | Completed 18 | 19 |

    20 | ) 21 | 22 | export default Footer 23 | -------------------------------------------------------------------------------- /reactjs-redux-todos-practice/src/components/Link.jsx: -------------------------------------------------------------------------------- 1 | import React, {PropTypes} from 'react' 2 | 3 | const Link = ({active, children, onClick}) => { 4 | if (active) { 5 | return {children} 6 | } 7 | return ( 8 | { 9 | e.preventDefault(); 10 | onClick(); 11 | }}>{children} 12 | ) 13 | } 14 | 15 | Link.propTypes = { 16 | active: PropTypes.bool.isRequired, 17 | children: PropTypes.node.isRequired, 18 | onClick: PropTypes.func.isRequired 19 | } 20 | 21 | export default Link 22 | -------------------------------------------------------------------------------- /reactjs-redux-todos-practice/src/components/Todo.jsx: -------------------------------------------------------------------------------- 1 | import React, {PropTypes} from 'react' 2 | 3 | const Todo = ({onClick, completed, text}) => ( 4 |
  • {text}
  • 9 | ) 10 | 11 | Todo.propTypes = { 12 | onClick: PropTypes.func.isRequired, 13 | completed: PropTypes.bool.isRequired, 14 | text: PropTypes.string.isRequired, 15 | } 16 | 17 | export default Todo 18 | -------------------------------------------------------------------------------- /reactjs-redux-todos-practice/src/components/TodoList.jsx: -------------------------------------------------------------------------------- 1 | import React, {PropTypes} from 'react' 2 | import Todo from './Todo.jsx' 3 | 4 | const TodoList = ({todos, onTodoClick}) => ( 5 |
      6 | {todos.map(todo => onTodoClick(todo.id)}/>)} 7 |
    8 | ) 9 | 10 | TodoList.propTypes = { 11 | todos: PropTypes.arrayOf(PropTypes.shape({id: PropTypes.number.isRequired, completed: PropTypes.bool.isRequired, text: PropTypes.string.isRequired}).isRequired).isRequired, 12 | onTodoClick: PropTypes.func.isRequired 13 | } 14 | 15 | export default TodoList 16 | -------------------------------------------------------------------------------- /reactjs-redux-todos-practice/src/containers/AddTodo.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import {addTodo} from '../actions' 3 | import TodoList from '../components/AddTodo.jsx' 4 | 5 | const mapDispatchToProps = (dispatch) => { 6 | return { 7 | onAddTodo: (text) => { 8 | dispatch(addTodo(text)) 9 | } 10 | } 11 | } 12 | 13 | const AddTodo = connect( 14 | undefined, 15 | mapDispatchToProps 16 | )(TodoList) 17 | 18 | export default AddTodo 19 | // import React from 'react' 20 | // import {connect} from 'react-redux' 21 | // import {addTodo} from '../actions' 22 | // 23 | // let AddTodo = ({dispatch}) => { 24 | // let input 25 | // 26 | // return ( 27 | //
    28 | //
    { 29 | // e.preventDefault();if (!input.value.trim()) { 30 | // return 31 | // } 32 | // dispatch(addTodo(input.value));input.value = ''; 33 | // }}> 34 | // { 35 | // input = node 36 | // }}/> 37 | // 40 | //
    41 | //
    42 | // ) 43 | // } 44 | // AddTodo = connect()(AddTodo) 45 | // 46 | // export default AddTodo 47 | -------------------------------------------------------------------------------- /reactjs-redux-todos-practice/src/containers/FilterLink.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { setVisibilityFilter } from '../actions' 3 | import Link from '../components/Link.jsx' 4 | 5 | const mapStateToProps = (state, ownProps) => { 6 | return { 7 | active: ownProps.filter === state.visibilityFilter 8 | } 9 | } 10 | 11 | const mapDispatchToProps = (dispatch, ownProps) => { 12 | return { 13 | onClick: () => { 14 | dispatch(setVisibilityFilter(ownProps.filter)) 15 | } 16 | } 17 | } 18 | 19 | const FilterLink = connect( 20 | mapStateToProps, 21 | mapDispatchToProps 22 | )(Link) 23 | 24 | export default FilterLink 25 | -------------------------------------------------------------------------------- /reactjs-redux-todos-practice/src/containers/VisibleTodoList.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { toggleTodo } from '../actions' 3 | import TodoList from '../components/TodoList.jsx' 4 | 5 | const getVisibleTodos = (todos, filter) => { 6 | switch (filter) { 7 | case 'SHOW_ALL': 8 | return todos 9 | case 'SHOW_COMPLETED': 10 | return todos.filter(t => t.completed) 11 | case 'SHOW_ACTIVE': 12 | return todos.filter(t => !t.completed) 13 | } 14 | } 15 | 16 | const mapStateToProps = (state) => { 17 | return { 18 | todos: getVisibleTodos(state.todos, state.visibilityFilter) 19 | } 20 | } 21 | 22 | const mapDispatchToProps = (dispatch) => { 23 | return { 24 | onTodoClick: (id) => { 25 | dispatch(toggleTodo(id)) 26 | } 27 | } 28 | } 29 | 30 | const VisibleTodoList = connect( 31 | mapStateToProps, 32 | mapDispatchToProps 33 | )(TodoList) 34 | 35 | export default VisibleTodoList 36 | -------------------------------------------------------------------------------- /reactjs-redux-todos-practice/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Todo 8 | 9 | 10 | 11 |
    12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /reactjs-redux-todos-practice/src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import { Provider } from 'react-redux' 4 | import { createStore } from 'redux' 5 | import todoApp from './reducers' 6 | import App from './components/App.jsx' 7 | 8 | let store = createStore(todoApp) 9 | 10 | render( 11 | 12 | 13 | , 14 | document.getElementById('app') 15 | ) 16 | -------------------------------------------------------------------------------- /reactjs-redux-todos-practice/src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | combineReducers 3 | } from 'redux' 4 | import todos from './todos' 5 | import visibilityFilter from './visibilityFilter' 6 | 7 | const todoApp = combineReducers({ 8 | todos, 9 | visibilityFilter 10 | }) 11 | 12 | export default todoApp 13 | -------------------------------------------------------------------------------- /reactjs-redux-todos-practice/src/reducers/todos.js: -------------------------------------------------------------------------------- 1 | const todos = (state = [], action) => { 2 | switch (action.type) { 3 | case 'ADD_TODO': 4 | return [ 5 | ...state, { 6 | id: action.id, 7 | text: action.text, 8 | completed: false 9 | } 10 | ] 11 | case 'TOGGLE_TODO': 12 | return state.map(todo => { 13 | if (todo.id !== action.id) { 14 | return todo 15 | } 16 | 17 | return Object.assign({}, todo, { 18 | completed: !todo.completed 19 | }) 20 | }) 21 | default: 22 | return state 23 | } 24 | } 25 | 26 | export default todos 27 | -------------------------------------------------------------------------------- /reactjs-redux-todos-practice/src/reducers/visibilityFilter.js: -------------------------------------------------------------------------------- 1 | const visibilityFilter = (state = 'SHOW_ALL', action) => { 2 | switch (action.type) { 3 | case 'SET_VISIBILITY_FILTER': 4 | return action.filter 5 | default: 6 | return state 7 | } 8 | } 9 | 10 | export default visibilityFilter 11 | -------------------------------------------------------------------------------- /reactjs-redux-todos-practice/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | const HTMLWebpackPluginConfig = new HtmlWebpackPlugin({ 5 | template: `${__dirname}/src/index.html`, 6 | filename: 'index.html', 7 | inject: 'body', 8 | }); 9 | 10 | module.exports = { 11 | entry: [ 12 | './src/index.jsx', 13 | ], 14 | output: { 15 | path: `${__dirname}`, 16 | filename: 'index_bundle.js', 17 | }, 18 | module: { 19 | preLoaders: [{ 20 | test: /\.jsx$|\.js$/, 21 | loader: 'eslint-loader', 22 | include: `${__dirname}/src`, 23 | exclude: /bundle\.js$/ 24 | }], 25 | loaders: [{ 26 | test: /\.js$|\.jsx$/, 27 | exclude: /node_modules/, 28 | loader: 'babel-loader', 29 | query: { 30 | presets: ['es2015', 'react'], 31 | }, 32 | }], 33 | }, 34 | devServer: { 35 | inline: true, 36 | port: 8008, 37 | }, 38 | plugins: [HTMLWebpackPluginConfig], 39 | }; 40 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # react-demo 2 | 3 | 目錄 | 技術 | 備註 | DEMO 4 | --- | --- | --- | --- 5 | reactjs-redux-todos-practice | ReactJS + Redux | 依照 Redux 設計模式,實作 POS 點單 | [URL](https://rz12345.github.io/react-demo/reactjs-redux-order-practice/) 6 | reactjs-redux-todos-practice | ReactJS + Redux | 依照 Redux 設計模式,實作 Todos 待辦清單 | [URL](https://rz12345.github.io/react-demo/reactjs-redux-todos-practice/) 7 | reactjs-flex-order-practice | ReactJS + Flux | 依照 Flux 設計模式,實作 POS 點單 | [URL](https://rz12345.github.io/react-demo/reactjs-flex-order-practice/) 8 | reactjs-order-practice | ReactJS | 基於 wPOS 重構 | [URL](https://rz12345.github.io/react-demo/reactjs-order-practice/) 9 | wPOS | ReactJS | POS 點單 | [URL](https://rz12345.github.io/react-demo/wPOS/) -------------------------------------------------------------------------------- /wPOS/.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | bower_components 3 | node_modules 4 | -------------------------------------------------------------------------------- /wPOS/db/categories.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id":1, 4 | "name":"\u4e3b\u98df", 5 | "user":"rz", 6 | "is_active":1, 7 | "created_at":"2016-06-09 12:27:01", 8 | "updated_at":"2016-06-09 12:27:01" 9 | }, 10 | { 11 | "id":2, 12 | "name":"\u6e6f\u54c1", 13 | "user":"rz", 14 | "is_active":1, 15 | "created_at":"2016-06-09 12:27:01", 16 | "updated_at":"2016-06-09 12:27:01" 17 | }, 18 | { 19 | "id":3, 20 | "name":"\u5c0f\u9ede", 21 | "user":"rz", 22 | "is_active":1, 23 | "created_at":"2016-06-09 12:27:01", 24 | "updated_at":"2016-06-09 12:27:01" 25 | }, 26 | { 27 | "id":4, 28 | "name":"\u5176\u4ed6", 29 | "user":"rz", 30 | "is_active":1, 31 | "created_at":"2016-06-09 12:27:01", 32 | "updated_at":"2016-06-09 12:27:01" 33 | } 34 | ] 35 | -------------------------------------------------------------------------------- /wPOS/db/payments.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id":2088, 4 | "order":{ 5 | "orderFromMenu":{ 6 | "1":"1", 7 | "9":"2" 8 | }, 9 | "orderFromAddition":null 10 | }, 11 | "summary":[ 12 | { 13 | "id":1, 14 | "name":"\u85e5\u81b3\u6e05\u71c9\u725b\u8089\u9eb5(\u5c0f)", 15 | "category":"\u4e3b\u98df", 16 | "price":80, 17 | "amount":1, 18 | "subtotal":80, 19 | "from":"menu" 20 | }, 21 | { 22 | "id":9, 23 | "name":"\u7d05\u71d2\u4e7e\u62cc\u725b\u8089\u9eb5(\u5c0f)", 24 | "category":"\u4e3b\u98df", 25 | "price":80, 26 | "amount":2, 27 | "subtotal":160, 28 | "from":"menu" 29 | } 30 | ], 31 | "total":240, 32 | "part":"\u4e2d\u5348", 33 | "is_paid":1, 34 | "is_to_go":0, 35 | "table":"二號桌", 36 | "user":"user", 37 | "deleted_at":null, 38 | "created_at":"2016-06-19 14:57:35", 39 | "updated_at":"2016-06-19 14:57:35" 40 | }, 41 | { 42 | "id":2087, 43 | "order":{ 44 | "orderFromMenu":{ 45 | "1":"1", 46 | "3":"1", 47 | "4":"1", 48 | "12":"1" 49 | }, 50 | "orderFromAddition":null 51 | }, 52 | "summary":[ 53 | { 54 | "id":1, 55 | "name":"\u85e5\u81b3\u6e05\u71c9\u725b\u8089\u9eb5(\u5c0f)", 56 | "category":"\u4e3b\u98df", 57 | "price":80, 58 | "amount":1, 59 | "subtotal":80, 60 | "from":"menu" 61 | }, 62 | { 63 | "id":3, 64 | "name":"\u85e5\u81b3\u6e05\u71c9\u725b\u8089\u6e6f\u9eb5(\u5c0f)", 65 | "category":"\u4e3b\u98df", 66 | "price":50, 67 | "amount":1, 68 | "subtotal":50, 69 | "from":"menu" 70 | }, 71 | { 72 | "id":4, 73 | "name":"\u85e5\u81b3\u6e05\u71c9\u725b\u8089\u6e6f\u9eb5(\u5927)", 74 | "category":"\u4e3b\u98df", 75 | "price":60, 76 | "amount":1, 77 | "subtotal":60, 78 | "from":"menu" 79 | }, 80 | { 81 | "id":12, 82 | "name":"\u9999\u8fa3\u725b\u6c41\u4e7e\u62cc\u9eb5(\u5927)", 83 | "category":"\u4e3b\u98df", 84 | "price":55, 85 | "amount":1, 86 | "subtotal":55, 87 | "from":"menu" 88 | } 89 | ], 90 | "total":245, 91 | "part":"\u4e2d\u5348", 92 | "is_paid":1, 93 | "is_to_go":0, 94 | "table":"三號桌", 95 | "user":"user", 96 | "deleted_at":null, 97 | "created_at":"2016-06-19 14:57:32", 98 | "updated_at":"2016-06-19 14:57:32" 99 | }, 100 | { 101 | "id":2086, 102 | "order":{ 103 | "orderFromMenu":{ 104 | "4":"1", 105 | "1":"1", 106 | "6":"1", 107 | "2":"1" 108 | }, 109 | "orderFromAddition":null 110 | }, 111 | "summary":[ 112 | { 113 | "id":4, 114 | "name":"\u85e5\u81b3\u6e05\u71c9\u725b\u8089\u6e6f\u9eb5(\u5927)", 115 | "category":"\u4e3b\u98df", 116 | "price":60, 117 | "amount":1, 118 | "subtotal":60, 119 | "from":"menu" 120 | }, 121 | { 122 | "id":1, 123 | "name":"\u85e5\u81b3\u6e05\u71c9\u725b\u8089\u9eb5(\u5c0f)", 124 | "category":"\u4e3b\u98df", 125 | "price":80, 126 | "amount":1, 127 | "subtotal":80, 128 | "from":"menu" 129 | }, 130 | { 131 | "id":6, 132 | "name":"\u62db\u724c\u7d05\u71d2\u725b\u8089\u9eb5(\u5927)", 133 | "category":"\u4e3b\u98df", 134 | "price":100, 135 | "amount":1, 136 | "subtotal":100, 137 | "from":"menu" 138 | }, 139 | { 140 | "id":2, 141 | "name":"\u85e5\u81b3\u6e05\u71c9\u725b\u8089\u9eb5(\u5927)", 142 | "category":"\u4e3b\u98df", 143 | "price":90, 144 | "amount":1, 145 | "subtotal":90, 146 | "from":"menu" 147 | } 148 | ], 149 | "total":330, 150 | "part":"\u4e2d\u5348", 151 | "is_paid":1, 152 | "is_to_go":0, 153 | "table":"五號桌", 154 | "user":"user", 155 | "deleted_at":null, 156 | "created_at":"2016-06-19 14:57:29", 157 | "updated_at":"2016-06-19 14:57:29" 158 | }, 159 | { 160 | "id":2085, 161 | "order":{ 162 | "orderFromMenu":{ 163 | "10":"2" 164 | }, 165 | "orderFromAddition":null 166 | }, 167 | "summary":[ 168 | { 169 | "id":10, 170 | "name":"\u7d05\u71d2\u4e7e\u62cc\u725b\u8089\u9eb5(\u5927)", 171 | "category":"\u4e3b\u98df", 172 | "price":90, 173 | "amount":2, 174 | "subtotal":180, 175 | "from":"menu" 176 | } 177 | ], 178 | "total":180, 179 | "part":"\u4e2d\u5348", 180 | "is_paid":1, 181 | "is_to_go":1, 182 | "table":null, 183 | "user":"user", 184 | "deleted_at":null, 185 | "created_at":"2016-06-19 14:57:05", 186 | "updated_at":"2016-06-19 14:57:05" 187 | } 188 | ] 189 | -------------------------------------------------------------------------------- /wPOS/db/tables.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name":"一號桌" 4 | }, 5 | { 6 | "name":"二號桌" 7 | }, 8 | { 9 | "name":"三號桌" 10 | }, 11 | { 12 | "name":"四號桌" 13 | }, 14 | { 15 | "name":"五號桌" 16 | }, 17 | { 18 | "name":"六號桌" 19 | } 20 | ] 21 | -------------------------------------------------------------------------------- /wPOS/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | POS 11 | 12 | 13 |
    14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /wPOS/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wPOS", 3 | "version": "0.0.1", 4 | "description": "A web-based POS demo", 5 | "devDependencies": { 6 | "babelify": "^6.1.3", 7 | "browserify": "^10.2.4", 8 | "fetch": "^1.0.1", 9 | "node-sass": "^3.7.0", 10 | "nodemon": "^1.9.2", 11 | "npm-run-all": "^2.1.1", 12 | "react": "^15.1.0", 13 | "react-dom": "^15.1.0", 14 | "react-router": "^2.4.1", 15 | "uglify": "^0.1.5", 16 | "watchify": "^3.7.0" 17 | }, 18 | "scripts": { 19 | "start": "npm run watch", 20 | "watch": "npm-run-all --parallel \"watch-*\"", 21 | "build-js": "browserify src/entry.jsx | uglifyjs -o ./bundle.js", 22 | "watch-js": "watchify src/entry.jsx -do ./bundle.js -v", 23 | "build-css": "node-sass --include-path src/styles src/styles/entry.scss ./main.css", 24 | "watch-css": "nodemon -e scss -x \"npm run build-css\"" 25 | }, 26 | "browserify": { 27 | "transform": [ 28 | "babelify" 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /wPOS/readme.md: -------------------------------------------------------------------------------- 1 | # wPOS 2 | 3 | ## 概觀 4 | 5 | 練習以 ReactJS 實作 POS 點單介面 6 | 7 | DEMO: 8 | https://rz12345.github.io/react-demo/wPOS/ 9 | 10 | ## 部屬 11 | 12 | ``` 13 | npm install 14 | bower install 15 | ``` 16 | 17 | ## 開發環境 18 | 19 | 開發時,透過腳本監控 css & js 變化,即時 compiled 20 | ``` 21 | npm start 22 | ``` 23 | 24 | ## 學習參考 25 | -| 敘述 | 網址 26 | --- | --- | --- 27 | ReactJS 基礎 | 24 小時,React 快速入門 | https://github.com/shiningjason1989/react-quick-tutorial/ 28 | ReactJS 技巧 | list item click toggle class | http://jsfiddle.net/faria/3nodz94g/ 29 | -------------------------------------------------------------------------------- /wPOS/src/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router' 3 | 4 | class App extends React.Component { 5 | render() { 6 | return ( 7 |
    8 | 28 |
    29 |
    30 |
    31 | {this.props.children} 32 |
    33 |
    34 |
    35 |
    36 | ) 37 | } 38 | } 39 | 40 | module.exports = App; 41 | -------------------------------------------------------------------------------- /wPOS/src/components/ChoiceButton.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | class ChoiceButton extends React.Component { 4 | render() { 5 | const {type, onClick, isSelected} = this.props; 6 | const classAttr = "btn btn-lg btn-block " + (isSelected 7 | ? "btn-primary" 8 | : "btn-default"); 9 | return ( 10 | 11 | ); 12 | } 13 | } 14 | ChoiceButton.defaultProps = { 15 | isSelected: false 16 | }; 17 | ChoiceButton.propTypes = { 18 | isSelected: React.PropTypes.bool.isRequired 19 | }; 20 | 21 | module.exports = ChoiceButton; 22 | -------------------------------------------------------------------------------- /wPOS/src/components/InputField.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | class InputField extends React.Component { 4 | constructor(props, context) { 5 | super(props, context); 6 | this.state = {value: props.value || ''}; 7 | this.handleChange = this.handleChange.bind(this); 8 | } 9 | 10 | handleChange(e) { 11 | const {onUpdateAddition} = this.props; 12 | // 更新元件的 state value 13 | this.setState({value: e.target.value}); 14 | 15 | // 一併更新父層的 addition 16 | if (onUpdateAddition) { 17 | onUpdateAddition(e.target.value); 18 | } 19 | } 20 | 21 | render() { 22 | return ( 23 | 29 | ); 30 | } 31 | } 32 | 33 | module.exports = InputField; 34 | -------------------------------------------------------------------------------- /wPOS/src/components/Loading.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | class Loading extends React.Component { 4 | render(){ 5 | return ( 6 |
    7 |
    8 |
    9 |
    10 |
    11 |
    12 |
    13 |
    14 |
    15 |
    16 |
    17 |
    18 |
    19 |
    20 | ); 21 | } 22 | 23 | } 24 | 25 | module.exports = Loading; -------------------------------------------------------------------------------- /wPOS/src/components/MenuCategory.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | // jsx components 4 | const MenuCategoryItem = require('./MenuCategoryItem.jsx'); 5 | 6 | class MenuCategory extends React.Component { 7 | handleClick(i, category) { 8 | const {onClickCategroy} = this.props; 9 | // 觸發切換分類 item 動作 10 | onClickCategroy && onClickCategroy(category); 11 | 12 | // 設定選擇的 category (項目變成 active 狀態) 13 | this.setState({ 14 | selectedItem: i 15 | }); 16 | } 17 | 18 | constructor(props, context) { 19 | super(props, context); 20 | this.state = { 21 | selectedItem: 0 22 | }; 23 | } 24 | 25 | render() { 26 | const {categories} = this.props; 27 | return ( 28 |
    29 |
      30 | {categories ? categories.map((category, i)=> 31 | () 35 | ) : ''} 36 |
    37 |
    38 | ); 39 | } 40 | } 41 | 42 | module.exports = MenuCategory; 43 | -------------------------------------------------------------------------------- /wPOS/src/components/MenuCategoryItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | // list item click toggle class 4 | // example: http://jsfiddle.net/faria/3nodz94g/ 5 | 6 | class MenuCategoryItem extends React.Component { 7 | render() { 8 | const {category,onClick,isSelected} = this.props; 9 | return ( 10 |
  • 13 |

    {category}

    14 |
  • 15 | ); 16 | } 17 | } 18 | MenuCategoryItem.defaultProps = { 19 | isSelected: false 20 | }; 21 | MenuCategoryItem.propTypes = { 22 | isSelected: React.PropTypes.bool.isRequired 23 | }; 24 | 25 | module.exports = MenuCategoryItem; 26 | -------------------------------------------------------------------------------- /wPOS/src/components/MenuItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | // jsx components 4 | const MenuRow = require('./MenuRow.jsx'); 5 | 6 | // js helper 7 | const ArrayHelper = require('./../helpers/ArrayHelper'); 8 | 9 | class MenuItem extends React.Component { 10 | render() { 11 | const { 12 | items, 13 | onClickItem, 14 | contentDivHeight 15 | } = this.props; 16 | const splitedItems = items ? ArrayHelper.splitArray2D(items, 4) : null; 17 | const categoriesHeight = 83; 18 | const itemsPageNavHeight = 101; 19 | const paddingHeight = 10; 20 | const rowHeight = contentDivHeight ? (contentDivHeight - categoriesHeight - itemsPageNavHeight - paddingHeight) / 3 : null; 21 | const menuItemsRowStyle = rowHeight 22 | ? { 23 | height: rowHeight + 'px' 24 | } 25 | : { 26 | height: 'inherit' 27 | }; 28 | 29 | 30 | return ( 31 |
    32 | {splitedItems ? splitedItems.map((items, i)=>( 33 | 34 | )) : ''} 35 |
    36 | ); 37 | } 38 | } 39 | 40 | module.exports = MenuItem; 41 | -------------------------------------------------------------------------------- /wPOS/src/components/MenuItemNav.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | class MenuItemNav extends React.Component { 4 | constructor(props, context) { 5 | super(props, context); 6 | this.state = { 7 | pagesCount: null, 8 | currentPage: null 9 | }; 10 | } 11 | 12 | render() { 13 | const {pagesCount, currentPage,onClickNav} = this.props; 14 | const hasNextPage = pagesCount - currentPage > 1; // 是否還有下一頁 15 | const hasPrevPage = currentPage - 1 > -1; // 是否還有上一頁 16 | const nextPage = hasNextPage 17 | ? currentPage + 1 18 | : currentPage; 19 | const prevPage = hasPrevPage 20 | ? currentPage - 1 21 | : currentPage; 22 | 23 | return ( 24 |
    25 |
    26 |
    27 | 28 |
    29 |
    30 | 31 |
    32 |
    33 |
    34 | ); 35 | } 36 | } 37 | 38 | module.exports = MenuItemNav; 39 | -------------------------------------------------------------------------------- /wPOS/src/components/MenuRow.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | // jsx components 4 | const MenuRowItem = require('./MenuRowItem.jsx'); 5 | 6 | class MenuRow extends React.Component { 7 | render() { 8 | const {items,onClickItem,style} = this.props; 9 | return ( 10 |
    11 | {items.map((item, i)=> ( 12 | 16 | ))} 17 |
    18 | ); 19 | } 20 | } 21 | 22 | module.exports = MenuRow; 23 | -------------------------------------------------------------------------------- /wPOS/src/components/MenuRowItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | class MenuRowItem extends React.Component { 4 | render() { 5 | const {item,className,onClickItem} = this.props; 6 | return ( 7 |
    onClickItem && onClickItem(item)}> 10 | {item.name} 11 |
    12 | ); 13 | } 14 | } 15 | 16 | module.exports = MenuRowItem; 17 | -------------------------------------------------------------------------------- /wPOS/src/components/OrderDetails.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | // jsx components 4 | const InputField = require('./InputField.jsx'); 5 | 6 | class OrderDetails extends React.Component { 7 | render() { 8 | const { 9 | order, 10 | addition, 11 | onClickItem, 12 | onClickDeleteAddition, 13 | onUpdateAddition, 14 | divHeight 15 | } = this.props; 16 | 17 | // 選單內的點單項目 18 | const OrderElements = order 19 | ? order.map((el) => ( 20 | 21 | {el.name} 22 | {el.amount} 23 | {el.price} 24 | {el.subtotal} 25 | 26 | 27 | 30 | 31 | 32 | )) 33 | : ''; 34 | 35 | // 加點項目 36 | const AdditionElements = addition 37 | ? addition.map((el) => ( 38 | 39 | onUpdateAddition(el.id, 'name', value)}/> 41 | onUpdateAddition(el.id, 'amount', value)}/> 43 | onUpdateAddition(el.id, 'price', value)}/> 45 | {el.subtotal} 46 | 47 | 48 | 49 | 52 | 53 | 54 | )) 55 | : ''; 56 | 57 | // orderDetails 動態高度 58 | const orderDetailsDivStyle = divHeight 59 | ? { 60 | height: divHeight + 'px' 61 | } 62 | : { 63 | height: 'inherit' 64 | }; 65 | 66 | return ( 67 |
    68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 77 | 78 | 79 | 80 | {OrderElements} 81 | {AdditionElements} 82 | 83 |
    品項數量單價小計 76 |
    84 |
    85 | ); 86 | } 87 | } 88 | 89 | module.exports = OrderDetails; 90 | -------------------------------------------------------------------------------- /wPOS/src/components/OrderList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | 4 | // config 5 | const API = require('./../configs/API'); 6 | 7 | // jsx components 8 | const OrderSubmit = require('./OrderSubmit.jsx'); 9 | const OrderDetails = require('./OrderDetails.jsx'); 10 | 11 | class OrderList extends React.Component { 12 | // 背景執行提交表單的動作 13 | handleSubmit(e) { 14 | e.preventDefault(); 15 | const {clearOrder} = this.props; 16 | const {order, addition, total} = this.state; 17 | // 提交 18 | alert('觸發提交動作, 清除 order!'); 19 | // const formNode = ReactDOM.findDOMNode(this); 20 | // fetch(API.post.payment, { 21 | // method: 'POST', 22 | // body: new FormData(formNode) 23 | // }); 24 | // 呼叫父層清除 order 項目的方法 25 | clearOrder && clearOrder(); 26 | } 27 | 28 | updateOrderSubmitHeight(height) { 29 | this.setState({ 30 | orderSubmitHeight: height // 取得 orderSubmitHeight 高度 31 | }); 32 | } 33 | 34 | constructor(props, context) { 35 | super(props, context); 36 | this.state = { 37 | OrderSubmitHeight: null 38 | }; 39 | this.updateOrderSubmitHeight = this.updateOrderSubmitHeight.bind(this); 40 | this.handleSubmit = this.handleSubmit.bind(this); 41 | } 42 | 43 | render() { 44 | const { 45 | order, 46 | addition, 47 | total, 48 | onClickItem, 49 | onClickNewAddition, 50 | onClickDeleteAddition, 51 | onUpdateAddition, 52 | contentDivHeight, 53 | panelHeadingHeight 54 | } = this.props; 55 | const {orderSubmitHeight} = this.state; 56 | 57 | // 動態 OrderDetails 計算方法 58 | // 1. Order 接 contentDivHeight & panelHeadingHeight 59 | // 2. 當 OrderSubmit 生成後 , 產生 OrderSubmitHeight 60 | // 3. contentDivHeight 減去 panelHeadingHeight && OrderSubmitHeight && 兩個 rowHeight 來得到 OrderDetails 必要之高度 61 | const rowHeight = 51; 62 | const orderDetailsHeight = (contentDivHeight && panelHeadingHeight && orderSubmitHeight) 63 | ? contentDivHeight - panelHeadingHeight - orderSubmitHeight 64 | : null; 65 | const props = { 66 | OrderDetails: { 67 | order: order, 68 | addition: addition, 69 | onClickItem: onClickItem, 70 | onClickDeleteAddition: onClickDeleteAddition, 71 | onUpdateAddition: onUpdateAddition, 72 | divHeight: orderDetailsHeight 73 | }, 74 | OrderSubmit: { 75 | order: order, 76 | total: total, 77 | onClickNewAddition: onClickNewAddition, 78 | updateOrderSubmitHeight: this.updateOrderSubmitHeight 79 | } 80 | }; 81 | 82 | return ( 83 |
    84 | 85 | 86 | 87 | ); 88 | } 89 | } 90 | 91 | module.exports = OrderList; 92 | -------------------------------------------------------------------------------- /wPOS/src/configs/API.js: -------------------------------------------------------------------------------- 1 | const serviceURL = './db/'; 2 | const API = { 3 | get:{ 4 | item: serviceURL + '/items.json', 5 | category: serviceURL + '/categories.json', 6 | table: serviceURL + '/tables.json', 7 | payment: { 8 | today: serviceURL + '/payments.json' 9 | } 10 | } 11 | }; 12 | 13 | module.exports = API; 14 | -------------------------------------------------------------------------------- /wPOS/src/entry.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import { Router, Route, hashHistory, IndexRoute } from 'react-router' 4 | 5 | const App = require('./App.jsx'); 6 | const Order = require('./pages/Order.jsx'); 7 | const Home = require('./pages/Home.jsx'); 8 | const NotFound = require('./pages/NotFound.jsx'); 9 | 10 | render(( 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ), document.getElementById('app')); 20 | -------------------------------------------------------------------------------- /wPOS/src/helpers/ArrayHelper.js: -------------------------------------------------------------------------------- 1 | class ArrayHelper { 2 | /** 3 | * 將陣列 haystack 依照 colNum 切成二維陣列 4 | * 5 | * @param haystack 6 | * @param colNum 7 | * @returns {Array} 8 | */ 9 | static splitArray2D(haystack, colNum) { 10 | var result = []; 11 | var rowNum = Math.ceil(haystack.length / colNum); // 無條件進位 12 | for (var i = 0; i < rowNum; i++) { 13 | // 第一階 array 14 | result[i] = []; 15 | for (var j = 0; j < colNum; j++) { 16 | // 第二階 array 17 | var idx = (i * colNum) + j; 18 | if (haystack[idx]) 19 | result[i].push(haystack[idx]); 20 | } 21 | } 22 | return result; 23 | }; 24 | } 25 | 26 | module.exports = ArrayHelper; 27 | -------------------------------------------------------------------------------- /wPOS/src/helpers/PaymentHelper.js: -------------------------------------------------------------------------------- 1 | class PaymentHelper { 2 | // 取得 payment 的摘要資訊 3 | static getPaymentSummary(payment){ 4 | // 這邊只要品名跟數量就好 5 | const data = payment.map((item) => item.name + ': ' + item.amount); 6 | return data.join(', '); 7 | } 8 | 9 | // 傳回該分類的 items 10 | static getItemsByCategory(items, category) { 11 | return items.filter((item) => item.category === category); 12 | } 13 | 14 | // 傳回該分頁的 items 15 | static getItemsByPage(items, idx) { 16 | return items[idx]; 17 | } 18 | 19 | // 新增項目至明細表中 20 | static addItemToOrder(order, item) { 21 | const target = order.find((el) => el.id === item.id); 22 | if (target) { 23 | // 已在 order 內, 更新 obj 的 amount & subtotal 24 | target.amount += 1; 25 | target.subtotal = target.amount * target.price; 26 | } else { 27 | // 若不在 order 內, 產生新 obj 28 | order.push({ 29 | id: item.id, 30 | name: item.name, 31 | amount: 1, 32 | price: item.price, 33 | subtotal: 1 * item.price 34 | }); 35 | } 36 | return order; 37 | } 38 | 39 | // 從明細表中移除項目 40 | static deleteItemFromOrder(order, item) { 41 | const target = order.find((el) => el.id === item.id); 42 | if (target) { 43 | target.amount -= 1; 44 | target.subtotal = target.amount * target.price; 45 | } 46 | return order.filter((el) => el.amount > 0); 47 | } 48 | 49 | // 新增加點項目 50 | static addAdditionToOrder(addition) { 51 | addition.push({ 52 | id: new Date().getTime(), 53 | name: '', 54 | amount: 1, 55 | price: 0, 56 | subtotal: 0 57 | }); 58 | return addition; 59 | } 60 | 61 | // 更新加點項目 62 | static updateAddition(addition, id, key, value) { 63 | const target = addition.find((el) => el.id === id); 64 | if (target) { 65 | target[key] = value; 66 | target.subtotal = target.amount * target.price; 67 | } 68 | return addition; 69 | } 70 | 71 | // 刪除加點項目 72 | static deleteAdditionFromOrder(addition, item) { 73 | const idx = addition.findIndex((el) => el.id === item.id); 74 | if (idx !== -1) 75 | addition.splice(idx, 1); 76 | return addition; 77 | } 78 | } 79 | 80 | module.exports = PaymentHelper; 81 | -------------------------------------------------------------------------------- /wPOS/src/pages/NotFound.jsx: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | class NotFound extends React.Component { 3 | render() { 4 | return ( 5 |
    6 |

    Not Found This Page

    7 |
    8 | ); 9 | } 10 | } 11 | module.exports = NotFound; 12 | -------------------------------------------------------------------------------- /wPOS/src/styles/components/loading.scss: -------------------------------------------------------------------------------- 1 | .sk-circle { 2 | margin: 100px auto; 3 | width: 40px; 4 | height: 40px; 5 | position: relative; 6 | } 7 | .sk-circle .sk-child { 8 | width: 100%; 9 | height: 100%; 10 | position: absolute; 11 | left: 0; 12 | top: 0; 13 | } 14 | .sk-circle .sk-child:before { 15 | content: ''; 16 | display: block; 17 | margin: 0 auto; 18 | width: 15%; 19 | height: 15%; 20 | background-color: #333; 21 | border-radius: 100%; 22 | -webkit-animation: sk-circleBounceDelay 1.2s infinite ease-in-out both; 23 | animation: sk-circleBounceDelay 1.2s infinite ease-in-out both; 24 | } 25 | .sk-circle .sk-circle2 { 26 | -webkit-transform: rotate(30deg); 27 | -ms-transform: rotate(30deg); 28 | transform: rotate(30deg); } 29 | .sk-circle .sk-circle3 { 30 | -webkit-transform: rotate(60deg); 31 | -ms-transform: rotate(60deg); 32 | transform: rotate(60deg); } 33 | .sk-circle .sk-circle4 { 34 | -webkit-transform: rotate(90deg); 35 | -ms-transform: rotate(90deg); 36 | transform: rotate(90deg); } 37 | .sk-circle .sk-circle5 { 38 | -webkit-transform: rotate(120deg); 39 | -ms-transform: rotate(120deg); 40 | transform: rotate(120deg); } 41 | .sk-circle .sk-circle6 { 42 | -webkit-transform: rotate(150deg); 43 | -ms-transform: rotate(150deg); 44 | transform: rotate(150deg); } 45 | .sk-circle .sk-circle7 { 46 | -webkit-transform: rotate(180deg); 47 | -ms-transform: rotate(180deg); 48 | transform: rotate(180deg); } 49 | .sk-circle .sk-circle8 { 50 | -webkit-transform: rotate(210deg); 51 | -ms-transform: rotate(210deg); 52 | transform: rotate(210deg); } 53 | .sk-circle .sk-circle9 { 54 | -webkit-transform: rotate(240deg); 55 | -ms-transform: rotate(240deg); 56 | transform: rotate(240deg); } 57 | .sk-circle .sk-circle10 { 58 | -webkit-transform: rotate(270deg); 59 | -ms-transform: rotate(270deg); 60 | transform: rotate(270deg); } 61 | .sk-circle .sk-circle11 { 62 | -webkit-transform: rotate(300deg); 63 | -ms-transform: rotate(300deg); 64 | transform: rotate(300deg); } 65 | .sk-circle .sk-circle12 { 66 | -webkit-transform: rotate(330deg); 67 | -ms-transform: rotate(330deg); 68 | transform: rotate(330deg); } 69 | .sk-circle .sk-circle2:before { 70 | -webkit-animation-delay: -1.1s; 71 | animation-delay: -1.1s; } 72 | .sk-circle .sk-circle3:before { 73 | -webkit-animation-delay: -1s; 74 | animation-delay: -1s; } 75 | .sk-circle .sk-circle4:before { 76 | -webkit-animation-delay: -0.9s; 77 | animation-delay: -0.9s; } 78 | .sk-circle .sk-circle5:before { 79 | -webkit-animation-delay: -0.8s; 80 | animation-delay: -0.8s; } 81 | .sk-circle .sk-circle6:before { 82 | -webkit-animation-delay: -0.7s; 83 | animation-delay: -0.7s; } 84 | .sk-circle .sk-circle7:before { 85 | -webkit-animation-delay: -0.6s; 86 | animation-delay: -0.6s; } 87 | .sk-circle .sk-circle8:before { 88 | -webkit-animation-delay: -0.5s; 89 | animation-delay: -0.5s; } 90 | .sk-circle .sk-circle9:before { 91 | -webkit-animation-delay: -0.4s; 92 | animation-delay: -0.4s; } 93 | .sk-circle .sk-circle10:before { 94 | -webkit-animation-delay: -0.3s; 95 | animation-delay: -0.3s; } 96 | .sk-circle .sk-circle11:before { 97 | -webkit-animation-delay: -0.2s; 98 | animation-delay: -0.2s; } 99 | .sk-circle .sk-circle12:before { 100 | -webkit-animation-delay: -0.1s; 101 | animation-delay: -0.1s; } 102 | 103 | @-webkit-keyframes sk-circleBounceDelay { 104 | 0%, 80%, 100% { 105 | -webkit-transform: scale(0); 106 | transform: scale(0); 107 | } 40% { 108 | -webkit-transform: scale(1); 109 | transform: scale(1); 110 | } 111 | } 112 | 113 | @keyframes sk-circleBounceDelay { 114 | 0%, 80%, 100% { 115 | -webkit-transform: scale(0); 116 | transform: scale(0); 117 | } 40% { 118 | -webkit-transform: scale(1); 119 | transform: scale(1); 120 | } 121 | } -------------------------------------------------------------------------------- /wPOS/src/styles/entry.scss: -------------------------------------------------------------------------------- 1 | // variables 2 | @import 'variables/font.scss'; 3 | @import 'variables/button.scss'; 4 | 5 | // layout 6 | @import 'layout/grid.scss'; 7 | @import 'layout/app.scss'; 8 | @import 'layout/grid.scss'; 9 | 10 | // Payment style 11 | @import 'pages/Order'; 12 | @import 'pages/Report.scss'; 13 | @import 'pages/Home.scss'; 14 | 15 | // loading style 16 | // ref:http://tobiasahlin.com/spinkit/ 17 | @import 'components/loading'; 18 | -------------------------------------------------------------------------------- /wPOS/src/styles/layout/app.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | /* The html and body elements cannot have any padding or margin. */ 5 | } 6 | 7 | 8 | // 滿版 window 高度, ref:http://www.bootply.com/97103 9 | .center-container { 10 | height:100%; 11 | } 12 | 13 | .center-row { 14 | height:100%; 15 | background-color: #9ac7d5; 16 | background-size:cover; 17 | overflow-y: hidden; 18 | } 19 | 20 | 21 | #app{ 22 | height: 100%; 23 | font-family: "微軟正黑體", "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; 24 | } 25 | 26 | /* Wrapper for page content to push down footer */ 27 | #wrap { 28 | min-height: 100%; 29 | height: 100%; 30 | /* Negative indent footer by its height */ 31 | margin: 0 auto 0 0; 32 | /* Pad bottom by footer height */ 33 | padding: 0 0 0 0; 34 | 35 | > .center-container { 36 | padding: 50px 0 0 0; 37 | margin:0; 38 | } 39 | } -------------------------------------------------------------------------------- /wPOS/src/styles/layout/grid.scss: -------------------------------------------------------------------------------- 1 | // 複寫 col-* 的 padding & margin 2 | .nopadding { 3 | padding: 0 !important; 4 | margin: 0 !important; 5 | } -------------------------------------------------------------------------------- /wPOS/src/styles/pages/Home.scss: -------------------------------------------------------------------------------- 1 | .home { 2 | .content { 3 | .home-details { 4 | // 該 div height 由 js 動態決定 5 | overflow-y: auto; // 依據 div height 作用自動滾輪 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /wPOS/src/styles/pages/Order.scss: -------------------------------------------------------------------------------- 1 | .order { 2 | .menu { 3 | position: relative; 4 | .item { 5 | // 選單項目滑動 6 | .flex-container { 7 | display: flex; 8 | /* primary flex container */ 9 | flex-direction: row; 10 | /* horizontal alignment of flex items (default value; can be omitted) */ 11 | align-items: stretch; 12 | /* will apply equal heights to flex items (default value; can be omitted) */ 13 | // height: 150px; // 高度由 js 動態決定 14 | .flex-item { 15 | display: flex; 16 | /* nested flex container */ 17 | flex-direction: column; 18 | /* vertical alignment of flex items */ 19 | justify-content: center; 20 | /* center flex items vertically */ 21 | align-items: center; 22 | /* center flex items horizontally */ 23 | flex: 1; 24 | box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); 25 | margin: 5px; 26 | padding: 1em; 27 | font-size: $fontValue; 28 | text-align: center; 29 | } 30 | } 31 | } 32 | .item-nav { 33 | position: absolute; 34 | bottom: 0; 35 | .btn { 36 | span { 37 | font-size: $fontTitle; 38 | } 39 | padding: 10px 0; 40 | } 41 | } 42 | } 43 | .content { 44 | .panel.center-container { 45 | 46 | .panel-heading{ 47 | font-size: $fontTitle; 48 | } 49 | .panel-body{ 50 | 51 | form{ 52 | //position: relative; 53 | .order-details { 54 | // 該 div height 由 js 動態決定 55 | overflow-y: auto; // 依據 div height 作用自動滾輪 56 | 57 | table{ 58 | font-size: $tableFont; 59 | } 60 | table { 61 | tbody tr td, 62 | thead tr th { 63 | &:nth-child(1) { 64 | width: 45%; 65 | } 66 | &:nth-child(2), 67 | &:nth-child(3){ 68 | width: 20%; 69 | } 70 | &:nth-child(4){ 71 | width: 10%; 72 | } 73 | &:nth-child(5) { 74 | width: 5%; 75 | } 76 | } 77 | } 78 | } 79 | .order-submit { 80 | //margin-bottom: 15px; 81 | //position: absolute; 82 | //bottom: 0; 83 | //left: 0; 84 | button[type=submit] { 85 | height: $orderButtonHeight * 2; 86 | font-size: $fontValue; 87 | } 88 | 89 | select[name=table] { 90 | height: $orderButtonHeight; 91 | } 92 | 93 | .total{ 94 | > div { 95 | height: $orderButtonHeight; 96 | } 97 | .title{ 98 | font-size: $fontValue; 99 | padding: 6px; 100 | color: #333; 101 | background-color: #f5f5f5; 102 | border-color: #ddd; 103 | border-bottom: 1px solid transparent; 104 | border-top-left-radius: 3px; 105 | border-top-right-radius: 3px; 106 | } 107 | .value { 108 | font-size: $fontValue; 109 | padding: 6px; 110 | } 111 | 112 | } 113 | } 114 | } 115 | } 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /wPOS/src/styles/variables/button.scss: -------------------------------------------------------------------------------- 1 | $orderButtonHeight: 46px; -------------------------------------------------------------------------------- /wPOS/src/styles/variables/font.scss: -------------------------------------------------------------------------------- 1 | $fontTitle: 36px; 2 | $fontValue: 24px; 3 | $tableFont: 20px; --------------------------------------------------------------------------------