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;
|
||||
}
|
||||
42
src/app/components/page/Page.tsx
Normal file
42
src/app/components/page/Page.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import React, {Fragment, memo} from 'react';
|
||||
import {Route, Switch} from 'react-router-dom';
|
||||
import {Container, createStyles, makeStyles} from '@material-ui/core';
|
||||
import {createStore} from '@reatom/core';
|
||||
import {context} from '@reatom/react';
|
||||
import mainPageRouter from '_pages/main/routing';
|
||||
import NotFoundPage from '_pages/not-found/components/page';
|
||||
import './Page.scss';
|
||||
|
||||
const useStyles = makeStyles(() =>
|
||||
createStyles({
|
||||
container: {
|
||||
height: '100hv',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const Page: React.FC = () => {
|
||||
const classes = useStyles();
|
||||
const store = createStore();
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className={classes.container}>
|
||||
<context.Provider value={store}>
|
||||
<Container>
|
||||
<Switch>
|
||||
{mainPageRouter}
|
||||
<Route>
|
||||
<NotFoundPage />
|
||||
</Route>
|
||||
</Switch>
|
||||
</Container>
|
||||
</context.Provider>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Page);
|
||||
1
src/app/components/page/index.ts
Normal file
1
src/app/components/page/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export {default} from './Page';
|
||||
17
src/app/index.tsx
Normal file
17
src/app/index.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import {HashRouter} from 'react-router-dom';
|
||||
import App from './components/page';
|
||||
|
||||
ReactDOM.render(
|
||||
/*
|
||||
* Выключаем стрикт мод, пока не починят
|
||||
* https://github.com/mui-org/material-ui/issues/13394
|
||||
*/
|
||||
// <React.StrictMode>
|
||||
<HashRouter >
|
||||
<App />
|
||||
</HashRouter>,
|
||||
// </React.StrictMode>
|
||||
document.getElementById('root')
|
||||
);
|
||||
3
src/core/consts/common.ts
Normal file
3
src/core/consts/common.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export const ROUTES = {
|
||||
MAIN: '/'
|
||||
};
|
||||
3
src/core/dts/comon.d.ts
vendored
Normal file
3
src/core/dts/comon.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
type Undefinable<T> = T | undefined;
|
||||
|
||||
type Nullable<T> = T | undefined | null;
|
||||
16
src/core/hooks/useEqualMemo.ts
Normal file
16
src/core/hooks/useEqualMemo.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import {DependencyList, useEffect, useState} from 'react';
|
||||
|
||||
const emptyDependency: DependencyList = [];
|
||||
|
||||
export function useEqualMemo<T>(func: () => T, args: DependencyList = emptyDependency): T {
|
||||
const [memoized, memo] = useState<T>(func());
|
||||
useEffect(() => {
|
||||
const data = func();
|
||||
if (!isEqual(memoized, data)) {
|
||||
memo(data);
|
||||
}
|
||||
}, [memoized, func, args]);
|
||||
|
||||
return memoized;
|
||||
}
|
||||
10
src/core/hooks/useParams.ts
Normal file
10
src/core/hooks/useParams.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import {useMemo} from 'react';
|
||||
import {useParams as useReactParams} from 'react-router-dom';
|
||||
import {getParamsFromUrl} from '_utils/getParamFromUrl';
|
||||
import {QueryParsers} from '_utils/getQueryFromUrl';
|
||||
|
||||
export function useParams<T extends Record<string, unknown>>(paramParsers: QueryParsers<T>) {
|
||||
const params = useReactParams<Record<keyof T, string>>();
|
||||
|
||||
return useMemo(() => getParamsFromUrl(paramParsers, params), [params, paramParsers]);
|
||||
}
|
||||
9
src/core/hooks/useQuery.ts
Normal file
9
src/core/hooks/useQuery.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import {useMemo} from 'react';
|
||||
import {useLocation} from 'react-router-dom';
|
||||
import {getQueryFromUrl, QueryParsers} from '_utils/getQueryFromUrl';
|
||||
|
||||
export function useQuery<T extends Record<string, unknown>>(queryParsers: QueryParsers<T>): T {
|
||||
const {search} = useLocation();
|
||||
|
||||
return useMemo(() => getQueryFromUrl(queryParsers, search), [search, queryParsers]);
|
||||
}
|
||||
10
src/core/hooks/useToggle.ts
Normal file
10
src/core/hooks/useToggle.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import {useCallback, useState} from 'react';
|
||||
|
||||
export const useToggle = (initValue = false): [boolean, () => void] => {
|
||||
const [isToggle, onToggle] = useState(initValue);
|
||||
const handleToggle = useCallback(() => onToggle(state => !state), [onToggle]);
|
||||
return [
|
||||
isToggle,
|
||||
handleToggle,
|
||||
];
|
||||
};
|
||||
51
src/core/infrastructure/Http.ts
Normal file
51
src/core/infrastructure/Http.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import axios, {AxiosRequestConfig} from 'axios';
|
||||
|
||||
enum Method {
|
||||
Get = 'get',
|
||||
Delete = 'delete',
|
||||
Head = 'head',
|
||||
Options = 'options',
|
||||
Post = 'post',
|
||||
Put = 'put',
|
||||
Patch = 'patch',
|
||||
}
|
||||
|
||||
type RequestConfig<Q, B> = Omit<AxiosRequestConfig, 'params' | 'data'> & {
|
||||
params?: Q;
|
||||
data?: B;
|
||||
};
|
||||
|
||||
const requestMiddleware = async <Q, B, R>(config: RequestConfig<Q, B>): Promise<R> => {
|
||||
const axiosResponse = await axios.request<R>(config);
|
||||
// Добавить обработку ошибок
|
||||
return axiosResponse.data;
|
||||
};
|
||||
|
||||
const request = <Q, B, R>(config: RequestConfig<Q, B>) => requestMiddleware<Q, B, R>(config);
|
||||
|
||||
const requestWithoutBody = (method: Method) => <Q, R>(url: string, query?: Q) => {
|
||||
return request<Q, never, R>({
|
||||
method,
|
||||
url,
|
||||
params: query,
|
||||
});
|
||||
};
|
||||
|
||||
const requestWithBody = (method: Method) => <Q, B, R>(url: string, query?: Q, body?: B) => {
|
||||
return request<Q, B, R>({
|
||||
method,
|
||||
url,
|
||||
params: query,
|
||||
data: body,
|
||||
});
|
||||
};
|
||||
|
||||
export const http = {
|
||||
get: requestWithoutBody(Method.Get),
|
||||
delete: requestWithoutBody(Method.Delete),
|
||||
head: requestWithoutBody(Method.Head),
|
||||
options: requestWithoutBody(Method.Options),
|
||||
post: requestWithBody(Method.Post),
|
||||
put: requestWithBody(Method.Put),
|
||||
patch: requestWithBody(Method.Patch),
|
||||
};
|
||||
7
src/core/infrastructure/atom/exampleAtom.ts
Normal file
7
src/core/infrastructure/atom/exampleAtom.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import {declareAction, declareAtom} from '@reatom/core';
|
||||
|
||||
export const changeNameAction = declareAction<string>();
|
||||
|
||||
export const nameAtom = declareAtom('', on => [
|
||||
on(changeNameAction, (_state, payload) => payload),
|
||||
]);
|
||||
54
src/core/referers/__test__/common.test.ts
Normal file
54
src/core/referers/__test__/common.test.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import {noop} from 'lodash';
|
||||
import {isEmptyObject, isNotEmpty, isObject} from '../common';
|
||||
|
||||
describe('isObject', () => {
|
||||
it('Должен вернуть true', () => {
|
||||
expect(isObject({})).toBeTruthy();
|
||||
expect(isObject({go: 8})).toBeTruthy();
|
||||
expect(isObject(Object.create(null))).toBeTruthy();
|
||||
expect(isObject(Object.create({}))).toBeTruthy();
|
||||
});
|
||||
|
||||
it('Должен вернуть false', () => {
|
||||
expect(isObject(null)).toBeFalsy();
|
||||
expect(isObject([])).toBeFalsy();
|
||||
expect(isObject(NaN)).toBeFalsy();
|
||||
expect(isObject(noop)).toBeFalsy();
|
||||
expect(isObject(0)).toBeFalsy();
|
||||
expect(isObject('')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isEmptyObject', () => {
|
||||
it('Должен вернуть true', () => {
|
||||
expect(isEmptyObject({})).toBeTruthy();
|
||||
expect(isEmptyObject(Object.create(null))).toBeTruthy();
|
||||
});
|
||||
|
||||
it('Должен вернуть false', () => {
|
||||
expect(isEmptyObject({g: 'g'})).toBeFalsy();
|
||||
expect(isEmptyObject({g: undefined})).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isNotEmpty', () => {
|
||||
it('Должен вернуть true', () => {
|
||||
expect(isNotEmpty(['3'])).toBeTruthy();
|
||||
expect(isNotEmpty({f: 'f'})).toBeTruthy();
|
||||
expect(isNotEmpty({f: undefined})).toBeTruthy();
|
||||
expect(isNotEmpty(0)).toBeTruthy();
|
||||
expect(isNotEmpty(12)).toBeTruthy();
|
||||
expect(isNotEmpty('fd')).toBeTruthy();
|
||||
expect(isNotEmpty('0')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('Должен вернуть false', () => {
|
||||
expect(isNotEmpty([])).toBeFalsy();
|
||||
expect(isNotEmpty({})).toBeFalsy();
|
||||
expect(isNotEmpty('')).toBeFalsy();
|
||||
expect(isNotEmpty(' ')).toBeFalsy();
|
||||
expect(isNotEmpty()).toBeFalsy();
|
||||
expect(isNotEmpty(null)).toBeFalsy();
|
||||
expect(isNotEmpty(NaN)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
48
src/core/referers/common.ts
Normal file
48
src/core/referers/common.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import {isNaN, isNumber, isString} from 'lodash';
|
||||
|
||||
export function isNullable<T>(value: Nullable<T>): value is (null | undefined) {
|
||||
return value === null || value === undefined;
|
||||
}
|
||||
|
||||
export function isNotNullable<T>(value: Nullable<T>): value is NonNullable<T> {
|
||||
return !isNullable(value);
|
||||
}
|
||||
|
||||
export function isObject<T>(value: Nullable<T>): value is T {
|
||||
return (
|
||||
isNotNullable(value)
|
||||
&& !Array.isArray(value)
|
||||
&& !isNaN(value)
|
||||
&& typeof value === 'object'
|
||||
);
|
||||
}
|
||||
|
||||
export const isEmptyObject = <T>(value: Nullable<T>): boolean => (
|
||||
!Object.keys(value ?? {}).length
|
||||
);
|
||||
|
||||
export const isNotEmpty = <T>(value?: Nullable<T>): value is T => {
|
||||
if (isString(value)) {
|
||||
return !!value?.trim();
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return !!value.length;
|
||||
}
|
||||
|
||||
if (isNaN(value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isNumber(value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isObject(value)) {
|
||||
return !isEmptyObject(value);
|
||||
}
|
||||
|
||||
return isNotNullable(value);
|
||||
};
|
||||
|
||||
export const isEmpty = (value: unknown) => !isNotEmpty(value);
|
||||
21
src/core/services/LocalStorageService.ts
Normal file
21
src/core/services/LocalStorageService.ts
Normal file
@ -0,0 +1,21 @@
|
||||
export const makeLocalStorageService = <T>(init: T, stateName: string) => {
|
||||
if (!localStorage.getItem(stateName)) {
|
||||
localStorage.setItem(stateName, JSON.stringify(init));
|
||||
}
|
||||
|
||||
return {
|
||||
set: (updatedState: T) => {
|
||||
localStorage.setItem(stateName, JSON.stringify(updatedState));
|
||||
return updatedState;
|
||||
},
|
||||
get: (): T => {
|
||||
const stringValue = localStorage.getItem(stateName) || '';
|
||||
|
||||
try {
|
||||
return JSON.parse(stringValue);
|
||||
} catch (e) {
|
||||
return init;
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
27
src/core/utils/__test__/getParamFromUrl.test.ts
Normal file
27
src/core/utils/__test__/getParamFromUrl.test.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import {getParamsFromUrl} from '../getParamFromUrl';
|
||||
import {QueryParsers} from '../getQueryFromUrl';
|
||||
import {booleanParser, numberParser, stringParser} from '../queryParsers';
|
||||
|
||||
describe('getParamsFromUrl', () => {
|
||||
type Params = {
|
||||
name?: string;
|
||||
id?: number;
|
||||
isNew: boolean;
|
||||
};
|
||||
|
||||
it('Получение параметров', () => {
|
||||
const parsers: QueryParsers<Params> = {
|
||||
name: stringParser(),
|
||||
id: numberParser(),
|
||||
isNew: booleanParser(false),
|
||||
};
|
||||
expect(getParamsFromUrl(parsers, {olo: 't'})).toEqual({isNew: false});
|
||||
expect(getParamsFromUrl(parsers, {name: 't'})).toEqual({name: 't', isNew: false});
|
||||
expect(getParamsFromUrl(parsers, {
|
||||
name: 't',
|
||||
id: '6',
|
||||
pageType: 'tags',
|
||||
isNew: 'true',
|
||||
})).toEqual({name: 't', id: 6, isNew: true});
|
||||
});
|
||||
});
|
||||
28
src/core/utils/__test__/getQueryParamFromUrl.test.ts
Normal file
28
src/core/utils/__test__/getQueryParamFromUrl.test.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import {getQueryFromUrl, QueryParsers} from '../getQueryFromUrl';
|
||||
import {arrayParser, booleanParser, numberParser, stringParser} from '../queryParsers';
|
||||
|
||||
describe('getQueryFromUrl', () => {
|
||||
type Query = {
|
||||
name?: string;
|
||||
id?: number;
|
||||
isNew: boolean;
|
||||
colors: string[];
|
||||
};
|
||||
|
||||
it('Получение параметров', () => {
|
||||
const parsers: QueryParsers<Query> = {
|
||||
name: stringParser(),
|
||||
id: numberParser(),
|
||||
isNew: booleanParser(false),
|
||||
colors: arrayParser([]),
|
||||
};
|
||||
expect(getQueryFromUrl(parsers, '?olo=2')).toEqual({isNew: false, colors: []});
|
||||
expect(getQueryFromUrl(parsers, '?olo=2&name=t')).toEqual({name: 't', isNew: false, colors: []});
|
||||
expect(getQueryFromUrl(parsers, '?name=t&id=6&pageType=tags&isNew=true&colors=red&colors=blue')).toEqual({
|
||||
name: 't',
|
||||
id: 6,
|
||||
isNew: true,
|
||||
colors: ['red', 'blue'],
|
||||
});
|
||||
});
|
||||
});
|
||||
31
src/core/utils/__test__/jsonParse.test.ts
Normal file
31
src/core/utils/__test__/jsonParse.test.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import {jsonParse} from '../jsonParse';
|
||||
|
||||
describe('jsonParse', () => {
|
||||
it('Должен вернуть значение', () => {
|
||||
expect(jsonParse('{}')).toEqual({});
|
||||
expect(jsonParse('null')).toBeNull();
|
||||
expect(jsonParse('0')).toBe(0);
|
||||
expect(jsonParse(' 1 ')).toBe(1);
|
||||
});
|
||||
|
||||
it('Должен вернуть значение при наличии дефолта', () => {
|
||||
expect(jsonParse('{}', {str: 9})).toEqual({});
|
||||
expect(jsonParse('null', {str: 9})).toBeNull();
|
||||
expect(jsonParse('0', {str: 9})).toBe(0);
|
||||
expect(jsonParse(' 1 ', {str: 9})).toBe(1);
|
||||
});
|
||||
|
||||
it('Должен вернуть undefined для не корректных значений', () => {
|
||||
expect(jsonParse()).toBeUndefined();
|
||||
expect(jsonParse('')).toBeUndefined();
|
||||
expect(jsonParse(' ')).toBeUndefined();
|
||||
expect(jsonParse('{"9')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('Должен вернуть дефолтное значение', () => {
|
||||
expect(jsonParse(undefined, 'to')).toBe('to');
|
||||
expect(jsonParse('', 'to')).toBe('to');
|
||||
expect(jsonParse(' ', 'to')).toBe('to');
|
||||
expect(jsonParse('./6dh', 9)).toBe(9);
|
||||
});
|
||||
});
|
||||
25
src/core/utils/__test__/parsers.test.ts
Normal file
25
src/core/utils/__test__/parsers.test.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import {toNumber} from '../parsers';
|
||||
|
||||
describe('toNumber', () => {
|
||||
it('Возвращает число', () => {
|
||||
expect(toNumber(0)).toBe(0);
|
||||
expect(toNumber(' 0 ')).toBe(0);
|
||||
expect(toNumber('0')).toBe(0);
|
||||
expect(toNumber('56')).toBe(56);
|
||||
expect(toNumber(' 56 ')).toBe(56);
|
||||
expect(toNumber(' 5.6 ')).toBe(5.6);
|
||||
expect(toNumber(' .9 ')).toBe(0.9);
|
||||
expect(toNumber(1.4)).toBe(1.4);
|
||||
expect(toNumber(.4)).toBe(0.4);
|
||||
});
|
||||
|
||||
it('Возвращает undefined', () => {
|
||||
expect(toNumber(' g')).toBeUndefined();
|
||||
expect(toNumber(null)).toBeUndefined();
|
||||
expect(toNumber(undefined)).toBeUndefined();
|
||||
expect(toNumber({})).toBeUndefined();
|
||||
expect(toNumber([])).toBeUndefined();
|
||||
expect(toNumber(' 4 5 ')).toBeUndefined();
|
||||
expect(toNumber(' 4 .5 ')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
43
src/core/utils/__test__/performTextSearch.test.ts
Normal file
43
src/core/utils/__test__/performTextSearch.test.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import {performTextSearch} from '../performTextSearch';
|
||||
|
||||
describe('performTextSearch', () => {
|
||||
it('Проверка отфильтрованных значений', () => {
|
||||
const items = [
|
||||
{name: 'Иван', age: 6, color: 'red'},
|
||||
{name: 'Сергей', age: 12, color: 'white'},
|
||||
{name: 'John', age: 33, color: 'black'},
|
||||
{name: 'James', age: 43, color: 'yellow'},
|
||||
];
|
||||
|
||||
expect(performTextSearch(items, 'ива', ['name'])).toEqual([
|
||||
{name: 'Иван', age: 6, color: 'red'},
|
||||
]);
|
||||
expect(performTextSearch(items, 'cth', ['name'])).toEqual([
|
||||
{name: 'Сергей', age: 12, color: 'white'},
|
||||
]);
|
||||
expect(performTextSearch(items, 'utq', ['name'])).toEqual([
|
||||
{name: 'Сергей', age: 12, color: 'white'},
|
||||
]);
|
||||
expect(performTextSearch(items, 'о', ['name'])).toEqual([
|
||||
{name: 'John', age: 33, color: 'black'},
|
||||
{name: 'James', age: 43, color: 'yellow'},
|
||||
]);
|
||||
expect(performTextSearch(items, 'e', ['name', 'color'])).toEqual([
|
||||
{name: 'Иван', age: 6, color: 'red'},
|
||||
{name: 'Сергей', age: 12, color: 'white'},
|
||||
{name: 'James', age: 43, color: 'yellow'},
|
||||
]);
|
||||
expect(performTextSearch(items, '3', ['age'])).toEqual([
|
||||
{name: 'John', age: 33, color: 'black'},
|
||||
{name: 'James', age: 43, color: 'yellow'},
|
||||
]);
|
||||
expect(performTextSearch(items, '', ['age'])).toEqual([
|
||||
{name: 'Иван', age: 6, color: 'red'},
|
||||
{name: 'Сергей', age: 12, color: 'white'},
|
||||
{name: 'John', age: 33, color: 'black'},
|
||||
{name: 'James', age: 43, color: 'yellow'},
|
||||
]);
|
||||
expect(performTextSearch(items, 'sdfsse23', ['age'])).toEqual([]);
|
||||
expect(performTextSearch([], 'sdfsse23', ['age'])).toEqual([]);
|
||||
});
|
||||
});
|
||||
98
src/core/utils/__test__/queryParsers.test.ts
Normal file
98
src/core/utils/__test__/queryParsers.test.ts
Normal file
@ -0,0 +1,98 @@
|
||||
import {arrayParser, booleanParser, numberParser, stringParser} from '../queryParsers';
|
||||
|
||||
describe('stringParser', () => {
|
||||
it('Вернет значение', () => {
|
||||
expect(stringParser()('trt')).toBe('trt');
|
||||
});
|
||||
|
||||
it('Вернет первое значение массива', () => {
|
||||
expect(stringParser()(['trt', 'ggt'])).toBe('trt');
|
||||
});
|
||||
|
||||
it('Вернет undefined', () => {
|
||||
expect(stringParser()()).toBeUndefined();
|
||||
expect(stringParser()([])).toBeUndefined();
|
||||
});
|
||||
|
||||
it('Вернет дефолт', () => {
|
||||
expect(stringParser('def')()).toBe('def');
|
||||
expect(stringParser('def')([])).toBe('def');
|
||||
});
|
||||
|
||||
it('Вернет значение даже при наличии дефолта', () => {
|
||||
expect(stringParser('def')(['trt'])).toBe('trt');
|
||||
expect(stringParser('def')('trt')).toBe('trt');
|
||||
});
|
||||
});
|
||||
|
||||
describe('numberParser', () => {
|
||||
it('Вернет значение', () => {
|
||||
expect(numberParser()('45')).toBe(45);
|
||||
});
|
||||
|
||||
it('Вернет первое значение массива', () => {
|
||||
expect(numberParser()(['45', '44'])).toBe(45);
|
||||
});
|
||||
|
||||
it('Вернет undefined', () => {
|
||||
expect(numberParser()()).toBeUndefined();
|
||||
expect(numberParser()([])).toBeUndefined();
|
||||
});
|
||||
|
||||
it('Вернет дефолт', () => {
|
||||
expect(numberParser(33)()).toBe(33);
|
||||
expect(numberParser(33)([])).toBe(33);
|
||||
});
|
||||
|
||||
it('Вернет значение даже при наличии дефолта', () => {
|
||||
expect(numberParser(33)(['45'])).toBe(45);
|
||||
expect(numberParser(33)('45')).toBe(45);
|
||||
});
|
||||
});
|
||||
|
||||
describe('booleanParser', () => {
|
||||
it('Вернет значение', () => {
|
||||
expect(booleanParser()('true')).toBe(true);
|
||||
expect(booleanParser()('false')).toBe(false);
|
||||
});
|
||||
|
||||
it('Вернет первое значение массива', () => {
|
||||
expect(booleanParser()(['true', 'false'])).toBe(true);
|
||||
});
|
||||
|
||||
it('Вернет undefined', () => {
|
||||
expect(booleanParser()()).toBeUndefined();
|
||||
expect(booleanParser()([])).toBeUndefined();
|
||||
});
|
||||
|
||||
it('Вернет дефолт', () => {
|
||||
expect(booleanParser(true)()).toBe(true);
|
||||
expect(booleanParser(false)([])).toBe(false);
|
||||
});
|
||||
|
||||
it('Вернет значение даже при наличии дефолта', () => {
|
||||
expect(booleanParser(false)(['true'])).toBe(true);
|
||||
expect(booleanParser(false)('true')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('arrayParser', () => {
|
||||
it('Вернет значение', () => {
|
||||
expect(arrayParser()('rtr')).toEqual(['rtr']);
|
||||
expect(arrayParser()(['rtr', 'rtr'])).toEqual(['rtr', 'rtr']);
|
||||
expect(arrayParser()([])).toEqual([]);
|
||||
});
|
||||
|
||||
it('Вернет undefined', () => {
|
||||
expect(arrayParser()()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('Вернет дефолт', () => {
|
||||
expect(arrayParser(['def'])()).toEqual(['def']);
|
||||
});
|
||||
|
||||
it('Вернет значение даже при наличии дефолта', () => {
|
||||
expect(arrayParser(['def'])('rtr')).toEqual(['rtr']);
|
||||
expect(arrayParser(['def'])(['rtr'])).toEqual(['rtr']);
|
||||
});
|
||||
});
|
||||
16
src/core/utils/__test__/toArray.test.ts
Normal file
16
src/core/utils/__test__/toArray.test.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import {toArray} from '../toArray';
|
||||
|
||||
describe('toArray', () => {
|
||||
it('Должен вернуть пустой массив', () => {
|
||||
expect(toArray(undefined)).toEqual([]);
|
||||
expect(toArray([])).toEqual([]);
|
||||
});
|
||||
|
||||
it('Должен вернуть массив', () => {
|
||||
expect(toArray('hji')).toEqual(['hji']);
|
||||
expect(toArray(null)).toEqual([null]);
|
||||
expect(toArray(0)).toEqual([0]);
|
||||
expect(toArray([0, null, 'gh'])).toEqual([0, null, 'gh']);
|
||||
expect(toArray([0, [null], 'gh'])).toEqual([0, [null], 'gh']);
|
||||
});
|
||||
});
|
||||
26
src/core/utils/__test__/toRequestParamValue.test.ts
Normal file
26
src/core/utils/__test__/toRequestParamValue.test.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import {toRequestParamValue} from '../toRequestParamValue';
|
||||
|
||||
describe('toRequestParamValue', () => {
|
||||
it('Возвращает простые значения', () => {
|
||||
expect(toRequestParamValue('trt')).toBe('trt');
|
||||
expect(toRequestParamValue(0)).toBe(0);
|
||||
expect(toRequestParamValue(false)).toBe(false);
|
||||
});
|
||||
|
||||
it('Простые пустые значения возвращают undefined', () => {
|
||||
expect(toRequestParamValue('')).toBeUndefined();
|
||||
expect(toRequestParamValue(null)).toBeUndefined();
|
||||
expect(toRequestParamValue(undefined)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('Возвращает заполненные объекты', () => {
|
||||
expect(toRequestParamValue({foo: undefined})).toMatchObject({foo: undefined});
|
||||
expect(toRequestParamValue({foo: 'bar'})).toMatchObject({foo: 'bar'});
|
||||
expect(toRequestParamValue(['rtt'])).toEqual(['rtt']);
|
||||
});
|
||||
|
||||
it('Пустые объекты возвращают undefined', () => {
|
||||
expect(toRequestParamValue({})).toBeUndefined();
|
||||
expect(toRequestParamValue([])).toBeUndefined();
|
||||
});
|
||||
});
|
||||
15
src/core/utils/getParamFromUrl.ts
Normal file
15
src/core/utils/getParamFromUrl.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import {QueryParsers} from './getQueryFromUrl';
|
||||
|
||||
export const getParamsFromUrl = <T extends Record<string, unknown>>(
|
||||
paramParsers: QueryParsers<T>,
|
||||
params: Record<string, string>
|
||||
) => {
|
||||
return Object.keys(paramParsers).reduce<T>((memo, key) => {
|
||||
const parser = paramParsers[key];
|
||||
|
||||
return {
|
||||
...memo,
|
||||
[key]: parser?.(params[key]),
|
||||
};
|
||||
}, {} as T);
|
||||
};
|
||||
19
src/core/utils/getQueryFromUrl.ts
Normal file
19
src/core/utils/getQueryFromUrl.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import {decode} from 'querystring';
|
||||
|
||||
export type QueryParser<T> = (value?: string | string[]) => T;
|
||||
export type QueryParsers<T> = {[K in keyof T]: QueryParser<T[K]>};
|
||||
|
||||
export const getQueryFromUrl = <T extends Record<string, unknown>>(queryParsers: QueryParsers<T>, search?: string) => {
|
||||
const query = decode((search || location.search).slice(1));
|
||||
|
||||
return Object.keys(queryParsers).reduce<T>((memo, key) => {
|
||||
if (key in queryParsers) {
|
||||
const parser = queryParsers[key];
|
||||
return {
|
||||
...memo,
|
||||
[key]: parser?.(query[key]),
|
||||
};
|
||||
}
|
||||
return memo;
|
||||
}, {} as T);
|
||||
};
|
||||
10
src/core/utils/jsonParse.ts
Normal file
10
src/core/utils/jsonParse.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export const jsonParse = <T>(str?: string, defaultValue?: T): Undefinable<T> => {
|
||||
const trimStr = str?.trim();
|
||||
try {
|
||||
const parsedValue = JSON.parse(trimStr ?? '');
|
||||
|
||||
return parsedValue === undefined ? defaultValue : parsedValue;
|
||||
} catch (e) {
|
||||
return defaultValue;
|
||||
}
|
||||
};
|
||||
3
src/core/utils/jsonStringify.ts
Normal file
3
src/core/utils/jsonStringify.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export const jsonStringify = <T>(obj: T, space = 4): string => (
|
||||
JSON.stringify(obj, null, space)
|
||||
);
|
||||
3
src/core/utils/objectEntries.ts
Normal file
3
src/core/utils/objectEntries.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export const objectEntries = <T extends Record<string, unknown>, R extends keyof T>(obj: T) => (
|
||||
Object.entries(obj) as Array<[R, T[R]]>
|
||||
);
|
||||
3
src/core/utils/objectKeys.ts
Normal file
3
src/core/utils/objectKeys.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export const objectKeys = <T extends Record<string, unknown>>(obj: T) => (
|
||||
Object.keys(obj) as Array<keyof T>
|
||||
);
|
||||
8
src/core/utils/parsers.ts
Normal file
8
src/core/utils/parsers.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import {isNumber, isString} from 'lodash';
|
||||
|
||||
export const toNumber = (value: unknown): Undefinable<number> => {
|
||||
if (isNumber(value) || isString(value)) {
|
||||
const prepareValue = Number(value);
|
||||
return Number.isNaN(prepareValue) ? undefined : prepareValue;
|
||||
}
|
||||
};
|
||||
29
src/core/utils/performTextSearch.ts
Normal file
29
src/core/utils/performTextSearch.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import ru from 'convert-layout/ru';
|
||||
import {isEmpty, isNotEmpty} from '_referers/common';
|
||||
|
||||
export function performTextSearch<T, K extends keyof T>(items: T[], searchText: string, searchProperties: K[]) {
|
||||
if (isEmpty(items) || isEmpty(searchText)) {
|
||||
return items;
|
||||
}
|
||||
|
||||
const query = searchText.toLowerCase();
|
||||
const queryToEn = ru.toEn(query);
|
||||
const queryToRu = ru.fromEn(query);
|
||||
|
||||
const hasQuery = (itemText: string) => {
|
||||
const text = itemText.toLowerCase();
|
||||
|
||||
/**
|
||||
* Т.к. convert-layout заменяет не все символы верно,
|
||||
* ищем так же по первоначальной строке
|
||||
* https://github.com/ai/convert-layout/issues/22
|
||||
*/
|
||||
return text.includes(query) || text.includes(queryToEn) || text.includes(queryToRu);
|
||||
};
|
||||
|
||||
return items.filter(item => searchProperties.some(property => {
|
||||
const propertyValue = item[property];
|
||||
const text = isNotEmpty(propertyValue) ? `${propertyValue}` : undefined;
|
||||
return text && hasQuery(text);
|
||||
}));
|
||||
}
|
||||
53
src/core/utils/queryParsers.ts
Normal file
53
src/core/utils/queryParsers.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import {head} from 'lodash';
|
||||
import {QueryParser} from './getQueryFromUrl';
|
||||
import {toNumber} from './parsers';
|
||||
|
||||
export function stringParser<T extends string>(): QueryParser<Undefinable<T>>;
|
||||
export function stringParser<T extends string>(defaultValue: T): QueryParser<T>;
|
||||
export function stringParser<T extends string>(defaultValue?: T) {
|
||||
return (val?: string | string[]) => {
|
||||
const value = Array.isArray(val) ? head(val) : val;
|
||||
|
||||
return value ?? defaultValue;
|
||||
};
|
||||
}
|
||||
|
||||
export function numberParser(): QueryParser<Undefinable<number>>;
|
||||
export function numberParser(defaultValue?: number): QueryParser<number>;
|
||||
export function numberParser(defaultValue?: number) {
|
||||
return (val?: string | string[]) => {
|
||||
const value = Array.isArray(val) ? head(val) : val;
|
||||
|
||||
return toNumber(value) ?? defaultValue;
|
||||
};
|
||||
}
|
||||
|
||||
export function booleanParser(): QueryParser<Undefinable<boolean>>;
|
||||
export function booleanParser(defaultValue: boolean): QueryParser<boolean>;
|
||||
export function booleanParser(defaultValue?: boolean) {
|
||||
return (val?: string | string[]) => {
|
||||
const value = Array.isArray(val) ? head(val) : val;
|
||||
|
||||
if (value === 'true') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (value === 'false') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
};
|
||||
}
|
||||
|
||||
export function arrayParser<T extends string>(): QueryParser<Undefinable<T[]>>;
|
||||
export function arrayParser<T extends string>(defaultValue: T[]): QueryParser<T[]>;
|
||||
export function arrayParser<T extends string>(defaultValue?: T[]) {
|
||||
return (val?: string | string[]) => {
|
||||
if (Array.isArray(val)) {
|
||||
return val;
|
||||
}
|
||||
|
||||
return val === undefined ? defaultValue : [val];
|
||||
};
|
||||
}
|
||||
9
src/core/utils/toArray.ts
Normal file
9
src/core/utils/toArray.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export const toArray = <T>(value?: T | T[]): T[] => {
|
||||
if (Array.isArray(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return [
|
||||
...(value !== undefined ? [value] : [])
|
||||
];
|
||||
};
|
||||
7
src/core/utils/toRequestParamValue.ts
Normal file
7
src/core/utils/toRequestParamValue.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import {isNotEmpty} from '_referers/common';
|
||||
|
||||
export function toRequestParamValue<T>(val: T): T;
|
||||
export function toRequestParamValue<T>(val?: T): Undefinable<T>;
|
||||
export function toRequestParamValue<T>(val?: T) {
|
||||
return isNotEmpty(val) ? val : undefined;
|
||||
}
|
||||
33
src/core/utils/triggerLink.ts
Normal file
33
src/core/utils/triggerLink.ts
Normal file
@ -0,0 +1,33 @@
|
||||
type Options = {
|
||||
download?: boolean | string;
|
||||
target?: '_self' | '_blank';
|
||||
};
|
||||
|
||||
const DEFAULT_OPTIONS = {
|
||||
target: '_self',
|
||||
download: false,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Использование этой функции требуется для открытия ссылок в новых
|
||||
* вкладках из методов сервиса. Внутри компонентов его не используем.
|
||||
*/
|
||||
export const triggerLink = (link: string, options?: Options) => {
|
||||
const finalOptions = {
|
||||
...DEFAULT_OPTIONS,
|
||||
...options
|
||||
};
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.href = link;
|
||||
a.target = finalOptions.target;
|
||||
|
||||
if (finalOptions.download === true) {
|
||||
a.download = 'yes';
|
||||
} else if (typeof finalOptions.download === 'string') {
|
||||
a.download = finalOptions.download;
|
||||
}
|
||||
|
||||
a.dispatchEvent(new MouseEvent('click'));
|
||||
document.removeChild(a);
|
||||
};
|
||||
21
src/pages/main/components/page/Page.tsx
Normal file
21
src/pages/main/components/page/Page.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import React, {memo} from 'react';
|
||||
import {changeNameAction, nameAtom} from '_infrastructure/atom/exampleAtom';
|
||||
import {useAction, useAtom} from '@reatom/react';
|
||||
|
||||
const MainPage: React.FC = () => {
|
||||
const name = useAtom(nameAtom);
|
||||
const handleChangeName = useAction(e => changeNameAction(e.currentTarget.value));
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>main page</div>
|
||||
|
||||
<form>
|
||||
<label htmlFor="name">Enter your name: </label>
|
||||
<input id="name" value={name} onChange={handleChangeName} />
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(MainPage);
|
||||
1
src/pages/main/components/page/index.ts
Normal file
1
src/pages/main/components/page/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export {default} from './Page';
|
||||
8
src/pages/main/routing.tsx
Normal file
8
src/pages/main/routing.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
import React from 'react';
|
||||
import {Route} from 'react-router-dom';
|
||||
import {ROUTES} from '_consts/common';
|
||||
import Page from './components/page';
|
||||
|
||||
export default (
|
||||
<Route component={Page} path={ROUTES.MAIN} exact />
|
||||
);
|
||||
9
src/pages/not-found/components/page/Page.tsx
Normal file
9
src/pages/not-found/components/page/Page.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import React, {memo} from 'react';
|
||||
|
||||
const NotFoundPage: React.FC = () => {
|
||||
return (
|
||||
<div>404: Not Found Page</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(NotFoundPage);
|
||||
1
src/pages/not-found/components/page/index.ts
Normal file
1
src/pages/not-found/components/page/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export {default} from './Page';
|
||||
Reference in New Issue
Block a user