mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-25 21:06:42 +07:00
comment menu
This commit is contained in:
parent
ab898cc40c
commit
1bf9fe6b83
14 changed files with 319 additions and 98 deletions
|
@ -3,18 +3,34 @@ import { CommentWrapper } from '~/components/containers/CommentWrapper';
|
|||
import { ICommentGroup, IComment } from '~/redux/types';
|
||||
import { CommentContent } from '~/components/node/CommentContent';
|
||||
import * as styles from './styles.scss';
|
||||
import { nodeLockComment, nodeEditComment } from '~/redux/node/actions';
|
||||
import { INodeState } from '~/redux/node/reducer';
|
||||
import { CommentForm } from '../CommentForm';
|
||||
|
||||
type IProps = HTMLAttributes<HTMLDivElement> & {
|
||||
is_empty?: boolean;
|
||||
is_loading?: boolean;
|
||||
comment_group?: ICommentGroup;
|
||||
comment_data: INodeState['comment_data'];
|
||||
is_same?: boolean;
|
||||
can_edit?: boolean;
|
||||
onDelete: (id: IComment['id'], is_deteted: boolean) => void;
|
||||
onDelete: typeof nodeLockComment;
|
||||
onEdit: typeof nodeEditComment;
|
||||
};
|
||||
|
||||
const Comment: FC<IProps> = memo(
|
||||
({ comment_group, is_empty, is_same, is_loading, className, can_edit, onDelete, ...props }) => {
|
||||
({
|
||||
comment_group,
|
||||
comment_data,
|
||||
is_empty,
|
||||
is_same,
|
||||
is_loading,
|
||||
className,
|
||||
can_edit,
|
||||
onDelete,
|
||||
onEdit,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<CommentWrapper
|
||||
className={className}
|
||||
|
@ -25,18 +41,25 @@ const Comment: FC<IProps> = memo(
|
|||
{...props}
|
||||
>
|
||||
<div className={styles.wrap}>
|
||||
{comment_group.comments.map(comment =>
|
||||
comment.deleted_at ? (
|
||||
<div key={comment.id}>deleted</div>
|
||||
) : (
|
||||
{comment_group.comments.map(comment => {
|
||||
if (comment.deleted_at) {
|
||||
return <div key={comment.id}>deleted</div>;
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(comment_data, comment.id)) {
|
||||
return <CommentForm id={comment.id} key={comment.id} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<CommentContent
|
||||
comment={comment}
|
||||
key={comment.id}
|
||||
can_edit={can_edit}
|
||||
onDelete={onDelete}
|
||||
onEdit={onEdit}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CommentWrapper>
|
||||
);
|
||||
|
|
|
@ -13,14 +13,17 @@ import classnames from 'classnames';
|
|||
import { PRESETS } from '~/constants/urls';
|
||||
import { COMMENT_BLOCK_RENDERERS } from '~/constants/comment';
|
||||
import { Icon } from '~/components/input/Icon';
|
||||
import { nodeLockComment, nodeEditComment } from '~/redux/node/actions';
|
||||
import { CommentMenu } from '../CommentMenu';
|
||||
|
||||
interface IProps {
|
||||
comment: IComment;
|
||||
can_edit: boolean;
|
||||
onDelete: (id: IComment['id'], is_deteted: boolean) => void;
|
||||
onDelete: typeof nodeLockComment;
|
||||
onEdit: typeof nodeEditComment;
|
||||
}
|
||||
|
||||
const CommentContent: FC<IProps> = memo(({ comment, can_edit, onDelete }) => {
|
||||
const CommentContent: FC<IProps> = memo(({ comment, can_edit, onDelete, onEdit }) => {
|
||||
const groupped = useMemo<Record<keyof typeof UPLOAD_TYPES, IFile[]>>(
|
||||
() =>
|
||||
reduce(
|
||||
|
@ -35,23 +38,20 @@ const CommentContent: FC<IProps> = memo(({ comment, can_edit, onDelete }) => {
|
|||
onDelete(comment.id, !comment.deleted_at);
|
||||
}, [comment, onDelete]);
|
||||
|
||||
const lock = useMemo(
|
||||
() =>
|
||||
can_edit ? (
|
||||
<div className={styles.lock} onClick={onLockClick}>
|
||||
<div>
|
||||
<Icon icon="close" />
|
||||
</div>
|
||||
</div>
|
||||
) : null,
|
||||
[can_edit, comment]
|
||||
const onEditClick = useCallback(() => {
|
||||
onEdit(comment.id);
|
||||
}, [comment, onEdit]);
|
||||
|
||||
const menu = useMemo(
|
||||
() => can_edit && <CommentMenu onDelete={onLockClick} onEdit={onEditClick} />,
|
||||
[can_edit, comment, onEditClick, onLockClick]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.wrap}>
|
||||
{comment.text && (
|
||||
<Group className={classnames(styles.block, styles.block_text)}>
|
||||
{lock}
|
||||
{menu}
|
||||
|
||||
{formatCommentText(path(['user', 'username'], comment), comment.text).map(
|
||||
(block, key) =>
|
||||
|
@ -65,7 +65,7 @@ const CommentContent: FC<IProps> = memo(({ comment, can_edit, onDelete }) => {
|
|||
|
||||
{groupped.image && groupped.image.length > 0 && (
|
||||
<div className={classnames(styles.block, styles.block_image)}>
|
||||
{lock}
|
||||
{menu}
|
||||
|
||||
<div className={styles.images}>
|
||||
{groupped.image.map(file => (
|
||||
|
@ -83,6 +83,8 @@ const CommentContent: FC<IProps> = memo(({ comment, can_edit, onDelete }) => {
|
|||
<>
|
||||
{groupped.audio.map(file => (
|
||||
<div className={classnames(styles.block, styles.block_audio)} key={file.id}>
|
||||
{menu}
|
||||
|
||||
<AudioPlayer file={file} />
|
||||
|
||||
<div className={styles.date}>{getPrettyDate(comment.created_at)}</div>
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
position: relative;
|
||||
}
|
||||
|
||||
.lock {
|
||||
.lock,
|
||||
.edit {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
|
@ -21,6 +22,7 @@
|
|||
transition: opacity 0.25s, transform 0.25s;
|
||||
cursor: pointer;
|
||||
background: $red;
|
||||
z-index: 2;
|
||||
|
||||
& > div {
|
||||
width: 20px;
|
||||
|
@ -46,6 +48,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
.edit {
|
||||
top: 28px;
|
||||
background: blue;
|
||||
}
|
||||
|
||||
.block {
|
||||
@include outer_shadow();
|
||||
min-height: $comment_height;
|
||||
|
@ -68,7 +75,8 @@
|
|||
}
|
||||
|
||||
&:hover {
|
||||
.lock {
|
||||
.lock,
|
||||
.edit {
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
touch-action: initial;
|
||||
|
|
|
@ -23,6 +23,7 @@ import { SortableImageGrid } from '~/components/editors/SortableImageGrid';
|
|||
import { moveArrItem } from '~/utils/fn';
|
||||
import { SortEnd } from 'react-sortable-hoc';
|
||||
import { SortableAudioGrid } from '~/components/editors/SortableAudioGrid';
|
||||
import { NodeCommentForm } from '../NodeCommentForm';
|
||||
|
||||
const mapStateToProps = (state: IState) => ({
|
||||
node: selectNode(state),
|
||||
|
@ -188,68 +189,66 @@ const CommentFormUnconnected: FC<IProps> = ({
|
|||
);
|
||||
|
||||
return (
|
||||
<CommentWrapper user={user}>
|
||||
<form onSubmit={onSubmit} className={styles.wrap}>
|
||||
<div className={styles.input}>
|
||||
<Textarea
|
||||
value={comment.text}
|
||||
handler={onInput}
|
||||
onKeyDown={onKeyDown}
|
||||
disabled={is_sending_comment}
|
||||
minRows={2}
|
||||
/>
|
||||
<form onSubmit={onSubmit} className={styles.wrap}>
|
||||
<div className={styles.input}>
|
||||
<Textarea
|
||||
value={comment.text}
|
||||
handler={onInput}
|
||||
onKeyDown={onKeyDown}
|
||||
disabled={is_sending_comment}
|
||||
minRows={2}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{(!!images.length || !!audios.length) && (
|
||||
<div className={styles.attaches}>
|
||||
{!!images.length && (
|
||||
<SortableImageGrid
|
||||
onDrop={onFileDrop}
|
||||
onSortEnd={onImageMove}
|
||||
axis="xy"
|
||||
items={images}
|
||||
locked={locked_images}
|
||||
pressDelay={50}
|
||||
helperClass={styles.helper}
|
||||
size={120}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!!audios.length && (
|
||||
<SortableAudioGrid
|
||||
items={audios}
|
||||
onDrop={onFileDrop}
|
||||
onSortEnd={onAudioMove}
|
||||
axis="y"
|
||||
locked={[]}
|
||||
pressDelay={50}
|
||||
helperClass={styles.helper}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(!!images.length || !!audios.length) && (
|
||||
<div className={styles.attaches}>
|
||||
{!!images.length && (
|
||||
<SortableImageGrid
|
||||
onDrop={onFileDrop}
|
||||
onSortEnd={onImageMove}
|
||||
axis="xy"
|
||||
items={images}
|
||||
locked={locked_images}
|
||||
pressDelay={50}
|
||||
helperClass={styles.helper}
|
||||
size={120}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!!audios.length && (
|
||||
<SortableAudioGrid
|
||||
items={audios}
|
||||
onDrop={onFileDrop}
|
||||
onSortEnd={onAudioMove}
|
||||
axis="y"
|
||||
locked={[]}
|
||||
pressDelay={50}
|
||||
helperClass={styles.helper}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Group horizontal className={styles.buttons}>
|
||||
<ButtonGroup>
|
||||
<Button iconLeft="image" size="small" color="gray" iconOnly>
|
||||
<input type="file" onInput={onInputChange} multiple accept="image/*" />
|
||||
</Button>
|
||||
|
||||
<Button iconRight="enter" size="small" color="gray" iconOnly>
|
||||
<input type="file" onInput={onInputChange} multiple accept="audio/*" />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
|
||||
<Filler />
|
||||
|
||||
{is_sending_comment && <LoaderCircle size={20} />}
|
||||
|
||||
<Button size="small" color="gray" iconRight="enter" disabled={is_sending_comment}>
|
||||
Сказать
|
||||
<Group horizontal className={styles.buttons}>
|
||||
<ButtonGroup>
|
||||
<Button iconLeft="image" size="small" color="gray" iconOnly>
|
||||
<input type="file" onInput={onInputChange} multiple accept="image/*" />
|
||||
</Button>
|
||||
</Group>
|
||||
</form>
|
||||
</CommentWrapper>
|
||||
|
||||
<Button iconRight="enter" size="small" color="gray" iconOnly>
|
||||
<input type="file" onInput={onInputChange} multiple accept="audio/*" />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
|
||||
<Filler />
|
||||
|
||||
{is_sending_comment && <LoaderCircle size={20} />}
|
||||
|
||||
<Button size="small" color="gray" iconRight="enter" disabled={is_sending_comment}>
|
||||
Сказать
|
||||
</Button>
|
||||
</Group>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
20
src/components/node/CommentMenu/index.tsx
Normal file
20
src/components/node/CommentMenu/index.tsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
import React, { FC } from 'react';
|
||||
import styles from './styles.scss';
|
||||
|
||||
interface IProps {
|
||||
onEdit: () => void;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
const CommentMenu: FC<IProps> = ({ onEdit, onDelete }) => {
|
||||
return (
|
||||
<div className={styles.wrap}>
|
||||
<div className={styles.menu}>
|
||||
<div className={styles.item}>Редактировать</div>
|
||||
<div className={styles.item}>Удалить</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { CommentMenu };
|
68
src/components/node/CommentMenu/styles.scss
Normal file
68
src/components/node/CommentMenu/styles.scss
Normal file
|
@ -0,0 +1,68 @@
|
|||
.wrap {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
z-index: 3;
|
||||
|
||||
&:hover {
|
||||
.menu {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: ' ';
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 100%;
|
||||
background: transparentize(black, 0.7);
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes appear {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.menu {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
z-index: 6;
|
||||
|
||||
animation: appear 0.25s forwards;
|
||||
}
|
||||
|
||||
.item {
|
||||
user-select: none;
|
||||
font: $font_12_semibold;
|
||||
text-transform: uppercase;
|
||||
padding: $gap;
|
||||
background: $content_bg;
|
||||
cursor: pointer;
|
||||
|
||||
&:first-child {
|
||||
border-top-left-radius: $radius;
|
||||
border-top-right-radius: $radius;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom-left-radius: $radius;
|
||||
border-bottom-right-radius: $radius;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: $primary;
|
||||
}
|
||||
}
|
36
src/components/node/NodeCommentForm/index.tsx
Normal file
36
src/components/node/NodeCommentForm/index.tsx
Normal file
|
@ -0,0 +1,36 @@
|
|||
import React, { FC, useCallback, KeyboardEventHandler, useEffect, useMemo } from 'react';
|
||||
import { Textarea } from '~/components/input/Textarea';
|
||||
import { CommentWrapper } from '~/components/containers/CommentWrapper';
|
||||
import * as styles from './styles.scss';
|
||||
import { Filler } from '~/components/containers/Filler';
|
||||
import { Button } from '~/components/input/Button';
|
||||
import assocPath from 'ramda/es/assocPath';
|
||||
import { InputHandler, IFileWithUUID, IFile } from '~/redux/types';
|
||||
import { connect } from 'react-redux';
|
||||
import * as NODE_ACTIONS from '~/redux/node/actions';
|
||||
import { selectNode } from '~/redux/node/selectors';
|
||||
import * as UPLOAD_ACTIONS from '~/redux/uploads/actions';
|
||||
import { selectUploads } from '~/redux/uploads/selectors';
|
||||
import { IState } from '~/redux/store';
|
||||
import { selectUser, selectAuthUser } from '~/redux/auth/selectors';
|
||||
import { CommentForm } from '../CommentForm';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
user: selectAuthUser(state),
|
||||
});
|
||||
|
||||
type IProps = ReturnType<typeof mapStateToProps> & {
|
||||
is_before?: boolean;
|
||||
};
|
||||
|
||||
const NodeCommentFormUnconnected: FC<IProps> = ({ user, is_before }) => {
|
||||
return (
|
||||
<CommentWrapper user={user}>
|
||||
<CommentForm id={0} is_before={is_before} />
|
||||
</CommentWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const NodeCommentForm = connect(mapStateToProps)(NodeCommentFormUnconnected);
|
||||
|
||||
export { NodeCommentForm };
|
|
@ -7,14 +7,18 @@ import { ICommentGroup, IComment } from '~/redux/types';
|
|||
import { groupCommentsByUser } from '~/utils/fn';
|
||||
import { IUser } from '~/redux/auth/types';
|
||||
import { canEditComment } from '~/utils/node';
|
||||
import { nodeLockComment, nodeEditComment } from '~/redux/node/actions';
|
||||
import { INodeState } from '~/redux/node/reducer';
|
||||
|
||||
interface IProps {
|
||||
comments?: IComment[];
|
||||
comment_data: INodeState['comment_data'];
|
||||
user: IUser;
|
||||
onDelete: (id: IComment['id'], is_deteted: boolean) => void;
|
||||
onDelete: typeof nodeLockComment;
|
||||
onEdit: typeof nodeEditComment;
|
||||
}
|
||||
|
||||
const NodeComments: FC<IProps> = memo(({ comments, user, onDelete }) => {
|
||||
const NodeComments: FC<IProps> = memo(({ comments, comment_data, user, onDelete, onEdit }) => {
|
||||
const groupped: ICommentGroup[] = useMemo(() => comments.reduce(groupCommentsByUser, []), [
|
||||
comments,
|
||||
]);
|
||||
|
@ -25,8 +29,10 @@ const NodeComments: FC<IProps> = memo(({ comments, user, onDelete }) => {
|
|||
<Comment
|
||||
key={group.ids.join()}
|
||||
comment_group={group}
|
||||
comment_data={comment_data}
|
||||
can_edit={canEditComment(group, user)}
|
||||
onDelete={onDelete}
|
||||
onEdit={onEdit}
|
||||
/>
|
||||
))}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue