init
This commit is contained in:
10
.eslintignore
Normal file
10
.eslintignore
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
/node_modules
|
||||||
|
/dist
|
||||||
|
/out
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
/src/reportWebVitals.ts
|
||||||
|
/src/setupTests.ts
|
||||||
|
webpack.config.js
|
||||||
|
jest.config.js
|
||||||
|
babel.config.js
|
||||||
148
.eslintrc.json
Normal file
148
.eslintrc.json
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"es2021": true,
|
||||||
|
"jest/globals": true
|
||||||
|
},
|
||||||
|
"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": "(useTypedQuery|useTypedParams)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"jsx-a11y/label-has-associated-control": 0,
|
||||||
|
"eqeqeq": "error",
|
||||||
|
"import/extensions": 0,
|
||||||
|
"react/prop-types": 0,
|
||||||
|
"no-underscore-dangle": 0,
|
||||||
|
"import/imports-first": ["error", "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/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",
|
||||||
|
"no-unused-vars": "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": "warn",
|
||||||
|
"@typescript-eslint/no-use-before-define": "off",
|
||||||
|
"@typescript-eslint/no-namespace": "off",
|
||||||
|
"@typescript-eslint/explicit-module-boundary-types": "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"
|
||||||
|
}
|
||||||
|
}
|
||||||
25
.gitignore
vendored
Normal file
25
.gitignore
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# 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*
|
||||||
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"
|
||||||
|
}
|
||||||
19
README.md
Normal file
19
README.md
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
Install App dependencies:
|
||||||
|
```
|
||||||
|
npm i
|
||||||
|
```
|
||||||
|
---
|
||||||
|
Start App and open page in your browser:
|
||||||
|
```
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
---
|
||||||
|
Start App without open:
|
||||||
|
```
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
---
|
||||||
|
Build your App:
|
||||||
|
```
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
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'
|
||||||
|
],
|
||||||
|
};
|
||||||
189
jest.config.js
Normal file
189
jest.config.js
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
// 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: {},
|
||||||
|
|
||||||
|
// 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__/**/*.[jt]s?(x)",
|
||||||
|
// "**/?(*.)+(spec|test).[tj]s?(x)"
|
||||||
|
// ],
|
||||||
|
|
||||||
|
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
|
||||||
|
// testPathIgnorePatterns: [
|
||||||
|
// "/node_modules/"
|
||||||
|
// ],
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
};
|
||||||
|
|
||||||
17353
package-lock.json
generated
Normal file
17353
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
63
package.json
Normal file
63
package.json
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
{
|
||||||
|
"name": "tracker",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@devexperts/remote-data-ts": "^2.0.4",
|
||||||
|
"@material-ui/core": "^4.11.0",
|
||||||
|
"@material-ui/icons": "^4.9.1",
|
||||||
|
"@most/adapter": "^1.0.0",
|
||||||
|
"@most/core": "^1.6.1",
|
||||||
|
"@most/scheduler": "^1.3.0",
|
||||||
|
"@most/types": "^1.1.0",
|
||||||
|
"@types/uuid": "^8.3.0",
|
||||||
|
"axios": "^0.21.0",
|
||||||
|
"file-loader": "^6.2.0",
|
||||||
|
"fp-ts": "^2.8.5",
|
||||||
|
"lodash": "^4.17.20",
|
||||||
|
"react": "^17.0.1",
|
||||||
|
"react-dom": "^17.0.1",
|
||||||
|
"react-router-dom": "^5.2.0",
|
||||||
|
"react-scripts": "4.0.0",
|
||||||
|
"ts-loader": "^8.0.7",
|
||||||
|
"typescript": "^4.0.3"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "webpack serve",
|
||||||
|
"dev": "webpack serve --open false",
|
||||||
|
"build": "webpack --mode=production",
|
||||||
|
"eslint": "eslint -c .eslintrc.json src --fix",
|
||||||
|
"tsc": "tsc --p ./tsconfig.json",
|
||||||
|
"lint": "npm run eslint && npm run tsc",
|
||||||
|
"test": "jest"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.12.3",
|
||||||
|
"@babel/preset-env": "^7.12.1",
|
||||||
|
"@babel/preset-typescript": "^7.12.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",
|
||||||
|
"@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-prettier": "^3.1.4",
|
||||||
|
"eslint-plugin-react": "^7.21.5",
|
||||||
|
"eslint-plugin-react-hooks": "^4.2.0",
|
||||||
|
"html-webpack-plugin": "^4.5.0",
|
||||||
|
"mini-css-extract-plugin": "^1.2.1",
|
||||||
|
"prettier": "^2.1.2",
|
||||||
|
"sass": "^1.28.0",
|
||||||
|
"sass-loader": "^10.0.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>Tracker App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
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;
|
||||||
|
}
|
||||||
29
src/app/components/page/Page.tsx
Normal file
29
src/app/components/page/Page.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import React, {memo} from 'react';
|
||||||
|
import {BrowserRouter, Route, Switch} from 'react-router-dom';
|
||||||
|
|
||||||
|
import mainPageRouter from '../../../pages/main/routing';
|
||||||
|
import queuesPageRouter from '../../../pages/queues/routing';
|
||||||
|
import tasksPageRouter from '../../../pages/tasks/routing';
|
||||||
|
import authResponsePageRouter from '../../../pages/auth-response/routing';
|
||||||
|
import NotFoundPage from '../../../pages/not-found/components/page/Page';
|
||||||
|
import TopMenu from '../top-menu/TopMenu';
|
||||||
|
import './Page.scss';
|
||||||
|
|
||||||
|
const Page: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<BrowserRouter>
|
||||||
|
<TopMenu />
|
||||||
|
<Switch>
|
||||||
|
{mainPageRouter}
|
||||||
|
{queuesPageRouter}
|
||||||
|
{tasksPageRouter}
|
||||||
|
{authResponsePageRouter}
|
||||||
|
<Route>
|
||||||
|
<NotFoundPage />
|
||||||
|
</Route>
|
||||||
|
</Switch>
|
||||||
|
</BrowserRouter>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(Page);
|
||||||
30
src/app/components/top-menu/MenuList.tsx
Normal file
30
src/app/components/top-menu/MenuList.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import {List, ListItem as MaterialListItem, ListItemIcon, ListItemText} from '@material-ui/core';
|
||||||
|
import React, {memo} from 'react';
|
||||||
|
import {Link} from 'react-router-dom';
|
||||||
|
|
||||||
|
import InboxIcon from '@material-ui/icons/MoveToInbox';
|
||||||
|
|
||||||
|
import {ListItem} from '../../../common/types';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
list: ListItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const MenuList: React.FC<Props> = ({list}) => {
|
||||||
|
return (
|
||||||
|
<List>
|
||||||
|
{list.map(({title, url}) => (
|
||||||
|
<Link to={url} key={url}>
|
||||||
|
<MaterialListItem button key={url}>
|
||||||
|
<ListItemIcon>
|
||||||
|
<InboxIcon />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={title} />
|
||||||
|
</MaterialListItem>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(MenuList);
|
||||||
59
src/app/components/top-menu/TopMenu.tsx
Normal file
59
src/app/components/top-menu/TopMenu.tsx
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import React, {memo} from 'react';
|
||||||
|
|
||||||
|
import {createStyles, makeStyles, Theme} from '@material-ui/core/styles';
|
||||||
|
import AppBar from '@material-ui/core/AppBar';
|
||||||
|
import Toolbar from '@material-ui/core/Toolbar';
|
||||||
|
import Typography from '@material-ui/core/Typography';
|
||||||
|
import IconButton from '@material-ui/core/IconButton';
|
||||||
|
import MenuIcon from '@material-ui/icons/Menu';
|
||||||
|
import {Divider, Drawer} from '@material-ui/core';
|
||||||
|
|
||||||
|
import {useToggle} from '../../../common/hooks/useToggle';
|
||||||
|
import {MENU} from '../../../common/consts';
|
||||||
|
import MenuList from './MenuList';
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
|
createStyles({
|
||||||
|
root: {
|
||||||
|
flexGrow: 1,
|
||||||
|
},
|
||||||
|
menuButton: {
|
||||||
|
marginRight: theme.spacing(2),
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
flexGrow: 1,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const TopMenu: React.FC = () => {
|
||||||
|
const classes = useStyles();
|
||||||
|
const [isToggle, handleToggle] = useToggle();
|
||||||
|
return (
|
||||||
|
<div className={classes.root}>
|
||||||
|
<AppBar position="static">
|
||||||
|
<Toolbar>
|
||||||
|
<IconButton edge="start" className={classes.menuButton} color="inherit" aria-label="menu" onClick={handleToggle}>
|
||||||
|
<MenuIcon />
|
||||||
|
</IconButton>
|
||||||
|
<Typography variant="h6" className={classes.title}>
|
||||||
|
Tracker App
|
||||||
|
</Typography>
|
||||||
|
</Toolbar>
|
||||||
|
</AppBar>
|
||||||
|
<Drawer anchor="top" open={isToggle} onClose={handleToggle}>
|
||||||
|
<div
|
||||||
|
role="presentation"
|
||||||
|
onClick={handleToggle}
|
||||||
|
onKeyDown={handleToggle}
|
||||||
|
>
|
||||||
|
<MenuList list={MENU.COMMON} />
|
||||||
|
<Divider />
|
||||||
|
<MenuList list={MENU.PERSONAL} />
|
||||||
|
</div>
|
||||||
|
</Drawer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(TopMenu);
|
||||||
7
src/common/__test__/utils.test.ts
Normal file
7
src/common/__test__/utils.test.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import {numberToString} from '../utils';
|
||||||
|
|
||||||
|
describe('test numberToString', () => {
|
||||||
|
it('success convert', () => {
|
||||||
|
expect(numberToString(56)).toBe('56');
|
||||||
|
});
|
||||||
|
});
|
||||||
70
src/common/api/RegionsApi.ts
Normal file
70
src/common/api/RegionsApi.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import {makeStorageApi} from './StorageApi';
|
||||||
|
|
||||||
|
type Region = {
|
||||||
|
name: string;
|
||||||
|
subject_number: number;
|
||||||
|
gibdd_codes: Array<number>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ResponseRegions = {
|
||||||
|
regions: Array<Region>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const api = makeStorageApi<ResponseRegions>({
|
||||||
|
key: 'russian_regions',
|
||||||
|
hook: '26502372-6bc4-4cdf-bbcc-41b3b71cb386',
|
||||||
|
description: 'Регионы России',
|
||||||
|
service_name: 'geo_services',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const regionsApi = {
|
||||||
|
request: async (): Promise<Region[]> => {
|
||||||
|
const {value: {regions}} = await api.request();
|
||||||
|
return regions;
|
||||||
|
},
|
||||||
|
find: async (name: string): Promise<Undefinable<Region>> => {
|
||||||
|
const regions = await regionsApi.request();
|
||||||
|
return regions.find(region => region.name === name);
|
||||||
|
},
|
||||||
|
create: async (newRegion: Region): Promise<Region> => {
|
||||||
|
const regions = await regionsApi.request();
|
||||||
|
const findedRegion = regions.find(region => region.name === newRegion.name);
|
||||||
|
if (findedRegion) {
|
||||||
|
throw new Error(`Город с именем "${newRegion.name}" уже существует`);
|
||||||
|
}
|
||||||
|
await api.update({
|
||||||
|
regions: [
|
||||||
|
...regions,
|
||||||
|
newRegion,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
return newRegion;
|
||||||
|
},
|
||||||
|
update: async (updatedRegion: Region): Promise<Region> => {
|
||||||
|
const regions = await regionsApi.request();
|
||||||
|
const findedIndex = regions.findIndex(region => region.name === updatedRegion.name);
|
||||||
|
if (findedIndex === -1) {
|
||||||
|
throw new Error(`Город с именем "${updatedRegion.name}" не найден`);
|
||||||
|
}
|
||||||
|
await api.update({
|
||||||
|
regions: regions.map((region, index) => {
|
||||||
|
if (findedIndex === index) {
|
||||||
|
return updatedRegion;
|
||||||
|
}
|
||||||
|
return region;
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
return updatedRegion;
|
||||||
|
},
|
||||||
|
remove: async (name: string): Promise<string> => {
|
||||||
|
const regions = await regionsApi.request();
|
||||||
|
const findedIndex = regions.findIndex(region => region.name === name);
|
||||||
|
if (findedIndex === -1) {
|
||||||
|
throw new Error(`Город с именем "${name}" не найден`);
|
||||||
|
}
|
||||||
|
await api.update({
|
||||||
|
regions: regions.filter(region => region.name === name),
|
||||||
|
});
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
};
|
||||||
44
src/common/api/StorageApi.ts
Normal file
44
src/common/api/StorageApi.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import {http} from '../infrastructure/Http';
|
||||||
|
|
||||||
|
type QueryRequest = {
|
||||||
|
hook: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResponseData<T> = {
|
||||||
|
key: string;
|
||||||
|
value: T;
|
||||||
|
description: string;
|
||||||
|
service_name: string;
|
||||||
|
author: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type RequestData<T> = Omit<ResponseData<T>, 'author'>;
|
||||||
|
|
||||||
|
type ApiConfig<T> = Omit<ResponseData<T>, 'value' | 'author'> & {
|
||||||
|
hook: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Api<T> = {
|
||||||
|
request: () => Promise<ResponseData<T>>;
|
||||||
|
update: (updateValue: T) => Promise<ResponseData<T>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ROOT_URL = 'https://api.storage.vigdorov.ru/store';
|
||||||
|
|
||||||
|
export const makeStorageApi = <T>({key, hook, ...body}: ApiConfig<T>): Api<T> => {
|
||||||
|
const config = {params: {hook}};
|
||||||
|
return {
|
||||||
|
request: async (): Promise<ResponseData<T>> => {
|
||||||
|
const {data} = await http.get<QueryRequest, ResponseData<T>>(`${ROOT_URL}/${key}`, config);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
update: async (updateValue: T): Promise<ResponseData<T>> => {
|
||||||
|
const {data} = await http.put<QueryRequest, RequestData<T>, ResponseData<T>>(ROOT_URL, {
|
||||||
|
...body,
|
||||||
|
key,
|
||||||
|
value: updateValue,
|
||||||
|
}, config);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
1
src/common/comon.d.ts
vendored
Normal file
1
src/common/comon.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
type Undefinable<T> = T | undefined;
|
||||||
32
src/common/consts.ts
Normal file
32
src/common/consts.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import {ListItem} from './types';
|
||||||
|
|
||||||
|
export const ROUTES = {
|
||||||
|
MAIN: '/',
|
||||||
|
QUEUES: '/queues',
|
||||||
|
TASKS: '/tasks',
|
||||||
|
SETTINGS: '/settings',
|
||||||
|
AUTH_RESPONSE: '/auth-response',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MENU: Record<string, ListItem[]> = {
|
||||||
|
COMMON: [
|
||||||
|
{
|
||||||
|
title: 'Главная',
|
||||||
|
url: ROUTES.MAIN,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Очереди',
|
||||||
|
url: ROUTES.QUEUES,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Задачи',
|
||||||
|
url: ROUTES.TASKS,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
PERSONAL: [
|
||||||
|
{
|
||||||
|
title: 'Настройки',
|
||||||
|
url: ROUTES.SETTINGS,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
};
|
||||||
16
src/common/hooks/useEqualMemo.ts
Normal file
16
src/common/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;
|
||||||
|
}
|
||||||
28
src/common/hooks/useQuery.ts
Normal file
28
src/common/hooks/useQuery.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import {parse, ParsedUrlQuery} from 'querystring';
|
||||||
|
import {useMemo} from 'react';
|
||||||
|
import {useLocation} from 'react-router-dom';
|
||||||
|
|
||||||
|
type QueryParser<T> = (value?: string | string[]) => Undefinable<T>;
|
||||||
|
export type QueryParsers<T> = Partial<{[K in keyof T]: QueryParser<T[K]>}>;
|
||||||
|
|
||||||
|
export function useQuery(): ParsedUrlQuery;
|
||||||
|
export function useQuery<T extends {[name: string]: unknown}>(queryParsers: QueryParsers<T>): Partial<T>;
|
||||||
|
export function useQuery<T extends {[name: string]: unknown}>(
|
||||||
|
queryParsers?: QueryParsers<T>
|
||||||
|
): ParsedUrlQuery | Partial<T> {
|
||||||
|
const {search} = useLocation();
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
const query = parse(search.slice(1));
|
||||||
|
return queryParsers ? Object.keys(query).reduce<Partial<T>>((memo, key) => {
|
||||||
|
if (key in queryParsers) {
|
||||||
|
const parser = queryParsers[key];
|
||||||
|
return {
|
||||||
|
...memo,
|
||||||
|
[key]: parser?.(query[key]),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return memo;
|
||||||
|
}, {}) : query;
|
||||||
|
}, [search, queryParsers]);
|
||||||
|
}
|
||||||
10
src/common/hooks/useToggle.ts
Normal file
10
src/common/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,
|
||||||
|
];
|
||||||
|
};
|
||||||
53
src/common/infrastructure/Http.ts
Normal file
53
src/common/infrastructure/Http.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import axios, {AxiosRequestConfig, AxiosResponse} from 'axios';
|
||||||
|
|
||||||
|
type RequestConfig<Q> = Omit<AxiosRequestConfig, 'params'> & {
|
||||||
|
params: Q;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const http = {
|
||||||
|
get: <Query, Response>(
|
||||||
|
url: string,
|
||||||
|
config: RequestConfig<Query>
|
||||||
|
): Promise<AxiosResponse<Response>> => {
|
||||||
|
return axios.get<Response>(url, config);
|
||||||
|
},
|
||||||
|
delete: <Query, Response>(
|
||||||
|
url: string,
|
||||||
|
config: RequestConfig<Query>
|
||||||
|
): Promise<AxiosResponse<Response>> => {
|
||||||
|
return axios.delete<Response>(url, config);
|
||||||
|
},
|
||||||
|
head: <Query, Response>(
|
||||||
|
url: string,
|
||||||
|
config: RequestConfig<Query>
|
||||||
|
): Promise<AxiosResponse<Response>> => {
|
||||||
|
return axios.head<Response>(url, config);
|
||||||
|
},
|
||||||
|
options: <Query, Response>(
|
||||||
|
url: string,
|
||||||
|
config: RequestConfig<Query>
|
||||||
|
): Promise<AxiosResponse<Response>> => {
|
||||||
|
return axios.options<Response>(url, config);
|
||||||
|
},
|
||||||
|
post: <Query, Body, Response>(
|
||||||
|
url: string,
|
||||||
|
body: Body,
|
||||||
|
config: RequestConfig<Query>
|
||||||
|
): Promise<AxiosResponse<Response>> => {
|
||||||
|
return axios.post<Response>(url, body, config);
|
||||||
|
},
|
||||||
|
put: <Query, Body, Response>(
|
||||||
|
url: string,
|
||||||
|
body: Body,
|
||||||
|
config: RequestConfig<Query>
|
||||||
|
): Promise<AxiosResponse<Response>> => {
|
||||||
|
return axios.post<Response>(url, body, config);
|
||||||
|
},
|
||||||
|
patch: <Query, Body, Response>(
|
||||||
|
url: string,
|
||||||
|
body: Body,
|
||||||
|
config: RequestConfig<Query>
|
||||||
|
): Promise<AxiosResponse<Response>> => {
|
||||||
|
return axios.post<Response>(url, body, config);
|
||||||
|
},
|
||||||
|
};
|
||||||
4
src/common/types.ts
Normal file
4
src/common/types.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export type ListItem = {
|
||||||
|
title: string;
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
1
src/common/utils.ts
Normal file
1
src/common/utils.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const numberToString = (num: number): string => num.toString();
|
||||||
11
src/index.tsx
Normal file
11
src/index.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
|
||||||
|
import App from './app/components/page/Page';
|
||||||
|
|
||||||
|
ReactDOM.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
document.getElementById('root')
|
||||||
|
);
|
||||||
23
src/pages/auth-response/components/page/Page.tsx
Normal file
23
src/pages/auth-response/components/page/Page.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import {parse} from 'querystring';
|
||||||
|
import React, {memo} from 'react';
|
||||||
|
import {QueryParsers, useQuery} from '../../../../common/hooks/useQuery';
|
||||||
|
import {QueryResponse, QueryResponseError} from '../../types';
|
||||||
|
|
||||||
|
type Person = {
|
||||||
|
name: string;
|
||||||
|
age: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parsers: QueryParsers<Person> = {
|
||||||
|
name: name => name ? name.toString() : '',
|
||||||
|
age: age => age ? Number(age) : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const AuthResponsePage: React.FC = () => {
|
||||||
|
const query = useQuery(parsers);
|
||||||
|
return (
|
||||||
|
<div>Auth Page</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(AuthResponsePage);
|
||||||
13
src/pages/auth-response/routing.tsx
Normal file
13
src/pages/auth-response/routing.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {Route} from 'react-router-dom';
|
||||||
|
|
||||||
|
import {ROUTES} from '../../common/consts';
|
||||||
|
import Page from './components/page/Page';
|
||||||
|
|
||||||
|
export default (
|
||||||
|
<Route
|
||||||
|
component={Page}
|
||||||
|
path={ROUTES.AUTH_RESPONSE}
|
||||||
|
exact
|
||||||
|
/>
|
||||||
|
);
|
||||||
98
src/pages/auth-response/types.ts
Normal file
98
src/pages/auth-response/types.ts
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
export type QueryRequest = {
|
||||||
|
/**
|
||||||
|
* При запросе токена следует указать значение «token»
|
||||||
|
*/
|
||||||
|
response_type: string;
|
||||||
|
/**
|
||||||
|
* Идентификатор приложения. Доступен в свойствах приложения
|
||||||
|
*/
|
||||||
|
client_id: string;
|
||||||
|
/**
|
||||||
|
* Уникальный идентификатор устройства, для которого запрашивается токен. Чтобы обеспечить
|
||||||
|
* уникальность, достаточно один раз сгенерировать UUID и использовать его при каждом запросе
|
||||||
|
* нового токена с данного устройства.
|
||||||
|
*
|
||||||
|
* Идентификатор должен быть не короче 6 символов и не длиннее 50. Допускается использовать
|
||||||
|
* только печатаемые ASCII-символы (с кодами от 32 до 126).
|
||||||
|
*/
|
||||||
|
device_id?: string;
|
||||||
|
/**
|
||||||
|
* Имя устройства, которое следует показывать пользователям. Не длиннее 100 символов.
|
||||||
|
*/
|
||||||
|
device_name?: string;
|
||||||
|
/**
|
||||||
|
* URL, на который нужно перенаправить пользователя после того, как он разрешил или отказал приложению
|
||||||
|
* в доступе. По умолчанию используется первый Callback URI, указанный в настройках приложения.
|
||||||
|
* В значении параметра допустимо указывать только те адреса, которые перечислены в настройках
|
||||||
|
* приложения. Если совпадение неточное, параметр игнорируется.
|
||||||
|
*/
|
||||||
|
redirect_uri?: string;
|
||||||
|
/**
|
||||||
|
* Явное указание аккаунта, для которого запрашивается токен. В значении параметра можно передавать логин
|
||||||
|
* аккаунта на Яндексе, а также адрес Яндекс.Почты или Яндекс.Почты для домена.
|
||||||
|
*/
|
||||||
|
login_hint?: string;
|
||||||
|
/**
|
||||||
|
* Список необходимых приложению в данный момент прав доступа, разделенных пробелом. Права должны
|
||||||
|
* запрашиваться из перечня, определенного при регистрации приложения. Если параметры scope
|
||||||
|
* и optional_scope не переданы, то токен будет выдан с правами, указанными при регистрации приложения.
|
||||||
|
*/
|
||||||
|
scope?: string;
|
||||||
|
/**
|
||||||
|
* Если параметры scope и optional_scope не переданы, то токен будет выдан с правами,
|
||||||
|
* указанными при регистрации приложения.
|
||||||
|
*/
|
||||||
|
optional_scope?: string;
|
||||||
|
/**
|
||||||
|
* Признак того, что у пользователя обязательно нужно запросить разрешение на доступ
|
||||||
|
* к аккаунту (даже если пользователь уже разрешил доступ данному приложению).
|
||||||
|
* Получив этот параметр, Яндекс.OAuth предложит пользователю разрешить доступ приложению
|
||||||
|
* и выбрать нужный аккаунт Яндекса.
|
||||||
|
*/
|
||||||
|
force_confirm?: 'yes' | true | 1;
|
||||||
|
/**
|
||||||
|
* Строка состояния, которую Яндекс.OAuth возвращает без изменения.
|
||||||
|
* Максимальная допустимая длина строки — 1024 символа.
|
||||||
|
*/
|
||||||
|
state?: string;
|
||||||
|
/**
|
||||||
|
* Признак облегченной верстки (без стандартной навигации Яндекса) для страницы разрешения доступа.
|
||||||
|
* Облегченную верстку стоит запрашивать, например, если страницу разрешения нужно отобразить
|
||||||
|
* в маленьком всплывающем окне.
|
||||||
|
*/
|
||||||
|
display?: 'popup';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type QueryResponse = {
|
||||||
|
/**
|
||||||
|
* OAuth-токен с запрошенными правами или с правами, указанными при регистрации приложения.
|
||||||
|
*/
|
||||||
|
access_token: string;
|
||||||
|
/**
|
||||||
|
* Время жизни токена в секундах.
|
||||||
|
*/
|
||||||
|
expires_in: string;
|
||||||
|
/**
|
||||||
|
* Тип выданного токена. Всегда принимает значение «bearer».
|
||||||
|
*/
|
||||||
|
token_type: 'bearer';
|
||||||
|
/**
|
||||||
|
* Значение параметра state из исходного запроса, если этот параметр был передан.
|
||||||
|
*/
|
||||||
|
state?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type QueryResponseError = {
|
||||||
|
/**
|
||||||
|
* Код ошибки
|
||||||
|
*/
|
||||||
|
error: 'access_denied' | 'unauthorized_client';
|
||||||
|
/**
|
||||||
|
* Описание ошибки
|
||||||
|
*/
|
||||||
|
error_description: string;
|
||||||
|
/**
|
||||||
|
* Значение параметра state из исходного запроса, если этот параметр был передан.
|
||||||
|
*/
|
||||||
|
state?: string;
|
||||||
|
};
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
import React, {FC, memo} from 'react';
|
||||||
|
import {chain, fromPromise, map} from '@most/core';
|
||||||
|
import {pipe} from 'fp-ts/lib/pipeable';
|
||||||
|
import {useStream} from '../../../../utils/useStream';
|
||||||
|
import {list$} from '../../../../services/service1';
|
||||||
|
|
||||||
|
const promise1: (id: number) => Promise<string> = (id: number) => new Promise(res => {
|
||||||
|
setTimeout(() => res(`${id} 123123`), 6000);
|
||||||
|
});
|
||||||
|
|
||||||
|
const getStreamFromPromise = (id: number) => fromPromise(promise1(id));
|
||||||
|
|
||||||
|
const ComponentStream: FC = () => {
|
||||||
|
const data = useStream(
|
||||||
|
pipe(
|
||||||
|
list$,
|
||||||
|
map(arr => {
|
||||||
|
return arr.length;
|
||||||
|
}),
|
||||||
|
chain(id => getStreamFromPromise(id))
|
||||||
|
),
|
||||||
|
''
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div>{data}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(ComponentStream);
|
||||||
19
src/pages/main/components/page/Page.tsx
Normal file
19
src/pages/main/components/page/Page.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import React, {memo} from 'react';
|
||||||
|
import {AuthService} from '../../../../services/AuthService';
|
||||||
|
import {useStream} from '../../../../utils/useStream';
|
||||||
|
import ComponentStream from '../component-stream/ComponentStream';
|
||||||
|
|
||||||
|
const MainPage: React.FC = () => {
|
||||||
|
const {isAuth} = useStream(AuthService.state$, AuthService.initState);
|
||||||
|
const toggle = () => AuthService.handleChangeAuth(!isAuth);
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
Main Page
|
||||||
|
Auth: {isAuth ? 'yes' : 'no'}
|
||||||
|
<button onClick={toggle}>click</button>
|
||||||
|
<ComponentStream />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(MainPage);
|
||||||
9
src/pages/main/routing.tsx
Normal file
9
src/pages/main/routing.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {Route} from 'react-router-dom';
|
||||||
|
import {ROUTES} from '../../common/consts';
|
||||||
|
|
||||||
|
import Page from './components/page/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);
|
||||||
13
src/pages/queues/components/page/Page.tsx
Normal file
13
src/pages/queues/components/page/Page.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import React, {memo} from 'react';
|
||||||
|
import QueueTable from '../queue-table/QueueTable';
|
||||||
|
|
||||||
|
const QueuesPage: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div>Queues Page</div>
|
||||||
|
<QueueTable />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(QueuesPage);
|
||||||
37
src/pages/queues/components/queue-table/QueueTable.tsx
Normal file
37
src/pages/queues/components/queue-table/QueueTable.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import {Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow} from '@material-ui/core';
|
||||||
|
import React, {memo} from 'react';
|
||||||
|
|
||||||
|
const rows = [
|
||||||
|
'Очередь №1',
|
||||||
|
'Тестовая очередь',
|
||||||
|
'Старая очередь',
|
||||||
|
'Не новая очередь',
|
||||||
|
'Прошлая очередь',
|
||||||
|
];
|
||||||
|
|
||||||
|
const QueueTable: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<TableContainer component={Paper}>
|
||||||
|
<Table aria-label="simple table">
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell component="th">№</TableCell>
|
||||||
|
<TableCell component="th">Название очереди</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{rows.map((row, index) => (
|
||||||
|
<TableRow key={row}>
|
||||||
|
<TableCell scope="row">
|
||||||
|
{index + 1}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{row}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(QueueTable);
|
||||||
9
src/pages/queues/routing.tsx
Normal file
9
src/pages/queues/routing.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {Route} from 'react-router-dom';
|
||||||
|
import {ROUTES} from '../../common/consts';
|
||||||
|
|
||||||
|
import Page from './components/page/Page';
|
||||||
|
|
||||||
|
export default (
|
||||||
|
<Route component={Page} path={ROUTES.QUEUES} exact />
|
||||||
|
);
|
||||||
9
src/pages/tasks/components/page/Page.tsx
Normal file
9
src/pages/tasks/components/page/Page.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import React, {memo} from 'react';
|
||||||
|
|
||||||
|
const TasksPage: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<div>Tasks Page</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(TasksPage);
|
||||||
9
src/pages/tasks/routing.tsx
Normal file
9
src/pages/tasks/routing.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {Route} from 'react-router-dom';
|
||||||
|
import {ROUTES} from '../../common/consts';
|
||||||
|
|
||||||
|
import Page from './components/page/Page';
|
||||||
|
|
||||||
|
export default (
|
||||||
|
<Route component={Page} path={ROUTES.TASKS} exact />
|
||||||
|
);
|
||||||
20
src/services/AuthService.ts
Normal file
20
src/services/AuthService.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import {createAdapter} from '@most/adapter';
|
||||||
|
import {state} from 'fp-ts/lib/State';
|
||||||
|
|
||||||
|
export namespace AuthService {
|
||||||
|
type State = {
|
||||||
|
isAuth: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initState: State = {
|
||||||
|
isAuth: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const [changeState, stream$] = createAdapter<State>();
|
||||||
|
|
||||||
|
export const handleChangeAuth = (isAuth: boolean): void => changeState({
|
||||||
|
...state,
|
||||||
|
isAuth,
|
||||||
|
});
|
||||||
|
export const state$ = stream$;
|
||||||
|
}
|
||||||
13
src/services/service1.ts
Normal file
13
src/services/service1.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import {createAdapter} from '@most/adapter';
|
||||||
|
|
||||||
|
const arr: Array<number> = [];
|
||||||
|
let inc = 0;
|
||||||
|
|
||||||
|
const [handler, stream$] = createAdapter<Array<number>>();
|
||||||
|
|
||||||
|
export const list$ = stream$;
|
||||||
|
setInterval(() => {
|
||||||
|
arr.push(inc += 1);
|
||||||
|
handler(arr);
|
||||||
|
}, 500);
|
||||||
|
|
||||||
26
src/utils/asyncDataUtils.tsx
Normal file
26
src/utils/asyncDataUtils.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import React, {ReactNode} from 'react';
|
||||||
|
import {RemoteData, fold, map} from '@devexperts/remote-data-ts';
|
||||||
|
import {Stream} from '@most/types';
|
||||||
|
import * as M from '@most/core';
|
||||||
|
import {pipe} from 'fp-ts/lib/pipeable';
|
||||||
|
|
||||||
|
export const renderAsyncData = <E, A>(
|
||||||
|
data: RemoteData<E, A>,
|
||||||
|
renderSuccessData: (successData: A) => ReactNode
|
||||||
|
): ReactNode => {
|
||||||
|
return fold<E, A, ReactNode>(
|
||||||
|
() => <div>Initial</div>,
|
||||||
|
() => <div>Pending</div>,
|
||||||
|
error => <div>{`${error}`}</div>,
|
||||||
|
successData => renderSuccessData(successData),
|
||||||
|
)(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mapRD = <E, A, R>(mapper: (val: A) => R) => {
|
||||||
|
return (stream$: Stream<RemoteData<E, A>>): Stream<RemoteData<E, R>> => {
|
||||||
|
return pipe(
|
||||||
|
stream$,
|
||||||
|
M.map(val => map(mapper)(val))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
};
|
||||||
32
src/utils/useStream.ts
Normal file
32
src/utils/useStream.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import {useEffect, useState} from 'react';
|
||||||
|
import {Stream, Sink} from '@most/types';
|
||||||
|
import {newDefaultScheduler} from '@most/scheduler';
|
||||||
|
import {pending, RemoteData} from '@devexperts/remote-data-ts';
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-empty-function
|
||||||
|
const emptyFunc = () => {};
|
||||||
|
|
||||||
|
export const useStream = <T>(stream$: Stream<T>, defaultValue: T): T => {
|
||||||
|
const [state, setState] = useState(defaultValue);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const sink: Sink<T> = {
|
||||||
|
event: (_, val) => {
|
||||||
|
setState(val);
|
||||||
|
},
|
||||||
|
end: emptyFunc,
|
||||||
|
error: emptyFunc
|
||||||
|
};
|
||||||
|
|
||||||
|
const effect$ = stream$.run(sink, newDefaultScheduler());
|
||||||
|
return () => {
|
||||||
|
effect$.dispose();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return state;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useStreamRD = <T, E = Error>(stream$: Stream<RemoteData<E, T>>): RemoteData<E, T> => {
|
||||||
|
return useStream(stream$, pending);
|
||||||
|
};
|
||||||
26
tsconfig.json
Normal file
26
tsconfig.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*.ts",
|
||||||
|
"src/**/*.tsx"
|
||||||
|
]
|
||||||
|
}
|
||||||
66
webpack.config.js
Normal file
66
webpack.config.js
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
const path = require('path');
|
||||||
|
var HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||||
|
const {CleanWebpackPlugin} = require('clean-webpack-plugin');
|
||||||
|
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
entry: './src/index.tsx',
|
||||||
|
output: {
|
||||||
|
path: path.resolve(__dirname, 'build'),
|
||||||
|
filename: 'index.js',
|
||||||
|
},
|
||||||
|
devServer: {
|
||||||
|
contentBase: './build',
|
||||||
|
historyApiFallback: true,
|
||||||
|
compress: true,
|
||||||
|
open: true,
|
||||||
|
port: 3189,
|
||||||
|
},
|
||||||
|
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',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.tsx', '.ts', '.js'],
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
template: './public/index.html',
|
||||||
|
filename: 'index.html',
|
||||||
|
favicon: './public/favicon.ico'
|
||||||
|
}),
|
||||||
|
new MiniCssExtractPlugin({
|
||||||
|
filename: '[name].css',
|
||||||
|
}),
|
||||||
|
new CleanWebpackPlugin(),
|
||||||
|
],
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user