debug-wip
This commit is contained in:
parent
57f5debdee
commit
4bc3a27254
|
@ -1,4 +1,4 @@
|
|||
import type { Author, Shout, Topic } from '../../graphql/schema/core.gen'
|
||||
import type { Author, Reaction, Shout, Topic } from '../../graphql/schema/core.gen'
|
||||
|
||||
import { getPagePath } from '@nanostores/router'
|
||||
import { createPopper } from '@popperjs/core'
|
||||
|
@ -312,10 +312,11 @@ export const FullArticle = (props: Props) => {
|
|||
},
|
||||
),
|
||||
)
|
||||
|
||||
const [ratings, setRatings] = createSignal<Reaction[]>([])
|
||||
onMount(async () => {
|
||||
install('G-LQ4B87H8C2')
|
||||
await loadReactionsBy({ by: { shout: props.article.slug } })
|
||||
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)
|
||||
|
@ -461,7 +462,11 @@ export const FullArticle = (props: Props) => {
|
|||
<div class="col-md-16 offset-md-5">
|
||||
<div class={styles.shoutStats}>
|
||||
<div class={styles.shoutStatsItem}>
|
||||
<ShoutRatingControl shout={props.article} class={styles.ratingControl} />
|
||||
<ShoutRatingControl
|
||||
shout={props.article}
|
||||
class={styles.ratingControl}
|
||||
ratings={ratings()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Popover content={t('Comment')} disabled={isActionPopupActive()}>
|
||||
|
|
|
@ -1,99 +1,111 @@
|
|||
import { clsx } from 'clsx'
|
||||
import { Show, createMemo, createSignal } from 'solid-js'
|
||||
import { Show, Suspense, createEffect, createMemo, createSignal, mergeProps, on } from 'solid-js'
|
||||
|
||||
import { useLocalize } from '../../context/localize'
|
||||
import { useReactions } from '../../context/reactions'
|
||||
import { useSession } from '../../context/session'
|
||||
import { ReactionKind, Shout } from '../../graphql/schema/core.gen'
|
||||
import { Author, 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
|
||||
ratings?: Reaction[]
|
||||
class?: string
|
||||
}
|
||||
|
||||
export const ShoutRatingControl = (props: ShoutRatingControlProps) => {
|
||||
const { t } = useLocalize()
|
||||
const { author, requireAuthentication } = useSession()
|
||||
const { reactionEntities, createReaction, deleteReaction, loadReactionsBy } = useReactions()
|
||||
const { createReaction, deleteReaction, loadReactionsBy } = useReactions()
|
||||
const [isLoading, setIsLoading] = createSignal(false)
|
||||
const [ratings, setRatings] = createSignal<Reaction[]>([])
|
||||
const [myRate, setMyRate] = createSignal<Reaction | undefined>()
|
||||
const [total, setTotal] = createSignal(props.shout?.stat?.rating || 0)
|
||||
|
||||
const checkReaction = (reactionKind: ReactionKind) =>
|
||||
Object.values(reactionEntities).some(
|
||||
(r) =>
|
||||
r.kind === reactionKind &&
|
||||
r.created_by.id === author()?.id &&
|
||||
r.shout.id === props.shout.id &&
|
||||
!r.reply_to,
|
||||
)
|
||||
createEffect(
|
||||
on(
|
||||
[() => props.ratings, author],
|
||||
([reactions, me]) => {
|
||||
console.debug('[ShoutRatingControl] on reactions update')
|
||||
const shoutRatings = Object.values(reactions).filter((r) => !r.reply_to)
|
||||
setRatings((_) => shoutRatings.sort(byCreated))
|
||||
setMyRate((_) => shoutRatings.find((r) => r.created_by.id === me?.id))
|
||||
// Extract likes and dislikes from shoutRatings using map
|
||||
const likes = shoutRatings.filter((rating) => rating.kind === 'LIKE').length
|
||||
const dislikes = shoutRatings.filter((rating) => rating.kind === 'DISLIKE').length
|
||||
|
||||
const isUpvoted = createMemo(() => checkReaction(ReactionKind.Like))
|
||||
const isDownvoted = createMemo(() => checkReaction(ReactionKind.Dislike))
|
||||
|
||||
const shoutRatingReactions = createMemo(() =>
|
||||
Object.values(reactionEntities).filter(
|
||||
(r) => ['LIKE', 'DISLIKE'].includes(r.kind) && r.shout.id === props.shout.id && !r.reply_to,
|
||||
// Calculate the total
|
||||
const total = likes - dislikes
|
||||
setTotal(total)
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
|
||||
const deleteShoutReaction = async (reactionKind: ReactionKind) => {
|
||||
const reactionToDelete = Object.values(reactionEntities).find(
|
||||
(r) =>
|
||||
r.kind === reactionKind &&
|
||||
r.created_by.id === author()?.id &&
|
||||
r.shout.id === props.shout.id &&
|
||||
!r.reply_to,
|
||||
)
|
||||
return deleteReaction(reactionToDelete.id)
|
||||
}
|
||||
|
||||
const handleRatingChange = (isUpvote: boolean) => {
|
||||
const handleRatingChange = (voteKind: ReactionKind) => {
|
||||
requireAuthentication(async () => {
|
||||
setIsLoading(true)
|
||||
if (isUpvoted()) {
|
||||
await deleteShoutReaction(ReactionKind.Like)
|
||||
} else if (isDownvoted()) {
|
||||
await deleteShoutReaction(ReactionKind.Dislike)
|
||||
|
||||
if (!myRate()) {
|
||||
console.debug('[ShoutRatingControl.handleRatingChange] shout wasnt voted by you before', myRate())
|
||||
const rateInput = { kind: voteKind, shout: props.shout.id }
|
||||
const fakeId = Date.now() + Math.floor(Math.random() * 1000)
|
||||
const savedRatings = [...props.ratings]
|
||||
mergeProps(props.ratings, [...props.ratings, { ...rateInput, id: fakeId, created_by: author() }])
|
||||
await createReaction(rateInput)
|
||||
console.debug(`[ShoutRatingControl.handleRatingChange] your ${voteKind} vote was created`)
|
||||
} else {
|
||||
await createReaction({
|
||||
kind: isUpvote ? ReactionKind.Like : ReactionKind.Dislike,
|
||||
shout: props.shout.id,
|
||||
})
|
||||
console.debug('[ShoutRatingControl.handleRatingChange] shout already has your vote', myRate())
|
||||
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)
|
||||
console.debug(`[ShoutRatingControl.handleRatingChange] your ${oppositeKind} vote was removed`)
|
||||
}
|
||||
if (myRate()?.kind === voteKind) {
|
||||
console.debug(`[ShoutRatingControl.handleRatingChange] cant vote ${voteKind} twice`)
|
||||
}
|
||||
}
|
||||
|
||||
loadShout(props.shout.slug)
|
||||
loadReactionsBy({
|
||||
by: { shout: props.shout.slug },
|
||||
})
|
||||
|
||||
const ratings = await loadReactionsBy({ by: { shout: props.shout.slug, rating: true } })
|
||||
mergeProps(props.ratings, ratings)
|
||||
const s = await loadShout(props.shout.slug)
|
||||
mergeProps(props.shout, s)
|
||||
setIsLoading(false)
|
||||
}, 'vote')
|
||||
}
|
||||
|
||||
const isNotDisliked = createMemo(() => !myRate() || myRate()?.kind === ReactionKind.Dislike)
|
||||
const isNotLiked = createMemo(() => !myRate() || myRate()?.kind === ReactionKind.Like)
|
||||
return (
|
||||
<div class={clsx(styles.rating, props.class)}>
|
||||
<button onClick={() => handleRatingChange(false)} disabled={isLoading()}>
|
||||
<Show when={!isDownvoted()} fallback={<Icon name="rating-control-checked" />}>
|
||||
<Icon name="rating-control-less" />
|
||||
</Show>
|
||||
<button onClick={() => handleRatingChange(ReactionKind.Dislike)} disabled={isLoading()}>
|
||||
<Icon
|
||||
name={isNotDisliked() ? 'rating-control-less' : 'rating-control-checked'}
|
||||
class={isLoading() ? 'rotating' : ''}
|
||||
/>
|
||||
</button>
|
||||
|
||||
<Popup trigger={<span class={styles.ratingValue}>{props.shout.stat.rating}</span>} variant="tiny">
|
||||
<Popup trigger={<span class={styles.ratingValue}>{total()}</span>} variant="tiny">
|
||||
<VotersList
|
||||
reactions={shoutRatingReactions()}
|
||||
fallbackMessage={t('This post has not been rated yet')}
|
||||
reactions={ratings()}
|
||||
fallbackMessage={isLoading() ? t('Loading') : t('This post has not been rated yet')}
|
||||
/>
|
||||
</Popup>
|
||||
|
||||
<button onClick={() => handleRatingChange(true)} disabled={isLoading()}>
|
||||
<Show when={!isUpvoted()} fallback={<Icon name="rating-control-checked" />}>
|
||||
<Icon name="rating-control-more" />
|
||||
</Show>
|
||||
<button onClick={() => handleRatingChange(ReactionKind.Like)} disabled={isLoading()}>
|
||||
<Icon
|
||||
name={isNotLiked() ? 'rating-control-more' : 'rating-control-checked'}
|
||||
class={isLoading() ? 'rotating' : ''}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -74,7 +74,7 @@ export const AuthorBadge = (props: Props) => {
|
|||
on(
|
||||
() => props.isFollowed,
|
||||
() => {
|
||||
setIsFollowed(props.isFollowed.value)
|
||||
setIsFollowed(props.isFollowed?.value)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
|
|
@ -126,7 +126,7 @@ export const AuthorView = (props: Props) => {
|
|||
|
||||
const fetchComments = async (commenter: Author) => {
|
||||
const data = await apiClient.getReactionsBy({
|
||||
by: { comment: false, created_by: commenter.id },
|
||||
by: { comment: true, created_by: commenter.id },
|
||||
})
|
||||
console.debug('[components.Author] fetched comments', data)
|
||||
setCommented(data)
|
||||
|
|
|
@ -9,6 +9,27 @@
|
|||
}
|
||||
}
|
||||
|
||||
.invert {
|
||||
filter: invert(100%);
|
||||
}
|
||||
|
||||
.rotating {
|
||||
/* Define the keyframes for the animation */
|
||||
@keyframes rotate {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Apply the animation to the element */
|
||||
animation: rotate .7s ease-out infinite; /* Rotate infinitely over 2 seconds using a linear timing function */
|
||||
}
|
||||
|
||||
|
||||
.notificationsCounter {
|
||||
background-color: #d00820;
|
||||
border: 2px solid #fff;
|
||||
|
|
|
@ -5,6 +5,7 @@ import { createStore, reconcile } from 'solid-js/store'
|
|||
|
||||
import { apiClient } from '../graphql/client/core'
|
||||
import { Reaction, ReactionBy, ReactionInput, ReactionKind } from '../graphql/schema/core.gen'
|
||||
import { useSession } from './session'
|
||||
|
||||
type ReactionsContextType = {
|
||||
reactionEntities: Record<number, Reaction>
|
||||
|
@ -30,6 +31,7 @@ export function useReactions() {
|
|||
|
||||
export const ReactionsProvider = (props: { children: JSX.Element }) => {
|
||||
const [reactionEntities, setReactionEntities] = createStore<Record<number, Reaction>>({})
|
||||
const { author } = useSession()
|
||||
|
||||
const loadReactionsBy = async ({
|
||||
by,
|
||||
|
@ -53,7 +55,18 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => {
|
|||
}
|
||||
|
||||
const createReaction = async (input: ReactionInput): Promise<void> => {
|
||||
const fakeId = Date.now() + Math.floor(Math.random() * 1000)
|
||||
setReactionEntities((rrr: Record<number, Reaction>) => ({
|
||||
...rrr,
|
||||
[fakeId]: {
|
||||
...input,
|
||||
id: fakeId,
|
||||
created_by: author(),
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
} as unknown as Reaction,
|
||||
}))
|
||||
const reaction = await apiClient.createReaction(input)
|
||||
setReactionEntities({ [fakeId]: undefined })
|
||||
if (!reaction) return
|
||||
const changes = {
|
||||
[reaction.id]: reaction,
|
||||
|
@ -79,13 +92,9 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => {
|
|||
setReactionEntities(changes)
|
||||
}
|
||||
|
||||
const deleteReaction = async (reaction_id: number): Promise<void> => {
|
||||
if (reaction_id) {
|
||||
await apiClient.destroyReaction(reaction_id)
|
||||
setReactionEntities({
|
||||
[reaction_id]: undefined,
|
||||
})
|
||||
}
|
||||
const deleteReaction = async (reaction: number): Promise<void> => {
|
||||
setReactionEntities({ [reaction]: undefined })
|
||||
await apiClient.destroyReaction(reaction)
|
||||
}
|
||||
|
||||
const updateReaction = async (id: number, input: ReactionInput): Promise<void> => {
|
||||
|
|
|
@ -178,8 +178,8 @@ export const apiClient = {
|
|||
console.debug('[graphql.client.core] createReaction: ', response)
|
||||
return response.data.create_reaction.reaction
|
||||
},
|
||||
destroyReaction: async (id: number) => {
|
||||
const response = await apiClient.private.mutation(reactionDestroy, { id: id }).toPromise()
|
||||
destroyReaction: async (reaction: number) => {
|
||||
const response = await apiClient.private.mutation(reactionDestroy, { reaction }).toPromise()
|
||||
console.debug('[graphql.client.core] destroyReaction:', response)
|
||||
return response.data.delete_reaction.reaction
|
||||
},
|
||||
|
|
|
@ -18,6 +18,7 @@ export default gql`
|
|||
slug
|
||||
}
|
||||
created_by {
|
||||
id
|
||||
name
|
||||
slug
|
||||
pic
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { gql } from '@urql/core'
|
||||
|
||||
export default gql`
|
||||
mutation DeleteReactionMutation($reaction_id: Int!) {
|
||||
delete_reaction(reaction_id: $reaction_id) {
|
||||
mutation DeleteReactionMutation($reaction: Int!) {
|
||||
delete_reaction(reaction_id: $reaction) {
|
||||
error
|
||||
reaction {
|
||||
id
|
||||
|
|
|
@ -13,6 +13,7 @@ export default gql`
|
|||
title
|
||||
}
|
||||
created_by {
|
||||
id
|
||||
name
|
||||
slug
|
||||
pic
|
||||
|
|
|
@ -36,14 +36,6 @@ export const ArticlePage = (props: PageProps) => {
|
|||
}
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
try {
|
||||
// document.body.appendChild(script)
|
||||
console.debug('TODO: connect ga')
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
}
|
||||
})
|
||||
const [scrollToComments, setScrollToComments] = createSignal<boolean>(false)
|
||||
|
||||
return (
|
||||
|
|
Loading…
Reference in New Issue
Block a user