debug-wip
All checks were successful
deploy / test (push) Successful in 2m13s
deploy / Update templates on Mailgun (push) Has been skipped

This commit is contained in:
Untone 2024-02-07 19:54:52 +03:00
parent 57f5debdee
commit 4bc3a27254
11 changed files with 122 additions and 81 deletions

View File

@ -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()}>

View File

@ -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>
)

View File

@ -74,7 +74,7 @@ export const AuthorBadge = (props: Props) => {
on(
() => props.isFollowed,
() => {
setIsFollowed(props.isFollowed.value)
setIsFollowed(props.isFollowed?.value)
},
),
)

View File

@ -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)

View File

@ -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;

View File

@ -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> => {

View File

@ -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
},

View File

@ -18,6 +18,7 @@ export default gql`
slug
}
created_by {
id
name
slug
pic

View File

@ -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

View File

@ -13,6 +13,7 @@ export default gql`
title
}
created_by {
id
name
slug
pic

View File

@ -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 (