#8. Добавление алиасов, разделение на чанки, разделение на страницы, организация фвйловой структуры (#9)
This commit is contained in:
7
src/core/__test__/utils.test.ts
Normal file
7
src/core/__test__/utils.test.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import {numberToString} from '_utils/common';
|
||||
|
||||
describe('test numberToString', () => {
|
||||
it('success convert', () => {
|
||||
expect(numberToString(56)).toBe('56');
|
||||
});
|
||||
});
|
||||
70
src/core/api/RegionsApi.ts
Normal file
70
src/core/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/core/api/StorageApi.ts
Normal file
44
src/core/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;
|
||||
},
|
||||
};
|
||||
};
|
||||
10
src/core/consts/common.ts
Normal file
10
src/core/consts/common.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export const ROUTES = {
|
||||
MAIN: '/',
|
||||
CHAOS_BOX: '/chaos-box',
|
||||
PROJECTS: '/projects',
|
||||
INFORMATION: '/information',
|
||||
TAGS: '/tags',
|
||||
CALENDAR: '/calendar',
|
||||
SETTINGS: '/settings',
|
||||
SIGN_IN: '/sign-in',
|
||||
};
|
||||
1
src/core/dts/comon.d.ts
vendored
Normal file
1
src/core/dts/comon.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
type Undefinable<T> = T | undefined;
|
||||
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;
|
||||
}
|
||||
72
src/core/hooks/useQuery.ts
Normal file
72
src/core/hooks/useQuery.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import {parse, ParsedUrlQuery} from 'querystring';
|
||||
import {head} from 'lodash';
|
||||
import {useMemo} from 'react';
|
||||
import {useLocation} from 'react-router-dom';
|
||||
import {toNumber} from '../utils/parsers';
|
||||
|
||||
type QueryParser<T> = (value?: string | string[]) => T;
|
||||
export type QueryParsers<T> = Partial<{[K in keyof T]: QueryParser<T[K]>}>;
|
||||
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
||||
// Date parser
|
||||
|
||||
// Array parser (должен уметь с enum)
|
||||
|
||||
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/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,
|
||||
];
|
||||
};
|
||||
53
src/core/infrastructure/Http.ts
Normal file
53
src/core/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);
|
||||
},
|
||||
};
|
||||
20
src/core/services/AuthService.ts
Normal file
20
src/core/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/core/services/service1.ts
Normal file
13
src/core/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);
|
||||
|
||||
4
src/core/types/common.ts
Normal file
4
src/core/types/common.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export type ListItem = {
|
||||
title: string;
|
||||
url: string;
|
||||
};
|
||||
26
src/core/utils/asyncDataUtils.tsx
Normal file
26
src/core/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))
|
||||
);
|
||||
};
|
||||
};
|
||||
1
src/core/utils/common.ts
Normal file
1
src/core/utils/common.ts
Normal file
@ -0,0 +1 @@
|
||||
export const numberToString = (num: number): string => num.toString();
|
||||
36
src/core/utils/createService.ts
Normal file
36
src/core/utils/createService.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import {startWith} from '@most/core';
|
||||
import {createAdapter} from '@most/adapter';
|
||||
import {pipe} from 'fp-ts/es6/pipeable';
|
||||
|
||||
type ServiceAction<State, ValType> = (data: State, val?: ValType) => State;
|
||||
|
||||
type ServiceActions<State, T extends Record<string, ServiceAction<State, unknown>>> = {
|
||||
[Key in keyof T]: T[Key] extends ServiceAction<State, infer D>
|
||||
? D extends void | undefined
|
||||
? () => State
|
||||
: (val: D) => State
|
||||
: never;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line
|
||||
export const createService = <State, Actions extends Record<string, ServiceAction<State, any>>>(
|
||||
initData: State,
|
||||
actions: Actions
|
||||
) => {
|
||||
const [handler, adapterStream$] = createAdapter<State>();
|
||||
let currValue = initData;
|
||||
const currStream$ = pipe(adapterStream$, startWith(initData));
|
||||
const currActions = Object.entries(actions).reduce((acc, [key, func]) => {
|
||||
// eslint-disable-next-line
|
||||
(acc as any)[key] = (val: unknown) => {
|
||||
currValue = func(currValue, val);
|
||||
handler(currValue);
|
||||
};
|
||||
return acc;
|
||||
}, {} as ServiceActions<State, Actions>);
|
||||
return {
|
||||
stream$: currStream$,
|
||||
actions: currActions,
|
||||
initialState: initData
|
||||
};
|
||||
};
|
||||
4
src/core/utils/parsers.ts
Normal file
4
src/core/utils/parsers.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export const toNumber = (val: unknown): Undefinable<number> => {
|
||||
const prepareValue = Number(val);
|
||||
return Number.isNaN(prepareValue) ? undefined : prepareValue;
|
||||
};
|
||||
32
src/core/utils/useStream.ts
Normal file
32
src/core/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
|
||||
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();
|
||||
};
|
||||
}, [stream$]);
|
||||
|
||||
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