parent
f8231fe28f
commit
1977493dcd
|
@ -1,12 +1,50 @@
|
|||
.comment {
|
||||
background-color: #fff;
|
||||
margin: 0 -2.4rem 1.5em;
|
||||
margin: 0 -2.4rem 0.5em;
|
||||
padding: 0.8rem 2.4rem;
|
||||
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 {
|
||||
background-color: #f6f6f6;
|
||||
|
||||
.commentControlReply,
|
||||
.commentControlShare,
|
||||
.commentControlDelete,
|
||||
|
@ -15,60 +53,22 @@
|
|||
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 {
|
||||
@include font-size(1.2rem);
|
||||
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.commentControlReply,
|
||||
.commentControlShare,
|
||||
.commentControlDelete,
|
||||
.commentControlEdit,
|
||||
.commentControlComplain {
|
||||
@include media-breakpoint-up(md) {
|
||||
opacity: 0;
|
||||
//opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
}
|
||||
|
||||
.commentControlReply,
|
||||
.commentControlShare,
|
||||
.commentControlDelete,
|
||||
|
@ -78,7 +78,6 @@
|
|||
width: 1.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.commentControl {
|
||||
border: none;
|
||||
color: #696969;
|
||||
|
@ -89,116 +88,74 @@
|
|||
padding: 0.2em 0.3em;
|
||||
transition: opacity 0.2s, color 0.3s, background-color 0.3s;
|
||||
vertical-align: top;
|
||||
|
||||
&:hover {
|
||||
background: #000;
|
||||
color: #fff;
|
||||
|
||||
.icon {
|
||||
filter: invert(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
filter: invert(0);
|
||||
margin-right: 0.3em;
|
||||
opacity: 0.6;
|
||||
transition: filter 0.3s, opacity 0.2s;
|
||||
|
||||
img {
|
||||
margin-bottom: -0.1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.commentControlReply {
|
||||
.icon {
|
||||
height: 1.2em;
|
||||
width: 1.2em;
|
||||
}
|
||||
}
|
||||
|
||||
.commentBody {
|
||||
@include font-size(1.5rem);
|
||||
|
||||
line-height: 1.47;
|
||||
}
|
||||
|
||||
.commentAuthor,
|
||||
.commentDate,
|
||||
.commentRating {
|
||||
@include font-size(1.2rem);
|
||||
}
|
||||
|
||||
.commentDate {
|
||||
color: rgb(0 0 0 / 30%);
|
||||
flex: 1;
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.commentDetails {
|
||||
display: flex;
|
||||
margin-bottom: 1.2rem;
|
||||
}
|
||||
|
||||
.commentRating {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.commentRatingValue {
|
||||
padding: 0 0.3em;
|
||||
}
|
||||
|
||||
.commentRatingPositive {
|
||||
color: #2bb452;
|
||||
}
|
||||
|
||||
.commentRatingNegative {
|
||||
color: #d00820;
|
||||
}
|
||||
|
||||
.commentRatingControl {
|
||||
border-left: 6px solid transparent;
|
||||
border-right: 6px solid transparent;
|
||||
height: 0;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.commentRatingControlUp {
|
||||
border-bottom: 8px solid rgb(0 0 0 / 40%);
|
||||
}
|
||||
|
||||
.commentRatingControlDown {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,32 +1,67 @@
|
|||
import styles from './Comment.module.scss'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
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 type { Author, Reaction as Point } from '../../graphql/types.gen'
|
||||
import type { Author, Reaction } from '../../graphql/types.gen'
|
||||
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 { deleteReaction } from '../../stores/zine/reactions'
|
||||
import { formatDate } from '../../utils'
|
||||
import { SharePopup } from './SharePopup'
|
||||
import stylesHeader from '../Nav/Header.module.scss'
|
||||
import Userpic from '../Author/Userpic'
|
||||
import { useSession } from '../../context/session'
|
||||
import { ReactionKind } from '../../graphql/types.gen'
|
||||
import GrowingTextarea from '../_shared/GrowingTextarea'
|
||||
|
||||
export default (props: {
|
||||
level?: number
|
||||
comment: Partial<Point>
|
||||
canEdit?: boolean
|
||||
type Props = {
|
||||
comment: Reaction
|
||||
compact?: boolean
|
||||
}) => {
|
||||
reactions?: Reaction[]
|
||||
}
|
||||
|
||||
const Comment = (props: Props) => {
|
||||
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 body = createMemo(() => (comment().body || '').trim())
|
||||
const remove = () => {
|
||||
const remove = async () => {
|
||||
if (comment()?.id) {
|
||||
console.log('[comment] removing', comment().id)
|
||||
deleteReaction(comment().id)
|
||||
try {
|
||||
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(() =>
|
||||
|
@ -34,7 +69,7 @@ export default (props: {
|
|||
)
|
||||
|
||||
return (
|
||||
<div class={clsx(styles.comment, { [styles[`commentLevel${props.level}`]]: Boolean(props.level) })}>
|
||||
<li class={styles.comment}>
|
||||
<Show when={!!body()}>
|
||||
<div class={styles.commentContent}>
|
||||
<Show
|
||||
|
@ -73,10 +108,9 @@ export default (props: {
|
|||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div
|
||||
class={styles.commentBody}
|
||||
contenteditable={props.canEdit}
|
||||
contenteditable={canEdit()}
|
||||
id={'comment-' + (comment().id || '')}
|
||||
>
|
||||
<MD body={body()} />
|
||||
|
@ -85,14 +119,15 @@ export default (props: {
|
|||
<Show when={!props.compact}>
|
||||
<div class={styles.commentControls}>
|
||||
<button
|
||||
class={clsx(styles.commentControl, styles.commentControlReply)}
|
||||
disabled={loading()}
|
||||
onClick={() => setIsReplyVisible(!isReplyVisible())}
|
||||
class={clsx(styles.commentControl, styles.commentControlReply)}
|
||||
>
|
||||
<Icon name="reply" class={styles.icon} />
|
||||
{t('Reply')}
|
||||
{loading() ? t('Loading') : t('Reply')}
|
||||
</button>
|
||||
|
||||
<Show when={props.canEdit}>
|
||||
<Show when={canEdit()}>
|
||||
{/*FIXME implement edit comment modal*/}
|
||||
{/*<button*/}
|
||||
{/* class={clsx(styles.commentControl, styles.commentControlEdit)}*/}
|
||||
|
@ -129,19 +164,27 @@ export default (props: {
|
|||
</div>
|
||||
|
||||
<Show when={isReplyVisible()}>
|
||||
<form class={styles.replyForm}>
|
||||
<textarea name="reply" id="reply" rows="5" />
|
||||
<div class={styles.replyFormControls}>
|
||||
<button class="button button--light" onClick={() => setIsReplyVisible(false)}>
|
||||
{t('Cancel')}
|
||||
</button>
|
||||
<button class="button">{t('Send')}</button>
|
||||
</div>
|
||||
</form>
|
||||
<GrowingTextarea
|
||||
placeholder={t('Write comment')}
|
||||
submitButtonText={t('Send')}
|
||||
cancelButtonText={t('cancel')}
|
||||
submit={(value) => handleCreate(value)}
|
||||
loading={loading()}
|
||||
errorMessage={errorMessage()}
|
||||
/>
|
||||
</Show>
|
||||
</Show>
|
||||
</div>
|
||||
</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
|
||||
|
|
|
@ -1,36 +1,33 @@
|
|||
import { For, Show, createMemo, createSignal, onMount } from 'solid-js'
|
||||
import { useSession } from '../../context/session'
|
||||
import Comment from './Comment'
|
||||
import { t } from '../../utils/intl'
|
||||
import { showModal } from '../../stores/ui'
|
||||
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 { clsx } from 'clsx'
|
||||
import { byCreated, byStat } from '../../utils/sortby'
|
||||
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 MAX_COMMENT_LEVEL = 6
|
||||
|
||||
export const CommentsTree = (props: { shoutSlug: string }) => {
|
||||
export const CommentsTree = (props: { shoutSlug: string; shoutId: number }) => {
|
||||
const [getCommentsPage, setCommentsPage] = createSignal(0)
|
||||
const [commentsOrder, setCommentsOrder] = createSignal<'rating' | 'createdAt'>('createdAt')
|
||||
const [isCommentsLoading, setIsCommentsLoading] = createSignal(false)
|
||||
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
|
||||
const { session } = useSession()
|
||||
const { sortedReactions, loadReactionsBy } = useReactionsStore()
|
||||
const reactions = createMemo<Reaction[]>(() =>
|
||||
sortedReactions()
|
||||
.sort(commentsOrder() === 'rating' ? byStat('rating') : byCreated)
|
||||
.filter((r) => r.shout.slug === props.shoutSlug)
|
||||
sortedReactions().sort(commentsOrder() === 'rating' ? byStat('rating') : byCreated)
|
||||
)
|
||||
|
||||
const { session } = useSession()
|
||||
const loadMore = async () => {
|
||||
try {
|
||||
const page = getCommentsPage()
|
||||
setIsCommentsLoading(true)
|
||||
|
||||
const { hasMore } = await loadReactionsBy({
|
||||
by: { shout: props.shoutSlug, comment: true },
|
||||
limit: ARTICLE_COMMENTS_PAGE_SIZE,
|
||||
|
@ -49,8 +46,32 @@ export const CommentsTree = (props: { shoutSlug: string }) => {
|
|||
return level
|
||||
}
|
||||
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 (
|
||||
<>
|
||||
<div>
|
||||
<Show when={!isCommentsLoading()} fallback={<Loading />}>
|
||||
<div class={styles.commentsHeaderWrapper}>
|
||||
<h2 id="comments" class={styles.commentsHeader}>
|
||||
|
@ -82,46 +103,27 @@ export const CommentsTree = (props: { shoutSlug: string }) => {
|
|||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<For each={reactions().reverse()}>
|
||||
{(reaction: Reaction) => (
|
||||
<Comment
|
||||
comment={reaction}
|
||||
level={getCommentLevel(reaction)}
|
||||
canEdit={reaction.createdBy?.slug === session()?.user?.slug}
|
||||
/>
|
||||
)}
|
||||
<ul class={styles.comments}>
|
||||
<For
|
||||
each={reactions()
|
||||
.reverse()
|
||||
.filter((r) => !r.replyTo)}
|
||||
>
|
||||
{(reaction) => <Comment reactions={reactions()} comment={reaction} />}
|
||||
</For>
|
||||
|
||||
</ul>
|
||||
<Show when={isLoadMoreButtonVisible()}>
|
||||
<button onClick={loadMore}>{t('Load more')}</button>
|
||||
</Show>
|
||||
<GrowingTextarea
|
||||
placeholder={t('Write comment')}
|
||||
submitButtonText={t('Send')}
|
||||
cancelButtonText={t('cancel')}
|
||||
submit={(value) => handleSubmitComment(value)}
|
||||
loading={loading()}
|
||||
errorMessage={errorMessage()}
|
||||
/>
|
||||
</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>
|
||||
</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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -224,7 +224,7 @@ export const FullArticle = (props: ArticleProps) => {
|
|||
)}
|
||||
</For>
|
||||
</div>
|
||||
<CommentsTree shoutSlug={props.article?.slug} />
|
||||
<CommentsTree shoutSlug={props.article?.slug} shoutId={props.article?.id} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -16,8 +16,7 @@
|
|||
justify-content: center;
|
||||
height: 0.9em;
|
||||
line-height: 0;
|
||||
@include font-size(3.6rem);
|
||||
|
||||
font-size: 1.6em;
|
||||
padding: 0;
|
||||
width: 0.9em;
|
||||
|
||||
|
|
|
@ -54,6 +54,10 @@
|
|||
padding: 0 0 0 42px;
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
a {
|
||||
background: #f7f7f7;
|
||||
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 {
|
||||
align-items: center;
|
||||
aspect-ratio: 1/1;
|
||||
|
@ -180,9 +195,12 @@
|
|||
}
|
||||
|
||||
.authorPage {
|
||||
@include media-breakpoint-down(md) {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.authorName {
|
||||
@include font-size(3.4rem);
|
||||
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.2em;
|
||||
}
|
||||
|
@ -195,10 +213,18 @@
|
|||
.authorSubscribe {
|
||||
margin-top: 2rem;
|
||||
padding-left: 0;
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.authorDetails {
|
||||
display: block;
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
flex: 1 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.buttonLabel {
|
||||
|
@ -237,6 +263,16 @@
|
|||
.button {
|
||||
margin-right: 1.6rem;
|
||||
vertical-align: middle;
|
||||
|
||||
&:last-of-type {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
display: block;
|
||||
margin-bottom: 0.5em;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -38,11 +38,13 @@ export const AuthorCard = (props: AuthorCardProps) => {
|
|||
actions: { loadSession }
|
||||
} = useSession()
|
||||
|
||||
if (!props.author) return false // FIXME: с сервера должен приходить автор реакции (ApiClient.CreateReaction)
|
||||
|
||||
const [isSubscribing, setIsSubscribing] = createSignal(false)
|
||||
|
||||
const subscribed = createMemo<boolean>(
|
||||
() => session()?.news?.authors?.some((u) => u === props.author.slug) || false
|
||||
)
|
||||
const subscribed = createMemo<boolean>(() => {
|
||||
return session()?.news?.authors?.some((u) => u === props.author.slug) || false
|
||||
})
|
||||
|
||||
const subscribe = async (really = true) => {
|
||||
setIsSubscribing(true)
|
||||
|
@ -177,7 +179,9 @@ export const AuthorCard = (props: AuthorCardProps) => {
|
|||
</button>
|
||||
|
||||
<Show when={!props.noSocialButtons}>
|
||||
<div class={styles.authorSubscribeSocial}>
|
||||
<For each={props.author.links}>{(link) => <a href={link} />}</For>
|
||||
</div>
|
||||
</Show>
|
||||
</Show>
|
||||
</div>
|
||||
|
|
|
@ -1,11 +1,22 @@
|
|||
.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 {
|
||||
.view-switcher {
|
||||
@include font-size(1.5rem);
|
||||
|
||||
margin-top: 0;
|
||||
|
||||
button {
|
||||
|
|
|
@ -5,7 +5,7 @@ import './Full.scss'
|
|||
export const AuthorFull = (props: { author: Author }) => {
|
||||
return (
|
||||
<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} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -35,12 +35,16 @@
|
|||
}
|
||||
|
||||
.big.circlewrap {
|
||||
margin-right: 4.8rem;
|
||||
margin-right: 0;
|
||||
max-width: 168px;
|
||||
min-width: 168px;
|
||||
height: 168px;
|
||||
width: 168px;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
margin-right: 4.8rem;
|
||||
}
|
||||
|
||||
.userpic {
|
||||
font-size: 2em;
|
||||
line-height: 168px;
|
||||
|
|
|
@ -11,8 +11,6 @@ import { useSession } from '../../context/session'
|
|||
import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
|
||||
const log = getLogger('TopicCard')
|
||||
|
||||
interface TopicProps {
|
||||
topic: Topic
|
||||
compact?: boolean
|
||||
|
|
|
@ -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 { t } from '../../utils/intl'
|
||||
import type { Shout, Reaction } from '../../graphql/types.gen'
|
||||
|
|
55
src/components/Views/Author.module.scss
Normal file
55
src/components/Views/Author.module.scss
Normal 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;
|
||||
}
|
|
@ -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 { Row1 } from '../Feed/Row1'
|
||||
import { Row2 } from '../Feed/Row2'
|
||||
import { Row3 } from '../Feed/Row3'
|
||||
import { AuthorFull } from '../Author/Full'
|
||||
import { t } from '../../utils/intl'
|
||||
import { useAuthorsStore } from '../../stores/zine/authors'
|
||||
|
@ -9,9 +9,17 @@ import { loadShouts, useArticlesStore } from '../../stores/zine/articles'
|
|||
|
||||
import { useTopicsStore } from '../../stores/zine/topics'
|
||||
import { useRouter } from '../../stores/router'
|
||||
import { Beside } from '../Feed/Beside'
|
||||
import { restoreScrollPosition, saveScrollPosition } from '../../utils/scroll'
|
||||
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
|
||||
type AuthorProps = {
|
||||
|
@ -23,7 +31,7 @@ type AuthorProps = {
|
|||
}
|
||||
|
||||
type AuthorPageSearchParams = {
|
||||
by: '' | 'viewed' | 'rating' | 'commented' | 'recent' | 'followed'
|
||||
by: '' | 'viewed' | 'rating' | 'commented' | 'recent' | 'followed' | 'about' | 'popular'
|
||||
}
|
||||
|
||||
export const PRERENDERED_ARTICLES_COUNT = 12
|
||||
|
@ -38,6 +46,7 @@ export const AuthorView = (props: AuthorProps) => {
|
|||
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
|
||||
|
||||
const author = createMemo(() => authorEntities()[props.authorSlug])
|
||||
const subscribers = Array.from({ length: 12 }).fill(author())
|
||||
const { searchParams, changeSearchParam } = useRouter<AuthorPageSearchParams>()
|
||||
|
||||
const loadMore = async () => {
|
||||
|
@ -69,6 +78,23 @@ export const AuthorView = (props: AuthorProps) => {
|
|||
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 (
|
||||
<div class="author-page">
|
||||
<Show when={author()} fallback={<div class="center">{t('Loading')}</div>}>
|
||||
|
@ -89,41 +115,78 @@ export const AuthorView = (props: AuthorProps) => {
|
|||
</li>
|
||||
<li classList={{ selected: searchParams().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>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="mode-switcher">
|
||||
{`${t('Show')} `}
|
||||
<span class="mode-switcher__control">{t('All posts')}</span>
|
||||
<div class={clsx('col-md-4', styles.additionalControls)}>
|
||||
<Popup
|
||||
{...props}
|
||||
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>
|
||||
|
||||
<Beside
|
||||
title={t('Topics which supported by author')}
|
||||
values={topicsByAuthor()[author().slug]?.slice(0, 5)}
|
||||
beside={sortedArticles()[0]}
|
||||
wrapper={'topic'}
|
||||
topicShortDescription={true}
|
||||
isTopicCompact={true}
|
||||
isTopicInRow={true}
|
||||
iconButton={true}
|
||||
/>
|
||||
<Row3 articles={sortedArticles().slice(1, 4)} />
|
||||
<Row2 articles={sortedArticles().slice(4, 6)} />
|
||||
<Row3 articles={sortedArticles().slice(6, 9)} />
|
||||
<Row3 articles={sortedArticles().slice(9, 12)} />
|
||||
<Switch fallback={<p>дефолтное состояние</p>}>
|
||||
<Match when={searchParams().by === 'about'}>
|
||||
<h1>About</h1>
|
||||
<p>{JSON.stringify(authorEntities())}</p>
|
||||
</Match>
|
||||
<Match when={searchParams().by === 'commented'}>
|
||||
<For each={commented()}>{(comment) => <Comment comment={comment} />}</For>
|
||||
</Match>
|
||||
<Match when={searchParams().by === 'popular'}>
|
||||
<Row1 article={sortedArticles()[0]} />
|
||||
<Row2 articles={sortedArticles().slice(1, 3)} isEqual={true} />
|
||||
<Row1 article={sortedArticles()[3]} />
|
||||
<Row2 articles={sortedArticles().slice(4, 6)} isEqual={true} />
|
||||
<Row1 article={sortedArticles()[6]} />
|
||||
<Row2 articles={sortedArticles().slice(7, 9)} isEqual={true} />
|
||||
|
||||
<For each={pages()}>
|
||||
{(page) => (
|
||||
<>
|
||||
<Row3 articles={page.slice(0, 3)} />
|
||||
<Row3 articles={page.slice(3, 6)} />
|
||||
<Row3 articles={page.slice(6, 9)} />
|
||||
<Row1 article={page[0]} />
|
||||
<Row2 articles={page.slice(1, 3)} isEqual={true} />
|
||||
<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>
|
||||
|
@ -135,6 +198,8 @@ export const AuthorView = (props: AuthorProps) => {
|
|||
</button>
|
||||
</p>
|
||||
</Show>
|
||||
</Match>
|
||||
</Switch>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -7,7 +7,7 @@ import { ArticleCard } from '../Feed/Card'
|
|||
import { AuthorCard } from '../Author/Card'
|
||||
import { t } from '../../utils/intl'
|
||||
import { FeedSidebar } from '../Feed/Sidebar'
|
||||
import CommentCard from '../Article/Comment'
|
||||
import Comment from '../Article/Comment'
|
||||
import { loadShouts, useArticlesStore } from '../../stores/zine/articles'
|
||||
import { useReactionsStore } from '../../stores/zine/reactions'
|
||||
import { useAuthorsStore } from '../../stores/zine/authors'
|
||||
|
@ -128,7 +128,7 @@ export const FeedView = () => {
|
|||
<section class="feed-comments">
|
||||
<h4>{t('Comments')}</h4>
|
||||
<For each={topComments()}>
|
||||
{(comment) => <CommentCard comment={comment} compact={true} />}
|
||||
{(comment) => <Comment comment={comment} reactions={[]} compact={true} />}
|
||||
</For>
|
||||
</section>
|
||||
<Show when={topTopics().length > 0}>
|
||||
|
|
61
src/components/_shared/Button/Button.module.scss
Normal file
61
src/components/_shared/Button/Button.module.scss
Normal 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;
|
||||
}
|
||||
}
|
30
src/components/_shared/Button/Button.tsx
Normal file
30
src/components/_shared/Button/Button.tsx
Normal 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
|
1
src/components/_shared/Button/index.ts
Normal file
1
src/components/_shared/Button/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { default } from './Button'
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
98
src/components/_shared/GrowingTextarea/GrowingTextarea.tsx
Normal file
98
src/components/_shared/GrowingTextarea/GrowingTextarea.tsx
Normal 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')}
|
||||
<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
|
1
src/components/_shared/GrowingTextarea/index.ts
Normal file
1
src/components/_shared/GrowingTextarea/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { default } from './GrowingTextarea'
|
|
@ -4,18 +4,19 @@
|
|||
|
||||
.popup {
|
||||
background: #fff;
|
||||
top: calc(100% + 8px);
|
||||
opacity: 1;
|
||||
color: #000;
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
min-width: 144px;
|
||||
opacity: 1;
|
||||
position: absolute;
|
||||
top: calc(100% + 8px);
|
||||
z-index: 100;
|
||||
|
||||
ul {
|
||||
margin-bottom: 0;
|
||||
|
||||
li {
|
||||
position: relative;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
@ -24,11 +25,12 @@
|
|||
|
||||
&.bordered {
|
||||
@include font-size(1.6rem);
|
||||
|
||||
border: 2px solid #000;
|
||||
padding: 2.4rem;
|
||||
|
||||
ul li {
|
||||
margin-bottom: 1.6rem;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
@ -37,11 +39,12 @@
|
|||
|
||||
&.tiny {
|
||||
@include font-size(1.4rem);
|
||||
|
||||
box-shadow: 0 4px 60px rgba(0, 0, 0, 0.1);
|
||||
padding: 1rem;
|
||||
|
||||
ul li {
|
||||
margin-bottom: 1rem;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
@ -67,22 +70,22 @@
|
|||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
img {
|
||||
.icon img {
|
||||
filter: invert(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: inline-block;
|
||||
width: 3.6rem;
|
||||
|
||||
img {
|
||||
filter: invert(1);
|
||||
max-height: 2rem;
|
||||
max-width: 2rem;
|
||||
transition: filter 0.3s;
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: inline-block;
|
||||
width: 3.6rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,26 +6,11 @@ export default gql`
|
|||
error
|
||||
reaction {
|
||||
id
|
||||
createdBy {
|
||||
slug
|
||||
name
|
||||
userpic
|
||||
}
|
||||
body
|
||||
kind
|
||||
range
|
||||
createdAt
|
||||
shout
|
||||
replyTo {
|
||||
id
|
||||
createdBy {
|
||||
slug
|
||||
userpic
|
||||
name
|
||||
}
|
||||
body
|
||||
kind
|
||||
}
|
||||
replyTo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
import { gql } from '@urql/core'
|
||||
|
||||
export default gql`
|
||||
mutation DeleteReactionMutation($id: Int!) {
|
||||
deleteReaction(id: $id) {
|
||||
mutation DeleteReactionMutation($reaction: Int!) {
|
||||
deleteReaction(reaction: $reaction) {
|
||||
error
|
||||
reaction {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
|
|
@ -4,6 +4,7 @@ export default gql`
|
|||
query LoadShoutQuery($slug: String!) {
|
||||
loadShout(slug: $slug) {
|
||||
_id: slug
|
||||
id
|
||||
title
|
||||
subtitle
|
||||
slug
|
||||
|
|
|
@ -4,6 +4,7 @@ export default gql`
|
|||
query LoadShoutsQuery($options: LoadShoutsOptions) {
|
||||
loadShouts(options: $options) {
|
||||
_id: slug
|
||||
id
|
||||
title
|
||||
subtitle
|
||||
slug
|
||||
|
|
|
@ -8,7 +8,9 @@ export default gql`
|
|||
range
|
||||
replyTo
|
||||
shout {
|
||||
id
|
||||
slug
|
||||
title
|
||||
}
|
||||
createdBy {
|
||||
name
|
||||
|
|
|
@ -238,7 +238,7 @@ export type MutationDeleteMessageArgs = {
|
|||
}
|
||||
|
||||
export type MutationDeleteReactionArgs = {
|
||||
id: Scalars['Int']
|
||||
reaction: Scalars['Int']
|
||||
}
|
||||
|
||||
export type MutationDeleteShoutArgs = {
|
||||
|
@ -498,10 +498,10 @@ export type ReactionBy = {
|
|||
|
||||
export type ReactionInput = {
|
||||
body?: InputMaybe<Scalars['String']>
|
||||
kind: Scalars['Int']
|
||||
kind: ReactionKind
|
||||
range?: InputMaybe<Scalars['String']>
|
||||
replyTo?: InputMaybe<Scalars['Int']>
|
||||
shout: Scalars['String']
|
||||
shout: Scalars['Int']
|
||||
}
|
||||
|
||||
export enum ReactionKind {
|
||||
|
|
|
@ -52,6 +52,7 @@
|
|||
"Fill email": "Введите почту",
|
||||
"Follow": "Подписаться",
|
||||
"Follow the topic": "Подписаться на тему",
|
||||
"Followers": "Подписчики",
|
||||
"Forgot password?": "Забыли пароль?",
|
||||
"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": "Познакомитесь с выдающимися людьми нашего времени, участвуйте в редактировании и обсуждении статей, выступайте экспертом, оценивайте материалы других авторов со всего мира и определяйте, какие статьи будут опубликованы в журнале",
|
||||
|
@ -188,6 +189,7 @@
|
|||
"create_group": "Создать группу",
|
||||
"discourse_theme": "Тема дискурса",
|
||||
"cancel": "Отмена",
|
||||
"Send": "Отправить",
|
||||
"group_chat": "Общий чат",
|
||||
"Choose who you want to write to": "Выберите кому хотите написать",
|
||||
"Start conversation": "Начать беседу",
|
||||
|
@ -211,5 +213,7 @@
|
|||
"Forward": "Переслать",
|
||||
"Select": "Выбрать",
|
||||
"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": "Чтобы написать комментарий, необходимо"
|
||||
}
|
||||
|
|
|
@ -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 { createSignal } from 'solid-js'
|
||||
// TODO: import { roomConnect } from '../../utils/p2p'
|
||||
|
@ -23,20 +23,28 @@ export const loadReactionsBy = async ({
|
|||
setSortedReactions(data)
|
||||
return { hasMore }
|
||||
}
|
||||
export const createReaction = async (reaction: Reaction) => {
|
||||
const { reaction: r } = await apiClient.createReaction({ reaction })
|
||||
return r
|
||||
|
||||
export const createReaction = async (
|
||||
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) => {
|
||||
const { reaction: r } = await apiClient.updateReaction({ reaction })
|
||||
return r
|
||||
}
|
||||
|
||||
export const deleteReaction = async (reactionId: number) => {
|
||||
const resp = await apiClient.destroyReaction({ id: reactionId })
|
||||
console.debug(resp)
|
||||
return resp
|
||||
}
|
||||
export const useReactionsStore = () => {
|
||||
return {
|
||||
reactionsByShout,
|
||||
|
|
|
@ -252,6 +252,20 @@ img {
|
|||
}
|
||||
}
|
||||
|
||||
.comments {
|
||||
margin: 0;
|
||||
|
||||
&,
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 1em 0 0 2.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.commentsHeaderWrapper {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
|
|
@ -10,9 +10,11 @@ import type {
|
|||
QueryLoadMessagesByArgs,
|
||||
MutationCreateChatArgs,
|
||||
MutationCreateMessageArgs,
|
||||
Chat,
|
||||
QueryLoadRecipientsArgs,
|
||||
ProfileInput
|
||||
ProfileInput,
|
||||
ReactionInput,
|
||||
Chat,
|
||||
ReactionBy
|
||||
} from '../graphql/types.gen'
|
||||
import { publicGraphQLClient } from '../graphql/publicGraphQLClient'
|
||||
import { getToken, privateGraphQLClient } from '../graphql/privateGraphQLClient'
|
||||
|
@ -226,27 +228,24 @@ export const apiClient = {
|
|||
},
|
||||
createArticle: async ({ article }: { article: ShoutInput }) => {
|
||||
const response = await privateGraphQLClient.mutation(createArticle, { shout: article }).toPromise()
|
||||
console.debug('createArticle response:', response)
|
||||
console.debug('[createArticle]:', response.data)
|
||||
return response.data.createShout
|
||||
},
|
||||
createReaction: async ({ reaction }) => {
|
||||
const response = await privateGraphQLClient.mutation(reactionCreate, { reaction }).toPromise()
|
||||
console.debug('[api-client] [api] create reaction mutation called')
|
||||
return response.data.createReaction
|
||||
createReaction: async (input: ReactionInput) => {
|
||||
const response = await privateGraphQLClient.mutation(reactionCreate, { reaction: input }).toPromise()
|
||||
console.debug('[createReaction]:', response)
|
||||
return response.data.createReaction.reaction
|
||||
},
|
||||
|
||||
// CUDL
|
||||
|
||||
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()
|
||||
|
||||
destroyReaction: async (id: number) => {
|
||||
const response = await privateGraphQLClient.mutation(reactionDestroy, { reaction: id }).toPromise()
|
||||
console.debug('[destroyReaction]:', response)
|
||||
return response.data.deleteReaction
|
||||
},
|
||||
updateReaction: async (reaction) => {
|
||||
const response = await privateGraphQLClient.mutation(reactionUpdate, reaction).toPromise()
|
||||
|
||||
return response.data.createReaction
|
||||
},
|
||||
getAuthorsBy: async (options: QueryLoadAuthorsByArgs) => {
|
||||
const resp = await publicGraphQLClient.query(authorsLoadBy, options).toPromise()
|
||||
return resp.data.loadAuthorsBy
|
||||
|
@ -269,7 +268,16 @@ export const apiClient = {
|
|||
if (resp.error) console.debug(resp)
|
||||
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()
|
||||
console.debug(resp)
|
||||
return resp.data.loadReactionsBy
|
||||
|
@ -288,15 +296,13 @@ export const apiClient = {
|
|||
|
||||
createMessage: async (options: MutationCreateMessageArgs) => {
|
||||
const resp = await privateGraphQLClient.mutation(createMessage, options).toPromise()
|
||||
return resp.data.createMessage.message
|
||||
return resp.data.createMessage
|
||||
},
|
||||
|
||||
getChatMessages: async (options: QueryLoadMessagesByArgs) => {
|
||||
const resp = await privateGraphQLClient.query(chatMessagesLoadBy, options).toPromise()
|
||||
console.log('[getChatMessages]', resp)
|
||||
return resp.data.loadMessagesBy.messages
|
||||
return resp.data.loadChat
|
||||
},
|
||||
|
||||
getRecipients: async (options: QueryLoadRecipientsArgs) => {
|
||||
const resp = await privateGraphQLClient.query(loadRecipients, options).toPromise()
|
||||
return resp.data.loadRecipients.members
|
||||
|
|
Loading…
Reference in New Issue
Block a user