1
0
Fork 0
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:
Fedor Katurov 2022-01-02 18:17:09 +07:00
parent efa3ba902d
commit f76a5a4798
106 changed files with 122 additions and 144 deletions

View file

@ -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 };
};

View file

@ -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]
);
};

View file

@ -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 };
};

View file

@ -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 };
};

View file

@ -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]
);
};

View file

@ -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 };
};

View file

@ -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,
};
};

View file

@ -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 };
};

View file

@ -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);
};

View file

@ -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]
);
};

View file

@ -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]);
};

View file

@ -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 };
};

View file

@ -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]);
};

View file

@ -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 };
};

View file

@ -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]);
};

View file

@ -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]
);

View file

@ -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]);
};

View file

@ -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 };
};

View file

@ -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,
]);
};

View file

@ -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 };
};

View file

@ -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 };
};

View file

@ -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]);
};

View file

@ -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>();

View file

@ -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,
]);
};

View file

@ -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];
};

View file

@ -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 };
};

View file

@ -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]);
};

View file

@ -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 };
};

View file

@ -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 };
};

View file

@ -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]
);
};

View file

@ -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 };
};

View file

@ -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]
);
};

View file

@ -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]);

View file

@ -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>();

View file

@ -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 };
};

View file

@ -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);

View file

@ -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 };
};

View file

@ -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);
};

View file

@ -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]
);
};

View file

@ -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]);
};

View file

@ -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]);
};

View file

@ -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];
};

View file

@ -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]
);

View file

@ -1,8 +0,0 @@
import { useEffect } from 'react';
export const useResizeHandler = (onResize: () => any) => {
useEffect(() => {
window.addEventListener('resize', onResize);
return () => window.removeEventListener('resize', onResize);
}, [onResize]);
};

View file

@ -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 || []);
};

View file

@ -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);

View file

@ -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]);
};

View file

@ -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;
};

View file

@ -1,4 +0,0 @@
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
import { selectUser } from '~/redux/auth/selectors';
export const useUser = () => useShallowSelect(selectUser);

View file

@ -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 {}

View file

@ -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'];