likes dislikes
This commit is contained in:
parent
dc85e222b6
commit
b3b1c48d92
|
@ -22,16 +22,14 @@ type Props = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CommentsTree = (props: Props) => {
|
export const CommentsTree = (props: Props) => {
|
||||||
const [isCommentsLoading, setIsCommentsLoading] = createSignal(false)
|
|
||||||
const [commentsOrder, setCommentsOrder] = createSignal<CommentsOrder>('createdAt')
|
const [commentsOrder, setCommentsOrder] = createSignal<CommentsOrder>('createdAt')
|
||||||
const {
|
const {
|
||||||
reactionEntities,
|
reactionEntities,
|
||||||
actions: { loadReactionsBy, createReaction }
|
actions: { createReaction }
|
||||||
} = useReactions()
|
} = useReactions()
|
||||||
|
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
|
|
||||||
// TODO: server side?
|
|
||||||
const [newReactionsCount, setNewReactionsCount] = createSignal<number>(0)
|
const [newReactionsCount, setNewReactionsCount] = createSignal<number>(0)
|
||||||
const [newReactions, setNewReactions] = createSignal<Reaction[]>([])
|
const [newReactions, setNewReactions] = createSignal<Reaction[]>([])
|
||||||
|
|
||||||
|
@ -79,7 +77,9 @@ export const CommentsTree = (props: Props) => {
|
||||||
setCookie()
|
setCookie()
|
||||||
} else if (Date.now() > dateFromCookie) {
|
} else if (Date.now() > dateFromCookie) {
|
||||||
const newComments = comments().filter((c) => {
|
const newComments = comments().filter((c) => {
|
||||||
if (c.replyTo) return
|
if (c.replyTo) {
|
||||||
|
return
|
||||||
|
}
|
||||||
const commentDate = new Date(c.createdAt).valueOf()
|
const commentDate = new Date(c.createdAt).valueOf()
|
||||||
return commentDate > dateFromCookie
|
return commentDate > dateFromCookie
|
||||||
})
|
})
|
||||||
|
@ -92,15 +92,7 @@ export const CommentsTree = (props: Props) => {
|
||||||
const { session } = useSession()
|
const { session } = useSession()
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
updateNewReactionsCount()
|
||||||
setIsCommentsLoading(true)
|
|
||||||
await loadReactionsBy({
|
|
||||||
by: { shout: props.shoutSlug }
|
|
||||||
})
|
|
||||||
updateNewReactionsCount()
|
|
||||||
} finally {
|
|
||||||
setIsCommentsLoading(false)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const [submitted, setSubmitted] = createSignal<boolean>(false)
|
const [submitted, setSubmitted] = createSignal<boolean>(false)
|
||||||
|
@ -118,80 +110,78 @@ export const CommentsTree = (props: Props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
<Show when={!isCommentsLoading()} fallback={<Loading />}>
|
<div class={styles.commentsHeaderWrapper}>
|
||||||
<div class={styles.commentsHeaderWrapper}>
|
<h2 id="comments" class={styles.commentsHeader}>
|
||||||
<h2 id="comments" class={styles.commentsHeader}>
|
{t('Comments')} {comments().length.toString() || ''}
|
||||||
{t('Comments')} {comments().length.toString() || ''}
|
<Show when={newReactionsCount() > 0}>
|
||||||
<Show when={newReactionsCount() > 0}>
|
<span class={styles.newReactions}> +{newReactionsCount()}</span>
|
||||||
<span class={styles.newReactions}> +{newReactionsCount()}</span>
|
</Show>
|
||||||
</Show>
|
</h2>
|
||||||
</h2>
|
|
||||||
|
|
||||||
<ul class={clsx(styles.commentsViewSwitcher, 'view-switcher')}>
|
<ul class={clsx(styles.commentsViewSwitcher, 'view-switcher')}>
|
||||||
<Show when={newReactionsCount() > 0}>
|
<Show when={newReactionsCount() > 0}>
|
||||||
<li classList={{ selected: commentsOrder() === 'newOnly' }}>
|
<li classList={{ selected: commentsOrder() === 'newOnly' }}>
|
||||||
<Button
|
|
||||||
variant="inline"
|
|
||||||
value={t('New only')}
|
|
||||||
onClick={() => {
|
|
||||||
setCommentsOrder('newOnly')
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
</Show>
|
|
||||||
<li classList={{ selected: commentsOrder() === 'createdAt' }}>
|
|
||||||
<Button
|
<Button
|
||||||
variant="inline"
|
variant="inline"
|
||||||
value={t('By time')}
|
value={t('New only')}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setCommentsOrder('createdAt')
|
setCommentsOrder('newOnly')
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
<li classList={{ selected: commentsOrder() === 'rating' }}>
|
</Show>
|
||||||
<Button
|
<li classList={{ selected: commentsOrder() === 'createdAt' }}>
|
||||||
variant="inline"
|
<Button
|
||||||
value={t('By rating')}
|
variant="inline"
|
||||||
onClick={() => {
|
value={t('By time')}
|
||||||
setCommentsOrder('rating')
|
onClick={() => {
|
||||||
}}
|
setCommentsOrder('createdAt')
|
||||||
/>
|
}}
|
||||||
</li>
|
/>
|
||||||
</ul>
|
</li>
|
||||||
</div>
|
<li classList={{ selected: commentsOrder() === 'rating' }}>
|
||||||
<ul class={styles.comments}>
|
<Button
|
||||||
<For each={sortedComments().filter((r) => !r.replyTo)}>
|
variant="inline"
|
||||||
{(reaction) => (
|
value={t('By rating')}
|
||||||
<Comment
|
onClick={() => {
|
||||||
sortedComments={sortedComments()}
|
setCommentsOrder('rating')
|
||||||
isArticleAuthor={Boolean(props.commentAuthors.some((a) => a.slug === session()?.user.slug))}
|
}}
|
||||||
comment={reaction}
|
/>
|
||||||
/>
|
</li>
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</ul>
|
</ul>
|
||||||
<ShowIfAuthenticated
|
</div>
|
||||||
fallback={
|
<ul class={styles.comments}>
|
||||||
<div class={styles.signInMessage} id="comments">
|
<For each={sortedComments().filter((r) => !r.replyTo)}>
|
||||||
{t('To write a comment, you must')}
|
{(reaction) => (
|
||||||
<a href="?modal=auth&mode=register" class={styles.link}>
|
<Comment
|
||||||
{t('sign up')}
|
sortedComments={sortedComments()}
|
||||||
</a>
|
isArticleAuthor={Boolean(props.commentAuthors.some((a) => a.slug === session()?.user.slug))}
|
||||||
{t('or')}
|
comment={reaction}
|
||||||
<a href="?modal=auth&mode=login" class={styles.link}>
|
/>
|
||||||
{t('sign in')}
|
)}
|
||||||
</a>
|
</For>
|
||||||
</div>
|
</ul>
|
||||||
}
|
<ShowIfAuthenticated
|
||||||
>
|
fallback={
|
||||||
<CommentEditor
|
<div class={styles.signInMessage} id="comments">
|
||||||
placeholder={t('Write a comment...')}
|
{t('To write a comment, you must')}
|
||||||
clear={submitted()}
|
<a href="?modal=auth&mode=register" class={styles.link}>
|
||||||
onSubmit={(value) => handleSubmitComment(value)}
|
{t('sign up')}
|
||||||
/>
|
</a>
|
||||||
</ShowIfAuthenticated>
|
{t('or')}
|
||||||
</Show>
|
<a href="?modal=auth&mode=login" class={styles.link}>
|
||||||
</div>
|
{t('sign in')}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<CommentEditor
|
||||||
|
placeholder={t('Write a comment...')}
|
||||||
|
clear={submitted()}
|
||||||
|
onSubmit={(value) => handleSubmitComment(value)}
|
||||||
|
/>
|
||||||
|
</ShowIfAuthenticated>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,8 @@ import { capitalize, formatDate } from '../../utils'
|
||||||
import './Full.scss'
|
import './Full.scss'
|
||||||
import { Icon } from '../_shared/Icon'
|
import { Icon } from '../_shared/Icon'
|
||||||
import { AuthorCard } from '../Author/Card'
|
import { AuthorCard } from '../Author/Card'
|
||||||
import { createMemo, For, Match, onMount, Show, Switch } from 'solid-js'
|
import { createEffect, createMemo, createSignal, For, Match, onMount, Show, Switch } from 'solid-js'
|
||||||
import type { Author, Shout } from '../../graphql/types.gen'
|
import type { Author, Reaction, Shout } from '../../graphql/types.gen'
|
||||||
import { ReactionKind } from '../../graphql/types.gen'
|
import { ReactionKind } from '../../graphql/types.gen'
|
||||||
|
|
||||||
import MD from './MD'
|
import MD from './MD'
|
||||||
|
@ -23,6 +23,7 @@ import { useReactions } from '../../context/reactions'
|
||||||
import { loadShout } from '../../stores/zine/articles'
|
import { loadShout } from '../../stores/zine/articles'
|
||||||
import { Title } from '@solidjs/meta'
|
import { Title } from '@solidjs/meta'
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../context/localize'
|
||||||
|
import { checkReaction } from '../../utils/checkReaction'
|
||||||
|
|
||||||
interface ArticleProps {
|
interface ArticleProps {
|
||||||
article: Shout
|
article: Shout
|
||||||
|
@ -59,7 +60,8 @@ const MediaView = (props: { media: MediaItem; kind: Shout['layout'] }) => {
|
||||||
|
|
||||||
export const FullArticle = (props: ArticleProps) => {
|
export const FullArticle = (props: ArticleProps) => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const { session } = useSession()
|
const { userSlug, session } = useSession()
|
||||||
|
const [isReactionsLoaded, setIsReactionsLoaded] = createSignal(false)
|
||||||
const formattedDate = createMemo(() => formatDate(new Date(props.article.createdAt)))
|
const formattedDate = createMemo(() => formatDate(new Date(props.article.createdAt)))
|
||||||
|
|
||||||
const mainTopic = createMemo(
|
const mainTopic = createMemo(
|
||||||
|
@ -81,6 +83,14 @@ export const FullArticle = (props: ArticleProps) => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
await loadReactionsBy({
|
||||||
|
by: { shout: props.article.slug }
|
||||||
|
})
|
||||||
|
|
||||||
|
setIsReactionsLoaded(true)
|
||||||
|
})
|
||||||
|
|
||||||
const canEdit = () => props.article.authors?.some((a) => a.slug === session()?.user?.slug)
|
const canEdit = () => props.article.authors?.some((a) => a.slug === session()?.user?.slug)
|
||||||
|
|
||||||
const bookmark = (ev) => {
|
const bookmark = (ev) => {
|
||||||
|
@ -96,27 +106,52 @@ export const FullArticle = (props: ArticleProps) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
const {
|
const {
|
||||||
actions: { createReaction }
|
reactionEntities,
|
||||||
|
actions: { loadReactionsBy, createReaction, deleteReaction }
|
||||||
} = useReactions()
|
} = useReactions()
|
||||||
|
|
||||||
const handleUpvote = async () => {
|
const updateReactions = () => {
|
||||||
await createReaction({
|
loadReactionsBy({
|
||||||
kind: ReactionKind.Like,
|
by: { shout: props.article.slug }
|
||||||
shout: props.article.id
|
|
||||||
})
|
})
|
||||||
|
|
||||||
await loadShout(props.article.slug)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDownvote = async () => {
|
const isUpvoted = createMemo(() =>
|
||||||
await createReaction({
|
checkReaction(Object.values(reactionEntities), ReactionKind.Like, userSlug(), props.article.id)
|
||||||
kind: ReactionKind.Dislike,
|
)
|
||||||
shout: props.article.id
|
|
||||||
})
|
|
||||||
|
|
||||||
await loadShout(props.article.slug)
|
const isDownvoted = createMemo(() =>
|
||||||
|
checkReaction(Object.values(reactionEntities), ReactionKind.Dislike, userSlug(), props.article.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleRatingChange = async (isUpvote: boolean) => {
|
||||||
|
const reactionKind = isUpvote ? ReactionKind.Like : ReactionKind.Dislike
|
||||||
|
const isReacted = (isUpvote && isUpvoted()) || (!isUpvote && isDownvoted())
|
||||||
|
|
||||||
|
if (isReacted) {
|
||||||
|
const reactionToDelete = Object.values(reactionEntities).find(
|
||||||
|
(r) =>
|
||||||
|
r.kind === reactionKind &&
|
||||||
|
r.createdBy.slug === userSlug() &&
|
||||||
|
r.shout.id === props.article.id &&
|
||||||
|
!r.replyTo
|
||||||
|
)
|
||||||
|
await deleteReaction(reactionToDelete.id)
|
||||||
|
} else {
|
||||||
|
await createReaction({
|
||||||
|
kind: reactionKind,
|
||||||
|
shout: props.article.id
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
loadShout(props.article.slug)
|
||||||
|
updateReactions()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
console.log('reactions', reactionEntities)
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Title>{props.article.title}</Title>
|
<Title>{props.article.title}</Title>
|
||||||
|
@ -200,8 +235,10 @@ export const FullArticle = (props: ArticleProps) => {
|
||||||
<RatingControl
|
<RatingControl
|
||||||
rating={props.article.stat?.rating}
|
rating={props.article.stat?.rating}
|
||||||
class={styles.ratingControl}
|
class={styles.ratingControl}
|
||||||
onUpvote={handleUpvote}
|
onUpvote={() => handleRatingChange(true)}
|
||||||
onDownvote={handleDownvote}
|
onDownvote={() => handleRatingChange(false)}
|
||||||
|
isUpvoted={isUpvoted()}
|
||||||
|
isDownvoted={isDownvoted()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -265,22 +302,24 @@ export const FullArticle = (props: ArticleProps) => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class={styles.shoutAuthorsList}>
|
<div class={styles.shoutAuthorsList}>
|
||||||
<Show when={props.article?.authors?.length > 1}>
|
<Show when={props.article.authors.length > 1}>
|
||||||
<h4>{t('Authors')}</h4>
|
<h4>{t('Authors')}</h4>
|
||||||
</Show>
|
</Show>
|
||||||
<For each={props.article?.authors}>
|
<For each={props.article.authors}>
|
||||||
{(a: Author) => (
|
{(a) => (
|
||||||
<div class="col-xl-6">
|
<div class="col-xl-6">
|
||||||
<AuthorCard author={a} compact={false} hasLink={true} liteButtons={true} />
|
<AuthorCard author={a} compact={false} hasLink={true} liteButtons={true} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
<CommentsTree
|
<Show when={isReactionsLoaded()}>
|
||||||
shoutId={props.article?.id}
|
<CommentsTree
|
||||||
shoutSlug={props.article?.slug}
|
shoutId={props.article.id}
|
||||||
commentAuthors={props.article?.authors}
|
shoutSlug={props.article.slug}
|
||||||
/>
|
commentAuthors={props.article.authors}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -1,6 +1,13 @@
|
||||||
.rating {
|
.rating {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
|
&.isDownvoted .downvoteButton,
|
||||||
|
&.isUpvoted .upvoteButton {
|
||||||
|
background: #000;
|
||||||
|
border-color: #000;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ratingValue {
|
.ratingValue {
|
||||||
|
|
|
@ -12,12 +12,17 @@ interface RatingControlProps {
|
||||||
|
|
||||||
export const RatingControl = (props: RatingControlProps) => {
|
export const RatingControl = (props: RatingControlProps) => {
|
||||||
return (
|
return (
|
||||||
<div class={clsx(props.class, styles.rating)}>
|
<div
|
||||||
<button class={styles.ratingControl} onClick={props.onDownvote}>
|
class={clsx(styles.rating, props.class, {
|
||||||
|
[styles.isUpvoted]: props.isUpvoted,
|
||||||
|
[styles.isDownvoted]: props.isDownvoted
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<button class={clsx(styles.ratingControl, styles.downvoteButton)} onClick={props.onDownvote}>
|
||||||
−
|
−
|
||||||
</button>
|
</button>
|
||||||
<span class={styles.ratingValue}>{props?.rating || ''}</span>
|
<span class={styles.ratingValue}>{props?.rating || ''}</span>
|
||||||
<button class={styles.ratingControl} onClick={props.onUpvote}>
|
<button class={clsx(styles.ratingControl, styles.upvoteButton)} onClick={props.onUpvote}>
|
||||||
+
|
+
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -12,6 +12,11 @@ import stylesHeader from '../Nav/Header.module.scss'
|
||||||
import { getDescription } from '../../utils/meta'
|
import { getDescription } from '../../utils/meta'
|
||||||
import { FeedArticlePopup } from './FeedArticlePopup'
|
import { FeedArticlePopup } from './FeedArticlePopup'
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../context/localize'
|
||||||
|
import { ReactionKind } from '../../graphql/types.gen'
|
||||||
|
import { loadShout } from '../../stores/zine/articles'
|
||||||
|
import { useReactions } from '../../context/reactions'
|
||||||
|
import { checkReaction } from '../../utils/checkReaction'
|
||||||
|
import { useSession } from '../../context/session'
|
||||||
|
|
||||||
interface ArticleCardProps {
|
interface ArticleCardProps {
|
||||||
settings?: {
|
settings?: {
|
||||||
|
@ -61,6 +66,13 @@ const getTitleAndSubtitle = (article: Shout): { title: string; subtitle: string
|
||||||
export const ArticleCard = (props: ArticleCardProps) => {
|
export const ArticleCard = (props: ArticleCardProps) => {
|
||||||
const { t, lang } = useLocalize()
|
const { t, lang } = useLocalize()
|
||||||
|
|
||||||
|
const { userSlug } = useSession()
|
||||||
|
|
||||||
|
const {
|
||||||
|
reactionEntities,
|
||||||
|
actions: { createReaction, deleteReaction, loadReactionsBy }
|
||||||
|
} = useReactions()
|
||||||
|
|
||||||
const mainTopic =
|
const mainTopic =
|
||||||
props.article.topics.find((articleTopic) => articleTopic.slug === props.article.mainTopic) ||
|
props.article.topics.find((articleTopic) => articleTopic.slug === props.article.mainTopic) ||
|
||||||
props.article.topics[0]
|
props.article.topics[0]
|
||||||
|
@ -73,7 +85,41 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
||||||
|
|
||||||
const { title, subtitle } = getTitleAndSubtitle(props.article)
|
const { title, subtitle } = getTitleAndSubtitle(props.article)
|
||||||
|
|
||||||
const { cover, layout, slug, authors, stat, body } = props.article
|
const { cover, layout, slug, authors, stat, body, id } = props.article
|
||||||
|
|
||||||
|
const updateReactions = () => {
|
||||||
|
loadReactionsBy({
|
||||||
|
by: { shout: slug }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const isUpvoted = createMemo(() =>
|
||||||
|
checkReaction(Object.values(reactionEntities), ReactionKind.Like, userSlug(), id)
|
||||||
|
)
|
||||||
|
|
||||||
|
const isDownvoted = createMemo(() =>
|
||||||
|
checkReaction(Object.values(reactionEntities), ReactionKind.Dislike, userSlug(), id)
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleRatingChange = async (isUpvote: boolean) => {
|
||||||
|
const reactionKind = isUpvote ? ReactionKind.Like : ReactionKind.Dislike
|
||||||
|
const isReacted = (isUpvote && isUpvoted()) || (!isUpvote && isDownvoted())
|
||||||
|
|
||||||
|
if (isReacted) {
|
||||||
|
const reactionToDelete = Object.values(reactionEntities).find(
|
||||||
|
(r) => r.kind === reactionKind && r.createdBy.slug === userSlug() && r.shout.id === id && !r.replyTo
|
||||||
|
)
|
||||||
|
await deleteReaction(reactionToDelete.id)
|
||||||
|
} else {
|
||||||
|
await createReaction({
|
||||||
|
kind: reactionKind,
|
||||||
|
shout: id
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
loadShout(slug)
|
||||||
|
updateReactions()
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
|
@ -165,7 +211,14 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
||||||
<Show when={props.settings?.isFeedMode}>
|
<Show when={props.settings?.isFeedMode}>
|
||||||
<section class={styles.shoutCardDetails}>
|
<section class={styles.shoutCardDetails}>
|
||||||
<div class={styles.shoutCardDetailsContent}>
|
<div class={styles.shoutCardDetailsContent}>
|
||||||
<RatingControl rating={stat?.rating} class={styles.shoutCardDetailsItem} />
|
<RatingControl
|
||||||
|
rating={stat.rating}
|
||||||
|
class={styles.shoutCardDetailsItem}
|
||||||
|
onUpvote={() => handleRatingChange(true)}
|
||||||
|
onDownvote={() => handleRatingChange(false)}
|
||||||
|
isUpvoted={isUpvoted()}
|
||||||
|
isDownvoted={isDownvoted()}
|
||||||
|
/>
|
||||||
|
|
||||||
<div class={clsx(styles.shoutCardDetailsItem, styles.shoutCardDetailsViewed)}>
|
<div class={clsx(styles.shoutCardDetailsItem, styles.shoutCardDetailsViewed)}>
|
||||||
<Icon name="eye" class={clsx(styles.icon, styles.feedControlIcon)} />
|
<Icon name="eye" class={clsx(styles.icon, styles.feedControlIcon)} />
|
||||||
|
|
|
@ -59,11 +59,18 @@ export const FeedView = () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
const loadMore = async () => {
|
const loadMore = async () => {
|
||||||
const { hasMore } = await loadShouts({
|
const { hasMore, newShouts } = await loadShouts({
|
||||||
filters: { visibility: 'community' },
|
filters: { visibility: 'community' },
|
||||||
limit: FEED_PAGE_SIZE,
|
limit: FEED_PAGE_SIZE,
|
||||||
offset: sortedArticles().length
|
offset: sortedArticles().length
|
||||||
})
|
})
|
||||||
|
|
||||||
|
loadReactionsBy({
|
||||||
|
by: {
|
||||||
|
shouts: newShouts.map((s) => s.slug)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
setIsLoadMoreButtonVisible(hasMore)
|
setIsLoadMoreButtonVisible(hasMore)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import type { JSX } from 'solid-js'
|
import type { JSX } from 'solid-js'
|
||||||
import { createContext, onCleanup, useContext } from 'solid-js'
|
import { createContext, onCleanup, useContext } from 'solid-js'
|
||||||
import type { Reaction, ReactionBy, ReactionInput } from '../graphql/types.gen'
|
import type { Reaction, ReactionBy, ReactionInput } from '../graphql/types.gen'
|
||||||
|
import { ReactionKind } from '../graphql/types.gen'
|
||||||
import { apiClient } from '../utils/apiClient'
|
import { apiClient } from '../utils/apiClient'
|
||||||
import { createStore } from 'solid-js/store'
|
import { createStore } from 'solid-js/store'
|
||||||
|
|
||||||
|
@ -41,12 +42,33 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => {
|
||||||
|
|
||||||
const createReaction = async (input: ReactionInput): Promise<void> => {
|
const createReaction = async (input: ReactionInput): Promise<void> => {
|
||||||
const reaction = await apiClient.createReaction(input)
|
const reaction = await apiClient.createReaction(input)
|
||||||
setReactionEntities(reaction.id, reaction)
|
|
||||||
|
const changes = {
|
||||||
|
[reaction.id]: reaction
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([ReactionKind.Like, ReactionKind.Dislike].includes(reaction.kind)) {
|
||||||
|
const oppositeReactionKind =
|
||||||
|
reaction.kind === ReactionKind.Like ? ReactionKind.Dislike : ReactionKind.Like
|
||||||
|
|
||||||
|
const oppositeReaction = Object.values(reactionEntities).find(
|
||||||
|
(r) =>
|
||||||
|
r.kind === oppositeReactionKind &&
|
||||||
|
r.createdBy.slug === reaction.createdBy.slug &&
|
||||||
|
r.shout.id === reaction.shout.id &&
|
||||||
|
r.replyTo === reaction.replyTo
|
||||||
|
)
|
||||||
|
|
||||||
|
if (oppositeReaction) {
|
||||||
|
changes[oppositeReaction.id] = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setReactionEntities(changes)
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteReaction = async (id: number): Promise<void> => {
|
const deleteReaction = async (id: number): Promise<void> => {
|
||||||
const reaction = await apiClient.destroyReaction(id)
|
const reaction = await apiClient.destroyReaction(id)
|
||||||
console.debug('[deleteReaction]:', reaction.id)
|
|
||||||
setReactionEntities((oldState) => ({
|
setReactionEntities((oldState) => ({
|
||||||
...oldState,
|
...oldState,
|
||||||
[reaction.id]: undefined
|
[reaction.id]: undefined
|
||||||
|
|
|
@ -2,16 +2,19 @@ import { createSignal, Show } from 'solid-js'
|
||||||
import { PageLayout } from '../../components/_shared/PageLayout'
|
import { PageLayout } from '../../components/_shared/PageLayout'
|
||||||
import { Donate } from '../../components/Discours/Donate'
|
import { Donate } from '../../components/Discours/Donate'
|
||||||
import { Icon } from '../../components/_shared/Icon'
|
import { Icon } from '../../components/_shared/Icon'
|
||||||
|
import { Meta, Title } from '@solidjs/meta'
|
||||||
// const title = t('Support us')
|
import { useLocalize } from '../../context/localize'
|
||||||
|
|
||||||
export const HelpPage = () => {
|
export const HelpPage = () => {
|
||||||
const [indexExpanded, setIndexExpanded] = createSignal(true)
|
const [indexExpanded, setIndexExpanded] = createSignal(true)
|
||||||
|
|
||||||
|
const { t } = useLocalize()
|
||||||
|
|
||||||
const toggleIndexExpanded = () => setIndexExpanded((oldExpanded) => !oldExpanded)
|
const toggleIndexExpanded = () => setIndexExpanded((oldExpanded) => !oldExpanded)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout>
|
<PageLayout>
|
||||||
|
<Title>{t('Support us')}</Title>
|
||||||
<Meta name="description">Здесь можно поддержать Дискурс материально.</Meta>
|
<Meta name="description">Здесь можно поддержать Дискурс материально.</Meta>
|
||||||
<Meta name="keywords">Discours.io, помощь, благотворительность</Meta>
|
<Meta name="keywords">Discours.io, помощь, благотворительность</Meta>
|
||||||
|
|
||||||
|
|
|
@ -126,24 +126,32 @@ const addSortedArticles = (articles: Shout[]) => {
|
||||||
export const loadShout = async (slug: string): Promise<void> => {
|
export const loadShout = async (slug: string): Promise<void> => {
|
||||||
const newArticle = await apiClient.getShout(slug)
|
const newArticle = await apiClient.getShout(slug)
|
||||||
addArticles([newArticle])
|
addArticles([newArticle])
|
||||||
|
const newArticleIndex = sortedArticles().findIndex((s) => s.id === newArticle.id)
|
||||||
|
if (newArticleIndex >= 0) {
|
||||||
|
const newSortedArticles = [...sortedArticles()]
|
||||||
|
newSortedArticles[newArticleIndex] = newArticle
|
||||||
|
setSortedArticles(newSortedArticles)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const loadShouts = async (options: LoadShoutsOptions): Promise<{ hasMore: boolean }> => {
|
export const loadShouts = async (
|
||||||
const newArticles = await apiClient.getShouts({
|
options: LoadShoutsOptions
|
||||||
|
): Promise<{ hasMore: boolean; newShouts: Shout[] }> => {
|
||||||
|
const newShouts = await apiClient.getShouts({
|
||||||
...options,
|
...options,
|
||||||
limit: options.limit + 1
|
limit: options.limit + 1
|
||||||
})
|
})
|
||||||
|
|
||||||
const hasMore = newArticles.length === options.limit + 1
|
const hasMore = newShouts.length === options.limit + 1
|
||||||
|
|
||||||
if (hasMore) {
|
if (hasMore) {
|
||||||
newArticles.splice(-1)
|
newShouts.splice(-1)
|
||||||
}
|
}
|
||||||
|
|
||||||
addArticles(newArticles)
|
addArticles(newShouts)
|
||||||
addSortedArticles(newArticles)
|
addSortedArticles(newShouts)
|
||||||
|
|
||||||
return { hasMore }
|
return { hasMore, newShouts }
|
||||||
}
|
}
|
||||||
|
|
||||||
export const resetSortedArticles = () => {
|
export const resetSortedArticles = () => {
|
||||||
|
|
|
@ -278,7 +278,7 @@ export const apiClient = {
|
||||||
const resp = await publicGraphQLClient
|
const resp = await publicGraphQLClient
|
||||||
.query(reactionsLoadBy, { by, limit: limit ?? 1000, offset: 0 })
|
.query(reactionsLoadBy, { by, limit: limit ?? 1000, offset: 0 })
|
||||||
.toPromise()
|
.toPromise()
|
||||||
console.debug(resp)
|
// console.debug(resp)
|
||||||
return resp.data.loadReactionsBy
|
return resp.data.loadReactionsBy
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
11
src/utils/checkReaction.ts
Normal file
11
src/utils/checkReaction.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import type { Reaction, ReactionKind } from '../graphql/types.gen'
|
||||||
|
|
||||||
|
export const checkReaction = (
|
||||||
|
reactions: Reaction[],
|
||||||
|
reactionKind: ReactionKind,
|
||||||
|
userSlug: string,
|
||||||
|
shoutId: number
|
||||||
|
) =>
|
||||||
|
reactions.some(
|
||||||
|
(r) => r.kind === reactionKind && r.createdBy.slug === userSlug && r.shout.id === shoutId && !r.replyTo
|
||||||
|
)
|
Loading…
Reference in New Issue
Block a user