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 new file mode 100644 index 00000000..0bf35ca6 --- /dev/null +++ b/src/containers/main/SidebarRouter/index.tsx @@ -0,0 +1,15 @@ +import React, { FC } from 'react'; +import { Route, Switch } from 'react-router'; +import { TagSidebar } from '~/containers/sidebars/TagSidebar'; + +interface IProps { + prefix?: string; +} + +const SidebarRouter: FC = ({ prefix = '' }) => ( + + + +); + +export { SidebarRouter }; diff --git a/src/containers/node/NodeLayout/index.tsx b/src/containers/node/NodeLayout/index.tsx index 1e6d7ec0..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'; @@ -31,6 +26,9 @@ import * as NODE_ACTIONS from '~/redux/node/actions'; 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), @@ -86,6 +84,7 @@ const NodeLayoutUnconnected: FC = memo( modalShowPhotoswipe, }) => { const [layout, setLayout] = useState({}); + const history = useHistory(); const updateLayout = useCallback(() => setLayout({}), []); @@ -101,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]); @@ -197,6 +203,7 @@ const NodeLayoutUnconnected: FC = memo( is_editable={is_user} tags={node.tags} onChange={onTagsChange} + onTagClick={onTagClick} /> )} @@ -231,6 +238,8 @@ const NodeLayoutUnconnected: FC = memo(