├── .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 | Archived Repository
5 | This code is no longer maintained. Feel free to fork it, but use it at your own risks.
6 |
7 |
8 |
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 | [](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 |
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 |
84 |
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 |
126 |
127 |
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 |
--------------------------------------------------------------------------------