mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-24 20:36:40 +07:00
removed lab reducer
This commit is contained in:
parent
e24ea4afeb
commit
2b7b756212
26 changed files with 242 additions and 369 deletions
|
@ -1,3 +1,4 @@
|
||||||
#REACT_APP_API_HOST=http://localhost:3334/
|
#REACT_APP_API_HOST=http://localhost:3334/
|
||||||
REACT_APP_API_HOST=https://pig.staging.vault48.org/
|
REACT_APP_API_HOST=https://pig.staging.vault48.org/
|
||||||
|
#REACT_APP_API_HOST=https://pig.vault48.org/
|
||||||
REACT_APP_REMOTE_CURRENT=https://pig.staging.vault48.org/static/
|
REACT_APP_REMOTE_CURRENT=https://pig.staging.vault48.org/static/
|
||||||
|
|
|
@ -5,7 +5,7 @@ import {
|
||||||
GetLabNodesResult,
|
GetLabNodesResult,
|
||||||
GetLabStatsResult,
|
GetLabStatsResult,
|
||||||
GetLabUpdatesResult,
|
GetLabUpdatesResult,
|
||||||
} from '~/redux/lab/types';
|
} from '~/types/lab';
|
||||||
|
|
||||||
export const getLabNodes = ({ after }: GetLabNodesRequest) =>
|
export const getLabNodes = ({ after }: GetLabNodesRequest) =>
|
||||||
api
|
api
|
|
@ -17,13 +17,12 @@ import * as AUTH_ACTIONS from '~/redux/auth/actions';
|
||||||
import { IState } from '~/redux/store';
|
import { IState } from '~/redux/store';
|
||||||
import isBefore from 'date-fns/isBefore';
|
import isBefore from 'date-fns/isBefore';
|
||||||
import { Authorized } from '~/components/containers/Authorized';
|
import { Authorized } from '~/components/containers/Authorized';
|
||||||
import { useShallowSelect } from '~/hooks/data/useShallowSelect';
|
|
||||||
import { selectLabUpdatesNodes } from '~/redux/lab/selectors';
|
|
||||||
import { Button } from '~/components/input/Button';
|
import { Button } from '~/components/input/Button';
|
||||||
import { useFlowStore } from '~/store/flow/useFlowStore';
|
import { useFlowStore } from '~/store/flow/useFlowStore';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
import { useShowModal } from '~/hooks/modal/useShowModal';
|
import { useShowModal } from '~/hooks/modal/useShowModal';
|
||||||
import { Dialog } from '~/constants/modal';
|
import { Dialog } from '~/constants/modal';
|
||||||
|
import { useGetLabStats } from '~/hooks/lab/useGetLabStats';
|
||||||
|
|
||||||
const mapStateToProps = (state: IState) => ({
|
const mapStateToProps = (state: IState) => ({
|
||||||
user: pick(['username', 'is_user', 'photo', 'last_seen_boris'])(selectUser(state)),
|
user: pick(['username', 'is_user', 'photo', 'last_seen_boris'])(selectUser(state)),
|
||||||
|
@ -49,9 +48,9 @@ const HeaderUnconnected: FC<IProps> = observer(
|
||||||
authOpenProfile,
|
authOpenProfile,
|
||||||
}) => {
|
}) => {
|
||||||
const [is_scrolled, setIsScrolled] = useState(false);
|
const [is_scrolled, setIsScrolled] = useState(false);
|
||||||
const labUpdates = useShallowSelect(selectLabUpdatesNodes);
|
|
||||||
const { updated: flowUpdates } = useFlowStore();
|
const { updated: flowUpdates } = useFlowStore();
|
||||||
const onLogin = useShowModal(Dialog.Login);
|
const onLogin = useShowModal(Dialog.Login);
|
||||||
|
const labStats = useGetLabStats();
|
||||||
|
|
||||||
const onScroll = useCallback(() => {
|
const onScroll = useCallback(() => {
|
||||||
const active = window.scrollY > 32;
|
const active = window.scrollY > 32;
|
||||||
|
@ -76,7 +75,7 @@ const HeaderUnconnected: FC<IProps> = observer(
|
||||||
[boris_commented_at, is_user, last_seen_boris]
|
[boris_commented_at, is_user, last_seen_boris]
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasLabUpdates = useMemo(() => labUpdates.length > 0, [labUpdates]);
|
const hasLabUpdates = useMemo(() => labStats.updates.length > 0, [labStats.updates]);
|
||||||
const hasFlowUpdates = useMemo(() => flowUpdates.length > 0, [flowUpdates]);
|
const hasFlowUpdates = useMemo(() => flowUpdates.length > 0, [flowUpdates]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import React, { FC, useMemo } from 'react';
|
import React, { FC } from 'react';
|
||||||
import Masonry from 'react-masonry-css';
|
import Masonry from 'react-masonry-css';
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
import { LabNode } from '~/components/lab/LabNode';
|
import { LabNode } from '~/components/lab/LabNode';
|
||||||
import { EMPTY_NODE, NODE_TYPES } from '~/constants/node';
|
import { EMPTY_NODE, NODE_TYPES } from '~/constants/node';
|
||||||
import { values } from 'ramda';
|
import { values } from 'ramda';
|
||||||
import { useLabPagination } from '~/hooks/lab/useLabPagination';
|
|
||||||
import { useLabContext } from '~/utils/context/LabContextProvider';
|
import { useLabContext } from '~/utils/context/LabContextProvider';
|
||||||
|
import { InfiniteScroll } from '~/components/containers/InfiniteScroll';
|
||||||
|
|
||||||
interface IProps {}
|
interface IProps {}
|
||||||
|
|
||||||
|
@ -27,11 +27,7 @@ const LoadingNode = () => (
|
||||||
);
|
);
|
||||||
|
|
||||||
const LabGrid: FC<IProps> = () => {
|
const LabGrid: FC<IProps> = () => {
|
||||||
const { isLoading, nodes, onLoadMore } = useLabContext();
|
const { isLoading, nodes, hasMore, loadMore } = useLabContext();
|
||||||
|
|
||||||
const columns = useMemo(() => Array.from(document.querySelectorAll(`.${styles.column}`)), []);
|
|
||||||
|
|
||||||
useLabPagination(isLoading, columns, onLoadMore);
|
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
|
@ -54,20 +50,22 @@ const LabGrid: FC<IProps> = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Masonry
|
<InfiniteScroll hasMore={hasMore} loadMore={loadMore}>
|
||||||
className={styles.wrap}
|
<Masonry
|
||||||
breakpointCols={breakpointCols}
|
className={styles.wrap}
|
||||||
columnClassName={styles.column}
|
breakpointCols={breakpointCols}
|
||||||
>
|
columnClassName={styles.column}
|
||||||
{nodes.map(node => (
|
>
|
||||||
<LabNode
|
{nodes.map(node => (
|
||||||
node={node.node}
|
<LabNode
|
||||||
key={node.node.id}
|
node={node.node}
|
||||||
lastSeen={node.last_seen}
|
key={node.node.id}
|
||||||
commentCount={node.comment_count}
|
lastSeen={node.last_seen}
|
||||||
/>
|
commentCount={node.comment_count}
|
||||||
))}
|
/>
|
||||||
</Masonry>
|
))}
|
||||||
|
</Masonry>
|
||||||
|
</InfiniteScroll>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,22 +1,21 @@
|
||||||
import { useShallowSelect } from '~/hooks/data/useShallowSelect';
|
|
||||||
import { useFlowLayout } from '~/hooks/flow/useFlowLayout';
|
import { useFlowLayout } from '~/hooks/flow/useFlowLayout';
|
||||||
import { selectLabUpdatesNodes } from '~/redux/lab/selectors';
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useFlowLoader } from '~/hooks/flow/useFlowLoader';
|
import { useFlowLoader } from '~/hooks/flow/useFlowLoader';
|
||||||
import { useFlowStore } from '~/store/flow/useFlowStore';
|
import { useFlowStore } from '~/store/flow/useFlowStore';
|
||||||
import { useInfiniteLoader } from '~/hooks/dom/useInfiniteLoader';
|
import { useInfiniteLoader } from '~/hooks/dom/useInfiniteLoader';
|
||||||
import { useFlowSetCellView } from '~/hooks/flow/useFlowSetCellView';
|
import { useFlowSetCellView } from '~/hooks/flow/useFlowSetCellView';
|
||||||
|
import { useGetLabStats } from '~/hooks/lab/useGetLabStats';
|
||||||
|
|
||||||
export const useFlow = () => {
|
export const useFlow = () => {
|
||||||
const { loadMore, isSyncing } = useFlowLoader();
|
const { loadMore, isSyncing } = useFlowLoader();
|
||||||
|
|
||||||
const { nodes, heroes, recent, updated } = useFlowStore();
|
const { nodes, heroes, recent, updated } = useFlowStore();
|
||||||
const { isFluid, toggleLayout } = useFlowLayout();
|
const { isFluid, toggleLayout } = useFlowLayout();
|
||||||
const labUpdates = useShallowSelect(selectLabUpdatesNodes);
|
const lab = useGetLabStats();
|
||||||
|
|
||||||
useInfiniteLoader(loadMore, isSyncing);
|
useInfiniteLoader(loadMore, isSyncing);
|
||||||
|
|
||||||
const updates = useMemo(() => [...updated, ...labUpdates].slice(0, 10), [updated, labUpdates]);
|
const updates = useMemo(() => [...updated, ...lab.updates].slice(0, 10), [lab.updates, updated]);
|
||||||
|
|
||||||
const onChangeCellView = useFlowSetCellView();
|
const onChangeCellView = useFlowSetCellView();
|
||||||
return { nodes, heroes, recent, updates, isFluid, toggleLayout, onChangeCellView };
|
return { nodes, heroes, recent, updates, isFluid, toggleLayout, onChangeCellView };
|
||||||
|
|
85
src/hooks/lab/useGetLabNodes.ts
Normal file
85
src/hooks/lab/useGetLabNodes.ts
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
import useSWRInfinite from 'swr/infinite';
|
||||||
|
import { KeyLoader } from 'swr';
|
||||||
|
import { GetLabNodesRequest, ILabNode } from '~/types/lab';
|
||||||
|
import { getLabNodes } from '~/api/lab';
|
||||||
|
import { flatten, last, uniqBy } from 'ramda';
|
||||||
|
import { useLabStore } from '~/store/lab/useLabStore';
|
||||||
|
import { useCallback, useEffect } from 'react';
|
||||||
|
import { INode } from '~/redux/types';
|
||||||
|
import { useUser } from '~/hooks/user/userUser';
|
||||||
|
|
||||||
|
const getKey: (isUser: boolean) => KeyLoader<ILabNode[]> = isUser => (index, prev) => {
|
||||||
|
if (!isUser) return null;
|
||||||
|
if (index > 0 && !prev?.length) return null;
|
||||||
|
|
||||||
|
const lastNode = last(prev || []);
|
||||||
|
if (!lastNode && index > 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props: GetLabNodesRequest = {
|
||||||
|
after: lastNode?.node.commented_at || lastNode?.node.created_at,
|
||||||
|
};
|
||||||
|
|
||||||
|
return JSON.stringify(props);
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseKey = (key: string): GetLabNodesRequest => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(key);
|
||||||
|
} catch (error) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetLabNodes = () => {
|
||||||
|
const labStore = useLabStore();
|
||||||
|
const { is_user } = useUser();
|
||||||
|
|
||||||
|
const { data, isValidating, size, setSize, mutate } = useSWRInfinite(
|
||||||
|
getKey(is_user),
|
||||||
|
async (key: string) => {
|
||||||
|
const result = await getLabNodes(parseKey(key));
|
||||||
|
return result.nodes;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fallbackData: [labStore.nodes],
|
||||||
|
onSuccess: data => labStore.setNodes(flatten(data)),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const nodes = uniqBy(n => n.node.id, flatten(data || []));
|
||||||
|
const hasMore = (data?.[size - 1]?.length || 0) >= 1;
|
||||||
|
const loadMore = useCallback(() => setSize(size + 1), [setSize, size]);
|
||||||
|
|
||||||
|
/** prepends list with a node */
|
||||||
|
const unshift = useCallback(
|
||||||
|
async (node: ILabNode) => {
|
||||||
|
await mutate([[node], ...(data || [])]);
|
||||||
|
},
|
||||||
|
[data, mutate]
|
||||||
|
);
|
||||||
|
|
||||||
|
/** updates node on cache */
|
||||||
|
const updateNode = useCallback(
|
||||||
|
async (nodeId: number, node: Partial<INode>) => {
|
||||||
|
await mutate(
|
||||||
|
data?.map(page =>
|
||||||
|
page.map(it => (it.node.id === nodeId ? { ...it, node: { ...it.node, node } } : it))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[data, mutate]
|
||||||
|
);
|
||||||
|
|
||||||
|
/** purge cache on exit */
|
||||||
|
useEffect(() => {
|
||||||
|
if (is_user) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
labStore.setNodes([]);
|
||||||
|
}, [is_user, labStore]);
|
||||||
|
|
||||||
|
return { nodes, isLoading: !data && isValidating, hasMore, loadMore, unshift, updateNode };
|
||||||
|
};
|
72
src/hooks/lab/useGetLabStats.ts
Normal file
72
src/hooks/lab/useGetLabStats.ts
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import { API } from '~/constants/api';
|
||||||
|
import { getLabStats, getLabUpdates } from '~/api/lab';
|
||||||
|
import { useLabStore } from '~/store/lab/useLabStore';
|
||||||
|
import { useCallback, useEffect } from 'react';
|
||||||
|
import { useUser } from '~/hooks/user/userUser';
|
||||||
|
|
||||||
|
const refreshInterval = 1000 * 60 * 5; // 5 minutes
|
||||||
|
|
||||||
|
export const useGetLabStats = () => {
|
||||||
|
const lab = useLabStore();
|
||||||
|
const { is_user } = useUser();
|
||||||
|
|
||||||
|
const { data: stats, isValidating: isValidatingStats } = useSWR(
|
||||||
|
is_user ? API.LAB.STATS : null,
|
||||||
|
async () => getLabStats(),
|
||||||
|
{
|
||||||
|
fallbackData: {
|
||||||
|
heroes: lab.heroes,
|
||||||
|
tags: lab.tags,
|
||||||
|
},
|
||||||
|
onSuccess: data => {
|
||||||
|
lab.setHeroes(data.heroes);
|
||||||
|
lab.setTags(data.tags);
|
||||||
|
},
|
||||||
|
refreshInterval,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: updatesData, isValidating: isValidatingUpdates, mutate: mutateUpdates } = useSWR(
|
||||||
|
is_user ? API.LAB.UPDATES : null,
|
||||||
|
async () => {
|
||||||
|
const result = await getLabUpdates();
|
||||||
|
return result.nodes;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fallbackData: lab.updates,
|
||||||
|
onSuccess: data => {
|
||||||
|
lab.setUpdates(data);
|
||||||
|
},
|
||||||
|
refreshInterval,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const heroes = stats?.heroes || [];
|
||||||
|
const tags = stats?.tags || [];
|
||||||
|
const updates = updatesData || [];
|
||||||
|
|
||||||
|
const isLoading = (!stats || !updates) && (isValidatingStats || isValidatingUpdates);
|
||||||
|
const seenNode = useCallback(
|
||||||
|
async (nodeId: number) => {
|
||||||
|
await mutateUpdates(
|
||||||
|
updates.filter(it => it.id !== nodeId),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[mutateUpdates, updates]
|
||||||
|
);
|
||||||
|
|
||||||
|
/** purge cache on exit */
|
||||||
|
useEffect(() => {
|
||||||
|
if (is_user) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lab.setHeroes([]);
|
||||||
|
lab.setTags([]);
|
||||||
|
lab.setUpdates([]);
|
||||||
|
}, [is_user, lab]);
|
||||||
|
|
||||||
|
return { heroes, tags, updates, isLoading, seenNode };
|
||||||
|
};
|
|
@ -1,35 +1,9 @@
|
||||||
import { useShallowSelect } from '~/hooks/data/useShallowSelect';
|
import { useGetLabNodes } from '~/hooks/lab/useGetLabNodes';
|
||||||
import {
|
import { useGetLabStats } from '~/hooks/lab/useGetLabStats';
|
||||||
selectLabList,
|
|
||||||
selectLabStatsHeroes,
|
|
||||||
selectLabStatsLoading,
|
|
||||||
selectLabStatsTags,
|
|
||||||
selectLabUpdatesNodes,
|
|
||||||
} from '~/redux/lab/selectors';
|
|
||||||
import { useDispatch } from 'react-redux';
|
|
||||||
import { useCallback, useEffect } from 'react';
|
|
||||||
import { labGetList, labGetMore, labGetStats } from '~/redux/lab/actions';
|
|
||||||
|
|
||||||
export const useLab = () => {
|
export const useLab = () => {
|
||||||
const { is_loading: isLoading, nodes, count } = useShallowSelect(selectLabList);
|
const { nodes, isLoading, loadMore, hasMore } = useGetLabNodes();
|
||||||
const dispatch = useDispatch();
|
const { tags, heroes, updates, isLoading: isLoadingStats } = useGetLabStats();
|
||||||
const tags = useShallowSelect(selectLabStatsTags);
|
|
||||||
const heroes = useShallowSelect(selectLabStatsHeroes);
|
|
||||||
const isLoadingStats = useShallowSelect(selectLabStatsLoading);
|
|
||||||
const updates = useShallowSelect(selectLabUpdatesNodes);
|
|
||||||
|
|
||||||
useEffect(() => {
|
return { isLoading, nodes, hasMore, loadMore, tags, heroes, isLoadingStats, updates };
|
||||||
dispatch(labGetList());
|
|
||||||
dispatch(labGetStats());
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
const onLoadMore = useCallback(() => {
|
|
||||||
if (nodes.length >= count) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(labGetMore());
|
|
||||||
}, [nodes, count, dispatch]);
|
|
||||||
|
|
||||||
return { isLoading, nodes, count, onLoadMore, tags, heroes, isLoadingStats, updates };
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,44 +0,0 @@
|
||||||
import { useCallback, useEffect, useMemo } from 'react';
|
|
||||||
|
|
||||||
export const useLabPagination = (
|
|
||||||
isLoading: boolean,
|
|
||||||
columns: Element[],
|
|
||||||
onLoadMore: () => void
|
|
||||||
) => {
|
|
||||||
const loadOnIntersection = useCallback<IntersectionObserverCallback>(
|
|
||||||
entries => {
|
|
||||||
const isVisible = entries.some(entry => entry.intersectionRatio > 0);
|
|
||||||
|
|
||||||
if (!isVisible) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
onLoadMore();
|
|
||||||
},
|
|
||||||
[onLoadMore]
|
|
||||||
);
|
|
||||||
|
|
||||||
const observer = useMemo(
|
|
||||||
() =>
|
|
||||||
new IntersectionObserver(loadOnIntersection, {
|
|
||||||
threshold: [0],
|
|
||||||
}),
|
|
||||||
[loadOnIntersection]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isLoading) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const lastItems = Array.from(columns)
|
|
||||||
.map(col => col.children.item(col.childNodes.length - 1))
|
|
||||||
.filter(el => el) as Element[];
|
|
||||||
|
|
||||||
lastItems.forEach(item => observer.observe(item));
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
lastItems.forEach(item => observer.unobserve(item));
|
|
||||||
};
|
|
||||||
}, [observer, columns, isLoading]);
|
|
||||||
};
|
|
|
@ -1,16 +1,12 @@
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { INode } from '~/redux/types';
|
import { INode } from '~/redux/types';
|
||||||
import { apiPostNode } from '~/api/node';
|
import { apiPostNode } from '~/api/node';
|
||||||
import { selectLabListNodes } from '~/redux/lab/selectors';
|
|
||||||
import { labSetList } from '~/redux/lab/actions';
|
|
||||||
import { useShallowSelect } from '~/hooks/data/useShallowSelect';
|
|
||||||
import { useDispatch } from 'react-redux';
|
|
||||||
import { useFlowStore } from '~/store/flow/useFlowStore';
|
import { useFlowStore } from '~/store/flow/useFlowStore';
|
||||||
|
import { useGetLabNodes } from '~/hooks/lab/useGetLabNodes';
|
||||||
|
|
||||||
export const useCreateNode = () => {
|
export const useCreateNode = () => {
|
||||||
const dispatch = useDispatch();
|
|
||||||
const flow = useFlowStore();
|
const flow = useFlowStore();
|
||||||
const labNodes = useShallowSelect(selectLabListNodes);
|
const lab = useGetLabNodes();
|
||||||
|
|
||||||
return useCallback(
|
return useCallback(
|
||||||
async (node: INode) => {
|
async (node: INode) => {
|
||||||
|
@ -19,13 +15,9 @@ export const useCreateNode = () => {
|
||||||
if (node.is_promoted) {
|
if (node.is_promoted) {
|
||||||
flow.setNodes([result.node, ...flow.nodes]);
|
flow.setNodes([result.node, ...flow.nodes]);
|
||||||
} else {
|
} else {
|
||||||
const updatedNodes = [
|
await lab.unshift({ node: result.node, comment_count: 0, last_seen: node.created_at });
|
||||||
{ node: result.node, comment_count: 0, last_seen: node.created_at },
|
|
||||||
...labNodes,
|
|
||||||
];
|
|
||||||
dispatch(labSetList({ nodes: updatedNodes }));
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[flow, labNodes, dispatch]
|
[flow, lab]
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
import { INode } from '~/redux/types';
|
import { INode } from '~/redux/types';
|
||||||
import { useDispatch } from 'react-redux';
|
|
||||||
import { labSeenNode } from '~/redux/lab/actions';
|
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useFlowStore } from '~/store/flow/useFlowStore';
|
import { useFlowStore } from '~/store/flow/useFlowStore';
|
||||||
|
import { useGetLabStats } from '~/hooks/lab/useGetLabStats';
|
||||||
|
|
||||||
// useOnNodeSeen updates node seen status across all needed places
|
// useOnNodeSeen updates node seen status across all needed places
|
||||||
export const useOnNodeSeen = (node?: INode) => {
|
export const useOnNodeSeen = (node?: INode) => {
|
||||||
|
const labStats = useGetLabStats();
|
||||||
const flow = useFlowStore();
|
const flow = useFlowStore();
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!node?.id) {
|
if (!node?.id) {
|
||||||
|
@ -18,7 +17,8 @@ export const useOnNodeSeen = (node?: INode) => {
|
||||||
if (node.is_promoted) {
|
if (node.is_promoted) {
|
||||||
flow.seenNode(node.id);
|
flow.seenNode(node.id);
|
||||||
} else {
|
} else {
|
||||||
dispatch(labSeenNode(node.id));
|
void labStats.seenNode(node.id);
|
||||||
}
|
}
|
||||||
}, [dispatch, flow, node]);
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [node?.id]);
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,17 +2,15 @@ import { useLoadNode } from '~/hooks/node/useLoadNode';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { INode } from '~/redux/types';
|
import { INode } from '~/redux/types';
|
||||||
import { apiPostNode } from '~/api/node';
|
import { apiPostNode } from '~/api/node';
|
||||||
import { selectLabListNodes } from '~/redux/lab/selectors';
|
|
||||||
import { labSetList } from '~/redux/lab/actions';
|
|
||||||
import { useShallowSelect } from '~/hooks/data/useShallowSelect';
|
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { useFlowStore } from '~/store/flow/useFlowStore';
|
import { useFlowStore } from '~/store/flow/useFlowStore';
|
||||||
|
import { useGetLabNodes } from '~/hooks/lab/useGetLabNodes';
|
||||||
|
|
||||||
export const useUpdateNode = (id: number) => {
|
export const useUpdateNode = (id: number) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const { update } = useLoadNode(id);
|
const { update } = useLoadNode(id);
|
||||||
const flow = useFlowStore();
|
const flow = useFlowStore();
|
||||||
const labNodes = useShallowSelect(selectLabListNodes);
|
const lab = useGetLabNodes();
|
||||||
|
|
||||||
return useCallback(
|
return useCallback(
|
||||||
async (node: INode) => {
|
async (node: INode) => {
|
||||||
|
@ -27,12 +25,9 @@ export const useUpdateNode = (id: number) => {
|
||||||
if (node.is_promoted) {
|
if (node.is_promoted) {
|
||||||
flow.updateNode(result.node.id!, result.node);
|
flow.updateNode(result.node.id!, result.node);
|
||||||
} else {
|
} else {
|
||||||
const updatedNodes = labNodes.map(item =>
|
await lab.updateNode(result.node.id!, result.node);
|
||||||
item.node.id === result.node.id ? { ...item, node: result.node } : item
|
|
||||||
);
|
|
||||||
dispatch(labSetList({ nodes: updatedNodes }));
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[update, flow, labNodes, dispatch]
|
[update, flow, lab]
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -55,16 +55,15 @@ import { messagesSet } from '~/redux/messages/actions';
|
||||||
import { SagaIterator } from 'redux-saga';
|
import { SagaIterator } from 'redux-saga';
|
||||||
import { isEmpty } from 'ramda';
|
import { isEmpty } from 'ramda';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
import { labGetUpdates } from '~/redux/lab/actions';
|
|
||||||
import { getMOBXStore } from '~/store';
|
import { getMOBXStore } from '~/store';
|
||||||
import { Dialog } from '~/constants/modal';
|
import { Dialog } from '~/constants/modal';
|
||||||
import { showErrorToast } from '~/utils/errors/showToast';
|
import { showErrorToast } from '~/utils/errors/showToast';
|
||||||
import { showToastSuccess, showToastInfo } from '~/utils/toast';
|
import { showToastInfo } from '~/utils/toast';
|
||||||
import { getRandomPhrase } from '~/constants/phrases';
|
import { getRandomPhrase } from '~/constants/phrases';
|
||||||
|
|
||||||
const modalStore = getMOBXStore().modal;
|
const modalStore = getMOBXStore().modal;
|
||||||
|
|
||||||
function* setTokenSaga({ token }: ReturnType<typeof authSetToken>) {
|
function setTokenSaga({ token }: ReturnType<typeof authSetToken>) {
|
||||||
localStorage.setItem('token', token);
|
localStorage.setItem('token', token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -205,7 +204,6 @@ function* getUpdates() {
|
||||||
function* startPollingSaga() {
|
function* startPollingSaga() {
|
||||||
while (true) {
|
while (true) {
|
||||||
yield call(getUpdates);
|
yield call(getUpdates);
|
||||||
yield put(labGetUpdates());
|
|
||||||
yield delay(60000);
|
yield delay(60000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,40 +0,0 @@
|
||||||
import { LAB_ACTIONS } from '~/redux/lab/constants';
|
|
||||||
import { ILabState } from '~/redux/lab/types';
|
|
||||||
import { INode } from '~/redux/types';
|
|
||||||
|
|
||||||
export const labGetList = (after?: string) => ({
|
|
||||||
type: LAB_ACTIONS.GET_LIST,
|
|
||||||
after,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const labSetList = (list: Partial<ILabState['list']>) => ({
|
|
||||||
type: LAB_ACTIONS.SET_LIST,
|
|
||||||
list,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const labGetStats = () => ({
|
|
||||||
type: LAB_ACTIONS.GET_STATS,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const labSetStats = (stats: Partial<ILabState['stats']>) => ({
|
|
||||||
type: LAB_ACTIONS.SET_STATS,
|
|
||||||
stats,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const labSetUpdates = (updates: Partial<ILabState['updates']>) => ({
|
|
||||||
type: LAB_ACTIONS.SET_UPDATES,
|
|
||||||
updates,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const labGetUpdates = () => ({
|
|
||||||
type: LAB_ACTIONS.GET_UPDATES,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const labSeenNode = (nodeId: INode['id']) => ({
|
|
||||||
type: LAB_ACTIONS.SEEN_NODE,
|
|
||||||
nodeId,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const labGetMore = () => ({
|
|
||||||
type: LAB_ACTIONS.GET_MORE,
|
|
||||||
});
|
|
|
@ -1,14 +0,0 @@
|
||||||
const prefix = 'LAB.';
|
|
||||||
|
|
||||||
export const LAB_ACTIONS = {
|
|
||||||
GET_LIST: `${prefix}GET_LIST`,
|
|
||||||
SET_LIST: `${prefix}SET_LIST`,
|
|
||||||
|
|
||||||
GET_STATS: `${prefix}GET_STATS`,
|
|
||||||
SET_STATS: `${prefix}SET_STATS`,
|
|
||||||
|
|
||||||
SET_UPDATES: `${prefix}SET_UPDATES`,
|
|
||||||
GET_UPDATES: `${prefix}GET_UPDATES`,
|
|
||||||
SEEN_NODE: `${prefix}SET_UPDATE_SEEN`,
|
|
||||||
GET_MORE: `${prefix}GET_MORE`,
|
|
||||||
};
|
|
|
@ -1,38 +0,0 @@
|
||||||
import { LAB_ACTIONS } from '~/redux/lab/constants';
|
|
||||||
import { labSetList, labSetStats, labSetUpdates } from '~/redux/lab/actions';
|
|
||||||
import { ILabState } from '~/redux/lab/types';
|
|
||||||
|
|
||||||
type LabHandler<T extends (...args: any) => any> = (
|
|
||||||
state: Readonly<ILabState>,
|
|
||||||
payload: ReturnType<T>
|
|
||||||
) => Readonly<ILabState>;
|
|
||||||
|
|
||||||
const setList: LabHandler<typeof labSetList> = (state, { list }) => ({
|
|
||||||
...state,
|
|
||||||
list: {
|
|
||||||
...state.list,
|
|
||||||
...list,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const setStats: LabHandler<typeof labSetStats> = (state, { stats }) => ({
|
|
||||||
...state,
|
|
||||||
stats: {
|
|
||||||
...state.stats,
|
|
||||||
...stats,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const setUpdates: LabHandler<typeof labSetUpdates> = (state, { updates }) => ({
|
|
||||||
...state,
|
|
||||||
updates: {
|
|
||||||
...state.updates,
|
|
||||||
...updates,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const LAB_HANDLERS = {
|
|
||||||
[LAB_ACTIONS.SET_LIST]: setList,
|
|
||||||
[LAB_ACTIONS.SET_STATS]: setStats,
|
|
||||||
[LAB_ACTIONS.SET_UPDATES]: setUpdates,
|
|
||||||
};
|
|
|
@ -1,24 +0,0 @@
|
||||||
import { createReducer } from '~/utils/reducer';
|
|
||||||
import { LAB_HANDLERS } from '~/redux/lab/handlers';
|
|
||||||
import { ILabState } from '~/redux/lab/types';
|
|
||||||
|
|
||||||
const INITIAL_STATE: ILabState = {
|
|
||||||
list: {
|
|
||||||
is_loading: false,
|
|
||||||
nodes: [],
|
|
||||||
count: 0,
|
|
||||||
error: '',
|
|
||||||
},
|
|
||||||
stats: {
|
|
||||||
is_loading: false,
|
|
||||||
heroes: [],
|
|
||||||
tags: [],
|
|
||||||
error: undefined,
|
|
||||||
},
|
|
||||||
updates: {
|
|
||||||
nodes: [],
|
|
||||||
isLoading: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default createReducer(INITIAL_STATE, LAB_HANDLERS);
|
|
|
@ -1,90 +0,0 @@
|
||||||
import { call, put, select, takeLeading } from 'redux-saga/effects';
|
|
||||||
import {
|
|
||||||
labGetList,
|
|
||||||
labSeenNode,
|
|
||||||
labSetList,
|
|
||||||
labSetStats,
|
|
||||||
labSetUpdates,
|
|
||||||
} from '~/redux/lab/actions';
|
|
||||||
import { LAB_ACTIONS } from '~/redux/lab/constants';
|
|
||||||
import { Unwrap } from '~/redux/types';
|
|
||||||
import { getLabNodes, getLabStats, getLabUpdates } from '~/redux/lab/api';
|
|
||||||
import { selectLabList, selectLabUpdatesNodes } from '~/redux/lab/selectors';
|
|
||||||
|
|
||||||
function* getList({ after = '' }: ReturnType<typeof labGetList>) {
|
|
||||||
try {
|
|
||||||
yield put(labSetList({ is_loading: true }));
|
|
||||||
const { nodes, count }: Unwrap<typeof getLabNodes> = yield call(getLabNodes, { after });
|
|
||||||
yield put(labSetList({ nodes, count }));
|
|
||||||
} catch (error) {
|
|
||||||
yield put(labSetList({ error: error.message }));
|
|
||||||
} finally {
|
|
||||||
yield put(labSetList({ is_loading: false }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function* getStats() {
|
|
||||||
try {
|
|
||||||
yield put(labSetStats({ is_loading: true }));
|
|
||||||
const { heroes, tags }: Unwrap<typeof getLabStats> = yield call(getLabStats);
|
|
||||||
yield put(labSetStats({ heroes, tags }));
|
|
||||||
} catch (error) {
|
|
||||||
yield put(labSetStats({ error: error.message }));
|
|
||||||
} finally {
|
|
||||||
yield put(labSetStats({ is_loading: false }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function* getUpdates() {
|
|
||||||
try {
|
|
||||||
yield put(labSetUpdates({ isLoading: true }));
|
|
||||||
const { nodes }: Unwrap<typeof getLabUpdates> = yield call(getLabUpdates);
|
|
||||||
yield put(labSetUpdates({ nodes }));
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error.message);
|
|
||||||
} finally {
|
|
||||||
yield put(labSetUpdates({ isLoading: false }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function* seenNode({ nodeId }: ReturnType<typeof labSeenNode>) {
|
|
||||||
const nodes: ReturnType<typeof selectLabUpdatesNodes> = yield select(selectLabUpdatesNodes);
|
|
||||||
const newNodes = nodes.filter(node => node.id != nodeId);
|
|
||||||
yield put(labSetUpdates({ nodes: newNodes }));
|
|
||||||
}
|
|
||||||
|
|
||||||
function* getMore() {
|
|
||||||
try {
|
|
||||||
yield put(labSetList({ is_loading: true }));
|
|
||||||
|
|
||||||
const list: ReturnType<typeof selectLabList> = yield select(selectLabList);
|
|
||||||
if (list.nodes.length === list.count) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const last = list.nodes[list.nodes.length - 1];
|
|
||||||
|
|
||||||
if (!last) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const after = last.node.commented_at || last.node.created_at;
|
|
||||||
const { nodes, count }: Unwrap<typeof getLabNodes> = yield call(getLabNodes, { after });
|
|
||||||
const newNodes = [...list.nodes, ...nodes];
|
|
||||||
|
|
||||||
yield put(labSetList({ nodes: newNodes, count }));
|
|
||||||
} catch (error) {
|
|
||||||
yield put(labSetList({ error: error.message }));
|
|
||||||
} finally {
|
|
||||||
yield put(labSetList({ is_loading: false }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function* labSaga() {
|
|
||||||
yield takeLeading(LAB_ACTIONS.GET_LIST, getList);
|
|
||||||
yield takeLeading(LAB_ACTIONS.GET_STATS, getStats);
|
|
||||||
|
|
||||||
yield takeLeading(LAB_ACTIONS.GET_UPDATES, getUpdates);
|
|
||||||
yield takeLeading(LAB_ACTIONS.SEEN_NODE, seenNode);
|
|
||||||
|
|
||||||
yield takeLeading(LAB_ACTIONS.GET_MORE, getMore);
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
import { IState } from '~/redux/store';
|
|
||||||
|
|
||||||
export const selectLab = (state: IState) => state.lab;
|
|
||||||
export const selectLabListNodes = (state: IState) => state.lab.list.nodes;
|
|
||||||
export const selectLabList = (state: IState) => state.lab.list;
|
|
||||||
export const selectLabStatsHeroes = (state: IState) => state.lab.stats.heroes;
|
|
||||||
export const selectLabStatsTags = (state: IState) => state.lab.stats.tags;
|
|
||||||
export const selectLabStatsLoading = (state: IState) => state.lab.stats.is_loading;
|
|
||||||
export const selectLabUpdates = (state: IState) => state.lab.updates;
|
|
||||||
export const selectLabUpdatesNodes = (state: IState) => state.lab.updates.nodes;
|
|
|
@ -11,10 +11,6 @@ import auth from '~/redux/auth';
|
||||||
import authSaga from '~/redux/auth/sagas';
|
import authSaga from '~/redux/auth/sagas';
|
||||||
import { IAuthState } from '~/redux/auth/types';
|
import { IAuthState } from '~/redux/auth/types';
|
||||||
|
|
||||||
import lab from '~/redux/lab';
|
|
||||||
import labSaga from '~/redux/lab/sagas';
|
|
||||||
import { ILabState } from '~/redux/lab/types';
|
|
||||||
|
|
||||||
import uploads, { IUploadState } from '~/redux/uploads/reducer';
|
import uploads, { IUploadState } from '~/redux/uploads/reducer';
|
||||||
import uploadSaga from '~/redux/uploads/sagas';
|
import uploadSaga from '~/redux/uploads/sagas';
|
||||||
|
|
||||||
|
@ -48,7 +44,6 @@ export interface IState {
|
||||||
uploads: IUploadState;
|
uploads: IUploadState;
|
||||||
player: IPlayerState;
|
player: IPlayerState;
|
||||||
messages: IMessagesState;
|
messages: IMessagesState;
|
||||||
lab: ILabState;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const sagaMiddleware = createSagaMiddleware();
|
export const sagaMiddleware = createSagaMiddleware();
|
||||||
|
@ -68,7 +63,6 @@ export const store = createStore(
|
||||||
uploads,
|
uploads,
|
||||||
player: persistReducer(playerPersistConfig, player),
|
player: persistReducer(playerPersistConfig, player),
|
||||||
messages,
|
messages,
|
||||||
lab: lab,
|
|
||||||
}),
|
}),
|
||||||
composeEnhancers(applyMiddleware(routerMiddleware(history), sagaMiddleware))
|
composeEnhancers(applyMiddleware(routerMiddleware(history), sagaMiddleware))
|
||||||
);
|
);
|
||||||
|
@ -81,7 +75,6 @@ export function configureStore(): {
|
||||||
sagaMiddleware.run(uploadSaga);
|
sagaMiddleware.run(uploadSaga);
|
||||||
sagaMiddleware.run(playerSaga);
|
sagaMiddleware.run(playerSaga);
|
||||||
sagaMiddleware.run(messagesSaga);
|
sagaMiddleware.run(messagesSaga);
|
||||||
sagaMiddleware.run(labSaga);
|
|
||||||
|
|
||||||
window.addEventListener('message', message => {
|
window.addEventListener('message', message => {
|
||||||
if (message && message.data && message.data.type === 'oauth_login' && message.data.token)
|
if (message && message.data && message.data.type === 'oauth_login' && message.data.token)
|
||||||
|
|
|
@ -2,11 +2,13 @@ import { makeAutoObservable } from 'mobx';
|
||||||
import { FlowStore } from '~/store/flow/FlowStore';
|
import { FlowStore } from '~/store/flow/FlowStore';
|
||||||
import { ModalStore } from '~/store/modal/ModalStore';
|
import { ModalStore } from '~/store/modal/ModalStore';
|
||||||
import { PhotoSwipeStore } from '~/store/photoSwipe/PhotoSwipeStore';
|
import { PhotoSwipeStore } from '~/store/photoSwipe/PhotoSwipeStore';
|
||||||
|
import { LabStore } from '~/store/lab/LabStore';
|
||||||
|
|
||||||
export class Store {
|
export class Store {
|
||||||
flow = new FlowStore();
|
flow = new FlowStore();
|
||||||
modal = new ModalStore();
|
modal = new ModalStore();
|
||||||
photoSwipe = new PhotoSwipeStore();
|
photoSwipe = new PhotoSwipeStore();
|
||||||
|
lab = new LabStore();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
makeAutoObservable(this);
|
makeAutoObservable(this);
|
||||||
|
|
22
src/store/lab/LabStore.ts
Normal file
22
src/store/lab/LabStore.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import { makeAutoObservable } from 'mobx';
|
||||||
|
import { IFlowNode, ITag } from '~/redux/types';
|
||||||
|
import { ILabNode } from '~/types/lab';
|
||||||
|
|
||||||
|
export class LabStore {
|
||||||
|
// actual nodes
|
||||||
|
nodes: ILabNode[] = [];
|
||||||
|
|
||||||
|
// stats
|
||||||
|
heroes: IFlowNode[] = [];
|
||||||
|
tags: ITag[] = [];
|
||||||
|
updates: IFlowNode[] = [];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
makeAutoObservable(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
setNodes = (nodes: ILabNode[]) => (this.nodes = nodes);
|
||||||
|
setHeroes = (heroes: IFlowNode[]) => (this.heroes = heroes);
|
||||||
|
setTags = (tags: ITag[]) => (this.tags = tags);
|
||||||
|
setUpdates = (updates: IFlowNode[]) => (this.updates = updates);
|
||||||
|
}
|
3
src/store/lab/useLabStore.ts
Normal file
3
src/store/lab/useLabStore.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import { useStore } from '~/utils/context/StoreContextProvider';
|
||||||
|
|
||||||
|
export const useLabStore = () => useStore().lab;
|
|
@ -1,24 +1,24 @@
|
||||||
import { ILabNode } from '~/redux/lab/types';
|
import { ILabNode } from '~/types/lab';
|
||||||
import React, { createContext, FC, useContext } from 'react';
|
import React, { createContext, FC, useContext } from 'react';
|
||||||
import { INode, ITag } from '~/redux/types';
|
import { IFlowNode, ITag } from '~/redux/types';
|
||||||
|
|
||||||
export interface LabContextProps {
|
export interface LabContextProps {
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
nodes: ILabNode[];
|
nodes: ILabNode[];
|
||||||
count: number;
|
hasMore: boolean;
|
||||||
onLoadMore: () => void;
|
loadMore: () => void;
|
||||||
|
|
||||||
tags: ITag[];
|
tags: ITag[];
|
||||||
heroes: Partial<INode>[];
|
heroes: IFlowNode[];
|
||||||
isLoadingStats: boolean;
|
isLoadingStats: boolean;
|
||||||
updates: Partial<INode>[];
|
updates: IFlowNode[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultValues: LabContextProps = {
|
const defaultValues: LabContextProps = {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
nodes: [],
|
nodes: [],
|
||||||
count: 0,
|
hasMore: false,
|
||||||
onLoadMore: () => {},
|
loadMore: () => {},
|
||||||
tags: [],
|
tags: [],
|
||||||
heroes: [],
|
heroes: [],
|
||||||
isLoadingStats: false,
|
isLoadingStats: false,
|
||||||
|
|
|
@ -5,14 +5,14 @@ import { useLab } from '~/hooks/lab/useLab';
|
||||||
interface LabProviderProps {}
|
interface LabProviderProps {}
|
||||||
|
|
||||||
const LabProvider: FC<LabProviderProps> = ({ children }) => {
|
const LabProvider: FC<LabProviderProps> = ({ children }) => {
|
||||||
const { isLoading, nodes, count, onLoadMore, tags, heroes, isLoadingStats, updates } = useLab();
|
const { isLoading, nodes, loadMore, hasMore, tags, heroes, isLoadingStats, updates } = useLab();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LabContextProvider
|
<LabContextProvider
|
||||||
isLoading={isLoading && !nodes.length}
|
isLoading={isLoading && !nodes.length}
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
count={count}
|
hasMore={hasMore}
|
||||||
onLoadMore={onLoadMore}
|
loadMore={loadMore}
|
||||||
tags={tags}
|
tags={tags}
|
||||||
heroes={heroes}
|
heroes={heroes}
|
||||||
isLoadingStats={isLoadingStats}
|
isLoadingStats={isLoadingStats}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue