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 { 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()}>
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
|
@ -74,7 +74,7 @@ export const AuthorBadge = (props: Props) => {
|
||||||
on(
|
on(
|
||||||
() => props.isFollowed,
|
() => 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 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)
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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> => {
|
||||||
|
|
|
@ -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
|
||||||
},
|
},
|
||||||
|
|
|
@ -18,6 +18,7 @@ export default gql`
|
||||||
slug
|
slug
|
||||||
}
|
}
|
||||||
created_by {
|
created_by {
|
||||||
|
id
|
||||||
name
|
name
|
||||||
slug
|
slug
|
||||||
pic
|
pic
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -13,6 +13,7 @@ export default gql`
|
||||||
title
|
title
|
||||||
}
|
}
|
||||||
created_by {
|
created_by {
|
||||||
|
id
|
||||||
name
|
name
|
||||||
slug
|
slug
|
||||||
pic
|
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)
|
const [scrollToComments, setScrollToComments] = createSignal<boolean>(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
Loading…
Reference in New Issue
Block a user