init
This commit is contained in:
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);
|
||||
};
|
||||
Reference in New Issue
Block a user