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:
parent
16d12f92da
commit
ddf2b6eda3
16 changed files with 149 additions and 28 deletions
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -9,6 +9,12 @@
|
|||
color: $red;
|
||||
}
|
||||
|
||||
&.has_prefix {
|
||||
input {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
height: $input_height;
|
||||
border: none;
|
||||
|
|
12
src/components/input/SearchInput/index.tsx
Normal file
12
src/components/input/SearchInput/index.tsx
Normal 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 };
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -2,4 +2,16 @@
|
|||
|
||||
.wrap {
|
||||
border-radius: $radius;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
@include tablet {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.search {
|
||||
@include tablet {
|
||||
margin-top: $gap;
|
||||
}
|
||||
}
|
||||
|
|
19
src/components/lab/LabNoResults/index.tsx
Normal file
19
src/components/lab/LabNoResults/index.tsx
Normal 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 };
|
16
src/components/lab/LabNoResults/styles.module.scss
Normal file
16
src/components/lab/LabNoResults/styles.module.scss
Normal 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;
|
||||
}
|
|
@ -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
|
||||
|
|
12
src/hooks/data/useDebouncedValue.ts
Normal file
12
src/hooks/data/useDebouncedValue.ts
Normal 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;
|
||||
};
|
|
@ -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,
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -30,6 +30,7 @@ export type GetLabNodesRequest = {
|
|||
offset?: number;
|
||||
after?: string;
|
||||
sort?: LabNodesSort;
|
||||
search?: string;
|
||||
};
|
||||
|
||||
export interface ILabNode {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue