init commit
This commit is contained in:
commit
4ce981c5b6
6
.eslintignore
Normal file
6
.eslintignore
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
dist/**
|
||||||
|
src/index.html
|
||||||
|
flow-typed/**
|
||||||
|
node_modules/**
|
||||||
|
seed/node_modules/**
|
||||||
|
dashboard/node_modules/**
|
||||||
13
.eslintrc.js
Normal file
13
.eslintrc.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
module.exports = {
|
||||||
|
"extends": "./seed/.eslintrc.js",
|
||||||
|
"settings": {
|
||||||
|
"import/resolver": {
|
||||||
|
"webpack": {
|
||||||
|
config: require.resolve('./seed/config/webpack/default.js')
|
||||||
|
},
|
||||||
|
"node": {
|
||||||
|
"paths": ["src", "seed/src"]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
.idea
|
||||||
|
node_modules
|
||||||
|
package-lock.json
|
||||||
|
dist
|
||||||
|
build
|
||||||
|
ssr
|
||||||
|
.DS_Store
|
||||||
7
.prettierrc.js
Normal file
7
.prettierrc.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
module.exports = {
|
||||||
|
singleQuote: true,
|
||||||
|
trailingComma: 'all',
|
||||||
|
printWidth: 100,
|
||||||
|
tabWidth: 2,
|
||||||
|
useTabs: false,
|
||||||
|
};
|
||||||
15
.vscode/launch.json
vendored
Normal file
15
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "pwa-chrome",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Launch Chrome against localhost",
|
||||||
|
"url": "http://localhost:8500",
|
||||||
|
"webRoot": "${workspaceFolder}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
123
README.md
Normal file
123
README.md
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
workinflex-frontend
|
||||||
|
|
||||||
|
# Project
|
||||||
|
|
||||||
|
Creobis admin
|
||||||
|
|
||||||
|
# Author
|
||||||
|
|
||||||
|
Nicolas Bernier
|
||||||
|
|
||||||
|
# Git modules
|
||||||
|
|
||||||
|
## Seed react
|
||||||
|
|
||||||
|
Add this git as a submodule
|
||||||
|
|
||||||
|
git@gitlab.com:makeit-group/apps/seed-react.git
|
||||||
|
|
||||||
|
(you must have the right access)
|
||||||
|
|
||||||
|
Add it as seed/ (the name/path => very important!)
|
||||||
|
|
||||||
|
## Seed admin
|
||||||
|
|
||||||
|
Add this git as a submodule
|
||||||
|
|
||||||
|
git@gitlab.com:makeit-group/apps/seed-admin.git
|
||||||
|
|
||||||
|
(you must have the right access)
|
||||||
|
|
||||||
|
Add it as admin/ (the name/path => very important!)
|
||||||
|
|
||||||
|
## Strucutre
|
||||||
|
|
||||||
|
the final structure of a project containing this seed shoudl be
|
||||||
|
|
||||||
|
-- project-name/
|
||||||
|
---- admin (seed-admin)
|
||||||
|
---- seed (seed-react)
|
||||||
|
|
||||||
|
# Installation
|
||||||
|
|
||||||
|
(after gitmodules)
|
||||||
|
|
||||||
|
npm i
|
||||||
|
cd seed
|
||||||
|
npm i
|
||||||
|
cd ../admin
|
||||||
|
npm i
|
||||||
|
|
||||||
|
or directly
|
||||||
|
|
||||||
|
npm i && cd seed && npm i && cd ../admin && npm i && cd ../seed
|
||||||
|
|
||||||
|
# Structure Project
|
||||||
|
|
||||||
|
/components : react part
|
||||||
|
|
||||||
|
- /enhancers : high order components
|
||||||
|
- /formItems : components used by redux-form
|
||||||
|
- /forms : redux-from forms
|
||||||
|
- /global : global components of the app (Navigation, Header, ...)
|
||||||
|
- /items : components used in multiple other components
|
||||||
|
- /layouts : layout components of the app
|
||||||
|
- /listItems : components used in list
|
||||||
|
- /pageItems : specific sub-part of pages (if used several times, shoudl be moved to items)
|
||||||
|
- /routes : page components (endpoint of the react-router)
|
||||||
|
- /structure : structural components taking children
|
||||||
|
|
||||||
|
/store : redux part
|
||||||
|
|
||||||
|
- /actions
|
||||||
|
- /apis
|
||||||
|
- /reducers
|
||||||
|
- /sagas
|
||||||
|
- /utils
|
||||||
|
|
||||||
|
/styles : general scss
|
||||||
|
|
||||||
|
/types : flow types
|
||||||
|
|
||||||
|
# Generation
|
||||||
|
|
||||||
|
Make sure to have plop installed globally
|
||||||
|
|
||||||
|
sudo npm i -g plop
|
||||||
|
|
||||||
|
then simply run the following command and follow the terminal instructions
|
||||||
|
|
||||||
|
plop
|
||||||
|
|
||||||
|
# Seed
|
||||||
|
|
||||||
|
The seed containes all the shared logic accross projects
|
||||||
|
|
||||||
|
# Storybook
|
||||||
|
|
||||||
|
cd ./seed
|
||||||
|
yarn storybook
|
||||||
|
|
||||||
|
it will also look at the parent stories
|
||||||
|
|
||||||
|
# Test
|
||||||
|
|
||||||
|
cd ./seed
|
||||||
|
yarn test
|
||||||
|
|
||||||
|
it will also look at the parent test
|
||||||
|
|
||||||
|
# Start build
|
||||||
|
|
||||||
|
cd ./seed
|
||||||
|
yarn start
|
||||||
|
|
||||||
|
yarn build
|
||||||
|
|
||||||
|
# Icons
|
||||||
|
|
||||||
|
Font Awesome is installed in the seed and imported in the admin
|
||||||
|
|
||||||
|
# Zeus
|
||||||
|
|
||||||
|
zeus https://dev-api.work-in-flex.com/app ./src/config --ts
|
||||||
3
assets/underline-1.svg
Normal file
3
assets/underline-1.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="271" height="16" viewBox="0 0 271 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M264.003 14C222.524 0.638327 42.6166 -2.39838 3.63666 9.14117C-16.3796 15.0667 151.561 -7.56085 269 10.9632" stroke="#5FB77D" stroke-width="3" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 282 B |
3
assets/underline-2.svg
Normal file
3
assets/underline-2.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="332" height="14" viewBox="0 0 332 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M14.1863 3.44825C119.252 15.4611 217.804 3.69118 313.555 2.46089C366.345 1.7826 298.814 21.3538 1.58734 6.64717" stroke="#5FB77D" stroke-width="3" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 286 B |
3
assets/underline-3.svg
Normal file
3
assets/underline-3.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="134" height="14" viewBox="0 0 134 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M7.10663 1.54643C48.7536 14.3741 87.6394 3.37081 125.499 2.88433C146.373 2.61612 119.816 21.6609 2.14859 4.64717" stroke="#5FB77D" stroke-width="3" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 287 B |
3
assets/underline-4.svg
Normal file
3
assets/underline-4.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="266" height="15" viewBox="0 0 266 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M253.707 13.2974C170.067 -0.0643103 91.3769 10.4385 15.0272 10.4385C-27.0661 10.4385 26.9725 -8.26351 263.784 10.2606" stroke="#5FB77D" stroke-width="3" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 292 B |
68
package.json
Normal file
68
package.json
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
{
|
||||||
|
"name": "workin-flex",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Work'in Flex app",
|
||||||
|
"main": "seed/src/main.tsx",
|
||||||
|
"author": "Nicolas Bernier",
|
||||||
|
"license": "MIT",
|
||||||
|
"private": true,
|
||||||
|
"deploy": {
|
||||||
|
"awsProfile": "workinflex",
|
||||||
|
"locale": {
|
||||||
|
"frontendUrl": "http://localhost:8500/"
|
||||||
|
},
|
||||||
|
"dev": {
|
||||||
|
"bucket": "s3://dev.work-in-flex.com/app",
|
||||||
|
"frontendUrl": "/app/"
|
||||||
|
},
|
||||||
|
"staging": {
|
||||||
|
"bucket": "s3://staging.work-in-flex.com/app",
|
||||||
|
"frontendUrl": "/app/"
|
||||||
|
},
|
||||||
|
"production": {
|
||||||
|
"bucket": "s3://production.work-in-flex.com/app",
|
||||||
|
"frontendUrl": "/app/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"eslint": "eslint ./node_modules/.bin/eslint src/. --ext .js --ext .tsx --fix",
|
||||||
|
"zeus": "zeus https://dev-api.work-in-flex.com/app ./src/config --ts",
|
||||||
|
"zeus:staging": "zeus https://staging-api.work-in-flex.com/app ./src/config --ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@stripe/react-stripe-js": "^1.1.2",
|
||||||
|
"@stripe/stripe-js": "^1.11.0",
|
||||||
|
"date-fns": "^2.16.1",
|
||||||
|
"google-map-react": "^2.1.9",
|
||||||
|
"points-cluster": "^0.1.4",
|
||||||
|
"rc-slider": "^9.5.1",
|
||||||
|
"react-date-range": "^1.1.3",
|
||||||
|
"react-scroll": "^1.8.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/jest": "^24.0.18",
|
||||||
|
"@types/node": "^11.15.14",
|
||||||
|
"@types/react": "^16.9.4",
|
||||||
|
"@types/react-dom": "^16.9.1",
|
||||||
|
"@types/react-redux": "^7.1.9",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^1.13.0",
|
||||||
|
"@typescript-eslint/parser": "^1.13.0",
|
||||||
|
"babel-eslint": "^10.0.2",
|
||||||
|
"eslint": "^5.16.0",
|
||||||
|
"eslint-config-airbnb": "^17.1.1",
|
||||||
|
"eslint-config-airbnb-base": "^13.2.0",
|
||||||
|
"eslint-config-airbnb-typescript": "^4.0.1",
|
||||||
|
"eslint-config-prettier": "^6.0.0",
|
||||||
|
"eslint-import-resolver-webpack": "^0.10.1",
|
||||||
|
"eslint-plugin-babel": "^5.1.0",
|
||||||
|
"eslint-plugin-import": "^2.18.2",
|
||||||
|
"eslint-plugin-jest": "^22.14.1",
|
||||||
|
"eslint-plugin-jsx-a11y": "^6.2.3",
|
||||||
|
"eslint-plugin-prettier": "^3.1.0",
|
||||||
|
"eslint-plugin-react": "^7.15.1",
|
||||||
|
"eslint-plugin-react-hooks": "4.3.0",
|
||||||
|
"prettier": "^1.18.2",
|
||||||
|
"prettier-stylelint": "^0.4.2",
|
||||||
|
"typescript": "^3.9.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
17
plopfile.js
Normal file
17
plopfile.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
/* eslint-disable import/no-extraneous-dependencies */
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const templateDir = 'seed/plop-templates';
|
||||||
|
|
||||||
|
const getTemplatePath = filename => path.join(templateDir, filename);
|
||||||
|
const componentPath = 'src/components';
|
||||||
|
const storePath = 'src/store';
|
||||||
|
|
||||||
|
const plopBase = require('./seed/plopbase');
|
||||||
|
|
||||||
|
module.exports = plopBase({
|
||||||
|
getTemplatePath,
|
||||||
|
path,
|
||||||
|
storePath,
|
||||||
|
componentPath,
|
||||||
|
});
|
||||||
2
seed/.eslintignore
Normal file
2
seed/.eslintignore
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
node_modules/**
|
||||||
|
flow-typed/**
|
||||||
72
seed/.eslintrc.js
Normal file
72
seed/.eslintrc.js
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
module.exports = {
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
extends: [
|
||||||
|
'airbnb',
|
||||||
|
'plugin:react/recommended',
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:jest/recommended',
|
||||||
|
// config-prettier + plugin-prettier to integrate prettier into eslint
|
||||||
|
'prettier',
|
||||||
|
'plugin:prettier/recommended',
|
||||||
|
],
|
||||||
|
plugins: ['@typescript-eslint', 'react-hooks', 'react', 'jest', 'prettier'],
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
es6: true,
|
||||||
|
},
|
||||||
|
parserOptions: {
|
||||||
|
ecmaFeatures: {
|
||||||
|
jsx: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// global variables
|
||||||
|
globals: {
|
||||||
|
__LOCALE__: false,
|
||||||
|
__DEV__: false,
|
||||||
|
__STAGING__: false,
|
||||||
|
__BROWSER__: true,
|
||||||
|
__SSR__: false,
|
||||||
|
__TEST__: false,
|
||||||
|
__PLATFORM__: 'readonly',
|
||||||
|
__ENV__: 'readonly',
|
||||||
|
__PROVIDER__: 'readonly',
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'linebreak-style': 'off',
|
||||||
|
'global-require': 'off',
|
||||||
|
'no-restricted-globals': 'off',
|
||||||
|
'func-names': 'off',
|
||||||
|
'no-console': 'off',
|
||||||
|
'no-continue': 'off',
|
||||||
|
'no-param-reassign': 'off',
|
||||||
|
'no-plusplus': 'off',
|
||||||
|
'no-loop-func': 'off',
|
||||||
|
//"class-methods-use-this": "off",
|
||||||
|
'jsx-a11y/no-static-element-interactions': 'off',
|
||||||
|
'jsx-a11y/click-events-have-key-events': 'off',
|
||||||
|
'react-hooks/exhaustive-deps': 'warn',
|
||||||
|
'react/prop-types': 'off',
|
||||||
|
'react/require-default-props': 'off',
|
||||||
|
'react/jsx-filename-extension': 'off',
|
||||||
|
'react/no-array-index-key': 'off',
|
||||||
|
'react/default-props-match-prop-types': 'off',
|
||||||
|
'react/jsx-props-no-multi-spaces': 'off',
|
||||||
|
'import/no-webpack-loader-syntax': 'off',
|
||||||
|
'import/no-extraneous-dependencies': 'off',
|
||||||
|
'no-underscore-dangle': 'off',
|
||||||
|
'prefer-destructuring': 'off',
|
||||||
|
'jsx-a11y/no-noninteractive-element-interactions': 'off',
|
||||||
|
'jsx-a11y/label-has-associated-control': 'off',
|
||||||
|
'jsx-a11y/label-has-for': 'off',
|
||||||
|
'jsx-a11y/no-noninteractive-tabindex': 'off',
|
||||||
|
'react/prefer-stateless-function': 0,
|
||||||
|
'prettier/prettier': 'error',
|
||||||
|
'import/extensions': 'off',
|
||||||
|
// Typescript
|
||||||
|
'@typescript-eslint/explicit-member-accessibility': 'off',
|
||||||
|
'@typescript-eslint/indent': 'off',
|
||||||
|
'@typescript-eslint/no-var-requires': 'off',
|
||||||
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
|
'@typescript-eslint/no-empty-interface': 'off',
|
||||||
|
},
|
||||||
|
};
|
||||||
15
seed/.gitignore
vendored
Normal file
15
seed/.gitignore
vendored
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
.idea
|
||||||
|
node_modules
|
||||||
|
package-lock.json
|
||||||
|
dist
|
||||||
|
build
|
||||||
|
|
||||||
|
sls/
|
||||||
|
|
||||||
|
ssr/client
|
||||||
|
ssr/server
|
||||||
|
ssr/serverless
|
||||||
|
|
||||||
|
.serverless/*
|
||||||
|
src/server/manifest.json
|
||||||
|
.DS_Store
|
||||||
56
seed/.htaccess
Normal file
56
seed/.htaccess
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
Options +FollowSymLinks -Indexes -MultiViews
|
||||||
|
#redirection
|
||||||
|
<IfModule mod_rewrite.c>
|
||||||
|
|
||||||
|
RewriteEngine On
|
||||||
|
|
||||||
|
|
||||||
|
# Don't rewrite files or directories
|
||||||
|
RewriteCond %{REQUEST_FILENAME} -f [OR]
|
||||||
|
RewriteCond %{REQUEST_FILENAME} -d
|
||||||
|
RewriteRule ^ - [L]
|
||||||
|
|
||||||
|
# Rewrite everything else to index.html to allow html5 state links
|
||||||
|
RewriteRule ^ index.html [L]
|
||||||
|
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
<IfModule mod_headers.c>
|
||||||
|
Header set Connection keep-alive
|
||||||
|
<FilesMatch "\.(ttf|ttc|otf|eot|woff|font.css|css|html|json|htm|js)$">
|
||||||
|
Header set Access-Control-Allow-Origin "*"
|
||||||
|
</FilesMatch>
|
||||||
|
<FilesMatch "\.(jpg|jpeg|png|gif|swf|svg)$">
|
||||||
|
Header set Cache-Control "max-age=604800, public"
|
||||||
|
</FilesMatch>
|
||||||
|
<FilesMatch "\.(js|css|html|json)$">
|
||||||
|
Header set Cache-Control "max-age=3600, public"
|
||||||
|
</FilesMatch>
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
<IfModule mod_deflate.c>
|
||||||
|
############################################
|
||||||
|
## enable apache served files compression
|
||||||
|
## http://developer.yahoo.com/performance/rules.html#gzip
|
||||||
|
|
||||||
|
# Insert filter on all content
|
||||||
|
SetOutputFilter DEFLATE
|
||||||
|
|
||||||
|
# Insert filter on selected content types only
|
||||||
|
# AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript
|
||||||
|
|
||||||
|
# Netscape 4.x has some problems...
|
||||||
|
BrowserMatch ^Mozilla/4 gzip-only-text/html
|
||||||
|
|
||||||
|
# Netscape 4.06-4.08 have some more problems
|
||||||
|
BrowserMatch ^Mozilla/4\.0[678] no-gzip
|
||||||
|
|
||||||
|
# MSIE masquerades as Netscape, but it is fine
|
||||||
|
BrowserMatch \bMSIE !no-gzip !gzip-only-text/html
|
||||||
|
|
||||||
|
# Don't compress images
|
||||||
|
#SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png)$ no-gzip dont-vary
|
||||||
|
|
||||||
|
# Make sure proxies don't deliver the wrong content
|
||||||
|
Header append Vary User-Agent env=!dont-vary
|
||||||
|
</IfModule>
|
||||||
2
seed/.prettierignore
Normal file
2
seed/.prettierignore
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
.gitlab-ci.yml
|
||||||
|
**/*.hbs
|
||||||
7
seed/.prettierrc.js
Normal file
7
seed/.prettierrc.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
module.exports = {
|
||||||
|
singleQuote: true,
|
||||||
|
trailingComma: 'all',
|
||||||
|
printWidth: 150,
|
||||||
|
tabWidth: 2,
|
||||||
|
useTabs: false,
|
||||||
|
};
|
||||||
11
seed/.storybook/addons.js
Normal file
11
seed/.storybook/addons.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
// Check here https://storybook.js.org/addons/addon-gallery/
|
||||||
|
|
||||||
|
import '@storybook/addons';
|
||||||
|
import '@storybook/addon-actions/register';
|
||||||
|
// import '@storybook/addon-a11y/register';
|
||||||
|
import '@storybook/addon-notes/register'
|
||||||
|
import '@storybook/addon-options/register';
|
||||||
|
import '@storybook/addon-knobs/register';
|
||||||
|
import '@storybook/addon-viewport/register';
|
||||||
|
import '@storybook/addon-backgrounds/register';
|
||||||
|
|
||||||
110
seed/.storybook/config.js
Normal file
110
seed/.storybook/config.js
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
// Polyfills
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
// Storybook
|
||||||
|
import { configure, addDecorator } from '@storybook/react';
|
||||||
|
// React router
|
||||||
|
import StoryRouter from 'storybook-react-router';
|
||||||
|
// Edit properties
|
||||||
|
import { withKnobs } from '@storybook/addon-knobs/react';
|
||||||
|
// Special options
|
||||||
|
import { withOptions } from '@storybook/addon-options';
|
||||||
|
// Check compliance
|
||||||
|
// import { checkA11y } from '@storybook/addon-a11y';
|
||||||
|
// Add notes
|
||||||
|
import { withNotes } from '@storybook/addon-notes';
|
||||||
|
// Viewport
|
||||||
|
import { configureViewport, INITIAL_VIEWPORTS } from '@storybook/addon-viewport';
|
||||||
|
// Background
|
||||||
|
import { withBackgrounds } from '@storybook/addon-backgrounds';
|
||||||
|
|
||||||
|
// Style
|
||||||
|
import 'styles/seedMain.scss';
|
||||||
|
|
||||||
|
// Global components
|
||||||
|
import Modals from 'components/global/Modals';
|
||||||
|
import Loader from 'components/global/Loader';
|
||||||
|
// FONT AWESOME
|
||||||
|
import { library } from '@fortawesome/fontawesome-svg-core';
|
||||||
|
// free icons
|
||||||
|
import { fab } from '@fortawesome/free-brands-svg-icons';
|
||||||
|
import { fas } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { far } from '@fortawesome/free-regular-svg-icons';
|
||||||
|
|
||||||
|
// store
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
|
||||||
|
import setupStore from 'store/setup';
|
||||||
|
|
||||||
|
import rootSaga from 'store/rootSaga';
|
||||||
|
import { routerMiddleware } from 'connected-react-router';
|
||||||
|
|
||||||
|
// Config icons
|
||||||
|
library.add(fab, fas, far);
|
||||||
|
|
||||||
|
const setup = setupStore({});
|
||||||
|
// run saga
|
||||||
|
setup.sagaRun(rootSaga);
|
||||||
|
|
||||||
|
const appDecorator = storyFn => (
|
||||||
|
<Provider store={setup.store}>
|
||||||
|
<div>
|
||||||
|
<Modals />
|
||||||
|
<Loader />
|
||||||
|
<div style={{ padding: 20, height: '100vh' }}>
|
||||||
|
<div style={{ transform: 'translate(0)', height: '100%', width: '100%' }}>{storyFn()}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
|
||||||
|
// automatically import all files ending in *.stories.js
|
||||||
|
const req = require.context('../src', true, /.stories.js$/);
|
||||||
|
const reqParent = require.context('../../src', true, /.stories.js$/);
|
||||||
|
// can be empty
|
||||||
|
let reqDahboard;
|
||||||
|
try {
|
||||||
|
reqDahboard = require.context('../../admin', true, /.stories.js$/);
|
||||||
|
} catch (e) {
|
||||||
|
// continue
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadStories() {
|
||||||
|
req.keys().forEach(filename => req(filename));
|
||||||
|
reqParent.keys().forEach(filename => reqParent(filename));
|
||||||
|
if (reqDahboard) reqDahboard.keys().forEach(filename => reqDahboard(filename));
|
||||||
|
}
|
||||||
|
|
||||||
|
configureViewport({
|
||||||
|
viewports: {
|
||||||
|
...INITIAL_VIEWPORTS,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
addDecorator(
|
||||||
|
withOptions({
|
||||||
|
/* name: 'CRA Kitchen Sink',
|
||||||
|
goFullScreen: false,
|
||||||
|
showAddonsPanel: true,
|
||||||
|
showSearchBox: false,
|
||||||
|
addonPanelInRight: true,
|
||||||
|
sortStoriesByKind: false,
|
||||||
|
hierarchySeparator: /\./,
|
||||||
|
hierarchyRootSeparator: /\|/,
|
||||||
|
enableShortcuts: true, */
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
addDecorator(
|
||||||
|
withBackgrounds([
|
||||||
|
{ name: 'white', value: '#FFF' },
|
||||||
|
{ name: 'dark', value: '#333' },
|
||||||
|
{ name: 'grey', value: '#BBB' },
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
// addDecorator(checkA11y);
|
||||||
|
addDecorator(appDecorator);
|
||||||
|
addDecorator(withKnobs);
|
||||||
|
addDecorator(withNotes);
|
||||||
|
addDecorator(StoryRouter());
|
||||||
|
|
||||||
|
// Configure
|
||||||
|
configure(loadStories, module);
|
||||||
10
seed/.storybook/main.js
Normal file
10
seed/.storybook/main.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
module.exports = {
|
||||||
|
"stories": [
|
||||||
|
"../src/**/*.stories.mdx",
|
||||||
|
"../src/**/*.stories.@(js|jsx|ts|tsx)"
|
||||||
|
],
|
||||||
|
"addons": [
|
||||||
|
"@storybook/addon-links",
|
||||||
|
"@storybook/addon-essentials"
|
||||||
|
]
|
||||||
|
}
|
||||||
9
seed/.storybook/preview.js
Normal file
9
seed/.storybook/preview.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export const parameters = {
|
||||||
|
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||||
|
controls: {
|
||||||
|
matchers: {
|
||||||
|
color: /(background|color)$/i,
|
||||||
|
date: /Date$/,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
32
seed/.storybook/webpack.config.js
Normal file
32
seed/.storybook/webpack.config.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
// you can use this file to add your custom webpack plugins, loaders and anything you like.
|
||||||
|
// This is just the basic way to add additional webpack configurations.
|
||||||
|
// For more information refer the docs: https://storybook.js.org/configurations/custom-webpack-config
|
||||||
|
|
||||||
|
// IMPORTANT
|
||||||
|
// When you add this file, we won't add the default configurations which is similar
|
||||||
|
// to "React Create App". This only has babel loader to load JavaScript.
|
||||||
|
|
||||||
|
// libs
|
||||||
|
const merge = require('webpack-merge');
|
||||||
|
const webpack = require('webpack');
|
||||||
|
|
||||||
|
const parts = require('../config/webpack/parts');
|
||||||
|
const paths = require('../config/paths');
|
||||||
|
|
||||||
|
module.exports = merge([
|
||||||
|
parts.modulePathResolve(Object.values(paths.src), Object.values(paths.modules)),
|
||||||
|
parts.loadFonts(),
|
||||||
|
parts.loadImages(),
|
||||||
|
parts.loadJavaScript(),
|
||||||
|
parts.loadCSS({
|
||||||
|
styleLoader: true,
|
||||||
|
}),
|
||||||
|
parts.setVariables({
|
||||||
|
__BROWSER__: true,
|
||||||
|
__SSR__: false,
|
||||||
|
__TEST__: false,
|
||||||
|
__ENV__: 'default',
|
||||||
|
__PLATFORM__: 'default',
|
||||||
|
__PROVIDER__: 'default',
|
||||||
|
}),
|
||||||
|
]);
|
||||||
285
seed/README.md
Normal file
285
seed/README.md
Normal file
@ -0,0 +1,285 @@
|
|||||||
|
# Make it Seed
|
||||||
|
|
||||||
|
## Author
|
||||||
|
|
||||||
|
Nicolas Bernier
|
||||||
|
|
||||||
|
## Infos
|
||||||
|
|
||||||
|
This project needs to be the submodule of a project.
|
||||||
|
He is not supposed to be launched on its own.
|
||||||
|
|
||||||
|
## Firebase
|
||||||
|
|
||||||
|
If you are using firebase, do not fodrget to add the following in the index.html file of the project
|
||||||
|
|
||||||
|
<!-- Firebase App is always required and must be first -->
|
||||||
|
<script src="https://www.gstatic.com/firebasejs/_VERSION_/firebase-app.js"></script>
|
||||||
|
<!-- Add additional services that you want to use -->
|
||||||
|
<script src="https://www.gstatic.com/firebasejs/_VERSION_/firebase-auth.js"></script>
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
/components : react part
|
||||||
|
|
||||||
|
- /enhancers : high order components
|
||||||
|
- /formItems : components used by redux-form
|
||||||
|
- /forms : redux-from forms
|
||||||
|
- /global : global components of the app (Navigation, Header, ...)
|
||||||
|
- /items : components used in multiple other components
|
||||||
|
- /layouts : layout components of the app
|
||||||
|
- /listItems : components used in list
|
||||||
|
- /pageItems : specific sub-part of pages (if used several times, shoudl be moved to items)
|
||||||
|
- /routes : page components (endpoint of the react-router)
|
||||||
|
- /structure : structural components taking children
|
||||||
|
|
||||||
|
/store : redux part
|
||||||
|
|
||||||
|
- /app // app resource
|
||||||
|
- /auth // auth resource
|
||||||
|
- /content // content ressource
|
||||||
|
- /setup // setup of the store
|
||||||
|
- /utils
|
||||||
|
- rootReducer.ts // root reducers
|
||||||
|
- rootSage.ts // root sagas
|
||||||
|
|
||||||
|
/styles : general scss
|
||||||
|
|
||||||
|
## Icons
|
||||||
|
|
||||||
|
We use this librairie (check in dependencies)
|
||||||
|
|
||||||
|
https://fontawesome.com/icons?d=gallery&s=brands,light,regular,solid&m=free
|
||||||
|
|
||||||
|
For project specific font-type icons:
|
||||||
|
|
||||||
|
### Creating a font zip on Icomoon
|
||||||
|
1. Get your folder with icons in `.svg`
|
||||||
|
2. Ensure your naming is correct, if not, do **not** rename in Icomoon but in your own folder, else it may break things
|
||||||
|
3. Go to https://icomoon.io/app and import that folder into a new Set that you create
|
||||||
|
4. Click on “Generate Font” in the bottom right area, then once on the page is loaded, a “Download” button appears where “Generate Font” was, click and download the zip
|
||||||
|
|
||||||
|
### Linking the created font to your project
|
||||||
|
1. In your project, replace your `[project]-font` with what was exported from Icomoon above *(example: `mygms-font`)*
|
||||||
|
2. Take everything in the `/font` folder and copy/paste it into `styling/components/fonts`, making sure that the names stay the same (Icomoon…)
|
||||||
|
3. Go to your `ProjectIcon` component and copy/paste the `style.css` file from your newly created `[project]-font` folder into `ProjectIcon.styled.ts` (from L.26 `.icon- … ` )
|
||||||
|
4. Select all instances of `.icon-` and replace with `&.icon-`
|
||||||
|
5. Select all instances of `'\` and replace with `'\\`
|
||||||
|
|
||||||
|
And you’re done!
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
Use component `ProjectIcon`
|
||||||
|
Name in `icon` prop has to be what is after the `icon-` in the icon’s list (find the icon list and names in the `demo.html` from your `[project]-font` folder)
|
||||||
|
|
||||||
|
`<ProjectIcon icon=‘arrow-right’/>`
|
||||||
|
|
||||||
|
|
||||||
|
## Eslint
|
||||||
|
|
||||||
|
Could be removed as it should be present in the parent for the IDE
|
||||||
|
|
||||||
|
Eslint package related modules to add in the parent :
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"babel-eslint": "^10.0.2",
|
||||||
|
"eslint": "^5.16.0",
|
||||||
|
"eslint-config-airbnb": "^17.1.1",
|
||||||
|
"eslint-config-airbnb-base": "^13.2.0",
|
||||||
|
"eslint-config-airbnb-typescript": "^4.0.1",
|
||||||
|
"eslint-config-prettier": "^6.0.0",
|
||||||
|
"eslint-import-resolver-webpack": "^0.10.1",
|
||||||
|
"eslint-plugin-babel": "^5.1.0",
|
||||||
|
"eslint-plugin-import": "^2.22.1",
|
||||||
|
"eslint-plugin-jest": "^22.14.1",
|
||||||
|
"eslint-plugin-jsx-a11y": "^6.2.3",
|
||||||
|
"eslint-plugin-prettier": "^3.1.0",
|
||||||
|
"eslint-plugin-react": "^7.14.3",
|
||||||
|
"eslint-plugin-react-hooks": "4.3.0",
|
||||||
|
"prettier": "^1.18.2",
|
||||||
|
"prettier-stylelint": "^0.4.2",
|
||||||
|
"typescript": "^3.9.5"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Reorder package.json
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm remove -S example && npm remove -D example;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Styles
|
||||||
|
|
||||||
|
Add a file in the parent
|
||||||
|
|
||||||
|
stylelint.config.js
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
module.exports = {
|
||||||
|
extends: './seed/stylelint.config.js',
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code formating (prettier && eslint)
|
||||||
|
|
||||||
|
Prettier handle code formating (tabs, space, ...)
|
||||||
|
Eslint handle code error
|
||||||
|
|
||||||
|
You need the plugins _eslint_ and _prettier - code formatter_ from VS Code
|
||||||
|
|
||||||
|
### Config
|
||||||
|
|
||||||
|
You only need the plugin eslint
|
||||||
|
|
||||||
|
1. In you eslint config, include the prettier config as below:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
'prettier/prettier': 'error',
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Then edit your VSCode as below
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"javascript.validate.enable": false,
|
||||||
|
"typescript.validate.enable": false,
|
||||||
|
// Editor
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
// Eslint
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Add a .prettierrc.js (or equivalent) in your root folder
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
module.exports = {
|
||||||
|
singleQuote: true,
|
||||||
|
trailingComma: 'all',
|
||||||
|
printWidth: 100,
|
||||||
|
tabWidth: 2,
|
||||||
|
useTabs: false,
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Other VSCode extansions
|
||||||
|
|
||||||
|
Make sure to have the following extensions :
|
||||||
|
|
||||||
|
SCSS intelliSence
|
||||||
|
stylelint
|
||||||
|
|
||||||
|
## Typescript
|
||||||
|
|
||||||
|
Add tsconfig.json in the parent
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react",
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"*": ["src/*", "admin/src/*", "seed/src/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Store
|
||||||
|
|
||||||
|
### Structure
|
||||||
|
|
||||||
|
a resource is a folder made of
|
||||||
|
|
||||||
|
resourceName/
|
||||||
|
- actions
|
||||||
|
- api
|
||||||
|
- constants
|
||||||
|
- index : gather everything (module based)
|
||||||
|
- reducer
|
||||||
|
- sagas
|
||||||
|
- selectors
|
||||||
|
|
||||||
|
In the parent :
|
||||||
|
|
||||||
|
create appSaga.ts in the store folder of this form
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { fork } from 'redux-saga/effects';
|
||||||
|
|
||||||
|
import YOUR_RESROURCE from 'store/YOUR_RESROURCE';
|
||||||
|
|
||||||
|
import bootupSaga from './bootup';
|
||||||
|
|
||||||
|
export default function* appSaga(): void {
|
||||||
|
yield fork(bootupSaga);
|
||||||
|
yield fork(YOUR_RESROURCE.saga);
|
||||||
|
...
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
create appReducers.ts in the store folder of this form
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import YOUR_RESROURCE from 'store/YOUR_RESROURCE';
|
||||||
|
...
|
||||||
|
|
||||||
|
const reducers = {
|
||||||
|
resource1: YOUR_RESROURCE.reducer,
|
||||||
|
...
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AppStoreState = {
|
||||||
|
resource1: ReturnType<typeof YOUR_RESROURCE.reducer>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default reducers;
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
# Zeus
|
||||||
|
|
||||||
|
graphql-zeus is a helper to perform graphql actions (query, mutation, subscription)
|
||||||
|
|
||||||
|
With v4, some minor syntax changes occured :
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const { Zeus, $ } = zeus;
|
||||||
|
|
||||||
|
Zeus.query({
|
||||||
|
...
|
||||||
|
})
|
||||||
|
Zeus.mutation({
|
||||||
|
...
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
have been replaced by
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const { Zeus, $ } = zeus;
|
||||||
|
Zeus('query', {
|
||||||
|
...
|
||||||
|
})
|
||||||
|
Zeus('mutation', {
|
||||||
|
...
|
||||||
|
})
|
||||||
|
```
|
||||||
30
seed/babel.config.js
Normal file
30
seed/babel.config.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
module.exports = {
|
||||||
|
presets: [
|
||||||
|
'@babel/preset-typescript',
|
||||||
|
|
||||||
|
[
|
||||||
|
'@babel/preset-env',
|
||||||
|
{
|
||||||
|
// add debug information for babel
|
||||||
|
// debug: true,
|
||||||
|
// Because of this, preset-env's behavior is different than browserslist: it does not use the defaults query when there are no targets are found in your Babel or browserslist config(s). If you want to use the defaults query, you will need to explicitly pass it as a target:
|
||||||
|
targets: 'defaults',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'@babel/preset-react',
|
||||||
|
'@babel/preset-flow',
|
||||||
|
],
|
||||||
|
plugins: [
|
||||||
|
'@loadable/babel-plugin',
|
||||||
|
'@emotion',
|
||||||
|
'@babel/plugin-proposal-object-rest-spread',
|
||||||
|
'@babel/plugin-proposal-class-properties',
|
||||||
|
'@babel/plugin-transform-runtime',
|
||||||
|
[
|
||||||
|
'const-enum',
|
||||||
|
{
|
||||||
|
transform: 'constObject',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
};
|
||||||
1
seed/config/jest/fileMock.js
Normal file
1
seed/config/jest/fileMock.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
module.exports = 'test-file-stub';
|
||||||
4
seed/config/jest/setup.js
Normal file
4
seed/config/jest/setup.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
const Enzyme = require('enzyme');
|
||||||
|
const Adapter = require('enzyme-adapter-react-16');
|
||||||
|
|
||||||
|
Enzyme.configure({ adapter: new Adapter() });
|
||||||
1
seed/config/jest/styleMock.js
Normal file
1
seed/config/jest/styleMock.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
module.exports = {};
|
||||||
58
seed/config/paths.js
Normal file
58
seed/config/paths.js
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const baseDir = path.join(__dirname, '..', '..');
|
||||||
|
|
||||||
|
const srcDir = 'src';
|
||||||
|
const adminDir = 'admin';
|
||||||
|
const seedDir = 'seed';
|
||||||
|
const blogDir = 'blog';
|
||||||
|
|
||||||
|
const buildDir = 'dist';
|
||||||
|
|
||||||
|
// classic ssr
|
||||||
|
const ssrBuild = 'ssr/';
|
||||||
|
const clientBuild = 'ssr/client';
|
||||||
|
const serverBuild = 'ssr/server';
|
||||||
|
|
||||||
|
// serverless ssr
|
||||||
|
const slsBuild = 'sls/';
|
||||||
|
const slsServerBuild = 'sls/server';
|
||||||
|
const slsClientBuild = 'sls/client';
|
||||||
|
|
||||||
|
const inApp = path.resolve.bind(path, baseDir);
|
||||||
|
const inAppSrc = file => inApp(srcDir, file);
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
baseDir,
|
||||||
|
main: path.resolve('src/main'),
|
||||||
|
server: path.resolve('src/server'),
|
||||||
|
serverless: path.resolve('src/server/serverless'),
|
||||||
|
publicPath: '/',
|
||||||
|
src: {
|
||||||
|
app: path.resolve(baseDir, 'src'),
|
||||||
|
admin: path.join(baseDir, adminDir, srcDir),
|
||||||
|
blog: path.join(baseDir, blogDir, srcDir),
|
||||||
|
seed: path.join(baseDir, seedDir, srcDir),
|
||||||
|
},
|
||||||
|
modules: {
|
||||||
|
appModules: path.join(baseDir, 'node_modules'),
|
||||||
|
// relative over direct
|
||||||
|
// A relative path will be scanned similarly to how Node scans for node_modules, by looking through the current directory as well as its ancestors (i.e. ./node_modules, ../node_modules, and on).
|
||||||
|
modules: 'node_modules',
|
||||||
|
adminModules: path.join(baseDir, adminDir, 'node_modules'),
|
||||||
|
blogModules: path.join(baseDir, blogDir, 'node_modules'),
|
||||||
|
seedModules: path.join(baseDir, seedDir, 'node_modules'),
|
||||||
|
},
|
||||||
|
defaultBuild: path.join(baseDir, buildDir),
|
||||||
|
// ssr
|
||||||
|
ssrBuild: path.resolve(ssrBuild),
|
||||||
|
clientBuild: path.resolve(clientBuild),
|
||||||
|
serverBuild: path.resolve(serverBuild),
|
||||||
|
// serverless
|
||||||
|
slsBuild: path.resolve(slsBuild),
|
||||||
|
slsServerBuild: path.resolve(slsServerBuild),
|
||||||
|
slsClientBuild: path.resolve(slsClientBuild),
|
||||||
|
// functions
|
||||||
|
inApp,
|
||||||
|
inAppSrc,
|
||||||
|
};
|
||||||
25
seed/config/webpack/common.js
Normal file
25
seed/config/webpack/common.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
/* eslint-disable import/no-extraneous-dependencies */
|
||||||
|
const merge = require('webpack-merge');
|
||||||
|
const LoadablePlugin = require('@loadable/webpack-plugin');
|
||||||
|
const webpack = require('webpack');
|
||||||
|
const paths = require('../paths');
|
||||||
|
const parts = require('./parts');
|
||||||
|
// const ManifestPlugin = require('webpack-manifest-plugin');
|
||||||
|
|
||||||
|
module.exports = merge([
|
||||||
|
parts.modulePathResolve(Object.values(paths.src), Object.values(paths.modules)),
|
||||||
|
parts.loadFonts(),
|
||||||
|
parts.loadImages(),
|
||||||
|
parts.loadVideos(),
|
||||||
|
parts.loadTxts(),
|
||||||
|
parts.loadGQL(),
|
||||||
|
{
|
||||||
|
plugins: [
|
||||||
|
new LoadablePlugin(),
|
||||||
|
new webpack.IgnorePlugin({
|
||||||
|
resourceRegExp: /^\.\/locale$/,
|
||||||
|
contextRegExp: /moment$/,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
73
seed/config/webpack/cordova.js
vendored
Normal file
73
seed/config/webpack/cordova.js
vendored
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
const HTMLWebpackPlugin = require('html-webpack-plugin');
|
||||||
|
const path = require('path');
|
||||||
|
const WebpackBar = require('webpackbar');
|
||||||
|
const Dotenv = require('dotenv-webpack');
|
||||||
|
|
||||||
|
const merge = require('webpack-merge');
|
||||||
|
const webpack = require('webpack');
|
||||||
|
// local
|
||||||
|
const parts = require('./parts');
|
||||||
|
const commonConfig = require('./common');
|
||||||
|
const paths = require('../paths');
|
||||||
|
|
||||||
|
const createConfig = config => {
|
||||||
|
const isProduction = config.mode === 'production';
|
||||||
|
|
||||||
|
return merge([
|
||||||
|
// clean in prod
|
||||||
|
parts.clean(paths.defaultBuild, paths.baseDir),
|
||||||
|
{
|
||||||
|
name: 'app',
|
||||||
|
// Entries configuration
|
||||||
|
entry: {
|
||||||
|
main: [paths.main],
|
||||||
|
},
|
||||||
|
// Source mapping or not
|
||||||
|
devtool: config.prod ? 'none' : 'eval-source-map',
|
||||||
|
output: {
|
||||||
|
path: paths.defaultBuild,
|
||||||
|
filename: 'main.js',
|
||||||
|
publicPath: './',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
parts.loadJavaScript({
|
||||||
|
prod: config.prod,
|
||||||
|
}),
|
||||||
|
parts.loadCSS({
|
||||||
|
prod: config.prod,
|
||||||
|
}),
|
||||||
|
parts.setVariables({
|
||||||
|
__BROWSER__: true,
|
||||||
|
__SSR__: false,
|
||||||
|
__TEST__: false,
|
||||||
|
__PLATFORM__: config.platform || 'default',
|
||||||
|
__PROVIDER__: config.provider || 'default',
|
||||||
|
__ENV__: config.environment || 'default',
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
plugins: [
|
||||||
|
new HTMLWebpackPlugin({
|
||||||
|
template: paths.inAppSrc('index.html'),
|
||||||
|
}),
|
||||||
|
new WebpackBar(),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// Prod plugins
|
||||||
|
isProduction && parts.imageOptimization(),
|
||||||
|
parts.optimize(),
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = (env, params) => {
|
||||||
|
const config = createConfig({
|
||||||
|
mode: params.locale ? 'development' : 'production',
|
||||||
|
port: params.port,
|
||||||
|
analyze: params.analyze,
|
||||||
|
platform: params.platform,
|
||||||
|
provider: params.provider,
|
||||||
|
environment: params.environment,
|
||||||
|
});
|
||||||
|
|
||||||
|
// mode : Possible values for mode are: none, development or production(default).
|
||||||
|
return merge(config, commonConfig);
|
||||||
|
};
|
||||||
116
seed/config/webpack/default.js
Normal file
116
seed/config/webpack/default.js
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
const HTMLWebpackPlugin = require('html-webpack-plugin');
|
||||||
|
const CopyPlugin = require('copy-webpack-plugin');
|
||||||
|
const path = require('path');
|
||||||
|
const WebpackBar = require('webpackbar');
|
||||||
|
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
||||||
|
|
||||||
|
const merge = require('webpack-merge');
|
||||||
|
|
||||||
|
// Parent package
|
||||||
|
const package = require('../../../package.json');
|
||||||
|
|
||||||
|
const deploy = package && package.deploy;
|
||||||
|
|
||||||
|
// local
|
||||||
|
const parts = require('./parts');
|
||||||
|
const commonConfig = require('./common');
|
||||||
|
const paths = require('../paths');
|
||||||
|
|
||||||
|
const createConfig = config => {
|
||||||
|
const isProduction = config.mode === 'production';
|
||||||
|
// const isDevelopment = config.mode === 'development';
|
||||||
|
|
||||||
|
return merge([
|
||||||
|
// clean in prod
|
||||||
|
isProduction && parts.clean(paths.defaultBuild, paths.baseDir),
|
||||||
|
{
|
||||||
|
mode: config.mode,
|
||||||
|
name: 'app',
|
||||||
|
// Entries configuration
|
||||||
|
entry: {
|
||||||
|
main: [paths.main],
|
||||||
|
},
|
||||||
|
// Source mapping or not
|
||||||
|
devtool: isProduction ? 'none' : 'eval-source-map',
|
||||||
|
},
|
||||||
|
// default
|
||||||
|
parts.outputPath({
|
||||||
|
publicPath: (isProduction && deploy && deploy.publicPath) || paths.publicPath,
|
||||||
|
filename: isProduction ? '[name].[contenthash].js' : undefined,
|
||||||
|
}),
|
||||||
|
parts.loadJavaScript({
|
||||||
|
prod: isProduction,
|
||||||
|
}),
|
||||||
|
parts.loadCSS({
|
||||||
|
prod: isProduction,
|
||||||
|
}),
|
||||||
|
parts.setVariables({
|
||||||
|
__BROWSER__: true,
|
||||||
|
__SSR__: false,
|
||||||
|
__TEST__: false,
|
||||||
|
// used for application with different "mode"
|
||||||
|
__PLATFORM__: config.platform || 'default',
|
||||||
|
__PROVIDER__: config.provider || 'default',
|
||||||
|
// environment targeted (dev vs staging vs prod vs beta ...)
|
||||||
|
__ENV__: config.environment || 'default',
|
||||||
|
}),
|
||||||
|
// launch dev server
|
||||||
|
!isProduction && {
|
||||||
|
devServer: {
|
||||||
|
// Enable gzip compression for everything served:
|
||||||
|
compress: true,
|
||||||
|
port: config.port || 4000,
|
||||||
|
// When using the HTML5 History API, the index.html page will likely have to be served in place of any 404 r
|
||||||
|
historyApiFallback: true,
|
||||||
|
hot: true,
|
||||||
|
allowedHosts: ['.lvh.me'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
config.analyze && {
|
||||||
|
plugins: [
|
||||||
|
// analyze dependencies sizes
|
||||||
|
new BundleAnalyzerPlugin({
|
||||||
|
analyzerPort: config.port ? config.port + 1000 : 5000,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// Common plugins
|
||||||
|
{
|
||||||
|
plugins: [
|
||||||
|
new CopyPlugin({
|
||||||
|
patterns: [
|
||||||
|
{
|
||||||
|
from: path.join(paths.baseDir, config.environment === 'staging' ? 'static-staging' : 'static'),
|
||||||
|
to: path.join(paths.defaultBuild, paths.publicPath),
|
||||||
|
noErrorOnMissing: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
new HTMLWebpackPlugin({
|
||||||
|
template: paths.inAppSrc('index.html'),
|
||||||
|
}),
|
||||||
|
new WebpackBar(),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// Prod plugins
|
||||||
|
isProduction && parts.imageOptimization(),
|
||||||
|
parts.optimize(),
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
// env can be given with --env.variable
|
||||||
|
// other params will be in params
|
||||||
|
module.exports = (env, params) => {
|
||||||
|
params = params || {};
|
||||||
|
const config = createConfig({
|
||||||
|
mode: params.locale ? 'development' : 'production',
|
||||||
|
port: params.port,
|
||||||
|
analyze: params.analyze,
|
||||||
|
platform: params.platform,
|
||||||
|
provider: params.provider,
|
||||||
|
environment: params.environment,
|
||||||
|
});
|
||||||
|
|
||||||
|
// mode : Possible values for mode are: none, development or production(default).
|
||||||
|
return merge(config, commonConfig);
|
||||||
|
};
|
||||||
79
seed/config/webpack/electron.js
Normal file
79
seed/config/webpack/electron.js
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
// const nodeExternals = require('webpack-node-externals');
|
||||||
|
const merge = require('webpack-merge');
|
||||||
|
const WebpackBar = require('webpackbar');
|
||||||
|
|
||||||
|
// local
|
||||||
|
const HTMLWebpackPlugin = require('html-webpack-plugin');
|
||||||
|
const parts = require('./parts');
|
||||||
|
const commonConfig = require('./common');
|
||||||
|
const paths = require('.././paths');
|
||||||
|
// Forces webpack-dev-server program to write bundle files to the file system.
|
||||||
|
|
||||||
|
const createConfig = config => {
|
||||||
|
const isProduction = config.mode === 'production';
|
||||||
|
|
||||||
|
return merge([
|
||||||
|
parts.clean(paths.defaultBuild, paths.baseDir),
|
||||||
|
{
|
||||||
|
name: 'electron',
|
||||||
|
entry: {
|
||||||
|
main: [paths.main],
|
||||||
|
},
|
||||||
|
target: 'electron-main',
|
||||||
|
// node: { __dirname: false },
|
||||||
|
// in prod we need the packages
|
||||||
|
node: {
|
||||||
|
// tell webpack that we actually want a working __dirname value
|
||||||
|
// (ref: https://webpack.js.org/configuration/node/#node-__dirname)
|
||||||
|
__dirname: false,
|
||||||
|
__filename: false,
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
path: paths.defaultBuild,
|
||||||
|
filename: 'main.js',
|
||||||
|
publicPath: './',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// investigate why adding prod true destory ssr in prod
|
||||||
|
parts.loadJavaScript(),
|
||||||
|
// need css loader to not fail building
|
||||||
|
parts.loadCSS(),
|
||||||
|
parts.setVariables({
|
||||||
|
__ELECTRON__: true,
|
||||||
|
__TEST__: false,
|
||||||
|
__BROWSER__: false,
|
||||||
|
__SSR__: false,
|
||||||
|
__PLATFORM__: config.platform || 'default',
|
||||||
|
__ENV__: config.environment || 'default',
|
||||||
|
__PROVIDER__: config.provider || 'default',
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
plugins: [
|
||||||
|
new HTMLWebpackPlugin({
|
||||||
|
template: paths.inAppSrc('index.html'),
|
||||||
|
}),
|
||||||
|
new WebpackBar({
|
||||||
|
name: 'Electron',
|
||||||
|
color: 'orange',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// Prod plugins
|
||||||
|
isProduction && parts.imageOptimization(),
|
||||||
|
parts.optimize(),
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = (env, params) => {
|
||||||
|
const config = createConfig({
|
||||||
|
mode: params.locale ? 'development' : 'production',
|
||||||
|
port: params.port,
|
||||||
|
analyze: params.analyze,
|
||||||
|
platform: params.platform,
|
||||||
|
provider: params.provider,
|
||||||
|
environment: params.environment,
|
||||||
|
});
|
||||||
|
|
||||||
|
// mode : Possible values for mode are: none, development or production(default).
|
||||||
|
return merge(commonConfig, config);
|
||||||
|
};
|
||||||
348
seed/config/webpack/parts.js
Normal file
348
seed/config/webpack/parts.js
Normal file
@ -0,0 +1,348 @@
|
|||||||
|
/* eslint-disable import/no-extraneous-dependencies */
|
||||||
|
const webpack = require('webpack');
|
||||||
|
const { CleanWebpackPlugin } = require('clean-webpack-plugin'); // installed via npm
|
||||||
|
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
|
||||||
|
const TerserPlugin = require('terser-webpack-plugin');
|
||||||
|
// const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin');
|
||||||
|
const ImageminPlugin = require('imagemin-webpack-plugin').default;
|
||||||
|
|
||||||
|
const CompressionPlugin = require('compression-webpack-plugin');
|
||||||
|
const zlib = require('zlib');
|
||||||
|
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||||
|
|
||||||
|
const merge = require('webpack-merge');
|
||||||
|
// local
|
||||||
|
const paths = require('../paths');
|
||||||
|
|
||||||
|
/*
|
||||||
|
Need to load the configs here so they can be applied to the external folders as wells
|
||||||
|
*/
|
||||||
|
const babelConfig = require('../../babel.config');
|
||||||
|
// Need required in the file for parent files
|
||||||
|
const postcssConfig = require('../../postcss.config');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve paths
|
||||||
|
*/
|
||||||
|
exports.modulePathResolve = (srcList, modulesList) => ({
|
||||||
|
resolve: {
|
||||||
|
modules: [...srcList, ...modulesList],
|
||||||
|
extensions: ['*', '.js', '.jsx', '.ts', '.tsx', '.json', '.mjs', '.gql', '.graphql'],
|
||||||
|
},
|
||||||
|
resolveLoader: {
|
||||||
|
modules: modulesList,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* Clean
|
||||||
|
*/
|
||||||
|
exports.clean = (path, root) => ({
|
||||||
|
plugins: [new CleanWebpackPlugin()],
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JS (see also .babelrc)
|
||||||
|
*/
|
||||||
|
exports.loadJavaScript = ({ include, exclude, prod } = {}) => ({
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.(js|jsx|ts|mjs|tsx)$/,
|
||||||
|
include,
|
||||||
|
// node_modules(?!([\\/](attr-accept|moment)))
|
||||||
|
exclude: exclude || /node_modules/,
|
||||||
|
use: [
|
||||||
|
{
|
||||||
|
loader: 'babel-loader',
|
||||||
|
options: {
|
||||||
|
presets: babelConfig.presets,
|
||||||
|
plugins: prod ? babelConfig.plugins.concat('transform-remove-console') : babelConfig.plugins,
|
||||||
|
},
|
||||||
|
// */
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default output path
|
||||||
|
*/
|
||||||
|
exports.outputPath = ({
|
||||||
|
path = paths.defaultBuild,
|
||||||
|
filename = '[name].js',
|
||||||
|
chunkFilename = '[name].[contenthash].js',
|
||||||
|
publicPath = paths.publicPath,
|
||||||
|
} = {}) => ({
|
||||||
|
output: {
|
||||||
|
path,
|
||||||
|
filename,
|
||||||
|
chunkFilename,
|
||||||
|
// This option specifies the public URL of the output directory when referenced in a browser. A relative URL is resolved relative to the HTML page (or <base> tag).
|
||||||
|
publicPath,
|
||||||
|
// https://github.com/webpack/webpack/issues/11660
|
||||||
|
// addition for webpack 5
|
||||||
|
// chunkLoading: false,
|
||||||
|
// wasmLoading: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (S)CSS
|
||||||
|
*/
|
||||||
|
exports.loadCSS = ({ include, exclude, prod = false, styleLoader = false, server = false } = {}) => {
|
||||||
|
const fileName = prod ? '[name].[contenthash].css' : '[name].css';
|
||||||
|
|
||||||
|
// use style loader in dev (no ssr)
|
||||||
|
const useStyleLoader = styleLoader;
|
||||||
|
|
||||||
|
const pluginOptions = {
|
||||||
|
filename: fileName,
|
||||||
|
ignoreOrder: !prod, // Enable to remove warnings about conflicting order
|
||||||
|
};
|
||||||
|
|
||||||
|
const plugin = new MiniCssExtractPlugin(pluginOptions);
|
||||||
|
|
||||||
|
const loaderOptions = {
|
||||||
|
// disabling esModule prevent from having warning for empty css files => should not be disabled in prod
|
||||||
|
esModule: prod,
|
||||||
|
};
|
||||||
|
|
||||||
|
const usages = [
|
||||||
|
{
|
||||||
|
loader: 'css-loader',
|
||||||
|
options: {
|
||||||
|
sourceMap: true,
|
||||||
|
// The option importLoaders allows you to configure how many loaders before css-loader should be applied to @imported resources.
|
||||||
|
importLoaders: 2,
|
||||||
|
// to use with localIdentName
|
||||||
|
modules: {
|
||||||
|
localIdentName: '[local]--[hash:base64:5]',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
loader: 'postcss-loader',
|
||||||
|
options: {
|
||||||
|
postcssOptions: postcssConfig,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
loader: 'sass-loader',
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!server) {
|
||||||
|
// if not loaded on server part, module styleIdentifiers will not be usable in the server
|
||||||
|
usages.unshift({
|
||||||
|
loader: useStyleLoader ? 'style-loader' : MiniCssExtractPlugin.loader,
|
||||||
|
options: loaderOptions,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.(css|sass|scss)$/,
|
||||||
|
include,
|
||||||
|
exclude,
|
||||||
|
use: usages,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
plugins: [plugin],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.setVariables = obj => {
|
||||||
|
const env = {};
|
||||||
|
const keys = Object.keys(obj);
|
||||||
|
for (let i = 0; i < keys.length; i += 1) {
|
||||||
|
const key = keys[i];
|
||||||
|
const value = obj[key];
|
||||||
|
env[key] = JSON.stringify(value);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
plugins: [new webpack.DefinePlugin(env)],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FONTS
|
||||||
|
*/
|
||||||
|
const FONTLIST = [
|
||||||
|
['woff', 'application/font-woff'],
|
||||||
|
['woff2', 'application/font-woff2'],
|
||||||
|
['otf', 'font/opentype'],
|
||||||
|
['ttf', 'application/octet-stream'],
|
||||||
|
['eot', 'application/vnd.ms-fontobject'],
|
||||||
|
['svg', 'image/svg+xml'],
|
||||||
|
];
|
||||||
|
|
||||||
|
exports.loadFonts = () => {
|
||||||
|
let rsltConf = {
|
||||||
|
module: {
|
||||||
|
rules: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
FONTLIST.forEach(font => {
|
||||||
|
const extension = font[0];
|
||||||
|
const mimetype = font[1];
|
||||||
|
|
||||||
|
const rule = {
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: new RegExp(`\\.${extension}$`),
|
||||||
|
loader: 'file-loader',
|
||||||
|
include: [/fonts?/],
|
||||||
|
options: {
|
||||||
|
name: 'fonts/[name].[ext]',
|
||||||
|
// limit: 10000,
|
||||||
|
mimetype,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
rsltConf = merge(rsltConf, rule);
|
||||||
|
});
|
||||||
|
|
||||||
|
return rsltConf;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Images
|
||||||
|
*/
|
||||||
|
exports.loadImages = () => ({
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.(png|jpe?g|gif|svg|ico)$/,
|
||||||
|
loader: 'file-loader',
|
||||||
|
exclude: [/fonts?/],
|
||||||
|
options: {
|
||||||
|
name: 'images/[name].[contenthash].[ext]',
|
||||||
|
// for url loader
|
||||||
|
// limit: 8192,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Images
|
||||||
|
*/
|
||||||
|
exports.loadVideos = () => ({
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.(mp4|mov)$/,
|
||||||
|
loader: 'file-loader',
|
||||||
|
options: {
|
||||||
|
name: 'videos/[name].[contenthash].[ext]',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Idea of splitting vendors
|
||||||
|
exports.optimize = () => ({
|
||||||
|
// https://webpack.js.org/plugins/compression-webpack-plugin/
|
||||||
|
plugins: [
|
||||||
|
// new CompressionPlugin({
|
||||||
|
// filename: '[path][base].br',
|
||||||
|
// algorithm: 'brotliCompress',
|
||||||
|
// test: /\.(js|css|html|svg)$/,
|
||||||
|
// compressionOptions: {
|
||||||
|
// params: {
|
||||||
|
// [zlib.constants.BROTLI_PARAM_QUALITY]: 11,
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// threshold: 10240,
|
||||||
|
// minRatio: 0.8,
|
||||||
|
// deleteOriginalAssets: false,
|
||||||
|
// }),
|
||||||
|
],
|
||||||
|
optimization: {
|
||||||
|
minimizer: [
|
||||||
|
new TerserPlugin({
|
||||||
|
terserOptions: {
|
||||||
|
safari10: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
new OptimizeCSSAssetsPlugin({}),
|
||||||
|
],
|
||||||
|
// use default splitChunks config with chnks all
|
||||||
|
// https://webpack.js.org/plugins/split-chunks-plugin/#optimizationsplitchunks
|
||||||
|
splitChunks: {
|
||||||
|
chunks: 'all',
|
||||||
|
// cacheGroups: {
|
||||||
|
// vendor: {
|
||||||
|
// // test with exclusion : /[\\/]node_modules[\\/](?!(attr-accept|moment))/
|
||||||
|
// test: /[\\/]node_modules[\\/]/,
|
||||||
|
// name: 'vendor',
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
},
|
||||||
|
// Source : create-react-app
|
||||||
|
// Keep the runtime chunk separated to enable long term caching
|
||||||
|
// https://twitter.com/wSokra/status/969679223278505985
|
||||||
|
// https://github.com/facebook/create-react-app/issues/5358
|
||||||
|
runtimeChunk: {
|
||||||
|
name: entrypoint => `runtime-${entrypoint.name}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Text loader
|
||||||
|
exports.loadTxts = () => ({
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.txt$/i,
|
||||||
|
use: 'raw-loader',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// GraphQL loader
|
||||||
|
exports.loadGQL = () => ({
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
// fixes https://github.com/graphql/graphql-js/issues/1272
|
||||||
|
{
|
||||||
|
test: /\.mjs$/,
|
||||||
|
include: /node_modules/,
|
||||||
|
type: 'javascript/auto',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
exports.imageOptimization = () => ({
|
||||||
|
plugins: [new ImageminPlugin({ test: /\.(jpe?g|png|gif)$/i })],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add stats
|
||||||
|
exports.stats = () => ({
|
||||||
|
// To discover
|
||||||
|
stats: {
|
||||||
|
cached: false,
|
||||||
|
cachedAssets: false,
|
||||||
|
chunks: false,
|
||||||
|
chunkModules: false,
|
||||||
|
colors: true,
|
||||||
|
hash: false,
|
||||||
|
modules: false,
|
||||||
|
reasons: false,
|
||||||
|
timings: true,
|
||||||
|
version: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
8
seed/config_example.js
Normal file
8
seed/config_example.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// This file is an example of project config with fake data
|
||||||
|
module.exports = {
|
||||||
|
frontends: {
|
||||||
|
staging: 'https://absolute_url_to_staging',
|
||||||
|
prod: 'https://absolute_url_to_prod',
|
||||||
|
dev: 'http://localhost:4000/',
|
||||||
|
},
|
||||||
|
};
|
||||||
26
seed/jest.config.js
Normal file
26
seed/jest.config.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
module.exports = {
|
||||||
|
setupFilesAfterEnv: ['<rootDir>/seed/config/jest/setup.js'],
|
||||||
|
testURL: 'http://localhost/',
|
||||||
|
testEnvironment: 'node',
|
||||||
|
verbose: true,
|
||||||
|
moduleNameMapper: {
|
||||||
|
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '<rootDir>/seed/config/jest/fileMock.js',
|
||||||
|
'\\.(css|scss)$': '<rootDir>/seed/config/jest/styleMock.js',
|
||||||
|
},
|
||||||
|
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$',
|
||||||
|
transformIgnorePatterns: ['[/\\\\]node_modules[/\\\\](?!(@amcharts)\\/).+\\.js$'],
|
||||||
|
rootDir: '../',
|
||||||
|
moduleDirectories: ['src', 'admin/src', 'blog/src', 'seed/src', 'node_modules', 'admin/node_modules', 'blog/node_modules', 'seed/node_modules'],
|
||||||
|
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
|
||||||
|
globals: {
|
||||||
|
__BROWSER__: true,
|
||||||
|
__TEST__: true,
|
||||||
|
__ENV__: 'default',
|
||||||
|
__PLATFORM__: 'default',
|
||||||
|
__PROVIDER__: 'default',
|
||||||
|
__LOCALE__: false,
|
||||||
|
__DEV__: false,
|
||||||
|
__STAGING__: false,
|
||||||
|
__SSR__: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
268
seed/package.json
Normal file
268
seed/package.json
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
{
|
||||||
|
"name": "seed-react",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"author": "Nicolas Bernier",
|
||||||
|
"main": "main.tsx",
|
||||||
|
"scripts": {
|
||||||
|
"preinstall": "([ ! -f package-lock.json ] && npm install --package-lock-only --ignore-scripts --no-audit); npx npm-force-resolutions",
|
||||||
|
"install:deps": "npm i && cd .. && npm i && cd seed",
|
||||||
|
"start": "webpack-dev-server --config config/webpack/default.js --locale",
|
||||||
|
"start:header": "NODE_OPTIONS='--max-http-header-size=100000' webpack-dev-server --config config/webpack/default.js --locale",
|
||||||
|
"electron": "webpack --watch --locale --config config/webpack/electron.js",
|
||||||
|
"cordova": "webpack --watch --locale --config config/webpack/cordova.js",
|
||||||
|
"build:dev": "webpack --environment=dev --config config/webpack/default.js",
|
||||||
|
"build:staging": "webpack --environment=staging --config config/webpack/default.js",
|
||||||
|
"build:prod": "webpack --environment=prod --config config/webpack/default.js",
|
||||||
|
"build": "webpack --config config/webpack/default.js",
|
||||||
|
"ssr": "node scripts/start.js",
|
||||||
|
"ssr:header": "NODE_OPTIONS='--max-http-header-size=100000 --max_old_space_size=2048' node scripts/start.js",
|
||||||
|
"serverless-ssr": "sls offline start --config ./serverless.yaml --environment=dev --port=3000",
|
||||||
|
"serverless-client": "webpack-dev-server --config config/webpack/ssr/slsClient.js",
|
||||||
|
"serverless-client-build": "webpack --environment production --config config/webpack/ssr/slsClient.js",
|
||||||
|
"deploy:sls": "node scripts/sls-deploy.js",
|
||||||
|
"deploy:sls:dev": "node scripts/sls-deploy.js dev",
|
||||||
|
"deploy:sls:staging": "node scripts/sls-deploy.js staging",
|
||||||
|
"deploy:sls:production": "node scripts/sls-deploy.js production",
|
||||||
|
"eslint": "eslint ./node_modules/.bin/eslint src/. --ext .js --fix",
|
||||||
|
"plop": "plop",
|
||||||
|
"storybook": "start-storybook -p 6006",
|
||||||
|
"test": "jest --env=jsdom",
|
||||||
|
"test:watch": "jest --env=jsdom --watch",
|
||||||
|
"build-storybook": "build-storybook"
|
||||||
|
},
|
||||||
|
"browserslist": [
|
||||||
|
"Explorer 10",
|
||||||
|
"Explorer 11",
|
||||||
|
"last 4 version",
|
||||||
|
"> 0.1%",
|
||||||
|
"maintained node versions"
|
||||||
|
],
|
||||||
|
"reactSnap": {
|
||||||
|
"source": "../dist",
|
||||||
|
"minifyHtml": {
|
||||||
|
"collapseWhitespace": false,
|
||||||
|
"removeComments": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"globals": {
|
||||||
|
"__LOCALE__": false,
|
||||||
|
"__DEV__": false,
|
||||||
|
"__STAGING__": false,
|
||||||
|
"__BROWSER__": true,
|
||||||
|
"__SSR__": false,
|
||||||
|
"__TEST__": false,
|
||||||
|
"__PLATFORM__": "default",
|
||||||
|
"__ENV__": "default",
|
||||||
|
"__PROVIDER__": "default"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@apollo/client": "3.3.7",
|
||||||
|
"@apollo/react-components": "^4.0.0",
|
||||||
|
"@emotion/react": "11.4.1",
|
||||||
|
"@emotion/styled": "11.3.0",
|
||||||
|
"@fortawesome/fontawesome-svg-core": "^1.2.28",
|
||||||
|
"@fortawesome/free-brands-svg-icons": "^5.13.0",
|
||||||
|
"@fortawesome/free-regular-svg-icons": "^5.13.0",
|
||||||
|
"@fortawesome/free-solid-svg-icons": "^5.13.0",
|
||||||
|
"@fortawesome/react-fontawesome": "^0.1.9",
|
||||||
|
"@loadable/component": "^5.14.1",
|
||||||
|
"@loadable/server": "^5.14.0",
|
||||||
|
"axios": "^0.21.1",
|
||||||
|
"classnames": "^2.2.6",
|
||||||
|
"connected-react-router": "6.9.1",
|
||||||
|
"cookies": "^0.8.0",
|
||||||
|
"cookies-js": "^1.2.3",
|
||||||
|
"core-js": "^3.15.2",
|
||||||
|
"cropperjs": "^1.5.5",
|
||||||
|
"cross-fetch": "^3.0.6",
|
||||||
|
"es-abstract": "^1.18.0-next.1",
|
||||||
|
"express-manifest-helpers": "^0.6.0",
|
||||||
|
"final-form": "^4.20.2",
|
||||||
|
"final-form-arrays": "^3.0.2",
|
||||||
|
"fuse.js": "^6.4.6",
|
||||||
|
"global": "^4.4.0",
|
||||||
|
"graphql": "15.5.3",
|
||||||
|
"graphql-tag": "^2.12.5",
|
||||||
|
"history": "^4.10.1",
|
||||||
|
"html-react-parser": "^0.14.2",
|
||||||
|
"iban": "0.0.12",
|
||||||
|
"immer": "9.0.7",
|
||||||
|
"is-mobile": "^2.2.2",
|
||||||
|
"js-cookie": "^3.0.1",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"moment": "^2.29.1",
|
||||||
|
"moment-range": "^4.0.2",
|
||||||
|
"moment-timezone": "^0.5.33",
|
||||||
|
"nodemon": "^2.0.7",
|
||||||
|
"payment": "^2.3.0",
|
||||||
|
"postcss": "^8.3.0",
|
||||||
|
"prismjs": "1.25.0",
|
||||||
|
"promise-polyfill": "8.1.0",
|
||||||
|
"qs": "^6.10.3",
|
||||||
|
"query-string": "^7.1.0",
|
||||||
|
"react": "^17.0.1",
|
||||||
|
"react-app-polyfill": "^2.0.0",
|
||||||
|
"react-code-input": "^3.10.0",
|
||||||
|
"react-color": "^2.18.1",
|
||||||
|
"react-date-picker": "^7.10.0",
|
||||||
|
"react-datetime": "^3.0.4",
|
||||||
|
"react-dnd": "^14.0.4",
|
||||||
|
"react-dnd-html5-backend": "^14.0.2",
|
||||||
|
"react-dom": "^17.0.1",
|
||||||
|
"react-dropzone": "^11.2.4",
|
||||||
|
"react-facebook-login": "^4.1.1",
|
||||||
|
"react-favicon": "0.0.18",
|
||||||
|
"react-final-form": "^6.5.3",
|
||||||
|
"react-final-form-arrays": "^3.1.3",
|
||||||
|
"react-final-form-hooks": "^2.0.2",
|
||||||
|
"react-google-login": "^5.2.1",
|
||||||
|
"react-helmet": "^6.0.0",
|
||||||
|
"react-icons": "^4.2.0",
|
||||||
|
"react-lazy-load-image-component": "^1.5.0",
|
||||||
|
"react-number-format": "^4.5.3",
|
||||||
|
"react-paginate": "^7.0.0",
|
||||||
|
"react-phone-input-2": "^2.14.0",
|
||||||
|
"react-player": "^2.9.0",
|
||||||
|
"react-redux": "^7.2.0",
|
||||||
|
"react-router": "^5.2.0",
|
||||||
|
"react-router-dom": "^5.2.0",
|
||||||
|
"react-simple-code-editor": "^0.11.0",
|
||||||
|
"react-switch": "^5.0.1",
|
||||||
|
"react-transition-group": "^4.4.1",
|
||||||
|
"react-virtualized": "^9.21.2",
|
||||||
|
"redux": "^4.0.5",
|
||||||
|
"redux-form": "^8.3.5",
|
||||||
|
"redux-persist": "6.0.0",
|
||||||
|
"redux-persist-cookie-storage": "^1.0.0",
|
||||||
|
"redux-persist-expire": "^1.1.0",
|
||||||
|
"redux-saga": "^0.16.0",
|
||||||
|
"sanitize-html": "^2.4.0",
|
||||||
|
"scheduler": "^0.15.0",
|
||||||
|
"serverless-http": "^2.4.1",
|
||||||
|
"tus-js-client": "^2.3.0",
|
||||||
|
"unflatten": "1.0.4",
|
||||||
|
"uniqid": "^5.2.0",
|
||||||
|
"universal-cookie": "^4.0.3",
|
||||||
|
"universal-cookie-express": "^4.0.3",
|
||||||
|
"unorm": "^1.6.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.12.10",
|
||||||
|
"@babel/plugin-proposal-class-properties": "^7.12.1",
|
||||||
|
"@babel/plugin-proposal-object-rest-spread": "^7.12.1",
|
||||||
|
"@babel/plugin-transform-runtime": "^7.12.10",
|
||||||
|
"@babel/preset-env": "^7.12.10",
|
||||||
|
"@babel/preset-flow": "^7.12.1",
|
||||||
|
"@babel/preset-react": "^7.12.10",
|
||||||
|
"@babel/preset-typescript": "^7.12.7",
|
||||||
|
"@babel/runtime": "^7.12.5",
|
||||||
|
"@emotion/babel-plugin": "11.3.0",
|
||||||
|
"@loadable/babel-plugin": "^5.13.2",
|
||||||
|
"@loadable/webpack-plugin": "^5.14.0",
|
||||||
|
"@storybook/addon-a11y": "^6.2.9",
|
||||||
|
"@storybook/addon-actions": "^6.3.0",
|
||||||
|
"@storybook/addon-backgrounds": "^6.2.9",
|
||||||
|
"@storybook/addon-essentials": "^6.3.0",
|
||||||
|
"@storybook/addon-knobs": "^6.2.9",
|
||||||
|
"@storybook/addon-links": "^6.3.0",
|
||||||
|
"@storybook/addon-notes": "^5.3.18",
|
||||||
|
"@storybook/addon-options": "^5.3.18",
|
||||||
|
"@storybook/addon-viewport": "^6.2.9",
|
||||||
|
"@storybook/addons": "^6.2.9",
|
||||||
|
"@storybook/cli": "^6.2.9",
|
||||||
|
"@storybook/react": "^6.3.0",
|
||||||
|
"@testing-library/jest-dom": "^5.12.0",
|
||||||
|
"@testing-library/react": "^11.2.7",
|
||||||
|
"@types/jest": "^24.9.1",
|
||||||
|
"@types/node": "^11.15.35",
|
||||||
|
"@types/react": "^16.9.55",
|
||||||
|
"@types/react-dom": "^16.9.9",
|
||||||
|
"@types/react-redux": "^7.1.9",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^1.13.0",
|
||||||
|
"@typescript-eslint/parser": "^1.13.0",
|
||||||
|
"babel-eslint": "^10.1.0",
|
||||||
|
"babel-loader": "^8.2.2",
|
||||||
|
"babel-plugin-const-enum": "^1.1.0",
|
||||||
|
"babel-plugin-transform-remove-console": "^6.9.4",
|
||||||
|
"browserslist": "^4.16.6",
|
||||||
|
"clean-webpack-plugin": "^3.0.0",
|
||||||
|
"compression-webpack-plugin": "^6.1.1",
|
||||||
|
"copy-webpack-plugin": "^6.2.1",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"cross-env": "^5.2.0",
|
||||||
|
"css-loader": "^5.2.0",
|
||||||
|
"dotenv-webpack": "^7.0.2",
|
||||||
|
"enzyme": "^3.11.0",
|
||||||
|
"enzyme-adapter-react-16": "^1.15.2",
|
||||||
|
"eslint": "^5.16.0",
|
||||||
|
"eslint-config-airbnb": "^17.1.1",
|
||||||
|
"eslint-config-airbnb-base": "^13.2.0",
|
||||||
|
"eslint-config-airbnb-typescript": "^4.0.1",
|
||||||
|
"eslint-config-prettier": "^6.15.0",
|
||||||
|
"eslint-import-resolver-webpack": "^0.10.1",
|
||||||
|
"eslint-plugin-babel": "^5.3.1",
|
||||||
|
"eslint-plugin-import": "^2.22.1",
|
||||||
|
"eslint-plugin-jest": "^22.17.0",
|
||||||
|
"eslint-plugin-jsx-a11y": "^6.4.1",
|
||||||
|
"eslint-plugin-prettier": "^3.1.4",
|
||||||
|
"eslint-plugin-react": "^7.21.5",
|
||||||
|
"eslint-plugin-react-hooks": "4.3.0",
|
||||||
|
"execa": "^4.0.1",
|
||||||
|
"express": "^4.17.1",
|
||||||
|
"file-loader": "^6.2.0",
|
||||||
|
"html-webpack-plugin": "^4.5.0",
|
||||||
|
"imagemin-gifsicle": "^7.0.0",
|
||||||
|
"imagemin-jpegtran": "^7.0.0",
|
||||||
|
"imagemin-mozjpeg": "^9.0.0",
|
||||||
|
"imagemin-optipng": "^8.0.0",
|
||||||
|
"imagemin-pngquant": "^9.0.1",
|
||||||
|
"imagemin-svgo": "^8.0.0",
|
||||||
|
"imagemin-webpack-plugin": "^2.4.2",
|
||||||
|
"inquirer-directory": "^2.2.0",
|
||||||
|
"jest": "^27.0.5",
|
||||||
|
"listr": "^0.14.3",
|
||||||
|
"mini-css-extract-plugin": "^1.6.0",
|
||||||
|
"optimize-css-assets-webpack-plugin": "^6.0.0",
|
||||||
|
"plop": "^2.6.0",
|
||||||
|
"postcss-loader": "^4.1.0",
|
||||||
|
"postcss-preset-env": "^6.7.0",
|
||||||
|
"prettier": "^1.18.2",
|
||||||
|
"prettier-stylelint": "^0.4.2",
|
||||||
|
"raw-loader": "^4.0.2",
|
||||||
|
"react-snap": "^1.23.0",
|
||||||
|
"redbox-react": "^1.6.0",
|
||||||
|
"redux-devtools-extension": "^2.13.8",
|
||||||
|
"redux-saga-test-plan": "^3.7.0",
|
||||||
|
"sass": "^1.34.1",
|
||||||
|
"sass-loader": "^10.1.1",
|
||||||
|
"serverless": "^2.29.0",
|
||||||
|
"serverless-offline": "^6.1.5",
|
||||||
|
"serverless-webpack": "^5.3.5",
|
||||||
|
"source-map-loader": "^2.0.1",
|
||||||
|
"storybook-react-router": "^1.0.8",
|
||||||
|
"style-loader": "^2.0.0",
|
||||||
|
"stylelint": "^13.13.1",
|
||||||
|
"stylelint-scss": "^3.17.2",
|
||||||
|
"terser-webpack-plugin": "^4.2.3",
|
||||||
|
"ts-jest": "^27.0.3",
|
||||||
|
"typescript": "^3.9.5",
|
||||||
|
"url-loader": "^4.1.1",
|
||||||
|
"webpack": "^4.44.2",
|
||||||
|
"webpack-bundle-analyzer": "^4.4.2",
|
||||||
|
"webpack-cli": "3.3.12",
|
||||||
|
"webpack-dev-middleware": "^3.7.2",
|
||||||
|
"webpack-dev-server": "3.11.2",
|
||||||
|
"webpack-hot-middleware": "2.25.1",
|
||||||
|
"webpack-manifest-plugin": "^2.2.0",
|
||||||
|
"webpack-merge": "^4.2.2",
|
||||||
|
"webpack-node-externals": "^2.5.2",
|
||||||
|
"webpackbar": "^4.0.0",
|
||||||
|
"write-file-webpack-plugin": "^4.5.1"
|
||||||
|
},
|
||||||
|
"resolutions": {
|
||||||
|
"immer": "9.0.7"
|
||||||
|
}
|
||||||
|
}
|
||||||
26
seed/plop-templates/component/component.hbs
Normal file
26
seed/plop-templates/component/component.hbs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import classNames from 'classnames/bind';
|
||||||
|
import styleIdentifiers from './{{pascalCase name}}.module.scss';
|
||||||
|
import TextItem from 'components/items/TextItem';
|
||||||
|
|
||||||
|
const styles = classNames.bind(styleIdentifiers);
|
||||||
|
|
||||||
|
export interface StateProps {};
|
||||||
|
|
||||||
|
export interface DispatchProps {};
|
||||||
|
|
||||||
|
export interface OwnProps {};
|
||||||
|
|
||||||
|
export type {{pascalCase name}}Props = StateProps & DispatchProps & OwnProps;
|
||||||
|
|
||||||
|
interface {{pascalCase name}}State {};
|
||||||
|
|
||||||
|
export default class {{pascalCase name}} extends Component<{{pascalCase name}}Props, {{pascalCase name}}State> {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className={styles('{{pascalCase name}}')}>
|
||||||
|
<TextItem path="{{pascalCase name}} generated" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
20
seed/plop-templates/component/componentDumb.hbs
Normal file
20
seed/plop-templates/component/componentDumb.hbs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import classNames from 'classnames/bind';
|
||||||
|
import styleIdentifiers from './{{pascalCase name}}.module.scss';
|
||||||
|
import TextItem from 'components/items/TextItem';
|
||||||
|
|
||||||
|
const styles = classNames.bind(styleIdentifiers);
|
||||||
|
|
||||||
|
interface {{pascalCase name}}Props {};
|
||||||
|
|
||||||
|
interface {{pascalCase name}}State {};
|
||||||
|
|
||||||
|
export default class {{pascalCase name}} extends Component<{{pascalCase name}}Props, {{pascalCase name}}State> {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className={styles('{{pascalCase name}}')}>
|
||||||
|
<TextItem path="{{pascalCase name}} generated" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
51
seed/plop-templates/component/componentForm.hbs
Normal file
51
seed/plop-templates/component/componentForm.hbs
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Field } from 'react-final-form';
|
||||||
|
|
||||||
|
// Redux part
|
||||||
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
|
import { StoreState } from 'store/rootReducer';
|
||||||
|
|
||||||
|
// Import actions here
|
||||||
|
|
||||||
|
import classNames from 'classnames/bind';
|
||||||
|
import Input from 'components/formItems/Input';
|
||||||
|
import Button from 'components/items/Button';
|
||||||
|
import FieldAdapter from 'components/formItems/FieldAdapter';
|
||||||
|
|
||||||
|
import { required, email, composeValidators } from 'store/utils/validation';
|
||||||
|
import styleIdentifiers from './{{pascalCase name}}.module.scss';
|
||||||
|
|
||||||
|
const styles = classNames.bind(styleIdentifiers);
|
||||||
|
|
||||||
|
export interface StateProps {}
|
||||||
|
|
||||||
|
export interface DispatchProps {}
|
||||||
|
|
||||||
|
export interface OwnProps {
|
||||||
|
onSubmit: Function;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type {{pascalCase name}}Props = StateProps & DispatchProps & OwnProps;
|
||||||
|
|
||||||
|
const {{pascalCase name}} = (props: {{pascalCase name}}Props) => {
|
||||||
|
const { handleSubmit, submitting, valid } = props;
|
||||||
|
|
||||||
|
// mapStateToProps
|
||||||
|
const content = useSelector((state: StoreState) => state.content.raw);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form className={styles('{{dashCase name}}')} onSubmit={handleSubmit}>
|
||||||
|
<FieldAdapter
|
||||||
|
component={Input}
|
||||||
|
name="email"
|
||||||
|
type="email"
|
||||||
|
label="Email"
|
||||||
|
isRequired
|
||||||
|
validate={composeValidators(required, email)}
|
||||||
|
/>
|
||||||
|
<Button disabled={submitting || !valid} label="Submit" type="submit" />
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {{pascalCase name}};
|
||||||
19
seed/plop-templates/component/componentHook.hbs
Normal file
19
seed/plop-templates/component/componentHook.hbs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
|
import classNames from 'classnames/bind';
|
||||||
|
import TextItem from 'components/items/TextItem';
|
||||||
|
import styleIdentifiers from './{{pascalCase name}}.module.scss';
|
||||||
|
|
||||||
|
const styles = classNames.bind(styleIdentifiers);
|
||||||
|
|
||||||
|
export interface {{pascalCase name}} {}
|
||||||
|
|
||||||
|
const {{pascalCase name}} = (props: {{pascalCase name}}Props) => {
|
||||||
|
return (
|
||||||
|
<div className={styles('{{dashCase name}}')}>
|
||||||
|
<TextItem path="{{pascalCase name}} Component" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {{pascalCase name}};
|
||||||
30
seed/plop-templates/component/componentHookRedux.hbs
Normal file
30
seed/plop-templates/component/componentHookRedux.hbs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
|
// Redux part
|
||||||
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
|
import { StoreState } from 'store/rootReducer';
|
||||||
|
|
||||||
|
// Import actions here
|
||||||
|
|
||||||
|
import classNames from 'classnames/bind';
|
||||||
|
import TextItem from 'components/items/TextItem';
|
||||||
|
import styleIdentifiers from './{{pascalCase name}}.module.scss';
|
||||||
|
|
||||||
|
const styles = classNames.bind(styleIdentifiers);
|
||||||
|
|
||||||
|
export interface {{pascalCase name}}Props {}
|
||||||
|
|
||||||
|
const {{pascalCase name}} = (props: {{pascalCase name}}Props) => {
|
||||||
|
// mapStateToProps
|
||||||
|
const lg = useSelector((state: StoreState) => state.content.lg);
|
||||||
|
// Allow to dispatch actions
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles('{{dashCase name}}')}>
|
||||||
|
<TextItem path="{{pascalCase name}} Component" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {{pascalCase name}};
|
||||||
3
seed/plop-templates/component/componentIndexDumb.hbs
Normal file
3
seed/plop-templates/component/componentIndexDumb.hbs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import {{pascalCase name}} from './{{pascalCase name}}';
|
||||||
|
|
||||||
|
export default {{pascalCase name}};
|
||||||
21
seed/plop-templates/component/componentIndexForm.hbs
Normal file
21
seed/plop-templates/component/componentIndexForm.hbs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { reduxForm } from 'redux-form';
|
||||||
|
import {{pascalCase name}}, { StateProps, DispatchProps, FormData, OwnProps } from './{{pascalCase name}}';
|
||||||
|
|
||||||
|
const createReduxForm = reduxForm<FormData, OwnProps>({
|
||||||
|
// a unique name for the form
|
||||||
|
form: '{{camelCase name}}',
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = createReduxForm({{pascalCase name}});
|
||||||
|
|
||||||
|
const mapStateToProps = (): Object => ({});
|
||||||
|
|
||||||
|
const mapDispatchToProps = {};
|
||||||
|
|
||||||
|
const Wrapped = connect<StateProps, DispatchProps, OwnProps>(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps,
|
||||||
|
)(form);
|
||||||
|
|
||||||
|
export default Wrapped;
|
||||||
15
seed/plop-templates/component/componentIndexSmart.hbs
Normal file
15
seed/plop-templates/component/componentIndexSmart.hbs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { StoreState } from 'store/rootReducer';
|
||||||
|
|
||||||
|
import {{pascalCase name}}, { StateProps, DispatchProps, OwnProps } from './{{pascalCase name}}';
|
||||||
|
|
||||||
|
const mapStateToProps = (state: StoreState): {} => ({});
|
||||||
|
|
||||||
|
const mapDispatchToProps = {};
|
||||||
|
|
||||||
|
const Wrapped = connect<StateProps, DispatchProps, OwnProps>(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps,
|
||||||
|
)({{pascalCase name}});
|
||||||
|
|
||||||
|
export default Wrapped;
|
||||||
4
seed/plop-templates/component/componentStyle.hbs
Normal file
4
seed/plop-templates/component/componentStyle.hbs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
@import '~styles/mixins';
|
||||||
|
|
||||||
|
.{{dashCase name}} {
|
||||||
|
}
|
||||||
17
seed/plop-templates/component/componentStyled.hbs
Normal file
17
seed/plop-templates/component/componentStyled.hbs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import TextItem from 'components/items/TextItem';
|
||||||
|
import * as Styled from './{{pascalCase name}}.styled';
|
||||||
|
|
||||||
|
export interface {{pascalCase name}}Props {}
|
||||||
|
|
||||||
|
export const {{pascalCase name}} = (props: {{pascalCase name}}Props) => {
|
||||||
|
return (
|
||||||
|
<Styled.{{pascalCase name}}>
|
||||||
|
<TextItem path="{{pascalCase name}} Component" />
|
||||||
|
</Styled.{{pascalCase name}}>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {{pascalCase name}};
|
||||||
26
seed/plop-templates/component/componentStyledRedux.hbs
Normal file
26
seed/plop-templates/component/componentStyledRedux.hbs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
|
// Redux part
|
||||||
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
|
import { StoreState } from 'store/rootReducer';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import TextItem from '../../items/TextItem';
|
||||||
|
import * as Styled from './{{pascalCase name}}.styled';
|
||||||
|
|
||||||
|
export interface {{pascalCase name}}Props {}
|
||||||
|
|
||||||
|
export const {{pascalCase name}} = (props: {{pascalCase name}}Props) => {
|
||||||
|
// mapStateToProps
|
||||||
|
const lg = useSelector((state: StoreState) => state.content.lg);
|
||||||
|
// Allow to dispatch actions
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Styled.{{pascalCase name}}>
|
||||||
|
<TextItem path="{{pascalCase name}} Component" />
|
||||||
|
</Styled.{{pascalCase name}}>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {{pascalCase name}};
|
||||||
9
seed/plop-templates/component/componentTest.hbs
Normal file
9
seed/plop-templates/component/componentTest.hbs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render, screen } from 'utils/test-utils';
|
||||||
|
import {{pascalCase name}} from './index';
|
||||||
|
|
||||||
|
describe('{{pascalCase name}}', () => {
|
||||||
|
it('should render without crashing', () => {
|
||||||
|
render(<{{pascalCase name}} />);
|
||||||
|
});
|
||||||
|
});
|
||||||
23
seed/plop-templates/component/customHook.hbs
Normal file
23
seed/plop-templates/component/customHook.hbs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
const use{{pascalCase name}} = (): any => {
|
||||||
|
const [value, setValue] = useState(null);
|
||||||
|
|
||||||
|
useEffect((): Function => {
|
||||||
|
const handleValueChange = (newValue): void => {
|
||||||
|
setValue(newValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
// subscribe function
|
||||||
|
window.addEventListener('keydown', handleValueChange);
|
||||||
|
|
||||||
|
return (): void => {
|
||||||
|
// unsubscribe function
|
||||||
|
window.removeEventListener('keydown', handleValueChange);
|
||||||
|
};
|
||||||
|
}, [value]); // Only re-subscribe if value changes
|
||||||
|
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default use{{pascalCase name}};
|
||||||
12
seed/plop-templates/component/story-form.hbs
Normal file
12
seed/plop-templates/component/story-form.hbs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { storiesOf } from '@storybook/react';
|
||||||
|
import { Form, Field } from 'react-final-form';
|
||||||
|
import { text, object, color, dom, array } from '@storybook/addon-knobs';
|
||||||
|
import {{ pascalCase name }} from './index';
|
||||||
|
|
||||||
|
storiesOf('seed/formItems/{{ pascalCase name }}', module)
|
||||||
|
.addDecorator(story => {
|
||||||
|
const Wrapper = <Form onSubmit={() => {}} render={story} />;
|
||||||
|
return <Wrapper />;
|
||||||
|
})
|
||||||
|
.add('Normal', () => <Field name="test" component={ {{ pascalCase name }} } label="test" />);
|
||||||
7
seed/plop-templates/component/story.hbs
Normal file
7
seed/plop-templates/component/story.hbs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { storiesOf } from '@storybook/react';
|
||||||
|
import { text, object, color, dom, array } from '@storybook/addon-knobs';
|
||||||
|
|
||||||
|
import {{ pascalCase name }} from './index';
|
||||||
|
|
||||||
|
storiesOf('newComponents/{{ pascalCase name }}', module).add('Default', () => <{{ pascalCase name }} />);
|
||||||
5
seed/plop-templates/component/styled.hbs
Normal file
5
seed/plop-templates/component/styled.hbs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { mediaQuery } from '../../../styles';
|
||||||
|
|
||||||
|
export const {{pascalCase name}} = styled.div`
|
||||||
|
`;
|
||||||
13
seed/plop-templates/resource/actions.hbs
Normal file
13
seed/plop-templates/resource/actions.hbs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import {
|
||||||
|
createActionStructure,
|
||||||
|
createAsyncStructure,
|
||||||
|
createResourceStructure,
|
||||||
|
} from 'store/utils/actions';
|
||||||
|
|
||||||
|
import constants from './constants';
|
||||||
|
|
||||||
|
const actions = {
|
||||||
|
...createResourceStructure(`${constants.prefix}`),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default actions;
|
||||||
8
seed/plop-templates/resource/apis.hbs
Normal file
8
seed/plop-templates/resource/apis.hbs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { call, crud } from 'store/utils/api';
|
||||||
|
import { config } from 'config/general';
|
||||||
|
|
||||||
|
const api = {
|
||||||
|
...crud('{{camelCase name}}'),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default api;
|
||||||
5
seed/plop-templates/resource/constants.hbs
Normal file
5
seed/plop-templates/resource/constants.hbs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
const prefix = '{{ camelCase name }}';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
prefix,
|
||||||
|
};
|
||||||
15
seed/plop-templates/resource/index.hbs
Normal file
15
seed/plop-templates/resource/index.hbs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import actions from './actions';
|
||||||
|
import constants from './constants';
|
||||||
|
import reducer from './reducer';
|
||||||
|
import selectors from './selectors';
|
||||||
|
import sagas from './sagas';
|
||||||
|
import apis from './apis';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
actions,
|
||||||
|
constants,
|
||||||
|
reducer,
|
||||||
|
selectors,
|
||||||
|
sagas,
|
||||||
|
apis,
|
||||||
|
};
|
||||||
15
seed/plop-templates/resource/indexApollo.hbs
Normal file
15
seed/plop-templates/resource/indexApollo.hbs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import actions from './actions';
|
||||||
|
import constants from './constants';
|
||||||
|
import reducer from './reducer';
|
||||||
|
import selectors from './selectors';
|
||||||
|
import sagas from './sagas';
|
||||||
|
import * as models from './models';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
actions,
|
||||||
|
constants,
|
||||||
|
reducer,
|
||||||
|
selectors,
|
||||||
|
sagas,
|
||||||
|
models,
|
||||||
|
};
|
||||||
12
seed/plop-templates/resource/models.hbs
Normal file
12
seed/plop-templates/resource/models.hbs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { translatableModel } from 'store/appModels';
|
||||||
|
|
||||||
|
const {{camelCase name}}ListModel = {
|
||||||
|
_id: true,
|
||||||
|
title: translatableModel,
|
||||||
|
};
|
||||||
|
|
||||||
|
const {{camelCase name}}Model = {
|
||||||
|
...{{camelCase name}}ListModel,
|
||||||
|
};
|
||||||
|
|
||||||
|
export { {{camelCase name}}ListModel, {{camelCase name}}Model };
|
||||||
62
seed/plop-templates/resource/reducer.hbs
Normal file
62
seed/plop-templates/resource/reducer.hbs
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { StoreAction } from 'store/utils/actions';
|
||||||
|
import produce from 'immer';
|
||||||
|
import {
|
||||||
|
handleRequest,
|
||||||
|
handleResponse,
|
||||||
|
handleUpdateResponse,
|
||||||
|
handleAddResponse,
|
||||||
|
handleDeleteResponse,
|
||||||
|
} from 'store/utils/reducers';
|
||||||
|
import auth from 'store/auth';
|
||||||
|
import actions from './actions';
|
||||||
|
|
||||||
|
interface {{pascalCase name}}State {
|
||||||
|
list: {} | null;
|
||||||
|
detail: {} | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const initialState: {{pascalCase name}}State = {
|
||||||
|
list: null,
|
||||||
|
detail: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const treatmentIn = values => {
|
||||||
|
const output = { ...values };
|
||||||
|
|
||||||
|
return output;
|
||||||
|
};
|
||||||
|
|
||||||
|
const reducer = (state: {{pascalCase name}}State = initialState, action: StoreAction): {{pascalCase name}}State => {
|
||||||
|
return produce(
|
||||||
|
state,
|
||||||
|
(draft): {{pascalCase name}}State => {
|
||||||
|
switch (action.type) {
|
||||||
|
case actions.list.request.constant:
|
||||||
|
handleRequest(draft, action, 'list', true);
|
||||||
|
break;
|
||||||
|
case actions.list.result.constant:
|
||||||
|
handleResponse(draft, action, 'list', treatmentIn);
|
||||||
|
break;
|
||||||
|
case actions.update.result.constant:
|
||||||
|
handleUpdateResponse(draft, action, 'list', treatmentIn);
|
||||||
|
break;
|
||||||
|
case actions.add.result.constant:
|
||||||
|
handleAddResponse(draft, action, 'list', treatmentIn);
|
||||||
|
break;
|
||||||
|
case actions.delete.result.constant:
|
||||||
|
handleDeleteResponse(draft, action, 'list');
|
||||||
|
break;
|
||||||
|
case actions.detail.result.constant:
|
||||||
|
handleResponse(draft, action, 'detail', treatmentIn);
|
||||||
|
break;
|
||||||
|
case auth.actions.logout.result.constant:
|
||||||
|
draft.list = null;
|
||||||
|
draft.detail = null;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default reducer;
|
||||||
28
seed/plop-templates/resource/sagas.hbs
Normal file
28
seed/plop-templates/resource/sagas.hbs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { put, takeLatest, all, call } from 'redux-saga/effects';
|
||||||
|
|
||||||
|
// Apis from parent
|
||||||
|
import { api } from 'store/apis';
|
||||||
|
// Action type
|
||||||
|
import { StoreAction } from 'store/utils/actions';
|
||||||
|
import { genericCrudSagas, apiCall } from 'store/utils/sagas';
|
||||||
|
|
||||||
|
import { config } from 'config/general';
|
||||||
|
|
||||||
|
import actions from './actions';
|
||||||
|
import selectors from './selectors';
|
||||||
|
|
||||||
|
const treatmentOut = values => {
|
||||||
|
const output = {
|
||||||
|
...values,
|
||||||
|
};
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
function* {{camelCase name}}Watchers() {
|
||||||
|
yield all([...genericCrudSagas(actions, '{{camelCase name}}')]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function* saga() {
|
||||||
|
yield call({{camelCase name}}Watchers);
|
||||||
|
}
|
||||||
110
seed/plop-templates/resource/sagasApollo.hbs
Normal file
110
seed/plop-templates/resource/sagasApollo.hbs
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import { put, takeEvery, all, call } from 'redux-saga/effects';
|
||||||
|
|
||||||
|
import gql from 'graphql-tag';
|
||||||
|
|
||||||
|
// Action type
|
||||||
|
import { StoreAction } from 'store/utils/actions';
|
||||||
|
|
||||||
|
import { genericMutation, genericQuery } from 'store/utils/apollo';
|
||||||
|
|
||||||
|
import { config, zeus } from 'config/general';
|
||||||
|
|
||||||
|
import { deepOmit } from 'store/utils/helper';
|
||||||
|
import actions from './actions';
|
||||||
|
import selectors from './selectors';
|
||||||
|
import * as models from './models';
|
||||||
|
|
||||||
|
const { Zeus, $ } = zeus;
|
||||||
|
|
||||||
|
export const treatmentOut = values => {
|
||||||
|
const output = {
|
||||||
|
...values,
|
||||||
|
};
|
||||||
|
|
||||||
|
return deepOmit(output, ['_id', '__typename', 'createdAt', 'updatedAt', 'get.*']);
|
||||||
|
};
|
||||||
|
|
||||||
|
function* list(action: StoreAction) {
|
||||||
|
yield call(
|
||||||
|
genericQuery({
|
||||||
|
objectReturn: '{{camelCase name}}sGetMany',
|
||||||
|
search: action?.payload?.data,
|
||||||
|
model: models.{{camelCase name}}ListModel,
|
||||||
|
actionRes: actions.list,
|
||||||
|
}),
|
||||||
|
action,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function* detail(action) {
|
||||||
|
yield call(
|
||||||
|
genericQuery({
|
||||||
|
objectReturn: '{{camelCase name}}sGetOne',
|
||||||
|
search: {
|
||||||
|
id: action?.payload?.id,
|
||||||
|
},
|
||||||
|
model: models.{{camelCase name}}Model,
|
||||||
|
actionRes: actions.detail,
|
||||||
|
}),
|
||||||
|
action,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MUTATIONS
|
||||||
|
*/
|
||||||
|
|
||||||
|
function* add(action: StoreAction) {
|
||||||
|
yield call(
|
||||||
|
genericMutation({
|
||||||
|
objectReturn: '{{camelCase name}}sAddOne',
|
||||||
|
model: models.{{camelCase name}}Model,
|
||||||
|
treatmentOut,
|
||||||
|
actionRes: actions.add,
|
||||||
|
}),
|
||||||
|
action,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function* update(action: StoreAction) {
|
||||||
|
yield call(
|
||||||
|
genericMutation({
|
||||||
|
objectReturn: '{{camelCase name}}sEditOne',
|
||||||
|
model: models.{{camelCase name}}Model,
|
||||||
|
inputs: {
|
||||||
|
id: action?.payload?.id,
|
||||||
|
},
|
||||||
|
treatmentOut,
|
||||||
|
actionRes: actions.update,
|
||||||
|
}),
|
||||||
|
action,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function* delete{{pascalCase name}}(action: StoreAction) {
|
||||||
|
yield call(
|
||||||
|
genericMutation({
|
||||||
|
objectReturn: '{{camelCase name}}sDeleteOne',
|
||||||
|
model: models.{{camelCase name}}Model,
|
||||||
|
inputs: {
|
||||||
|
id: action?.payload?.id,
|
||||||
|
},
|
||||||
|
actionRes: actions.delete,
|
||||||
|
}),
|
||||||
|
action,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function* {{camelCase name}}Watchers() {
|
||||||
|
yield all([
|
||||||
|
takeEvery(actions.list.request.constant, list),
|
||||||
|
takeEvery(actions.detail.request.constant, detail),
|
||||||
|
takeEvery(actions.add.request.constant, add),
|
||||||
|
takeEvery(actions.update.request.constant, update),
|
||||||
|
takeEvery(actions.delete.request.constant, delete{{pascalCase name}}),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function* saga() {
|
||||||
|
yield call({{camelCase name}}Watchers);
|
||||||
|
}
|
||||||
7
seed/plop-templates/resource/selectors.hbs
Normal file
7
seed/plop-templates/resource/selectors.hbs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { StoreState } from 'store/rootReducer';
|
||||||
|
|
||||||
|
const getList = (state: StoreState): string => state.{{ camelCase name }} && state.{{ camelCase name }}.list;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
getList,
|
||||||
|
};
|
||||||
285
seed/plopbase.js
Normal file
285
seed/plopbase.js
Normal file
@ -0,0 +1,285 @@
|
|||||||
|
module.exports = function(config) {
|
||||||
|
return function(plop) {
|
||||||
|
plop.setPrompt('directory', require('inquirer-directory'));
|
||||||
|
|
||||||
|
const genComponent = ({ targetPath, storyfile, componentfile, stylefile, testfile, isEmotion }) => {
|
||||||
|
const actions = [];
|
||||||
|
|
||||||
|
actions.push({
|
||||||
|
type: 'add',
|
||||||
|
path: `${targetPath}/{{pascalCase name}}/index.tsx`,
|
||||||
|
templateFile: config.getTemplatePath(componentfile),
|
||||||
|
});
|
||||||
|
if (isEmotion) {
|
||||||
|
actions.push({
|
||||||
|
type: 'add',
|
||||||
|
path: `${targetPath}/{{pascalCase name}}/{{pascalCase name}}.styled.ts`,
|
||||||
|
templateFile: config.getTemplatePath('component/styled.hbs'),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
stylefile = stylefile || 'component/componentStyle.hbs';
|
||||||
|
|
||||||
|
actions.push({
|
||||||
|
type: 'add',
|
||||||
|
path: `${targetPath}/{{pascalCase name}}/{{pascalCase name}}.module.scss`,
|
||||||
|
templateFile: config.getTemplatePath(stylefile),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (testfile) {
|
||||||
|
actions.push({
|
||||||
|
type: 'add',
|
||||||
|
path: `${targetPath}/{{pascalCase name}}/{{pascalCase name}}.test.tsx`,
|
||||||
|
templateFile: config.getTemplatePath(testfile),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (storyfile) {
|
||||||
|
actions.push({
|
||||||
|
type: 'add',
|
||||||
|
path: `${targetPath}/{{pascalCase name}}/{{pascalCase name}}.stories.tsx`,
|
||||||
|
templateFile: config.getTemplatePath(storyfile),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
};
|
||||||
|
|
||||||
|
const componentPrompts = () => {
|
||||||
|
const prompts = [];
|
||||||
|
|
||||||
|
prompts.push({
|
||||||
|
type: 'input',
|
||||||
|
name: 'name',
|
||||||
|
message: 'Component name please',
|
||||||
|
});
|
||||||
|
|
||||||
|
prompts.push({
|
||||||
|
type: 'directory',
|
||||||
|
name: 'path',
|
||||||
|
message: 'Where should the component go?',
|
||||||
|
basePath: config.componentPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
prompts.push({
|
||||||
|
type: 'checkbox',
|
||||||
|
name: 'options',
|
||||||
|
choices: [
|
||||||
|
{ name: 'redux', checked: true },
|
||||||
|
{ name: 'emotion', checked: false },
|
||||||
|
{ name: 'test', checked: true },
|
||||||
|
// { name: 'story', checked: true },
|
||||||
|
// { name: 'formItem', checked: false },
|
||||||
|
],
|
||||||
|
message: 'Select configuration options',
|
||||||
|
});
|
||||||
|
|
||||||
|
return prompts;
|
||||||
|
};
|
||||||
|
|
||||||
|
const genNormalComponent = data => {
|
||||||
|
const componentMapping = {
|
||||||
|
default: {
|
||||||
|
redux: 'component/componentHookRedux.hbs',
|
||||||
|
normal: 'component/componentHook.hbs',
|
||||||
|
},
|
||||||
|
emotion: {
|
||||||
|
redux: 'component/componentStyledRedux.hbs',
|
||||||
|
normal: 'component/componentStyled.hbs',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const isEmotion = data.options.includes('emotion');
|
||||||
|
const componentfile = componentMapping[isEmotion ? 'emotion' : 'default'][data.options.includes('redux') ? 'redux' : 'normal'];
|
||||||
|
|
||||||
|
// let storyfile = data.options.includes('story') ? 'component/story.hbs' : null;
|
||||||
|
// storyfile = data.options.includes('formItem') ? 'component/story-form.hbs' : storyfile;
|
||||||
|
|
||||||
|
const testfile = data.options.includes('test') && 'component/componentTest.hbs';
|
||||||
|
const targetPath = config.path.join(config.componentPath, data.path);
|
||||||
|
|
||||||
|
return genComponent({
|
||||||
|
componentfile,
|
||||||
|
targetPath,
|
||||||
|
// storyfile,
|
||||||
|
testfile,
|
||||||
|
isEmotion,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const componentFormPrompts = () => {
|
||||||
|
const prompts = [];
|
||||||
|
|
||||||
|
prompts.push({
|
||||||
|
type: 'input',
|
||||||
|
name: 'name',
|
||||||
|
message: 'Component name please',
|
||||||
|
});
|
||||||
|
|
||||||
|
return prompts;
|
||||||
|
};
|
||||||
|
|
||||||
|
const genFormComponent = data => {
|
||||||
|
const componentfile = 'component/componentForm.hbs';
|
||||||
|
const targetPath = config.path.join(config.componentPath, '/forms');
|
||||||
|
|
||||||
|
data.name += 'Form';
|
||||||
|
|
||||||
|
const actions = genComponent({ targetPath, componentfile });
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resource
|
||||||
|
*/
|
||||||
|
const resourcePrompts = () => {
|
||||||
|
const prompts = [];
|
||||||
|
|
||||||
|
prompts.push({
|
||||||
|
type: 'input',
|
||||||
|
name: 'name',
|
||||||
|
message: 'Resource name please',
|
||||||
|
});
|
||||||
|
|
||||||
|
prompts.push({
|
||||||
|
type: 'checkbox',
|
||||||
|
name: 'options',
|
||||||
|
choices: [{ name: 'apollo', checked: true }],
|
||||||
|
message: 'Select configuration options',
|
||||||
|
});
|
||||||
|
|
||||||
|
return prompts;
|
||||||
|
};
|
||||||
|
|
||||||
|
const genResource = data => {
|
||||||
|
const apollo = data.options.includes('apollo');
|
||||||
|
|
||||||
|
const constantsFile = 'resource/constants.hbs';
|
||||||
|
const actionsFile = 'resource/actions.hbs';
|
||||||
|
const reducerFile = 'resource/reducer.hbs';
|
||||||
|
|
||||||
|
const sagasFile = 'resource/sagas.hbs';
|
||||||
|
const apisFile = 'resource/apis.hbs';
|
||||||
|
|
||||||
|
const sagasApolloFile = 'resource/sagasApollo.hbs';
|
||||||
|
const modelsFile = 'resource/models.hbs';
|
||||||
|
|
||||||
|
const selectorsFile = 'resource/selectors.hbs';
|
||||||
|
|
||||||
|
const indexFile = 'resource/index.hbs';
|
||||||
|
const indexApolloFile = 'resource/indexApollo.hbs';
|
||||||
|
|
||||||
|
const actions = [];
|
||||||
|
// data name
|
||||||
|
actions.push({
|
||||||
|
type: 'add',
|
||||||
|
path: `${config.storePath}/{{camelCase name}}/constants.ts`,
|
||||||
|
templateFile: config.getTemplatePath(constantsFile),
|
||||||
|
});
|
||||||
|
actions.push({
|
||||||
|
type: 'add',
|
||||||
|
path: `${config.storePath}/{{camelCase name}}/actions.ts`,
|
||||||
|
templateFile: config.getTemplatePath(actionsFile),
|
||||||
|
});
|
||||||
|
actions.push({
|
||||||
|
type: 'add',
|
||||||
|
path: `${config.storePath}/{{camelCase name}}/reducer.ts`,
|
||||||
|
templateFile: config.getTemplatePath(reducerFile),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (apollo) {
|
||||||
|
actions.push({
|
||||||
|
type: 'add',
|
||||||
|
path: `${config.storePath}/{{camelCase name}}/models.ts`,
|
||||||
|
templateFile: config.getTemplatePath(modelsFile),
|
||||||
|
});
|
||||||
|
actions.push({
|
||||||
|
type: 'add',
|
||||||
|
path: `${config.storePath}/{{camelCase name}}/sagas.ts`,
|
||||||
|
templateFile: config.getTemplatePath(sagasApolloFile),
|
||||||
|
});
|
||||||
|
actions.push({
|
||||||
|
type: 'add',
|
||||||
|
path: `${config.storePath}/{{camelCase name}}/index.ts`,
|
||||||
|
templateFile: config.getTemplatePath(indexApolloFile),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
actions.push({
|
||||||
|
type: 'add',
|
||||||
|
path: `${config.storePath}/{{camelCase name}}/sagas.ts`,
|
||||||
|
templateFile: config.getTemplatePath(sagasFile),
|
||||||
|
});
|
||||||
|
actions.push({
|
||||||
|
type: 'add',
|
||||||
|
path: `${config.storePath}/{{camelCase name}}/apis.ts`,
|
||||||
|
templateFile: config.getTemplatePath(apisFile),
|
||||||
|
});
|
||||||
|
actions.push({
|
||||||
|
type: 'add',
|
||||||
|
path: `${config.storePath}/{{camelCase name}}/index.ts`,
|
||||||
|
templateFile: config.getTemplatePath(indexFile),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
actions.push({
|
||||||
|
type: 'add',
|
||||||
|
path: `${config.storePath}/{{camelCase name}}/selectors.ts`,
|
||||||
|
templateFile: config.getTemplatePath(selectorsFile),
|
||||||
|
});
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Custom hook
|
||||||
|
*/
|
||||||
|
|
||||||
|
const hookPromps = () => {
|
||||||
|
const prompts = [];
|
||||||
|
|
||||||
|
prompts.push({
|
||||||
|
type: 'input',
|
||||||
|
name: 'name',
|
||||||
|
message: 'custom Hook name please',
|
||||||
|
});
|
||||||
|
|
||||||
|
return prompts;
|
||||||
|
};
|
||||||
|
|
||||||
|
const genHook = data => {
|
||||||
|
const hookFile = 'component/customHook.hbs';
|
||||||
|
|
||||||
|
const hookPath = config.path.join(config.componentPath, '/hooks');
|
||||||
|
|
||||||
|
const actions = [];
|
||||||
|
actions.push({
|
||||||
|
type: 'add',
|
||||||
|
path: `${hookPath}/{{camelCase name}}.ts`,
|
||||||
|
templateFile: config.getTemplatePath(hookFile),
|
||||||
|
});
|
||||||
|
return actions;
|
||||||
|
};
|
||||||
|
|
||||||
|
plop.setGenerator('Component', {
|
||||||
|
description: 'Make a component',
|
||||||
|
prompts: componentPrompts(),
|
||||||
|
actions: genNormalComponent,
|
||||||
|
});
|
||||||
|
|
||||||
|
plop.setGenerator('Component Form', {
|
||||||
|
description: 'Make a form component',
|
||||||
|
prompts: componentFormPrompts(),
|
||||||
|
actions: genFormComponent,
|
||||||
|
});
|
||||||
|
|
||||||
|
plop.setGenerator('Resource', {
|
||||||
|
description: 'Make a resource in the store (action, reducer & saga)',
|
||||||
|
prompts: resourcePrompts(),
|
||||||
|
actions: genResource,
|
||||||
|
});
|
||||||
|
|
||||||
|
plop.setGenerator('Hook', {
|
||||||
|
description: 'Make a custom reusable hookc',
|
||||||
|
prompts: hookPromps(),
|
||||||
|
actions: genHook,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
17
seed/plopfile.js
Normal file
17
seed/plopfile.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
/* eslint-disable import/no-extraneous-dependencies */
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const templateDir = 'plop-templates';
|
||||||
|
|
||||||
|
const getTemplatePath = filename => path.join(templateDir, filename);
|
||||||
|
const componentPath = 'src/components';
|
||||||
|
const storePath = 'src/store';
|
||||||
|
|
||||||
|
const plopBase = require('./plopbase');
|
||||||
|
|
||||||
|
module.exports = plopBase({
|
||||||
|
getTemplatePath,
|
||||||
|
path,
|
||||||
|
storePath,
|
||||||
|
componentPath,
|
||||||
|
});
|
||||||
10
seed/postcss.config.js
Normal file
10
seed/postcss.config.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: [
|
||||||
|
[
|
||||||
|
'postcss-preset-env',
|
||||||
|
{
|
||||||
|
// Options
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
};
|
||||||
110
seed/scripts/build.js
Normal file
110
seed/scripts/build.js
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
const webpack = require('webpack');
|
||||||
|
const rimraf = require('rimraf');
|
||||||
|
const { choosePort } = require('react-dev-utils/WebpackDevServerUtils');
|
||||||
|
|
||||||
|
const clientConfigBuilder = require('../config/webpack/ssr/client.js');
|
||||||
|
const serverConfigBuilder = require('../config/webpack/ssr/server.js');
|
||||||
|
|
||||||
|
const paths = require('../config/paths');
|
||||||
|
const { logMessage, compilerPromise, sleep } = require('./utils');
|
||||||
|
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
|
||||||
|
// const generateStaticHTML = async () => {
|
||||||
|
// const nodemon = require('nodemon');
|
||||||
|
// const fs = require('fs');
|
||||||
|
// const puppeteer = require('puppeteer');
|
||||||
|
// const port = await choosePort('localhost', 8505);
|
||||||
|
|
||||||
|
// process.env.PORT = port;
|
||||||
|
|
||||||
|
// const script = nodemon({
|
||||||
|
// script: `${paths.serverBuild}/server.js`,
|
||||||
|
// ignore: ['*'],
|
||||||
|
// });
|
||||||
|
|
||||||
|
// script.on('start', async () => {
|
||||||
|
// try {
|
||||||
|
// await sleep(2000);
|
||||||
|
// const browser = await puppeteer.launch();
|
||||||
|
// const page = await browser.newPage();
|
||||||
|
// await page.goto(`http://localhost:${port}`);
|
||||||
|
// const pageContent = await page.content();
|
||||||
|
// fs.writeFileSync(`${paths.clientBuild}/index.html`, pageContent);
|
||||||
|
// await browser.close();
|
||||||
|
// script.emit('quit');
|
||||||
|
// } catch (err) {
|
||||||
|
// script.emit('quit');
|
||||||
|
// console.log(err);
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
// script.on('exit', code => {
|
||||||
|
// process.exit(code);
|
||||||
|
// });
|
||||||
|
|
||||||
|
// script.on('crash', () => {
|
||||||
|
// process.exit(1);
|
||||||
|
// });
|
||||||
|
// };
|
||||||
|
|
||||||
|
const build = async () => {
|
||||||
|
rimraf.sync(paths.clientBuild);
|
||||||
|
rimraf.sync(paths.serverBuild);
|
||||||
|
|
||||||
|
let generateStatic = false;
|
||||||
|
let staging = false;
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i];
|
||||||
|
if (arg === 'static' || arg === '-static') generateStatic = true;
|
||||||
|
|
||||||
|
if (arg === 'staging' || arg === '-staging') staging = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const clientConfig = clientConfigBuilder({
|
||||||
|
prod: true,
|
||||||
|
staging,
|
||||||
|
});
|
||||||
|
const serverConfig = serverConfigBuilder({
|
||||||
|
prod: true,
|
||||||
|
staging,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Combine (as for dev)
|
||||||
|
const multiCompiler = webpack([clientConfig, serverConfig]);
|
||||||
|
|
||||||
|
const clientCompiler = multiCompiler.compilers.find(compiler => compiler.name === 'client');
|
||||||
|
const serverCompiler = multiCompiler.compilers.find(compiler => compiler.name === 'server');
|
||||||
|
|
||||||
|
const clientPromise = compilerPromise('client', clientCompiler);
|
||||||
|
const serverPromise = compilerPromise('server', serverCompiler);
|
||||||
|
|
||||||
|
serverCompiler.watch({}, (error, stats) => {
|
||||||
|
if (!error && !stats.hasErrors()) {
|
||||||
|
console.log(stats.toString(serverConfig.stats));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
clientCompiler.watch({}, (error, stats) => {
|
||||||
|
if (!error && !stats.hasErrors()) {
|
||||||
|
console.log(stats.toString(clientConfig.stats));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// wait until client and server is compiled
|
||||||
|
try {
|
||||||
|
await serverPromise;
|
||||||
|
await clientPromise;
|
||||||
|
// not very necessary
|
||||||
|
|
||||||
|
if (generateStatic) {
|
||||||
|
// await generateStaticHTML();
|
||||||
|
}
|
||||||
|
logMessage('Done!', 'info');
|
||||||
|
process.exit();
|
||||||
|
} catch (error) {
|
||||||
|
logMessage(error, 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
build();
|
||||||
95
seed/scripts/sls-deploy.js
Normal file
95
seed/scripts/sls-deploy.js
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
const execa = require('execa');
|
||||||
|
const Listr = require('listr');
|
||||||
|
const package = require('../../package.json');
|
||||||
|
|
||||||
|
// Arguments checks
|
||||||
|
|
||||||
|
const stage = process.argv[2] || 'dev';
|
||||||
|
|
||||||
|
/* eslint-disable */
|
||||||
|
if (!package.deploy) throw 'no deploy config in parent package.json';
|
||||||
|
if (!package.deploy.awsProfile) throw 'no deploy awsProfile in parent package.json';
|
||||||
|
if (!package.deploy[stage]) throw `no deploy config for this stage ${stage} in parent package.json`;
|
||||||
|
if (!package.deploy[stage].bucket || package.deploy[stage].bucket === '') throw `no BUCKET config for this stage ${stage} in parent package.json`;
|
||||||
|
/* eslint-enable */
|
||||||
|
|
||||||
|
const awsProfile = package.deploy.awsProfile;
|
||||||
|
const bucketUrl = package.deploy[stage].bucket;
|
||||||
|
const distributionId = package.deploy[stage].id;
|
||||||
|
|
||||||
|
const deployBase = function(basePath) {
|
||||||
|
return new Listr(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
title: 'Build client-ssr',
|
||||||
|
task: async () => {
|
||||||
|
const { stdout } = await execa('webpack', ['--environment', stage, '--config', 'config/webpack/ssr/slsClient.js'], {
|
||||||
|
all: true,
|
||||||
|
});
|
||||||
|
console.log(stdout);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// title: 'Copy manifest to server-ssr',
|
||||||
|
// task: () =>
|
||||||
|
// execa('cp', [
|
||||||
|
// 'sls/client/loadable-stats.json',
|
||||||
|
// 'src/server/loadable-stats.json',
|
||||||
|
// ]).then(result => {}),
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
title: 'Deploy client',
|
||||||
|
task: async () => {
|
||||||
|
const { stdout } = await execa(
|
||||||
|
'aws',
|
||||||
|
['s3', 'sync', './sls/client', bucketUrl, '--profile', awsProfile, '--sse', '--delete', '--cache-control', 'max-age=31536000,public'],
|
||||||
|
{
|
||||||
|
all: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
console.log(stdout);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Deploy server',
|
||||||
|
task: async () => {
|
||||||
|
const { stdout } = await execa('sls', ['deploy', '--config', './serverless.yaml', '--stage', stage, '--aws-profile', awsProfile], {
|
||||||
|
all: true,
|
||||||
|
});
|
||||||
|
console.log(stdout);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// title: 'Invalidate cache',
|
||||||
|
// task: async () => {
|
||||||
|
// const { stdout } = await execa(
|
||||||
|
// 'aws',
|
||||||
|
// [
|
||||||
|
// 'cloudfront',
|
||||||
|
// '--aws-profile',
|
||||||
|
// awsProfile,
|
||||||
|
// 'create-invalidation',
|
||||||
|
// '--distribution-id',
|
||||||
|
// distributionId,
|
||||||
|
// '--path',
|
||||||
|
// '"/*"',
|
||||||
|
// ],
|
||||||
|
// {
|
||||||
|
// all: true,
|
||||||
|
// },
|
||||||
|
// );
|
||||||
|
// console.log(stdout);
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
],
|
||||||
|
{ concurrent: false },
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
deployBase('')
|
||||||
|
.run()
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
127
seed/scripts/start.js
Normal file
127
seed/scripts/start.js
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
const webpack = require('webpack');
|
||||||
|
const nodemon = require('nodemon');
|
||||||
|
const rimraf = require('rimraf');
|
||||||
|
// webpack-dev-middleware is a wrapper that will emit files processed by webpack to a server. This is used in webpack-dev-server internally, however it's available as a separate package to allow more custom setups if desired.
|
||||||
|
const webpackDevMiddleware = require('webpack-dev-middleware');
|
||||||
|
const webpackHotMiddleware = require('webpack-hot-middleware');
|
||||||
|
const express = require('express');
|
||||||
|
|
||||||
|
const clientConfig = require('../config/webpack/ssr/client.js')();
|
||||||
|
const serverConfig = require('../config/webpack/ssr/server.js')();
|
||||||
|
|
||||||
|
const paths = require('../config/paths');
|
||||||
|
const { logMessage, compilerPromise } = require('./utils');
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
// Example commands :
|
||||||
|
// cross-env NODE_ENV=development PORT=5000 node scripts/start.js
|
||||||
|
|
||||||
|
const WEBPACK_PORT = process.env.WEBPACK_PORT || (!isNaN(Number(process.env.PORT)) ? Number(process.env.PORT) + 1 : 8501);
|
||||||
|
|
||||||
|
const start = async () => {
|
||||||
|
// Remove folders
|
||||||
|
rimraf.sync(paths.clientBuild);
|
||||||
|
rimraf.sync(paths.serverBuild);
|
||||||
|
|
||||||
|
// Add the client which connects to our middleware
|
||||||
|
// You can use full urls like 'webpack-hot-middleware/client?path=http://localhost:3000/__webpack_hmr'
|
||||||
|
// useful if you run your app from another point like django
|
||||||
|
clientConfig.entry.bundle = [`webpack-hot-middleware/client?path=http://localhost:${WEBPACK_PORT}/__webpack_hmr`, ...clientConfig.entry.bundle];
|
||||||
|
|
||||||
|
// Customize the main hot update filename. create an history in the client output folder
|
||||||
|
clientConfig.output.hotUpdateMainFilename = 'updates/[hash].hot-update.json';
|
||||||
|
clientConfig.output.hotUpdateChunkFilename = 'updates/[id].[hash].hot-update.js';
|
||||||
|
|
||||||
|
// Allow hot reload (if path was relative, no need to add those)
|
||||||
|
const publicPath = clientConfig.output.publicPath;
|
||||||
|
|
||||||
|
clientConfig.output.publicPath = [`http://localhost:${WEBPACK_PORT}`, publicPath].join('/').replace(/([^:+])\/+/g, '$1/');
|
||||||
|
|
||||||
|
serverConfig.output.publicPath = [`http://localhost:${WEBPACK_PORT}`, publicPath].join('/').replace(/([^:+])\/+/g, '$1/');
|
||||||
|
|
||||||
|
// Combine compilers
|
||||||
|
const multiCompiler = webpack([clientConfig, serverConfig]);
|
||||||
|
|
||||||
|
const clientCompiler = multiCompiler.compilers.find(compiler => compiler.name === 'client');
|
||||||
|
const serverCompiler = multiCompiler.compilers.find(compiler => compiler.name === 'server');
|
||||||
|
|
||||||
|
// Make sure to know when the compilation is done
|
||||||
|
const clientPromise = compilerPromise('client', clientCompiler);
|
||||||
|
const serverPromise = compilerPromise('server', serverCompiler);
|
||||||
|
|
||||||
|
// Stats need be to explored
|
||||||
|
const watchOptions = {
|
||||||
|
// poll: true,
|
||||||
|
ignored: /node_modules/,
|
||||||
|
stats: clientConfig.stats,
|
||||||
|
};
|
||||||
|
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
res.header('Access-Control-Allow-Origin', '*');
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use(
|
||||||
|
// Tell express to use the webpack-dev-middleware and use the webpack.config.js
|
||||||
|
// configuration file as a base.
|
||||||
|
webpackDevMiddleware(clientCompiler, {
|
||||||
|
publicPath: clientConfig.output.publicPath,
|
||||||
|
stats: clientConfig.stats,
|
||||||
|
watchOptions,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
app.use(webpackHotMiddleware(clientCompiler));
|
||||||
|
|
||||||
|
app.listen(WEBPACK_PORT);
|
||||||
|
|
||||||
|
// Here we use simply the watch of webpack
|
||||||
|
serverCompiler.watch(watchOptions, (error, stats) => {
|
||||||
|
if (!error && !stats.hasErrors()) {
|
||||||
|
console.log(stats.toString(serverConfig.stats));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
logMessage(error, 'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stats.hasErrors()) {
|
||||||
|
const info = stats.toJson();
|
||||||
|
const errors = info.errors[0].split('\n');
|
||||||
|
logMessage(errors[0], 'error');
|
||||||
|
logMessage(errors[1], 'error');
|
||||||
|
logMessage(errors[2], 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// wait until client and server is compiled
|
||||||
|
try {
|
||||||
|
await serverPromise;
|
||||||
|
await clientPromise;
|
||||||
|
} catch (error) {
|
||||||
|
logMessage(error, 'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
const script = nodemon({
|
||||||
|
script: `${paths.serverBuild}/server.js`,
|
||||||
|
ignore: ['src', 'scripts', 'config', './*.*', 'build/client'],
|
||||||
|
});
|
||||||
|
|
||||||
|
script.on('restart', () => {
|
||||||
|
logMessage('Server side app has been restarted.', 'warning');
|
||||||
|
});
|
||||||
|
|
||||||
|
script.on('quit', () => {
|
||||||
|
console.log('Process ended');
|
||||||
|
process.exit();
|
||||||
|
});
|
||||||
|
|
||||||
|
script.on('error', () => {
|
||||||
|
logMessage('An error occured. Exiting', 'error');
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
start();
|
||||||
29
seed/scripts/utils.js
Normal file
29
seed/scripts/utils.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
const chalk = require('chalk');
|
||||||
|
|
||||||
|
const logMessage = (message, level = 'info') => {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
const color = level === 'error' ? 'red' : level === 'warning' ? 'yellow' : 'white';
|
||||||
|
console.log(`[${new Date().toISOString()}]`, chalk[color](message));
|
||||||
|
};
|
||||||
|
|
||||||
|
const compilerPromise = (name, compiler) =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
compiler.hooks.compile.tap(name, () => {
|
||||||
|
logMessage(`[${name}] Compiling `);
|
||||||
|
});
|
||||||
|
compiler.hooks.done.tap(name, stats => {
|
||||||
|
if (!stats.hasErrors()) {
|
||||||
|
return resolve();
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line
|
||||||
|
return reject(`Failed to compile ${name}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
compilerPromise,
|
||||||
|
logMessage,
|
||||||
|
sleep,
|
||||||
|
};
|
||||||
3
seed/serverless.js
Normal file
3
seed/serverless.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
const handler = require('./src/server/serverless').default;
|
||||||
|
|
||||||
|
export default handler;
|
||||||
25
seed/serverless.yaml
Normal file
25
seed/serverless.yaml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
app: ${file(../package.json):name}
|
||||||
|
service: frontend
|
||||||
|
provider:
|
||||||
|
name: aws
|
||||||
|
stage: ${opt:stage,'dev'}
|
||||||
|
runtime: nodejs12.x
|
||||||
|
region: eu-central-1
|
||||||
|
environment: ${self:custom.environment}
|
||||||
|
|
||||||
|
plugins:
|
||||||
|
- serverless-webpack
|
||||||
|
- serverless-offline
|
||||||
|
custom:
|
||||||
|
stage: ${opt:stage, self:provider.stage}
|
||||||
|
webpack:
|
||||||
|
webpackConfig: ./config/webpack/ssr/slsServer.js
|
||||||
|
|
||||||
|
functions:
|
||||||
|
frontend:
|
||||||
|
handler: serverless.default
|
||||||
|
memorySize: 2048 # optional, in MB, default is 1024
|
||||||
|
timeout: 20 # optional, in seconds, default is 6
|
||||||
|
events:
|
||||||
|
- http: GET {proxy+}
|
||||||
|
- http: GET /
|
||||||
35
seed/src/appComponents/ClientApp.tsx
Normal file
35
seed/src/appComponents/ClientApp.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import { ConnectedRouter } from 'connected-react-router';
|
||||||
|
import Main from 'components/layouts/Main';
|
||||||
|
import { ApolloProvider } from '@apollo/client';
|
||||||
|
import { IconContext } from 'react-icons';
|
||||||
|
import { DndProvider } from 'react-dnd';
|
||||||
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||||
|
|
||||||
|
import config from 'config/general';
|
||||||
|
import client from 'store/rootApollo';
|
||||||
|
|
||||||
|
// Default app used without SSR
|
||||||
|
const ClientApp = props => {
|
||||||
|
const { store, history } = props;
|
||||||
|
|
||||||
|
const renderApp = () => {
|
||||||
|
return (
|
||||||
|
<Provider store={store}>
|
||||||
|
<DndProvider backend={HTML5Backend}>
|
||||||
|
<ConnectedRouter history={history}>
|
||||||
|
<IconContext.Provider value={{ style: { verticalAlign: 'middle' } }}>
|
||||||
|
<Main />
|
||||||
|
</IconContext.Provider>
|
||||||
|
</ConnectedRouter>
|
||||||
|
</DndProvider>
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!config.apollo) return renderApp();
|
||||||
|
return <ApolloProvider client={client.getInstance()}>{renderApp()}</ApolloProvider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ClientApp;
|
||||||
36
seed/src/appComponents/ServerApp.tsx
Normal file
36
seed/src/appComponents/ServerApp.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { StaticRouter } from 'react-router';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import Main from 'components/layouts/Main';
|
||||||
|
import { ApolloProvider } from '@apollo/client';
|
||||||
|
import { IconContext } from 'react-icons';
|
||||||
|
import { DndProvider } from 'react-dnd';
|
||||||
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||||
|
|
||||||
|
import config from 'config/general';
|
||||||
|
import client from 'store/rootApollo';
|
||||||
|
|
||||||
|
// Static server app (to have prefilled html)
|
||||||
|
const ServerApp = props => {
|
||||||
|
// context here is req from server
|
||||||
|
const { store, location, context } = props;
|
||||||
|
|
||||||
|
const renderApp = () => {
|
||||||
|
return (
|
||||||
|
<Provider store={store}>
|
||||||
|
<DndProvider backend={HTML5Backend}>
|
||||||
|
<StaticRouter context={context || {}} location={location}>
|
||||||
|
<IconContext.Provider value={{ style: { verticalAlign: 'middle' } }}>
|
||||||
|
<Main server />
|
||||||
|
</IconContext.Provider>
|
||||||
|
</StaticRouter>
|
||||||
|
</DndProvider>
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!config.apollo) return renderApp();
|
||||||
|
return <ApolloProvider client={client}>{renderApp()}</ApolloProvider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ServerApp;
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { storiesOf } from '@storybook/react';
|
||||||
|
import { text, object, color, dom, array } from '@storybook/addon-knobs';
|
||||||
|
|
||||||
|
import CustomArticle from './index';
|
||||||
|
|
||||||
|
storiesOf('newComponents/CustomArticle', module).add('Default', () => <CustomArticle />);
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render, screen } from 'utils/test-utils';
|
||||||
|
import CustomArticle from './index';
|
||||||
|
|
||||||
|
describe('CustomArticle', () => {
|
||||||
|
it('should render without crashing', () => {
|
||||||
|
render(<CustomArticle />);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
@import '~styles/mixins';
|
||||||
|
|
||||||
|
.custom-article {
|
||||||
|
}
|
||||||
101
seed/src/components/enhancers/CustomArticle/index.tsx
Normal file
101
seed/src/components/enhancers/CustomArticle/index.tsx
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
// Redux part
|
||||||
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
|
import { StoreState } from 'store/rootReducer';
|
||||||
|
|
||||||
|
// Import actions here
|
||||||
|
|
||||||
|
import classNames from 'classnames/bind';
|
||||||
|
import TextItem from 'components/items/TextItem';
|
||||||
|
import parse from 'html-react-parser';
|
||||||
|
import { withRouter } from 'react-router-dom';
|
||||||
|
import article from 'store/article';
|
||||||
|
import config from 'config/general';
|
||||||
|
import styleIdentifiers from './CustomArticle.module.scss';
|
||||||
|
|
||||||
|
const styles = classNames.bind(styleIdentifiers);
|
||||||
|
|
||||||
|
export interface CustomArticleProps {}
|
||||||
|
|
||||||
|
const CustomArticle = (props: CustomArticleProps) => {
|
||||||
|
const {
|
||||||
|
nodeItem,
|
||||||
|
wrapperComponent,
|
||||||
|
className,
|
||||||
|
wrapperClassName,
|
||||||
|
containerClassName,
|
||||||
|
paramKey,
|
||||||
|
customNode,
|
||||||
|
match: { params },
|
||||||
|
...rest
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
// mapStateToProps
|
||||||
|
const lg = useSelector((state: StoreState) => state.content.lg);
|
||||||
|
const detail = useSelector((state: StoreState) => state.article.detail?.data);
|
||||||
|
// Allow to dispatch actions
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const loadArticle = data => dispatch(article.actions.getOneArticle.request.action(data));
|
||||||
|
|
||||||
|
function removeStyle(toRemove) {
|
||||||
|
for (let index = 0; index < toRemove.length; index++) {
|
||||||
|
const element = toRemove[index];
|
||||||
|
if (element && element.attribs && element.attribs.style) {
|
||||||
|
element.attribs.style = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return toRemove;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderContent(content): JSX {
|
||||||
|
const newContent = content;
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
replace: (domNode): JSX => {
|
||||||
|
if (nodeItem && customNode && customNode.includes(domNode.name)) {
|
||||||
|
const NodeItem = nodeItem;
|
||||||
|
return <NodeItem {...domNode} />;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return parse(newContent, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (detail?.urls?.[lg] !== params[paramKey || 'url'] || detail?.urls?.[config.defaultLanguage] !== params[paramKey || 'url']) {
|
||||||
|
loadArticle({
|
||||||
|
data: { url: params[paramKey], lg },
|
||||||
|
noFeedback: true,
|
||||||
|
callbackError: () => {
|
||||||
|
loadArticle({ data: { url: params[paramKey], lg: config.defaultLanguage }, noFeedback: true });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [params]);
|
||||||
|
|
||||||
|
const WrapperComponent = wrapperComponent;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles('custom-article', containerClassName)}>
|
||||||
|
<div className={styles('CustomArticle', wrapperClassName)}>
|
||||||
|
{WrapperComponent ? (
|
||||||
|
<WrapperComponent item={detail} {...rest}>
|
||||||
|
<div className={styles('content-artice', className)}>
|
||||||
|
{detail ? renderContent(detail.content[lg] || detail.content[config.defaultLanguage]) : <div>Loading ...</div>}
|
||||||
|
</div>
|
||||||
|
</WrapperComponent>
|
||||||
|
) : (
|
||||||
|
<div className={styles('content-artice', className)}>
|
||||||
|
{detail ? renderContent(detail.content[lg] || detail.content[config.defaultLanguage]) : <div>Loading ...</div>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withRouter(CustomArticle);
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { storiesOf } from '@storybook/react';
|
||||||
|
import { text, object, color, dom, array } from '@storybook/addon-knobs';
|
||||||
|
|
||||||
|
import CustomDropdown from './CustomDropdown';
|
||||||
|
|
||||||
|
storiesOf('newComponents/CustomDropdown', module).add('Default', (): JSX => <CustomDropdown />);
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render, screen } from 'utils/test-utils';
|
||||||
|
import CustomDropdown from './CustomDropdown';
|
||||||
|
|
||||||
|
describe('CustomDropdown', (): void => {
|
||||||
|
it('should render without crashing', (): void => {
|
||||||
|
render(<CustomDropdown />);
|
||||||
|
});
|
||||||
|
});
|
||||||
204
seed/src/components/enhancers/CustomDropdown/CustomDropdown.tsx
Normal file
204
seed/src/components/enhancers/CustomDropdown/CustomDropdown.tsx
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import classNames from 'classnames/bind';
|
||||||
|
import { mobileAndTabletCheck } from 'store/utils/helper';
|
||||||
|
import styleIdentifiers from './customDropdown.scss';
|
||||||
|
|
||||||
|
const styles = classNames.bind(styleIdentifiers);
|
||||||
|
|
||||||
|
export interface StateProps {}
|
||||||
|
|
||||||
|
export interface DispatchProps {}
|
||||||
|
|
||||||
|
export interface OwnProps {
|
||||||
|
decal?: boolean;
|
||||||
|
active: boolean;
|
||||||
|
forceClose: boolean;
|
||||||
|
toggleContent: boolean;
|
||||||
|
actionContent: Function;
|
||||||
|
action: Function;
|
||||||
|
label: React.Node;
|
||||||
|
item: React.Node;
|
||||||
|
arrowColor: string;
|
||||||
|
className: string;
|
||||||
|
direction: string;
|
||||||
|
arrowStyle: {} | string;
|
||||||
|
dropdownContentStyle: string;
|
||||||
|
children: React.Node;
|
||||||
|
selectorClass?: string;
|
||||||
|
contentClass?: string;
|
||||||
|
activeClass?: string;
|
||||||
|
activeClassName?: string;
|
||||||
|
contentClassName?: string;
|
||||||
|
selectorClassName?: string;
|
||||||
|
selectorContentClassName?: string;
|
||||||
|
noArrow: boolean;
|
||||||
|
hover: boolean;
|
||||||
|
spacing: number;
|
||||||
|
fixedMobile?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CustomDropdownProps = StateProps & DispatchProps & OwnProps;
|
||||||
|
|
||||||
|
interface CustomDropdownState {
|
||||||
|
dropdownActive: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CustomDropdown(ItemComponent: React.ComponentType<any>): JSX {
|
||||||
|
class DropdownContainer extends React.Component<CustomDropdownProps, CustomDropdownState> {
|
||||||
|
constructor(props: CustomDropdownProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
const { active } = this.props;
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
dropdownActive: active || false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount = (): void => {
|
||||||
|
document.addEventListener('mouseup', this.handleClickOutside);
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidUpdate = (prevProps: CustomDropdownProps): void => {
|
||||||
|
const { forceClose } = this.props;
|
||||||
|
|
||||||
|
if (forceClose && !prevProps.forceClose) {
|
||||||
|
this.toggleClick(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
componentWillUnmount = (): void => {
|
||||||
|
document.removeEventListener('mouseup', this.handleClickOutside);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the wrapper ref
|
||||||
|
*/
|
||||||
|
setWrapperRef = (node: HTMLElement): void => {
|
||||||
|
this.wrapperRef = node;
|
||||||
|
};
|
||||||
|
|
||||||
|
toggleClick = (forcedValue: boolean): void => {
|
||||||
|
const { action } = this.props;
|
||||||
|
const { dropdownActive } = this.state;
|
||||||
|
|
||||||
|
if (forcedValue !== undefined && forcedValue === dropdownActive) return;
|
||||||
|
|
||||||
|
if (!dropdownActive && action) {
|
||||||
|
action();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
dropdownActive: (forcedValue !== undefined && forcedValue) || !dropdownActive,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
actionContent = (): void => {
|
||||||
|
const { toggleContent, actionContent } = this.props;
|
||||||
|
const { dropdownActive } = this.state;
|
||||||
|
|
||||||
|
if (toggleContent) {
|
||||||
|
this.setState({ dropdownActive: !dropdownActive });
|
||||||
|
}
|
||||||
|
if (actionContent) {
|
||||||
|
actionContent();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alert if clicked on outside of element
|
||||||
|
*/
|
||||||
|
handleClickOutside = (event: MouseEvent): void => {
|
||||||
|
const { target } = event;
|
||||||
|
const { dropdownActive } = this.state;
|
||||||
|
if (target instanceof HTMLElement) {
|
||||||
|
if (dropdownActive && this.wrapperRef && !this.wrapperRef.contains(target)) {
|
||||||
|
this.setState({ dropdownActive: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
itemButton = (): void => {
|
||||||
|
const { label, item } = this.props;
|
||||||
|
if (label) {
|
||||||
|
return <span>{label}</span>;
|
||||||
|
}
|
||||||
|
if (item) {
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
render(): JSX {
|
||||||
|
// TODO: rename class prop, shouldn't use the reserved keyword
|
||||||
|
const {
|
||||||
|
className,
|
||||||
|
direction,
|
||||||
|
arrowColor,
|
||||||
|
decal,
|
||||||
|
arrowStyle,
|
||||||
|
selectorClass,
|
||||||
|
selectorClassName,
|
||||||
|
selectorContentClassName,
|
||||||
|
contentClass,
|
||||||
|
contentClassName,
|
||||||
|
dropdownContentStyle,
|
||||||
|
children,
|
||||||
|
noArrow,
|
||||||
|
activeClass,
|
||||||
|
activeClassName,
|
||||||
|
hover,
|
||||||
|
spacing,
|
||||||
|
center,
|
||||||
|
fixedMobile,
|
||||||
|
} = this.props;
|
||||||
|
const { dropdownActive } = this.state;
|
||||||
|
|
||||||
|
const mobile = typeof navigator === 'object' && mobileAndTabletCheck();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles(
|
||||||
|
'CustomDropdown',
|
||||||
|
className,
|
||||||
|
decal && 'decal',
|
||||||
|
fixedMobile && 'fixed-mobile',
|
||||||
|
noArrow && 'no-triangle',
|
||||||
|
direction || 'bottom',
|
||||||
|
)}
|
||||||
|
onMouseEnter={(): void => hover && !mobile && this.toggleClick(true)}
|
||||||
|
onMouseLeave={(): void => hover && !mobile && this.toggleClick(false)}
|
||||||
|
ref={this.setWrapperRef}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={styles(
|
||||||
|
'dropdown-selector',
|
||||||
|
dropdownActive && 'active',
|
||||||
|
dropdownActive && activeClass,
|
||||||
|
dropdownActive && activeClassName,
|
||||||
|
selectorClass,
|
||||||
|
selectorClassName,
|
||||||
|
)}
|
||||||
|
style={spacing && { marginBottom: `${spacing}px`, marginTop: `${spacing}px` }}
|
||||||
|
onClick={() => this.toggleClick()}
|
||||||
|
>
|
||||||
|
<div className={styles('dropdown-selector-content', selectorContentClassName)}>
|
||||||
|
<ItemComponent active={dropdownActive} {...this.props} close={force => this.toggleClick(force)} />
|
||||||
|
</div>
|
||||||
|
{!noArrow && <div className={styles('arrow', arrowColor)} style={arrowStyle} />}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={styles('dropdown-content', dropdownActive && 'active', center && 'center', contentClass, contentClassName)}
|
||||||
|
style={dropdownContentStyle}
|
||||||
|
onClick={this.actionContent}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return DropdownContainer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CustomDropdown;
|
||||||
214
seed/src/components/enhancers/CustomDropdown/customDropdown.scss
Normal file
214
seed/src/components/enhancers/CustomDropdown/customDropdown.scss
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
@import '~styles/mixins';
|
||||||
|
|
||||||
|
.CustomDropdown {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
$decal: 5px;
|
||||||
|
$color_arrow: $default_color_dropdown_arrow;
|
||||||
|
$size_arrow: $dropdown_size_arrow;
|
||||||
|
$decal_arrow: 1px;
|
||||||
|
|
||||||
|
.dropdown-selector {
|
||||||
|
cursor: pointer;
|
||||||
|
//so it wraps only the content
|
||||||
|
z-index: 3;
|
||||||
|
|
||||||
|
.arrow {
|
||||||
|
opacity: 0;
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
z-index: 3;
|
||||||
|
//box-shadow: 0 9px 15px 0 rgba(0, 0, 0, 0.36);
|
||||||
|
@include transition(opacity);
|
||||||
|
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active .arrow {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-selector-content {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-content {
|
||||||
|
text-transform: none;
|
||||||
|
|
||||||
|
width: auto;
|
||||||
|
@include transition(all);
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
right: 0px;
|
||||||
|
max-height: 0;
|
||||||
|
min-width: 100px;
|
||||||
|
z-index: 2;
|
||||||
|
opacity: 0;
|
||||||
|
border-radius: $border_radius;
|
||||||
|
background-color: white;
|
||||||
|
box-shadow: 0 10px 10px rgba(0, 0, 0, 0.08);
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
&.active,
|
||||||
|
&.debug {
|
||||||
|
pointer-events: all;
|
||||||
|
opacity: 1;
|
||||||
|
// arbitrary high so that we see no animation but max-height 0 avoid invisible spacing on views
|
||||||
|
max-height: 1000px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.center {
|
||||||
|
right: auto;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.icon-tooltip {
|
||||||
|
min-width: 50px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.fixed-mobile {
|
||||||
|
.dropdown-content {
|
||||||
|
@include phone {
|
||||||
|
position: fixed !important;
|
||||||
|
left: 50% !important;
|
||||||
|
top: 50% !important;
|
||||||
|
transform: translate(-50%, -50%) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.bottom {
|
||||||
|
.dropdown-selector .arrow {
|
||||||
|
@include arrow('top', $color_arrow, $size_arrow);
|
||||||
|
&.dark {
|
||||||
|
@include arrow('top', $color-dark, $size_arrow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-content {
|
||||||
|
top: calc(100% + #{$size_arrow - $decal_arrow});
|
||||||
|
}
|
||||||
|
|
||||||
|
&.no-triangle .dropdown-content {
|
||||||
|
top: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
// With decal
|
||||||
|
&.decal {
|
||||||
|
&.no-triangle .dropdown-content,
|
||||||
|
.dropdown-selector .arrow {
|
||||||
|
top: calc(100% + #{$decal});
|
||||||
|
}
|
||||||
|
.dropdown-content {
|
||||||
|
top: calc(100% + #{$decal + $size_arrow - $decal_arrow});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.top {
|
||||||
|
.dropdown-selector .arrow {
|
||||||
|
top: auto;
|
||||||
|
bottom: 100%;
|
||||||
|
@include arrow('bottom', $color_arrow, $size_arrow);
|
||||||
|
|
||||||
|
&.dark {
|
||||||
|
@include arrow('bottom', $color-dark, $size_arrow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-content {
|
||||||
|
top: auto;
|
||||||
|
bottom: calc(100% + #{$size_arrow - $decal_arrow});
|
||||||
|
}
|
||||||
|
|
||||||
|
&.no-triangle .dropdown-content {
|
||||||
|
bottom: 100%;
|
||||||
|
}
|
||||||
|
// With decal
|
||||||
|
&.decal {
|
||||||
|
&.no-triangle .dropdown-content,
|
||||||
|
.dropdown-selector .arrow {
|
||||||
|
bottom: calc(100% + #{$decal});
|
||||||
|
}
|
||||||
|
.dropdown-content {
|
||||||
|
top: auto;
|
||||||
|
bottom: calc(100% + #{$decal + $size_arrow - $decal_arrow});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.left {
|
||||||
|
.dropdown-selector .arrow {
|
||||||
|
@include v_align;
|
||||||
|
left: auto;
|
||||||
|
right: 100%;
|
||||||
|
@include arrow('right', $color_arrow, $size_arrow);
|
||||||
|
|
||||||
|
&.dark {
|
||||||
|
@include arrow('right', $color-dark, $size_arrow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-content {
|
||||||
|
left: auto;
|
||||||
|
right: calc(100% + #{$size_arrow - $decal_arrow});
|
||||||
|
@include v_align;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.no-triangle .dropdown-content {
|
||||||
|
right: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
// With decal
|
||||||
|
&.decal {
|
||||||
|
&.no-triangle .dropdown-content,
|
||||||
|
.dropdown-selector .arrow {
|
||||||
|
right: calc(100% + #{$decal});
|
||||||
|
}
|
||||||
|
.dropdown-content {
|
||||||
|
right: calc(100% + #{$decal + $size_arrow - $decal_arrow});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.right {
|
||||||
|
.dropdown-selector .arrow {
|
||||||
|
@include v_align;
|
||||||
|
left: 100%;
|
||||||
|
@include arrow('left', $color_arrow, $size_arrow);
|
||||||
|
|
||||||
|
&.dark {
|
||||||
|
@include arrow('left', $color-dark, $size_arrow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-content {
|
||||||
|
left: calc(100% + #{$size_arrow - $decal_arrow});
|
||||||
|
@include v_align;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.no-triangle .dropdown-content {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
// With decal
|
||||||
|
&.decal {
|
||||||
|
&.no-triangle .dropdown-content,
|
||||||
|
.dropdown-selector .arrow {
|
||||||
|
left: calc(100% + #{$decal});
|
||||||
|
}
|
||||||
|
.dropdown-content {
|
||||||
|
left: calc(100% + #{$decal + $size_arrow - $decal_arrow});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3
seed/src/components/enhancers/CustomDropdown/index.tsx
Normal file
3
seed/src/components/enhancers/CustomDropdown/index.tsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import CustomDropdown from './CustomDropdown';
|
||||||
|
|
||||||
|
export default CustomDropdown;
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
@import '~styles/mixins';
|
||||||
|
|
||||||
|
.marketing-form {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.loading-container {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 0.6;
|
||||||
|
background: #fff;
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
width: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-marketing-form {
|
||||||
|
.loading-form {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { storiesOf } from '@storybook/react';
|
||||||
|
import { text, object, color, dom, array } from '@storybook/addon-knobs';
|
||||||
|
|
||||||
|
import CustomMarketingForm from './index';
|
||||||
|
|
||||||
|
storiesOf('newComponents/CustomMarketingForm', module).add('Default', () => <CustomMarketingForm />);
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render, screen } from 'utils/test-utils';
|
||||||
|
import CustomMarketingForm from './index';
|
||||||
|
|
||||||
|
describe('CustomMarketingForm', () => {
|
||||||
|
it('should render without crashing', () => {
|
||||||
|
render(<CustomMarketingForm />);
|
||||||
|
});
|
||||||
|
});
|
||||||
185
seed/src/components/enhancers/CustomMarketingForm/index.tsx
Normal file
185
seed/src/components/enhancers/CustomMarketingForm/index.tsx
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
// Redux part
|
||||||
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
|
import { StoreState } from 'store/rootReducer';
|
||||||
|
|
||||||
|
// Import actions here
|
||||||
|
|
||||||
|
import classNames from 'classnames/bind';
|
||||||
|
import TextItem from 'components/items/TextItem';
|
||||||
|
import app from 'store/app';
|
||||||
|
import { Form } from 'react-final-form';
|
||||||
|
import sortBy from 'lodash/sortBy';
|
||||||
|
import Button from 'components/items/Button';
|
||||||
|
import Loading from 'components/items/Loading';
|
||||||
|
import { required, email, composeValidators, isTrue } from 'store/utils/validation';
|
||||||
|
import { asyncAction, syncAction } from 'store/utils/actions';
|
||||||
|
import FieldAdapter from 'components/formItems/FieldAdapter';
|
||||||
|
import styleIdentifiers from './CustomMarketingForm.module.scss';
|
||||||
|
|
||||||
|
const styles = classNames.bind(styleIdentifiers);
|
||||||
|
|
||||||
|
export interface CustomMarketingFormProps {
|
||||||
|
itemComponent: any;
|
||||||
|
buttonComponent: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MarketingForm = props => {
|
||||||
|
const {
|
||||||
|
className,
|
||||||
|
itemComponent,
|
||||||
|
buttonComponent = Button,
|
||||||
|
inputProps,
|
||||||
|
formData,
|
||||||
|
buttonProps,
|
||||||
|
lg,
|
||||||
|
loadingClassName,
|
||||||
|
sendLoader,
|
||||||
|
handleSubmit,
|
||||||
|
useAdapter,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
function renderList(schema) {
|
||||||
|
return schema.list;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderValidation(schema) {
|
||||||
|
if (schema.type === 'email' && schema.required) return composeValidators(email, required);
|
||||||
|
if (schema.type === 'email' && !schema.required) return composeValidators(email);
|
||||||
|
if (schema.type === 'checkbox' && schema.required) return composeValidators(isTrue);
|
||||||
|
if (schema.required) return composeValidators(required);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FieldComponent = itemComponent;
|
||||||
|
const ButtonComponent = buttonComponent;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form className={styles('marketing-form', className)} onSubmit={handleSubmit}>
|
||||||
|
{FieldComponent &&
|
||||||
|
sortBy(formData, 'position').map((item, key) => {
|
||||||
|
if (useAdapter) {
|
||||||
|
return (
|
||||||
|
<FieldAdapter
|
||||||
|
name={item.title}
|
||||||
|
label={(item.label && item.label[lg]) || item.label}
|
||||||
|
key={key}
|
||||||
|
placeholder={(item.placeholder && item.placeholder[lg]) || item.placeholder}
|
||||||
|
items={renderList(item.schema)}
|
||||||
|
component={itemComponent}
|
||||||
|
type={item.schema.type !== 'radio' ? item.schema.type : ''}
|
||||||
|
validate={renderValidation(item.schema)}
|
||||||
|
seed={item.schema && item.schema.type}
|
||||||
|
{...item.schema}
|
||||||
|
{...inputProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<FieldComponent
|
||||||
|
name={item.title}
|
||||||
|
label={(item.label && item.label[lg]) || item.label}
|
||||||
|
placeholder={(item.placeholder && item.placeholder[lg]) || item.placeholder}
|
||||||
|
{...item.schema}
|
||||||
|
type={item.schema.type !== 'radio' ? item.schema.type : ''}
|
||||||
|
{...inputProps}
|
||||||
|
items={renderList(item.schema)}
|
||||||
|
key={key}
|
||||||
|
seed={item.schema && item.schema.type}
|
||||||
|
validate={renderValidation(item.schema)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{!FieldComponent && <div>Please pass your project Input</div>}
|
||||||
|
{ButtonComponent && <ButtonComponent type="submit" {...buttonProps} />}
|
||||||
|
{sendLoader && (
|
||||||
|
<div className={styles('loading-container', loadingClassName)}>
|
||||||
|
<Loading className={styles('loading')} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const CustomMarketingForm = (props: CustomMarketingFormProps) => {
|
||||||
|
const {
|
||||||
|
formId,
|
||||||
|
buttonProps,
|
||||||
|
onSubmit,
|
||||||
|
className,
|
||||||
|
inputProps,
|
||||||
|
loadingClassName,
|
||||||
|
initialValues,
|
||||||
|
itemComponent,
|
||||||
|
buttonComponent,
|
||||||
|
useAdapter,
|
||||||
|
noSubmitAPI,
|
||||||
|
} = props;
|
||||||
|
// mapStateToProps
|
||||||
|
const lg = useSelector((state: StoreState) => state.content.lg);
|
||||||
|
// Allow to dispatch actions
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const getForm = data => dispatch(app.actions.form.get.request.action(data));
|
||||||
|
const submitForm = asyncAction(dispatch, app.actions.form.submit);
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState(null);
|
||||||
|
const [sendLoader, setSendLoader] = useState(false);
|
||||||
|
|
||||||
|
async function handleSubmit(e) {
|
||||||
|
setSendLoader(true);
|
||||||
|
if (!e.email && localStorage.getItem('ACemail')) {
|
||||||
|
e.email = localStorage.getItem('ACemail');
|
||||||
|
}
|
||||||
|
|
||||||
|
let res;
|
||||||
|
if (!noSubmitAPI) {
|
||||||
|
res = await submitForm({ id: formId, input: { formData: e } });
|
||||||
|
}
|
||||||
|
if (window && window.vgo && e.email) {
|
||||||
|
localStorage.setItem('ACemail', e.email);
|
||||||
|
window.vgo('setEmail', e.email);
|
||||||
|
window.vgo('process');
|
||||||
|
}
|
||||||
|
setSendLoader(false);
|
||||||
|
if (onSubmit) onSubmit(e, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (formId)
|
||||||
|
getForm({
|
||||||
|
id: formId,
|
||||||
|
lg,
|
||||||
|
callback: e => {
|
||||||
|
if (e?.formData) setFormData(e.formData);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [formId, lg]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles('custom-marketing-form')}>
|
||||||
|
{formData ? (
|
||||||
|
<Form
|
||||||
|
component={MarketingForm}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
formData={formData}
|
||||||
|
itemComponent={itemComponent}
|
||||||
|
buttonComponent={buttonComponent}
|
||||||
|
initialValues={initialValues}
|
||||||
|
inputProps={inputProps}
|
||||||
|
buttonProps={buttonProps}
|
||||||
|
loadingClassName={loadingClassName}
|
||||||
|
className={className}
|
||||||
|
sendLoader={sendLoader}
|
||||||
|
lg={lg}
|
||||||
|
useAdapter={useAdapter}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className={styles('loading-form')}>Loading ...</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CustomMarketingForm;
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
/* eslint-disable import/no-extraneous-dependencies */
|
||||||
|
import React from 'react';
|
||||||
|
import { storiesOf } from '@storybook/react';
|
||||||
|
// import CustomSuggestions from './CustomSuggestions';
|
||||||
|
|
||||||
|
/* storiesOf('seed/formItems/CustomSuggestions', module).add(
|
||||||
|
'CustomSuggestions component',
|
||||||
|
() => <CustomSuggestions />,
|
||||||
|
); */
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render, screen } from 'utils/test-utils';
|
||||||
|
import CustomSuggestions from './CustomSuggestions';
|
||||||
|
|
||||||
|
describe('CustomSuggestions', () => {
|
||||||
|
it('should render without crashing', () => {
|
||||||
|
const itemComponent = () => {};
|
||||||
|
const EnhancedComp = CustomSuggestions(itemComponent);
|
||||||
|
render(<EnhancedComp input={{ value: {} }} defaultValue="any" searchValue="any" items={[]} textKey="text" />);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,160 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import classNames from 'classnames/bind';
|
||||||
|
import Suggestions from 'components/structure/Suggestions';
|
||||||
|
|
||||||
|
import find from 'lodash/find';
|
||||||
|
|
||||||
|
import styleIdentifiers from './customSuggestions.scss';
|
||||||
|
|
||||||
|
const styles = classNames.bind(styleIdentifiers);
|
||||||
|
|
||||||
|
export interface CustomSuggestionsProps {
|
||||||
|
load?: Function;
|
||||||
|
items: object[];
|
||||||
|
input: {};
|
||||||
|
actionChange?: Function;
|
||||||
|
defaultValue: any;
|
||||||
|
textKey: string;
|
||||||
|
searchValue: any;
|
||||||
|
useId?: boolean;
|
||||||
|
forcedOpen?: boolean;
|
||||||
|
size?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CustomSuggestionsState {
|
||||||
|
searched?: boolean;
|
||||||
|
items: object[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function Wrapper(ItemComponent: React.ComponentType<any>) {
|
||||||
|
return class CustomSuggestions extends React.Component<CustomSuggestionsProps, CustomSuggestionsState> {
|
||||||
|
constructor(props: CustomSuggestionsProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
items: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const { load } = this.props;
|
||||||
|
this.handleItems({});
|
||||||
|
if (load) load('');
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps: CustomSuggestionsProps) {
|
||||||
|
this.handleItems(prevProps);
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange = (e: KeyboardEvent) => {
|
||||||
|
const { load, input, actionChange, defaultValue, textKey } = this.props;
|
||||||
|
|
||||||
|
const str = e.currentTarget.value;
|
||||||
|
|
||||||
|
if (load) load(str);
|
||||||
|
|
||||||
|
let value;
|
||||||
|
if (defaultValue) {
|
||||||
|
value = {
|
||||||
|
...defaultValue,
|
||||||
|
[textKey || 'name']: str,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
value = {
|
||||||
|
[textKey || 'name']: str,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
input.onChange(value);
|
||||||
|
|
||||||
|
if (actionChange && !str) actionChange();
|
||||||
|
};
|
||||||
|
|
||||||
|
handleItems = (prevProps: {}) => {
|
||||||
|
const { items, textKey, searchValue } = this.props;
|
||||||
|
|
||||||
|
const { searched } = this.state;
|
||||||
|
|
||||||
|
if (prevProps.items !== items) {
|
||||||
|
const newData = [];
|
||||||
|
// from server
|
||||||
|
if (items) {
|
||||||
|
// handle list
|
||||||
|
for (let i = 0; i < items.length; i += 1) {
|
||||||
|
const elem = items[i];
|
||||||
|
|
||||||
|
newData.push({
|
||||||
|
value: elem.id,
|
||||||
|
// text will be displayed
|
||||||
|
text: elem[textKey || 'name'],
|
||||||
|
...elem,
|
||||||
|
id: elem.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (searchValue && !searched && items.length > 0) {
|
||||||
|
const item = find(newData, it => it[textKey || 'name'] === searchValue);
|
||||||
|
if (item) this.selectValue(item, true);
|
||||||
|
this.setState({
|
||||||
|
items: newData,
|
||||||
|
searched: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
items: newData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
items,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
selectValue = (item: {}, disableAction: boolean) => {
|
||||||
|
const { input, actionChange, load, useId, textKey } = this.props;
|
||||||
|
|
||||||
|
const value = useId ? item.id : item;
|
||||||
|
|
||||||
|
input.onChange(value);
|
||||||
|
|
||||||
|
if (load) load(item[textKey || 'name']);
|
||||||
|
|
||||||
|
if (actionChange && !disableAction) actionChange(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
filteredItems = () => {
|
||||||
|
const { items } = this.state;
|
||||||
|
|
||||||
|
return items || [];
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { input, useId, forcedOpen, size, textKey } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles('CustomSuggestions')}>
|
||||||
|
<Suggestions
|
||||||
|
closeOnSelect
|
||||||
|
forcedOpen={forcedOpen}
|
||||||
|
selectValue={this.selectValue}
|
||||||
|
value={input.value}
|
||||||
|
useId={useId}
|
||||||
|
size={size}
|
||||||
|
items={this.filteredItems()}
|
||||||
|
>
|
||||||
|
<ItemComponent
|
||||||
|
{...this.props}
|
||||||
|
input={{
|
||||||
|
value: input.value[textKey || 'name'] || '',
|
||||||
|
}}
|
||||||
|
onChange={this.onChange}
|
||||||
|
/>
|
||||||
|
</Suggestions>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Wrapper;
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
@import '~styles/mixins';
|
||||||
|
|
||||||
|
.CustomSuggestions {
|
||||||
|
}
|
||||||
3
seed/src/components/enhancers/CustomSuggestions/index.ts
Normal file
3
seed/src/components/enhancers/CustomSuggestions/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import CustomSuggestions from './CustomSuggestions';
|
||||||
|
|
||||||
|
export default CustomSuggestions;
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
/* eslint-disable import/no-extraneous-dependencies */
|
||||||
|
import React from 'react';
|
||||||
|
import { storiesOf } from '@storybook/react';
|
||||||
|
// import CustomTable from './CustomTable';
|
||||||
|
|
||||||
|
/* storiesOf('seed/structure/CustomTable', module).add('CustomTable component',
|
||||||
|
() => <CustomTable />,
|
||||||
|
); */
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render, screen } from 'utils/test-utils';
|
||||||
|
import CustomTable from './CustomTable';
|
||||||
|
|
||||||
|
describe('CustomTable', () => {
|
||||||
|
it('should render without crashing', () => {
|
||||||
|
const ItemComponent = () => {};
|
||||||
|
const EnhancedComp = CustomTable(ItemComponent);
|
||||||
|
render(<EnhancedComp head={{ key: '1', type: '', props: {}, ref: {} }} headers={[{}]} items={[]} />);
|
||||||
|
});
|
||||||
|
});
|
||||||
378
seed/src/components/enhancers/CustomTable/CustomTable.tsx
Normal file
378
seed/src/components/enhancers/CustomTable/CustomTable.tsx
Normal file
@ -0,0 +1,378 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import classNames from 'classnames/bind';
|
||||||
|
|
||||||
|
import findIndex from 'lodash/findIndex';
|
||||||
|
import orderBy from 'lodash/orderBy';
|
||||||
|
import get from 'lodash/get';
|
||||||
|
|
||||||
|
import TextItem from 'components/items/TextItem';
|
||||||
|
import Button from 'components/items/Button';
|
||||||
|
import ReactPaginate from 'react-paginate';
|
||||||
|
import { withRouter } from 'react-router-dom';
|
||||||
|
import qs from 'query-string';
|
||||||
|
import { IoIosArrowDown } from 'react-icons/io';
|
||||||
|
import styleIdentifiers from './customTable.scss';
|
||||||
|
|
||||||
|
const styles = classNames.bind(styleIdentifiers);
|
||||||
|
|
||||||
|
export interface CustomTableProps {
|
||||||
|
noCheckbox?: boolean;
|
||||||
|
items: any[];
|
||||||
|
headers: object[];
|
||||||
|
loaded?: boolean;
|
||||||
|
loading?: boolean;
|
||||||
|
customOrder?: Function;
|
||||||
|
itemProps?: {};
|
||||||
|
noShadow?: boolean;
|
||||||
|
location: {};
|
||||||
|
history: {};
|
||||||
|
url?: string;
|
||||||
|
loadFunc?: Function;
|
||||||
|
handleFilters?: Function;
|
||||||
|
subhead?: React.Element<any>;
|
||||||
|
actions?: React.Element<any>;
|
||||||
|
legend?: React.Element<any>;
|
||||||
|
actionDelete?: Function;
|
||||||
|
title?: string;
|
||||||
|
loaded?: boolean;
|
||||||
|
loading?: boolean;
|
||||||
|
loadMore?: Function;
|
||||||
|
noMore?: boolean;
|
||||||
|
itemProps?: {};
|
||||||
|
// filters
|
||||||
|
displayFilters?: boolean;
|
||||||
|
filtersProps?: {};
|
||||||
|
filtersInitialValues?: {};
|
||||||
|
// reset
|
||||||
|
pagination?: {};
|
||||||
|
currentPage?: number;
|
||||||
|
noResultMessage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CustomTableState {
|
||||||
|
list: any[];
|
||||||
|
filters: {};
|
||||||
|
masterCheck: boolean;
|
||||||
|
order: string;
|
||||||
|
asc: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CustomTable(ItemComponent: React.ComponentType<any>, FiltersForm?: React.ComponentType<any>) {
|
||||||
|
class Table extends React.Component<CustomTableProps, CustomTableState> {
|
||||||
|
constructor(props: CustomTableProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
const { location } = props;
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
// used to check the item internaly (could have used the store as well)
|
||||||
|
list: [],
|
||||||
|
filters: qs.parse(location.search),
|
||||||
|
masterCheck: false,
|
||||||
|
order: '',
|
||||||
|
asc: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.updateList();
|
||||||
|
this.loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps: CustomTableProps) {
|
||||||
|
this.updateList(prevProps);
|
||||||
|
this.onUpdate(prevProps);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Could be done in reducers
|
||||||
|
onItemClick = (id: number, targetValue?: boolean) => {
|
||||||
|
const list = this.items();
|
||||||
|
|
||||||
|
const index = findIndex(list, { id });
|
||||||
|
const newElement = list[index];
|
||||||
|
|
||||||
|
let masterCheck = true;
|
||||||
|
// toggle part
|
||||||
|
if (targetValue !== undefined) newElement.checked = targetValue;
|
||||||
|
else if (newElement.checked === true) {
|
||||||
|
newElement.checked = false;
|
||||||
|
} else {
|
||||||
|
newElement.checked = true;
|
||||||
|
}
|
||||||
|
list.splice(index, 1, newElement);
|
||||||
|
|
||||||
|
if (!targetValue) {
|
||||||
|
list.forEach(element => {
|
||||||
|
if (!element.checked) masterCheck = false;
|
||||||
|
});
|
||||||
|
this.setState({ list, masterCheck });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onUpdate = (prevProps?: CustomTableProps) => {
|
||||||
|
const { location } = this.props;
|
||||||
|
|
||||||
|
if (!prevProps || location !== prevProps.location) {
|
||||||
|
this.setState(
|
||||||
|
{
|
||||||
|
filters: qs.parse(location.search),
|
||||||
|
},
|
||||||
|
this.loadData,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
getCheckedItems = () => {
|
||||||
|
const list = this.items();
|
||||||
|
|
||||||
|
const data = [];
|
||||||
|
if (!list || !Array.isArray(list)) return data;
|
||||||
|
list.forEach(element => {
|
||||||
|
if (element.checked) data.push(element);
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update location
|
||||||
|
setFilters = filters => {
|
||||||
|
const { history, url } = this.props;
|
||||||
|
|
||||||
|
history.push({
|
||||||
|
pathname: url,
|
||||||
|
search: qs.stringify(filters),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update the list displayed internaly
|
||||||
|
updateList = (prevProps?: {}) => {
|
||||||
|
const { items } = this.props;
|
||||||
|
if (items && (!prevProps || prevProps.items !== items)) {
|
||||||
|
this.setState({
|
||||||
|
list: items,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadData = () => {
|
||||||
|
const { loadFunc } = this.props;
|
||||||
|
|
||||||
|
const { filters } = this.state;
|
||||||
|
if (loadFunc) {
|
||||||
|
loadFunc(filters);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
applyFilters = values => {
|
||||||
|
const { filters } = this.state;
|
||||||
|
const { handleFilters } = this.props;
|
||||||
|
|
||||||
|
const newFilters = handleFilters ? handleFilters(values, filters) : values;
|
||||||
|
|
||||||
|
if (newFilters) this.setFilters(newFilters);
|
||||||
|
};
|
||||||
|
|
||||||
|
loadPage = page => {
|
||||||
|
const currentPage = parseInt(page.selected, 10) + 1;
|
||||||
|
|
||||||
|
const { filters } = this.state;
|
||||||
|
|
||||||
|
const newFilters = {
|
||||||
|
...filters,
|
||||||
|
page: currentPage,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.setFilters(newFilters);
|
||||||
|
};
|
||||||
|
|
||||||
|
masterCheck = () => {
|
||||||
|
const { masterCheck } = this.state;
|
||||||
|
|
||||||
|
const list = this.items();
|
||||||
|
list.forEach(element => {
|
||||||
|
this.onItemClick(element.id, !masterCheck);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
masterCheck: !masterCheck,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get internal list
|
||||||
|
items = () => {
|
||||||
|
const { list } = this.state;
|
||||||
|
return list || [];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get actually dispalyed data
|
||||||
|
filtered = () => {
|
||||||
|
const { order, asc } = this.state;
|
||||||
|
const { customOrder } = this.props;
|
||||||
|
|
||||||
|
const list = this.items();
|
||||||
|
|
||||||
|
const sorted = orderBy(
|
||||||
|
list,
|
||||||
|
(item: {}) => {
|
||||||
|
if (customOrder) return customOrder(item, order) || false;
|
||||||
|
const val = get(item, order);
|
||||||
|
if (!isNaN(val)) return parseFloat(val);
|
||||||
|
return val || false;
|
||||||
|
},
|
||||||
|
[asc ? 'asc' : 'desc'],
|
||||||
|
);
|
||||||
|
|
||||||
|
return sorted;
|
||||||
|
};
|
||||||
|
|
||||||
|
orderBy = (field?: string) => {
|
||||||
|
const { order, asc } = this.state;
|
||||||
|
|
||||||
|
if (field && order !== field) {
|
||||||
|
this.setState({
|
||||||
|
order: field,
|
||||||
|
asc: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
asc: !asc,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
listHeadItem = (name: string, field?: string) => {
|
||||||
|
const { order, asc } = this.state;
|
||||||
|
if (field) {
|
||||||
|
return (
|
||||||
|
<div className={styles('sortable-head')} onClick={() => this.orderBy(field)}>
|
||||||
|
<TextItem path={name} />
|
||||||
|
{order === field && (
|
||||||
|
<div className={styles('arrow', !asc && 'inverted')}>
|
||||||
|
<IoIosArrowDown />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className={styles('not-sortable-head')}>
|
||||||
|
<TextItem path={name} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
noCheckbox,
|
||||||
|
subhead,
|
||||||
|
actions,
|
||||||
|
actionDelete,
|
||||||
|
headers,
|
||||||
|
title,
|
||||||
|
loaded,
|
||||||
|
loading,
|
||||||
|
loadMore,
|
||||||
|
itemProps,
|
||||||
|
pagination,
|
||||||
|
noShadow,
|
||||||
|
currentPage,
|
||||||
|
noResultMessage,
|
||||||
|
} = this.props;
|
||||||
|
const { masterCheck, filters } = this.state;
|
||||||
|
const list = this.filtered();
|
||||||
|
|
||||||
|
const checkedList = this.getCheckedItems();
|
||||||
|
return (
|
||||||
|
<div className={styles('CustomTable')}>
|
||||||
|
{title && (
|
||||||
|
<div className={styles('head')}>
|
||||||
|
<div className={styles('title')}>
|
||||||
|
<h1>
|
||||||
|
<TextItem path={title} />
|
||||||
|
</h1>
|
||||||
|
{subhead && <div className={styles('subhead')}>{subhead}</div>}
|
||||||
|
</div>
|
||||||
|
<div className={styles('actions')}>
|
||||||
|
{!checkedList || checkedList.length === 0 ? (
|
||||||
|
actions
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
noMargin
|
||||||
|
relative
|
||||||
|
shadow
|
||||||
|
className={styles('delete')}
|
||||||
|
color="red"
|
||||||
|
label="general.messages.deleteSelection"
|
||||||
|
action={() => actionDelete && actionDelete(checkedList)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{FiltersForm && (
|
||||||
|
<div className={styles('filters')}>
|
||||||
|
<FiltersForm onSubmit={this.applyFilters} initialValues={filters} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={styles('listing', noShadow && 'no-shadow')}>
|
||||||
|
{loaded && list.length === 0 && (
|
||||||
|
<div className={styles('no-result')}>
|
||||||
|
<TextItem path={noResultMessage || 'admin.messages.noResult'} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{list.length > 0 && (
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th onClick={this.masterCheck} className={styles('box-wrapper', noCheckbox && 'small')}>
|
||||||
|
{!noCheckbox && <div className={styles('box', masterCheck && 'checked')} />}
|
||||||
|
</th>
|
||||||
|
{headers.map((item, key) => (
|
||||||
|
<th key={item.title || key}>{this.listHeadItem(item.title, item.key)}</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{list.map((item, key) => (
|
||||||
|
<ItemComponent action={() => this.onItemClick(item.id)} item={item} key={item.id || key} noCheckbox={noCheckbox} {...itemProps} />
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
)}
|
||||||
|
{loading && (
|
||||||
|
<div className={styles('loading')}>
|
||||||
|
<TextItem path="general.messages.loading" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{loadMore && pagination && pagination.current_page < pagination.last_page && (
|
||||||
|
<div className={styles('buttons')}>
|
||||||
|
<Button noMargin relative shadow color="blue" label="Charger plus" action={() => loadMore(pagination)} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{pagination && pagination.last_page > 1 && (
|
||||||
|
<ReactPaginate
|
||||||
|
pageCount={pagination.last_page}
|
||||||
|
pageRangeDisplayed={3}
|
||||||
|
marginPagesDisplayed={1}
|
||||||
|
// initialPage={parseInt(currentPage - 1, 10)}
|
||||||
|
forcePage={currentPage && parseInt(currentPage - 1, 10)}
|
||||||
|
disableInitialCallback
|
||||||
|
breakLabel="..."
|
||||||
|
onPageChange={this.loadPage}
|
||||||
|
previousLabel="Précédent"
|
||||||
|
nextLabel="Suivant"
|
||||||
|
containerClassName={styles('pages')}
|
||||||
|
pageClassName={styles('page')}
|
||||||
|
activeClassName={styles('active')}
|
||||||
|
disabledClassName={styles('disabled')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return withRouter(Table);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CustomTable;
|
||||||
130
seed/src/components/enhancers/CustomTable/customTable.scss
Normal file
130
seed/src/components/enhancers/CustomTable/customTable.scss
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
@import '~styles/mixins';
|
||||||
|
@import '~styles/items/headListing';
|
||||||
|
@import '~styles/items/checkedBox';
|
||||||
|
|
||||||
|
.CustomTable {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.head {
|
||||||
|
@include headListing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-result,
|
||||||
|
.loading {
|
||||||
|
padding: 20px;
|
||||||
|
//background-color:$color-grey-background;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pages {
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 15px;
|
||||||
|
text-align: right;
|
||||||
|
|
||||||
|
li {
|
||||||
|
@include middle;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page {
|
||||||
|
a {
|
||||||
|
border: 1px solid $color_border;
|
||||||
|
@include ball(25px);
|
||||||
|
display: inline-block;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: $border_radius;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: $color-grey-background;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
margin: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled {
|
||||||
|
a {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.listing {
|
||||||
|
@include card;
|
||||||
|
|
||||||
|
&.no-shadow {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-wrapper {
|
||||||
|
@include checkedBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortable-head {
|
||||||
|
width: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 13px 0;
|
||||||
|
text-align: left;
|
||||||
|
padding-right: 30px;
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
.arrow {
|
||||||
|
@include v_align;
|
||||||
|
right: 8px;
|
||||||
|
|
||||||
|
&.inverted {
|
||||||
|
transform: translateY(-50%) rotate(180deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-sortable-head {
|
||||||
|
padding: $table_cell_spacing;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
text-align: left;
|
||||||
|
font-size: 12px;
|
||||||
|
color: $color-grey-font;
|
||||||
|
text-transform: uppercase;
|
||||||
|
vertical-align: middle;
|
||||||
|
//opacity: 0.6;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
td,
|
||||||
|
th {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody {
|
||||||
|
tr {
|
||||||
|
border-top: 1px solid $color_border;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
margin-top: 20px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
3
seed/src/components/enhancers/CustomTable/index.ts
Normal file
3
seed/src/components/enhancers/CustomTable/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import CustomTable from './CustomTable';
|
||||||
|
|
||||||
|
export default CustomTable;
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { storiesOf } from '@storybook/react';
|
||||||
|
import { text, object, color, dom, array } from '@storybook/addon-knobs';
|
||||||
|
|
||||||
|
import MosaicStructure from './MosaicStructure';
|
||||||
|
|
||||||
|
storiesOf('newComponents/MosaicStructure', module).add('Default', () => <MosaicStructure />);
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render, screen } from 'utils/test-utils';
|
||||||
|
import MosaicStructure from './MosaicStructure';
|
||||||
|
|
||||||
|
describe('MosaicStructure', () => {
|
||||||
|
it('should render without crashing', () => {
|
||||||
|
render(<MosaicStructure />);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,149 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import classNames from 'classnames/bind';
|
||||||
|
import window from 'global/window';
|
||||||
|
import styleIdentifiers from './mosaicStructure.scss';
|
||||||
|
|
||||||
|
const styles = classNames.bind(styleIdentifiers);
|
||||||
|
|
||||||
|
export interface MosaicStructureProps {
|
||||||
|
minWidth?: number;
|
||||||
|
scrollableColumns?: boolean;
|
||||||
|
direction: 'ltrtl' | 'rtltr' | 'ltr' | 'rtl';
|
||||||
|
items: object[] | object;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MosaicStructureState {
|
||||||
|
columns: any[][] | null;
|
||||||
|
numColumns: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MosaicStructure(ItemComponent: React.ComponentType<any>) {
|
||||||
|
class Structure extends React.Component<MosaicStructureProps, MosaicStructureState> {
|
||||||
|
constructor(props: MosaicStructureProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
columns: null,
|
||||||
|
numColumns: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.mosaicRef = React.createRef();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.updateNumColumns();
|
||||||
|
if (window.addEventListener) window.addEventListener('resize', this.updateNumColumns);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
const { items } = this.props;
|
||||||
|
if (items !== prevProps.items) {
|
||||||
|
this.updateNumColumns();
|
||||||
|
if (window.addEventListener) window.addEventListener('resize', this.updateNumColumns);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
if (window.addEventListener) window.removeEventListener('resize', this.updateNumColumns);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateNumColumns = () => {
|
||||||
|
let { minWidth, width } = this.props;
|
||||||
|
|
||||||
|
if (!this.mosaicRef) return;
|
||||||
|
|
||||||
|
width = width || this.mosaicRef.current.offsetWidth;
|
||||||
|
if (!width) return;
|
||||||
|
|
||||||
|
if (!minWidth) {
|
||||||
|
minWidth = 500;
|
||||||
|
}
|
||||||
|
let num = Math.floor(width / minWidth);
|
||||||
|
if (num <= 0) num = 1;
|
||||||
|
this.setState(
|
||||||
|
{
|
||||||
|
numColumns: num,
|
||||||
|
},
|
||||||
|
this.constructColumns,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
constructColumns = () => {
|
||||||
|
const { items } = this.props;
|
||||||
|
const { numColumns } = this.state;
|
||||||
|
|
||||||
|
if (!items) return;
|
||||||
|
|
||||||
|
let { direction } = this.props;
|
||||||
|
direction = direction || 'ltr';
|
||||||
|
|
||||||
|
let list;
|
||||||
|
if (typeof items === 'object') {
|
||||||
|
list = Object.values(items);
|
||||||
|
} else {
|
||||||
|
list = items;
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = [];
|
||||||
|
for (let i = 0; i < numColumns; i++) {
|
||||||
|
columns.push([]);
|
||||||
|
}
|
||||||
|
let current = direction.charAt(0) === 'l' ? 0 : numColumns - 1;
|
||||||
|
let change = false;
|
||||||
|
|
||||||
|
for (let i = 0; i < list.length; i++) {
|
||||||
|
columns[current].push(list[i]);
|
||||||
|
|
||||||
|
if (direction === 'ltrtl') {
|
||||||
|
if (!change) current++;
|
||||||
|
else current--;
|
||||||
|
} else if (direction === 'rtltr') {
|
||||||
|
if (!change) current--;
|
||||||
|
else current++;
|
||||||
|
} else if (direction === 'ltr') {
|
||||||
|
current++;
|
||||||
|
if (current >= numColumns) current = 0;
|
||||||
|
} else if (direction === 'rtl') {
|
||||||
|
current--;
|
||||||
|
if (current < 0) current = numColumns - 1;
|
||||||
|
}
|
||||||
|
// right to left to right or left to right to left
|
||||||
|
if (direction === 'rtltr' || direction === 'ltrtl') {
|
||||||
|
if (current >= numColumns) {
|
||||||
|
change = !change;
|
||||||
|
current = numColumns - 1;
|
||||||
|
} else if (current < 0) {
|
||||||
|
change = !change;
|
||||||
|
current = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
columns,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { scrollableColumns, className } = this.props;
|
||||||
|
const { columns, numColumns } = this.state;
|
||||||
|
return (
|
||||||
|
<div className={styles('MosaicStructure', className)} ref={this.mosaicRef}>
|
||||||
|
{columns &&
|
||||||
|
columns.map((column, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={styles('column', scrollableColumns && 'scrollable')}
|
||||||
|
style={{ width: `${100 / numColumns}%` }}
|
||||||
|
>
|
||||||
|
{column.map((item, ind) => (
|
||||||
|
<ItemComponent {...this.props} item={item} key={ind} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Structure;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MosaicStructure;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user