1
0
Fork 0
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:
Fedor Katurov 2019-12-03 15:02:03 +07:00
parent ab898cc40c
commit 1bf9fe6b83
14 changed files with 319 additions and 98 deletions

View file

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

View file

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

View file

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

View file

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

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

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

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

View file

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