1
0
Fork 0
mirror of https://github.com/muerwre/vault-frontend.git synced 2025-04-25 04:46:40 +07:00

Merge pull request #37 from muerwre/master

Update dev
This commit is contained in:
muerwre 2021-03-06 15:47:20 +07:00 committed by GitHub
commit 5ba36976eb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 740 additions and 441 deletions

View file

@ -34,6 +34,7 @@
"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",
@ -71,6 +72,7 @@
"@types/ramda": "^0.26.33", "@types/ramda": "^0.26.33",
"@types/react-redux": "^7.1.11", "@types/react-redux": "^7.1.11",
"@types/yup": "^0.29.11", "@types/yup": "^0.29.11",
"@types/swiper": "^5.4.2",
"craco-alias": "^2.1.1", "craco-alias": "^2.1.1",
"craco-fast-refresh": "^1.0.2", "craco-fast-refresh": "^1.0.2",
"prettier": "^1.18.2" "prettier": "^1.18.2"

View file

@ -11,22 +11,22 @@ interface IProps extends DetailsHTMLAttributes<HTMLDivElement> {}
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 (

View file

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

View 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 styles from '~/containers/node/NodeLayout/styles.module.scss';
import { NodeCommentsBlock } from '~/components/node/NodeCommentsBlock';
import { NodeCommentForm } from '~/components/node/NodeCommentForm';
import { Sticky } from '~/components/containers/Sticky';
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';
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}>
<Sticky>
<Group style={{ flex: 1, minWidth: 0 }}>
<NodeTagsBlock node={node} isLoading={isLoading} />
<NodeRelatedBlock isLoading={isLoading} node={node} related={related} />
</Group>
</Sticky>
</div>
</Group>
</Padder>
</Group>
);
};
export { NodeBottomBlock };

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

View file

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

View file

@ -0,0 +1,94 @@
import React, { FC, useCallback, useEffect, useState } from 'react';
import { INodeComponentProps } from '~/redux/node/constants';
import SwiperCore, { A11y, Pagination, SwiperOptions } 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 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([Pagination, A11y]);
interface IProps extends INodeComponentProps {}
const breakpoints: SwiperOptions['breakpoints'] = {
599: {
spaceBetween: 20,
},
};
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);
}, [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
onInit={resetSwiper}
>
{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 };

View file

@ -0,0 +1,73 @@
@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;
}
}
.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;
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;
}
}

View file

@ -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); return (
const getPlace = useCallback(() => { <div className={styles.place}>
if (!ref.current) return; <NodePanelInner
node={node}
const { bottom } = ref.current!.getBoundingClientRect(); onEdit={onEdit}
onLike={onLike}
setStack(bottom > window.innerHeight); onStar={onStar}
}, [ref]); onLock={onLock}
canEdit={can_edit}
useEffect(() => getPlace(), [layout]); canLike={can_like}
canStar={can_star}
useEffect(() => { isLoading={!!isLoading}
window.addEventListener('scroll', getPlace); />
window.addEventListener('resize', getPlace); </div>
);
return () => { }, shallowEqual);
window.removeEventListener('scroll', getPlace);
window.removeEventListener('resize', getPlace);
};
}, [layout, getPlace]);
return (
<div className={styles.place} ref={ref}>
{/*
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
node={node}
onEdit={onEdit}
onLike={onLike}
onStar={onStar}
onLock={onLock}
can_edit={can_edit}
can_like={can_like}
can_star={can_star}
is_loading={!!is_loading}
/>
</div>
);
}
);
export { NodePanel }; export { NodePanel };

View file

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

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

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

View file

@ -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,28 +62,30 @@ const FlowLayoutUnconnected: FC<IProps> = ({
}, []); }, []);
return ( return (
<div className={styles.grid}> <Container>
<div className={styles.hero}> <div className={styles.grid}>
<FlowHero heroes={heroes} /> <div className={styles.hero}>
</div> <FlowHero heroes={heroes} />
</div>
<div className={styles.stamp}> <div className={styles.stamp}>
<FlowStamp <FlowStamp
recent={recent} recent={recent}
updated={updated} updated={updated}
search={search} search={search}
flowChangeSearch={flowChangeSearch} flowChangeSearch={flowChangeSearch}
onLoadMore={onLoadMoreSearch} onLoadMore={onLoadMoreSearch}
/>
</div>
<FlowGrid
nodes={nodes}
user={user}
onSelect={nodeGotoNode}
onChangeCellView={flowSetCellView}
/> />
</div> </div>
</Container>
<FlowGrid
nodes={nodes}
user={user}
onSelect={nodeGotoNode}
onChangeCellView={flowSetCellView}
/>
</div>
); );
}; };

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

View file

@ -0,0 +1,12 @@
@import "~/styles/variables.scss";
.container {
width: 100%;
max-width: $content_width;
margin: auto;
padding: 0 $gap;
@include tablet {
padding: 0;
}
}

View file

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

View file

@ -19,6 +19,7 @@ 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';
type IProps = {}; type IProps = {};
@ -55,59 +56,61 @@ const BorisLayout: FC<IProps> = () => {
}, [dispatch]); }, [dispatch]);
return ( return (
<div className={styles.wrap}> <Container>
<div className={styles.cover} /> <div className={styles.wrap}>
<div className={styles.cover} />
<div className={styles.image}> <div className={styles.image}>
<div className={styles.caption}> <div className={styles.caption}>
<div className={styles.caption_text}>{title}</div> <div className={styles.caption_text}>{title}</div>
</div>
<img src={boris} alt="Борис" />
</div> </div>
<img src={boris} alt="Борис" /> <div className={styles.container}>
</div> <Card className={styles.content}>
<Group className={styles.grid}>
{user.is_user && <NodeCommentForm isBefore nodeId={node.current.id} />}
<div className={styles.container}> {node.is_loading_comments ? (
<Card className={styles.content}> <NodeNoComments is_loading count={7} />
<Group className={styles.grid}> ) : (
{user.is_user && <NodeCommentForm isBefore nodeId={node.current.id} />} <NodeComments
comments={comments}
{node.is_loading_comments ? ( count={node.comment_count}
<NodeNoComments is_loading count={7} /> user={user}
) : ( order="ASC"
<NodeComments />
comments={comments} )}
count={node.comment_count}
user={user}
order="ASC"
/>
)}
</Group>
<Footer />
</Card>
<Group className={styles.stats}>
<Sticky>
<Group className={styles.stats__container}>
<div className={styles.stats__about}>
<h4>Господи-боженьки, где это я?</h4>
<p>
Всё впорядке, это &mdash; главный штаб Суицидальных Роботов, строителей Убежища.
</p>
<p>Здесь мы сидим и слушаем всё, что вас беспокоит.</p>
<p>Все виновные будут наказаны. Невиновные, впрочем, тоже. </p>
<p className="grey">//&nbsp;Такова&nbsp;жизнь.</p>
</div>
<div className={styles.stats__wrap}>
<BorisStats stats={stats} />
</div>
</Group> </Group>
</Sticky>
</Group> <Footer />
</Card>
<Group className={styles.stats}>
<Sticky>
<Group className={styles.stats__container}>
<div className={styles.stats__about}>
<h4>Господи-боженьки, где это я?</h4>
<p>
Всё впорядке, это &mdash; главный штаб Суицидальных Роботов, строителей Убежища.
</p>
<p>Здесь мы сидим и слушаем всё, что вас беспокоит.</p>
<p>Все виновные будут наказаны. Невиновные, впрочем, тоже. </p>
<p className="grey">//&nbsp;Такова&nbsp;жизнь.</p>
</div>
<div className={styles.stats__wrap}>
<BorisStats stats={stats} />
</div>
</Group>
</Sticky>
</Group>
</div>
</div> </div>
</div> </Container>
); );
}; };

View file

@ -1,249 +1,67 @@
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 (
<> <>
{!!head && createNodeBlock(head)} {head}
<Card className={styles.node} seamless> <Container>
{!!block && createNodeBlock(block)} <Card className={styles.node} seamless>
{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> comments={comments}
<Padder> isLoading={is_loading}
<Group horizontal className={styles.content}> commentsCount={comment_count}
<Group className={styles.comments}> commentsOrder="DESC"
{inline && <div className={styles.inline}>{createNodeBlock(inline)}</div>} related={related}
/>
{is_loading || is_loading_comments || (!comments.length && !inline) ? ( <Footer />
<NodeNoComments is_loading={is_loading_comments || is_loading} /> </Card>
) : ( </Container>
<NodeComments
count={comment_count}
comments={comments}
user={user}
order="DESC"
/>
)}
{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 />
</Card>
<SidebarRouter prefix="/post:id" /> <SidebarRouter prefix="/post:id" />
</> </>
@ -251,6 +69,4 @@ const NodeLayoutUnconnected: FC<IProps> = memo(
} }
); );
const NodeLayout = connect(mapStateToProps, mapDispatchToProps)(NodeLayoutUnconnected); export { NodeLayout };
export { NodeLayout, NodeLayoutUnconnected };

View file

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

View file

@ -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,9 +11,9 @@ 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 { NodeImageSwiperBlock } from '~/components/node/NodeImageSwiperBlock';
const prefix = 'NODE.'; const prefix = 'NODE.';
export const NODE_ACTIONS = { export const NODE_ACTIONS = {
@ -76,17 +75,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 = {

View file

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

View file

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

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

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

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

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

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

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

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

View file

@ -0,0 +1,7 @@
import { useEffect } from 'react';
export const useScrollToTop = (deps?: any[]) => {
useEffect(() => {
window.scrollTo(0, 0);
}, deps || []);
};

View file

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

View file

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

View file

@ -1761,6 +1761,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"
@ -4084,6 +4089,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"
@ -5772,9 +5784,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"
@ -10449,6 +10461,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 +10806,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"