From 1977493dcd2497066454b453a221098eb867c39b Mon Sep 17 00:00:00 2001
From: Ilya Y <75578537+ilya-bkv@users.noreply.github.com>
Date: Fri, 20 Jan 2023 07:40:55 +0300
Subject: [PATCH] Delete reaction (#85)
* Delete reactions
---
src/components/Article/Comment.module.scss | 131 ++++++----------
src/components/Article/Comment.tsx | 99 ++++++++----
src/components/Article/CommentsTree.tsx | 98 ++++++------
src/components/Article/FullArticle.tsx | 2 +-
.../Article/RatingControl.module.scss | 3 +-
src/components/Author/Card.module.scss | 38 ++++-
src/components/Author/Card.tsx | 12 +-
src/components/Author/Full.scss | 15 +-
src/components/Author/Full.tsx | 2 +-
src/components/Author/Userpic.module.scss | 6 +-
src/components/Topic/Card.tsx | 2 -
src/components/Views/Article.tsx | 2 +-
src/components/Views/Author.module.scss | 55 +++++++
src/components/Views/Author.tsx | 143 +++++++++++++-----
src/components/Views/Feed.tsx | 4 +-
.../_shared/Button/Button.module.scss | 61 ++++++++
src/components/_shared/Button/Button.tsx | 30 ++++
src/components/_shared/Button/index.ts | 1 +
.../GrowingTextarea.module.scss | 98 ++++++++++++
.../GrowingTextarea/GrowingTextarea.tsx | 98 ++++++++++++
.../_shared/GrowingTextarea/index.ts | 1 +
.../_shared/Popup/Popup.module.scss | 31 ++--
src/graphql/mutation/reaction-create.ts | 17 +--
src/graphql/mutation/reaction-destroy.ts | 7 +-
src/graphql/query/article-load.ts | 1 +
src/graphql/query/articles-load-by.ts | 1 +
src/graphql/query/reactions-load-by.ts | 2 +
src/graphql/types.gen.ts | 6 +-
src/locales/ru.json | 6 +-
src/stores/zine/reactions.ts | 26 ++--
src/styles/Article.module.scss | 14 ++
src/utils/apiClient.ts | 52 ++++---
32 files changed, 777 insertions(+), 287 deletions(-)
create mode 100644 src/components/Views/Author.module.scss
create mode 100644 src/components/_shared/Button/Button.module.scss
create mode 100644 src/components/_shared/Button/Button.tsx
create mode 100644 src/components/_shared/Button/index.ts
create mode 100644 src/components/_shared/GrowingTextarea/GrowingTextarea.module.scss
create mode 100644 src/components/_shared/GrowingTextarea/GrowingTextarea.tsx
create mode 100644 src/components/_shared/GrowingTextarea/index.ts
diff --git a/src/components/Article/Comment.module.scss b/src/components/Article/Comment.module.scss
index e1ae44ba..ea01b08b 100644
--- a/src/components/Article/Comment.module.scss
+++ b/src/components/Article/Comment.module.scss
@@ -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;
- }
-}
diff --git a/src/components/Article/Comment.tsx b/src/components/Article/Comment.tsx
index 0b3e5ee9..b3eef418 100644
--- a/src/components/Article/Comment.tsx
+++ b/src/components/Article/Comment.tsx
@@ -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
- 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(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 (
-
-
+
)
diff --git a/src/components/Article/RatingControl.module.scss b/src/components/Article/RatingControl.module.scss
index 68001845..1d871050 100644
--- a/src/components/Article/RatingControl.module.scss
+++ b/src/components/Article/RatingControl.module.scss
@@ -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;
diff --git a/src/components/Author/Card.module.scss b/src/components/Author/Card.module.scss
index 19df71b5..e8f9062a 100644
--- a/src/components/Author/Card.module.scss
+++ b/src/components/Author/Card.module.scss
@@ -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;
+ }
}
}
diff --git a/src/components/Author/Card.tsx b/src/components/Author/Card.tsx
index 3daa4a10..a821fd4c 100644
--- a/src/components/Author/Card.tsx
+++ b/src/components/Author/Card.tsx
@@ -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(
- () => session()?.news?.authors?.some((u) => u === props.author.slug) || false
- )
+ const subscribed = createMemo(() => {
+ 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) => {
- {(link) => }
+
diff --git a/src/components/Author/Full.scss b/src/components/Author/Full.scss
index 13f5e9ca..801cd85c 100644
--- a/src/components/Author/Full.scss
+++ b/src/components/Author/Full.scss
@@ -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 {
diff --git a/src/components/Author/Full.tsx b/src/components/Author/Full.tsx
index b240ae2b..0d621af4 100644
--- a/src/components/Author/Full.tsx
+++ b/src/components/Author/Full.tsx
@@ -5,7 +5,7 @@ import './Full.scss'
export const AuthorFull = (props: { author: Author }) => {
return (
-
diff --git a/src/components/Author/Userpic.module.scss b/src/components/Author/Userpic.module.scss
index 20ed085e..18855e50 100644
--- a/src/components/Author/Userpic.module.scss
+++ b/src/components/Author/Userpic.module.scss
@@ -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;
diff --git a/src/components/Topic/Card.tsx b/src/components/Topic/Card.tsx
index 6e6e73e8..4ade9dd4 100644
--- a/src/components/Topic/Card.tsx
+++ b/src/components/Topic/Card.tsx
@@ -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
diff --git a/src/components/Views/Article.tsx b/src/components/Views/Article.tsx
index 3a4bdbca..7e982677 100644
--- a/src/components/Views/Article.tsx
+++ b/src/components/Views/Article.tsx
@@ -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'
diff --git a/src/components/Views/Author.module.scss b/src/components/Views/Author.module.scss
new file mode 100644
index 00000000..9b279d03
--- /dev/null
+++ b/src/components/Views/Author.module.scss
@@ -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;
+}
diff --git a/src/components/Views/Author.tsx b/src/components/Views/Author.tsx
index 37b3030a..a2aa1e99 100644
--- a/src/components/Views/Author.tsx
+++ b/src/components/Views/Author.tsx
@@ -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
()
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 (
{t('Loading')}
}>
@@ -89,52 +115,91 @@ export const AuthorView = (props: AuthorProps) => {
+
+
+
+
+
+
-
-
- {`${t('Show')} `}
-
{t('All posts')}
+
+ }
+ variant="tiny"
+ >
+
+
+ {(item: Author) => (
+ -
+
+
+ )}
+
+
+
+
+
+ Карма
+
-
-
-
-
-
+ дефолтное состояние
}>
+
+ About
+ {JSON.stringify(authorEntities())}
+
+
+ {(comment) => }
+
+
+
+
+
+
+
+
-
- {(page) => (
- <>
-
-
-
- >
- )}
-
+
+ {(page) => (
+ <>
+
+
+
+
+
+
+ >
+ )}
+
-
-
-
-
-
+
+
+
+
+
+
+
)
diff --git a/src/components/Views/Feed.tsx b/src/components/Views/Feed.tsx
index b02f33bd..a2cb73d1 100644
--- a/src/components/Views/Feed.tsx
+++ b/src/components/Views/Feed.tsx
@@ -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 = () => {
0}>
diff --git a/src/components/_shared/Button/Button.module.scss b/src/components/_shared/Button/Button.module.scss
new file mode 100644
index 00000000..68515649
--- /dev/null
+++ b/src/components/_shared/Button/Button.module.scss
@@ -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;
+ }
+}
diff --git a/src/components/_shared/Button/Button.tsx b/src/components/_shared/Button/Button.tsx
new file mode 100644
index 00000000..109f3783
--- /dev/null
+++ b/src/components/_shared/Button/Button.tsx
@@ -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 (
+
+ )
+}
+
+export default Button
diff --git a/src/components/_shared/Button/index.ts b/src/components/_shared/Button/index.ts
new file mode 100644
index 00000000..3389ecb8
--- /dev/null
+++ b/src/components/_shared/Button/index.ts
@@ -0,0 +1 @@
+export { default } from './Button'
diff --git a/src/components/_shared/GrowingTextarea/GrowingTextarea.module.scss b/src/components/_shared/GrowingTextarea/GrowingTextarea.module.scss
new file mode 100644
index 00000000..549c8965
--- /dev/null
+++ b/src/components/_shared/GrowingTextarea/GrowingTextarea.module.scss
@@ -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;
+ }
+ }
+ }
+}
diff --git a/src/components/_shared/GrowingTextarea/GrowingTextarea.tsx b/src/components/_shared/GrowingTextarea/GrowingTextarea.tsx
new file mode 100644
index 00000000..18cf00ca
--- /dev/null
+++ b/src/components/_shared/GrowingTextarea/GrowingTextarea.tsx
@@ -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('')
+
+ 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 (
+
+ )
+}
+
+export default GrowingTextarea
diff --git a/src/components/_shared/GrowingTextarea/index.ts b/src/components/_shared/GrowingTextarea/index.ts
new file mode 100644
index 00000000..a0d16325
--- /dev/null
+++ b/src/components/_shared/GrowingTextarea/index.ts
@@ -0,0 +1 @@
+export { default } from './GrowingTextarea'
diff --git a/src/components/_shared/Popup/Popup.module.scss b/src/components/_shared/Popup/Popup.module.scss
index b1890f63..3325cc7c 100644
--- a/src/components/_shared/Popup/Popup.module.scss
+++ b/src/components/_shared/Popup/Popup.module.scss
@@ -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);
}
}
}
- img {
- filter: invert(1);
- max-height: 2rem;
- max-width: 2rem;
- transition: filter 0.3s;
- }
-
.icon {
display: inline-block;
width: 3.6rem;
+
+ img {
+ filter: invert(1);
+ max-height: 2rem;
+ max-width: 2rem;
+ transition: filter 0.3s;
+ }
}
}
diff --git a/src/graphql/mutation/reaction-create.ts b/src/graphql/mutation/reaction-create.ts
index 253d43f9..38194e76 100644
--- a/src/graphql/mutation/reaction-create.ts
+++ b/src/graphql/mutation/reaction-create.ts
@@ -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
}
}
}
diff --git a/src/graphql/mutation/reaction-destroy.ts b/src/graphql/mutation/reaction-destroy.ts
index ca71af55..8be2fa78 100644
--- a/src/graphql/mutation/reaction-destroy.ts
+++ b/src/graphql/mutation/reaction-destroy.ts
@@ -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
+ }
}
}
`
diff --git a/src/graphql/query/article-load.ts b/src/graphql/query/article-load.ts
index fbbf5ff4..43b0dd4a 100644
--- a/src/graphql/query/article-load.ts
+++ b/src/graphql/query/article-load.ts
@@ -4,6 +4,7 @@ export default gql`
query LoadShoutQuery($slug: String!) {
loadShout(slug: $slug) {
_id: slug
+ id
title
subtitle
slug
diff --git a/src/graphql/query/articles-load-by.ts b/src/graphql/query/articles-load-by.ts
index 6ad979e2..4baad70a 100644
--- a/src/graphql/query/articles-load-by.ts
+++ b/src/graphql/query/articles-load-by.ts
@@ -4,6 +4,7 @@ export default gql`
query LoadShoutsQuery($options: LoadShoutsOptions) {
loadShouts(options: $options) {
_id: slug
+ id
title
subtitle
slug
diff --git a/src/graphql/query/reactions-load-by.ts b/src/graphql/query/reactions-load-by.ts
index cf34e13e..e404283d 100644
--- a/src/graphql/query/reactions-load-by.ts
+++ b/src/graphql/query/reactions-load-by.ts
@@ -8,7 +8,9 @@ export default gql`
range
replyTo
shout {
+ id
slug
+ title
}
createdBy {
name
diff --git a/src/graphql/types.gen.ts b/src/graphql/types.gen.ts
index 228912ca..a67be3cb 100644
--- a/src/graphql/types.gen.ts
+++ b/src/graphql/types.gen.ts
@@ -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
- kind: Scalars['Int']
+ kind: ReactionKind
range?: InputMaybe
replyTo?: InputMaybe
- shout: Scalars['String']
+ shout: Scalars['Int']
}
export enum ReactionKind {
diff --git a/src/locales/ru.json b/src/locales/ru.json
index b9e8c8db..0a6b696e 100644
--- a/src/locales/ru.json
+++ b/src/locales/ru.json
@@ -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": "Чтобы написать комментарий, необходимо"
}
diff --git a/src/stores/zine/reactions.ts b/src/stores/zine/reactions.ts
index d757204b..4cae72f3 100644
--- a/src/stores/zine/reactions.ts
+++ b/src/stores/zine/reactions.ts
@@ -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,
diff --git a/src/styles/Article.module.scss b/src/styles/Article.module.scss
index 82ff7331..eea60cf6 100644
--- a/src/styles/Article.module.scss
+++ b/src/styles/Article.module.scss
@@ -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;
diff --git a/src/utils/apiClient.ts b/src/utils/apiClient.ts
index 113047bd..7909ddac 100644
--- a/src/utils/apiClient.ts
+++ b/src/utils/apiClient.ts
@@ -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
+ r.replyTo === props.comment.id)}>
+ {(reaction) => }
+
+
+@@ -82,46 +103,27 @@ export const CommentsTree = (props: { shoutSlug: string }) => {
+ !r.replyTo)}
+ >
+ {(reaction) => }
+
+