#10. Добавление localStorage сервиса, добавлены верхнее и нижнее меню приложения (#11)

This commit is contained in:
Nikolay
2020-12-26 22:33:23 +03:00
committed by GitHub
parent 31ad97954b
commit ef4a6ecbc8
37 changed files with 373 additions and 174 deletions

View File

@ -84,8 +84,9 @@
"require-await": "warn", "require-await": "warn",
"wrap-iife": ["warn", "inside"], "wrap-iife": ["warn", "inside"],
"yoda": "warn", "yoda": "warn",
"no-shadow": "warn", "no-shadow": "off",
"@typescript-eslint/no-use-before-define": "off", "@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/no-shadow": "warn",
"@typescript-eslint/no-namespace": "off", "@typescript-eslint/no-namespace": "off",
"@typescript-eslint/explicit-module-boundary-types": "off", "@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-unused-vars": ["error"], "@typescript-eslint/no-unused-vars": ["error"],

View File

@ -0,0 +1,80 @@
import {AppBar, createStyles, Fab, IconButton, makeStyles, Theme, Toolbar} from '@material-ui/core';
import React, {memo} from 'react';
import MoreIcon from '@material-ui/icons/MoreVert';
import AddIcon from '@material-ui/icons/Add';
import MoveToInboxIcon from '@material-ui/icons/MoveToInbox';
import CalendarTodayIcon from '@material-ui/icons/CalendarToday';
import ListAltIcon from '@material-ui/icons/ListAlt';
const useStyles = makeStyles((theme: Theme) =>
createStyles({
iconRight: {
marginRight: theme.spacing(2),
},
appBar: {
top: 'auto',
bottom: 0,
},
grow: {
flexGrow: 1,
},
fabButton: {
position: 'absolute',
zIndex: 1,
top: -30,
left: 0,
right: 0,
margin: '0 auto',
},
}),
);
const BothMenu: React.FC = () => {
const classes = useStyles();
return (
<AppBar
position="fixed"
color="primary"
className={classes.appBar}
>
<Toolbar>
<IconButton
className={classes.iconRight}
edge="start"
color="inherit"
>
<MoveToInboxIcon />
</IconButton>
<IconButton
edge="end"
color="inherit"
>
<ListAltIcon />
</IconButton>
<Fab
color="secondary"
className={classes.fabButton}
>
<AddIcon />
</Fab>
<div className={classes.grow} />
<IconButton
className={classes.iconRight}
edge="start"
color="inherit"
>
<CalendarTodayIcon />
</IconButton>
<IconButton
edge="end"
color="inherit"
>
<MoreIcon />
</IconButton>
</Toolbar>
</AppBar>
);
};
export default memo(BothMenu);

View File

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

View File

@ -1,5 +1,5 @@
import React, {memo} from 'react'; import React, {Fragment, memo} from 'react';
import {HashRouter, Route, Switch} from 'react-router-dom'; import {Route, Switch} from 'react-router-dom';
import mainPageRouter from '_pages/main/routing'; import mainPageRouter from '_pages/main/routing';
import chaosBoxPageRouter from '_pages/chaos-box/routing'; import chaosBoxPageRouter from '_pages/chaos-box/routing';
import calendarPageRouter from '_pages/calendar/routing'; import calendarPageRouter from '_pages/calendar/routing';
@ -8,14 +8,16 @@ import projectsPageRouter from '_pages/projects/routing';
import settingsPageRouter from '_pages/settings/routing'; import settingsPageRouter from '_pages/settings/routing';
import signInPageRouter from '_pages/sign-in/routing'; import signInPageRouter from '_pages/sign-in/routing';
import tagsPageRouter from '_pages/tags/routing'; import tagsPageRouter from '_pages/tags/routing';
import NotFoundPage from '_pages/not-found/components/page/Page'; import NotFoundPage from '_pages/not-found/components/page';
import TopMenu from '../top-menu/TopMenu'; import TopMenu from '../top-menu';
import './Page.scss'; import './Page.scss';
import BothMenu from '../both-menu';
const Page: React.FC = () => { const Page: React.FC = () => {
return ( return (
<HashRouter> <Fragment>
<TopMenu /> <TopMenu />
<div>
<Switch> <Switch>
{mainPageRouter} {mainPageRouter}
{chaosBoxPageRouter} {chaosBoxPageRouter}
@ -29,7 +31,9 @@ const Page: React.FC = () => {
<NotFoundPage /> <NotFoundPage />
</Route> </Route>
</Switch> </Switch>
</HashRouter> </div>
<BothMenu />
</Fragment>
); );
}; };

View File

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

View File

@ -1,28 +0,0 @@
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 '_types/common';
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);

View File

@ -1,32 +1,72 @@
import React, {memo} from 'react'; import React, {memo} from 'react';
import {createStyles, makeStyles, Theme} from '@material-ui/core/styles'; import {useHistory} from 'react-router-dom';
import {createStyles, makeStyles} from '@material-ui/core/styles';
import AppBar from '@material-ui/core/AppBar'; import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar'; import Toolbar from '@material-ui/core/Toolbar';
import Typography from '@material-ui/core/Typography'; import Typography from '@material-ui/core/Typography';
import IconButton from '@material-ui/core/IconButton';
import ArrowBackIosIcon from '@material-ui/icons/ArrowBackIos';
import SearchIcon from '@material-ui/icons/Search';
import {Avatar} from '@material-ui/core';
import {useParams} from '_hooks/useParams';
import {PageType} from '_enums/common';
import {PAGE_TITLE, ROUTES} from '_consts/common';
const useStyles = makeStyles((theme: Theme) => const NO_NAME_AVATAR = 'https://d.newsweek.com/en/full/425257/02-10-putin-economy.jpg';
const useStyles = makeStyles(() =>
createStyles({ createStyles({
root: { root: {
flexGrow: 1, flexGrow: 1,
}, },
menuButton: {
marginRight: theme.spacing(2),
},
title: { title: {
flexGrow: 1, flexGrow: 1,
display: 'flex',
justifyContent: 'center',
}, },
}), }),
); );
const TopMenu: React.FC = () => { const TopMenu: React.FC = () => {
const classes = useStyles(); const classes = useStyles();
const {pageType} = useParams();
const history = useHistory();
const handleGoRoot = () => {
history.push(ROUTES.MAIN);
};
const title = PAGE_TITLE[pageType];
return ( return (
<div className={classes.root}> <div className={classes.root}>
<AppBar position="static"> <AppBar position="static">
<Toolbar> <Toolbar>
<Typography variant="h6" className={classes.title}> {pageType === PageType.Main && (
Free your brain <IconButton
edge="start"
color="inherit"
>
<SearchIcon />
</IconButton>
)}
{pageType !== PageType.Main && (
<IconButton
onClick={handleGoRoot}
edge="start"
color="inherit"
>
<ArrowBackIosIcon />
</IconButton>
)}
<Typography
variant="h6"
className={classes.title}
>
{title}
</Typography> </Typography>
<Avatar src={NO_NAME_AVATAR} />
</Toolbar> </Toolbar>
</AppBar> </AppBar>
</div> </div>

View File

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

View File

@ -1,10 +1,13 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import App from './components/page/Page'; import {HashRouter} from 'react-router-dom';
import App from './components/page';
ReactDOM.render( ReactDOM.render(
<React.StrictMode> <React.StrictMode>
<HashRouter >
<App /> <App />
</HashRouter>
</React.StrictMode>, </React.StrictMode>,
document.getElementById('root') document.getElementById('root')
); );

View File

@ -1,3 +1,5 @@
import {PageType} from '../enums/common';
export const ROUTES = { export const ROUTES = {
MAIN: '/', MAIN: '/',
CHAOS_BOX: '/chaos-box', CHAOS_BOX: '/chaos-box',
@ -8,3 +10,14 @@ export const ROUTES = {
SETTINGS: '/settings', SETTINGS: '/settings',
SIGN_IN: '/sign-in', SIGN_IN: '/sign-in',
}; };
export const PAGE_TITLE = {
[PageType.Main]: 'Free your brain',
[PageType.ChaosBox]: 'Chaos box',
[PageType.Calendar]: 'Calendar',
[PageType.Information]: 'Information',
[PageType.Tags]: 'Tags',
[PageType.Projects]: 'Projects',
[PageType.Settings]: 'Settings',
[PageType.SigIn]: 'SigIn',
};

21
src/core/enums/common.ts Normal file
View File

@ -0,0 +1,21 @@
export const enum TaskStatus {
Progress = 'progress',
Removed = 'removed',
Done = 'done',
}
export const enum FolderType {
Project = 'project',
Information = 'information',
}
export enum PageType {
Main = '',
ChaosBox = 'chaos-box',
Projects = 'projects',
Information = 'information',
Tags = 'tags',
Calendar = 'calendar',
Settings = 'settings',
SigIn = 'sign-in',
}

View File

@ -0,0 +1,25 @@
import {useMemo} from 'react';
import {useLocation, useParams as useReactParams} from 'react-router-dom';
import {PageType} from '../enums/common';
import {getPageType} from '../utils/common';
type ParamsParser<T> = (value?: string) => T;
export type ParamsParsers<T> = Partial<{[K in keyof T]: ParamsParser<T[K]>}>;
export function useParams<T extends {[name: string]: unknown}>(paramParsers: ParamsParsers<T> = {}) {
const params = useReactParams<Record<keyof T, string>>();
const {pathname} = useLocation();
return useMemo(() => {
return Object.keys(paramParsers).reduce<T & {pageType: PageType}>((memo, key) => {
const parser = paramParsers[key];
return {
...memo,
[key]: parser?.(params[key]),
};
}, {
pageType: getPageType(pathname),
} as T & {pageType: PageType});
}, [params, paramParsers, pathname]);
}

View File

@ -49,16 +49,14 @@ export function booleanParser(defaultValue?: boolean) {
// Array parser (должен уметь с enum) // 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}>( export function useQuery<T extends {[name: string]: unknown}>(
queryParsers?: QueryParsers<T> queryParsers: QueryParsers<T>
): ParsedUrlQuery | Partial<T> { ): ParsedUrlQuery | Partial<T> {
const {search} = useLocation(); const {search} = useLocation();
return useMemo(() => { return useMemo(() => {
const query = parse(search.slice(1)); const query = parse(search.slice(1));
return queryParsers ? Object.keys(query).reduce<Partial<T>>((memo, key) => { return queryParsers ? Object.keys(queryParsers).reduce<T>((memo, key) => {
if (key in queryParsers) { if (key in queryParsers) {
const parser = queryParsers[key]; const parser = queryParsers[key];
return { return {
@ -67,6 +65,6 @@ export function useQuery<T extends {[name: string]: unknown}>(
}; };
} }
return memo; return memo;
}, {}) : query; }, {} as T) : query;
}, [search, queryParsers]); }, [search, queryParsers]);
} }

View File

@ -0,0 +1,5 @@
import {PageType} from '../enums/common';
export const isPageType = (value?: string): value is PageType => (
Object.values(PageType).some(pageType => pageType === value)
);

View File

@ -1,20 +0,0 @@
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,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,36 @@
import {v4} from 'uuid';
import {TaskStatus} from '../enums/common';
import {Task} from '../types/common';
import {createService} from '../utils/createService';
import {makeLocalStorageService} from './LocalStorageService';
const TASK_STORAGE_NAME = 'FYB_TASK_STORAGE';
const INIT_TASKS: Task[] = [
{
id: v4(),
status: TaskStatus.Progress,
created_at: '2021-03-01T13:00+03:00',
title: 'Первая таска',
body: 'Описание таски',
},
{
id: v4(),
status: TaskStatus.Progress,
created_at: '2021-03-01T13:00+03:00',
title: 'Вторая таска',
body: 'Описание таски',
},
{
id: v4(),
status: TaskStatus.Progress,
created_at: '2021-03-01T13:00+03:00',
title: 'Третья таска',
body: 'Описание таски',
},
];
const taskListService = makeLocalStorageService(INIT_TASKS, TASK_STORAGE_NAME);
export const tasksService = createService(taskListService.get(), {
});

View File

@ -1,13 +0,0 @@
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);

View File

@ -1,4 +1,49 @@
export type ListItem = { import {FolderType, TaskStatus} from '../enums/common';
title: string;
url: string; export type Task = {
/**
* Идентификатор
*/
id: string;
title?: string;
body?: string;
created_at: string;
start_at?: string;
end_at?: string;
/**
* Контекст выполнения, теги
*/
tags?: string[];
/**
* Папка, проект, список
*/
folder?: string;
status: TaskStatus;
};
export type Folder = {
/**
* Идентификатор
*/
id: string;
name: string;
type: FolderType;
/**
* Папка, проект
*/
folder?: string;
removed?: boolean;
};
export type Tag = {
/**
* Идентификатор
*/
id: string;
name: string;
/**
* Цвет тега в формате #FFFFFF
*/
color?: string;
removed?: boolean;
}; };

View File

@ -1 +1,9 @@
import {PageType} from '../enums/common';
import {isPageType} from '../referers/common';
export const numberToString = (num: number): string => num.toString(); export const numberToString = (num: number): string => num.toString();
export const getPageType = (pathname?: string): PageType => {
const path = pathname?.startsWith('/') ? pathname.slice(1) : pathname ?? '';
return isPageType(path) ? path : PageType.Main;
};

View File

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

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import {Route} from 'react-router-dom'; import {Route} from 'react-router-dom';
import {ROUTES} from '_consts/common'; import {ROUTES} from '_consts/common';
import Page from './components/page/Page'; import Page from './components/page';
export default ( export default (
<Route component={Page} path={ROUTES.CHAOS_BOX} exact /> <Route component={Page} path={ROUTES.CHAOS_BOX} exact />

View File

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

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import {Route} from 'react-router-dom'; import {Route} from 'react-router-dom';
import {ROUTES} from '_consts/common'; import {ROUTES} from '_consts/common';
import Page from './components/page/Page'; import Page from './components/page';
export default ( export default (
<Route component={Page} path={ROUTES.INFORMATION} exact /> <Route component={Page} path={ROUTES.INFORMATION} exact />

View File

@ -1,32 +0,0 @@
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);

View File

@ -1,41 +1,26 @@
import React, {memo} from 'react'; import React, {memo} from 'react';
import {AuthService} from '_services/AuthService';
import {useStream} from '_utils/useStream'; import {useStream} from '_utils/useStream';
import {createService} from '_utils/createService'; import {tasksService} from '_services/TasksService';
import ComponentStream from '../component-stream/ComponentStream'; import {List, ListItem, ListItemIcon, ListItemText} from '@material-ui/core';
import InboxIcon from '@material-ui/icons/Inbox';
const service = createService(1, {
changeWithStr: (state: number, val: string) => {
const parsedNumber = Number(val);
if (Number.isNaN(val)) {
return state;
}
return parsedNumber;
},
add: (state: number) => {
return state + 1;
},
sub: (state: number) => {
return state - 1;
}
});
const MainPage: React.FC = () => { const MainPage: React.FC = () => {
const {isAuth} = useStream(AuthService.state$, AuthService.initState); const taskList = useStream(tasksService.stream$, []);
const toggle = () => AuthService.handleChangeAuth(!isAuth);
const data = useStream(service.stream$, 0);
return ( return (
<div> <List component="nav" aria-label="main mailbox folders">
Main Page {taskList.map(task => (
Auth: {isAuth ? 'yes' : 'no'} <ListItem button key={task.id}>
<button onClick={toggle}>click</button> <ListItemIcon>
<ComponentStream /> <InboxIcon />
<div>{data}</div> </ListItemIcon>
<button onClick={service.actions.add}>Add</button> <ListItemText
<button onClick={service.actions.sub}>Sub</button> primary={task.title ?? 'Нет темы'}
</div> secondary={task.body ?? 'Нет Описания'}
/>
</ListItem>
))}
</List>
); );
}; };

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import {Route} from 'react-router-dom'; import {Route} from 'react-router-dom';
import {ROUTES} from '_consts/common'; import {ROUTES} from '_consts/common';
import Page from './components/page/Page'; import Page from './components/page';
export default ( export default (
<Route component={Page} path={ROUTES.PROJECTS} exact /> <Route component={Page} path={ROUTES.PROJECTS} exact />

View File

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

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import {Route} from 'react-router-dom'; import {Route} from 'react-router-dom';
import {ROUTES} from '_consts/common'; import {ROUTES} from '_consts/common';
import Page from './components/page/Page'; import Page from './components/page';
export default ( export default (
<Route component={Page} path={ROUTES.SETTINGS} exact /> <Route component={Page} path={ROUTES.SETTINGS} exact />

View File

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

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import {Route} from 'react-router-dom'; import {Route} from 'react-router-dom';
import {ROUTES} from '_consts/common'; import {ROUTES} from '_consts/common';
import Page from './components/page/Page'; import Page from './components/page';
export default ( export default (
<Route component={Page} path={ROUTES.SIGN_IN} exact /> <Route component={Page} path={ROUTES.SIGN_IN} exact />

View File

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

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import {Route} from 'react-router-dom'; import {Route} from 'react-router-dom';
import {ROUTES} from '_consts/common'; import {ROUTES} from '_consts/common';
import Page from './components/page/Page'; import Page from './components/page';
export default ( export default (
<Route component={Page} path={ROUTES.TAGS} exact /> <Route component={Page} path={ROUTES.TAGS} exact />

View File

@ -30,6 +30,7 @@
"_utils/*": ["./src/core/utils/*"], "_utils/*": ["./src/core/utils/*"],
"_enums/*": ["./src/core/enums/*"], "_enums/*": ["./src/core/enums/*"],
"_pages/*": ["./src/pages/*"], "_pages/*": ["./src/pages/*"],
"_referers/*": ["./src/referers/*"],
} }
}, },
"include": [ "include": [

View File

@ -5,6 +5,7 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const webpack = require('webpack'); const webpack = require('webpack');
module.exports = { module.exports = {
mode: 'development',
entry: { entry: {
app: { app: {
import: './src/app/index.tsx', import: './src/app/index.tsx',
@ -38,6 +39,7 @@ module.exports = {
_utils: path.resolve(__dirname, 'src/core/utils/'), _utils: path.resolve(__dirname, 'src/core/utils/'),
_enums: path.resolve(__dirname, 'src/core/enums/'), _enums: path.resolve(__dirname, 'src/core/enums/'),
_pages: path.resolve(__dirname, 'src/pages/'), _pages: path.resolve(__dirname, 'src/pages/'),
_referers: path.resolve(__dirname, 'src/referers/'),
} }
}, },
optimization: { optimization: {