Merge branch 'comments_reactions' into 'dev'
Comments rating See merge request discoursio/discoursio-webapp!38
This commit is contained in:
commit
9e69f280cf
|
@ -201,39 +201,6 @@
|
||||||
margin-bottom: 1.2rem;
|
margin-bottom: 1.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.commentRating {
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.commentRatingValue {
|
|
||||||
padding: 0 0.3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.commentRatingPositive {
|
|
||||||
color: #2bb452;
|
|
||||||
}
|
|
||||||
|
|
||||||
.commentRatingNegative {
|
|
||||||
color: #d00820;
|
|
||||||
}
|
|
||||||
|
|
||||||
.commentRatingControl {
|
|
||||||
border-left: 6px solid transparent;
|
|
||||||
border-right: 6px solid transparent;
|
|
||||||
height: 0;
|
|
||||||
width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.commentRatingControlUp {
|
|
||||||
border-bottom: 8px solid rgb(0 0 0 / 40%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.commentRatingControlDown {
|
|
||||||
border-top: 8px solid rgb(0 0 0 / 40%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.compactUserpic {
|
.compactUserpic {
|
||||||
height: 28px;
|
height: 28px;
|
||||||
width: 28px;
|
width: 28px;
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { useReactions } from '../../context/reactions'
|
||||||
import { useSnackbar } from '../../context/snackbar'
|
import { useSnackbar } from '../../context/snackbar'
|
||||||
import { ShowIfAuthenticated } from '../_shared/ShowIfAuthenticated'
|
import { ShowIfAuthenticated } from '../_shared/ShowIfAuthenticated'
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../context/localize'
|
||||||
import Cookie from 'js-cookie'
|
import { CommentRatingControl } from './CommentRatingControl'
|
||||||
|
|
||||||
const CommentEditor = lazy(() => import('../_shared/CommentEditor'))
|
const CommentEditor = lazy(() => import('../_shared/CommentEditor'))
|
||||||
|
|
||||||
|
@ -140,17 +140,7 @@ export const Comment = (props: Props) => {
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<CommentRatingControl comment={comment()} />
|
||||||
class={styles.commentRating}
|
|
||||||
classList={{
|
|
||||||
[styles.commentRatingPositive]: comment().stat.rating > 0,
|
|
||||||
[styles.commentRatingNegative]: comment().stat.rating < 0
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<button class={clsx(styles.commentRatingControl, styles.commentRatingControlUp)} />
|
|
||||||
<div class={styles.commentRatingValue}>{comment().stat.rating || 0}</div>
|
|
||||||
<button class={clsx(styles.commentRatingControl, styles.commentRatingControlDown)} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<div class={styles.commentBody} id={'comment-' + (comment().id || '')}>
|
<div class={styles.commentBody} id={'comment-' + (comment().id || '')}>
|
||||||
|
|
45
src/components/Article/CommentRatingControl.module.scss
Normal file
45
src/components/Article/CommentRatingControl.module.scss
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
.commentRating {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
.commentRatingValue {
|
||||||
|
padding: 0 0.3em;
|
||||||
|
margin: 0 0.6rem;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.3s ease-in-out;
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.commentRatingPositive {
|
||||||
|
color: #2bb452;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commentRatingNegative {
|
||||||
|
color: #d00820;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commentRatingControl {
|
||||||
|
border-left: 6px solid transparent;
|
||||||
|
border-right: 6px solid transparent;
|
||||||
|
height: 0;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commentRatingControlUp {
|
||||||
|
border-bottom: 8px solid rgb(0 0 0 / 40%);
|
||||||
|
&.voted {
|
||||||
|
border-bottom-color: #2bb452;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.commentRatingControlDown {
|
||||||
|
border-top: 8px solid rgb(0 0 0 / 40%);
|
||||||
|
&.voted {
|
||||||
|
border-top-color: #d00820;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
125
src/components/Article/CommentRatingControl.tsx
Normal file
125
src/components/Article/CommentRatingControl.tsx
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
import { clsx } from 'clsx'
|
||||||
|
import styles from './CommentRatingControl.module.scss'
|
||||||
|
import type { Reaction } from '../../graphql/types.gen'
|
||||||
|
import { ReactionKind } from '../../graphql/types.gen'
|
||||||
|
import { useSession } from '../../context/session'
|
||||||
|
import { useReactions } from '../../context/reactions'
|
||||||
|
import { createMemo, For } from 'solid-js'
|
||||||
|
import { loadShout } from '../../stores/zine/articles'
|
||||||
|
import { Popup } from '../_shared/Popup'
|
||||||
|
import { useLocalize } from '../../context/localize'
|
||||||
|
import { useSnackbar } from '../../context/snackbar'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
comment: Reaction
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CommentRatingControl = (props: Props) => {
|
||||||
|
const { t } = useLocalize()
|
||||||
|
const { userSlug } = useSession()
|
||||||
|
const {
|
||||||
|
actions: { showSnackbar }
|
||||||
|
} = useSnackbar()
|
||||||
|
const {
|
||||||
|
reactionEntities,
|
||||||
|
actions: { createReaction, deleteReaction, loadReactionsBy }
|
||||||
|
} = useReactions()
|
||||||
|
|
||||||
|
const checkReaction = (reactionKind: ReactionKind) =>
|
||||||
|
Object.values(reactionEntities).some(
|
||||||
|
(r) =>
|
||||||
|
r.kind === reactionKind &&
|
||||||
|
r.createdBy.slug === userSlug() &&
|
||||||
|
r.shout.id === props.comment.shout.id &&
|
||||||
|
r.replyTo === props.comment.id
|
||||||
|
)
|
||||||
|
const isUpvoted = createMemo(() => checkReaction(ReactionKind.Like))
|
||||||
|
const isDownvoted = createMemo(() => checkReaction(ReactionKind.Dislike))
|
||||||
|
const canVote = createMemo(() => userSlug() !== props.comment.createdBy.slug)
|
||||||
|
|
||||||
|
const shoutRatingReactions = createMemo(() =>
|
||||||
|
Object.values(reactionEntities).filter(
|
||||||
|
(r) =>
|
||||||
|
[ReactionKind.Like, ReactionKind.Dislike].includes(r.kind) &&
|
||||||
|
r.shout.id === props.comment.shout.id &&
|
||||||
|
r.replyTo === props.comment.id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
const deleteCommentReaction = async (reactionKind: ReactionKind) => {
|
||||||
|
const reactionToDelete = Object.values(reactionEntities).find(
|
||||||
|
(r) =>
|
||||||
|
r.kind === reactionKind &&
|
||||||
|
r.createdBy.slug === userSlug() &&
|
||||||
|
r.shout.id === props.comment.shout.id &&
|
||||||
|
r.replyTo === props.comment.id
|
||||||
|
)
|
||||||
|
return deleteReaction(reactionToDelete.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRatingChange = async (isUpvote: boolean) => {
|
||||||
|
try {
|
||||||
|
if (isUpvoted()) {
|
||||||
|
await deleteCommentReaction(ReactionKind.Like)
|
||||||
|
} else if (isDownvoted()) {
|
||||||
|
await deleteCommentReaction(ReactionKind.Dislike)
|
||||||
|
} else {
|
||||||
|
await createReaction({
|
||||||
|
kind: isUpvote ? ReactionKind.Like : ReactionKind.Dislike,
|
||||||
|
shout: props.comment.shout.id,
|
||||||
|
replyTo: props.comment.id
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showSnackbar({ type: 'error', body: t('Error') })
|
||||||
|
}
|
||||||
|
|
||||||
|
await loadShout(props.comment.shout.slug)
|
||||||
|
await loadReactionsBy({
|
||||||
|
by: { shout: props.comment.shout.slug }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class={styles.commentRating}>
|
||||||
|
<button
|
||||||
|
role="button"
|
||||||
|
disabled={!canVote() || !userSlug()}
|
||||||
|
onClick={() => handleRatingChange(true)}
|
||||||
|
class={clsx(styles.commentRatingControl, styles.commentRatingControlUp, {
|
||||||
|
[styles.voted]: isUpvoted()
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<Popup
|
||||||
|
trigger={
|
||||||
|
<div
|
||||||
|
class={clsx(styles.commentRatingValue, {
|
||||||
|
[styles.commentRatingPositive]: props.comment.stat.rating > 0,
|
||||||
|
[styles.commentRatingNegative]: props.comment.stat.rating < 0
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{props.comment.stat.rating || 0}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
variant="tiny"
|
||||||
|
>
|
||||||
|
<ul class={clsx('nodash')}>
|
||||||
|
<For each={shoutRatingReactions()}>
|
||||||
|
{(reaction) => (
|
||||||
|
<li>
|
||||||
|
{reaction.kind === ReactionKind.Like ? <>+1</> : <>−1</>} {reaction.createdBy.name}
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</ul>
|
||||||
|
</Popup>
|
||||||
|
<button
|
||||||
|
role="button"
|
||||||
|
disabled={!canVote() || !userSlug()}
|
||||||
|
onClick={() => handleRatingChange(false)}
|
||||||
|
class={clsx(styles.commentRatingControl, styles.commentRatingControlDown, {
|
||||||
|
[styles.voted]: isDownvoted()
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -6,7 +6,6 @@ import { clsx } from 'clsx'
|
||||||
import styles from './Settings.module.scss'
|
import styles from './Settings.module.scss'
|
||||||
import { useProfileForm } from '../../context/profile'
|
import { useProfileForm } from '../../context/profile'
|
||||||
import validateUrl from '../../utils/validateUrl'
|
import validateUrl from '../../utils/validateUrl'
|
||||||
|
|
||||||
import { createFileUploader, UploadFile } from '@solid-primitives/upload'
|
import { createFileUploader, UploadFile } from '@solid-primitives/upload'
|
||||||
import { Loading } from '../../components/_shared/Loading'
|
import { Loading } from '../../components/_shared/Loading'
|
||||||
import { useSession } from '../../context/session'
|
import { useSession } from '../../context/session'
|
||||||
|
|
3
src/utils/clone.ts
Normal file
3
src/utils/clone.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
// debug nested objects console.log('message', clone(obj))
|
||||||
|
|
||||||
|
export const clone = <T>(obj: T): T => JSON.parse(JSON.stringify(obj))
|
Loading…
Reference in New Issue
Block a user