├── .eslintrc ├── .gitignore ├── README.md ├── package.json ├── public ├── data.js ├── favicon.ico └── index.html ├── src ├── App.css ├── App.js ├── App.test.js ├── Layout.js ├── Login.js ├── Menu.js ├── authClient.js ├── buttons │ ├── DeleteButton.js │ └── EditButton.js ├── categories │ ├── LinkToRelatedProducts.js │ └── index.js ├── commands │ ├── Basket.js │ ├── NbItemsField.js │ └── index.js ├── configuration │ ├── Configuration.js │ └── actions.js ├── dashboard │ ├── Dashboard.js │ ├── MonthlyRevenue.js │ ├── NbNewOrders.js │ ├── NewCustomers.js │ ├── PendingOrders.js │ ├── PendingReviews.js │ ├── Welcome.js │ └── index.js ├── i18n │ ├── en.js │ ├── fr.js │ └── index.js ├── index.css ├── index.js ├── products │ ├── GridList.js │ ├── Poster.js │ ├── ProductRefField.js │ ├── ProductReferenceField.js │ ├── ThumbnailField.js │ └── index.js ├── restClient.js ├── restServer.js ├── reviews │ ├── AcceptButton.js │ ├── ApproveButton.js │ ├── RejectButton.js │ ├── ReviewEditActions.js │ ├── StarRatingField.js │ ├── index.js │ ├── reviewActions.js │ ├── reviewSaga.js │ └── rowStyle.js ├── routes.js ├── sagas.js ├── segments │ ├── LinkToRelatedCustomers.js │ ├── Segments.js │ └── data.js ├── themeReducer.js └── visitors │ ├── AvatarField.js │ ├── CustomerReferenceField.js │ ├── FullNameField.js │ ├── SegmentInput.js │ ├── SegmentsField.js │ ├── SegmentsInput.js │ ├── index.js │ └── segments.js └── yarn.lock /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "mocha": true, 5 | "node": true, 6 | "phantomjs": true, 7 | "protractor": true, 8 | }, 9 | "extends": "airbnb", 10 | "parser": "babel-eslint", 11 | "rules": { 12 | "indent": ["warn", 4], 13 | "max-len": ["off"], 14 | "react/jsx-indent": ["warn", 4], 15 | "react/jsx-indent-props": ["warn", 4], 16 | "react/jsx-filename-extension": ["off"] 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # testing 7 | coverage 8 | 9 | # production 10 | build 11 | 12 | # misc 13 | .DS_Store 14 | .env 15 | npm-debug.log 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 |
archivedArchived Repository
5 | This code is no longer maintained. Feel free to fork it, but use it at your own risks. 6 |
9 | 10 | # Admin-on-rest Demo 11 | 12 | This is a demo of the [admin-on-rest](https://github.com/marmelab/admin-on-rest) library for React.js. It creates a working administration for a fake poster shop named Posters Galore. You can test it online at http://marmelab.com/admin-on-rest-demo. 13 | 14 | [![admin-on-rest-demo](https://marmelab.com/admin-on-rest/img/admin-on-rest-demo-still.png)](https://vimeo.com/205118063) 15 | 16 | Admin-on-rest usually requires a REST server to provide data. In this demo however, the REST server is simulated by the browser (using [FakeRest](https://github.com/marmelab/FakeRest)). You can see the source data in [public/data.js](https://github.com/marmelab/admin-on-rest-demo/tree/master/public/data.js). 17 | 18 | To explore the source code, start with [src/index.js](https://github.com/marmelab/admin-on-rest-demo/blob/master/src/index.js). 19 | 20 | **Note**: This project was bootstrapped with [Create React App](https://github.com/facebookincubator/create-react-app). 21 | 22 | ## Available Scripts 23 | 24 | In the project directory, you can run: 25 | 26 | ### `npm start` 27 | 28 | Runs the app in the development mode.
29 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 30 | 31 | The page will reload if you make edits.
32 | You will also see any lint errors in the console. 33 | 34 | ### `npm test` 35 | 36 | Launches the test runner in the interactive watch mode.
37 | See the section about [running tests](#running-tests) for more information. 38 | 39 | ### `npm run build` 40 | 41 | Builds the app for production to the `build` folder.
42 | It correctly bundles React in production mode and optimizes the build for the best performance. 43 | 44 | The build is minified and the filenames include the hashes.
45 | Your app is ready to be deployed! 46 | 47 | ### `npm run deploy` 48 | 49 | Deploy the build to GitHub gh-pages. 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "admin-on-rest-demo", 3 | "version": "1.2.0", 4 | "private": true, 5 | "devDependencies": { 6 | "babel-eslint": "^7.1.1", 7 | "babel-polyfill": "^6.16.0", 8 | "eslint": "^3.10.2", 9 | "eslint-config-airbnb": "15.0.1", 10 | "eslint-config-airbnb-base": "~11.2.0", 11 | "eslint-import-resolver-node": "^0.2.3", 12 | "eslint-module-utils": "^2.0.0", 13 | "eslint-plugin-import": "^2.2.0", 14 | "eslint-plugin-jsx-a11y": "5.0.1", 15 | "eslint-plugin-react": "~7.0.1", 16 | "fakerest": "^1.2.1", 17 | "fetch-mock": "^5.5.0", 18 | "gh-pages": "^0.12.0", 19 | "react-scripts": "1.0.5" 20 | }, 21 | "dependencies": { 22 | "admin-on-rest": "~1.3.2", 23 | "aor-language-french": "^1.8.0", 24 | "aor-rich-text-input": "^1.0.0", 25 | "material-ui": "~0.19.0", 26 | "prop-types": "~15.5.7", 27 | "react": "~15.5.4", 28 | "react-dom": "~15.5.4", 29 | "react-redux": "~5.0.4", 30 | "react-router-dom": "~4.1.0", 31 | "react-tap-event-plugin": "~2.0.1", 32 | "redux-form": "~7.0.3", 33 | "redux-saga": "~0.15.0" 34 | }, 35 | "scripts": { 36 | "start": "react-scripts start", 37 | "build": "react-scripts build", 38 | "test": "react-scripts test --env=jsdom", 39 | "eject": "react-scripts eject", 40 | "deploy": "gh-pages -d build" 41 | }, 42 | "homepage": "http://marmelab.com/admin-on-rest-demo/" 43 | } 44 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marmelab/admin-on-rest-demo/fd6b85b46068e39a5f2f6c9fd1035f6f7796f318/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 16 | Posters Galore Administration 17 | 99 | 100 | 101 | 102 |
103 |
104 |
Loading...
105 |
106 |
107 | 117 | 118 | 127 | 128 | 129 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | animation: App-logo-spin infinite 20s linear; 7 | height: 80px; 8 | } 9 | 10 | .App-header { 11 | background-color: #222; 12 | height: 150px; 13 | padding: 20px; 14 | color: white; 15 | } 16 | 17 | .App-intro { 18 | font-size: large; 19 | } 20 | 21 | @keyframes App-logo-spin { 22 | from { transform: rotate(0deg); } 23 | to { transform: rotate(360deg); } 24 | } 25 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | import React, { Component } from 'react'; 3 | import { Admin, Delete, Resource } from 'admin-on-rest'; 4 | 5 | import './App.css'; 6 | 7 | import authClient from './authClient'; 8 | import sagas from './sagas'; 9 | import themeReducer from './themeReducer'; 10 | import Login from './Login'; 11 | import Layout from './Layout'; 12 | import Menu from './Menu'; 13 | import { Dashboard } from './dashboard'; 14 | import customRoutes from './routes'; 15 | import translations from './i18n'; 16 | 17 | import { VisitorList, VisitorEdit, VisitorDelete, VisitorIcon } from './visitors'; 18 | import { CommandList, CommandEdit, CommandIcon } from './commands'; 19 | import { ProductList, ProductCreate, ProductEdit, ProductIcon } from './products'; 20 | import { CategoryList, CategoryEdit, CategoryIcon } from './categories'; 21 | import { ReviewList, ReviewEdit, ReviewIcon } from './reviews'; 22 | 23 | import restClient from './restClient'; 24 | import fakeRestServer from './restServer'; 25 | 26 | class App extends Component { 27 | componentWillMount() { 28 | this.restoreFetch = fakeRestServer(); 29 | } 30 | 31 | componentWillUnmount() { 32 | this.restoreFetch(); 33 | } 34 | 35 | render() { 36 | return ( 37 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | ); 57 | } 58 | } 59 | 60 | export default App; 61 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | }); 9 | -------------------------------------------------------------------------------- /src/Layout.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import darkBaseTheme from 'material-ui/styles/baseThemes/darkBaseTheme'; 3 | import { Layout, defaultTheme } from 'admin-on-rest'; 4 | 5 | export default connect(state => ({ 6 | theme: state.theme === 'dark' ? darkBaseTheme : defaultTheme, 7 | }))(Layout); 8 | -------------------------------------------------------------------------------- /src/Login.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { propTypes, reduxForm, Field } from 'redux-form'; 4 | import { connect } from 'react-redux'; 5 | import compose from 'recompose/compose'; 6 | 7 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; 8 | import getMuiTheme from 'material-ui/styles/getMuiTheme'; 9 | import { Card, CardActions } from 'material-ui/Card'; 10 | import Avatar from 'material-ui/Avatar'; 11 | import RaisedButton from 'material-ui/RaisedButton'; 12 | import TextField from 'material-ui/TextField'; 13 | import LockIcon from 'material-ui/svg-icons/action/lock-outline'; 14 | import { cyan500, pinkA200 } from 'material-ui/styles/colors'; 15 | 16 | import { Notification, translate, userLogin as userLoginAction } from 'admin-on-rest'; 17 | 18 | const styles = { 19 | main: { 20 | display: 'flex', 21 | flexDirection: 'column', 22 | minHeight: '100vh', 23 | alignItems: 'center', 24 | justifyContent: 'center', 25 | }, 26 | card: { 27 | minWidth: 300, 28 | }, 29 | avatar: { 30 | margin: '1em', 31 | textAlign: 'center ', 32 | }, 33 | form: { 34 | padding: '0 1em 1em 1em', 35 | }, 36 | input: { 37 | display: 'flex', 38 | }, 39 | hint: { 40 | textAlign: 'center', 41 | marginTop: '1em', 42 | color: '#ccc', 43 | }, 44 | }; 45 | 46 | function getColorsFromTheme(theme) { 47 | if (!theme) return { primary1Color: cyan500, accent1Color: pinkA200 }; 48 | const { 49 | palette: { 50 | primary1Color, 51 | accent1Color, 52 | }, 53 | } = theme; 54 | return { primary1Color, accent1Color }; 55 | } 56 | 57 | // see http://redux-form.com/6.4.3/examples/material-ui/ 58 | const renderInput = ({ meta: { touched, error } = {}, input: { ...inputProps }, ...props }) => 59 | ; 65 | 66 | class Login extends Component { 67 | 68 | login = ({ username, password }) => { 69 | const { userLogin, location } = this.props; 70 | userLogin({ username, password }, location.state ? location.state.nextPathname : '/'); 71 | } 72 | 73 | render() { 74 | const { handleSubmit, submitting, theme, translate } = this.props; 75 | const muiTheme = getMuiTheme(theme); 76 | const { primary1Color, accent1Color } = getColorsFromTheme(muiTheme); 77 | return ( 78 | 79 |
80 | 81 |
82 | } size={60} /> 83 |
84 |
85 |
86 |

Hint: demo / demo

87 |
88 | 93 |
94 |
95 | 101 |
102 |
103 | 104 | 105 | 106 |
107 |
108 | 109 |
110 |
111 | ); 112 | } 113 | } 114 | 115 | Login.propTypes = { 116 | ...propTypes, 117 | authClient: PropTypes.func, 118 | previousRoute: PropTypes.string, 119 | theme: PropTypes.object.isRequired, 120 | translate: PropTypes.func.isRequired, 121 | }; 122 | 123 | Login.defaultProps = { 124 | theme: {}, 125 | }; 126 | 127 | const enhance = compose( 128 | translate, 129 | reduxForm({ 130 | form: 'signIn', 131 | validate: (values, props) => { 132 | const errors = {}; 133 | const { translate } = props; 134 | if (!values.username) errors.username = translate('aor.validation.required'); 135 | if (!values.password) errors.password = translate('aor.validation.required'); 136 | return errors; 137 | }, 138 | }), 139 | connect(null, { userLogin: userLoginAction }), 140 | ); 141 | 142 | export default enhance(Login); 143 | -------------------------------------------------------------------------------- /src/Menu.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import compose from 'recompose/compose'; 4 | import SettingsIcon from 'material-ui/svg-icons/action/settings'; 5 | import LabelIcon from 'material-ui/svg-icons/action/label'; 6 | import { translate, DashboardMenuItem, MenuItemLink } from 'admin-on-rest'; 7 | 8 | import { VisitorIcon } from './visitors'; 9 | import { CommandIcon } from './commands'; 10 | import { ProductIcon } from './products'; 11 | import { CategoryIcon } from './categories'; 12 | import { ReviewIcon } from './reviews'; 13 | 14 | const items = [ 15 | { name: 'customers', icon: }, 16 | { name: 'segments', icon: }, 17 | { name: 'commands', icon: }, 18 | { name: 'products', icon: }, 19 | { name: 'categories', icon: }, 20 | { name: 'reviews', icon: }, 21 | ]; 22 | 23 | const styles = { 24 | main: { 25 | display: 'flex', 26 | flexDirection: 'column', 27 | justifyContent: 'flex-start', 28 | height: '100%', 29 | }, 30 | }; 31 | 32 | const Menu = ({ onMenuTap, translate, logout }) => ( 33 |
34 | 35 | {items.map(item => ( 36 | 43 | ))} 44 | } 48 | onClick={onMenuTap} 49 | /> 50 | {logout} 51 |
52 | ); 53 | 54 | const enhance = compose( 55 | connect(state => ({ 56 | theme: state.theme, 57 | locale: state.locale, 58 | })), 59 | translate, 60 | ); 61 | 62 | export default enhance(Menu); 63 | -------------------------------------------------------------------------------- /src/authClient.js: -------------------------------------------------------------------------------- 1 | import { AUTH_LOGIN, AUTH_LOGOUT, AUTH_CHECK, AUTH_ERROR } from 'admin-on-rest'; 2 | 3 | export default (type, params) => { 4 | if (type === AUTH_LOGIN) { 5 | const { username } = params; 6 | localStorage.setItem('username', username); 7 | // accept all username/password combinations 8 | return Promise.resolve(); 9 | } 10 | if (type === AUTH_LOGOUT) { 11 | localStorage.removeItem('username'); 12 | return Promise.resolve(); 13 | } 14 | if (type === AUTH_ERROR) { 15 | return Promise.resolve(); 16 | } 17 | if (type === AUTH_CHECK) { 18 | return localStorage.getItem('username') ? Promise.resolve() : Promise.reject(); 19 | } 20 | return Promise.reject('Unkown method'); 21 | }; 22 | -------------------------------------------------------------------------------- /src/buttons/DeleteButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Link } from 'react-router-dom'; 4 | import IconButton from 'material-ui/IconButton'; 5 | import {cyan500} from 'material-ui/styles/colors'; 6 | import ActionDelete from 'material-ui/svg-icons/action/delete'; 7 | 8 | const DeleteButton = ({ basePath = '', record = {} }) => ( 9 | } 11 | style={{ overflow: 'inherit' }} 12 | > 13 | 14 | 15 | ); 16 | 17 | DeleteButton.propTypes = { 18 | basePath: PropTypes.string, 19 | record: PropTypes.object, 20 | }; 21 | 22 | DeleteButton.defaultProps = { 23 | style: { padding: 0 }, 24 | }; 25 | 26 | export default DeleteButton; 27 | -------------------------------------------------------------------------------- /src/buttons/EditButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Link } from 'react-router-dom'; 4 | import IconButton from 'material-ui/IconButton'; 5 | import {cyan500} from 'material-ui/styles/colors'; 6 | import ContentCreate from 'material-ui/svg-icons/content/create'; 7 | 8 | const EditButton = ({ basePath = '', record = {} }) => ( 9 | } 11 | style={{ overflow: 'inherit' }} 12 | > 13 | 14 | 15 | ); 16 | 17 | EditButton.propTypes = { 18 | basePath: PropTypes.string, 19 | record: PropTypes.object, 20 | }; 21 | 22 | EditButton.defaultProps = { 23 | style: { padding: 0 }, 24 | }; 25 | 26 | export default EditButton; 27 | -------------------------------------------------------------------------------- /src/categories/LinkToRelatedProducts.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import FlatButton from 'material-ui/FlatButton'; 3 | import { Link } from 'react-router-dom'; 4 | import { translate } from 'admin-on-rest'; 5 | import { stringify } from 'query-string'; 6 | 7 | import { ProductIcon } from '../products'; 8 | 9 | const LinkToRelatedProducts = ({ record, translate }) => ( 10 | } 14 | containerElement={} 20 | /> 21 | ); 22 | 23 | export default translate(LinkToRelatedProducts); 24 | -------------------------------------------------------------------------------- /src/categories/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | translate, 4 | Datagrid, 5 | Edit, 6 | EditButton, 7 | List, 8 | NumberField, 9 | ReferenceManyField, 10 | SimpleForm, 11 | TextField, 12 | TextInput, 13 | } from 'admin-on-rest'; 14 | import Icon from 'material-ui/svg-icons/action/bookmark'; 15 | 16 | import ThumbnailField from '../products/ThumbnailField'; 17 | import ProductRefField from '../products/ProductRefField'; 18 | import LinkToRelatedProducts from './LinkToRelatedProducts'; 19 | 20 | export const CategoryIcon = Icon; 21 | 22 | export const CategoryList = (props) => ( 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ); 31 | 32 | const CategoryTitle = translate(({ record, translate }) => {translate('resources.categories.name', { smart_count: 1 })} "{record.name}"); 33 | 34 | export const CategoryEdit = (props) => ( 35 | } {...props}> 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | ); 52 | -------------------------------------------------------------------------------- /src/commands/Basket.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { 4 | Table, 5 | TableBody, 6 | TableHeader, 7 | TableHeaderColumn, 8 | TableRow, 9 | TableRowColumn, 10 | } from 'material-ui/Table'; 11 | import Paper from 'material-ui/Paper'; 12 | import { translate, crudGetMany as crudGetManyAction } from 'admin-on-rest'; 13 | import compose from 'recompose/compose'; 14 | 15 | class Basket extends Component { 16 | componentDidMount() { 17 | this.fetchData(); 18 | } 19 | fetchData() { 20 | const { record: { basket }, crudGetMany } = this.props; 21 | crudGetMany('products', basket.map(item => item.product_id)); 22 | } 23 | render() { 24 | const { record, products, translate } = this.props; 25 | const { basket } = record; 26 | return ( 27 | 28 | 29 | 30 | 31 | 32 | {translate('resources.commands.fields.basket.reference')} 33 | 34 | 35 | {translate('resources.commands.fields.basket.unit_price')} 36 | 37 | 38 | {translate('resources.commands.fields.basket.quantity')} 39 | 40 | 41 | {translate('resources.commands.fields.basket.total')} 42 | 43 | 44 | 45 | 46 | {basket.map(item => products[item.product_id] && ( 47 | 48 | 49 | {products[item.product_id].reference} 50 | 51 | 52 | {products[item.product_id].price.toLocaleString(undefined, { style: 'currency', currency: 'USD' })} 53 | 54 | 55 | {item.quantity} 56 | 57 | 58 | {(products[item.product_id].price * item.quantity).toLocaleString(undefined, { style: 'currency', currency: 'USD' })} 59 | 60 | ) 61 | )} 62 | 63 | 64 | {translate('resources.commands.fields.basket.sum')} 65 | 66 | {record.total_ex_taxes.toLocaleString(undefined, { style: 'currency', currency: 'USD' })} 67 | 68 | 69 | 70 | 71 | {translate('resources.commands.fields.basket.delivery')} 72 | 73 | {record.delivery_fees.toLocaleString(undefined, { style: 'currency', currency: 'USD' })} 74 | 75 | 76 | 77 | 78 | {translate('resources.commands.fields.basket.tax_rate')} 79 | 80 | {record.tax_rate.toLocaleString(undefined, { style: 'percent' })} 81 | 82 | 83 | 84 | 85 | {translate('resources.commands.fields.basket.total')} 86 | 87 | {record.total.toLocaleString(undefined, { style: 'currency', currency: 'USD' })} 88 | 89 | 90 | 91 |
92 |
93 | ); 94 | } 95 | } 96 | 97 | const mapStateToProps = (state, props) => { 98 | const { record: { basket } } = props; 99 | const productIds = basket.map(item => item.product_id); 100 | return { 101 | products: productIds 102 | .map(productId => state.admin.resources.products.data[productId]) 103 | .filter(r => typeof r !== 'undefined') 104 | .reduce((prev, next) => { 105 | prev[next.id] = next; 106 | return prev; 107 | }, {}), 108 | }; 109 | }; 110 | 111 | const enhance = compose( 112 | translate, 113 | connect(mapStateToProps, { 114 | crudGetMany: crudGetManyAction, 115 | }) 116 | ); 117 | 118 | export default enhance(Basket); 119 | -------------------------------------------------------------------------------- /src/commands/NbItemsField.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FunctionField } from 'admin-on-rest'; 3 | 4 | const render = record => record.basket.length; 5 | 6 | const NbItemsField = (props) => ; 7 | 8 | NbItemsField.defaultProps = { 9 | label: 'Nb Items', 10 | style: { textAlign: 'right' }, 11 | headerStyle: { textAlign: 'right' }, 12 | }; 13 | 14 | export default NbItemsField; 15 | -------------------------------------------------------------------------------- /src/commands/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | translate, 4 | AutocompleteInput, 5 | BooleanField, 6 | BooleanInput, 7 | Datagrid, 8 | DateField, 9 | DateInput, 10 | Edit, 11 | EditButton, 12 | Filter, 13 | List, 14 | NullableBooleanInput, 15 | NumberField, 16 | ReferenceInput, 17 | SelectInput, 18 | SimpleForm, 19 | TextField, 20 | TextInput, 21 | } from 'admin-on-rest'; 22 | import Icon from 'material-ui/svg-icons/editor/attach-money'; 23 | 24 | import Basket from './Basket'; 25 | import NbItemsField from './NbItemsField'; 26 | import CustomerReferenceField from '../visitors/CustomerReferenceField'; 27 | 28 | export const CommandIcon = Icon; 29 | 30 | const CommandFilter = (props) => ( 31 | 32 | 33 | 34 | `${choice.first_name} ${choice.last_name}`} /> 35 | 36 | 41 | 42 | 43 | 44 | 45 | 46 | ); 47 | 48 | export const CommandList = (props) => ( 49 | } sort={{ field: 'date', order: 'DESC' }} perPage={25}> 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | ); 62 | 63 | const CommandTitle = translate(({ record, translate }) => {translate('resources.commands.name', { smart_count: 1 })} #{record.reference}); 64 | 65 | export const CommandEdit = translate(({ translate, ...rest }) => ( 66 | } {...rest}> 67 | 68 | 69 | 70 | 71 | `${choice.first_name} ${choice.last_name}`} /> 72 | 73 | 78 | 79 |
80 | 81 | 82 | )); 83 | -------------------------------------------------------------------------------- /src/configuration/Configuration.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Card, CardText } from 'material-ui/Card'; 4 | import RaisedButton from 'material-ui/RaisedButton'; 5 | import { translate, changeLocale as changeLocaleAction, ViewTitle } from 'admin-on-rest'; 6 | 7 | import { changeTheme as changeThemeAction } from './actions'; 8 | 9 | const styles = { 10 | label: { width: '10em', display: 'inline-block' }, 11 | button: { margin: '1em' }, 12 | }; 13 | 14 | const Configuration = ({ theme, locale, changeTheme, changeLocale, translate }) => ( 15 | 16 | 17 | 18 |
{translate('pos.theme.name')}
19 | changeTheme('light')} /> 20 | changeTheme('dark')} /> 21 |
22 | 23 |
{translate('pos.language')}
24 | changeLocale('en')} /> 25 | changeLocale('fr')} /> 26 |
27 |
28 | ); 29 | 30 | const mapStateToProps = state => ({ 31 | theme: state.theme, 32 | locale: state.locale, 33 | }); 34 | 35 | export default connect(mapStateToProps, { 36 | changeLocale: changeLocaleAction, 37 | changeTheme: changeThemeAction, 38 | })(translate(Configuration)); 39 | -------------------------------------------------------------------------------- /src/configuration/actions.js: -------------------------------------------------------------------------------- 1 | export const CHANGE_THEME = 'CHANGE_THEME'; 2 | 3 | export const changeTheme = theme => ({ 4 | type: CHANGE_THEME, 5 | payload: theme, 6 | }); 7 | -------------------------------------------------------------------------------- /src/dashboard/Dashboard.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import withWidth from 'material-ui/utils/withWidth'; 3 | import { AppBarMobile, GET_LIST, GET_MANY } from 'admin-on-rest'; 4 | 5 | import Welcome from './Welcome'; 6 | import MonthlyRevenue from './MonthlyRevenue'; 7 | import NbNewOrders from './NbNewOrders'; 8 | import PendingOrders from './PendingOrders'; 9 | import PendingReviews from './PendingReviews'; 10 | import NewCustomers from './NewCustomers'; 11 | import restClient from '../restClient'; 12 | 13 | const styles = { 14 | welcome: { marginBottom: '2em' }, 15 | flex: { display: 'flex' }, 16 | leftCol: { flex: 1, marginRight: '1em' }, 17 | rightCol: { flex: 1, marginLeft: '1em' }, 18 | singleCol: { marginTop: '2em' }, 19 | }; 20 | 21 | class Dashboard extends Component { 22 | state = {}; 23 | 24 | componentDidMount() { 25 | const d = new Date(); 26 | d.setDate(d.getDate() - 30); 27 | restClient(GET_LIST, 'commands', { 28 | filter: { date_gte: d.toISOString() }, 29 | sort: { field: 'date', order: 'DESC' }, 30 | pagination: { page: 1, perPage: 50 }, 31 | }) 32 | .then(response => response.data 33 | .filter(order => order.status !== 'cancelled') 34 | .reduce((stats, order) => { 35 | if (order.status !== 'cancelled') { 36 | stats.revenue += order.total; 37 | stats.nbNewOrders++; 38 | } 39 | if (order.status === 'ordered') { 40 | stats.pendingOrders.push(order); 41 | } 42 | return stats; 43 | }, { revenue: 0, nbNewOrders: 0, pendingOrders: [] }) 44 | ) 45 | .then(({ revenue, nbNewOrders, pendingOrders }) => { 46 | this.setState({ 47 | revenue: revenue.toLocaleString(undefined, { 48 | style: 'currency', 49 | currency: 'USD', 50 | minimumFractionDigits: 0, 51 | maximumFractionDigits: 0, 52 | }), 53 | nbNewOrders, 54 | pendingOrders, 55 | }); 56 | return pendingOrders; 57 | }) 58 | .then(pendingOrders => pendingOrders.map(order => order.customer_id)) 59 | .then(customerIds => restClient(GET_MANY, 'customers', { ids: customerIds })) 60 | .then(response => response.data) 61 | .then(customers => customers.reduce((prev, customer) => { 62 | prev[customer.id] = customer; // eslint-disable-line no-param-reassign 63 | return prev; 64 | }, {})) 65 | .then(customers => this.setState({ pendingOrdersCustomers: customers })); 66 | 67 | restClient(GET_LIST, 'reviews', { 68 | filter: { status: 'pending' }, 69 | sort: { field: 'date', order: 'DESC' }, 70 | pagination: { page: 1, perPage: 100 }, 71 | }) 72 | .then(response => response.data) 73 | .then(reviews => { 74 | const nbPendingReviews = reviews.reduce(nb => ++nb, 0); 75 | const pendingReviews = reviews.slice(0, Math.min(10, reviews.length)); 76 | this.setState({ pendingReviews, nbPendingReviews }); 77 | return pendingReviews; 78 | }) 79 | .then(reviews => reviews.map(review => review.customer_id)) 80 | .then(customerIds => restClient(GET_MANY, 'customers', { ids: customerIds })) 81 | .then(response => response.data) 82 | .then(customers => customers.reduce((prev, customer) => { 83 | prev[customer.id] = customer; // eslint-disable-line no-param-reassign 84 | return prev; 85 | }, {})) 86 | .then(customers => this.setState({ pendingReviewsCustomers: customers })); 87 | 88 | restClient(GET_LIST, 'customers', { 89 | filter: { has_ordered: true, first_seen_gte: d.toISOString() }, 90 | sort: { field: 'first_seen', order: 'DESC' }, 91 | pagination: { page: 1, perPage: 100 }, 92 | }) 93 | .then(response => response.data) 94 | .then(newCustomers => { 95 | this.setState({ newCustomers }); 96 | this.setState({ nbNewCustomers: newCustomers.reduce(nb => ++nb, 0) }) 97 | }) 98 | } 99 | 100 | render() { 101 | const { 102 | nbNewCustomers, 103 | nbNewOrders, 104 | nbPendingReviews, 105 | newCustomers, 106 | pendingOrders, 107 | pendingOrdersCustomers, 108 | pendingReviews, 109 | pendingReviewsCustomers, 110 | revenue, 111 | } = this.state; 112 | const { width } = this.props; 113 | return ( 114 |
115 | {width === 1 && } 116 | 117 |
118 |
119 |
120 | 121 | 122 |
123 |
124 | 125 |
126 |
127 |
128 |
129 | 130 | 131 |
132 |
133 |
134 |
135 | ); 136 | } 137 | } 138 | 139 | export default withWidth()(Dashboard); 140 | -------------------------------------------------------------------------------- /src/dashboard/MonthlyRevenue.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Card, CardTitle } from 'material-ui/Card'; 3 | import DollarIcon from 'material-ui/svg-icons/editor/attach-money'; 4 | import { translate } from 'admin-on-rest'; 5 | 6 | const styles = { 7 | card: { borderLeft: 'solid 4px #31708f', flex: '1', marginRight: '1em' }, 8 | icon: { float: 'right', width: 64, height: 64, padding: 16, color: '#31708f' }, 9 | }; 10 | 11 | export default translate(({ value, translate }) => ( 12 | 13 | 14 | 15 | 16 | )); 17 | -------------------------------------------------------------------------------- /src/dashboard/NbNewOrders.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Card, CardTitle } from 'material-ui/Card'; 3 | import ShoppingCartIcon from 'material-ui/svg-icons/action/shopping-cart'; 4 | import { translate } from 'admin-on-rest'; 5 | 6 | const styles = { 7 | card: { borderLeft: 'solid 4px #ff9800', flex: 1, marginLeft: '1em' }, 8 | icon: { float: 'right', width: 64, height: 64, padding: 16, color: '#ff9800' }, 9 | }; 10 | 11 | export default translate(({ value, translate }) => ( 12 | 13 | 14 | 15 | 16 | )); 17 | -------------------------------------------------------------------------------- /src/dashboard/NewCustomers.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Card, CardTitle } from 'material-ui/Card'; 3 | import { List, ListItem } from 'material-ui/List'; 4 | import Avatar from 'material-ui/Avatar'; 5 | import CustomerIcon from 'material-ui/svg-icons/social/person-add'; 6 | import { translate } from 'admin-on-rest'; 7 | 8 | const styles = { 9 | card: { borderLeft: 'solid 4px #4caf50', flex: 1, marginLeft: '1em' }, 10 | icon: { float: 'right', width: 64, height: 64, padding: 16, color: '#4caf50' }, 11 | }; 12 | 13 | export default translate(({ visitors = [], nb, translate }) => ( 14 | 15 | 16 | 17 | 18 | {visitors.map(record => 19 | } > 20 | {record.first_name} {record.last_name} 21 | 22 | )} 23 | 24 | 25 | )); 26 | -------------------------------------------------------------------------------- /src/dashboard/PendingOrders.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Card, CardTitle } from 'material-ui/Card'; 3 | import { List, ListItem } from 'material-ui/List'; 4 | import Avatar from 'material-ui/Avatar'; 5 | import { translate } from 'admin-on-rest'; 6 | 7 | const style = { flex: 1 }; 8 | 9 | export default translate(({ orders = [], customers = {}, translate }) => ( 10 | 11 | 12 | 13 | {orders.map(record => 14 | 20 | {translate('pos.dashboard.order.items', { 21 | smart_count: record.basket.length, 22 | nb_items: record.basket.length, 23 | customer_name: customers[record.customer_id] ? `${customers[record.customer_id].first_name} ${customers[record.customer_id].last_name}` : '' 24 | })} 25 |

26 | } 27 | rightAvatar={{record.total}$} 28 | leftAvatar={customers[record.customer_id] ? : } 29 | /> 30 | )} 31 |
32 |
33 | )); 34 | -------------------------------------------------------------------------------- /src/dashboard/PendingReviews.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Card, CardTitle } from 'material-ui/Card'; 3 | import { List, ListItem } from 'material-ui/List'; 4 | import CommentIcon from 'material-ui/svg-icons/communication/comment'; 5 | import Avatar from 'material-ui/Avatar'; 6 | import { Link } from 'react-router-dom'; 7 | import { translate } from 'admin-on-rest'; 8 | 9 | import StarRatingField from '../reviews/StarRatingField'; 10 | 11 | const styles = { 12 | titleLink: { textDecoration: 'none', color: '#000' }, 13 | card: { borderLeft: 'solid 4px #f44336', flex: 1, marginRight: '1em' }, 14 | icon: { float: 'right', width: 64, height: 64, padding: 16, color: '#f44336' }, 15 | }; 16 | 17 | const location = { pathname: 'reviews', query: { filter: JSON.stringify({ status: 'pending' }) } }; 18 | 19 | export default translate(({ reviews = [], customers = {}, nb, translate }) => ( 20 | 21 | 22 | {nb}} subtitle={translate('pos.dashboard.pending_reviews')} /> 23 | 24 | {reviews.map(record => 25 | } 29 | secondaryText={record.comment} 30 | secondaryTextLines={2} 31 | leftAvatar={customers[record.customer_id] ? : } 32 | /> 33 | )} 34 | 35 | 36 | )); 37 | -------------------------------------------------------------------------------- /src/dashboard/Welcome.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Card, CardHeader, CardActions } from 'material-ui/Card'; 3 | import Avatar from 'material-ui/Avatar'; 4 | import LightBulbIcon from 'material-ui/svg-icons/action/lightbulb-outline'; 5 | import HomeIcon from 'material-ui/svg-icons/action/home'; 6 | import CodeIcon from 'material-ui/svg-icons/action/code'; 7 | import FlatButton from 'material-ui/FlatButton'; 8 | import { translate } from 'admin-on-rest'; 9 | 10 | export default translate(({ style, translate }) => ( 11 | 12 | } />} 16 | /> 17 | 18 | } href="https://marmelab.com/admin-on-rest/" /> 19 | } href="https://github.com/marmelab/admin-on-rest-demo" /> 20 | 21 | 22 | )); 23 | -------------------------------------------------------------------------------- /src/dashboard/index.js: -------------------------------------------------------------------------------- 1 | import DashboardComponent from './Dashboard'; 2 | 3 | export const Dashboard = DashboardComponent; 4 | -------------------------------------------------------------------------------- /src/i18n/en.js: -------------------------------------------------------------------------------- 1 | export default { 2 | pos: { 3 | search: 'Search', 4 | configuration: 'Configuration', 5 | language: 'Language', 6 | theme: { 7 | name: 'Theme', 8 | light: 'Light', 9 | dark: 'Dark', 10 | }, 11 | dashboard: { 12 | monthly_revenue: 'Monthly Revenue', 13 | new_orders: 'New Orders', 14 | pending_reviews: 'Pending Reviews', 15 | new_customers: 'New Customers', 16 | pending_orders: 'Pending Orders', 17 | order: { 18 | items: 'by %{customer_name}, one item |||| by %{customer_name}, %{nb_items} items', 19 | }, 20 | welcome: { 21 | title: 'Welcome to admin-on-rest demo', 22 | subtitle: 'This is the admin of an imaginary poster shop. Fell free to explore and modify the data - it\'s local to your computer, and will reset each time you reload.', 23 | aor_button: 'Admin-on-rest website', 24 | demo_button: 'Source for this demo', 25 | }, 26 | }, 27 | }, 28 | resources: { 29 | customers: { 30 | name: 'Customer |||| Customers', 31 | fields: { 32 | commands: 'Orders', 33 | groups: 'Segments', 34 | last_seen_gte: 'Visited Since', 35 | name: 'Name', 36 | }, 37 | tabs: { 38 | identity: 'Identity', 39 | address: 'Address', 40 | orders: 'Orders', 41 | reviews: 'Reviews', 42 | stats: 'Stats', 43 | }, 44 | page: { 45 | delete: 'Delete Customer', 46 | }, 47 | 48 | }, 49 | commands: { 50 | name: 'Order |||| Orders', 51 | fields: { 52 | basket: { 53 | delivery: 'Delivery', 54 | reference: 'Reference', 55 | quantity: 'Quantity', 56 | sum: 'Sum', 57 | tax_rate: 'Tax Rate', 58 | total: 'Total', 59 | unit_price: 'Unit Price', 60 | }, 61 | customer_id: 'Customer', 62 | date_gte: 'Passed Since', 63 | date_lte: 'Passed Before', 64 | total_gte: 'Min amount', 65 | }, 66 | }, 67 | products: { 68 | name: 'Poster |||| Posters', 69 | fields: { 70 | category_id: 'Category', 71 | height_gte: 'Min height', 72 | height_lte: 'Max height', 73 | height: 'Height', 74 | image: 'Image', 75 | price: 'Price', 76 | reference: 'Reference', 77 | stock_lte: 'Low Stock', 78 | stock: 'Stock', 79 | thumbnail: 'Thumbnail', 80 | width_gte: 'Min width', 81 | width_lte: 'mx_width', 82 | width: 'Width', 83 | }, 84 | tabs: { 85 | image: 'Image', 86 | details: 'Details', 87 | description: 'Description', 88 | reviews: 'Reviews', 89 | }, 90 | }, 91 | categories: { 92 | name: 'Category |||| Categories', 93 | fields: { 94 | products: 'Products', 95 | }, 96 | 97 | }, 98 | reviews: { 99 | name: 'Review |||| Reviews', 100 | fields: { 101 | customer_id: 'Customer', 102 | command_id: 'Order', 103 | product_id: 'Product', 104 | date_gte: 'Posted since', 105 | date_lte: 'Posted before', 106 | date: 'Date', 107 | comment: 'Comment', 108 | rating: 'Rating', 109 | }, 110 | action: { 111 | accept: 'Accept', 112 | reject: 'Reject', 113 | }, 114 | notification: { 115 | approved_success: 'Review approved', 116 | approved_error: 'Error: Review not approved', 117 | rejected_success: 'Review rejected', 118 | rejected_error: 'Error: Review not rejected', 119 | }, 120 | }, 121 | segments: { 122 | name: 'Segments', 123 | fields: { 124 | customers: 'Customers', 125 | name: 'Name', 126 | }, 127 | data: { 128 | compulsive: 'Compulsive', 129 | collector: 'Collector', 130 | ordered_once: 'Ordered once', 131 | regular: 'Regular', 132 | returns: 'Returns', 133 | reviewer: 'Reviewer', 134 | }, 135 | }, 136 | }, 137 | }; 138 | -------------------------------------------------------------------------------- /src/i18n/fr.js: -------------------------------------------------------------------------------- 1 | export default { 2 | pos: { 3 | search: 'Rechercher', 4 | configuration: 'Configuration', 5 | language: 'Langue', 6 | theme: { 7 | name: 'Theme', 8 | light: 'Clair', 9 | dark: 'Obscur', 10 | }, 11 | dashboard: { 12 | monthly_revenue: 'CA à 30 jours', 13 | new_orders: 'Nouvelles commandes', 14 | pending_reviews: 'Commentaires à modérer', 15 | new_customers: 'Nouveaux clients', 16 | pending_orders: 'Commandes à traiter', 17 | order: { 18 | items: 'par %{customer_name}, un poster |||| par %{customer_name}, %{nb_items} posters', 19 | }, 20 | welcome: { 21 | title: 'Bienvenue sur la démo d\'admin-on-rest', 22 | subtitle: 'Ceci est le back-office d\'un magasin de posters imaginaire. N\'hésitez pas à explorer et à modifier les données. La démo s\'exécute en local dans votre navigateur, et se remet à zéro chaque fois que vous rechargez la page.', 23 | aor_button: 'Site web d\'admin-on-rest', 24 | demo_button: 'Code source de cette démo', 25 | }, 26 | }, 27 | }, 28 | resources: { 29 | customers: { 30 | name: 'Client |||| Clients', 31 | fields: { 32 | address: 'Rue', 33 | birthday: 'Anniversaire', 34 | city: 'Ville', 35 | commands: 'Commandes', 36 | first_name: 'Prénom', 37 | first_seen: 'Première visite', 38 | groups: 'Segments', 39 | has_newsletter: 'Abonné à la newsletter', 40 | has_ordered: 'A commandé', 41 | last_name: 'Nom', 42 | last_seen: 'Vu le', 43 | last_seen_gte: 'Vu depuis', 44 | latest_purchase: 'Dernier achat', 45 | name: 'Nom', 46 | total_spent: 'Dépenses', 47 | zipcode: 'Code postal', 48 | }, 49 | tabs: { 50 | identity: 'Identité', 51 | address: 'Adresse', 52 | orders: 'Commandes', 53 | reviews: 'Commentaires', 54 | stats: 'Statistiques', 55 | }, 56 | page: { 57 | delete: 'Supprimer le client', 58 | }, 59 | }, 60 | commands: { 61 | name: 'Commande |||| Commandes', 62 | fields: { 63 | basket: { 64 | delivery: 'Frais de livraison', 65 | reference: 'Référence', 66 | quantity: 'Quantité', 67 | sum: 'Sous-total', 68 | tax_rate: 'TVA', 69 | total: 'Total', 70 | unit_price: 'P.U.', 71 | }, 72 | customer_id: 'Client', 73 | date_gte: 'Passées depuis', 74 | date_lte: 'Passées avant', 75 | nb_items: 'Nb articles', 76 | reference: 'Référence', 77 | returned: 'Annulée', 78 | status: 'Etat', 79 | total_gte: 'Montant minimum', 80 | }, 81 | }, 82 | products: { 83 | name: 'Poster |||| Posters', 84 | fields: { 85 | category_id: 'Catégorie', 86 | height_gte: 'Hauteur mini', 87 | height_lte: 'Hauteur maxi', 88 | height: 'Hauteur', 89 | image: 'Photo', 90 | price: 'Prix', 91 | reference: 'Référence', 92 | stock_lte: 'Stock faible', 93 | stock: 'Stock', 94 | thumbnail: 'Aperçu', 95 | width_gte: 'Largeur mini', 96 | width_lte: 'Margeur maxi', 97 | width: 'Largeur', 98 | }, 99 | tabs: { 100 | image: 'Image', 101 | details: 'Détails', 102 | description: 'Description', 103 | reviews: 'Commentaires', 104 | }, 105 | }, 106 | categories: { 107 | name: 'Catégorie |||| Catégories', 108 | fields: { 109 | name: 'Nom', 110 | products: 'Produits', 111 | }, 112 | }, 113 | reviews: { 114 | name: 'Commentaire |||| Commentaires', 115 | fields: { 116 | customer_id: 'Client', 117 | command_id: 'Commande', 118 | product_id: 'Produit', 119 | date_gte: 'Publié depuis', 120 | date_lte: 'Publié avant', 121 | date: 'Date', 122 | comment: 'Texte', 123 | status: 'Statut', 124 | rating: 'Classement', 125 | }, 126 | action: { 127 | accept: 'Accepter', 128 | reject: 'Rejeter', 129 | }, 130 | notification: { 131 | approved_success: 'Commentaire approuvé', 132 | approved_error: 'Erreur: Commentaire non approuvé', 133 | rejected_success: 'Commentaire rejeté', 134 | rejected_error: 'Erreur: Commentaire non rejeté', 135 | }, 136 | }, 137 | segments: { 138 | name: 'Segments', 139 | fields: { 140 | customers: 'Clients', 141 | name: 'Nom', 142 | }, 143 | data: { 144 | compulsive: 'Compulsif', 145 | collector: 'Collectionneur', 146 | ordered_once: 'A commandé', 147 | regular: 'Régulier', 148 | returns: 'A renvoyé', 149 | reviewer: 'Commentateur', 150 | }, 151 | }, 152 | }, 153 | }; 154 | -------------------------------------------------------------------------------- /src/i18n/index.js: -------------------------------------------------------------------------------- 1 | import { englishMessages } from 'admin-on-rest'; 2 | import frenchMessages from 'aor-language-french'; 3 | 4 | import customFrenchMessages from './fr'; 5 | import customEnglishMessages from './en'; 6 | 7 | export default { 8 | fr: { ...frenchMessages, ...customFrenchMessages }, 9 | en: { ...englishMessages, ...customEnglishMessages }, 10 | }; 11 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import './index.css'; 5 | 6 | // This one is already present in admin-on-rest Layout, but it seems it does nothing if called after the initial ReactDOM.render() 7 | // @link https://github.com/callemall/material-ui/issues/4670#issuecomment-235031917 8 | import injectTapEventPlugin from 'react-tap-event-plugin'; 9 | try { 10 | injectTapEventPlugin(); 11 | } catch (e) { 12 | // do nothing 13 | } 14 | 15 | ReactDOM.render( 16 | , 17 | document.getElementById('root') 18 | ); 19 | -------------------------------------------------------------------------------- /src/products/GridList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { GridList as MuiGridList, GridTile } from 'material-ui/GridList'; 3 | import { NumberField, EditButton } from 'admin-on-rest'; 4 | 5 | const styles = { 6 | root: { 7 | margin: '-2px', 8 | }, 9 | gridList: { 10 | width: '100%', 11 | margin: 0, 12 | }, 13 | }; 14 | 15 | const GridList = ({ ids, isLoading, data, currentSort, basePath, rowStyle }) => ( 16 |
17 | 18 | {ids.map((id) => ( 19 | {data[id].width}x{data[id].height}, } 23 | actionIcon={} 24 | titleBackground="linear-gradient(to top, rgba(0,0,0,0.8) 0%,rgba(0,0,0,0.4) 70%,rgba(0,0,0,0) 100%)" 25 | > 26 | 27 | 28 | ))} 29 | 30 |
31 | ); 32 | 33 | export default GridList; 34 | -------------------------------------------------------------------------------- /src/products/Poster.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Card, CardMedia } from 'material-ui/Card'; 3 | 4 | const Poster = ({ record }) => ( 5 | 6 | 7 | 8 | 9 | 10 | ); 11 | 12 | export default Poster; 13 | -------------------------------------------------------------------------------- /src/products/ProductRefField.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | const ProductRefField = ({ record, basePath }) => 5 | {record.reference}; 6 | 7 | ProductRefField.defaultProps = { 8 | source: 'id', 9 | label: 'Reference', 10 | }; 11 | 12 | export default ProductRefField; 13 | -------------------------------------------------------------------------------- /src/products/ProductReferenceField.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ReferenceField, TextField } from 'admin-on-rest'; 3 | 4 | const ProductReferenceField = (props) => ( 5 | 6 | 7 | 8 | ) 9 | ProductReferenceField.defaultProps = { 10 | source: 'product_id', 11 | addLabel: true, 12 | }; 13 | 14 | export default ProductReferenceField; 15 | -------------------------------------------------------------------------------- /src/products/ThumbnailField.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const ThumbnailField = ({ record }) => ; 4 | 5 | ThumbnailField.defaultProps = { 6 | style: { padding: '0 0 0 16px' }, 7 | }; 8 | 9 | export default ThumbnailField; 10 | -------------------------------------------------------------------------------- /src/products/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | translate, 4 | Create, 5 | Datagrid, 6 | DateField, 7 | Edit, 8 | EditButton, 9 | Filter, 10 | FormTab, 11 | List, 12 | NumberInput, 13 | ReferenceInput, 14 | ReferenceManyField, 15 | SelectInput, 16 | TabbedForm, 17 | TextField, 18 | TextInput, 19 | } from 'admin-on-rest'; 20 | import Icon from 'material-ui/svg-icons/image/collections'; 21 | import Chip from 'material-ui/Chip'; 22 | import RichTextInput from 'aor-rich-text-input'; 23 | 24 | import CustomerReferenceField from '../visitors/CustomerReferenceField'; 25 | import StarRatingField from '../reviews/StarRatingField'; 26 | import GridList from './GridList'; 27 | import Poster from './Poster'; 28 | 29 | export const ProductIcon = Icon; 30 | 31 | const QuickFilter = translate(({ label, translate }) => {translate(label)}); 32 | 33 | export const ProductFilter = props => ( 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | ); 46 | 47 | export const ProductList = props => ( 48 | } perPage={20}> 49 | 50 | 51 | ); 52 | 53 | export const ProductCreate = (props) => ( 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | ); 76 | 77 | const ProductTitle = ({ record }) => Poster #{record.reference}; 78 | export const ProductEdit = (props) => ( 79 | }> 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | ); 114 | -------------------------------------------------------------------------------- /src/restClient.js: -------------------------------------------------------------------------------- 1 | import { simpleRestClient } from 'admin-on-rest'; 2 | 3 | const restClient = simpleRestClient('http://localhost:3000'); 4 | export default (type, resource, params) => new Promise(resolve => setTimeout(() => resolve(restClient(type, resource, params)), 500)); 5 | -------------------------------------------------------------------------------- /src/restServer.js: -------------------------------------------------------------------------------- 1 | /* global data */ 2 | import FakeRest from 'fakerest'; 3 | import fetchMock from 'fetch-mock'; 4 | 5 | export default () => { 6 | const restServer = new FakeRest.FetchServer('http://localhost:3000'); 7 | restServer.init(data); 8 | restServer.toggleLogging(); // logging is off by default, enable it 9 | fetchMock.mock('^http://localhost:3000', restServer.getHandler()); 10 | return () => fetchMock.restore(); 11 | }; 12 | -------------------------------------------------------------------------------- /src/reviews/AcceptButton.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | import FlatButton from 'material-ui/FlatButton'; 5 | import ThumbUp from 'material-ui/svg-icons/action/thumb-up'; 6 | import { translate } from 'admin-on-rest'; 7 | import compose from 'recompose/compose'; 8 | import { reviewApprove as reviewApproveAction } from './reviewActions'; 9 | 10 | class AcceptButton extends Component { 11 | handleApprove = () => { 12 | const { reviewApprove, record } = this.props; 13 | reviewApprove(record.id, record); 14 | } 15 | 16 | render() { 17 | const { record, translate } = this.props; 18 | return record && record.status === 'pending' ? } 23 | /> : ; 24 | } 25 | } 26 | 27 | AcceptButton.propTypes = { 28 | record: PropTypes.object, 29 | reviewApprove: PropTypes.func, 30 | translate: PropTypes.func, 31 | }; 32 | 33 | const enhance = compose( 34 | translate, 35 | connect(null, { 36 | reviewApprove: reviewApproveAction, 37 | }) 38 | ); 39 | 40 | export default enhance(AcceptButton); 41 | -------------------------------------------------------------------------------- /src/reviews/ApproveButton.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | import IconButton from 'material-ui/IconButton'; 5 | import ThumbUp from 'material-ui/svg-icons/action/thumb-up'; 6 | import ThumbDown from 'material-ui/svg-icons/action/thumb-down'; 7 | import { reviewApprove as reviewApproveAction, reviewReject as reviewRejectAction } from './reviewActions'; 8 | 9 | class ApproveButton extends Component { 10 | handleApprove = () => { 11 | const { reviewApprove, record } = this.props; 12 | reviewApprove(record.id, record); 13 | } 14 | 15 | handleReject = () => { 16 | const { reviewReject, record } = this.props; 17 | reviewReject(record.id, record); 18 | } 19 | 20 | render() { 21 | const { record } = this.props; 22 | return ( 23 | 24 | 25 | 26 | 27 | ); 28 | } 29 | } 30 | 31 | ApproveButton.propTypes = { 32 | record: PropTypes.object, 33 | reviewApprove: PropTypes.func, 34 | reviewReject: PropTypes.func, 35 | }; 36 | 37 | export default connect(null, { 38 | reviewApprove: reviewApproveAction, 39 | reviewReject: reviewRejectAction, 40 | })(ApproveButton); 41 | -------------------------------------------------------------------------------- /src/reviews/RejectButton.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | import FlatButton from 'material-ui/FlatButton'; 5 | import ThumbDown from 'material-ui/svg-icons/action/thumb-down'; 6 | import { translate } from 'admin-on-rest'; 7 | import compose from 'recompose/compose'; 8 | import { reviewReject as reviewRejectAction } from './reviewActions'; 9 | 10 | class AcceptButton extends Component { 11 | handleApprove = () => { 12 | const { reviewReject, record } = this.props; 13 | reviewReject(record.id, record); 14 | } 15 | 16 | render() { 17 | const { record, translate } = this.props; 18 | return record && record.status === 'pending' ? } 23 | /> : ; 24 | } 25 | } 26 | 27 | AcceptButton.propTypes = { 28 | record: PropTypes.object, 29 | reviewReject: PropTypes.func, 30 | translate: PropTypes.func, 31 | }; 32 | 33 | const enhance = compose( 34 | translate, 35 | connect(null, { 36 | reviewReject: reviewRejectAction, 37 | }) 38 | ); 39 | 40 | export default enhance(AcceptButton); 41 | -------------------------------------------------------------------------------- /src/reviews/ReviewEditActions.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { CardActions } from 'material-ui/Card'; 3 | import { ListButton, DeleteButton, RefreshButton } from 'admin-on-rest'; 4 | import AcceptButton from './AcceptButton'; 5 | import RejectButton from './RejectButton'; 6 | 7 | const cardActionStyle = { 8 | zIndex: 2, 9 | display: 'inline-block', 10 | float: 'right', 11 | }; 12 | 13 | const ReviewEditActions = ({ basePath, data, hasDelete, hasShow, refresh }) => ( 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | 23 | export default ReviewEditActions; 24 | -------------------------------------------------------------------------------- /src/reviews/StarRatingField.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon from 'material-ui/svg-icons/action/stars'; 3 | 4 | const style = { opacity: 0.87, width: 20, height: 20 }; 5 | 6 | const StarRatingField = ({ record }) => ( 7 | 8 | {Array(record.rating).fill(true).map((_, i) => )} 9 | 10 | ); 11 | 12 | StarRatingField.defaultProps = { 13 | label: 'resources.reviews.fields.rating', 14 | source: 'rating', 15 | addLabel: true, 16 | }; 17 | 18 | export default StarRatingField; 19 | -------------------------------------------------------------------------------- /src/reviews/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | AutocompleteInput, 4 | Datagrid, 5 | DateField, 6 | DateInput, 7 | Edit, 8 | EditButton, 9 | Filter, 10 | List, 11 | LongTextInput, 12 | ReferenceField, 13 | ReferenceInput, 14 | SelectInput, 15 | SimpleForm, 16 | TextField, 17 | TextInput, 18 | } from 'admin-on-rest'; 19 | import Icon from 'material-ui/svg-icons/communication/comment'; 20 | 21 | import ProductReferenceField from '../products/ProductReferenceField'; 22 | import CustomerReferenceField from '../visitors/CustomerReferenceField'; 23 | import StarRatingField from './StarRatingField'; 24 | import ApproveButton from './ApproveButton'; 25 | import ReviewEditActions from './ReviewEditActions'; 26 | import rowStyle from './rowStyle'; 27 | 28 | export const ReviewIcon = Icon; 29 | 30 | export const ReviewFilter = (props) => ( 31 | 32 | 33 | 38 | 39 | `${choice.first_name} ${choice.last_name}`} /> 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | ); 48 | 49 | export const ReviewList = (props) => ( 50 | } perPage={25} sort={{ field: 'date', order: 'DESC' }}> 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | ); 63 | 64 | const detailStyle = { display: 'inline-block', verticalAlign: 'top', marginRight: '2em', minWidth: '8em' }; 65 | export const ReviewEdit = (props) => ( 66 | }> 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 81 | 82 | 83 | ) 84 | -------------------------------------------------------------------------------- /src/reviews/reviewActions.js: -------------------------------------------------------------------------------- 1 | import { UPDATE } from 'admin-on-rest'; 2 | 3 | export const REVIEW_APPROVE = 'REVIEW_APPROVE'; 4 | export const REVIEW_APPROVE_LOADING = 'REVIEW_APPROVE_LOADING'; 5 | export const REVIEW_APPROVE_FAILURE = 'REVIEW_APPROVE_FAILURE'; 6 | export const REVIEW_APPROVE_SUCCESS = 'REVIEW_APPROVE_SUCCESS'; 7 | 8 | export const reviewApprove = (id, data, basePath) => ({ 9 | type: REVIEW_APPROVE, 10 | payload: { id, data: { ...data, status: 'accepted' }, basePath }, 11 | meta: { resource: 'reviews', fetch: UPDATE, cancelPrevious: false }, 12 | }); 13 | 14 | export const REVIEW_REJECT = 'REVIEW_REJECT'; 15 | export const REVIEW_REJECT_LOADING = 'REVIEW_REJECT_LOADING'; 16 | export const REVIEW_REJECT_FAILURE = 'REVIEW_REJECT_FAILURE'; 17 | export const REVIEW_REJECT_SUCCESS = 'REVIEW_REJECT_SUCCESS'; 18 | 19 | export const reviewReject = (id, data, basePath) => ({ 20 | type: REVIEW_REJECT, 21 | payload: { id, data: { ...data, status: 'rejected' }, basePath }, 22 | meta: { resource: 'reviews', fetch: UPDATE, cancelPrevious: false }, 23 | }); 24 | -------------------------------------------------------------------------------- /src/reviews/reviewSaga.js: -------------------------------------------------------------------------------- 1 | import { put, takeEvery } from 'redux-saga/effects'; 2 | import { push } from 'react-router-redux'; 3 | import { showNotification } from 'admin-on-rest'; 4 | import { 5 | REVIEW_APPROVE_SUCCESS, 6 | REVIEW_APPROVE_FAILURE, 7 | REVIEW_REJECT_SUCCESS, 8 | REVIEW_REJECT_FAILURE, 9 | } from './reviewActions'; 10 | 11 | export default function* reviewSaga() { 12 | yield [ 13 | takeEvery(REVIEW_APPROVE_SUCCESS, function* () { 14 | yield put(showNotification('resources.reviews.notification.approved_success')); 15 | yield put(push('/reviews')); 16 | }), 17 | takeEvery(REVIEW_APPROVE_FAILURE, function* ({ error }) { 18 | yield put(showNotification('resources.reviews.notification.approved_error', 'warning')); 19 | console.error(error); 20 | }), 21 | takeEvery(REVIEW_REJECT_SUCCESS, function* () { 22 | yield put(showNotification('resources.reviews.notification.rejected_success')); 23 | yield put(push('/reviews')); 24 | }), 25 | takeEvery(REVIEW_REJECT_FAILURE, function* ({ error }) { 26 | yield put(showNotification('resources.reviews.notification.rejected_error', 'warning')); 27 | console.error(error); 28 | }), 29 | ]; 30 | } 31 | -------------------------------------------------------------------------------- /src/reviews/rowStyle.js: -------------------------------------------------------------------------------- 1 | const rowStyle = (record) => { 2 | if (record.status === 'accepted') return { backgroundColor: '#dfd' }; 3 | if (record.status === 'pending') return { backgroundColor: '#ffd' }; 4 | if (record.status === 'rejected') return { backgroundColor: '#fdd' }; 5 | return {}; 6 | }; 7 | 8 | export default rowStyle; 9 | -------------------------------------------------------------------------------- /src/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route } from 'react-router-dom'; 3 | import Configuration from './configuration/Configuration'; 4 | import Segments from './segments/Segments'; 5 | 6 | export default [ 7 | , 8 | , 9 | ]; 10 | -------------------------------------------------------------------------------- /src/sagas.js: -------------------------------------------------------------------------------- 1 | import reviewSaga from './reviews/reviewSaga'; 2 | 3 | export default [ 4 | reviewSaga, 5 | ]; 6 | -------------------------------------------------------------------------------- /src/segments/LinkToRelatedCustomers.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import FlatButton from 'material-ui/FlatButton'; 3 | import { Link } from 'react-router-dom'; 4 | import { translate } from 'admin-on-rest'; 5 | import { stringify } from 'query-string'; 6 | 7 | import { VisitorIcon } from '../visitors'; 8 | 9 | const LinkToRelatedCustomers = ({ segment, translate }) => ( 10 | } 14 | containerElement={} 18 | /> 19 | ); 20 | 21 | export default translate(LinkToRelatedCustomers); 22 | -------------------------------------------------------------------------------- /src/segments/Segments.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Card } from 'material-ui/Card'; 3 | import { Table, TableBody, TableHeader, TableHeaderColumn, TableRow, TableRowColumn} from 'material-ui/Table'; 4 | import { translate, ViewTitle } from 'admin-on-rest'; 5 | 6 | import LinkToRelatedCustomers from './LinkToRelatedCustomers'; 7 | import segments from './data'; 8 | 9 | export default translate(({ translate }) => ( 10 | 11 | 12 | 13 | 14 | 15 | {translate('resources.segments.fields.name')} 16 | 17 | 18 | 19 | 20 | {segments.map(segment => ( 21 | 22 | {translate(segment.name)} 23 | 24 | 25 | 26 | 27 | ))} 28 | 29 |
30 |
31 | )); 32 | -------------------------------------------------------------------------------- /src/segments/data.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { id: 'compulsive', name: 'resources.segments.data.compulsive' }, 3 | { id: 'collector', name: 'resources.segments.data.collector' }, 4 | { id: 'ordered_once', name: 'resources.segments.data.ordered_once' }, 5 | { id: 'regular', name: 'resources.segments.data.regular' }, 6 | { id: 'returns', name: 'resources.segments.data.returns' }, 7 | { id: 'reviewer', name: 'resources.segments.data.reviewer' }, 8 | ]; 9 | -------------------------------------------------------------------------------- /src/themeReducer.js: -------------------------------------------------------------------------------- 1 | import { CHANGE_THEME } from './configuration/actions'; 2 | 3 | export default (previousState = 'light', { type, payload }) => { 4 | if (type === CHANGE_THEME) { 5 | return payload; 6 | } 7 | return previousState; 8 | }; 9 | -------------------------------------------------------------------------------- /src/visitors/AvatarField.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Avatar from 'material-ui/Avatar'; 3 | 4 | const style= { verticalAlign: 'middle' }; 5 | const AvatarField = ({ record, size }) => 6 | ; 7 | 8 | AvatarField.defaultProps = { 9 | size: 25, 10 | }; 11 | 12 | export default AvatarField; 13 | -------------------------------------------------------------------------------- /src/visitors/CustomerReferenceField.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ReferenceField } from 'admin-on-rest'; 3 | 4 | import FullNameField from './FullNameField'; 5 | 6 | const CustomerReferenceField = (props) => ( 7 | 8 | 9 | 10 | ); 11 | CustomerReferenceField.defaultProps = { 12 | source: 'customer_id', 13 | addLabel: true, 14 | }; 15 | 16 | export default CustomerReferenceField; 17 | -------------------------------------------------------------------------------- /src/visitors/FullNameField.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import AvatarField from './AvatarField'; 3 | import pure from 'recompose/pure'; 4 | 5 | const FullNameField = ({ record = {}, size = 25 }) => 6 | 7 |   8 | {record.first_name} {record.last_name} 9 | ; 10 | 11 | 12 | const PureFullNameField = pure(FullNameField); 13 | 14 | PureFullNameField.defaultProps = { 15 | source: 'last_name', 16 | label: 'resources.customers.fields.name', 17 | }; 18 | 19 | export default PureFullNameField; 20 | -------------------------------------------------------------------------------- /src/visitors/SegmentInput.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { translate, SelectInput } from 'admin-on-rest'; 3 | 4 | import segments from '../segments/data'; 5 | 6 | const SegmentInput = ({ translate, ...rest }) => ( 7 | ({ id: segment.id, name: translate(segment.name) }))} /> 8 | ); 9 | 10 | const TranslatedSegmentInput = translate(SegmentInput); 11 | 12 | TranslatedSegmentInput.defaultProps = { 13 | addLabel: true, 14 | addField: true, 15 | source: 'groups', 16 | }; 17 | 18 | export default TranslatedSegmentInput; 19 | -------------------------------------------------------------------------------- /src/visitors/SegmentsField.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Chip from 'material-ui/Chip'; 3 | import { translate } from 'admin-on-rest'; 4 | import segments from '../segments/data'; 5 | 6 | const styles = { 7 | main: { display: 'flex', flexWrap: 'wrap' }, 8 | chip: { margin: 4 }, 9 | }; 10 | 11 | const SegmentsField = ({ record, translate }) => ( 12 | 13 | {record.groups.map(segment => ( 14 | 15 | {translate(segments.find(s => s.id === segment).name)} 16 | 17 | ))} 18 | 19 | ); 20 | 21 | const TranslatedSegmentsField = translate(SegmentsField); 22 | 23 | TranslatedSegmentsField.defaultProps = { 24 | addLabel: true, 25 | source: 'groups', 26 | }; 27 | 28 | export default TranslatedSegmentsField; 29 | -------------------------------------------------------------------------------- /src/visitors/SegmentsInput.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { translate, SelectArrayInput } from 'admin-on-rest'; 3 | 4 | import segments from '../segments/data'; 5 | 6 | const SegmentsInput = ({ translate, ...rest }) => ( 7 | ({ id: segment.id, name: translate(segment.name) }))} /> 8 | ); 9 | 10 | const TranslatedSegmentsInput = translate(SegmentsInput); 11 | 12 | TranslatedSegmentsInput.defaultProps = { 13 | addField: true, 14 | source: 'groups', 15 | }; 16 | 17 | export default TranslatedSegmentsInput; 18 | -------------------------------------------------------------------------------- /src/visitors/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | translate, 4 | BooleanField, 5 | Datagrid, 6 | DateField, 7 | DateInput, 8 | Delete, 9 | Edit, 10 | Filter, 11 | FormTab, 12 | List, 13 | LongTextInput, 14 | NullableBooleanInput, 15 | NumberField, 16 | ReferenceManyField, 17 | TabbedForm, 18 | TextField, 19 | TextInput, 20 | } from 'admin-on-rest'; 21 | import Icon from 'material-ui/svg-icons/social/person'; 22 | 23 | import EditButton from '../buttons/EditButton'; 24 | import NbItemsField from '../commands/NbItemsField'; 25 | import ProductReferenceField from '../products/ProductReferenceField'; 26 | import StarRatingField from '../reviews/StarRatingField'; 27 | import FullNameField from './FullNameField'; 28 | import SegmentsField from './SegmentsField'; 29 | import SegmentInput from './SegmentInput'; 30 | import SegmentsInput from './SegmentsInput'; 31 | 32 | export const VisitorIcon = Icon; 33 | 34 | const VisitorFilter = (props) => ( 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | ); 43 | 44 | const colored = WrappedComponent => props => props.record[props.source] > 500 ? 45 | : 46 | ; 47 | 48 | const ColoredNumberField = colored(NumberField); 49 | ColoredNumberField.defaultProps = NumberField.defaultProps; 50 | 51 | export const VisitorList = (props) => ( 52 | } sort={{ field: 'last_seen', order: 'DESC' }} perPage={25}> 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | ); 65 | 66 | const VisitorTitle = ({ record }) => record ? : null; 67 | 68 | export const VisitorEdit = (props) => ( 69 | } {...props}> 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | ); 115 | 116 | const VisitorDeleteTitle = translate(({ record, translate }) => 117 | {translate('resources.customers.page.delete')}  118 | {record && } 119 | {record && `${record.first_name} ${record.last_name}`} 120 | ); 121 | 122 | export const VisitorDelete = (props) => } />; 123 | -------------------------------------------------------------------------------- /src/visitors/segments.js: -------------------------------------------------------------------------------- 1 | const segments = ['compulsive', 'collector', 'ordered_once', 'regular', 'returns', 'reviewer'] 2 | 3 | function capitalizeFirstLetter(string) { 4 | return string.charAt(0).toUpperCase() + string.slice(1); 5 | } 6 | 7 | export default segments.map(segment => ({ id: segment, name: capitalizeFirstLetter(segment) })); 8 | --------------------------------------------------------------------------------