diff --git a/src/components/Article/FullArticle.tsx b/src/components/Article/FullArticle.tsx index 2438fae5..49e299ac 100644 --- a/src/components/Article/FullArticle.tsx +++ b/src/components/Article/FullArticle.tsx @@ -2,14 +2,14 @@ import { capitalize, formatDate } from '../../utils' import './Full.scss' import { Icon } from '../_shared/Icon' import { AuthorCard } from '../Author/Card' -import { createMemo, createSignal, For, Match, onMount, Show, Switch } from 'solid-js' +import { createMemo, For, Match, onMount, Show, Switch } from 'solid-js' import type { Author, Shout } from '../../graphql/types.gen' import { t } from '../../utils/intl' import MD from './MD' import { SharePopup } from './SharePopup' import stylesHeader from '../Nav/Header.module.scss' import styles from '../../styles/Article.module.scss' -import RatingControl from './RatingControl' +import { RatingControl } from './RatingControl' import { clsx } from 'clsx' import { CommentsTree } from './CommentsTree' import { useSession } from '../../context/session' diff --git a/src/components/Article/RatingControl.tsx b/src/components/Article/RatingControl.tsx index c18f20e3..d8622b2c 100644 --- a/src/components/Article/RatingControl.tsx +++ b/src/components/Article/RatingControl.tsx @@ -20,5 +20,3 @@ export const RatingControl = (props: RatingControlProps) => { ) } - -export default RatingControl diff --git a/src/components/Author/Card.module.scss b/src/components/Author/Card.module.scss index e91b971d..f2a20fc8 100644 --- a/src/components/Author/Card.module.scss +++ b/src/components/Author/Card.module.scss @@ -277,3 +277,7 @@ margin-top: -0.6em; } } + +.isSubscribing { + color: transparent; +} diff --git a/src/components/Author/Card.tsx b/src/components/Author/Card.tsx index d88eb45d..c0f1b60a 100644 --- a/src/components/Author/Card.tsx +++ b/src/components/Author/Card.tsx @@ -2,7 +2,7 @@ import type { Author } from '../../graphql/types.gen' import Userpic from './Userpic' import { Icon } from '../_shared/Icon' import styles from './Card.module.scss' -import { createMemo, For, Show } from 'solid-js' +import { createMemo, createSignal, For, Show } from 'solid-js' import { translit } from '../../utils/ru2en' import { t } from '../../utils/intl' import { locale } from '../../stores/ui' @@ -10,6 +10,8 @@ import { follow, unfollow } from '../../stores/zine/common' import { clsx } from 'clsx' import { useSession } from '../../context/session' import { StatMetrics } from '../_shared/StatMetrics' +import { FollowingEntity } from '../../graphql/types.gen' +import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient' interface AuthorCardProps { caption?: string @@ -28,17 +30,32 @@ interface AuthorCardProps { } export const AuthorCard = (props: AuthorCardProps) => { - const { session } = useSession() + const { + session, + actions: { loadSession } + } = useSession() + + const [isSubscribing, setIsSubscribing] = createSignal(false) const subscribed = createMemo( () => session()?.news?.authors?.some((u) => u === props.author.slug) || false ) + + const subscribe = async (really = true) => { + setIsSubscribing(true) + + await (really + ? follow({ what: FollowingEntity.Author, slug: props.author.slug }) + : unfollow({ what: FollowingEntity.Author, slug: props.author.slug })) + + await loadSession() + setIsSubscribing(false) + } + const canFollow = createMemo(() => !props.hideFollow && session()?.user?.slug !== props.author.slug) const name = () => { - return props.author.name === 'Дискурс' && locale() !== 'ru' - ? 'Discours' - : translit(props.author.name || '', locale() || 'ru') + return props.author.name === 'Дискурс' && locale() !== 'ru' ? 'Discours' : translit(props.author.name) } // TODO: reimplement AuthorCard return ( @@ -75,72 +92,80 @@ export const AuthorCard = (props: AuthorCardProps) => { class={styles.authorAbout} classList={{ 'text-truncate': props.truncateBio }} innerHTML={props.author.bio} - > + /> - - -
- follow} - class={clsx('button', styles.button)} - classList={{ - [styles.buttonSubscribe]: !props.isAuthorsList, - 'button--subscribe': !props.isAuthorsList, - 'button--subscribe-topic': props.isAuthorsList, - [styles.buttonWrite]: props.isAuthorsList - }} + + + +
+ subscribe(true)} + class={clsx('button', styles.button)} + classList={{ + [styles.buttonSubscribe]: !props.isAuthorsList, + 'button--subscribe': !props.isAuthorsList, + 'button--subscribe-topic': props.isAuthorsList, + [styles.buttonWrite]: props.isAuthorsList, + [styles.isSubscribing]: isSubscribing() + }} + disabled={isSubscribing()} + > + + + + {t('Follow')} + + } > - - - - {t('Follow')} - - } - > - - {t('Unfollow')} - - - - + + - - {(link) => } - + + {(link) => } + + +
-
-
+ + ) diff --git a/src/components/EditorNew/Editor.tsx b/src/components/EditorNew/Editor.tsx index d8ea9ded..d9560fc9 100644 --- a/src/components/EditorNew/Editor.tsx +++ b/src/components/EditorNew/Editor.tsx @@ -59,7 +59,7 @@ export const Editor = () => { const handleSaveButtonClick = () => { const article: ShoutInput = { body: getHtml(editorViewRef.current.state), - community: 'discours', // ? + community: 1, // 'discours' ? slug: 'new-' + Math.floor(Math.random() * 1000000) } createArticle({ article }) diff --git a/src/components/Feed/Card.tsx b/src/components/Feed/Card.tsx index dd2f099e..f521c136 100644 --- a/src/components/Feed/Card.tsx +++ b/src/components/Feed/Card.tsx @@ -7,8 +7,8 @@ import { Icon } from '../_shared/Icon' import styles from './Card.module.scss' import { locale } from '../../stores/ui' import { clsx } from 'clsx' -import CardTopic from './CardTopic' -import RatingControl from '../Article/RatingControl' +import { CardTopic } from './CardTopic' +import { RatingControl } from '../Article/RatingControl' interface ArticleCardProps { settings?: { @@ -107,7 +107,7 @@ export const ArticleCard = (props: ArticleCardProps) => { { {(author, index) => { const name = - author.name === 'Дискурс' && locale() !== 'ru' - ? 'Discours' - : translit(author.name || '', locale() || 'ru') + author.name === 'Дискурс' && locale() !== 'ru' ? 'Discours' : translit(author.name) return ( <> diff --git a/src/components/Feed/CardTopic.tsx b/src/components/Feed/CardTopic.tsx index 9f85f00e..617d10d9 100644 --- a/src/components/Feed/CardTopic.tsx +++ b/src/components/Feed/CardTopic.tsx @@ -6,7 +6,7 @@ interface CardTopicProps { isFloorImportant?: boolean } -export default (props: CardTopicProps) => { +export const CardTopic = (props: CardTopicProps) => { return (
{ const inviteUsers: inviteUser[] = props.users.map((user) => ({ ...user, selected: false })) const [theme, setTheme] = createSignal('') - const [slugs, setSlugs] = createSignal([]) + const [slugs, setSlugs] = createSignal([]) const [collectionToInvite, setCollectionToInvite] = createSignal(inviteUsers) let textInput: HTMLInputElement @@ -37,7 +37,7 @@ const CreateModalContent = (props: Props) => { return user.selected === true }) .map((user) => { - return user['slug'] + return user.id }) }) if (slugs().length > 2 && theme().length === 0) { diff --git a/src/components/Nav/AuthModal/RegisterForm.tsx b/src/components/Nav/AuthModal/RegisterForm.tsx index aaefe61d..d6feb5a7 100644 --- a/src/components/Nav/AuthModal/RegisterForm.tsx +++ b/src/components/Nav/AuthModal/RegisterForm.tsx @@ -61,14 +61,14 @@ export const RegisterForm = () => { const newValidationErrors: ValidationErrors = {} - const clearName = name().trim() - const clearEmail = email().trim() + const cleanName = name().trim() + const cleanEmail = email().trim() - if (!clearName) { + if (!cleanName) { newValidationErrors.name = t('Please enter a name to sign your comments and publication') } - if (!clearEmail) { + if (!cleanEmail) { newValidationErrors.email = t('Please enter email') } else if (!isValidEmail(email())) { newValidationErrors.email = t('Invalid email') @@ -80,7 +80,7 @@ export const RegisterForm = () => { setValidationErrors(newValidationErrors) - const emailCheckResult = await checkEmail(clearEmail) + const emailCheckResult = await checkEmail(cleanEmail) const isValid = Object.keys(newValidationErrors).length === 0 && !emailCheckResult @@ -92,8 +92,8 @@ export const RegisterForm = () => { try { await register({ - name: clearName, - email: clearEmail, + name: cleanName, + email: cleanEmail, password: password() }) @@ -123,13 +123,13 @@ export const RegisterForm = () => {
handleNameInput(event.currentTarget.value)} /> - +
{validationErrors().name}
diff --git a/src/components/Nav/HeaderAuth.tsx b/src/components/Nav/HeaderAuth.tsx index 629f8524..a86befd2 100644 --- a/src/components/Nav/HeaderAuth.tsx +++ b/src/components/Nav/HeaderAuth.tsx @@ -3,7 +3,7 @@ import { clsx } from 'clsx' import { useRouter } from '../../stores/router' import { t } from '../../utils/intl' import { Icon } from '../_shared/Icon' -import { createSignal, Show } from 'solid-js' +import { createEffect, createSignal, Show } from 'solid-js' import Notifications from './Notifications' import { ProfilePopup } from './ProfilePopup' import Userpic from '../Author/Userpic' @@ -38,7 +38,7 @@ export const HeaderAuth = (props: HeaderAuthProps) => { return ( - +
diff --git a/src/components/Nav/Modal.module.scss b/src/components/Nav/Modal.module.scss index 746202e0..c6b1fd39 100644 --- a/src/components/Nav/Modal.module.scss +++ b/src/components/Nav/Modal.module.scss @@ -23,15 +23,16 @@ position: absolute; top: 1em; cursor: pointer; - height: 0.8em; + height: 18px; + width: 16px; opacity: 1; padding: 0; right: 0; transition: opacity 0.3s; - width: 0.8em; z-index: 1; svg { + display: block; pointer-events: none; } @@ -55,6 +56,7 @@ @media (min-width: 800px) and (max-width: 991px) { width: 80%; } + .close { right: 12px; top: 12px; diff --git a/src/components/Topic/Card.module.scss b/src/components/Topic/Card.module.scss index a4e63fe4..88faeca1 100644 --- a/src/components/Topic/Card.module.scss +++ b/src/components/Topic/Card.module.scss @@ -116,3 +116,7 @@ .buttonCompact { margin-top: 0.6rem; } + +.isSubscribing { + color: transparent; +} diff --git a/src/components/Topic/Card.tsx b/src/components/Topic/Card.tsx index 6cdc394a..8dcd2f23 100644 --- a/src/components/Topic/Card.tsx +++ b/src/components/Topic/Card.tsx @@ -1,6 +1,6 @@ import { capitalize } from '../../utils' import styles from './Card.module.scss' -import { createMemo, Show } from 'solid-js' +import { createEffect, createMemo, createSignal, Show } from 'solid-js' import type { Topic } from '../../graphql/types.gen' import { FollowingEntity } from '../../graphql/types.gen' import { t } from '../../utils/intl' @@ -9,6 +9,7 @@ import { getLogger } from '../../utils/logger' import { clsx } from 'clsx' import { useSession } from '../../context/session' import { StatMetrics } from '../_shared/StatMetrics' +import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient' const log = getLogger('TopicCard') @@ -25,7 +26,12 @@ interface TopicProps { } export const TopicCard = (props: TopicProps) => { - const { session } = useSession() + const { + session, + actions: { loadSession } + } = useSession() + + const [isSubscribing, setIsSubscribing] = createSignal(false) const subscribed = createMemo(() => { if (!session()?.user?.slug || !session()?.news?.topics) { @@ -35,14 +41,17 @@ export const TopicCard = (props: TopicProps) => { return session()?.news.topics.includes(props.topic.slug) }) - // FIXME use store actions const subscribe = async (really = true) => { - if (really) { - follow({ what: FollowingEntity.Topic, slug: props.topic.slug }) - } else { - unfollow({ what: FollowingEntity.Topic, slug: props.topic.slug }) - } + setIsSubscribing(true) + + await (really + ? follow({ what: FollowingEntity.Topic, slug: props.topic.slug }) + : unfollow({ what: FollowingEntity.Topic, slug: props.topic.slug })) + + await loadSession() + setIsSubscribing(false) } + return (
{ class={styles.controlContainer} classList={{ 'col-md-3': !props.compact && !props.subscribeButtonBottom }} > - + - } - > - - + +
) diff --git a/src/components/Views/AllAuthors.tsx b/src/components/Views/AllAuthors.tsx index d6115ddc..5e138c8e 100644 --- a/src/components/Views/AllAuthors.tsx +++ b/src/components/Views/AllAuthors.tsx @@ -1,7 +1,7 @@ import { createEffect, createMemo, createSignal, For, onMount, Show } from 'solid-js' import type { Author } from '../../graphql/types.gen' import { t } from '../../utils/intl' -import { AuthorsSortBy, setAuthorsSort, useAuthorsStore } from '../../stores/zine/authors' +import { setAuthorsSort, useAuthorsStore } from '../../stores/zine/authors' import { useRouter } from '../../stores/router' import { AuthorCard } from '../Author/Card' import { clsx } from 'clsx' @@ -22,36 +22,38 @@ type AllAuthorsViewProps = { } const PAGE_SIZE = 20 -const ALPHABET = [...'@АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ'] +const ALPHABET = [...'АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ@'] export const AllAuthorsView = (props: AllAuthorsViewProps) => { const [limit, setLimit] = createSignal(PAGE_SIZE) - const { searchParams, changeSearchParam } = useRouter() - const [filterResults, setFilterResults] = createSignal([]) + const { searchParams, changeSearchParam } = useRouter() const { sortedAuthors } = useAuthorsStore({ authors: props.authors, - sortBy: (searchParams().by || 'shouts') as AuthorsSortBy + sortBy: searchParams().by || 'shouts' }) + const [searchQuery, setSearchQuery] = createSignal('') + const { session } = useSession() onMount(() => { if (!searchParams().by) { - setAuthorsSort('name') - changeSearchParam('by', 'name') + changeSearchParam('by', 'shouts') } }) + createEffect(() => { - setAuthorsSort((searchParams().by || 'shouts') as AuthorsSortBy) - setFilterResults(sortedAuthors()) - setLimit(PAGE_SIZE) + setAuthorsSort(searchParams().by || 'shouts') }) const byLetter = createMemo<{ [letter: string]: Author[] }>(() => { return sortedAuthors().reduce((acc, author) => { let letter = author.name.trim().split(' ').pop().at(0).toUpperCase() - if (!/[А-я]/i.test(letter) && locale() === 'ru') letter = '@' + + if (/[^ËА-яё]/.test(letter) && locale() === 'ru') letter = '@' + if (!acc[letter]) acc[letter] = [] + acc[letter].push(author) return acc }, {} as { [letter: string]: Author[] }) @@ -60,41 +62,34 @@ export const AllAuthorsView = (props: AllAuthorsViewProps) => { const sortedKeys = createMemo(() => { const keys = Object.keys(byLetter()) keys.sort() + keys.push(keys.shift()) return keys }) const subscribed = (s) => Boolean(session()?.news?.authors && session()?.news?.authors?.includes(s || '')) - // eslint-disable-next-line sonarjs/cognitive-complexity - const filterAuthors = (value) => { - /* very stupid filter by string algorithm with no deps */ - let q = value.toLowerCase() - if (q.length > 0) { - setFilterResults([]) - if (locale() === 'ru') q = translit(q, 'ru') - const aaa: Author[] = sortedAuthors() - sortedAuthors().forEach((author) => { - let flag = false - author.slug.split('-').forEach((w) => { - if (w.startsWith(q)) flag = true - }) + const filteredAuthors = createMemo(() => { + let q = searchQuery().toLowerCase() - if (!flag) { - let wrds: string = author.name.toLowerCase() - if (locale() === 'ru') wrds = translit(wrds, 'ru') - wrds.split(' ').forEach((w: string) => { - if (w.startsWith(q)) flag = true - }) - } - - if (!flag && aaa.includes(author)) { - const idx = aaa.indexOf(author) - aaa.splice(idx, 1) - } - }) - setFilterResults(aaa) + if (q.length === 0) { + return sortedAuthors() } - } + + if (locale() === 'ru') q = translit(q) + + return sortedAuthors().filter((author) => { + if (author.slug.split('-').some((w) => w.startsWith(q))) { + return true + } + + let name = author.name.toLowerCase() + if (locale() === 'ru') { + name = translit(name) + } + + return name.split(' ').some((word) => word.startsWith(q)) + }) + }) const showMore = () => setLimit((oldLimit) => oldLimit + PAGE_SIZE) const AllAuthorsHead = () => ( @@ -104,7 +99,7 @@ export const AllAuthorsView = (props: AllAuthorsViewProps) => {

{t('Subscribe who you like to tune your personal feed')}

    -
  • +
  • {t('By shouts')}
  • @@ -115,7 +110,7 @@ export const AllAuthorsView = (props: AllAuthorsViewProps) => {
@@ -139,7 +134,10 @@ export const AllAuthorsView = (props: AllAuthorsViewProps) => { scrollHandler(`letter-${index()}`)} + onClick={(event) => { + event.preventDefault() + scrollHandler(`letter-${index()}`) + }} > {letter} @@ -152,9 +150,9 @@ export const AllAuthorsView = (props: AllAuthorsViewProps) => {
- {(letter, index) => ( + {(letter) => (
-

{letter}

+

{letter}

@@ -178,8 +176,8 @@ export const AllAuthorsView = (props: AllAuthorsViewProps) => { - - + + {(author) => (
@@ -197,7 +195,7 @@ export const AllAuthorsView = (props: AllAuthorsViewProps) => { - limit() && searchParams().by !== 'name'}> + limit() && searchParams().by !== 'name'}>