├── .flowconfig ├── .gitignore ├── .tbstore ├── configuration.yml ├── description.md └── images │ ├── image-1.png │ ├── image-2.png │ ├── image-3.png │ ├── image-4.png │ ├── image-5.png │ └── image-cover.png ├── Readme.md ├── babel.config.json ├── dev.md ├── gulpfile.js ├── js ├── .eslintrc.js ├── back │ ├── actions │ │ ├── creators.js │ │ ├── index.js │ │ └── types.js │ ├── app.jsx │ ├── app.less │ ├── commands │ │ ├── activate-account.js │ │ ├── approve-review.js │ │ ├── check-module-version.js │ │ ├── delete-criterion.js │ │ ├── delete-review.js │ │ ├── export-reviews.js │ │ ├── index.js │ │ ├── load-data.js │ │ ├── migrate-data.js │ │ ├── save-criterion.js │ │ ├── save-review.js │ │ ├── save-settings.js │ │ ├── set-latest-version.js │ │ ├── set-reviewed.js │ │ ├── undelete-review.js │ │ └── upload-yopto.js │ ├── components │ │ ├── create-review │ │ │ ├── create-review-dialog.jsx │ │ │ └── index.js │ │ ├── edit-review │ │ │ ├── edit-review-dialog.jsx │ │ │ ├── edit-review-form.jsx │ │ │ ├── edit-review-form.less │ │ │ ├── index.js │ │ │ └── view-review-form.jsx │ │ ├── markdown │ │ │ ├── markdown.jsx │ │ │ └── markdown.less │ │ ├── navigation │ │ │ ├── navigation.jsx │ │ │ └── navigation.less │ │ ├── registration │ │ │ ├── index.js │ │ │ ├── legal.jsx │ │ │ ├── registration-view.jsx │ │ │ └── registration.jsx │ │ ├── reviews-table │ │ │ ├── controller.jsx │ │ │ ├── images.jsx │ │ │ ├── index.js │ │ │ ├── reviews-table.jsx │ │ │ ├── table-head.jsx │ │ │ ├── table-toolbar.jsx │ │ │ └── types.js │ │ ├── section │ │ │ ├── section.jsx │ │ │ └── section.less │ │ ├── select-customer │ │ │ ├── index.jsx │ │ │ └── select-customer.jsx │ │ ├── select-entity-type │ │ │ └── select-entity-type.jsx │ │ └── select-entity │ │ │ ├── index.jsx │ │ │ ├── select-entity.jsx │ │ │ └── select-entity.less │ ├── index.js │ ├── pages │ │ ├── criteria │ │ │ ├── criteria-section.jsx │ │ │ ├── criteria.jsx │ │ │ ├── criteria.less │ │ │ ├── form.jsx │ │ │ ├── index.js │ │ │ ├── tab.jsx │ │ │ └── tabs.jsx │ │ ├── moderation │ │ │ ├── index.js │ │ │ └── moderation.jsx │ │ ├── reviews │ │ │ ├── index.js │ │ │ ├── migrate-data │ │ │ │ ├── index.jsx │ │ │ │ └── migrate-data.jsx │ │ │ └── reviews.jsx │ │ ├── settings │ │ │ ├── index.js │ │ │ ├── review-preview.jsx │ │ │ ├── settings.jsx │ │ │ ├── shape-select.jsx │ │ │ └── style.less │ │ ├── snackbar │ │ │ └── index.js │ │ └── support │ │ │ ├── index.js │ │ │ ├── support.jsx │ │ │ ├── support.less │ │ │ └── warning.jsx │ ├── reducer │ │ ├── account.js │ │ ├── criteria.js │ │ ├── data.js │ │ ├── index.js │ │ ├── routing-state.js │ │ ├── settings.js │ │ ├── snackbar.js │ │ └── ui.js │ ├── routing │ │ ├── criteria.js │ │ ├── index.js │ │ ├── moderation.js │ │ ├── reviews.js │ │ ├── settings.js │ │ └── support.js │ ├── selectors │ │ ├── account.js │ │ ├── criteria.js │ │ ├── data.js │ │ ├── routing-state.js │ │ ├── settings.js │ │ ├── snackbar.js │ │ └── ui.js │ ├── types.js │ └── utils │ │ ├── common.js │ │ ├── criteria.js │ │ ├── drilldown.js │ │ └── markdown.js ├── common │ ├── components │ │ ├── add-avatar │ │ │ └── add-avatar.jsx │ │ ├── badge │ │ │ ├── badge.jsx │ │ │ └── badge.less │ │ ├── bootstrap │ │ │ └── bootstrap.jsx │ │ ├── color-picker │ │ │ ├── board.jsx │ │ │ ├── circle.jsx │ │ │ ├── color-picker.jsx │ │ │ ├── color-picker.less │ │ │ ├── color.jsx │ │ │ ├── preset.jsx │ │ │ ├── ribbon.jsx │ │ │ ├── trigger.jsx │ │ │ └── types.js │ │ ├── confirm-delete │ │ │ ├── confirm-delete.jsx │ │ │ └── confirm-delete.less │ │ ├── criteria │ │ │ ├── block.jsx │ │ │ └── inline.jsx │ │ ├── dialog │ │ │ └── index.jsx │ │ ├── grading-shape │ │ │ └── grading-shape.jsx │ │ ├── grading │ │ │ └── grading.jsx │ │ ├── multilang │ │ │ ├── multilang.jsx │ │ │ └── multilang.less │ │ ├── page-with-footer │ │ │ ├── page-with-footer.jsx │ │ │ └── page-with-footer.less │ │ ├── portal │ │ │ └── portal.jsx │ │ ├── review-list-item-with-entity │ │ │ └── review-list-item-with-entity.jsx │ │ ├── review-list-item │ │ │ ├── review-list-item.jsx │ │ │ └── review-list-item.less │ │ ├── review-list-paging │ │ │ └── review-list-paging.jsx │ │ ├── snackbar │ │ │ └── snackbar.jsx │ │ ├── space │ │ │ └── space.jsx │ │ ├── text-area │ │ │ ├── text-area.jsx │ │ │ └── text-area.less │ │ ├── text-with-tags │ │ │ └── text-with-tags.jsx │ │ └── theme │ │ │ └── theme.jsx │ ├── types.js │ └── utils │ │ ├── browser.js │ │ ├── format.js │ │ ├── input.js │ │ ├── ramda.js │ │ ├── redux.js │ │ ├── reviews.js │ │ ├── storage.js │ │ ├── translation.js │ │ ├── url.js │ │ ├── validation.js │ │ └── version.js ├── flow-typed │ └── global.js ├── front │ ├── actions │ │ ├── creators.js │ │ ├── index.js │ │ └── types.js │ ├── app.jsx │ ├── commands │ │ ├── delete-review.js │ │ ├── index.js │ │ ├── load-list.js │ │ ├── report-review.js │ │ ├── save-review.js │ │ ├── upload-image.js │ │ └── vote-review.js │ ├── components │ │ ├── delete-review │ │ │ └── index.js │ │ ├── edit-review │ │ │ ├── edit-review-dialog │ │ │ │ ├── edit-review-dialog.jsx │ │ │ │ └── grades.jsx │ │ │ ├── edit-review-form │ │ │ │ └── edit-review-form.jsx │ │ │ └── index.js │ │ └── snackbar │ │ │ └── index.js │ ├── index.js │ ├── reducer │ │ ├── delete-review.js │ │ ├── edit-review.js │ │ ├── entities.js │ │ ├── gdpr.js │ │ ├── index.js │ │ ├── lists.js │ │ ├── reviews.js │ │ ├── snackbar.js │ │ └── visitor-reviews.js │ ├── selectors │ │ ├── delete-review.js │ │ ├── edit-review.js │ │ ├── entities.js │ │ ├── gdpr.js │ │ ├── lists.js │ │ ├── reviews.js │ │ ├── snackbar.js │ │ └── visitor-reviews.js │ ├── types.js │ ├── utils │ │ ├── entities.js │ │ ├── gdpr.js │ │ ├── init.js │ │ ├── list.js │ │ └── visitor.js │ └── widgets │ │ ├── entity-review-list │ │ ├── entity-review-list.jsx │ │ └── index.js │ │ ├── my-reviews │ │ ├── index.js │ │ └── my-reviews.jsx │ │ └── review-list │ │ ├── index.js │ │ └── review-list.jsx ├── webpack-config-translation.js └── webpack-config.js ├── package.json ├── php ├── LICENSE.txt ├── app-translation.php ├── classes │ ├── actor.php │ ├── backup.php │ ├── color.php │ ├── csrf.php │ ├── csv-reader.php │ ├── employee-permissions.php │ ├── front-app.php │ ├── gdpr │ │ ├── basic.php │ │ ├── gdpr.php │ │ ├── interface.php │ │ └── psgdpr.php │ ├── integration │ │ ├── conseqs-trigger.php │ │ ├── datakick.php │ │ └── krona.php │ ├── migration-utils.php │ ├── no-permissions.php │ ├── notifications.php │ ├── permissions.php │ ├── platform │ │ ├── platform.php │ │ ├── ps16.php │ │ ├── ps17.php │ │ ├── ps8.php │ │ └── thirtybees.php │ ├── review-list.php │ ├── review-query.php │ ├── settings.php │ ├── shapes.php │ ├── utils.php │ ├── visitor-permissions.php │ └── visitor.php ├── config.xml.src ├── controllers │ ├── admin │ │ └── AdminRevwsBackendController.php │ └── front │ │ ├── AllReviews.php │ │ ├── EmailAction.php │ │ ├── MyReviews.php │ │ └── api.php ├── data │ └── images │ │ └── empty ├── index.php ├── license-header.php ├── license-header.tpl ├── logo.png ├── mails │ ├── bg │ │ ├── revws-admin-needs-approval.html │ │ ├── revws-admin-needs-approval.txt │ │ ├── revws-admin-review-created.html │ │ ├── revws-admin-review-created.txt │ │ ├── revws-admin-review-deleted.html │ │ ├── revws-admin-review-deleted.txt │ │ ├── revws-admin-review-updated.html │ │ ├── revws-admin-review-updated.txt │ │ ├── revws-author-review-approved.html │ │ ├── revws-author-review-approved.txt │ │ ├── revws-author-review-deleted.html │ │ ├── revws-author-review-deleted.txt │ │ ├── revws-author-review-replied.html │ │ ├── revws-author-review-replied.txt │ │ ├── revws-author-thank-you.html │ │ └── revws-author-thank-you.txt │ ├── cs │ │ ├── revws-admin-needs-approval.html │ │ ├── revws-admin-needs-approval.txt │ │ ├── revws-admin-review-created.html │ │ ├── revws-admin-review-created.txt │ │ ├── revws-admin-review-deleted.html │ │ ├── revws-admin-review-deleted.txt │ │ ├── revws-admin-review-updated.html │ │ ├── revws-admin-review-updated.txt │ │ ├── revws-author-review-approved.html │ │ ├── revws-author-review-approved.txt │ │ ├── revws-author-review-deleted.html │ │ ├── revws-author-review-deleted.txt │ │ ├── revws-author-review-replied.html │ │ ├── revws-author-review-replied.txt │ │ ├── revws-author-thank-you.html │ │ └── revws-author-thank-you.txt │ ├── de │ │ ├── revws-author-review-approved.html │ │ ├── revws-author-review-approved.txt │ │ ├── revws-author-review-deleted.html │ │ ├── revws-author-review-deleted.txt │ │ ├── revws-author-review-replied.html │ │ ├── revws-author-review-replied.txt │ │ ├── revws-author-thank-you.html │ │ └── revws-author-thank-you.txt │ ├── en │ │ ├── revws-admin-needs-approval.html │ │ ├── revws-admin-needs-approval.txt │ │ ├── revws-admin-review-created.html │ │ ├── revws-admin-review-created.txt │ │ ├── revws-admin-review-deleted.html │ │ ├── revws-admin-review-deleted.txt │ │ ├── revws-admin-review-updated.html │ │ ├── revws-admin-review-updated.txt │ │ ├── revws-author-review-approved.html │ │ ├── revws-author-review-approved.txt │ │ ├── revws-author-review-deleted.html │ │ ├── revws-author-review-deleted.txt │ │ ├── revws-author-review-replied.html │ │ ├── revws-author-review-replied.txt │ │ ├── revws-author-thank-you.html │ │ └── revws-author-thank-you.txt │ ├── es │ │ ├── revws-admin-needs-approval.html │ │ ├── revws-admin-needs-approval.txt │ │ ├── revws-admin-review-created.html │ │ ├── revws-admin-review-created.txt │ │ ├── revws-admin-review-deleted.html │ │ ├── revws-admin-review-deleted.txt │ │ ├── revws-admin-review-updated.html │ │ ├── revws-admin-review-updated.txt │ │ ├── revws-author-review-approved.html │ │ ├── revws-author-review-approved.txt │ │ ├── revws-author-review-deleted.html │ │ ├── revws-author-review-deleted.txt │ │ ├── revws-author-review-replied.html │ │ ├── revws-author-review-replied.txt │ │ ├── revws-author-thank-you.html │ │ └── revws-author-thank-you.txt │ ├── fr │ │ ├── revws-admin-needs-approval.html │ │ ├── revws-admin-needs-approval.txt │ │ ├── revws-admin-review-created.html │ │ ├── revws-admin-review-created.txt │ │ ├── revws-admin-review-deleted.html │ │ ├── revws-admin-review-deleted.txt │ │ ├── revws-admin-review-updated.html │ │ ├── revws-admin-review-updated.txt │ │ ├── revws-author-review-approved.html │ │ ├── revws-author-review-approved.txt │ │ ├── revws-author-review-deleted.html │ │ ├── revws-author-review-deleted.txt │ │ ├── revws-author-review-replied.html │ │ ├── revws-author-review-replied.txt │ │ ├── revws-author-thank-you.html │ │ └── revws-author-thank-you.txt │ ├── it │ │ ├── revws-admin-needs-approval.html │ │ ├── revws-admin-needs-approval.txt │ │ ├── revws-admin-review-created.html │ │ ├── revws-admin-review-created.txt │ │ ├── revws-admin-review-deleted.html │ │ ├── revws-admin-review-deleted.txt │ │ ├── revws-admin-review-updated.html │ │ ├── revws-admin-review-updated.txt │ │ ├── revws-author-review-approved.html │ │ ├── revws-author-review-approved.txt │ │ ├── revws-author-review-deleted.html │ │ ├── revws-author-review-deleted.txt │ │ ├── revws-author-review-replied.html │ │ ├── revws-author-review-replied.txt │ │ ├── revws-author-thank-you.html │ │ └── revws-author-thank-you.txt │ ├── mx │ │ ├── revws-admin-needs-approval.html │ │ ├── revws-admin-needs-approval.txt │ │ ├── revws-admin-review-created.html │ │ ├── revws-admin-review-created.txt │ │ ├── revws-admin-review-deleted.html │ │ ├── revws-admin-review-deleted.txt │ │ ├── revws-admin-review-updated.html │ │ ├── revws-admin-review-updated.txt │ │ ├── revws-author-review-approved.html │ │ ├── revws-author-review-approved.txt │ │ ├── revws-author-review-deleted.html │ │ ├── revws-author-review-deleted.txt │ │ ├── revws-author-review-replied.html │ │ ├── revws-author-review-replied.txt │ │ ├── revws-author-thank-you.html │ │ └── revws-author-thank-you.txt │ ├── nl │ │ ├── revws-admin-needs-approval.html │ │ ├── revws-admin-needs-approval.txt │ │ ├── revws-admin-review-created.html │ │ ├── revws-admin-review-created.txt │ │ ├── revws-admin-review-deleted.html │ │ ├── revws-admin-review-deleted.txt │ │ ├── revws-admin-review-updated.html │ │ ├── revws-admin-review-updated.txt │ │ ├── revws-author-review-approved.html │ │ ├── revws-author-review-approved.txt │ │ ├── revws-author-review-deleted.html │ │ ├── revws-author-review-deleted.txt │ │ ├── revws-author-review-replied.html │ │ ├── revws-author-review-replied.txt │ │ ├── revws-author-thank-you.html │ │ └── revws-author-thank-you.txt │ ├── pl │ │ ├── revws-admin-needs-approval.html │ │ ├── revws-admin-needs-approval.txt │ │ ├── revws-admin-review-created.html │ │ ├── revws-admin-review-created.txt │ │ ├── revws-admin-review-deleted.html │ │ ├── revws-admin-review-deleted.txt │ │ ├── revws-admin-review-updated.html │ │ ├── revws-admin-review-updated.txt │ │ ├── revws-author-review-approved.html │ │ ├── revws-author-review-approved.txt │ │ ├── revws-author-review-deleted.html │ │ ├── revws-author-review-deleted.txt │ │ ├── revws-author-review-replied.html │ │ ├── revws-author-review-replied.txt │ │ ├── revws-author-thank-you.html │ │ └── revws-author-thank-you.txt │ ├── pt │ │ ├── revws-admin-needs-approval.html │ │ ├── revws-admin-needs-approval.txt │ │ ├── revws-admin-review-created.html │ │ ├── revws-admin-review-created.txt │ │ ├── revws-admin-review-deleted.html │ │ ├── revws-admin-review-deleted.txt │ │ ├── revws-admin-review-updated.html │ │ ├── revws-admin-review-updated.txt │ │ ├── revws-author-review-approved.html │ │ ├── revws-author-review-approved.txt │ │ ├── revws-author-review-deleted.html │ │ ├── revws-author-review-deleted.txt │ │ ├── revws-author-review-replied.html │ │ ├── revws-author-review-replied.txt │ │ ├── revws-author-thank-you.html │ │ └── revws-author-thank-you.txt │ └── ru │ │ ├── revws-admin-needs-approval.html │ │ ├── revws-admin-needs-approval.txt │ │ ├── revws-admin-review-created.html │ │ ├── revws-admin-review-created.txt │ │ ├── revws-admin-review-deleted.html │ │ ├── revws-admin-review-deleted.txt │ │ ├── revws-admin-review-updated.html │ │ ├── revws-admin-review-updated.txt │ │ ├── revws-author-review-approved.html │ │ ├── revws-author-review-approved.txt │ │ ├── revws-author-review-deleted.html │ │ ├── revws-author-review-deleted.txt │ │ ├── revws-author-review-replied.html │ │ ├── revws-author-review-replied.txt │ │ ├── revws-author-thank-you.html │ │ └── revws-author-thank-you.txt ├── model │ ├── criterion.php │ └── review.php ├── revws.php ├── sql │ ├── add-entity-type.sql │ ├── fix-table-charset.sql │ ├── install.sql │ ├── migrate-productcomments.sql │ ├── migrate-yotpo.sql │ ├── uninstall.sql │ ├── update-review-image.sql │ └── update-verified-buyer.sql └── views │ ├── css │ ├── back.css │ ├── back.ps17.css │ ├── fallback.css │ └── themes │ │ ├── classic.css │ │ └── empty │ ├── img │ ├── productcomments.png │ ├── verified-buyer-badge.svg │ └── yotpo.svg │ ├── js │ ├── revws_bootstrap.js │ └── revws_bootstrap.ps17.js │ └── templates │ ├── admin │ └── backend.tpl │ ├── front │ ├── all-reviews.ps17.tpl │ ├── all-reviews.tpl │ ├── css.ps17.tpl │ ├── css.tpl │ ├── email-action-approval.ps17.tpl │ ├── email-action-approval.tpl │ ├── email-action-error.ps17.tpl │ ├── email-action-error.tpl │ ├── my-account-block.tpl │ ├── my-account.ps17.tpl │ ├── my-account.tpl │ ├── my-reviews.ps17.tpl │ └── my-reviews.tpl │ ├── helpers │ └── grading.tpl │ ├── hook │ ├── display-revws-review.tpl │ ├── product_footer.tpl │ ├── product_list.ps17.tpl │ ├── product_list.tpl │ ├── product_tab_content.tpl │ ├── product_tab_header.tpl │ ├── products_comparison.tpl │ ├── review-average.ps17.tpl │ ├── review-average.tpl │ └── widget.tpl │ └── widgets │ ├── entity-review-list │ └── entity-review-list.tpl │ ├── my-reviews │ └── my-reviews.tpl │ └── review-list │ ├── item-with-entity.tpl │ ├── item.tpl │ ├── list.tpl │ └── paging.tpl ├── translations ├── bg.json ├── cs.json ├── de.json ├── en.json ├── es.json ├── fr.json ├── it.json ├── mx.json ├── nl.json ├── pl.json ├── pt.json └── ru.json ├── utils ├── merge-keys.php ├── transl-to-keys.php └── translation.php └── yarn.lock /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/node_modules/.*/node_modules/.* 3 | .*/node_modules/babel-core/.* 4 | .*/node_modules/babel-loader/.* 5 | .*/node_modules/babel-register/.* 6 | .*/node_modules/config-chain/.* 7 | .*/node_modules/flow-bin/.* 8 | .*/node_modules/inline-style-prefixer/.* 9 | .*/node_modules/invariant/.* 10 | .*/node_modules/mkdirp/.* 11 | .*/node_modules/nodemon/.* 12 | .*/node_modules/npmconf/.* 13 | .*/node_modules/react-hot-loader/.* 14 | .*/node_modules/rimraf/.* 15 | .*/node_modules/watchify/.* 16 | .*/node_modules/webpack-dev-server/.* 17 | .*/node_modules/webpack/.* 18 | .*/node_modules/react-event-listener/.* 19 | .*/node_modules/draft-js/.* 20 | .*/node_modules/radium/.* 21 | 22 | [include] 23 | 24 | [libs] 25 | js/flow-typed 26 | 27 | [options] 28 | module.system=haste 29 | module.name_mapper='.*\(.css\)' -> 'CSSModule' 30 | module.name_mapper='.*\(.less\)' -> 'CSSModule' 31 | module.name_mapper='^\(types\)$' -> '/js/\1' 32 | module.name_mapper='^front\/\(.*\)$' -> '/js/front/\1' 33 | module.name_mapper='^back\/\(.*\)$' -> '/js/back/\1' 34 | module.name_mapper='^common\/\(.*\)$' -> '/js/common/\1' 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | revws*.zip 3 | build 4 | php/config.xml 5 | php/views/css/revws-*.css 6 | yarn-error.log 7 | -------------------------------------------------------------------------------- /.tbstore/configuration.yml: -------------------------------------------------------------------------------- 1 | module_name: Revws - Product Reviews 2 | compatible_versions: 1.0.0+ 3 | author: datakick 4 | category: front office 5 | tags: 6 | - product reviews 7 | description_short: This module let your customers easily create product reviews 8 | description: description.md 9 | images: 10 | - image-cover.png 11 | - image-1.png 12 | - image-2.png 13 | - image-3.png 14 | - image-4.png 15 | - image-5.png 16 | license: AFL 3.0 17 | php_version: 18 | - 5.4 19 | - 5.5 20 | - 5.6 21 | - 7.0 22 | - 7.1 23 | gdpr_compliant: true 24 | -------------------------------------------------------------------------------- /.tbstore/description.md: -------------------------------------------------------------------------------- 1 | ## description 2 | 3 | This free module let your customer create product reviews in the most user friendly way possible. 4 | 5 | ![screencast](https://www.getdatakick.com/images/extras/revws-product-reviews/screencast.gif) 6 | 7 | During module development I've cooperated closely with actual [merchants](https://forum.thirtybees.com/topic/1235/i-m-going-to-create-a-free-module), so you can be sure this module covers the most common use cases. 8 | 9 | ## Features 10 | 11 | - both customer and guest reviews 12 | - review moderation by administration 13 | - multiple review criteria 14 | - theming options - you don't have to use standard star symbol anymore 15 | - google structured data / rich snippets support 16 | - customers can edit / delete their reviews 17 | - voting and report abuse buttons 18 | - review suggestions based on recent purchase 19 | - comprehensive settings - you can tweak almost anything 20 | - and much more 21 | 22 | ## Accessing review data 23 | 24 | This free module is integrated with [DataKick module](https://www.getdatakick.com/) - your review data will be available in lists, xml exports, you can use inline editing and mass updates functionality. You can even import your reviews. Minimal required version of datakick module is 2.1.0 25 | 26 | ![price alert datakick integration](https://www.getdatakick.com/images/extras/revws-product-reviews/image-5.png) 27 | -------------------------------------------------------------------------------- /.tbstore/images/image-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getdatakick/revws/cd2fb72d790b89be82f5a2f6e50d2ff866f74756/.tbstore/images/image-1.png -------------------------------------------------------------------------------- /.tbstore/images/image-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getdatakick/revws/cd2fb72d790b89be82f5a2f6e50d2ff866f74756/.tbstore/images/image-2.png -------------------------------------------------------------------------------- /.tbstore/images/image-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getdatakick/revws/cd2fb72d790b89be82f5a2f6e50d2ff866f74756/.tbstore/images/image-3.png -------------------------------------------------------------------------------- /.tbstore/images/image-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getdatakick/revws/cd2fb72d790b89be82f5a2f6e50d2ff866f74756/.tbstore/images/image-4.png -------------------------------------------------------------------------------- /.tbstore/images/image-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getdatakick/revws/cd2fb72d790b89be82f5a2f6e50d2ff866f74756/.tbstore/images/image-5.png -------------------------------------------------------------------------------- /.tbstore/images/image-cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getdatakick/revws/cd2fb72d790b89be82f5a2f6e50d2ff866f74756/.tbstore/images/image-cover.png -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react", 5 | "@babel/preset-flow" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /js/back/actions/types.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export default { 4 | activateAccount: 'ACTIVATE_ACCOUNT', 5 | activateAccountFailed: 'ACTIVATE_ACCOUNT_FAILED', 6 | checkModuleVersion: 'CHECK_MODULE_VERSION', 7 | checkModuleVersionFailed: 'CHECK_MODULE_VERSION_FAILED', 8 | setLatestVersion: 'SET_LATEST_VERSION', 9 | 10 | goTo: 'GO_TO', 11 | 12 | loadData: 'LOAD_DATA', 13 | setData: 'SET_DATA', 14 | 15 | setSettings: 'SET_SETTINGS', 16 | setSize: 'SET_SIZE', 17 | setSnackbar: 'SET_SNACKBAR', 18 | 19 | setCriteria: 'SET_CRITERIA', 20 | saveCriterion: 'SAVE_CRITERION', 21 | criterionSaved: 'CRITERION_SAVED', 22 | deleteCriterion: 'DELETE_CRITERION', 23 | criterionDeleted: 'CRITERION_DELETED', 24 | 25 | reviewUpdated: 'REVIEW_UPDATED', 26 | reviewCreated: 'REVIEW_CREATED', 27 | reviewDeleted: 'REVIEW_DELETED', 28 | approveReview: 'APPROVE_REVIEW', 29 | deleteReview: 'DELETE_REVIEW', 30 | undeleteReview: 'UNDELETE_REVIEW', 31 | saveReview: 'SAVE_REVIEW', 32 | 33 | migrateData: 'MIGRATE_DATA', 34 | uploadYotpoCsv: 'UPLOAD_YOTPO_CSV', 35 | exportReviews: 'EXPORT_REVIEWS', 36 | 37 | refreshData: 'REFRESH_DATA', 38 | setReviewed: 'SET_REVIEWED', 39 | }; 40 | -------------------------------------------------------------------------------- /js/back/app.less: -------------------------------------------------------------------------------- 1 | .app { 2 | padding-top: 20px; 3 | h2 { 4 | font-family: "Roboto", "Helvetica", "Arial", sans-serif; 5 | color: #666; 6 | font-weight: 500; 7 | font-size: 1.5rem; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /js/back/commands/activate-account.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Api } from 'common/types.js'; 3 | import type { GlobalDataType } from 'back/types.js'; 4 | import type { ActivateAccountAction } from 'back/actions/index.js'; 5 | import { activateAccountFailed, setSnackbar } from 'back/actions/creators.js'; 6 | import { getApiUrl } from 'back/utils/common.js'; 7 | 8 | export const activateAccount = (data: GlobalDataType): ((action: ActivateAccountAction, store: any, api: Api) => void) => (action: ActivateAccountAction, store: any, api: Api) => { 9 | window.$.ajax({ 10 | url: getApiUrl(data), 11 | type: 'POST', 12 | dataType: 'json', 13 | data: { 14 | json: JSON.stringify({ 15 | module: 'revws', 16 | command: 'activate', 17 | payload: { 18 | domain: location.hostname, 19 | version: data.version, 20 | licenseType: 'free', 21 | platform: data.platform, 22 | platformVersion: data.platformVersion, 23 | email: action.email, 24 | emailPreferences: action.emailPreferences 25 | } 26 | }) 27 | }, 28 | success: (data) => { 29 | if (data) { 30 | if (data.error) { 31 | store.dispatch(setSnackbar(data.error)); 32 | store.dispatch(activateAccountFailed()); 33 | } else { 34 | api('activate', {}); 35 | } 36 | } else { 37 | store.dispatch(activateAccountFailed()); 38 | } 39 | }, 40 | error: (res) => { 41 | if (res.responseJSON && res.responseJSON.error) { 42 | store.dispatch(setSnackbar(res.responseJSON.error)); 43 | } 44 | store.dispatch(activateAccountFailed()); 45 | }, 46 | }); 47 | }; 48 | -------------------------------------------------------------------------------- /js/back/commands/approve-review.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { Api } from 'common/types.js'; 4 | import type { ApproveReviewAction } from 'back/actions/index.js'; 5 | import { setSnackbar, reviewUpdated } from 'back/actions/creators.js'; 6 | import { fixReview } from 'common/utils/reviews.js'; 7 | 8 | 9 | export const approveReview = (action: ApproveReviewAction, store: any, api: Api) => { 10 | const id = action.id; 11 | api('approveReview', { id }).then(result => { 12 | if (result.type === 'success') { 13 | store.dispatch(reviewUpdated(fixReview(result.data))); 14 | store.dispatch(setSnackbar(__('Review has been approved'))); 15 | } else { 16 | store.dispatch(setSnackbar(__('Failed to approve review'))); 17 | } 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /js/back/commands/delete-criterion.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { Api } from 'common/types.js'; 4 | import type { DeleteCriterionAction } from 'back/actions/index.js'; 5 | import { setSnackbar, criterionDeleted } from 'back/actions/creators.js'; 6 | 7 | export const deleteCriterion = (action: DeleteCriterionAction, store: any, api: Api) => { 8 | const id = action.id; 9 | api('deleteCriterion', { id }).then(result => { 10 | if (result.type === 'success') { 11 | store.dispatch(criterionDeleted(id)); 12 | store.dispatch(setSnackbar(__('Criterion deleted'))); 13 | } else { 14 | store.dispatch(setSnackbar(__('Failed to delete criterion'))); 15 | } 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /js/back/commands/delete-review.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { Api } from 'common/types.js'; 4 | import type { DeleteReviewAction } from 'back/actions/index.js'; 5 | import { setSnackbar, reviewUpdated, reviewDeleted } from 'back/actions/creators.js'; 6 | import { fixReview } from 'common/utils/reviews.js'; 7 | 8 | export const deleteReview = (action: DeleteReviewAction, store: any, api: Api) => { 9 | const { id, permanently } = action; 10 | api('deleteReview', { id, permanently }).then(result => { 11 | if (result.type === 'success') { 12 | if (permanently) { 13 | store.dispatch(reviewDeleted(id)); 14 | store.dispatch(setSnackbar(__('Review has been deleted'))); 15 | } else { 16 | store.dispatch(reviewUpdated(fixReview(result.data))); 17 | store.dispatch(setSnackbar(__('Review has been marked as deleted'))); 18 | } 19 | } else { 20 | store.dispatch(setSnackbar(__('Failed to delete review'))); 21 | } 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /js/back/commands/export-reviews.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { Api } from 'common/types.js'; 4 | import type { ExportReviewsAction } from 'back/actions/index.js'; 5 | import { setSnackbar } from 'back/actions/creators.js'; 6 | 7 | export const exportReviews = (action: ExportReviewsAction, store: any, api: Api) => { 8 | api('export', {}).then(result => { 9 | if (result.type === 'success') { 10 | const blob = new Blob([result.data], { type: 'text/xml' }); 11 | const link = document.createElement('a'); 12 | link.href = window.URL.createObjectURL(blob); 13 | link.download = 'revws.xml'; 14 | link.style.display = 'none'; 15 | const body = document.body; 16 | if (body) { 17 | body.appendChild(link); 18 | link.click(); 19 | setTimeout(() => body.removeChild(link), 1000); 20 | } 21 | store.dispatch(setSnackbar(__('Reviews has been exported'))); 22 | } else { 23 | store.dispatch(setSnackbar(__('Failed to export reviews'))); 24 | } 25 | }); 26 | }; 27 | -------------------------------------------------------------------------------- /js/back/commands/load-data.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { Api } from 'common/types.js'; 4 | import type { LoadDataAction } from 'back/actions/index.js'; 5 | import { assoc } from 'ramda'; 6 | import { setSnackbar, setData } from 'back/actions/creators.js'; 7 | import { fixReviews } from 'common/utils/reviews.js'; 8 | import type { LoadTypes } from 'back/types.js'; 9 | 10 | const fixReviewPayload = (payload: any) => assoc('reviews', fixReviews(payload.reviews), payload); 11 | 12 | const fixData = (load: LoadTypes, data: any) => { 13 | for (let k in load) { 14 | const def = load[k]; 15 | if (def.record === 'reviews') { 16 | data = assoc(k, fixReviewPayload(data[k]), data); 17 | } 18 | } 19 | return data; 20 | }; 21 | 22 | export const loadData = (action: LoadDataAction, store: any, api: Api) => { 23 | api('loadData', { types: action.types }).then(result => { 24 | if (result.type === 'success') { 25 | store.dispatch(setData(fixData(action.types, result.data))); 26 | } else { 27 | store.dispatch(setSnackbar(__('Failed to load data'))); 28 | } 29 | }); 30 | }; 31 | -------------------------------------------------------------------------------- /js/back/commands/migrate-data.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { Api } from 'common/types.js'; 4 | import type { MigrateDataAction } from 'back/actions/index.js'; 5 | import { setSnackbar, setCriteria, refreshData } from 'back/actions/creators.js'; 6 | 7 | export const migrateData = (action: MigrateDataAction, store: any, api: Api) => { 8 | const { source, payload } = action; 9 | api('migrateData', { source, payload }).then(result => { 10 | store.dispatch(refreshData()); 11 | if (result.type === 'success') { 12 | store.dispatch(setCriteria(result.data.criteria)); 13 | store.dispatch(setSnackbar(__('Data has been imported'))); 14 | } else { 15 | store.dispatch(setSnackbar(__('Failed to migrate data'))); 16 | } 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /js/back/commands/save-criterion.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { Api } from 'common/types.js'; 4 | import type { SaveCriterionAction } from 'back/actions/index.js'; 5 | import { setSnackbar, criterionSaved } from 'back/actions/creators.js'; 6 | 7 | export const saveCriterion = (action: SaveCriterionAction, store: any, api: Api) => { 8 | api('saveCriterion', action.criterion).then(result => { 9 | if (result.type === 'success') { 10 | const criterion = result.data; 11 | store.dispatch(criterionSaved(criterion)); 12 | store.dispatch(setSnackbar(__('Criterion saved'))); 13 | } else { 14 | store.dispatch(setSnackbar(__('Failed to save criterion'))); 15 | } 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /js/back/commands/save-review.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { Api } from 'common/types.js'; 4 | import type { SaveReviewAction } from 'back/actions/index.js'; 5 | import { setSnackbar, reviewUpdated, reviewCreated } from 'back/actions/creators.js'; 6 | import { fixReview } from 'common/utils/reviews.js'; 7 | import moment from 'moment'; 8 | 9 | export const saveReview = (action: SaveReviewAction, store: any, api: Api) => { 10 | const review = { 11 | ...action.review, 12 | date: moment(action.review.date).format('YYYY-MM-DD') 13 | }; 14 | api('saveReview', review).then(result => { 15 | if (result.type === 'success') { 16 | if (review.id > 0) { 17 | store.dispatch(reviewUpdated(fixReview(result.data))); 18 | store.dispatch(setSnackbar(__('Review saved'))); 19 | } else { 20 | store.dispatch(reviewCreated(fixReview(result.data))); 21 | store.dispatch(setSnackbar(__('Review has been created'))); 22 | } 23 | } else { 24 | store.dispatch(setSnackbar(__('Failed to save review'))); 25 | } 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /js/back/commands/save-settings.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { Api } from 'common/types.js'; 4 | import type { SetSettingsAction } from 'back/actions/index.js'; 5 | import { setSnackbar } from 'back/actions/creators.js'; 6 | 7 | export const saveSettings = (action: SetSettingsAction, store: any, api: Api) => { 8 | api('saveSettings', action.settings).then(result => { 9 | if (result.type === 'success') { 10 | store.dispatch(setSnackbar(__('Settings successfully saved'))); 11 | } else { 12 | store.dispatch(setSnackbar(__('Failed to update settings'))); 13 | } 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /js/back/commands/set-latest-version.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { Api } from 'common/types.js'; 4 | import type { SetLatestVersionAction } from 'back/actions/index.js'; 5 | import { pick } from 'ramda'; 6 | 7 | export const setLatestVersion = (action: SetLatestVersionAction, store: any, api: Api) => { 8 | api('setLatestVersion', pick(['version', 'ts', 'notes', 'paid'], action)); 9 | }; 10 | -------------------------------------------------------------------------------- /js/back/commands/set-reviewed.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { Api } from 'common/types.js'; 4 | import type { SetReviewedAction } from 'back/actions/index.js'; 5 | import { setSnackbar } from 'back/actions/creators.js'; 6 | 7 | export const setReviewed = (action: SetReviewedAction, store: any, api: Api) => { 8 | api('setReviewed', {}) 9 | .then(() => store.dispatch(setSnackbar(__('Thank you for your review')))); 10 | }; 11 | -------------------------------------------------------------------------------- /js/back/commands/undelete-review.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { Api } from 'common/types.js'; 4 | import type { UndeleteReviewAction } from 'back/actions/index.js'; 5 | import { setSnackbar, reviewUpdated } from 'back/actions/creators.js'; 6 | import { fixReview } from 'common/utils/reviews.js'; 7 | 8 | export const undeleteReview = (action: UndeleteReviewAction, store: any, api: Api) => { 9 | const id = action.id; 10 | api('undeleteReview', { id }).then(result => { 11 | if (result.type === 'success') { 12 | store.dispatch(reviewUpdated(fixReview(result.data))); 13 | store.dispatch(setSnackbar(__('Review has been activated'))); 14 | } else { 15 | store.dispatch(setSnackbar(__('Failed to undelete review'))); 16 | } 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /js/back/commands/upload-yopto.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { Api } from 'common/types.js'; 4 | import type { UploadYotpoCsvAction } from 'back/actions/index.js'; 5 | import { setSnackbar, setCriteria } from 'back/actions/creators.js'; 6 | 7 | export const uploadYotpoCsv = (action: UploadYotpoCsvAction, store: any, api: Api) => { 8 | api('importYotpo', { file: action.file }).then(result => { 9 | if (result.type === 'success') { 10 | const { total, success, siteReviews, errors } = result.data.result; 11 | store.dispatch(setCriteria(result.data.criteria)); 12 | if (total === success) { 13 | store.dispatch(setSnackbar(__('%s reviews has been imported', success))); 14 | } else { 15 | store.dispatch(setSnackbar(__('%s reviews out of %s has been imported. See javascript console for more info!', success, total))); 16 | } 17 | console.info("YOTPO IMPORT"); 18 | console.info("Total lines: "+total); 19 | console.info("Successfully imported: "+success); 20 | console.info("Skipped site reviews: "+siteReviews); 21 | for (let i=0; i { 15 | static displayName: ?string = 'Markdown'; 16 | 17 | render(): null | Element<"div"> { 18 | const { content, className, ...rest } = this.props; 19 | const __html = content ? toHTML(content) : null; 20 | return content ? ( 21 |
26 | ) : null; 27 | } 28 | } 29 | 30 | export default Markdown; 31 | -------------------------------------------------------------------------------- /js/back/components/markdown/markdown.less: -------------------------------------------------------------------------------- 1 | .markdown { 2 | p { 3 | margin-top: 0px; 4 | margin-bottom: 10px; 5 | } 6 | ul { 7 | padding-left: 25px; 8 | } 9 | li { 10 | margin-bottom: 6px; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /js/back/components/navigation/navigation.less: -------------------------------------------------------------------------------- 1 | .root { 2 | display: flex; 3 | margin-bottom: 30px; 4 | } 5 | 6 | .tab { 7 | position: relative; 8 | } 9 | 10 | .left { 11 | flex-grow: 1; 12 | } 13 | 14 | .right { 15 | flex-grow: 0; 16 | &>div { 17 | padding-left: 10px; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /js/back/components/registration/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { ComponentType } from 'react'; 3 | import type { State } from 'back/reducer/index.js'; 4 | import type { EmailPreferences } from 'back/types.js'; 5 | import type { Props } from './registration.jsx'; 6 | import Component from './registration.jsx'; 7 | import { connect } from 'react-redux'; 8 | import { activateAccount } from 'back/actions/creators.js'; 9 | import { isActivated } from 'back/selectors/account.js'; 10 | 11 | type OwnProps = {| 12 | show: boolean, 13 | |} 14 | 15 | type Actions = {| 16 | activateAccount: (string, EmailPreferences) => void 17 | |} 18 | 19 | type PassedProps = {| 20 | isRtl: boolean, 21 | |} 22 | 23 | const mapStateToProps = (state: State): OwnProps => ({ 24 | show: !isActivated(state), 25 | }); 26 | 27 | const actions = { 28 | activateAccount 29 | }; 30 | 31 | const merge = (props: OwnProps, actions: Actions, passed: PassedProps): Props => ({ 32 | ...props, 33 | ...actions, 34 | ...passed 35 | }); 36 | 37 | 38 | const connectRedux = connect(mapStateToProps, actions, merge); 39 | const ConnectedComponent: ComponentType = connectRedux(Component); 40 | 41 | export default ConnectedComponent; 42 | -------------------------------------------------------------------------------- /js/back/components/registration/legal.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type {Node} from 'react'; 3 | import React from 'react'; 4 | import SvgIcon from 'material-ui/SvgIcon'; 5 | 6 | class LegalIcon extends React.PureComponent<{}> { 7 | static displayName: ?string = 'XmlIcon'; 8 | 9 | render(): Node { 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ); 24 | } 25 | } 26 | export default LegalIcon; 27 | -------------------------------------------------------------------------------- /js/back/components/reviews-table/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { ComponentType } from 'react'; 3 | import type { State } from 'back/reducer/index.js'; 4 | import type { LoadPagination } from 'back/types.js'; 5 | import type { InputProps } from './controller.jsx'; 6 | import { connect } from 'react-redux'; 7 | import { saveReview, loadData, approveReview, deleteReview, deletePermReview, undeleteReview } from 'back/actions/creators.js'; 8 | import Controller from './controller.jsx'; 9 | 10 | const mapStateToProps = (state: State) => ({ 11 | data: state.data 12 | }); 13 | 14 | const actions = { 15 | approveReview, 16 | deleteReview, 17 | deletePermReview, 18 | undeleteReview, 19 | saveReview, 20 | loadData: (key: string, pagination: LoadPagination) => loadData({ 21 | [ key ]: { 22 | record: 'reviews', 23 | pagination 24 | } 25 | }) 26 | }; 27 | 28 | const connectRedux = connect(mapStateToProps, actions); 29 | const ConnectedComponent: ComponentType = connectRedux(Controller); 30 | 31 | export default ConnectedComponent; 32 | -------------------------------------------------------------------------------- /js/back/components/reviews-table/types.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { ListOrder, EntityType } from 'common/types.js'; 3 | 4 | export type Filters = { 5 | deleted?: boolean, 6 | validated?: boolean, 7 | entityType?: EntityType 8 | } 9 | 10 | export type Column = { 11 | id: string, 12 | label: string, 13 | disablePadding?: boolean, 14 | sort?: ListOrder 15 | } 16 | -------------------------------------------------------------------------------- /js/back/components/section/section.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type {Node} from 'react'; 3 | import React from 'react'; 4 | import styles from './section.less'; 5 | import Paper from 'material-ui/Paper'; 6 | 7 | type Props = { 8 | id: string, 9 | label: string, 10 | subheader?: ?string, 11 | children: any, 12 | indent: boolean 13 | }; 14 | 15 | class SettingsSection extends React.PureComponent { 16 | 17 | static defaultProps: {|indent: boolean|} = { 18 | indent: true 19 | } 20 | 21 | render(): Node { 22 | const { id, subheader, label, children, indent } = this.props; 23 | return ( 24 | 25 |

{ label }

26 | {subheader && ( 27 |
28 | {subheader} 29 |
30 | )} 31 |
32 | { children } 33 |
34 |
35 | ); 36 | } 37 | } 38 | 39 | export default SettingsSection; 40 | -------------------------------------------------------------------------------- /js/back/components/section/section.less: -------------------------------------------------------------------------------- 1 | .section { 2 | padding: 0px 24px 30px 24px; 3 | margin-bottom: 3rem; 4 | .space { 5 | margin-top: 2rem; 6 | } 7 | label { 8 | width: 100%; 9 | } 10 | } 11 | 12 | .sectionLabel { 13 | margin: 0; 14 | font-weight:500; 15 | min-height: 64px; 16 | display: flex; 17 | align-items: center; 18 | } 19 | 20 | .sectionContent { 21 | margin-top: 2rem; 22 | } 23 | 24 | .subheader { 25 | font-size: 120%; 26 | margin-top: -0.5rem; 27 | line-height: 1.5em; 28 | color: #999; 29 | } 30 | -------------------------------------------------------------------------------- /js/back/components/select-customer/index.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { ComponentType } from 'react'; 3 | import type { CustomerInfoType } from 'common/types.js'; 4 | import { connect } from 'react-redux'; 5 | import { mapObject } from 'common/utils/redux.js'; 6 | import { getCustomers } from 'back/selectors/data.js'; 7 | import { loadData } from 'back/actions/creators.js'; 8 | import SelectCustomer from './select-customer.jsx'; 9 | 10 | const mapStateToProps = mapObject({ 11 | customers: getCustomers, 12 | }); 13 | 14 | const actions = { 15 | loadData 16 | }; 17 | 18 | const connectRedux = connect(mapStateToProps, actions); 19 | const ConnectedComponent: ComponentType<{ 20 | onSelect: (CustomerInfoType) => void, 21 | }> = connectRedux(SelectCustomer); 22 | 23 | export default ConnectedComponent; 24 | -------------------------------------------------------------------------------- /js/back/components/select-entity-type/select-entity-type.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type {Node} from 'react'; 3 | import React from 'react'; 4 | import type { EntityType } from 'common/types.js'; 5 | import { sortBy, prop, toPairs } from 'ramda'; 6 | import List, { ListItem, ListItemText } from 'material-ui/List'; 7 | 8 | export type Props = { 9 | entityTypes: { [ EntityType ]: string }, 10 | onSelect: (EntityType) => void 11 | } 12 | 13 | class SelectEntityType extends React.PureComponent { 14 | static displayName:?string = 'SelectEntityType'; 15 | 16 | render: (() => Node) = () => { 17 | const { entityTypes } = this.props; 18 | const pairs = sortBy(prop(1), toPairs(entityTypes)); 19 | return ( 20 | 21 | { pairs.map(this.renderEntityType) } 22 | 23 | ); 24 | } 25 | 26 | renderEntityType: ((pair: [EntityType, string]) => Node) = (pair: [EntityType, string]) => { 27 | const [ type, name ] = pair; 28 | return ( 29 | this.props.onSelect(type)}> 30 | 31 | 32 | ); 33 | } 34 | } 35 | 36 | export default SelectEntityType; 37 | -------------------------------------------------------------------------------- /js/back/components/select-entity/index.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { ComponentType } from 'react'; 3 | import { connect } from 'react-redux'; 4 | import { mapObject } from 'common/utils/redux.js'; 5 | import { getEntities } from 'back/selectors/data.js'; 6 | import { loadData } from 'back/actions/creators.js'; 7 | import SelectEntity from './select-entity.jsx'; 8 | import type { InputProps } from './select-entity.jsx'; 9 | 10 | const mapStateToProps = mapObject({ 11 | entities: getEntities, 12 | }); 13 | 14 | const actions = { 15 | loadData 16 | }; 17 | 18 | const connectRedux = connect(mapStateToProps, actions); 19 | const ConnectedComponent: ComponentType = connectRedux(SelectEntity); 20 | 21 | export default ConnectedComponent; 22 | -------------------------------------------------------------------------------- /js/back/components/select-entity/select-entity.less: -------------------------------------------------------------------------------- 1 | .suggest { 2 | height: 48px * 8 + 10; 3 | } 4 | 5 | .center { 6 | display: flex; 7 | flex-direction: column; 8 | align-items: center; 9 | justify-content: center; 10 | height: 100%; 11 | color: #ccc; 12 | font-size: 2rem; 13 | } 14 | 15 | .number { 16 | min-width: 50px; 17 | color: #999; 18 | } 19 | 20 | .email { 21 | float: right; 22 | color: #999; 23 | }; 24 | -------------------------------------------------------------------------------- /js/back/pages/criteria/criteria.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type {Element} from 'react'; 4 | import React from 'react'; 5 | import { filter } from 'ramda'; 6 | import CriteriaSection from './criteria-section.jsx'; 7 | import Section from 'back/components/section/section.jsx'; 8 | import type { EntityType, LanguagesType, KeyValue } from 'common/types.js'; 9 | import type { Load, FullCriteria, FullCriterion } from 'back/types.js'; 10 | 11 | export type Props = {| 12 | criteria: FullCriteria, 13 | products: ?KeyValue, 14 | categories: ?KeyValue, 15 | language: number, 16 | languages: LanguagesType, 17 | loadData: ({ 18 | [ string ]: Load 19 | }) => void, 20 | onSaveCriterion: (FullCriterion) => void, 21 | onDeleteCriterion: (number) => void, 22 | |}; 23 | 24 | class CriteriaPage extends React.PureComponent { 25 | static displayName: ?string = 'CriteriaPage'; 26 | 27 | render(): Element<"div"> { 28 | const { criteria, ...rest } = this.props; 29 | return ( 30 |
31 |
32 | 38 |
39 |
40 | ); 41 | } 42 | } 43 | 44 | const getCriteria = (type: EntityType, criteria: FullCriteria): FullCriteria => filter((crit: FullCriterion) => crit.entityType === type, criteria); 45 | 46 | export default CriteriaPage; 47 | -------------------------------------------------------------------------------- /js/back/pages/criteria/criteria.less: -------------------------------------------------------------------------------- 1 | .section { 2 | .avatar { 3 | background-color: #fff; 4 | color: rgba(0, 0, 0, 0.54); 5 | } 6 | } 7 | 8 | .group { 9 | } 10 | 11 | .space { 12 | margin-top: 2rem; 13 | } 14 | 15 | .dialog { 16 | height: 90vh; 17 | } 18 | -------------------------------------------------------------------------------- /js/back/pages/moderation/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { ComponentType } from 'react'; 3 | import type { GlobalDataType, SettingsType, FullCriteria } from 'back/types.js'; 4 | import type { State } from 'back/reducer/index.js'; 5 | import type { Props } from './moderation.jsx'; 6 | import { connect } from 'react-redux'; 7 | import { getSettings } from 'back/selectors/settings.js'; 8 | import { getFullCriteria } from 'back/selectors/criteria.js'; 9 | import Moderation from './moderation.jsx'; 10 | import { convertCriteria } from 'back/utils/criteria.js'; 11 | 12 | 13 | type OwnProps = {| 14 | settings: SettingsType, 15 | fullCriteria: FullCriteria 16 | |} 17 | 18 | type Actions = {| 19 | |} 20 | 21 | type PassedProps = {| 22 | data: GlobalDataType 23 | |} 24 | 25 | 26 | const mapStateToProps = (state: State): OwnProps => ({ 27 | settings: getSettings(state), 28 | fullCriteria: getFullCriteria(state) 29 | }); 30 | 31 | const actions = { 32 | }; 33 | 34 | const merge = (props: OwnProps, actions: Actions, passed: PassedProps):Props => { 35 | const { fullCriteria, ...rest } = props; 36 | return { 37 | ...rest, 38 | ...actions, 39 | ...passed, 40 | criteria: convertCriteria(passed.data.language, fullCriteria) 41 | }; 42 | }; 43 | 44 | const connectRedux = connect(mapStateToProps, actions, merge); 45 | const ConnectedComponent: ComponentType = connectRedux(Moderation); 46 | 47 | export default ConnectedComponent; 48 | -------------------------------------------------------------------------------- /js/back/pages/moderation/moderation.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type {Node} from 'react'; 4 | import type { SettingsType, GlobalDataType } from 'back/types.js'; 5 | import type { CriteriaType } from 'common/types.js'; 6 | import React from 'react'; 7 | import ReviewsTable from 'back/components/reviews-table/index.js'; 8 | 9 | export type Props = { 10 | data: GlobalDataType, 11 | settings: SettingsType, 12 | criteria: CriteriaType 13 | }; 14 | 15 | class ModerationPage extends React.PureComponent { 16 | static displayName: ?string = 'ModerationPage'; 17 | 18 | render(): Node { 19 | const { settings, data, criteria } = this.props; 20 | const shape = data.shapes[settings.theme.shape]; 21 | return ( 22 | 41 | ); 42 | } 43 | } 44 | 45 | export default ModerationPage; 46 | -------------------------------------------------------------------------------- /js/back/pages/reviews/migrate-data/index.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { ComponentType } from 'react'; 3 | import type { InputProps } from './migrate-data.jsx'; 4 | import { connect } from 'react-redux'; 5 | import { migrateData, uploadYotpoCsv } from 'back/actions/creators.js'; 6 | import MigrateData from './migrate-data.jsx'; 7 | 8 | const actions = { 9 | onMigrate: migrateData, 10 | onUploadYotpo: uploadYotpoCsv 11 | }; 12 | 13 | const connectRedux = connect(null, actions); 14 | const ConnectedComponent: ComponentType = connectRedux(MigrateData); 15 | 16 | export default ConnectedComponent; 17 | -------------------------------------------------------------------------------- /js/back/pages/settings/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { ComponentType } from 'react'; 3 | import type { GlobalDataType, SettingsType, GoTo } from 'back/types.js'; 4 | import type { State } from 'back/reducer/index.js'; 5 | import type { Props } from './settings.jsx'; 6 | import { connect } from 'react-redux'; 7 | import { getWidth } from 'back/selectors/ui.js'; 8 | import { getSettings } from 'back/selectors/settings.js'; 9 | import { setSettings } from 'back/actions/creators.js'; 10 | import Settings from './settings.jsx'; 11 | 12 | type OwnProps = {| 13 | pageWidth: number, 14 | settings: SettingsType 15 | |} 16 | 17 | type Actions = {| 18 | saveSettings: (SettingsType) => void 19 | |} 20 | 21 | type PassedProps = {| 22 | data: GlobalDataType, 23 | goTo: GoTo, 24 | |}; 25 | 26 | const mapStateToProps = (state: State): OwnProps => ({ 27 | pageWidth: getWidth(state), 28 | settings: getSettings(state) 29 | }); 30 | 31 | const actions = { 32 | saveSettings: setSettings, 33 | }; 34 | 35 | const merge = (props: OwnProps, actions: Actions, passed: PassedProps): Props => ({ 36 | ...props, 37 | ...actions, 38 | ...passed 39 | }); 40 | 41 | const connectRedux = connect(mapStateToProps, actions, merge); 42 | const ConnectedComponent: ComponentType = connectRedux(Settings); 43 | 44 | export default ConnectedComponent; 45 | -------------------------------------------------------------------------------- /js/back/pages/settings/shape-select.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type {Node} from 'react'; 3 | import React from 'react'; 4 | import type { GradingShapeType, ShapeColorsType } from 'common/types.js'; 5 | import { map, toPairs } from 'ramda'; 6 | import { MenuItem } from 'material-ui/Menu'; 7 | import { InputLabel } from 'material-ui/Input'; 8 | import { FormControl } from 'material-ui/Form'; 9 | import Select from 'material-ui/Select'; 10 | import Grading from 'common/components/grading/grading.jsx'; 11 | import styles from './style.less'; 12 | 13 | type Props = { 14 | shape: string, 15 | colors: ShapeColorsType, 16 | onChange: (string)=>void, 17 | shapes: { 18 | [ string ]: GradingShapeType 19 | } 20 | }; 21 | 22 | class ShapeSelect extends React.PureComponent { 23 | static displayName: ?string = 'ShapeSelect'; 24 | 25 | render(): Node { 26 | const { shape, shapes, onChange } = this.props; 27 | return ( 28 | 29 | {__('Choose rating style')} 30 | 35 | 36 | ); 37 | } 38 | 39 | renderItem: ((pair: [string, GradingShapeType]) => Node) = (pair: [string, GradingShapeType]) => { 40 | const key = pair[0]; 41 | const shape = pair[1]; 42 | return ( 43 | 44 | 50 | 51 | ); 52 | } 53 | } 54 | 55 | export default ShapeSelect; 56 | -------------------------------------------------------------------------------- /js/back/pages/settings/style.less: -------------------------------------------------------------------------------- 1 | .groups { 2 | display: flex; 3 | flex-direction: row; 4 | flex-wrap: wrap; 5 | .group { 6 | padding-top: 2rem; 7 | } 8 | .group:not(:last-child) { 9 | margin-right: 120px; 10 | } 11 | } 12 | 13 | .group { 14 | flex-grow: 0; 15 | flex-shrink: 0; 16 | max-width: 250px; 17 | } 18 | 19 | .grading { 20 | padding-left: 1rem; 21 | padding-right: 1rem; 22 | } 23 | 24 | .footerContent { 25 | float: right; 26 | button { 27 | margin-left: 1rem; 28 | } 29 | } 30 | 31 | .sectionList { 32 | list-style-type: none; 33 | padding-left: 0px; 34 | } 35 | 36 | .sectionListItem { 37 | a { 38 | cursor: pointer; 39 | text-decoration: none; 40 | color: #b5b5b5; 41 | font-size: 1.1rem; 42 | line-height: 1.6rem; 43 | } 44 | a:hover { 45 | color: #222; 46 | } 47 | } 48 | 49 | .activeSection { 50 | a { 51 | color: #222; 52 | } 53 | } 54 | 55 | .margin { 56 | margin-top: 4rem; 57 | margin-bottom: 2rem; 58 | } 59 | 60 | .preview { 61 | border: 1px solid #eee; 62 | } 63 | 64 | .subSection { 65 | width: 100%; 66 | padding-left: 2rem; 67 | } 68 | 69 | .note { 70 | margin-top: 1rem; 71 | font-size: 80%; 72 | color: #999; 73 | pre { 74 | background: #eee; 75 | padding: 1em; 76 | } 77 | } 78 | 79 | .note2 { 80 | font-size: 95%; 81 | margin-top: 1rem; 82 | line-height: 1.5em; 83 | color: #999; 84 | } 85 | 86 | .space { 87 | margin-top: 2rem; 88 | } 89 | -------------------------------------------------------------------------------- /js/back/pages/snackbar/index.js: -------------------------------------------------------------------------------- 1 | 2 | // @flow 3 | import type { ComponentType } from 'react'; 4 | import Snackbar from 'common/components/snackbar/snackbar.jsx'; 5 | import { connect } from 'react-redux'; 6 | import { mapObject } from 'common/utils/redux.js'; 7 | import { getMessage } from 'back/selectors/snackbar.js'; 8 | import { setSnackbar } from 'back/actions/creators.js'; 9 | 10 | const mapStateToProps = mapObject({ 11 | message: getMessage, 12 | }); 13 | 14 | const actions = { 15 | setSnackbar: setSnackbar, 16 | }; 17 | 18 | const connectRedux = connect(mapStateToProps, actions); 19 | const ConnectedComponent: ComponentType<{}> = connectRedux(Snackbar); 20 | 21 | export default ConnectedComponent; 22 | -------------------------------------------------------------------------------- /js/back/pages/support/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { ComponentType } from 'react'; 3 | import type { GlobalDataType } from 'back/types.js'; 4 | import { connect } from 'react-redux'; 5 | import { mapObject } from 'common/utils/redux.js'; 6 | import { isNewVersionAvailable, getLatestVersion, checkingVersion, getLastCheck, getNotes, getPaidNotes, shouldReview } from 'back/selectors/account.js'; 7 | import { checkModuleVersion, setReviewed } from 'back/actions/creators.js'; 8 | import SupportPage from './support.jsx'; 9 | 10 | const mapStateToProps = mapObject({ 11 | newVersionAvailable: isNewVersionAvailable, 12 | lastCheck: getLastCheck, 13 | latestVersion: getLatestVersion, 14 | checking: checkingVersion, 15 | notes: getNotes, 16 | paidNotes: getPaidNotes, 17 | shouldReview: shouldReview, 18 | }); 19 | 20 | const actions = { 21 | checkUpdate: checkModuleVersion, 22 | setReviewed: setReviewed, 23 | }; 24 | 25 | const connectRedux = connect(mapStateToProps, actions); 26 | const ConnectedComponent: ComponentType<{data: GlobalDataType}> = connectRedux(SupportPage); 27 | 28 | export default ConnectedComponent; 29 | -------------------------------------------------------------------------------- /js/back/pages/support/support.less: -------------------------------------------------------------------------------- 1 | 2 | 3 | .root { 4 | font-size: 1.2em; 5 | } 6 | 7 | .link { 8 | margin-bottom: 0.5em; 9 | strong { 10 | margin-right: 10px; 11 | } 12 | } 13 | 14 | .inline { 15 | display: flex; 16 | flex-direction: row; 17 | align-items: center; 18 | min-height: 50px; 19 | svg { 20 | width: 50px; 21 | height: 50px; 22 | margin-right: 10px; 23 | } 24 | } 25 | 26 | .note { 27 | font-size: 1em; 28 | color: #999; 29 | margin-bottom: 1.5em; 30 | } 31 | 32 | .accent { 33 | svg { 34 | fill: #ff4081; 35 | } 36 | } 37 | 38 | .link, .note { 39 | a { 40 | font-family: "Roboto", "Helvetica", "Arial", sans-serif; 41 | text-decoration: none; 42 | color: #3f51b5; 43 | &:hover { 44 | text-decoration: underline; 45 | } 46 | } 47 | } 48 | 49 | .notes { 50 | padding: 20px; 51 | background: #eee; 52 | margin-bottom: 1.5em; 53 | } 54 | 55 | .paid { 56 | padding-top: 20px; 57 | padding-bottom: 20px; 58 | } 59 | 60 | .warnings { 61 | color: #666; 62 | li:hover { 63 | color: #222; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /js/back/pages/support/warning.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type {Node} from 'react'; 3 | import React from 'react'; 4 | import type { WarningMessageType } from 'back/types.js'; 5 | import WarningIcon from 'material-ui-icons/Warning'; 6 | import EmailIcon from 'material-ui-icons/Email'; 7 | import { ListItem, ListItemIcon, ListItemText } from 'material-ui/List'; 8 | import ExpandLess from 'material-ui-icons/ExpandLess'; 9 | import ExpandMore from 'material-ui-icons/ExpandMore'; 10 | 11 | type Props = WarningMessageType; 12 | 13 | type State = { 14 | opened: boolean 15 | } 16 | 17 | class Warning extends React.PureComponent { 18 | static displayName: ?string = 'Warning'; 19 | 20 | state: State = { 21 | opened: false 22 | } 23 | 24 | render(): Node { 25 | const { icon, message, hint } = this.props; 26 | const opened = this.state.opened; 27 | return ( 28 | this.setState({ opened: !opened })}> 29 | 30 | {this.renderWarningIcon(icon)} 31 | 32 | 35 | {opened ? : } 36 | 37 | ); 38 | } 39 | 40 | renderWarningIcon: ((icon: string) => Node) = (icon: string) => { 41 | const style = { color: '#8b0000' }; 42 | switch (icon) { 43 | case 'email': 44 | return ; 45 | case 'warning': 46 | default: 47 | return ; 48 | } 49 | } 50 | } 51 | 52 | export default Warning; 53 | -------------------------------------------------------------------------------- /js/back/reducer/criteria.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Action } from 'back/actions/index.js'; 3 | import type { FullCriteria } from 'back/types.js'; 4 | import { dissoc, assoc } from 'ramda'; 5 | import { asObject } from 'common/utils/input.js'; 6 | import Types from 'back/actions/types.js'; 7 | 8 | export type State = { 9 | loading: boolean, 10 | criteria: FullCriteria, 11 | } 12 | 13 | const defaultState = (criteria:FullCriteria):State => ({ 14 | loading: false, 15 | criteria 16 | }); 17 | 18 | const fixCriteria = (crit: any): FullCriteria => asObject(crit); 19 | 20 | export default (criteria: FullCriteria): ((state?: State, action: Action) => State) => { 21 | return (state?: State, action:Action): State => { 22 | state = state || defaultState(fixCriteria(criteria)); 23 | if (action.type === Types.saveCriterion || action.type === Types.migrateData) { 24 | return { ...state, loading: true }; 25 | } 26 | if (action.type === Types.setCriteria) { 27 | return { loading: false, criteria: fixCriteria(action.criteria) }; 28 | } 29 | if (action.type === Types.criterionSaved) { 30 | const crit = action.criterion; 31 | const criteria = assoc(crit.id, crit, state.criteria); 32 | return { loading: false, criteria }; 33 | } 34 | if (action.type === Types.criterionDeleted) { 35 | const criteria = dissoc(action.id, state.criteria); 36 | return { loading: false, criteria }; 37 | } 38 | return state; 39 | }; 40 | }; 41 | -------------------------------------------------------------------------------- /js/back/reducer/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { combineReducers } from 'redux'; 4 | import type { RoutingState } from 'back/routing/index.js'; 5 | import type { FullCriteria, SettingsType, AccountType } from 'back/types.js'; 6 | import snackbar from './snackbar.js'; 7 | import createSettings from './settings.js'; 8 | import createCriteria from './criteria.js'; 9 | import createRouting from './routing-state.js'; 10 | import ui from './ui.js'; 11 | import data from './data.js'; 12 | import createAccount from './account.js'; 13 | 14 | import type { State as StateSnackbar } from './snackbar.js'; 15 | import type { State as StateSettings } from './settings.js'; 16 | import type { State as StateCriteria } from './criteria.js'; 17 | import type { State as StateRouting } from './routing-state.js'; 18 | import type { State as StateUi } from './ui.js'; 19 | import type { State as StateData } from './data.js'; 20 | import type { State as StateAccount } from './account.js'; 21 | 22 | export type State = { 23 | routingState: StateRouting, 24 | snackbar: StateSnackbar, 25 | ui: StateUi, 26 | settings: StateSettings, 27 | criteria: StateCriteria, 28 | data: StateData, 29 | account: StateAccount, 30 | } 31 | 32 | const createReducer = (route: RoutingState, defaultSettings: SettingsType, defaultCriteria: FullCriteria, accountData: AccountType): any => { 33 | const settings = createSettings(defaultSettings); 34 | const criteria = createCriteria(defaultCriteria); 35 | const routingState = createRouting(route); 36 | const account = createAccount(accountData); 37 | return combineReducers({ 38 | routingState, 39 | snackbar, 40 | ui, 41 | settings, 42 | criteria, 43 | data, 44 | account 45 | }); 46 | }; 47 | 48 | export default createReducer; 49 | -------------------------------------------------------------------------------- /js/back/reducer/routing-state.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { Action } from 'back/actions/index.js'; 4 | import type { RoutingState } from 'back/routing/index.js'; 5 | import Types from 'back/actions/types.js'; 6 | 7 | type reducerType = (?RoutingState, Action) => RoutingState; 8 | 9 | export type State = RoutingState; 10 | 11 | export default (initialState: RoutingState): reducerType => { 12 | return (state: ?State, action: Action): State => { 13 | const curState = state || initialState; 14 | if (action.type === Types.goTo) { 15 | return action.routingState; 16 | } 17 | return curState; 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /js/back/reducer/settings.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Action } from 'back/actions/index.js'; 3 | import type { SettingsType } from 'back/types.js'; 4 | import Types from 'back/actions/types.js'; 5 | 6 | export type State = SettingsType; 7 | 8 | export default (defaultConfig: SettingsType): ((state?: State, action: Action) => State) => { 9 | return (state?: State, action:Action): State => { 10 | state = state || defaultConfig; 11 | 12 | if (action.type === Types.setSettings) { 13 | return action.settings; 14 | } 15 | 16 | return state; 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /js/back/reducer/snackbar.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Action } from 'back/actions/index.js'; 3 | import Types from 'back/actions/types.js'; 4 | 5 | export type State = { 6 | message: ?string 7 | } 8 | 9 | const defaultState: State = { 10 | message: null 11 | }; 12 | 13 | export default (state?: State, action:Action): State => { 14 | state = state || defaultState; 15 | 16 | if (action.type === Types.setSnackbar) { 17 | return { 18 | message: action.message 19 | }; 20 | } 21 | 22 | return state; 23 | }; 24 | -------------------------------------------------------------------------------- /js/back/reducer/ui.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Action } from 'back/actions/index.js'; 3 | import Types from 'back/actions/types.js'; 4 | 5 | export type State = { 6 | width: number, 7 | height: number 8 | } 9 | 10 | const defaultState: State = { 11 | width: 1000, 12 | height: 100 13 | }; 14 | 15 | export default (state?: State, action:Action): State => { 16 | state = state || defaultState; 17 | 18 | if (action.type === Types.setSize) { 19 | return { 20 | width: action.width, 21 | height: action.height 22 | }; 23 | } 24 | 25 | return state; 26 | }; 27 | -------------------------------------------------------------------------------- /js/back/routing/criteria.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { RouteDefinition } from 'back/types.js'; 3 | import Criteria from 'back/pages/criteria/index.js'; 4 | 5 | export type CriteriaPage = { 6 | type: 'criteria', 7 | showNavigation: boolean 8 | } 9 | 10 | const toUrl = (settings: CriteriaPage) => { 11 | return '/criteria'; 12 | }; 13 | 14 | const toState = (url: string): ?CriteriaPage => { 15 | if (url === '/criteria') { 16 | return criteriaPage(); 17 | } 18 | }; 19 | 20 | export const criteriaPage = ():CriteriaPage => ({ 21 | type: 'criteria', 22 | showNavigation: true 23 | }); 24 | 25 | export const criteriaRoute: RouteDefinition = { 26 | toUrl, 27 | toState, 28 | component: Criteria 29 | }; 30 | -------------------------------------------------------------------------------- /js/back/routing/moderation.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { RouteDefinition } from 'back/types.js'; 3 | import Moderation from 'back/pages/moderation/index.js'; 4 | 5 | export type ModerationPage = { 6 | type: 'moderation', 7 | showNavigation: boolean 8 | } 9 | 10 | const toUrl = (moderation: ModerationPage) => { 11 | return '/moderation'; 12 | }; 13 | 14 | const toState = (url: string): ?ModerationPage => { 15 | if (url === '/moderation') { 16 | return moderationPage(); 17 | } 18 | }; 19 | 20 | export const moderationPage = ():ModerationPage => ({ 21 | type: 'moderation', 22 | showNavigation: true 23 | }); 24 | 25 | export const moderationRoute: RouteDefinition = { 26 | toUrl, 27 | toState, 28 | component: Moderation 29 | }; 30 | -------------------------------------------------------------------------------- /js/back/routing/reviews.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { RouteDefinition } from 'back/types.js'; 3 | import Reviews from 'back/pages/reviews/index.js'; 4 | 5 | export type SubPage = 'list' | 'create' | 'data'; 6 | 7 | export type ReviewsPage = { 8 | type: 'reviews', 9 | subpage: SubPage, 10 | showNavigation: boolean, 11 | } 12 | 13 | const toUrl = (reviews: ReviewsPage) => { 14 | let url = '/reviews'; 15 | if (reviews.subpage != 'list') { 16 | url += '/' + reviews.subpage; 17 | } 18 | return url; 19 | }; 20 | 21 | const toState = (url: string): ?ReviewsPage => { 22 | if (url === '/reviews') { 23 | return reviewsPage(); 24 | } 25 | if (url === '/reviews/create') { 26 | return reviewsPage('create'); 27 | } 28 | if (url === '/reviews/data') { 29 | return reviewsPage('data'); 30 | } 31 | }; 32 | 33 | export const reviewsPage = (subpage: SubPage = 'list'):ReviewsPage => ({ 34 | type: 'reviews', 35 | subpage, 36 | showNavigation: true 37 | }); 38 | 39 | export const reviewsRoute: RouteDefinition = { 40 | toUrl, 41 | toState, 42 | component: Reviews 43 | }; 44 | -------------------------------------------------------------------------------- /js/back/routing/settings.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { RouteDefinition } from 'back/types.js'; 3 | import Settings from 'back/pages/settings/index.js'; 4 | 5 | export type SettingsPage = { 6 | type: 'settings', 7 | showNavigation: boolean 8 | } 9 | 10 | const toUrl = (settings: SettingsPage) => { 11 | return '/settings'; 12 | }; 13 | 14 | const toState = (url: string): ?SettingsPage => { 15 | if (url === '/settings') { 16 | return settingsPage(); 17 | } 18 | }; 19 | 20 | const settingsPage = ():SettingsPage => ({ 21 | type: 'settings', 22 | showNavigation: false 23 | }); 24 | 25 | export const settingsRoute: RouteDefinition = { 26 | toUrl, 27 | toState, 28 | component: Settings 29 | }; 30 | -------------------------------------------------------------------------------- /js/back/routing/support.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { RouteDefinition } from 'back/types.js'; 3 | import Support from 'back/pages/support/index.js'; 4 | 5 | export type SupportPage = { 6 | type: 'support', 7 | showNavigation: boolean 8 | } 9 | 10 | const toUrl = (support: SupportPage) => { 11 | return '/support'; 12 | }; 13 | 14 | const toState = (url: string): ?SupportPage => { 15 | if (url === '/support') { 16 | return supportPage(); 17 | } 18 | }; 19 | 20 | export const supportPage = ():SupportPage => ({ 21 | type: 'support', 22 | showNavigation: true 23 | }); 24 | 25 | export const supportRoute: RouteDefinition = { 26 | toUrl, 27 | toState, 28 | component: Support 29 | }; 30 | -------------------------------------------------------------------------------- /js/back/selectors/account.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { versionNum } from 'common/utils/version.js'; 4 | import type { State } from 'back/reducer/index.js'; 5 | 6 | export const isActivated: ((State) => boolean) = state => state.account.activated; 7 | export const getLatestVersion = (state: State): ?string => state.account.latestVersion; 8 | export const getLastCheck =(state: State): ?number => state.account.lastCheck; 9 | export const shouldReview =(state: State): boolean => state.account.shouldReview; 10 | export const getVersion = (state: State): string => state.account.version; 11 | export const getNotes = (state: State): ?string => state.account.notes; 12 | export const getPaidNotes = (state: State): ?string => state.account.paid; 13 | export const checkingVersion = (state: State): boolean => state.account.checking; 14 | export const isNewVersionAvailable = (state: any): boolean => versionNum(getLatestVersion(state)) > versionNum(getVersion(state)); 15 | -------------------------------------------------------------------------------- /js/back/selectors/criteria.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type {FullCriteria} from 'back/types.js'; 3 | import type { State } from 'back/reducer/index.js'; 4 | 5 | export const isLoading = (state: State): boolean => state.criteria.loading; 6 | export const getFullCriteria = (state: State): FullCriteria => state.criteria.criteria; 7 | -------------------------------------------------------------------------------- /js/back/selectors/data.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { State as StateData } from "back/reducer/data"; 3 | import type { State } from 'back/reducer/index.js'; 4 | 5 | export const getEntities = (state: State): ?any => state.data.entities; 6 | export const getProducts = (state: State): ?any => state.data.products; 7 | export const getCustomers = (state: State): ?any => state.data.customers; 8 | export const getCategories = (state: State): ?any => state.data.categories; 9 | 10 | export const getData = (state: State): StateData=> state.data; 11 | -------------------------------------------------------------------------------- /js/back/selectors/routing-state.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type {State as StateRouting} from "back/reducer/routing-state"; 4 | import type { State } from 'back/reducer/index.js'; 5 | 6 | export const getRoutingState = (state: State): StateRouting => state.routingState; 7 | -------------------------------------------------------------------------------- /js/back/selectors/settings.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type {State as StateSettings } from "back/reducer/settings"; 3 | import type { State } from 'back/reducer/index.js'; 4 | 5 | export const getSettings = (state: State): StateSettings => state.settings; 6 | -------------------------------------------------------------------------------- /js/back/selectors/snackbar.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { State } from 'back/reducer/index.js'; 3 | 4 | export const getMessage = (state: State): ?string => state.snackbar.message; 5 | -------------------------------------------------------------------------------- /js/back/selectors/ui.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { State } from 'back/reducer/index.js'; 3 | 4 | export const getWidth = (state: State): number => state.ui.width; 5 | export const getHeight = (state: State): number => state.ui.height; 6 | -------------------------------------------------------------------------------- /js/back/utils/common.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { GlobalDataType } from 'back/types.js'; 4 | import { mergeRight, reduce, append, toPairs } from 'ramda'; 5 | 6 | const storeBase = 'https://store.getdatakick.com'; 7 | 8 | const getUrlComponent = (pair: Array) => encodeURIComponent(pair[0]) + '=' + encodeURIComponent(pair[1]); 9 | 10 | export const getApiUrl = (data: GlobalDataType): string => { 11 | return data.storeUrl || (storeBase + '/en/module/datakickweb/api'); 12 | }; 13 | 14 | export const getWebUrl = (campaign: string, path: string, params: {} = {}): string => { 15 | if (!path || path.length == 0) { 16 | path = '/'; 17 | } else { 18 | if (path[0] != '/') 19 | path = '/'+path; 20 | if (path[path.length - 1] != '/') 21 | path = path + '/'; 22 | } 23 | params = mergeRight({ 'utm_campaign': campaign, 'utm_source': 'chex', 'utm_medium': 'web' }, params); 24 | const pars = reduce((ret, pair) => append(getUrlComponent(pair), ret), [], toPairs(params)).join('&'); 25 | return storeBase + path + '?' + pars; 26 | }; 27 | -------------------------------------------------------------------------------- /js/back/utils/criteria.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { map } from 'ramda'; 3 | import type { FullCriteria, FullCriterion } from 'back/types.js'; 4 | import type { CriteriaType, CriterionType } from 'common/types.js'; 5 | 6 | const toCriterion = (language: number) => (crit: FullCriterion): CriterionType => ({ 7 | id: crit.id, 8 | label: crit.label[language] 9 | }); 10 | 11 | export const convertCriteria = (language: number, fullCriteria: FullCriteria): CriteriaType => map(toCriterion(language), fullCriteria); 12 | -------------------------------------------------------------------------------- /js/back/utils/drilldown.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { DrilldownUrls } from 'back/types.js'; 3 | import type { EntityType } from 'common/types.js'; 4 | 5 | const getUrl = (type: string, urls: DrilldownUrls, id: number) => { 6 | const url = urls[type]; 7 | if (! url) { 8 | throw new Error('Invalid url type '+type); 9 | } 10 | return decodeURI(url).replace('999', `${id}`); 11 | }; 12 | 13 | export const editProductUrl = (urls: DrilldownUrls, productId: number): string => getUrl('editProduct', urls, productId); 14 | export const viewCustomerUrl = (urls: DrilldownUrls, customerId: number): string => getUrl('viewCustomer', urls, customerId); 15 | export const editCustomerUrl = (urls: DrilldownUrls, customerId: number): string => getUrl('editCustomer', urls, customerId); 16 | export const viewOrderUrl = (urls: DrilldownUrls, orderId: number): string => getUrl('viewOrder', urls, orderId); 17 | 18 | const functions = { 19 | product: editProductUrl 20 | }; 21 | 22 | export const editEntityUrl = (urls: DrilldownUrls, entityType: EntityType, entityId: number): ?string => { 23 | const func = functions[entityType]; 24 | return func ? func(urls, entityId) : null; 25 | }; 26 | -------------------------------------------------------------------------------- /js/back/utils/markdown.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { markdown } from 'markdown'; 4 | import { is, forEach } from 'ramda'; 5 | 6 | const isArray = is(Array); 7 | 8 | const fixLinks = (m:any) => { 9 | if (m[0] === "link") { 10 | m[1].target = '_blank'; 11 | } else { 12 | forEach(n => isArray(n) && fixLinks(n), m); 13 | } 14 | }; 15 | 16 | export const toHTML = (text: string): any => { 17 | const tree = markdown.parse(text); 18 | fixLinks(tree); 19 | return markdown.renderJsonML(markdown.toHTMLTree(tree)); 20 | }; 21 | -------------------------------------------------------------------------------- /js/common/components/add-avatar/add-avatar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withStyles } from 'material-ui/styles'; 3 | import pink from 'material-ui/colors/pink'; 4 | import Avatar from 'material-ui/Avatar'; 5 | import AddIcon from 'material-ui-icons/Add'; 6 | 7 | const styles = { 8 | pinkAvatar: { 9 | color: '#fff', 10 | backgroundColor: pink[500], 11 | }, 12 | }; 13 | 14 | function IconAvatars(props) { 15 | const { classes } = props; 16 | return ( 17 | 18 | 19 | 20 | ); 21 | } 22 | export default withStyles(styles)(IconAvatars); 23 | -------------------------------------------------------------------------------- /js/common/components/badge/badge.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type {Element} from 'react'; 3 | import React from 'react'; 4 | import styles from './badge.less'; 5 | 6 | type Props = { 7 | children: any, 8 | color?: string, 9 | backgroundColor?: string 10 | }; 11 | 12 | class Badge extends React.PureComponent { 13 | static displayName: ?string = 'Badge'; 14 | 15 | render(): Element<"span"> { 16 | const { children, color, backgroundColor } = this.props; 17 | const style = { 18 | color, 19 | backgroundColor 20 | }; 21 | return ( 22 | 23 | { children } 24 | 25 | ); 26 | } 27 | } 28 | 29 | export default Badge; 30 | -------------------------------------------------------------------------------- /js/common/components/badge/badge.less: -------------------------------------------------------------------------------- 1 | .badge { 2 | padding: 0.15em; 3 | top: -0.75em; 4 | min-width: 1.5em; 5 | right: -1.7em; 6 | height: 1.5em; 7 | display: flex; 8 | position: absolute; 9 | font-size: 0.85em; 10 | line-height: 1.65em; 11 | justify-content: center; 12 | border-radius: 50%; 13 | flex-direction: row; 14 | color: white; 15 | background-color: #ff4081; 16 | } 17 | 18 | :global(.platform-ps17) .badge { 19 | line-height: initial; 20 | } -------------------------------------------------------------------------------- /js/common/components/bootstrap/bootstrap.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type {Element} from 'react'; 3 | import React from 'react'; 4 | import classnames from 'classnames'; 5 | 6 | type Props = {| 7 | className: ?string, 8 | children: any 9 | |}; 10 | 11 | class Bootstrap extends React.PureComponent { 12 | static displayName: ?string = 'Bootstrap'; 13 | 14 | render(): Element<"div"> { 15 | const { children, className, ...rest } = this.props; 16 | return ( 17 |
18 | { children } 19 |
20 | ); 21 | } 22 | } 23 | 24 | export default Bootstrap; 25 | -------------------------------------------------------------------------------- /js/common/components/color-picker/circle.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type {Element} from 'react'; 4 | import React from 'react'; 5 | import { mergeRight } from 'ramda'; 6 | 7 | type Props = { 8 | color: string, 9 | size: number, 10 | onClick?: ()=>void, 11 | style?: {} 12 | }; 13 | 14 | class Circle extends React.PureComponent { 15 | static displayName: ?string = 'ColorPicker/Circle'; 16 | 17 | render(): Element<"div"> { 18 | const { color, size, onClick, style={} } = this.props; 19 | const merged = mergeRight({ 20 | width: size, 21 | height: size, 22 | border: '1px solid #ddd', 23 | backgroundColor: color, 24 | borderRadius: size, 25 | cursor: onClick ? 'pointer' : 'default' 26 | }, style); 27 | 28 | return
; 29 | } 30 | } 31 | 32 | export default Circle; 33 | -------------------------------------------------------------------------------- /js/common/components/color-picker/preset.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type {Element} from 'react'; 4 | import React from 'react'; 5 | import Circle from './circle.jsx'; 6 | import styles from './color-picker.less'; 7 | 8 | type Props = {| 9 | label: string, 10 | colors: Array, 11 | onSelect: string => void 12 | |} 13 | 14 | const COUNT = 10; 15 | const WIDTH = 300 / COUNT; 16 | const SIZE = WIDTH - 10; 17 | 18 | class Preset extends React.PureComponent { 19 | static displayName: ?string = 'Preset'; 20 | 21 | renderCircle: ((color: string, i: number) => Element<"div">) = (color:string, i: number) => { 22 | return ( 23 |
24 | this.props.onSelect(color)} 29 | color={color} /> 30 |
31 | ); 32 | }; 33 | 34 | render(): null | Element<"div"> { 35 | const { label, colors } = this.props; 36 | const toRender = colors.slice(0, 10); 37 | const width = toRender.length * WIDTH; 38 | return width ? ( 39 |
40 |
{label}
41 |
42 | { toRender.map(this.renderCircle) } 43 |
44 |
45 | ) : null; 46 | } 47 | } 48 | 49 | export default Preset; 50 | -------------------------------------------------------------------------------- /js/common/components/color-picker/types.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export type Position = { 4 | x: number, 5 | y: number 6 | }; 7 | 8 | export type HSV = { 9 | h: number, 10 | s: number, 11 | v: number 12 | }; 13 | 14 | export type PresetType = {| 15 | label: string, 16 | colors: Array 17 | |}; 18 | -------------------------------------------------------------------------------- /js/common/components/confirm-delete/confirm-delete.less: -------------------------------------------------------------------------------- 1 | .single { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | justify-content: center; 6 | height: 350px; 7 | h2 { 8 | padding-bottom: 3rem; 9 | font-size: 1.5em; 10 | color: #999; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /js/common/components/criteria/block.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type {Element} from 'react'; 3 | import React from 'react'; 4 | import type { GradingType, CriterionType, GradingShapeType, ShapeColorsType } from 'common/types.js'; 5 | import Grading from 'common/components/grading/grading.jsx'; 6 | 7 | type Props = { 8 | grades: GradingType, 9 | criteria: Array, 10 | shape: GradingShapeType, 11 | colors?: ShapeColorsType, 12 | shapeSize: number, 13 | }; 14 | 15 | class CriteriaBlock extends React.PureComponent { 16 | static displayName: ?string = 'CriteriaBlock'; 17 | 18 | render(): Element<"div"> { 19 | const { criteria } = this.props; 20 | return ( 21 |
22 | 23 | 24 | { criteria.map(this.renderCriterion) } 25 | 26 |
27 |
28 | ); 29 | } 30 | 31 | renderCriterion: ((crit: CriterionType, i: number) => Element<"tr">) = (crit: CriterionType, i: number) => { 32 | const { grades, shape, shapeSize, colors } = this.props; 33 | const grade = grades[crit.id]; 34 | return ( 35 | 36 | { crit.label } 37 | 38 | 44 | 45 | 46 | ); 47 | } 48 | } 49 | 50 | export default CriteriaBlock; 51 | -------------------------------------------------------------------------------- /js/common/components/criteria/inline.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type {Element} from 'react'; 3 | import React from 'react'; 4 | import type { GradingType, CriterionType, GradingShapeType, ShapeColorsType } from 'common/types.js'; 5 | import Grading from 'common/components/grading/grading.jsx'; 6 | 7 | type Props = { 8 | grades: GradingType, 9 | criteria: Array, 10 | shape: GradingShapeType, 11 | colors?: ShapeColorsType, 12 | shapeSize: number, 13 | }; 14 | 15 | class CriteriaBlock extends React.PureComponent { 16 | static displayName: ?string = 'CriteriaBlock'; 17 | 18 | render(): Element<"div"> { 19 | const { criteria } = this.props; 20 | return ( 21 |
22 | { criteria.map(this.renderCriterion) } 23 |
24 | ); 25 | } 26 | 27 | renderCriterion: ((crit: CriterionType, i: number) => Element<"div">) = (crit: CriterionType, i: number) => { 28 | const { grades, shape, shapeSize, colors } = this.props; 29 | const grade = grades[crit.id]; 30 | return ( 31 |
32 | { crit.label } 33 | 40 |
41 | ); 42 | } 43 | } 44 | 45 | export default CriteriaBlock; 46 | -------------------------------------------------------------------------------- /js/common/components/dialog/index.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type {Node} from 'react'; 4 | import React from 'react'; 5 | import MUIDialog from 'material-ui/Dialog'; 6 | export { DialogActions, DialogContent, DialogTitle, DialogContentText, withMobileDialog } from 'material-ui/Dialog'; 7 | 8 | export default (props: any): Node => ( 9 | 12 | ); 13 | -------------------------------------------------------------------------------- /js/common/components/grading-shape/grading-shape.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type {Element} from 'react'; 3 | import React from 'react'; 4 | import { mergeRight } from 'ramda'; 5 | import classnames from 'classnames'; 6 | import type { GradingShapeType, ShapeColorsType } from 'common/types.js'; 7 | 8 | type Props = {| 9 | id: number, 10 | shape: GradingShapeType, 11 | size: number, 12 | on: boolean, 13 | highlighted?: boolean, 14 | colors?: ShapeColorsType, 15 | |}; 16 | 17 | class GradingShape extends React.PureComponent { 18 | static displayName: ?string = 'GradingShape'; 19 | 20 | render(): Element<"svg"> { 21 | const { shape, size, on, highlighted, colors, id, ...rest } = this.props; 22 | const { path, viewBox, strokeWidth } = shape; 23 | const className = classnames('revws-grade', { 24 | 'revws-grade-on': on, 25 | 'revws-grade-off': !on, 26 | 'revws-grade-highlight': highlighted 27 | }); 28 | let pathStyle = { strokeWidth }; 29 | if (colors) { 30 | pathStyle = mergeRight(pathStyle, { 31 | stroke: on ? colors.borderColor : colors.borderColorOff, 32 | fill: on ? colors.fillColor : colors.fillColorOff 33 | }); 34 | } 35 | const style = { 36 | width: size, 37 | height: size 38 | }; 39 | return ( 40 | 41 | 42 | 43 | ); 44 | } 45 | } 46 | 47 | export default GradingShape; 48 | -------------------------------------------------------------------------------- /js/common/components/multilang/multilang.less: -------------------------------------------------------------------------------- 1 | .root { 2 | display: flex; 3 | flex-direction: row; 4 | } 5 | 6 | .fullWidth { 7 | .label { 8 | flex: 1; 9 | } 10 | width: 100%; 11 | } 12 | 13 | .lang { 14 | padding-top: 1rem; 15 | } 16 | -------------------------------------------------------------------------------- /js/common/components/page-with-footer/page-with-footer.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type {Element} from 'react'; 3 | import React from 'react'; 4 | import styles from './page-with-footer.less'; 5 | import classnames from 'classnames'; 6 | 7 | type Props = { 8 | content: any, 9 | footer: any, 10 | showFooter: boolean 11 | }; 12 | 13 | class PageWithFooter extends React.PureComponent { 14 | static displayName: ?string = 'PageWithFooter'; 15 | 16 | render(): Element<"div"> { 17 | const { content, footer, showFooter } = this.props; 18 | const clazz = classnames(styles.footer, { 19 | [ styles.open ]: showFooter 20 | }); 21 | return ( 22 |
23 |
24 | { content } 25 |
26 |
27 |
28 | { footer } 29 |
30 |
31 |
32 | ); 33 | } 34 | } 35 | 36 | export default PageWithFooter; 37 | -------------------------------------------------------------------------------- /js/common/components/page-with-footer/page-with-footer.less: -------------------------------------------------------------------------------- 1 | .root { 2 | position: relative; 3 | padding-bottom: 60px; 4 | width: 100%; 5 | } 6 | 7 | .footer { 8 | position: fixed; 9 | left: 0px; 10 | width: 100%; 11 | line-height: 56px; 12 | bottom: -120px; 13 | transition: all 250ms linear; 14 | height: 56px; 15 | box-shadow: rgba(0, 0, 0, 0.16) 0px -4px 8px; 16 | background: #fff; 17 | opacity: 0; 18 | } 19 | 20 | .footerContent { 21 | padding-right: 20px; 22 | } 23 | 24 | .open { 25 | bottom: 0px; 26 | opacity: 1; 27 | z-index: 499; 28 | } 29 | -------------------------------------------------------------------------------- /js/common/components/portal/portal.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | 5 | type Props = { 6 | nodeId: string, 7 | children: any 8 | }; 9 | 10 | class Portal extends React.PureComponent { 11 | static displayName: ?string = 'Portal'; 12 | domNode: ?any = null; 13 | 14 | componentWillMount() { 15 | const nodeId = this.props.nodeId; 16 | if (document) { 17 | const node = document.getElementById(nodeId); 18 | if (node) { 19 | while (node.hasChildNodes()) { 20 | const child = node.lastChild; 21 | if (child) { 22 | node.removeChild(child); 23 | } else { 24 | break; 25 | } 26 | } 27 | } 28 | this.domNode = node; 29 | } 30 | } 31 | 32 | componentWillUnmount() { 33 | this.domNode = null; 34 | } 35 | 36 | render(): any | null { 37 | return this.domNode ? ReactDOM.createPortal(this.props.children, this.domNode) : null; 38 | } 39 | } 40 | 41 | export default Portal; 42 | -------------------------------------------------------------------------------- /js/common/components/review-list-item/review-list-item.less: -------------------------------------------------------------------------------- 1 | .reply { 2 | display: flex; 3 | line-height: 24px; 4 | color: #666; 5 | cursor: pointer; 6 | svg { 7 | fill: #666; 8 | margin-right: 1rem; 9 | } 10 | &:hover { 11 | color: #222; 12 | svg { 13 | fill: #222; 14 | } 15 | } 16 | } 17 | 18 | .editable { 19 | cursor: pointer; 20 | &:hover { 21 | color: #222; 22 | } 23 | } 24 | 25 | .margin { 26 | margin-top: 20px; 27 | } 28 | -------------------------------------------------------------------------------- /js/common/components/review-list-paging/review-list-paging.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type {Element} from 'react'; 3 | import React from 'react'; 4 | import KeyboardArrowLeft from 'material-ui-icons/KeyboardArrowLeft'; 5 | import KeyboardArrowRight from 'material-ui-icons/KeyboardArrowRight'; 6 | import IconButton from 'material-ui/IconButton'; 7 | 8 | type Props = { 9 | page: number, 10 | pages: number, 11 | loading: boolean, 12 | loadPage: (number)=>void, 13 | }; 14 | 15 | class ReviewListPaging extends React.PureComponent { 16 | static displayName: ?string = 'ReviewListPaging'; 17 | 18 | render(): Element<"div"> { 19 | const { page, pages, loading, loadPage } = this.props; 20 | return ( 21 |
22 | loadPage(page - 1)} 24 | disabled={loading || page === 0}> 25 | 26 | 27 | loadPage(page + 1)} 29 | disabled={loading || page == pages - 1}> 30 | 31 | 32 |
33 | ); 34 | } 35 | } 36 | 37 | export default ReviewListPaging; 38 | -------------------------------------------------------------------------------- /js/common/components/snackbar/snackbar.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type {Node} from 'react'; 3 | import React from 'react'; 4 | import Button from 'material-ui/Button'; 5 | import Snackbar from 'material-ui/Snackbar'; 6 | 7 | type Props = { 8 | message: ?string, 9 | setSnackbar: (?string) => void, 10 | anchorOrigin: { 11 | vertical: string, 12 | horizontal: string 13 | }, 14 | }; 15 | 16 | class AppSnackbar extends React.PureComponent { 17 | static displayName: ?string = 'AppSnackbar'; 18 | 19 | static defaultProps: {|anchorOrigin: {|horizontal: string, vertical: string|}|} = { 20 | anchorOrigin: { 21 | vertical: 'bottom', 22 | horizontal: 'left' 23 | } 24 | } 25 | 26 | render(): Node { 27 | const { anchorOrigin, message } = this.props; 28 | return ( 29 | 37 | {__('Close')} 38 | 39 | ]} /> 40 | ); 41 | } 42 | 43 | onClose: ((e: Event, reason: ?string) => void) = (e: Event, reason: ?string) => { 44 | if (reason != 'clickaway') { 45 | this.props.setSnackbar(null); 46 | } 47 | } 48 | } 49 | 50 | export default AppSnackbar; 51 | -------------------------------------------------------------------------------- /js/common/components/space/space.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type {Element} from 'react'; 3 | import React from 'react'; 4 | import classnames from 'classnames'; 5 | 6 | type Props = {| 7 | small?: boolean, 8 | large?: boolean, 9 | className?: string 10 | |}; 11 | 12 | class Space extends React.PureComponent { 13 | static displayName: ?string = 'Space'; 14 | 15 | render(): Element<"div"> { 16 | const { small, large, className, ...rest } = this.props; 17 | const clazz = classnames(className, 'revws-space', { 18 | 'revws-space-small': small, 19 | 'revws-space-large': large, 20 | }); 21 | return ( 22 |
23 | ); 24 | } 25 | } 26 | 27 | export default Space; 28 | -------------------------------------------------------------------------------- /js/common/components/text-area/text-area.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type {Element} from 'react'; 3 | import React from 'react'; 4 | import classnames from 'classnames'; 5 | import { InputLabel } from 'material-ui/Input'; 6 | import { FormHelperText } from 'material-ui/Form'; 7 | import styles from './text-area.less'; 8 | 9 | type Props = { 10 | value: ?string, 11 | rows: number, 12 | label: string, 13 | placeholder?: string, 14 | error?: ?boolean, 15 | helperText?: ?string, 16 | onChange?: (any)=>void 17 | }; 18 | 19 | type State = { 20 | focus: boolean 21 | } 22 | 23 | class TextArea extends React.PureComponent { 24 | static displayName: ?string = 'TextArea'; 25 | 26 | static defaultProps: {|rows: number|} = { 27 | rows: 5 28 | }; 29 | 30 | state: State = { 31 | focus: false 32 | }; 33 | 34 | render(): Element<"div"> { 35 | const { placeholder, label, value, onChange, error, helperText, ...rest } = this.props; 36 | const clazz = classnames(styles.textArea, { 37 | [ styles.invalid ]: !!error 38 | }); 39 | return ( 40 |
41 | {label} 42 |