refactoring-ratings

This commit is contained in:
Untone 2024-02-15 15:51:04 +03:00
parent 82c6841523
commit e336754226
7 changed files with 83 additions and 149 deletions

View File

@ -380,7 +380,7 @@
"This email is already taken. If it's you": "This email is already taken. If it's you",
"This functionality is currently not available, we would like to work on this issue. Use the download link.": "This functionality is currently not available, we would like to work on this issue. Use the download link.",
"This month": "This month",
"This post has not been rated yet": "This post has not been rated yet",
"No one rated yet": "No one rated yet",
"This way we ll realize that you re a real person and ll take your vote into account. And you ll see how others voted": "This way we ll realize that you re a real person and ll take your vote into account. And you ll see how others voted",
"This way you ll be able to subscribe to authors, interesting topics and customize your feed": "This way you ll be able to subscribe to authors, interesting topics and customize your feed",
"This week": "This week",

View File

@ -402,7 +402,7 @@
"This email is already taken. If it's you": "Такой email уже зарегистрирован. Если это вы",
"This functionality is currently not available, we would like to work on this issue. Use the download link.": "В данный момент этот функционал не доступен, бы работаем над этой проблемой. Воспользуйтесь загрузкой по ссылке.",
"This month": "За месяц",
"This post has not been rated yet": "Эту публикацию еще пока никто не оценил",
"No one rated yet": "Ещё никто не оценивал",
"This way we ll realize that you re a real person and ll take your vote into account. And you ll see how others voted": "Так мы поймем, что вы реальный человек, и учтем ваш голос. А вы увидите, как проголосовали другие",
"This way you ll be able to subscribe to authors, interesting topics and customize your feed": "Так вы сможете подписаться на авторов, интересные темы и настроить свою ленту",
"This week": "За неделю",

View File

@ -14,7 +14,7 @@ import { Userpic } from '../../Author/Userpic'
import { Icon } from '../../_shared/Icon'
import { ShowIfAuthenticated } from '../../_shared/ShowIfAuthenticated'
import { CommentDate } from '../CommentDate'
import { CommentRatingControl } from '../CommentRatingControl'
import { RatingControl as CommentRatingControl } from '../RatingControl'
import styles from './Comment.module.scss'

View File

@ -1,118 +0,0 @@
import { clsx } from 'clsx'
import { createMemo } from 'solid-js'
import { useLocalize } from '../../context/localize'
import { useReactions } from '../../context/reactions'
import { useSession } from '../../context/session'
import { useSnackbar } from '../../context/snackbar'
import { Reaction, ReactionKind } from '../../graphql/schema/core.gen'
import { loadShout } from '../../stores/zine/articles'
import { Popup } from '../_shared/Popup'
import { VotersList } from '../_shared/VotersList'
import styles from './CommentRatingControl.module.scss'
type Props = {
comment: Reaction
}
export const CommentRatingControl = (props: Props) => {
const { t } = useLocalize()
const { author } = useSession()
const { showSnackbar } = useSnackbar()
const { reactionEntities, createReaction, deleteReaction, loadReactionsBy } = useReactions()
const checkReaction = (reactionKind: ReactionKind) =>
Object.values(reactionEntities).some(
(r) =>
r.kind === reactionKind &&
r.created_by.slug === author()?.slug &&
r.shout.id === props.comment.shout.id &&
r.reply_to === props.comment.id,
)
const isUpvoted = createMemo(() => checkReaction(ReactionKind.Like))
const isDownvoted = createMemo(() => checkReaction(ReactionKind.Dislike))
const canVote = createMemo(() => author()?.slug !== props.comment.created_by.slug)
const commentRatingReactions = createMemo(() =>
Object.values(reactionEntities).filter(
(r) =>
[ReactionKind.Like, ReactionKind.Dislike].includes(r.kind) &&
r.shout.id === props.comment.shout.id &&
r.reply_to === props.comment.id,
),
)
const deleteCommentReaction = async (reactionKind: ReactionKind) => {
const reactionToDelete = Object.values(reactionEntities).find(
(r) =>
r.kind === reactionKind &&
r.created_by.slug === author()?.slug &&
r.shout.id === props.comment.shout.id &&
r.reply_to === 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,
reply_to: props.comment.id,
})
}
} catch {
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() && author())}
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"
>
<VotersList
reactions={commentRatingReactions()}
fallbackMessage={t('This comment has not yet been rated')}
/>
</Popup>
<button
role="button"
disabled={!(canVote() && author())}
onClick={() => handleRatingChange(false)}
class={clsx(styles.commentRatingControl, styles.commentRatingControlDown, {
[styles.voted]: isDownvoted(),
})}
/>
</div>
)
}

View File

@ -35,8 +35,8 @@ import { VideoPlayer } from '../_shared/VideoPlayer'
import { AudioHeader } from './AudioHeader'
import { AudioPlayer } from './AudioPlayer'
import { CommentsTree } from './CommentsTree'
import { RatingControl as ShoutRatingControl } from './RatingControl'
import { SharePopup, getShareUrl } from './SharePopup'
import { ShoutRatingControl } from './ShoutRatingControl'
import stylesHeader from '../Nav/Header/Header.module.scss'
import styles from './Article.module.scss'
@ -313,17 +313,27 @@ export const FullArticle = (props: Props) => {
),
)
const [ratings, setRatings] = createSignal<Reaction[]>([])
onMount(async () => {
install('G-LQ4B87H8C2')
const rrr = await loadReactionsBy({ by: { shout: props.article.slug } })
setRatings((_) => rrr.filter((r) => ['LIKE', 'DISLIKE'].includes(r.kind)))
setIsReactionsLoaded(true)
document.title = props.article.title
window?.addEventListener('resize', updateIframeSizes)
onMount(async () => {
document.title = props.article?.title
install('G-LQ4B87H8C2')
window?.addEventListener('resize', updateIframeSizes)
onCleanup(() => window.removeEventListener('resize', updateIframeSizes))
})
createEffect(
on(
() => props.article,
async (shout: Shout) => {
setIsReactionsLoaded(false)
const rrr = await loadReactionsBy({ by: { shout: shout?.slug } })
setRatings((_) => rrr.filter((r) => ['LIKE', 'DISLIKE'].includes(r.kind)))
setIsReactionsLoaded(true)
},
{ defer: true },
),
)
const cover = props.article.cover ?? 'production/image/logo_image.png'
const ogImage = getOpenGraphImageUrl(cover, {
title: props.article.title,

View File

@ -1,24 +1,27 @@
import { clsx } from 'clsx'
import { Show, Suspense, createEffect, createMemo, createSignal, mergeProps, on } from 'solid-js'
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'
import { Author, Reaction, ReactionKind, Shout } from '../../graphql/schema/core.gen'
import { Reaction, ReactionKind, Shout } from '../../graphql/schema/core.gen'
import { loadShout } from '../../stores/zine/articles'
import { byCreated } from '../../utils/sortby'
import { Icon } from '../_shared/Icon'
import { Popup } from '../_shared/Popup'
import { VotersList } from '../_shared/VotersList'
import styles from './ShoutRatingControl.module.scss'
interface ShoutRatingControlProps {
shout: Shout
import stylesComment from './CommentRatingControl.module.scss'
import stylesShout from './ShoutRatingControl.module.scss'
interface RatingControlProps {
shout?: Shout
comment?: Reaction
ratings?: Reaction[]
class?: string
}
export const ShoutRatingControl = (props: ShoutRatingControlProps) => {
export const RatingControl = (props: RatingControlProps) => {
const { t } = useLocalize()
const { author, requireAuthentication } = useSession()
const { createReaction, deleteReaction, loadReactionsBy } = useReactions()
@ -85,27 +88,66 @@ export const ShoutRatingControl = (props: ShoutRatingControlProps) => {
}
const isNotDisliked = createMemo(() => !myRate() || myRate()?.kind === ReactionKind.Dislike)
const isNotLiked = createMemo(() => !myRate() || myRate()?.kind === ReactionKind.Like)
const getTrigger = () => {
return props.comment ? (
<div
class={clsx(stylesComment.commentRatingValue, {
[stylesComment.commentRatingPositive]: props.comment.stat.rating > 0,
[stylesComment.commentRatingNegative]: props.comment.stat.rating < 0,
})}
>
{props.comment.stat.rating || 0}
</div>
) : (
<span class={stylesShout.ratingValue}>{total()}</span>
)
}
return (
<div class={clsx(styles.rating, props.class)}>
<button onClick={() => handleRatingChange(ReactionKind.Dislike)} disabled={isLoading()}>
<div class={clsx(props.comment ? stylesComment.commentRating : stylesShout.rating, props.class)}>
<button
onClick={() => handleRatingChange(ReactionKind.Dislike)}
disabled={isLoading()}
class={
props.comment
? clsx(stylesComment.commentRatingControl, stylesComment.commentRatingControlUp, {
[stylesComment.voted]: myRate()?.kind === 'LIKE',
})
: ''
}
>
<Show when={!props.comment}>
<Icon
name={isNotDisliked() ? 'rating-control-less' : 'rating-control-checked'}
class={isLoading() ? 'rotating' : ''}
/>
</Show>
</button>
<Popup trigger={<span class={styles.ratingValue}>{total()}</span>} variant="tiny">
<Popup trigger={getTrigger()} variant="tiny">
<VotersList
reactions={ratings()}
fallbackMessage={isLoading() ? t('Loading') : t('This post has not been rated yet')}
fallbackMessage={isLoading() ? t('Loading') : t('No one rated yet')}
/>
</Popup>
<button onClick={() => handleRatingChange(ReactionKind.Like)} disabled={isLoading()}>
<button
onClick={() => handleRatingChange(ReactionKind.Like)}
disabled={isLoading()}
class={
props.comment
? clsx(stylesComment.commentRatingControl, stylesComment.commentRatingControlDown, {
[stylesComment.voted]: myRate()?.kind === 'DISLIKE',
})
: ''
}
>
<Show when={!props.comment}>
<Icon
name={isNotLiked() ? 'rating-control-more' : 'rating-control-checked'}
class={isLoading() ? 'rotating' : ''}
/>
</Show>
</button>
</div>
)

View File

@ -10,8 +10,8 @@ import { router, useRouter } from '../../../stores/router'
import { capitalize } from '../../../utils/capitalize'
import { getDescription } from '../../../utils/meta'
import { CoverImage } from '../../Article/CoverImage'
import { RatingControl as ShoutRatingControl } from '../../Article/RatingControl'
import { SharePopup, getShareUrl } from '../../Article/SharePopup'
import { ShoutRatingControl } from '../../Article/ShoutRatingControl'
import { AuthorLink } from '../../Author/AuthorLink'
import { Icon } from '../../_shared/Icon'
import { Image } from '../../_shared/Image'