webapp/src/components/Article/RatingControl.tsx

195 lines
6.5 KiB
TypeScript
Raw Normal View History

import { clsx } from 'clsx'
2024-02-15 12:51:04 +00:00
import { Show, createEffect, createMemo, createSignal, mergeProps, on } from 'solid-js'
import { useLocalize } from '../../context/localize'
import { useReactions } from '../../context/reactions'
import { useSession } from '../../context/session'
2024-02-15 12:51:04 +00:00
import { Reaction, ReactionKind, Shout } from '../../graphql/schema/core.gen'
import { loadShout } from '../../stores/zine/articles'
2024-02-07 16:54:52 +00:00
import { byCreated } from '../../utils/sortby'
import { Icon } from '../_shared/Icon'
import { Popup } from '../_shared/Popup'
2023-03-09 23:56:19 +00:00
import { VotersList } from '../_shared/VotersList'
2024-02-16 11:29:27 +00:00
import { useRouter } from '../../stores/router'
2024-02-15 12:51:04 +00:00
import stylesComment from './CommentRatingControl.module.scss'
import stylesShout from './ShoutRatingControl.module.scss'
interface RatingControlProps {
shout?: Shout
comment?: Reaction
2024-02-07 16:54:52 +00:00
ratings?: Reaction[]
class?: string
}
2024-02-15 12:51:04 +00:00
export const RatingControl = (props: RatingControlProps) => {
2024-02-16 11:29:27 +00:00
const { t, lang } = useLocalize()
const { changeSearchParams } = useRouter()
2024-02-04 17:40:15 +00:00
const { author, requireAuthentication } = useSession()
2024-02-07 16:54:52 +00:00
const { createReaction, deleteReaction, loadReactionsBy } = useReactions()
2024-01-23 14:41:49 +00:00
const [isLoading, setIsLoading] = createSignal(false)
2024-02-07 16:54:52 +00:00
const [ratings, setRatings] = createSignal<Reaction[]>([])
const [myRate, setMyRate] = createSignal<Reaction | undefined>()
2024-02-15 19:47:02 +00:00
const [total, setTotal] = createSignal(props.comment?.stat?.rating || props.shout?.stat?.rating || 0)
2024-02-15 13:41:14 +00:00
createEffect(
on(
2024-02-16 08:01:40 +00:00
() => props.comment,
(comment) => {
2024-02-16 08:29:06 +00:00
if (comment) {
setTotal(comment?.stat?.rating)
}
2024-02-16 08:01:40 +00:00
},
{ defer: true },
),
)
createEffect(
on(
() => props.shout,
(shout) => {
2024-02-16 08:29:06 +00:00
if (shout) {
setTotal(shout?.stat?.rating)
}
2024-02-15 13:41:14 +00:00
},
{ defer: true },
),
)
2024-01-23 14:41:49 +00:00
2024-02-07 16:54:52 +00:00
createEffect(
on(
[() => props.ratings, author],
([reactions, me]) => {
2024-02-15 13:41:14 +00:00
console.debug('[RatingControl] on reactions update')
const ratingVotes = Object.values(reactions).filter((r) => !r.reply_to)
setRatings((_) => ratingVotes.sort(byCreated))
setMyRate((_) => ratingVotes.find((r) => r.created_by.id === me?.id))
2024-02-07 16:54:52 +00:00
// Extract likes and dislikes from shoutRatings using map
2024-02-15 13:41:14 +00:00
const likes = ratingVotes.filter((rating) => rating.kind === 'LIKE').length
const dislikes = ratingVotes.filter((rating) => rating.kind === 'DISLIKE').length
2024-02-07 16:54:52 +00:00
// Calculate the total
const total = likes - dislikes
setTotal(total)
},
{ defer: true },
2024-01-23 14:41:49 +00:00
),
)
2024-02-07 16:54:52 +00:00
const handleRatingChange = (voteKind: ReactionKind) => {
requireAuthentication(async () => {
2024-01-23 14:43:26 +00:00
setIsLoading(true)
2024-02-07 16:54:52 +00:00
if (!myRate()) {
2024-02-15 13:41:14 +00:00
console.debug('[RatingControl.handleRatingChange] wasnt voted by you before', myRate())
const rateInput = { kind: voteKind, shout: props.shout?.id }
2024-02-07 16:54:52 +00:00
const fakeId = Date.now() + Math.floor(Math.random() * 1000)
2024-02-15 13:41:14 +00:00
// const savedRatings = [...props.ratings]
2024-02-07 16:54:52 +00:00
mergeProps(props.ratings, [...props.ratings, { ...rateInput, id: fakeId, created_by: author() }])
await createReaction(rateInput)
2024-02-15 13:41:14 +00:00
console.debug(`[RatingControl.handleRatingChange] your ${voteKind} vote was created`)
2024-01-23 14:41:49 +00:00
} else {
2024-02-15 13:41:14 +00:00
console.debug('[RatingControl.handleRatingChange] already has your vote', myRate())
2024-02-07 16:54:52 +00:00
const oppositeKind = voteKind === ReactionKind.Like ? ReactionKind.Dislike : ReactionKind.Like
if (myRate()?.kind === oppositeKind) {
mergeProps(
props.ratings,
props.ratings.filter((r) => r.id === myRate().id),
)
await deleteReaction(myRate().id)
setMyRate(undefined)
2024-02-15 13:41:14 +00:00
console.debug(`[RatingControl.handleRatingChange] your ${oppositeKind} vote was removed`)
2024-02-07 16:54:52 +00:00
}
if (myRate()?.kind === voteKind) {
2024-02-15 13:41:14 +00:00
console.debug(`[RatingControl.handleRatingChange] cant vote ${voteKind} twice`)
2024-02-07 16:54:52 +00:00
}
2024-01-23 00:25:00 +00:00
}
2024-01-23 14:41:49 +00:00
2024-02-15 13:41:14 +00:00
const ratings = await loadReactionsBy({ by: { shout: props.shout?.slug, rating: true } })
2024-02-07 16:54:52 +00:00
mergeProps(props.ratings, ratings)
2024-02-15 13:41:14 +00:00
const s = await loadShout(props.shout?.slug)
2024-02-07 16:54:52 +00:00
mergeProps(props.shout, s)
2024-01-23 14:43:26 +00:00
setIsLoading(false)
}, 'vote')
}
2024-02-16 08:01:40 +00:00
2024-02-07 16:54:52 +00:00
const isNotDisliked = createMemo(() => !myRate() || myRate()?.kind === ReactionKind.Dislike)
const isNotLiked = createMemo(() => !myRate() || myRate()?.kind === ReactionKind.Like)
2024-02-15 12:51:04 +00:00
2024-02-16 08:01:40 +00:00
const getTrigger = createMemo(() => {
2024-02-15 12:51:04 +00:00
return props.comment ? (
<div
class={clsx(stylesComment.commentRatingValue, {
2024-02-15 13:41:14 +00:00
[stylesComment.commentRatingPositive]: total() > 0,
[stylesComment.commentRatingNegative]: total() < 0,
2024-02-15 12:51:04 +00:00
})}
>
2024-02-15 13:41:14 +00:00
{total()}
2024-02-15 12:51:04 +00:00
</div>
) : (
<span class={stylesShout.ratingValue}>{total()}</span>
)
2024-02-16 08:01:40 +00:00
})
return (
2024-02-15 12:51:04 +00:00
<div class={clsx(props.comment ? stylesComment.commentRating : stylesShout.rating, props.class)}>
<button
onClick={() => handleRatingChange(ReactionKind.Dislike)}
disabled={isLoading()}
class={
2024-02-16 08:02:00 +00:00
props.comment
? clsx(stylesComment.commentRatingControl, stylesComment.commentRatingControlUp, {
[stylesComment.voted]: myRate()?.kind === 'LIKE',
})
: ''
2024-02-15 12:51:04 +00:00
}
>
<Show when={!props.comment}>
<Icon
name={isNotDisliked() ? 'rating-control-less' : 'rating-control-checked'}
class={isLoading() ? 'rotating' : ''}
/>
</Show>
</button>
2024-02-15 12:51:04 +00:00
<Popup trigger={getTrigger()} variant="tiny">
2024-02-16 11:29:27 +00:00
<Show
when={author()}
fallback={
<>
<span class="link" onClick={() => changeSearchParams({ mode: 'login' })}>
{t('Enter')}
</span>
{lang() === 'ru' ? ', ' : ' '}
{t('to see the voters')}
</>
}
>
2024-02-16 10:59:32 +00:00
<VotersList
reactions={ratings()}
fallbackMessage={isLoading() ? t('Loading') : t('No one rated yet')}
/>
</Show>
</Popup>
2024-02-15 12:51:04 +00:00
<button
onClick={() => handleRatingChange(ReactionKind.Like)}
disabled={isLoading()}
class={
2024-02-16 08:02:00 +00:00
props.comment
? clsx(stylesComment.commentRatingControl, stylesComment.commentRatingControlDown, {
[stylesComment.voted]: myRate()?.kind === 'DISLIKE',
})
: ''
2024-02-15 12:51:04 +00:00
}
>
<Show when={!props.comment}>
<Icon
name={isNotLiked() ? 'rating-control-more' : 'rating-control-checked'}
class={isLoading() ? 'rotating' : ''}
/>
</Show>
</button>
</div>
)
}