+
-
-
-
{title}
+
+
+
+
-

-
+
+
+
+ {user.is_user && }
-
-
-
- {user.is_user && }
-
- {node.is_loading_comments ? (
-
- ) : (
-
- )}
-
-
-
-
-
-
-
-
-
-
Господи-боженьки, где это я?
-
-
- Всё впорядке, это — главный штаб Суицидальных Роботов, строителей Убежища.
-
-
Здесь мы сидим и слушаем всё, что вас беспокоит.
-
Все виновные будут наказаны. Невиновные, впрочем, тоже.
-
// Такова жизнь.
-
-
-
-
-
+ {node.is_loading_comments ? (
+
+ ) : (
+
+ )}
-
-
+
+
+
+
+
+
+
+
+
Господи-боженьки, где это я?
+
+
+ Всё впорядке, это — главный штаб Суицидальных Роботов, строителей Убежища.
+
+
Здесь мы сидим и слушаем всё, что вас беспокоит.
+
Все виновные будут наказаны. Невиновные, впрочем, тоже.
+
// Такова жизнь.
+
+
+
+
+
+
+
+
+
-
+
);
};
diff --git a/src/containers/node/NodeLayout/index.tsx b/src/containers/node/NodeLayout/index.tsx
index 00c3ef7c..35dd2cf2 100644
--- a/src/containers/node/NodeLayout/index.tsx
+++ b/src/containers/node/NodeLayout/index.tsx
@@ -1,256 +1,72 @@
-import React, { createElement, FC, memo, useCallback, useEffect, useMemo, useState } from 'react';
-import { RouteComponentProps, useHistory } from 'react-router';
-import { connect } from 'react-redux';
-import { canEditNode, canLikeNode, canStarNode } from '~/utils/node';
+import React, { FC, memo } from 'react';
+import { RouteComponentProps } from 'react-router';
import { selectNode } from '~/redux/node/selectors';
import { Card } from '~/components/containers/Card';
import { NodePanel } from '~/components/node/NodePanel';
-import { Group } from '~/components/containers/Group';
-import { Padder } from '~/components/containers/Padder';
-import { NodeNoComments } from '~/components/node/NodeNoComments';
-import { NodeRelated } from '~/components/node/NodeRelated';
-import { NodeComments } from '~/components/node/NodeComments';
-import { NodeTags } from '~/components/node/NodeTags';
-import {
- INodeComponentProps,
- NODE_COMPONENTS,
- NODE_HEADS,
- NODE_INLINES,
-} from '~/redux/node/constants';
-import { selectUser } from '~/redux/auth/selectors';
-import { path, pick, prop } from 'ramda';
-import { NodeRelatedPlaceholder } from '~/components/node/NodeRelated/placeholder';
-import { NodeDeletedBadge } from '~/components/node/NodeDeletedBadge';
-import { NodeCommentForm } from '~/components/node/NodeCommentForm';
-import { Sticky } from '~/components/containers/Sticky';
import { Footer } from '~/components/main/Footer';
-import { Link } from 'react-router-dom';
import styles from './styles.module.scss';
-import * as NODE_ACTIONS from '~/redux/node/actions';
-import * as MODAL_ACTIONS from '~/redux/modal/actions';
-import { IState } from '~/redux/store';
-import { selectModal } from '~/redux/modal/selectors';
import { SidebarRouter } from '~/containers/main/SidebarRouter';
-import { ITag } from '~/redux/types';
-import { URLS } from '~/constants/urls';
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
+import { Container } from '~/containers/main/Container';
+import { useNodeBlocks } from '~/utils/hooks/node/useNodeBlocks';
+import { NodeBottomBlock } from '~/components/node/NodeBottomBlock';
+import { useNodeCoverImage } from '~/utils/hooks/node/useNodeCoverImage';
+import { useScrollToTop } from '~/utils/hooks/useScrollToTop';
+import { useLoadNode } from '~/utils/hooks/node/useLoadNode';
-const mapStateToProps = (state: IState) => ({
- node: selectNode(state),
- user: selectUser(state),
- modal: pick(['is_shown'])(selectModal(state)),
-});
+type IProps = RouteComponentProps<{ id: string }> & {};
-const mapDispatchToProps = {
- nodeGotoNode: NODE_ACTIONS.nodeGotoNode,
- nodeUpdateTags: NODE_ACTIONS.nodeUpdateTags,
- nodeSetCoverImage: NODE_ACTIONS.nodeSetCoverImage,
- nodeEdit: NODE_ACTIONS.nodeEdit,
- nodeLike: NODE_ACTIONS.nodeLike,
- nodeStar: NODE_ACTIONS.nodeStar,
- nodeLock: NODE_ACTIONS.nodeLock,
- nodeLockComment: NODE_ACTIONS.nodeLockComment,
- nodeEditComment: NODE_ACTIONS.nodeEditComment,
- nodeLoadMoreComments: NODE_ACTIONS.nodeLoadMoreComments,
- modalShowPhotoswipe: MODAL_ACTIONS.modalShowPhotoswipe,
-};
-
-type IProps = ReturnType
&
- typeof mapDispatchToProps &
- RouteComponentProps<{ id: string }> & {};
-
-const NodeLayoutUnconnected: FC = memo(
+const NodeLayout: FC = memo(
({
match: {
params: { id },
},
- modal: { is_shown: is_modal_shown },
- user,
- user: { is_user },
- nodeGotoNode,
- nodeUpdateTags,
- nodeEdit,
- nodeLike,
- nodeStar,
- nodeLock,
- nodeSetCoverImage,
- modalShowPhotoswipe,
}) => {
- const [layout, setLayout] = useState({});
- const history = useHistory();
const {
is_loading,
- is_loading_comments,
- comments = [],
- current: node,
- related,
+ current,
+ comments,
comment_count,
+ is_loading_comments,
+ related,
} = useShallowSelect(selectNode);
- const updateLayout = useCallback(() => setLayout({}), []);
- useEffect(() => {
- if (is_loading) return;
- nodeGotoNode(parseInt(id, 10), null);
- }, [nodeGotoNode, id]);
+ useNodeCoverImage(current);
+ useScrollToTop([id]);
+ useLoadNode(id, is_loading);
- const onTagsChange = useCallback(
- (tags: string[]) => {
- nodeUpdateTags(node.id, tags);
- },
- [node, nodeUpdateTags]
- );
-
- const onTagClick = useCallback(
- (tag: Partial) => {
- if (!node?.id || !tag?.title) {
- return;
- }
-
- history.push(URLS.NODE_TAG_URL(node.id, encodeURIComponent(tag.title)));
- },
- [history, node.id]
- );
-
- const can_edit = useMemo(() => canEditNode(node, user), [node, user]);
- const can_like = useMemo(() => canLikeNode(node, user), [node, user]);
- const can_star = useMemo(() => canStarNode(node, user), [node, user]);
-
- const head = useMemo(() => node?.type && prop(node?.type, NODE_HEADS), [node.type]);
- const block = useMemo(() => node?.type && prop(node?.type, NODE_COMPONENTS), [node.type]);
- const inline = useMemo(() => node?.type && prop(node?.type, NODE_INLINES), [node.type]);
-
- const onEdit = useCallback(() => nodeEdit(node.id), [nodeEdit, node]);
- const onLike = useCallback(() => nodeLike(node.id), [nodeLike, node]);
- const onStar = useCallback(() => nodeStar(node.id), [nodeStar, node]);
- const onLock = useCallback(() => nodeLock(node.id, !node.deleted_at), [nodeStar, node]);
-
- const createNodeBlock = useCallback(
- (block: FC) =>
- block &&
- createElement(block, {
- node,
- is_loading,
- updateLayout,
- layout,
- modalShowPhotoswipe,
- is_modal_shown,
- }),
- [node, is_loading, updateLayout, layout, modalShowPhotoswipe, is_modal_shown]
- );
-
- useEffect(() => {
- if (!node.cover) return;
- nodeSetCoverImage(node.cover);
- return () => nodeSetCoverImage(null);
- }, [nodeSetCoverImage, node.cover]);
-
- useEffect(() => {
- window.scrollTo(0, 0);
- }, [id]);
+ const { head, block } = useNodeBlocks(current, is_loading);
return (
- <>
- {!!head && createNodeBlock(head)}
+
+ {head}
-
- {!!block && createNodeBlock(block)}
+
+
+ {block}
-
+
- {node.deleted_at ? (
-
- ) : (
-
-
-
-
- {inline && {createNodeBlock(inline)}
}
+
- {is_loading || is_loading_comments || (!comments.length && !inline) ? (
-
- ) : (
-
- )}
-
- {is_user && !is_loading && }
-
-
-
-
-
- {!is_loading && (
-
- )}
-
- {is_loading && }
-
- {!is_loading &&
- related &&
- related.albums &&
- !!node?.id &&
- Object.keys(related.albums)
- .filter(album => related.albums[album].length > 0)
- .map(album => (
-
- {album}
-
- }
- items={related.albums[album]}
- key={album}
- />
- ))}
-
- {!is_loading &&
- related &&
- related.similar &&
- related.similar.length > 0 && (
-
- )}
-
-
-
-
-
-
- )}
-
-
-
+
+
+
- >
+
);
}
);
-const NodeLayout = connect(mapStateToProps, mapDispatchToProps)(NodeLayoutUnconnected);
-
-export { NodeLayout, NodeLayoutUnconnected };
+export { NodeLayout };
diff --git a/src/containers/node/NodeLayout/styles.module.scss b/src/containers/node/NodeLayout/styles.module.scss
index f799e1a4..0a824c0d 100644
--- a/src/containers/node/NodeLayout/styles.module.scss
+++ b/src/containers/node/NodeLayout/styles.module.scss
@@ -2,6 +2,7 @@
.content {
align-items: stretch !important;
+
@include vertical_at_tablet;
}
diff --git a/src/redux/boris/api.ts b/src/redux/boris/api.ts
index c1bd5a72..d8cc8867 100644
--- a/src/redux/boris/api.ts
+++ b/src/redux/boris/api.ts
@@ -1,10 +1,20 @@
import git from '~/stats/git.json';
import { API } from '~/constants/api';
-import { api, resultMiddleware, errorMiddleware, cleanResult } from '~/utils/api';
+import { api, cleanResult } from '~/utils/api';
import { IBorisState, IStatBackend } from './reducer';
-import { IResultWithStatus } from '../types';
+import axios from 'axios';
+import { IGetGithubIssuesResult } from '~/redux/boris/types';
export const getBorisGitStats = () => Promise.resolve(git);
export const getBorisBackendStats = () =>
api.get(API.BORIS.GET_BACKEND_STATS).then(cleanResult);
+
+export const getGithubIssues = () => {
+ return axios
+ .get('https://api.github.com/repos/muerwre/vault-frontend/issues', {
+ params: { state: 'all', sort: 'created' },
+ })
+ .then(result => result.data)
+ .catch(() => []);
+};
diff --git a/src/redux/boris/reducer.ts b/src/redux/boris/reducer.ts
index 2032c793..5e182674 100644
--- a/src/redux/boris/reducer.ts
+++ b/src/redux/boris/reducer.ts
@@ -1,5 +1,6 @@
import { createReducer } from '~/utils/reducer';
import { BORIS_HANDLERS } from './handlers';
+import { IGithubIssue } from '~/redux/boris/types';
export type IStatGitRow = {
commit: string;
@@ -31,6 +32,7 @@ export type IStatBackend = {
export type IBorisState = Readonly<{
stats: {
git: Partial[];
+ issues: IGithubIssue[];
backend?: IStatBackend;
is_loading: boolean;
};
@@ -39,6 +41,7 @@ export type IBorisState = Readonly<{
const BORIS_INITIAL_STATE: IBorisState = {
stats: {
git: [],
+ issues: [],
backend: undefined,
is_loading: false,
},
diff --git a/src/redux/boris/sagas.ts b/src/redux/boris/sagas.ts
index a0b1d003..b17e2c16 100644
--- a/src/redux/boris/sagas.ts
+++ b/src/redux/boris/sagas.ts
@@ -1,17 +1,17 @@
-import { takeLatest, put, call } from 'redux-saga/effects';
+import { call, put, takeLatest } from 'redux-saga/effects';
import { BORIS_ACTIONS } from './constants';
import { borisSetStats } from './actions';
-import { getBorisGitStats, getBorisBackendStats } from './api';
+import { getBorisBackendStats, getGithubIssues } from './api';
import { Unwrap } from '../types';
function* loadStats() {
try {
yield put(borisSetStats({ is_loading: true }));
- const git: Unwrap = yield call(getBorisGitStats);
const backend: Unwrap = yield call(getBorisBackendStats);
+ const issues: Unwrap = yield call(getGithubIssues);
- yield put(borisSetStats({ git, backend }));
+ yield put(borisSetStats({ issues, backend }));
} catch (e) {
yield put(borisSetStats({ git: [], backend: undefined }));
} finally {
diff --git a/src/redux/boris/types.ts b/src/redux/boris/types.ts
new file mode 100644
index 00000000..73552b25
--- /dev/null
+++ b/src/redux/boris/types.ts
@@ -0,0 +1,12 @@
+export interface IGithubIssue {
+ id: string;
+ url: string;
+ html_url: string;
+ body: string;
+ title: string;
+ state: 'open' | 'closed';
+ created_at: string;
+ pull_request?: unknown;
+}
+
+export type IGetGithubIssuesResult = IGithubIssue[];
diff --git a/src/redux/node/actions.ts b/src/redux/node/actions.ts
index 05012f98..1faa62df 100644
--- a/src/redux/node/actions.ts
+++ b/src/redux/node/actions.ts
@@ -129,7 +129,7 @@ export const nodeSetEditor = (editor: INode) => ({
editor,
});
-export const nodeSetCoverImage = (current_cover_image: IFile) => ({
+export const nodeSetCoverImage = (current_cover_image?: IFile) => ({
type: NODE_ACTIONS.SET_COVER_IMAGE,
current_cover_image,
});
diff --git a/src/redux/node/constants.ts b/src/redux/node/constants.ts
index dfc3bd3e..8cc79869 100644
--- a/src/redux/node/constants.ts
+++ b/src/redux/node/constants.ts
@@ -1,6 +1,5 @@
-import { FC, ReactElement } from 'react';
+import { FC } from 'react';
import { IComment, INode, ValueOf } from '../types';
-import { NodeImageSlideBlock } from '~/components/node/NodeImageSlideBlock';
import { NodeTextBlock } from '~/components/node/NodeTextBlock';
import { NodeAudioBlock } from '~/components/node/NodeAudioBlock';
import { NodeVideoBlock } from '~/components/node/NodeVideoBlock';
@@ -12,10 +11,10 @@ import { AudioEditor } from '~/components/editors/AudioEditor';
import { EditorImageUploadButton } from '~/components/editors/EditorImageUploadButton';
import { EditorAudioUploadButton } from '~/components/editors/EditorAudioUploadButton';
import { EditorUploadCoverButton } from '~/components/editors/EditorUploadCoverButton';
-import { modalShowPhotoswipe } from '../modal/actions';
import { IEditorComponentProps, NodeEditorProps } from '~/redux/node/types';
import { EditorFiller } from '~/components/editors/EditorFiller';
import { EditorPublicSwitch } from '~/components/editors/EditorPublicSwitch';
+import { NodeImageSwiperBlock } from '~/components/node/NodeImageSwiperBlock';
const prefix = 'NODE.';
export const NODE_ACTIONS = {
@@ -79,17 +78,13 @@ export const NODE_TYPES = {
export type INodeComponentProps = {
node: INode;
- is_loading: boolean;
- is_modal_shown: boolean;
- layout: {};
- updateLayout: () => void;
- modalShowPhotoswipe: typeof modalShowPhotoswipe;
+ isLoading: boolean;
};
export type INodeComponents = Record, FC>;
export const NODE_HEADS: INodeComponents = {
- [NODE_TYPES.IMAGE]: NodeImageSlideBlock,
+ [NODE_TYPES.IMAGE]: NodeImageSwiperBlock,
};
export const NODE_COMPONENTS: INodeComponents = {
diff --git a/src/redux/node/reducer.ts b/src/redux/node/reducer.ts
index 438524cd..b72b1682 100644
--- a/src/redux/node/reducer.ts
+++ b/src/redux/node/reducer.ts
@@ -1,16 +1,14 @@
import { createReducer } from '~/utils/reducer';
-import { INode, IComment, IFile } from '../types';
-import { EMPTY_NODE, EMPTY_COMMENT } from './constants';
+import { IComment, IFile, INode } from '../types';
+import { EMPTY_COMMENT, EMPTY_NODE } from './constants';
import { NODE_HANDLERS } from './handlers';
+import { INodeRelated } from '~/redux/node/types';
export type INodeState = Readonly<{
editor: INode;
current: INode;
comments: IComment[];
- related: {
- albums: Record;
- similar: INode[];
- };
+ related: INodeRelated;
comment_data: Record;
comment_count: number;
current_cover_image?: IFile;
diff --git a/src/redux/node/types.ts b/src/redux/node/types.ts
index 4dba0cc3..33c06955 100644
--- a/src/redux/node/types.ts
+++ b/src/redux/node/types.ts
@@ -89,3 +89,8 @@ export type NodeEditorProps = {
temp: string[];
setTemp: (val: string[]) => void;
};
+
+export type INodeRelated = {
+ albums: Record;
+ similar: INode[];
+};
diff --git a/src/redux/store.ts b/src/redux/store.ts
index eb9c60ff..ea16f709 100644
--- a/src/redux/store.ts
+++ b/src/redux/store.ts
@@ -75,7 +75,9 @@ export const sagaMiddleware = createSagaMiddleware();
export const history = createBrowserHistory();
const composeEnhancers =
- typeof window === 'object' && (window).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
+ typeof window === 'object' &&
+ (window).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ &&
+ process.env.NODE_ENV === 'development'
? (window).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({})
: compose;
diff --git a/src/redux/tag/sagas.ts b/src/redux/tag/sagas.ts
index eb1c3f16..90c5cf1f 100644
--- a/src/redux/tag/sagas.ts
+++ b/src/redux/tag/sagas.ts
@@ -11,7 +11,7 @@ import { apiGetTagSuggestions, apiGetNodesOfTag } from '~/redux/tag/api';
import { Unwrap } from '~/redux/types';
function* loadTagNodes({ tag }: ReturnType) {
- yield put(tagSetNodes({ isLoading: true, list: [] }));
+ yield put(tagSetNodes({ isLoading: true }));
try {
const { list }: ReturnType = yield select(selectTagNodes);
diff --git a/src/styles/common/markdown.module.scss b/src/styles/common/markdown.module.scss
index 9cfcb7e1..82cb22a5 100644
--- a/src/styles/common/markdown.module.scss
+++ b/src/styles/common/markdown.module.scss
@@ -55,6 +55,10 @@ $margin: 1em;
p {
margin-bottom: $margin;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
}
h5, h4, h3, h2, h1 {
diff --git a/src/utils/hooks/node/useLoadNode.ts b/src/utils/hooks/node/useLoadNode.ts
new file mode 100644
index 00000000..6d21392a
--- /dev/null
+++ b/src/utils/hooks/node/useLoadNode.ts
@@ -0,0 +1,13 @@
+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, isLoading: boolean) => {
+ const dispatch = useDispatch();
+
+ useEffect(() => {
+ if (isLoading) return;
+ dispatch(nodeGotoNode(parseInt(id, 10), undefined));
+ }, [dispatch, id]);
+};
diff --git a/src/utils/hooks/node/useNodeActions.ts b/src/utils/hooks/node/useNodeActions.ts
new file mode 100644
index 00000000..cd3bb124
--- /dev/null
+++ b/src/utils/hooks/node/useNodeActions.ts
@@ -0,0 +1,19 @@
+import { INode } from '~/redux/types';
+import { useCallback } from 'react';
+import { useDispatch } from 'react-redux';
+import { nodeEdit, nodeLike, nodeLock, nodeStar } from '~/redux/node/actions';
+
+export const useNodeActions = (node: INode) => {
+ const dispatch = useDispatch();
+
+ const onEdit = useCallback(() => dispatch(nodeEdit(node.id)), [dispatch, nodeEdit, node]);
+ const onLike = useCallback(() => dispatch(nodeLike(node.id)), [dispatch, nodeLike, node]);
+ const onStar = useCallback(() => dispatch(nodeStar(node.id)), [dispatch, nodeStar, node]);
+ const onLock = useCallback(() => dispatch(nodeLock(node.id, !node.deleted_at)), [
+ dispatch,
+ nodeLock,
+ node,
+ ]);
+
+ return { onEdit, onLike, onStar, onLock };
+};
diff --git a/src/utils/hooks/node/useNodeAudios.ts b/src/utils/hooks/node/useNodeAudios.ts
new file mode 100644
index 00000000..7ece487f
--- /dev/null
+++ b/src/utils/hooks/node/useNodeAudios.ts
@@ -0,0 +1,9 @@
+import { INode } from '~/redux/types';
+import { useMemo } from 'react';
+import { UPLOAD_TYPES } from '~/redux/uploads/constants';
+
+export const useNodeAudios = (node: INode) => {
+ return useMemo(() => node.files.filter(file => file && file.type === UPLOAD_TYPES.AUDIO), [
+ node.files,
+ ]);
+};
diff --git a/src/utils/hooks/node/useNodeBlocks.ts b/src/utils/hooks/node/useNodeBlocks.ts
new file mode 100644
index 00000000..823e522c
--- /dev/null
+++ b/src/utils/hooks/node/useNodeBlocks.ts
@@ -0,0 +1,39 @@
+import { INode } from '~/redux/types';
+import { createElement, FC, useCallback, useMemo } from 'react';
+import { isNil, prop } from 'ramda';
+import {
+ INodeComponentProps,
+ 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) =>
+ !isNil(block) &&
+ createElement(block, {
+ node,
+ isLoading,
+ }),
+ [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]
+ );
+
+ return { head, block, inline };
+};
diff --git a/src/utils/hooks/node/useNodeCoverImage.ts b/src/utils/hooks/node/useNodeCoverImage.ts
new file mode 100644
index 00000000..5f4e7b39
--- /dev/null
+++ b/src/utils/hooks/node/useNodeCoverImage.ts
@@ -0,0 +1,16 @@
+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 () => {
+ nodeSetCoverImage(undefined);
+ };
+ }, [dispatch, node.cover, node.id]);
+};
diff --git a/src/utils/hooks/node/useNodeImages.ts b/src/utils/hooks/node/useNodeImages.ts
new file mode 100644
index 00000000..4f6b71d5
--- /dev/null
+++ b/src/utils/hooks/node/useNodeImages.ts
@@ -0,0 +1,9 @@
+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,
+ ]);
+};
diff --git a/src/utils/hooks/node/useNodePermissions.ts b/src/utils/hooks/node/useNodePermissions.ts
new file mode 100644
index 00000000..4f93ba78
--- /dev/null
+++ b/src/utils/hooks/node/useNodePermissions.ts
@@ -0,0 +1,14 @@
+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];
+};
diff --git a/src/utils/hooks/useInputPasteUpload.ts b/src/utils/hooks/useInputPasteUpload.ts
new file mode 100644
index 00000000..dff574a1
--- /dev/null
+++ b/src/utils/hooks/useInputPasteUpload.ts
@@ -0,0 +1,24 @@
+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]);
+ }, []);
+
+ useEffect(() => {
+ if (!input) return;
+
+ input.addEventListener('paste', onPaste);
+
+ return () => input.removeEventListener('paste', onPaste);
+ }, [input, onPaste]);
+};
diff --git a/src/utils/hooks/useScrollToTop.ts b/src/utils/hooks/useScrollToTop.ts
new file mode 100644
index 00000000..e1e03cc4
--- /dev/null
+++ b/src/utils/hooks/useScrollToTop.ts
@@ -0,0 +1,7 @@
+import { useEffect } from 'react';
+
+export const useScrollToTop = (deps?: any[]) => {
+ useEffect(() => {
+ window.scrollTo(0, 0);
+ }, deps || []);
+};
diff --git a/src/utils/hooks/user/userUser.ts b/src/utils/hooks/user/userUser.ts
new file mode 100644
index 00000000..018b5fde
--- /dev/null
+++ b/src/utils/hooks/user/userUser.ts
@@ -0,0 +1,4 @@
+import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
+import { selectUser } from '~/redux/auth/selectors';
+
+export const useUser = () => useShallowSelect(selectUser);
diff --git a/src/utils/node.ts b/src/utils/node.ts
index d8254eda..f00d006c 100644
--- a/src/utils/node.ts
+++ b/src/utils/node.ts
@@ -1,10 +1,8 @@
import { USER_ROLES } from '~/redux/auth/constants';
-import { ICommentGroup, IFile, INode } from '~/redux/types';
+import { ICommentGroup, INode } from '~/redux/types';
import { IUser } from '~/redux/auth/types';
import { path } from 'ramda';
import { NODE_TYPES } from '~/redux/node/constants';
-import { useMemo } from 'react';
-import { UPLOAD_TYPES } from '~/redux/uploads/constants';
export const canEditNode = (node: Partial, user: Partial): boolean =>
path(['role'], user) === USER_ROLES.ADMIN ||
@@ -21,11 +19,3 @@ export const canStarNode = (node: Partial, user: Partial): boolean
node.type === NODE_TYPES.IMAGE &&
path(['role'], user) &&
path(['role'], user) === USER_ROLES.ADMIN;
-
-export const useNodeImages = (node: INode): IFile[] => {
- return useMemo(
- () =>
- (node && node.files && node.files.filter(({ type }) => type === UPLOAD_TYPES.IMAGE)) || [],
- [node.files]
- );
-};
diff --git a/src/utils/uploader.ts b/src/utils/uploader.ts
index c1ad5941..3ade4cf8 100644
--- a/src/utils/uploader.ts
+++ b/src/utils/uploader.ts
@@ -74,3 +74,37 @@ export const fakeUploader = ({
export const getFileType = (file: File): keyof typeof UPLOAD_TYPES | undefined =>
(file.type && Object.keys(FILE_MIMES).find(mime => FILE_MIMES[mime].includes(file.type))) ||
undefined;
+
+// getImageFromPaste returns any images from paste event
+export const getImageFromPaste = (event: ClipboardEvent): Promise => {
+ const items = event.clipboardData?.items;
+
+ return new Promise(resolve => {
+ for (let index in items) {
+ const item = items[index];
+
+ if (item.kind === 'file' && item.type.match(/^image\//)) {
+ const blob = item.getAsFile();
+ const reader = new FileReader();
+ const type = item.type;
+
+ reader.onload = function(e) {
+ if (!e.target?.result) {
+ return;
+ }
+
+ resolve(
+ new File([e.target?.result], 'paste.png', {
+ type,
+ lastModified: new Date().getTime(),
+ })
+ );
+ };
+
+ reader.readAsArrayBuffer(blob);
+ }
+ }
+
+ // resolve(undefined);
+ });
+};
diff --git a/yarn.lock b/yarn.lock
index 67104881..a3992a4e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1109,6 +1109,13 @@
dependencies:
regenerator-runtime "^0.13.4"
+"@babel/runtime@^7.1.5":
+ version "7.13.10"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.10.tgz#47d42a57b6095f4468da440388fdbad8bebf0d7d"
+ integrity sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw==
+ dependencies:
+ regenerator-runtime "^0.13.4"
+
"@babel/runtime@^7.10.5":
version "7.13.7"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.7.tgz#d494e39d198ee9ca04f4dcb76d25d9d7a1dc961a"
@@ -1761,6 +1768,11 @@
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==
+"@types/swiper@^5.4.2":
+ version "5.4.2"
+ resolved "https://registry.yarnpkg.com/@types/swiper/-/swiper-5.4.2.tgz#ff206cf5aea787f580b5dd9b466b4bcb8e0442f3"
+ integrity sha512-/7MaVDZ8ltMCZb6yfg1HWBRjwFjy9ytKpuPSZfNTrxpkQCaGQZdpceDSqKaSfGmJcVF0NcBFRsGTStyytV7grw==
+
"@types/testing-library__jest-dom@^5.9.1":
version "5.9.5"
resolved "https://registry.yarnpkg.com/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.9.5.tgz#5bf25c91ad2d7b38f264b12275e5c92a66d849b0"
@@ -2632,10 +2644,10 @@ bluebird@^3.5.5:
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
-bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.4.0:
- version "4.11.9"
- resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828"
- integrity sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==
+bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.9:
+ version "4.12.0"
+ resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88"
+ integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==
bn.js@^5.0.0, bn.js@^5.1.1:
version "5.1.3"
@@ -2711,7 +2723,7 @@ braces@~3.0.2:
dependencies:
fill-range "^7.0.1"
-brorand@^1.0.1:
+brorand@^1.0.1, brorand@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=
@@ -4084,6 +4096,13 @@ dom-serializer@0:
domelementtype "^2.0.1"
entities "^2.0.0"
+dom7@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/dom7/-/dom7-3.0.0.tgz#b861ce5d67a6becd7aaa3ad02942ff14b1240331"
+ integrity sha512-oNlcUdHsC4zb7Msx7JN3K0Nro1dzJ48knvBOnDPKJ2GV9wl1i5vydJZUSyOfrkKFDZEud/jBsTk92S/VGSAe/g==
+ dependencies:
+ ssr-window "^3.0.0-alpha.1"
+
domain-browser@^1.1.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
@@ -4188,17 +4207,17 @@ electron-to-chromium@^1.3.378, electron-to-chromium@^1.3.591:
integrity sha512-ctRyXD9y0mZu8pgeNwBUhLP3Guyr5YuqkfLKYmpTwYx7o9JtCEJme9JVX4xBXPr5ZNvr/iBXUvHLFEVJQThATg==
elliptic@^6.5.3:
- version "6.5.3"
- resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6"
- integrity sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==
+ version "6.5.4"
+ resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb"
+ integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==
dependencies:
- bn.js "^4.4.0"
- brorand "^1.0.1"
+ bn.js "^4.11.9"
+ brorand "^1.1.0"
hash.js "^1.0.0"
- hmac-drbg "^1.0.0"
- inherits "^2.0.1"
- minimalistic-assert "^1.0.0"
- minimalistic-crypto-utils "^1.0.0"
+ hmac-drbg "^1.0.1"
+ inherits "^2.0.4"
+ minimalistic-assert "^1.0.1"
+ minimalistic-crypto-utils "^1.0.1"
emoji-regex@^7.0.1, emoji-regex@^7.0.2:
version "7.0.3"
@@ -5457,7 +5476,7 @@ history@^4.9.0:
tiny-warning "^1.0.0"
value-equal "^1.0.1"
-hmac-drbg@^1.0.0:
+hmac-drbg@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=
@@ -5772,9 +5791,9 @@ inherits@2.0.3:
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
ini@^1.3.5:
- version "1.3.5"
- resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
- integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
+ version "1.3.8"
+ resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
+ integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
inquirer@7.0.4:
version "7.0.4"
@@ -7354,7 +7373,7 @@ minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1:
resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==
-minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1:
+minimalistic-crypto-utils@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=
@@ -9442,6 +9461,14 @@ react-sortable-hoc@^1.11:
invariant "^2.2.4"
prop-types "^15.5.7"
+react-sticky-box@^0.9.3:
+ version "0.9.3"
+ resolved "https://registry.yarnpkg.com/react-sticky-box/-/react-sticky-box-0.9.3.tgz#8450d4cef8e4fdd7b0351520365bc98c97da11af"
+ integrity sha512-Y/qO7vTqAvXuRR6G6ZCW4fX2Bz0GZRwiiLTVeZN5CVz9wzs37ev0Xj3KSKF/PzF0jifwATivI4t24qXG8rSz4Q==
+ dependencies:
+ "@babel/runtime" "^7.1.5"
+ resize-observer-polyfill "^1.5.1"
+
react@^17.0.1:
version "17.0.1"
resolved "https://registry.yarnpkg.com/react/-/react-17.0.1.tgz#6e0600416bd57574e3f86d92edba3d9008726127"
@@ -9768,6 +9795,11 @@ requires-port@^1.0.0:
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
+resize-observer-polyfill@^1.5.1:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
+ integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
+
resize-sensor@^0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/resize-sensor/-/resize-sensor-0.0.6.tgz#75147dcb273de6832760e461d2e28de6dcf88c45"
@@ -10449,6 +10481,11 @@ sshpk@^1.7.0:
safer-buffer "^2.0.2"
tweetnacl "~0.14.0"
+ssr-window@^3.0.0, ssr-window@^3.0.0-alpha.1:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/ssr-window/-/ssr-window-3.0.0.tgz#fd5b82801638943e0cc704c4691801435af7ac37"
+ integrity sha512-q+8UfWDg9Itrg0yWK7oe5p/XRCJpJF9OBtXfOPgSJl+u3Xd5KI328RUEvUqSMVM9CiQUEf1QdBzJMkYGErj9QA==
+
ssri@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.1.tgz#2a3c41b28dd45b62b63676ecb74001265ae9edd8"
@@ -10789,6 +10826,14 @@ svgo@^1.0.0, svgo@^1.2.2:
unquote "~1.1.1"
util.promisify "~1.0.0"
+swiper@^6.5.0:
+ version "6.5.0"
+ resolved "https://registry.yarnpkg.com/swiper/-/swiper-6.5.0.tgz#4ca2243b44fccef47ee28199377666607d8c5141"
+ integrity sha512-cSx1SpfgrHlgwku++3Ce3cjPBpXgB7P+bGik5S3+F+j6ID0NUeV6qtmedFdr3C8jXR/W+TJPVNIT9fH/cwVAiA==
+ dependencies:
+ dom7 "^3.0.0"
+ ssr-window "^3.0.0"
+
symbol-observable@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"