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

getting node results for search

This commit is contained in:
Fedor Katurov 2020-04-18 16:06:10 +07:00
parent 41a35f1490
commit 94ac596b92
15 changed files with 308 additions and 164 deletions

View file

@ -1,4 +1,4 @@
import React, { FC, useState, useCallback, FormEvent } from 'react';
import React, { FC } from 'react';
import * as styles from './styles.scss';
import { IFlowState } from '~/redux/flow/reducer';
import { getURL, getPrettyDate } from '~/utils/dom';
@ -6,7 +6,6 @@ import { Link } from 'react-router-dom';
import { URLS, PRESETS } from '~/constants/urls';
import classNames from 'classnames';
import { NodeRelatedItem } from '~/components/node/NodeRelatedItem';
import { InputText } from '~/components/input/InputText';
interface IProps {
recent: IFlowState['recent'];
@ -14,55 +13,39 @@ interface IProps {
}
const FlowRecent: FC<IProps> = ({ recent, updated }) => {
const [search, setSearch] = useState('');
const onSearchSubmit = useCallback((event: FormEvent) => {
event.preventDefault();
}, []);
return (
<div>
<form className={styles.search} onSubmit={onSearchSubmit}>
<InputText title="Поиск" value={search} handler={setSearch} />
</form>
<>
{updated &&
updated.slice(0, 20).map(node => (
<Link key={node.id} className={styles.item} to={URLS.NODE_URL(node.id)}>
<div
className={classNames(styles.thumb, styles.new)}
style={{
backgroundImage: `url("${getURL({ url: node.thumbnail }, PRESETS.avatar)}")`,
}}
/>
<div className={styles.grid}>
<div className={styles.grid_label}>
<span>Что нового?</span>
</div>
<div className={styles.info}>
<div className={styles.title}>{node.title}</div>
<div className={styles.comment}>{getPrettyDate(node.created_at)}</div>
</div>
</Link>
))}
{updated &&
updated.slice(0, 20).map(node => (
<Link key={node.id} className={styles.item} to={URLS.NODE_URL(node.id)}>
<div
className={classNames(styles.thumb, styles.new)}
style={{
backgroundImage: `url("${getURL({ url: node.thumbnail }, PRESETS.avatar)}")`,
}}
/>
{recent &&
recent.slice(0, 20).map(node => (
<Link key={node.id} className={styles.item} to={URLS.NODE_URL(node.id)}>
<div className={styles.thumb}>
<NodeRelatedItem item={node} />
</div>
<div className={styles.info}>
<div className={styles.title}>{node.title}</div>
<div className={styles.comment}>{getPrettyDate(node.created_at)}</div>
</div>
</Link>
))}
{recent &&
recent.slice(0, 20).map(node => (
<Link key={node.id} className={styles.item} to={URLS.NODE_URL(node.id)}>
<div className={styles.thumb}>
<NodeRelatedItem item={node} />
</div>
<div className={styles.info}>
<div className={styles.title}>{node.title}</div>
<div className={styles.comment}>{getPrettyDate(node.created_at)}</div>
</div>
</Link>
))}
</div>
</div>
<div className={styles.info}>
<div className={styles.title}>{node.title}</div>
<div className={styles.comment}>{getPrettyDate(node.created_at)}</div>
</div>
</Link>
))}
</>
);
};

View file

@ -1,29 +1,3 @@
.grid {
display: flex;
justify-content: stretch;
flex-direction: column;
flex: 1;
background: $content_bg;
padding: $gap;
border-radius: 0 0 $radius $radius;
@include outer_shadow();
}
.grid_label {
margin-bottom: $gap;
@include title_with_line();
color: transparentize(white, $amount: 0.8);
}
.search {
background: lighten($content_bg, 4%);
border-radius: $radius $radius 0 0;
padding: $gap;
@include outer_shadow();
}
.item {
display: flex;
align-items: center;

View file

@ -0,0 +1,21 @@
import React, { FC } from 'react';
import styles from './styles.scss';
import { IFlowState } from '~/redux/flow/reducer';
import { LoaderCircle } from '~/components/input/LoaderCircle';
interface IProps {
search: IFlowState['search'];
}
const FlowSearchResults: FC<IProps> = ({ search }) => {
if (search.is_loading) {
return (
<div className={styles.loading}>
<LoaderCircle size={64} />
</div>
);
}
return <div className={styles.wrap}>SEARCH</div>;
};
export { FlowSearchResults };

View file

@ -0,0 +1,12 @@
.wrap {
flex: 1;
background: red;
}
.loading {
display: flex;
align-items: center;
justify-content: center;
flex: 1;
opacity: 0.3;
}

View file

@ -0,0 +1,60 @@
import React, { FC, useCallback, FormEvent } from 'react';
import { IFlowState } from '~/redux/flow/reducer';
import { InputText } from '~/components/input/InputText';
import { FlowRecent } from '../FlowRecent';
import classnames from 'classnames';
import * as styles from './styles.scss';
import * as FLOW_ACTIONS from '~/redux/flow/actions';
import { FlowSearchResults } from '../FlowSearchResults';
interface IProps {
recent: IFlowState['recent'];
updated: IFlowState['updated'];
search: IFlowState['search'];
flowChangeSearch: typeof FLOW_ACTIONS.flowChangeSearch;
}
const FlowStamp: FC<IProps> = ({ recent, updated, search, flowChangeSearch }) => {
const onSearchChange = useCallback((text: string) => flowChangeSearch({ text }), [
flowChangeSearch,
]);
const onSearchSubmit = useCallback((event: FormEvent) => {
event.preventDefault();
}, []);
return (
<div className={styles.wrap}>
<form className={styles.search} onSubmit={onSearchSubmit}>
<InputText title="Поиск" value={search.text} handler={onSearchChange} />
</form>
<div className={styles.grid}>
{search.text ? (
<>
<div className={styles.label}>
<span className={styles.label_text}>Результаты поиска</span>
</div>
<div className={styles.items}>
<FlowSearchResults search={search} />
</div>
</>
) : (
<>
<div className={styles.label}>
<span className={styles.label_text}>Что нового?</span>
</div>
<div className={styles.items}>
<FlowRecent updated={updated} recent={recent} />
</div>
</>
)}
</div>
</div>
);
};
export { FlowStamp };

View file

@ -0,0 +1,55 @@
.wrap {
display: flex;
flex-direction: column;
width: 100%;
background: $content_bg;
border-radius: $radius;
}
.grid {
display: flex;
justify-content: stretch;
flex-direction: column;
flex: 1;
border-radius: $radius;
@include outer_shadow();
}
.items {
padding: 0 $gap 0 $gap;
flex: 1;
display: flex;
flex-direction: column;
}
.label {
display: flex;
flex-direction: row;
min-width: 0;
padding: $gap;
border-radius: $radius;
@include title_with_line();
color: transparentize(white, $amount: 0.8);
&_search {
color: white;
padding-left: $gap * 1.2;
}
}
.label_text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.search {
background: lighten($content_bg, 4%);
border-radius: $radius $radius 0 0;
padding: $gap;
@include outer_shadow();
}

View file

@ -23,7 +23,7 @@
}
.text {
margin: 0 $gap;
margin-left: $gap / 2;
}
.placeholder {

View file

@ -30,4 +30,5 @@ export const API = {
`/node/${id}/comment/${comment_id}/lock`,
SET_CELL_VIEW: (id: INode['id']) => `/node/${id}/cell-view`,
},
SEARCH: '/search',
};

View file

@ -7,11 +7,12 @@ import * as FLOW_ACTIONS from '~/redux/flow/actions';
import pick from 'ramda/es/pick';
import { selectUser } from '~/redux/auth/selectors';
import { FlowHero } from '~/components/flow/FlowHero';
import { FlowRecent } from '~/components/flow/FlowRecent';
import styles from './styles.scss';
import { IState } from '~/redux/store';
import { FlowStamp } from '~/components/flow/FlowStamp';
const mapStateToProps = state => ({
flow: pick(['nodes', 'heroes', 'recent', 'updated', 'is_loading'], selectFlow(state)),
const mapStateToProps = (state: IState) => ({
flow: pick(['nodes', 'heroes', 'recent', 'updated', 'is_loading', 'search'], selectFlow(state)),
user: pick(['role', 'id'], selectUser(state)),
});
@ -19,16 +20,18 @@ const mapDispatchToProps = {
nodeGotoNode: NODE_ACTIONS.nodeGotoNode,
flowSetCellView: FLOW_ACTIONS.flowSetCellView,
flowGetMore: FLOW_ACTIONS.flowGetMore,
flowChangeSearch: FLOW_ACTIONS.flowChangeSearch,
};
type IProps = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & {};
const FlowLayoutUnconnected: FC<IProps> = ({
flow: { nodes, heroes, recent, updated, is_loading },
flow: { nodes, heroes, recent, updated, is_loading, search },
user,
nodeGotoNode,
flowSetCellView,
flowGetMore,
flowChangeSearch,
}) => {
const loadMore = useCallback(() => {
const pos = window.scrollY + window.innerHeight - document.body.scrollHeight;
@ -51,7 +54,12 @@ const FlowLayoutUnconnected: FC<IProps> = ({
</div>
<div className={styles.stamp}>
<FlowRecent recent={recent} updated={updated} />
<FlowStamp
recent={recent}
updated={updated}
flowChangeSearch={flowChangeSearch}
search={search}
/>
</div>
<FlowGrid

View file

@ -1,43 +1,48 @@
import { FLOW_ACTIONS } from "./constants";
import { IFlowState } from "./reducer";
import { INode } from "../types";
import { FLOW_ACTIONS } from './constants';
import { IFlowState } from './reducer';
import { INode } from '../types';
export const flowSetNodes = (nodes: IFlowState["nodes"]) => ({
export const flowSetNodes = (nodes: IFlowState['nodes']) => ({
nodes,
type: FLOW_ACTIONS.SET_NODES
type: FLOW_ACTIONS.SET_NODES,
});
export const flowSetHeroes = (heroes: IFlowState["heroes"]) => ({
export const flowSetHeroes = (heroes: IFlowState['heroes']) => ({
heroes,
type: FLOW_ACTIONS.SET_HEROES
type: FLOW_ACTIONS.SET_HEROES,
});
export const flowSetRecent = (recent: IFlowState["recent"]) => ({
export const flowSetRecent = (recent: IFlowState['recent']) => ({
recent,
type: FLOW_ACTIONS.SET_RECENT
type: FLOW_ACTIONS.SET_RECENT,
});
export const flowSetUpdated = (updated: IFlowState["updated"]) => ({
export const flowSetUpdated = (updated: IFlowState['updated']) => ({
updated,
type: FLOW_ACTIONS.SET_UPDATED
type: FLOW_ACTIONS.SET_UPDATED,
});
export const flowSetCellView = (id: INode["id"], flow: INode["flow"]) => ({
export const flowSetCellView = (id: INode['id'], flow: INode['flow']) => ({
type: FLOW_ACTIONS.SET_CELL_VIEW,
id,
flow
});
export const flowSetRange = (range: IFlowState["range"]) => ({
range,
type: FLOW_ACTIONS.SET_RANGE
flow,
});
export const flowGetMore = () => ({
type: FLOW_ACTIONS.GET_MORE
type: FLOW_ACTIONS.GET_MORE,
});
export const flowSetFlow = (data: Partial<IFlowState>) => ({
type: FLOW_ACTIONS.SET_FLOW,
data
data,
});
export const flowSetSearch = (search: Partial<IFlowState['search']>) => ({
type: FLOW_ACTIONS.SET_SEARCH,
search,
});
export const flowChangeSearch = (search: Partial<IFlowState['search']>) => ({
type: FLOW_ACTIONS.CHANGE_SEARCH,
search,
});

View file

@ -1,16 +1,11 @@
import {
api,
configWithToken,
resultMiddleware,
errorMiddleware
} from "~/utils/api";
import { INode, IResultWithStatus } from "../types";
import { API } from "~/constants/api";
import { flowSetCellView } from "~/redux/flow/actions";
import { api, configWithToken, resultMiddleware, errorMiddleware } from '~/utils/api';
import { INode, IResultWithStatus } from '../types';
import { API } from '~/constants/api';
import { flowSetCellView } from '~/redux/flow/actions';
export const postNode = ({
access,
node
node,
}: {
access: string;
node: INode;
@ -20,24 +15,26 @@ export const postNode = ({
.then(resultMiddleware)
.catch(errorMiddleware);
// export const getNodes = ({
// from = null
// }: {
// from: string;
// }): Promise<IResultWithStatus<{ nodes: INode[] }>> =>
// api
// .get(API.NODE.GET, { params: { from } })
// .then(resultMiddleware)
// .catch(errorMiddleware);
export const postCellView = ({
id,
flow,
access
}: ReturnType<typeof flowSetCellView> & { access: string }): Promise<
IResultWithStatus<{ is_liked: INode["is_liked"] }>
> =>
access,
}: ReturnType<typeof flowSetCellView> & { access: string }): Promise<IResultWithStatus<{
is_liked: INode['is_liked'];
}>> =>
api
.post(API.NODE.SET_CELL_VIEW(id), { flow }, configWithToken(access))
.then(resultMiddleware)
.catch(errorMiddleware);
export const getSearchResults = ({
access,
text,
}: {
access: string;
text: string;
}): Promise<IResultWithStatus<{ nodes: INode[]; total: number }>> =>
api
.get(API.SEARCH, configWithToken(access, { params: { text } }))
.then(resultMiddleware)
.catch(errorMiddleware);

View file

@ -1,4 +1,4 @@
const prefix = "FLOW.";
const prefix = 'FLOW.';
export const FLOW_ACTIONS = {
GET_FLOW: `${prefix}GET_FLOW`,
@ -9,5 +9,8 @@ export const FLOW_ACTIONS = {
SET_UPDATED: `${prefix}SET_UPDATED`,
SET_RANGE: `${prefix}SET_RANGE`,
SET_CELL_VIEW: `${prefix}SET_CELL_VIEW`,
GET_MORE: `${prefix}GET_MORE`
GET_MORE: `${prefix}GET_MORE`,
SET_SEARCH: `${prefix}SET_SEARCH`,
CHANGE_SEARCH: `${prefix}CHANGE_SEARCH`,
};

View file

@ -1,46 +1,41 @@
import assocPath from "ramda/es/assocPath";
import { FLOW_ACTIONS } from "./constants";
import assocPath from 'ramda/es/assocPath';
import { FLOW_ACTIONS } from './constants';
import {
flowSetNodes,
flowSetHeroes,
flowSetRecent,
flowSetUpdated,
flowSetRange,
flowSetFlow
} from "./actions";
import { IFlowState } from "./reducer";
flowSetFlow,
flowSetSearch,
} from './actions';
import { IFlowState } from './reducer';
const setNodes = (
state: IFlowState,
{ nodes }: ReturnType<typeof flowSetNodes>
) => assocPath(["nodes"], nodes, state);
const setNodes = (state: IFlowState, { nodes }: ReturnType<typeof flowSetNodes>) =>
assocPath(['nodes'], nodes, state);
const setHeroes = (
state: IFlowState,
{ heroes }: ReturnType<typeof flowSetHeroes>
) => assocPath(["heroes"], heroes, state);
const setHeroes = (state: IFlowState, { heroes }: ReturnType<typeof flowSetHeroes>) =>
assocPath(['heroes'], heroes, state);
const setRecent = (
state: IFlowState,
{ recent }: ReturnType<typeof flowSetRecent>
) => assocPath(["recent"], recent, state);
const setRecent = (state: IFlowState, { recent }: ReturnType<typeof flowSetRecent>) =>
assocPath(['recent'], recent, state);
const setUpdated = (
state: IFlowState,
{ updated }: ReturnType<typeof flowSetUpdated>
) => assocPath(["updated"], updated, state);
const setUpdated = (state: IFlowState, { updated }: ReturnType<typeof flowSetUpdated>) =>
assocPath(['updated'], updated, state);
const setRange = (
state: IFlowState,
{ range }: ReturnType<typeof flowSetRange>
) => assocPath(["range"], range, state);
const setFlow = (state: IFlowState, { data }: ReturnType<typeof flowSetFlow>): IFlowState => ({
...state,
...data,
});
const setFlow = (
const setSearch = (
state: IFlowState,
{ data }: ReturnType<typeof flowSetFlow>
{ search }: ReturnType<typeof flowSetSearch>
): IFlowState => ({
...state,
...data
search: {
...state.search,
...search,
},
});
export const FLOW_HANDLERS = {
@ -48,6 +43,6 @@ export const FLOW_HANDLERS = {
[FLOW_ACTIONS.SET_HEROES]: setHeroes,
[FLOW_ACTIONS.SET_RECENT]: setRecent,
[FLOW_ACTIONS.SET_UPDATED]: setUpdated,
[FLOW_ACTIONS.SET_RANGE]: setRange,
[FLOW_ACTIONS.SET_FLOW]: setFlow
[FLOW_ACTIONS.SET_FLOW]: setFlow,
[FLOW_ACTIONS.SET_SEARCH]: setSearch,
};

View file

@ -1,6 +1,6 @@
import { createReducer } from "~/utils/reducer";
import { INode, IError } from "../types";
import { FLOW_HANDLERS } from "./handlers";
import { createReducer } from '~/utils/reducer';
import { INode, IError } from '../types';
import { FLOW_HANDLERS } from './handlers';
export type IFlowState = Readonly<{
is_loading: boolean;
@ -8,7 +8,11 @@ export type IFlowState = Readonly<{
heroes: Partial<INode>[];
recent: Partial<INode>[];
updated: Partial<INode>[];
range: [string, string];
search: {
text: string;
is_loading: boolean;
results: INode[];
};
error: IError;
}>;
@ -17,9 +21,13 @@ const INITIAL_STATE: IFlowState = {
heroes: [],
recent: [],
updated: [],
range: [null, null], // drop it, we use realtime range calc
search: {
text: '',
is_loading: false,
results: [],
},
is_loading: false,
error: null
error: null,
};
export default createReducer(INITIAL_STATE, FLOW_HANDLERS);

View file

@ -9,11 +9,13 @@ import {
flowSetRecent,
flowSetUpdated,
flowSetFlow,
flowChangeSearch,
flowSetSearch,
} from './actions';
import { IResultWithStatus, INode } from '../types';
import { IResultWithStatus, INode, Unwrap } from '../types';
import { selectFlowNodes } from './selectors';
import { reqWrapper } from '../auth/sagas';
import { postCellView } from './api';
import { postCellView, getSearchResults } from './api';
import { IFlowState } from './reducer';
import uniq from 'ramda/es/uniq';
@ -110,8 +112,28 @@ function* getMore() {
yield delay(1000);
}
function* changeSearch({ search }: ReturnType<typeof flowChangeSearch>) {
yield put(
flowSetSearch({
...search,
is_loading: !!search.text,
})
);
if (!search.text) return;
yield delay(500);
const res: Unwrap<typeof getSearchResults> = yield call(reqWrapper, getSearchResults, {
...search,
});
console.log(res);
}
export default function* nodeSaga() {
yield takeLatest([FLOW_ACTIONS.GET_FLOW, REHYDRATE], onGetFlow);
yield takeLatest(FLOW_ACTIONS.SET_CELL_VIEW, onSetCellView);
yield takeLeading(FLOW_ACTIONS.GET_MORE, getMore);
yield takeLatest(FLOW_ACTIONS.CHANGE_SEARCH, changeSearch);
}