├── .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 | 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 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 |
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 |
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 { 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 |
129 |
130 |
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 |
--------------------------------------------------------------------------------