Delete reaction (#85)

* Delete reactions
This commit is contained in:
Ilya Y 2023-01-20 07:40:55 +03:00 committed by GitHub
parent f8231fe28f
commit 1977493dcd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 777 additions and 287 deletions

View File

@ -1,12 +1,50 @@
.comment { .comment {
background-color: #fff; margin: 0 -2.4rem 0.5em;
margin: 0 -2.4rem 1.5em;
padding: 0.8rem 2.4rem; padding: 0.8rem 2.4rem;
transition: background-color 0.3s; transition: background-color 0.3s;
position: relative;
&:last-child {
margin-bottom: 0;
}
.comment {
&:before,
&:after {
content: '';
left: 0;
position: absolute;
}
&:before {
border-bottom: 2px solid #ccc;
border-left: 2px solid #ccc;
border-radius: 0 0 0 1.2rem;
top: -1rem;
height: 2.4rem;
width: 1.2rem;
}
&:after {
background: #ccc;
height: 100%;
top: 0;
width: 2px;
}
&:last-child:after {
display: none;
}
}
.shout-body {
@include font-size(1.5rem);
margin-bottom: 1em;
*:last-child {
margin-bottom: 0;
}
}
.author {
align-items: center;
margin-bottom: 1.4rem;
}
}
.commentContent {
&:hover { &:hover {
background-color: #f6f6f6;
.commentControlReply, .commentControlReply,
.commentControlShare, .commentControlShare,
.commentControlDelete, .commentControlDelete,
@ -15,60 +53,22 @@
opacity: 1; opacity: 1;
} }
} }
.shout-body {
@include font-size(1.5rem);
margin-bottom: 1em;
*:last-child {
margin-bottom: 0;
}
}
.author {
align-items: center;
margin-bottom: 1.4rem;
}
} }
.commentLevel1 {
margin-left: 3.2rem;
}
.commentLevel2 {
margin-left: 6.4rem;
}
.commentLevel3 {
margin-left: 9.6rem;
}
.commentLevel4 {
margin-left: 12.8rem;
}
.commentLevel5 {
margin-left: 16rem;
}
.commentControls { .commentControls {
@include font-size(1.2rem); @include font-size(1.2rem);
margin-bottom: 0.5em; margin-bottom: 0.5em;
} }
.commentControlReply, .commentControlReply,
.commentControlShare, .commentControlShare,
.commentControlDelete, .commentControlDelete,
.commentControlEdit, .commentControlEdit,
.commentControlComplain { .commentControlComplain {
@include media-breakpoint-up(md) { @include media-breakpoint-up(md) {
opacity: 0; //opacity: 0;
transition: opacity 0.3s; transition: opacity 0.3s;
} }
} }
.commentControlReply, .commentControlReply,
.commentControlShare, .commentControlShare,
.commentControlDelete, .commentControlDelete,
@ -78,7 +78,6 @@
width: 1.2rem; width: 1.2rem;
} }
} }
.commentControl { .commentControl {
border: none; border: none;
color: #696969; color: #696969;
@ -89,116 +88,74 @@
padding: 0.2em 0.3em; padding: 0.2em 0.3em;
transition: opacity 0.2s, color 0.3s, background-color 0.3s; transition: opacity 0.2s, color 0.3s, background-color 0.3s;
vertical-align: top; vertical-align: top;
&:hover { &:hover {
background: #000; background: #000;
color: #fff; color: #fff;
.icon { .icon {
filter: invert(1); filter: invert(1);
opacity: 1; opacity: 1;
} }
} }
.icon { .icon {
filter: invert(0); filter: invert(0);
margin-right: 0.3em; margin-right: 0.3em;
opacity: 0.6; opacity: 0.6;
transition: filter 0.3s, opacity 0.2s; transition: filter 0.3s, opacity 0.2s;
img { img {
margin-bottom: -0.1em; margin-bottom: -0.1em;
} }
} }
} }
.commentControlReply { .commentControlReply {
.icon { .icon {
height: 1.2em; height: 1.2em;
width: 1.2em; width: 1.2em;
} }
} }
.commentBody { .commentBody {
@include font-size(1.5rem); @include font-size(1.5rem);
line-height: 1.47; line-height: 1.47;
} }
.commentAuthor, .commentAuthor,
.commentDate, .commentDate,
.commentRating { .commentRating {
@include font-size(1.2rem); @include font-size(1.2rem);
} }
.commentDate { .commentDate {
color: rgb(0 0 0 / 30%); color: rgb(0 0 0 / 30%);
flex: 1; flex: 1;
@include media-breakpoint-down(md) { @include media-breakpoint-down(md) {
margin-left: 1rem; margin-left: 1rem;
} }
} }
.commentDetails { .commentDetails {
display: flex; display: flex;
margin-bottom: 1.2rem; margin-bottom: 1.2rem;
} }
.commentRating { .commentRating {
align-items: center; align-items: center;
display: flex; display: flex;
font-weight: bold; font-weight: bold;
} }
.commentRatingValue { .commentRatingValue {
padding: 0 0.3em; padding: 0 0.3em;
} }
.commentRatingPositive { .commentRatingPositive {
color: #2bb452; color: #2bb452;
} }
.commentRatingNegative { .commentRatingNegative {
color: #d00820; color: #d00820;
} }
.commentRatingControl { .commentRatingControl {
border-left: 6px solid transparent; border-left: 6px solid transparent;
border-right: 6px solid transparent; border-right: 6px solid transparent;
height: 0; height: 0;
width: 0; width: 0;
} }
.commentRatingControlUp { .commentRatingControlUp {
border-bottom: 8px solid rgb(0 0 0 / 40%); border-bottom: 8px solid rgb(0 0 0 / 40%);
} }
.commentRatingControlDown { .commentRatingControlDown {
border-top: 8px solid rgb(0 0 0 / 40%); border-top: 8px solid rgb(0 0 0 / 40%);
} }
.replyForm {
background: #fff;
border: 2px solid rgb(38 56 217 / 50%);
border-radius: 0.8rem;
margin-left: 2.4rem;
position: relative;
textarea {
border: none;
border-radius: 0.8rem;
padding-top: 1.2rem;
}
}
.replyFormControls {
padding: 0.5rem 1.6rem 1.6rem;
text-align: right;
button {
@include font-size(1.6rem);
margin-left: 1.2rem;
}
}

View File

@ -1,32 +1,67 @@
import styles from './Comment.module.scss' import styles from './Comment.module.scss'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
import { AuthorCard } from '../Author/Card' import { AuthorCard } from '../Author/Card'
import { Show, createMemo, createSignal } from 'solid-js' import { Show, createMemo, createSignal, For } from 'solid-js'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import type { Author, Reaction as Point } from '../../graphql/types.gen' import type { Author, Reaction } from '../../graphql/types.gen'
import { t } from '../../utils/intl' import { t } from '../../utils/intl'
// import { createReaction, updateReaction, deleteReaction } from '../../stores/zine/reactions' import { createReaction, deleteReaction } from '../../stores/zine/reactions'
import MD from './MD' import MD from './MD'
import { deleteReaction } from '../../stores/zine/reactions'
import { formatDate } from '../../utils' import { formatDate } from '../../utils'
import { SharePopup } from './SharePopup' import { SharePopup } from './SharePopup'
import stylesHeader from '../Nav/Header.module.scss' import stylesHeader from '../Nav/Header.module.scss'
import Userpic from '../Author/Userpic' import Userpic from '../Author/Userpic'
import { useSession } from '../../context/session'
import { ReactionKind } from '../../graphql/types.gen'
import GrowingTextarea from '../_shared/GrowingTextarea'
export default (props: { type Props = {
level?: number comment: Reaction
comment: Partial<Point>
canEdit?: boolean
compact?: boolean compact?: boolean
}) => { reactions?: Reaction[]
}
const Comment = (props: Props) => {
const [isReplyVisible, setIsReplyVisible] = createSignal(false) const [isReplyVisible, setIsReplyVisible] = createSignal(false)
const [loading, setLoading] = createSignal(false)
const [errorMessage, setErrorMessage] = createSignal<string | null>(null)
const { session } = useSession()
const canEdit = createMemo(() => props.comment.createdBy?.slug === session()?.user?.slug)
const comment = createMemo(() => props.comment) const comment = createMemo(() => props.comment)
const body = createMemo(() => (comment().body || '').trim()) const body = createMemo(() => (comment().body || '').trim())
const remove = () => { const remove = async () => {
if (comment()?.id) { if (comment()?.id) {
console.log('[comment] removing', comment().id) try {
deleteReaction(comment().id) await deleteReaction(comment().id)
} catch (error) {
console.error('[deleteReaction]', error)
}
}
}
const handleCreate = async (value) => {
try {
setLoading(true)
await createReaction(
{
kind: ReactionKind.Comment,
replyTo: props.comment.id,
body: value,
shout: props.comment.shout.id
},
{
name: session().user.name,
userpic: session().user.userpic,
slug: session().user.slug
}
)
setIsReplyVisible(false)
setLoading(false)
} catch (error) {
console.error('[handleCreate reaction]:', error)
setErrorMessage(t('Something went wrong, please try again'))
} }
} }
const formattedDate = createMemo(() => const formattedDate = createMemo(() =>
@ -34,7 +69,7 @@ export default (props: {
) )
return ( return (
<div class={clsx(styles.comment, { [styles[`commentLevel${props.level}`]]: Boolean(props.level) })}> <li class={styles.comment}>
<Show when={!!body()}> <Show when={!!body()}>
<div class={styles.commentContent}> <div class={styles.commentContent}>
<Show <Show
@ -73,10 +108,9 @@ export default (props: {
</div> </div>
</div> </div>
</Show> </Show>
<div <div
class={styles.commentBody} class={styles.commentBody}
contenteditable={props.canEdit} contenteditable={canEdit()}
id={'comment-' + (comment().id || '')} id={'comment-' + (comment().id || '')}
> >
<MD body={body()} /> <MD body={body()} />
@ -85,14 +119,15 @@ export default (props: {
<Show when={!props.compact}> <Show when={!props.compact}>
<div class={styles.commentControls}> <div class={styles.commentControls}>
<button <button
class={clsx(styles.commentControl, styles.commentControlReply)} disabled={loading()}
onClick={() => setIsReplyVisible(!isReplyVisible())} onClick={() => setIsReplyVisible(!isReplyVisible())}
class={clsx(styles.commentControl, styles.commentControlReply)}
> >
<Icon name="reply" class={styles.icon} /> <Icon name="reply" class={styles.icon} />
{t('Reply')} {loading() ? t('Loading') : t('Reply')}
</button> </button>
<Show when={props.canEdit}> <Show when={canEdit()}>
{/*FIXME implement edit comment modal*/} {/*FIXME implement edit comment modal*/}
{/*<button*/} {/*<button*/}
{/* class={clsx(styles.commentControl, styles.commentControlEdit)}*/} {/* class={clsx(styles.commentControl, styles.commentControlEdit)}*/}
@ -129,19 +164,27 @@ export default (props: {
</div> </div>
<Show when={isReplyVisible()}> <Show when={isReplyVisible()}>
<form class={styles.replyForm}> <GrowingTextarea
<textarea name="reply" id="reply" rows="5" /> placeholder={t('Write comment')}
<div class={styles.replyFormControls}> submitButtonText={t('Send')}
<button class="button button--light" onClick={() => setIsReplyVisible(false)}> cancelButtonText={t('cancel')}
{t('Cancel')} submit={(value) => handleCreate(value)}
</button> loading={loading()}
<button class="button">{t('Send')}</button> errorMessage={errorMessage()}
</div> />
</form>
</Show> </Show>
</Show> </Show>
</div> </div>
</Show> </Show>
</div> <Show when={props.reactions}>
<ul>
<For each={props.reactions.filter((r) => r.replyTo === props.comment.id)}>
{(reaction) => <Comment reactions={props.reactions} comment={reaction} />}
</For>
</ul>
</Show>
</li>
) )
} }
export default Comment

View File

@ -1,36 +1,33 @@
import { For, Show, createMemo, createSignal, onMount } from 'solid-js' import { For, Show, createMemo, createSignal, onMount } from 'solid-js'
import { useSession } from '../../context/session'
import Comment from './Comment' import Comment from './Comment'
import { t } from '../../utils/intl' import { t } from '../../utils/intl'
import { showModal } from '../../stores/ui'
import styles from '../../styles/Article.module.scss' import styles from '../../styles/Article.module.scss'
import { useReactionsStore } from '../../stores/zine/reactions' import { createReaction, useReactionsStore } from '../../stores/zine/reactions'
import type { Reaction } from '../../graphql/types.gen' import type { Reaction } from '../../graphql/types.gen'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { byCreated, byStat } from '../../utils/sortby' import { byCreated, byStat } from '../../utils/sortby'
import { Loading } from '../Loading' import { Loading } from '../Loading'
import GrowingTextarea from '../_shared/GrowingTextarea'
import { ReactionKind } from '../../graphql/types.gen'
import { useSession } from '../../context/session'
const ARTICLE_COMMENTS_PAGE_SIZE = 50 const ARTICLE_COMMENTS_PAGE_SIZE = 50
const MAX_COMMENT_LEVEL = 6 const MAX_COMMENT_LEVEL = 6
export const CommentsTree = (props: { shoutSlug: string }) => { export const CommentsTree = (props: { shoutSlug: string; shoutId: number }) => {
const [getCommentsPage, setCommentsPage] = createSignal(0) const [getCommentsPage, setCommentsPage] = createSignal(0)
const [commentsOrder, setCommentsOrder] = createSignal<'rating' | 'createdAt'>('createdAt') const [commentsOrder, setCommentsOrder] = createSignal<'rating' | 'createdAt'>('createdAt')
const [isCommentsLoading, setIsCommentsLoading] = createSignal(false) const [isCommentsLoading, setIsCommentsLoading] = createSignal(false)
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false) const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
const { session } = useSession()
const { sortedReactions, loadReactionsBy } = useReactionsStore() const { sortedReactions, loadReactionsBy } = useReactionsStore()
const reactions = createMemo<Reaction[]>(() => const reactions = createMemo<Reaction[]>(() =>
sortedReactions() sortedReactions().sort(commentsOrder() === 'rating' ? byStat('rating') : byCreated)
.sort(commentsOrder() === 'rating' ? byStat('rating') : byCreated)
.filter((r) => r.shout.slug === props.shoutSlug)
) )
const { session } = useSession()
const loadMore = async () => { const loadMore = async () => {
try { try {
const page = getCommentsPage() const page = getCommentsPage()
setIsCommentsLoading(true) setIsCommentsLoading(true)
const { hasMore } = await loadReactionsBy({ const { hasMore } = await loadReactionsBy({
by: { shout: props.shoutSlug, comment: true }, by: { shout: props.shoutSlug, comment: true },
limit: ARTICLE_COMMENTS_PAGE_SIZE, limit: ARTICLE_COMMENTS_PAGE_SIZE,
@ -49,8 +46,32 @@ export const CommentsTree = (props: { shoutSlug: string }) => {
return level return level
} }
onMount(async () => await loadMore()) onMount(async () => await loadMore())
const [loading, setLoading] = createSignal<boolean>(false)
const [errorMessage, setErrorMessage] = createSignal<string | null>(null)
const handleSubmitComment = async (value) => {
try {
setLoading(true)
await createReaction(
{
kind: ReactionKind.Comment,
body: value,
shout: props.shoutId
},
{
name: session().user.name,
userpic: session().user.userpic,
slug: session().user.slug
}
)
setLoading(false)
} catch (error) {
setErrorMessage(t('Something went wrong, please try again'))
console.error('[handleCreate reaction]:', error)
}
}
return ( return (
<> <div>
<Show when={!isCommentsLoading()} fallback={<Loading />}> <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}>
@ -82,46 +103,27 @@ export const CommentsTree = (props: { shoutSlug: string }) => {
</li> </li>
</ul> </ul>
</div> </div>
<ul class={styles.comments}>
<For each={reactions().reverse()}> <For
{(reaction: Reaction) => ( each={reactions()
<Comment .reverse()
comment={reaction} .filter((r) => !r.replyTo)}
level={getCommentLevel(reaction)} >
canEdit={reaction.createdBy?.slug === session()?.user?.slug} {(reaction) => <Comment reactions={reactions()} comment={reaction} />}
/>
)}
</For> </For>
</ul>
<Show when={isLoadMoreButtonVisible()}> <Show when={isLoadMoreButtonVisible()}>
<button onClick={loadMore}>{t('Load more')}</button> <button onClick={loadMore}>{t('Load more')}</button>
</Show> </Show>
<GrowingTextarea
placeholder={t('Write comment')}
submitButtonText={t('Send')}
cancelButtonText={t('cancel')}
submit={(value) => handleSubmitComment(value)}
loading={loading()}
errorMessage={errorMessage()}
/>
</Show> </Show>
<Show
when={!session()?.user?.slug}
fallback={
<form class={styles.commentForm}>
<div class="pretty-form__item">
<input type="text" id="new-comment" placeholder={t('Write comment')} />
<label for="new-comment">{t('Write comment')}</label>
</div> </div>
</form>
}
>
<div class={styles.commentWarning} id="comments">
{t('To leave a comment you please')}
<a
href={''}
onClick={(evt) => {
evt.preventDefault()
showModal('auth')
}}
>
<i>{t('sign up or sign in')}</i>
</a>
</div>
</Show>
</>
) )
} }

View File

@ -224,7 +224,7 @@ export const FullArticle = (props: ArticleProps) => {
)} )}
</For> </For>
</div> </div>
<CommentsTree shoutSlug={props.article?.slug} /> <CommentsTree shoutSlug={props.article?.slug} shoutId={props.article?.id} />
</div> </div>
</div> </div>
) )

View File

@ -16,8 +16,7 @@
justify-content: center; justify-content: center;
height: 0.9em; height: 0.9em;
line-height: 0; line-height: 0;
@include font-size(3.6rem); font-size: 1.6em;
padding: 0; padding: 0;
width: 0.9em; width: 0.9em;

View File

@ -54,6 +54,10 @@
padding: 0 0 0 42px; padding: 0 0 0 42px;
} }
@include media-breakpoint-down(md) {
flex-wrap: wrap;
}
a { a {
background: #f7f7f7; background: #f7f7f7;
border: none; border: none;
@ -136,6 +140,17 @@
} }
} }
.authorSubscribeSocial {
align-items: center;
display: flex;
@include media-breakpoint-down(sm) {
flex: 1 100%;
justify-content: center;
margin-top: 1em;
}
}
.buttonSubscribe { .buttonSubscribe {
align-items: center; align-items: center;
aspect-ratio: 1/1; aspect-ratio: 1/1;
@ -180,9 +195,12 @@
} }
.authorPage { .authorPage {
@include media-breakpoint-down(md) {
justify-content: center;
}
.authorName { .authorName {
@include font-size(3.4rem); @include font-size(3.4rem);
font-weight: 500; font-weight: 500;
margin-bottom: 0.2em; margin-bottom: 0.2em;
} }
@ -195,10 +213,18 @@
.authorSubscribe { .authorSubscribe {
margin-top: 2rem; margin-top: 2rem;
padding-left: 0; padding-left: 0;
@include media-breakpoint-down(md) {
justify-content: center;
}
} }
.authorDetails { .authorDetails {
display: block; display: block;
@include media-breakpoint-down(md) {
flex: 1 100%;
}
} }
.buttonLabel { .buttonLabel {
@ -237,6 +263,16 @@
.button { .button {
margin-right: 1.6rem; margin-right: 1.6rem;
vertical-align: middle; vertical-align: middle;
&:last-of-type {
margin-right: 0;
}
@include media-breakpoint-down(sm) {
display: block;
margin-bottom: 0.5em;
margin-right: 0;
}
} }
} }

View File

@ -38,11 +38,13 @@ export const AuthorCard = (props: AuthorCardProps) => {
actions: { loadSession } actions: { loadSession }
} = useSession() } = useSession()
if (!props.author) return false // FIXME: с сервера должен приходить автор реакции (ApiClient.CreateReaction)
const [isSubscribing, setIsSubscribing] = createSignal(false) const [isSubscribing, setIsSubscribing] = createSignal(false)
const subscribed = createMemo<boolean>( const subscribed = createMemo<boolean>(() => {
() => session()?.news?.authors?.some((u) => u === props.author.slug) || false return session()?.news?.authors?.some((u) => u === props.author.slug) || false
) })
const subscribe = async (really = true) => { const subscribe = async (really = true) => {
setIsSubscribing(true) setIsSubscribing(true)
@ -177,7 +179,9 @@ export const AuthorCard = (props: AuthorCardProps) => {
</button> </button>
<Show when={!props.noSocialButtons}> <Show when={!props.noSocialButtons}>
<div class={styles.authorSubscribeSocial}>
<For each={props.author.links}>{(link) => <a href={link} />}</For> <For each={props.author.links}>{(link) => <a href={link} />}</For>
</div>
</Show> </Show>
</Show> </Show>
</div> </div>

View File

@ -1,11 +1,22 @@
.user-details { .user-details {
margin-bottom: 5.4rem; margin: 0 0 5.4rem;
@include media-breakpoint-up(md) {
margin-left: 160px;
}
@include media-breakpoint-up(lg) {
margin-left: 235px;
}
@include media-breakpoint-down(md) {
text-align: center;
}
} }
.author-page { .author-page {
.view-switcher { .view-switcher {
@include font-size(1.5rem); @include font-size(1.5rem);
margin-top: 0; margin-top: 0;
button { button {

View File

@ -5,7 +5,7 @@ import './Full.scss'
export const AuthorFull = (props: { author: Author }) => { export const AuthorFull = (props: { author: Author }) => {
return ( return (
<div class="row"> <div class="row">
<div class="col-md-8 offset-md-2 user-details"> <div class="col-md-9 col-lg-8 user-details">
<AuthorCard author={props.author} compact={false} isAuthorPage={true} /> <AuthorCard author={props.author} compact={false} isAuthorPage={true} />
</div> </div>
</div> </div>

View File

@ -35,12 +35,16 @@
} }
.big.circlewrap { .big.circlewrap {
margin-right: 4.8rem; margin-right: 0;
max-width: 168px; max-width: 168px;
min-width: 168px; min-width: 168px;
height: 168px; height: 168px;
width: 168px; width: 168px;
@include media-breakpoint-up(md) {
margin-right: 4.8rem;
}
.userpic { .userpic {
font-size: 2em; font-size: 2em;
line-height: 168px; line-height: 168px;

View File

@ -11,8 +11,6 @@ import { useSession } from '../../context/session'
import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient' import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
const log = getLogger('TopicCard')
interface TopicProps { interface TopicProps {
topic: Topic topic: Topic
compact?: boolean compact?: boolean

View File

@ -1,4 +1,4 @@
import { createEffect, createMemo, createSignal, onMount, Show, Suspense } from 'solid-js' import { onMount, Show, Suspense } from 'solid-js'
import { FullArticle } from '../Article/FullArticle' import { FullArticle } from '../Article/FullArticle'
import { t } from '../../utils/intl' import { t } from '../../utils/intl'
import type { Shout, Reaction } from '../../graphql/types.gen' import type { Shout, Reaction } from '../../graphql/types.gen'

View File

@ -0,0 +1,55 @@
.ratingContainer {
@include font-size(1.5rem);
display: inline-block;
vertical-align: top;
}
.ratingControl {
@include font-size(1.5rem);
display: inline-flex;
margin-left: 1em;
vertical-align: middle;
}
.additionalControls {
white-space: nowrap;
@include media-breakpoint-up(md) {
text-align: right;
}
}
.userpic {
background: #fff;
box-shadow: 0 0 0 2px #fff;
display: inline-block;
margin-right: -1.2rem;
vertical-align: top;
}
.subscribers {
cursor: pointer;
display: inline-block;
margin: -0.4rem 2em 0 0;
vertical-align: top;
}
.subscribersCounter {
background: #fff;
border: 2px solid #000;
border-radius: 100%;
@include font-size(1rem);
font-weight: bold;
height: 32px;
line-height: 30px;
position: relative;
text-align: center;
width: 32px;
z-index: 1;
}
.subscribersList {
max-height: 15em;
overflow: auto;
position: relative;
}

View File

@ -1,7 +1,7 @@
import { Show, createMemo, createSignal, For, onMount } from 'solid-js' import { Show, createMemo, createSignal, Switch, onMount, For, Match, createEffect } from 'solid-js'
import type { Author, Shout } from '../../graphql/types.gen' import type { Author, Shout } from '../../graphql/types.gen'
import { Row1 } from '../Feed/Row1'
import { Row2 } from '../Feed/Row2' import { Row2 } from '../Feed/Row2'
import { Row3 } from '../Feed/Row3'
import { AuthorFull } from '../Author/Full' import { AuthorFull } from '../Author/Full'
import { t } from '../../utils/intl' import { t } from '../../utils/intl'
import { useAuthorsStore } from '../../stores/zine/authors' import { useAuthorsStore } from '../../stores/zine/authors'
@ -9,9 +9,17 @@ import { loadShouts, useArticlesStore } from '../../stores/zine/articles'
import { useTopicsStore } from '../../stores/zine/topics' import { useTopicsStore } from '../../stores/zine/topics'
import { useRouter } from '../../stores/router' import { useRouter } from '../../stores/router'
import { Beside } from '../Feed/Beside'
import { restoreScrollPosition, saveScrollPosition } from '../../utils/scroll' import { restoreScrollPosition, saveScrollPosition } from '../../utils/scroll'
import { splitToPages } from '../../utils/splitToPages' import { splitToPages } from '../../utils/splitToPages'
import { RatingControl } from '../Article/RatingControl'
import styles from './Author.module.scss'
import { clsx } from 'clsx'
import Userpic from '../Author/Userpic'
import { Popup } from '../_shared/Popup'
import { AuthorCard } from '../Author/Card'
import { loadReactionsBy, REACTIONS_AMOUNT_PER_PAGE } from '../../stores/zine/reactions'
import { apiClient } from '../../utils/apiClient'
import Comment from '../Article/Comment'
// TODO: load reactions on client // TODO: load reactions on client
type AuthorProps = { type AuthorProps = {
@ -23,7 +31,7 @@ type AuthorProps = {
} }
type AuthorPageSearchParams = { type AuthorPageSearchParams = {
by: '' | 'viewed' | 'rating' | 'commented' | 'recent' | 'followed' by: '' | 'viewed' | 'rating' | 'commented' | 'recent' | 'followed' | 'about' | 'popular'
} }
export const PRERENDERED_ARTICLES_COUNT = 12 export const PRERENDERED_ARTICLES_COUNT = 12
@ -38,6 +46,7 @@ export const AuthorView = (props: AuthorProps) => {
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false) const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
const author = createMemo(() => authorEntities()[props.authorSlug]) const author = createMemo(() => authorEntities()[props.authorSlug])
const subscribers = Array.from({ length: 12 }).fill(author())
const { searchParams, changeSearchParam } = useRouter<AuthorPageSearchParams>() const { searchParams, changeSearchParam } = useRouter<AuthorPageSearchParams>()
const loadMore = async () => { const loadMore = async () => {
@ -69,6 +78,23 @@ export const AuthorView = (props: AuthorProps) => {
splitToPages(sortedArticles(), PRERENDERED_ARTICLES_COUNT, LOAD_MORE_PAGE_SIZE) splitToPages(sortedArticles(), PRERENDERED_ARTICLES_COUNT, LOAD_MORE_PAGE_SIZE)
) )
console.log('!!! authorEntities():', author())
const [commented, setCommented] = createSignal([])
createEffect(async () => {
if (searchParams().by === 'commented') {
try {
const data = await apiClient.getReactionsBy({
by: { comment: true, createdBy: props.authorSlug },
limit: 100,
offset: 0
})
setCommented(data)
} catch (error) {
console.log('!!! error:', error)
}
}
})
return ( return (
<div class="author-page"> <div class="author-page">
<Show when={author()} fallback={<div class="center">{t('Loading')}</div>}> <Show when={author()} fallback={<div class="center">{t('Loading')}</div>}>
@ -89,41 +115,78 @@ export const AuthorView = (props: AuthorProps) => {
</li> </li>
<li classList={{ selected: searchParams().by === 'commented' }}> <li classList={{ selected: searchParams().by === 'commented' }}>
<button type="button" onClick={() => changeSearchParam('by', 'commented')}> <button type="button" onClick={() => changeSearchParam('by', 'commented')}>
{t('Discussing')} {t('Comments')}
</button>
</li>
<li classList={{ selected: searchParams().by === 'popular' }}>
<button type="button" onClick={() => changeSearchParam('by', 'popular')}>
Популярное
</button>
</li>
<li classList={{ selected: searchParams().by === 'about' }}>
<button type="button" onClick={() => changeSearchParam('by', 'about')}>
О себе
</button> </button>
</li> </li>
</ul> </ul>
</div> </div>
<div class="col-md-4"> <div class={clsx('col-md-4', styles.additionalControls)}>
<div class="mode-switcher"> <Popup
{`${t('Show')} `} {...props}
<span class="mode-switcher__control">{t('All posts')}</span> trigger={
<div class={styles.subscribers}>
<Userpic user={author()} class={styles.userpic} />
<Userpic user={author()} class={styles.userpic} />
<Userpic user={author()} class={styles.userpic} />
<div class={clsx(styles.userpic, styles.subscribersCounter)}>12</div>
</div>
}
variant="tiny"
>
<ul class={clsx('nodash', styles.subscribersList)}>
<For each={subscribers}>
{(item: Author) => (
<li>
<AuthorCard author={item} hideDescription={true} hideFollow={true} hasLink={true} />
</li>
)}
</For>
</ul>
</Popup>
<div class={styles.ratingContainer}>
Карма
<RatingControl rating={19} class={styles.ratingControl} />
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<Beside <Switch fallback={<p>дефолтное состояние</p>}>
title={t('Topics which supported by author')} <Match when={searchParams().by === 'about'}>
values={topicsByAuthor()[author().slug]?.slice(0, 5)} <h1>About</h1>
beside={sortedArticles()[0]} <p>{JSON.stringify(authorEntities())}</p>
wrapper={'topic'} </Match>
topicShortDescription={true} <Match when={searchParams().by === 'commented'}>
isTopicCompact={true} <For each={commented()}>{(comment) => <Comment comment={comment} />}</For>
isTopicInRow={true} </Match>
iconButton={true} <Match when={searchParams().by === 'popular'}>
/> <Row1 article={sortedArticles()[0]} />
<Row3 articles={sortedArticles().slice(1, 4)} /> <Row2 articles={sortedArticles().slice(1, 3)} isEqual={true} />
<Row2 articles={sortedArticles().slice(4, 6)} /> <Row1 article={sortedArticles()[3]} />
<Row3 articles={sortedArticles().slice(6, 9)} /> <Row2 articles={sortedArticles().slice(4, 6)} isEqual={true} />
<Row3 articles={sortedArticles().slice(9, 12)} /> <Row1 article={sortedArticles()[6]} />
<Row2 articles={sortedArticles().slice(7, 9)} isEqual={true} />
<For each={pages()}> <For each={pages()}>
{(page) => ( {(page) => (
<> <>
<Row3 articles={page.slice(0, 3)} /> <Row1 article={page[0]} />
<Row3 articles={page.slice(3, 6)} /> <Row2 articles={page.slice(1, 3)} isEqual={true} />
<Row3 articles={page.slice(6, 9)} /> <Row1 article={page[3]} />
<Row2 articles={page.slice(4, 6)} isEqual={true} />
<Row1 article={page[6]} />
<Row2 articles={page.slice(7, 9)} isEqual={true} />
</> </>
)} )}
</For> </For>
@ -135,6 +198,8 @@ export const AuthorView = (props: AuthorProps) => {
</button> </button>
</p> </p>
</Show> </Show>
</Match>
</Switch>
</Show> </Show>
</div> </div>
) )

View File

@ -7,7 +7,7 @@ import { ArticleCard } from '../Feed/Card'
import { AuthorCard } from '../Author/Card' import { AuthorCard } from '../Author/Card'
import { t } from '../../utils/intl' import { t } from '../../utils/intl'
import { FeedSidebar } from '../Feed/Sidebar' import { FeedSidebar } from '../Feed/Sidebar'
import CommentCard from '../Article/Comment' import Comment from '../Article/Comment'
import { loadShouts, useArticlesStore } from '../../stores/zine/articles' import { loadShouts, useArticlesStore } from '../../stores/zine/articles'
import { useReactionsStore } from '../../stores/zine/reactions' import { useReactionsStore } from '../../stores/zine/reactions'
import { useAuthorsStore } from '../../stores/zine/authors' import { useAuthorsStore } from '../../stores/zine/authors'
@ -128,7 +128,7 @@ export const FeedView = () => {
<section class="feed-comments"> <section class="feed-comments">
<h4>{t('Comments')}</h4> <h4>{t('Comments')}</h4>
<For each={topComments()}> <For each={topComments()}>
{(comment) => <CommentCard comment={comment} compact={true} />} {(comment) => <Comment comment={comment} reactions={[]} compact={true} />}
</For> </For>
</section> </section>
<Show when={topTopics().length > 0}> <Show when={topTopics().length > 0}>

View File

@ -0,0 +1,61 @@
.button {
border-radius: 2px;
display: flex;
align-items: center;
font-weight: 500;
cursor: pointer;
&.primary {
background: #000;
color: #fff;
&:hover {
color: #ccc;
}
&:active {
color: #9fa1a7;
}
}
&.secondary {
background: #f7f7f7;
color: #141414;
&:hover {
background: #e8e8e8;
}
&:active {
background: #ccc;
}
}
&:disabled,
&:disabled:hover {
cursor: default;
color: #9fa1a7;
background: #f6f6f6;
}
&.loading,
&.loading:hover {
background: #f6f6f6;
}
&.L {
height: 56px;
min-width: 80px;
font-size: 20px;
padding: 16px 20px;
}
&.M {
height: 40px;
min-width: 64px;
font-size: 17px;
padding: 8px 16px;
}
&.S {
height: 32px;
min-width: 53px;
font-size: 15px;
padding: 8px 16px;
}
}

View File

@ -0,0 +1,30 @@
import type { JSX } from 'solid-js'
import { clsx } from 'clsx'
import styles from './Button.module.scss'
type Props = {
value: string | JSX.Element
size?: 'S' | 'M' | 'L'
variant?: 'primary' | 'secondary'
type?: 'submit' | 'button'
loading?: boolean
disabled?: boolean
onClick?: () => void
}
const Button = (props: Props) => {
return (
<button
onClick={props.onClick}
type={props.type ?? 'button'}
disabled={props.loading || props.disabled}
class={clsx(styles.button, styles[props.size ?? 'M'], styles[props.variant ?? 'primary'], {
[styles.loading]: props.loading
})}
>
{props.value}
</button>
)
}
export default Button

View File

@ -0,0 +1 @@
export { default } from './Button'

View File

@ -0,0 +1,98 @@
.GrowingTextarea {
.wrapper {
border: 2px solid #e8e8e8;
border-radius: 8px;
padding: 16px;
.growArea {
display: grid;
width: 100%;
&::after {
content: attr(data-replicated-value) ' ';
white-space: pre-wrap;
visibility: hidden;
transition: height 1.3s ease-in-out;
}
& textarea {
margin-bottom: 0;
font-family: inherit;
resize: none;
overflow: hidden;
border: none;
padding: 0;
background: transparent;
font-size: 15px;
line-height: 20px;
&:focus,
&:focus-visible,
&:active {
border: none;
outline: none;
box-shadow: none;
}
}
&::after,
& textarea {
font-weight: 400;
font-size: 14px;
line-height: 20px;
padding: 0;
grid-area: 1 / 1 / 2 / 2;
width: 100%;
min-height: unset;
}
}
.actions {
display: flex;
flex-direction: row;
overflow: hidden;
max-height: 0;
height: 0;
opacity: 1;
transition: all 0.3s ease-in-out;
&.visible {
max-height: 88px;
height: auto;
}
.buttons {
margin-top: 16px;
display: flex;
flex-direction: row;
gap: 12px;
margin-left: auto;
}
}
}
.error {
color: red;
font-size: 12px;
}
.loginMessage {
margin: 16px 0;
display: flex;
background: #f1f2f3;
border-radius: 8px;
padding: 16px;
text-align: center;
font-size: 20px;
.link {
color: #2638d9;
text-decoration: none;
border: none;
transition: 0.3s ease-in-out;
&:hover {
text-decoration: underline;
background: unset;
}
}
}
}

View File

@ -0,0 +1,98 @@
import styles from './GrowingTextarea.module.scss'
import { showModal } from '../../../stores/ui'
import { createEffect, createSignal, Show } from 'solid-js'
import { t } from '../../../utils/intl'
import Button from '../Button'
import { clsx } from 'clsx'
import { useSession } from '../../../context/session'
type Props = {
placeholder?: string
submit?: (value: string) => void
submitButtonText?: string
cancelButtonText?: string
loading?: boolean
errorMessage?: string
loginRequired?: boolean
}
let growArea // textarea autoresize ghost element
const GrowingTextarea = (props: Props) => {
const { session } = useSession()
const [inputText, setInputText] = createSignal<string | undefined>('')
const handleChangeMessage = (event) => {
setInputText(event.target.value)
}
createEffect(() => {
growArea.dataset.replicatedValue = inputText()
})
const handleSubmit = (event) => {
event.preventDefault()
props.submit(inputText())
if (!props.errorMessage) {
setInputText('')
}
}
return (
<form onSubmit={(event) => handleSubmit(event)} class={styles.GrowingTextarea}>
<div class={styles.wrapper}>
<div class={styles.growArea} ref={growArea}>
<textarea
value={inputText()}
rows={1}
onInput={(event) => handleChangeMessage(event)}
placeholder={props?.placeholder}
/>
</div>
<div class={clsx(styles.actions, { [styles.visible]: inputText().trim().length > 0 })}>
<div class={styles.buttons}>
<Show when={props.cancelButtonText}>
<Button
variant="secondary"
size="M"
loading={props.loading}
onClick={() => setInputText('')}
value={props.cancelButtonText}
/>
</Show>
<Show when={props.submitButtonText}>
<Button
variant="primary"
size="M"
type="submit"
loading={props.loading}
value={props.submitButtonText}
/>
</Show>
</div>
</div>
</div>
<Show when={props.errorMessage}>
<div class={styles.error}>{props.errorMessage}</div>
</Show>
<Show when={!session()}>
<div class={styles.loginMessage}>
<div>
{t('To write a comment, you must')}&nbsp;
<a
class={styles.link}
href={''}
onClick={(evt) => {
evt.preventDefault()
showModal('auth')
}}
>
{t('sign up or sign in')}
</a>
</div>
</div>
</Show>
</form>
)
}
export default GrowingTextarea

View File

@ -0,0 +1 @@
export { default } from './GrowingTextarea'

View File

@ -4,18 +4,19 @@
.popup { .popup {
background: #fff; background: #fff;
top: calc(100% + 8px);
opacity: 1;
color: #000; color: #000;
position: absolute;
z-index: 100;
min-width: 144px; min-width: 144px;
opacity: 1;
position: absolute;
top: calc(100% + 8px);
z-index: 100;
ul { ul {
margin-bottom: 0; margin-bottom: 0;
li { li {
position: relative; position: relative;
&:last-child { &:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
@ -24,11 +25,12 @@
&.bordered { &.bordered {
@include font-size(1.6rem); @include font-size(1.6rem);
border: 2px solid #000; border: 2px solid #000;
padding: 2.4rem; padding: 2.4rem;
ul li { ul li {
margin-bottom: 1.6rem; margin-bottom: 1.6rem;
&:last-child { &:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
@ -37,11 +39,12 @@
&.tiny { &.tiny {
@include font-size(1.4rem); @include font-size(1.4rem);
box-shadow: 0 4px 60px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 60px rgba(0, 0, 0, 0.1);
padding: 1rem; padding: 1rem;
ul li { ul li {
margin-bottom: 1rem; margin-bottom: 1rem;
&:last-child { &:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
@ -67,22 +70,22 @@
white-space: nowrap; white-space: nowrap;
&:hover { &:hover {
img { .icon img {
filter: invert(0); filter: invert(0);
} }
} }
} }
.icon {
display: inline-block;
width: 3.6rem;
img { img {
filter: invert(1); filter: invert(1);
max-height: 2rem; max-height: 2rem;
max-width: 2rem; max-width: 2rem;
transition: filter 0.3s; transition: filter 0.3s;
} }
.icon {
display: inline-block;
width: 3.6rem;
} }
} }

View File

@ -6,26 +6,11 @@ export default gql`
error error
reaction { reaction {
id id
createdBy {
slug
name
userpic
}
body body
kind kind
range range
createdAt createdAt
shout replyTo
replyTo {
id
createdBy {
slug
userpic
name
}
body
kind
}
} }
} }
} }

View File

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

View File

@ -4,6 +4,7 @@ export default gql`
query LoadShoutQuery($slug: String!) { query LoadShoutQuery($slug: String!) {
loadShout(slug: $slug) { loadShout(slug: $slug) {
_id: slug _id: slug
id
title title
subtitle subtitle
slug slug

View File

@ -4,6 +4,7 @@ export default gql`
query LoadShoutsQuery($options: LoadShoutsOptions) { query LoadShoutsQuery($options: LoadShoutsOptions) {
loadShouts(options: $options) { loadShouts(options: $options) {
_id: slug _id: slug
id
title title
subtitle subtitle
slug slug

View File

@ -8,7 +8,9 @@ export default gql`
range range
replyTo replyTo
shout { shout {
id
slug slug
title
} }
createdBy { createdBy {
name name

View File

@ -238,7 +238,7 @@ export type MutationDeleteMessageArgs = {
} }
export type MutationDeleteReactionArgs = { export type MutationDeleteReactionArgs = {
id: Scalars['Int'] reaction: Scalars['Int']
} }
export type MutationDeleteShoutArgs = { export type MutationDeleteShoutArgs = {
@ -498,10 +498,10 @@ export type ReactionBy = {
export type ReactionInput = { export type ReactionInput = {
body?: InputMaybe<Scalars['String']> body?: InputMaybe<Scalars['String']>
kind: Scalars['Int'] kind: ReactionKind
range?: InputMaybe<Scalars['String']> range?: InputMaybe<Scalars['String']>
replyTo?: InputMaybe<Scalars['Int']> replyTo?: InputMaybe<Scalars['Int']>
shout: Scalars['String'] shout: Scalars['Int']
} }
export enum ReactionKind { export enum ReactionKind {

View File

@ -52,6 +52,7 @@
"Fill email": "Введите почту", "Fill email": "Введите почту",
"Follow": "Подписаться", "Follow": "Подписаться",
"Follow the topic": "Подписаться на тему", "Follow the topic": "Подписаться на тему",
"Followers": "Подписчики",
"Forgot password?": "Забыли пароль?", "Forgot password?": "Забыли пароль?",
"Full name": "Имя и фамилия", "Full name": "Имя и фамилия",
"Get to know the most intelligent people of our time, edit and discuss the articles, share your expertise, rate and decide what to publish in the magazine": "Познакомитесь с выдающимися людьми нашего времени, участвуйте в редактировании и обсуждении статей, выступайте экспертом, оценивайте материалы других авторов со всего мира и определяйте, какие статьи будут опубликованы в журнале", "Get to know the most intelligent people of our time, edit and discuss the articles, share your expertise, rate and decide what to publish in the magazine": "Познакомитесь с выдающимися людьми нашего времени, участвуйте в редактировании и обсуждении статей, выступайте экспертом, оценивайте материалы других авторов со всего мира и определяйте, какие статьи будут опубликованы в журнале",
@ -188,6 +189,7 @@
"create_group": "Создать группу", "create_group": "Создать группу",
"discourse_theme": "Тема дискурса", "discourse_theme": "Тема дискурса",
"cancel": "Отмена", "cancel": "Отмена",
"Send": "Отправить",
"group_chat": "Общий чат", "group_chat": "Общий чат",
"Choose who you want to write to": "Выберите кому хотите написать", "Choose who you want to write to": "Выберите кому хотите написать",
"Start conversation": "Начать беседу", "Start conversation": "Начать беседу",
@ -211,5 +213,7 @@
"Forward": "Переслать", "Forward": "Переслать",
"Select": "Выбрать", "Select": "Выбрать",
"slug is used by another user": "Имя уже занято другим пользователем", "slug is used by another user": "Имя уже занято другим пользователем",
"It does not look like url": "Это не похоже на ссылку" "It does not look like url": "Это не похоже на ссылку",
"Something went wrong, please try again": "Что-то пошло не так, попробуйте еще раз",
"To write a comment, you must": "Чтобы написать комментарий, необходимо"
} }

View File

@ -1,4 +1,4 @@
import type { Reaction } from '../../graphql/types.gen' import type { Reaction, ReactionInput, User } from '../../graphql/types.gen'
import { apiClient } from '../../utils/apiClient' import { apiClient } from '../../utils/apiClient'
import { createSignal } from 'solid-js' import { createSignal } from 'solid-js'
// TODO: import { roomConnect } from '../../utils/p2p' // TODO: import { roomConnect } from '../../utils/p2p'
@ -23,20 +23,28 @@ export const loadReactionsBy = async ({
setSortedReactions(data) setSortedReactions(data)
return { hasMore } return { hasMore }
} }
export const createReaction = async (reaction: Reaction) => {
const { reaction: r } = await apiClient.createReaction({ reaction }) export const createReaction = async (
return r input: ReactionInput,
createdBy: { name: string; userpic: string; slug: string }
) => {
const reaction = await apiClient.createReaction(input)
reaction.shout = { id: input.shout }
reaction.createdBy = createdBy
setSortedReactions((prev) => [...prev, reaction])
} }
export const deleteReaction = async (reactionId: number) => {
const reaction = await apiClient.destroyReaction(reactionId)
console.debug('[deleteReaction]:', reaction.reaction.id)
setSortedReactions(sortedReactions().filter((item) => item.id !== reaction.reaction.id))
}
export const updateReaction = async (reaction: Reaction) => { export const updateReaction = async (reaction: Reaction) => {
const { reaction: r } = await apiClient.updateReaction({ reaction }) const { reaction: r } = await apiClient.updateReaction({ reaction })
return r return r
} }
export const deleteReaction = async (reactionId: number) => {
const resp = await apiClient.destroyReaction({ id: reactionId })
console.debug(resp)
return resp
}
export const useReactionsStore = () => { export const useReactionsStore = () => {
return { return {
reactionsByShout, reactionsByShout,

View File

@ -252,6 +252,20 @@ img {
} }
} }
.comments {
margin: 0;
&,
ul {
list-style: none;
padding: 0;
}
ul {
margin: 1em 0 0 2.4rem;
}
}
.commentsHeaderWrapper { .commentsHeaderWrapper {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;

View File

@ -10,9 +10,11 @@ import type {
QueryLoadMessagesByArgs, QueryLoadMessagesByArgs,
MutationCreateChatArgs, MutationCreateChatArgs,
MutationCreateMessageArgs, MutationCreateMessageArgs,
Chat,
QueryLoadRecipientsArgs, QueryLoadRecipientsArgs,
ProfileInput ProfileInput,
ReactionInput,
Chat,
ReactionBy
} from '../graphql/types.gen' } from '../graphql/types.gen'
import { publicGraphQLClient } from '../graphql/publicGraphQLClient' import { publicGraphQLClient } from '../graphql/publicGraphQLClient'
import { getToken, privateGraphQLClient } from '../graphql/privateGraphQLClient' import { getToken, privateGraphQLClient } from '../graphql/privateGraphQLClient'
@ -226,27 +228,24 @@ export const apiClient = {
}, },
createArticle: async ({ article }: { article: ShoutInput }) => { createArticle: async ({ article }: { article: ShoutInput }) => {
const response = await privateGraphQLClient.mutation(createArticle, { shout: article }).toPromise() const response = await privateGraphQLClient.mutation(createArticle, { shout: article }).toPromise()
console.debug('createArticle response:', response) console.debug('[createArticle]:', response.data)
return response.data.createShout return response.data.createShout
}, },
createReaction: async ({ reaction }) => { createReaction: async (input: ReactionInput) => {
const response = await privateGraphQLClient.mutation(reactionCreate, { reaction }).toPromise() const response = await privateGraphQLClient.mutation(reactionCreate, { reaction: input }).toPromise()
console.debug('[api-client] [api] create reaction mutation called') console.debug('[createReaction]:', response)
return response.data.createReaction return response.data.createReaction.reaction
}, },
destroyReaction: async (id: number) => {
// CUDL const response = await privateGraphQLClient.mutation(reactionDestroy, { reaction: id }).toPromise()
console.debug('[destroyReaction]:', response)
updateReaction: async ({ reaction }) => {
const response = await privateGraphQLClient.mutation(reactionUpdate, { reaction }).toPromise()
return response.data.createReaction
},
destroyReaction: async ({ id }) => {
const response = await privateGraphQLClient.mutation(reactionDestroy, { id }).toPromise()
return response.data.deleteReaction return response.data.deleteReaction
}, },
updateReaction: async (reaction) => {
const response = await privateGraphQLClient.mutation(reactionUpdate, reaction).toPromise()
return response.data.createReaction
},
getAuthorsBy: async (options: QueryLoadAuthorsByArgs) => { getAuthorsBy: async (options: QueryLoadAuthorsByArgs) => {
const resp = await publicGraphQLClient.query(authorsLoadBy, options).toPromise() const resp = await publicGraphQLClient.query(authorsLoadBy, options).toPromise()
return resp.data.loadAuthorsBy return resp.data.loadAuthorsBy
@ -269,7 +268,16 @@ export const apiClient = {
if (resp.error) console.debug(resp) if (resp.error) console.debug(resp)
return resp.data.loadShouts return resp.data.loadShouts
}, },
getReactionsBy: async ({ by, limit = REACTIONS_AMOUNT_PER_PAGE, offset = 0 }) => {
getReactionsBy: async ({
by,
limit = REACTIONS_AMOUNT_PER_PAGE,
offset = 0
}: {
by: ReactionBy
limit: number
offset: number
}) => {
const resp = await publicGraphQLClient.query(reactionsLoadBy, { by, limit, offset }).toPromise() const resp = await publicGraphQLClient.query(reactionsLoadBy, { by, limit, offset }).toPromise()
console.debug(resp) console.debug(resp)
return resp.data.loadReactionsBy return resp.data.loadReactionsBy
@ -288,15 +296,13 @@ export const apiClient = {
createMessage: async (options: MutationCreateMessageArgs) => { createMessage: async (options: MutationCreateMessageArgs) => {
const resp = await privateGraphQLClient.mutation(createMessage, options).toPromise() const resp = await privateGraphQLClient.mutation(createMessage, options).toPromise()
return resp.data.createMessage.message return resp.data.createMessage
}, },
getChatMessages: async (options: QueryLoadMessagesByArgs) => { getChatMessages: async (options: QueryLoadMessagesByArgs) => {
const resp = await privateGraphQLClient.query(chatMessagesLoadBy, options).toPromise() const resp = await privateGraphQLClient.query(chatMessagesLoadBy, options).toPromise()
console.log('[getChatMessages]', resp) return resp.data.loadChat
return resp.data.loadMessagesBy.messages
}, },
getRecipients: async (options: QueryLoadRecipientsArgs) => { getRecipients: async (options: QueryLoadRecipientsArgs) => {
const resp = await privateGraphQLClient.query(loadRecipients, options).toPromise() const resp = await privateGraphQLClient.query(loadRecipients, options).toPromise()
return resp.data.loadRecipients.members return resp.data.loadRecipients.members