diff --git a/src/components/Article/FullArticle.tsx b/src/components/Article/FullArticle.tsx index a8c54c98..9119274c 100644 --- a/src/components/Article/FullArticle.tsx +++ b/src/components/Article/FullArticle.tsx @@ -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([]) 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) => {
- +
diff --git a/src/components/Article/ShoutRatingControl.tsx b/src/components/Article/ShoutRatingControl.tsx index 173016f4..409c65d6 100644 --- a/src/components/Article/ShoutRatingControl.tsx +++ b/src/components/Article/ShoutRatingControl.tsx @@ -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([]) + const [myRate, setMyRate] = createSignal() + 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 (
- - {props.shout.stat.rating}} variant="tiny"> + {total()}} variant="tiny"> -
) diff --git a/src/components/Author/AuthorBadge/AuthorBadge.tsx b/src/components/Author/AuthorBadge/AuthorBadge.tsx index 35537140..15255eb2 100644 --- a/src/components/Author/AuthorBadge/AuthorBadge.tsx +++ b/src/components/Author/AuthorBadge/AuthorBadge.tsx @@ -74,7 +74,7 @@ export const AuthorBadge = (props: Props) => { on( () => props.isFollowed, () => { - setIsFollowed(props.isFollowed.value) + setIsFollowed(props.isFollowed?.value) }, ), ) diff --git a/src/components/Views/Author/Author.tsx b/src/components/Views/Author/Author.tsx index 1d8d8f7b..05ad5d72 100644 --- a/src/components/Views/Author/Author.tsx +++ b/src/components/Views/Author/Author.tsx @@ -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) diff --git a/src/components/_shared/Icon/Icon.module.scss b/src/components/_shared/Icon/Icon.module.scss index 6618efa9..1b7025c9 100644 --- a/src/components/_shared/Icon/Icon.module.scss +++ b/src/components/_shared/Icon/Icon.module.scss @@ -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; diff --git a/src/context/reactions.tsx b/src/context/reactions.tsx index 48da0594..ccd0ee25 100644 --- a/src/context/reactions.tsx +++ b/src/context/reactions.tsx @@ -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 @@ -30,6 +31,7 @@ export function useReactions() { export const ReactionsProvider = (props: { children: JSX.Element }) => { const [reactionEntities, setReactionEntities] = createStore>({}) + const { author } = useSession() const loadReactionsBy = async ({ by, @@ -53,7 +55,18 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => { } const createReaction = async (input: ReactionInput): Promise => { + const fakeId = Date.now() + Math.floor(Math.random() * 1000) + setReactionEntities((rrr: Record) => ({ + ...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 => { - if (reaction_id) { - await apiClient.destroyReaction(reaction_id) - setReactionEntities({ - [reaction_id]: undefined, - }) - } + const deleteReaction = async (reaction: number): Promise => { + setReactionEntities({ [reaction]: undefined }) + await apiClient.destroyReaction(reaction) } const updateReaction = async (id: number, input: ReactionInput): Promise => { diff --git a/src/graphql/client/core.ts b/src/graphql/client/core.ts index c80fe931..1fc303e4 100644 --- a/src/graphql/client/core.ts +++ b/src/graphql/client/core.ts @@ -175,11 +175,11 @@ export const apiClient = { }, createReaction: async (input: ReactionInput) => { 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 }, - 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 }, diff --git a/src/graphql/mutation/core/reaction-create.ts b/src/graphql/mutation/core/reaction-create.ts index 72852d97..e60824d1 100644 --- a/src/graphql/mutation/core/reaction-create.ts +++ b/src/graphql/mutation/core/reaction-create.ts @@ -18,6 +18,7 @@ export default gql` slug } created_by { + id name slug pic diff --git a/src/graphql/mutation/core/reaction-destroy.ts b/src/graphql/mutation/core/reaction-destroy.ts index be1b5828..e3a608f8 100644 --- a/src/graphql/mutation/core/reaction-destroy.ts +++ b/src/graphql/mutation/core/reaction-destroy.ts @@ -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 diff --git a/src/graphql/query/core/reactions-load-by.ts b/src/graphql/query/core/reactions-load-by.ts index c0700f07..0ef15693 100644 --- a/src/graphql/query/core/reactions-load-by.ts +++ b/src/graphql/query/core/reactions-load-by.ts @@ -13,6 +13,7 @@ export default gql` title } created_by { + id name slug pic diff --git a/src/pages/article.page.tsx b/src/pages/article.page.tsx index a4b8500d..50a3b7e5 100644 --- a/src/pages/article.page.tsx +++ b/src/pages/article.page.tsx @@ -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(false) return (