mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-25 12:56:41 +07:00
refactored hooks directory
This commit is contained in:
parent
efa3ba902d
commit
f76a5a4798
106 changed files with 122 additions and 144 deletions
|
@ -1,48 +0,0 @@
|
|||
import { useDispatch } from 'react-redux';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import isBefore from 'date-fns/isBefore';
|
||||
import { authSetState, authSetUser } from '~/redux/auth/actions';
|
||||
import { borisLoadStats } from '~/redux/boris/actions';
|
||||
import { useUser } from '~/utils/hooks/user/userUser';
|
||||
import { IComment } from '~/redux/types';
|
||||
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
|
||||
import { selectAuthIsTester } from '~/redux/auth/selectors';
|
||||
import { selectBorisStats } from '~/redux/boris/selectors';
|
||||
import { useRandomPhrase } from '~/constants/phrases';
|
||||
|
||||
export const useBoris = (comments: IComment[]) => {
|
||||
const dispatch = useDispatch();
|
||||
const user = useUser();
|
||||
|
||||
useEffect(() => {
|
||||
const last_comment = comments[0];
|
||||
|
||||
if (!last_comment) return;
|
||||
|
||||
if (
|
||||
user.last_seen_boris &&
|
||||
last_comment.created_at &&
|
||||
!isBefore(new Date(user.last_seen_boris), new Date(last_comment.created_at))
|
||||
)
|
||||
return;
|
||||
|
||||
dispatch(authSetUser({ last_seen_boris: last_comment.created_at }));
|
||||
}, [user.last_seen_boris, dispatch, comments]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(borisLoadStats());
|
||||
}, [dispatch]);
|
||||
|
||||
const setIsBetaTester = useCallback(
|
||||
(is_tester: boolean) => {
|
||||
dispatch(authSetState({ is_tester }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const isTester = useShallowSelect(selectAuthIsTester);
|
||||
const stats = useShallowSelect(selectBorisStats);
|
||||
const title = useRandomPhrase('BORIS_TITLE');
|
||||
|
||||
return { setIsBetaTester, isTester, stats, title };
|
||||
};
|
|
@ -1,34 +0,0 @@
|
|||
import { useCallback } from 'react';
|
||||
import { INode } from '~/redux/types';
|
||||
import { apiPostNode } from '~/redux/node/api';
|
||||
import { selectFlowNodes } from '~/redux/flow/selectors';
|
||||
import { flowSetNodes } from '~/redux/flow/actions';
|
||||
import { selectLabListNodes } from '~/redux/lab/selectors';
|
||||
import { labSetList } from '~/redux/lab/actions';
|
||||
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
export const useCreateNode = () => {
|
||||
const dispatch = useDispatch();
|
||||
const flowNodes = useShallowSelect(selectFlowNodes);
|
||||
const labNodes = useShallowSelect(selectLabListNodes);
|
||||
|
||||
return useCallback(
|
||||
async (node: INode) => {
|
||||
const result = await apiPostNode({ node });
|
||||
|
||||
// TODO: use another store here someday
|
||||
if (node.is_promoted) {
|
||||
const updatedNodes = [result.node, ...flowNodes];
|
||||
dispatch(flowSetNodes(updatedNodes));
|
||||
} else {
|
||||
const updatedNodes = [
|
||||
{ node: result.node, comment_count: 0, last_seen: node.created_at },
|
||||
...labNodes,
|
||||
];
|
||||
dispatch(labSetList({ nodes: updatedNodes }));
|
||||
}
|
||||
},
|
||||
[flowNodes, labNodes, dispatch]
|
||||
);
|
||||
};
|
|
@ -1,30 +0,0 @@
|
|||
import useSWR from 'swr';
|
||||
import { ApiGetNodeResponse } from '~/redux/node/types';
|
||||
import { API } from '~/constants/api';
|
||||
import { useOnNodeSeen } from '~/utils/hooks/node/useOnNodeSeen';
|
||||
import { apiGetNode } from '~/redux/node/api';
|
||||
import { useCallback } from 'react';
|
||||
import { INode } from '~/redux/types';
|
||||
import { EMPTY_NODE } from '~/redux/node/constants';
|
||||
|
||||
export const useGetNode = (id: number) => {
|
||||
const { data, isValidating, mutate } = useSWR<ApiGetNodeResponse>(API.NODE.GET_NODE(id), () =>
|
||||
apiGetNode({ id })
|
||||
);
|
||||
|
||||
const update = useCallback(
|
||||
async (node?: Partial<INode>) => {
|
||||
if (!data?.node) {
|
||||
await mutate();
|
||||
return;
|
||||
}
|
||||
|
||||
await mutate({ node: { ...data.node, ...node } }, true);
|
||||
},
|
||||
[data, mutate]
|
||||
);
|
||||
|
||||
useOnNodeSeen(data?.node);
|
||||
|
||||
return { node: data?.node || EMPTY_NODE, isLoading: isValidating && !data, update };
|
||||
};
|
|
@ -1,16 +0,0 @@
|
|||
import { INode } from '~/redux/types';
|
||||
import useSWR from 'swr';
|
||||
import { ApiGetNodeRelatedResult } from '~/redux/node/types';
|
||||
import { API } from '~/constants/api';
|
||||
import { useCallback } from 'react';
|
||||
import { apiGetNodeRelated } from '~/redux/node/api';
|
||||
|
||||
export const useGetNodeRelated = (id?: INode['id']) => {
|
||||
const { data, isValidating, mutate } = useSWR<ApiGetNodeRelatedResult>(API.NODE.RELATED(id), () =>
|
||||
apiGetNodeRelated({ id })
|
||||
);
|
||||
|
||||
const refresh = useCallback(() => mutate(data, true), [data, mutate]);
|
||||
|
||||
return { related: data?.related, isLoading: isValidating && !data, refresh };
|
||||
};
|
|
@ -1,43 +0,0 @@
|
|||
import { useGetNode } from '~/utils/hooks/data/useGetNode';
|
||||
import { useCallback } from 'react';
|
||||
import { INode } from '~/redux/types';
|
||||
import { apiPostNode } from '~/redux/node/api';
|
||||
import { selectFlowNodes } from '~/redux/flow/selectors';
|
||||
import { flowSetNodes } from '~/redux/flow/actions';
|
||||
import { selectLabListNodes } from '~/redux/lab/selectors';
|
||||
import { labSetList } from '~/redux/lab/actions';
|
||||
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
export const useUpdateNode = (id: number) => {
|
||||
const dispatch = useDispatch();
|
||||
const { update } = useGetNode(id);
|
||||
const flowNodes = useShallowSelect(selectFlowNodes);
|
||||
const labNodes = useShallowSelect(selectLabListNodes);
|
||||
|
||||
return useCallback(
|
||||
async (node: INode) => {
|
||||
const result = await apiPostNode({ node });
|
||||
|
||||
if (!update) {
|
||||
return;
|
||||
}
|
||||
|
||||
await update(result.node);
|
||||
|
||||
// TODO: use another store here someday
|
||||
if (node.is_promoted) {
|
||||
const updatedNodes = flowNodes.map(item =>
|
||||
item.id === result.node.id ? result.node : item
|
||||
);
|
||||
dispatch(flowSetNodes(updatedNodes));
|
||||
} else {
|
||||
const updatedNodes = labNodes.map(item =>
|
||||
item.node.id === result.node.id ? { ...item, node: result.node } : item
|
||||
);
|
||||
dispatch(labSetList({ nodes: updatedNodes }));
|
||||
}
|
||||
},
|
||||
[update, flowNodes, dispatch, labNodes]
|
||||
);
|
||||
};
|
|
@ -1,27 +0,0 @@
|
|||
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
|
||||
import { selectFlow } from '~/redux/flow/selectors';
|
||||
import { useFlowLayout } from '~/utils/hooks/flow/useFlowLayout';
|
||||
import { selectLabUpdatesNodes } from '~/redux/lab/selectors';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useFlowPagination } from '~/utils/hooks/flow/useFlowPagination';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { FlowDisplay, INode } from '~/redux/types';
|
||||
import { flowSetCellView } from '~/redux/flow/actions';
|
||||
|
||||
export const useFlow = () => {
|
||||
const { nodes, heroes, recent, updated, isLoading } = useShallowSelect(selectFlow);
|
||||
const { isFluid, toggleLayout } = useFlowLayout();
|
||||
const labUpdates = useShallowSelect(selectLabUpdatesNodes);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useFlowPagination({ isLoading });
|
||||
|
||||
const updates = useMemo(() => [...updated, ...labUpdates].slice(0, 10), [updated, labUpdates]);
|
||||
|
||||
const onChangeCellView = useCallback(
|
||||
(id: INode['id'], val: FlowDisplay) => dispatch(flowSetCellView(id, val)),
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
return { nodes, heroes, recent, updates, isFluid, toggleLayout, onChangeCellView };
|
||||
};
|
|
@ -1,46 +0,0 @@
|
|||
import { useCallback } from 'react';
|
||||
import { FlowDisplay, INode } from '~/redux/types';
|
||||
|
||||
export const useFlowCellControls = (
|
||||
id: INode['id'],
|
||||
description: string | undefined,
|
||||
flow: FlowDisplay,
|
||||
onChangeCellView: (id: INode['id'], flow: FlowDisplay) => void
|
||||
) => {
|
||||
const onChange = useCallback(
|
||||
(value: Partial<FlowDisplay>) => onChangeCellView(id, { ...flow, ...value }),
|
||||
[flow, id, onChangeCellView]
|
||||
);
|
||||
|
||||
const hasDescription = !!description && description.length > 32;
|
||||
|
||||
const toggleViewDescription = useCallback(() => {
|
||||
const show_description = !(flow && flow.show_description);
|
||||
onChange({ show_description });
|
||||
}, [flow, onChange]);
|
||||
|
||||
const setViewSingle = useCallback(() => {
|
||||
onChange({ display: 'single' });
|
||||
}, [onChange]);
|
||||
|
||||
const setViewHorizontal = useCallback(() => {
|
||||
onChange({ display: 'horizontal' });
|
||||
}, [onChange]);
|
||||
|
||||
const setViewVertical = useCallback(() => {
|
||||
onChange({ display: 'vertical' });
|
||||
}, [onChange]);
|
||||
|
||||
const setViewQuadro = useCallback(() => {
|
||||
onChange({ display: 'quadro' });
|
||||
}, [onChange]);
|
||||
|
||||
return {
|
||||
hasDescription,
|
||||
setViewHorizontal,
|
||||
setViewVertical,
|
||||
setViewQuadro,
|
||||
setViewSingle,
|
||||
toggleViewDescription,
|
||||
};
|
||||
};
|
|
@ -1,19 +0,0 @@
|
|||
import { useCallback } from 'react';
|
||||
import { usePersistedState } from '~/utils/hooks/usePersistedState';
|
||||
import { experimentalFeatures } from '~/constants/features';
|
||||
|
||||
enum Layout {
|
||||
Fluid = 'fluid',
|
||||
Default = 'default',
|
||||
}
|
||||
|
||||
export const useFlowLayout = () => {
|
||||
const [layout, setLayout] = usePersistedState('flow_layout', Layout.Default);
|
||||
const isFluid = layout === Layout.Fluid && experimentalFeatures.liquidFlow;
|
||||
|
||||
const toggleLayout = useCallback(() => {
|
||||
setLayout(isFluid ? Layout.Default : Layout.Fluid);
|
||||
}, [setLayout, isFluid]);
|
||||
|
||||
return { isFluid, toggleLayout };
|
||||
};
|
|
@ -1,10 +0,0 @@
|
|||
import { useCallback } from 'react';
|
||||
import { flowGetMore } from '~/redux/flow/actions';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useInfiniteLoader } from '~/utils/hooks/useInfiniteLoader';
|
||||
|
||||
export const useFlowPagination = ({ isLoading }) => {
|
||||
const dispatch = useDispatch();
|
||||
const loadMore = useCallback(() => dispatch(flowGetMore()), [dispatch]);
|
||||
useInfiniteLoader(loadMore, isLoading);
|
||||
};
|
|
@ -1,58 +0,0 @@
|
|||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
export const useCloseOnEscape = (onRequestClose?: () => void, ignore_inputs = false) => {
|
||||
const onEscape = useCallback(
|
||||
event => {
|
||||
if (event.key !== 'Escape' || !onRequestClose) return;
|
||||
if (
|
||||
ignore_inputs &&
|
||||
(event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA')
|
||||
)
|
||||
return;
|
||||
|
||||
onRequestClose();
|
||||
},
|
||||
[ignore_inputs, onRequestClose]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('keyup', onEscape);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keyup', onEscape);
|
||||
};
|
||||
}, [onEscape]);
|
||||
};
|
||||
|
||||
export const useDelayedReady = (setReady: (val: boolean) => void, delay: number = 500) => {
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => setReady(true), delay);
|
||||
|
||||
return () => {
|
||||
if (timer) clearTimeout(timer);
|
||||
};
|
||||
}, [delay, setReady]);
|
||||
};
|
||||
|
||||
/**
|
||||
* useDropZone returns onDrop handler to upload files
|
||||
* @param onUpload -- upload callback
|
||||
* @param allowedTypes -- list of allowed types
|
||||
*/
|
||||
export const useFileDropZone = (onUpload: (file: File[]) => void, allowedTypes?: string[]) => {
|
||||
return useCallback(
|
||||
event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const files: File[] = Array.from((event.dataTransfer?.files as File[]) || []).filter(
|
||||
(file: File) => file?.type && (!allowedTypes || allowedTypes.includes(file.type))
|
||||
);
|
||||
|
||||
if (!files || !files.length) return;
|
||||
|
||||
onUpload(files);
|
||||
},
|
||||
[allowedTypes, onUpload]
|
||||
);
|
||||
};
|
|
@ -1,23 +0,0 @@
|
|||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
export const useArrows = (onNext: () => void, onPrev: () => void, locked) => {
|
||||
const onKeyDown = useCallback(
|
||||
event => {
|
||||
if ((event.target.tagName && ['TEXTAREA', 'INPUT'].includes(event.target.tagName)) || locked)
|
||||
return;
|
||||
|
||||
switch (event.key) {
|
||||
case 'ArrowLeft':
|
||||
return onPrev();
|
||||
case 'ArrowRight':
|
||||
return onNext();
|
||||
}
|
||||
},
|
||||
[onNext, onPrev, locked]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('keydown', onKeyDown);
|
||||
return () => window.removeEventListener('keydown', onKeyDown);
|
||||
}, [onKeyDown]);
|
||||
};
|
|
@ -1,35 +0,0 @@
|
|||
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
|
||||
import {
|
||||
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 = () => {
|
||||
const { is_loading: isLoading, nodes, count } = useShallowSelect(selectLabList);
|
||||
const dispatch = useDispatch();
|
||||
const tags = useShallowSelect(selectLabStatsTags);
|
||||
const heroes = useShallowSelect(selectLabStatsHeroes);
|
||||
const isLoadingStats = useShallowSelect(selectLabStatsLoading);
|
||||
const updates = useShallowSelect(selectLabUpdatesNodes);
|
||||
|
||||
useEffect(() => {
|
||||
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,20 +0,0 @@
|
|||
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
|
||||
import { selectNode } from '~/redux/node/selectors';
|
||||
import { useLoadNode } from '~/utils/hooks/node/useLoadNode';
|
||||
import { useOnNodeSeen } from '~/utils/hooks/node/useOnNodeSeen';
|
||||
|
||||
export const useFullNode = (id: string) => {
|
||||
const {
|
||||
is_loading: isLoading,
|
||||
current: node,
|
||||
comments,
|
||||
comment_count: commentsCount,
|
||||
is_loading_comments: isLoadingComments,
|
||||
lastSeenCurrent,
|
||||
} = useShallowSelect(selectNode);
|
||||
|
||||
useLoadNode(id);
|
||||
// useOnNodeSeen(node);
|
||||
|
||||
return { node, comments, commentsCount, lastSeenCurrent, isLoading, isLoadingComments };
|
||||
};
|
|
@ -1,10 +0,0 @@
|
|||
import { INode } from '~/redux/types';
|
||||
import { useHistory } from 'react-router';
|
||||
import { useCallback } from 'react';
|
||||
import { URLS } from '~/constants/urls';
|
||||
|
||||
// useGotoNode returns fn, that navigates to node
|
||||
export const useGotoNode = (id: INode['id']) => {
|
||||
const history = useHistory();
|
||||
return useCallback(() => history.push(URLS.NODE_URL(id)), [history, id]);
|
||||
};
|
|
@ -1,17 +0,0 @@
|
|||
import { IComment } from '~/redux/types';
|
||||
import { useMemo } from 'react';
|
||||
import { groupCommentsByUser } from '~/utils/fn';
|
||||
|
||||
export const useGrouppedComments = (
|
||||
comments: IComment[],
|
||||
order: 'ASC' | 'DESC',
|
||||
lastSeen?: string
|
||||
) =>
|
||||
useMemo(
|
||||
() =>
|
||||
(order === 'DESC' ? [...comments].reverse() : comments).reduce(
|
||||
groupCommentsByUser(lastSeen),
|
||||
[]
|
||||
),
|
||||
[comments, lastSeen, order]
|
||||
);
|
|
@ -1,12 +0,0 @@
|
|||
import { useEffect } from 'react';
|
||||
import { nodeGotoNode } from '~/redux/node/actions';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
// useLoadNode loads node on id change
|
||||
export const useLoadNode = (id: any) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(nodeGotoNode(parseInt(id, 10), undefined));
|
||||
}, [dispatch, id]);
|
||||
};
|
|
@ -1,54 +0,0 @@
|
|||
import { INode } from '~/redux/types';
|
||||
import { useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { modalShowDialog } from '~/redux/modal/actions';
|
||||
import { NODE_EDITOR_DIALOGS } from '~/constants/dialogs';
|
||||
import { apiLockNode, apiPostNodeHeroic, apiPostNodeLike } from '~/redux/node/api';
|
||||
import { showErrorToast } from '~/utils/errors/showToast';
|
||||
|
||||
export const useNodeActions = (node: INode, update: (node: Partial<INode>) => Promise<unknown>) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onEdit = useCallback(() => {
|
||||
if (!node.type) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(modalShowDialog(NODE_EDITOR_DIALOGS[node.type]));
|
||||
}, [dispatch, node]);
|
||||
|
||||
const onLike = useCallback(async () => {
|
||||
try {
|
||||
const result = await apiPostNodeLike({ id: node.id });
|
||||
const likeCount = node.like_count || 0;
|
||||
|
||||
if (result.is_liked) {
|
||||
await update({ like_count: likeCount + 1 });
|
||||
} else {
|
||||
await update({ like_count: likeCount - 1 });
|
||||
}
|
||||
} catch (error) {
|
||||
showErrorToast(error);
|
||||
}
|
||||
}, [node.id, node.like_count, update]);
|
||||
|
||||
const onStar = useCallback(async () => {
|
||||
try {
|
||||
const result = await apiPostNodeHeroic({ id: node.id });
|
||||
await update({ is_heroic: result.is_heroic });
|
||||
} catch (error) {
|
||||
showErrorToast(error);
|
||||
}
|
||||
}, [node.id, update]);
|
||||
|
||||
const onLock = useCallback(async () => {
|
||||
try {
|
||||
const result = await apiLockNode({ id: node.id, is_locked: !node.deleted_at });
|
||||
await update({ deleted_at: result.deleted_at });
|
||||
} catch (error) {
|
||||
showErrorToast(error);
|
||||
}
|
||||
}, [node.deleted_at, node.id, update]);
|
||||
|
||||
return { onEdit, onLike, onStar, onLock };
|
||||
};
|
|
@ -1,13 +0,0 @@
|
|||
import { INode } from '~/redux/types';
|
||||
import { useMemo } from 'react';
|
||||
import { UPLOAD_TYPES } from '~/redux/uploads/constants';
|
||||
|
||||
export const useNodeAudios = (node: INode) => {
|
||||
if (!node?.files) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return useMemo(() => node.files.filter(file => file && file.type === UPLOAD_TYPES.AUDIO), [
|
||||
node.files,
|
||||
]);
|
||||
};
|
|
@ -1,49 +0,0 @@
|
|||
import { INode } from '~/redux/types';
|
||||
import { createElement, FC, useCallback, useMemo } from 'react';
|
||||
import { isNil, prop } from 'ramda';
|
||||
import {
|
||||
INodeComponentProps,
|
||||
LAB_PREVIEW_LAYOUT,
|
||||
NODE_COMPONENTS,
|
||||
NODE_HEADS,
|
||||
NODE_INLINES,
|
||||
} from '~/redux/node/constants';
|
||||
|
||||
// useNodeBlocks returns head, block and inline blocks of node
|
||||
export const useNodeBlocks = (node: INode, isLoading: boolean) => {
|
||||
const createNodeBlock = useCallback(
|
||||
(block?: FC<INodeComponentProps>, key = 0) =>
|
||||
!isNil(block) &&
|
||||
createElement(block, {
|
||||
node,
|
||||
isLoading,
|
||||
key: `${node.id}-${key}`,
|
||||
}),
|
||||
[node, isLoading]
|
||||
);
|
||||
|
||||
const head = useMemo(
|
||||
() => createNodeBlock(node?.type ? prop(node?.type, NODE_HEADS) : undefined),
|
||||
[node, createNodeBlock]
|
||||
);
|
||||
|
||||
const block = useMemo(
|
||||
() => createNodeBlock(node?.type ? prop(node?.type, NODE_COMPONENTS) : undefined),
|
||||
[node, createNodeBlock]
|
||||
);
|
||||
|
||||
const inline = useMemo(
|
||||
() => createNodeBlock(node?.type ? prop(node?.type, NODE_INLINES) : undefined),
|
||||
[node, createNodeBlock]
|
||||
);
|
||||
|
||||
const lab = useMemo(
|
||||
() =>
|
||||
node?.type && prop(node.type, LAB_PREVIEW_LAYOUT)
|
||||
? prop(node.type, LAB_PREVIEW_LAYOUT).map((comp, i) => createNodeBlock(comp, i))
|
||||
: undefined,
|
||||
[node, createNodeBlock]
|
||||
);
|
||||
|
||||
return { head, block, inline, lab };
|
||||
};
|
|
@ -1,17 +0,0 @@
|
|||
import { useCallback } from 'react';
|
||||
import { nodeLoadMoreComments, nodeLockComment } from '~/redux/node/actions';
|
||||
import { IComment } from '~/redux/types';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
export const useNodeComments = (nodeId: number) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onLoadMoreComments = useCallback(() => dispatch(nodeLoadMoreComments()), [dispatch]);
|
||||
|
||||
const onDelete = useCallback(
|
||||
(id: IComment['id'], locked: boolean) => dispatch(nodeLockComment(id, locked, nodeId)),
|
||||
[dispatch, nodeId]
|
||||
);
|
||||
|
||||
return { onLoadMoreComments, onDelete };
|
||||
};
|
|
@ -1,16 +0,0 @@
|
|||
import { INode } from '~/redux/types';
|
||||
import { useEffect } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { nodeSetCoverImage } from '~/redux/node/actions';
|
||||
|
||||
export const useNodeCoverImage = (node: INode) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(nodeSetCoverImage(node.cover));
|
||||
|
||||
return () => {
|
||||
dispatch(nodeSetCoverImage(undefined));
|
||||
};
|
||||
}, [dispatch, node.cover, node.id]);
|
||||
};
|
|
@ -1,69 +0,0 @@
|
|||
import { INode } from '~/redux/types';
|
||||
import { FileUploader } from '~/utils/hooks/useFileUploader';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { FormikConfig, FormikHelpers, useFormik, useFormikContext } from 'formik';
|
||||
import { object } from 'yup';
|
||||
import { keys } from 'ramda';
|
||||
import { showErrorToast } from '~/utils/errors/showToast';
|
||||
|
||||
const validationSchema = object().shape({});
|
||||
|
||||
const afterSubmit = ({ resetForm, setStatus, setSubmitting, setErrors }: FormikHelpers<INode>) => (
|
||||
e?: string,
|
||||
errors?: Record<string, string>
|
||||
) => {
|
||||
setSubmitting(false);
|
||||
|
||||
if (e) {
|
||||
setStatus(e);
|
||||
showErrorToast(e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (errors && keys(errors).length) {
|
||||
setErrors(errors);
|
||||
return;
|
||||
}
|
||||
|
||||
if (resetForm) {
|
||||
resetForm();
|
||||
}
|
||||
};
|
||||
|
||||
export const useNodeFormFormik = (
|
||||
values: INode,
|
||||
uploader: FileUploader,
|
||||
stopEditing: () => void,
|
||||
sendSaveRequest: (node: INode) => Promise<unknown>
|
||||
) => {
|
||||
const { current: initialValues } = useRef(values);
|
||||
|
||||
const onReset = useCallback(() => {
|
||||
uploader.setFiles([]);
|
||||
|
||||
if (stopEditing) stopEditing();
|
||||
}, [uploader, stopEditing]);
|
||||
|
||||
const onSubmit = useCallback<FormikConfig<INode>['onSubmit']>(
|
||||
async (values, helpers) => {
|
||||
try {
|
||||
await sendSaveRequest({ ...values, files: uploader.files });
|
||||
afterSubmit(helpers)();
|
||||
} catch (error) {
|
||||
afterSubmit(helpers)(error?.response?.data?.error, error?.response?.data?.errors);
|
||||
}
|
||||
},
|
||||
[sendSaveRequest, uploader.files]
|
||||
);
|
||||
|
||||
return useFormik<INode>({
|
||||
initialValues,
|
||||
validationSchema,
|
||||
onSubmit,
|
||||
onReset,
|
||||
initialStatus: '',
|
||||
validateOnChange: true,
|
||||
});
|
||||
};
|
||||
|
||||
export const useNodeFormContext = () => useFormikContext<INode>();
|
|
@ -1,9 +0,0 @@
|
|||
import { INode } from '~/redux/types';
|
||||
import { useMemo } from 'react';
|
||||
import { UPLOAD_TYPES } from '~/redux/uploads/constants';
|
||||
|
||||
export const useNodeImages = (node: INode) => {
|
||||
return useMemo(() => node.files.filter(file => file && file.type === UPLOAD_TYPES.IMAGE), [
|
||||
node.files,
|
||||
]);
|
||||
};
|
|
@ -1,14 +0,0 @@
|
|||
import { useMemo } from 'react';
|
||||
import { canEditNode, canLikeNode, canStarNode } from '~/utils/node';
|
||||
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
|
||||
import { selectUser } from '~/redux/auth/selectors';
|
||||
import { INode } from '~/redux/types';
|
||||
|
||||
export const useNodePermissions = (node?: INode) => {
|
||||
const user = useShallowSelect(selectUser);
|
||||
const edit = useMemo(() => canEditNode(node, user), [node, user]);
|
||||
const like = useMemo(() => canLikeNode(node, user), [node, user]);
|
||||
const star = useMemo(() => canStarNode(node, user), [node, user]);
|
||||
|
||||
return [edit, like, star];
|
||||
};
|
|
@ -1,48 +0,0 @@
|
|||
import { useHistory } from 'react-router';
|
||||
import { useCallback } from 'react';
|
||||
import { ITag } from '~/redux/types';
|
||||
import { URLS } from '~/constants/urls';
|
||||
import { useGetNode } from '~/utils/hooks/data/useGetNode';
|
||||
import { apiDeleteNodeTag, apiPostNodeTags } from '~/redux/node/api';
|
||||
|
||||
export const useNodeTags = (id: number) => {
|
||||
const { update } = useGetNode(id);
|
||||
const history = useHistory();
|
||||
|
||||
const onChange = useCallback(
|
||||
async (tags: string[]) => {
|
||||
try {
|
||||
const result = await apiPostNodeTags({ id, tags });
|
||||
await update({ tags: result.node.tags });
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
}
|
||||
},
|
||||
[id, update]
|
||||
);
|
||||
|
||||
const onClick = useCallback(
|
||||
(tag: Partial<ITag>) => {
|
||||
if (!id || !tag?.title) {
|
||||
return;
|
||||
}
|
||||
|
||||
history.push(URLS.NODE_TAG_URL(id, encodeURIComponent(tag.title)));
|
||||
},
|
||||
[history, id]
|
||||
);
|
||||
|
||||
const onDelete = useCallback(
|
||||
async (tagId: ITag['ID']) => {
|
||||
try {
|
||||
const result = await apiDeleteNodeTag({ id, tagId });
|
||||
await update({ tags: result.tags });
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
},
|
||||
[id, update]
|
||||
);
|
||||
|
||||
return { onDelete, onChange, onClick };
|
||||
};
|
|
@ -1,23 +0,0 @@
|
|||
import { INode } from '~/redux/types';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { labSeenNode } from '~/redux/lab/actions';
|
||||
import { flowSeenNode } from '~/redux/flow/actions';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
// useOnNodeSeen updates node seen status across all needed places
|
||||
export const useOnNodeSeen = (node?: INode) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
if (!node?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove node from updated
|
||||
if (node.is_promoted) {
|
||||
dispatch(flowSeenNode(node.id));
|
||||
} else {
|
||||
dispatch(labSeenNode(node.id));
|
||||
}
|
||||
}, [dispatch, node]);
|
||||
};
|
|
@ -1,17 +0,0 @@
|
|||
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
|
||||
import { selectPlayer } from '~/redux/player/selectors';
|
||||
import { useCallback } from 'react';
|
||||
import { playerPause, playerPlay, playerSeek, playerStop } from '~/redux/player/actions';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
export const usePlayer = () => {
|
||||
const { status, file } = useShallowSelect(selectPlayer);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onPlayerPlay = useCallback(() => dispatch(playerPlay()), [dispatch]);
|
||||
const onPlayerPause = useCallback(() => dispatch(playerPause()), [dispatch]);
|
||||
const onPlayerSeek = useCallback((pos: number) => dispatch(playerSeek(pos)), [dispatch]);
|
||||
const onPlayerStop = useCallback(() => dispatch(playerStop()), [dispatch]);
|
||||
|
||||
return { status, file, onPlayerPlay, onPlayerSeek, onPlayerPause, onPlayerStop };
|
||||
};
|
|
@ -1,24 +0,0 @@
|
|||
import { useCallback } from 'react';
|
||||
import { flowChangeSearch, flowLoadMoreSearch } from '~/redux/flow/actions';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
|
||||
import { selectFlow } from '~/redux/flow/selectors';
|
||||
|
||||
export const useSearch = () => {
|
||||
const dispatch = useDispatch();
|
||||
const { search } = useShallowSelect(selectFlow);
|
||||
|
||||
const onSearchLoadMore = useCallback(() => {
|
||||
if (search.is_loading_more) return;
|
||||
dispatch(flowLoadMoreSearch());
|
||||
}, [search.is_loading_more, dispatch]);
|
||||
|
||||
const onSearchChange = useCallback(
|
||||
(text: string) => {
|
||||
dispatch(flowChangeSearch({ text }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
return { onSearchChange, onSearchLoadMore, search };
|
||||
};
|
|
@ -1,24 +0,0 @@
|
|||
import { useEffect } from 'react';
|
||||
import { history } from '~/redux/store';
|
||||
|
||||
/**
|
||||
* useBlockBackButton - blocks back navigation and calls {callback}
|
||||
* @param callback
|
||||
*/
|
||||
export const useBlockBackButton = (callback?: () => void) => {
|
||||
useEffect(
|
||||
() =>
|
||||
history.listen((newLocation, action) => {
|
||||
if (action !== 'POP') {
|
||||
return;
|
||||
}
|
||||
|
||||
history.goForward();
|
||||
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
}),
|
||||
[callback]
|
||||
);
|
||||
};
|
|
@ -1,33 +0,0 @@
|
|||
/**
|
||||
* Handles blur by detecting clicks outside refs.
|
||||
*/
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useCloseOnEscape } from '~/utils/hooks/index';
|
||||
|
||||
export const useClickOutsideFocus = () => {
|
||||
const ref = useRef<HTMLElement>();
|
||||
const [isActive, setIsActive] = useState(false);
|
||||
|
||||
const activate = useCallback(() => setIsActive(true), [setIsActive]);
|
||||
const deactivate = useCallback(() => setIsActive(false), [setIsActive]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isActive || !ref.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const deactivator = (event: MouseEvent) => {
|
||||
if (!ref.current?.contains(event.target as Node)) {
|
||||
deactivate();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mouseup', deactivator);
|
||||
|
||||
return () => document.removeEventListener('mouseup', deactivator);
|
||||
}, [deactivate, isActive]);
|
||||
|
||||
useCloseOnEscape(deactivate);
|
||||
|
||||
return { ref, isActive, activate, deactivate };
|
||||
};
|
|
@ -1,10 +0,0 @@
|
|||
import { useMemo } from 'react';
|
||||
import { normalizeBrightColor } from '~/utils/color';
|
||||
import { stringToColour } from '~/utils/dom';
|
||||
|
||||
export const useColorFromString = (val?: string, saturation = 3, lightness = 3) => {
|
||||
return useMemo(
|
||||
() => (val && normalizeBrightColor(stringToColour(val), saturation, lightness)) || '',
|
||||
[lightness, saturation, val]
|
||||
);
|
||||
};
|
|
@ -1,22 +0,0 @@
|
|||
import { useMemo } from 'react';
|
||||
import { adjustHue } from 'color2k';
|
||||
import { normalizeBrightColor } from '~/utils/color';
|
||||
import { stringToColour } from '~/utils/dom';
|
||||
|
||||
export const useColorGradientFromString = (
|
||||
val?: string,
|
||||
saturation = 3,
|
||||
lightness = 3,
|
||||
angle = 155
|
||||
) =>
|
||||
useMemo(() => {
|
||||
if (!val) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const color = normalizeBrightColor(stringToColour(val), saturation, lightness);
|
||||
const second = normalizeBrightColor(adjustHue(color, 45), saturation, lightness);
|
||||
const third = normalizeBrightColor(adjustHue(color, 90), saturation, lightness);
|
||||
|
||||
return `linear-gradient(${angle}deg, ${color}, ${second}, ${third})`;
|
||||
}, [angle, lightness, saturation, val]);
|
|
@ -1,79 +0,0 @@
|
|||
import { IComment, INode } from '~/redux/types';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { FormikHelpers, useFormik, useFormikContext } from 'formik';
|
||||
import { array, object, string } from 'yup';
|
||||
import { FileUploader } from '~/utils/hooks/useFileUploader';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { nodePostLocalComment } from '~/redux/node/actions';
|
||||
|
||||
const validationSchema = object().shape({
|
||||
text: string(),
|
||||
files: array(),
|
||||
});
|
||||
|
||||
const onSuccess = ({ resetForm, setStatus, setSubmitting }: FormikHelpers<IComment>) => (
|
||||
e?: string
|
||||
) => {
|
||||
setSubmitting(false);
|
||||
|
||||
if (e) {
|
||||
setStatus(e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (resetForm) {
|
||||
resetForm();
|
||||
}
|
||||
};
|
||||
|
||||
export const useCommentFormFormik = (
|
||||
values: IComment,
|
||||
nodeId: INode['id'],
|
||||
uploader: FileUploader,
|
||||
stopEditing?: () => void
|
||||
) => {
|
||||
const dispatch = useDispatch();
|
||||
const { current: initialValues } = useRef(values);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
(values: IComment, helpers: FormikHelpers<IComment>) => {
|
||||
helpers.setSubmitting(true);
|
||||
dispatch(
|
||||
nodePostLocalComment(
|
||||
nodeId,
|
||||
{
|
||||
...values,
|
||||
files: uploader.files,
|
||||
},
|
||||
onSuccess(helpers)
|
||||
)
|
||||
);
|
||||
},
|
||||
[dispatch, nodeId, uploader.files]
|
||||
);
|
||||
|
||||
const onReset = useCallback(() => {
|
||||
uploader.setFiles([]);
|
||||
|
||||
if (stopEditing) stopEditing();
|
||||
}, [uploader, stopEditing]);
|
||||
|
||||
const formik = useFormik({
|
||||
initialValues,
|
||||
validationSchema,
|
||||
onSubmit,
|
||||
initialStatus: '',
|
||||
onReset,
|
||||
validateOnChange: true,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (formik.status) {
|
||||
formik.setStatus('');
|
||||
}
|
||||
}, [formik, formik.values.text]);
|
||||
|
||||
return formik;
|
||||
};
|
||||
|
||||
export const useCommentFormContext = () => useFormikContext<IComment>();
|
|
@ -1,49 +0,0 @@
|
|||
import React, { createContext, FC, useCallback, useContext, useEffect, useState } from 'react';
|
||||
|
||||
const DragContext = createContext({
|
||||
isDragging: false,
|
||||
setIsDragging: (val: boolean) => {},
|
||||
});
|
||||
|
||||
export const DragDetectorProvider: FC = ({ children }) => {
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
|
||||
return (
|
||||
<DragContext.Provider value={{ isDragging, setIsDragging }}>{children}</DragContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useDragDetector = () => {
|
||||
const { isDragging, setIsDragging } = useContext(DragContext);
|
||||
|
||||
const onStopDragging = useCallback(() => setIsDragging(false), [setIsDragging]);
|
||||
|
||||
useEffect(() => {
|
||||
const addClass = () => setIsDragging(true);
|
||||
|
||||
const removeClass = event => {
|
||||
// Small hack to ignore intersection with child elements
|
||||
if (event.pageX !== 0 && event.pageY !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsDragging(false);
|
||||
};
|
||||
|
||||
document.addEventListener('dragenter', addClass);
|
||||
document.addEventListener('dragover', addClass);
|
||||
document.addEventListener('dragleave', removeClass);
|
||||
document.addEventListener('blur', removeClass);
|
||||
document.addEventListener('drop', onStopDragging);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('dragenter', addClass);
|
||||
document.removeEventListener('dragover', addClass);
|
||||
document.removeEventListener('dragleave', removeClass);
|
||||
document.removeEventListener('blur', removeClass);
|
||||
document.removeEventListener('drop', onStopDragging);
|
||||
};
|
||||
}, [onStopDragging, setIsDragging]);
|
||||
|
||||
return { isDragging, onStopDragging };
|
||||
};
|
|
@ -1,89 +0,0 @@
|
|||
import React, {
|
||||
createContext,
|
||||
FC,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { IFile, IFileWithUUID } from '~/redux/types';
|
||||
import { UPLOAD_SUBJECTS, UPLOAD_TARGETS } from '~/redux/uploads/constants';
|
||||
import { getFileType } from '~/utils/uploader';
|
||||
import uuid from 'uuid4';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { uploadUploadFiles } from '~/redux/uploads/actions';
|
||||
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
|
||||
import { selectUploads } from '~/redux/uploads/selectors';
|
||||
import { path } from 'ramda';
|
||||
import { IUploadStatus } from '~/redux/uploads/reducer';
|
||||
|
||||
export const useFileUploader = (
|
||||
subject: typeof UPLOAD_SUBJECTS[keyof typeof UPLOAD_SUBJECTS],
|
||||
target: typeof UPLOAD_TARGETS[keyof typeof UPLOAD_TARGETS],
|
||||
initialFiles?: IFile[]
|
||||
) => {
|
||||
const dispatch = useDispatch();
|
||||
const { files: uploadedFiles, statuses } = useShallowSelect(selectUploads);
|
||||
|
||||
const [files, setFiles] = useState<IFile[]>(initialFiles || []);
|
||||
const [pendingIDs, setPendingIDs] = useState<string[]>([]);
|
||||
|
||||
const uploadFiles = useCallback(
|
||||
(files: File[]) => {
|
||||
const items: IFileWithUUID[] = files.map(
|
||||
(file: File): IFileWithUUID => ({
|
||||
file,
|
||||
temp_id: uuid(),
|
||||
subject,
|
||||
target,
|
||||
type: getFileType(file),
|
||||
})
|
||||
);
|
||||
|
||||
const temps = items.filter(el => !!el.temp_id).map(file => file.temp_id!);
|
||||
|
||||
setPendingIDs([...pendingIDs, ...temps]);
|
||||
dispatch(uploadUploadFiles(items));
|
||||
},
|
||||
[pendingIDs, setPendingIDs, dispatch, subject, target]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const added = pendingIDs
|
||||
.map(temp_uuid => path([temp_uuid, 'uuid'], statuses) as IUploadStatus['uuid'])
|
||||
.filter(el => el)
|
||||
.map(el => (path([String(el)], uploadedFiles) as IFile) || undefined)
|
||||
.filter(el => !!el! && !files.some(file => file && file.id === el.id));
|
||||
|
||||
const newPending = pendingIDs.filter(
|
||||
temp_id =>
|
||||
statuses[temp_id] &&
|
||||
(!statuses[temp_id].uuid || !added.some(file => file.id === statuses[temp_id].uuid))
|
||||
);
|
||||
|
||||
if (added.length) {
|
||||
setPendingIDs(newPending);
|
||||
setFiles([...files, ...added]);
|
||||
}
|
||||
}, [statuses, files, pendingIDs, setFiles, setPendingIDs, uploadedFiles]);
|
||||
|
||||
const pending = useMemo(() => pendingIDs.map(id => statuses[id]).filter(el => !!el), [
|
||||
statuses,
|
||||
pendingIDs,
|
||||
]);
|
||||
|
||||
const isLoading = pending.length > 0;
|
||||
|
||||
return { uploadFiles, pending, files, setFiles, isUploading: isLoading };
|
||||
};
|
||||
|
||||
export type FileUploader = ReturnType<typeof useFileUploader>;
|
||||
const FileUploaderContext = createContext<FileUploader | undefined>(undefined);
|
||||
|
||||
export const FileUploaderProvider: FC<{ value: FileUploader; children }> = ({
|
||||
value,
|
||||
children,
|
||||
}) => <FileUploaderContext.Provider value={value}>{children}</FileUploaderContext.Provider>;
|
||||
|
||||
export const useFileUploaderContext = () => useContext(FileUploaderContext);
|
|
@ -1,18 +0,0 @@
|
|||
import { useCallback, useState } from 'react';
|
||||
|
||||
export const useFocusEvent = (initialState = false) => {
|
||||
const [focused, setFocused] = useState(initialState);
|
||||
|
||||
const onFocus = useCallback(
|
||||
event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
setFocused(true);
|
||||
},
|
||||
[setFocused]
|
||||
);
|
||||
const onBlur = useCallback(() => setTimeout(() => setFocused(false), 300), [setFocused]);
|
||||
|
||||
return { focused, onBlur, onFocus };
|
||||
};
|
|
@ -1,47 +0,0 @@
|
|||
import { useCallback } from 'react';
|
||||
|
||||
export const useFormatWrapper = (
|
||||
target: HTMLTextAreaElement,
|
||||
onChange: (val: string) => void,
|
||||
prefix = '',
|
||||
suffix = ''
|
||||
) => {
|
||||
return useCallback(
|
||||
event => {
|
||||
event.preventDefault();
|
||||
wrapTextInsideInput(target, prefix, suffix, onChange);
|
||||
},
|
||||
[target, onChange, prefix, suffix]
|
||||
);
|
||||
};
|
||||
|
||||
export const wrapTextInsideInput = (
|
||||
target: HTMLTextAreaElement,
|
||||
prefix: string,
|
||||
suffix: string,
|
||||
onChange: (val: string) => void
|
||||
) => {
|
||||
if (!target) return;
|
||||
|
||||
const start = target.selectionStart;
|
||||
const end = target.selectionEnd;
|
||||
const selection = target.value.substring(start, end);
|
||||
|
||||
const replacement = prefix + selection + suffix;
|
||||
|
||||
onChange(
|
||||
target.value.substring(0, start) +
|
||||
replacement +
|
||||
target.value.substring(end, target.value.length)
|
||||
);
|
||||
|
||||
target.focus();
|
||||
|
||||
setTimeout(() => {
|
||||
if (start === end) {
|
||||
target.selectionEnd = end + prefix.length;
|
||||
} else {
|
||||
target.selectionEnd = end + prefix.length + suffix.length;
|
||||
}
|
||||
}, 0);
|
||||
};
|
|
@ -1,13 +0,0 @@
|
|||
import { useCallback } from 'react';
|
||||
import { IFile } from '~/redux/types';
|
||||
import { modalShowPhotoswipe } from '~/redux/modal/actions';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
export const useImageModal = () => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
return useCallback(
|
||||
(images: IFile[], index: number) => dispatch(modalShowPhotoswipe(images, index)),
|
||||
[dispatch]
|
||||
);
|
||||
};
|
|
@ -1,17 +0,0 @@
|
|||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
export const useInfiniteLoader = (loader: () => void, isLoading?: boolean) => {
|
||||
const onLoadMore = useCallback(() => {
|
||||
const pos = window.scrollY + window.innerHeight - document.body.scrollHeight;
|
||||
|
||||
if (isLoading || pos < -600) return;
|
||||
|
||||
loader();
|
||||
}, [loader, isLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('scroll', onLoadMore);
|
||||
|
||||
return () => window.removeEventListener('scroll', onLoadMore);
|
||||
}, [onLoadMore]);
|
||||
};
|
|
@ -1,27 +0,0 @@
|
|||
import { useCallback, useEffect } from 'react';
|
||||
import { getImageFromPaste } from '~/utils/uploader';
|
||||
|
||||
// useInputPasteUpload attaches event listener to input, that calls onUpload if user pasted any image
|
||||
export const useInputPasteUpload = (
|
||||
input: HTMLTextAreaElement | HTMLInputElement | undefined,
|
||||
onUpload: (files: File[]) => void
|
||||
) => {
|
||||
const onPaste = useCallback(
|
||||
async event => {
|
||||
const image = await getImageFromPaste(event);
|
||||
|
||||
if (!image) return;
|
||||
|
||||
onUpload([image]);
|
||||
},
|
||||
[onUpload]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!input) return;
|
||||
|
||||
input.addEventListener('paste', onPaste);
|
||||
|
||||
return () => input.removeEventListener('paste', onPaste);
|
||||
}, [input, onPaste]);
|
||||
};
|
|
@ -1,19 +0,0 @@
|
|||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
export const usePersistedState = (key: string, initial: string): [string, (val: string) => any] => {
|
||||
const stored = useMemo(() => {
|
||||
try {
|
||||
return localStorage.getItem(`vault_${key}`) || initial;
|
||||
} catch (e) {
|
||||
return initial;
|
||||
}
|
||||
}, [key]);
|
||||
|
||||
const [val, setVal] = useState<string>(stored);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(`vault_${key}`, val);
|
||||
}, [val]);
|
||||
|
||||
return [val, setVal];
|
||||
};
|
|
@ -1,38 +0,0 @@
|
|||
import { useMemo } from 'react';
|
||||
import { Modifier } from 'react-popper';
|
||||
|
||||
const sameWidth = {
|
||||
name: 'sameWidth',
|
||||
enabled: true,
|
||||
phase: 'beforeWrite',
|
||||
requires: ['computeStyles'],
|
||||
fn: ({ state }: { state: any }) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
state.styles.popper.width = `${state.rects.reference.width}px`;
|
||||
},
|
||||
effect: ({ state }: { state: any }) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
state.elements.popper.style.width = `${state.elements.reference.offsetWidth}px`;
|
||||
},
|
||||
};
|
||||
|
||||
export const usePopperModifiers = (offsetX = 0, offsetY = 10, justify?: boolean): Modifier<any>[] =>
|
||||
useMemo(
|
||||
() =>
|
||||
[
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [offsetX, offsetY],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'preventOverflow',
|
||||
options: {
|
||||
padding: 10,
|
||||
},
|
||||
},
|
||||
...(justify ? [sameWidth] : []),
|
||||
] as Modifier<any>[],
|
||||
[offsetX, offsetY, justify]
|
||||
);
|
|
@ -1,8 +0,0 @@
|
|||
import { useEffect } from 'react';
|
||||
|
||||
export const useResizeHandler = (onResize: () => any) => {
|
||||
useEffect(() => {
|
||||
window.addEventListener('resize', onResize);
|
||||
return () => window.removeEventListener('resize', onResize);
|
||||
}, [onResize]);
|
||||
};
|
|
@ -1,19 +0,0 @@
|
|||
import { useEffect } from 'react';
|
||||
import { NEW_COMMENT_CLASSNAME } from '~/constants/comment';
|
||||
|
||||
export const useScrollToTop = (deps?: any[]) => {
|
||||
useEffect(() => {
|
||||
const targetElement = document.querySelector(`.${NEW_COMMENT_CLASSNAME}`);
|
||||
|
||||
if (!targetElement) {
|
||||
window.scrollTo(0, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
const bounds = targetElement.getBoundingClientRect();
|
||||
window.scrollTo({
|
||||
top: bounds.top - 100,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}, deps || []);
|
||||
};
|
|
@ -1,5 +0,0 @@
|
|||
import { shallowEqual, useSelector } from 'react-redux';
|
||||
import { IState } from '~/redux/store';
|
||||
|
||||
export const useShallowSelect = <T extends (state: IState) => any>(selector: T): ReturnType<T> =>
|
||||
useSelector(selector, shallowEqual);
|
|
@ -1,17 +0,0 @@
|
|||
import { ERROR_LITERAL } from '~/constants/errors';
|
||||
import { has } from 'ramda';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const useTranslatedError = (error: string | undefined) => {
|
||||
return useMemo(() => {
|
||||
if (!error) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!has(error, ERROR_LITERAL)) {
|
||||
return error;
|
||||
}
|
||||
|
||||
return ERROR_LITERAL[error];
|
||||
}, [error]);
|
||||
};
|
|
@ -1,21 +0,0 @@
|
|||
import { IUser } from '~/redux/auth/types';
|
||||
import { useRandomPhrase } from '~/constants/phrases';
|
||||
import { differenceInDays, parseISO } from 'date-fns';
|
||||
import { INACTIVE_ACCOUNT_DAYS } from '~/constants/user';
|
||||
|
||||
const today = new Date();
|
||||
|
||||
export const useUserDescription = (user?: Partial<IUser>) => {
|
||||
const randomPhrase = useRandomPhrase('USER_DESCRIPTION');
|
||||
|
||||
if (!user) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const lastSeen = user.last_seen ? parseISO(user.last_seen) : undefined;
|
||||
if (!lastSeen || differenceInDays(today, lastSeen) > INACTIVE_ACCOUNT_DAYS) {
|
||||
return 'Юнит деактивирован';
|
||||
}
|
||||
|
||||
return user.description || randomPhrase;
|
||||
};
|
|
@ -1,4 +0,0 @@
|
|||
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
|
||||
import { selectUser } from '~/redux/auth/selectors';
|
||||
|
||||
export const useUser = () => useShallowSelect(selectUser);
|
|
@ -1,6 +1,6 @@
|
|||
import React, { FC } from 'react';
|
||||
import { LabContextProvider } from '~/utils/context/LabContextProvider';
|
||||
import { useLab } from '~/utils/hooks/lab/useLab';
|
||||
import { useLab } from '~/hooks/lab/useLab';
|
||||
|
||||
interface LabProviderProps {}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import React, { FC, useEffect } from 'react';
|
|||
import { INode, ITag } from '~/redux/types';
|
||||
import { NodeRelatedContextProvider } from '~/utils/context/NodeRelatedContextProvider';
|
||||
import { INodeRelated } from '~/redux/node/types';
|
||||
import { useGetNodeRelated } from '~/utils/hooks/data/useGetNodeRelated';
|
||||
import { useGetNodeRelated } from '~/hooks/node/useGetNodeRelated';
|
||||
|
||||
interface NodeRelatedProviderProps {
|
||||
id: INode['id'];
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue