init
This commit is contained in:
11
.eslintignore
Normal file
11
.eslintignore
Normal file
@ -0,0 +1,11 @@
|
||||
/node_modules
|
||||
/dist
|
||||
/out
|
||||
.vscode
|
||||
.idea
|
||||
/src/reportWebVitals.ts
|
||||
/src/setupTests.ts
|
||||
webpack.config.js
|
||||
jest.config.js
|
||||
babel.config.js
|
||||
/scripts
|
||||
159
.eslintrc.json
Normal file
159
.eslintrc.json
Normal file
@ -0,0 +1,159 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true,
|
||||
"jest/globals": true
|
||||
},
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "detect"
|
||||
}
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"prettier",
|
||||
"prettier/react",
|
||||
"plugin:import/errors",
|
||||
"plugin:import/warnings",
|
||||
"plugin:jest/recommended",
|
||||
"plugin:react-hooks/recommended"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
"tsx": true
|
||||
},
|
||||
"ecmaVersion": 12,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": ["react", "@typescript-eslint", "prettier", "jest"],
|
||||
"rules": {
|
||||
"react/jsx-filename-extension": [
|
||||
1,
|
||||
{
|
||||
"extensions": [".ts", ".tsx"]
|
||||
}
|
||||
],
|
||||
"react-hooks/exhaustive-deps": [
|
||||
"warn",
|
||||
{
|
||||
"additionalHooks": "(useEqualMemo|useStream)"
|
||||
}
|
||||
],
|
||||
"jsx-a11y/label-has-associated-control": 0,
|
||||
"eqeqeq": "error",
|
||||
"import/extensions": 0,
|
||||
"react/prop-types": 0,
|
||||
"no-underscore-dangle": 0,
|
||||
"import/imports-first": ["warn", "absolute-first"],
|
||||
"import/prefer-default-export": 0,
|
||||
"import/no-unresolved": 0,
|
||||
"import/newline-after-import": "error",
|
||||
"react/jsx-props-no-spreading": 0,
|
||||
"class-methods-use-this": 0,
|
||||
"react/prefer-stateless-function": 0,
|
||||
"react/jsx-fragments": 0,
|
||||
"react/jsx-key": "warn",
|
||||
"react/no-array-index-key": 0,
|
||||
"react/destructuring-assignment": 0,
|
||||
"no-console": [
|
||||
"warn",
|
||||
{
|
||||
"allow": ["warn", "error"]
|
||||
}
|
||||
],
|
||||
"semi": "warn",
|
||||
"quotes": ["warn", "single"],
|
||||
"array-callback-return": [
|
||||
"warn",
|
||||
{
|
||||
"allowImplicit": true,
|
||||
"checkForEach": true
|
||||
}
|
||||
],
|
||||
"no-trailing-spaces": "warn",
|
||||
"default-case": "warn",
|
||||
"default-param-last": "warn",
|
||||
"no-alert": "warn",
|
||||
"no-constructor-return": "warn",
|
||||
"no-else-return": "warn",
|
||||
"no-empty-function": "warn",
|
||||
"no-multi-spaces": "warn",
|
||||
"no-multi-str": "warn",
|
||||
"no-new": "warn",
|
||||
"no-param-reassign": "warn",
|
||||
"no-sequences": "warn",
|
||||
"no-useless-concat": "warn",
|
||||
"prefer-promise-reject-errors": "warn",
|
||||
"require-await": "warn",
|
||||
"wrap-iife": ["warn", "inside"],
|
||||
"yoda": "warn",
|
||||
"no-shadow": "off",
|
||||
"@typescript-eslint/no-use-before-define": "off",
|
||||
"@typescript-eslint/no-shadow": "warn",
|
||||
"@typescript-eslint/no-namespace": "off",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/no-unused-vars": "warn",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"array-bracket-spacing": ["warn", "never"],
|
||||
"block-spacing": ["warn", "never"],
|
||||
"brace-style": ["warn", "1tbs", {"allowSingleLine": true}],
|
||||
"capitalized-comments": ["warn"],
|
||||
"comma-dangle": ["warn", "only-multiline"],
|
||||
"comma-spacing": ["warn", {"before": false, "after": true}],
|
||||
"computed-property-spacing": ["warn", "never"],
|
||||
"eol-last": ["warn", "always"],
|
||||
"func-call-spacing": ["warn", "never"],
|
||||
"keyword-spacing": ["warn", {"before": true}],
|
||||
"line-comment-position": ["warn", {"position": "above"}],
|
||||
"lines-between-class-members": ["warn", "always"],
|
||||
"max-len": [
|
||||
"warn",
|
||||
{
|
||||
"code": 120,
|
||||
"ignoreComments": true,
|
||||
"ignoreUrls": true,
|
||||
"ignoreStrings": true,
|
||||
"ignoreTemplateLiterals": true,
|
||||
"ignoreRegExpLiterals": true
|
||||
}
|
||||
],
|
||||
"multiline-comment-style": ["warn", "starred-block"],
|
||||
"new-cap": "warn",
|
||||
"new-parens": "warn",
|
||||
"newline-per-chained-call": ["warn", {"ignoreChainWithDepth": 3}],
|
||||
"no-bitwise": "warn",
|
||||
"no-inline-comments": "warn",
|
||||
"no-lonely-if": "warn",
|
||||
"no-multi-assign": "warn",
|
||||
"no-multiple-empty-lines": ["warn", {"max": 1}],
|
||||
"no-nested-ternary": "warn",
|
||||
"no-plusplus": "warn",
|
||||
"object-curly-spacing": ["warn", "never"],
|
||||
"object-property-newline": ["warn", {"allowAllPropertiesOnSameLine": true}],
|
||||
"key-spacing": ["warn", {"beforeColon": false, "afterColon": true}],
|
||||
"space-before-blocks": "warn",
|
||||
"space-before-function-paren": [
|
||||
"warn",
|
||||
{
|
||||
"anonymous": "always",
|
||||
"named": "never",
|
||||
"asyncArrow": "always"
|
||||
}
|
||||
],
|
||||
"space-in-parens": ["warn", "never"],
|
||||
"space-infix-ops": "warn",
|
||||
"arrow-parens": ["warn", "as-needed"],
|
||||
"arrow-spacing": "warn",
|
||||
"no-duplicate-imports": "warn",
|
||||
"no-useless-computed-key": "warn",
|
||||
"no-useless-constructor": "warn",
|
||||
"no-var": "warn",
|
||||
"prefer-const": "warn",
|
||||
"prefer-rest-params": "warn",
|
||||
"prefer-template": "warn",
|
||||
"template-curly-spacing": "warn"
|
||||
}
|
||||
}
|
||||
27
.gitignore
vendored
Normal file
27
.gitignore
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
.idea
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
/dist
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
tsconfig.dev.json
|
||||
11
.prettierrc
Normal file
11
.prettierrc
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"tabWidth": 4,
|
||||
"useTabs": false,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"bracketSpacing": false,
|
||||
"jsxBracketSameLine": false,
|
||||
"arrowParens": "avoid"
|
||||
}
|
||||
12
babel.config.js
Normal file
12
babel.config.js
Normal file
@ -0,0 +1,12 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-env',
|
||||
{
|
||||
targets: {node: 'current'}
|
||||
},
|
||||
|
||||
],
|
||||
'@babel/preset-typescript'
|
||||
],
|
||||
};
|
||||
190
jest.config.js
Normal file
190
jest.config.js
Normal file
@ -0,0 +1,190 @@
|
||||
const aliases = require('./scripts/create-symlinks/config.json');
|
||||
|
||||
// For a detailed explanation regarding each configuration property, visit:
|
||||
// https://jestjs.io/docs/en/configuration.html
|
||||
|
||||
module.exports = {
|
||||
// All imported modules in your tests should be mocked automatically
|
||||
// automock: false,
|
||||
|
||||
// Stop running tests after `n` failures
|
||||
// bail: 0,
|
||||
|
||||
// The directory where Jest should store its cached dependency information
|
||||
// cacheDirectory: "/private/var/folders/95/rl6ym4qj7vg7y7myqqlvjzzn4q2cvt/T/jest_kdlda2",
|
||||
|
||||
// Automatically clear mock calls and instances between every test
|
||||
clearMocks: true,
|
||||
|
||||
// Indicates whether the coverage information should be collected while executing the test
|
||||
// collectCoverage: false,
|
||||
|
||||
// An array of glob patterns indicating a set of files for which coverage information should be collected
|
||||
// collectCoverageFrom: undefined,
|
||||
|
||||
// The directory where Jest should output its coverage files
|
||||
// coverageDirectory: undefined,
|
||||
|
||||
// An array of regexp pattern strings used to skip coverage collection
|
||||
// coveragePathIgnorePatterns: [
|
||||
// "/node_modules/"
|
||||
// ],
|
||||
|
||||
// Indicates which provider should be used to instrument code for coverage
|
||||
// coverageProvider: "babel",
|
||||
|
||||
// A list of reporter names that Jest uses when writing coverage reports
|
||||
// coverageReporters: [
|
||||
// "json",
|
||||
// "text",
|
||||
// "lcov",
|
||||
// "clover"
|
||||
// ],
|
||||
|
||||
// An object that configures minimum threshold enforcement for coverage results
|
||||
// coverageThreshold: undefined,
|
||||
|
||||
// A path to a custom dependency extractor
|
||||
// dependencyExtractor: undefined,
|
||||
|
||||
// Make calling deprecated APIs throw helpful error messages
|
||||
// errorOnDeprecated: false,
|
||||
|
||||
// Force coverage collection from ignored files using an array of glob patterns
|
||||
// forceCoverageMatch: [],
|
||||
|
||||
// A path to a module which exports an async function that is triggered once before all test suites
|
||||
// globalSetup: undefined,
|
||||
|
||||
// A path to a module which exports an async function that is triggered once after all test suites
|
||||
// globalTeardown: undefined,
|
||||
|
||||
// A set of global variables that need to be available in all test environments
|
||||
// globals: {},
|
||||
|
||||
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
|
||||
// maxWorkers: "50%",
|
||||
|
||||
// An array of directory names to be searched recursively up from the requiring module's location
|
||||
// moduleDirectories: [
|
||||
// "node_modules"
|
||||
// ],
|
||||
|
||||
// An array of file extensions your modules use
|
||||
// moduleFileExtensions: [
|
||||
// "js",
|
||||
// "json",
|
||||
// "jsx",
|
||||
// "ts",
|
||||
// "tsx",
|
||||
// "node"
|
||||
// ],
|
||||
|
||||
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
|
||||
moduleNameMapper: Object.entries(aliases).reduce((acc, [key, aliasPath]) => ({
|
||||
...acc,
|
||||
[`^${key}(.*)$`]: `<rootDir>/${aliasPath}$1`,
|
||||
}), {}),
|
||||
|
||||
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
|
||||
// modulePathIgnorePatterns: [],
|
||||
|
||||
// Activates notifications for test results
|
||||
// notify: false,
|
||||
|
||||
// An enum that specifies notification mode. Requires { notify: true }
|
||||
// notifyMode: "failure-change",
|
||||
|
||||
// A preset that is used as a base for Jest's configuration
|
||||
// preset: undefined,
|
||||
|
||||
// Run tests from one or more projects
|
||||
// projects: undefined,
|
||||
|
||||
// Use this configuration option to add custom reporters to Jest
|
||||
// reporters: undefined,
|
||||
|
||||
// Automatically reset mock state between every test
|
||||
// resetMocks: false,
|
||||
|
||||
// Reset the module registry before running each individual test
|
||||
// resetModules: false,
|
||||
|
||||
// A path to a custom resolver
|
||||
// resolver: undefined,
|
||||
|
||||
// Automatically restore mock state between every test
|
||||
// restoreMocks: false,
|
||||
|
||||
// The root directory that Jest should scan for tests and modules within
|
||||
// rootDir: undefined,
|
||||
|
||||
// A list of paths to directories that Jest should use to search for files in
|
||||
// roots: [
|
||||
// "<rootDir>"
|
||||
// ],
|
||||
|
||||
// Allows you to use a custom runner instead of Jest's default test runner
|
||||
// runner: "jest-runner",
|
||||
|
||||
// The paths to modules that run some code to configure or set up the testing environment before each test
|
||||
// setupFiles: [],
|
||||
|
||||
// A list of paths to modules that run some code to configure or set up the testing framework before each test
|
||||
// setupFilesAfterEnv: [],
|
||||
|
||||
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
|
||||
// snapshotSerializers: [],
|
||||
|
||||
// The test environment that will be used for testing
|
||||
// testEnvironment: "jest-environment-jsdom",
|
||||
|
||||
// Options that will be passed to the testEnvironment
|
||||
// testEnvironmentOptions: {},
|
||||
|
||||
// Adds a location field to test results
|
||||
// testLocationInResults: false,
|
||||
|
||||
// The glob patterns Jest uses to detect test files
|
||||
testMatch: ['**/__tests__/**/*.(j|t)s?(x)', '**/?(*.)+(spec|test).(j|t)s?(x)'],
|
||||
|
||||
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
|
||||
testPathIgnorePatterns: [
|
||||
'(.*)/dist'
|
||||
],
|
||||
|
||||
// The regexp pattern or array of patterns that Jest uses to detect test files
|
||||
// testRegex: [],
|
||||
|
||||
// This option allows the use of a custom results processor
|
||||
// testResultsProcessor: undefined,
|
||||
|
||||
// This option allows use of a custom test runner
|
||||
// testRunner: "jasmine2",
|
||||
|
||||
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
|
||||
// testURL: "http://localhost",
|
||||
|
||||
// Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
|
||||
// timers: "real",
|
||||
|
||||
// A map from regular expressions to paths to transformers
|
||||
// transform: undefined,
|
||||
|
||||
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
|
||||
// transformIgnorePatterns: [
|
||||
// "/node_modules/"
|
||||
// ],
|
||||
|
||||
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
|
||||
// unmockedModulePathPatterns: undefined,
|
||||
|
||||
// Indicates whether each individual test should be reported during the run
|
||||
// verbose: undefined,
|
||||
|
||||
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
|
||||
// watchPathIgnorePatterns: [],
|
||||
|
||||
// Whether to use watchman for file crawling
|
||||
// watchman: true,
|
||||
};
|
||||
24609
package-lock.json
generated
Normal file
24609
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
78
package.json
Normal file
78
package.json
Normal file
@ -0,0 +1,78 @@
|
||||
{
|
||||
"name": "crypto-bot-frontend",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"symlinks": "node scripts/create-symlinks/index",
|
||||
"start": "webpack serve",
|
||||
"init": "npm run clean && npm i && npm run symlinks",
|
||||
"dev": "webpack serve --open false",
|
||||
"build": "webpack --mode=production",
|
||||
"eslint": "eslint -c .eslintrc.json src --fix",
|
||||
"tsc": "tsc --p ./tsconfig.json",
|
||||
"test": "jest",
|
||||
"prune": "node scripts/find-unused-code",
|
||||
"checks": "npm run eslint && npm run tsc && npm run prune && npm run test && npm run build",
|
||||
"clean": "rm -rf node_modules && rm -rf build"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://gitlab.vigdorov.ru/cryptobot/crypto-bot-frontend"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@reatom/core": "^1.1.5",
|
||||
"@reatom/react": "^1.1.5",
|
||||
"antd": "^4.16.2",
|
||||
"axios": "^0.21.0",
|
||||
"convert-layout": "^0.8.2",
|
||||
"date-fns": "^2.16.1",
|
||||
"fp-ts": "^2.8.5",
|
||||
"history": "^5.0.0",
|
||||
"lodash": "^4.17.20",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"ts-loader": "^8.0.7",
|
||||
"typescript": "^4.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.12.3",
|
||||
"@babel/preset-env": "^7.12.1",
|
||||
"@babel/preset-typescript": "^7.12.1",
|
||||
"@types/convert-layout": "^0.5.1",
|
||||
"@types/jest": "^26.0.15",
|
||||
"@types/lodash": "^4.14.165",
|
||||
"@types/node": "^12.19.1",
|
||||
"@types/react": "^16.9.53",
|
||||
"@types/react-dom": "^16.9.8",
|
||||
"@types/react-router-dom": "^5.1.6",
|
||||
"@types/uuid": "^8.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.6.0",
|
||||
"@typescript-eslint/parser": "^4.6.0",
|
||||
"babel-jest": "^26.6.1",
|
||||
"clean-webpack-plugin": "^3.0.0",
|
||||
"css-loader": "^5.0.0",
|
||||
"eslint": "^7.12.1",
|
||||
"eslint-config-prettier": "^6.15.0",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"eslint-plugin-jest": "^24.1.3",
|
||||
"eslint-plugin-prettier": "^3.1.4",
|
||||
"eslint-plugin-react": "^7.21.5",
|
||||
"eslint-plugin-react-hooks": "^4.2.0",
|
||||
"file-loader": "^6.2.0",
|
||||
"html-webpack-plugin": "^5.0.0-beta.1",
|
||||
"mini-css-extract-plugin": "^1.2.1",
|
||||
"prettier": "^2.1.2",
|
||||
"react-scripts": "^4.0.1",
|
||||
"sass": "^1.28.0",
|
||||
"sass-loader": "^10.0.4",
|
||||
"ts-prune": "^0.8.4",
|
||||
"webpack": "^5.3.2",
|
||||
"webpack-cli": "^4.1.0",
|
||||
"webpack-dev-server": "^3.11.0"
|
||||
}
|
||||
}
|
||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
18
public/index.html
Normal file
18
public/index.html
Normal file
@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="./favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="description" content="Web site created by cool programmers" />
|
||||
<title>Crypto bot: dev</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
14
scripts/create-symlinks/config.json
Normal file
14
scripts/create-symlinks/config.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"_api": "src/core/api",
|
||||
"_blocks": "src/core/blocks",
|
||||
"_consts": "src/core/consts",
|
||||
"_hooks": "src/core/hooks",
|
||||
"_hoks": "src/core/hoks",
|
||||
"_services": "src/core/services",
|
||||
"_types": "src/core/types",
|
||||
"_utils": "src/core/utils",
|
||||
"_infrastructure": "src/core/infrastructure",
|
||||
"_enums": "src/core/enums",
|
||||
"_referers": "src/core/referers",
|
||||
"_pages": "src/pages"
|
||||
}
|
||||
50
scripts/create-symlinks/index.js
Normal file
50
scripts/create-symlinks/index.js
Normal file
@ -0,0 +1,50 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const aliases = require('./config.json');
|
||||
const tsconfig = require('../../tsconfig.json');
|
||||
|
||||
const CURRENT_FOLDER = process.cwd();
|
||||
|
||||
function createTsConfigDev() {
|
||||
const tsConfigDev = {
|
||||
...tsconfig,
|
||||
compilerOptions: {
|
||||
...tsconfig.compilerOptions,
|
||||
paths: Object.entries(aliases).reduce((acc, [key, value]) => ({
|
||||
...acc,
|
||||
[`${key}/*`]: [`./${value}/*`],
|
||||
}), {}),
|
||||
}
|
||||
};
|
||||
fs.writeFileSync(path.resolve('tsconfig.dev.json'), JSON.stringify(tsConfigDev, null, 4));
|
||||
}
|
||||
|
||||
function createSymlinks() {
|
||||
if (!fs.existsSync('./node_modules')) {
|
||||
fs.mkdirSync('node_modules');
|
||||
}
|
||||
|
||||
try {
|
||||
for (const alias in aliases) {
|
||||
const folder = aliases[alias];
|
||||
const pathInPackage = path.resolve(CURRENT_FOLDER, folder);
|
||||
if (!fs.existsSync(folder)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const symlinkPath = path.resolve(`./node_modules/${alias}`);
|
||||
if (fs.existsSync(symlinkPath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
fs.symlinkSync(pathInPackage, symlinkPath);
|
||||
console.info(`${symlinkPath} --> ${pathInPackage}`);
|
||||
|
||||
}
|
||||
} catch (e) {
|
||||
console.info(`${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
createSymlinks();
|
||||
createTsConfigDev();
|
||||
5
scripts/find-unused-code/ignore-files.js
Normal file
5
scripts/find-unused-code/ignore-files.js
Normal file
@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
ignore: [
|
||||
'src/core',
|
||||
],
|
||||
};
|
||||
15
scripts/find-unused-code/index.js
Normal file
15
scripts/find-unused-code/index.js
Normal file
@ -0,0 +1,15 @@
|
||||
const runner = require("ts-prune/lib/runner");
|
||||
const {ignore} = require('./ignore-files');
|
||||
|
||||
const error = [];
|
||||
|
||||
runner.run({ project: 'tsconfig.dev.json' }, (text) => {
|
||||
if (ignore.every(ign => !text.includes(ign))) {
|
||||
error.push(text);
|
||||
}
|
||||
});
|
||||
setTimeout(() => {
|
||||
if (error.length) {
|
||||
throw new Error(`Присутствует не используемый код: \n${error.join('\n')}`);
|
||||
}
|
||||
}, 0);
|
||||
17
src/app/components/page/Page.scss
Normal file
17
src/app/components/page/Page.scss
Normal file
@ -0,0 +1,17 @@
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#root {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
42
src/app/components/page/Page.tsx
Normal file
42
src/app/components/page/Page.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import React, {Fragment, memo} from 'react';
|
||||
import {Route, Switch} from 'react-router-dom';
|
||||
import {Container, createStyles, makeStyles} from '@material-ui/core';
|
||||
import {createStore} from '@reatom/core';
|
||||
import {context} from '@reatom/react';
|
||||
import mainPageRouter from '_pages/main/routing';
|
||||
import NotFoundPage from '_pages/not-found/components/page';
|
||||
import './Page.scss';
|
||||
|
||||
const useStyles = makeStyles(() =>
|
||||
createStyles({
|
||||
container: {
|
||||
height: '100hv',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const Page: React.FC = () => {
|
||||
const classes = useStyles();
|
||||
const store = createStore();
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className={classes.container}>
|
||||
<context.Provider value={store}>
|
||||
<Container>
|
||||
<Switch>
|
||||
{mainPageRouter}
|
||||
<Route>
|
||||
<NotFoundPage />
|
||||
</Route>
|
||||
</Switch>
|
||||
</Container>
|
||||
</context.Provider>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Page);
|
||||
1
src/app/components/page/index.ts
Normal file
1
src/app/components/page/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export {default} from './Page';
|
||||
17
src/app/index.tsx
Normal file
17
src/app/index.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import {HashRouter} from 'react-router-dom';
|
||||
import App from './components/page';
|
||||
|
||||
ReactDOM.render(
|
||||
/*
|
||||
* Выключаем стрикт мод, пока не починят
|
||||
* https://github.com/mui-org/material-ui/issues/13394
|
||||
*/
|
||||
// <React.StrictMode>
|
||||
<HashRouter >
|
||||
<App />
|
||||
</HashRouter>,
|
||||
// </React.StrictMode>
|
||||
document.getElementById('root')
|
||||
);
|
||||
3
src/core/consts/common.ts
Normal file
3
src/core/consts/common.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export const ROUTES = {
|
||||
MAIN: '/'
|
||||
};
|
||||
3
src/core/dts/comon.d.ts
vendored
Normal file
3
src/core/dts/comon.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
type Undefinable<T> = T | undefined;
|
||||
|
||||
type Nullable<T> = T | undefined | null;
|
||||
16
src/core/hooks/useEqualMemo.ts
Normal file
16
src/core/hooks/useEqualMemo.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import {DependencyList, useEffect, useState} from 'react';
|
||||
|
||||
const emptyDependency: DependencyList = [];
|
||||
|
||||
export function useEqualMemo<T>(func: () => T, args: DependencyList = emptyDependency): T {
|
||||
const [memoized, memo] = useState<T>(func());
|
||||
useEffect(() => {
|
||||
const data = func();
|
||||
if (!isEqual(memoized, data)) {
|
||||
memo(data);
|
||||
}
|
||||
}, [memoized, func, args]);
|
||||
|
||||
return memoized;
|
||||
}
|
||||
10
src/core/hooks/useParams.ts
Normal file
10
src/core/hooks/useParams.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import {useMemo} from 'react';
|
||||
import {useParams as useReactParams} from 'react-router-dom';
|
||||
import {getParamsFromUrl} from '_utils/getParamFromUrl';
|
||||
import {QueryParsers} from '_utils/getQueryFromUrl';
|
||||
|
||||
export function useParams<T extends Record<string, unknown>>(paramParsers: QueryParsers<T>) {
|
||||
const params = useReactParams<Record<keyof T, string>>();
|
||||
|
||||
return useMemo(() => getParamsFromUrl(paramParsers, params), [params, paramParsers]);
|
||||
}
|
||||
9
src/core/hooks/useQuery.ts
Normal file
9
src/core/hooks/useQuery.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import {useMemo} from 'react';
|
||||
import {useLocation} from 'react-router-dom';
|
||||
import {getQueryFromUrl, QueryParsers} from '_utils/getQueryFromUrl';
|
||||
|
||||
export function useQuery<T extends Record<string, unknown>>(queryParsers: QueryParsers<T>): T {
|
||||
const {search} = useLocation();
|
||||
|
||||
return useMemo(() => getQueryFromUrl(queryParsers, search), [search, queryParsers]);
|
||||
}
|
||||
10
src/core/hooks/useToggle.ts
Normal file
10
src/core/hooks/useToggle.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import {useCallback, useState} from 'react';
|
||||
|
||||
export const useToggle = (initValue = false): [boolean, () => void] => {
|
||||
const [isToggle, onToggle] = useState(initValue);
|
||||
const handleToggle = useCallback(() => onToggle(state => !state), [onToggle]);
|
||||
return [
|
||||
isToggle,
|
||||
handleToggle,
|
||||
];
|
||||
};
|
||||
51
src/core/infrastructure/Http.ts
Normal file
51
src/core/infrastructure/Http.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import axios, {AxiosRequestConfig} from 'axios';
|
||||
|
||||
enum Method {
|
||||
Get = 'get',
|
||||
Delete = 'delete',
|
||||
Head = 'head',
|
||||
Options = 'options',
|
||||
Post = 'post',
|
||||
Put = 'put',
|
||||
Patch = 'patch',
|
||||
}
|
||||
|
||||
type RequestConfig<Q, B> = Omit<AxiosRequestConfig, 'params' | 'data'> & {
|
||||
params?: Q;
|
||||
data?: B;
|
||||
};
|
||||
|
||||
const requestMiddleware = async <Q, B, R>(config: RequestConfig<Q, B>): Promise<R> => {
|
||||
const axiosResponse = await axios.request<R>(config);
|
||||
// Добавить обработку ошибок
|
||||
return axiosResponse.data;
|
||||
};
|
||||
|
||||
const request = <Q, B, R>(config: RequestConfig<Q, B>) => requestMiddleware<Q, B, R>(config);
|
||||
|
||||
const requestWithoutBody = (method: Method) => <Q, R>(url: string, query?: Q) => {
|
||||
return request<Q, never, R>({
|
||||
method,
|
||||
url,
|
||||
params: query,
|
||||
});
|
||||
};
|
||||
|
||||
const requestWithBody = (method: Method) => <Q, B, R>(url: string, query?: Q, body?: B) => {
|
||||
return request<Q, B, R>({
|
||||
method,
|
||||
url,
|
||||
params: query,
|
||||
data: body,
|
||||
});
|
||||
};
|
||||
|
||||
export const http = {
|
||||
get: requestWithoutBody(Method.Get),
|
||||
delete: requestWithoutBody(Method.Delete),
|
||||
head: requestWithoutBody(Method.Head),
|
||||
options: requestWithoutBody(Method.Options),
|
||||
post: requestWithBody(Method.Post),
|
||||
put: requestWithBody(Method.Put),
|
||||
patch: requestWithBody(Method.Patch),
|
||||
};
|
||||
7
src/core/infrastructure/atom/exampleAtom.ts
Normal file
7
src/core/infrastructure/atom/exampleAtom.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import {declareAction, declareAtom} from '@reatom/core';
|
||||
|
||||
export const changeNameAction = declareAction<string>();
|
||||
|
||||
export const nameAtom = declareAtom('', on => [
|
||||
on(changeNameAction, (_state, payload) => payload),
|
||||
]);
|
||||
54
src/core/referers/__test__/common.test.ts
Normal file
54
src/core/referers/__test__/common.test.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import {noop} from 'lodash';
|
||||
import {isEmptyObject, isNotEmpty, isObject} from '../common';
|
||||
|
||||
describe('isObject', () => {
|
||||
it('Должен вернуть true', () => {
|
||||
expect(isObject({})).toBeTruthy();
|
||||
expect(isObject({go: 8})).toBeTruthy();
|
||||
expect(isObject(Object.create(null))).toBeTruthy();
|
||||
expect(isObject(Object.create({}))).toBeTruthy();
|
||||
});
|
||||
|
||||
it('Должен вернуть false', () => {
|
||||
expect(isObject(null)).toBeFalsy();
|
||||
expect(isObject([])).toBeFalsy();
|
||||
expect(isObject(NaN)).toBeFalsy();
|
||||
expect(isObject(noop)).toBeFalsy();
|
||||
expect(isObject(0)).toBeFalsy();
|
||||
expect(isObject('')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isEmptyObject', () => {
|
||||
it('Должен вернуть true', () => {
|
||||
expect(isEmptyObject({})).toBeTruthy();
|
||||
expect(isEmptyObject(Object.create(null))).toBeTruthy();
|
||||
});
|
||||
|
||||
it('Должен вернуть false', () => {
|
||||
expect(isEmptyObject({g: 'g'})).toBeFalsy();
|
||||
expect(isEmptyObject({g: undefined})).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isNotEmpty', () => {
|
||||
it('Должен вернуть true', () => {
|
||||
expect(isNotEmpty(['3'])).toBeTruthy();
|
||||
expect(isNotEmpty({f: 'f'})).toBeTruthy();
|
||||
expect(isNotEmpty({f: undefined})).toBeTruthy();
|
||||
expect(isNotEmpty(0)).toBeTruthy();
|
||||
expect(isNotEmpty(12)).toBeTruthy();
|
||||
expect(isNotEmpty('fd')).toBeTruthy();
|
||||
expect(isNotEmpty('0')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('Должен вернуть false', () => {
|
||||
expect(isNotEmpty([])).toBeFalsy();
|
||||
expect(isNotEmpty({})).toBeFalsy();
|
||||
expect(isNotEmpty('')).toBeFalsy();
|
||||
expect(isNotEmpty(' ')).toBeFalsy();
|
||||
expect(isNotEmpty()).toBeFalsy();
|
||||
expect(isNotEmpty(null)).toBeFalsy();
|
||||
expect(isNotEmpty(NaN)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
48
src/core/referers/common.ts
Normal file
48
src/core/referers/common.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import {isNaN, isNumber, isString} from 'lodash';
|
||||
|
||||
export function isNullable<T>(value: Nullable<T>): value is (null | undefined) {
|
||||
return value === null || value === undefined;
|
||||
}
|
||||
|
||||
export function isNotNullable<T>(value: Nullable<T>): value is NonNullable<T> {
|
||||
return !isNullable(value);
|
||||
}
|
||||
|
||||
export function isObject<T>(value: Nullable<T>): value is T {
|
||||
return (
|
||||
isNotNullable(value)
|
||||
&& !Array.isArray(value)
|
||||
&& !isNaN(value)
|
||||
&& typeof value === 'object'
|
||||
);
|
||||
}
|
||||
|
||||
export const isEmptyObject = <T>(value: Nullable<T>): boolean => (
|
||||
!Object.keys(value ?? {}).length
|
||||
);
|
||||
|
||||
export const isNotEmpty = <T>(value?: Nullable<T>): value is T => {
|
||||
if (isString(value)) {
|
||||
return !!value?.trim();
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return !!value.length;
|
||||
}
|
||||
|
||||
if (isNaN(value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isNumber(value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isObject(value)) {
|
||||
return !isEmptyObject(value);
|
||||
}
|
||||
|
||||
return isNotNullable(value);
|
||||
};
|
||||
|
||||
export const isEmpty = (value: unknown) => !isNotEmpty(value);
|
||||
21
src/core/services/LocalStorageService.ts
Normal file
21
src/core/services/LocalStorageService.ts
Normal file
@ -0,0 +1,21 @@
|
||||
export const makeLocalStorageService = <T>(init: T, stateName: string) => {
|
||||
if (!localStorage.getItem(stateName)) {
|
||||
localStorage.setItem(stateName, JSON.stringify(init));
|
||||
}
|
||||
|
||||
return {
|
||||
set: (updatedState: T) => {
|
||||
localStorage.setItem(stateName, JSON.stringify(updatedState));
|
||||
return updatedState;
|
||||
},
|
||||
get: (): T => {
|
||||
const stringValue = localStorage.getItem(stateName) || '';
|
||||
|
||||
try {
|
||||
return JSON.parse(stringValue);
|
||||
} catch (e) {
|
||||
return init;
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
27
src/core/utils/__test__/getParamFromUrl.test.ts
Normal file
27
src/core/utils/__test__/getParamFromUrl.test.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import {getParamsFromUrl} from '../getParamFromUrl';
|
||||
import {QueryParsers} from '../getQueryFromUrl';
|
||||
import {booleanParser, numberParser, stringParser} from '../queryParsers';
|
||||
|
||||
describe('getParamsFromUrl', () => {
|
||||
type Params = {
|
||||
name?: string;
|
||||
id?: number;
|
||||
isNew: boolean;
|
||||
};
|
||||
|
||||
it('Получение параметров', () => {
|
||||
const parsers: QueryParsers<Params> = {
|
||||
name: stringParser(),
|
||||
id: numberParser(),
|
||||
isNew: booleanParser(false),
|
||||
};
|
||||
expect(getParamsFromUrl(parsers, {olo: 't'})).toEqual({isNew: false});
|
||||
expect(getParamsFromUrl(parsers, {name: 't'})).toEqual({name: 't', isNew: false});
|
||||
expect(getParamsFromUrl(parsers, {
|
||||
name: 't',
|
||||
id: '6',
|
||||
pageType: 'tags',
|
||||
isNew: 'true',
|
||||
})).toEqual({name: 't', id: 6, isNew: true});
|
||||
});
|
||||
});
|
||||
28
src/core/utils/__test__/getQueryParamFromUrl.test.ts
Normal file
28
src/core/utils/__test__/getQueryParamFromUrl.test.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import {getQueryFromUrl, QueryParsers} from '../getQueryFromUrl';
|
||||
import {arrayParser, booleanParser, numberParser, stringParser} from '../queryParsers';
|
||||
|
||||
describe('getQueryFromUrl', () => {
|
||||
type Query = {
|
||||
name?: string;
|
||||
id?: number;
|
||||
isNew: boolean;
|
||||
colors: string[];
|
||||
};
|
||||
|
||||
it('Получение параметров', () => {
|
||||
const parsers: QueryParsers<Query> = {
|
||||
name: stringParser(),
|
||||
id: numberParser(),
|
||||
isNew: booleanParser(false),
|
||||
colors: arrayParser([]),
|
||||
};
|
||||
expect(getQueryFromUrl(parsers, '?olo=2')).toEqual({isNew: false, colors: []});
|
||||
expect(getQueryFromUrl(parsers, '?olo=2&name=t')).toEqual({name: 't', isNew: false, colors: []});
|
||||
expect(getQueryFromUrl(parsers, '?name=t&id=6&pageType=tags&isNew=true&colors=red&colors=blue')).toEqual({
|
||||
name: 't',
|
||||
id: 6,
|
||||
isNew: true,
|
||||
colors: ['red', 'blue'],
|
||||
});
|
||||
});
|
||||
});
|
||||
31
src/core/utils/__test__/jsonParse.test.ts
Normal file
31
src/core/utils/__test__/jsonParse.test.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import {jsonParse} from '../jsonParse';
|
||||
|
||||
describe('jsonParse', () => {
|
||||
it('Должен вернуть значение', () => {
|
||||
expect(jsonParse('{}')).toEqual({});
|
||||
expect(jsonParse('null')).toBeNull();
|
||||
expect(jsonParse('0')).toBe(0);
|
||||
expect(jsonParse(' 1 ')).toBe(1);
|
||||
});
|
||||
|
||||
it('Должен вернуть значение при наличии дефолта', () => {
|
||||
expect(jsonParse('{}', {str: 9})).toEqual({});
|
||||
expect(jsonParse('null', {str: 9})).toBeNull();
|
||||
expect(jsonParse('0', {str: 9})).toBe(0);
|
||||
expect(jsonParse(' 1 ', {str: 9})).toBe(1);
|
||||
});
|
||||
|
||||
it('Должен вернуть undefined для не корректных значений', () => {
|
||||
expect(jsonParse()).toBeUndefined();
|
||||
expect(jsonParse('')).toBeUndefined();
|
||||
expect(jsonParse(' ')).toBeUndefined();
|
||||
expect(jsonParse('{"9')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('Должен вернуть дефолтное значение', () => {
|
||||
expect(jsonParse(undefined, 'to')).toBe('to');
|
||||
expect(jsonParse('', 'to')).toBe('to');
|
||||
expect(jsonParse(' ', 'to')).toBe('to');
|
||||
expect(jsonParse('./6dh', 9)).toBe(9);
|
||||
});
|
||||
});
|
||||
25
src/core/utils/__test__/parsers.test.ts
Normal file
25
src/core/utils/__test__/parsers.test.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import {toNumber} from '../parsers';
|
||||
|
||||
describe('toNumber', () => {
|
||||
it('Возвращает число', () => {
|
||||
expect(toNumber(0)).toBe(0);
|
||||
expect(toNumber(' 0 ')).toBe(0);
|
||||
expect(toNumber('0')).toBe(0);
|
||||
expect(toNumber('56')).toBe(56);
|
||||
expect(toNumber(' 56 ')).toBe(56);
|
||||
expect(toNumber(' 5.6 ')).toBe(5.6);
|
||||
expect(toNumber(' .9 ')).toBe(0.9);
|
||||
expect(toNumber(1.4)).toBe(1.4);
|
||||
expect(toNumber(.4)).toBe(0.4);
|
||||
});
|
||||
|
||||
it('Возвращает undefined', () => {
|
||||
expect(toNumber(' g')).toBeUndefined();
|
||||
expect(toNumber(null)).toBeUndefined();
|
||||
expect(toNumber(undefined)).toBeUndefined();
|
||||
expect(toNumber({})).toBeUndefined();
|
||||
expect(toNumber([])).toBeUndefined();
|
||||
expect(toNumber(' 4 5 ')).toBeUndefined();
|
||||
expect(toNumber(' 4 .5 ')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
43
src/core/utils/__test__/performTextSearch.test.ts
Normal file
43
src/core/utils/__test__/performTextSearch.test.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import {performTextSearch} from '../performTextSearch';
|
||||
|
||||
describe('performTextSearch', () => {
|
||||
it('Проверка отфильтрованных значений', () => {
|
||||
const items = [
|
||||
{name: 'Иван', age: 6, color: 'red'},
|
||||
{name: 'Сергей', age: 12, color: 'white'},
|
||||
{name: 'John', age: 33, color: 'black'},
|
||||
{name: 'James', age: 43, color: 'yellow'},
|
||||
];
|
||||
|
||||
expect(performTextSearch(items, 'ива', ['name'])).toEqual([
|
||||
{name: 'Иван', age: 6, color: 'red'},
|
||||
]);
|
||||
expect(performTextSearch(items, 'cth', ['name'])).toEqual([
|
||||
{name: 'Сергей', age: 12, color: 'white'},
|
||||
]);
|
||||
expect(performTextSearch(items, 'utq', ['name'])).toEqual([
|
||||
{name: 'Сергей', age: 12, color: 'white'},
|
||||
]);
|
||||
expect(performTextSearch(items, 'о', ['name'])).toEqual([
|
||||
{name: 'John', age: 33, color: 'black'},
|
||||
{name: 'James', age: 43, color: 'yellow'},
|
||||
]);
|
||||
expect(performTextSearch(items, 'e', ['name', 'color'])).toEqual([
|
||||
{name: 'Иван', age: 6, color: 'red'},
|
||||
{name: 'Сергей', age: 12, color: 'white'},
|
||||
{name: 'James', age: 43, color: 'yellow'},
|
||||
]);
|
||||
expect(performTextSearch(items, '3', ['age'])).toEqual([
|
||||
{name: 'John', age: 33, color: 'black'},
|
||||
{name: 'James', age: 43, color: 'yellow'},
|
||||
]);
|
||||
expect(performTextSearch(items, '', ['age'])).toEqual([
|
||||
{name: 'Иван', age: 6, color: 'red'},
|
||||
{name: 'Сергей', age: 12, color: 'white'},
|
||||
{name: 'John', age: 33, color: 'black'},
|
||||
{name: 'James', age: 43, color: 'yellow'},
|
||||
]);
|
||||
expect(performTextSearch(items, 'sdfsse23', ['age'])).toEqual([]);
|
||||
expect(performTextSearch([], 'sdfsse23', ['age'])).toEqual([]);
|
||||
});
|
||||
});
|
||||
98
src/core/utils/__test__/queryParsers.test.ts
Normal file
98
src/core/utils/__test__/queryParsers.test.ts
Normal file
@ -0,0 +1,98 @@
|
||||
import {arrayParser, booleanParser, numberParser, stringParser} from '../queryParsers';
|
||||
|
||||
describe('stringParser', () => {
|
||||
it('Вернет значение', () => {
|
||||
expect(stringParser()('trt')).toBe('trt');
|
||||
});
|
||||
|
||||
it('Вернет первое значение массива', () => {
|
||||
expect(stringParser()(['trt', 'ggt'])).toBe('trt');
|
||||
});
|
||||
|
||||
it('Вернет undefined', () => {
|
||||
expect(stringParser()()).toBeUndefined();
|
||||
expect(stringParser()([])).toBeUndefined();
|
||||
});
|
||||
|
||||
it('Вернет дефолт', () => {
|
||||
expect(stringParser('def')()).toBe('def');
|
||||
expect(stringParser('def')([])).toBe('def');
|
||||
});
|
||||
|
||||
it('Вернет значение даже при наличии дефолта', () => {
|
||||
expect(stringParser('def')(['trt'])).toBe('trt');
|
||||
expect(stringParser('def')('trt')).toBe('trt');
|
||||
});
|
||||
});
|
||||
|
||||
describe('numberParser', () => {
|
||||
it('Вернет значение', () => {
|
||||
expect(numberParser()('45')).toBe(45);
|
||||
});
|
||||
|
||||
it('Вернет первое значение массива', () => {
|
||||
expect(numberParser()(['45', '44'])).toBe(45);
|
||||
});
|
||||
|
||||
it('Вернет undefined', () => {
|
||||
expect(numberParser()()).toBeUndefined();
|
||||
expect(numberParser()([])).toBeUndefined();
|
||||
});
|
||||
|
||||
it('Вернет дефолт', () => {
|
||||
expect(numberParser(33)()).toBe(33);
|
||||
expect(numberParser(33)([])).toBe(33);
|
||||
});
|
||||
|
||||
it('Вернет значение даже при наличии дефолта', () => {
|
||||
expect(numberParser(33)(['45'])).toBe(45);
|
||||
expect(numberParser(33)('45')).toBe(45);
|
||||
});
|
||||
});
|
||||
|
||||
describe('booleanParser', () => {
|
||||
it('Вернет значение', () => {
|
||||
expect(booleanParser()('true')).toBe(true);
|
||||
expect(booleanParser()('false')).toBe(false);
|
||||
});
|
||||
|
||||
it('Вернет первое значение массива', () => {
|
||||
expect(booleanParser()(['true', 'false'])).toBe(true);
|
||||
});
|
||||
|
||||
it('Вернет undefined', () => {
|
||||
expect(booleanParser()()).toBeUndefined();
|
||||
expect(booleanParser()([])).toBeUndefined();
|
||||
});
|
||||
|
||||
it('Вернет дефолт', () => {
|
||||
expect(booleanParser(true)()).toBe(true);
|
||||
expect(booleanParser(false)([])).toBe(false);
|
||||
});
|
||||
|
||||
it('Вернет значение даже при наличии дефолта', () => {
|
||||
expect(booleanParser(false)(['true'])).toBe(true);
|
||||
expect(booleanParser(false)('true')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('arrayParser', () => {
|
||||
it('Вернет значение', () => {
|
||||
expect(arrayParser()('rtr')).toEqual(['rtr']);
|
||||
expect(arrayParser()(['rtr', 'rtr'])).toEqual(['rtr', 'rtr']);
|
||||
expect(arrayParser()([])).toEqual([]);
|
||||
});
|
||||
|
||||
it('Вернет undefined', () => {
|
||||
expect(arrayParser()()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('Вернет дефолт', () => {
|
||||
expect(arrayParser(['def'])()).toEqual(['def']);
|
||||
});
|
||||
|
||||
it('Вернет значение даже при наличии дефолта', () => {
|
||||
expect(arrayParser(['def'])('rtr')).toEqual(['rtr']);
|
||||
expect(arrayParser(['def'])(['rtr'])).toEqual(['rtr']);
|
||||
});
|
||||
});
|
||||
16
src/core/utils/__test__/toArray.test.ts
Normal file
16
src/core/utils/__test__/toArray.test.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import {toArray} from '../toArray';
|
||||
|
||||
describe('toArray', () => {
|
||||
it('Должен вернуть пустой массив', () => {
|
||||
expect(toArray(undefined)).toEqual([]);
|
||||
expect(toArray([])).toEqual([]);
|
||||
});
|
||||
|
||||
it('Должен вернуть массив', () => {
|
||||
expect(toArray('hji')).toEqual(['hji']);
|
||||
expect(toArray(null)).toEqual([null]);
|
||||
expect(toArray(0)).toEqual([0]);
|
||||
expect(toArray([0, null, 'gh'])).toEqual([0, null, 'gh']);
|
||||
expect(toArray([0, [null], 'gh'])).toEqual([0, [null], 'gh']);
|
||||
});
|
||||
});
|
||||
26
src/core/utils/__test__/toRequestParamValue.test.ts
Normal file
26
src/core/utils/__test__/toRequestParamValue.test.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import {toRequestParamValue} from '../toRequestParamValue';
|
||||
|
||||
describe('toRequestParamValue', () => {
|
||||
it('Возвращает простые значения', () => {
|
||||
expect(toRequestParamValue('trt')).toBe('trt');
|
||||
expect(toRequestParamValue(0)).toBe(0);
|
||||
expect(toRequestParamValue(false)).toBe(false);
|
||||
});
|
||||
|
||||
it('Простые пустые значения возвращают undefined', () => {
|
||||
expect(toRequestParamValue('')).toBeUndefined();
|
||||
expect(toRequestParamValue(null)).toBeUndefined();
|
||||
expect(toRequestParamValue(undefined)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('Возвращает заполненные объекты', () => {
|
||||
expect(toRequestParamValue({foo: undefined})).toMatchObject({foo: undefined});
|
||||
expect(toRequestParamValue({foo: 'bar'})).toMatchObject({foo: 'bar'});
|
||||
expect(toRequestParamValue(['rtt'])).toEqual(['rtt']);
|
||||
});
|
||||
|
||||
it('Пустые объекты возвращают undefined', () => {
|
||||
expect(toRequestParamValue({})).toBeUndefined();
|
||||
expect(toRequestParamValue([])).toBeUndefined();
|
||||
});
|
||||
});
|
||||
15
src/core/utils/getParamFromUrl.ts
Normal file
15
src/core/utils/getParamFromUrl.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import {QueryParsers} from './getQueryFromUrl';
|
||||
|
||||
export const getParamsFromUrl = <T extends Record<string, unknown>>(
|
||||
paramParsers: QueryParsers<T>,
|
||||
params: Record<string, string>
|
||||
) => {
|
||||
return Object.keys(paramParsers).reduce<T>((memo, key) => {
|
||||
const parser = paramParsers[key];
|
||||
|
||||
return {
|
||||
...memo,
|
||||
[key]: parser?.(params[key]),
|
||||
};
|
||||
}, {} as T);
|
||||
};
|
||||
19
src/core/utils/getQueryFromUrl.ts
Normal file
19
src/core/utils/getQueryFromUrl.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import {decode} from 'querystring';
|
||||
|
||||
export type QueryParser<T> = (value?: string | string[]) => T;
|
||||
export type QueryParsers<T> = {[K in keyof T]: QueryParser<T[K]>};
|
||||
|
||||
export const getQueryFromUrl = <T extends Record<string, unknown>>(queryParsers: QueryParsers<T>, search?: string) => {
|
||||
const query = decode((search || location.search).slice(1));
|
||||
|
||||
return Object.keys(queryParsers).reduce<T>((memo, key) => {
|
||||
if (key in queryParsers) {
|
||||
const parser = queryParsers[key];
|
||||
return {
|
||||
...memo,
|
||||
[key]: parser?.(query[key]),
|
||||
};
|
||||
}
|
||||
return memo;
|
||||
}, {} as T);
|
||||
};
|
||||
10
src/core/utils/jsonParse.ts
Normal file
10
src/core/utils/jsonParse.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export const jsonParse = <T>(str?: string, defaultValue?: T): Undefinable<T> => {
|
||||
const trimStr = str?.trim();
|
||||
try {
|
||||
const parsedValue = JSON.parse(trimStr ?? '');
|
||||
|
||||
return parsedValue === undefined ? defaultValue : parsedValue;
|
||||
} catch (e) {
|
||||
return defaultValue;
|
||||
}
|
||||
};
|
||||
3
src/core/utils/jsonStringify.ts
Normal file
3
src/core/utils/jsonStringify.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export const jsonStringify = <T>(obj: T, space = 4): string => (
|
||||
JSON.stringify(obj, null, space)
|
||||
);
|
||||
3
src/core/utils/objectEntries.ts
Normal file
3
src/core/utils/objectEntries.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export const objectEntries = <T extends Record<string, unknown>, R extends keyof T>(obj: T) => (
|
||||
Object.entries(obj) as Array<[R, T[R]]>
|
||||
);
|
||||
3
src/core/utils/objectKeys.ts
Normal file
3
src/core/utils/objectKeys.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export const objectKeys = <T extends Record<string, unknown>>(obj: T) => (
|
||||
Object.keys(obj) as Array<keyof T>
|
||||
);
|
||||
8
src/core/utils/parsers.ts
Normal file
8
src/core/utils/parsers.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import {isNumber, isString} from 'lodash';
|
||||
|
||||
export const toNumber = (value: unknown): Undefinable<number> => {
|
||||
if (isNumber(value) || isString(value)) {
|
||||
const prepareValue = Number(value);
|
||||
return Number.isNaN(prepareValue) ? undefined : prepareValue;
|
||||
}
|
||||
};
|
||||
29
src/core/utils/performTextSearch.ts
Normal file
29
src/core/utils/performTextSearch.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import ru from 'convert-layout/ru';
|
||||
import {isEmpty, isNotEmpty} from '_referers/common';
|
||||
|
||||
export function performTextSearch<T, K extends keyof T>(items: T[], searchText: string, searchProperties: K[]) {
|
||||
if (isEmpty(items) || isEmpty(searchText)) {
|
||||
return items;
|
||||
}
|
||||
|
||||
const query = searchText.toLowerCase();
|
||||
const queryToEn = ru.toEn(query);
|
||||
const queryToRu = ru.fromEn(query);
|
||||
|
||||
const hasQuery = (itemText: string) => {
|
||||
const text = itemText.toLowerCase();
|
||||
|
||||
/**
|
||||
* Т.к. convert-layout заменяет не все символы верно,
|
||||
* ищем так же по первоначальной строке
|
||||
* https://github.com/ai/convert-layout/issues/22
|
||||
*/
|
||||
return text.includes(query) || text.includes(queryToEn) || text.includes(queryToRu);
|
||||
};
|
||||
|
||||
return items.filter(item => searchProperties.some(property => {
|
||||
const propertyValue = item[property];
|
||||
const text = isNotEmpty(propertyValue) ? `${propertyValue}` : undefined;
|
||||
return text && hasQuery(text);
|
||||
}));
|
||||
}
|
||||
53
src/core/utils/queryParsers.ts
Normal file
53
src/core/utils/queryParsers.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import {head} from 'lodash';
|
||||
import {QueryParser} from './getQueryFromUrl';
|
||||
import {toNumber} from './parsers';
|
||||
|
||||
export function stringParser<T extends string>(): QueryParser<Undefinable<T>>;
|
||||
export function stringParser<T extends string>(defaultValue: T): QueryParser<T>;
|
||||
export function stringParser<T extends string>(defaultValue?: T) {
|
||||
return (val?: string | string[]) => {
|
||||
const value = Array.isArray(val) ? head(val) : val;
|
||||
|
||||
return value ?? defaultValue;
|
||||
};
|
||||
}
|
||||
|
||||
export function numberParser(): QueryParser<Undefinable<number>>;
|
||||
export function numberParser(defaultValue?: number): QueryParser<number>;
|
||||
export function numberParser(defaultValue?: number) {
|
||||
return (val?: string | string[]) => {
|
||||
const value = Array.isArray(val) ? head(val) : val;
|
||||
|
||||
return toNumber(value) ?? defaultValue;
|
||||
};
|
||||
}
|
||||
|
||||
export function booleanParser(): QueryParser<Undefinable<boolean>>;
|
||||
export function booleanParser(defaultValue: boolean): QueryParser<boolean>;
|
||||
export function booleanParser(defaultValue?: boolean) {
|
||||
return (val?: string | string[]) => {
|
||||
const value = Array.isArray(val) ? head(val) : val;
|
||||
|
||||
if (value === 'true') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (value === 'false') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
};
|
||||
}
|
||||
|
||||
export function arrayParser<T extends string>(): QueryParser<Undefinable<T[]>>;
|
||||
export function arrayParser<T extends string>(defaultValue: T[]): QueryParser<T[]>;
|
||||
export function arrayParser<T extends string>(defaultValue?: T[]) {
|
||||
return (val?: string | string[]) => {
|
||||
if (Array.isArray(val)) {
|
||||
return val;
|
||||
}
|
||||
|
||||
return val === undefined ? defaultValue : [val];
|
||||
};
|
||||
}
|
||||
9
src/core/utils/toArray.ts
Normal file
9
src/core/utils/toArray.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export const toArray = <T>(value?: T | T[]): T[] => {
|
||||
if (Array.isArray(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return [
|
||||
...(value !== undefined ? [value] : [])
|
||||
];
|
||||
};
|
||||
7
src/core/utils/toRequestParamValue.ts
Normal file
7
src/core/utils/toRequestParamValue.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import {isNotEmpty} from '_referers/common';
|
||||
|
||||
export function toRequestParamValue<T>(val: T): T;
|
||||
export function toRequestParamValue<T>(val?: T): Undefinable<T>;
|
||||
export function toRequestParamValue<T>(val?: T) {
|
||||
return isNotEmpty(val) ? val : undefined;
|
||||
}
|
||||
33
src/core/utils/triggerLink.ts
Normal file
33
src/core/utils/triggerLink.ts
Normal file
@ -0,0 +1,33 @@
|
||||
type Options = {
|
||||
download?: boolean | string;
|
||||
target?: '_self' | '_blank';
|
||||
};
|
||||
|
||||
const DEFAULT_OPTIONS = {
|
||||
target: '_self',
|
||||
download: false,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Использование этой функции требуется для открытия ссылок в новых
|
||||
* вкладках из методов сервиса. Внутри компонентов его не используем.
|
||||
*/
|
||||
export const triggerLink = (link: string, options?: Options) => {
|
||||
const finalOptions = {
|
||||
...DEFAULT_OPTIONS,
|
||||
...options
|
||||
};
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.href = link;
|
||||
a.target = finalOptions.target;
|
||||
|
||||
if (finalOptions.download === true) {
|
||||
a.download = 'yes';
|
||||
} else if (typeof finalOptions.download === 'string') {
|
||||
a.download = finalOptions.download;
|
||||
}
|
||||
|
||||
a.dispatchEvent(new MouseEvent('click'));
|
||||
document.removeChild(a);
|
||||
};
|
||||
21
src/pages/main/components/page/Page.tsx
Normal file
21
src/pages/main/components/page/Page.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import React, {memo} from 'react';
|
||||
import {changeNameAction, nameAtom} from '_infrastructure/atom/exampleAtom';
|
||||
import {useAction, useAtom} from '@reatom/react';
|
||||
|
||||
const MainPage: React.FC = () => {
|
||||
const name = useAtom(nameAtom);
|
||||
const handleChangeName = useAction(e => changeNameAction(e.currentTarget.value));
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>main page</div>
|
||||
|
||||
<form>
|
||||
<label htmlFor="name">Enter your name: </label>
|
||||
<input id="name" value={name} onChange={handleChangeName} />
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(MainPage);
|
||||
1
src/pages/main/components/page/index.ts
Normal file
1
src/pages/main/components/page/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export {default} from './Page';
|
||||
8
src/pages/main/routing.tsx
Normal file
8
src/pages/main/routing.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
import React from 'react';
|
||||
import {Route} from 'react-router-dom';
|
||||
import {ROUTES} from '_consts/common';
|
||||
import Page from './components/page';
|
||||
|
||||
export default (
|
||||
<Route component={Page} path={ROUTES.MAIN} exact />
|
||||
);
|
||||
9
src/pages/not-found/components/page/Page.tsx
Normal file
9
src/pages/not-found/components/page/Page.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import React, {memo} from 'react';
|
||||
|
||||
const NotFoundPage: React.FC = () => {
|
||||
return (
|
||||
<div>404: Not Found Page</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(NotFoundPage);
|
||||
1
src/pages/not-found/components/page/index.ts
Normal file
1
src/pages/not-found/components/page/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export {default} from './Page';
|
||||
27
tsconfig.json
Normal file
27
tsconfig.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"outDir": "./dist/",
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": false,
|
||||
"noEmit": false,
|
||||
"jsx": "react",
|
||||
"paths": {
|
||||
"*": ["*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx"]
|
||||
}
|
||||
92
webpack.config.js
Normal file
92
webpack.config.js
Normal file
@ -0,0 +1,92 @@
|
||||
const path = require('path');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const {CleanWebpackPlugin} = require('clean-webpack-plugin');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
const webpack = require('webpack');
|
||||
const aliases = require('./scripts/create-symlinks/config.json');
|
||||
|
||||
module.exports = {
|
||||
mode: 'development',
|
||||
entry: {
|
||||
app: {
|
||||
import: './src/app/index.tsx',
|
||||
dependOn: [
|
||||
'rct',
|
||||
],
|
||||
},
|
||||
'rct': ['react', 'react-dom', 'react-router-dom'],
|
||||
},
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'build'),
|
||||
filename: '[fullhash].[name].js',
|
||||
},
|
||||
devServer: {
|
||||
contentBase: './build',
|
||||
historyApiFallback: true,
|
||||
compress: true,
|
||||
open: true,
|
||||
port: 3189,
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.tsx', '.ts', '.js'],
|
||||
alias: Object.entries(aliases).reduce((acc, [key, aliasPath]) => ({
|
||||
...acc,
|
||||
[key]: path.resolve(__dirname, `${aliasPath}/`),
|
||||
}), {}),
|
||||
},
|
||||
optimization: {
|
||||
splitChunks: {
|
||||
chunks: 'all',
|
||||
cacheGroups: {
|
||||
defaultVendors: {
|
||||
test: /[\\/]node_modules[\\/]/,
|
||||
enforce: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.[tj]sx?$/,
|
||||
use: 'ts-loader',
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpe?g|gif|ico)$/i,
|
||||
use: [
|
||||
{
|
||||
loader: 'file-loader',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.(txt|json)$/i,
|
||||
use: [
|
||||
{
|
||||
loader: 'file-loader',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.(sa|sc|c)ss$/,
|
||||
use: [
|
||||
MiniCssExtractPlugin.loader,
|
||||
'css-loader',
|
||||
'sass-loader',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin({
|
||||
template: './public/index.html',
|
||||
filename: 'index.html',
|
||||
favicon: './public/favicon.ico'
|
||||
}),
|
||||
new MiniCssExtractPlugin({
|
||||
filename: '[fullhash].[name].css',
|
||||
}),
|
||||
new CleanWebpackPlugin(),
|
||||
],
|
||||
};
|
||||
Reference in New Issue
Block a user