diff --git a/src/components/flow/FlowSearchResults/index.tsx b/src/components/flow/FlowSearchResults/index.tsx index 7bd3da35..81dba800 100644 --- a/src/components/flow/FlowSearchResults/index.tsx +++ b/src/components/flow/FlowSearchResults/index.tsx @@ -1,13 +1,30 @@ -import React, { FC } from 'react'; +import React, { FC, useCallback, MouseEvent } from 'react'; import styles from './styles.scss'; import { IFlowState } from '~/redux/flow/reducer'; import { LoaderCircle } from '~/components/input/LoaderCircle'; +import { URLS, PRESETS } from '~/constants/urls'; +import { Link } from 'react-router-dom'; +import classNames from 'classnames'; +import { getURL, getPrettyDate } from '~/utils/dom'; interface IProps { search: IFlowState['search']; + onLoadMore: () => void; } -const FlowSearchResults: FC = ({ search }) => { +const FlowSearchResults: FC = ({ search, onLoadMore }) => { + const onScroll = useCallback( + event => { + const el = event.target; + const bottom = el.scrollHeight - el.scrollTop - el.clientHeight; + + if (bottom > 100) return; + + onLoadMore(); + }, + [onLoadMore] + ); + if (search.is_loading) { return (
@@ -15,7 +32,26 @@ const FlowSearchResults: FC = ({ search }) => {
); } - return
SEARCH
; + + return ( +
+ {search.results.map(node => ( + +
+ +
+
{node.title}
+
{getPrettyDate(node.created_at)}
+
+ + ))} +
+ ); }; export { FlowSearchResults }; diff --git a/src/components/flow/FlowSearchResults/styles.scss b/src/components/flow/FlowSearchResults/styles.scss index cbfae3c8..86a8e8f8 100644 --- a/src/components/flow/FlowSearchResults/styles.scss +++ b/src/components/flow/FlowSearchResults/styles.scss @@ -1,6 +1,6 @@ .wrap { flex: 1; - background: red; + overflow: auto; } .loading { @@ -10,3 +10,61 @@ flex: 1; opacity: 0.3; } + +.item { + display: flex; + align-items: center; + justify-content: center; + font: $font_12_regular; + border-radius: $radius; + flex-direction: row; + margin-bottom: $gap; + color: white; + text-decoration: none; +} + +.thumb { + height: 48px; + margin-right: $gap; + background: 50% 50% no-repeat; + background-size: cover; + border-radius: $radius; + flex: 0 0 48px; + display: flex; + align-items: center; + justify-content: center; + position: relative; + + &.new { + &::after { + content: ' '; + width: 12px; + height: 12px; + border-radius: 100%; + background: $red; + box-shadow: $content_bg 0 0 0 5px; + position: absolute; + right: -2px; + bottom: -2px; + } + } +} + +.info { + flex: 1; + min-width: 0; +} + +.title { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + font: $font_16_semibold; + text-transform: capitalize; +} + +.comment { + font: $font_12_regular; + margin-top: 4px; + opacity: 0.5; +} diff --git a/src/components/flow/FlowStamp/index.tsx b/src/components/flow/FlowStamp/index.tsx index 21ecddd2..bd8f11ed 100644 --- a/src/components/flow/FlowStamp/index.tsx +++ b/src/components/flow/FlowStamp/index.tsx @@ -13,9 +13,10 @@ interface IProps { updated: IFlowState['updated']; search: IFlowState['search']; flowChangeSearch: typeof FLOW_ACTIONS.flowChangeSearch; + onLoadMore: () => void; } -const FlowStamp: FC = ({ recent, updated, search, flowChangeSearch }) => { +const FlowStamp: FC = ({ recent, updated, search, flowChangeSearch, onLoadMore }) => { const onSearchChange = useCallback((text: string) => flowChangeSearch({ text }), [ flowChangeSearch, ]); @@ -35,16 +36,19 @@ const FlowStamp: FC = ({ recent, updated, search, flowChangeSearch }) => <>
Результаты поиска + + {!search.is_loading && search.total}
- +
) : ( <>
Что нового? +
diff --git a/src/components/flow/FlowStamp/styles.scss b/src/components/flow/FlowStamp/styles.scss index 098ade69..a0497505 100644 --- a/src/components/flow/FlowStamp/styles.scss +++ b/src/components/flow/FlowStamp/styles.scss @@ -12,6 +12,7 @@ flex-direction: column; flex: 1; border-radius: $radius; + overflow: hidden; @include outer_shadow(); } @@ -21,6 +22,7 @@ flex: 1; display: flex; flex-direction: column; + overflow: hidden; } .label { @@ -38,6 +40,10 @@ color: white; padding-left: $gap * 1.2; } + + & > :global(.line) { + margin-right: $gap; + } } .label_text { diff --git a/src/constants/api.ts b/src/constants/api.ts index 40923951..6464a302 100644 --- a/src/constants/api.ts +++ b/src/constants/api.ts @@ -30,5 +30,7 @@ export const API = { `/node/${id}/comment/${comment_id}/lock`, SET_CELL_VIEW: (id: INode['id']) => `/node/${id}/cell-view`, }, - SEARCH: '/search', + SEARCH: { + NODES: '/search/nodes', + }, }; diff --git a/src/containers/flow/FlowLayout/index.tsx b/src/containers/flow/FlowLayout/index.tsx index c0051318..ed92ea74 100644 --- a/src/containers/flow/FlowLayout/index.tsx +++ b/src/containers/flow/FlowLayout/index.tsx @@ -21,6 +21,7 @@ const mapDispatchToProps = { flowSetCellView: FLOW_ACTIONS.flowSetCellView, flowGetMore: FLOW_ACTIONS.flowGetMore, flowChangeSearch: FLOW_ACTIONS.flowChangeSearch, + flowLoadMoreSearch: FLOW_ACTIONS.flowLoadMoreSearch, }; type IProps = ReturnType & typeof mapDispatchToProps & {}; @@ -32,8 +33,9 @@ const FlowLayoutUnconnected: FC = ({ flowSetCellView, flowGetMore, flowChangeSearch, + flowLoadMoreSearch, }) => { - const loadMore = useCallback(() => { + const onLoadMore = useCallback(() => { const pos = window.scrollY + window.innerHeight - document.body.scrollHeight; if (is_loading || pos < -600) return; @@ -41,11 +43,16 @@ const FlowLayoutUnconnected: FC = ({ flowGetMore(); }, [flowGetMore, is_loading]); - useEffect(() => { - window.addEventListener('scroll', loadMore); + const onLoadMoreSearch = useCallback(() => { + if (search.is_loading_more) return; + flowLoadMoreSearch(); + }, [search.is_loading_more, flowLoadMoreSearch]); - return () => window.removeEventListener('scroll', loadMore); - }, [loadMore]); + useEffect(() => { + window.addEventListener('scroll', onLoadMore); + + return () => window.removeEventListener('scroll', onLoadMore); + }, [onLoadMore]); return (
@@ -57,15 +64,16 @@ const FlowLayoutUnconnected: FC = ({
diff --git a/src/redux/flow/actions.ts b/src/redux/flow/actions.ts index 5b535cf6..97764d3f 100644 --- a/src/redux/flow/actions.ts +++ b/src/redux/flow/actions.ts @@ -46,3 +46,7 @@ export const flowChangeSearch = (search: Partial) => ({ type: FLOW_ACTIONS.CHANGE_SEARCH, search, }); + +export const flowLoadMoreSearch = () => ({ + type: FLOW_ACTIONS.LOAD_MORE_SEARCH, +}); diff --git a/src/redux/flow/api.ts b/src/redux/flow/api.ts index 5f54ca09..f23a8101 100644 --- a/src/redux/flow/api.ts +++ b/src/redux/flow/api.ts @@ -2,6 +2,7 @@ import { api, configWithToken, resultMiddleware, errorMiddleware } from '~/utils import { INode, IResultWithStatus } from '../types'; import { API } from '~/constants/api'; import { flowSetCellView } from '~/redux/flow/actions'; +import { IFlowState } from './reducer'; export const postNode = ({ access, @@ -30,11 +31,12 @@ export const postCellView = ({ export const getSearchResults = ({ access, text, -}: { + skip = 0, +}: IFlowState['search'] & { access: string; - text: string; + skip: number; }): Promise> => api - .get(API.SEARCH, configWithToken(access, { params: { text } })) + .get(API.SEARCH.NODES, configWithToken(access, { params: { text, skip } })) .then(resultMiddleware) .catch(errorMiddleware); diff --git a/src/redux/flow/constants.ts b/src/redux/flow/constants.ts index 60f9f70b..fecb6d93 100644 --- a/src/redux/flow/constants.ts +++ b/src/redux/flow/constants.ts @@ -13,4 +13,5 @@ export const FLOW_ACTIONS = { SET_SEARCH: `${prefix}SET_SEARCH`, CHANGE_SEARCH: `${prefix}CHANGE_SEARCH`, + LOAD_MORE_SEARCH: `${prefix}LOAD_MORE_SEARCH`, }; diff --git a/src/redux/flow/reducer.ts b/src/redux/flow/reducer.ts index 2a141123..1bf96683 100644 --- a/src/redux/flow/reducer.ts +++ b/src/redux/flow/reducer.ts @@ -10,8 +10,10 @@ export type IFlowState = Readonly<{ updated: Partial[]; search: { text: string; - is_loading: boolean; results: INode[]; + total: number; + is_loading: boolean; + is_loading_more: boolean; }; error: IError; }>; @@ -23,8 +25,10 @@ const INITIAL_STATE: IFlowState = { updated: [], search: { text: '', - is_loading: false, results: [], + total: 0, + is_loading: false, + is_loading_more: false, }, is_loading: false, error: null, diff --git a/src/redux/flow/sagas.ts b/src/redux/flow/sagas.ts index adb88110..ec96a5cc 100644 --- a/src/redux/flow/sagas.ts +++ b/src/redux/flow/sagas.ts @@ -1,4 +1,4 @@ -import { takeLatest, call, put, select, takeLeading, delay } from 'redux-saga/effects'; +import { takeLatest, call, put, select, takeLeading, delay, race, take } from 'redux-saga/effects'; import { REHYDRATE } from 'redux-persist'; import { FLOW_ACTIONS } from './constants'; import { getNodeDiff } from '../node/api'; @@ -13,7 +13,7 @@ import { flowSetSearch, } from './actions'; import { IResultWithStatus, INode, Unwrap } from '../types'; -import { selectFlowNodes } from './selectors'; +import { selectFlowNodes, selectFlow } from './selectors'; import { reqWrapper } from '../auth/sagas'; import { postCellView, getSearchResults } from './api'; import { IFlowState } from './reducer'; @@ -124,11 +124,59 @@ function* changeSearch({ search }: ReturnType) { yield delay(500); - const res: Unwrap = yield call(reqWrapper, getSearchResults, { - ...search, + const { data, error }: Unwrap> = yield call( + reqWrapper, + getSearchResults, + { + ...search, + } + ); + + if (error) { + yield put(flowSetSearch({ is_loading: false, results: [], total: 0 })); + return; + } + + yield put( + flowSetSearch({ + is_loading: false, + results: data.nodes, + total: data.total, + }) + ); +} + +function* loadMoreSearch() { + yield put( + flowSetSearch({ + is_loading_more: true, + }) + ); + + const { search }: ReturnType = yield select(selectFlow); + + const { + result, + delay, + }: { result: Unwrap>; delay: any } = yield race({ + result: call(reqWrapper, getSearchResults, { + ...search, + skip: search.results.length, + }), + delay: take(FLOW_ACTIONS.CHANGE_SEARCH), }); - console.log(res); + if (delay || result.error) { + return put(flowSetSearch({ is_loading_more: false })); + } + + yield put( + flowSetSearch({ + results: [...search.results, ...result.data.nodes], + total: result.data.total, + is_loading_more: false, + }) + ); } export default function* nodeSaga() { @@ -136,4 +184,5 @@ export default function* nodeSaga() { yield takeLatest(FLOW_ACTIONS.SET_CELL_VIEW, onSetCellView); yield takeLeading(FLOW_ACTIONS.GET_MORE, getMore); yield takeLatest(FLOW_ACTIONS.CHANGE_SEARCH, changeSearch); + yield takeLatest(FLOW_ACTIONS.LOAD_MORE_SEARCH, loadMoreSearch); } diff --git a/src/styles/variables.scss b/src/styles/variables.scss index b53b40b4..bc83165e 100644 --- a/src/styles/variables.scss +++ b/src/styles/variables.scss @@ -165,8 +165,7 @@ $input_shadow_filled: $input_shadow; padding-right: $gap; } - &:after { - content: ' '; + & > :global(.line) { flex: 1; height: 2px; background: transparentize(white, 0.95);