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 { getPagePath } from '@nanostores/router'
import { createPopper } from '@popperjs/core' import { createPopper } from '@popperjs/core'
@ -312,10 +312,11 @@ export const FullArticle = (props: Props) => {
}, },
), ),
) )
const [ratings, setRatings] = createSignal<Reaction[]>([])
onMount(async () => { onMount(async () => {
install('G-LQ4B87H8C2') 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) setIsReactionsLoaded(true)
document.title = props.article.title document.title = props.article.title
window?.addEventListener('resize', updateIframeSizes) window?.addEventListener('resize', updateIframeSizes)
@ -461,7 +462,11 @@ export const FullArticle = (props: Props) => {
<div class="col-md-16 offset-md-5"> <div class="col-md-16 offset-md-5">
<div class={styles.shoutStats}> <div class={styles.shoutStats}>
<div class={styles.shoutStatsItem}> <div class={styles.shoutStatsItem}>
<ShoutRatingControl shout={props.article} class={styles.ratingControl} /> <ShoutRatingControl
shout={props.article}
class={styles.ratingControl}
ratings={ratings()}
/>
</div> </div>
<Popover content={t('Comment')} disabled={isActionPopupActive()}> <Popover content={t('Comment')} disabled={isActionPopupActive()}>

View File

@ -1,99 +1,111 @@
import { clsx } from 'clsx' 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 { 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 { ReactionKind, Shout } from '../../graphql/schema/core.gen' import { Author, 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 { 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' import styles from './ShoutRatingControl.module.scss'
interface ShoutRatingControlProps { interface ShoutRatingControlProps {
shout: Shout shout: Shout
ratings?: Reaction[]
class?: string class?: string
} }
export const ShoutRatingControl = (props: ShoutRatingControlProps) => { export const ShoutRatingControl = (props: ShoutRatingControlProps) => {
const { t } = useLocalize() const { t } = useLocalize()
const { author, requireAuthentication } = useSession() const { author, requireAuthentication } = useSession()
const { reactionEntities, createReaction, deleteReaction, loadReactionsBy } = useReactions() const { createReaction, deleteReaction, loadReactionsBy } = useReactions()
const [isLoading, setIsLoading] = createSignal(false) 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) => createEffect(
Object.values(reactionEntities).some( on(
(r) => [() => props.ratings, author],
r.kind === reactionKind && ([reactions, me]) => {
r.created_by.id === author()?.id && console.debug('[ShoutRatingControl] on reactions update')
r.shout.id === props.shout.id && const shoutRatings = Object.values(reactions).filter((r) => !r.reply_to)
!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)) // Calculate the total
const isDownvoted = createMemo(() => checkReaction(ReactionKind.Dislike)) const total = likes - dislikes
setTotal(total)
const shoutRatingReactions = createMemo(() => },
Object.values(reactionEntities).filter( { defer: true },
(r) => ['LIKE', 'DISLIKE'].includes(r.kind) && r.shout.id === props.shout.id && !r.reply_to,
), ),
) )
const deleteShoutReaction = async (reactionKind: ReactionKind) => { const handleRatingChange = (voteKind: 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) => {
requireAuthentication(async () => { requireAuthentication(async () => {
setIsLoading(true) setIsLoading(true)
if (isUpvoted()) {
await deleteShoutReaction(ReactionKind.Like) if (!myRate()) {
} else if (isDownvoted()) { console.debug('[ShoutRatingControl.handleRatingChange] shout wasnt voted by you before', myRate())
await deleteShoutReaction(ReactionKind.Dislike) 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 { } else {
await createReaction({ console.debug('[ShoutRatingControl.handleRatingChange] shout already has your vote', myRate())
kind: isUpvote ? ReactionKind.Like : ReactionKind.Dislike, const oppositeKind = voteKind === ReactionKind.Like ? ReactionKind.Dislike : ReactionKind.Like
shout: props.shout.id, 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) const ratings = await loadReactionsBy({ by: { shout: props.shout.slug, rating: true } })
loadReactionsBy({ mergeProps(props.ratings, ratings)
by: { shout: props.shout.slug }, const s = await loadShout(props.shout.slug)
}) mergeProps(props.shout, s)
setIsLoading(false) setIsLoading(false)
}, 'vote') }, 'vote')
} }
const isNotDisliked = createMemo(() => !myRate() || myRate()?.kind === ReactionKind.Dislike)
const isNotLiked = createMemo(() => !myRate() || myRate()?.kind === ReactionKind.Like)
return ( return (
<div class={clsx(styles.rating, props.class)}> <div class={clsx(styles.rating, props.class)}>
<button onClick={() => handleRatingChange(false)} disabled={isLoading()}> <button onClick={() => handleRatingChange(ReactionKind.Dislike)} disabled={isLoading()}>
<Show when={!isDownvoted()} fallback={<Icon name="rating-control-checked" />}> <Icon
<Icon name="rating-control-less" /> name={isNotDisliked() ? 'rating-control-less' : 'rating-control-checked'}
</Show> class={isLoading() ? 'rotating' : ''}
/>
</button> </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 <VotersList
reactions={shoutRatingReactions()} reactions={ratings()}
fallbackMessage={t('This post has not been rated yet')} fallbackMessage={isLoading() ? t('Loading') : t('This post has not been rated yet')}
/> />
</Popup> </Popup>
<button onClick={() => handleRatingChange(true)} disabled={isLoading()}> <button onClick={() => handleRatingChange(ReactionKind.Like)} disabled={isLoading()}>
<Show when={!isUpvoted()} fallback={<Icon name="rating-control-checked" />}> <Icon
<Icon name="rating-control-more" /> name={isNotLiked() ? 'rating-control-more' : 'rating-control-checked'}
</Show> class={isLoading() ? 'rotating' : ''}
/>
</button> </button>
</div> </div>
) )

View File

@ -74,7 +74,7 @@ export const AuthorBadge = (props: Props) => {
on( on(
() => props.isFollowed, () => 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 fetchComments = async (commenter: Author) => {
const data = await apiClient.getReactionsBy({ 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) console.debug('[components.Author] fetched comments', data)
setCommented(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 { .notificationsCounter {
background-color: #d00820; background-color: #d00820;
border: 2px solid #fff; border: 2px solid #fff;

View File

@ -5,6 +5,7 @@ import { createStore, reconcile } from 'solid-js/store'
import { apiClient } from '../graphql/client/core' import { apiClient } from '../graphql/client/core'
import { Reaction, ReactionBy, ReactionInput, ReactionKind } from '../graphql/schema/core.gen' import { Reaction, ReactionBy, ReactionInput, ReactionKind } from '../graphql/schema/core.gen'
import { useSession } from './session'
type ReactionsContextType = { type ReactionsContextType = {
reactionEntities: Record<number, Reaction> reactionEntities: Record<number, Reaction>
@ -30,6 +31,7 @@ export function useReactions() {
export const ReactionsProvider = (props: { children: JSX.Element }) => { export const ReactionsProvider = (props: { children: JSX.Element }) => {
const [reactionEntities, setReactionEntities] = createStore<Record<number, Reaction>>({}) const [reactionEntities, setReactionEntities] = createStore<Record<number, Reaction>>({})
const { author } = useSession()
const loadReactionsBy = async ({ const loadReactionsBy = async ({
by, by,
@ -53,7 +55,18 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => {
} }
const createReaction = async (input: ReactionInput): Promise<void> => { 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) const reaction = await apiClient.createReaction(input)
setReactionEntities({ [fakeId]: undefined })
if (!reaction) return if (!reaction) return
const changes = { const changes = {
[reaction.id]: reaction, [reaction.id]: reaction,
@ -79,13 +92,9 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => {
setReactionEntities(changes) setReactionEntities(changes)
} }
const deleteReaction = async (reaction_id: number): Promise<void> => { const deleteReaction = async (reaction: number): Promise<void> => {
if (reaction_id) { setReactionEntities({ [reaction]: undefined })
await apiClient.destroyReaction(reaction_id) await apiClient.destroyReaction(reaction)
setReactionEntities({
[reaction_id]: undefined,
})
}
} }
const updateReaction = async (id: number, input: ReactionInput): Promise<void> => { const updateReaction = async (id: number, input: ReactionInput): Promise<void> => {

View File

@ -175,11 +175,11 @@ export const apiClient = {
}, },
createReaction: async (input: ReactionInput) => { createReaction: async (input: ReactionInput) => {
const response = await apiClient.private.mutation(reactionCreate, { reaction: input }).toPromise() const response = await apiClient.private.mutation(reactionCreate, { reaction: input }).toPromise()
console.debug('[graphql.client.core] createReaction:', response) console.debug('[graphql.client.core] createReaction: ', response)
return response.data.create_reaction.reaction return response.data.create_reaction.reaction
}, },
destroyReaction: async (id: number) => { destroyReaction: async (reaction: number) => {
const response = await apiClient.private.mutation(reactionDestroy, { id: id }).toPromise() const response = await apiClient.private.mutation(reactionDestroy, { reaction }).toPromise()
console.debug('[graphql.client.core] destroyReaction:', response) console.debug('[graphql.client.core] destroyReaction:', response)
return response.data.delete_reaction.reaction return response.data.delete_reaction.reaction
}, },

View File

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

View File

@ -1,8 +1,8 @@
import { gql } from '@urql/core' import { gql } from '@urql/core'
export default gql` export default gql`
mutation DeleteReactionMutation($reaction_id: Int!) { mutation DeleteReactionMutation($reaction: Int!) {
delete_reaction(reaction_id: $reaction_id) { delete_reaction(reaction_id: $reaction) {
error error
reaction { reaction {
id id

View File

@ -13,6 +13,7 @@ export default gql`
title title
} }
created_by { created_by {
id
name name
slug slug
pic 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) const [scrollToComments, setScrollToComments] = createSignal<boolean>(false)
return ( return (