#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

@ -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 {HashRouter, Route, Switch} from 'react-router-dom';
import React, {Fragment, memo} from 'react';
import {Route, Switch} from 'react-router-dom';
import mainPageRouter from '_pages/main/routing';
import chaosBoxPageRouter from '_pages/chaos-box/routing';
import calendarPageRouter from '_pages/calendar/routing';
@ -8,28 +8,32 @@ import projectsPageRouter from '_pages/projects/routing';
import settingsPageRouter from '_pages/settings/routing';
import signInPageRouter from '_pages/sign-in/routing';
import tagsPageRouter from '_pages/tags/routing';
import NotFoundPage from '_pages/not-found/components/page/Page';
import TopMenu from '../top-menu/TopMenu';
import NotFoundPage from '_pages/not-found/components/page';
import TopMenu from '../top-menu';
import './Page.scss';
import BothMenu from '../both-menu';
const Page: React.FC = () => {
return (
<HashRouter>
<Fragment>
<TopMenu />
<Switch>
{mainPageRouter}
{chaosBoxPageRouter}
{calendarPageRouter}
{informationPageRouter}
{projectsPageRouter}
{settingsPageRouter}
{signInPageRouter}
{tagsPageRouter}
<Route>
<NotFoundPage />
</Route>
</Switch>
</HashRouter>
<div>
<Switch>
{mainPageRouter}
{chaosBoxPageRouter}
{calendarPageRouter}
{informationPageRouter}
{projectsPageRouter}
{settingsPageRouter}
{signInPageRouter}
{tagsPageRouter}
<Route>
<NotFoundPage />
</Route>
</Switch>
</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 {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 Toolbar from '@material-ui/core/Toolbar';
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({
root: {
flexGrow: 1,
},
menuButton: {
marginRight: theme.spacing(2),
},
title: {
flexGrow: 1,
display: 'flex',
justifyContent: 'center',
},
}),
);
const TopMenu: React.FC = () => {
const classes = useStyles();
const {pageType} = useParams();
const history = useHistory();
const handleGoRoot = () => {
history.push(ROUTES.MAIN);
};
const title = PAGE_TITLE[pageType];
return (
<div className={classes.root}>
<AppBar position="static">
<Toolbar>
<Typography variant="h6" className={classes.title}>
Free your brain
{pageType === PageType.Main && (
<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>
<Avatar src={NO_NAME_AVATAR} />
</Toolbar>
</AppBar>
</div>

View File

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

View File

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

View File

@ -1,3 +1,5 @@
import {PageType} from '../enums/common';
export const ROUTES = {
MAIN: '/',
CHAOS_BOX: '/chaos-box',
@ -8,3 +10,14 @@ export const ROUTES = {
SETTINGS: '/settings',
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)
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>
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) => {
return queryParsers ? Object.keys(queryParsers).reduce<T>((memo, key) => {
if (key in queryParsers) {
const parser = queryParsers[key];
return {
@ -67,6 +65,6 @@ export function useQuery<T extends {[name: string]: unknown}>(
};
}
return memo;
}, {}) : query;
}, {} as T) : query;
}, [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 = {
title: string;
url: string;
import {FolderType, TaskStatus} from '../enums/common';
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 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 {Route} from 'react-router-dom';
import {ROUTES} from '_consts/common';
import Page from './components/page/Page';
import Page from './components/page';
export default (
<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 {Route} from 'react-router-dom';
import {ROUTES} from '_consts/common';
import Page from './components/page/Page';
import Page from './components/page';
export default (
<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 {AuthService} from '_services/AuthService';
import {useStream} from '_utils/useStream';
import {createService} from '_utils/createService';
import ComponentStream from '../component-stream/ComponentStream';
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;
}
});
import {tasksService} from '_services/TasksService';
import {List, ListItem, ListItemIcon, ListItemText} from '@material-ui/core';
import InboxIcon from '@material-ui/icons/Inbox';
const MainPage: React.FC = () => {
const {isAuth} = useStream(AuthService.state$, AuthService.initState);
const toggle = () => AuthService.handleChangeAuth(!isAuth);
const data = useStream(service.stream$, 0);
const taskList = useStream(tasksService.stream$, []);
return (
<div>
Main Page
Auth: {isAuth ? 'yes' : 'no'}
<button onClick={toggle}>click</button>
<ComponentStream />
<div>{data}</div>
<button onClick={service.actions.add}>Add</button>
<button onClick={service.actions.sub}>Sub</button>
</div>
<List component="nav" aria-label="main mailbox folders">
{taskList.map(task => (
<ListItem button key={task.id}>
<ListItemIcon>
<InboxIcon />
</ListItemIcon>
<ListItemText
primary={task.title ?? 'Нет темы'}
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 {Route} from 'react-router-dom';
import {ROUTES} from '_consts/common';
import Page from './components/page/Page';
import Page from './components/page';
export default (
<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 {Route} from 'react-router-dom';
import {ROUTES} from '_consts/common';
import Page from './components/page/Page';
import Page from './components/page';
export default (
<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 {Route} from 'react-router-dom';
import {ROUTES} from '_consts/common';
import Page from './components/page/Page';
import Page from './components/page';
export default (
<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 {Route} from 'react-router-dom';
import {ROUTES} from '_consts/common';
import Page from './components/page/Page';
import Page from './components/page';
export default (
<Route component={Page} path={ROUTES.TAGS} exact />