├── .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 ├── aorApolloClient.js ├── apolloClient ├── index.js └── schema.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 ├── 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 ├── SegmentsField.js ├── SegmentsInput.js ├── index.js └── segments.js /.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 GraphQL 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-graphql-demo. 13 | 14 | Admin-on-rest usually requires a REST server to provide data. In this demo however, a GraphQL endoint is simulated by the browser (using [apollographql/graphql-tools](https://github.com/apollographql/graphql-tools)). You can see the source data in [public/data.js](https://github.com/marmelab/admin-on-rest-graphql-demo/tree/master/public/data.js). 15 | 16 | To explore the source code, start with [src/index.js](https://github.com/marmelab/admin-on-rest-graphql-demo/blob/master/src/index.js). 17 | 18 | **Note**: This project was bootstrapped with [Create React App](https://github.com/facebookincubator/create-react-app). 19 | 20 | ## Available Scripts 21 | 22 | In the project directory, you can run: 23 | 24 | ### `npm start` 25 | 26 | Runs the app in the development mode.
27 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 28 | 29 | The page will reload if you make edits.
30 | You will also see any lint errors in the console. 31 | 32 | ### `npm test` 33 | 34 | Launches the test runner in the interactive watch mode.
35 | See the section about [running tests](#running-tests) for more information. 36 | 37 | ### `npm run build` 38 | 39 | Builds the app for production to the `build` folder.
40 | It correctly bundles React in production mode and optimizes the build for the best performance. 41 | 42 | The build is minified and the filenames include the hashes.
43 | Your app is ready to be deployed! 44 | 45 | ### `npm run deploy` 46 | 47 | Deploy the build to GitHub gh-pages. 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "admin-on-rest-graphql-demo", 3 | "version": "1.0.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": "^13.0.0", 10 | "eslint-config-airbnb-base": "^10.0.1", 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": "^2.2.3", 15 | "eslint-plugin-react": "^6.7.1", 16 | "fakerest": "^1.2.1", 17 | "fetch-mock": "^5.5.0", 18 | "gh-pages": "^0.12.0", 19 | "react-scripts": "0.9.5" 20 | }, 21 | "dependencies": { 22 | "admin-on-rest": "~1.0.0", 23 | "aor-language-french": "^1.8.0", 24 | "aor-rich-text-input": "^1.0.0", 25 | "aor-simple-graphql-client": "^0.0.7", 26 | "apollo-client": "^1.2.0", 27 | "apollo-test-utils": "^0.3.0", 28 | "casual-browserify": "^1.5.12", 29 | "graphql": "^0.9.6", 30 | "graphql-tools": "^0.11.0", 31 | "lodash.upperfirst": "^4.3.1", 32 | "material-ui": "~0.16.4", 33 | "pluralize": "^4.0.0", 34 | "prop-types": "~15.5.7", 35 | "react": "~15.5.4", 36 | "react-dom": "~15.5.4", 37 | "react-redux": "~5.0.4", 38 | "react-router-dom": "~4.1.0", 39 | "react-tap-event-plugin": "~2.0.1", 40 | "redux-form": "~6.6.2", 41 | "redux-saga": "~0.14.6" 42 | }, 43 | "scripts": { 44 | "start": "react-scripts start", 45 | "build": "react-scripts build", 46 | "test": "react-scripts test --env=jsdom", 47 | "eject": "react-scripts eject", 48 | "deploy": "gh-pages -d build" 49 | }, 50 | "homepage": "http://marmelab.com/admin-on-rest-graphql-demo/" 51 | } 52 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marmelab/admin-on-rest-graphql-demo/eeaa79d18b28faefb2a710c09f1e27725d0dce19/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 | import buildApolloClient from './aorApolloClient'; 5 | 6 | import './App.css'; 7 | 8 | import authClient from './authClient'; 9 | import sagas from './sagas'; 10 | import themeReducer from './themeReducer'; 11 | import Login from './Login'; 12 | import Layout from './Layout'; 13 | import Menu from './Menu'; 14 | import { Dashboard } from './dashboard'; 15 | import customRoutes from './routes'; 16 | import translations from './i18n'; 17 | 18 | import { VisitorList, VisitorEdit, VisitorDelete, VisitorIcon } from './visitors'; 19 | import { CommandList, CommandEdit, CommandIcon } from './commands'; 20 | import { ProductList, ProductCreate, ProductEdit, ProductIcon } from './products'; 21 | import { CategoryList, CategoryEdit, CategoryIcon } from './categories'; 22 | import { ReviewList, ReviewEdit, ReviewIcon } from './reviews'; 23 | 24 | class App extends Component { 25 | constructor() { 26 | super(); 27 | this.state = { restClient: null }; 28 | } 29 | componentWillMount() { 30 | buildApolloClient() 31 | .then(restClient => this.setState({ restClient })); 32 | } 33 | 34 | render() { 35 | if (!this.state.restClient) { 36 | return
Loading
; 37 | } 38 | 39 | return ( 40 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | ); 60 | } 61 | } 62 | 63 | export default App; 64 | -------------------------------------------------------------------------------- /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 { Link } from 'react-router-dom'; 4 | import compose from 'recompose/compose'; 5 | import MenuItem from 'material-ui/MenuItem'; 6 | import SettingsIcon from 'material-ui/svg-icons/action/settings'; 7 | import LabelIcon from 'material-ui/svg-icons/action/label'; 8 | import { translate, DashboardMenuItem } from 'admin-on-rest'; 9 | 10 | import { VisitorIcon } from './visitors'; 11 | import { CommandIcon } from './commands'; 12 | import { ProductIcon } from './products'; 13 | import { CategoryIcon } from './categories'; 14 | import { ReviewIcon } from './reviews'; 15 | 16 | const items = [ 17 | { name: 'customers', resource: 'Customer', icon: }, 18 | { name: 'segments', icon: }, 19 | { name: 'commands', resource: 'Command', icon: }, 20 | { name: 'products', resource: 'Product', icon: }, 21 | { name: 'categories', resource: 'Category', icon: }, 22 | { name: 'reviews', resource: 'Review', icon: }, 23 | ]; 24 | 25 | const styles = { 26 | main: { 27 | display: 'flex', 28 | flexDirection: 'column', 29 | justifyContent: 'flex-start', 30 | height: '100%', 31 | }, 32 | }; 33 | 34 | const Menu = ({ onMenuTap, translate, logout }) => ( 35 |
36 | 37 | {items.map(item => ( 38 | } 41 | primaryText={translate(`resources.${item.name}.name`, { smart_count: 2 })} 42 | leftIcon={item.icon} 43 | onTouchTap={onMenuTap} 44 | /> 45 | ))} 46 | } 48 | primaryText={translate('pos.configuration')} 49 | leftIcon={} 50 | onTouchTap={onMenuTap} 51 | /> 52 | {logout} 53 |
54 | ); 55 | 56 | const enhance = compose( 57 | connect(state => ({ 58 | theme: state.theme, 59 | locale: state.locale, 60 | })), 61 | translate, 62 | ); 63 | 64 | export default enhance(Menu); 65 | -------------------------------------------------------------------------------- /src/aorApolloClient.js: -------------------------------------------------------------------------------- 1 | import { buildApolloClient } from 'aor-simple-graphql-client'; 2 | import gql from 'graphql-tag'; 3 | 4 | import apolloClient from './apolloClient'; 5 | 6 | export default () => buildApolloClient({ 7 | client: apolloClient, 8 | queries: { 9 | Command: { 10 | GET_LIST: gql` 11 | query getPageOfCommands($page: Int, $perPage: Int, $sortField: String, $sortOrder: String, $filter: String) { 12 | getPageOfCommands(page: $page, perPage: $perPage, sortField: $sortField, sortOrder: $sortOrder, filter: $filter) { 13 | items { 14 | id 15 | reference 16 | customer_id 17 | total_ex_taxes 18 | delivery_fees 19 | tax_rate 20 | taxes 21 | total 22 | status 23 | returned 24 | basket { product_id, quantity } 25 | } 26 | totalCount 27 | } 28 | } 29 | `, 30 | GET_ONE: gql` 31 | query getCommand($id: ID!) { 32 | getCommand(id: $id) { 33 | id 34 | reference 35 | customer_id 36 | total_ex_taxes 37 | delivery_fees 38 | tax_rate 39 | taxes 40 | total 41 | status 42 | returned 43 | basket { product_id, quantity } 44 | } 45 | } 46 | `, 47 | CREATE: gql` 48 | mutation createCommand($data: String!) { 49 | createCommand(data: $data) { 50 | id 51 | reference 52 | customer_id 53 | total_ex_taxes 54 | delivery_fees 55 | tax_rate 56 | taxes 57 | total 58 | status 59 | returned 60 | basket { product_id, quantity } 61 | } 62 | } 63 | `, 64 | UPDATE: gql` 65 | mutation updateCommand($data: String!) { 66 | updateCommand(data: $data) { 67 | id 68 | reference 69 | customer_id 70 | total_ex_taxes 71 | delivery_fees 72 | tax_rate 73 | taxes 74 | total 75 | status 76 | returned 77 | basket { product_id, quantity } 78 | } 79 | } 80 | `, 81 | }, 82 | }, 83 | }); 84 | -------------------------------------------------------------------------------- /src/apolloClient/index.js: -------------------------------------------------------------------------------- 1 | /* global data */ 2 | import { makeExecutableSchema, addMockFunctionsToSchema } from 'graphql-tools'; 3 | import ApolloClient from 'apollo-client'; 4 | import { mockNetworkInterfaceWithSchema } from 'apollo-test-utils'; 5 | import pluralize from 'pluralize'; 6 | 7 | import typeDefs from './schema'; 8 | 9 | const schema = makeExecutableSchema({ typeDefs }); 10 | 11 | const currentData = data; 12 | 13 | const mockQueriesForEntity = (entity) => { 14 | const entityData = currentData[pluralize(entity).toLowerCase()]; 15 | 16 | return { 17 | [`getPageOf${pluralize(entity)}`]: (r, { page, perPage, filter }) => { 18 | const filters = JSON.parse(filter); 19 | let items = entityData; 20 | 21 | if (filters.ids) { 22 | items = items.filter(d => filters.ids.includes(d.id.toString())); 23 | } else { 24 | Object.keys(filters).filter(key => key !== 'q').forEach((key) => { 25 | if (key.indexOf('_lte') !== -1) { 26 | // less than or equal 27 | const realKey = key.replace(/(_lte)$/, ''); 28 | items = items.filter(d => d[realKey] <= filters[key]); 29 | return; 30 | } 31 | if (key.indexOf('_gte') !== -1) { 32 | // less than or equal 33 | const realKey = key.replace(/(_gte)$/, ''); 34 | items = items.filter(d => d[realKey] >= filters[key]); 35 | return; 36 | } 37 | if (key.indexOf('_lt') !== -1) { 38 | // less than or equal 39 | const realKey = key.replace(/(_lt)$/, ''); 40 | items = items.filter(d => d[realKey] < filters[key]); 41 | return; 42 | } 43 | if (key.indexOf('_gt') !== -1) { 44 | // less than or equal 45 | const realKey = key.replace(/(_gt)$/, ''); 46 | items = items.filter(d => d[realKey] > filters[key]); 47 | return; 48 | } 49 | 50 | items = items.filter(d => d[key] == filters[key]); 51 | }); 52 | 53 | if (filters.q) { 54 | items = items.filter(d => Object.keys(d).some(key => d[key].toString().includes(filters.q))); 55 | } 56 | } 57 | 58 | if (page !== undefined && perPage) { 59 | items = items.slice(page * perPage, (page * perPage) + perPage); 60 | } 61 | 62 | return { 63 | items, 64 | totalCount: entityData.length, 65 | }; 66 | }, 67 | [`get${entity}`]: (r, { id }) => entityData.find(d => d.id == id), 68 | }; 69 | }; 70 | 71 | const mockMutationsForEntity = (entity) => { 72 | let entityData = currentData[pluralize(entity).toLowerCase()]; 73 | 74 | return { 75 | [`create${entity}`]: (root, { data }) => { 76 | const { __typename, ...parsedData } = JSON.parse(data); 77 | const newEntity = { 78 | id: entityData[entityData.length - 1].id + 1, 79 | ...parsedData, 80 | }; 81 | 82 | entityData.push(newEntity); 83 | return newEntity; 84 | }, 85 | [`update${entity}`]: (root, { data }) => { 86 | const { id, __typename, ...parsedData } = JSON.parse(data); 87 | const parsedId = parseInt(id, 10); 88 | const indexOfEntity = entityData.findIndex(e => e.id === parsedId); 89 | 90 | entityData[indexOfEntity] = { id: parsedId, ...parsedData }; 91 | return parsedData; 92 | }, 93 | [`remove${entity}`]: (root, { id }) => { 94 | entityData = entityData.filter(e => e.id !== id); 95 | }, 96 | }; 97 | }; 98 | 99 | const mocks = { 100 | Query: () => ({ 101 | ...mockQueriesForEntity('Customer'), 102 | ...mockQueriesForEntity('Category'), 103 | ...mockQueriesForEntity('Product'), 104 | ...mockQueriesForEntity('Command'), 105 | ...mockQueriesForEntity('Review'), 106 | }), 107 | Mutation: () => ({ 108 | ...mockMutationsForEntity('Customer'), 109 | ...mockMutationsForEntity('Category'), 110 | ...mockMutationsForEntity('Product'), 111 | ...mockMutationsForEntity('Command'), 112 | ...mockMutationsForEntity('Review'), 113 | }), 114 | }; 115 | 116 | addMockFunctionsToSchema({ 117 | schema, 118 | mocks, 119 | }); 120 | 121 | const mockNetworkInterface = mockNetworkInterfaceWithSchema({ schema }); 122 | 123 | const client = new ApolloClient({ 124 | networkInterface: mockNetworkInterface, 125 | }); 126 | 127 | export default client; 128 | -------------------------------------------------------------------------------- /src/apolloClient/schema.js: -------------------------------------------------------------------------------- 1 | export default ` 2 | type Customer { 3 | id: ID! 4 | first_name: String 5 | last_name: String 6 | email: String 7 | address: String 8 | zipcode: String 9 | city: String 10 | avatar: String 11 | birthday: String 12 | first_seen: String 13 | last_seen: String 14 | has_ordered: Boolean, 15 | latest_purchase: String 16 | has_newsletter: String 17 | groups: [String], 18 | nb_commands: Int 19 | total_spent: Float 20 | } 21 | 22 | type CustomerPage { 23 | items: [Customer] 24 | totalCount: Int 25 | } 26 | 27 | type Category { 28 | id: ID! 29 | name: String! 30 | } 31 | 32 | type CategoryPage { 33 | items: [Category] 34 | totalCount: Int 35 | } 36 | 37 | type Product { 38 | id: ID! 39 | category_id: ID! 40 | reference: String 41 | width: Float 42 | height: Float 43 | price: Float 44 | thumbnail: String 45 | image: String 46 | description: String 47 | stock: Int 48 | } 49 | 50 | type ProductPage { 51 | items: [Product] 52 | totalCount: Int 53 | } 54 | 55 | type CommandItem { 56 | product_id: ID! 57 | quantity: Int 58 | } 59 | 60 | type Command { 61 | id: ID! 62 | reference: String 63 | customer_id: ID! 64 | total_ex_taxes: Float 65 | delivery_fees: Float 66 | tax_rate: Float 67 | taxes: Float 68 | total: Float 69 | status: String 70 | returned: Boolean 71 | basket: [CommandItem] 72 | } 73 | 74 | type CommandPage { 75 | items: [Command] 76 | totalCount: Int 77 | } 78 | 79 | type Review { 80 | id: ID! 81 | date: String 82 | status: String 83 | command_id: ID! 84 | product_id: ID! 85 | customer_id: ID! 86 | rating: Int 87 | comment: String 88 | } 89 | 90 | type ReviewPage { 91 | items: [Review] 92 | totalCount: Int 93 | } 94 | 95 | type Query { 96 | getPageOfCustomers(page: Int, perPage: Int, sortField: String, sortOrder: String, filter: String): CustomerPage 97 | getCustomer(id: ID!): Customer 98 | 99 | getPageOfCategories(page: Int, perPage: Int, sortField: String, sortOrder: String, filter: String): CategoryPage 100 | getCategory(id: ID!): Category 101 | 102 | getPageOfProducts(page: Int, perPage: Int, sortField: String, sortOrder: String, filter: String): ProductPage 103 | getProduct(id: ID!): Product 104 | 105 | getPageOfCommands(page: Int, perPage: Int, sortField: String, sortOrder: String, filter: String): CommandPage 106 | getCommand(id: ID!): Command 107 | 108 | getPageOfReviews(page: Int, perPage: Int, sortField: String, sortOrder: String, filter: String): ReviewPage 109 | getReview(id: ID!): Review 110 | } 111 | 112 | type Mutation { 113 | createCustomer(data: String): Customer 114 | updateCustomer(data: String): Customer 115 | removeCustomer(id: ID!): Boolean 116 | 117 | createCategory(data: String): Category 118 | updateCategory(data: String): Category 119 | removeCategory(id: ID!): Boolean 120 | 121 | createProduct(data: String): Product 122 | updateProduct(data: String): Product 123 | removeProduct(id: ID!): Boolean 124 | 125 | createCommand(data: String): Command 126 | updateCommand(data: String): Command 127 | removeCommand(id: ID!): Boolean 128 | 129 | createReview(data: String): Review 130 | updateReview(data: String): Review 131 | removeReview(id: ID!): Boolean 132 | } 133 | `; 134 | -------------------------------------------------------------------------------- /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('Product', 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.Product.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 | 43 | 44 | 45 | 46 | 47 | 48 | ); 49 | 50 | export const CommandList = props => ( 51 | } sort={{ field: 'date', order: 'DESC' }} perPage={25}> 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | ); 64 | 65 | const CommandTitle = translate(({ record, translate }) => {translate('resources.commands.name', { smart_count: 1 })} #{record.reference}); 66 | 67 | export const CommandEdit = translate(({ translate, ...rest }) => ( 68 | } {...rest}> 69 | 70 | 71 | 72 | 73 | `${choice.first_name} ${choice.last_name}`} /> 74 | 75 | 82 | 83 |
84 | 85 | 86 | )); 87 | -------------------------------------------------------------------------------- /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 | import buildApolloClient from '../aorApolloClient'; 5 | 6 | import Welcome from './Welcome'; 7 | import MonthlyRevenue from './MonthlyRevenue'; 8 | import NbNewOrders from './NbNewOrders'; 9 | import PendingOrders from './PendingOrders'; 10 | import PendingReviews from './PendingReviews'; 11 | import NewCustomers from './NewCustomers'; 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 = { restClient: null }; 23 | 24 | componentWillMount() { 25 | buildApolloClient() 26 | .then(restClient => this.setState({ restClient }, () => { 27 | const d = new Date(); 28 | d.setDate(d.getDate() - 30); 29 | this.state.restClient(GET_LIST, 'Command', { 30 | filter: { date_gte: d.toISOString() }, 31 | sort: { field: 'date', order: 'DESC' }, 32 | pagination: { page: 1, perPage: 50 }, 33 | }) 34 | .then(response => response.data 35 | .filter(order => order.status !== 'cancelled') 36 | .reduce((stats, order) => { 37 | if (order.status !== 'cancelled') { 38 | stats.revenue += order.total; 39 | stats.nbNewOrders++; 40 | } 41 | if (order.status === 'ordered') { 42 | stats.pendingOrders.push(order); 43 | } 44 | return stats; 45 | }, { revenue: 0, nbNewOrders: 0, pendingOrders: [] }), 46 | ) 47 | .then(({ revenue, nbNewOrders, pendingOrders }) => { 48 | this.setState({ 49 | revenue: revenue.toLocaleString(undefined, { 50 | style: 'currency', 51 | currency: 'USD', 52 | minimumFractionDigits: 0, 53 | maximumFractionDigits: 0, 54 | }), 55 | nbNewOrders, 56 | pendingOrders, 57 | }); 58 | return pendingOrders; 59 | }) 60 | .then(pendingOrders => pendingOrders.map(order => order.customer_id)) 61 | .then(customerIds => restClient(GET_MANY, 'Customer', { ids: customerIds })) 62 | .then(response => response.data) 63 | .then(customers => customers.reduce((prev, customer) => { 64 | prev[customer.id] = customer; // eslint-disable-line no-param-reassign 65 | return prev; 66 | }, {})) 67 | .then(customers => this.setState({ pendingOrdersCustomers: customers })); 68 | 69 | this.state.restClient(GET_LIST, 'Review', { 70 | filter: { status: 'pending' }, 71 | sort: { field: 'date', order: 'DESC' }, 72 | pagination: { page: 1, perPage: 100 }, 73 | }) 74 | .then(response => response.data) 75 | .then((reviews) => { 76 | const nbPendingReviews = reviews.reduce(nb => ++nb, 0); 77 | const pendingReviews = reviews.slice(0, Math.min(10, reviews.length)); 78 | this.setState({ pendingReviews, nbPendingReviews }); 79 | return pendingReviews; 80 | }) 81 | .then(reviews => reviews.map(review => review.customer_id)) 82 | .then(customerIds => restClient(GET_MANY, 'Customer', { ids: customerIds })) 83 | .then(response => response.data) 84 | .then(customers => customers.reduce((prev, customer) => { 85 | prev[customer.id] = customer; // eslint-disable-line no-param-reassign 86 | return prev; 87 | }, {})) 88 | .then(customers => this.setState({ pendingReviewsCustomers: customers })); 89 | 90 | this.state.restClient(GET_LIST, 'Customer', { 91 | filter: { has_ordered: true, first_seen_gte: d.toISOString() }, 92 | sort: { field: 'first_seen', order: 'DESC' }, 93 | pagination: { page: 1, perPage: 100 }, 94 | }) 95 | .then(response => response.data) 96 | .then((newCustomers) => { 97 | this.setState({ newCustomers }); 98 | this.setState({ nbNewCustomers: newCustomers.reduce(nb => ++nb, 0) }); 99 | }); 100 | })); 101 | } 102 | 103 | render() { 104 | const { 105 | nbNewCustomers, 106 | nbNewOrders, 107 | nbPendingReviews, 108 | newCustomers, 109 | pendingOrders, 110 | pendingOrdersCustomers, 111 | pendingReviews, 112 | pendingReviewsCustomers, 113 | revenue, 114 | } = this.state; 115 | const { width } = this.props; 116 | return ( 117 |
118 | {width === 1 && } 119 | 120 |
121 |
122 |
123 | 124 | 125 |
126 |
127 | 128 |
129 |
130 |
131 |
132 | 133 | 134 |
135 |
136 |
137 |
138 | ); 139 | } 140 | } 141 | 142 | export default withWidth()(Dashboard); 143 | -------------------------------------------------------------------------------- /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: 'Review', 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 GrainIcon from 'material-ui/svg-icons/image/grain'; 8 | import FlatButton from 'material-ui/FlatButton'; 9 | import { translate } from 'admin-on-rest'; 10 | 11 | export default translate(({ style, translate }) => ( 12 | 13 | } />} 17 | /> 18 | 19 | } href="https://marmelab.com/admin-on-rest/" /> 20 | } href="https://github.com/marmelab/aor-simple-graphql-client/" /> 21 | } href="https://github.com/marmelab/admin-on-rest-graphql-demo" /> 22 | 23 | 24 | )); 25 | -------------------------------------------------------------------------------- /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 | aor_graphql_button: 'aor GraphQL client repository', 25 | demo_button: 'Source for this demo', 26 | }, 27 | }, 28 | }, 29 | resources: { 30 | customers: { 31 | name: 'Customer |||| Customers', 32 | fields: { 33 | commands: 'Orders', 34 | groups: 'Segments', 35 | last_seen_gte: 'Visited Since', 36 | name: 'Name', 37 | }, 38 | tabs: { 39 | identity: 'Identity', 40 | address: 'Address', 41 | orders: 'Orders', 42 | reviews: 'Reviews', 43 | stats: 'Stats', 44 | }, 45 | page: { 46 | delete: 'Delete Customer', 47 | }, 48 | 49 | }, 50 | commands: { 51 | name: 'Order |||| Orders', 52 | fields: { 53 | basket: { 54 | delivery: 'Delivery', 55 | reference: 'Reference', 56 | quantity: 'Quantity', 57 | sum: 'Sum', 58 | tax_rate: 'Tax Rate', 59 | total: 'Total', 60 | unit_price: 'Unit Price', 61 | }, 62 | customer_id: 'Customer', 63 | date_gte: 'Passed Since', 64 | date_lte: 'Passed Before', 65 | total_gte: 'Min amount', 66 | }, 67 | }, 68 | products: { 69 | name: 'Poster |||| Posters', 70 | fields: { 71 | category_id: 'Category', 72 | height_gte: 'Min height', 73 | height_lte: 'Max height', 74 | height: 'Height', 75 | image: 'Image', 76 | price: 'Price', 77 | reference: 'Reference', 78 | stock_lte: 'Low Stock', 79 | stock: 'Stock', 80 | thumbnail: 'Thumbnail', 81 | width_gte: 'Min width', 82 | width_lte: 'mx_width', 83 | width: 'Width', 84 | }, 85 | tabs: { 86 | image: 'Image', 87 | details: 'Details', 88 | description: 'Description', 89 | reviews: 'Reviews', 90 | }, 91 | }, 92 | categories: { 93 | name: 'Category |||| Categories', 94 | fields: { 95 | products: 'Products', 96 | }, 97 | 98 | }, 99 | reviews: { 100 | name: 'Review |||| Reviews', 101 | fields: { 102 | customer_id: 'Customer', 103 | command_id: 'Order', 104 | product_id: 'Product', 105 | date_gte: 'Posted since', 106 | date_lte: 'Posted before', 107 | date: 'Date', 108 | comment: 'Comment', 109 | rating: 'Rating', 110 | }, 111 | action: { 112 | accept: 'Accept', 113 | reject: 'Reject', 114 | }, 115 | notification: { 116 | approved_success: 'Review approved', 117 | approved_error: 'Error: Review not approved', 118 | rejected_success: 'Review rejected', 119 | rejected_error: 'Error: Review not rejected', 120 | }, 121 | }, 122 | segments: { 123 | name: 'Segments', 124 | fields: { 125 | customers: 'Customers', 126 | name: 'Name', 127 | }, 128 | data: { 129 | compulsive: 'Compulsive', 130 | collector: 'Collector', 131 | ordered_once: 'Ordered once', 132 | regular: 'Regular', 133 | returns: 'Returns', 134 | reviewer: 'Reviewer', 135 | }, 136 | }, 137 | }, 138 | }; 139 | -------------------------------------------------------------------------------- /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 | aor_graphql_button: 'Dépôt du client GraphQL pour aor', 25 | demo_button: 'Code source de cette démo', 26 | }, 27 | }, 28 | }, 29 | resources: { 30 | customers: { 31 | name: 'Client |||| Clients', 32 | fields: { 33 | address: 'Rue', 34 | birthday: 'Anniversaire', 35 | city: 'Ville', 36 | commands: 'Commandes', 37 | first_name: 'Prénom', 38 | first_seen: 'Première visite', 39 | groups: 'Segments', 40 | has_newsletter: 'Abonné à la newsletter', 41 | has_ordered: 'A commandé', 42 | last_name: 'Nom', 43 | last_seen: 'Vu le', 44 | last_seen_gte: 'Vu depuis', 45 | latest_purchase: 'Dernier achat', 46 | name: 'Nom', 47 | total_spent: 'Dépenses', 48 | zipcode: 'Code postal', 49 | }, 50 | tabs: { 51 | identity: 'Identité', 52 | address: 'Adresse', 53 | orders: 'Commandes', 54 | reviews: 'Commentaires', 55 | stats: 'Statistiques', 56 | }, 57 | page: { 58 | delete: 'Supprimer le client', 59 | }, 60 | }, 61 | commands: { 62 | name: 'Commande |||| Commandes', 63 | fields: { 64 | basket: { 65 | delivery: 'Frais de livraison', 66 | reference: 'Référence', 67 | quantity: 'Quantité', 68 | sum: 'Sous-total', 69 | tax_rate: 'TVA', 70 | total: 'Total', 71 | unit_price: 'P.U.', 72 | }, 73 | customer_id: 'Client', 74 | date_gte: 'Passées depuis', 75 | date_lte: 'Passées avant', 76 | nb_items: 'Nb articles', 77 | reference: 'Référence', 78 | returned: 'Annulée', 79 | status: 'Etat', 80 | total_gte: 'Montant minimum', 81 | }, 82 | }, 83 | products: { 84 | name: 'Poster |||| Posters', 85 | fields: { 86 | category_id: 'Catégorie', 87 | height_gte: 'Hauteur mini', 88 | height_lte: 'Hauteur maxi', 89 | height: 'Hauteur', 90 | image: 'Photo', 91 | price: 'Prix', 92 | reference: 'Référence', 93 | stock_lte: 'Stock faible', 94 | stock: 'Stock', 95 | thumbnail: 'Aperçu', 96 | width_gte: 'Largeur mini', 97 | width_lte: 'Margeur maxi', 98 | width: 'Largeur', 99 | }, 100 | tabs: { 101 | image: 'Image', 102 | details: 'Détails', 103 | description: 'Description', 104 | reviews: 'Commentaires', 105 | }, 106 | }, 107 | categories: { 108 | name: 'Catégorie |||| Catégories', 109 | fields: { 110 | name: 'Nom', 111 | products: 'Produits', 112 | }, 113 | }, 114 | reviews: { 115 | name: 'Commentaire |||| Commentaires', 116 | fields: { 117 | customer_id: 'Client', 118 | command_id: 'Commande', 119 | product_id: 'Produit', 120 | date_gte: 'Publié depuis', 121 | date_lte: 'Publié avant', 122 | date: 'Date', 123 | comment: 'Texte', 124 | status: 'Statut', 125 | rating: 'Classement', 126 | }, 127 | action: { 128 | accept: 'Accepter', 129 | reject: 'Rejeter', 130 | }, 131 | notification: { 132 | approved_success: 'Commentaire approuvé', 133 | approved_error: 'Erreur: Commentaire non approuvé', 134 | rejected_success: 'Commentaire rejeté', 135 | rejected_error: 'Erreur: Commentaire non rejeté', 136 | }, 137 | }, 138 | segments: { 139 | name: 'Segments', 140 | fields: { 141 | customers: 'Clients', 142 | name: 'Nom', 143 | }, 144 | data: { 145 | compulsive: 'Compulsif', 146 | collector: 'Collectionneur', 147 | ordered_once: 'A commandé', 148 | regular: 'Régulier', 149 | returns: 'A renvoyé', 150 | reviewer: 'Commentateur', 151 | }, 152 | }, 153 | }, 154 | }; 155 | -------------------------------------------------------------------------------- /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/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 | 40 | 41 | `${choice.first_name} ${choice.last_name}`} /> 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | ); 50 | 51 | export const ReviewList = props => ( 52 | } perPage={25} sort={{ field: 'date', order: 'DESC' }}> 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | ); 65 | 66 | const detailStyle = { display: 'inline-block', verticalAlign: 'top', marginRight: '2em', minWidth: '8em' }; 67 | export const ReviewEdit = props => ( 68 | }> 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 85 | 86 | 87 | ); 88 | -------------------------------------------------------------------------------- /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/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, SelectInput } 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 | addLabel: true, 14 | addField: true, 15 | source: 'groups', 16 | }; 17 | 18 | export default TranslatedSegmentsInput; 19 | -------------------------------------------------------------------------------- /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 SegmentsInput from './SegmentsInput'; 30 | 31 | export const VisitorIcon = Icon; 32 | 33 | const VisitorFilter = props => ( 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | ); 42 | 43 | const colored = WrappedComponent => props => props.record[props.source] > 500 ? 44 | : 45 | ; 46 | 47 | const ColoredNumberField = colored(NumberField); 48 | ColoredNumberField.defaultProps = NumberField.defaultProps; 49 | 50 | export const VisitorList = props => ( 51 | } sort={{ field: 'last_seen', order: 'DESC' }} perPage={25}> 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | ); 64 | 65 | const VisitorTitle = ({ record }) => record ? : null; 66 | 67 | export const VisitorEdit = props => ( 68 | } {...props}> 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | ); 114 | 115 | const VisitorDeleteTitle = translate(({ record, translate }) => 116 | {translate('resources.customers.page.delete')}  117 | {record && } 118 | {record && `${record.first_name} ${record.last_name}`} 119 | ); 120 | 121 | export const VisitorDelete = props => } />; 122 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------