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