mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-25 12:56:41 +07:00
Merge branch 'master' into 23-labs
# Conflicts: # src/redux/node/constants.ts
This commit is contained in:
commit
8316b46efe
56 changed files with 1085 additions and 527 deletions
|
@ -29,11 +29,13 @@
|
||||||
"react-router-dom": "^5.1.2",
|
"react-router-dom": "^5.1.2",
|
||||||
"react-scripts": "3.4.4",
|
"react-scripts": "3.4.4",
|
||||||
"react-sortable-hoc": "^1.11",
|
"react-sortable-hoc": "^1.11",
|
||||||
|
"react-sticky-box": "^0.9.3",
|
||||||
"redux": "^4.0.1",
|
"redux": "^4.0.1",
|
||||||
"redux-persist": "^5.10.0",
|
"redux-persist": "^5.10.0",
|
||||||
"redux-saga": "^1.1.1",
|
"redux-saga": "^1.1.1",
|
||||||
"resize-sensor": "^0.0.6",
|
"resize-sensor": "^0.0.6",
|
||||||
"sticky-sidebar": "^3.3.1",
|
"sticky-sidebar": "^3.3.1",
|
||||||
|
"swiper": "^6.5.0",
|
||||||
"throttle-debounce": "^2.1.0",
|
"throttle-debounce": "^2.1.0",
|
||||||
"typescript": "^4.0.5",
|
"typescript": "^4.0.5",
|
||||||
"uuid4": "^1.1.4",
|
"uuid4": "^1.1.4",
|
||||||
|
@ -70,6 +72,7 @@
|
||||||
"@types/node": "^11.13.22",
|
"@types/node": "^11.13.22",
|
||||||
"@types/ramda": "^0.26.33",
|
"@types/ramda": "^0.26.33",
|
||||||
"@types/react-redux": "^7.1.11",
|
"@types/react-redux": "^7.1.11",
|
||||||
|
"@types/swiper": "^5.4.2",
|
||||||
"@types/yup": "^0.29.11",
|
"@types/yup": "^0.29.11",
|
||||||
"craco-alias": "^2.1.1",
|
"craco-alias": "^2.1.1",
|
||||||
"craco-fast-refresh": "^1.0.2",
|
"craco-fast-refresh": "^1.0.2",
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
<meta name="theme-color" content="#222222">
|
<meta name="theme-color" content="#222222">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, user-scalable=0" />
|
||||||
<link href="https://fonts.googleapis.com/css?family=Montserrat:400,500,600,700,800&display=swap&subset=cyrillic"
|
<link href="https://fonts.googleapis.com/css?family=Montserrat:400,500,600,700,800&display=swap&subset=cyrillic"
|
||||||
rel="stylesheet" />
|
rel="stylesheet" />
|
||||||
<title>Убежище</title>
|
<title>Убежище</title>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { FC } from 'react';
|
import React, { FC, useMemo } from 'react';
|
||||||
import { IBorisState } from '~/redux/boris/reducer';
|
import { IBorisState } from '~/redux/boris/reducer';
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
import { Placeholder } from '~/components/placeholders/Placeholder';
|
import { Placeholder } from '~/components/placeholders/Placeholder';
|
||||||
|
@ -9,7 +9,17 @@ interface IProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const BorisStatsGit: FC<IProps> = ({ stats }) => {
|
const BorisStatsGit: FC<IProps> = ({ stats }) => {
|
||||||
if (!stats.git.length) return null;
|
if (!stats.issues.length) return null;
|
||||||
|
|
||||||
|
const open = useMemo(
|
||||||
|
() => stats.issues.filter(el => !el.pull_request && el.state === 'open').slice(0, 5),
|
||||||
|
[stats.issues]
|
||||||
|
);
|
||||||
|
|
||||||
|
const closed = useMemo(
|
||||||
|
() => stats.issues.filter(el => !el.pull_request && el.state === 'closed').slice(0, 5),
|
||||||
|
[stats.issues]
|
||||||
|
);
|
||||||
|
|
||||||
if (stats.is_loading) {
|
if (stats.is_loading) {
|
||||||
return (
|
return (
|
||||||
|
@ -35,11 +45,12 @@ const BorisStatsGit: FC<IProps> = ({ stats }) => {
|
||||||
<img src="https://jenkins.vault48.org/api/badges/muerwre/vault-golang/status.svg" />
|
<img src="https://jenkins.vault48.org/api/badges/muerwre/vault-golang/status.svg" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{stats.git
|
{open.map(data => (
|
||||||
.filter(data => data.commit && data.timestamp && data.subject)
|
<BorisStatsGitCard data={data} key={data.id} />
|
||||||
.slice(0, 5)
|
))}
|
||||||
.map(data => (
|
|
||||||
<BorisStatsGitCard data={data} key={data.commit} />
|
{closed.map(data => (
|
||||||
|
<BorisStatsGitCard data={data} key={data.id} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,22 +1,33 @@
|
||||||
import React, { FC } from 'react';
|
import React, { FC, useMemo } from 'react';
|
||||||
import { IStatGitRow } from '~/redux/boris/reducer';
|
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
import { getPrettyDate } from '~/utils/dom';
|
import { getPrettyDate } from '~/utils/dom';
|
||||||
|
import { IGithubIssue } from '~/redux/boris/types';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
data: Partial<IStatGitRow>;
|
data: IGithubIssue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BorisStatsGitCard: FC<IProps> = ({ data: { timestamp, subject } }) => {
|
const stateLabels: Record<IGithubIssue['state'], string> = {
|
||||||
if (!subject || !timestamp) return null;
|
open: 'Ожидает',
|
||||||
|
closed: 'Сделано',
|
||||||
|
};
|
||||||
|
|
||||||
|
const BorisStatsGitCard: FC<IProps> = ({ data: { created_at, title, html_url, state } }) => {
|
||||||
|
if (!title || !created_at) return null;
|
||||||
|
|
||||||
|
const date = useMemo(() => getPrettyDate(created_at), [created_at]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrap}>
|
<div className={styles.wrap}>
|
||||||
<div className={styles.time}>
|
<div className={styles.time}>
|
||||||
{getPrettyDate(new Date(parseInt(`${timestamp}000`)).toISOString())}
|
<span className={classNames(styles.icon, styles[state])}>{stateLabels[state]}</span>
|
||||||
|
{date}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.subject}>{subject}</div>
|
<a className={styles.subject} href={html_url} target="_blank">
|
||||||
|
{title}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -12,10 +12,28 @@
|
||||||
.time {
|
.time {
|
||||||
font: $font_12_regular;
|
font: $font_12_regular;
|
||||||
line-height: 17px;
|
line-height: 17px;
|
||||||
opacity: 0.3;
|
color: transparentize(white, 0.7)
|
||||||
}
|
}
|
||||||
|
|
||||||
.subject {
|
.subject {
|
||||||
font: $font_14_regular;
|
font: $font_14_regular;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
font: $font_10_semibold;
|
||||||
|
margin-right: 5px;
|
||||||
|
border-radius: 2px;
|
||||||
|
padding: 2px 0;
|
||||||
|
text-transform: uppercase;
|
||||||
|
|
||||||
|
&.open {
|
||||||
|
color: $red;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.closed {
|
||||||
|
color: $green;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,8 @@ const CommentEmbedBlockUnconnected: FC<Props> = memo(
|
||||||
return (match && match[1]) || '';
|
return (match && match[1]) || '';
|
||||||
}, [block.content]);
|
}, [block.content]);
|
||||||
|
|
||||||
|
const url = useMemo(() => `https://youtube.com/watch?v=${id}`, [id]);
|
||||||
|
|
||||||
const preview = useMemo(() => getYoutubeThumb(block.content), [block.content]);
|
const preview = useMemo(() => getYoutubeThumb(block.content), [block.content]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -47,7 +49,7 @@ const CommentEmbedBlockUnconnected: FC<Props> = memo(
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.embed}>
|
<div className={styles.embed}>
|
||||||
<a href={id[0]} target="_blank" />
|
<a href={url} target="_blank" />
|
||||||
|
|
||||||
<div className={styles.preview}>
|
<div className={styles.preview}>
|
||||||
<div style={{ backgroundImage: `url("${preview}")` }}>
|
<div style={{ backgroundImage: `url("${preview}")` }}>
|
||||||
|
|
|
@ -14,7 +14,7 @@ import { EMPTY_COMMENT } from '~/redux/node/constants';
|
||||||
import { CommentFormDropzone } from '~/components/comment/CommentFormDropzone';
|
import { CommentFormDropzone } from '~/components/comment/CommentFormDropzone';
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
import { ERROR_LITERAL } from '~/constants/errors';
|
import { ERROR_LITERAL } from '~/constants/errors';
|
||||||
import { Group } from '~/components/containers/Group';
|
import { useInputPasteUpload } from '~/utils/hooks/useInputPasteUpload';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
comment?: IComment;
|
comment?: IComment;
|
||||||
|
@ -47,6 +47,7 @@ const CommentForm: FC<IProps> = ({ comment, nodeId, onCancelEdit }) => {
|
||||||
}, [formik]);
|
}, [formik]);
|
||||||
|
|
||||||
const error = formik.status || formik.errors.text;
|
const error = formik.status || formik.errors.text;
|
||||||
|
useInputPasteUpload(textarea, uploader.uploadFiles);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CommentFormDropzone onUpload={uploader.uploadFiles}>
|
<CommentFormDropzone onUpload={uploader.uploadFiles}>
|
||||||
|
@ -65,16 +66,21 @@ const CommentForm: FC<IProps> = ({ comment, nodeId, onCancelEdit }) => {
|
||||||
|
|
||||||
<CommentFormAttaches />
|
<CommentFormAttaches />
|
||||||
|
|
||||||
<Group horizontal className={styles.buttons}>
|
<div className={styles.buttons}>
|
||||||
|
<div className={styles.buttons_attach}>
|
||||||
<CommentFormAttachButtons onUpload={uploader.uploadFiles} />
|
<CommentFormAttachButtons onUpload={uploader.uploadFiles} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.buttons_format}>
|
||||||
{!!textarea && (
|
{!!textarea && (
|
||||||
<CommentFormFormatButtons
|
<CommentFormFormatButtons
|
||||||
element={textarea}
|
element={textarea}
|
||||||
handler={formik.handleChange('text')}
|
handler={formik.handleChange('text')}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.buttons_submit}>
|
||||||
{isLoading && <LoaderCircle size={20} />}
|
{isLoading && <LoaderCircle size={20} />}
|
||||||
|
|
||||||
{isEditing && (
|
{isEditing && (
|
||||||
|
@ -92,7 +98,8 @@ const CommentForm: FC<IProps> = ({ comment, nodeId, onCancelEdit }) => {
|
||||||
>
|
>
|
||||||
{!isEditing ? 'Сказать' : 'Сохранить'}
|
{!isEditing ? 'Сказать' : 'Сохранить'}
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</div>
|
||||||
|
</div>
|
||||||
</FileUploaderProvider>
|
</FileUploaderProvider>
|
||||||
</FormikProvider>
|
</FormikProvider>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -21,13 +21,42 @@
|
||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-direction: row;
|
|
||||||
background: transparentize(black, 0.8);
|
background: transparentize(black, 0.8);
|
||||||
padding: $gap / 2;
|
padding: $gap / 2;
|
||||||
border-radius: 0 0 $radius $radius;
|
border-radius: 0 0 $radius $radius;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
column-gap: $gap;
|
||||||
|
grid-template-columns: auto 1fr auto;
|
||||||
|
grid-template-rows: 1fr;
|
||||||
|
grid-template-areas: "attach format submit";
|
||||||
|
|
||||||
|
@media(max-width: 470px) {
|
||||||
|
padding: $gap;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
grid-template-rows: 1fr 1fr;
|
||||||
|
grid-template-areas:
|
||||||
|
"attach format"
|
||||||
|
"submit submit";
|
||||||
|
row-gap: $gap;
|
||||||
|
}
|
||||||
|
|
||||||
|
&_attach {
|
||||||
|
grid-area: attach;
|
||||||
|
}
|
||||||
|
|
||||||
|
&_format {
|
||||||
|
grid-area: format;
|
||||||
|
}
|
||||||
|
|
||||||
|
&_submit {
|
||||||
|
grid-area: submit;
|
||||||
|
display: grid;
|
||||||
|
grid-auto-flow: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: flex-end;
|
||||||
|
column-gap: $gap / 2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.uploads {
|
.uploads {
|
||||||
|
|
|
@ -2,11 +2,8 @@
|
||||||
|
|
||||||
.wrap {
|
.wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: nowrap;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
@media(max-width: 480px) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,6 @@
|
||||||
@include tablet {
|
@include tablet {
|
||||||
:global(.comment-author) {
|
:global(.comment-author) {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
color: red;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,32 +1,34 @@
|
||||||
import React, { DetailsHTMLAttributes, FC, useEffect, useRef } from 'react';
|
import React, { DetailsHTMLAttributes, FC, useEffect, useRef } from 'react';
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
import StickySidebar from 'sticky-sidebar';
|
|
||||||
import classnames from 'classnames';
|
|
||||||
import ResizeSensor from 'resize-sensor';
|
import ResizeSensor from 'resize-sensor';
|
||||||
|
(window as any).ResizeSensor = ResizeSensor;
|
||||||
|
|
||||||
|
import StickySidebar from 'sticky-sidebar';
|
||||||
|
(window as any).StickySidebar = StickySidebar;
|
||||||
|
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
interface IProps extends DetailsHTMLAttributes<HTMLDivElement> {}
|
interface IProps extends DetailsHTMLAttributes<HTMLDivElement> {}
|
||||||
|
|
||||||
(window as any).StickySidebar = StickySidebar;
|
|
||||||
(window as any).ResizeSensor = ResizeSensor;
|
|
||||||
|
|
||||||
const Sticky: FC<IProps> = ({ children }) => {
|
const Sticky: FC<IProps> = ({ children }) => {
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
let sb;
|
const sb = useRef<StickySidebar>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ref.current) return;
|
if (!ref.current) return;
|
||||||
|
|
||||||
sb = new StickySidebar(ref.current, {
|
sb.current = new StickySidebar(ref.current, {
|
||||||
resizeSensor: true,
|
resizeSensor: true,
|
||||||
topSpacing: 72,
|
topSpacing: 72,
|
||||||
bottomSpacing: 10,
|
bottomSpacing: 10,
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => sb.destroy();
|
return () => sb.current?.destroy();
|
||||||
}, [ref.current, children]);
|
}, [ref.current, sb.current, children]);
|
||||||
|
|
||||||
if (sb) {
|
if (sb) {
|
||||||
sb.updateSticky();
|
sb.current?.updateSticky();
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -8,6 +8,8 @@ import { selectUploads } from '~/redux/uploads/selectors';
|
||||||
import * as UPLOAD_ACTIONS from '~/redux/uploads/actions';
|
import * as UPLOAD_ACTIONS from '~/redux/uploads/actions';
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
import { NodeEditorProps } from '~/redux/node/types';
|
import { NodeEditorProps } from '~/redux/node/types';
|
||||||
|
import { useNodeImages } from '~/utils/hooks/node/useNodeImages';
|
||||||
|
import { useNodeAudios } from '~/utils/hooks/node/useNodeAudios';
|
||||||
|
|
||||||
const mapStateToProps = selectUploads;
|
const mapStateToProps = selectUploads;
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
|
@ -17,10 +19,7 @@ const mapDispatchToProps = {
|
||||||
type IProps = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & NodeEditorProps;
|
type IProps = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & NodeEditorProps;
|
||||||
|
|
||||||
const AudioEditorUnconnected: FC<IProps> = ({ data, setData, temp, statuses }) => {
|
const AudioEditorUnconnected: FC<IProps> = ({ data, setData, temp, statuses }) => {
|
||||||
const images = useMemo(
|
const images = useNodeImages(data);
|
||||||
() => data.files.filter(file => file && file.type === UPLOAD_TYPES.IMAGE),
|
|
||||||
[data.files]
|
|
||||||
);
|
|
||||||
|
|
||||||
const pending_images = useMemo(
|
const pending_images = useMemo(
|
||||||
() =>
|
() =>
|
||||||
|
@ -30,10 +29,7 @@ const AudioEditorUnconnected: FC<IProps> = ({ data, setData, temp, statuses }) =
|
||||||
[temp, statuses]
|
[temp, statuses]
|
||||||
);
|
);
|
||||||
|
|
||||||
const audios = useMemo(
|
const audios = useNodeAudios(data);
|
||||||
() => data.files.filter(file => file && file.type === UPLOAD_TYPES.AUDIO),
|
|
||||||
[data.files]
|
|
||||||
);
|
|
||||||
|
|
||||||
const pending_audios = useMemo(
|
const pending_audios = useMemo(
|
||||||
() =>
|
() =>
|
||||||
|
|
|
@ -11,7 +11,6 @@ const FlowRecent: FC<IProps> = ({ recent, updated }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{updated && updated.map(node => <FlowRecentItem node={node} key={node.id} has_new />)}
|
{updated && updated.map(node => <FlowRecentItem node={node} key={node.id} has_new />)}
|
||||||
|
|
||||||
{recent && recent.map(node => <FlowRecentItem node={node} key={node.id} />)}
|
{recent && recent.map(node => <FlowRecentItem node={node} key={node.id} />)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -44,7 +44,6 @@
|
||||||
@include outer_shadow();
|
@include outer_shadow();
|
||||||
|
|
||||||
input {
|
input {
|
||||||
color: red;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|
75
src/components/node/NodeBottomBlock/index.tsx
Normal file
75
src/components/node/NodeBottomBlock/index.tsx
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import { NodeDeletedBadge } from '~/components/node/NodeDeletedBadge';
|
||||||
|
import { Group } from '~/components/containers/Group';
|
||||||
|
import { Padder } from '~/components/containers/Padder';
|
||||||
|
import { NodeCommentsBlock } from '~/components/node/NodeCommentsBlock';
|
||||||
|
import { NodeCommentForm } from '~/components/node/NodeCommentForm';
|
||||||
|
import { NodeRelatedBlock } from '~/components/node/NodeRelatedBlock';
|
||||||
|
import { useNodeBlocks } from '~/utils/hooks/node/useNodeBlocks';
|
||||||
|
import { IComment, INode } from '~/redux/types';
|
||||||
|
import { useUser } from '~/utils/hooks/user/userUser';
|
||||||
|
import { NodeTagsBlock } from '~/components/node/NodeTagsBlock';
|
||||||
|
import { INodeRelated } from '~/redux/node/types';
|
||||||
|
import StickyBox from 'react-sticky-box/dist/esnext';
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
node: INode;
|
||||||
|
isLoading: boolean;
|
||||||
|
commentsOrder: 'ASC' | 'DESC';
|
||||||
|
comments: IComment[];
|
||||||
|
commentsCount: number;
|
||||||
|
isLoadingComments: boolean;
|
||||||
|
related: INodeRelated;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NodeBottomBlock: FC<IProps> = ({
|
||||||
|
node,
|
||||||
|
isLoading,
|
||||||
|
isLoadingComments,
|
||||||
|
comments,
|
||||||
|
commentsCount,
|
||||||
|
commentsOrder,
|
||||||
|
related,
|
||||||
|
}) => {
|
||||||
|
const { inline } = useNodeBlocks(node, isLoading);
|
||||||
|
const { is_user } = useUser();
|
||||||
|
|
||||||
|
if (node.deleted_at) {
|
||||||
|
return <NodeDeletedBadge />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group>
|
||||||
|
<Padder>
|
||||||
|
<Group horizontal className={styles.content}>
|
||||||
|
<Group className={styles.comments}>
|
||||||
|
{inline && <div className={styles.inline}>{inline}</div>}
|
||||||
|
|
||||||
|
<NodeCommentsBlock
|
||||||
|
isLoading={isLoading}
|
||||||
|
isLoadingComments={isLoadingComments}
|
||||||
|
comments={comments}
|
||||||
|
count={commentsCount}
|
||||||
|
order={commentsOrder}
|
||||||
|
node={node}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{is_user && !isLoading && <NodeCommentForm nodeId={node.id} />}
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<div className={styles.panel}>
|
||||||
|
<StickyBox className={styles.sticky} offsetTop={72}>
|
||||||
|
<Group style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<NodeTagsBlock node={node} isLoading={isLoading} />
|
||||||
|
<NodeRelatedBlock isLoading={isLoading} node={node} related={related} />
|
||||||
|
</Group>
|
||||||
|
</StickyBox>
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
</Padder>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { NodeBottomBlock };
|
48
src/components/node/NodeBottomBlock/styles.module.scss
Normal file
48
src/components/node/NodeBottomBlock/styles.module.scss
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
@import "~/styles/variables.scss";
|
||||||
|
|
||||||
|
.sticky {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
align-items: stretch !important;
|
||||||
|
@include vertical_at_tablet;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comments {
|
||||||
|
flex: 3 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
justify-content: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
flex: 2 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
flex: 1 3;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: flex-start;
|
||||||
|
padding-left: $gap / 2;
|
||||||
|
min-width: 0;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
padding-left: 0;
|
||||||
|
padding-top: $comment_height / 2;
|
||||||
|
flex: 1 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
background: $node_buttons_bg;
|
||||||
|
flex: 1;
|
||||||
|
border-radius: $panel_radius;
|
||||||
|
box-shadow: $comment_shadow;
|
||||||
|
}
|
28
src/components/node/NodeCommentsBlock/index.tsx
Normal file
28
src/components/node/NodeCommentsBlock/index.tsx
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import { NodeNoComments } from '~/components/node/NodeNoComments';
|
||||||
|
import { NodeComments } from '~/components/node/NodeComments';
|
||||||
|
import { IComment, INode } from '~/redux/types';
|
||||||
|
import { useNodeBlocks } from '~/utils/hooks/node/useNodeBlocks';
|
||||||
|
import { useUser } from '~/utils/hooks/user/userUser';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
order: 'ASC' | 'DESC';
|
||||||
|
node: INode;
|
||||||
|
comments: IComment[];
|
||||||
|
count: number;
|
||||||
|
isLoading: boolean;
|
||||||
|
isLoadingComments: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NodeCommentsBlock: FC<IProps> = ({ isLoading, isLoadingComments, node, comments, count }) => {
|
||||||
|
const user = useUser();
|
||||||
|
const { inline } = useNodeBlocks(node, isLoading);
|
||||||
|
|
||||||
|
return isLoading || isLoadingComments || (!comments.length && !inline) ? (
|
||||||
|
<NodeNoComments is_loading={isLoadingComments || isLoading} />
|
||||||
|
) : (
|
||||||
|
<NodeComments count={count} comments={comments} user={user} order="DESC" />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { NodeCommentsBlock };
|
|
@ -8,21 +8,24 @@ import { PRESETS } from '~/constants/urls';
|
||||||
import { throttle } from 'throttle-debounce';
|
import { throttle } from 'throttle-debounce';
|
||||||
import { Icon } from '~/components/input/Icon';
|
import { Icon } from '~/components/input/Icon';
|
||||||
import { useArrows } from '~/utils/hooks/keys';
|
import { useArrows } from '~/utils/hooks/keys';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { modalShowPhotoswipe } from '~/redux/modal/actions';
|
||||||
|
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
|
||||||
|
import { selectModal } from '~/redux/modal/selectors';
|
||||||
|
|
||||||
interface IProps extends INodeComponentProps {}
|
interface IProps extends INodeComponentProps {
|
||||||
|
updateLayout?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
const getX = event =>
|
const getX = event =>
|
||||||
(event.touches && event.touches.length) || (event.changedTouches && event.changedTouches.length)
|
(event.touches && event.touches.length) || (event.changedTouches && event.changedTouches.length)
|
||||||
? (event.touches.length && event.touches[0].clientX) || event.changedTouches[0].clientX
|
? (event.touches.length && event.touches[0].clientX) || event.changedTouches[0].clientX
|
||||||
: event.clientX;
|
: event.clientX;
|
||||||
|
|
||||||
const NodeImageSlideBlock: FC<IProps> = ({
|
const NodeImageSlideBlock: FC<IProps> = ({ node, isLoading, updateLayout = () => {} }) => {
|
||||||
node,
|
const dispatch = useDispatch();
|
||||||
is_loading,
|
const { is_shown } = useShallowSelect(selectModal);
|
||||||
is_modal_shown,
|
|
||||||
updateLayout,
|
|
||||||
modalShowPhotoswipe,
|
|
||||||
}) => {
|
|
||||||
const [current, setCurrent] = useState(0);
|
const [current, setCurrent] = useState(0);
|
||||||
const [height, setHeight] = useState(window.innerHeight - 143);
|
const [height, setHeight] = useState(window.innerHeight - 143);
|
||||||
const [max_height, setMaxHeight] = useState(960);
|
const [max_height, setMaxHeight] = useState(960);
|
||||||
|
@ -88,7 +91,7 @@ const NodeImageSlideBlock: FC<IProps> = ({
|
||||||
const { width } = wrap.current.getBoundingClientRect();
|
const { width } = wrap.current.getBoundingClientRect();
|
||||||
const fallback = window.innerHeight - 143;
|
const fallback = window.innerHeight - 143;
|
||||||
|
|
||||||
if (is_loading) {
|
if (isLoading) {
|
||||||
setHeight(fallback);
|
setHeight(fallback);
|
||||||
return () => clearTimeout(timeout);
|
return () => clearTimeout(timeout);
|
||||||
}
|
}
|
||||||
|
@ -118,7 +121,7 @@ const NodeImageSlideBlock: FC<IProps> = ({
|
||||||
return () => {
|
return () => {
|
||||||
if (timeout) clearTimeout(timeout);
|
if (timeout) clearTimeout(timeout);
|
||||||
};
|
};
|
||||||
}, [is_dragging, wrap, offset, heights, max_height, images, is_loading, updateLayout]);
|
}, [is_dragging, wrap, offset, heights, max_height, images, isLoading, updateLayout]);
|
||||||
|
|
||||||
const onDrag = useCallback(
|
const onDrag = useCallback(
|
||||||
event => {
|
event => {
|
||||||
|
@ -162,8 +165,8 @@ const NodeImageSlideBlock: FC<IProps> = ({
|
||||||
normalizeOffset();
|
normalizeOffset();
|
||||||
}, [wrap, setMaxHeight, normalizeOffset]);
|
}, [wrap, setMaxHeight, normalizeOffset]);
|
||||||
|
|
||||||
const onOpenPhotoSwipe = useCallback(() => modalShowPhotoswipe(images, current), [
|
const onOpenPhotoSwipe = useCallback(() => dispatch(modalShowPhotoswipe(images, current)), [
|
||||||
modalShowPhotoswipe,
|
dispatch,
|
||||||
images,
|
images,
|
||||||
current,
|
current,
|
||||||
]);
|
]);
|
||||||
|
@ -241,7 +244,7 @@ const NodeImageSlideBlock: FC<IProps> = ({
|
||||||
images,
|
images,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useArrows(onNext, onPrev, is_modal_shown);
|
useArrows(onNext, onPrev, is_shown);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setOffset(0);
|
setOffset(0);
|
||||||
|
@ -249,7 +252,7 @@ const NodeImageSlideBlock: FC<IProps> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrap}>
|
<div className={styles.wrap}>
|
||||||
<div className={classNames(styles.cutter, { [styles.is_loading]: is_loading })} ref={wrap}>
|
<div className={classNames(styles.cutter, { [styles.is_loading]: isLoading })} ref={wrap}>
|
||||||
<div
|
<div
|
||||||
className={classNames(styles.image_container, { [styles.is_dragging]: is_dragging })}
|
className={classNames(styles.image_container, { [styles.is_dragging]: is_dragging })}
|
||||||
style={{
|
style={{
|
||||||
|
@ -261,7 +264,7 @@ const NodeImageSlideBlock: FC<IProps> = ({
|
||||||
onTouchStart={startDragging}
|
onTouchStart={startDragging}
|
||||||
ref={slide}
|
ref={slide}
|
||||||
>
|
>
|
||||||
{!is_loading &&
|
{!isLoading &&
|
||||||
images.map((file, index) => (
|
images.map((file, index) => (
|
||||||
<div
|
<div
|
||||||
className={classNames(styles.image_wrap, {
|
className={classNames(styles.image_wrap, {
|
||||||
|
|
104
src/components/node/NodeImageSwiperBlock/index.tsx
Normal file
104
src/components/node/NodeImageSwiperBlock/index.tsx
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
import React, { FC, useCallback, useEffect, useState } from 'react';
|
||||||
|
import { INodeComponentProps } from '~/redux/node/constants';
|
||||||
|
import SwiperCore, { A11y, Pagination, Navigation, SwiperOptions, Keyboard } from 'swiper';
|
||||||
|
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||||
|
|
||||||
|
import 'swiper/swiper.scss';
|
||||||
|
import 'swiper/components/pagination/pagination.scss';
|
||||||
|
import 'swiper/components/scrollbar/scrollbar.scss';
|
||||||
|
import 'swiper/components/zoom/zoom.scss';
|
||||||
|
import 'swiper/components/navigation/navigation.scss';
|
||||||
|
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import { useNodeImages } from '~/utils/hooks/node/useNodeImages';
|
||||||
|
import { getURL } from '~/utils/dom';
|
||||||
|
import { PRESETS } from '~/constants/urls';
|
||||||
|
import SwiperClass from 'swiper/types/swiper-class';
|
||||||
|
import { modalShowPhotoswipe } from '~/redux/modal/actions';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
|
||||||
|
SwiperCore.use([Navigation, Pagination, A11y]);
|
||||||
|
|
||||||
|
interface IProps extends INodeComponentProps {}
|
||||||
|
|
||||||
|
const breakpoints: SwiperOptions['breakpoints'] = {
|
||||||
|
599: {
|
||||||
|
spaceBetween: 20,
|
||||||
|
navigation: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const NodeImageSwiperBlock: FC<IProps> = ({ node }) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const [controlledSwiper, setControlledSwiper] = useState<SwiperClass | undefined>(undefined);
|
||||||
|
|
||||||
|
const images = useNodeImages(node);
|
||||||
|
|
||||||
|
const updateSwiper = useCallback(() => {
|
||||||
|
if (!controlledSwiper) return;
|
||||||
|
|
||||||
|
controlledSwiper.updateSlides();
|
||||||
|
controlledSwiper.updateSize();
|
||||||
|
controlledSwiper.update();
|
||||||
|
}, [controlledSwiper]);
|
||||||
|
|
||||||
|
const resetSwiper = useCallback(() => {
|
||||||
|
if (!controlledSwiper) return;
|
||||||
|
controlledSwiper.slideTo(0, 0);
|
||||||
|
setTimeout(() => controlledSwiper.slideTo(0, 0), 300);
|
||||||
|
}, [controlledSwiper]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateSwiper();
|
||||||
|
resetSwiper();
|
||||||
|
}, [images, updateSwiper, resetSwiper]);
|
||||||
|
|
||||||
|
const onOpenPhotoSwipe = useCallback(
|
||||||
|
() => dispatch(modalShowPhotoswipe(images, controlledSwiper?.activeIndex || 0)),
|
||||||
|
[dispatch, images, controlledSwiper]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!images?.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.wrapper}>
|
||||||
|
<Swiper
|
||||||
|
initialSlide={0}
|
||||||
|
slidesPerView="auto"
|
||||||
|
centeredSlides
|
||||||
|
onSwiper={setControlledSwiper}
|
||||||
|
grabCursor
|
||||||
|
autoHeight
|
||||||
|
breakpoints={breakpoints}
|
||||||
|
pagination={{ type: 'fraction' }}
|
||||||
|
observeSlideChildren
|
||||||
|
observeParents
|
||||||
|
resizeObserver
|
||||||
|
watchOverflow
|
||||||
|
updateOnImagesReady
|
||||||
|
onInit={resetSwiper}
|
||||||
|
keyboard={{
|
||||||
|
enabled: true,
|
||||||
|
onlyInViewport: false,
|
||||||
|
}}
|
||||||
|
zoom
|
||||||
|
>
|
||||||
|
{images.map(file => (
|
||||||
|
<SwiperSlide className={styles.slide} key={file.id}>
|
||||||
|
<img
|
||||||
|
className={styles.image}
|
||||||
|
src={getURL(file, PRESETS['1600'])}
|
||||||
|
alt={node.title}
|
||||||
|
onLoad={updateSwiper}
|
||||||
|
onClick={onOpenPhotoSwipe}
|
||||||
|
/>
|
||||||
|
</SwiperSlide>
|
||||||
|
))}
|
||||||
|
</Swiper>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { NodeImageSwiperBlock };
|
85
src/components/node/NodeImageSwiperBlock/styles.module.scss
Normal file
85
src/components/node/NodeImageSwiperBlock/styles.module.scss
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
@import "~/styles/variables.scss";
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
border-radius: $radius;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
:global(.swiper-pagination) {
|
||||||
|
left: 50%;
|
||||||
|
bottom: $gap * 2;
|
||||||
|
transform: translate(-50%, 0);
|
||||||
|
background: darken($comment_bg, 4%);
|
||||||
|
width: auto;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font: $font_10_semibold;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.swiper-container) {
|
||||||
|
width: 100vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.swiper-button-next),
|
||||||
|
:global(.swiper-button-prev) {
|
||||||
|
color: white;
|
||||||
|
font-size: 10px;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide {
|
||||||
|
text-align: center;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font: $font_32_bold;
|
||||||
|
display: flex;
|
||||||
|
border-radius: $radius;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: auto;
|
||||||
|
max-width: 100vw;
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate(0, 10px);
|
||||||
|
filter: brightness(50%) saturate(0.5);
|
||||||
|
transition: opacity 0.5s, filter 0.5s, transform 0.5s;
|
||||||
|
padding-bottom: $gap * 1.5;
|
||||||
|
padding-top: $gap;
|
||||||
|
|
||||||
|
&:global(.swiper-slide-active) {
|
||||||
|
opacity: 1;
|
||||||
|
filter: brightness(100%);
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@include tablet {
|
||||||
|
padding-bottom: 0;
|
||||||
|
padding-top: 0;
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
max-height: calc(100vh - 70px - 70px);
|
||||||
|
max-width: 100%;
|
||||||
|
border-radius: $radius;
|
||||||
|
transition: box-shadow 1s;
|
||||||
|
box-shadow: transparentize(black, 0.7) 0 3px 5px;
|
||||||
|
|
||||||
|
:global(.swiper-slide-active) & {
|
||||||
|
box-shadow: transparentize(black, 0.9) 0 10px 5px 4px,
|
||||||
|
transparentize(black, 0.7) 0 5px 5px,
|
||||||
|
transparentize(white, 0.95) 0 -1px 2px,
|
||||||
|
transparentize(white, 0.95) 0 -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include tablet {
|
||||||
|
padding-bottom: 0;
|
||||||
|
max-height: 100vh;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,85 +1,35 @@
|
||||||
import React, { FC, useCallback, useEffect, useRef, useState, memo } from 'react';
|
import React, { FC, memo } from 'react';
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
import { INode } from '~/redux/types';
|
import { INode } from '~/redux/types';
|
||||||
import { createPortal } from 'react-dom';
|
|
||||||
import { NodePanelInner } from '~/components/node/NodePanelInner';
|
import { NodePanelInner } from '~/components/node/NodePanelInner';
|
||||||
|
import { useNodePermissions } from '~/utils/hooks/node/useNodePermissions';
|
||||||
|
import { useNodeActions } from '~/utils/hooks/node/useNodeActions';
|
||||||
|
import { shallowEqual } from 'react-redux';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
node: Partial<INode>;
|
node: INode;
|
||||||
layout: {};
|
isLoading: boolean;
|
||||||
|
|
||||||
can_edit: boolean;
|
|
||||||
can_like: boolean;
|
|
||||||
can_star: boolean;
|
|
||||||
|
|
||||||
is_loading?: boolean;
|
|
||||||
|
|
||||||
onEdit: () => void;
|
|
||||||
onLike: () => void;
|
|
||||||
onStar: () => void;
|
|
||||||
onLock: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const NodePanel: FC<IProps> = memo(
|
const NodePanel: FC<IProps> = memo(({ node, isLoading }) => {
|
||||||
({ node, layout, can_edit, can_like, can_star, is_loading, onEdit, onLike, onStar, onLock }) => {
|
const [can_edit, can_like, can_star] = useNodePermissions(node);
|
||||||
const [stack, setStack] = useState(false);
|
const { onEdit, onLike, onStar, onLock } = useNodeActions(node);
|
||||||
|
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
|
||||||
const getPlace = useCallback(() => {
|
|
||||||
if (!ref.current) return;
|
|
||||||
|
|
||||||
const { bottom } = ref.current!.getBoundingClientRect();
|
|
||||||
|
|
||||||
setStack(bottom > window.innerHeight);
|
|
||||||
}, [ref]);
|
|
||||||
|
|
||||||
useEffect(() => getPlace(), [layout]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
window.addEventListener('scroll', getPlace);
|
|
||||||
window.addEventListener('resize', getPlace);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('scroll', getPlace);
|
|
||||||
window.removeEventListener('resize', getPlace);
|
|
||||||
};
|
|
||||||
}, [layout, getPlace]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.place} ref={ref}>
|
<div className={styles.place}>
|
||||||
{/*
|
|
||||||
stack &&
|
|
||||||
createPortal(
|
|
||||||
<NodePanelInner
|
|
||||||
node={node}
|
|
||||||
can_edit={can_edit}
|
|
||||||
can_like={can_like}
|
|
||||||
can_star={can_star}
|
|
||||||
onEdit={onEdit}
|
|
||||||
onLike={onLike}
|
|
||||||
onStar={onStar}
|
|
||||||
onLock={onLock}
|
|
||||||
is_loading={is_loading}
|
|
||||||
stack
|
|
||||||
/>,
|
|
||||||
document.body
|
|
||||||
)
|
|
||||||
*/}
|
|
||||||
|
|
||||||
<NodePanelInner
|
<NodePanelInner
|
||||||
node={node}
|
node={node}
|
||||||
onEdit={onEdit}
|
onEdit={onEdit}
|
||||||
onLike={onLike}
|
onLike={onLike}
|
||||||
onStar={onStar}
|
onStar={onStar}
|
||||||
onLock={onLock}
|
onLock={onLock}
|
||||||
can_edit={can_edit}
|
canEdit={can_edit}
|
||||||
can_like={can_like}
|
canLike={can_like}
|
||||||
can_star={can_star}
|
canStar={can_star}
|
||||||
is_loading={!!is_loading}
|
isLoading={!!isLoading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}, shallowEqual);
|
||||||
);
|
|
||||||
|
|
||||||
export { NodePanel };
|
export { NodePanel };
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import React, { FC, memo } from 'react';
|
import React, { FC, memo } from 'react';
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
import { Group } from '~/components/containers/Group';
|
|
||||||
import { Filler } from '~/components/containers/Filler';
|
|
||||||
import { Icon } from '~/components/input/Icon';
|
import { Icon } from '~/components/input/Icon';
|
||||||
import { INode } from '~/redux/types';
|
import { INode } from '~/redux/types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
@ -12,11 +10,11 @@ interface IProps {
|
||||||
node: Partial<INode>;
|
node: Partial<INode>;
|
||||||
stack?: boolean;
|
stack?: boolean;
|
||||||
|
|
||||||
can_edit: boolean;
|
canEdit: boolean;
|
||||||
can_like: boolean;
|
canLike: boolean;
|
||||||
can_star: boolean;
|
canStar: boolean;
|
||||||
|
|
||||||
is_loading: boolean;
|
isLoading: boolean;
|
||||||
|
|
||||||
onEdit: () => void;
|
onEdit: () => void;
|
||||||
onLike: () => void;
|
onLike: () => void;
|
||||||
|
@ -29,11 +27,11 @@ const NodePanelInner: FC<IProps> = memo(
|
||||||
node: { title, user, is_liked, is_heroic, deleted_at, created_at, like_count },
|
node: { title, user, is_liked, is_heroic, deleted_at, created_at, like_count },
|
||||||
stack,
|
stack,
|
||||||
|
|
||||||
can_star,
|
canStar,
|
||||||
can_edit,
|
canEdit,
|
||||||
can_like,
|
canLike,
|
||||||
|
|
||||||
is_loading,
|
isLoading,
|
||||||
|
|
||||||
onStar,
|
onStar,
|
||||||
onEdit,
|
onEdit,
|
||||||
|
@ -45,12 +43,12 @@ const NodePanelInner: FC<IProps> = memo(
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<div className={styles.panel}>
|
<div className={styles.panel}>
|
||||||
<div className={styles.title}>
|
<div className={styles.title}>
|
||||||
{is_loading ? <Placeholder width="40%" /> : title || '...'}
|
{isLoading ? <Placeholder width="40%" /> : title || '...'}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{user && user.username && (
|
{user && user.username && (
|
||||||
<div className={styles.name}>
|
<div className={styles.name}>
|
||||||
{is_loading ? (
|
{isLoading ? (
|
||||||
<Placeholder width="100px" />
|
<Placeholder width="100px" />
|
||||||
) : (
|
) : (
|
||||||
`~${user.username.toLocaleLowerCase()}, ${getPrettyDate(created_at)}`
|
`~${user.username.toLocaleLowerCase()}, ${getPrettyDate(created_at)}`
|
||||||
|
@ -59,14 +57,14 @@ const NodePanelInner: FC<IProps> = memo(
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{can_edit && (
|
{canEdit && (
|
||||||
<div className={styles.editor_menu}>
|
<div className={styles.editor_menu}>
|
||||||
<div className={styles.editor_menu_button}>
|
<div className={styles.editor_menu_button}>
|
||||||
<Icon icon="dots-vertical" size={24} />
|
<Icon icon="dots-vertical" size={24} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.editor_buttons}>
|
<div className={styles.editor_buttons}>
|
||||||
{can_star && (
|
{canStar && (
|
||||||
<div className={classNames(styles.star, { is_heroic })}>
|
<div className={classNames(styles.star, { is_heroic })}>
|
||||||
{is_heroic ? (
|
{is_heroic ? (
|
||||||
<Icon icon="star_full" size={24} onClick={onStar} />
|
<Icon icon="star_full" size={24} onClick={onStar} />
|
||||||
|
@ -88,7 +86,7 @@ const NodePanelInner: FC<IProps> = memo(
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={styles.buttons}>
|
<div className={styles.buttons}>
|
||||||
{can_like && (
|
{canLike && (
|
||||||
<div className={classNames(styles.like, { is_liked })}>
|
<div className={classNames(styles.like, { is_liked })}>
|
||||||
{is_liked ? (
|
{is_liked ? (
|
||||||
<Icon icon="heart_full" size={24} onClick={onLike} />
|
<Icon icon="heart_full" size={24} onClick={onLike} />
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
grid-row-gap: $gap;
|
grid-row-gap: $gap;
|
||||||
|
|
||||||
@include tablet {
|
@include tablet {
|
||||||
grid-template-columns: repeat(6, 1fr);
|
grid-template-columns: repeat(3, 1fr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
44
src/components/node/NodeRelatedBlock/index.tsx
Normal file
44
src/components/node/NodeRelatedBlock/index.tsx
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import { NodeRelatedPlaceholder } from '~/components/node/NodeRelated/placeholder';
|
||||||
|
import { NodeRelated } from '~/components/node/NodeRelated';
|
||||||
|
import { URLS } from '~/constants/urls';
|
||||||
|
import { INode } from '~/redux/types';
|
||||||
|
import { INodeRelated } from '~/redux/node/types';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
isLoading: boolean;
|
||||||
|
node: INode;
|
||||||
|
related: INodeRelated;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NodeRelatedBlock: FC<IProps> = ({ isLoading, node, related }) => {
|
||||||
|
if (isLoading) {
|
||||||
|
return <NodeRelatedPlaceholder />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{related &&
|
||||||
|
related.albums &&
|
||||||
|
!!node?.id &&
|
||||||
|
Object.keys(related.albums)
|
||||||
|
.filter(album => related.albums[album].length > 0)
|
||||||
|
.map(album => (
|
||||||
|
<NodeRelated
|
||||||
|
title={
|
||||||
|
<Link to={URLS.NODE_TAG_URL(node.id!, encodeURIComponent(album))}>{album}</Link>
|
||||||
|
}
|
||||||
|
items={related.albums[album]}
|
||||||
|
key={album}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{related && related.similar && related.similar.length > 0 && (
|
||||||
|
<NodeRelated title="ПОХОЖИЕ" items={related.similar} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { NodeRelatedBlock };
|
52
src/components/node/NodeTagsBlock/index.tsx
Normal file
52
src/components/node/NodeTagsBlock/index.tsx
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import React, { FC, useCallback } from 'react';
|
||||||
|
import { INode, ITag } from '~/redux/types';
|
||||||
|
import { URLS } from '~/constants/urls';
|
||||||
|
import { nodeUpdateTags } from '~/redux/node/actions';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { useHistory } from 'react-router';
|
||||||
|
import { NodeTags } from '~/components/node/NodeTags';
|
||||||
|
import { useUser } from '~/utils/hooks/user/userUser';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
node: INode;
|
||||||
|
isLoading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NodeTagsBlock: FC<IProps> = ({ node, isLoading }) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const history = useHistory();
|
||||||
|
const { is_user } = useUser();
|
||||||
|
|
||||||
|
const onTagsChange = useCallback(
|
||||||
|
(tags: string[]) => {
|
||||||
|
dispatch(nodeUpdateTags(node.id, tags));
|
||||||
|
},
|
||||||
|
[dispatch, node]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onTagClick = useCallback(
|
||||||
|
(tag: Partial<ITag>) => {
|
||||||
|
if (!node?.id || !tag?.title) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
history.push(URLS.NODE_TAG_URL(node.id, encodeURIComponent(tag.title)));
|
||||||
|
},
|
||||||
|
[history, node]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NodeTags
|
||||||
|
is_editable={is_user}
|
||||||
|
tags={node.tags}
|
||||||
|
onChange={onTagsChange}
|
||||||
|
onTagClick={onTagClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { NodeTagsBlock };
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { FC, useEffect, useCallback } from 'react';
|
import React, { FC, useCallback, useEffect } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { FlowGrid } from '~/components/flow/FlowGrid';
|
import { FlowGrid } from '~/components/flow/FlowGrid';
|
||||||
import { selectFlow } from '~/redux/flow/selectors';
|
import { selectFlow } from '~/redux/flow/selectors';
|
||||||
|
@ -10,6 +10,7 @@ import { FlowHero } from '~/components/flow/FlowHero';
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
import { IState } from '~/redux/store';
|
import { IState } from '~/redux/store';
|
||||||
import { FlowStamp } from '~/components/flow/FlowStamp';
|
import { FlowStamp } from '~/components/flow/FlowStamp';
|
||||||
|
import { Container } from '~/containers/main/Container';
|
||||||
|
|
||||||
const mapStateToProps = (state: IState) => ({
|
const mapStateToProps = (state: IState) => ({
|
||||||
flow: pick(['nodes', 'heroes', 'recent', 'updated', 'is_loading', 'search'], selectFlow(state)),
|
flow: pick(['nodes', 'heroes', 'recent', 'updated', 'is_loading', 'search'], selectFlow(state)),
|
||||||
|
@ -61,6 +62,7 @@ const FlowLayoutUnconnected: FC<IProps> = ({
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Container>
|
||||||
<div className={styles.grid}>
|
<div className={styles.grid}>
|
||||||
<div className={styles.hero}>
|
<div className={styles.hero}>
|
||||||
<FlowHero heroes={heroes} />
|
<FlowHero heroes={heroes} />
|
||||||
|
@ -83,6 +85,7 @@ const FlowLayoutUnconnected: FC<IProps> = ({
|
||||||
onChangeCellView={flowSetCellView}
|
onChangeCellView={flowSetCellView}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</Container>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
13
src/containers/main/Container/index.tsx
Normal file
13
src/containers/main/Container/index.tsx
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Container: FC<IProps> = ({ className, children }) => (
|
||||||
|
<div className={classNames(styles.container, className)}>{children}</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export { Container };
|
16
src/containers/main/Container/styles.module.scss
Normal file
16
src/containers/main/Container/styles.module.scss
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
@import "~/styles/variables.scss";
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: $content_width;
|
||||||
|
margin: auto;
|
||||||
|
padding: 0 $gap;
|
||||||
|
|
||||||
|
@include tablet {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: $content_width + $gap * 4) {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,24 +2,18 @@
|
||||||
|
|
||||||
.wrapper {
|
.wrapper {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0 $gap;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
||||||
@include tablet {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: $content_width;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
padding-bottom: 29px;
|
padding-bottom: 29px;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
@ -12,13 +12,14 @@ import { NodeCommentForm } from '~/components/node/NodeCommentForm';
|
||||||
import isBefore from 'date-fns/isBefore';
|
import isBefore from 'date-fns/isBefore';
|
||||||
import { Card } from '~/components/containers/Card';
|
import { Card } from '~/components/containers/Card';
|
||||||
import { Footer } from '~/components/main/Footer';
|
import { Footer } from '~/components/main/Footer';
|
||||||
import { Sticky } from '~/components/containers/Sticky';
|
|
||||||
import { BorisStats } from '~/components/boris/BorisStats';
|
import { BorisStats } from '~/components/boris/BorisStats';
|
||||||
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
|
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
|
||||||
import { selectBorisStats } from '~/redux/boris/selectors';
|
import { selectBorisStats } from '~/redux/boris/selectors';
|
||||||
import { authSetUser } from '~/redux/auth/actions';
|
import { authSetUser } from '~/redux/auth/actions';
|
||||||
import { nodeLoadNode } from '~/redux/node/actions';
|
import { nodeLoadNode } from '~/redux/node/actions';
|
||||||
import { borisLoadStats } from '~/redux/boris/actions';
|
import { borisLoadStats } from '~/redux/boris/actions';
|
||||||
|
import { Container } from '~/containers/main/Container';
|
||||||
|
import StickyBox from 'react-sticky-box/dist/esnext';
|
||||||
|
|
||||||
type IProps = {};
|
type IProps = {};
|
||||||
|
|
||||||
|
@ -55,6 +56,7 @@ const BorisLayout: FC<IProps> = () => {
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Container>
|
||||||
<div className={styles.wrap}>
|
<div className={styles.wrap}>
|
||||||
<div className={styles.cover} />
|
<div className={styles.cover} />
|
||||||
|
|
||||||
|
@ -87,7 +89,7 @@ const BorisLayout: FC<IProps> = () => {
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Group className={styles.stats}>
|
<Group className={styles.stats}>
|
||||||
<Sticky>
|
<StickyBox className={styles.sticky} offsetTop={72} offsetBottom={10}>
|
||||||
<Group className={styles.stats__container}>
|
<Group className={styles.stats__container}>
|
||||||
<div className={styles.stats__about}>
|
<div className={styles.stats__about}>
|
||||||
<h4>Господи-боженьки, где это я?</h4>
|
<h4>Господи-боженьки, где это я?</h4>
|
||||||
|
@ -104,10 +106,11 @@ const BorisLayout: FC<IProps> = () => {
|
||||||
<BorisStats stats={stats} />
|
<BorisStats stats={stats} />
|
||||||
</div>
|
</div>
|
||||||
</Group>
|
</Group>
|
||||||
</Sticky>
|
</StickyBox>
|
||||||
</Group>
|
</Group>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</Container>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,256 +1,72 @@
|
||||||
import React, { createElement, FC, memo, useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { FC, memo } from 'react';
|
||||||
import { RouteComponentProps, useHistory } from 'react-router';
|
import { RouteComponentProps } from 'react-router';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { canEditNode, canLikeNode, canStarNode } from '~/utils/node';
|
|
||||||
import { selectNode } from '~/redux/node/selectors';
|
import { selectNode } from '~/redux/node/selectors';
|
||||||
import { Card } from '~/components/containers/Card';
|
import { Card } from '~/components/containers/Card';
|
||||||
|
|
||||||
import { NodePanel } from '~/components/node/NodePanel';
|
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 { Footer } from '~/components/main/Footer';
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
import styles from './styles.module.scss';
|
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 { SidebarRouter } from '~/containers/main/SidebarRouter';
|
||||||
import { ITag } from '~/redux/types';
|
|
||||||
import { URLS } from '~/constants/urls';
|
|
||||||
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
|
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) => ({
|
type IProps = RouteComponentProps<{ id: string }> & {};
|
||||||
node: selectNode(state),
|
|
||||||
user: selectUser(state),
|
|
||||||
modal: pick(['is_shown'])(selectModal(state)),
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const NodeLayout: FC<IProps> = memo(
|
||||||
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 mapStateToProps> &
|
|
||||||
typeof mapDispatchToProps &
|
|
||||||
RouteComponentProps<{ id: string }> & {};
|
|
||||||
|
|
||||||
const NodeLayoutUnconnected: FC<IProps> = memo(
|
|
||||||
({
|
({
|
||||||
match: {
|
match: {
|
||||||
params: { id },
|
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 {
|
const {
|
||||||
is_loading,
|
is_loading,
|
||||||
is_loading_comments,
|
current,
|
||||||
comments = [],
|
comments,
|
||||||
current: node,
|
|
||||||
related,
|
|
||||||
comment_count,
|
comment_count,
|
||||||
|
is_loading_comments,
|
||||||
|
related,
|
||||||
} = useShallowSelect(selectNode);
|
} = useShallowSelect(selectNode);
|
||||||
const updateLayout = useCallback(() => setLayout({}), []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useNodeCoverImage(current);
|
||||||
if (is_loading) return;
|
useScrollToTop([id]);
|
||||||
nodeGotoNode(parseInt(id, 10), null);
|
useLoadNode(id, is_loading);
|
||||||
}, [nodeGotoNode, id]);
|
|
||||||
|
|
||||||
const onTagsChange = useCallback(
|
const { head, block } = useNodeBlocks(current, is_loading);
|
||||||
(tags: string[]) => {
|
|
||||||
nodeUpdateTags(node.id, tags);
|
|
||||||
},
|
|
||||||
[node, nodeUpdateTags]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onTagClick = useCallback(
|
|
||||||
(tag: Partial<ITag>) => {
|
|
||||||
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<INodeComponentProps>) =>
|
|
||||||
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]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className={styles.wrap}>
|
||||||
{!!head && createNodeBlock(head)}
|
{head}
|
||||||
|
|
||||||
|
<Container>
|
||||||
<Card className={styles.node} seamless>
|
<Card className={styles.node} seamless>
|
||||||
{!!block && createNodeBlock(block)}
|
{block}
|
||||||
|
|
||||||
<NodePanel
|
<NodePanel node={current} isLoading={is_loading} />
|
||||||
node={pick(
|
|
||||||
['title', 'user', 'is_liked', 'is_heroic', 'deleted_at', 'created_at', 'like_count'],
|
|
||||||
node
|
|
||||||
)}
|
|
||||||
layout={layout}
|
|
||||||
can_edit={can_edit}
|
|
||||||
can_like={can_like}
|
|
||||||
can_star={can_star}
|
|
||||||
onEdit={onEdit}
|
|
||||||
onLike={onLike}
|
|
||||||
onStar={onStar}
|
|
||||||
onLock={onLock}
|
|
||||||
is_loading={is_loading}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{node.deleted_at ? (
|
<NodeBottomBlock
|
||||||
<NodeDeletedBadge />
|
node={current}
|
||||||
) : (
|
isLoadingComments={is_loading_comments}
|
||||||
<Group>
|
|
||||||
<Padder>
|
|
||||||
<Group horizontal className={styles.content}>
|
|
||||||
<Group className={styles.comments}>
|
|
||||||
{inline && <div className={styles.inline}>{createNodeBlock(inline)}</div>}
|
|
||||||
|
|
||||||
{is_loading || is_loading_comments || (!comments.length && !inline) ? (
|
|
||||||
<NodeNoComments is_loading={is_loading_comments || is_loading} />
|
|
||||||
) : (
|
|
||||||
<NodeComments
|
|
||||||
count={comment_count}
|
|
||||||
comments={comments}
|
comments={comments}
|
||||||
user={user}
|
isLoading={is_loading}
|
||||||
order="DESC"
|
commentsCount={comment_count}
|
||||||
|
commentsOrder="DESC"
|
||||||
|
related={related}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
|
|
||||||
{is_user && !is_loading && <NodeCommentForm nodeId={node.id} />}
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<div className={styles.panel}>
|
|
||||||
<Sticky>
|
|
||||||
<Group style={{ flex: 1, minWidth: 0 }}>
|
|
||||||
{!is_loading && (
|
|
||||||
<NodeTags
|
|
||||||
is_editable={is_user}
|
|
||||||
tags={node.tags}
|
|
||||||
onChange={onTagsChange}
|
|
||||||
onTagClick={onTagClick}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{is_loading && <NodeRelatedPlaceholder />}
|
|
||||||
|
|
||||||
{!is_loading &&
|
|
||||||
related &&
|
|
||||||
related.albums &&
|
|
||||||
!!node?.id &&
|
|
||||||
Object.keys(related.albums)
|
|
||||||
.filter(album => related.albums[album].length > 0)
|
|
||||||
.map(album => (
|
|
||||||
<NodeRelated
|
|
||||||
title={
|
|
||||||
<Link to={URLS.NODE_TAG_URL(node.id!, encodeURIComponent(album))}>
|
|
||||||
{album}
|
|
||||||
</Link>
|
|
||||||
}
|
|
||||||
items={related.albums[album]}
|
|
||||||
key={album}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{!is_loading &&
|
|
||||||
related &&
|
|
||||||
related.similar &&
|
|
||||||
related.similar.length > 0 && (
|
|
||||||
<NodeRelated title="ПОХОЖИЕ" items={related.similar} />
|
|
||||||
)}
|
|
||||||
</Group>
|
|
||||||
</Sticky>
|
|
||||||
</div>
|
|
||||||
</Group>
|
|
||||||
</Padder>
|
|
||||||
</Group>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Footer />
|
<Footer />
|
||||||
</Card>
|
</Card>
|
||||||
|
</Container>
|
||||||
|
|
||||||
<SidebarRouter prefix="/post:id" />
|
<SidebarRouter prefix="/post:id" />
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const NodeLayout = connect(mapStateToProps, mapDispatchToProps)(NodeLayoutUnconnected);
|
export { NodeLayout };
|
||||||
|
|
||||||
export { NodeLayout, NodeLayoutUnconnected };
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
align-items: stretch !important;
|
align-items: stretch !important;
|
||||||
|
|
||||||
@include vertical_at_tablet;
|
@include vertical_at_tablet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,20 @@
|
||||||
import git from '~/stats/git.json';
|
import git from '~/stats/git.json';
|
||||||
import { API } from '~/constants/api';
|
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 { IBorisState, IStatBackend } from './reducer';
|
||||||
import { IResultWithStatus } from '../types';
|
import axios from 'axios';
|
||||||
|
import { IGetGithubIssuesResult } from '~/redux/boris/types';
|
||||||
|
|
||||||
export const getBorisGitStats = () => Promise.resolve<IBorisState['stats']['git']>(git);
|
export const getBorisGitStats = () => Promise.resolve<IBorisState['stats']['git']>(git);
|
||||||
|
|
||||||
export const getBorisBackendStats = () =>
|
export const getBorisBackendStats = () =>
|
||||||
api.get<IStatBackend>(API.BORIS.GET_BACKEND_STATS).then(cleanResult);
|
api.get<IStatBackend>(API.BORIS.GET_BACKEND_STATS).then(cleanResult);
|
||||||
|
|
||||||
|
export const getGithubIssues = () => {
|
||||||
|
return axios
|
||||||
|
.get<IGetGithubIssuesResult>('https://api.github.com/repos/muerwre/vault-frontend/issues', {
|
||||||
|
params: { state: 'all', sort: 'created' },
|
||||||
|
})
|
||||||
|
.then(result => result.data)
|
||||||
|
.catch(() => []);
|
||||||
|
};
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { createReducer } from '~/utils/reducer';
|
import { createReducer } from '~/utils/reducer';
|
||||||
import { BORIS_HANDLERS } from './handlers';
|
import { BORIS_HANDLERS } from './handlers';
|
||||||
|
import { IGithubIssue } from '~/redux/boris/types';
|
||||||
|
|
||||||
export type IStatGitRow = {
|
export type IStatGitRow = {
|
||||||
commit: string;
|
commit: string;
|
||||||
|
@ -31,6 +32,7 @@ export type IStatBackend = {
|
||||||
export type IBorisState = Readonly<{
|
export type IBorisState = Readonly<{
|
||||||
stats: {
|
stats: {
|
||||||
git: Partial<IStatGitRow>[];
|
git: Partial<IStatGitRow>[];
|
||||||
|
issues: IGithubIssue[];
|
||||||
backend?: IStatBackend;
|
backend?: IStatBackend;
|
||||||
is_loading: boolean;
|
is_loading: boolean;
|
||||||
};
|
};
|
||||||
|
@ -39,6 +41,7 @@ export type IBorisState = Readonly<{
|
||||||
const BORIS_INITIAL_STATE: IBorisState = {
|
const BORIS_INITIAL_STATE: IBorisState = {
|
||||||
stats: {
|
stats: {
|
||||||
git: [],
|
git: [],
|
||||||
|
issues: [],
|
||||||
backend: undefined,
|
backend: undefined,
|
||||||
is_loading: false,
|
is_loading: false,
|
||||||
},
|
},
|
||||||
|
|
|
@ -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 { BORIS_ACTIONS } from './constants';
|
||||||
import { borisSetStats } from './actions';
|
import { borisSetStats } from './actions';
|
||||||
import { getBorisGitStats, getBorisBackendStats } from './api';
|
import { getBorisBackendStats, getGithubIssues } from './api';
|
||||||
import { Unwrap } from '../types';
|
import { Unwrap } from '../types';
|
||||||
|
|
||||||
function* loadStats() {
|
function* loadStats() {
|
||||||
try {
|
try {
|
||||||
yield put(borisSetStats({ is_loading: true }));
|
yield put(borisSetStats({ is_loading: true }));
|
||||||
|
|
||||||
const git: Unwrap<typeof getBorisGitStats> = yield call(getBorisGitStats);
|
|
||||||
const backend: Unwrap<typeof getBorisBackendStats> = yield call(getBorisBackendStats);
|
const backend: Unwrap<typeof getBorisBackendStats> = yield call(getBorisBackendStats);
|
||||||
|
const issues: Unwrap<typeof getGithubIssues> = yield call(getGithubIssues);
|
||||||
|
|
||||||
yield put(borisSetStats({ git, backend }));
|
yield put(borisSetStats({ issues, backend }));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
yield put(borisSetStats({ git: [], backend: undefined }));
|
yield put(borisSetStats({ git: [], backend: undefined }));
|
||||||
} finally {
|
} finally {
|
||||||
|
|
12
src/redux/boris/types.ts
Normal file
12
src/redux/boris/types.ts
Normal file
|
@ -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[];
|
|
@ -129,7 +129,7 @@ export const nodeSetEditor = (editor: INode) => ({
|
||||||
editor,
|
editor,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const nodeSetCoverImage = (current_cover_image: IFile) => ({
|
export const nodeSetCoverImage = (current_cover_image?: IFile) => ({
|
||||||
type: NODE_ACTIONS.SET_COVER_IMAGE,
|
type: NODE_ACTIONS.SET_COVER_IMAGE,
|
||||||
current_cover_image,
|
current_cover_image,
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { FC, ReactElement } from 'react';
|
import { FC } from 'react';
|
||||||
import { IComment, INode, ValueOf } from '../types';
|
import { IComment, INode, ValueOf } from '../types';
|
||||||
import { NodeImageSlideBlock } from '~/components/node/NodeImageSlideBlock';
|
|
||||||
import { NodeTextBlock } from '~/components/node/NodeTextBlock';
|
import { NodeTextBlock } from '~/components/node/NodeTextBlock';
|
||||||
import { NodeAudioBlock } from '~/components/node/NodeAudioBlock';
|
import { NodeAudioBlock } from '~/components/node/NodeAudioBlock';
|
||||||
import { NodeVideoBlock } from '~/components/node/NodeVideoBlock';
|
import { NodeVideoBlock } from '~/components/node/NodeVideoBlock';
|
||||||
|
@ -12,10 +11,10 @@ import { AudioEditor } from '~/components/editors/AudioEditor';
|
||||||
import { EditorImageUploadButton } from '~/components/editors/EditorImageUploadButton';
|
import { EditorImageUploadButton } from '~/components/editors/EditorImageUploadButton';
|
||||||
import { EditorAudioUploadButton } from '~/components/editors/EditorAudioUploadButton';
|
import { EditorAudioUploadButton } from '~/components/editors/EditorAudioUploadButton';
|
||||||
import { EditorUploadCoverButton } from '~/components/editors/EditorUploadCoverButton';
|
import { EditorUploadCoverButton } from '~/components/editors/EditorUploadCoverButton';
|
||||||
import { modalShowPhotoswipe } from '../modal/actions';
|
|
||||||
import { IEditorComponentProps, NodeEditorProps } from '~/redux/node/types';
|
import { IEditorComponentProps, NodeEditorProps } from '~/redux/node/types';
|
||||||
import { EditorFiller } from '~/components/editors/EditorFiller';
|
import { EditorFiller } from '~/components/editors/EditorFiller';
|
||||||
import { EditorPublicSwitch } from '~/components/editors/EditorPublicSwitch';
|
import { EditorPublicSwitch } from '~/components/editors/EditorPublicSwitch';
|
||||||
|
import { NodeImageSwiperBlock } from '~/components/node/NodeImageSwiperBlock';
|
||||||
|
|
||||||
const prefix = 'NODE.';
|
const prefix = 'NODE.';
|
||||||
export const NODE_ACTIONS = {
|
export const NODE_ACTIONS = {
|
||||||
|
@ -79,17 +78,13 @@ export const NODE_TYPES = {
|
||||||
|
|
||||||
export type INodeComponentProps = {
|
export type INodeComponentProps = {
|
||||||
node: INode;
|
node: INode;
|
||||||
is_loading: boolean;
|
isLoading: boolean;
|
||||||
is_modal_shown: boolean;
|
|
||||||
layout: {};
|
|
||||||
updateLayout: () => void;
|
|
||||||
modalShowPhotoswipe: typeof modalShowPhotoswipe;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type INodeComponents = Record<ValueOf<typeof NODE_TYPES>, FC<INodeComponentProps>>;
|
export type INodeComponents = Record<ValueOf<typeof NODE_TYPES>, FC<INodeComponentProps>>;
|
||||||
|
|
||||||
export const NODE_HEADS: INodeComponents = {
|
export const NODE_HEADS: INodeComponents = {
|
||||||
[NODE_TYPES.IMAGE]: NodeImageSlideBlock,
|
[NODE_TYPES.IMAGE]: NodeImageSwiperBlock,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NODE_COMPONENTS: INodeComponents = {
|
export const NODE_COMPONENTS: INodeComponents = {
|
||||||
|
|
|
@ -1,16 +1,14 @@
|
||||||
import { createReducer } from '~/utils/reducer';
|
import { createReducer } from '~/utils/reducer';
|
||||||
import { INode, IComment, IFile } from '../types';
|
import { IComment, IFile, INode } from '../types';
|
||||||
import { EMPTY_NODE, EMPTY_COMMENT } from './constants';
|
import { EMPTY_COMMENT, EMPTY_NODE } from './constants';
|
||||||
import { NODE_HANDLERS } from './handlers';
|
import { NODE_HANDLERS } from './handlers';
|
||||||
|
import { INodeRelated } from '~/redux/node/types';
|
||||||
|
|
||||||
export type INodeState = Readonly<{
|
export type INodeState = Readonly<{
|
||||||
editor: INode;
|
editor: INode;
|
||||||
current: INode;
|
current: INode;
|
||||||
comments: IComment[];
|
comments: IComment[];
|
||||||
related: {
|
related: INodeRelated;
|
||||||
albums: Record<string, INode[]>;
|
|
||||||
similar: INode[];
|
|
||||||
};
|
|
||||||
comment_data: Record<number, IComment>;
|
comment_data: Record<number, IComment>;
|
||||||
comment_count: number;
|
comment_count: number;
|
||||||
current_cover_image?: IFile;
|
current_cover_image?: IFile;
|
||||||
|
|
|
@ -89,3 +89,8 @@ export type NodeEditorProps = {
|
||||||
temp: string[];
|
temp: string[];
|
||||||
setTemp: (val: string[]) => void;
|
setTemp: (val: string[]) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type INodeRelated = {
|
||||||
|
albums: Record<string, INode[]>;
|
||||||
|
similar: INode[];
|
||||||
|
};
|
||||||
|
|
|
@ -75,7 +75,9 @@ export const sagaMiddleware = createSagaMiddleware();
|
||||||
export const history = createBrowserHistory();
|
export const history = createBrowserHistory();
|
||||||
|
|
||||||
const composeEnhancers =
|
const composeEnhancers =
|
||||||
typeof window === 'object' && (<any>window).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
|
typeof window === 'object' &&
|
||||||
|
(<any>window).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ &&
|
||||||
|
process.env.NODE_ENV === 'development'
|
||||||
? (<any>window).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({})
|
? (<any>window).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({})
|
||||||
: compose;
|
: compose;
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { apiGetTagSuggestions, apiGetNodesOfTag } from '~/redux/tag/api';
|
||||||
import { Unwrap } from '~/redux/types';
|
import { Unwrap } from '~/redux/types';
|
||||||
|
|
||||||
function* loadTagNodes({ tag }: ReturnType<typeof tagLoadNodes>) {
|
function* loadTagNodes({ tag }: ReturnType<typeof tagLoadNodes>) {
|
||||||
yield put(tagSetNodes({ isLoading: true, list: [] }));
|
yield put(tagSetNodes({ isLoading: true }));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { list }: ReturnType<typeof selectTagNodes> = yield select(selectTagNodes);
|
const { list }: ReturnType<typeof selectTagNodes> = yield select(selectTagNodes);
|
||||||
|
|
|
@ -55,6 +55,10 @@ $margin: 1em;
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin-bottom: $margin;
|
margin-bottom: $margin;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
h5, h4, h3, h2, h1 {
|
h5, h4, h3, h2, h1 {
|
||||||
|
|
13
src/utils/hooks/node/useLoadNode.ts
Normal file
13
src/utils/hooks/node/useLoadNode.ts
Normal file
|
@ -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]);
|
||||||
|
};
|
19
src/utils/hooks/node/useNodeActions.ts
Normal file
19
src/utils/hooks/node/useNodeActions.ts
Normal file
|
@ -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 };
|
||||||
|
};
|
9
src/utils/hooks/node/useNodeAudios.ts
Normal file
9
src/utils/hooks/node/useNodeAudios.ts
Normal file
|
@ -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,
|
||||||
|
]);
|
||||||
|
};
|
39
src/utils/hooks/node/useNodeBlocks.ts
Normal file
39
src/utils/hooks/node/useNodeBlocks.ts
Normal file
|
@ -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<INodeComponentProps>) =>
|
||||||
|
!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 };
|
||||||
|
};
|
16
src/utils/hooks/node/useNodeCoverImage.ts
Normal file
16
src/utils/hooks/node/useNodeCoverImage.ts
Normal file
|
@ -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]);
|
||||||
|
};
|
9
src/utils/hooks/node/useNodeImages.ts
Normal file
9
src/utils/hooks/node/useNodeImages.ts
Normal file
|
@ -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,
|
||||||
|
]);
|
||||||
|
};
|
14
src/utils/hooks/node/useNodePermissions.ts
Normal file
14
src/utils/hooks/node/useNodePermissions.ts
Normal file
|
@ -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];
|
||||||
|
};
|
24
src/utils/hooks/useInputPasteUpload.ts
Normal file
24
src/utils/hooks/useInputPasteUpload.ts
Normal file
|
@ -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]);
|
||||||
|
};
|
7
src/utils/hooks/useScrollToTop.ts
Normal file
7
src/utils/hooks/useScrollToTop.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
export const useScrollToTop = (deps?: any[]) => {
|
||||||
|
useEffect(() => {
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
}, deps || []);
|
||||||
|
};
|
4
src/utils/hooks/user/userUser.ts
Normal file
4
src/utils/hooks/user/userUser.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
import { useShallowSelect } from '~/utils/hooks/useShallowSelect';
|
||||||
|
import { selectUser } from '~/redux/auth/selectors';
|
||||||
|
|
||||||
|
export const useUser = () => useShallowSelect(selectUser);
|
|
@ -1,10 +1,8 @@
|
||||||
import { USER_ROLES } from '~/redux/auth/constants';
|
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 { IUser } from '~/redux/auth/types';
|
||||||
import { path } from 'ramda';
|
import { path } from 'ramda';
|
||||||
import { NODE_TYPES } from '~/redux/node/constants';
|
import { NODE_TYPES } from '~/redux/node/constants';
|
||||||
import { useMemo } from 'react';
|
|
||||||
import { UPLOAD_TYPES } from '~/redux/uploads/constants';
|
|
||||||
|
|
||||||
export const canEditNode = (node: Partial<INode>, user: Partial<IUser>): boolean =>
|
export const canEditNode = (node: Partial<INode>, user: Partial<IUser>): boolean =>
|
||||||
path(['role'], user) === USER_ROLES.ADMIN ||
|
path(['role'], user) === USER_ROLES.ADMIN ||
|
||||||
|
@ -21,11 +19,3 @@ export const canStarNode = (node: Partial<INode>, user: Partial<IUser>): boolean
|
||||||
node.type === NODE_TYPES.IMAGE &&
|
node.type === NODE_TYPES.IMAGE &&
|
||||||
path(['role'], user) &&
|
path(['role'], user) &&
|
||||||
path(['role'], user) === USER_ROLES.ADMIN;
|
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]
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
|
@ -74,3 +74,37 @@ export const fakeUploader = ({
|
||||||
export const getFileType = (file: File): keyof typeof UPLOAD_TYPES | undefined =>
|
export const getFileType = (file: File): keyof typeof UPLOAD_TYPES | undefined =>
|
||||||
(file.type && Object.keys(FILE_MIMES).find(mime => FILE_MIMES[mime].includes(file.type))) ||
|
(file.type && Object.keys(FILE_MIMES).find(mime => FILE_MIMES[mime].includes(file.type))) ||
|
||||||
undefined;
|
undefined;
|
||||||
|
|
||||||
|
// getImageFromPaste returns any images from paste event
|
||||||
|
export const getImageFromPaste = (event: ClipboardEvent): Promise<File | undefined> => {
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
83
yarn.lock
83
yarn.lock
|
@ -1109,6 +1109,13 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
regenerator-runtime "^0.13.4"
|
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":
|
"@babel/runtime@^7.10.5":
|
||||||
version "7.13.7"
|
version "7.13.7"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.7.tgz#d494e39d198ee9ca04f4dcb76d25d9d7a1dc961a"
|
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"
|
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
|
||||||
integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==
|
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":
|
"@types/testing-library__jest-dom@^5.9.1":
|
||||||
version "5.9.5"
|
version "5.9.5"
|
||||||
resolved "https://registry.yarnpkg.com/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.9.5.tgz#5bf25c91ad2d7b38f264b12275e5c92a66d849b0"
|
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"
|
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
|
||||||
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
|
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
|
||||||
|
|
||||||
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.4.0:
|
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.9:
|
||||||
version "4.11.9"
|
version "4.12.0"
|
||||||
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828"
|
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88"
|
||||||
integrity sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==
|
integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==
|
||||||
|
|
||||||
bn.js@^5.0.0, bn.js@^5.1.1:
|
bn.js@^5.0.0, bn.js@^5.1.1:
|
||||||
version "5.1.3"
|
version "5.1.3"
|
||||||
|
@ -2711,7 +2723,7 @@ braces@~3.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
fill-range "^7.0.1"
|
fill-range "^7.0.1"
|
||||||
|
|
||||||
brorand@^1.0.1:
|
brorand@^1.0.1, brorand@^1.1.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
|
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
|
||||||
integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=
|
integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=
|
||||||
|
@ -4084,6 +4096,13 @@ dom-serializer@0:
|
||||||
domelementtype "^2.0.1"
|
domelementtype "^2.0.1"
|
||||||
entities "^2.0.0"
|
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:
|
domain-browser@^1.1.1:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
|
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==
|
integrity sha512-ctRyXD9y0mZu8pgeNwBUhLP3Guyr5YuqkfLKYmpTwYx7o9JtCEJme9JVX4xBXPr5ZNvr/iBXUvHLFEVJQThATg==
|
||||||
|
|
||||||
elliptic@^6.5.3:
|
elliptic@^6.5.3:
|
||||||
version "6.5.3"
|
version "6.5.4"
|
||||||
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6"
|
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb"
|
||||||
integrity sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==
|
integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
bn.js "^4.4.0"
|
bn.js "^4.11.9"
|
||||||
brorand "^1.0.1"
|
brorand "^1.1.0"
|
||||||
hash.js "^1.0.0"
|
hash.js "^1.0.0"
|
||||||
hmac-drbg "^1.0.0"
|
hmac-drbg "^1.0.1"
|
||||||
inherits "^2.0.1"
|
inherits "^2.0.4"
|
||||||
minimalistic-assert "^1.0.0"
|
minimalistic-assert "^1.0.1"
|
||||||
minimalistic-crypto-utils "^1.0.0"
|
minimalistic-crypto-utils "^1.0.1"
|
||||||
|
|
||||||
emoji-regex@^7.0.1, emoji-regex@^7.0.2:
|
emoji-regex@^7.0.1, emoji-regex@^7.0.2:
|
||||||
version "7.0.3"
|
version "7.0.3"
|
||||||
|
@ -5457,7 +5476,7 @@ history@^4.9.0:
|
||||||
tiny-warning "^1.0.0"
|
tiny-warning "^1.0.0"
|
||||||
value-equal "^1.0.1"
|
value-equal "^1.0.1"
|
||||||
|
|
||||||
hmac-drbg@^1.0.0:
|
hmac-drbg@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
|
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
|
||||||
integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=
|
integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=
|
||||||
|
@ -5772,9 +5791,9 @@ inherits@2.0.3:
|
||||||
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
|
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
|
||||||
|
|
||||||
ini@^1.3.5:
|
ini@^1.3.5:
|
||||||
version "1.3.5"
|
version "1.3.8"
|
||||||
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
|
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
|
||||||
integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
|
integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
|
||||||
|
|
||||||
inquirer@7.0.4:
|
inquirer@7.0.4:
|
||||||
version "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"
|
resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
|
||||||
integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==
|
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"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
|
resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
|
||||||
integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=
|
integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=
|
||||||
|
@ -9442,6 +9461,14 @@ react-sortable-hoc@^1.11:
|
||||||
invariant "^2.2.4"
|
invariant "^2.2.4"
|
||||||
prop-types "^15.5.7"
|
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:
|
react@^17.0.1:
|
||||||
version "17.0.1"
|
version "17.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/react/-/react-17.0.1.tgz#6e0600416bd57574e3f86d92edba3d9008726127"
|
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"
|
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
|
||||||
integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
|
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:
|
resize-sensor@^0.0.6:
|
||||||
version "0.0.6"
|
version "0.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/resize-sensor/-/resize-sensor-0.0.6.tgz#75147dcb273de6832760e461d2e28de6dcf88c45"
|
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"
|
safer-buffer "^2.0.2"
|
||||||
tweetnacl "~0.14.0"
|
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:
|
ssri@^6.0.1:
|
||||||
version "6.0.1"
|
version "6.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.1.tgz#2a3c41b28dd45b62b63676ecb74001265ae9edd8"
|
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"
|
unquote "~1.1.1"
|
||||||
util.promisify "~1.0.0"
|
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:
|
symbol-observable@^1.2.0:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
|
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue