init commit

This commit is contained in:
Valdior 2025-05-14 21:49:03 +02:00
commit 4ce981c5b6
1055 changed files with 60729 additions and 0 deletions

6
.eslintignore Normal file
View File

@ -0,0 +1,6 @@
dist/**
src/index.html
flow-typed/**
node_modules/**
seed/node_modules/**
dashboard/node_modules/**

13
.eslintrc.js Normal file
View 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
View File

@ -0,0 +1,7 @@
.idea
node_modules
package-lock.json
dist
build
ssr
.DS_Store

7
.prettierrc.js Normal file
View File

@ -0,0 +1,7 @@
module.exports = {
singleQuote: true,
trailingComma: 'all',
printWidth: 100,
tabWidth: 2,
useTabs: false,
};

15
.vscode/launch.json vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,2 @@
node_modules/**
flow-typed/**

72
seed/.eslintrc.js Normal file
View 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
View 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
View 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
View File

@ -0,0 +1,2 @@
.gitlab-ci.yml
**/*.hbs

7
seed/.prettierrc.js Normal file
View File

@ -0,0 +1,7 @@
module.exports = {
singleQuote: true,
trailingComma: 'all',
printWidth: 150,
tabWidth: 2,
useTabs: false,
};

11
seed/.storybook/addons.js Normal file
View 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
View 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
View File

@ -0,0 +1,10 @@
module.exports = {
"stories": [
"../src/**/*.stories.mdx",
"../src/**/*.stories.@(js|jsx|ts|tsx)"
],
"addons": [
"@storybook/addon-links",
"@storybook/addon-essentials"
]
}

View File

@ -0,0 +1,9 @@
export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
}

View 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
View 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 youre done!
## Usage
Use component `ProjectIcon`
Name in `icon` prop has to be what is after the `icon-` in the icons 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
View 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',
},
],
],
};

View File

@ -0,0 +1 @@
module.exports = 'test-file-stub';

View File

@ -0,0 +1,4 @@
const Enzyme = require('enzyme');
const Adapter = require('enzyme-adapter-react-16');
Enzyme.configure({ adapter: new Adapter() });

View File

@ -0,0 +1 @@
module.exports = {};

58
seed/config/paths.js Normal file
View 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,
};

View 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
View 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);
};

View 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);
};

View 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);
};

View 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
View 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
View 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
View 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"
}
}

View 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>
);
}
}

View 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>
);
}
}

View 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}};

View 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}};

View 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}};

View File

@ -0,0 +1,3 @@
import {{pascalCase name}} from './{{pascalCase name}}';
export default {{pascalCase name}};

View 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;

View 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;

View File

@ -0,0 +1,4 @@
@import '~styles/mixins';
.{{dashCase name}} {
}

View 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}};

View 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}};

View 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}} />);
});
});

View 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}};

View 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" />);

View 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 }} />);

View File

@ -0,0 +1,5 @@
import styled from '@emotion/styled';
import { mediaQuery } from '../../../styles';
export const {{pascalCase name}} = styled.div`
`;

View 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;

View 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;

View File

@ -0,0 +1,5 @@
const prefix = '{{ camelCase name }}';
export default {
prefix,
};

View 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,
};

View 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,
};

View 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 };

View 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;

View 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);
}

View 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);
}

View 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
View 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
View 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
View File

@ -0,0 +1,10 @@
module.exports = {
plugins: [
[
'postcss-preset-env',
{
// Options
},
],
],
};

110
seed/scripts/build.js Normal file
View 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();

View 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
View 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
View 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
View File

@ -0,0 +1,3 @@
const handler = require('./src/server/serverless').default;
export default handler;

25
seed/serverless.yaml Normal file
View 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 /

View 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;

View 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;

View 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 CustomArticle from './index';
storiesOf('newComponents/CustomArticle', module).add('Default', () => <CustomArticle />);

View File

@ -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 />);
});
});

View File

@ -0,0 +1,4 @@
@import '~styles/mixins';
.custom-article {
}

View 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);

View File

@ -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 />);

View File

@ -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 />);
});
});

View 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;

View 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});
}
}
}
}

View File

@ -0,0 +1,3 @@
import CustomDropdown from './CustomDropdown';
export default CustomDropdown;

View File

@ -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;
}
}

View 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 CustomMarketingForm from './index';
storiesOf('newComponents/CustomMarketingForm', module).add('Default', () => <CustomMarketingForm />);

View File

@ -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 />);
});
});

View 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;

View File

@ -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 />,
); */

View File

@ -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" />);
});
});

View File

@ -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;

View File

@ -0,0 +1,4 @@
@import '~styles/mixins';
.CustomSuggestions {
}

View File

@ -0,0 +1,3 @@
import CustomSuggestions from './CustomSuggestions';
export default CustomSuggestions;

View File

@ -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 />,
); */

View File

@ -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={[]} />);
});
});

View 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;

View 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;
}
}

View File

@ -0,0 +1,3 @@
import CustomTable from './CustomTable';
export default CustomTable;

View File

@ -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 />);

View File

@ -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 />);
});
});

View File

@ -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