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

added lab search

This commit is contained in:
Fedor Katurov 2022-03-29 17:40:48 +07:00
parent 16d12f92da
commit ddf2b6eda3
16 changed files with 149 additions and 28 deletions

View file

@ -7,9 +7,9 @@ import {
} from '~/types/lab';
import { api, cleanResult } from '~/utils/api';
export const getLabNodes = ({ offset, limit, sort }: GetLabNodesRequest) =>
export const getLabNodes = ({ offset, limit, sort, search }: GetLabNodesRequest) =>
api
.get<GetLabNodesResult>(API.LAB.NODES, { params: { offset, limit, sort } })
.get<GetLabNodesResult>(API.LAB.NODES, { params: { offset, limit, sort, search } })
.then(cleanResult);
export const getLabStats = () => api.get<GetLabStatsResult>(API.LAB.STATS).then(cleanResult);

View file

@ -1,4 +1,13 @@
import React, { ChangeEvent, FC, useCallback, useState } from 'react';
import React, {
ChangeEvent,
DetailedHTMLProps,
FC,
InputHTMLAttributes,
ReactElement,
ReactNode,
useCallback,
useState,
} from 'react';
import classNames from 'classnames';
@ -6,11 +15,21 @@ import { Icon } from '~/components/input/Icon';
import { InputWrapper } from '~/components/input/InputWrapper';
import { useTranslatedError } from '~/hooks/data/useTranslatedError';
import { useFocusEvent } from '~/hooks/dom/useFocusEvent';
import { IInputTextProps } from '~/types';
import styles from './styles.module.scss';
const InputText: FC<IInputTextProps> = ({
export type InputTextProps = Omit<
DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>,
'prefix'
> & {
handler?: (value: string) => void;
title?: string;
error?: string;
suffix?: ReactNode;
prefix?: ReactNode;
};
const InputText: FC<InputTextProps> = ({
className = '',
handler,
required = false,
@ -43,7 +62,12 @@ const InputText: FC<IInputTextProps> = ({
return (
<InputWrapper title={title} error={translatedError} focused={focused} notEmpty={!!value}>
<div className={classNames(styles.input, { [styles.has_error]: !!error })}>
<div
className={classNames(styles.input, {
[styles.has_error]: !!error,
[styles.has_prefix]: prefix,
})}
>
{!!prefix && <div className={styles.prefix}>{prefix}</div>}
<input

View file

@ -9,6 +9,12 @@
color: $red;
}
&.has_prefix {
input {
padding-left: 0;
}
}
input {
height: $input_height;
border: none;

View file

@ -0,0 +1,12 @@
import React, { VFC } from 'react';
import { Icon } from '~/components/input/Icon';
import { InputText, InputTextProps } from '~/components/input/InputText';
interface SearchInputProps extends Omit<InputTextProps, 'prefix' | 'suffix'> {}
const SearchInput: VFC<SearchInputProps> = ({ ...props }) => (
<InputText {...props} prefix={<Icon icon="search" />} />
);
export { SearchInput };

View file

@ -1,6 +1,9 @@
import React, { FC } from 'react';
import { Filler } from '~/components/containers/Filler';
import { Group } from '~/components/containers/Group';
import { InputText } from '~/components/input/InputText';
import { SearchInput } from '~/components/input/SearchInput';
import { HorizontalMenu } from '~/components/menu/HorizontalMenu';
import { LabNodesSort } from '~/types/lab';
import { useLabContext } from '~/utils/context/LabContextProvider';
@ -12,10 +15,10 @@ interface IProps {
}
const LabHead: FC<IProps> = ({ isLoading }) => {
const { sort, setSort } = useLabContext();
const { sort, setSort, search, setSearch } = useLabContext();
return (
<Group className={styles.wrap} horizontal>
<div className={styles.wrap}>
<HorizontalMenu>
<HorizontalMenu.Item
color="green"
@ -47,7 +50,13 @@ const LabHead: FC<IProps> = ({ isLoading }) => {
Важные
</HorizontalMenu.Item>
</HorizontalMenu>
</Group>
<Filler />
<div className={styles.search}>
<SearchInput value={search} handler={setSearch} placeholder="Поиск" />
</div>
</div>
);
};

View file

@ -2,4 +2,16 @@
.wrap {
border-radius: $radius;
display: flex;
flex-direction: row;
@include tablet {
flex-direction: column;
}
}
.search {
@include tablet {
margin-top: $gap;
}
}

View file

@ -0,0 +1,19 @@
import React, { VFC } from 'react';
import { Card } from '~/components/containers/Card';
import { Button } from '~/components/input/Button';
import styles from './styles.module.scss';
interface LabNoResultsProps {
resetSearch: () => void;
}
const LabNoResults: VFC<LabNoResultsProps> = ({ resetSearch }) => (
<Card className={styles.wrap}>
<div className={styles.title}> Здесь ничего нет</div>
<Button onClick={resetSearch}>Сбросить поиск</Button>
</Card>
);
export { LabNoResults };

View file

@ -0,0 +1,16 @@
@import "src/styles/variables";
.wrap {
padding: $gap * 2;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 33vh;
}
.title {
text-transform: uppercase;
font: $font_20_semibold;
margin-bottom: $gap * 2;
}

View file

@ -3,6 +3,7 @@ import React, { FC } from 'react';
import Masonry from 'react-masonry-css';
import { InfiniteScroll } from '~/components/containers/InfiniteScroll';
import { LabNoResults } from '~/components/lab/LabNoResults';
import { LabNode } from '~/components/lab/LabNode';
import { EMPTY_NODE, NODE_TYPES } from '~/constants/node';
import { useLabContext } from '~/utils/context/LabContextProvider';
@ -30,7 +31,7 @@ const LoadingNode = () => (
);
const LabGrid: FC<IProps> = () => {
const { isLoading, nodes, hasMore, loadMore } = useLabContext();
const { isLoading, nodes, hasMore, loadMore, search, setSearch } = useLabContext();
if (isLoading) {
return (
@ -52,6 +53,10 @@ const LabGrid: FC<IProps> = () => {
);
}
if (search && !nodes.length) {
return <LabNoResults resetSearch={() => setSearch('')} />;
}
return (
<InfiniteScroll hasMore={hasMore} loadMore={loadMore}>
<Masonry

View file

@ -0,0 +1,12 @@
import { useEffect, useState } from 'react';
export const useDebouncedValue = <T>(val: T, delay = 300) => {
const [state, setState] = useState<T>(val);
useEffect(() => {
const timeout = setTimeout(() => setState(val), delay);
return () => clearTimeout(timeout);
}, [val, delay]);
return state;
};

View file

@ -4,15 +4,17 @@ import useSWRInfinite, { SWRInfiniteKeyLoader } from 'swr/infinite';
import { getLabNodes } from '~/api/lab';
import { useAuth } from '~/hooks/auth/useAuth';
import { useDebouncedValue } from '~/hooks/data/useDebouncedValue';
import { useLabStore } from '~/store/lab/useLabStore';
import { INode } from '~/types';
import { GetLabNodesRequest, ILabNode, LabNodesSort } from '~/types/lab';
import { flatten, uniqBy } from '~/utils/ramda';
const getKey: (isUser: boolean, sort?: LabNodesSort) => SWRInfiniteKeyLoader = (isUser, sort) => (
index,
prev: ILabNode[]
) => {
const getKey: (isUser: boolean, sort?: LabNodesSort, search?: string) => SWRInfiniteKeyLoader = (
isUser,
sort,
search
) => (index, prev: ILabNode[]) => {
if (!isUser) return null;
if (index > 0 && (!prev?.length || prev.length < 20)) return null;
@ -20,6 +22,7 @@ const getKey: (isUser: boolean, sort?: LabNodesSort) => SWRInfiniteKeyLoader = (
limit: 20,
offset: index * 20,
sort: sort || LabNodesSort.New,
search: search || '',
};
return JSON.stringify(props);
@ -33,12 +36,13 @@ const parseKey = (key: string): GetLabNodesRequest => {
}
};
export const useGetLabNodes = (sort?: LabNodesSort) => {
export const useGetLabNodes = (sort?: LabNodesSort, search?: string) => {
const labStore = useLabStore();
const { isUser } = useAuth();
const searchDebounced = useDebouncedValue(search);
const { data, isValidating, size, setSize, mutate } = useSWRInfinite(
getKey(isUser, sort),
getKey(isUser, sort, searchDebounced),
async (key: string) => {
const result = await getLabNodes(parseKey(key));
return result.nodes;
@ -46,6 +50,7 @@ export const useGetLabNodes = (sort?: LabNodesSort) => {
{
fallbackData: [labStore.nodes],
onSuccess: data => labStore.setNodes(flatten(data)),
dedupingInterval: 300,
}
);

View file

@ -6,8 +6,9 @@ import { LabNodesSort } from '~/types/lab';
export const useLab = () => {
const [sort, setSort] = useState<LabNodesSort>(LabNodesSort.New);
const [search, setSearch] = useState('');
const { nodes, isLoading, loadMore, hasMore } = useGetLabNodes(sort);
const { nodes, isLoading, loadMore, hasMore } = useGetLabNodes(sort, search);
const { tags, heroes, updates, isLoading: isLoadingStats } = useGetLabStats();
return {
@ -21,5 +22,7 @@ export const useLab = () => {
updates,
sort,
setSort,
search,
setSearch,
};
};

View file

@ -15,17 +15,6 @@ export interface ITag {
readonly updated_at: string;
}
export type IInputTextProps = DetailedHTMLProps<
InputHTMLAttributes<HTMLInputElement>,
HTMLInputElement
> & {
handler?: (value: string) => void;
title?: string;
error?: string;
suffix?: ReactElement;
prefix?: ReactElement;
};
export type IIcon = string;
export type ValueOf<T> = T[keyof T];

View file

@ -30,6 +30,7 @@ export type GetLabNodesRequest = {
offset?: number;
after?: string;
sort?: LabNodesSort;
search?: string;
};
export interface ILabNode {

View file

@ -15,6 +15,8 @@ export interface LabContextProps {
updates: IFlowNode[];
sort: LabNodesSort;
setSort: (sort: LabNodesSort) => void;
search: string;
setSearch: (val: string) => void;
}
const defaultValues: LabContextProps = {
@ -28,6 +30,8 @@ const defaultValues: LabContextProps = {
updates: [],
sort: LabNodesSort.New,
setSort: () => {},
search: '',
setSearch: () => {},
};
const LabContext = createContext<LabContextProps>(defaultValues);

View file

@ -17,6 +17,8 @@ const LabProvider: FC<LabProviderProps> = ({ children }) => {
updates,
sort,
setSort,
search,
setSearch,
} = useLab();
return (
@ -31,6 +33,8 @@ const LabProvider: FC<LabProviderProps> = ({ children }) => {
updates={updates}
sort={sort}
setSort={setSort}
search={search}
setSearch={setSearch}
>
{children}
</LabContextProvider>