diff --git a/package.json b/package.json index 7332d17b..44c76853 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "resolve-url-loader": "^3.0.1", "style-loader": "^0.21.0", "ts-node": "^8.4.1", - "typescript": "^3.6.4", + "typescript": "^3.7.2", "uglifyjs-webpack-plugin": "^1.3.0", "webpack": "^4.41.2", "webpack-cli": "^3.3.9", diff --git a/src/components/containers/InfiniteScroll/index.tsx b/src/components/containers/InfiniteScroll/index.tsx new file mode 100644 index 00000000..70db3103 --- /dev/null +++ b/src/components/containers/InfiniteScroll/index.tsx @@ -0,0 +1,42 @@ +import React, { FC, HTMLAttributes, useCallback, useEffect, useRef } from 'react'; +import styles from './styles.module.scss'; + +interface IProps extends HTMLAttributes { + hasMore: boolean; + scrollReactPx?: number; + loadMore: () => void; +} + +const InfiniteScroll: FC = ({ children, hasMore, scrollReactPx, loadMore, ...props }) => { + const ref = useRef(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 ( +
+ {children} + {hasMore &&
} +
+ ); +}; + +export { InfiniteScroll }; diff --git a/src/components/containers/InfiniteScroll/styles.module.scss b/src/components/containers/InfiniteScroll/styles.module.scss new file mode 100644 index 00000000..9fd1a252 --- /dev/null +++ b/src/components/containers/InfiniteScroll/styles.module.scss @@ -0,0 +1,2 @@ +.more { +} diff --git a/src/components/flow/FlowRecentItem/styles.scss b/src/components/flow/FlowRecentItem/styles.scss index d103d429..42381a98 100644 --- a/src/components/flow/FlowRecentItem/styles.scss +++ b/src/components/flow/FlowRecentItem/styles.scss @@ -54,4 +54,7 @@ font: $font_12_regular; margin-top: 4px; opacity: 0.5; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } diff --git a/src/components/node/NodeTags/index.tsx b/src/components/node/NodeTags/index.tsx index 6c541051..910d2590 100644 --- a/src/components/node/NodeTags/index.tsx +++ b/src/components/node/NodeTags/index.tsx @@ -6,10 +6,13 @@ interface IProps { is_editable?: boolean; tags: ITag[]; onChange?: (tags: string[]) => void; + onTagClick?: (tag: Partial) => void; } -const NodeTags: FC = memo(({ is_editable, tags, onChange }) => ( - -)); +const NodeTags: FC = memo(({ is_editable, tags, onChange, onTagClick }) => { + return ( + + ); +}); export { NodeTags }; diff --git a/src/components/node/Tag/index.tsx b/src/components/node/Tag/index.tsx index fd8e4896..f9bb1c8a 100644 --- a/src/components/node/Tag/index.tsx +++ b/src/components/node/Tag/index.tsx @@ -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 { ITag } from '~/redux/types'; import classNames = require('classnames'); @@ -11,6 +11,7 @@ const getTagFeature = (tag: Partial) => { interface IProps { tag: Partial; + size?: 'normal' | 'big'; is_hoverable?: boolean; is_editing?: boolean; @@ -18,32 +19,51 @@ interface IProps { onInput?: ChangeEventHandler; onKeyUp?: KeyboardEventHandler; onBlur?: FocusEventHandler; + onClick?: (tag: Partial) => void; } -const Tag: FC = ({ tag, is_hoverable, is_editing, onInput, onKeyUp, onBlur }) => ( -
-
-
{tag.title}
+const Tag: FC = ({ + tag, + is_hoverable, + is_editing, + size = 'normal', + onInput, + onKeyUp, + onBlur, + onClick, +}) => { + const onClickHandler = useCallback(() => { + if (!onClick) return; + onClick(tag); + }, [tag, onClick]); - {onInput && ( - - )} -
-); + return ( +
+
+
{tag.title}
+ + {onInput && ( + + )} +
+ ); +}; export { Tag }; diff --git a/src/components/node/Tag/styles.scss b/src/components/node/Tag/styles.scss index d5280c5c..1fb4d6df 100644 --- a/src/components/node/Tag/styles.scss +++ b/src/components/node/Tag/styles.scss @@ -1,6 +1,9 @@ +$big: 1.2; + .tag { @include outer_shadow(); + cursor: default; height: $tag_height; background: $tag_bg; display: flex; @@ -14,6 +17,17 @@ margin: 0 $gap $gap 0; 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) { cursor: pointer; } @@ -56,6 +70,10 @@ min-width: 100px; } + &:global(.clickable) { + cursor: pointer; + } + input { background: none; border: none; @@ -80,7 +98,6 @@ width: $tag_height; height: $tag_height; display: flex; - // padding-right: 0px; align-items: center; justify-content: center; flex: 0 0 $tag_height; diff --git a/src/components/node/Tags/index.tsx b/src/components/node/Tags/index.tsx index eafb933f..894dde37 100644 --- a/src/components/node/Tags/index.tsx +++ b/src/components/node/Tags/index.tsx @@ -1,13 +1,13 @@ import React, { + ChangeEvent, FC, HTMLAttributes, - useState, + KeyboardEvent, useCallback, useEffect, - KeyboardEvent, - ChangeEvent, - useRef, useMemo, + useRef, + useState, } from 'react'; import { TagField } from '~/components/containers/TagField'; import { ITag } from '~/redux/types'; @@ -18,9 +18,10 @@ type IProps = HTMLAttributes & { tags: Partial[]; is_editable?: boolean; onTagsChange?: (tags: string[]) => void; + onTagClick?: (tag: Partial) => void; }; -export const Tags: FC = ({ tags, is_editable, onTagsChange, ...props }) => { +export const Tags: FC = ({ tags, is_editable, onTagsChange, onTagClick, ...props }) => { const [input, setInput] = useState(''); const [data, setData] = useState([]); const timer = useRef(null); @@ -98,11 +99,11 @@ export const Tags: FC = ({ tags, is_editable, onTagsChange, ...props }) return ( {catTags.map(tag => ( - + ))} {ordinaryTags.map(tag => ( - + ))} {data.map(tag => ( diff --git a/src/components/sidebar/TagSidebarList/index.tsx b/src/components/sidebar/TagSidebarList/index.tsx new file mode 100644 index 00000000..b4c11027 --- /dev/null +++ b/src/components/sidebar/TagSidebarList/index.tsx @@ -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 = ({ nodes }) => ( +
+ {nodes.map(node => ( + + ))} +
+); + +export { TagSidebarList }; diff --git a/src/components/sidebar/TagSidebarList/styles.module.scss b/src/components/sidebar/TagSidebarList/styles.module.scss new file mode 100644 index 00000000..868192b4 --- /dev/null +++ b/src/components/sidebar/TagSidebarList/styles.module.scss @@ -0,0 +1,4 @@ +.list { + flex: 1; + flex-direction: column; +} diff --git a/src/constants/api.ts b/src/constants/api.ts index 991fe958..d942c61c 100644 --- a/src/constants/api.ts +++ b/src/constants/api.ts @@ -1,4 +1,4 @@ -import { INode, IComment } from '~/redux/types'; +import { IComment, INode } from '~/redux/types'; import { ISocialProvider } from '~/redux/auth/types'; export const API = { @@ -46,4 +46,7 @@ export const API = { BORIS: { GET_BACKEND_STATS: '/stats/', }, + TAG: { + NODES: `/tag/nodes`, + }, }; diff --git a/src/constants/urls.ts b/src/constants/urls.ts index 055bc214..606e6407 100644 --- a/src/constants/urls.ts +++ b/src/constants/urls.ts @@ -13,6 +13,7 @@ export const URLS = { BACKEND_DOWN: '/oopsie', }, NODE_URL: (id: number | string) => `/post${id}`, + NODE_TAG_URL: (id: number, tagName: string) => `/post${id}/tag/${tagName}`, PROFILE: (username: string) => `/~${username}`, PROFILE_PAGE: (username: string) => `/profile/${username}`, }; diff --git a/src/containers/main/SidebarRouter/index.tsx b/src/containers/main/SidebarRouter/index.tsx index 72e45d1f..0bf35ca6 100644 --- a/src/containers/main/SidebarRouter/index.tsx +++ b/src/containers/main/SidebarRouter/index.tsx @@ -8,7 +8,7 @@ interface IProps { const SidebarRouter: FC = ({ prefix = '' }) => ( - + ); diff --git a/src/containers/node/NodeLayout/index.tsx b/src/containers/node/NodeLayout/index.tsx index 16d28c65..478a1837 100644 --- a/src/containers/node/NodeLayout/index.tsx +++ b/src/containers/node/NodeLayout/index.tsx @@ -1,5 +1,5 @@ -import React, { FC, createElement, useEffect, useCallback, useState, useMemo, memo } from 'react'; -import { RouteComponentProps } from 'react-router'; +import React, { createElement, FC, memo, useCallback, useEffect, useMemo, useState } from 'react'; +import { RouteComponentProps, useHistory } from 'react-router'; import { connect } from 'react-redux'; import { canEditNode, canLikeNode, canStarNode } from '~/utils/node'; import { selectNode } from '~/redux/node/selectors'; @@ -12,12 +12,7 @@ import { NodeNoComments } from '~/components/node/NodeNoComments'; import { NodeRelated } from '~/components/node/NodeRelated'; import { NodeComments } from '~/components/node/NodeComments'; import { NodeTags } from '~/components/node/NodeTags'; -import { - NODE_COMPONENTS, - NODE_INLINES, - NODE_HEADS, - INodeComponentProps, -} from '~/redux/node/constants'; +import { INodeComponentProps, NODE_COMPONENTS, NODE_HEADS, NODE_INLINES, } from '~/redux/node/constants'; import { selectUser } from '~/redux/auth/selectors'; import pick from 'ramda/es/pick'; 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 { selectModal } from '~/redux/modal/selectors'; import { SidebarRouter } from '~/containers/main/SidebarRouter'; +import { ITag } from '~/redux/types'; +import { URLS } from '~/constants/urls'; const mapStateToProps = (state: IState) => ({ node: selectNode(state), @@ -87,6 +84,7 @@ const NodeLayoutUnconnected: FC = memo( modalShowPhotoswipe, }) => { const [layout, setLayout] = useState({}); + const history = useHistory(); const updateLayout = useCallback(() => setLayout({}), []); @@ -102,6 +100,13 @@ const NodeLayoutUnconnected: FC = memo( [node, nodeUpdateTags] ); + const onTagClick = useCallback( + (tag: Partial) => { + 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_like = useMemo(() => canLikeNode(node, user), [node, user]); const can_star = useMemo(() => canStarNode(node, user), [node, user]); @@ -198,6 +203,7 @@ const NodeLayoutUnconnected: FC = memo( is_editable={is_user} tags={node.tags} onChange={onTagsChange} + onTagClick={onTagClick} /> )} diff --git a/src/containers/sidebars/SidebarWrapper/index.tsx b/src/containers/sidebars/SidebarWrapper/index.tsx index 1fea623b..28c2b7db 100644 --- a/src/containers/sidebars/SidebarWrapper/index.tsx +++ b/src/containers/sidebars/SidebarWrapper/index.tsx @@ -2,21 +2,27 @@ import React, { FC, useEffect, useRef } from 'react'; import styles from './styles.module.scss'; import { createPortal } from 'react-dom'; import { disableBodyScroll, enableBodyScroll } from 'body-scroll-lock'; +import { useCloseOnEscape } from '~/utils/hooks'; -interface IProps {} +interface IProps { + onClose?: () => void; +} -const SidebarWrapper: FC = ({ children }) => { +const SidebarWrapper: FC = ({ children, onClose }) => { const ref = useRef(null); + useCloseOnEscape(onClose); + useEffect(() => { if (!ref.current) return; - disableBodyScroll(ref.current); + disableBodyScroll(ref.current, { reserveScrollBarGap: true }); return () => enableBodyScroll(ref.current); }, [ref.current]); return createPortal(
+
{children}
diff --git a/src/containers/sidebars/SidebarWrapper/styles.module.scss b/src/containers/sidebars/SidebarWrapper/styles.module.scss index f89efd80..a581c1af 100644 --- a/src/containers/sidebars/SidebarWrapper/styles.module.scss +++ b/src/containers/sidebars/SidebarWrapper/styles.module.scss @@ -1,3 +1,12 @@ +@keyframes appear { + 0% { opacity: 0; } + 100% { opacity: 1; } +} + +@keyframes slideIn { + 100% { transform: translate(0, 0); } +} + .wrapper { position: fixed; top: 0; @@ -9,6 +18,8 @@ flex-direction: row; z-index: 20; justify-content: flex-end; + overflow: hidden; + animation: appear 0.25s forwards; @include can_backdrop { background: transparentize($content_bg, 0.15); @@ -18,9 +29,27 @@ } .content { - flex: 0 0 33vw; + flex: 0 1 33vw; width: 33vw; + min-width: 480px; + max-width: 100vw; height: 100%; 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; } diff --git a/src/containers/sidebars/TagSidebar/index.tsx b/src/containers/sidebars/TagSidebar/index.tsx index fdd0d990..bcef7151 100644 --- a/src/containers/sidebars/TagSidebar/index.tsx +++ b/src/containers/sidebars/TagSidebar/index.tsx @@ -1,8 +1,102 @@ -import React, { FC } from 'react'; +import React, { FC, useCallback, useEffect, useMemo } from 'react'; 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 = () => TAGS; +const mapDispatchToProps = { + tagLoadNodes: ACTIONS.tagLoadNodes, + tagSetNodes: ACTIONS.tagSetNodes, +}; + +type Props = ReturnType & typeof mapDispatchToProps & {}; + +const TagSidebarUnconnected: FC = ({ 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 ( + +
+
+
+ {nodes.count > 0 && ( +
+
+
+ )} + +
+ +
+ + {nodes.isLoading && ( +
+ +
+ )} + +
+ + + +
+
+ + {!nodes.count && !nodes.isLoading ? ( +
+ +
+ У этого тэга нет постов +
+
+ Такие дела +
+
+ ) : ( + + + + )} +
+
+ + ); +}; + +const TagSidebar = connect(mapStateToProps, mapDispatchToProps)(TagSidebarUnconnected); export { TagSidebar }; diff --git a/src/containers/sidebars/TagSidebar/styles.module.scss b/src/containers/sidebars/TagSidebar/styles.module.scss new file mode 100644 index 00000000..9038443b --- /dev/null +++ b/src/containers/sidebars/TagSidebar/styles.module.scss @@ -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; + } +} diff --git a/src/redux/node/constants.ts b/src/redux/node/constants.ts index 76d421b0..9d3da6af 100644 --- a/src/redux/node/constants.ts +++ b/src/redux/node/constants.ts @@ -1,5 +1,5 @@ -import { FC, ReactElement } from 'react'; -import { INode, ValueOf, IComment } from '../types'; +import { FC } from 'react'; +import { IComment, INode, ValueOf } from '../types'; import { NodeImageSlideBlock } from '~/components/node/NodeImageSlideBlock'; import { NodeTextBlock } from '~/components/node/NodeTextBlock'; import { NodeAudioBlock } from '~/components/node/NodeAudioBlock'; @@ -12,7 +12,6 @@ import { AudioEditor } from '~/components/editors/AudioEditor'; import { EditorImageUploadButton } from '~/components/editors/EditorImageUploadButton'; import { EditorAudioUploadButton } from '~/components/editors/EditorAudioUploadButton'; import { EditorUploadCoverButton } from '~/components/editors/EditorUploadCoverButton'; -import { Filler } from '~/components/containers/Filler'; import { modalShowPhotoswipe } from '../modal/actions'; import { IEditorComponentProps } from '~/redux/node/types'; import { EditorFiller } from '~/components/editors/EditorFiller'; diff --git a/src/redux/store.ts b/src/redux/store.ts index 523b069a..a26248eb 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -34,6 +34,9 @@ import borisSaga from './boris/sagas'; import messages, { IMessagesState } from './messages'; import messagesSaga from './messages/sagas'; +import tag, { ITagState } from './tag'; +import tagSaga from './tag/sagas'; + const authPersistConfig: PersistConfig = { key: 'auth', whitelist: ['token', 'user', 'updates'], @@ -62,6 +65,7 @@ export interface IState { player: IPlayerState; boris: IBorisState; messages: IMessagesState; + tag: ITagState; } export const sagaMiddleware = createSagaMiddleware(); @@ -83,6 +87,7 @@ export const store = createStore( flow: persistReducer(flowPersistConfig, flow), player: persistReducer(playerPersistConfig, player), messages, + tag: tag, }), composeEnhancers(applyMiddleware(routerMiddleware(history), sagaMiddleware)) ); @@ -99,6 +104,7 @@ export function configureStore(): { sagaMiddleware.run(modalSaga); sagaMiddleware.run(borisSaga); sagaMiddleware.run(messagesSaga); + sagaMiddleware.run(tagSaga); window.addEventListener('message', message => { if (message && message.data && message.data.type === 'oauth_login' && message.data.token) diff --git a/src/redux/tag/actions.ts b/src/redux/tag/actions.ts new file mode 100644 index 00000000..835d3ca3 --- /dev/null +++ b/src/redux/tag/actions.ts @@ -0,0 +1,12 @@ +import { ITagState } from '~/redux/tag/index'; +import { TAG_ACTIONS } from '~/redux/tag/constants'; + +export const tagSetNodes = (nodes: Partial) => ({ + type: TAG_ACTIONS.SET_TAG_NODES, + nodes, +}); + +export const tagLoadNodes = (tag: string) => ({ + type: TAG_ACTIONS.LOAD_TAG_NODES, + tag, +}); diff --git a/src/redux/tag/api.ts b/src/redux/tag/api.ts new file mode 100644 index 00000000..92d87081 --- /dev/null +++ b/src/redux/tag/api.ts @@ -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> => + api + .get(API.TAG.NODES, configWithToken(access, { params: { name: tag, offset, limit } })) + .then(resultMiddleware) + .catch(errorMiddleware); diff --git a/src/redux/tag/constants.ts b/src/redux/tag/constants.ts new file mode 100644 index 00000000..5c6b04f4 --- /dev/null +++ b/src/redux/tag/constants.ts @@ -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`, +}; diff --git a/src/redux/tag/handlers.ts b/src/redux/tag/handlers.ts new file mode 100644 index 00000000..b0bc92b1 --- /dev/null +++ b/src/redux/tag/handlers.ts @@ -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) => ({ + ...state, + nodes: { + ...state.nodes, + ...nodes, + }, +}); + +export const TAG_HANDLERS = { + [TAG_ACTIONS.SET_TAG_NODES]: setNodes, +}; diff --git a/src/redux/tag/index.ts b/src/redux/tag/index.ts new file mode 100644 index 00000000..fddaff42 --- /dev/null +++ b/src/redux/tag/index.ts @@ -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); diff --git a/src/redux/tag/sagas.ts b/src/redux/tag/sagas.ts new file mode 100644 index 00000000..1b11f38d --- /dev/null +++ b/src/redux/tag/sagas.ts @@ -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) { + yield put(tagSetNodes({ isLoading: true })); + + try { + const { list }: ReturnType = yield select(selectTagNodes); + const { data, error }: Unwrap> = 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); +} diff --git a/src/redux/tag/selectors.ts b/src/redux/tag/selectors.ts new file mode 100644 index 00000000..338bfda5 --- /dev/null +++ b/src/redux/tag/selectors.ts @@ -0,0 +1,4 @@ +import { IState } from '~/redux/store'; + +export const selectTag = (state: IState) => state.tag; +export const selectTagNodes = (state: IState) => state.tag.nodes; diff --git a/src/sprites/Sprites.tsx b/src/sprites/Sprites.tsx index 25cfa44c..a8efa18e 100644 --- a/src/sprites/Sprites.tsx +++ b/src/sprites/Sprites.tsx @@ -207,6 +207,14 @@ const Sprites: FC<{}> = () => ( + + + + + + + + diff --git a/src/utils/api/index.ts b/src/utils/api/index.ts index a9f1adec..e7268343 100644 --- a/src/utils/api/index.ts +++ b/src/utils/api/index.ts @@ -23,15 +23,12 @@ export const HTTP_RESPONSES = { TOO_MANY_REQUESTS: 429, }; -export const resultMiddleware = ({ +export const resultMiddleware = ({ status, data }: { status; data: T }) => ({ status, data, -}: { - status: number; - data: T; -}): { status: number; data: T } => ({ status, data }); +}); -export const errorMiddleware = (debug): IResultWithStatus => +export const errorMiddleware = (debug): IResultWithStatus => debug && debug.response ? { status: debug.response.status, @@ -41,7 +38,7 @@ export const errorMiddleware = (debug): IResultWithStatus => } : { status: HTTP_RESPONSES.CONNECTION_REFUSED, - data: {} as T & IApiErrorResult, + data: {} as T & IApiErrorResult & any, debug, error: 'Ошибка сети', }; diff --git a/src/utils/dom.ts b/src/utils/dom.ts index 1ed6ea97..b78d6422 100644 --- a/src/utils/dom.ts +++ b/src/utils/dom.ts @@ -6,7 +6,7 @@ import differenceInMinutes from 'date-fns/differenceInMinutes'; import ru from 'date-fns/locale/ru'; import Axios from 'axios'; 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'; export const getStyle = (oElm: any, strCssRule: string) => { @@ -63,20 +63,25 @@ export const describeArc = ( ].join(' '); }; -export const getURL = (file: Partial, size?: typeof PRESETS[keyof typeof PRESETS]) => { - if (!file || !file.url) return null; - +export const getURLFromString = ( + url: string, + size?: typeof PRESETS[keyof typeof PRESETS] +): string => { if (size) { - return file.url + return url .replace('REMOTE_CURRENT://', `${process.env.REMOTE_CURRENT}cache/${size}/`) .replace('REMOTE_OLD://', process.env.REMOTE_OLD); } - return file.url + return url .replace('REMOTE_CURRENT://', process.env.REMOTE_CURRENT) .replace('REMOTE_OLD://', process.env.REMOTE_OLD); }; +export const getURL = (file: Partial, size?: typeof PRESETS[keyof typeof PRESETS]) => { + return file?.url ? getURLFromString(file.url, size) : null; +}; + export const formatText = (text: string): string => !text ? '' diff --git a/yarn.lock b/yarn.lock index 7c74c586..f53e787b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9879,10 +9879,10 @@ typescript-tuple@^2.2.1: dependencies: typescript-compare "^0.0.2" -typescript@^3.6.4: - version "3.7.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.2.tgz#27e489b95fa5909445e9fef5ee48d81697ad18fb" - integrity sha512-ml7V7JfiN2Xwvcer+XAf2csGO1bPBdRbFCkYBczNZggrBZ9c7G3riSUeJmqEU5uOtXNPMhE3n+R4FA/3YOAWOQ== +typescript@^3.7.2: + version "3.9.7" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.7.tgz#98d600a5ebdc38f40cb277522f12dc800e9e25fa" + integrity sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw== uglify-es@^3.3.4: version "3.3.9"