1
0
Fork 0
mirror of https://github.com/muerwre/vault-frontend.git synced 2025-04-25 04:46:40 +07:00

made tags panel

This commit is contained in:
Fedor Katurov 2020-10-31 15:06:08 +07:00
parent f2289f4530
commit c4f60f3d81
31 changed files with 552 additions and 75 deletions

View file

@ -47,7 +47,7 @@
"resolve-url-loader": "^3.0.1", "resolve-url-loader": "^3.0.1",
"style-loader": "^0.21.0", "style-loader": "^0.21.0",
"ts-node": "^8.4.1", "ts-node": "^8.4.1",
"typescript": "^3.6.4", "typescript": "^3.7.2",
"uglifyjs-webpack-plugin": "^1.3.0", "uglifyjs-webpack-plugin": "^1.3.0",
"webpack": "^4.41.2", "webpack": "^4.41.2",
"webpack-cli": "^3.3.9", "webpack-cli": "^3.3.9",

View file

@ -0,0 +1,42 @@
import React, { FC, HTMLAttributes, useCallback, useEffect, useRef } from 'react';
import styles from './styles.module.scss';
interface IProps extends HTMLAttributes<HTMLDivElement> {
hasMore: boolean;
scrollReactPx?: number;
loadMore: () => void;
}
const InfiniteScroll: FC<IProps> = ({ children, hasMore, scrollReactPx, loadMore, ...props }) => {
const ref = useRef<HTMLDivElement>(null);
const onScrollEnd = useCallback(
(entries: IntersectionObserverEntry[]) => {
if (!hasMore || !entries[0].isIntersecting) return;
loadMore();
},
[hasMore, loadMore]
);
useEffect(() => {
if (!ref.current) return;
const observer = new IntersectionObserver(onScrollEnd, {
root: null,
rootMargin: '200px',
threshold: 1.0,
});
observer.observe(ref.current);
return () => observer.disconnect();
}, [ref.current, onScrollEnd]);
return (
<div {...props}>
{children}
{hasMore && <div className={styles.more} ref={ref} />}
</div>
);
};
export { InfiniteScroll };

View file

@ -0,0 +1,2 @@
.more {
}

View file

@ -54,4 +54,7 @@
font: $font_12_regular; font: $font_12_regular;
margin-top: 4px; margin-top: 4px;
opacity: 0.5; opacity: 0.5;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }

View file

@ -6,10 +6,13 @@ interface IProps {
is_editable?: boolean; is_editable?: boolean;
tags: ITag[]; tags: ITag[];
onChange?: (tags: string[]) => void; onChange?: (tags: string[]) => void;
onTagClick?: (tag: Partial<ITag>) => void;
} }
const NodeTags: FC<IProps> = memo(({ is_editable, tags, onChange }) => ( const NodeTags: FC<IProps> = memo(({ is_editable, tags, onChange, onTagClick }) => {
<Tags tags={tags} is_editable={is_editable} onTagsChange={onChange} /> return (
)); <Tags tags={tags} is_editable={is_editable} onTagsChange={onChange} onTagClick={onTagClick} />
);
});
export { NodeTags }; export { NodeTags };

View file

@ -1,4 +1,4 @@
import React, { ChangeEventHandler, FC, FocusEventHandler, KeyboardEventHandler } from 'react'; import React, { ChangeEventHandler, FC, FocusEventHandler, KeyboardEventHandler, useCallback, } from 'react';
import * as styles from './styles.scss'; import * as styles from './styles.scss';
import { ITag } from '~/redux/types'; import { ITag } from '~/redux/types';
import classNames = require('classnames'); import classNames = require('classnames');
@ -11,6 +11,7 @@ const getTagFeature = (tag: Partial<ITag>) => {
interface IProps { interface IProps {
tag: Partial<ITag>; tag: Partial<ITag>;
size?: 'normal' | 'big';
is_hoverable?: boolean; is_hoverable?: boolean;
is_editing?: boolean; is_editing?: boolean;
@ -18,15 +19,33 @@ interface IProps {
onInput?: ChangeEventHandler<HTMLInputElement>; onInput?: ChangeEventHandler<HTMLInputElement>;
onKeyUp?: KeyboardEventHandler; onKeyUp?: KeyboardEventHandler;
onBlur?: FocusEventHandler<HTMLInputElement>; onBlur?: FocusEventHandler<HTMLInputElement>;
onClick?: (tag: Partial<ITag>) => void;
} }
const Tag: FC<IProps> = ({ tag, is_hoverable, is_editing, onInput, onKeyUp, onBlur }) => ( const Tag: FC<IProps> = ({
tag,
is_hoverable,
is_editing,
size = 'normal',
onInput,
onKeyUp,
onBlur,
onClick,
}) => {
const onClickHandler = useCallback(() => {
if (!onClick) return;
onClick(tag);
}, [tag, onClick]);
return (
<div <div
className={classNames(styles.tag, getTagFeature(tag), { className={classNames(styles.tag, getTagFeature(tag), size, {
is_hoverable, is_hoverable,
is_editing, is_editing,
input: !!onInput, input: !!onInput,
clickable: !!onClick,
})} })}
onClick={onClickHandler}
> >
<div className={styles.hole} /> <div className={styles.hole} />
<div className={styles.title}>{tag.title}</div> <div className={styles.title}>{tag.title}</div>
@ -44,6 +63,7 @@ const Tag: FC<IProps> = ({ tag, is_hoverable, is_editing, onInput, onKeyUp, onBl
/> />
)} )}
</div> </div>
); );
};
export { Tag }; export { Tag };

View file

@ -1,6 +1,9 @@
$big: 1.2;
.tag { .tag {
@include outer_shadow(); @include outer_shadow();
cursor: default;
height: $tag_height; height: $tag_height;
background: $tag_bg; background: $tag_bg;
display: flex; display: flex;
@ -14,6 +17,17 @@
margin: 0 $gap $gap 0; margin: 0 $gap $gap 0;
position: relative; position: relative;
&:global(.big) {
height: $tag_height * $big;
font: $font_16_semibold;
border-radius: ($tag_height * $big / 2) 3px 3px ($tag_height * $big / 2);
.hole {
width: $tag_height * $big;
height: $tag_height * $big;
}
}
&:global(.is_hoverable) { &:global(.is_hoverable) {
cursor: pointer; cursor: pointer;
} }
@ -56,6 +70,10 @@
min-width: 100px; min-width: 100px;
} }
&:global(.clickable) {
cursor: pointer;
}
input { input {
background: none; background: none;
border: none; border: none;
@ -80,7 +98,6 @@
width: $tag_height; width: $tag_height;
height: $tag_height; height: $tag_height;
display: flex; display: flex;
// padding-right: 0px;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
flex: 0 0 $tag_height; flex: 0 0 $tag_height;

View file

@ -1,13 +1,13 @@
import React, { import React, {
ChangeEvent,
FC, FC,
HTMLAttributes, HTMLAttributes,
useState, KeyboardEvent,
useCallback, useCallback,
useEffect, useEffect,
KeyboardEvent,
ChangeEvent,
useRef,
useMemo, useMemo,
useRef,
useState,
} from 'react'; } from 'react';
import { TagField } from '~/components/containers/TagField'; import { TagField } from '~/components/containers/TagField';
import { ITag } from '~/redux/types'; import { ITag } from '~/redux/types';
@ -18,9 +18,10 @@ type IProps = HTMLAttributes<HTMLDivElement> & {
tags: Partial<ITag>[]; tags: Partial<ITag>[];
is_editable?: boolean; is_editable?: boolean;
onTagsChange?: (tags: string[]) => void; onTagsChange?: (tags: string[]) => void;
onTagClick?: (tag: Partial<ITag>) => void;
}; };
export const Tags: FC<IProps> = ({ tags, is_editable, onTagsChange, ...props }) => { export const Tags: FC<IProps> = ({ tags, is_editable, onTagsChange, onTagClick, ...props }) => {
const [input, setInput] = useState(''); const [input, setInput] = useState('');
const [data, setData] = useState([]); const [data, setData] = useState([]);
const timer = useRef(null); const timer = useRef(null);
@ -98,11 +99,11 @@ export const Tags: FC<IProps> = ({ tags, is_editable, onTagsChange, ...props })
return ( return (
<TagField {...props}> <TagField {...props}>
{catTags.map(tag => ( {catTags.map(tag => (
<Tag key={tag.title} tag={tag} /> <Tag key={tag.title} tag={tag} onClick={onTagClick} />
))} ))}
{ordinaryTags.map(tag => ( {ordinaryTags.map(tag => (
<Tag key={tag.title} tag={tag} /> <Tag key={tag.title} tag={tag} onClick={onTagClick} />
))} ))}
{data.map(tag => ( {data.map(tag => (

View file

@ -0,0 +1,18 @@
import React, { FC } from 'react';
import { INode } from '~/redux/types';
import styles from './styles.module.scss';
import { FlowRecentItem } from '~/components/flow/FlowRecentItem';
interface IProps {
nodes: INode[];
}
const TagSidebarList: FC<IProps> = ({ nodes }) => (
<div className={styles.list}>
{nodes.map(node => (
<FlowRecentItem node={node} key={node.id} />
))}
</div>
);
export { TagSidebarList };

View file

@ -0,0 +1,4 @@
.list {
flex: 1;
flex-direction: column;
}

View file

@ -1,4 +1,4 @@
import { INode, IComment } from '~/redux/types'; import { IComment, INode } from '~/redux/types';
import { ISocialProvider } from '~/redux/auth/types'; import { ISocialProvider } from '~/redux/auth/types';
export const API = { export const API = {
@ -46,4 +46,7 @@ export const API = {
BORIS: { BORIS: {
GET_BACKEND_STATS: '/stats/', GET_BACKEND_STATS: '/stats/',
}, },
TAG: {
NODES: `/tag/nodes`,
},
}; };

View file

@ -13,6 +13,7 @@ export const URLS = {
BACKEND_DOWN: '/oopsie', BACKEND_DOWN: '/oopsie',
}, },
NODE_URL: (id: number | string) => `/post${id}`, NODE_URL: (id: number | string) => `/post${id}`,
NODE_TAG_URL: (id: number, tagName: string) => `/post${id}/tag/${tagName}`,
PROFILE: (username: string) => `/~${username}`, PROFILE: (username: string) => `/~${username}`,
PROFILE_PAGE: (username: string) => `/profile/${username}`, PROFILE_PAGE: (username: string) => `/profile/${username}`,
}; };

View file

@ -8,7 +8,7 @@ interface IProps {
const SidebarRouter: FC<IProps> = ({ prefix = '' }) => ( const SidebarRouter: FC<IProps> = ({ prefix = '' }) => (
<Switch> <Switch>
<Route path={`${prefix}/tag/:name`} component={TagSidebar} /> <Route path={`${prefix}/tag/:tag`} component={TagSidebar} />
</Switch> </Switch>
); );

View file

@ -1,5 +1,5 @@
import React, { FC, createElement, useEffect, useCallback, useState, useMemo, memo } from 'react'; import React, { createElement, FC, memo, useCallback, useEffect, useMemo, useState } from 'react';
import { RouteComponentProps } from 'react-router'; import { RouteComponentProps, useHistory } from 'react-router';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { canEditNode, canLikeNode, canStarNode } from '~/utils/node'; import { canEditNode, canLikeNode, canStarNode } from '~/utils/node';
import { selectNode } from '~/redux/node/selectors'; import { selectNode } from '~/redux/node/selectors';
@ -12,12 +12,7 @@ import { NodeNoComments } from '~/components/node/NodeNoComments';
import { NodeRelated } from '~/components/node/NodeRelated'; import { NodeRelated } from '~/components/node/NodeRelated';
import { NodeComments } from '~/components/node/NodeComments'; import { NodeComments } from '~/components/node/NodeComments';
import { NodeTags } from '~/components/node/NodeTags'; import { NodeTags } from '~/components/node/NodeTags';
import { import { INodeComponentProps, NODE_COMPONENTS, NODE_HEADS, NODE_INLINES, } from '~/redux/node/constants';
NODE_COMPONENTS,
NODE_INLINES,
NODE_HEADS,
INodeComponentProps,
} from '~/redux/node/constants';
import { selectUser } from '~/redux/auth/selectors'; import { selectUser } from '~/redux/auth/selectors';
import pick from 'ramda/es/pick'; import pick from 'ramda/es/pick';
import { NodeRelatedPlaceholder } from '~/components/node/NodeRelated/placeholder'; import { NodeRelatedPlaceholder } from '~/components/node/NodeRelated/placeholder';
@ -32,6 +27,8 @@ import * as MODAL_ACTIONS from '~/redux/modal/actions';
import { IState } from '~/redux/store'; import { IState } from '~/redux/store';
import { selectModal } from '~/redux/modal/selectors'; import { selectModal } from '~/redux/modal/selectors';
import { SidebarRouter } from '~/containers/main/SidebarRouter'; import { SidebarRouter } from '~/containers/main/SidebarRouter';
import { ITag } from '~/redux/types';
import { URLS } from '~/constants/urls';
const mapStateToProps = (state: IState) => ({ const mapStateToProps = (state: IState) => ({
node: selectNode(state), node: selectNode(state),
@ -87,6 +84,7 @@ const NodeLayoutUnconnected: FC<IProps> = memo(
modalShowPhotoswipe, modalShowPhotoswipe,
}) => { }) => {
const [layout, setLayout] = useState({}); const [layout, setLayout] = useState({});
const history = useHistory();
const updateLayout = useCallback(() => setLayout({}), []); const updateLayout = useCallback(() => setLayout({}), []);
@ -102,6 +100,13 @@ const NodeLayoutUnconnected: FC<IProps> = memo(
[node, nodeUpdateTags] [node, nodeUpdateTags]
); );
const onTagClick = useCallback(
(tag: Partial<ITag>) => {
history.push(URLS.NODE_TAG_URL(node.id, encodeURIComponent(tag.title)));
},
[history, node.id]
);
const can_edit = useMemo(() => canEditNode(node, user), [node, user]); const can_edit = useMemo(() => canEditNode(node, user), [node, user]);
const can_like = useMemo(() => canLikeNode(node, user), [node, user]); const can_like = useMemo(() => canLikeNode(node, user), [node, user]);
const can_star = useMemo(() => canStarNode(node, user), [node, user]); const can_star = useMemo(() => canStarNode(node, user), [node, user]);
@ -198,6 +203,7 @@ const NodeLayoutUnconnected: FC<IProps> = memo(
is_editable={is_user} is_editable={is_user}
tags={node.tags} tags={node.tags}
onChange={onTagsChange} onChange={onTagsChange}
onTagClick={onTagClick}
/> />
)} )}

View file

@ -2,21 +2,27 @@ import React, { FC, useEffect, useRef } from 'react';
import styles from './styles.module.scss'; import styles from './styles.module.scss';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { disableBodyScroll, enableBodyScroll } from 'body-scroll-lock'; import { disableBodyScroll, enableBodyScroll } from 'body-scroll-lock';
import { useCloseOnEscape } from '~/utils/hooks';
interface IProps {} interface IProps {
onClose?: () => void;
}
const SidebarWrapper: FC<IProps> = ({ children }) => { const SidebarWrapper: FC<IProps> = ({ children, onClose }) => {
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
useCloseOnEscape(onClose);
useEffect(() => { useEffect(() => {
if (!ref.current) return; if (!ref.current) return;
disableBodyScroll(ref.current); disableBodyScroll(ref.current, { reserveScrollBarGap: true });
return () => enableBodyScroll(ref.current); return () => enableBodyScroll(ref.current);
}, [ref.current]); }, [ref.current]);
return createPortal( return createPortal(
<div className={styles.wrapper}> <div className={styles.wrapper}>
<div className={styles.clicker} onClick={onClose} />
<div className={styles.content} ref={ref}> <div className={styles.content} ref={ref}>
{children} {children}
</div> </div>

View file

@ -1,3 +1,12 @@
@keyframes appear {
0% { opacity: 0; }
100% { opacity: 1; }
}
@keyframes slideIn {
100% { transform: translate(0, 0); }
}
.wrapper { .wrapper {
position: fixed; position: fixed;
top: 0; top: 0;
@ -9,6 +18,8 @@
flex-direction: row; flex-direction: row;
z-index: 20; z-index: 20;
justify-content: flex-end; justify-content: flex-end;
overflow: hidden;
animation: appear 0.25s forwards;
@include can_backdrop { @include can_backdrop {
background: transparentize($content_bg, 0.15); background: transparentize($content_bg, 0.15);
@ -18,9 +29,27 @@
} }
.content { .content {
flex: 0 0 33vw; flex: 0 1 33vw;
width: 33vw; width: 33vw;
min-width: 480px;
max-width: 100vw;
height: 100%; height: 100%;
overflow: auto; overflow: auto;
background: $content_bg; display: flex;
align-items: center;
justify-content: flex-end;
animation: slideIn 0.5s 0.1s forwards;
transform: translate(100%, 0);
position: relative;
z-index: 2;
}
.clicker {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
cursor: pointer;
} }

View file

@ -1,8 +1,102 @@
import React, { FC } from 'react'; import React, { FC, useCallback, useEffect, useMemo } from 'react';
import { SidebarWrapper } from '~/containers/sidebars/SidebarWrapper'; import { SidebarWrapper } from '~/containers/sidebars/SidebarWrapper';
import styles from './styles.module.scss';
import { useHistory, useRouteMatch } from 'react-router';
import { Tag } from '~/components/node/Tag';
import { Icon } from '~/components/input/Icon';
import { Link } from 'react-router-dom';
import { TagSidebarList } from '~/components/sidebar/TagSidebarList';
import { connect } from 'react-redux';
import { selectTagNodes } from '~/redux/tag/selectors';
import * as ACTIONS from '~/redux/tag/actions';
import { LoaderCircle } from '~/components/input/LoaderCircle';
import { InfiniteScroll } from '~/components/containers/InfiniteScroll';
interface IProps {} const mapStateToProps = state => ({
nodes: selectTagNodes(state),
});
const TagSidebar: FC<IProps> = () => <SidebarWrapper>TAGS</SidebarWrapper>; const mapDispatchToProps = {
tagLoadNodes: ACTIONS.tagLoadNodes,
tagSetNodes: ACTIONS.tagSetNodes,
};
type Props = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & {};
const TagSidebarUnconnected: FC<Props> = ({ nodes, tagLoadNodes, tagSetNodes }) => {
const {
params: { tag },
url,
} = useRouteMatch<{ tag: string }>();
const history = useHistory();
const basePath = url.replace(new RegExp(`\/tag\/${tag}$`), '');
useEffect(() => {
tagLoadNodes(tag);
return () => tagSetNodes({ list: [], count: 0 });
}, [tag]);
const loadMore = useCallback(() => {
if (nodes.isLoading) return;
tagLoadNodes(tag);
}, [tagLoadNodes, tag, nodes.isLoading]);
const title = useMemo(() => decodeURIComponent(tag), [tag]);
const progress = nodes.count > 0 ? `${(nodes.list.length / nodes.count) * 100}%` : '0';
const onClose = useCallback(() => history.push(basePath), [basePath]);
const hasMore = nodes.count > nodes.list.length;
return (
<SidebarWrapper onClose={onClose}>
<div className={styles.wrap}>
<div className={styles.content}>
<div className={styles.head}>
{nodes.count > 0 && (
<div className={styles.progress}>
<div className={styles.bar} style={{ width: progress }} />
</div>
)}
<div className={styles.tag}>
<Tag tag={{ title }} size="big" />
</div>
{nodes.isLoading && (
<div className={styles.sync}>
<LoaderCircle size={20} />
</div>
)}
<div className={styles.close}>
<Link to={basePath}>
<Icon icon="close" size={32} />
</Link>
</div>
</div>
{!nodes.count && !nodes.isLoading ? (
<div className={styles.none}>
<Icon icon="sad" size={120} />
<div>
У этого тэга нет постов
<br />
<br />
Такие дела
</div>
</div>
) : (
<InfiniteScroll hasMore={hasMore} loadMore={loadMore} className={styles.list}>
<TagSidebarList nodes={nodes.list} />
</InfiniteScroll>
)}
</div>
</div>
</SidebarWrapper>
);
};
const TagSidebar = connect(mapStateToProps, mapDispatchToProps)(TagSidebarUnconnected);
export { TagSidebar }; export { TagSidebar };

View file

@ -0,0 +1,105 @@
.wrap {
@include outer_shadow;
height: 100%;
box-sizing: border-box;
display: flex;
flex: 0 1 400px;
max-width: 100vw;
position: relative;
}
.content {
background: $content_bg;
height: 100%;
box-sizing: border-box;
overflow: auto;
display: flex;
min-height: 0;
flex-direction: column;
width: 100%;
max-width: 400px;
}
.head {
display: flex;
align-items: center;
justify-content: center;
padding: $gap;
background: lighten($content_bg, 2%);
}
.tag {
flex: 1;
display: flex;
& > * {
margin-bottom: 0;
}
}
.close {
cursor: pointer;
svg {
fill: white;
stroke: white;
color: white;
}
}
.list {
flex: 1 1 100%;
padding: $gap;
overflow: auto;
}
.sync {
padding-left: $gap * 2;
svg {
fill: transparentize(white, 0.5);
}
}
.progress {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
background: darken($content_bg, 2%);
height: 2px;
z-index: 2;
pointer-events: none;
touch-action: none;
}
.bar {
position: absolute;
top: 0;
left: 0;
height: 100%;
background: lighten($content_bg, 20%);
}
.none {
padding: 40px $gap;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: transparentize(white, 0.9);
fill: currentColor;
font: $font_18_semibold;
stroke: none;
line-height: 26px;
flex: 1;
div {
margin-top: 24px;
max-width: 200px;
text-align: center;
font-weight: 900;
text-transform: uppercase;
}
}

View file

@ -1,5 +1,5 @@
import { FC, ReactElement } from 'react'; import { FC } from 'react';
import { INode, ValueOf, IComment } from '../types'; import { IComment, INode, ValueOf } from '../types';
import { NodeImageSlideBlock } from '~/components/node/NodeImageSlideBlock'; import { NodeImageSlideBlock } from '~/components/node/NodeImageSlideBlock';
import { NodeTextBlock } from '~/components/node/NodeTextBlock'; import { NodeTextBlock } from '~/components/node/NodeTextBlock';
import { NodeAudioBlock } from '~/components/node/NodeAudioBlock'; import { NodeAudioBlock } from '~/components/node/NodeAudioBlock';
@ -12,7 +12,6 @@ import { AudioEditor } from '~/components/editors/AudioEditor';
import { EditorImageUploadButton } from '~/components/editors/EditorImageUploadButton'; import { EditorImageUploadButton } from '~/components/editors/EditorImageUploadButton';
import { EditorAudioUploadButton } from '~/components/editors/EditorAudioUploadButton'; import { EditorAudioUploadButton } from '~/components/editors/EditorAudioUploadButton';
import { EditorUploadCoverButton } from '~/components/editors/EditorUploadCoverButton'; import { EditorUploadCoverButton } from '~/components/editors/EditorUploadCoverButton';
import { Filler } from '~/components/containers/Filler';
import { modalShowPhotoswipe } from '../modal/actions'; import { modalShowPhotoswipe } from '../modal/actions';
import { IEditorComponentProps } from '~/redux/node/types'; import { IEditorComponentProps } from '~/redux/node/types';
import { EditorFiller } from '~/components/editors/EditorFiller'; import { EditorFiller } from '~/components/editors/EditorFiller';

View file

@ -34,6 +34,9 @@ import borisSaga from './boris/sagas';
import messages, { IMessagesState } from './messages'; import messages, { IMessagesState } from './messages';
import messagesSaga from './messages/sagas'; import messagesSaga from './messages/sagas';
import tag, { ITagState } from './tag';
import tagSaga from './tag/sagas';
const authPersistConfig: PersistConfig = { const authPersistConfig: PersistConfig = {
key: 'auth', key: 'auth',
whitelist: ['token', 'user', 'updates'], whitelist: ['token', 'user', 'updates'],
@ -62,6 +65,7 @@ export interface IState {
player: IPlayerState; player: IPlayerState;
boris: IBorisState; boris: IBorisState;
messages: IMessagesState; messages: IMessagesState;
tag: ITagState;
} }
export const sagaMiddleware = createSagaMiddleware(); export const sagaMiddleware = createSagaMiddleware();
@ -83,6 +87,7 @@ export const store = createStore(
flow: persistReducer(flowPersistConfig, flow), flow: persistReducer(flowPersistConfig, flow),
player: persistReducer(playerPersistConfig, player), player: persistReducer(playerPersistConfig, player),
messages, messages,
tag: tag,
}), }),
composeEnhancers(applyMiddleware(routerMiddleware(history), sagaMiddleware)) composeEnhancers(applyMiddleware(routerMiddleware(history), sagaMiddleware))
); );
@ -99,6 +104,7 @@ export function configureStore(): {
sagaMiddleware.run(modalSaga); sagaMiddleware.run(modalSaga);
sagaMiddleware.run(borisSaga); sagaMiddleware.run(borisSaga);
sagaMiddleware.run(messagesSaga); sagaMiddleware.run(messagesSaga);
sagaMiddleware.run(tagSaga);
window.addEventListener('message', message => { window.addEventListener('message', message => {
if (message && message.data && message.data.type === 'oauth_login' && message.data.token) if (message && message.data && message.data.type === 'oauth_login' && message.data.token)

12
src/redux/tag/actions.ts Normal file
View file

@ -0,0 +1,12 @@
import { ITagState } from '~/redux/tag/index';
import { TAG_ACTIONS } from '~/redux/tag/constants';
export const tagSetNodes = (nodes: Partial<ITagState['nodes']>) => ({
type: TAG_ACTIONS.SET_TAG_NODES,
nodes,
});
export const tagLoadNodes = (tag: string) => ({
type: TAG_ACTIONS.LOAD_TAG_NODES,
tag,
});

19
src/redux/tag/api.ts Normal file
View file

@ -0,0 +1,19 @@
import { INode, IResultWithStatus } from '~/redux/types';
import { api, configWithToken, errorMiddleware, resultMiddleware } from '~/utils/api';
import { API } from '~/constants/api';
export const getTagNodes = ({
access,
tag,
offset,
limit,
}: {
access: string;
tag: string;
offset: number;
limit: number;
}): Promise<IResultWithStatus<{ nodes: INode[]; count: number }>> =>
api
.get(API.TAG.NODES, configWithToken(access, { params: { name: tag, offset, limit } }))
.then(resultMiddleware)
.catch(errorMiddleware);

View file

@ -0,0 +1,6 @@
const prefix = 'TAG.';
export const TAG_ACTIONS = {
LOAD_TAG_NODES: `${prefix}LOAD_TAG_NODES`,
SET_TAG_NODES: `${prefix}SET_TAG_NODES`,
};

15
src/redux/tag/handlers.ts Normal file
View file

@ -0,0 +1,15 @@
import { TAG_ACTIONS } from '~/redux/tag/constants';
import { ITagState } from '~/redux/tag/index';
import { tagSetNodes } from '~/redux/tag/actions';
const setNodes = (state: ITagState, { nodes }: ReturnType<typeof tagSetNodes>) => ({
...state,
nodes: {
...state.nodes,
...nodes,
},
});
export const TAG_HANDLERS = {
[TAG_ACTIONS.SET_TAG_NODES]: setNodes,
};

21
src/redux/tag/index.ts Normal file
View file

@ -0,0 +1,21 @@
import { createReducer } from '~/utils/reducer';
import { INode } from '~/redux/types';
import { TAG_HANDLERS } from '~/redux/tag/handlers';
export interface ITagState {
nodes: {
list: INode[];
count: number;
isLoading: boolean;
};
}
const INITIAL_STATE: ITagState = {
nodes: {
list: [],
count: 0,
isLoading: true,
},
};
export default createReducer(INITIAL_STATE, TAG_HANDLERS);

31
src/redux/tag/sagas.ts Normal file
View file

@ -0,0 +1,31 @@
import { TAG_ACTIONS } from '~/redux/tag/constants';
import { call, put, select, takeLatest } from 'redux-saga/effects';
import { tagLoadNodes, tagSetNodes } from '~/redux/tag/actions';
import { reqWrapper } from '~/redux/auth/sagas';
import { selectTagNodes } from '~/redux/tag/selectors';
import { getTagNodes } from '~/redux/tag/api';
import { Unwrap } from '~/redux/types';
function* loadTagNodes({ tag }: ReturnType<typeof tagLoadNodes>) {
yield put(tagSetNodes({ isLoading: true }));
try {
const { list }: ReturnType<typeof selectTagNodes> = yield select(selectTagNodes);
const { data, error }: Unwrap<ReturnType<typeof getTagNodes>> = yield call(
reqWrapper,
getTagNodes,
{ tag, limit: 18, offset: list.length }
);
if (error) throw new Error(error);
yield put(tagSetNodes({ isLoading: false, list: [...list, ...data.nodes], count: data.count }));
} catch (e) {
console.log(e);
yield put(tagSetNodes({ isLoading: false }));
}
}
export default function* tagSaga() {
yield takeLatest(TAG_ACTIONS.LOAD_TAG_NODES, loadTagNodes);
}

View file

@ -0,0 +1,4 @@
import { IState } from '~/redux/store';
export const selectTag = (state: IState) => state.tag;
export const selectTagNodes = (state: IState) => state.tag.nodes;

View file

@ -207,6 +207,14 @@ const Sprites: FC<{}> = () => (
<path d="M14 12c0-1.1-.9-2-2-2s-2 .9-2 2 .9 2 2 2 2-.9 2-2zm-2-9c-4.97 0-9 4.03-9 9H0l4 4 4-4H5c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.51 0-2.91-.49-4.06-1.3l-1.42 1.44C8.04 20.3 9.94 21 12 21c4.97 0 9-4.03 9-9s-4.03-9-9-9z" /> <path d="M14 12c0-1.1-.9-2-2-2s-2 .9-2 2 .9 2 2 2 2-.9 2-2zm-2-9c-4.97 0-9 4.03-9 9H0l4 4 4-4H5c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.51 0-2.91-.49-4.06-1.3l-1.42 1.44C8.04 20.3 9.94 21 12 21c4.97 0 9-4.03 9-9s-4.03-9-9-9z" />
</g> </g>
<g id="sad" stroke="none">
<path fill="none" d="M0 0h24v24H0V0z" />
<path d="M0 0h24v24H0V0z" fill="none" />
<circle cx="15.5" cy="9.5" r="1.5" />
<circle cx="8.5" cy="9.5" r="1.5" />
<path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm0-3.5c.73 0 1.39.19 1.97.53.12-.14.86-.98 1.01-1.14-.85-.56-1.87-.89-2.98-.89-1.11 0-2.13.33-2.99.88.97 1.09.01.02 1.01 1.14.59-.33 1.25-.52 1.98-.52z" />
</g>
<g id="settings" stroke="none"> <g id="settings" stroke="none">
<path fill="none" d="M0 0h24v24H0V0z" /> <path fill="none" d="M0 0h24v24H0V0z" />
<path d="M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.07,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z" /> <path d="M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.07,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z" />

View file

@ -23,15 +23,12 @@ export const HTTP_RESPONSES = {
TOO_MANY_REQUESTS: 429, TOO_MANY_REQUESTS: 429,
}; };
export const resultMiddleware = <T extends {}>({ export const resultMiddleware = <T extends any>({ status, data }: { status; data: T }) => ({
status, status,
data, data,
}: { });
status: number;
data: T;
}): { status: number; data: T } => ({ status, data });
export const errorMiddleware = <T extends any>(debug): IResultWithStatus<T> => export const errorMiddleware = <T extends any = any>(debug): IResultWithStatus<T> =>
debug && debug.response debug && debug.response
? { ? {
status: debug.response.status, status: debug.response.status,
@ -41,7 +38,7 @@ export const errorMiddleware = <T extends any>(debug): IResultWithStatus<T> =>
} }
: { : {
status: HTTP_RESPONSES.CONNECTION_REFUSED, status: HTTP_RESPONSES.CONNECTION_REFUSED,
data: {} as T & IApiErrorResult, data: {} as T & IApiErrorResult & any,
debug, debug,
error: 'Ошибка сети', error: 'Ошибка сети',
}; };

View file

@ -6,7 +6,7 @@ import differenceInMinutes from 'date-fns/differenceInMinutes';
import ru from 'date-fns/locale/ru'; import ru from 'date-fns/locale/ru';
import Axios from 'axios'; import Axios from 'axios';
import { PRESETS } from '~/constants/urls'; import { PRESETS } from '~/constants/urls';
import { ICommentBlock, COMMENT_BLOCK_DETECTORS, COMMENT_BLOCK_TYPES } from '~/constants/comment'; import { COMMENT_BLOCK_DETECTORS, COMMENT_BLOCK_TYPES, ICommentBlock } from '~/constants/comment';
import format from 'date-fns/format'; import format from 'date-fns/format';
export const getStyle = (oElm: any, strCssRule: string) => { export const getStyle = (oElm: any, strCssRule: string) => {
@ -63,20 +63,25 @@ export const describeArc = (
].join(' '); ].join(' ');
}; };
export const getURL = (file: Partial<IFile>, size?: typeof PRESETS[keyof typeof PRESETS]) => { export const getURLFromString = (
if (!file || !file.url) return null; url: string,
size?: typeof PRESETS[keyof typeof PRESETS]
): string => {
if (size) { if (size) {
return file.url return url
.replace('REMOTE_CURRENT://', `${process.env.REMOTE_CURRENT}cache/${size}/`) .replace('REMOTE_CURRENT://', `${process.env.REMOTE_CURRENT}cache/${size}/`)
.replace('REMOTE_OLD://', process.env.REMOTE_OLD); .replace('REMOTE_OLD://', process.env.REMOTE_OLD);
} }
return file.url return url
.replace('REMOTE_CURRENT://', process.env.REMOTE_CURRENT) .replace('REMOTE_CURRENT://', process.env.REMOTE_CURRENT)
.replace('REMOTE_OLD://', process.env.REMOTE_OLD); .replace('REMOTE_OLD://', process.env.REMOTE_OLD);
}; };
export const getURL = (file: Partial<IFile>, size?: typeof PRESETS[keyof typeof PRESETS]) => {
return file?.url ? getURLFromString(file.url, size) : null;
};
export const formatText = (text: string): string => export const formatText = (text: string): string =>
!text !text
? '' ? ''

View file

@ -9879,10 +9879,10 @@ typescript-tuple@^2.2.1:
dependencies: dependencies:
typescript-compare "^0.0.2" typescript-compare "^0.0.2"
typescript@^3.6.4: typescript@^3.7.2:
version "3.7.2" version "3.9.7"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.2.tgz#27e489b95fa5909445e9fef5ee48d81697ad18fb" resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.7.tgz#98d600a5ebdc38f40cb277522f12dc800e9e25fa"
integrity sha512-ml7V7JfiN2Xwvcer+XAf2csGO1bPBdRbFCkYBczNZggrBZ9c7G3riSUeJmqEU5uOtXNPMhE3n+R4FA/3YOAWOQ== integrity sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==
uglify-es@^3.3.4: uglify-es@^3.3.4:
version "3.3.9" version "3.3.9"