This commit is contained in:
2021-06-12 17:48:26 +03:00
commit 3e68914c92
56 changed files with 26153 additions and 0 deletions

View File

@ -0,0 +1,17 @@
html {
height: 100%;
}
body {
height: 100%;
margin: 0;
}
#root {
height: 100%;
}
a {
color: inherit;
text-decoration: none;
}

View 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);

View File

@ -0,0 +1 @@
export {default} from './Page';

17
src/app/index.tsx Normal file
View 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')
);

View File

@ -0,0 +1,3 @@
export const ROUTES = {
MAIN: '/'
};

3
src/core/dts/comon.d.ts vendored Normal file
View File

@ -0,0 +1,3 @@
type Undefinable<T> = T | undefined;
type Nullable<T> = T | undefined | null;

View 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;
}

View 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]);
}

View 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]);
}

View 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,
];
};

View 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),
};

View 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),
]);

View 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();
});
});

View 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);

View 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;
}
},
};
};

View 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});
});
});

View 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'],
});
});
});

View 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);
});
});

View 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();
});
});

View 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([]);
});
});

View 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']);
});
});

View 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']);
});
});

View 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();
});
});

View 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);
};

View 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);
};

View 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;
}
};

View File

@ -0,0 +1,3 @@
export const jsonStringify = <T>(obj: T, space = 4): string => (
JSON.stringify(obj, null, space)
);

View 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]]>
);

View File

@ -0,0 +1,3 @@
export const objectKeys = <T extends Record<string, unknown>>(obj: T) => (
Object.keys(obj) as Array<keyof T>
);

View 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;
}
};

View 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);
}));
}

View 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];
};
}

View File

@ -0,0 +1,9 @@
export const toArray = <T>(value?: T | T[]): T[] => {
if (Array.isArray(value)) {
return value;
}
return [
...(value !== undefined ? [value] : [])
];
};

View 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;
}

View 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);
};

View 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);

View File

@ -0,0 +1 @@
export {default} from './Page';

View 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 />
);

View 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);

View File

@ -0,0 +1 @@
export {default} from './Page';