mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-24 20:36:40 +07:00
let users like comments
This commit is contained in:
parent
822f51f5de
commit
bd802ede10
22 changed files with 332 additions and 154 deletions
|
@ -13,20 +13,22 @@ import { CommendDeleted } from '../../node/CommendDeleted';
|
|||
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
type IProps = HTMLAttributes<HTMLDivElement> & {
|
||||
type Props = HTMLAttributes<HTMLDivElement> & {
|
||||
nodeId: number;
|
||||
isEmpty?: boolean;
|
||||
isLoading?: boolean;
|
||||
group: ICommentGroup;
|
||||
isSame?: boolean;
|
||||
canEdit?: boolean;
|
||||
canLike?: boolean;
|
||||
highlighted?: boolean;
|
||||
saveComment: (data: IComment) => Promise<IComment | undefined>;
|
||||
onDelete: (id: IComment['id'], isLocked: boolean) => void;
|
||||
onLike: (id: IComment['id'], isLiked: boolean) => void;
|
||||
onShowImageModal: (images: IFile[], index: number) => void;
|
||||
};
|
||||
|
||||
const Comment: FC<IProps> = memo(
|
||||
const Comment: FC<Props> = memo(
|
||||
({
|
||||
group,
|
||||
nodeId,
|
||||
|
@ -36,7 +38,9 @@ const Comment: FC<IProps> = memo(
|
|||
className,
|
||||
highlighted,
|
||||
canEdit,
|
||||
canLike,
|
||||
onDelete,
|
||||
onLike,
|
||||
onShowImageModal,
|
||||
saveComment,
|
||||
...props
|
||||
|
@ -84,10 +88,12 @@ const Comment: FC<IProps> = memo(
|
|||
saveComment={saveComment}
|
||||
nodeId={nodeId}
|
||||
comment={comment}
|
||||
key={comment.id}
|
||||
canEdit={!!canEdit}
|
||||
onDelete={onDelete}
|
||||
canLike={!!canLike}
|
||||
onLike={() => onLike(comment.id, !comment.liked)}
|
||||
onDelete={(val: boolean) => onDelete(comment.id, val)}
|
||||
onShowImageModal={onShowImageModal}
|
||||
key={comment.id}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import React, {
|
||||
import {
|
||||
createElement,
|
||||
FC,
|
||||
Fragment,
|
||||
memo,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
|
@ -11,7 +10,6 @@ import React, {
|
|||
|
||||
import classnames from 'classnames';
|
||||
|
||||
import { Authorized } from '~/components/containers/Authorized';
|
||||
import { Group } from '~/components/containers/Group';
|
||||
import { AudioPlayer } from '~/components/media/AudioPlayer';
|
||||
import { COMMENT_BLOCK_RENDERERS } from '~/constants/comment';
|
||||
|
@ -22,6 +20,7 @@ import { append, assocPath, path, reduce } from '~/utils/ramda';
|
|||
|
||||
import { CommentEditingForm } from '../CommentEditingForm';
|
||||
import { CommentImageGrid } from '../CommentImageGrid';
|
||||
import { CommentLike } from '../CommentLike';
|
||||
import { CommentMenu } from '../CommentMenu';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
|
@ -31,17 +30,21 @@ interface IProps {
|
|||
nodeId: number;
|
||||
comment: IComment;
|
||||
canEdit: boolean;
|
||||
canLike: boolean;
|
||||
saveComment: (data: IComment) => Promise<IComment | undefined>;
|
||||
onDelete: (id: IComment['id'], isLocked: boolean) => void;
|
||||
onDelete: (isLocked: boolean) => void;
|
||||
onLike: () => void;
|
||||
onShowImageModal: (images: IFile[], index: number) => void;
|
||||
}
|
||||
|
||||
const CommentContent: FC<IProps> = memo(
|
||||
({
|
||||
comment,
|
||||
canEdit,
|
||||
nodeId,
|
||||
saveComment,
|
||||
canEdit,
|
||||
canLike,
|
||||
onLike,
|
||||
onDelete,
|
||||
onShowImageModal,
|
||||
prefix,
|
||||
|
@ -65,7 +68,7 @@ const CommentContent: FC<IProps> = memo(
|
|||
);
|
||||
|
||||
const onLockClick = useCallback(() => {
|
||||
onDelete(comment.id, !comment.deleted_at);
|
||||
onDelete(!comment.deleted_at);
|
||||
}, [comment, onDelete]);
|
||||
|
||||
const onImageClick = useCallback(
|
||||
|
@ -75,15 +78,12 @@ const CommentContent: FC<IProps> = memo(
|
|||
);
|
||||
|
||||
const menu = useMemo(
|
||||
() => (
|
||||
<div>
|
||||
{canEdit && (
|
||||
<Authorized>
|
||||
<CommentMenu onDelete={onLockClick} onEdit={startEditing} />
|
||||
</Authorized>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
() =>
|
||||
canEdit && (
|
||||
<div className={styles.menu}>
|
||||
<CommentMenu onDelete={onLockClick} onEdit={startEditing} />
|
||||
</div>
|
||||
),
|
||||
[canEdit, startEditing, onLockClick],
|
||||
);
|
||||
|
||||
|
@ -110,57 +110,56 @@ const CommentContent: FC<IProps> = memo(
|
|||
<div className={styles.wrap}>
|
||||
{!!prefix && <div className={styles.prefix}>{prefix}</div>}
|
||||
|
||||
{comment.text.trim() && (
|
||||
<Group className={classnames(styles.block, styles.block_text)}>
|
||||
{menu}
|
||||
<div className={styles.content}>
|
||||
{menu}
|
||||
|
||||
<Group className={styles.renderers}>
|
||||
{blocks.map(
|
||||
(block, key) =>
|
||||
COMMENT_BLOCK_RENDERERS[block.type] &&
|
||||
createElement(COMMENT_BLOCK_RENDERERS[block.type], {
|
||||
block,
|
||||
key,
|
||||
}),
|
||||
)}
|
||||
</Group>
|
||||
<div>
|
||||
{comment.text.trim() && (
|
||||
<Group className={classnames(styles.block, styles.block_text)}>
|
||||
<Group className={styles.renderers}>
|
||||
{blocks.map(
|
||||
(block, key) =>
|
||||
COMMENT_BLOCK_RENDERERS[block.type] &&
|
||||
createElement(COMMENT_BLOCK_RENDERERS[block.type], {
|
||||
block,
|
||||
key,
|
||||
}),
|
||||
)}
|
||||
</Group>
|
||||
</Group>
|
||||
)}
|
||||
|
||||
<div className={styles.date}>
|
||||
{getPrettyDate(comment.created_at)}
|
||||
</div>
|
||||
</Group>
|
||||
)}
|
||||
|
||||
{groupped.image && groupped.image.length > 0 && (
|
||||
<div className={classnames(styles.block, styles.block_image)}>
|
||||
{menu}
|
||||
|
||||
<CommentImageGrid files={groupped.image} onClick={onImageClick} />
|
||||
|
||||
<div className={styles.date}>
|
||||
{getPrettyDate(comment.created_at)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{groupped.audio && groupped.audio.length > 0 && (
|
||||
<Fragment>
|
||||
{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>
|
||||
{groupped.image && groupped.image.length > 0 && (
|
||||
<div className={classnames(styles.block, styles.block_image)}>
|
||||
<CommentImageGrid
|
||||
files={groupped.image}
|
||||
onClick={onImageClick}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</Fragment>
|
||||
)}
|
||||
)}
|
||||
|
||||
{groupped.audio &&
|
||||
groupped.audio.length > 0 &&
|
||||
groupped.audio.map((file) => (
|
||||
<div
|
||||
className={classnames(styles.block, styles.block_audio)}
|
||||
key={file.id}
|
||||
>
|
||||
<AudioPlayer file={file} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.date}>
|
||||
{getPrettyDate(comment.created_at)}
|
||||
<CommentLike
|
||||
onLike={onLike}
|
||||
count={comment.like_count}
|
||||
active={canLike}
|
||||
liked={comment.liked}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -54,7 +54,14 @@
|
|||
top: 28px;
|
||||
}
|
||||
|
||||
.content {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.block {
|
||||
@include row_shadow;
|
||||
|
||||
min-height: $comment_height;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
@ -90,13 +97,7 @@
|
|||
}
|
||||
|
||||
.block_image {
|
||||
padding-bottom: 0 !important;
|
||||
|
||||
.date {
|
||||
background: $content_bg;
|
||||
border-radius: $radius 0 $radius 0;
|
||||
color: $gray_25;
|
||||
}
|
||||
padding: $gap / 2;
|
||||
}
|
||||
|
||||
.block_text {
|
||||
|
@ -105,16 +106,20 @@
|
|||
|
||||
.date {
|
||||
position: absolute;
|
||||
bottom: 1px;
|
||||
bottom: 1px; // should not cover block shadow
|
||||
right: 0;
|
||||
font: $font_12_regular;
|
||||
font: $font_12_medium;
|
||||
color: $gray_75;
|
||||
padding: 0 6px 2px;
|
||||
fill: $gray_75;
|
||||
padding: 3px 6px;
|
||||
z-index: 2;
|
||||
background: $content_bg_light;
|
||||
border-radius: 4px;
|
||||
pointer-events: none;
|
||||
touch-action: none;
|
||||
user-select: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.audios {
|
||||
|
@ -141,3 +146,13 @@
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.menu {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
z-index: 10;
|
||||
outline: none;
|
||||
}
|
||||
|
|
44
src/components/comment/CommentLike/index.tsx
Normal file
44
src/components/comment/CommentLike/index.tsx
Normal file
|
@ -0,0 +1,44 @@
|
|||
import React, { FC } from 'react';
|
||||
|
||||
import classnames from 'classnames';
|
||||
|
||||
import { Icon } from '~/components/input/Icon';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
interface CommentLikeProps {
|
||||
className?: string;
|
||||
count?: number;
|
||||
active?: boolean;
|
||||
liked?: boolean;
|
||||
onLike?: () => void;
|
||||
}
|
||||
|
||||
const CommentLike: FC<CommentLikeProps> = ({
|
||||
className,
|
||||
count,
|
||||
active,
|
||||
liked,
|
||||
onLike,
|
||||
}) => {
|
||||
if (!active && !count) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={active ? onLike : undefined}
|
||||
className={classnames(styles.likes, className, {
|
||||
[styles.liked]: active && liked,
|
||||
[styles.active]: active,
|
||||
})}
|
||||
>
|
||||
<div className={styles.icon}>
|
||||
<Icon icon={count ? 'heart_full' : 'heart'} size={14} />
|
||||
</div>
|
||||
|
||||
{Boolean(count) && <span className={styles.count}>{count}</span>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export { CommentLike };
|
34
src/components/comment/CommentLike/styles.module.scss
Normal file
34
src/components/comment/CommentLike/styles.module.scss
Normal file
|
@ -0,0 +1,34 @@
|
|||
@import 'src/styles/variables';
|
||||
|
||||
.likes {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
position: relative;
|
||||
|
||||
&.active {
|
||||
cursor: pointer;
|
||||
|
||||
&::before {
|
||||
content: ' ';
|
||||
position: absolute;
|
||||
inset: -10px -10px -8px -16px;
|
||||
}
|
||||
|
||||
&:not(.liked):hover .icon {
|
||||
@include pulse;
|
||||
}
|
||||
}
|
||||
|
||||
&.liked {
|
||||
color: $color_like;
|
||||
fill: currentColor;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
position: relative;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
|
@ -1,13 +1,6 @@
|
|||
@import 'src/styles/variables';
|
||||
|
||||
.wrap {
|
||||
position: absolute;
|
||||
right: -3px;
|
||||
top: -3px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
z-index: 10;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ const NodeLikeButton: FC<NodeLikeButtonProps> = ({
|
|||
[styles.is_liked]: active,
|
||||
})}
|
||||
>
|
||||
{active ? (
|
||||
{count ? (
|
||||
<Icon icon="heart_full" size={24} onClick={onClick} />
|
||||
) : (
|
||||
<Icon icon="heart" size={24} onClick={onClick} />
|
||||
|
|
|
@ -1,31 +1,5 @@
|
|||
@import 'src/styles/variables';
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
45% {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
60% {
|
||||
transform: scale(1.4);
|
||||
}
|
||||
|
||||
75% {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
90% {
|
||||
transform: scale(1.4);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.like {
|
||||
transition: fill, stroke 0.25s;
|
||||
will-change: transform;
|
||||
|
@ -46,8 +20,9 @@
|
|||
}
|
||||
|
||||
&:hover {
|
||||
@include pulse;
|
||||
|
||||
fill: $color_like;
|
||||
animation: pulse 0.75s infinite;
|
||||
|
||||
.count {
|
||||
opacity: 0;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue