refactoring-ratings
This commit is contained in:
parent
82c6841523
commit
e336754226
|
@ -380,7 +380,7 @@
|
||||||
"This email is already taken. If it's you": "This email is already taken. If it's you",
|
"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 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 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 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 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",
|
"This week": "This week",
|
||||||
|
|
|
@ -402,7 +402,7 @@
|
||||||
"This email is already taken. If it's you": "Такой email уже зарегистрирован. Если это вы",
|
"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 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": "Эту публикацию еще пока никто не оценил",
|
"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": "За неделю",
|
||||||
|
|
|
@ -14,7 +14,7 @@ import { Userpic } from '../../Author/Userpic'
|
||||||
import { Icon } from '../../_shared/Icon'
|
import { Icon } from '../../_shared/Icon'
|
||||||
import { ShowIfAuthenticated } from '../../_shared/ShowIfAuthenticated'
|
import { ShowIfAuthenticated } from '../../_shared/ShowIfAuthenticated'
|
||||||
import { CommentDate } from '../CommentDate'
|
import { CommentDate } from '../CommentDate'
|
||||||
import { CommentRatingControl } from '../CommentRatingControl'
|
import { RatingControl as CommentRatingControl } from '../RatingControl'
|
||||||
|
|
||||||
import styles from './Comment.module.scss'
|
import styles from './Comment.module.scss'
|
||||||
|
|
||||||
|
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -35,8 +35,8 @@ import { VideoPlayer } from '../_shared/VideoPlayer'
|
||||||
import { AudioHeader } from './AudioHeader'
|
import { AudioHeader } from './AudioHeader'
|
||||||
import { AudioPlayer } from './AudioPlayer'
|
import { AudioPlayer } from './AudioPlayer'
|
||||||
import { CommentsTree } from './CommentsTree'
|
import { CommentsTree } from './CommentsTree'
|
||||||
|
import { RatingControl as ShoutRatingControl } from './RatingControl'
|
||||||
import { SharePopup, getShareUrl } from './SharePopup'
|
import { SharePopup, getShareUrl } from './SharePopup'
|
||||||
import { ShoutRatingControl } from './ShoutRatingControl'
|
|
||||||
|
|
||||||
import stylesHeader from '../Nav/Header/Header.module.scss'
|
import stylesHeader from '../Nav/Header/Header.module.scss'
|
||||||
import styles from './Article.module.scss'
|
import styles from './Article.module.scss'
|
||||||
|
@ -313,17 +313,27 @@ export const FullArticle = (props: Props) => {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
const [ratings, setRatings] = createSignal<Reaction[]>([])
|
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))
|
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 cover = props.article.cover ?? 'production/image/logo_image.png'
|
||||||
const ogImage = getOpenGraphImageUrl(cover, {
|
const ogImage = getOpenGraphImageUrl(cover, {
|
||||||
title: props.article.title,
|
title: props.article.title,
|
||||||
|
|
|
@ -1,24 +1,27 @@
|
||||||
import { clsx } from 'clsx'
|
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 { useLocalize } from '../../context/localize'
|
||||||
import { useReactions } from '../../context/reactions'
|
import { useReactions } from '../../context/reactions'
|
||||||
import { useSession } from '../../context/session'
|
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 { loadShout } from '../../stores/zine/articles'
|
||||||
import { byCreated } from '../../utils/sortby'
|
import { byCreated } from '../../utils/sortby'
|
||||||
import { Icon } from '../_shared/Icon'
|
import { Icon } from '../_shared/Icon'
|
||||||
import { Popup } from '../_shared/Popup'
|
import { Popup } from '../_shared/Popup'
|
||||||
import { VotersList } from '../_shared/VotersList'
|
import { VotersList } from '../_shared/VotersList'
|
||||||
import styles from './ShoutRatingControl.module.scss'
|
|
||||||
|
|
||||||
interface ShoutRatingControlProps {
|
import stylesComment from './CommentRatingControl.module.scss'
|
||||||
shout: Shout
|
import stylesShout from './ShoutRatingControl.module.scss'
|
||||||
|
|
||||||
|
interface RatingControlProps {
|
||||||
|
shout?: Shout
|
||||||
|
comment?: Reaction
|
||||||
ratings?: Reaction[]
|
ratings?: Reaction[]
|
||||||
class?: string
|
class?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShoutRatingControl = (props: ShoutRatingControlProps) => {
|
export const RatingControl = (props: RatingControlProps) => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const { author, requireAuthentication } = useSession()
|
const { author, requireAuthentication } = useSession()
|
||||||
const { createReaction, deleteReaction, loadReactionsBy } = useReactions()
|
const { createReaction, deleteReaction, loadReactionsBy } = useReactions()
|
||||||
|
@ -85,27 +88,66 @@ export const ShoutRatingControl = (props: ShoutRatingControlProps) => {
|
||||||
}
|
}
|
||||||
const isNotDisliked = createMemo(() => !myRate() || myRate()?.kind === ReactionKind.Dislike)
|
const isNotDisliked = createMemo(() => !myRate() || myRate()?.kind === ReactionKind.Dislike)
|
||||||
const isNotLiked = createMemo(() => !myRate() || myRate()?.kind === ReactionKind.Like)
|
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 (
|
return (
|
||||||
<div class={clsx(styles.rating, props.class)}>
|
<div class={clsx(props.comment ? stylesComment.commentRating : stylesShout.rating, props.class)}>
|
||||||
<button onClick={() => handleRatingChange(ReactionKind.Dislike)} disabled={isLoading()}>
|
<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
|
<Icon
|
||||||
name={isNotDisliked() ? 'rating-control-less' : 'rating-control-checked'}
|
name={isNotDisliked() ? 'rating-control-less' : 'rating-control-checked'}
|
||||||
class={isLoading() ? 'rotating' : ''}
|
class={isLoading() ? 'rotating' : ''}
|
||||||
/>
|
/>
|
||||||
|
</Show>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<Popup trigger={<span class={styles.ratingValue}>{total()}</span>} variant="tiny">
|
<Popup trigger={getTrigger()} variant="tiny">
|
||||||
<VotersList
|
<VotersList
|
||||||
reactions={ratings()}
|
reactions={ratings()}
|
||||||
fallbackMessage={isLoading() ? t('Loading') : t('This post has not been rated yet')}
|
fallbackMessage={isLoading() ? t('Loading') : t('No one rated yet')}
|
||||||
/>
|
/>
|
||||||
</Popup>
|
</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
|
<Icon
|
||||||
name={isNotLiked() ? 'rating-control-more' : 'rating-control-checked'}
|
name={isNotLiked() ? 'rating-control-more' : 'rating-control-checked'}
|
||||||
class={isLoading() ? 'rotating' : ''}
|
class={isLoading() ? 'rotating' : ''}
|
||||||
/>
|
/>
|
||||||
|
</Show>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
|
@ -10,8 +10,8 @@ import { router, useRouter } from '../../../stores/router'
|
||||||
import { capitalize } from '../../../utils/capitalize'
|
import { capitalize } from '../../../utils/capitalize'
|
||||||
import { getDescription } from '../../../utils/meta'
|
import { getDescription } from '../../../utils/meta'
|
||||||
import { CoverImage } from '../../Article/CoverImage'
|
import { CoverImage } from '../../Article/CoverImage'
|
||||||
|
import { RatingControl as ShoutRatingControl } from '../../Article/RatingControl'
|
||||||
import { SharePopup, getShareUrl } from '../../Article/SharePopup'
|
import { SharePopup, getShareUrl } from '../../Article/SharePopup'
|
||||||
import { ShoutRatingControl } from '../../Article/ShoutRatingControl'
|
|
||||||
import { AuthorLink } from '../../Author/AuthorLink'
|
import { AuthorLink } from '../../Author/AuthorLink'
|
||||||
import { Icon } from '../../_shared/Icon'
|
import { Icon } from '../../_shared/Icon'
|
||||||
import { Image } from '../../_shared/Image'
|
import { Image } from '../../_shared/Image'
|
||||||
|
|
Loading…
Reference in New Issue
Block a user