#8. Добавление алиасов, разделение на чанки, разделение на страницы, организация фвйловой структуры (#9)

This commit is contained in:
Nikolay
2020-12-26 14:38:38 +03:00
committed by GitHub
parent faea0fb77a
commit 31ad97954b
58 changed files with 21397 additions and 17637 deletions

View File

@ -0,0 +1,7 @@
import {numberToString} from '_utils/common';
describe('test numberToString', () => {
it('success convert', () => {
expect(numberToString(56)).toBe('56');
});
});

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

View 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
View 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
View File

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

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

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

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

View 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
View File

@ -0,0 +1,4 @@
export type ListItem = {
title: string;
url: string;
};

View 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
View File

@ -0,0 +1 @@
export const numberToString = (num: number): string => num.toString();

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

View File

@ -0,0 +1,4 @@
export const toNumber = (val: unknown): Undefinable<number> => {
const prepareValue = Number(val);
return Number.isNaN(prepareValue) ? undefined : prepareValue;
};

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