From 03024ca58d7d098d278245ac8b262bb889acf67f Mon Sep 17 00:00:00 2001 From: kvakazyambra Date: Fri, 4 Nov 2022 11:13:11 +0300 Subject: [PATCH 01/34] Fixed create article control on mobile --- src/components/Nav/Header.module.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Nav/Header.module.scss b/src/components/Nav/Header.module.scss index c8ac34a4..74a81550 100644 --- a/src/components/Nav/Header.module.scss +++ b/src/components/Nav/Header.module.scss @@ -460,9 +460,9 @@ } .userControlItemWritePost { - width: auto; - @include media-breakpoint-up(lg) { + width: auto; + .icon { display: none; } From d0c0f3ab93162deeff088f2c6b2696ac4897e34c Mon Sep 17 00:00:00 2001 From: kvakazyambra Date: Fri, 4 Nov 2022 16:05:50 +0300 Subject: [PATCH 02/34] Fixed document scroll position when modal opened --- src/components/Nav/Header.tsx | 15 +++++++++++++++ src/styles/app.scss | 1 - 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/components/Nav/Header.tsx b/src/components/Nav/Header.tsx index e9d6b598..8e94b426 100644 --- a/src/components/Nav/Header.tsx +++ b/src/components/Nav/Header.tsx @@ -49,9 +49,24 @@ export const Header = (props: Props) => { const toggleWarnings = () => setVisibleWarnings(!visibleWarnings()) const toggleFixed = () => setFixed((oldFixed) => !oldFixed) // effects + + let windowScrollTop = 0 + createEffect(() => { + const mainContent = document.querySelector('.main-content') + + if (fixed() || modal() !== null) { + windowScrollTop = window.scrollY + mainContent.style.marginTop = `-${windowScrollTop}px` + } + document.body.classList.toggle('fixed', fixed() || modal() !== null) document.body.classList.toggle(styles.fixed, fixed() && !modal()) + + if (!fixed() && !modal()) { + mainContent.style.marginTop = '' + window.scrollTo(0, windowScrollTop) + } }) // derived diff --git a/src/styles/app.scss b/src/styles/app.scss index 0e6e02e6..fff35a04 100644 --- a/src/styles/app.scss +++ b/src/styles/app.scss @@ -565,7 +565,6 @@ astro-island { min-height: 300px; padding-top: 100px; position: relative; - transition: all 1s ease; } .main-content--no-padding { From 2e53d894c4a98bd60aeb4658be67d54e563b579e Mon Sep 17 00:00:00 2001 From: kvakazyambra Date: Fri, 4 Nov 2022 16:58:42 +0300 Subject: [PATCH 03/34] Fixed linter error --- src/components/Nav/Header.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Nav/Header.tsx b/src/components/Nav/Header.tsx index 8e94b426..f87c6825 100644 --- a/src/components/Nav/Header.tsx +++ b/src/components/Nav/Header.tsx @@ -53,7 +53,7 @@ export const Header = (props: Props) => { let windowScrollTop = 0 createEffect(() => { - const mainContent = document.querySelector('.main-content') + const mainContent = document.querySelector('.main-content') as HTMLDivElement if (fixed() || modal() !== null) { windowScrollTop = window.scrollY From fc4ce0e79d967c4d8d48a2d6e489795db53af887 Mon Sep 17 00:00:00 2001 From: kvakazyambra Date: Fri, 4 Nov 2022 17:25:55 +0300 Subject: [PATCH 04/34] Fixed swiper fractional values --- src/components/Feed/Slider.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Feed/Slider.tsx b/src/components/Feed/Slider.tsx index 686b4f1f..87406264 100644 --- a/src/components/Feed/Slider.tsx +++ b/src/components/Feed/Slider.tsx @@ -20,6 +20,7 @@ export default (props: SliderProps) => { let nextEl: HTMLDivElement | undefined let prevEl: HTMLDivElement | undefined const opts: SwiperOptions = { + roundLengths: true, loop: true, centeredSlides: true, slidesPerView: 1, From 9581f21d7b942e758facd92731223d81424d49cd Mon Sep 17 00:00:00 2001 From: kvakazyambra Date: Tue, 8 Nov 2022 00:07:42 +0300 Subject: [PATCH 05/34] Topic page fixes --- src/components/Feed/Beside.module.scss | 2 + src/components/Nav/Modal.scss | 3 +- .../Topic/{Full.scss => Full.module.scss} | 12 +- src/components/Topic/Full.tsx | 59 +++--- src/components/Views/Author.tsx | 1 - src/components/Views/Topic.tsx | 176 ++++++++++-------- src/styles/{Topic.scss => Topic.module.scss} | 6 +- 7 files changed, 144 insertions(+), 115 deletions(-) rename src/components/Topic/{Full.scss => Full.module.scss} (66%) rename src/styles/{Topic.scss => Topic.module.scss} (82%) diff --git a/src/components/Feed/Beside.module.scss b/src/components/Feed/Beside.module.scss index 1e5486ab..fb3d7b66 100644 --- a/src/components/Feed/Beside.module.scss +++ b/src/components/Feed/Beside.module.scss @@ -12,6 +12,8 @@ } li { + margin-bottom: 1em; + &.top { border-bottom: 1px solid #e1e1e1; display: flex; diff --git a/src/components/Nav/Modal.scss b/src/components/Nav/Modal.scss index 6be20367..8c7bc2f0 100644 --- a/src/components/Nav/Modal.scss +++ b/src/components/Nav/Modal.scss @@ -1,11 +1,12 @@ .modalwrap { - pointer-events: all; align-items: center; background: rgb(20 20 20 / 70%); display: flex; justify-content: center; height: 100%; left: 0; + overflow: auto; + pointer-events: all; position: fixed; top: 0; width: 100%; diff --git a/src/components/Topic/Full.scss b/src/components/Topic/Full.module.scss similarity index 66% rename from src/components/Topic/Full.scss rename to src/components/Topic/Full.module.scss index ef8c4baf..f0c39156 100644 --- a/src/components/Topic/Full.scss +++ b/src/components/Topic/Full.module.scss @@ -1,11 +1,19 @@ -.topic__header { +.topicHeader { @include font-size(1.7rem); padding-top: 5.8rem; text-align: center; + + h1 { + color: #2638d9; + font-weight: 500; + text-transform: uppercase; + + @include font-size(2rem); + } } -.topic__actions { +.topicActions { margin-top: 2.8rem; button, diff --git a/src/components/Topic/Full.tsx b/src/components/Topic/Full.tsx index 5923ead9..c6bb57fa 100644 --- a/src/components/Topic/Full.tsx +++ b/src/components/Topic/Full.tsx @@ -1,10 +1,11 @@ import { createMemo, Show } from 'solid-js' import type { Topic } from '../../graphql/types.gen' import { FollowingEntity } from '../../graphql/types.gen' -import './Full.scss' +import styles from './Full.module.scss' import { useAuthStore } from '../../stores/auth' import { follow, unfollow } from '../../stores/zine/common' import { t } from '../../utils/intl' +import { clsx } from 'clsx' type Props = { topic: Topic @@ -15,37 +16,35 @@ export const FullTopic = (props: Props) => { const subscribed = createMemo(() => session()?.news?.topics?.includes(props.topic?.slug)) return ( -
-
- -
-

#{props.topic.title}

-

{props.topic.body}

-
- - - - - - - {t('Write about the topic')} -
- - {props.topic.title} +
+ +
+

#{props.topic.title}

+

{props.topic.body}

+
+ + + + + + {t('Write about the topic')}
- -
+ + {props.topic.title} + +
+
) } diff --git a/src/components/Views/Author.tsx b/src/components/Views/Author.tsx index 52cd189f..59278d34 100644 --- a/src/components/Views/Author.tsx +++ b/src/components/Views/Author.tsx @@ -7,7 +7,6 @@ import { t } from '../../utils/intl' import { useAuthorsStore } from '../../stores/zine/authors' import { loadAuthorArticles, useArticlesStore } from '../../stores/zine/articles' -import '../../styles/Topic.scss' import { useTopicsStore } from '../../stores/zine/topics' import { useRouter } from '../../stores/router' import { Beside } from '../Feed/Beside' diff --git a/src/components/Views/Topic.tsx b/src/components/Views/Topic.tsx index 8e96ea6f..a50bf23a 100644 --- a/src/components/Views/Topic.tsx +++ b/src/components/Views/Topic.tsx @@ -4,7 +4,7 @@ import { Row3 } from '../Feed/Row3' import { Row2 } from '../Feed/Row2' import { Beside } from '../Feed/Beside' import { ArticleCard } from '../Feed/Card' -import '../../styles/Topic.scss' +import styles from '../../styles/Topic.module.scss' import { FullTopic } from '../Topic/Full' import { t } from '../../utils/intl' import { useRouter } from '../../stores/router' @@ -13,6 +13,9 @@ import { loadPublishedArticles, useArticlesStore } from '../../stores/zine/artic import { useAuthorsStore } from '../../stores/zine/authors' import { restoreScrollPosition, saveScrollPosition } from '../../utils/scroll' import { splitToPages } from '../../utils/splitToPages' +import { clsx } from 'clsx' +import Slider from '../Feed/Slider' +import { Row1 } from '../Feed/Row1' type TopicsPageSearchParams = { by: 'comments' | '' | 'recent' | 'viewed' | 'rating' | 'commented' @@ -70,94 +73,111 @@ export const TopicView = (props: TopicProps) => { ) return ( -
+
-
-
-
    -
  • - -
  • - {/*TODO: server sort*/} - {/*
  • */} - {/* */} - {/*
  • */} - {/*
  • */} - {/* */} - {/*
  • */} - {/*
  • */} - {/* */} - {/*
  • */} -
-
-
-
- {`${t('Show')} `} - {t('All posts')} +
+
+
+
    +
  • + +
  • + {/*TODO: server sort*/} + {/*
  • */} + {/* */} + {/*
  • */} + {/*
  • */} + {/* */} + {/*
  • */} + {/*
  • */} + {/* */} + {/*
  • */} +
+
+
+
+ {`${t('Show')} `} + {t('All posts')} +
-
-
-
-

{title()}

- - {(article) => ( -
- -
- )} -
+ + + + + + + + + + + + +
+
+
+

{title()}

+ + {(article) => ( +
+ +
+ )} +
+
-
+ -
- 5}> - - - - - - - + 5}> + + + + + + - - {(page) => ( - <> - - - - - )} - + + {(page) => ( + <> + + + + + )} + - -

- -

-
-
+ +

+ +

+
) diff --git a/src/styles/Topic.scss b/src/styles/Topic.module.scss similarity index 82% rename from src/styles/Topic.scss rename to src/styles/Topic.module.scss index 20b16c28..fa18beab 100644 --- a/src/styles/Topic.scss +++ b/src/styles/Topic.module.scss @@ -1,11 +1,11 @@ -.topic-page { - .group__controls { +.topicPage { + .groupControls { align-items: baseline; margin-bottom: 4rem; margin-top: 7rem; } - .floor--important { + .floorImportant { a:hover { background: #fff; color: #000 !important; From 7142b58bc9385509587c5fda2f55710deb09abcb Mon Sep 17 00:00:00 2001 From: kvakazyambra Date: Tue, 8 Nov 2022 09:10:06 +0300 Subject: [PATCH 06/34] About pages style fixes --- .../Pages/about/DiscussionRulesPage.tsx | 186 ++++--- src/components/Pages/about/DogmaPage.tsx | 4 +- src/components/Pages/about/GuidePage.tsx | 442 ++++++++-------- src/components/Pages/about/HelpPage.tsx | 255 ++++----- src/components/Pages/about/ManifestPage.tsx | 170 +++--- src/components/Pages/about/PartnersPage.tsx | 5 +- src/components/Pages/about/PrinciplesPage.tsx | 295 +++++------ src/components/Pages/about/ProjectsPage.tsx | 5 +- src/components/Pages/about/TermsOfUsePage.tsx | 494 +++++++++--------- src/components/Pages/about/ThanksPage.tsx | 4 +- src/styles/app.scss | 12 + 11 files changed, 966 insertions(+), 906 deletions(-) diff --git a/src/components/Pages/about/DiscussionRulesPage.tsx b/src/components/Pages/about/DiscussionRulesPage.tsx index eb8e89ce..23797f7e 100644 --- a/src/components/Pages/about/DiscussionRulesPage.tsx +++ b/src/components/Pages/about/DiscussionRulesPage.tsx @@ -7,112 +7,110 @@ export const DiscussionRulesPage = () => {
-
+

-
-

- Открытая редакция существует благодаря дружному сообществу авторов - и читателей — вдумчивых и сознательных людей, приверженных ценностям - гуманизма, демократии и прав человека. Мы очень ценим атмосферу осмысленного - общения, которая здесь сложилась. Чтобы сохранить ее такой же уютной - и творческой, мы составили правила общения в сообществе, руководствуясь - которыми каждый мог бы соучаствовать в плодотворных дискуссиях, не задевая - других. Ключевой принцип этих правил предельно прост — уважайте ближних, - постарайтесь не нарушать законы Российской Федерации без крайней - на то необходимости и помните, что в дискуссиях чутких - и здравомыслящих людей рождается истина. -

+

+ Открытая редакция существует благодаря дружному сообществу авторов + и читателей — вдумчивых и сознательных людей, приверженных ценностям + гуманизма, демократии и прав человека. Мы очень ценим атмосферу осмысленного + общения, которая здесь сложилась. Чтобы сохранить ее такой же уютной + и творческой, мы составили правила общения в сообществе, руководствуясь + которыми каждый мог бы соучаствовать в плодотворных дискуссиях, не задевая + других. Ключевой принцип этих правил предельно прост — уважайте ближних, + постарайтесь не нарушать законы Российской Федерации без крайней + на то необходимости и помните, что в дискуссиях чутких + и здравомыслящих людей рождается истина. +

-

За что можно получить дырку в карме и выиграть бан в сообществе

-
    -
  1. -

    - Оскорбления, личные нападки, травля и угрозы. В любом виде. Конкретного - человека или социальной группы — не суть. Агрессия, переход - на личности и токсичность едва ли способствуют плодотворному общению. -

    -
  2. +

    За что можно получить дырку в карме и выиграть бан в сообществе

    +
      +
    1. +

      + Оскорбления, личные нападки, травля и угрозы. В любом виде. Конкретного человека + или социальной группы — не суть. Агрессия, переход на личности + и токсичность едва ли способствуют плодотворному общению. +

      +
    2. -
    3. -

      - Шовинизм, расизм, сексизм, гомофобия, пропаганда ненависти, педофилии, суицида, - распространение детской порнографии и другого человеконенавистнического контента. -

      -
    4. +
    5. +

      + Шовинизм, расизм, сексизм, гомофобия, пропаганда ненависти, педофилии, суицида, + распространение детской порнографии и другого человеконенавистнического контента. +

      +
    6. -
    7. -

      - Спам, реклама, фейкньюз, ссылки на пропагандистские СМИ, вбросы дезинформации, - специально уводящий от темы флуд, провокации, разжигание конфликтов, намеренный - срыв дискуссий. -

      -
    8. +
    9. +

      + Спам, реклама, фейкньюз, ссылки на пропагандистские СМИ, вбросы дезинформации, + специально уводящий от темы флуд, провокации, разжигание конфликтов, намеренный срыв + дискуссий. +

      +
    10. -
    11. -

      - Неаргументированная критика и комментарии вроде «отстой», «зачем - я это увидел/а», «не читал, но осуждаю», - «либераху порвало», «лол», «скатились», - «первый нах» и тому подобные. Односложные реплики не подразумевают - возможность обогащающего диалога, не продуктивны и никак не помогают - авторам делать материалы лучше, а читателям — разобраться. -

      -
    12. -
    +
  3. +

    + Неаргументированная критика и комментарии вроде «отстой», «зачем + я это увидел/а», «не читал, но осуждаю», «либераху + порвало», «лол», «скатились», «первый нах» + и тому подобные. Односложные реплики не подразумевают возможность обогащающего + диалога, не продуктивны и никак не помогают авторам делать материалы лучше, + а читателям — разобраться. +

    +
  4. +
-

За что можно получить лучи добра и благодарности в сообществе

-
    -
  1. -

    - Вежливость и конструктивность. Мы выступаем - за конструктивный диалог, аргументированные комментарии и доброжелательное - отношение друг к другу. Задавайте содержательные вопросы, пишите развернутые - комментарии, подкрепляйте их аргументами, чтобы диалог был полезен всем участникам, - помогая глубже понять тему и разобраться в вопросе. И, пожалуйста, уважайте - собеседника, даже если он вам лично не импонирует: только так получаются - продуктивные дискуссии. -

    -
  2. +

    За что можно получить лучи добра и благодарности в сообществе

    +
      +
    1. +

      + Вежливость и конструктивность. Мы выступаем + за конструктивный диалог, аргументированные комментарии и доброжелательное + отношение друг к другу. Задавайте содержательные вопросы, пишите развернутые + комментарии, подкрепляйте их аргументами, чтобы диалог был полезен всем участникам, + помогая глубже понять тему и разобраться в вопросе. И, пожалуйста, уважайте + собеседника, даже если он вам лично не импонирует: только так получаются + продуктивные дискуссии. +

      +
    2. -
    3. -

      - Обмен знаниями и историями. Осмысленные высказывания по теме - поста, оригинальные рассуждения, рассказы о личном опыте и проектах, обмен - профессиональной экспертизой, наблюдения и реальные истории - из жизни — чем больше мы делимся друг с другом знаниями, тем - интереснее и плодотворнее становится наше общение. Помните, что каждый вдумчивый - ответ повышает качество дискуссий в сообществе и делает чтение самиздата ещё - интереснее. -

      -
    4. +
    5. +

      + Обмен знаниями и историями. Осмысленные высказывания по теме + поста, оригинальные рассуждения, рассказы о личном опыте и проектах, обмен + профессиональной экспертизой, наблюдения и реальные истории + из жизни — чем больше мы делимся друг с другом знаниями, тем + интереснее и плодотворнее становится наше общение. Помните, что каждый вдумчивый + ответ повышает качество дискуссий в сообществе и делает чтение самиздата ещё + интереснее. +

      +
    6. -
    7. -

      - Чувство юмора и добродушие. Остроумие и дружелюбие - не только направляют дискуссии в продуктивное русло, но и улучшают - настроение. Не вредите негативом, которого в интернете и без нас хватает, - и не травите на корню классные инициативы — всё великое - начинается с малого. Мы за поддерживающую и вдохновляющую атмосферу - в сообществе. Надеемся, вы тоже. -

      -
    8. +
    9. +

      + Чувство юмора и добродушие. Остроумие и дружелюбие + не только направляют дискуссии в продуктивное русло, но и улучшают + настроение. Не вредите негативом, которого в интернете и без нас хватает, + и не травите на корню классные инициативы — всё великое + начинается с малого. Мы за поддерживающую и вдохновляющую атмосферу + в сообществе. Надеемся, вы тоже. +

      +
    10. -
    11. -

      - Благодарность и поддержка. Если публикация вам зашла, - не стесняйтесь ставить лайки, делиться понравившимися материалами, благодарить - авторов, читателей, художников и редакторов в комментариях. Цените - и поддерживайте классные проекты, сильные тексты, новое искусство, осмысленные - комментарии и вклад других в самиздат — сотрудничество делает нас - сильнее и усиливает звучание идей и смыслов, которые помогают лучше понимать - мир. -

      -
    12. -
    -
+
  • +

    + Благодарность и поддержка. Если публикация вам зашла, + не стесняйтесь ставить лайки, делиться понравившимися материалами, благодарить + авторов, читателей, художников и редакторов в комментариях. Цените + и поддерживайте классные проекты, сильные тексты, новое искусство, осмысленные + комментарии и вклад других в самиздат — сотрудничество делает нас + сильнее и усиливает звучание идей и смыслов, которые помогают лучше понимать + мир. +

    +
  • +
    diff --git a/src/components/Pages/about/DogmaPage.tsx b/src/components/Pages/about/DogmaPage.tsx index b083987d..86730bd6 100644 --- a/src/components/Pages/about/DogmaPage.tsx +++ b/src/components/Pages/about/DogmaPage.tsx @@ -7,8 +7,8 @@ export const DogmaPage = () => {
    -

    Редакционные принципы

    -
    +
    +

    Редакционные принципы

    Дискурс - журнал с открытой горизонтальной редакцией. Содержание журнала определяется прямым голосованием его авторов. Мы нередко занимаем различные позиции по разным проблемам, но diff --git a/src/components/Pages/about/GuidePage.tsx b/src/components/Pages/about/GuidePage.tsx index a1cfc21d..d5bb6793 100644 --- a/src/components/Pages/about/GuidePage.tsx +++ b/src/components/Pages/about/GuidePage.tsx @@ -1,9 +1,14 @@ +import { createSignal, Show } from 'solid-js' import { MainLayout } from '../../Layouts/MainLayout' import { t } from '../../../utils/intl' export const GuidePage = () => { const title = t('How it works') + const [indexExpanded, setIndexExpanded] = createSignal(false) + + const toggleIndexExpanded = () => setIndexExpanded((oldExpanded) => !oldExpanded) + return ( {/**/} @@ -16,64 +21,38 @@ export const GuidePage = () => {

    - - -
    +

    Как устроен Дискурс

    -
    -

    - Дискурс — независимый журнал о культуре, науке, искусстве и обществе - с открытой редакцией. У нас нет главного редактора, - инвестора и вообще никого, кто бы принимал единоличные решения. Вместо - традиционных иерархий Дискурс основан на принципах прямой демократии: в нашем - горизонтальном сообществе все редакционные вопросы решаются открытым голосованием авторов - журнала. Вот как это работает. -

    -

    Как устроен сайт Дискурса

    -

    Дискурс состоит из четырех основных разделов:

    -
      -
    • -

      - Темы — у нас публикуются исследования, обзоры, - эссе, интервью, репортажи, аналитика и другие материалы о культуре, науке, - искусстве и обществе. -

      -
    • -
    • -

      - Искусство — здесь, например, представлены - художественные произведения: литература, живопись, музыка, фотографии, видео. Этот - раздел помогает прозвучать новому искусству, которое создают российские художники, - писатели, режиссёры и музыканты. -

      -
    • - {/* +

      + Дискурс — независимый журнал о культуре, науке, искусстве и обществе + с открытой редакцией. У нас нет главного редактора, + инвестора и вообще никого, кто бы принимал единоличные решения. Вместо традиционных + иерархий Дискурс основан на принципах прямой демократии: в нашем горизонтальном + сообществе все редакционные вопросы решаются открытым голосованием авторов журнала. Вот как + это работает. +

      +

      Как устроен сайт Дискурса

      +

      Дискурс состоит из четырех основных разделов:

      +
        +
      • +

        + Темы — у нас публикуются исследования, обзоры, эссе, + интервью, репортажи, аналитика и другие материалы о культуре, науке, искусстве + и обществе. +

        +
      • +
      • +

        + Искусство — здесь, например, представлены + художественные произведения: литература, живопись, музыка, фотографии, видео. Этот раздел + помогает прозвучать новому искусству, которое создают российские художники, писатели, + режиссёры и музыканты. +

        +
      • + {/*
      • События — в этом разделе @@ -94,176 +73,207 @@ export const GuidePage = () => {

      • */} -
      -

      - Материалы в Дискурсе объединяются по темам - — ключевым словам, которые располагаются в конце материалов и связывают - материалы по жанрам (например, - интервью, репортажи,{' '} - эссе, ликбезы), по тематике ( - кино, философия,{' '} - история, абсурдизм,{' '} - секс и т.д.) или в серии (как « - Законы мира» или « - За линией Маннергейма»). Темы - объединяют сотни публикаций, помогают ориентироваться в журнале и следить - за интересными материалами. -

      +
    +

    + Материалы в Дискурсе объединяются по темам + — ключевым словам, которые располагаются в конце материалов и связывают + материалы по жанрам (например, + интервью, репортажи,{' '} + эссе, ликбезы), по тематике ( + кино, философия,{' '} + история, абсурдизм,{' '} + секс и т.д.) или в серии (как « + Законы мира» или « + За линией Маннергейма»). Темы объединяют + сотни публикаций, помогают ориентироваться в журнале и следить за интересными + материалами. +

    -
    -

    Как стать автором журнала

    +
    +

    Как стать автором журнала

    +

    + Дискурс объединяет журналистов, активистов, музыкантов, художников, фотографов, режиссеров, + философов, ученых и других замечательных людей. Каждый может{' '} + прислать + свой материал в журнал. Формат и тематика не имеют значения, единственное, + что важно — хороший ли материал. Если + сообщество поддержит вашу публикацию, она выйдет в журнале и станет доступна + тысячам наших читателей. +

    +
    + +

    Как проходит голосование

    +

    + Все присылаемые в Дискурс материалы попадают в  + «Редакцию». Это внутренний раздел сайта, где участники сообщества + решают, что будет опубликовано в Дискурсе. Как только работа получает одобрение как + минимум пятерых авторов открытой редакции, она немедленно публикуется в журнале. + Если же материал набирает более 20% голосов «против», + он не выходит и может быть отправлен на доработку. Жестких сроков + рассмотрения материалов у нас нет, иногда это занимает час, иногда месяц, + обычно — несколько дней. +

    +
    +

    + Как только сообщество поддержит публикацию, вы получите приглашение + в интернет-редакцию и сможете голосовать за новые материалы. +

    +
    + +

    Как мы делаем тексты друг друга лучше

    +

    + Дискурс — журнал с совместным редактированием. Совершенствовать тексты нам + помогает система ремарок. Вы можете выделить часть текста в любой статье + и оставить к ней замечание, вопрос или предложение — автор текста получит + совет на почту и сможет его учесть. Так мы устраняем опечатки, неточности + и советуем друг другу, как сделать тексты качественнее и интереснее. +

    +

    + Среди участников сообщества есть профессиональные редакторы, которые помогают авторам делать + тексты лучше. Если вашему материалу потребуется доработка, они помогут отредактировать текст, + подобрать иллюстрации, придумать заголовок и красиво сверстать публикацию. Если + вы хотите обсудить текст, прежде чем загрузить материал в интернет-редакцию — + разместите его в google-документе, откройте доступ к редактированию по ссылке + и напишите нам на  + + welcome@discours.io + + . +

    +

    + Если у вас возникают трудности с тем, чтобы подобрать к своему материалу + иллюстрации, тоже пишите на  + + почту + + — наши коллеги-художники могут вам помочь{' '} + + в режиме совместного редактирования + + . +

    + +

    Что сообщество дает авторам

    +
      +
    • - Дискурс объединяет журналистов, активистов, музыкантов, художников, фотографов, - режиссеров, философов, ученых и других замечательных людей. Каждый может{' '} - прислать - свой материал в журнал. Формат и тематика не имеют значения, единственное, - что важно — хороший ли материал. - Если сообщество поддержит вашу публикацию, она выйдет в журнале и станет - доступна тысячам наших читателей. + Право определять, каким будет журнал. Дискурс — это + общественная институция, созданная людьми и ради людей, функционирующая + на условиях прямой демократии. Авторы публикуют статьи и художественные проекты, + участвуют в обсуждениях, голосуют за работы коллег и таким образом вносят + свой вклад в развитие проекта, определяя содержание и направление журнала.

      -
    - -

    Как проходит голосование

    -

    - Все присылаемые в Дискурс материалы попадают в  - «Редакцию». Это внутренний раздел сайта, где участники - сообщества решают, что будет опубликовано в Дискурсе. Как только работа получает - одобрение как минимум пятерых авторов открытой редакции, она немедленно публикуется - в журнале. Если же материал набирает более 20% голосов «против», - он не выходит и может быть отправлен на доработку. Жестких сроков - рассмотрения материалов у нас нет, иногда это занимает час, иногда месяц, - обычно — несколько дней. -

    -
    + +
  • - Как только сообщество поддержит публикацию, вы получите приглашение - в интернет-редакцию и сможете голосовать за новые материалы. + Возможность обратиться к широкой аудитории. Дискурс читают десятки + тысяч людей, и с каждым днем их становится больше.

    -
  • + +
  • +

    + Поддержка редакции. Дискурс предоставляет авторам аккредитацию + на мероприятия, базу контактов, юридическую поддержку, ознакомление с книжными, + кино- и музыкальными новинками до их выхода в свет. Если что-то + из этого вам понадобится, пишите на почту{' '} + + welcome@discours.io + +  — поможем. +

    +
  • +
  • +

    + Пресс-карты для корреспондентов. Три опубликованные статьи позволяют + авторам Дискурса получить официальные удостоверения журналистов (пресс-карты) + на следующий год. Пресс-карты удостоверяют, что вы журналист и можете + пользоваться всеми теми правами, которые гарантирует Закон о СМИ. Кроме того, многие + культурные институции (музеи, галереи и др.) предоставляют журналистам право + свободного входа. +

    +
  • +
  • +

    + Помощь сотен специалистов в разных областях. В основе Дискурса + лежит идея совместного редактирования. Участники редакционного сообщества — + несколько сотен журналистов, исследователей, художников, литераторов из разных стран + — изучают материалы друг друга до публикации и помогают сделать + их качественнее и интереснее. Так, в редакции нередко складываются + творческие союзы: например, авторов текстов и художников, создающих для них + иллюстрации. +

    +
  • +
  • +

    + Пространство общения полное выдающихся людей. Дискурс — + большое живое сообщество интеллектуалов, разбросанных по всему земному шару. Вступив + в редакцию, вы сможете познакомиться со множеством интересных людей, + которые определяют повестку завтрашнего дня, вдохновляют окружающих, создают новое + и изучают старое, ищут знания и готовы ими делиться, чтобы менять мир + в соответствии со своими идеалами. +

    +
  • + -

    Как мы делаем тексты друг друга лучше

    -

    - Дискурс — журнал с совместным редактированием. Совершенствовать тексты нам - помогает система ремарок. Вы можете выделить часть текста в любой статье - и оставить к ней замечание, вопрос или предложение — автор текста - получит совет на почту и сможет его учесть. Так мы устраняем опечатки, - неточности и советуем друг другу, как сделать тексты качественнее и интереснее. -

    -

    - Среди участников сообщества есть профессиональные редакторы, которые помогают авторам делать - тексты лучше. Если вашему материалу потребуется доработка, они помогут отредактировать - текст, подобрать иллюстрации, придумать заголовок и красиво сверстать публикацию. Если - вы хотите обсудить текст, прежде чем загрузить материал в - интернет-редакцию — разместите его в google-документе, откройте доступ - к редактированию по ссылке и напишите нам на  - - welcome@discours.io - - . -

    -

    - Если у вас возникают трудности с тем, чтобы подобрать к своему материалу - иллюстрации, тоже пишите на  - - почту - - — наши коллеги-художники могут вам помочь{' '} - - в режиме совместного редактирования - - . -

    +

    Как быть в курсе

    +

    + За свежими публикациями Дискурса можно следить не только на сайте, + но и на страницах в  + + Фейсбуке + + ,{' '} + + ВКонтакте + {' '} + и  + + Телеграме + + . А ещё раз в месяц мы отправляем почтовую рассылку{' '} + с дайджестом лучших материалов. +

    +

    + Если вы хотите сотрудничать, что-то обсудить или предложить — пожалуйста, пишите + на  + + welcome@discours.io + + . Мы обязательно ответим. +

    +
    -

    Что сообщество дает авторам

    -
      -
    • -

      - Право определять, каким будет журнал. Дискурс — это - общественная институция, созданная людьми и ради людей, функционирующая - на условиях прямой демократии. Авторы публикуют статьи и художественные - проекты, участвуют в обсуждениях, голосуют за работы коллег и таким - образом вносят свой вклад в развитие проекта, определяя содержание - и направление журнала. -

      -
    • -
    • -

      - Возможность обратиться к широкой аудитории. Дискурс читают десятки - тысяч людей, и с каждым днем их становится больше. -

      -
    • -
    • -

      - Поддержка редакции. Дискурс предоставляет авторам аккредитацию - на мероприятия, базу контактов, юридическую поддержку, ознакомление - с книжными, кино- и музыкальными новинками до их выхода в свет. - Если что-то из этого вам понадобится, пишите на почту{' '} - - welcome@discours.io - -  — поможем. -

      -
    • -
    • -

      - Пресс-карты для корреспондентов. Три опубликованные статьи позволяют - авторам Дискурса получить официальные удостоверения журналистов (пресс-карты) - на следующий год. Пресс-карты удостоверяют, что вы журналист и можете - пользоваться всеми теми правами, которые гарантирует Закон о СМИ. Кроме того, - многие культурные институции (музеи, галереи и др.) предоставляют журналистам право - свободного входа. -

      -
    • -
    • -

      - Помощь сотен специалистов в разных областях. В основе - Дискурса лежит идея совместного редактирования. Участники редакционного - сообщества — несколько сотен журналистов, исследователей, художников, - литераторов из разных стран — изучают материалы друг друга до публикации - и помогают сделать их качественнее и интереснее. Так, в редакции - нередко складываются творческие союзы: например, авторов текстов и художников, - создающих для них иллюстрации. -

      -
    • -
    • -

      - Пространство общения полное выдающихся людей. Дискурс — - большое живое сообщество интеллектуалов, разбросанных по всему земному шару. - Вступив в редакцию, вы сможете познакомиться со множеством интересных - людей, которые определяют повестку завтрашнего дня, вдохновляют окружающих, создают - новое и изучают старое, ищут знания и готовы ими делиться, чтобы менять мир - в соответствии со своими идеалами. -

      -
    • -
    +
    +

    + +

    -

    Как быть в курсе

    -

    - За свежими публикациями Дискурса можно следить не только на сайте, - но и на страницах в  - - Фейсбуке - - ,{' '} - - ВКонтакте - {' '} - и  - - Телеграме - - . А ещё раз в месяц мы отправляем почтовую рассылку{' '} - с дайджестом лучших материалов. -

    -

    - Если вы хотите сотрудничать, что-то обсудить или предложить — пожалуйста, пишите - на  - - welcome@discours.io - - . Мы обязательно ответим. -

    -
    + + +
    diff --git a/src/components/Pages/about/HelpPage.tsx b/src/components/Pages/about/HelpPage.tsx index f50fe2a8..3149bc0a 100644 --- a/src/components/Pages/about/HelpPage.tsx +++ b/src/components/Pages/about/HelpPage.tsx @@ -1,9 +1,14 @@ +import { createSignal, Show } from 'solid-js' import { MainLayout } from '../../Layouts/MainLayout' import { Donate } from '../../Discours/Donate' // const title = t('Support us') export const HelpPage = () => { + const [indexExpanded, setIndexExpanded] = createSignal(false) + + const toggleIndexExpanded = () => setIndexExpanded((oldExpanded) => !oldExpanded) + return ( {/*Здесь можно поддержать Дискурс материально.*/} @@ -13,134 +18,140 @@ export const HelpPage = () => {
    - - -
    +

    Как вы можете поддержать Дискурс?

    -
    -

    - Дискурс — уникальное независимое издание с горизонтальной редакцией, - существующее в интересах своих читателей. Ваша поддержка действительно много - значит — не только для редакции Дискурса, но и для сохранения - свободной мысли и некоммерческого искусства в нашем обществе. -

    -

    - Дискурс существует на добровольных началах. Никакой медиахолдинг, фонд или - государственная структура не финансирует нас — благодаря этому мы можем - писать о том, что важно, а не о том, что выгодно. Сообщество наших - волонтеров ежедневно трудится, чтобы рассказывать вам интересные, не освещенные другими - изданиями истории — но мы не сможем делать это без вашей помощи. - Пожертвования читателей составляют основу нашего бюджета и позволяют нам существовать. -

    -

    - Если вам нравится то, что мы делаем и вы хотите, чтобы Дискурс - продолжался, пожалуйста, поддержите проект. -

    -
    -
    - -
    +

    + Дискурс — уникальное независимое издание с горизонтальной редакцией, + существующее в интересах своих читателей. Ваша поддержка действительно много + значит — не только для редакции Дискурса, но и для сохранения + свободной мысли и некоммерческого искусства в нашем обществе. +

    +

    + Дискурс существует на добровольных началах. Никакой медиахолдинг, фонд или + государственная структура не финансирует нас — благодаря этому мы можем + писать о том, что важно, а не о том, что выгодно. Сообщество наших + волонтеров ежедневно трудится, чтобы рассказывать вам интересные, не освещенные другими + изданиями истории — но мы не сможем делать это без вашей помощи. + Пожертвования читателей составляют основу нашего бюджета и позволяют нам существовать. +

    +

    + Если вам нравится то, что мы делаем и вы хотите, чтобы Дискурс + продолжался, пожалуйста, поддержите проект. +

    +
    +
    +
    -

    На что пойдут деньги?

    -

    - Ваши пожертвования пойдут на оплату серверов, содержание офиса, зарплату редакции - и налоги, оплату юридического сопровождения и труда бухгалтера, совершенствование - сайта, аренду помещения для открытой редакции, на печать альманаха Дискурс - с лучшими текстами авторов за полгода, а также на другие редакционные - и технические расходы. -

    -

    Ваша помощь позволит нам

    -
      -
    • -

      Оставаться бесплатным изданием.

      -

      - Мы делаем открытый журнал для всех желающих, а также собираем искусство лучших - авторов по всему миру. Ваша поддержка позволяет нам становиться лучше. -

      -
    • -
    • -

      Создавать еще больше контента.

      -

      - Каждый день к нам присоединяются новые люди, и чем больше нас становится, тем - больше мы творим и строже оцениваем результаты творчества друг друга. - В результате повышается и количество, и качество контента. Каждый день мы - трудимся, чтобы открывать нашим читателям новые грани окружающего мира. -

      -
    • -
    • -

      Развивать форматы и расширять деятельность Дискурса.

      -

      - Мы создаем различные спецпроекты и регулярно проводим необычные мероприятия. - Мы хотим приносить пользу человечеству всеми возможными способами. -

      -
    • -
    • -

      Модернизировать сайт.

      -

      - Мы совершенствуем платформу и стараемся сделать проект максимально удобным для - вас. Мы работаем над мобильной версией, новым дизайном, фукционалом, системой - регистрации, навигации и рекомендаций, которые сделают наше общение еще - увлекательней. -

      -
    • -
    • -

      Выпускать альманах.

      -

      - Выпускать раз в полугодие печатный альманах Дискурс с 33 лучшими текстами - сайта. -

      -
    • -
    • -

      Захватить весь мир

      -

      и принести «Дискурс» в каждый дом.

      -
    • -
    -

    Войдите в попечительский совет Дискурса

    -

    - Вы хотите сделать крупное пожертвование? Станьте попечителем Дискурса — - - напишите нам - - , мы будем рады единомышленникам. -

    -

    Как ещё можно поддержать Дискурс?

    -

    - Есть много других способов поддержать Дискурс и труд наших авторов. Например, - вы можете периодически рассказывать о проекте своим друзьям в соцсетях, - делиться хорошими материалами или — что еще лучше — публиковать свои - статьи в «Дискурсе». Но главное, что вы можете сделать для - Дискурса, — читать нас. Мы вкладываем в журнал душу, и внимание каждого - читателя убеждает нас в правильности выбранного пути. Не переключайтесь. -

    -

    - Если вы хотите помочь проекту, но у вас возникли вопросы, напишите нам письмо - по адресу{' '} - - welcome@discours.io - - . -

    +

    На что пойдут деньги?

    +

    + Ваши пожертвования пойдут на оплату серверов, содержание офиса, зарплату редакции + и налоги, оплату юридического сопровождения и труда бухгалтера, совершенствование + сайта, аренду помещения для открытой редакции, на печать альманаха Дискурс с лучшими + текстами авторов за полгода, а также на другие редакционные и технические + расходы. +

    +

    Ваша помощь позволит нам

    +
      +
    • +

      Оставаться бесплатным изданием.

      +

      + Мы делаем открытый журнал для всех желающих, а также собираем искусство лучших + авторов по всему миру. Ваша поддержка позволяет нам становиться лучше. +

      +
    • +
    • +

      Создавать еще больше контента.

      +

      + Каждый день к нам присоединяются новые люди, и чем больше нас становится, тем + больше мы творим и строже оцениваем результаты творчества друг друга. + В результате повышается и количество, и качество контента. Каждый день мы + трудимся, чтобы открывать нашим читателям новые грани окружающего мира. +

      +
    • +
    • +

      Развивать форматы и расширять деятельность Дискурса.

      +

      + Мы создаем различные спецпроекты и регулярно проводим необычные мероприятия. + Мы хотим приносить пользу человечеству всеми возможными способами. +

      +
    • +
    • +

      Модернизировать сайт.

      +

      + Мы совершенствуем платформу и стараемся сделать проект максимально удобным для + вас. Мы работаем над мобильной версией, новым дизайном, фукционалом, системой + регистрации, навигации и рекомендаций, которые сделают наше общение еще + увлекательней. +

      +
    • +
    • +

      Выпускать альманах.

      +

      + Выпускать раз в полугодие печатный альманах Дискурс с 33 лучшими текстами + сайта. +

      +
    • +
    • +

      Захватить весь мир

      +

      и принести «Дискурс» в каждый дом.

      +
    • +
    +

    Войдите в попечительский совет Дискурса

    +

    + Вы хотите сделать крупное пожертвование? Станьте попечителем Дискурса — + + напишите нам + + , мы будем рады единомышленникам. +

    +

    Как ещё можно поддержать Дискурс?

    +

    + Есть много других способов поддержать Дискурс и труд наших авторов. Например, + вы можете периодически рассказывать о проекте своим друзьям в соцсетях, + делиться хорошими материалами или — что еще лучше — публиковать свои + статьи в «Дискурсе». Но главное, что вы можете сделать для + Дискурса, — читать нас. Мы вкладываем в журнал душу, и внимание каждого + читателя убеждает нас в правильности выбранного пути. Не переключайтесь. +

    +

    + Если вы хотите помочь проекту, но у вас возникли вопросы, напишите нам письмо + по адресу{' '} + + welcome@discours.io + + . +

    +
    + +
    diff --git a/src/components/Pages/about/ManifestPage.tsx b/src/components/Pages/about/ManifestPage.tsx index a6453f43..1a05df50 100644 --- a/src/components/Pages/about/ManifestPage.tsx +++ b/src/components/Pages/about/ManifestPage.tsx @@ -1,3 +1,4 @@ +import { createSignal, Show } from 'solid-js' import { MainLayout } from '../../Layouts/MainLayout' import { Modal } from '../../Nav/Modal' import { Feedback } from '../../Discours/Feedback' @@ -7,6 +8,10 @@ import Opener from '../../Nav/Opener' // title={t('Manifest')} export const ManifestPage = () => { + const [indexExpanded, setIndexExpanded] = createSignal(false) + + const toggleIndexExpanded = () => setIndexExpanded((oldExpanded) => !oldExpanded) + return ( @@ -17,80 +22,50 @@ export const ManifestPage = () => {
    - - -
    +

    Манифест

    -
    -

    - Дискурс — независимый художественно-аналитический журнал с горизонтальной - редакцией, основанный на принципах свободы слова, прямой демократии и совместного - редактирования. Дискурс создаётся открытым медиасообществом ученых, журналистов, музыкантов, - писателей, предпринимателей, философов, инженеров, художников и специалистов - со всего мира, объединившихся, чтобы вместе делать общий журнал и объяснять - с разных точек зрения мозаичную картину современности. -

    -

    - Мы пишем о культуре, науке и обществе, рассказываем о новых идеях - и современном искусстве, публикуем статьи, исследования, репортажи, интервью людей, чью - прямую речь стоит услышать, и работы художников из разных стран — - от фильмов и музыки до живописи и фотографии. Помогая друг другу делать - публикации качественнее и общим голосованием выбирая лучшие материалы для журнала, - мы создаём новую горизонтальную журналистику, чтобы честно рассказывать о важном - и интересном. -

    -

    - Редакция Дискурса открыта для всех: у нас нет цензуры, запретных тем - и идеологических рамок. Каждый может прислать материал{' '} - в журнал и присоединиться к редакции. Предоставляя - трибуну для независимой журналистики и художественных проектов, мы помогаем людям - рассказывать свои истории так, чтобы они были услышаны. Мы убеждены: чем больше голосов - будет звучать на Дискурсе, тем громче в полифонии мнений будет слышна истина. -

    -
    +

    + Дискурс — независимый художественно-аналитический журнал с горизонтальной + редакцией, основанный на принципах свободы слова, прямой демократии и совместного + редактирования. Дискурс создаётся открытым медиасообществом ученых, журналистов, музыкантов, + писателей, предпринимателей, философов, инженеров, художников и специалистов + со всего мира, объединившихся, чтобы вместе делать общий журнал и объяснять + с разных точек зрения мозаичную картину современности. +

    +

    + Мы пишем о культуре, науке и обществе, рассказываем о новых идеях + и современном искусстве, публикуем статьи, исследования, репортажи, интервью людей, чью + прямую речь стоит услышать, и работы художников из разных стран — + от фильмов и музыки до живописи и фотографии. Помогая друг другу делать + публикации качественнее и общим голосованием выбирая лучшие материалы для журнала, + мы создаём новую горизонтальную журналистику, чтобы честно рассказывать о важном + и интересном. +

    +

    + Редакция Дискурса открыта для всех: у нас нет цензуры, запретных тем + и идеологических рамок. Каждый может прислать материал{' '} + в журнал и присоединиться к редакции. Предоставляя + трибуну для независимой журналистики и художественных проектов, мы помогаем людям + рассказывать свои истории так, чтобы они были услышаны. Мы убеждены: чем больше голосов + будет звучать на Дискурсе, тем громче в полифонии мнений будет слышна истина. +

    Как участвовать в самиздате

    -
    -

    - Дискурс создается открытым сообществом энтузиастов новой - независимой журналистики. Участвовать в открытой редакции и помогать журналу можно - следующими способами: -

    -

    Предлагать материалы

    +

    + Дискурс создается открытым сообществом энтузиастов новой + независимой журналистики. Участвовать в открытой редакции и помогать журналу можно + следующими способами: +

    +
    + +

    Предлагать материалы

    +

    Создавайте свои статьи и художественные работы — лучшие из них будут опубликованы в журнале. Дискурс — некоммерческое @@ -98,7 +73,12 @@ export const ManifestPage = () => { поддержку редакции, право голоса, множество других возможностей и читателей по всему миру.

    - +
    + +
    + + +

    Дискурс существует на пожертвования читателей. Если вам нравится журнал, пожалуйста,

    @@ -106,7 +86,12 @@ export const ManifestPage = () => { поддержите нашу работу. Ваши пожертвования пойдут на выпуск новых материалов, оплату серверов, труда программистов, дизайнеров и редакторов.

    -

    Сотрудничать с журналом

    +
    + +
    + +

    Сотрудничать с журналом

    +

    Мы всегда открыты для сотрудничества и рады единомышленникам. Если вы хотите помогать журналу с редактурой, корректурой, иллюстрациями, переводами, версткой, подкастами, @@ -125,7 +110,12 @@ export const ManifestPage = () => { и медиаинструментов находится{' '} в свободном доступе на GitHub.

    -

    Как еще можно помочь

    +
    + +
    + +

    Как еще можно помочь

    +

    Советуйте Дискурс друзьям и знакомым. Обсуждайте и распространяйте наши публикации — все материалы открытой редакции можно читать и перепечатывать @@ -141,19 +131,57 @@ export const ManifestPage = () => { интересными темами, о которых хотели бы узнать больше, и историями, которые нужно рассказать.

    -
    +

    Будем на связи

    -
    +

    Если вы хотите предложить материал, сотрудничать, рассказать о проблеме, которую нужно осветить, сообщить об ошибке или баге, что-то обсудить, уточнить или посоветовать, пожалуйста, напишите нам здесь или на почту{' '} welcome@discours.io. Мы обязательно ответим и постараемся реализовать все хорошие задумки. -

    +

    +
    + +
    diff --git a/src/components/Pages/about/PartnersPage.tsx b/src/components/Pages/about/PartnersPage.tsx index a9eef34d..e94e3994 100644 --- a/src/components/Pages/about/PartnersPage.tsx +++ b/src/components/Pages/about/PartnersPage.tsx @@ -8,8 +8,9 @@ export const PartnersPage = () => {
    -
    {t('Partners')}
    -
    +
    +

    {t('Partners')}

    +
    diff --git a/src/components/Pages/about/PrinciplesPage.tsx b/src/components/Pages/about/PrinciplesPage.tsx index 4691bbf1..ff4d8510 100644 --- a/src/components/Pages/about/PrinciplesPage.tsx +++ b/src/components/Pages/about/PrinciplesPage.tsx @@ -7,173 +7,168 @@ export const PrinciplesPage = () => {
    -
    +

    {title}

    -
    -
      -
    1. -

      - Горизонтальность. Мы все разные, и это классно. Вертикалей - в мире достаточно, мы — горизонтальное сообщество и ценим наши - различия, потому что знаем — в них наша сила. Благодаря разнообразию - сотен голосов, усиливающих друг друга, в сообществе складывается неповторимая - синергия, которая помогает вместе достигать большего. -

      -
    2. -
    3. -

      - Многоголосие. Мы ценим свободу слова и аргументированные - мнения. Предоставляя трибуну каждому, кому есть что сказать, самиздат отражает полифонию - позиций, знаний и опыта, которые открывают более полную картину реальности. -

      -
    4. -
    5. -

      - Взаимопомощь. Мы помогаем друг другу, потому что хотим, чтобы - в мире было еще больше хорошего. Обсуждая что-то, мы всегда интересуемся, чем - можем помочь. В самиздате можно найти специалистов практически в любых сферах - и получить поддержку от сотен людей. Благодаря коллективной экспертизе - глобального сообщества в самиздате выходят крутейшие публикации, которыми можно - вечно гордиться. -

      -
    6. -
    7. -

      - Взаимоуважение. Мы ценим, искренне уважаем друг друга - и вместо борщевиков враждебности культивируем цветы добра, мира, знания - и юмора. Нам некогда доказывать друг другу, кто круче. Гораздо приятнее - сотрудничать, помогать и создавать что-то важное, интересное и полезное. -

      -
    8. -
    9. -

      - Созидание. Мы создаем, потому что любим создавать. Мы открыто - делимся опытом, дарим идеи, обмениваемся мнениями и благодарим за критику, - используя ее для совершенствования мастерства и саморазвития. Мы знаем, - что мир не идеальное место, и делаем всё возможное, чтобы он стал лучше. -

      -
    10. -
    -
    +
      +
    1. +

      + Горизонтальность. Мы все разные, и это классно. Вертикалей + в мире достаточно, мы — горизонтальное сообщество и ценим наши + различия, потому что знаем — в них наша сила. Благодаря разнообразию сотен + голосов, усиливающих друг друга, в сообществе складывается неповторимая синергия, + которая помогает вместе достигать большего. +

      +
    2. +
    3. +

      + Многоголосие. Мы ценим свободу слова и аргументированные + мнения. Предоставляя трибуну каждому, кому есть что сказать, самиздат отражает полифонию + позиций, знаний и опыта, которые открывают более полную картину реальности. +

      +
    4. +
    5. +

      + Взаимопомощь. Мы помогаем друг другу, потому что хотим, чтобы + в мире было еще больше хорошего. Обсуждая что-то, мы всегда интересуемся, чем + можем помочь. В самиздате можно найти специалистов практически в любых сферах + и получить поддержку от сотен людей. Благодаря коллективной экспертизе + глобального сообщества в самиздате выходят крутейшие публикации, которыми можно вечно + гордиться. +

      +
    6. +
    7. +

      + Взаимоуважение. Мы ценим, искренне уважаем друг друга и вместо + борщевиков враждебности культивируем цветы добра, мира, знания и юмора. Нам некогда + доказывать друг другу, кто круче. Гораздо приятнее сотрудничать, помогать и создавать + что-то важное, интересное и полезное. +

      +
    8. +
    9. +

      + Созидание. Мы создаем, потому что любим создавать. Мы открыто + делимся опытом, дарим идеи, обмениваемся мнениями и благодарим за критику, + используя ее для совершенствования мастерства и саморазвития. Мы знаем, что + мир не идеальное место, и делаем всё возможное, чтобы он стал лучше. +

      +
    10. +

    Как участвовать в самиздате

    -
    -

    - Открытая редакция объединяет сотни потрясающих людей со всего мира, которые делают - крутейшие вещи. Это пространство, где доверяют, вдохновляют, исследуют и создают новое - вместе. Поскольку все в сообществе очень разные, как-то мы собрались и решили - зафиксировать базовые ценности открытой редакции, а заодно придумали универсальные - правила взаимодействия, чтобы общение было не только плодотворным, - но и приятным для всех участников сообщества. -

    -
      -
    1. -

      - Действуем, помогаем и делимся. В редакции мы создаем - свои проекты и помогаем другим создавать свои — советами, делом, - участием, вовлеченностью. Мы открыто делимся опытом, мнениями и идеями, потому - что ценим силу сотрудничества и знаем, что идеи реализуются скорее, лучше - и веселее, если над ними трудиться сообща. -

      -
    2. +

      + Открытая редакция объединяет сотни потрясающих людей со всего мира, которые делают + крутейшие вещи. Это пространство, где доверяют, вдохновляют, исследуют и создают новое + вместе. Поскольку все в сообществе очень разные, как-то мы собрались и решили + зафиксировать базовые ценности открытой редакции, а заодно придумали универсальные + правила взаимодействия, чтобы общение было не только плодотворным, + но и приятным для всех участников сообщества. +

      +
        +
      1. +

        + Действуем, помогаем и делимся. В редакции мы создаем свои + проекты и помогаем другим создавать свои — советами, делом, участием, + вовлеченностью. Мы открыто делимся опытом, мнениями и идеями, потому что ценим + силу сотрудничества и знаем, что идеи реализуются скорее, лучше и веселее, если + над ними трудиться сообща. +

        +
      2. -
      3. -

        - Общаемся дружелюбно. Помните, по ту сторону монитора - находятся реальные люди. Неуважение ранит других так же, как ранило бы вас - самих. Поэтому не стоит кричать (даже капслоком), заполнять эфир желчью - и бросаться грубостями — так вы рискуете не только растерять - доверие окружающих, но и остаться непонятым. -

        -
      4. +
      5. +

        + Общаемся дружелюбно. Помните, по ту сторону монитора находятся + реальные люди. Неуважение ранит других так же, как ранило бы вас самих. Поэтому + не стоит кричать (даже капслоком), заполнять эфир желчью и бросаться + грубостями — так вы рискуете не только растерять доверие окружающих, + но и остаться непонятым. +

        +
      6. -
      7. -

        - Критикуем и реагируем конструктивно. Самиздат про то, чтобы - разбираться в сложных вещах всем сообществом, поэтому мы тактично и без - агрессии делимся мнениями, стараясь убедительно аргументировать позиции. - И с благодарностью принимаем критику, используя ее для улучшения наших - проектов. Мы верим, что каждый участник сообщества имеет добрые намерения, - и придерживаемся принципов доброжелательной критики, стараемся делиться - советами — лучшим средством для самосовершенствования. Обоснованная критика - помогает и адресату, и всем участникам сообщества досконально изучить тему - и глубже разобраться в проблеме. -

        -
      8. +
      9. +

        + Критикуем и реагируем конструктивно. Самиздат про то, чтобы + разбираться в сложных вещах всем сообществом, поэтому мы тактично и без + агрессии делимся мнениями, стараясь убедительно аргументировать позиции. + И с благодарностью принимаем критику, используя ее для улучшения наших + проектов. Мы верим, что каждый участник сообщества имеет добрые намерения, + и придерживаемся принципов доброжелательной критики, стараемся делиться + советами — лучшим средством для самосовершенствования. Обоснованная критика + помогает и адресату, и всем участникам сообщества досконально изучить тему + и глубже разобраться в проблеме. +

        +
      10. -
      11. -

        - Решаем трудности не агрессией, а диалогом. Обесценивать - мнения и оскорблять других людей только потому, что вы с ними - не согласны, — не лучший способ донести свою точку зрения. Конечно, - важно высказаться, если вас что-то не устраивает и откровенно бесит. - Но прежде чем сжигать оппонента гневом, попробуйте понять, почему этот - «нехороший человек» так поступает. Возможно, аргументы собеседника окажутся - убедительными или вам удастся изменить его мнение. В любом случае конфликты - решаются в диалогах и проходят, а налаженное взаимопонимание останется - надолго. -

        -
      12. +
      13. +

        + Решаем трудности не агрессией, а диалогом. Обесценивать мнения + и оскорблять других людей только потому, что вы с ними + не согласны, — не лучший способ донести свою точку зрения. Конечно, + важно высказаться, если вас что-то не устраивает и откровенно бесит. + Но прежде чем сжигать оппонента гневом, попробуйте понять, почему этот + «нехороший человек» так поступает. Возможно, аргументы собеседника окажутся + убедительными или вам удастся изменить его мнение. В любом случае конфликты решаются + в диалогах и проходят, а налаженное взаимопонимание останется надолго. +

        +
      14. -
      15. -

        - Не переходим на личности — это признак токсичности - . Всегда мудрее обсуждать точку зрения человека, а не его самого, даже если - он вам не импонирует. Предвзятое отношение ограничивает кругозор, добавляет - преждевременные морщины и не помогает окружающим стать лучше. Вежливость - и взаимоуважение — краеугольная основа вдумчивых и осмысленных - дискуссий. -

        -
      16. +
      17. +

        + Не переходим на личности — это признак токсичности. + Всегда мудрее обсуждать точку зрения человека, а не его самого, даже если + он вам не импонирует. Предвзятое отношение ограничивает кругозор, добавляет + преждевременные морщины и не помогает окружающим стать лучше. Вежливость + и взаимоуважение — краеугольная основа вдумчивых и осмысленных + дискуссий. +

        +
      18. -
      19. -

        - Благодарим за помощь. Благодарите коллег даже за самые, - казалось бы, простые вещи. «Спасибо» не зря называют волшебным - словом — на искренней благодарности держится любое подлинное - сотрудничество. Поддержка воодушевляет на новые подвиги и напоминает, что мир - делают прекрасным не машины, а живые люди. -

        -
      20. +
      21. +

        + Благодарим за помощь. Благодарите коллег даже за самые, + казалось бы, простые вещи. «Спасибо» не зря называют волшебным + словом — на искренней благодарности держится любое подлинное + сотрудничество. Поддержка воодушевляет на новые подвиги и напоминает, что мир + делают прекрасным не машины, а живые люди. +

        +
      22. -
      23. -

        - Даем еще один шанс. Все совершают ошибки, и за один проступок - не стоит вычеркивать людей из жизни. Ошибки нужны, чтобы на них учиться - и делать выводы. Однако если многократно и систематически нарушать правила - сообщества, наверняка можно заслужить минусы в карму от других участников - и потерять доступ к сообществу. -

        -
      24. +
      25. +

        + Даем еще один шанс. Все совершают ошибки, и за один проступок + не стоит вычеркивать людей из жизни. Ошибки нужны, чтобы на них учиться + и делать выводы. Однако если многократно и систематически нарушать правила + сообщества, наверняка можно заслужить минусы в карму от других участников + и потерять доступ к сообществу. +

        +
      26. -
      27. -

        - Вместе создаем идеальную среду общения. Открытая редакция — - это утопическое пространство обогащающей и осмысленной коммуникации. Атмосфера - горизонтального сообщества складывается из действий каждого, поэтому - мы действуем так, чтобы способствовать сотворчеству, коллективному познанию - и развитию самиздата и нашей альтернативной интеллектуальной медиасреды. -

        -
      28. +
      29. +

        + Вместе создаем идеальную среду общения. Открытая редакция — + это утопическое пространство обогащающей и осмысленной коммуникации. Атмосфера + горизонтального сообщества складывается из действий каждого, поэтому + мы действуем так, чтобы способствовать сотворчеству, коллективному познанию + и развитию самиздата и нашей альтернативной интеллектуальной медиасреды. +

        +
      30. -
      31. -

        - Помним, что всё в сообществе зависит от нас. Если нам чего-то - не хватает, мы начинаем действовать — рассказываем об идее, - находим единомышленников, готовим и запускаем проект. Так в сообществе - становится на одну крутую активность больше. Так появилось наше сообщество. Так - появился самиздат и все проекты открытой редакции. Чтобы в сообществе - случилось что-то прекрасное, достаточно просто положить этому начало. -

        -
      32. -
      -
    +
  • +

    + Помним, что всё в сообществе зависит от нас. Если нам чего-то + не хватает, мы начинаем действовать — рассказываем об идее, + находим единомышленников, готовим и запускаем проект. Так в сообществе + становится на одну крутую активность больше. Так появилось наше сообщество. Так + появился самиздат и все проекты открытой редакции. Чтобы в сообществе случилось + что-то прекрасное, достаточно просто положить этому начало. +

    +
  • +
    diff --git a/src/components/Pages/about/ProjectsPage.tsx b/src/components/Pages/about/ProjectsPage.tsx index 37a3e251..7c625ef5 100644 --- a/src/components/Pages/about/ProjectsPage.tsx +++ b/src/components/Pages/about/ProjectsPage.tsx @@ -8,8 +8,9 @@ export const ProjectsPage = () => {
    -
    {t('Projects')}
    -
    +
    +

    {t('Projects')}

    +
    diff --git a/src/components/Pages/about/TermsOfUsePage.tsx b/src/components/Pages/about/TermsOfUsePage.tsx index a8b20121..942fb6f0 100644 --- a/src/components/Pages/about/TermsOfUsePage.tsx +++ b/src/components/Pages/about/TermsOfUsePage.tsx @@ -1,8 +1,13 @@ +import { createSignal, Show } from 'solid-js' import { MainLayout } from '../../Layouts/MainLayout' // const title = t('Terms of use') export const TermsOfUsePage = () => { + const [indexExpanded, setIndexExpanded] = createSignal(false) + + const toggleIndexExpanded = () => setIndexExpanded((oldExpanded) => !oldExpanded) + return ( {/**/} @@ -11,254 +16,255 @@ export const TermsOfUsePage = () => { {/**/}
    - - -
    +

    Пользовательское соглашение

    -
    -

    - Дискурс — это сообщество творческих людей, объединенных идеей делать интересный - журнал для всех желающих. Авторы Дискурса сообща посредством прямого голосования определяют - содержание журнала. -

    -

    Для того, чтобы Дискурс работал без помех, разработаны настоящие Правила.

    -

    Определения

    -

    - Сайт — портал discours.io -

    -

    - Пользователь — лицо, пользующееся Сайтом, либо юридическое лицо, - обладающее правами на интеллектуальную собственность. -

    -

    - Публикация контента — размещение Пользователем посредством Сайта - объектов авторских прав и другой информации для других пользователей. -

    -

    - Издательство — администрация сайта, которая занимается - технической и издательской деятельностью для обеспечения функционирования Сайта - и Альманаха. Издательство не вмешивается в принятие редакционных решений - авторским сообществом. -

    -

    - Альманах «Дискурс» (свидетельство о регистрации СМИ: ПИ - № ФС77-63947 от 18.12.15) — печатное периодическое издание, которое - выходит раз в год и состоит из лучших публикаций на Сайте за это - время. -

    - -
      -
    1. -

      - Вся информация на сайте (включая тексты, изображения, видеоматериалы, аудиозаписи, - программный код, дизайн сайта и т.д.) является объектом интеллектуальной - собственности ее правообладателей и охраняется законодательством РФ. -

      -
    2. -
    3. -

      - Публикуя контент на сайте, Пользователь на безвозмездной основе предоставляет - Издательству право на воспроизведение, распространение, перевод, редактирование - контента. Данное право предоставляется Издательству на весь срок действия авторских - прав Пользователя. -

      -
    4. -
    5. -

      - Пользователь предоставляет Издательству право редактировать контент, в том числе - вносить в него изменения, сокращения и дополнения, снабжать его иллюстрациями - и пояснениями, исправлять ошибки и уточнять фактические сведения, при условии, - что этим не искажается авторский замысел. -

      -
    6. -
    7. -

      - Обнародование контента осуществляется Издательством в соответствии с условиями - лицензии{' '} - - Creative Commons BY-NC-ND 4.0 - - . Все материалы сайта предназначены исключительно для личного некоммерческого - использования. Права на дизайн и программный код сайта принадлежат - Издательству. -

      -
    8. -
    9. -

      - Все аудиовизуальные произведения являются собственностью своих авторов - и правообладателей и используются только в образовательных - и информационных целях. Если вы являетесь собственником того или иного - произведения и не согласны с его размещением на сайте, пожалуйста, - напишите на  - - welcome@discours.io - - . -

      -
    10. -
    11. -

      - Цитирование, распространение, доведение до всеобщего сведения материалов Cайта - приветствуется. При использовании материалов сайта необходимо указать имя автора - и активную ссылку на материал на Сайте. -

      -
    12. -
    -

    Правила поведения

    -
      -
    1. -

      - Находясь на Сайте, Пользователь подтверждает свое совершеннолетие, - правоспособность, а также согласие с настоящими Правилами и политикой - конфиденциальности и готовность нести полную ответственность за их соблюдение. -

      -
    2. -
    3. -

      На сайте запрещено:

      -
        -
      • - Публиковать контент, авторские права на который принадлежат третьим лицам, без - согласия этих лиц. Если авторские права на контент принадлежат нескольким лицам, - то его публикация предполагает согласие их всех. -
      • -
      • Размещать коммерческую и политическую рекламу.
      • -
      • - Целенаправленно препятствовать нормальному функционированию сообщества и сайта - discours.io -
      • -
      • Выдавать себя за другого человека и представляться его именем.
      • -
      • - Размещать информацию, которая не соответствует целям создания Сайта, ущемляет - интересы других пользователей или третьих лиц, нарушает законы Российской Федерации. -
      • -
      -
    4. -
    5. -

      - Пользователь несет всю ответственность за содержание публикуемого контента - и свое взаимодействие с другими пользователями, и обязуется возместить - все расходы в случае предъявления каких-либо претензий третьими лицами. - Издательство не несет ответственности за содержание публикуемой пользователями - информации, в том числе за размещенные на сайте комментарии. Переписка - между Пользователем и Издательством считается юридически значимой. Настоящие - Правила могут быть изменены Издательством, изменения вступают в силу с момента - публикации на Сайте. -

      -
    6. -
    7. -

      - Если Пользователь очевидно и целенаправленно нарушает правила, Издательство может - и принять в отношении автора следующие меры: вынести предупреждение - и обязать автора устранить допущенное нарушение, удалить контент, нарушающий - правила, заблокировать или удалить аккаунт нарушителя. -

      -
    8. -
    -

    Политика конфиденциальности

    -
      -
    1. -

      Сайт может собирать у пользователей следующие данные:

      -
        -
      • -

        - Данные, которые пользователи сообщают о себе сами при подаче заявки, - регистрации, авторизации или заполнения профиля, в том числе ФИО - и контактную информацию. Конфиденциальные данные, такие как идентификатор - и электронный адрес, используются для идентификации пользователя. Данные - профиля, размещённые публично по желанию пользователя, которое выражается - фактом их предоставления, используется для демонстрации другим пользователям - той информации о себе, которую пользователь готов предоставить. -

        -
      • -
      • -

        - Данные, собранные автоматическим путем, такие, как cookie-файлы. Эти - неперсонализированные данные могут использоваться для сбора статистики - и улучшения работы сайта. -

        -
      • -
      -
    2. -
    3. -

      - Издательство обеспечивает конфиденциальность персональных данных и применяет все - необходимые организационные и технические меры по их защите. -

      -
    4. -
    5. -

      - По желанию пользователя Издательство готово удалить любую информацию о нем, - собранную автоматическим путем. Для этого следует написать на адрес электронной - почты{' '} - - welcome@discours.io - - . -

      -
    6. -
    7. -

      - Если в информации, предоставляемой Издательству Пользователем, содержатся - персональные данные последнего, то фактом их предоставления он соглашается - на их обработку любым способом, не запрещенным законодательством РФ. -

      -

      - Общедоступные видео на сайте могут транслироваться с YouTube - и регулируются{' '} - - политикой конфиденциальности Google - - . Загрузка видео на сайт также означает согласие с  - - Условиями использования YouTube - - . -

      -
    8. -
    9. -

      - Данные, которые мы получаем от вас, мы используем только - в соответствии с принципами обработки данных, указанными в этом - документе. -

      -
    10. -
    -

    Обратная связь

    -

    - Любые вопросы и предложения по поводу функционирования сайта можно направить - по электронной почте{' '} - - welcome@discours.io - {' '} - или через форму «предложить идею». -

    -
    +

    + Дискурс — это сообщество творческих людей, объединенных идеей делать интересный + журнал для всех желающих. Авторы Дискурса сообща посредством прямого голосования определяют + содержание журнала. +

    +

    Для того, чтобы Дискурс работал без помех, разработаны настоящие Правила.

    +

    Определения

    +

    + Сайт — портал discours.io +

    +

    + Пользователь — лицо, пользующееся Сайтом, либо юридическое лицо, + обладающее правами на интеллектуальную собственность. +

    +

    + Публикация контента — размещение Пользователем посредством Сайта + объектов авторских прав и другой информации для других пользователей. +

    +

    + Издательство — администрация сайта, которая занимается технической + и издательской деятельностью для обеспечения функционирования Сайта и Альманаха. + Издательство не вмешивается в принятие редакционных решений авторским сообществом. +

    +

    + Альманах «Дискурс» (свидетельство о регистрации СМИ: ПИ + № ФС77-63947 от 18.12.15) — печатное периодическое издание, которое + выходит раз в год и состоит из лучших публикаций на Сайте за это + время. +

    + +
      +
    1. +

      + Вся информация на сайте (включая тексты, изображения, видеоматериалы, аудиозаписи, + программный код, дизайн сайта и т.д.) является объектом интеллектуальной + собственности ее правообладателей и охраняется законодательством РФ. +

      +
    2. +
    3. +

      + Публикуя контент на сайте, Пользователь на безвозмездной основе предоставляет + Издательству право на воспроизведение, распространение, перевод, редактирование + контента. Данное право предоставляется Издательству на весь срок действия авторских + прав Пользователя. +

      +
    4. +
    5. +

      + Пользователь предоставляет Издательству право редактировать контент, в том числе + вносить в него изменения, сокращения и дополнения, снабжать его иллюстрациями + и пояснениями, исправлять ошибки и уточнять фактические сведения, при условии, + что этим не искажается авторский замысел. +

      +
    6. +
    7. +

      + Обнародование контента осуществляется Издательством в соответствии с условиями + лицензии{' '} + + Creative Commons BY-NC-ND 4.0 + + . Все материалы сайта предназначены исключительно для личного некоммерческого + использования. Права на дизайн и программный код сайта принадлежат Издательству. +

      +
    8. +
    9. +

      + Все аудиовизуальные произведения являются собственностью своих авторов + и правообладателей и используются только в образовательных + и информационных целях. Если вы являетесь собственником того или иного + произведения и не согласны с его размещением на сайте, пожалуйста, + напишите на  + + welcome@discours.io + + . +

      +
    10. +
    11. +

      + Цитирование, распространение, доведение до всеобщего сведения материалов Cайта + приветствуется. При использовании материалов сайта необходимо указать имя автора + и активную ссылку на материал на Сайте. +

      +
    12. +
    +

    Правила поведения

    +
      +
    1. +

      + Находясь на Сайте, Пользователь подтверждает свое совершеннолетие, правоспособность, + а также согласие с настоящими Правилами и политикой конфиденциальности + и готовность нести полную ответственность за их соблюдение. +

      +
    2. +
    3. +

      На сайте запрещено:

      +
        +
      • + Публиковать контент, авторские права на который принадлежат третьим лицам, без + согласия этих лиц. Если авторские права на контент принадлежат нескольким лицам, + то его публикация предполагает согласие их всех. +
      • +
      • Размещать коммерческую и политическую рекламу.
      • +
      • + Целенаправленно препятствовать нормальному функционированию сообщества и сайта + discours.io +
      • +
      • Выдавать себя за другого человека и представляться его именем.
      • +
      • + Размещать информацию, которая не соответствует целям создания Сайта, ущемляет + интересы других пользователей или третьих лиц, нарушает законы Российской Федерации. +
      • +
      +
    4. +
    5. +

      + Пользователь несет всю ответственность за содержание публикуемого контента + и свое взаимодействие с другими пользователями, и обязуется возместить все + расходы в случае предъявления каких-либо претензий третьими лицами. Издательство + не несет ответственности за содержание публикуемой пользователями информации, + в том числе за размещенные на сайте комментарии. Переписка между + Пользователем и Издательством считается юридически значимой. Настоящие Правила могут + быть изменены Издательством, изменения вступают в силу с момента публикации + на Сайте. +

      +
    6. +
    7. +

      + Если Пользователь очевидно и целенаправленно нарушает правила, Издательство может + и принять в отношении автора следующие меры: вынести предупреждение + и обязать автора устранить допущенное нарушение, удалить контент, нарушающий правила, + заблокировать или удалить аккаунт нарушителя. +

      +
    8. +
    +

    Политика конфиденциальности

    +
      +
    1. +

      Сайт может собирать у пользователей следующие данные:

      +
        +
      • +

        + Данные, которые пользователи сообщают о себе сами при подаче заявки, регистрации, + авторизации или заполнения профиля, в том числе ФИО и контактную информацию. + Конфиденциальные данные, такие как идентификатор и электронный адрес, + используются для идентификации пользователя. Данные профиля, размещённые публично + по желанию пользователя, которое выражается фактом их предоставления, + используется для демонстрации другим пользователям той информации о себе, которую + пользователь готов предоставить. +

        +
      • +
      • +

        + Данные, собранные автоматическим путем, такие, как cookie-файлы. Эти + неперсонализированные данные могут использоваться для сбора статистики + и улучшения работы сайта. +

        +
      • +
      +
    2. +
    3. +

      + Издательство обеспечивает конфиденциальность персональных данных и применяет все + необходимые организационные и технические меры по их защите. +

      +
    4. +
    5. +

      + По желанию пользователя Издательство готово удалить любую информацию о нем, + собранную автоматическим путем. Для этого следует написать на адрес электронной почты{' '} + + welcome@discours.io + + . +

      +
    6. +
    7. +

      + Если в информации, предоставляемой Издательству Пользователем, содержатся + персональные данные последнего, то фактом их предоставления он соглашается + на их обработку любым способом, не запрещенным законодательством РФ. +

      +

      + Общедоступные видео на сайте могут транслироваться с YouTube и регулируются{' '} + + политикой конфиденциальности Google + + . Загрузка видео на сайт также означает согласие с  + + Условиями использования YouTube + + . +

      +
    8. +
    9. +

      + Данные, которые мы получаем от вас, мы используем только + в соответствии с принципами обработки данных, указанными в этом документе. +

      +
    10. +
    +

    Обратная связь

    +

    + Любые вопросы и предложения по поводу функционирования сайта можно направить + по электронной почте{' '} + + welcome@discours.io + {' '} + или через форму «предложить идею». +

    +
    + +
    diff --git a/src/components/Pages/about/ThanksPage.tsx b/src/components/Pages/about/ThanksPage.tsx index 20ad2f9c..7b9a5f8b 100644 --- a/src/components/Pages/about/ThanksPage.tsx +++ b/src/components/Pages/about/ThanksPage.tsx @@ -12,12 +12,10 @@ export const ThanksPage = () => {
    -
    +

    {title}

    -
    -
    {/*

    Команда

    diff --git a/src/styles/app.scss b/src/styles/app.scss index fff35a04..40a9971b 100644 --- a/src/styles/app.scss +++ b/src/styles/app.scss @@ -92,6 +92,7 @@ h2 { line-height: 1.1; margin-bottom: 0.5em; + margin-top: 1.5em; } h3 { @@ -228,6 +229,7 @@ button { background: #fff; border: 2px solid #000; border-radius: 0.8rem; + color: #000; &:hover { background: #000; @@ -642,3 +644,13 @@ astro-island { padding: 0.6em 1.5em; } } + +details { + margin-bottom: 1.5em; + + summary h3 { + display: inline-block; + cursor: pointer; + margin-bottom: 0; + } +} From 54d4c4d0550d233b1499350b89d3c809b3902724 Mon Sep 17 00:00:00 2001 From: kvakazyambra Date: Tue, 8 Nov 2022 09:41:50 +0300 Subject: [PATCH 07/34] About pages fixed grid --- .../Pages/about/DiscussionRulesPage.tsx | 2 +- src/components/Pages/about/DogmaPage.tsx | 2 +- src/components/Pages/about/GuidePage.tsx | 68 ++++++++-------- src/components/Pages/about/HelpPage.tsx | 56 ++++++------- src/components/Pages/about/ManifestPage.tsx | 78 +++++++++---------- src/components/Pages/about/PartnersPage.tsx | 2 +- src/components/Pages/about/PrinciplesPage.tsx | 2 +- src/components/Pages/about/ProjectsPage.tsx | 2 +- src/components/Pages/about/TermsOfUsePage.tsx | 68 ++++++++-------- src/components/Pages/about/ThanksPage.tsx | 2 +- src/styles/app.scss | 6 +- 11 files changed, 145 insertions(+), 143 deletions(-) diff --git a/src/components/Pages/about/DiscussionRulesPage.tsx b/src/components/Pages/about/DiscussionRulesPage.tsx index 23797f7e..0ec5105a 100644 --- a/src/components/Pages/about/DiscussionRulesPage.tsx +++ b/src/components/Pages/about/DiscussionRulesPage.tsx @@ -7,7 +7,7 @@ export const DiscussionRulesPage = () => {

    -
    +

    diff --git a/src/components/Pages/about/DogmaPage.tsx b/src/components/Pages/about/DogmaPage.tsx index 86730bd6..061432ed 100644 --- a/src/components/Pages/about/DogmaPage.tsx +++ b/src/components/Pages/about/DogmaPage.tsx @@ -7,7 +7,7 @@ export const DogmaPage = () => {
    -
    +

    Редакционные принципы

    Дискурс - журнал с открытой горизонтальной редакцией. Содержание журнала определяется прямым diff --git a/src/components/Pages/about/GuidePage.tsx b/src/components/Pages/about/GuidePage.tsx index d5bb6793..15366782 100644 --- a/src/components/Pages/about/GuidePage.tsx +++ b/src/components/Pages/about/GuidePage.tsx @@ -21,7 +21,40 @@ export const GuidePage = () => {

    -
    + + +

    Как устроен Дискурс

    @@ -242,39 +275,6 @@ export const GuidePage = () => { . Мы обязательно ответим.

    - -
    diff --git a/src/components/Pages/about/HelpPage.tsx b/src/components/Pages/about/HelpPage.tsx index 3149bc0a..d96355fa 100644 --- a/src/components/Pages/about/HelpPage.tsx +++ b/src/components/Pages/about/HelpPage.tsx @@ -18,7 +18,34 @@ export const HelpPage = () => {
    -
    + + +

    Как вы можете поддержать Дискурс?

    @@ -126,33 +153,6 @@ export const HelpPage = () => { .

    - -
    diff --git a/src/components/Pages/about/ManifestPage.tsx b/src/components/Pages/about/ManifestPage.tsx index 1a05df50..bcbd7a69 100644 --- a/src/components/Pages/about/ManifestPage.tsx +++ b/src/components/Pages/about/ManifestPage.tsx @@ -22,7 +22,45 @@ export const ManifestPage = () => {
    -
    + + +

    Манифест

    @@ -145,44 +183,6 @@ export const ManifestPage = () => { и постараемся реализовать все хорошие задумки.

    - -
    diff --git a/src/components/Pages/about/PartnersPage.tsx b/src/components/Pages/about/PartnersPage.tsx index e94e3994..410ba866 100644 --- a/src/components/Pages/about/PartnersPage.tsx +++ b/src/components/Pages/about/PartnersPage.tsx @@ -8,7 +8,7 @@ export const PartnersPage = () => {
    -
    +

    {t('Partners')}

    diff --git a/src/components/Pages/about/PrinciplesPage.tsx b/src/components/Pages/about/PrinciplesPage.tsx index ff4d8510..7260b5cf 100644 --- a/src/components/Pages/about/PrinciplesPage.tsx +++ b/src/components/Pages/about/PrinciplesPage.tsx @@ -7,7 +7,7 @@ export const PrinciplesPage = () => {
    -
    +

    {title}

    diff --git a/src/components/Pages/about/ProjectsPage.tsx b/src/components/Pages/about/ProjectsPage.tsx index 7c625ef5..45c4b0e1 100644 --- a/src/components/Pages/about/ProjectsPage.tsx +++ b/src/components/Pages/about/ProjectsPage.tsx @@ -8,7 +8,7 @@ export const ProjectsPage = () => {
    -
    +

    {t('Projects')}

    diff --git a/src/components/Pages/about/TermsOfUsePage.tsx b/src/components/Pages/about/TermsOfUsePage.tsx index 942fb6f0..7577bd3c 100644 --- a/src/components/Pages/about/TermsOfUsePage.tsx +++ b/src/components/Pages/about/TermsOfUsePage.tsx @@ -16,7 +16,40 @@ export const TermsOfUsePage = () => { {/**/}
    -
    + + +

    Пользовательское соглашение

    @@ -233,39 +266,6 @@ export const TermsOfUsePage = () => { или через форму «предложить идею».

    - -
    diff --git a/src/components/Pages/about/ThanksPage.tsx b/src/components/Pages/about/ThanksPage.tsx index 7b9a5f8b..b516e4c6 100644 --- a/src/components/Pages/about/ThanksPage.tsx +++ b/src/components/Pages/about/ThanksPage.tsx @@ -12,7 +12,7 @@ export const ThanksPage = () => {
    -
    +

    {title}

    diff --git a/src/styles/app.scss b/src/styles/app.scss index 40a9971b..1372872d 100644 --- a/src/styles/app.scss +++ b/src/styles/app.scss @@ -3,7 +3,7 @@ @import 'bootstrap/scss/mixins/utilities'; @import 'bootstrap/scss/containers'; @import 'bootstrap/scss/grid'; -@import 'bootstrap/scss/utilities'; +@import 'bootstrap/scss/bootstrap-utilities'; :root { --background-color: #fff; @@ -586,7 +586,9 @@ astro-island { } .container--static-page { - padding-top: 1.5em; + @include media-breakpoint-up(md) { + padding-top: 1.5em; + } } .shift-content { From 1b84f51f50bc4af668822cb6befe86dd1de63a42 Mon Sep 17 00:00:00 2001 From: kvakazyambra Date: Tue, 8 Nov 2022 22:48:00 +0300 Subject: [PATCH 08/34] About pages style fixes --- public/icons/expand.svg | 2 + .../Pages/about/DiscussionRulesPage.tsx | 2 +- src/components/Pages/about/DogmaPage.tsx | 2 +- src/components/Pages/about/GuidePage.tsx | 4 +- src/components/Pages/about/HelpPage.tsx | 4 +- src/components/Pages/about/ManifestPage.tsx | 4 +- src/components/Pages/about/PartnersPage.tsx | 2 +- src/components/Pages/about/PrinciplesPage.tsx | 2 +- src/components/Pages/about/ProjectsPage.tsx | 2 +- src/components/Pages/about/TermsOfUsePage.tsx | 4 +- src/components/Pages/about/ThanksPage.tsx | 2 +- src/styles/app.scss | 37 +++++++++++++++++-- 12 files changed, 49 insertions(+), 18 deletions(-) create mode 100644 public/icons/expand.svg diff --git a/public/icons/expand.svg b/public/icons/expand.svg new file mode 100644 index 00000000..fcbcd21c --- /dev/null +++ b/public/icons/expand.svg @@ -0,0 +1,2 @@ + + diff --git a/src/components/Pages/about/DiscussionRulesPage.tsx b/src/components/Pages/about/DiscussionRulesPage.tsx index 0ec5105a..832c1c0e 100644 --- a/src/components/Pages/about/DiscussionRulesPage.tsx +++ b/src/components/Pages/about/DiscussionRulesPage.tsx @@ -7,7 +7,7 @@ export const DiscussionRulesPage = () => {
    -
    +

    diff --git a/src/components/Pages/about/DogmaPage.tsx b/src/components/Pages/about/DogmaPage.tsx index 061432ed..cf764cd4 100644 --- a/src/components/Pages/about/DogmaPage.tsx +++ b/src/components/Pages/about/DogmaPage.tsx @@ -7,7 +7,7 @@ export const DogmaPage = () => {
    -
    +

    Редакционные принципы

    Дискурс - журнал с открытой горизонтальной редакцией. Содержание журнала определяется прямым diff --git a/src/components/Pages/about/GuidePage.tsx b/src/components/Pages/about/GuidePage.tsx index 15366782..3b55056f 100644 --- a/src/components/Pages/about/GuidePage.tsx +++ b/src/components/Pages/about/GuidePage.tsx @@ -21,7 +21,7 @@ export const GuidePage = () => {

    -
    +

    -
    +

    Как устроен Дискурс

    diff --git a/src/components/Pages/about/HelpPage.tsx b/src/components/Pages/about/HelpPage.tsx index d96355fa..6e0275bd 100644 --- a/src/components/Pages/about/HelpPage.tsx +++ b/src/components/Pages/about/HelpPage.tsx @@ -18,7 +18,7 @@ export const HelpPage = () => {
    -
    +

    -
    +

    Как вы можете поддержать Дискурс?

    diff --git a/src/components/Pages/about/ManifestPage.tsx b/src/components/Pages/about/ManifestPage.tsx index bcbd7a69..4b42465e 100644 --- a/src/components/Pages/about/ManifestPage.tsx +++ b/src/components/Pages/about/ManifestPage.tsx @@ -22,7 +22,7 @@ export const ManifestPage = () => {
    -
    +

    -
    +

    Манифест

    diff --git a/src/components/Pages/about/PartnersPage.tsx b/src/components/Pages/about/PartnersPage.tsx index 410ba866..3db7c4eb 100644 --- a/src/components/Pages/about/PartnersPage.tsx +++ b/src/components/Pages/about/PartnersPage.tsx @@ -8,7 +8,7 @@ export const PartnersPage = () => {
    -
    +

    {t('Partners')}

    diff --git a/src/components/Pages/about/PrinciplesPage.tsx b/src/components/Pages/about/PrinciplesPage.tsx index 7260b5cf..d22cb77a 100644 --- a/src/components/Pages/about/PrinciplesPage.tsx +++ b/src/components/Pages/about/PrinciplesPage.tsx @@ -7,7 +7,7 @@ export const PrinciplesPage = () => {
    -
    +

    {title}

    diff --git a/src/components/Pages/about/ProjectsPage.tsx b/src/components/Pages/about/ProjectsPage.tsx index 45c4b0e1..f78c25ff 100644 --- a/src/components/Pages/about/ProjectsPage.tsx +++ b/src/components/Pages/about/ProjectsPage.tsx @@ -8,7 +8,7 @@ export const ProjectsPage = () => {
    -
    +

    {t('Projects')}

    diff --git a/src/components/Pages/about/TermsOfUsePage.tsx b/src/components/Pages/about/TermsOfUsePage.tsx index 7577bd3c..9ea7e9bc 100644 --- a/src/components/Pages/about/TermsOfUsePage.tsx +++ b/src/components/Pages/about/TermsOfUsePage.tsx @@ -16,7 +16,7 @@ export const TermsOfUsePage = () => { {/**/}
    -
    +

    -
    +

    Пользовательское соглашение

    diff --git a/src/components/Pages/about/ThanksPage.tsx b/src/components/Pages/about/ThanksPage.tsx index b516e4c6..6f6938d1 100644 --- a/src/components/Pages/about/ThanksPage.tsx +++ b/src/components/Pages/about/ThanksPage.tsx @@ -12,7 +12,7 @@ export const ThanksPage = () => {
    -
    +

    {title}

    diff --git a/src/styles/app.scss b/src/styles/app.scss index 1372872d..0b63000e 100644 --- a/src/styles/app.scss +++ b/src/styles/app.scss @@ -75,6 +75,7 @@ h2 { .wrapped { background: #000; color: #fff; + margin-left: -0.15em; padding: 0 0.15em; box-decoration-break: clone; -webkit-box-decoration-break: clone; @@ -650,9 +651,37 @@ astro-island { details { margin-bottom: 1.5em; - summary h3 { - display: inline-block; - cursor: pointer; - margin-bottom: 0; + summary { + display: block; + position: relative; + + &::marker { + display: none; + } + + h3 { + display: inline-block; + cursor: pointer; + margin-bottom: 0; + + &:before { + content: ''; + background: url(/icons/expand.svg) no-repeat; + background-size: contain; + height: 1.3rem; + margin-right: 0.5em; + position: absolute; + right: 100%; + top: 0.35em; + transition: transform 0.3s; + width: 2rem; + } + } + } + + &[open] { + h3:before { + transform: rotate(180deg); + } } } From c9958adadc2b21dcd3091ed237879fdeb3e6232b Mon Sep 17 00:00:00 2001 From: kvakazyambra Date: Wed, 9 Nov 2022 22:02:12 +0300 Subject: [PATCH 09/34] Fixed topics page style --- src/components/Topic/Card.module.scss | 19 +++-- src/components/Topic/Card.tsx | 22 ++--- src/components/Views/AllAuthors.tsx | 13 +-- src/components/Views/AllTopics.tsx | 80 ++++++++++--------- .../{AllTopics.scss => AllTopics.module.scss} | 10 ++- src/styles/app.scss | 34 ++++++-- 6 files changed, 111 insertions(+), 67 deletions(-) rename src/styles/{AllTopics.scss => AllTopics.module.scss} (84%) diff --git a/src/components/Topic/Card.module.scss b/src/components/Topic/Card.module.scss index 801f7eaa..bbfab772 100644 --- a/src/components/Topic/Card.module.scss +++ b/src/components/Topic/Card.module.scss @@ -9,10 +9,6 @@ .topicDetailsItem { margin-bottom: 1.2rem; - - @include media-breakpoint-up(md) { - margin-bottom: 3.2rem; - } } } } @@ -33,9 +29,9 @@ .topicTitle { font-weight: bold; - @include font-size(1.7rem); + @include font-size(2.2rem); - margin-bottom: 0.8rem; + margin-bottom: 1.2rem; } .topicAvatar { @@ -56,10 +52,10 @@ } .topicDescription { - @include font-size(1.5rem); + @include font-size(1.6rem); color: #696969; - margin: 0 0 0.8rem; + margin: 0 0 1.6rem; &.compact { font-size: medium; @@ -85,6 +81,7 @@ } .topicDetailsItem { + @include font-size(1.5rem); margin-right: 1.6rem; white-space: nowrap; @@ -104,3 +101,9 @@ float: right; } } + +.controlContainer { + @include media-breakpoint-up(md) { + text-align: right; + } +} diff --git a/src/components/Topic/Card.tsx b/src/components/Topic/Card.tsx index da9d6d3f..16594b58 100644 --- a/src/components/Topic/Card.tsx +++ b/src/components/Topic/Card.tsx @@ -8,6 +8,7 @@ import { locale } from '../../stores/ui' import { useAuthStore } from '../../stores/auth' import { follow, unfollow } from '../../stores/zine/common' import { getLogger } from '../../utils/logger' +import { clsx } from 'clsx' const log = getLogger('TopicCard') @@ -49,11 +50,11 @@ export const TopicCard = (props: TopicProps) => { [styles.topicInRow]: props.isTopicInRow }} > -
    +
    - +
    @@ -65,7 +66,7 @@ export const TopicCard = (props: TopicProps) => {
    {props.topic.body} @@ -75,7 +76,7 @@ export const TopicCard = (props: TopicProps) => {
    - + {props.topic.stat?.shouts + ' ' + t('post') + @@ -84,7 +85,7 @@ export const TopicCard = (props: TopicProps) => { locale() === 'ru' ? ['ов', '', 'а'] : ['s', '', 's'] )} - + {props.topic.stat?.authors + ' ' + t('author') + @@ -127,21 +128,24 @@ export const TopicCard = (props: TopicProps) => {
    -
    +
    subscribe(true)} class="button--light button--subscribe-topic"> + - + {t('Follow')} + {t('Follow')} } >
    diff --git a/src/components/Views/AllAuthors.tsx b/src/components/Views/AllAuthors.tsx index 7c4ad362..070738ba 100644 --- a/src/components/Views/AllAuthors.tsx +++ b/src/components/Views/AllAuthors.tsx @@ -6,7 +6,8 @@ import { t } from '../../utils/intl' import { useAuthorsStore, setAuthorsSort } from '../../stores/zine/authors' import { handleClientRouteLinkClick, useRouter } from '../../stores/router' import { useAuthStore } from '../../stores/auth' -import '../../styles/AllTopics.scss' +import styles from '../../styles/AllTopics.module.scss' +import { clsx } from 'clsx' type AllAuthorsPageSearchParams = { by: '' | 'name' | 'shouts' | 'rating' @@ -56,19 +57,19 @@ export const AllAuthorsView = (props: Props) => { // log.debug(getSearchParams()) return ( -
    +
    0}>
    -
    -
      +
      • {t('By shouts')} @@ -111,13 +112,13 @@ export const AllAuthorsView = (props: Props) => { > {(letter) => ( -
        +

        {letter}

        {(author: Author) => ( -
        +
        diff --git a/src/components/Views/AllTopics.tsx b/src/components/Views/AllTopics.tsx index 91f28e90..618811a4 100644 --- a/src/components/Views/AllTopics.tsx +++ b/src/components/Views/AllTopics.tsx @@ -6,7 +6,9 @@ import { setTopicsSort, useTopicsStore } from '../../stores/zine/topics' import { handleClientRouteLinkClick, useRouter } from '../../stores/router' import { TopicCard } from '../Topic/Card' import { useAuthStore } from '../../stores/auth' -import '../../styles/AllTopics.scss' +import styles from '../../styles/AllTopics.module.scss' +import cardStyles from '../Topic/Card.module.scss' +import { clsx } from 'clsx' type AllTopicsPageSearchParams = { by: 'shouts' | 'authors' | 'title' | '' @@ -52,54 +54,58 @@ export const AllTopicsView = (props: AllTopicsViewProps) => { const subscribed = (s) => Boolean(session()?.news?.topics && session()?.news?.topics?.includes(s || '')) return ( -
        +
        0}>
        -
        - + ( -
        +
        {(topic) => ( @@ -110,13 +116,13 @@ export const AllTopicsView = (props: AllTopicsViewProps) => { > {(letter) => ( -
        +

        {letter}

        {(topic) => ( -
        +
        diff --git a/src/styles/AllTopics.scss b/src/styles/AllTopics.module.scss similarity index 84% rename from src/styles/AllTopics.scss rename to src/styles/AllTopics.module.scss index 2fd3ab75..5dca6c2a 100644 --- a/src/styles/AllTopics.scss +++ b/src/styles/AllTopics.module.scss @@ -1,5 +1,5 @@ -.all-topics-page { - .page-header, +.allTopicsPage { + .pageHeader, .group h2 { @include media-breakpoint-down(sm) { margin-left: 1.3rem; @@ -38,3 +38,9 @@ width: auto; } } + +.viewSwitcher { + @include media-breakpoint-down(sm) { + margin: 0 2.6rem; + } +} diff --git a/src/styles/app.scss b/src/styles/app.scss index 0b63000e..7258a3ed 100644 --- a/src/styles/app.scss +++ b/src/styles/app.scss @@ -404,12 +404,14 @@ figcaption { } .view-switcher { - @include font-size(2.2rem); + @include font-size(1.7rem); + display: flex; + flex-wrap: wrap; font-weight: bold; list-style: none; - margin: 0; - padding: 0; + margin: 2.4rem 0 0; + //padding: 0; .all-topics-page & { @include media-breakpoint-down(sm) { @@ -420,7 +422,11 @@ figcaption { li { display: inline-block; margin-right: 1em; - margin-bottom: 0.5em; + margin-bottom: 0.6em; + + &:last-child { + margin-right: 0; + } } button { @@ -436,6 +442,11 @@ figcaption { a { border-bottom: 2px solid #fff; + color: rgba(0, 0, 0, 0.5); + + &:hover { + color: #fff; + } } .selected { @@ -458,7 +469,12 @@ figcaption { } .view-switcher__search { - margin-left: 2em; + text-align: right; + + @include media-breakpoint-up(sm) { + flex: 1; + margin-left: 2em; + } .icon { display: inline-block; @@ -685,3 +701,11 @@ details { } } } + +.text-truncate { + display: -webkit-box !important; + overflow: hidden; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + white-space: normal; +} From 64907a84e6e7273889d2a47002dbc15682771558 Mon Sep 17 00:00:00 2001 From: kvakazyambra Date: Wed, 9 Nov 2022 22:51:41 +0300 Subject: [PATCH 10/34] Fixed content index control style --- .../icons/content-index-control-expanded.svg | 3 ++ public/icons/content-index-control.svg | 4 +++ src/components/Pages/about/GuidePage.tsx | 12 ++++++-- src/components/Pages/about/HelpPage.tsx | 12 ++++++-- src/components/Pages/about/ManifestPage.tsx | 12 ++++++-- src/components/Pages/about/TermsOfUsePage.tsx | 12 ++++++-- src/styles/app.scss | 30 ++++++++++++++++++- 7 files changed, 72 insertions(+), 13 deletions(-) create mode 100644 public/icons/content-index-control-expanded.svg create mode 100644 public/icons/content-index-control.svg diff --git a/public/icons/content-index-control-expanded.svg b/public/icons/content-index-control-expanded.svg new file mode 100644 index 00000000..01980281 --- /dev/null +++ b/public/icons/content-index-control-expanded.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/icons/content-index-control.svg b/public/icons/content-index-control.svg new file mode 100644 index 00000000..7d2861d0 --- /dev/null +++ b/public/icons/content-index-control.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/Pages/about/GuidePage.tsx b/src/components/Pages/about/GuidePage.tsx index 3b55056f..4d216ea6 100644 --- a/src/components/Pages/about/GuidePage.tsx +++ b/src/components/Pages/about/GuidePage.tsx @@ -1,6 +1,7 @@ import { createSignal, Show } from 'solid-js' import { MainLayout } from '../../Layouts/MainLayout' import { t } from '../../../utils/intl' +import { Icon } from '../../Nav/Icon' export const GuidePage = () => { const title = t('How it works') @@ -22,9 +23,14 @@ export const GuidePage = () => {
        -

        -

        diff --git a/src/components/Pages/about/HelpPage.tsx b/src/components/Pages/about/HelpPage.tsx index 6e0275bd..ce728c2c 100644 --- a/src/components/Pages/about/HelpPage.tsx +++ b/src/components/Pages/about/HelpPage.tsx @@ -1,6 +1,7 @@ import { createSignal, Show } from 'solid-js' import { MainLayout } from '../../Layouts/MainLayout' import { Donate } from '../../Discours/Donate' +import { Icon } from '../../Nav/Icon' // const title = t('Support us') @@ -19,9 +20,14 @@ export const HelpPage = () => {
        -

        -

        diff --git a/src/components/Pages/about/ManifestPage.tsx b/src/components/Pages/about/ManifestPage.tsx index 4b42465e..9834ad81 100644 --- a/src/components/Pages/about/ManifestPage.tsx +++ b/src/components/Pages/about/ManifestPage.tsx @@ -4,6 +4,7 @@ import { Modal } from '../../Nav/Modal' import { Feedback } from '../../Discours/Feedback' import Subscribe from '../../Discours/Subscribe' import Opener from '../../Nav/Opener' +import { Icon } from '../../Nav/Icon' // title={t('Manifest')} @@ -23,9 +24,14 @@ export const ManifestPage = () => {
        -

        -

        diff --git a/src/components/Pages/about/TermsOfUsePage.tsx b/src/components/Pages/about/TermsOfUsePage.tsx index 9ea7e9bc..67293a2c 100644 --- a/src/components/Pages/about/TermsOfUsePage.tsx +++ b/src/components/Pages/about/TermsOfUsePage.tsx @@ -1,5 +1,6 @@ import { createSignal, Show } from 'solid-js' import { MainLayout } from '../../Layouts/MainLayout' +import { Icon } from '../../Nav/Icon' // const title = t('Terms of use') @@ -17,9 +18,14 @@ export const TermsOfUsePage = () => {
        -

        -

        diff --git a/src/styles/app.scss b/src/styles/app.scss index 7258a3ed..5697045b 100644 --- a/src/styles/app.scss +++ b/src/styles/app.scss @@ -32,7 +32,7 @@ html { body { font-family: Muller, Arial, Helvetica, sans-serif; font-size: 2rem; - line-height: 1.4; + line-height: 1.6; min-height: 100%; text-size-adjust: 100%; @@ -239,6 +239,30 @@ button { } } +.button--content-index { + border: 2px solid #fff; + background: none; + height: 3.2rem; + padding: 0; + width: 3.2rem; + + .icon, + img { + vertical-align: middle; + width: auto; + } + + &:hover { + .icon { + opacity: 0.5; + } + } +} + +.content-index-control-container { + text-align: right; +} + form { .pretty-form__item { position: relative; @@ -654,6 +678,10 @@ astro-island { li { margin-bottom: 1em; } + + a { + border: none; + } } .load-more-container { From c92fd115c47d2a4011e5ddeb27aabf333b22663a Mon Sep 17 00:00:00 2001 From: kvakazyambra Date: Thu, 10 Nov 2022 00:39:54 +0300 Subject: [PATCH 11/34] Fixed about pages style --- .../Pages/about/DiscussionRulesPage.tsx | 2 +- src/components/Pages/about/DogmaPage.tsx | 2 +- src/components/Pages/about/GuidePage.tsx | 22 ++++----- src/components/Pages/about/HelpPage.tsx | 22 ++++----- src/components/Pages/about/ManifestPage.tsx | 22 ++++----- src/components/Pages/about/PartnersPage.tsx | 2 +- src/components/Pages/about/PrinciplesPage.tsx | 2 +- src/components/Pages/about/ProjectsPage.tsx | 2 +- src/components/Pages/about/TermsOfUsePage.tsx | 22 ++++----- src/components/Pages/about/ThanksPage.tsx | 2 +- src/styles/app.scss | 47 ++++++++++++++----- 11 files changed, 81 insertions(+), 66 deletions(-) diff --git a/src/components/Pages/about/DiscussionRulesPage.tsx b/src/components/Pages/about/DiscussionRulesPage.tsx index 832c1c0e..11ee4565 100644 --- a/src/components/Pages/about/DiscussionRulesPage.tsx +++ b/src/components/Pages/about/DiscussionRulesPage.tsx @@ -7,7 +7,7 @@ export const DiscussionRulesPage = () => {
        -
        +

        diff --git a/src/components/Pages/about/DogmaPage.tsx b/src/components/Pages/about/DogmaPage.tsx index cf764cd4..1407c73d 100644 --- a/src/components/Pages/about/DogmaPage.tsx +++ b/src/components/Pages/about/DogmaPage.tsx @@ -7,7 +7,7 @@ export const DogmaPage = () => {
        -
        +

        Редакционные принципы

        Дискурс - журнал с открытой горизонтальной редакцией. Содержание журнала определяется прямым diff --git a/src/components/Pages/about/GuidePage.tsx b/src/components/Pages/about/GuidePage.tsx index 4d216ea6..b4d690ff 100644 --- a/src/components/Pages/about/GuidePage.tsx +++ b/src/components/Pages/about/GuidePage.tsx @@ -22,17 +22,15 @@ export const GuidePage = () => {

        -
        -

        - -

        +
        +
        -
        +

        Как устроен Дискурс

        diff --git a/src/components/Pages/about/HelpPage.tsx b/src/components/Pages/about/HelpPage.tsx index ce728c2c..1a8cd4bb 100644 --- a/src/components/Pages/about/HelpPage.tsx +++ b/src/components/Pages/about/HelpPage.tsx @@ -19,17 +19,15 @@ export const HelpPage = () => {
        -
        -

        - -

        +
        +
        -
        +

        Как вы можете поддержать Дискурс?

        diff --git a/src/components/Pages/about/ManifestPage.tsx b/src/components/Pages/about/ManifestPage.tsx index 9834ad81..f5cb088a 100644 --- a/src/components/Pages/about/ManifestPage.tsx +++ b/src/components/Pages/about/ManifestPage.tsx @@ -23,17 +23,15 @@ export const ManifestPage = () => {
        -
        -

        - -

        +
        +
        -
        +

        Манифест

        diff --git a/src/components/Pages/about/PartnersPage.tsx b/src/components/Pages/about/PartnersPage.tsx index 3db7c4eb..4800e380 100644 --- a/src/components/Pages/about/PartnersPage.tsx +++ b/src/components/Pages/about/PartnersPage.tsx @@ -8,7 +8,7 @@ export const PartnersPage = () => {
        -
        +

        {t('Partners')}

        diff --git a/src/components/Pages/about/PrinciplesPage.tsx b/src/components/Pages/about/PrinciplesPage.tsx index d22cb77a..e52eed41 100644 --- a/src/components/Pages/about/PrinciplesPage.tsx +++ b/src/components/Pages/about/PrinciplesPage.tsx @@ -7,7 +7,7 @@ export const PrinciplesPage = () => {
        -
        +

        {title}

        diff --git a/src/components/Pages/about/ProjectsPage.tsx b/src/components/Pages/about/ProjectsPage.tsx index f78c25ff..63a7e55e 100644 --- a/src/components/Pages/about/ProjectsPage.tsx +++ b/src/components/Pages/about/ProjectsPage.tsx @@ -8,7 +8,7 @@ export const ProjectsPage = () => {
        -
        +

        {t('Projects')}

        diff --git a/src/components/Pages/about/TermsOfUsePage.tsx b/src/components/Pages/about/TermsOfUsePage.tsx index 67293a2c..7dad2295 100644 --- a/src/components/Pages/about/TermsOfUsePage.tsx +++ b/src/components/Pages/about/TermsOfUsePage.tsx @@ -17,17 +17,15 @@ export const TermsOfUsePage = () => { {/**/}
        -
        -

        - -

        +
        +
        -
        +

        Пользовательское соглашение

        diff --git a/src/components/Pages/about/ThanksPage.tsx b/src/components/Pages/about/ThanksPage.tsx index 6f6938d1..c4efa93d 100644 --- a/src/components/Pages/about/ThanksPage.tsx +++ b/src/components/Pages/about/ThanksPage.tsx @@ -12,7 +12,7 @@ export const ThanksPage = () => {
        -
        +

        {title}

        diff --git a/src/styles/app.scss b/src/styles/app.scss index 5697045b..38da5391 100644 --- a/src/styles/app.scss +++ b/src/styles/app.scss @@ -243,8 +243,23 @@ button { border: 2px solid #fff; background: none; height: 3.2rem; + float: right; padding: 0; + position: absolute; + right: $container-padding-x * 0.5; + top: -0.5rem; width: 3.2rem; + z-index: 1; + + @include media-breakpoint-up(md) { + margin-top: -0.5rem; + position: sticky; + top: 90px; + } + + @include media-breakpoint-up(sm) { + right: $container-padding-x; + } .icon, img { @@ -259,10 +274,6 @@ button { } } -.content-index-control-container { - text-align: right; -} - form { .pretty-form__item { position: relative; @@ -617,18 +628,26 @@ astro-island { .container { max-width: 1400px; - - // margin-left: 201px; - // width: auto; - - @include media-breakpoint-up(md) { - // padding-left: 227px; - } } .container--static-page { + @include font-size(1.7rem); + + color: #404040; + position: relative; + @include media-breakpoint-up(md) { padding-top: 1.5em; + + > .row { + flex-wrap: nowrap; + justify-content: space-between; + margin-right: 0; + } + } + + .order-md-last { + padding-right: 0; } } @@ -664,7 +683,7 @@ astro-island { .content-index { @include font-size(1.4rem); - margin-bottom: 2em; + margin: 0 3.6rem 2em 0; @include media-breakpoint-up(md) { position: sticky; @@ -695,6 +714,10 @@ astro-island { details { margin-bottom: 1.5em; + @include media-breakpoint-down(md) { + padding-left: 3rem; + } + summary { display: block; position: relative; From 580b8426b89729dc14acedce681cfdc0cf49155b Mon Sep 17 00:00:00 2001 From: kvakazyambra Date: Thu, 10 Nov 2022 00:47:52 +0300 Subject: [PATCH 12/34] Fixed selection style --- src/components/Discours/Footer.module.scss | 5 +++++ src/components/Discours/Hero.scss | 5 +++++ src/styles/app.scss | 15 +++++++++++++++ 3 files changed, 25 insertions(+) diff --git a/src/components/Discours/Footer.module.scss b/src/components/Discours/Footer.module.scss index 767f4874..b65481f2 100644 --- a/src/components/Discours/Footer.module.scss +++ b/src/components/Discours/Footer.module.scss @@ -39,6 +39,11 @@ padding: 0 $container-padding-x; } } + + ::selection { + background: #fff; + color: #000; + } } .discoursFooterContent { diff --git a/src/components/Discours/Hero.scss b/src/components/Discours/Hero.scss index 1a780d7a..1a02d14b 100644 --- a/src/components/Discours/Hero.scss +++ b/src/components/Discours/Hero.scss @@ -19,6 +19,11 @@ .wide-container { padding: 0; } + + ::selection { + background: #fff; + color: #000; + } } .about-discours__actions { diff --git a/src/styles/app.scss b/src/styles/app.scss index 38da5391..cb43480c 100644 --- a/src/styles/app.scss +++ b/src/styles/app.scss @@ -20,6 +20,11 @@ box-sizing: border-box; } +::selection { + background: #000; + color: #fff; +} + html { color: $default-color; font-size: 62.5%; @@ -79,6 +84,11 @@ h2 { padding: 0 0.15em; box-decoration-break: clone; -webkit-box-decoration-break: clone; + + &::selection { + background: #fff; + color: #000; + } } } @@ -569,6 +579,11 @@ figcaption { padding-top: $grid-gutter-width; } + ::selection { + background: #fff; + color: #000; + } + h2 { @include font-size(4.4rem); From 071d700bf07639c7815dbf2eccab1556d6b19079 Mon Sep 17 00:00:00 2001 From: kvakazyambra Date: Thu, 10 Nov 2022 01:14:14 +0300 Subject: [PATCH 13/34] Removed redundant block on the topic page --- src/components/Views/Topic.tsx | 24 +----------------------- src/styles/app.scss | 2 +- 2 files changed, 2 insertions(+), 24 deletions(-) diff --git a/src/components/Views/Topic.tsx b/src/components/Views/Topic.tsx index a50bf23a..934e4ec9 100644 --- a/src/components/Views/Topic.tsx +++ b/src/components/Views/Topic.tsx @@ -122,9 +122,7 @@ export const TopicView = (props: TopicProps) => { wrapper={'author'} /> - - - + { wrapper={'top-article'} /> - -
        -
        -
        -

        {title()}

        - - {(article) => ( -
        - -
        - )} -
        -
        -
        -
        -
        - 5}> diff --git a/src/styles/app.scss b/src/styles/app.scss index cb43480c..2e23e357 100644 --- a/src/styles/app.scss +++ b/src/styles/app.scss @@ -456,7 +456,7 @@ figcaption { font-weight: bold; list-style: none; margin: 2.4rem 0 0; - //padding: 0; + padding: 0; .all-topics-page & { @include media-breakpoint-down(sm) { From 31d837eca2418ce2837961876fd2df5b8a58d3dc Mon Sep 17 00:00:00 2001 From: Igor Lobanov Date: Wed, 9 Nov 2022 18:57:35 +0100 Subject: [PATCH 14/34] New editor WIP --- package.json | 1 - public/icons/expand.svg | 2 - .../components/Editor.module.scss | 0 .../components/Editor.tsx | 0 .../components/Error.module.scss | 0 .../components/Error.tsx | 0 .../components/Layout.module.scss | 0 .../components/Layout.tsx | 0 .../components/ProseMirror.tsx | 0 .../components/Sidebar.module.scss | 0 .../components/Sidebar.tsx | 0 .../{Editor => EditorExample}/db.ts | 0 .../{Editor => EditorExample}/env.ts | 0 .../{Editor => EditorExample}/markdown.ts | 0 .../prosemirror/extension/base.ts | 0 .../prosemirror/extension/code.ts | 0 .../prosemirror/extension/collab.ts | 0 .../prosemirror/extension/drag-handle.ts | 0 .../prosemirror/extension/image.ts | 0 .../prosemirror/extension/link.ts | 0 .../prosemirror/extension/mark-input-rule.ts | 0 .../prosemirror/extension/markdown.ts | 0 .../prosemirror/extension/menu.ts | 0 .../prosemirror/extension/paste-markdown.ts | 0 .../prosemirror/extension/placeholder.ts | 0 .../prosemirror/extension/prompt.ts | 0 .../prosemirror/extension/scroll.ts | 0 .../prosemirror/extension/selection.ts | 0 .../prosemirror/extension/strikethrough.ts | 0 .../prosemirror/extension/table.ts | 0 .../prosemirror/extension/todo-list.ts | 0 .../prosemirror/helpers.ts | 0 .../prosemirror/p2p.ts | 0 .../prosemirror/setup.ts | 0 .../{Editor => EditorExample}/remote.ts | 0 .../store/actions.ts | 0 .../store/context.ts | 0 .../styles/ArticlesList.scss.bak | 0 .../styles/ProseMirror.scss | 0 src/components/EditorNew/Editor.tsx | 102 ++++++ src/components/EditorNew/Sidebar.module.scss | 221 ++++++++++++ src/components/EditorNew/Sidebar.tsx | 113 ++++++ .../EditorNew/prosemirror/helpers/menu.ts | 215 ++++++++++++ .../EditorNew/prosemirror/helpers/prompt.ts | 197 +++++++++++ .../prosemirror/plugins/customKeymap.ts | 18 + .../prosemirror/plugins/dragHandle.ts | 48 +++ .../EditorNew/prosemirror/plugins/image.ts | 50 +++ .../EditorNew/prosemirror/plugins/index.ts | 26 ++ .../prosemirror/plugins/placeholder.ts | 23 ++ .../prosemirror/plugins/selectionMenu.ts | 53 +++ .../EditorNew/prosemirror/schema.ts | 172 +++++++++ .../styles/ProseMirror.module.scss | 21 ++ .../prosemirror/styles/ProseMirror.scss | 329 ++++++++++++++++++ .../EditorNew/prosemirror/views/image.ts | 68 ++++ src/components/Feed/Beside.module.scss | 2 - src/components/Nav/Modal.scss | 3 +- .../Topic/{Full.module.scss => Full.scss} | 12 +- src/components/Topic/Full.tsx | 59 ++-- src/components/Views/Author.tsx | 1 + src/components/Views/Create.tsx | 66 +--- src/graphql/mutation/article-create.ts | 2 - src/graphql/types.gen.ts | 104 +++--- src/locales/ru.json | 3 +- src/stores/editor.ts | 5 +- src/stores/inbox.ts | 4 +- src/stores/zine/articles.ts | 10 +- src/styles/{Topic.module.scss => Topic.scss} | 6 +- src/utils/apiClient.ts | 8 +- src/utils/config.ts | 2 +- yarn.lock | 9 +- 70 files changed, 1772 insertions(+), 183 deletions(-) delete mode 100644 public/icons/expand.svg rename src/components/{Editor => EditorExample}/components/Editor.module.scss (100%) rename src/components/{Editor => EditorExample}/components/Editor.tsx (100%) rename src/components/{Editor => EditorExample}/components/Error.module.scss (100%) rename src/components/{Editor => EditorExample}/components/Error.tsx (100%) rename src/components/{Editor => EditorExample}/components/Layout.module.scss (100%) rename src/components/{Editor => EditorExample}/components/Layout.tsx (100%) rename src/components/{Editor => EditorExample}/components/ProseMirror.tsx (100%) rename src/components/{Editor => EditorExample}/components/Sidebar.module.scss (100%) rename src/components/{Editor => EditorExample}/components/Sidebar.tsx (100%) rename src/components/{Editor => EditorExample}/db.ts (100%) rename src/components/{Editor => EditorExample}/env.ts (100%) rename src/components/{Editor => EditorExample}/markdown.ts (100%) rename src/components/{Editor => EditorExample}/prosemirror/extension/base.ts (100%) rename src/components/{Editor => EditorExample}/prosemirror/extension/code.ts (100%) rename src/components/{Editor => EditorExample}/prosemirror/extension/collab.ts (100%) rename src/components/{Editor => EditorExample}/prosemirror/extension/drag-handle.ts (100%) rename src/components/{Editor => EditorExample}/prosemirror/extension/image.ts (100%) rename src/components/{Editor => EditorExample}/prosemirror/extension/link.ts (100%) rename src/components/{Editor => EditorExample}/prosemirror/extension/mark-input-rule.ts (100%) rename src/components/{Editor => EditorExample}/prosemirror/extension/markdown.ts (100%) rename src/components/{Editor => EditorExample}/prosemirror/extension/menu.ts (100%) rename src/components/{Editor => EditorExample}/prosemirror/extension/paste-markdown.ts (100%) rename src/components/{Editor => EditorExample}/prosemirror/extension/placeholder.ts (100%) rename src/components/{Editor => EditorExample}/prosemirror/extension/prompt.ts (100%) rename src/components/{Editor => EditorExample}/prosemirror/extension/scroll.ts (100%) rename src/components/{Editor => EditorExample}/prosemirror/extension/selection.ts (100%) rename src/components/{Editor => EditorExample}/prosemirror/extension/strikethrough.ts (100%) rename src/components/{Editor => EditorExample}/prosemirror/extension/table.ts (100%) rename src/components/{Editor => EditorExample}/prosemirror/extension/todo-list.ts (100%) rename src/components/{Editor => EditorExample}/prosemirror/helpers.ts (100%) rename src/components/{Editor => EditorExample}/prosemirror/p2p.ts (100%) rename src/components/{Editor => EditorExample}/prosemirror/setup.ts (100%) rename src/components/{Editor => EditorExample}/remote.ts (100%) rename src/components/{Editor => EditorExample}/store/actions.ts (100%) rename src/components/{Editor => EditorExample}/store/context.ts (100%) rename src/components/{Editor => EditorExample}/styles/ArticlesList.scss.bak (100%) rename src/components/{Editor => EditorExample}/styles/ProseMirror.scss (100%) create mode 100644 src/components/EditorNew/Editor.tsx create mode 100644 src/components/EditorNew/Sidebar.module.scss create mode 100644 src/components/EditorNew/Sidebar.tsx create mode 100644 src/components/EditorNew/prosemirror/helpers/menu.ts create mode 100644 src/components/EditorNew/prosemirror/helpers/prompt.ts create mode 100644 src/components/EditorNew/prosemirror/plugins/customKeymap.ts create mode 100644 src/components/EditorNew/prosemirror/plugins/dragHandle.ts create mode 100644 src/components/EditorNew/prosemirror/plugins/image.ts create mode 100644 src/components/EditorNew/prosemirror/plugins/index.ts create mode 100644 src/components/EditorNew/prosemirror/plugins/placeholder.ts create mode 100644 src/components/EditorNew/prosemirror/plugins/selectionMenu.ts create mode 100644 src/components/EditorNew/prosemirror/schema.ts create mode 100644 src/components/EditorNew/prosemirror/styles/ProseMirror.module.scss create mode 100644 src/components/EditorNew/prosemirror/styles/ProseMirror.scss create mode 100644 src/components/EditorNew/prosemirror/views/image.ts rename src/components/Topic/{Full.module.scss => Full.scss} (66%) rename src/styles/{Topic.module.scss => Topic.scss} (82%) diff --git a/package.json b/package.json index b5bb2ea6..2988cc2f 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,6 @@ "prosemirror-markdown": "^1.9.4", "prosemirror-menu": "^1.2.1", "prosemirror-model": "^1.16.0", - "prosemirror-schema-basic": "^1.2.0", "prosemirror-schema-list": "^1.2.2", "prosemirror-state": "^1.4.1", "prosemirror-view": "^1.28.1", diff --git a/public/icons/expand.svg b/public/icons/expand.svg deleted file mode 100644 index fcbcd21c..00000000 --- a/public/icons/expand.svg +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/src/components/Editor/components/Editor.module.scss b/src/components/EditorExample/components/Editor.module.scss similarity index 100% rename from src/components/Editor/components/Editor.module.scss rename to src/components/EditorExample/components/Editor.module.scss diff --git a/src/components/Editor/components/Editor.tsx b/src/components/EditorExample/components/Editor.tsx similarity index 100% rename from src/components/Editor/components/Editor.tsx rename to src/components/EditorExample/components/Editor.tsx diff --git a/src/components/Editor/components/Error.module.scss b/src/components/EditorExample/components/Error.module.scss similarity index 100% rename from src/components/Editor/components/Error.module.scss rename to src/components/EditorExample/components/Error.module.scss diff --git a/src/components/Editor/components/Error.tsx b/src/components/EditorExample/components/Error.tsx similarity index 100% rename from src/components/Editor/components/Error.tsx rename to src/components/EditorExample/components/Error.tsx diff --git a/src/components/Editor/components/Layout.module.scss b/src/components/EditorExample/components/Layout.module.scss similarity index 100% rename from src/components/Editor/components/Layout.module.scss rename to src/components/EditorExample/components/Layout.module.scss diff --git a/src/components/Editor/components/Layout.tsx b/src/components/EditorExample/components/Layout.tsx similarity index 100% rename from src/components/Editor/components/Layout.tsx rename to src/components/EditorExample/components/Layout.tsx diff --git a/src/components/Editor/components/ProseMirror.tsx b/src/components/EditorExample/components/ProseMirror.tsx similarity index 100% rename from src/components/Editor/components/ProseMirror.tsx rename to src/components/EditorExample/components/ProseMirror.tsx diff --git a/src/components/Editor/components/Sidebar.module.scss b/src/components/EditorExample/components/Sidebar.module.scss similarity index 100% rename from src/components/Editor/components/Sidebar.module.scss rename to src/components/EditorExample/components/Sidebar.module.scss diff --git a/src/components/Editor/components/Sidebar.tsx b/src/components/EditorExample/components/Sidebar.tsx similarity index 100% rename from src/components/Editor/components/Sidebar.tsx rename to src/components/EditorExample/components/Sidebar.tsx diff --git a/src/components/Editor/db.ts b/src/components/EditorExample/db.ts similarity index 100% rename from src/components/Editor/db.ts rename to src/components/EditorExample/db.ts diff --git a/src/components/Editor/env.ts b/src/components/EditorExample/env.ts similarity index 100% rename from src/components/Editor/env.ts rename to src/components/EditorExample/env.ts diff --git a/src/components/Editor/markdown.ts b/src/components/EditorExample/markdown.ts similarity index 100% rename from src/components/Editor/markdown.ts rename to src/components/EditorExample/markdown.ts diff --git a/src/components/Editor/prosemirror/extension/base.ts b/src/components/EditorExample/prosemirror/extension/base.ts similarity index 100% rename from src/components/Editor/prosemirror/extension/base.ts rename to src/components/EditorExample/prosemirror/extension/base.ts diff --git a/src/components/Editor/prosemirror/extension/code.ts b/src/components/EditorExample/prosemirror/extension/code.ts similarity index 100% rename from src/components/Editor/prosemirror/extension/code.ts rename to src/components/EditorExample/prosemirror/extension/code.ts diff --git a/src/components/Editor/prosemirror/extension/collab.ts b/src/components/EditorExample/prosemirror/extension/collab.ts similarity index 100% rename from src/components/Editor/prosemirror/extension/collab.ts rename to src/components/EditorExample/prosemirror/extension/collab.ts diff --git a/src/components/Editor/prosemirror/extension/drag-handle.ts b/src/components/EditorExample/prosemirror/extension/drag-handle.ts similarity index 100% rename from src/components/Editor/prosemirror/extension/drag-handle.ts rename to src/components/EditorExample/prosemirror/extension/drag-handle.ts diff --git a/src/components/Editor/prosemirror/extension/image.ts b/src/components/EditorExample/prosemirror/extension/image.ts similarity index 100% rename from src/components/Editor/prosemirror/extension/image.ts rename to src/components/EditorExample/prosemirror/extension/image.ts diff --git a/src/components/Editor/prosemirror/extension/link.ts b/src/components/EditorExample/prosemirror/extension/link.ts similarity index 100% rename from src/components/Editor/prosemirror/extension/link.ts rename to src/components/EditorExample/prosemirror/extension/link.ts diff --git a/src/components/Editor/prosemirror/extension/mark-input-rule.ts b/src/components/EditorExample/prosemirror/extension/mark-input-rule.ts similarity index 100% rename from src/components/Editor/prosemirror/extension/mark-input-rule.ts rename to src/components/EditorExample/prosemirror/extension/mark-input-rule.ts diff --git a/src/components/Editor/prosemirror/extension/markdown.ts b/src/components/EditorExample/prosemirror/extension/markdown.ts similarity index 100% rename from src/components/Editor/prosemirror/extension/markdown.ts rename to src/components/EditorExample/prosemirror/extension/markdown.ts diff --git a/src/components/Editor/prosemirror/extension/menu.ts b/src/components/EditorExample/prosemirror/extension/menu.ts similarity index 100% rename from src/components/Editor/prosemirror/extension/menu.ts rename to src/components/EditorExample/prosemirror/extension/menu.ts diff --git a/src/components/Editor/prosemirror/extension/paste-markdown.ts b/src/components/EditorExample/prosemirror/extension/paste-markdown.ts similarity index 100% rename from src/components/Editor/prosemirror/extension/paste-markdown.ts rename to src/components/EditorExample/prosemirror/extension/paste-markdown.ts diff --git a/src/components/Editor/prosemirror/extension/placeholder.ts b/src/components/EditorExample/prosemirror/extension/placeholder.ts similarity index 100% rename from src/components/Editor/prosemirror/extension/placeholder.ts rename to src/components/EditorExample/prosemirror/extension/placeholder.ts diff --git a/src/components/Editor/prosemirror/extension/prompt.ts b/src/components/EditorExample/prosemirror/extension/prompt.ts similarity index 100% rename from src/components/Editor/prosemirror/extension/prompt.ts rename to src/components/EditorExample/prosemirror/extension/prompt.ts diff --git a/src/components/Editor/prosemirror/extension/scroll.ts b/src/components/EditorExample/prosemirror/extension/scroll.ts similarity index 100% rename from src/components/Editor/prosemirror/extension/scroll.ts rename to src/components/EditorExample/prosemirror/extension/scroll.ts diff --git a/src/components/Editor/prosemirror/extension/selection.ts b/src/components/EditorExample/prosemirror/extension/selection.ts similarity index 100% rename from src/components/Editor/prosemirror/extension/selection.ts rename to src/components/EditorExample/prosemirror/extension/selection.ts diff --git a/src/components/Editor/prosemirror/extension/strikethrough.ts b/src/components/EditorExample/prosemirror/extension/strikethrough.ts similarity index 100% rename from src/components/Editor/prosemirror/extension/strikethrough.ts rename to src/components/EditorExample/prosemirror/extension/strikethrough.ts diff --git a/src/components/Editor/prosemirror/extension/table.ts b/src/components/EditorExample/prosemirror/extension/table.ts similarity index 100% rename from src/components/Editor/prosemirror/extension/table.ts rename to src/components/EditorExample/prosemirror/extension/table.ts diff --git a/src/components/Editor/prosemirror/extension/todo-list.ts b/src/components/EditorExample/prosemirror/extension/todo-list.ts similarity index 100% rename from src/components/Editor/prosemirror/extension/todo-list.ts rename to src/components/EditorExample/prosemirror/extension/todo-list.ts diff --git a/src/components/Editor/prosemirror/helpers.ts b/src/components/EditorExample/prosemirror/helpers.ts similarity index 100% rename from src/components/Editor/prosemirror/helpers.ts rename to src/components/EditorExample/prosemirror/helpers.ts diff --git a/src/components/Editor/prosemirror/p2p.ts b/src/components/EditorExample/prosemirror/p2p.ts similarity index 100% rename from src/components/Editor/prosemirror/p2p.ts rename to src/components/EditorExample/prosemirror/p2p.ts diff --git a/src/components/Editor/prosemirror/setup.ts b/src/components/EditorExample/prosemirror/setup.ts similarity index 100% rename from src/components/Editor/prosemirror/setup.ts rename to src/components/EditorExample/prosemirror/setup.ts diff --git a/src/components/Editor/remote.ts b/src/components/EditorExample/remote.ts similarity index 100% rename from src/components/Editor/remote.ts rename to src/components/EditorExample/remote.ts diff --git a/src/components/Editor/store/actions.ts b/src/components/EditorExample/store/actions.ts similarity index 100% rename from src/components/Editor/store/actions.ts rename to src/components/EditorExample/store/actions.ts diff --git a/src/components/Editor/store/context.ts b/src/components/EditorExample/store/context.ts similarity index 100% rename from src/components/Editor/store/context.ts rename to src/components/EditorExample/store/context.ts diff --git a/src/components/Editor/styles/ArticlesList.scss.bak b/src/components/EditorExample/styles/ArticlesList.scss.bak similarity index 100% rename from src/components/Editor/styles/ArticlesList.scss.bak rename to src/components/EditorExample/styles/ArticlesList.scss.bak diff --git a/src/components/Editor/styles/ProseMirror.scss b/src/components/EditorExample/styles/ProseMirror.scss similarity index 100% rename from src/components/Editor/styles/ProseMirror.scss rename to src/components/EditorExample/styles/ProseMirror.scss diff --git a/src/components/EditorNew/Editor.tsx b/src/components/EditorNew/Editor.tsx new file mode 100644 index 00000000..b9696fff --- /dev/null +++ b/src/components/EditorNew/Editor.tsx @@ -0,0 +1,102 @@ +import { createSignal, onMount } from 'solid-js' +import { EditorState, Transaction } from 'prosemirror-state' +import { EditorView, MarkViewConstructor, NodeViewConstructor } from 'prosemirror-view' +import './prosemirror/styles/ProseMirror.scss' +import type { Nodes, Marks } from './prosemirror/schema' +import { createImageView } from './prosemirror/views/image' +import { MarkdownSerializer } from 'prosemirror-markdown' +import { schema } from './prosemirror/schema' +import { createPlugins } from './prosemirror/plugins' + +import debounce from 'lodash/debounce' +import { DOMSerializer } from 'prosemirror-model' +import { clsx } from 'clsx' +import styles from '../Nav/AuthModal/AuthModal.module.scss' +import { t } from '../../utils/intl' +import { apiClient } from '../../utils/apiClient' +import { createArticle } from '../../stores/zine/articles' +import type { InputMaybe, Scalars, ShoutInput } from '../../graphql/types.gen' +import { Sidebar } from './Sidebar' + +const htmlContainer = typeof document === 'undefined' ? null : document.createElement('div') + +const getHtml = (state: EditorState) => { + const fragment = DOMSerializer.fromSchema(schema).serializeFragment(state.doc.content) + htmlContainer.replaceChildren(fragment) + return htmlContainer.innerHTML +} + +export const Editor = () => { + const [markdown, setMarkdown] = createSignal('') + const [html, setHtml] = createSignal('') + + const editorElRef: { + current: HTMLDivElement + } = { + current: null + } + + const editorViewRef: { current: EditorView } = { current: null } + + const update = (state: EditorState) => { + const newHtml = getHtml(state) + setHtml(newHtml) + // setMarkdown(state.toJSON()) + // const el = document.createElement('div') + } + + const debouncedUpdate = debounce(update, 500) + + const dispatchTransaction = (tr: Transaction) => { + const newState = editorViewRef.current.state.apply(tr) + editorViewRef.current.updateState(newState) + debouncedUpdate(newState) + } + + onMount(async () => { + const plugins = createPlugins({ schema }) + + const nodeViews: Partial> = { + image: createImageView + } + + const markViews: Partial> = {} + + editorViewRef.current = new EditorView(editorElRef.current, { + state: EditorState.create({ + schema, + plugins + }), + nodeViews, + markViews, + dispatchTransaction + }) + + editorViewRef.current.focus() + }) + + const handleSaveButtonClick = () => { + const article: ShoutInput = { + body: getHtml(editorViewRef.current.state), + community: 'discours', // ? + slug: 'new-' + Math.floor(Math.random() * 1000000) + } + createArticle({ article }) + } + + return ( +
        +
        +
        (editorElRef.current = el)} /> +
        Markdown:
        +
        {markdown()}
        +
        HTML:
        +
        {html()}
        + +
        + +
        + ) +} diff --git a/src/components/EditorNew/Sidebar.module.scss b/src/components/EditorNew/Sidebar.module.scss new file mode 100644 index 00000000..187cd08b --- /dev/null +++ b/src/components/EditorNew/Sidebar.module.scss @@ -0,0 +1,221 @@ +.sidebarContainer { + color: rgb(255 255 255 / 50%); + @include font-size(1.6rem); + + overflow: hidden; + position: relative; + top: 0; + + p { + color: var(--foreground); + } + + h4 { + @include font-size(120%); + + margin-left: 1rem; + } + + button { + height: auto; + min-height: 50px; + padding: 0 1rem; + width: 100%; + } +} + +.sidebarOff { + background: #1f1f1f; + height: 100%; + min-height: 100vh; + padding: 40px 20px 20px; + top: 0; + transform: translateX(0); + transition: transform 0.3s; + overflow-y: auto; + scrollbar-width: none; + width: 350px; + + .sidebarContainerHidden & { + transform: translateX(100%); + } + + ::-webkit-scrollbar { + display: none; + } +} + +.sidebarOpener { + color: #000; + cursor: pointer; + opacity: 1; + position: absolute; + top: 1em; + transition: opacity 0.3s; + + &:hover { + opacity: 0.5; + } + + &::after { + background-image: url("data:image/svg+xml,%3Csvg width='18' height='18' viewBox='0 0 18 18' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cmask id='mask0_1090_23825' style='mask-type:alpha' maskUnits='userSpaceOnUse' x='0' y='14' width='4' height='4'%3E%3Crect y='14.8237' width='3.17647' height='3.17647' fill='%23fff'/%3E%3C/mask%3E%3Cg mask='url(%23mask0_1090_23825)'%3E%3Cpath d='M16.0941 1.05908H0.847027C0.379194 1.05908 0 1.43828 0 1.90611V18.0003L3.38824 14.612H16.0942C16.562 14.612 16.9412 14.2328 16.9412 13.765V1.90614C16.9412 1.43831 16.562 1.05912 16.0942 1.05912L16.0941 1.05908ZM15.2471 12.9179H1.69412V2.7532H15.2471V12.9179Z' fill='black'/%3E%3C/g%3E%3Crect x='1' y='1' width='16' height='12.8235' stroke='black' stroke-width='2'/%3E%3Crect x='4.23535' y='3.17627' width='9.52941' height='2.11765' fill='black'/%3E%3Crect x='4.23535' y='9.5293' width='7.41176' height='2.11765' fill='black'/%3E%3Crect x='4.23535' y='6.35303' width='5.29412' height='2.11765' fill='black'/%3E%3C/svg%3E"); + content: ''; + height: 18px; + left: 100%; + margin-left: 0.3em; + position: absolute; + top: 50%; + transform: translateY(-50%); + width: 18px; + } +} + +.sidebarCloser { + background-image: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M13.1517 0.423857L0.42375 13.1518L2.84812 15.5761L15.576 2.84822L13.1517 0.423857Z M15.576 13.1518L2.84812 0.423855L0.423751 2.84822L13.1517 15.5761L15.576 13.1518Z' fill='white'/%3E%3C/svg%3E%0A"); + cursor: pointer; + height: 16px; + opacity: 1; + position: absolute; + transition: opacity 0.3s; + top: 20px; + width: 16px; + + &:hover { + opacity: 0.5; + } +} + +.sidebarLabel { + color: var(--foreground); + + > i { + text-transform: none; + } +} + +.sidebarContainer button, +.sidebarContainer a, +.sidebarItem { + margin: 0; + outline: none; + display: flex; + align-items: center; + line-height: 24px; + text-align: left; +} + +.sidebarContainer a, +.sidebarItem { + font-size: 18px; + padding: 2px 0; + width: 100%; +} + +.sidebarLink { + background: none; + border: 0; + color: inherit; + cursor: pointer; + font-size: inherit; + justify-content: flex-start; + + &:hover { + color: #fff !important; + } + + &:active { + > span i { + position: relative; + box-shadow: none; + top: 1px; + } + } + + &[disabled] { + color: var(--foreground); + cursor: not-allowed; + } + + &.draft { + color: rgb(255 255 255 / 50%); + line-height: 1.4; + margin: 0 0 1em 1.5em; + width: calc(100% - 2rem); + + &:hover { + background: none; + } + } + + > span { + justify-self: flex-end; + margin-left: auto; + + > i { + border: 1px solid; + border-bottom-width: 2px; + border-radius: 0.2rem; + display: inline-block; + color: inherit; + font-size: 13px; + line-height: 1.4; + margin: 0 0.5em 0 0; + padding: 1px 4px; + + &:last-child { + text-transform: uppercase; + } + } + } +} + +.themeSwitcher { + border-bottom: 1px solid rgb(255 255 255 / 30%); + border-top: 1px solid rgb(255 255 255 / 30%); + display: flex; + justify-content: space-between; + margin: 1rem; + padding: 1em 0; + + input[type='checkbox'] { + opacity: 0; + position: absolute; + + + label { + background: url("data:image/svg+xml,%3Csvg width='10' height='10' viewBox='0 0 10 10' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6.20869 7.73227C5.22953 7.36499 4.38795 6.70402 3.79906 5.83976C3.2103 4.97565 2.90318 3.95064 2.91979 2.90512C2.93639 1.8597 3.27597 0.844915 3.8919 0C2.82862 0.254038 1.87585 0.844877 1.17594 1.68438C0.475894 2.52388 0.0660276 3.5671 0.00731938 4.6585C-0.0513888 5.74989 0.244296 6.83095 0.850296 7.74073C1.45631 8.65037 2.34006 9.33992 3.36994 9.70637C4.39987 10.073 5.52063 10.0969 6.56523 9.77466C7.60985 9.45247 8.52223 8.80134 9.16667 7.91837C8.1842 8.15404 7.15363 8.08912 6.20869 7.73205V7.73227Z' fill='white'/%3E%3C/svg%3E%0A") + no-repeat 30px 9px, + url("data:image/svg+xml,%3Csvg width='12' height='12' viewBox='0 0 12 12' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6.41196 0H5.58811V2.43024H6.41196V0ZM5.99988 8.96576C4.36601 8.96576 3.03419 7.63397 3.03419 6.00007C3.04792 4.3662 4.36598 3.04818 5.99988 3.03439C7.63375 3.03439 8.96557 4.3662 8.96557 6.00007C8.96557 7.63395 7.63375 8.96576 5.99988 8.96576ZM5.58811 9.56977H6.41196V12H5.58811V9.56977ZM12.0002 5.58811H9.56996V6.41196H12.0002V5.58811ZM0 5.58811H2.43024V6.41196H0V5.58811ZM8.81339 3.76727L10.5318 2.04891L9.94925 1.46641L8.23089 3.18477L8.81339 3.76727ZM3.7745 8.8129L2.05614 10.5313L1.47364 9.94877L3.192 8.2304L3.7745 8.8129ZM9.95043 10.5269L10.5329 9.94437L8.81456 8.22601L8.23207 8.80851L9.95043 10.5269ZM3.76864 3.18731L3.18614 3.76981L1.46778 2.05145L2.05028 1.46895L3.76864 3.18731Z' fill='%231F1F1F'/%3E%3C/svg%3E%0A") + #000 no-repeat 8px 8px; + border-radius: 14px; + cursor: pointer; + display: block; + height: 28px; + line-height: 10em; + overflow: hidden; + position: relative; + transition: background-color 0.3s; + width: 46px; + + &::before { + background-color: #fff; + border-radius: 100%; + content: ''; + height: 16px; + left: 6px; + position: absolute; + top: 6px; + transition: left 0.3s, color 0.3s; + width: 16px; + } + } + + &:checked + label { + background-color: #fff; + + &::before { + background-color: #1f1f1f; + left: 24px; + } + } + } +} diff --git a/src/components/EditorNew/Sidebar.tsx b/src/components/EditorNew/Sidebar.tsx new file mode 100644 index 00000000..f45f2172 --- /dev/null +++ b/src/components/EditorNew/Sidebar.tsx @@ -0,0 +1,113 @@ +import { For, Show, createEffect, createSignal, onCleanup, onMount } from 'solid-js' +import type { JSX } from 'solid-js' +import { undo, redo } from 'prosemirror-history' +import { clsx } from 'clsx' +import styles from './Sidebar.module.scss' +import { useOutsideClickHandler } from '../../utils/useOutsideClickHandler' +import { useEscKeyDownHandler } from '../../utils/useEscKeyDownHandler' +import type { EditorView } from 'prosemirror-view' + +const Off = (props) =>
        {props.children}
        + +const Link = (props: { + withMargin?: boolean + disabled?: boolean + title?: string + className?: string + children: JSX.Element + onClick?: () => void +}) => ( + +) + +const Keys = (props: { keys: string[] }) => ( + + {(k) => {k}} + +) + +type SidebarProps = { + editorViewRef: { + current: EditorView + } +} + +export const Sidebar = (props: SidebarProps) => { + const [lastAction, setLastAction] = createSignal() + + const { editorViewRef } = props + + const onUndo = () => undo(editorViewRef.current.state, editorViewRef.current.dispatch) + const onRedo = () => redo(editorViewRef.current.state, editorViewRef.current.dispatch) + + const [isHidden, setIsHidden] = createSignal(true) + + const toggleSidebar = () => { + setIsHidden((oldIsHidden) => !oldIsHidden) + } + + createEffect(() => { + setLastAction() + }) + + createEffect(() => { + if (!lastAction()) return + const id = setTimeout(() => { + setLastAction() + }, 1000) + onCleanup(() => clearTimeout(id)) + }) + + const [mod, setMod] = createSignal<'Ctrl' | 'Cmd'>('Ctrl') + + onMount(() => { + setMod(navigator.platform.includes('Mac') ? 'Cmd' : 'Ctrl') + }) + + const containerRef: { current: HTMLElement } = { + current: null + } + + useEscKeyDownHandler(() => setIsHidden(true)) + useOutsideClickHandler({ + containerRef, + predicate: () => !isHidden(), + handler: () => setIsHidden(true) + }) + + return ( +
        (containerRef.current = el)} + > + + Советы и предложения + + + editorViewRef.current.focus()}> +
        + +
        + + Undo + + + Redo + +
        + +
        + ) +} diff --git a/src/components/EditorNew/prosemirror/helpers/menu.ts b/src/components/EditorNew/prosemirror/helpers/menu.ts new file mode 100644 index 00000000..6013fe85 --- /dev/null +++ b/src/components/EditorNew/prosemirror/helpers/menu.ts @@ -0,0 +1,215 @@ +import { toggleMark } from 'prosemirror-commands' +import { wrapInList } from 'prosemirror-schema-list' +import { blockTypeItem, icons, MenuItem, wrapItem, Dropdown } from 'prosemirror-menu' + +import type { NodeSelection } from 'prosemirror-state' + +import { TextField, openPrompt } from './prompt' + +import type { DiscoursSchema } from '../schema' + +function wrapListItem(nodeType, options) { + return cmdItem(wrapInList(nodeType, options.attrs), options) +} + +function canInsert(state, nodeType) { + const $from = state.selection.$from + + for (let d = $from.depth; d >= 0; d--) { + const index = $from.index(d) + + if ($from.node(d).canReplaceWith(index, index, nodeType)) return true + } + + return false +} + +function insertImageItem(nodeType) { + return new MenuItem({ + icon: icons.image, + label: 'image', + enable(state) { + return canInsert(state, nodeType) + }, + run(state, _, view) { + const { + from, + to, + node: { attrs } + } = state.selection as NodeSelection + + openPrompt({ + title: 'Insert image', + fields: { + src: new TextField({ + label: 'Location', + required: true, + value: attrs && attrs.src + }), + title: new TextField({ label: 'Title', value: attrs && attrs.title }), + alt: new TextField({ + label: 'Description', + value: attrs ? attrs.alt : state.doc.textBetween(from, to, ' ') + }) + }, + onSubmit(newAttrs) { + view.dispatch(view.state.tr.replaceSelectionWith(nodeType.createAndFill(newAttrs))) + view.focus() + } + }) + } + }) +} + +function cmdItem(cmd, options) { + const passedOptions = { + label: options.title, + run: cmd + } + + for (const prop in options) passedOptions[prop] = options[prop] + + if ((!options.enable || options.enable === true) && !options.select) { + passedOptions[options.enable ? 'enable' : 'select'] = (state) => cmd(state) + } + + return new MenuItem(passedOptions) +} + +function markActive(state, type) { + const { from, $from, to, empty } = state.selection + + if (empty) return type.isInSet(state.storedMarks || $from.marks()) + + return state.doc.rangeHasMark(from, to, type) +} + +function markItem(markType, options) { + const passedOptions = { + active(state) { + return markActive(state, markType) + }, + enable: true + } + + for (const prop in options) passedOptions[prop] = options[prop] + + return cmdItem(toggleMark(markType), passedOptions) +} + +function linkItem(markType) { + return new MenuItem({ + title: 'Add or remove link', + icon: { + width: 18, + height: 18, + path: 'M3.27177 14.7277C2.06258 13.5186 2.06258 11.5527 3.27177 10.3435L6.10029 7.51502L4.75675 6.17148L1.92823 9C-0.0234511 10.9517 -0.0234511 14.1196 1.92823 16.0713C3.87991 18.023 7.04785 18.023 8.99952 16.0713L11.828 13.2428L10.4845 11.8992L7.65598 14.7277C6.44679 15.9369 4.48097 15.9369 3.27177 14.7277ZM6.87756 12.536L12.5346 6.87895L11.1203 5.46469L5.4633 11.1217L6.87756 12.536ZM6.17055 4.75768L8.99907 1.92916C10.9507 -0.0225206 14.1187 -0.0225201 16.0704 1.92916C18.022 3.88084 18.022 7.04878 16.0704 9.00046L13.2418 11.829L11.8983 10.4854L14.7268 7.65691C15.936 6.44772 15.936 4.4819 14.7268 3.27271C13.5176 2.06351 11.5518 2.06351 10.3426 3.2727L7.51409 6.10122L6.17055 4.75768Z' + }, + active(state) { + return markActive(state, markType) + }, + enable(state) { + return !state.selection.empty + }, + run(state, dispatch, view) { + if (markActive(state, markType)) { + toggleMark(markType)(state, dispatch) + + return true + } + + openPrompt({ + fields: { + href: new TextField({ + label: 'Link target', + required: true + }) + }, + onSubmit(attrs) { + toggleMark(markType, attrs)(view.state, view.dispatch) + view.focus() + } + }) + } + }) +} + +export const buildMenuItems = (schema: DiscoursSchema) => { + const toggleStrong = markItem(schema.marks.strong, { + title: 'Toggle strong style', + icon: { + width: 13, + height: 16, + path: 'M9.82857 7.76C10.9371 6.99429 11.7143 5.73714 11.7143 4.57143C11.7143 1.98857 9.71428 0 7.14286 0H0V16H8.04571C10.4343 16 12.2857 14.0571 12.2857 11.6686C12.2857 9.93143 11.3029 8.44571 9.82857 7.76ZM3.42799 2.85708H6.85656C7.80513 2.85708 8.57085 3.6228 8.57085 4.57137C8.57085 5.51994 7.80513 6.28565 6.85656 6.28565H3.42799V2.85708ZM3.42799 13.1429H7.42799C8.37656 13.1429 9.14228 12.3772 9.14228 11.4286C9.14228 10.4801 8.37656 9.71434 7.42799 9.71434H3.42799V13.1429Z' + } + }) + + const toggleEm = markItem(schema.marks.em, { + title: 'Toggle emphasis', + icon: { + width: 14, + height: 16, + path: 'M4.39216 0V3.42857H6.81882L3.06353 12.5714H0V16H8.78431V12.5714H6.35765L10.1129 3.42857H13.1765V0H4.39216Z' + } + }) + + const toggleLink = linkItem(schema.marks.link) + + const insertImage = insertImageItem(schema.nodes.image) + + const wrapBlockQuote = wrapItem(schema.nodes.blockquote, { + title: 'Wrap in block quote', + icon: icons.blockquote + }) + + const headingIcons = [ + 'M0 12H2.57143V7.16571H7.95429V12H10.5257V0H7.95429V4.83429H2.57143V0H0V12Z M12.6801 12H19.3315V9.78857H17.3944V0.342858H15.5087L12.6801 1.42286V3.75429L14.8744 2.93143V9.78857H12.6801V12Z', + 'M0 12H2.57143V7.16571H7.95429V12H10.5257V0H7.95429V4.83429H2.57143V0H0V12Z M12.4915 12H21.2515V9.78857H15.4229C15.4229 9.05143 16.6229 8.43429 17.9944 7.59429C19.5372 6.68571 21.1658 5.52 21.1658 3.54857C21.1658 1.16571 19.2458 0.102858 16.8972 0.102858C15.4744 0.102858 14.0858 0.48 12.8858 1.33714V3.73714C14.1201 2.79429 15.4915 2.36571 16.6744 2.36571C17.8229 2.36571 18.5772 2.79429 18.5772 3.65143C18.5772 4.76571 17.5487 5.22857 16.3315 5.93143C14.6172 6.94286 12.4915 8.02286 12.4915 10.8514V12Z', + 'M0 11.7647H2.52101V7.02521H7.79832V11.7647H10.3193V0H7.79832V4.7395H2.52101V0H0V11.7647Z M16.3474 12C18.7004 12 20.9189 11.042 20.9189 8.63866C20.9189 6.95798 19.8936 6.06723 18.7172 5.71429C19.7928 5.34454 20.4483 4.43697 20.4483 3.2605C20.4483 1.17647 18.6836 0.100841 16.3138 0.100841C14.9189 0.100841 13.6079 0.436975 12.5827 0.991597V3.34454C13.7088 2.63865 14.9357 2.31933 15.9609 2.31933C17.339 2.31933 18.0617 2.78992 18.0617 3.61345C18.0617 4.40336 17.3558 4.82353 16.2466 4.80672L14.6668 4.78992L14.6499 6.97479H16.5323C17.6752 6.97479 18.5155 7.31092 18.5155 8.28571C18.5155 9.36134 17.4399 9.7647 16.1457 9.78151C14.8348 9.79832 13.692 9.59664 12.381 8.87395V11.2269C13.692 11.7647 14.8852 12 16.3474 12Z' + ] + + // 3 is the max heading level mb move to constant + const headings: MenuItem[] = [] + + for (let i = 0; i < 3; i++) { + headings.push( + blockTypeItem(schema.nodes.heading, { + label: `H${i + 1}`, + attrs: { level: i + 1 }, + icon: { + width: 22, + height: 12, + path: headingIcons[i] + } + }) + ) + } + + const typeMenu = new Dropdown([...headings, wrapBlockQuote], { + label: 'Тт', + class: 'editor-dropdown' + }) + + const wrapBulletList = wrapListItem(schema.nodes.bullet_list, { + title: 'Wrap in bullet list', + icon: { + width: 20, + height: 16, + path: 'M0.000114441 1.6C0.000114441 0.714665 0.71478 0 1.60011 0C2.48544 0 3.20011 0.714665 3.20011 1.6C3.20011 2.48533 2.48544 3.19999 1.60011 3.19999C0.71478 3.19999 0.000114441 2.48533 0.000114441 1.6ZM0 8.00013C0 7.1148 0.714665 6.40014 1.6 6.40014C2.48533 6.40014 3.19999 7.1148 3.19999 8.00013C3.19999 8.88547 2.48533 9.60013 1.6 9.60013C0.714665 9.60013 0 8.88547 0 8.00013ZM1.6 12.8C0.714665 12.8 0 13.5254 0 14.4C0 15.2747 0.725332 16 1.6 16C2.47466 16 3.19999 15.2747 3.19999 14.4C3.19999 13.5254 2.48533 12.8 1.6 12.8ZM19.7333 15.4662H4.79999V13.3329H19.7333V15.4662ZM4.79999 9.06677H19.7333V6.93344H4.79999V9.06677ZM4.79999 2.66664V0.533307H19.7333V2.66664H4.79999Z' + } + }) + + const wrapOrderedList = wrapListItem(schema.nodes.ordered_list, { + title: 'Wrap in ordered list', + icon: { + width: 19, + height: 16, + path: 'M2.00002 4.00003H1.00001V1.00001H0V0H2.00002V4.00003ZM2.00002 13.5V13H0V12H3.00003V16H0V15H2.00002V14.5H1.00001V13.5H2.00002ZM0 6.99998H1.80002L0 9.1V10H3.00003V9H1.20001L3.00003 6.89998V5.99998H0V6.99998ZM4.9987 2.99967V0.999648H18.9988V2.99967H4.9987ZM4.9987 15.0001H18.9988V13.0001H4.9987V15.0001ZM18.9988 8.99987H4.9987V6.99986H18.9988V8.99987Z' + } + }) + + const listMenu = [wrapBulletList, wrapOrderedList] + const inlineMenu = [toggleStrong, toggleEm] + + return [[typeMenu, ...inlineMenu, toggleLink, insertImage, ...listMenu]] +} diff --git a/src/components/EditorNew/prosemirror/helpers/prompt.ts b/src/components/EditorNew/prosemirror/helpers/prompt.ts new file mode 100644 index 00000000..96a9528d --- /dev/null +++ b/src/components/EditorNew/prosemirror/helpers/prompt.ts @@ -0,0 +1,197 @@ +const prefix = 'ProseMirror-prompt' + +const createButton = ({ + textContent, + type = 'button', + className, + onClick +}: { + textContent: string + type?: 'button' | 'submit' + className: string + onClick?: () => void +}) => { + const button = document.createElement('button') + button.type = type + button.className = className + button.textContent = textContent + + if (onClick) { + button.addEventListener('click', onClick) + } + + return button +} + +// eslint-disable-next-line sonarjs/cognitive-complexity +export function openPrompt(options: { + title?: string + fields: Record + onSubmit: (values: Record) => void +}) { + const wrapper = document.body.appendChild(document.createElement('div')) + wrapper.className = prefix + + const mouseOutside = (ev: MouseEvent & { target: Node }) => { + if (!wrapper.contains(ev.target)) { + close() + } + } + + setTimeout(() => window.addEventListener('mousedown', mouseOutside), 50) + + const close = () => { + window.removeEventListener('mousedown', mouseOutside) + if (wrapper.parentNode) wrapper.remove() + } + + const domFields: HTMLElement[] = [] + + Object.keys(options.fields).forEach((name) => { + domFields.push(options.fields[name].render()) + }) + + const submitButton = createButton({ textContent: 'OK', type: 'submit', className: prefix + '-submit' }) + const cancelButton = createButton({ + className: prefix + '-cancel', + textContent: 'Cancel', + onClick: close + }) + + const form = wrapper.appendChild(document.createElement('form')) + + if (options.title) { + form.appendChild(document.createElement('h5')).textContent = options.title + } + + domFields.forEach((fieldEl: HTMLElement) => { + form.appendChild(document.createElement('div')).appendChild(fieldEl) + }) + + const buttons = form.appendChild(document.createElement('div')) + buttons.className = prefix + '-buttons' + buttons.appendChild(submitButton) + buttons.appendChild(document.createTextNode(' ')) + buttons.appendChild(cancelButton) + + const box = wrapper.getBoundingClientRect() + wrapper.style.top = (window.innerHeight - box.height) / 2 + 'px' + wrapper.style.left = (window.innerWidth - box.width) / 2 + 'px' + + const submit = () => { + const values = getValues(options.fields, domFields) + if (values) { + close() + options.onSubmit(values) + } + } + + form.addEventListener('submit', (e) => { + e.preventDefault() + submit() + }) + + form.addEventListener('keydown', (e) => { + if (e.key === 'Escape') { + e.preventDefault() + close() + } else if (e.key === 'Enter' && !(e.ctrlKey || e.metaKey || e.shiftKey)) { + e.preventDefault() + submit() + } else if (e.key === 'Tab') { + window.setTimeout(() => { + if (!wrapper.contains(document.activeElement)) close() + }, 500) + } + }) + + form.querySelector('input')?.focus() +} + +function getValues(fields: Record, domFields: HTMLElement[]) { + const result = {} + + // TODO: make field read its own value, maybe move to SolidJS + const fieldNames = Object.keys(fields) + + for (const [i, fieldName] of fieldNames.entries()) { + const field = fields[fieldName] + + const dom = domFields[i] + const value = field.read(dom) + const bad = field.validate(value) + + if (bad) { + reportInvalid(dom, bad) + return null + } + + result[fieldName] = field.clean(value) + } + + return result +} + +function reportInvalid(dom: HTMLElement, message: string) { + const msg: HTMLElement = dom.parentNode.appendChild(document.createElement('div')) + msg.style.left = dom.offsetLeft + dom.offsetWidth + 2 + 'px' + msg.style.top = dom.offsetTop - 5 + 'px' + msg.className = 'ProseMirror-invalid' + msg.textContent = message + setTimeout(msg.remove, 1500) +} + +export abstract class Field { + options: any + + constructor(options: any) { + this.options = options + } + + read(dom: any) { + return dom.value + } + + // :: (any) → ?string + // A field-type-specific validation function. + validateType(_value) { + return typeof _value === typeof '' + } + + validate(value: any) { + if (!value && this.options.required) return 'Required field' + + return this.validateType(value) || (this.options.validate && this.options.validate(value)) + } + + clean(value: any) { + return this.options.clean ? this.options.clean(value) : value + } + + abstract render(): HTMLElement +} + +export class TextField extends Field { + render() { + const input: HTMLInputElement = document.createElement('input') + + input.type = 'text' + input.placeholder = this.options.label + input.value = this.options.value || '' + input.autocomplete = 'off' + return input + } +} + +export class SelectField extends Field { + render() { + const select = document.createElement('select') + this.options.options.forEach((o: { value: string; label: string }) => { + const opt = select.appendChild(document.createElement('option')) + opt.value = o.value + opt.selected = o.value === this.options.value + opt.label = o.label + }) + return select + } +} diff --git a/src/components/EditorNew/prosemirror/plugins/customKeymap.ts b/src/components/EditorNew/prosemirror/plugins/customKeymap.ts new file mode 100644 index 00000000..0f06d3cd --- /dev/null +++ b/src/components/EditorNew/prosemirror/plugins/customKeymap.ts @@ -0,0 +1,18 @@ +import { baseKeymap } from 'prosemirror-commands' +import type { Command } from 'prosemirror-state' +import { redo, undo } from 'prosemirror-history' +import { keymap } from 'prosemirror-keymap' + +export const customKeymap = () => { + const bindings: { + [key: string]: Command + } = { + ...baseKeymap, + Tab: () => true, + // TODO: collab + [`Mod-z`]: undo, + [`Shift-Mod-z`]: redo + } + + return keymap(bindings) +} diff --git a/src/components/EditorNew/prosemirror/plugins/dragHandle.ts b/src/components/EditorNew/prosemirror/plugins/dragHandle.ts new file mode 100644 index 00000000..33d31f5d --- /dev/null +++ b/src/components/EditorNew/prosemirror/plugins/dragHandle.ts @@ -0,0 +1,48 @@ +import { Plugin, NodeSelection } from 'prosemirror-state' +import { DecorationSet, Decoration } from 'prosemirror-view' + +const handleIcon = ` + + + ` + +const createDragHandle = () => { + const handle = document.createElement('span') + handle.setAttribute('contenteditable', 'false') + const icon = document.createElement('span') + icon.innerHTML = handleIcon + handle.appendChild(icon) + handle.classList.add('handle') + return handle +} + +export const dragHandle = () => + new Plugin({ + props: { + decorations(state) { + const decos = [] + state.doc.forEach((node, pos) => { + decos.push( + Decoration.widget(pos + 1, createDragHandle), + Decoration.node(pos, pos + node.nodeSize, { class: 'draggable' }) + ) + }) + + return DecorationSet.create(state.doc, decos) + }, + handleDOMEvents: { + mousedown: (editorView, event: MouseEvent & { target: Element }) => { + const target = event.target + + if (target.classList.contains('handle')) { + const pos = editorView.posAtCoords({ left: event.x, top: event.y }) + const resolved = editorView.state.doc.resolve(pos.pos) + const tr = editorView.state.tr + tr.setSelection(NodeSelection.create(editorView.state.doc, resolved.before())) + editorView.dispatch(tr) + return false + } + } + } + } + }) diff --git a/src/components/EditorNew/prosemirror/plugins/image.ts b/src/components/EditorNew/prosemirror/plugins/image.ts new file mode 100644 index 00000000..a07c64f6 --- /dev/null +++ b/src/components/EditorNew/prosemirror/plugins/image.ts @@ -0,0 +1,50 @@ +import { Plugin } from 'prosemirror-state' +import type { DiscoursSchema } from '../schema' + +const REGEX = /^!\[([^[\]]*?)]\((.+?)\)\s+/ +const MAX_MATCH = 500 + +const isUrl = (str: string) => { + try { + const url = new URL(str) + return url.protocol === 'http:' || url.protocol === 'https:' + } catch { + return false + } +} + +const isBlank = (text: string) => text === ' ' || text === '\u00A0' + +export const imageInput = (schema: DiscoursSchema) => + new Plugin({ + props: { + handleTextInput(view, from, to, text) { + if (view.composing || !isBlank(text)) return false + const $from = view.state.doc.resolve(from) + if ($from.parent.type.spec.code) return false + const textBefore = + $from.parent.textBetween( + Math.max(0, $from.parentOffset - MAX_MATCH), + $from.parentOffset, + null, + '\uFFFC' + ) + text + + const match = REGEX.exec(textBefore) + if (match) { + const [, title, src] = match + if (isUrl(src)) { + const node = schema.node('image', { src, title }) + const start = from - (match[0].length - text.length) + const tr = view.state.tr + tr.delete(start, to) + tr.insert(start, node) + view.dispatch(tr) + return true + } + + return false + } + } + } + }) diff --git a/src/components/EditorNew/prosemirror/plugins/index.ts b/src/components/EditorNew/prosemirror/plugins/index.ts new file mode 100644 index 00000000..7a4f6439 --- /dev/null +++ b/src/components/EditorNew/prosemirror/plugins/index.ts @@ -0,0 +1,26 @@ +import { keymap } from 'prosemirror-keymap' +import { baseKeymap } from 'prosemirror-commands' +import { history } from 'prosemirror-history' +import { dropCursor } from 'prosemirror-dropcursor' +import { placeholder } from './placeholder' +import { t } from '../../../../utils/intl' +import styles from '../styles/ProseMirror.module.scss' +import type { DiscoursSchema } from '../schema' +import { dragHandle } from './dragHandle' +import { selectionMenu } from './selectionMenu' +import { imageInput } from './image' +import { customKeymap } from './customKeymap' + +export const createPlugins = ({ schema }: { schema: DiscoursSchema }) => { + return [ + placeholder(t('Just start typing...')), + customKeymap(), + history(), + dropCursor({ class: styles.dropCursor }), + selectionMenu(schema), + dragHandle(), + imageInput(schema) + // TODO + // link(), + ] +} diff --git a/src/components/EditorNew/prosemirror/plugins/placeholder.ts b/src/components/EditorNew/prosemirror/plugins/placeholder.ts new file mode 100644 index 00000000..25a4142e --- /dev/null +++ b/src/components/EditorNew/prosemirror/plugins/placeholder.ts @@ -0,0 +1,23 @@ +import { Plugin } from 'prosemirror-state' +import { DecorationSet, Decoration } from 'prosemirror-view' +import styles from '../styles/ProseMirror.module.scss' + +export const placeholder = (text: string): Plugin => + new Plugin({ + props: { + decorations(state) { + const { doc } = state + + if (doc.childCount > 1 || !doc.firstChild.isTextblock || doc.firstChild.content.size > 0) { + return + } + + const div = document.createElement('div') + div.setAttribute('contenteditable', 'false') + div.classList.add(styles.placeholder) + div.textContent = text + + return DecorationSet.create(doc, [Decoration.widget(1, div)]) + } + } + }) diff --git a/src/components/EditorNew/prosemirror/plugins/selectionMenu.ts b/src/components/EditorNew/prosemirror/plugins/selectionMenu.ts new file mode 100644 index 00000000..2de17ac7 --- /dev/null +++ b/src/components/EditorNew/prosemirror/plugins/selectionMenu.ts @@ -0,0 +1,53 @@ +import { renderGrouped } from 'prosemirror-menu' +import { EditorState, Plugin } from 'prosemirror-state' +import styles from '../styles/ProseMirror.module.scss' +import type { EditorView } from 'prosemirror-view' +import type { DiscoursSchema } from '../schema' +import { buildMenuItems } from '../helpers/menu' + +export class SelectionMenuView { + tooltip: HTMLDivElement + + constructor(view: EditorView, schema: DiscoursSchema) { + this.tooltip = document.createElement('div') + this.tooltip.className = styles.selectionMenu + view.dom.parentNode.appendChild(this.tooltip) + const { dom } = renderGrouped(view, buildMenuItems(schema)) + this.tooltip.appendChild(dom) + this.update(view, null) + } + + update(view: EditorView, lastState: EditorState) { + const state = view.state + + if (lastState && lastState.doc.eq(state.doc) && lastState.selection.eq(state.selection)) { + return + } + + if (state.selection.empty) { + this.tooltip.style.display = 'none' + return + } + + this.tooltip.style.display = '' + const { from, to } = state.selection + const start = view.coordsAtPos(from) + const end = view.coordsAtPos(to) + const box = this.tooltip.offsetParent.getBoundingClientRect() + const width = this.tooltip.getBoundingClientRect().width + const left = (start.left + end.left - width) / 2 + this.tooltip.style.left = `${left - box.left}px` + this.tooltip.style.bottom = `${box.bottom - start.top + 8}px` + } + + destroy() { + this.tooltip.remove() + } +} + +export const selectionMenu = (schema: DiscoursSchema) => + new Plugin({ + view(editorView: EditorView) { + return new SelectionMenuView(editorView, schema) + } + }) diff --git a/src/components/EditorNew/prosemirror/schema.ts b/src/components/EditorNew/prosemirror/schema.ts new file mode 100644 index 00000000..47b39188 --- /dev/null +++ b/src/components/EditorNew/prosemirror/schema.ts @@ -0,0 +1,172 @@ +import { Node, Schema, SchemaSpec } from 'prosemirror-model' + +export type Nodes = + | 'doc' + | 'paragraph' + | 'text' + | 'heading' + | 'ordered_list' + | 'bullet_list' + | 'list_item' + | 'blockquote' + | 'image' + | 'embed' + +export type Marks = 'strong' | 'em' | 'strikethrough' | 'note' | 'link' | 'highlight' + +export type DiscoursSchema = Schema + +export const schemaSpec: SchemaSpec = { + nodes: { + doc: { + content: 'block+' + }, + paragraph: { + content: 'inline*', + group: 'block', + parseDOM: [{ tag: 'p' }], + toDOM: () => ['p', 0] + }, + text: { + group: 'inline' + }, + heading: { + attrs: { level: { default: 1 } }, + content: 'inline*', + group: 'block', + defining: true, + parseDOM: [ + { tag: 'h1', attrs: { level: 1 } }, + { tag: 'h2', attrs: { level: 2 } }, + { tag: 'h3', attrs: { level: 3 } } + ], + toDOM(node) { + return ['h' + node.attrs.level, 0] + } + }, + ordered_list: { + group: 'block', + content: 'list_item+', + attrs: { order: { default: 1 } }, + parseDOM: [ + { + tag: 'ol', + getAttrs(dom: HTMLElement) { + return { order: dom.hasAttribute('start') ? +dom.getAttribute('start') : 1 } + } + } + ], + toDOM(node) { + return node.attrs.order === 1 ? ['ol', 0] : ['ol', { start: node.attrs.order }, 0] + } + }, + bullet_list: { + group: 'block', + content: 'list_item+', + parseDOM: [{ tag: 'ul' }], + toDOM() { + return ['ul', 0] + } + }, + list_item: { + content: 'paragraph block*', + parseDOM: [{ tag: 'li' }], + toDOM() { + return ['li', 0] + }, + defining: true + }, + blockquote: { + content: 'block+', + group: 'block', + defining: true, + parseDOM: [{ tag: 'blockquote' }], + toDOM() { + return ['blockquote', 0] + } + }, + embed: {}, + /// + image: { + inline: true, + attrs: { + src: {}, + alt: { default: null }, + title: { default: null }, + path: { default: null }, + width: { default: null } + }, + group: 'inline', + draggable: true, + parseDOM: [ + { + tag: 'img[src]', + getAttrs: (dom: HTMLElement) => ({ + src: dom.getAttribute('src'), + title: dom.getAttribute('title'), + alt: dom.getAttribute('alt'), + path: dom.dataset.path + }) + } + ], + toDOM: (node: Node) => [ + 'img', + { + src: node.attrs.src, + title: node.attrs.title, + alt: node.attrs.alt, + 'data-path': node.attrs.path + } + ] + } + }, + marks: { + strong: { + parseDOM: [ + { tag: 'strong' }, + // This works around a Google Docs misbehavior where + // pasted content will be inexplicably wrapped in `` + // tags with a font-weight normal. + { tag: 'b', getAttrs: (node: HTMLElement) => node.style.fontWeight !== 'normal' && null }, + { + style: 'font-weight', + getAttrs: (value: string) => /^(bold(er)?|[5-9]\d{2,})$/.test(value) && null + } + ], + toDOM() { + return ['strong', 0] + } + }, + em: { + parseDOM: [{ tag: 'i' }, { tag: 'em' }, { style: 'font-style=italic' }], + toDOM() { + return ['em', 0] + } + }, + link: { + attrs: { + href: {}, + title: { default: null } + }, + inclusive: false, + parseDOM: [ + { + tag: 'a[href]', + getAttrs(dom: HTMLElement) { + return { href: dom.getAttribute('href'), title: dom.getAttribute('title') } + } + } + ], + toDOM(node) { + const { href, title } = node.attrs + return ['a', { href, title }, 0] + } + }, + // TODO: + highlight: {}, + strikethrough: {}, + note: {} + } +} + +export const schema = new Schema(schemaSpec) diff --git a/src/components/EditorNew/prosemirror/styles/ProseMirror.module.scss b/src/components/EditorNew/prosemirror/styles/ProseMirror.module.scss new file mode 100644 index 00000000..24cde362 --- /dev/null +++ b/src/components/EditorNew/prosemirror/styles/ProseMirror.module.scss @@ -0,0 +1,21 @@ +.dropCursor { + // TODO check why important + height: 2px !important; + opacity: 0.5; +} + +.selectionMenu { + background: #fff; + box-shadow: 0 4px 10px rgb(0 0 0 / 25%); + color: #000; + display: flex; + position: absolute; + z-index: 100; +} + +.placeholder { + opacity: 0.3; + position: absolute; + pointer-events: none; + user-select: none; +} diff --git a/src/components/EditorNew/prosemirror/styles/ProseMirror.scss b/src/components/EditorNew/prosemirror/styles/ProseMirror.scss new file mode 100644 index 00000000..07f1982e --- /dev/null +++ b/src/components/EditorNew/prosemirror/styles/ProseMirror.scss @@ -0,0 +1,329 @@ +.ProseMirror { + color: var(--foreground); + background-color: var(--background); + position: relative; + word-wrap: break-word; + white-space: pre-wrap; + font-variant-ligatures: none; + outline: none; + margin: 1em 1em 1em 0; + + .dark & { + color: var(--background); + background-color: var(--foreground); + } + + .draggable { + position: relative; + margin-left: -30px; + padding-left: 30px; + } + + .handle { + position: absolute; + left: 0; + top: 0; + height: calc(var(--font-fize) * 1.6px); + opacity: 0; + cursor: move; + transition: opacity 0.3s; + display: inline-flex; + align-items: center; + justify-content: center; + + > span { + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 3px; + padding: 6px; + fill: var(--foreground); + pointer-events: none; + user-select: none; + } + + &:hover > span { + background: var(--foreground); + } + } + + h1 .handle { + height: calc(var(--font-size) * 2.3px); + } + + .draggable:hover .handle { + opacity: 1; + } + + blockquote { + border-left: 2px solid; + @include font-size(1.6rem); + + margin: 1.5em 0; + padding-left: 1.6em; + } +} + +.ProseMirror-menuitem { + display: flex; + font-size: small; + + &:hover { + > * { + background: #eee; + } + + .ProseMirror-menu-disabled { + background: inherit; + } + } + + > * { + cursor: pointer; + align-items: center; + display: flex; + padding: 0.8rem 1em; + } +} + +.ProseMirror-textblock-dropdown { + min-width: 3em; +} + +.ProseMirror-menu { + margin: 0 -4px; + line-height: 1; +} + +.ProseMirror-tooltip .ProseMirror-menu { + width: fit-content; + white-space: pre; +} + +.ProseMirror-menuseparator { + border-right: 1px solid #ddd; +} + +.ProseMirror-menu-dropdown, +.ProseMirror-menu-dropdown-menu { + padding: 4px; + white-space: nowrap; +} + +.ProseMirror-menu-dropdown { + vertical-align: 1px; + cursor: pointer; + position: relative; + padding-right: 15px; +} + +.ProseMirror-menu-dropdown-wrap { + padding: 1px 0 1px 4px; + display: inline-block; + position: relative; +} + +.ProseMirror-menu-dropdown::after { + content: ''; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 4px solid currentcolor; + opacity: 0.6; + position: absolute; + right: 4px; + top: calc(50% - 2px); +} + +.ProseMirror-menu-dropdown-menu, +.ProseMirror-menu-submenu { + position: absolute; + background: white; + color: #666; + border: 1px solid #aaa; + padding: 2px; +} + +.ProseMirror-menu-dropdown-menu { + z-index: 15; + + /* min-width: 6em; */ +} + +.ProseMirror-menu-dropdown-item { + cursor: pointer; + padding: 2px 8px 2px 4px; +} + +.ProseMirror-menu-dropdown-item:hover { + background: #f2f2f2; +} + +.ProseMirror-menu-submenu-wrap { + position: relative; + margin-right: -4px; +} + +.ProseMirror-menu-submenu-label::after { + content: ''; + border-top: 4px solid transparent; + border-bottom: 4px solid transparent; + border-left: 4px solid currentcolor; + opacity: 0.6; + position: absolute; + right: 4px; + top: calc(50% - 4px); +} + +.ProseMirror-menu-submenu { + display: none; + left: 100%; + top: -3px; +} + +.ProseMirror-menu-active { + background: #eee; +} + +.ProseMirror-menu-disabled { + cursor: default; + opacity: 0.3; +} + +.ProseMirror-menu-submenu-wrap:hover .ProseMirror-menu-submenu, +.ProseMirror-menu-submenu-wrap-active .ProseMirror-menu-submenu { + display: block; +} + +.ProseMirror-menubar { + border-top-left-radius: inherit; + border-top-right-radius: inherit; + display: flex; + position: relative; + min-height: 1em; + color: #666; + padding: 0 1.5em; + top: 0; + left: 0; + right: 0; + border-bottom: 1px solid silver; + background: white; + z-index: 10; + box-sizing: border-box; + overflow: visible; +} + +.ProseMirror-icon { + cursor: pointer; + line-height: 0.8; +} + +.ProseMirror-menu-disabled.ProseMirror-icon { + cursor: default; +} + +.ProseMirror-icon svg { + fill: currentcolor; + height: 1em; +} + +.ProseMirror-icon span { + vertical-align: text-top; +} + +.ProseMirror pre { + white-space: pre-wrap; +} + +.ProseMirror li { + position: relative; +} + +.ProseMirror-hideselection *::selection { + background: transparent; +} + +.ProseMirror-hideselection { + caret-color: transparent; +} + +.ProseMirror-selectednode { + outline: 2px solid #8cf; +} + +/* Make sure li selections wrap around markers */ +li.ProseMirror-selectednode { + outline: none; +} + +li.ProseMirror-selectednode::after { + content: ''; + position: absolute; + left: -32px; + right: -2px; + top: -2px; + bottom: -2px; + border: 2px solid #8cf; + pointer-events: none; +} + +.ProseMirror .empty-node::before { + position: absolute; + color: #aaa; + cursor: text; +} + +.ProseMirror .empty-node:hover::before { + color: #777; +} + +.ProseMirror.editor_empty::before { + position: absolute; + content: attr(data-placeholder); + pointer-events: none; + color: var(--ui-color-placeholder); +} + +.ProseMirror-prompt { + background: #fff; + box-shadow: 0 4px 10px rgb(0 0 0 / 25%); + font-size: 0.7em; + position: absolute; +} + +.ProseMirror-prompt input[type='text'] { + border: none; + font-size: 100%; + margin-bottom: 0; + padding: 0.5em 7.5em 0.5em 0.5em; +} + +.ProseMirror-prompt-buttons { + position: absolute; + right: 0; + top: 50%; + transform: translateY(-50%); + height: 100%; +} + +.ProseMirror-prompt-buttons button { + border: none; + cursor: pointer; + display: inline-block; + font-size: 90%; + height: 100%; + line-height: 10em; + margin-bottom: 0; + overflow: hidden; + vertical-align: top; + width: 2.5em; +} + +.ProseMirror-prompt-submit { + background: url("data:image/svg+xml,%3Csvg width='19' height='15' viewBox='0 0 19 15' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M19 2.73787L16.2621 0L6.78964 9.47248L2.73787 5.42071L0 8.15858L6.78964 14.9482L19 2.73787Z' fill='%23393840'/%3E%3C/svg%3E") + center no-repeat; +} + +.ProseMirror-prompt-cancel { + background: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M13.1512 0.423856L0.423263 13.1518L2.84763 15.5761L15.5756 2.84822L13.1512 0.423856Z M15.5755 13.1518L2.84763 0.423855L0.423263 2.84822L13.1512 15.5761L15.5755 13.1518Z' fill='%23393840'/%3E%3C/svg%3E%0A") + center no-repeat; +} diff --git a/src/components/EditorNew/prosemirror/views/image.ts b/src/components/EditorNew/prosemirror/views/image.ts new file mode 100644 index 00000000..d53bf0dd --- /dev/null +++ b/src/components/EditorNew/prosemirror/views/image.ts @@ -0,0 +1,68 @@ +import type { EditorView, NodeView, NodeViewConstructor } from 'prosemirror-view' +import type { Node } from 'prosemirror-model' + +class ImageView implements NodeView { + node: Node + view: EditorView + getPos: () => number + dom: Element + container: HTMLElement + handle: HTMLElement + onResizeFn: any + onResizeEndFn: any + width: number + updating: number + + constructor(node: Node, view: EditorView, getPos: () => number) { + this.node = node + this.view = view + this.getPos = getPos + this.onResizeFn = this.onResize.bind(this) + this.onResizeEndFn = this.onResizeEnd.bind(this) + + this.container = document.createElement('span') + this.container.className = 'image-container' + if (node.attrs.width) this.setWidth(node.attrs.width) + + const image = document.createElement('img') + image.setAttribute('title', node.attrs.title ?? '') + image.setAttribute('src', node.attrs.src) + + this.handle = document.createElement('span') + this.handle.className = 'resize-handle' + this.handle.addEventListener('mousedown', (e) => { + e.preventDefault() + window.addEventListener('mousemove', this.onResizeFn) + window.addEventListener('mouseup', this.onResizeEndFn) + }) + + this.container.appendChild(image) + this.container.appendChild(this.handle) + this.dom = this.container + } + + onResize(e: MouseEvent) { + this.width = e.pageX - this.container.getBoundingClientRect().left + this.setWidth(this.width) + } + + onResizeEnd() { + window.removeEventListener('mousemove', this.onResizeFn) + if (this.updating === this.width) return + this.updating = this.width + const tr = this.view.state.tr + tr.setNodeMarkup(this.getPos(), undefined, { + ...this.node.attrs, + width: this.width + }) + + this.view.dispatch(tr) + } + + setWidth(width: number) { + this.container.style.width = width + 'px' + } +} + +export const createImageView: NodeViewConstructor = (node: Node, view: EditorView, getPos: () => number) => + new ImageView(node, view, getPos) diff --git a/src/components/Feed/Beside.module.scss b/src/components/Feed/Beside.module.scss index fb3d7b66..1e5486ab 100644 --- a/src/components/Feed/Beside.module.scss +++ b/src/components/Feed/Beside.module.scss @@ -12,8 +12,6 @@ } li { - margin-bottom: 1em; - &.top { border-bottom: 1px solid #e1e1e1; display: flex; diff --git a/src/components/Nav/Modal.scss b/src/components/Nav/Modal.scss index 8c7bc2f0..6be20367 100644 --- a/src/components/Nav/Modal.scss +++ b/src/components/Nav/Modal.scss @@ -1,12 +1,11 @@ .modalwrap { + pointer-events: all; align-items: center; background: rgb(20 20 20 / 70%); display: flex; justify-content: center; height: 100%; left: 0; - overflow: auto; - pointer-events: all; position: fixed; top: 0; width: 100%; diff --git a/src/components/Topic/Full.module.scss b/src/components/Topic/Full.scss similarity index 66% rename from src/components/Topic/Full.module.scss rename to src/components/Topic/Full.scss index f0c39156..ef8c4baf 100644 --- a/src/components/Topic/Full.module.scss +++ b/src/components/Topic/Full.scss @@ -1,19 +1,11 @@ -.topicHeader { +.topic__header { @include font-size(1.7rem); padding-top: 5.8rem; text-align: center; - - h1 { - color: #2638d9; - font-weight: 500; - text-transform: uppercase; - - @include font-size(2rem); - } } -.topicActions { +.topic__actions { margin-top: 2.8rem; button, diff --git a/src/components/Topic/Full.tsx b/src/components/Topic/Full.tsx index c6bb57fa..5923ead9 100644 --- a/src/components/Topic/Full.tsx +++ b/src/components/Topic/Full.tsx @@ -1,11 +1,10 @@ import { createMemo, Show } from 'solid-js' import type { Topic } from '../../graphql/types.gen' import { FollowingEntity } from '../../graphql/types.gen' -import styles from './Full.module.scss' +import './Full.scss' import { useAuthStore } from '../../stores/auth' import { follow, unfollow } from '../../stores/zine/common' import { t } from '../../utils/intl' -import { clsx } from 'clsx' type Props = { topic: Topic @@ -16,35 +15,37 @@ export const FullTopic = (props: Props) => { const subscribed = createMemo(() => session()?.news?.topics?.includes(props.topic?.slug)) return ( -
        - -
        -

        #{props.topic.title}

        -

        {props.topic.body}

        -
        - - +
        +
        + +
        +

        #{props.topic.title}

        +

        {props.topic.body}

        +
        + + + + + + + {t('Write about the topic')} +
        + + {props.topic.title} - - - - {t('Write about the topic')}
        - - {props.topic.title} - -
        - + +
        ) } diff --git a/src/components/Views/Author.tsx b/src/components/Views/Author.tsx index 59278d34..52cd189f 100644 --- a/src/components/Views/Author.tsx +++ b/src/components/Views/Author.tsx @@ -7,6 +7,7 @@ import { t } from '../../utils/intl' import { useAuthorsStore } from '../../stores/zine/authors' import { loadAuthorArticles, useArticlesStore } from '../../stores/zine/articles' +import '../../styles/Topic.scss' import { useTopicsStore } from '../../stores/zine/topics' import { useRouter } from '../../stores/router' import { Beside } from '../Feed/Beside' diff --git a/src/components/Views/Create.tsx b/src/components/Views/Create.tsx index 757ea2b2..2281ddf5 100644 --- a/src/components/Views/Create.tsx +++ b/src/components/Views/Create.tsx @@ -1,72 +1,16 @@ -import { Show, onCleanup, createEffect, onError, onMount, untrack, createSignal } from 'solid-js' -import { createMutable, unwrap } from 'solid-js/store' -import { State, StateContext, newState } from '../Editor/store/context' -import { createCtrl } from '../Editor/store/actions' -import { Layout } from '../Editor/components/Layout' -import { Editor } from '../Editor/components/Editor' -import { Sidebar } from '../Editor/components/Sidebar' -import ErrorView from '../Editor/components/Error' - -const matchDark = () => window.matchMedia('(prefers-color-scheme: dark)') +import { Show, onMount, createSignal } from 'solid-js' +import { Editor } from '../EditorNew/Editor' export const CreateView = () => { + // don't render anything on server + // usage of isServer causing hydration errors const [isMounted, setIsMounted] = createSignal(false) onMount(() => setIsMounted(true)) - const onChangeTheme = () => ctrl.updateTheme() - onMount(() => { - matchDark().addEventListener('change', onChangeTheme) - onCleanup(() => matchDark().removeEventListener('change', onChangeTheme)) - }) - - const [store, ctrl] = createCtrl(newState()) - const mouseEnterCoords = createMutable({ x: 0, y: 0 }) - - const onMouseEnter = (e: MouseEvent) => { - mouseEnterCoords.x = e.pageX - mouseEnterCoords.y = e.pageY - } - - onMount(async () => { - console.debug('[create] view mounted') - if (store.error) { - console.error(store.error) - return - } - await ctrl.init() - }) - - onError((error) => { - console.error('[create] error:', error) - ctrl.setState({ error: { id: 'exception', props: { error } } }) - }) - - createEffect((prev) => { - const lastModified = store.lastModified - if (!lastModified || (store.loading === 'initialized' && prev === 'loading')) { - return store.loading - } - const state: State = untrack(() => unwrap(store)) - ctrl.saveState(state) - console.debug('[create] status update') - return store.loading - }, store.loading) - return ( - - - }> - - - - - + ) } diff --git a/src/graphql/mutation/article-create.ts b/src/graphql/mutation/article-create.ts index 62523e87..0f2ef0ba 100644 --- a/src/graphql/mutation/article-create.ts +++ b/src/graphql/mutation/article-create.ts @@ -9,13 +9,11 @@ export default gql` slug title subtitle - image body topics { _id: slug title slug - image } authors { _id: slug diff --git a/src/graphql/types.gen.ts b/src/graphql/types.gen.ts index 5b233587..cde59629 100644 --- a/src/graphql/types.gen.ts +++ b/src/graphql/types.gen.ts @@ -31,29 +31,29 @@ export type Author = { } export type Chat = { - createdAt: Scalars['DateTime'] + createdAt: Scalars['Int'] createdBy: User description?: Maybe - id: Scalars['Int'] - messages?: Maybe>> + id: Scalars['String'] + messages: Array> title?: Maybe - updatedAt: Scalars['DateTime'] + unread?: Maybe + updatedAt: Scalars['Int'] users: Array> } -export type ChatResult = { - createdAt: Scalars['DateTime'] - createdBy?: Maybe - error?: Maybe - members: Array> - messages?: Maybe>> - title?: Maybe +export type ChatInput = { + description?: InputMaybe + id: Scalars['String'] + title?: InputMaybe } -export type ChatUpdatedResult = { - error?: Maybe - message?: Maybe - status?: Maybe +export type ChatMember = { + invitedAt?: Maybe + invitedBy?: Maybe + name: Scalars['String'] + pic?: Maybe + slug: Scalars['String'] } export type Collab = { @@ -95,17 +95,6 @@ export type CommunityInput = { title: Scalars['String'] } -export type CreateChatResult = { - chatId?: Maybe - error?: Maybe -} - -export type EnterChatResult = { - chat?: Maybe - error?: Maybe - messages?: Maybe>> -} - export enum FollowingEntity { Author = 'AUTHOR', Community = 'COMMUNITY', @@ -116,17 +105,11 @@ export enum FollowingEntity { export type Message = { author: Scalars['String'] body: Scalars['String'] - chatRoom: Scalars['Int'] - createdAt: Scalars['DateTime'] + chatId: Scalars['String'] + createdAt: Scalars['Int'] id: Scalars['Int'] - replyTo?: Maybe - updatedAt: Scalars['DateTime'] - visibleForUsers: Array> -} - -export type MessageResult = { - error?: Maybe - message?: Maybe + replyTo?: Maybe + updatedAt?: Maybe } export enum MessageStatus { @@ -137,10 +120,10 @@ export enum MessageStatus { export type Mutation = { confirmEmail: AuthResult - createChat: CreateChatResult + createChat: Result createCollection: Result createCommunity: Result - createMessage: MessageResult + createMessage: Result createReaction: Result createShout: Result createTopic: Result @@ -150,6 +133,7 @@ export type Mutation = { deleteReaction: Result deleteShout: Result destroyTopic: Result + enterChat: Result follow: Result incrementView: Result inviteAuthor: Result @@ -161,9 +145,10 @@ export type Mutation = { removeAuthor: Result sendLink: Result unfollow: Result + updateChat: Result updateCollection: Result updateCommunity: Result - updateMessage: MessageResult + updateMessage: Result updateProfile: Result updateReaction: Result updateShout: Result @@ -171,11 +156,12 @@ export type Mutation = { } export type MutationConfirmEmailArgs = { - code: Scalars['String'] + token: Scalars['String'] } export type MutationCreateChatArgs = { - description?: InputMaybe + members: Array> + title?: InputMaybe } export type MutationCreateCollectionArgs = { @@ -189,7 +175,7 @@ export type MutationCreateCommunityArgs = { export type MutationCreateMessageArgs = { body: Scalars['String'] chatId: Scalars['String'] - replyTo?: InputMaybe + replyTo?: InputMaybe } export type MutationCreateReactionArgs = { @@ -229,6 +215,10 @@ export type MutationDestroyTopicArgs = { slug: Scalars['String'] } +export type MutationEnterChatArgs = { + chatId: Scalars['String'] +} + export type MutationFollowArgs = { slug: Scalars['String'] what: FollowingEntity @@ -279,6 +269,10 @@ export type MutationUnfollowArgs = { what: FollowingEntity } +export type MutationUpdateChatArgs = { + chat: ChatInput +} + export type MutationUpdateCollectionArgs = { collection: CollectionInput } @@ -335,7 +329,6 @@ export type ProfileInput = { export type Query = { authorsAll: Array> collectionsAll: Array> - enterChat: ChatResult getCollabs: Array> getCommunities: Array> getCommunity: Community @@ -344,9 +337,9 @@ export type Query = { getUserRoles: Array> getUsersBySlugs: Array> isEmailUsed: Scalars['Boolean'] - loadChat: Array> + loadChat: Result markdownBody: Scalars['String'] - myChats: Array> + myChats: Result reactionsByAuthor: Array> reactionsForShouts: Array> recentAll: Array> @@ -378,10 +371,6 @@ export type Query = { userReactedShouts: Array> } -export type QueryEnterChatArgs = { - chatId: Scalars['String'] -} - export type QueryGetCommunityArgs = { slug?: InputMaybe } @@ -407,9 +396,9 @@ export type QueryIsEmailUsedArgs = { } export type QueryLoadChatArgs = { + amount?: InputMaybe chatId: Scalars['String'] - page: Scalars['Int'] - size: Scalars['Int'] + offset?: InputMaybe } export type QueryMarkdownBodyArgs = { @@ -619,9 +608,14 @@ export type Resource = { export type Result = { author?: Maybe authors?: Maybe>> + chat?: Maybe + chats?: Maybe>> communities?: Maybe>> community?: Maybe error?: Maybe + members?: Maybe>> + message?: Maybe + messages?: Maybe>> reaction?: Maybe reactions?: Maybe>> shout?: Maybe @@ -674,7 +668,7 @@ export type ShoutInput = { topic_slugs?: InputMaybe>> versionOf?: InputMaybe visibleForRoles?: InputMaybe>> - visibleForUsers?: InputMaybe>> + visibleForUsers?: InputMaybe>> } export type Stat = { @@ -686,15 +680,15 @@ export type Stat = { } export type Subscription = { - chatUpdated: ChatUpdatedResult + newMessage: Message onlineUpdated: Array reactionUpdated: ReactionUpdating shoutUpdated: Shout userUpdated: User } -export type SubscriptionChatUpdatedArgs = { - chatId: Scalars['String'] +export type SubscriptionNewMessageArgs = { + chats?: InputMaybe> } export type SubscriptionReactionUpdatedArgs = { diff --git a/src/locales/ru.json b/src/locales/ru.json index bf90bb84..e8b1a94d 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -168,5 +168,6 @@ "We've sent you a message with a link to enter our website.": "Мы выслали вам письмо с ссылкой на почту. Перейдите по ссылке в письме, чтобы войти на сайт.", "Send link again": "Прислать ссылку ещё раз", "Link sent, check your email": "Ссылка отправлена, проверьте почту", - "Create post": "Создать публикацию" + "Create post": "Создать публикацию", + "Just start typing...": "Просто начните печатать..." } diff --git a/src/stores/editor.ts b/src/stores/editor.ts index 3868646d..8acb0c42 100644 --- a/src/stores/editor.ts +++ b/src/stores/editor.ts @@ -2,7 +2,8 @@ import { persistentMap } from '@nanostores/persistent' import type { Reaction } from '../graphql/types.gen' import { atom } from 'nanostores' import { createSignal } from 'solid-js' -import type { Draft } from '../components/Editor/store/context' + +// import type { Draft } from '../components/EditorExample/store/context' interface Collab { authors: string[] // slugs @@ -12,7 +13,7 @@ interface Collab { title?: string } -export const drafts = persistentMap<{ [key: string]: Draft }>( +export const drafts = persistentMap<{ [key: string]: string }>( 'drafts', {}, { diff --git a/src/stores/inbox.ts b/src/stores/inbox.ts index 7223b188..bbf7c471 100644 --- a/src/stores/inbox.ts +++ b/src/stores/inbox.ts @@ -1,4 +1,4 @@ import { atom } from 'nanostores' -import type { ChatResult } from '../graphql/types.gen' +import type { Chat } from '../graphql/types.gen' -export const chats = atom([]) +export const chats = atom([]) diff --git a/src/stores/zine/articles.ts b/src/stores/zine/articles.ts index b53790ba..a7ac1eee 100644 --- a/src/stores/zine/articles.ts +++ b/src/stores/zine/articles.ts @@ -1,4 +1,4 @@ -import type { Author, Shout, Topic } from '../../graphql/types.gen' +import type { Author, Shout, ShoutInput, Topic } from '../../graphql/types.gen' import { apiClient } from '../../utils/apiClient' import { addAuthorsByTopic } from './authors' import { addTopicsByAuthor } from './topics' @@ -272,6 +272,14 @@ export const loadArticle = async ({ slug }: { slug: string }): Promise => addArticles([article]) } +export const createArticle = async ({ article }: { article: ShoutInput }) => { + try { + await apiClient.createArticle({ article }) + } catch (error) { + console.error(error) + } +} + type InitialState = { sortedArticles?: Shout[] topRatedArticles?: Shout[] diff --git a/src/styles/Topic.module.scss b/src/styles/Topic.scss similarity index 82% rename from src/styles/Topic.module.scss rename to src/styles/Topic.scss index fa18beab..20b16c28 100644 --- a/src/styles/Topic.module.scss +++ b/src/styles/Topic.scss @@ -1,11 +1,11 @@ -.topicPage { - .groupControls { +.topic-page { + .group__controls { align-items: baseline; margin-bottom: 4rem; margin-top: 7rem; } - .floorImportant { + .floor--important { a:hover { background: #fff; color: #000 !important; diff --git a/src/utils/apiClient.ts b/src/utils/apiClient.ts index 670d7b8d..d798488e 100644 --- a/src/utils/apiClient.ts +++ b/src/utils/apiClient.ts @@ -1,4 +1,4 @@ -import type { Reaction, Shout, FollowingEntity, AuthResult } from '../graphql/types.gen' +import type { Reaction, Shout, FollowingEntity, AuthResult, ShoutInput } from '../graphql/types.gen' import { publicGraphQLClient } from '../graphql/publicGraphQLClient' import { privateGraphQLClient } from '../graphql/privateGraphQLClient' import articleBySlug from '../graphql/query/article-by-slug' @@ -27,6 +27,7 @@ import reactionDestroy from '../graphql/mutation/reaction-destroy' import reactionUpdate from '../graphql/mutation/reaction-update' import authorsBySlugs from '../graphql/query/authors-by-slugs' import incrementView from '../graphql/mutation/increment-view' +import createArticle from '../graphql/mutation/article-create' import myChats from '../graphql/query/my-chats' const FEED_SIZE = 50 @@ -307,6 +308,11 @@ export const apiClient = { const response = await publicGraphQLClient.query(authorsBySlugs, { slugs }).toPromise() return response.data.getUsersBySlugs }, + createArticle: async ({ article }: { article: ShoutInput }) => { + const response = await privateGraphQLClient.mutation(createArticle, { shout: article }).toPromise() + console.debug('createArticle response:', response) + return response.data.createShout + }, createReaction: async ({ reaction }) => { const response = await privateGraphQLClient.mutation(reactionCreate, { reaction }).toPromise() console.debug('[api-client] [api] create reaction mutation called') diff --git a/src/utils/config.ts b/src/utils/config.ts index 75f03f39..ab0ab70b 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -1,4 +1,4 @@ export const isDev = import.meta.env.MODE === 'development' export const apiBaseUrl = 'https://newapi.discours.io' -// export const apiBaseUrl = 'http://localhost:8000' +// export const apiBaseUrl = 'http://localhost:8080' diff --git a/yarn.lock b/yarn.lock index 764e2393..786a141a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7928,20 +7928,13 @@ prosemirror-menu@^1.0.0, prosemirror-menu@^1.2.1: prosemirror-history "^1.0.0" prosemirror-state "^1.0.0" -prosemirror-model@^1.0.0, prosemirror-model@^1.16.0, prosemirror-model@^1.2.0: +prosemirror-model@^1.0.0, prosemirror-model@^1.16.0: version "1.18.1" resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.18.1.tgz#1d5d6b6de7b983ee67a479dc607165fdef3935bd" integrity sha512-IxSVBKAEMjD7s3n8cgtwMlxAXZrC7Mlag7zYsAKDndAqnDScvSmp/UdnRTV/B33lTCVU3CCm7dyAn/rVVD0mcw== dependencies: orderedmap "^2.0.0" -prosemirror-schema-basic@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.0.tgz#c33ad74426efae1d41e2260371866f623e8eb10e" - integrity sha512-JMN/ammP94ObOUS6cpIy121r0MEDN9V95mAxFVALwC4bbmhpWXGjBGHTA5LHPPdbqZKyR6Jar1Akv4Z5k9CNLw== - dependencies: - prosemirror-model "^1.2.0" - prosemirror-schema-list@^1.0.0, prosemirror-schema-list@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/prosemirror-schema-list/-/prosemirror-schema-list-1.2.2.tgz#bafda37b72367d39accdcaf6ddf8fb654a16e8e5" From a169fc3b811be394964440902ad29c635730d6e7 Mon Sep 17 00:00:00 2001 From: Igor Lobanov Date: Thu, 10 Nov 2022 13:50:47 +0100 Subject: [PATCH 15/34] fixes and lint --- public/icons/expand.svg | 2 + src/components/Discours/Footer.module.scss | 2 +- src/components/Discours/Subscribe.tsx | 2 +- .../EditorExample/components/Sidebar.tsx | 1 - src/components/EditorNew/Editor.tsx | 29 +-------- src/components/EditorNew/Sidebar.tsx | 2 +- .../EditorNew/prosemirror/plugins/index.ts | 2 - src/components/Feed/Beside.module.scss | 2 + src/components/Nav/Modal.scss | 3 +- src/components/Nav/Modal.tsx | 2 +- src/components/Nav/Popup.tsx | 2 +- src/components/Nav/ProfileModal.tsx | 1 + src/components/Topic/Card.module.scss | 1 + .../Topic/{Full.scss => Full.module.scss} | 12 +++- src/components/Topic/Full.tsx | 59 +++++++++---------- src/components/Views/Author.tsx | 1 - src/components/Views/Topic.tsx | 1 - src/graphql/types.gen.ts | 13 ++++ src/styles/{Topic.scss => Topic.module.scss} | 6 +- src/styles/app.scss | 6 +- src/utils/useOutsideClickHandler.ts | 2 +- 21 files changed, 75 insertions(+), 76 deletions(-) create mode 100644 public/icons/expand.svg rename src/components/Topic/{Full.scss => Full.module.scss} (66%) rename src/styles/{Topic.scss => Topic.module.scss} (82%) diff --git a/public/icons/expand.svg b/public/icons/expand.svg new file mode 100644 index 00000000..fcbcd21c --- /dev/null +++ b/public/icons/expand.svg @@ -0,0 +1,2 @@ + + diff --git a/src/components/Discours/Footer.module.scss b/src/components/Discours/Footer.module.scss index b65481f2..990b9ed6 100644 --- a/src/components/Discours/Footer.module.scss +++ b/src/components/Discours/Footer.module.scss @@ -61,7 +61,7 @@ padding-top: 1.6rem; a { - color: rgba(255, 255, 255, 0.7); + color: rgb(255 255 255 / 70%); &:hover { color: #fff; diff --git a/src/components/Discours/Subscribe.tsx b/src/components/Discours/Subscribe.tsx index 1ce9f59b..20cdaee4 100644 --- a/src/components/Discours/Subscribe.tsx +++ b/src/components/Discours/Subscribe.tsx @@ -1,4 +1,4 @@ -import { createSignal, onMount } from 'solid-js' +import { createSignal } from 'solid-js' import styles from './Subscribe.module.scss' import { t } from '../../utils/intl' import { clsx } from 'clsx' diff --git a/src/components/EditorExample/components/Sidebar.tsx b/src/components/EditorExample/components/Sidebar.tsx index 043f5660..97104ad5 100644 --- a/src/components/EditorExample/components/Sidebar.tsx +++ b/src/components/EditorExample/components/Sidebar.tsx @@ -9,7 +9,6 @@ import { clsx } from 'clsx' import styles from './Sidebar.module.scss' import { useOutsideClickHandler } from '../../../utils/useOutsideClickHandler' import { useEscKeyDownHandler } from '../../../utils/useEscKeyDownHandler' -import { hideModal } from '../../../stores/ui' const Off = (props) =>
        {props.children}
        diff --git a/src/components/EditorNew/Editor.tsx b/src/components/EditorNew/Editor.tsx index b9696fff..d8ea9ded 100644 --- a/src/components/EditorNew/Editor.tsx +++ b/src/components/EditorNew/Editor.tsx @@ -1,21 +1,15 @@ -import { createSignal, onMount } from 'solid-js' +import { onMount } from 'solid-js' import { EditorState, Transaction } from 'prosemirror-state' import { EditorView, MarkViewConstructor, NodeViewConstructor } from 'prosemirror-view' import './prosemirror/styles/ProseMirror.scss' import type { Nodes, Marks } from './prosemirror/schema' import { createImageView } from './prosemirror/views/image' -import { MarkdownSerializer } from 'prosemirror-markdown' import { schema } from './prosemirror/schema' import { createPlugins } from './prosemirror/plugins' - -import debounce from 'lodash/debounce' import { DOMSerializer } from 'prosemirror-model' import { clsx } from 'clsx' -import styles from '../Nav/AuthModal/AuthModal.module.scss' -import { t } from '../../utils/intl' -import { apiClient } from '../../utils/apiClient' import { createArticle } from '../../stores/zine/articles' -import type { InputMaybe, Scalars, ShoutInput } from '../../graphql/types.gen' +import type { ShoutInput } from '../../graphql/types.gen' import { Sidebar } from './Sidebar' const htmlContainer = typeof document === 'undefined' ? null : document.createElement('div') @@ -27,9 +21,6 @@ const getHtml = (state: EditorState) => { } export const Editor = () => { - const [markdown, setMarkdown] = createSignal('') - const [html, setHtml] = createSignal('') - const editorElRef: { current: HTMLDivElement } = { @@ -38,19 +29,9 @@ export const Editor = () => { const editorViewRef: { current: EditorView } = { current: null } - const update = (state: EditorState) => { - const newHtml = getHtml(state) - setHtml(newHtml) - // setMarkdown(state.toJSON()) - // const el = document.createElement('div') - } - - const debouncedUpdate = debounce(update, 500) - const dispatchTransaction = (tr: Transaction) => { const newState = editorViewRef.current.state.apply(tr) editorViewRef.current.updateState(newState) - debouncedUpdate(newState) } onMount(async () => { @@ -88,12 +69,8 @@ export const Editor = () => {
        (editorElRef.current = el)} /> -
        Markdown:
        -
        {markdown()}
        -
        HTML:
        -
        {html()}
        diff --git a/src/components/EditorNew/Sidebar.tsx b/src/components/EditorNew/Sidebar.tsx index f45f2172..a014b5d6 100644 --- a/src/components/EditorNew/Sidebar.tsx +++ b/src/components/EditorNew/Sidebar.tsx @@ -1,4 +1,4 @@ -import { For, Show, createEffect, createSignal, onCleanup, onMount } from 'solid-js' +import { For, createEffect, createSignal, onCleanup, onMount } from 'solid-js' import type { JSX } from 'solid-js' import { undo, redo } from 'prosemirror-history' import { clsx } from 'clsx' diff --git a/src/components/EditorNew/prosemirror/plugins/index.ts b/src/components/EditorNew/prosemirror/plugins/index.ts index 7a4f6439..c9c412f9 100644 --- a/src/components/EditorNew/prosemirror/plugins/index.ts +++ b/src/components/EditorNew/prosemirror/plugins/index.ts @@ -1,5 +1,3 @@ -import { keymap } from 'prosemirror-keymap' -import { baseKeymap } from 'prosemirror-commands' import { history } from 'prosemirror-history' import { dropCursor } from 'prosemirror-dropcursor' import { placeholder } from './placeholder' diff --git a/src/components/Feed/Beside.module.scss b/src/components/Feed/Beside.module.scss index 1e5486ab..fb3d7b66 100644 --- a/src/components/Feed/Beside.module.scss +++ b/src/components/Feed/Beside.module.scss @@ -12,6 +12,8 @@ } li { + margin-bottom: 1em; + &.top { border-bottom: 1px solid #e1e1e1; display: flex; diff --git a/src/components/Nav/Modal.scss b/src/components/Nav/Modal.scss index 6be20367..8c7bc2f0 100644 --- a/src/components/Nav/Modal.scss +++ b/src/components/Nav/Modal.scss @@ -1,11 +1,12 @@ .modalwrap { - pointer-events: all; align-items: center; background: rgb(20 20 20 / 70%); display: flex; justify-content: center; height: 100%; left: 0; + overflow: auto; + pointer-events: all; position: fixed; top: 0; width: 100%; diff --git a/src/components/Nav/Modal.tsx b/src/components/Nav/Modal.tsx index 2f9e2872..2ebe709d 100644 --- a/src/components/Nav/Modal.tsx +++ b/src/components/Nav/Modal.tsx @@ -1,4 +1,4 @@ -import { createEffect, createSignal, onCleanup, onMount, Show } from 'solid-js' +import { createEffect, createSignal, Show } from 'solid-js' import type { JSX } from 'solid-js' import { getLogger } from '../../utils/logger' import './Modal.scss' diff --git a/src/components/Nav/Popup.tsx b/src/components/Nav/Popup.tsx index 5ee469b9..bfb4ef89 100644 --- a/src/components/Nav/Popup.tsx +++ b/src/components/Nav/Popup.tsx @@ -1,4 +1,4 @@ -import { createEffect, createSignal, JSX, onCleanup, onMount, Show } from 'solid-js' +import { createEffect, createSignal, JSX, Show } from 'solid-js' import styles from './Popup.module.scss' import { clsx } from 'clsx' import { useOutsideClickHandler } from '../../utils/useOutsideClickHandler' diff --git a/src/components/Nav/ProfileModal.tsx b/src/components/Nav/ProfileModal.tsx index ae32f97b..2beab06f 100644 --- a/src/components/Nav/ProfileModal.tsx +++ b/src/components/Nav/ProfileModal.tsx @@ -15,6 +15,7 @@ export default () => { const author = createMemo(() => { const a: Author = { + id: null, name: 'anonymous', userpic: '', slug: '' diff --git a/src/components/Topic/Card.module.scss b/src/components/Topic/Card.module.scss index bbfab772..0f499cdd 100644 --- a/src/components/Topic/Card.module.scss +++ b/src/components/Topic/Card.module.scss @@ -82,6 +82,7 @@ .topicDetailsItem { @include font-size(1.5rem); + margin-right: 1.6rem; white-space: nowrap; diff --git a/src/components/Topic/Full.scss b/src/components/Topic/Full.module.scss similarity index 66% rename from src/components/Topic/Full.scss rename to src/components/Topic/Full.module.scss index ef8c4baf..f0c39156 100644 --- a/src/components/Topic/Full.scss +++ b/src/components/Topic/Full.module.scss @@ -1,11 +1,19 @@ -.topic__header { +.topicHeader { @include font-size(1.7rem); padding-top: 5.8rem; text-align: center; + + h1 { + color: #2638d9; + font-weight: 500; + text-transform: uppercase; + + @include font-size(2rem); + } } -.topic__actions { +.topicActions { margin-top: 2.8rem; button, diff --git a/src/components/Topic/Full.tsx b/src/components/Topic/Full.tsx index 5923ead9..c6bb57fa 100644 --- a/src/components/Topic/Full.tsx +++ b/src/components/Topic/Full.tsx @@ -1,10 +1,11 @@ import { createMemo, Show } from 'solid-js' import type { Topic } from '../../graphql/types.gen' import { FollowingEntity } from '../../graphql/types.gen' -import './Full.scss' +import styles from './Full.module.scss' import { useAuthStore } from '../../stores/auth' import { follow, unfollow } from '../../stores/zine/common' import { t } from '../../utils/intl' +import { clsx } from 'clsx' type Props = { topic: Topic @@ -15,37 +16,35 @@ export const FullTopic = (props: Props) => { const subscribed = createMemo(() => session()?.news?.topics?.includes(props.topic?.slug)) return ( -
        -
        - -
        -

        #{props.topic.title}

        -

        {props.topic.body}

        -
        - - - - - - - {t('Write about the topic')} -
        - - {props.topic.title} +
        + +
        +

        #{props.topic.title}

        +

        {props.topic.body}

        +
        + + + + + + {t('Write about the topic')}
        - -
        + + {props.topic.title} + +
        +
        ) } diff --git a/src/components/Views/Author.tsx b/src/components/Views/Author.tsx index 52cd189f..59278d34 100644 --- a/src/components/Views/Author.tsx +++ b/src/components/Views/Author.tsx @@ -7,7 +7,6 @@ import { t } from '../../utils/intl' import { useAuthorsStore } from '../../stores/zine/authors' import { loadAuthorArticles, useArticlesStore } from '../../stores/zine/articles' -import '../../styles/Topic.scss' import { useTopicsStore } from '../../stores/zine/topics' import { useRouter } from '../../stores/router' import { Beside } from '../Feed/Beside' diff --git a/src/components/Views/Topic.tsx b/src/components/Views/Topic.tsx index 934e4ec9..a2760967 100644 --- a/src/components/Views/Topic.tsx +++ b/src/components/Views/Topic.tsx @@ -3,7 +3,6 @@ import type { Shout, Topic } from '../../graphql/types.gen' import { Row3 } from '../Feed/Row3' import { Row2 } from '../Feed/Row2' import { Beside } from '../Feed/Beside' -import { ArticleCard } from '../Feed/Card' import styles from '../../styles/Topic.module.scss' import { FullTopic } from '../Topic/Full' import { t } from '../../utils/intl' diff --git a/src/graphql/types.gen.ts b/src/graphql/types.gen.ts index cde59629..7be17aa5 100644 --- a/src/graphql/types.gen.ts +++ b/src/graphql/types.gen.ts @@ -24,6 +24,7 @@ export type AuthResult = { export type Author = { bio?: Maybe caption?: Maybe + id: Scalars['Int'] links?: Maybe>> name: Scalars['String'] slug: Scalars['String'] @@ -69,6 +70,7 @@ export type Collection = { createdAt: Scalars['DateTime'] createdBy: User desc?: Maybe + id: Scalars['Int'] publishedAt?: Maybe slug: Scalars['String'] title: Scalars['String'] @@ -84,6 +86,7 @@ export type Community = { createdAt: Scalars['DateTime'] createdBy: User desc?: Maybe + id: Scalars['Int'] name: Scalars['String'] pic: Scalars['String'] slug: Scalars['String'] @@ -329,10 +332,12 @@ export type ProfileInput = { export type Query = { authorsAll: Array> collectionsAll: Array> + getAuthor: User getCollabs: Array> getCommunities: Array> getCommunity: Community getShoutBySlug: Shout + getTopic: Topic getUserCollections: Array> getUserRoles: Array> getUsersBySlugs: Array> @@ -371,6 +376,10 @@ export type Query = { userReactedShouts: Array> } +export type QueryGetAuthorArgs = { + slug: Scalars['String'] +} + export type QueryGetCommunityArgs = { slug?: InputMaybe } @@ -379,6 +388,10 @@ export type QueryGetShoutBySlugArgs = { slug: Scalars['String'] } +export type QueryGetTopicArgs = { + slug: Scalars['String'] +} + export type QueryGetUserCollectionsArgs = { author: Scalars['String'] } diff --git a/src/styles/Topic.scss b/src/styles/Topic.module.scss similarity index 82% rename from src/styles/Topic.scss rename to src/styles/Topic.module.scss index 20b16c28..fa18beab 100644 --- a/src/styles/Topic.scss +++ b/src/styles/Topic.module.scss @@ -1,11 +1,11 @@ -.topic-page { - .group__controls { +.topicPage { + .groupControls { align-items: baseline; margin-bottom: 4rem; margin-top: 7rem; } - .floor--important { + .floorImportant { a:hover { background: #fff; color: #000 !important; diff --git a/src/styles/app.scss b/src/styles/app.scss index 2e23e357..0adff8f6 100644 --- a/src/styles/app.scss +++ b/src/styles/app.scss @@ -487,7 +487,7 @@ figcaption { a { border-bottom: 2px solid #fff; - color: rgba(0, 0, 0, 0.5); + color: rgb(0 0 0 / 50%); &:hover { color: #fff; @@ -746,7 +746,7 @@ details { cursor: pointer; margin-bottom: 0; - &:before { + &::before { content: ''; background: url(/icons/expand.svg) no-repeat; background-size: contain; @@ -762,7 +762,7 @@ details { } &[open] { - h3:before { + h3::before { transform: rotate(180deg); } } diff --git a/src/utils/useOutsideClickHandler.ts b/src/utils/useOutsideClickHandler.ts index cfeb71a1..e1102d4f 100644 --- a/src/utils/useOutsideClickHandler.ts +++ b/src/utils/useOutsideClickHandler.ts @@ -17,7 +17,7 @@ export const useOutsideClickHandler = (options: Options) => { return } - options.handler() + handler() } onMount(() => { From 76d43514e6822a3fecdf6f1a8df3d1effa09d424 Mon Sep 17 00:00:00 2001 From: kvakazyambra Date: Fri, 11 Nov 2022 11:58:22 +0300 Subject: [PATCH 16/34] Fixed all authors page --- src/components/Author/Card.module.scss | 40 +++++++++++++- src/components/Author/Card.tsx | 67 +++++++++++++++++------ src/components/Author/Userpic.module.scss | 14 ++++- src/components/Author/Userpic.tsx | 15 +++-- src/components/Views/AllAuthors.tsx | 9 +-- src/styles/AllTopics.module.scss | 13 +---- src/styles/app.scss | 7 +++ 7 files changed, 125 insertions(+), 40 deletions(-) diff --git a/src/components/Author/Card.module.scss b/src/components/Author/Card.module.scss index a68b6e5b..ad75417e 100644 --- a/src/components/Author/Card.module.scss +++ b/src/components/Author/Card.module.scss @@ -15,12 +15,24 @@ .authorDetails { display: flex; flex: 1; - padding-right: 1.2rem; + //padding-right: 1.2rem; width: max-content; + + @include media-breakpoint-down(sm) { + flex-wrap: wrap; + } } .authorDetailsWrapper { - flex: 1; + flex: 1 100%; + + @include media-breakpoint-up(sm) { + flex: 1; + } + + @include media-breakpoint-up(md) { + padding-right: 1.2rem; + } } .authorName { @@ -33,6 +45,7 @@ .authorAbout { font-size: 1.5rem; color: rgb(0 0 0 / 60%); + word-break: break-word; } .authorSubscribe { @@ -118,6 +131,10 @@ button { margin-right: 0.5em; + + &:last-child { + margin-right: 0; + } } } @@ -221,3 +238,22 @@ vertical-align: middle; } } + +.authorsListItem { + .authorName { + @include font-size(2.2rem); + font-weight: bold; + } + + .authorSubscribe { + align-items: baseline; + + @include media-breakpoint-down(sm) { + padding: 1rem 0 0; + } + } + + .buttonLabel { + display: block; + } +} diff --git a/src/components/Author/Card.tsx b/src/components/Author/Card.tsx index 4608163c..4477ee34 100644 --- a/src/components/Author/Card.tsx +++ b/src/components/Author/Card.tsx @@ -1,7 +1,7 @@ import type { Author } from '../../graphql/types.gen' import Userpic from './Userpic' import { Icon } from '../Nav/Icon' -import style from './Card.module.scss' +import styles from './Card.module.scss' import { createMemo, For, Show } from 'solid-js' import { translit } from '../../utils/ru2en' import { t } from '../../utils/intl' @@ -19,6 +19,7 @@ interface AuthorCardProps { author: Author isAuthorPage?: boolean noSocialButtons?: boolean + isAuthorsList?: boolean } export const AuthorCard = (props: AuthorCardProps) => { @@ -36,51 +37,81 @@ export const AuthorCard = (props: AuthorCardProps) => { } // TODO: reimplement AuthorCard return ( -
        - +
        + -
        -
        +
        +
        - + {name()} -
        {name()}
        +
        {name()}
        -
        {bio()}
        +
        + {bio()} +
        -
        +
        follow} - class={clsx('button button--subscribe', style.button, style.buttonSubscribe)} + class={clsx('button', styles.button)} + classList={{ + [styles.buttonSubscribe]: !props.isAuthorsList, + 'button--subscribe': !props.isAuthorsList, + 'button--subscribe-topic': props.isAuthorsList, + [styles.buttonWrite]: props.isAuthorsList + }} > - -  {t('Follow')} + + +   + + {t('Follow')} } > - - diff --git a/src/components/Author/Userpic.module.scss b/src/components/Author/Userpic.module.scss index 7006922a..20ed085e 100644 --- a/src/components/Author/Userpic.module.scss +++ b/src/components/Author/Userpic.module.scss @@ -20,7 +20,7 @@ border: 2px solid black; background-color: white; text-align: center; - line-height: 32px; + line-height: 28px; } .anonymous { @@ -47,3 +47,15 @@ width: 100%; } } + +.authorsList { + margin-right: 2.4rem; + max-height: 6.8rem; + min-width: 6.8rem; + height: 6.8rem; + width: 6.8rem; + + .userpic { + line-height: 6.4rem; + } +} diff --git a/src/components/Author/Userpic.tsx b/src/components/Author/Userpic.tsx index 0ed8744a..75dc918b 100644 --- a/src/components/Author/Userpic.tsx +++ b/src/components/Author/Userpic.tsx @@ -1,6 +1,6 @@ import { Show } from 'solid-js' import type { Author } from '../../graphql/types.gen' -import style from './Userpic.module.scss' +import styles from './Userpic.module.scss' import { clsx } from 'clsx' interface UserpicProps { @@ -8,6 +8,7 @@ interface UserpicProps { hasLink?: boolean isBig?: boolean class?: string + isAuthorsList?: boolean } export default (props: UserpicProps) => { @@ -18,7 +19,13 @@ export default (props: UserpicProps) => { } return ( -
        +
        { /> } > -
        {letters()}
        +
        {letters()}
        @@ -47,7 +54,7 @@ export default (props: UserpicProps) => { /> } > -
        {letters()}
        +
        {letters()}
        diff --git a/src/components/Views/AllAuthors.tsx b/src/components/Views/AllAuthors.tsx index 070738ba..576bc1b9 100644 --- a/src/components/Views/AllAuthors.tsx +++ b/src/components/Views/AllAuthors.tsx @@ -60,15 +60,15 @@ export const AllAuthorsView = (props: Props) => {
        0}>
        -
        +
        -
        +

        {t('Authors')}

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

        -
        +
        • @@ -95,7 +95,7 @@ export const AllAuthorsView = (props: Props) => { ( -
          +
          {(author) => ( { hasLink={true} subscribed={subscribed(author.slug)} noSocialButtons={true} + isAuthorsList={true} /> )} diff --git a/src/styles/AllTopics.module.scss b/src/styles/AllTopics.module.scss index 5dca6c2a..d6aecc23 100644 --- a/src/styles/AllTopics.module.scss +++ b/src/styles/AllTopics.module.scss @@ -1,11 +1,4 @@ .allTopicsPage { - .pageHeader, - .group h2 { - @include media-breakpoint-down(sm) { - margin-left: 1.3rem; - } - } - .group { font-weight: bold; margin: 3em 0 9.6rem; @@ -39,8 +32,6 @@ } } -.viewSwitcher { - @include media-breakpoint-down(sm) { - margin: 0 2.6rem; - } +.stats { + margin-top: 2.4rem; } diff --git a/src/styles/app.scss b/src/styles/app.scss index 0adff8f6..40b47d56 100644 --- a/src/styles/app.scss +++ b/src/styles/app.scss @@ -515,6 +515,7 @@ figcaption { .view-switcher__search { text-align: right; + white-space: nowrap; @include media-breakpoint-up(sm) { flex: 1; @@ -643,6 +644,12 @@ astro-island { .container { max-width: 1400px; + padding: 0; + width: auto; + + @include media-breakpoint-down(sm) { + padding: 0 $container-padding-x * 0.5; + } } .container--static-page { From 8ffa30f1be7b2418077f55eb9f1799facb8a0820 Mon Sep 17 00:00:00 2001 From: kvakazyambra Date: Fri, 11 Nov 2022 12:34:40 +0300 Subject: [PATCH 17/34] Minor fixes about pages --- src/components/Pages/about/GuidePage.tsx | 6 +++-- src/components/Pages/about/HelpPage.tsx | 6 +++-- src/components/Pages/about/ManifestPage.tsx | 8 ++++--- src/components/Pages/about/TermsOfUsePage.tsx | 6 +++-- src/styles/app.scss | 24 +++++++++++++++++-- 5 files changed, 39 insertions(+), 11 deletions(-) diff --git a/src/components/Pages/about/GuidePage.tsx b/src/components/Pages/about/GuidePage.tsx index b4d690ff..b1158c60 100644 --- a/src/components/Pages/about/GuidePage.tsx +++ b/src/components/Pages/about/GuidePage.tsx @@ -6,7 +6,7 @@ import { Icon } from '../../Nav/Icon' export const GuidePage = () => { const title = t('How it works') - const [indexExpanded, setIndexExpanded] = createSignal(false) + const [indexExpanded, setIndexExpanded] = createSignal(true) const toggleIndexExpanded = () => setIndexExpanded((oldExpanded) => !oldExpanded) @@ -28,12 +28,14 @@ export const GuidePage = () => { - +
          -
          - - } - > - - { - setIsProfilePopupVisible(isVisible) - }} - containerCssClass={styles.control} - trigger={ -
          - -
          - } - /> - + + +
          + { + setIsSharePopupVisible(isVisible) + }} + containerCssClass={styles.control} + trigger={} + /> + + + + event.preventDefault()}> + + + event.preventDefault()}> + +
          - -
          - { - setIsSharePopupVisible(isVisible) - }} - containerCssClass={styles.control} - trigger={} - /> - - - - event.preventDefault()}> - - - event.preventDefault()}> - - -
          -
          -
          +
          diff --git a/src/components/Nav/HeaderAuth.tsx b/src/components/Nav/HeaderAuth.tsx new file mode 100644 index 00000000..970d9005 --- /dev/null +++ b/src/components/Nav/HeaderAuth.tsx @@ -0,0 +1,111 @@ +import styles from './Header.module.scss' +import { clsx } from 'clsx' +import { handleClientRouteLinkClick, useRouter } from '../../stores/router' +import { t } from '../../utils/intl' +import { Icon } from './Icon' +import { createSignal, onMount, Show } from 'solid-js' +import Notifications from './Notifications' +import { ProfilePopup } from './ProfilePopup' +import Userpic from '../Author/Userpic' +import type { Author } from '../../graphql/types.gen' +import { showModal, useWarningsStore } from '../../stores/ui' +import { useAuth } from '../../context/auth' + +type HeaderAuthProps = { + setIsProfilePopupVisible: (value: boolean) => void +} + +export const HeaderAuth = (props: HeaderAuthProps) => { + const [isMounted, setIsMounted] = createSignal(false) + const { page } = useRouter() + const [visibleWarnings, setVisibleWarnings] = createSignal(false) + const { warnings } = useWarningsStore() + + const { session, isAuthenticated } = useAuth() + + const toggleWarnings = () => setVisibleWarnings(!visibleWarnings()) + + const handleBellIconClick = (event: Event) => { + event.preventDefault() + + if (!isAuthenticated()) { + showModal('auth') + return + } + + toggleWarnings() + } + + onMount(() => { + setIsMounted(true) + }) + + return ( + + +
          + + } + > + + { + props.setIsProfilePopupVisible(isVisible) + }} + containerCssClass={styles.control} + trigger={ +
          + +
          + } + /> + +
          +
          + + + ) +} diff --git a/src/components/Nav/ProfileModal.tsx b/src/components/Nav/ProfileModal.tsx index 2beab06f..10b69d7f 100644 --- a/src/components/Nav/ProfileModal.tsx +++ b/src/components/Nav/ProfileModal.tsx @@ -2,16 +2,19 @@ import { AuthorCard } from '../Author/Card' import type { Author } from '../../graphql/types.gen' import { t } from '../../utils/intl' import { hideModal } from '../../stores/ui' -import { useAuthStore, signOut } from '../../stores/auth' import { createMemo, For } from 'solid-js' +import { useAuth } from '../../context/auth' -const quit = () => { - signOut() - hideModal() -} +export const ProfileModal = () => { + const { + session, + actions: { signOut } + } = useAuth() -export default () => { - const { session } = useAuthStore() + const quit = () => { + signOut() + hideModal() + } const author = createMemo(() => { const a: Author = { diff --git a/src/components/Nav/ProfilePopup.tsx b/src/components/Nav/ProfilePopup.tsx index 204034d0..f0fb7772 100644 --- a/src/components/Nav/ProfilePopup.tsx +++ b/src/components/Nav/ProfilePopup.tsx @@ -1,11 +1,14 @@ import { Popup, PopupProps } from './Popup' -import { signOut, useAuthStore } from '../../stores/auth' import styles from './Popup.module.scss' +import { useAuth } from '../../context/auth' type ProfilePopupProps = Omit export const ProfilePopup = (props: ProfilePopupProps) => { - const { session } = useAuthStore() + const { + session, + actions: { signOut } + } = useAuth() return ( diff --git a/src/components/Nav/Topics.tsx b/src/components/Nav/Topics.tsx index 14750cf9..5ce88cce 100644 --- a/src/components/Nav/Topics.tsx +++ b/src/components/Nav/Topics.tsx @@ -4,6 +4,7 @@ import { Icon } from './Icon' import './Topics.scss' import { t } from '../../utils/intl' import { locale } from '../../stores/ui' +import { handleClientRouteLinkClick } from '../../stores/router' export const NavTopics = (props: { topics: Topic[] }) => { const tag = (topic: Topic) => @@ -17,7 +18,7 @@ export const NavTopics = (props: { topics: Topic[] }) => { {(topic) => (
        • - + #{tag(topic)}
        • diff --git a/src/components/Pages/TopicPage.tsx b/src/components/Pages/TopicPage.tsx index 16e0c95a..35a94c92 100644 --- a/src/components/Pages/TopicPage.tsx +++ b/src/components/Pages/TopicPage.tsx @@ -8,7 +8,7 @@ import { loadTopic } from '../../stores/zine/topics' import { Loading } from '../Loading' export const TopicPage = (props: PageProps) => { - const [isLoaded, setIsLoaded] = createSignal(Boolean(props.authorArticles) && Boolean(props.author)) + const [isLoaded, setIsLoaded] = createSignal(Boolean(props.topicArticles) && Boolean(props.topic)) const slug = createMemo(() => { const { page: getPage } = useRouter() diff --git a/src/components/Root.tsx b/src/components/Root.tsx index fc1a7ff9..787bcca1 100644 --- a/src/components/Root.tsx +++ b/src/components/Root.tsx @@ -2,12 +2,11 @@ // import 'solid-devtools' import { MODALS, setLocale, showModal } from '../stores/ui' -import { Component, createEffect, createMemo, onMount } from 'solid-js' +import { Component, createEffect, createMemo } from 'solid-js' import { Routes, useRouter } from '../stores/router' import { Dynamic, isServer } from 'solid-js/web' -import { getLogger } from '../utils/logger' -import type { PageProps } from './types' +import type { PageProps, RootSearchParams } from './types' import { HomePage } from './Pages/HomePage' import { AllTopicsPage } from './Pages/AllTopicsPage' @@ -30,7 +29,7 @@ import { TermsOfUsePage } from './Pages/about/TermsOfUsePage' import { ThanksPage } from './Pages/about/ThanksPage' import { CreatePage } from './Pages/CreatePage' import { ConnectPage } from './Pages/ConnectPage' -import { renewSession } from '../stores/auth' +import { AuthProvider } from '../context/auth' // TODO: lazy load // const HomePage = lazy(() => import('./Pages/HomePage')) @@ -52,13 +51,6 @@ import { renewSession } from '../stores/auth' // const ThanksPage = lazy(() => import('./Pages/about/ThanksPage')) // const CreatePage = lazy(() => import('./Pages/about/CreatePage')) -const log = getLogger('root') - -type RootSearchParams = { - modal: string - lang: string -} - const pagesMap: Record> = { connect: ConnectPage, create: CreatePage, @@ -92,10 +84,6 @@ export const Root = (props: PageProps) => { } }) - onMount(() => { - renewSession() - }) - const pageComponent = createMemo(() => { const result = pagesMap[page().route] @@ -114,5 +102,9 @@ export const Root = (props: PageProps) => { }) } - return + return ( + + + + ) } diff --git a/src/components/Topic/Card.tsx b/src/components/Topic/Card.tsx index 16594b58..ffcf8b9f 100644 --- a/src/components/Topic/Card.tsx +++ b/src/components/Topic/Card.tsx @@ -5,10 +5,10 @@ import type { Topic } from '../../graphql/types.gen' import { FollowingEntity } from '../../graphql/types.gen' import { t } from '../../utils/intl' import { locale } from '../../stores/ui' -import { useAuthStore } from '../../stores/auth' import { follow, unfollow } from '../../stores/zine/common' import { getLogger } from '../../utils/logger' import { clsx } from 'clsx' +import { useAuth } from '../../context/auth' const log = getLogger('TopicCard') @@ -24,7 +24,7 @@ interface TopicProps { } export const TopicCard = (props: TopicProps) => { - const { session } = useAuthStore() + const { session } = useAuth() const subscribed = createMemo(() => { if (!session()?.user?.slug || !session()?.news?.topics) { diff --git a/src/components/Topic/Full.tsx b/src/components/Topic/Full.tsx index c6bb57fa..70d78da8 100644 --- a/src/components/Topic/Full.tsx +++ b/src/components/Topic/Full.tsx @@ -2,17 +2,17 @@ import { createMemo, Show } from 'solid-js' import type { Topic } from '../../graphql/types.gen' import { FollowingEntity } from '../../graphql/types.gen' import styles from './Full.module.scss' -import { useAuthStore } from '../../stores/auth' import { follow, unfollow } from '../../stores/zine/common' import { t } from '../../utils/intl' import { clsx } from 'clsx' +import { useAuth } from '../../context/auth' type Props = { topic: Topic } export const FullTopic = (props: Props) => { - const { session } = useAuthStore() + const { session } = useAuth() const subscribed = createMemo(() => session()?.news?.topics?.includes(props.topic?.slug)) return ( diff --git a/src/components/Views/AllAuthors.tsx b/src/components/Views/AllAuthors.tsx index 2ba33ed0..eec96c19 100644 --- a/src/components/Views/AllAuthors.tsx +++ b/src/components/Views/AllAuthors.tsx @@ -1,13 +1,13 @@ -import { createEffect, createMemo, For, Show } from 'solid-js' +import { createEffect, createMemo, createSignal, For, Show } from 'solid-js' import type { Author } from '../../graphql/types.gen' import { AuthorCard } from '../Author/Card' import { Icon } from '../Nav/Icon' import { t } from '../../utils/intl' import { useAuthorsStore, setAuthorsSort } from '../../stores/zine/authors' import { handleClientRouteLinkClick, useRouter } from '../../stores/router' -import { useAuthStore } from '../../stores/auth' import styles from '../../styles/AllTopics.module.scss' import { clsx } from 'clsx' +import { useAuth } from '../../context/auth' type AllAuthorsPageSearchParams = { by: '' | 'name' | 'shouts' | 'rating' @@ -17,10 +17,13 @@ type Props = { authors: Author[] } +const PAGE_SIZE = 20 + export const AllAuthorsView = (props: Props) => { const { sortedAuthors } = useAuthorsStore({ authors: props.authors }) + const [limit, setLimit] = createSignal(PAGE_SIZE) - const { session } = useAuthStore() + const { session } = useAuth() createEffect(() => { setAuthorsSort(searchParams().by || 'shouts') @@ -54,7 +57,7 @@ export const AllAuthorsView = (props: Props) => { return keys }) - // log.debug(getSearchParams()) + const showMore = () => setLimit((oldLimit) => oldLimit + PAGE_SIZE) return (
          @@ -95,7 +98,7 @@ export const AllAuthorsView = (props: Props) => { when={!searchParams().by || searchParams().by === 'name'} fallback={() => (
          - + {(author) => ( { /> )} + limit()}> +
          + +
          +
          )} > diff --git a/src/components/Views/AllTopics.tsx b/src/components/Views/AllTopics.tsx index 687385eb..2261a10c 100644 --- a/src/components/Views/AllTopics.tsx +++ b/src/components/Views/AllTopics.tsx @@ -1,14 +1,13 @@ -import { createEffect, createMemo, For, Show } from 'solid-js' +import { createEffect, createMemo, createSignal, For, Show } from 'solid-js' import type { Topic } from '../../graphql/types.gen' import { Icon } from '../Nav/Icon' import { t } from '../../utils/intl' import { setTopicsSort, useTopicsStore } from '../../stores/zine/topics' import { handleClientRouteLinkClick, useRouter } from '../../stores/router' import { TopicCard } from '../Topic/Card' -import { useAuthStore } from '../../stores/auth' import styles from '../../styles/AllTopics.module.scss' -import cardStyles from '../Topic/Card.module.scss' import { clsx } from 'clsx' +import { useAuth } from '../../context/auth' type AllTopicsPageSearchParams = { by: 'shouts' | 'authors' | 'title' | '' @@ -18,18 +17,22 @@ type AllTopicsViewProps = { topics: Topic[] } +const PAGE_SIZE = 20 + export const AllTopicsView = (props: AllTopicsViewProps) => { const { searchParams, changeSearchParam } = useRouter() + const [limit, setLimit] = createSignal(PAGE_SIZE) const { sortedTopics } = useTopicsStore({ topics: props.topics, sortBy: searchParams().by || 'shouts' }) - const { session } = useAuthStore() + const { session } = useAuth() createEffect(() => { setTopicsSort(searchParams().by || 'shouts') + setLimit(PAGE_SIZE) }) const byLetter = createMemo<{ [letter: string]: Topic[] }>(() => { @@ -53,6 +56,8 @@ export const AllTopicsView = (props: AllTopicsViewProps) => { const subscribed = (s) => Boolean(session()?.news?.topics && session()?.news?.topics?.includes(s || '')) + const showMore = () => setLimit((oldLimit) => oldLimit + PAGE_SIZE) + return (
          0}> @@ -102,9 +107,20 @@ export const AllTopicsView = (props: AllTopicsViewProps) => { ( - - {(topic) => } - + <> + + {(topic) => ( + + )} + + limit()}> +
          + +
          +
          + )} > diff --git a/src/components/Views/Feed.tsx b/src/components/Views/Feed.tsx index c791331c..36318f12 100644 --- a/src/components/Views/Feed.tsx +++ b/src/components/Views/Feed.tsx @@ -8,13 +8,13 @@ import { ArticleCard } from '../Feed/Card' import { AuthorCard } from '../Author/Card' import { t } from '../../utils/intl' import { FeedSidebar } from '../Feed/Sidebar' -import { useAuthStore } from '../../stores/auth' import CommentCard from '../Article/Comment' import { loadRecentArticles, useArticlesStore } from '../../stores/zine/articles' import { useReactionsStore } from '../../stores/zine/reactions' import { useAuthorsStore } from '../../stores/zine/authors' import { useTopicsStore } from '../../stores/zine/topics' import { useTopAuthorsStore } from '../../stores/zine/topAuthors' +import { useAuth } from '../../context/auth' // const AUTHORSHIP_REACTIONS = [ // ReactionKind.Accept, @@ -32,7 +32,7 @@ export const FeedView = () => { const { sortedAuthors } = useAuthorsStore() const { topTopics } = useTopicsStore() const { topAuthors } = useTopAuthorsStore() - const { session } = useAuthStore() + const { session } = useAuth() const topReactions = createMemo(() => sortBy(reactions(), byCreated)) diff --git a/src/components/Views/Topic.tsx b/src/components/Views/Topic.tsx index a2760967..27c0d27e 100644 --- a/src/components/Views/Topic.tsx +++ b/src/components/Views/Topic.tsx @@ -8,7 +8,7 @@ import { FullTopic } from '../Topic/Full' import { t } from '../../utils/intl' import { useRouter } from '../../stores/router' import { useTopicsStore } from '../../stores/zine/topics' -import { loadPublishedArticles, useArticlesStore } from '../../stores/zine/articles' +import { loadTopicArticles, useArticlesStore } from '../../stores/zine/articles' import { useAuthorsStore } from '../../stores/zine/authors' import { restoreScrollPosition, saveScrollPosition } from '../../utils/scroll' import { splitToPages } from '../../utils/splitToPages' @@ -26,7 +26,7 @@ interface TopicProps { topicSlug: string } -export const PRERENDERED_ARTICLES_COUNT = 21 +export const PRERENDERED_ARTICLES_COUNT = 28 const LOAD_MORE_PAGE_SIZE = 9 // Row3 + Row3 + Row3 export const TopicView = (props: TopicProps) => { @@ -44,7 +44,8 @@ export const TopicView = (props: TopicProps) => { const loadMore = async () => { saveScrollPosition() - const { hasMore } = await loadPublishedArticles({ + const { hasMore } = await loadTopicArticles({ + topicSlug: topic().slug, limit: LOAD_MORE_PAGE_SIZE, offset: sortedArticles().length }) @@ -112,7 +113,7 @@ export const TopicView = (props: TopicProps) => {
          - + { wrapper={'top-article'} /> - 5}> - - - - - - + + + + + + + + + {(page) => ( diff --git a/src/components/types.ts b/src/components/types.ts index f3158bb7..866bb4f5 100644 --- a/src/components/types.ts +++ b/src/components/types.ts @@ -17,3 +17,8 @@ export type PageProps = { searchResults?: Shout[] chats?: Chat[] } + +export type RootSearchParams = { + modal: string + lang: string +} diff --git a/src/context/auth.tsx b/src/context/auth.tsx new file mode 100644 index 00000000..7fe11945 --- /dev/null +++ b/src/context/auth.tsx @@ -0,0 +1,98 @@ +import type { Accessor, InitializedResource, JSX } from 'solid-js' +import { createContext, createMemo, createResource, onMount, useContext } from 'solid-js' +import type { AuthResult } from '../graphql/types.gen' +import { apiClient } from '../utils/apiClient' +import { resetToken, setToken } from '../graphql/privateGraphQLClient' + +type AuthContextType = { + session: InitializedResource + isAuthenticated: Accessor + actions: { + refreshSession: () => AuthResult | Promise + signIn: ({ email, password }: { email: string; password: string }) => Promise + signOut: () => Promise + confirmEmail: (token: string) => Promise + } +} + +const AuthContext = createContext() + +const refreshSession = async (): Promise => { + try { + const authResult = await apiClient.getSession() + setToken(authResult.token) + return authResult + } catch (error) { + console.error('renewSession error:', error) + resetToken() + return null + } +} + +export const register = async ({ + name, + email, + password +}: { + name: string + email: string + password: string +}) => { + await apiClient.authRegister({ + name, + email, + password + }) +} + +export const signSendLink = async ({ email, lang }: { email: string; lang: string }) => { + return await apiClient.authSendLink({ email, lang }) +} + +export function useAuth() { + return useContext(AuthContext) +} + +export const AuthProvider = (props: { children: JSX.Element }) => { + const [session, { refetch: refetchRefreshSession, mutate }] = createResource(refreshSession, { + ssrLoadFrom: 'initial', + initialValue: null + }) + + const isAuthenticated = createMemo(() => Boolean(session()?.user?.slug)) + + const signIn = async ({ email, password }: { email: string; password: string }) => { + const authResult = await apiClient.authLogin({ email, password }) + mutate(authResult) + setToken(authResult.token) + console.debug('signed in') + } + + const signOut = async () => { + // TODO: call backend to revoke token + mutate(null) + resetToken() + console.debug('signed out') + } + + const confirmEmail = async (token: string) => { + const authResult = await apiClient.confirmEmail({ token }) + mutate(authResult) + setToken(authResult.token) + } + + const actions = { + refreshSession: refetchRefreshSession, + signIn, + signOut, + confirmEmail + } + + const value: AuthContextType = { session, isAuthenticated, actions } + + onMount(() => { + refetchRefreshSession() + }) + + return {props.children} +} diff --git a/src/graphql/privateGraphQLClient.ts b/src/graphql/privateGraphQLClient.ts index dc9ff986..0de1202a 100644 --- a/src/graphql/privateGraphQLClient.ts +++ b/src/graphql/privateGraphQLClient.ts @@ -27,7 +27,6 @@ const options: ClientOptions = { // меняем через setToken, например при получении значения с сервера // скорее всего придумаем что-нибудь получше со временем const token = localStorage.getItem(TOKEN_LOCAL_STORAGE_KEY) - const headers = { Auth: token } return { headers } }, diff --git a/src/graphql/query/author-by-slug.ts b/src/graphql/query/author-by-slug.ts new file mode 100644 index 00000000..bf35df61 --- /dev/null +++ b/src/graphql/query/author-by-slug.ts @@ -0,0 +1,22 @@ +import { gql } from '@urql/core' + +export default gql` + query GetAuthorBySlugQuery($slug: String!) { + getAuthor(slug: $slug) { + _id: slug + slug + name + bio + userpic + communities + links + createdAt + lastSeen + ratings { + _id: rater + rater + value + } + } + } +` diff --git a/src/graphql/query/authors-all.ts b/src/graphql/query/authors-all.ts index 4570a611..4b02a3a5 100644 --- a/src/graphql/query/authors-all.ts +++ b/src/graphql/query/authors-all.ts @@ -8,14 +8,11 @@ export default gql` name bio userpic - communities links - createdAt lastSeen - ratings { - _id: rater - rater - value + stat { + followers + followings } } } diff --git a/src/graphql/query/topic-by-slug.ts b/src/graphql/query/topic-by-slug.ts new file mode 100644 index 00000000..0e440496 --- /dev/null +++ b/src/graphql/query/topic-by-slug.ts @@ -0,0 +1,22 @@ +import { gql } from '@urql/core' + +export default gql` + query TopicBySlugQuery($slug: String!) { + getTopic(slug: $slug) { + title + body + slug + pic + parents + children + # community + stat { + _id: shouts + shouts + authors + # viewed + followers + } + } + } +` diff --git a/src/graphql/types.gen.ts b/src/graphql/types.gen.ts index 7be17aa5..8f2e7454 100644 --- a/src/graphql/types.gen.ts +++ b/src/graphql/types.gen.ts @@ -25,18 +25,30 @@ export type Author = { bio?: Maybe caption?: Maybe id: Scalars['Int'] + lastSeen?: Maybe links?: Maybe>> name: Scalars['String'] + roles?: Maybe>> slug: Scalars['String'] + stat?: Maybe userpic?: Maybe } +export type AuthorStat = { + commented?: Maybe + followers?: Maybe + followings?: Maybe + rating?: Maybe +} + export type Chat = { + admins?: Maybe>> createdAt: Scalars['Int'] createdBy: User description?: Maybe id: Scalars['String'] messages: Array> + private?: Maybe title?: Maybe unread?: Maybe updatedAt: Scalars['Int'] @@ -53,8 +65,8 @@ export type ChatMember = { invitedAt?: Maybe invitedBy?: Maybe name: Scalars['String'] - pic?: Maybe slug: Scalars['String'] + userpic?: Maybe } export type Collab = { @@ -130,6 +142,7 @@ export type Mutation = { createReaction: Result createShout: Result createTopic: Result + deleteChat: Result deleteCollection: Result deleteCommunity: Result deleteMessage: Result @@ -193,6 +206,10 @@ export type MutationCreateTopicArgs = { input: TopicInput } +export type MutationDeleteChatArgs = { + chatId: Scalars['String'] +} + export type MutationDeleteCollectionArgs = { slug: Scalars['String'] } @@ -330,7 +347,7 @@ export type ProfileInput = { } export type Query = { - authorsAll: Array> + authorsAll: Array> collectionsAll: Array> getAuthor: User getCollabs: Array> @@ -340,39 +357,46 @@ export type Query = { getTopic: Topic getUserCollections: Array> getUserRoles: Array> - getUsersBySlugs: Array> + getUsersBySlugs: Array> isEmailUsed: Scalars['Boolean'] - loadChat: Result + loadChats: Result + loadMessages: Result markdownBody: Scalars['String'] - myChats: Result reactionsByAuthor: Array> reactionsForShouts: Array> recentAll: Array> recentCandidates: Array> recentCommented: Array> + recentLayoutShouts: Array> recentPublished: Array> recentReacted: Array> + searchChats: Result + searchMessages: Result searchQuery?: Maybe>> + searchUsers: Result shoutsByAuthors: Array> shoutsByCollection: Array> shoutsByCommunities: Array> + shoutsByLayout: Array> shoutsByTopics: Array> shoutsForFeed: Array> signIn: AuthResult signOut: AuthResult topAuthors: Array> topCommented: Array> + topLayoutShouts: Array> topMonth: Array> + topMonthLayoutShouts: Array> topOverall: Array> topPublished: Array> topicsAll: Array> topicsByAuthor: Array> topicsByCommunity: Array> topicsRandom: Array> - userFollowedAuthors: Array> + userFollowedAuthors: Array> userFollowedCommunities: Array> userFollowedTopics: Array> - userFollowers: Array> + userFollowers: Array> userReactedShouts: Array> } @@ -408,7 +432,12 @@ export type QueryIsEmailUsedArgs = { email: Scalars['String'] } -export type QueryLoadChatArgs = { +export type QueryLoadChatsArgs = { + amount?: InputMaybe + offset?: InputMaybe +} + +export type QueryLoadMessagesArgs = { amount?: InputMaybe chatId: Scalars['String'] offset?: InputMaybe @@ -445,6 +474,12 @@ export type QueryRecentCommentedArgs = { offset: Scalars['Int'] } +export type QueryRecentLayoutShoutsArgs = { + amount?: InputMaybe + layout: Scalars['String'] + offset?: InputMaybe +} + export type QueryRecentPublishedArgs = { limit: Scalars['Int'] offset: Scalars['Int'] @@ -455,12 +490,30 @@ export type QueryRecentReactedArgs = { offset: Scalars['Int'] } +export type QuerySearchChatsArgs = { + amount?: InputMaybe + offset?: InputMaybe + q: Scalars['String'] +} + +export type QuerySearchMessagesArgs = { + amount?: InputMaybe + offset?: InputMaybe + q: Scalars['String'] +} + export type QuerySearchQueryArgs = { limit: Scalars['Int'] offset: Scalars['Int'] q?: InputMaybe } +export type QuerySearchUsersArgs = { + amount?: InputMaybe + offset?: InputMaybe + q: Scalars['String'] +} + export type QueryShoutsByAuthorsArgs = { limit: Scalars['Int'] offset: Scalars['Int'] @@ -479,6 +532,12 @@ export type QueryShoutsByCommunitiesArgs = { slugs: Array> } +export type QueryShoutsByLayoutArgs = { + amount: Scalars['Int'] + layout?: InputMaybe + offset: Scalars['Int'] +} + export type QueryShoutsByTopicsArgs = { limit: Scalars['Int'] offset: Scalars['Int'] @@ -506,11 +565,23 @@ export type QueryTopCommentedArgs = { offset: Scalars['Int'] } +export type QueryTopLayoutShoutsArgs = { + amount?: InputMaybe + layout: Scalars['String'] + offset?: InputMaybe +} + export type QueryTopMonthArgs = { limit: Scalars['Int'] offset: Scalars['Int'] } +export type QueryTopMonthLayoutShoutsArgs = { + amount?: InputMaybe + layout: Scalars['String'] + offset?: InputMaybe +} + export type QueryTopOverallArgs = { limit: Scalars['Int'] offset: Scalars['Int'] @@ -619,8 +690,8 @@ export type Resource = { } export type Result = { - author?: Maybe - authors?: Maybe>> + author?: Maybe + authors?: Maybe>> chat?: Maybe chats?: Maybe>> communities?: Maybe>> @@ -633,8 +704,10 @@ export type Result = { reactions?: Maybe>> shout?: Maybe shouts?: Maybe>> + slugs?: Maybe>> topic?: Maybe topics?: Maybe>> + uids?: Maybe>> } export type Role = { @@ -653,11 +726,11 @@ export type Shout = { createdAt: Scalars['DateTime'] deletedAt?: Maybe deletedBy?: Maybe - draft?: Maybe id: Scalars['Int'] lang?: Maybe layout?: Maybe mainTopic?: Maybe + media?: Maybe publishedAt?: Maybe publishedBy?: Maybe slug: Scalars['String'] @@ -667,8 +740,8 @@ export type Shout = { topics?: Maybe>> updatedAt?: Maybe updatedBy?: Maybe - versionOf?: Maybe - visibleFor?: Maybe>> + versionOf?: Maybe + visibility?: Maybe } export type ShoutInput = { diff --git a/src/pages/author/[slug]/index.astro b/src/pages/author/[slug]/index.astro index 50e29aa4..f7a2cc9b 100644 --- a/src/pages/author/[slug]/index.astro +++ b/src/pages/author/[slug]/index.astro @@ -7,7 +7,7 @@ import { PRERENDERED_ARTICLES_COUNT } from '../../../components/Views/Author' const slug = Astro.params.slug.toString() const articles = await apiClient.getArticlesForAuthors({ authorSlugs: [slug], limit: PRERENDERED_ARTICLES_COUNT }) -const author = articles[0].authors.find((a) => a.slug === slug) +const author = await apiClient.getAuthor({ slug }) const { pathname, search } = Astro.url initRouter(pathname, search) diff --git a/src/pages/topic/[slug].astro b/src/pages/topic/[slug].astro index 91ebde65..c8f61246 100644 --- a/src/pages/topic/[slug].astro +++ b/src/pages/topic/[slug].astro @@ -6,7 +6,7 @@ import { PRERENDERED_ARTICLES_COUNT } from '../../components/Views/Topic' const slug = Astro.params.slug?.toString() || '' const articles = await apiClient.getArticlesForTopics({ topicSlugs: [slug], limit: PRERENDERED_ARTICLES_COUNT }) -const topic = articles[0].topics.find(({ slug: topicSlug }) => topicSlug === slug) +const topic = await apiClient.getTopic({ slug }) import { initRouter } from '../../stores/router' diff --git a/src/stores/auth.ts b/src/stores/auth.ts deleted file mode 100644 index 9662ebe8..00000000 --- a/src/stores/auth.ts +++ /dev/null @@ -1,74 +0,0 @@ -import type { AuthResult } from '../graphql/types.gen' -import { resetToken, setToken } from '../graphql/privateGraphQLClient' -import { apiClient } from '../utils/apiClient' -import { createSignal } from 'solid-js' - -const [session, setSession] = createSignal(null) - -export const signIn = async (params) => { - const authResult = await apiClient.authLogin(params) - setSession(authResult) - setToken(authResult.token) - console.debug('signed in') -} -export const signOut = () => { - // TODO: call backend to revoke token - setSession(null) - resetToken() - console.debug('signed out') -} - -export const [emailChecks, setEmailChecks] = createSignal<{ [email: string]: boolean }>({}) - -export const checkEmail = async (email: string): Promise => { - if (emailChecks()[email]) { - return true - } - - const checkResult = await apiClient.authCheckEmail({ email }) - - if (checkResult) { - setEmailChecks((oldEmailChecks) => ({ ...oldEmailChecks, [email]: true })) - return true - } - - return false -} - -export const [resetCode, setResetCode] = createSignal('') - -export const register = async ({ - name, - email, - password -}: { - name: string - email: string - password: string -}) => { - await apiClient.authRegister({ - name, - email, - password - }) -} - -export const signSendLink = async ({ email, lang }: { email: string; lang: string }) => { - return await apiClient.authSendLink({ email, lang }) -} - -export const renewSession = async () => { - const authResult = await apiClient.getSession() // token in header - setToken(authResult.token) - setSession(authResult) -} - -export const confirmEmail = async (token: string) => { - const authResult = await apiClient.confirmEmail({ token }) - setToken(authResult.token) - setSession(authResult) -} - -export const useAuthStore = () => { - return { session, emailChecks } -} diff --git a/src/stores/emailChecks.ts b/src/stores/emailChecks.ts new file mode 100644 index 00000000..62c5e71f --- /dev/null +++ b/src/stores/emailChecks.ts @@ -0,0 +1,23 @@ +import { apiClient } from '../utils/apiClient' +import { createSignal } from 'solid-js' + +const [emailChecks, setEmailChecks] = createSignal<{ [email: string]: boolean }>({}) + +export const checkEmail = async (email: string): Promise => { + if (emailChecks()[email]) { + return true + } + + const checkResult = await apiClient.authCheckEmail({ email }) + + if (checkResult) { + setEmailChecks((oldEmailChecks) => ({ ...oldEmailChecks, [email]: true })) + return true + } + + return false +} + +export const useEmailChecks = () => { + return { emailChecks } +} diff --git a/src/stores/ui.ts b/src/stores/ui.ts index 8b71e4d6..d71801a8 100644 --- a/src/stores/ui.ts +++ b/src/stores/ui.ts @@ -1,6 +1,8 @@ //import { persistentAtom } from '@nanostores/persistent' import { createSignal } from 'solid-js' import { useRouter } from './router' +import type { AuthModalSearchParams, ConfirmEmailSearchParams } from '../components/Nav/AuthModal/types' +import type { RootSearchParams } from '../components/types' //export const locale = persistentAtom('locale', 'ru') export const [locale, setLocale] = createSignal('ru') @@ -26,10 +28,22 @@ const [modal, setModal] = createSignal(null) const [warnings, setWarnings] = createSignal([]) export const showModal = (modalType: ModalType) => setModal(modalType) + +// TODO: find a better solution export const hideModal = () => { - const { changeSearchParam } = useRouter() + const { searchParams, changeSearchParam } = useRouter< + AuthModalSearchParams & ConfirmEmailSearchParams & RootSearchParams + >() + + if (searchParams().modal === 'auth') { + if (searchParams().mode === 'confirm-email') { + changeSearchParam('token', null, true) + } + changeSearchParam('mode', null, true) + } + changeSearchParam('modal', null, true) - changeSearchParam('mode', null, true) + setModal(null) } diff --git a/src/stores/zine/authors.ts b/src/stores/zine/authors.ts index 2a5cd7f8..527ae5e5 100644 --- a/src/stores/zine/authors.ts +++ b/src/stores/zine/authors.ts @@ -52,9 +52,7 @@ const addAuthors = (authors: Author[]) => { } export const loadAuthor = async ({ slug }: { slug: string }): Promise => { - // TODO: - const articles = await apiClient.getArticlesForAuthors({ authorSlugs: [slug], limit: 1 }) - const author = articles[0].authors.find((a) => a.slug === slug) + const author = await apiClient.getAuthor({ slug }) addAuthors([author]) } diff --git a/src/stores/zine/topics.ts b/src/stores/zine/topics.ts index 4992d873..12e3abf3 100644 --- a/src/stores/zine/topics.ts +++ b/src/stores/zine/topics.ts @@ -100,9 +100,7 @@ export const loadRandomTopics = async (): Promise => { } export const loadTopic = async ({ slug }: { slug: string }): Promise => { - // TODO: - const articles = await apiClient.getArticlesForTopics({ topicSlugs: [slug], limit: 1 }) - const topic = articles[0].topics.find(({ slug: topicSlug }) => topicSlug === slug) + const topic = await apiClient.getTopic({ slug }) addTopics([topic]) } diff --git a/src/styles/AllTopics.module.scss b/src/styles/AllTopics.module.scss index d6aecc23..c146af9e 100644 --- a/src/styles/AllTopics.module.scss +++ b/src/styles/AllTopics.module.scss @@ -35,3 +35,12 @@ .stats { margin-top: 2.4rem; } + +.loadMoreContainer { + margin-top: 48px; + text-align: center; + + .loadMoreButton { + padding: 0.6em 1.5em; + } +} diff --git a/src/styles/app.scss b/src/styles/app.scss index ba11cbcc..7bb2d4b7 100644 --- a/src/styles/app.scss +++ b/src/styles/app.scss @@ -663,7 +663,7 @@ astro-island { width: auto; @include media-breakpoint-down(sm) { - //padding: 0 $container-padding-x * 0.5; + // padding: 0 $container-padding-x * 0.5; } } diff --git a/src/utils/apiClient.ts b/src/utils/apiClient.ts index d798488e..8f8e65eb 100644 --- a/src/utils/apiClient.ts +++ b/src/utils/apiClient.ts @@ -1,4 +1,12 @@ -import type { Reaction, Shout, FollowingEntity, AuthResult, ShoutInput } from '../graphql/types.gen' +import type { + Reaction, + Shout, + FollowingEntity, + AuthResult, + ShoutInput, + Topic, + Author +} from '../graphql/types.gen' import { publicGraphQLClient } from '../graphql/publicGraphQLClient' import { privateGraphQLClient } from '../graphql/privateGraphQLClient' import articleBySlug from '../graphql/query/article-by-slug' @@ -25,10 +33,11 @@ import authorsAll from '../graphql/query/authors-all' import reactionCreate from '../graphql/mutation/reaction-create' import reactionDestroy from '../graphql/mutation/reaction-destroy' import reactionUpdate from '../graphql/mutation/reaction-update' -import authorsBySlugs from '../graphql/query/authors-by-slugs' import incrementView from '../graphql/mutation/increment-view' import createArticle from '../graphql/mutation/article-create' import myChats from '../graphql/query/my-chats' +import authorBySlug from '../graphql/query/author-by-slug' +import topicBySlug from '../graphql/query/topic-by-slug' const FEED_SIZE = 50 @@ -44,7 +53,7 @@ export class ApiError extends Error { } export const apiClient = { - authLogin: async ({ email, password }): Promise => { + authLogin: async ({ email, password }: { email: string; password: string }): Promise => { const response = await publicGraphQLClient.query(authLoginQuery, { email, password }).toPromise() // console.debug('[api-client] authLogin', { response }) if (response.error) { @@ -98,6 +107,11 @@ export const apiClient = { authSendLink: async ({ email, lang }) => { // send link with code on email const response = await publicGraphQLClient.mutation(authSendLinkMutation, { email, lang }).toPromise() + + if (response.error) { + throw new ApiError('unknown', response.error.message) + } + return response.data.sendLink }, confirmEmail: async ({ token }: { token: string }) => { @@ -241,7 +255,6 @@ export const apiClient = { const response = await privateGraphQLClient.mutation(mySession, {}).toPromise() if (response.error) { - // TODO throw new ApiError('unknown', response.error.message) } @@ -274,9 +287,13 @@ export const apiClient = { } return response.data.authorsAll }, - getAuthor: async ({ slug }: { slug: string }) => { - const response = await publicGraphQLClient.query(authorsBySlugs, { slugs: [slug] }).toPromise() - return response.data.getUsersBySlugs + getAuthor: async ({ slug }: { slug: string }): Promise => { + const response = await publicGraphQLClient.query(authorBySlug, { slug }).toPromise() + return response.data.getAuthor + }, + getTopic: async ({ slug }: { slug: string }): Promise => { + const response = await publicGraphQLClient.query(topicBySlug, { slug }).toPromise() + return response.data.getTopic }, getArticle: async ({ slug }: { slug: string }): Promise => { const response = await publicGraphQLClient.query(articleBySlug, { slug }).toPromise() @@ -304,10 +321,6 @@ export const apiClient = { return response.data.reactionsForShouts }, - getAuthorsBySlugs: async ({ slugs }) => { - const response = await publicGraphQLClient.query(authorsBySlugs, { slugs }).toPromise() - return response.data.getUsersBySlugs - }, createArticle: async ({ article }: { article: ShoutInput }) => { const response = await privateGraphQLClient.mutation(createArticle, { shout: article }).toPromise() console.debug('createArticle response:', response) diff --git a/src/utils/config.ts b/src/utils/config.ts index ab0ab70b..4678a034 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -1,4 +1,4 @@ export const isDev = import.meta.env.MODE === 'development' -export const apiBaseUrl = 'https://newapi.discours.io' +export const apiBaseUrl = 'https://testapi.discours.io' // export const apiBaseUrl = 'http://localhost:8080' From f66015cd771415d08ecb7cce9270a621c1d6fdd7 Mon Sep 17 00:00:00 2001 From: Igor Lobanov Date: Mon, 14 Nov 2022 02:17:12 +0100 Subject: [PATCH 26/34] more error handling --- src/components/Author/Card.module.scss | 3 +- src/components/Nav/AuthModal/EmailConfirm.tsx | 56 +++++++++++++++---- .../Nav/AuthModal/ForgotPasswordForm.tsx | 25 ++++++++- src/components/Nav/AuthModal/LoginForm.tsx | 1 + src/context/auth.tsx | 3 + src/graphql/privateGraphQLClient.ts | 4 ++ src/locales/ru.json | 4 +- src/utils/apiClient.ts | 32 ++++++++++- 8 files changed, 111 insertions(+), 17 deletions(-) diff --git a/src/components/Author/Card.module.scss b/src/components/Author/Card.module.scss index bb45951c..b51d4c13 100644 --- a/src/components/Author/Card.module.scss +++ b/src/components/Author/Card.module.scss @@ -15,9 +15,10 @@ .authorDetails { display: flex; flex: 1; - // padding-right: 1.2rem; width: max-content; + // padding-right: 1.2rem; + @include media-breakpoint-down(sm) { flex-wrap: wrap; } diff --git a/src/components/Nav/AuthModal/EmailConfirm.tsx b/src/components/Nav/AuthModal/EmailConfirm.tsx index d458ca3b..ac93ae77 100644 --- a/src/components/Nav/AuthModal/EmailConfirm.tsx +++ b/src/components/Nav/AuthModal/EmailConfirm.tsx @@ -1,11 +1,13 @@ import styles from './AuthModal.module.scss' import { clsx } from 'clsx' import { t } from '../../../utils/intl' -import { hideModal } from '../../../stores/ui' -import { createMemo, onMount, Show } from 'solid-js' -import { useRouter } from '../../../stores/router' +import { hideModal, locale } from '../../../stores/ui' +import { createMemo, createSignal, onMount, Show } from 'solid-js' +import { handleClientRouteLinkClick, useRouter } from '../../../stores/router' import type { ConfirmEmailSearchParams } from './types' -import { useAuth } from '../../../context/auth' +import { signSendLink, useAuth } from '../../../context/auth' +import { ApiError } from '../../../utils/apiClient' +import { email } from './sharedLogic' export const EmailConfirm = () => { const { @@ -13,6 +15,9 @@ export const EmailConfirm = () => { actions: { confirmEmail } } = useAuth() + const [isTokenExpired, setIsTokenExpired] = createSignal(false) + const [isTokenInvalid, setIsTokenInvalid] = createSignal(false) + const confirmedEmail = createMemo(() => session()?.user?.email || '') const { searchParams } = useRouter() @@ -22,23 +27,54 @@ export const EmailConfirm = () => { try { await confirmEmail(token) } catch (error) { + if (error instanceof ApiError) { + if (error.code === 'token_expired') { + setIsTokenExpired(true) + return + } + + if (error.code === 'token_invalid') { + setIsTokenInvalid(true) + return + } + } + console.log(error) } }) return (
          -
          {t('Hooray! Welcome!')}
          + {/* TODO: texts */} + +
          Ссылка больше не действительна
          + +
          + +
          Неправильная ссылка
          + +
          +
          {t('Hooray! Welcome!')}
          {t("You've confirmed email")} {confirmedEmail()}
          +
          + +
          -
          - -
          ) } diff --git a/src/components/Nav/AuthModal/ForgotPasswordForm.tsx b/src/components/Nav/AuthModal/ForgotPasswordForm.tsx index d6b3621e..81116779 100644 --- a/src/components/Nav/AuthModal/ForgotPasswordForm.tsx +++ b/src/components/Nav/AuthModal/ForgotPasswordForm.tsx @@ -8,6 +8,7 @@ import type { AuthModalSearchParams } from './types' import { isValidEmail } from './validators' import { locale } from '../../../stores/ui' import { signSendLink } from '../../../context/auth' +import { ApiError } from '../../../utils/apiClient' type FormFields = { email: string @@ -26,11 +27,13 @@ export const ForgotPasswordForm = () => { const [submitError, setSubmitError] = createSignal('') const [isSubmitting, setIsSubmitting] = createSignal(false) const [validationErrors, setValidationErrors] = createSignal({}) + const [isUserNotFount, setIsUserNotFound] = createSignal(false) const handleSubmit = async (event: Event) => { event.preventDefault() setSubmitError('') + setIsUserNotFound(false) const newValidationErrors: ValidationErrors = {} @@ -51,9 +54,12 @@ export const ForgotPasswordForm = () => { setIsSubmitting(true) try { - const result = await signSendLink({ email: email(), lang: locale() }) - if (result.error) setSubmitError(result.error) + await signSendLink({ email: email(), lang: locale() }) } catch (error) { + if (error instanceof ApiError && error.code === 'user_not_found') { + setIsUserNotFound(true) + return + } setSubmitError(error.message) } finally { setIsSubmitting(false) @@ -71,6 +77,21 @@ export const ForgotPasswordForm = () => {
        + +
        + {/*TODO: text*/} + {t("We can't find you, check email or")}{' '} + { + event.preventDefault() + changeSearchParam('mode', 'register') + }} + > + {t('register')} + +
        +
        {validationErrors().email}
        diff --git a/src/components/Nav/AuthModal/LoginForm.tsx b/src/components/Nav/AuthModal/LoginForm.tsx index 92d5cfd0..50669263 100644 --- a/src/components/Nav/AuthModal/LoginForm.tsx +++ b/src/components/Nav/AuthModal/LoginForm.tsx @@ -57,6 +57,7 @@ export const LoginForm = () => { event.preventDefault() setIsLinkSent(false) + setIsEmailNotConfirmed(false) setSubmitError('') const newValidationErrors: ValidationErrors = {} diff --git a/src/context/auth.tsx b/src/context/auth.tsx index 7fe11945..71792145 100644 --- a/src/context/auth.tsx +++ b/src/context/auth.tsx @@ -20,6 +20,9 @@ const AuthContext = createContext() const refreshSession = async (): Promise => { try { const authResult = await apiClient.getSession() + if (!authResult) { + return null + } setToken(authResult.token) return authResult } catch (error) { diff --git a/src/graphql/privateGraphQLClient.ts b/src/graphql/privateGraphQLClient.ts index 0de1202a..442343cf 100644 --- a/src/graphql/privateGraphQLClient.ts +++ b/src/graphql/privateGraphQLClient.ts @@ -10,6 +10,10 @@ if (isDev) { exchanges.unshift(devtoolsExchange) } +export const getToken = (): string => { + return localStorage.getItem(TOKEN_LOCAL_STORAGE_KEY) +} + export const setToken = (token: string) => { localStorage.setItem(TOKEN_LOCAL_STORAGE_KEY, token) } diff --git a/src/locales/ru.json b/src/locales/ru.json index e8b1a94d..a7644c70 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -169,5 +169,7 @@ "Send link again": "Прислать ссылку ещё раз", "Link sent, check your email": "Ссылка отправлена, проверьте почту", "Create post": "Создать публикацию", - "Just start typing...": "Просто начните печатать..." + "Just start typing...": "Просто начните печатать...", + "We can't find you, check email or": "Не можем вас найти, проверьте адрес электронной почты или", + "register": "зарегистрируйтесь" } diff --git a/src/utils/apiClient.ts b/src/utils/apiClient.ts index 8f8e65eb..61769847 100644 --- a/src/utils/apiClient.ts +++ b/src/utils/apiClient.ts @@ -8,7 +8,7 @@ import type { Author } from '../graphql/types.gen' import { publicGraphQLClient } from '../graphql/publicGraphQLClient' -import { privateGraphQLClient } from '../graphql/privateGraphQLClient' +import { getToken, privateGraphQLClient } from '../graphql/privateGraphQLClient' import articleBySlug from '../graphql/query/article-by-slug' import articlesRecentAll from '../graphql/query/articles-recent-all' import articlesRecentPublished from '../graphql/query/articles-recent-published' @@ -41,7 +41,13 @@ import topicBySlug from '../graphql/query/topic-by-slug' const FEED_SIZE = 50 -type ApiErrorCode = 'unknown' | 'email_not_confirmed' | 'user_not_found' | 'user_already_exists' +type ApiErrorCode = + | 'unknown' + | 'email_not_confirmed' + | 'user_not_found' + | 'user_already_exists' + | 'token_expired' + | 'token_invalid' export class ApiError extends Error { code: ApiErrorCode @@ -109,16 +115,32 @@ export const apiClient = { const response = await publicGraphQLClient.mutation(authSendLinkMutation, { email, lang }).toPromise() if (response.error) { + if (response.error.message === '[GraphQL] User not found') { + throw new ApiError('user_not_found', response.error.message) + } + throw new ApiError('unknown', response.error.message) } + if (response.data.sendLink.error) { + throw new ApiError('unknown', response.data.sendLink.message) + } + return response.data.sendLink }, confirmEmail: async ({ token }: { token: string }) => { // confirm email with code from link const response = await publicGraphQLClient.mutation(authConfirmEmailMutation, { token }).toPromise() - if (response.error) { + // TODO: better error communication + if (response.error.message === '[GraphQL] check token lifetime') { + throw new ApiError('token_expired', response.error.message) + } + + if (response.error.message === '[GraphQL] token is not valid') { + throw new ApiError('token_invalid', response.error.message) + } + throw new ApiError('unknown', response.error.message) } @@ -251,6 +273,10 @@ export const apiClient = { }, getSession: async (): Promise => { + if (!getToken()) { + return null + } + // renew session with auth token in header (!) const response = await privateGraphQLClient.mutation(mySession, {}).toPromise() From b56f5e5751b795e8b99832ba50a40f655bedc7c0 Mon Sep 17 00:00:00 2001 From: Igor Lobanov Date: Mon, 14 Nov 2022 02:20:02 +0100 Subject: [PATCH 27/34] lint --- src/components/Nav/AuthModal/EmailConfirm.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/Nav/AuthModal/EmailConfirm.tsx b/src/components/Nav/AuthModal/EmailConfirm.tsx index ac93ae77..7525bdf2 100644 --- a/src/components/Nav/AuthModal/EmailConfirm.tsx +++ b/src/components/Nav/AuthModal/EmailConfirm.tsx @@ -1,13 +1,12 @@ import styles from './AuthModal.module.scss' import { clsx } from 'clsx' import { t } from '../../../utils/intl' -import { hideModal, locale } from '../../../stores/ui' +import { hideModal } from '../../../stores/ui' import { createMemo, createSignal, onMount, Show } from 'solid-js' import { handleClientRouteLinkClick, useRouter } from '../../../stores/router' import type { ConfirmEmailSearchParams } from './types' -import { signSendLink, useAuth } from '../../../context/auth' +import { useAuth } from '../../../context/auth' import { ApiError } from '../../../utils/apiClient' -import { email } from './sharedLogic' export const EmailConfirm = () => { const { From 269f7328d81cb9103b69701737f9a8d838335ee4 Mon Sep 17 00:00:00 2001 From: Igor Lobanov Date: Mon, 14 Nov 2022 09:24:20 +0100 Subject: [PATCH 28/34] testapi -> newapi --- codegen.yml | 2 +- src/utils/config.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/codegen.yml b/codegen.yml index 04ea8542..1e8c8a47 100644 --- a/codegen.yml +++ b/codegen.yml @@ -1,5 +1,5 @@ overwrite: true -schema: 'https://testapi.discours.io/graphql' +schema: 'https://newapi.discours.io/graphql' generates: src/graphql/introspec.gen.ts: plugins: diff --git a/src/utils/config.ts b/src/utils/config.ts index 4678a034..ab0ab70b 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -1,4 +1,4 @@ export const isDev = import.meta.env.MODE === 'development' -export const apiBaseUrl = 'https://testapi.discours.io' +export const apiBaseUrl = 'https://newapi.discours.io' // export const apiBaseUrl = 'http://localhost:8080' From 338d9e3d081b7a4ad62c63b9da5ee8df2ea399ca Mon Sep 17 00:00:00 2001 From: Igor Lobanov Date: Mon, 14 Nov 2022 11:02:08 +0100 Subject: [PATCH 29/34] code review --- src/components/Article/FullArticle.tsx | 4 +-- src/components/Article/SharePopup.tsx | 6 ++-- src/components/Author/Card.tsx | 4 +-- src/components/Feed/Sidebar.tsx | 4 +-- src/components/Nav/AuthModal/EmailConfirm.tsx | 4 +-- .../Nav/AuthModal/ForgotPasswordForm.tsx | 2 +- src/components/Nav/AuthModal/LoginForm.tsx | 5 +-- src/components/Nav/AuthModal/RegisterForm.tsx | 2 +- src/components/Nav/HeaderAuth.tsx | 14 +++----- src/components/Nav/ProfileModal.tsx | 4 +-- src/components/Nav/ProfilePopup.tsx | 9 ++--- src/components/Root.tsx | 6 ++-- src/components/Topic/Card.tsx | 4 +-- src/components/Topic/Full.tsx | 4 +-- src/components/Views/AllAuthors.tsx | 4 +-- src/components/Views/AllTopics.tsx | 4 +-- src/components/Views/Create.tsx | 12 ++----- src/components/Views/Feed.tsx | 4 +-- src/components/_shared/ClientContainer.tsx | 12 +++++++ .../{Nav => _shared}/Popup.module.scss | 0 src/components/{Nav => _shared}/Popup.tsx | 0 src/context/{auth.tsx => session.tsx} | 34 ++++--------------- src/stores/auth.ts | 21 ++++++++++++ 23 files changed, 85 insertions(+), 78 deletions(-) create mode 100644 src/components/_shared/ClientContainer.tsx rename src/components/{Nav => _shared}/Popup.module.scss (100%) rename src/components/{Nav => _shared}/Popup.tsx (100%) rename src/context/{auth.tsx => session.tsx} (73%) create mode 100644 src/stores/auth.ts diff --git a/src/components/Article/FullArticle.tsx b/src/components/Article/FullArticle.tsx index 717aff64..d521c250 100644 --- a/src/components/Article/FullArticle.tsx +++ b/src/components/Article/FullArticle.tsx @@ -10,7 +10,7 @@ import { showModal } from '../../stores/ui' import { incrementView } from '../../stores/zine/articles' import MD from './MD' import { SharePopup } from './SharePopup' -import { useAuth } from '../../context/auth' +import { useSession } from '../../context/session' const MAX_COMMENT_LEVEL = 6 @@ -38,7 +38,7 @@ const formatDate = (date: Date) => { } export const FullArticle = (props: ArticleProps) => { - const { session } = useAuth() + const { session } = useSession() onMount(() => { incrementView({ articleSlug: props.article.slug }) diff --git a/src/components/Article/SharePopup.tsx b/src/components/Article/SharePopup.tsx index 851cf599..fe0de743 100644 --- a/src/components/Article/SharePopup.tsx +++ b/src/components/Article/SharePopup.tsx @@ -1,7 +1,9 @@ import { Icon } from '../Nav/Icon' -import styles from '../Nav/Popup.module.scss' import { t } from '../../utils/intl' -import { Popup, PopupProps } from '../Nav/Popup' + +import styles from '../_shared/Popup.module.scss' +import type { PopupProps } from '../_shared/Popup' +import { Popup } from '../_shared/Popup' type SharePopupProps = Omit diff --git a/src/components/Author/Card.tsx b/src/components/Author/Card.tsx index 7cfa677d..4115eda1 100644 --- a/src/components/Author/Card.tsx +++ b/src/components/Author/Card.tsx @@ -8,7 +8,7 @@ import { t } from '../../utils/intl' import { locale } from '../../stores/ui' import { follow, unfollow } from '../../stores/zine/common' import { clsx } from 'clsx' -import { useAuth } from '../../context/auth' +import { useSession } from '../../context/session' interface AuthorCardProps { compact?: boolean @@ -23,7 +23,7 @@ interface AuthorCardProps { } export const AuthorCard = (props: AuthorCardProps) => { - const { session } = useAuth() + const { session } = useSession() const subscribed = createMemo( () => session()?.news?.authors?.some((u) => u === props.author.slug) || false diff --git a/src/components/Feed/Sidebar.tsx b/src/components/Feed/Sidebar.tsx index 31b8858c..1dc4edc1 100644 --- a/src/components/Feed/Sidebar.tsx +++ b/src/components/Feed/Sidebar.tsx @@ -6,7 +6,7 @@ import { Icon } from '../Nav/Icon' import { useTopicsStore } from '../../stores/zine/topics' import { useArticlesStore } from '../../stores/zine/articles' import { useSeenStore } from '../../stores/zine/seen' -import { useAuth } from '../../context/auth' +import { useSession } from '../../context/session' type FeedSidebarProps = { authors: Author[] @@ -14,7 +14,7 @@ type FeedSidebarProps = { export const FeedSidebar = (props: FeedSidebarProps) => { const { getSeen: seen } = useSeenStore() - const { session } = useAuth() + const { session } = useSession() const { authorEntities } = useAuthorsStore({ authors: props.authors }) const { articlesByTopic } = useArticlesStore() const { topicEntities } = useTopicsStore() diff --git a/src/components/Nav/AuthModal/EmailConfirm.tsx b/src/components/Nav/AuthModal/EmailConfirm.tsx index 7525bdf2..b734d23d 100644 --- a/src/components/Nav/AuthModal/EmailConfirm.tsx +++ b/src/components/Nav/AuthModal/EmailConfirm.tsx @@ -5,14 +5,14 @@ import { hideModal } from '../../../stores/ui' import { createMemo, createSignal, onMount, Show } from 'solid-js' import { handleClientRouteLinkClick, useRouter } from '../../../stores/router' import type { ConfirmEmailSearchParams } from './types' -import { useAuth } from '../../../context/auth' import { ApiError } from '../../../utils/apiClient' +import { useSession } from '../../../context/session' export const EmailConfirm = () => { const { session, actions: { confirmEmail } - } = useAuth() + } = useSession() const [isTokenExpired, setIsTokenExpired] = createSignal(false) const [isTokenInvalid, setIsTokenInvalid] = createSignal(false) diff --git a/src/components/Nav/AuthModal/ForgotPasswordForm.tsx b/src/components/Nav/AuthModal/ForgotPasswordForm.tsx index 81116779..6a92582b 100644 --- a/src/components/Nav/AuthModal/ForgotPasswordForm.tsx +++ b/src/components/Nav/AuthModal/ForgotPasswordForm.tsx @@ -7,8 +7,8 @@ import { email, setEmail } from './sharedLogic' import type { AuthModalSearchParams } from './types' import { isValidEmail } from './validators' import { locale } from '../../../stores/ui' -import { signSendLink } from '../../../context/auth' import { ApiError } from '../../../utils/apiClient' +import { signSendLink } from '../../../stores/auth' type FormFields = { email: string diff --git a/src/components/Nav/AuthModal/LoginForm.tsx b/src/components/Nav/AuthModal/LoginForm.tsx index 50669263..89f98d66 100644 --- a/src/components/Nav/AuthModal/LoginForm.tsx +++ b/src/components/Nav/AuthModal/LoginForm.tsx @@ -9,7 +9,8 @@ import { email, setEmail } from './sharedLogic' import { useRouter } from '../../../stores/router' import type { AuthModalSearchParams } from './types' import { hideModal, locale } from '../../../stores/ui' -import { signSendLink, useAuth } from '../../../context/auth' +import { useSession } from '../../../context/session' +import { signSendLink } from '../../../stores/auth' type FormFields = { email: string @@ -28,7 +29,7 @@ export const LoginForm = () => { const { actions: { signIn } - } = useAuth() + } = useSession() const { changeSearchParam } = useRouter() diff --git a/src/components/Nav/AuthModal/RegisterForm.tsx b/src/components/Nav/AuthModal/RegisterForm.tsx index d936f659..b5f56fb7 100644 --- a/src/components/Nav/AuthModal/RegisterForm.tsx +++ b/src/components/Nav/AuthModal/RegisterForm.tsx @@ -11,7 +11,7 @@ import { useRouter } from '../../../stores/router' import type { AuthModalSearchParams } from './types' import { hideModal } from '../../../stores/ui' import { checkEmail, useEmailChecks } from '../../../stores/emailChecks' -import { register } from '../../../context/auth' +import { register } from '../../../stores/auth' type FormFields = { name: string diff --git a/src/components/Nav/HeaderAuth.tsx b/src/components/Nav/HeaderAuth.tsx index 970d9005..a98566d8 100644 --- a/src/components/Nav/HeaderAuth.tsx +++ b/src/components/Nav/HeaderAuth.tsx @@ -9,19 +9,19 @@ import { ProfilePopup } from './ProfilePopup' import Userpic from '../Author/Userpic' import type { Author } from '../../graphql/types.gen' import { showModal, useWarningsStore } from '../../stores/ui' -import { useAuth } from '../../context/auth' +import { ClientContainer } from '../_shared/ClientContainer' +import { useSession } from '../../context/session' type HeaderAuthProps = { setIsProfilePopupVisible: (value: boolean) => void } export const HeaderAuth = (props: HeaderAuthProps) => { - const [isMounted, setIsMounted] = createSignal(false) const { page } = useRouter() const [visibleWarnings, setVisibleWarnings] = createSignal(false) const { warnings } = useWarningsStore() - const { session, isAuthenticated } = useAuth() + const { session, isAuthenticated } = useSession() const toggleWarnings = () => setVisibleWarnings(!visibleWarnings()) @@ -36,12 +36,8 @@ export const HeaderAuth = (props: HeaderAuthProps) => { toggleWarnings() } - onMount(() => { - setIsMounted(true) - }) - return ( - +
        @@ -106,6 +102,6 @@ export const HeaderAuth = (props: HeaderAuthProps) => {
        -
        + ) } diff --git a/src/components/Nav/ProfileModal.tsx b/src/components/Nav/ProfileModal.tsx index 10b69d7f..ddfabe87 100644 --- a/src/components/Nav/ProfileModal.tsx +++ b/src/components/Nav/ProfileModal.tsx @@ -3,13 +3,13 @@ import type { Author } from '../../graphql/types.gen' import { t } from '../../utils/intl' import { hideModal } from '../../stores/ui' import { createMemo, For } from 'solid-js' -import { useAuth } from '../../context/auth' +import { useSession } from '../../context/session' export const ProfileModal = () => { const { session, actions: { signOut } - } = useAuth() + } = useSession() const quit = () => { signOut() diff --git a/src/components/Nav/ProfilePopup.tsx b/src/components/Nav/ProfilePopup.tsx index f0fb7772..807b0cdd 100644 --- a/src/components/Nav/ProfilePopup.tsx +++ b/src/components/Nav/ProfilePopup.tsx @@ -1,6 +1,7 @@ -import { Popup, PopupProps } from './Popup' -import styles from './Popup.module.scss' -import { useAuth } from '../../context/auth' +import { useSession } from '../../context/session' +import type { PopupProps } from '../_shared/Popup' +import { Popup } from '../_shared/Popup' +import styles from '../_shared/Popup.module.scss' type ProfilePopupProps = Omit @@ -8,7 +9,7 @@ export const ProfilePopup = (props: ProfilePopupProps) => { const { session, actions: { signOut } - } = useAuth() + } = useSession() return ( diff --git a/src/components/Root.tsx b/src/components/Root.tsx index 787bcca1..46efc845 100644 --- a/src/components/Root.tsx +++ b/src/components/Root.tsx @@ -29,7 +29,7 @@ import { TermsOfUsePage } from './Pages/about/TermsOfUsePage' import { ThanksPage } from './Pages/about/ThanksPage' import { CreatePage } from './Pages/CreatePage' import { ConnectPage } from './Pages/ConnectPage' -import { AuthProvider } from '../context/auth' +import { SessionProvider } from '../context/session' // TODO: lazy load // const HomePage = lazy(() => import('./Pages/HomePage')) @@ -103,8 +103,8 @@ export const Root = (props: PageProps) => { } return ( - + - + ) } diff --git a/src/components/Topic/Card.tsx b/src/components/Topic/Card.tsx index ffcf8b9f..0b153cbe 100644 --- a/src/components/Topic/Card.tsx +++ b/src/components/Topic/Card.tsx @@ -8,7 +8,7 @@ import { locale } from '../../stores/ui' import { follow, unfollow } from '../../stores/zine/common' import { getLogger } from '../../utils/logger' import { clsx } from 'clsx' -import { useAuth } from '../../context/auth' +import { useSession } from '../../context/session' const log = getLogger('TopicCard') @@ -24,7 +24,7 @@ interface TopicProps { } export const TopicCard = (props: TopicProps) => { - const { session } = useAuth() + const { session } = useSession() const subscribed = createMemo(() => { if (!session()?.user?.slug || !session()?.news?.topics) { diff --git a/src/components/Topic/Full.tsx b/src/components/Topic/Full.tsx index 70d78da8..86c48852 100644 --- a/src/components/Topic/Full.tsx +++ b/src/components/Topic/Full.tsx @@ -5,14 +5,14 @@ import styles from './Full.module.scss' import { follow, unfollow } from '../../stores/zine/common' import { t } from '../../utils/intl' import { clsx } from 'clsx' -import { useAuth } from '../../context/auth' +import { useSession } from '../../context/session' type Props = { topic: Topic } export const FullTopic = (props: Props) => { - const { session } = useAuth() + const { session } = useSession() const subscribed = createMemo(() => session()?.news?.topics?.includes(props.topic?.slug)) return ( diff --git a/src/components/Views/AllAuthors.tsx b/src/components/Views/AllAuthors.tsx index eec96c19..99fedb64 100644 --- a/src/components/Views/AllAuthors.tsx +++ b/src/components/Views/AllAuthors.tsx @@ -7,7 +7,7 @@ import { useAuthorsStore, setAuthorsSort } from '../../stores/zine/authors' import { handleClientRouteLinkClick, useRouter } from '../../stores/router' import styles from '../../styles/AllTopics.module.scss' import { clsx } from 'clsx' -import { useAuth } from '../../context/auth' +import { useSession } from '../../context/session' type AllAuthorsPageSearchParams = { by: '' | 'name' | 'shouts' | 'rating' @@ -23,7 +23,7 @@ export const AllAuthorsView = (props: Props) => { const { sortedAuthors } = useAuthorsStore({ authors: props.authors }) const [limit, setLimit] = createSignal(PAGE_SIZE) - const { session } = useAuth() + const { session } = useSession() createEffect(() => { setAuthorsSort(searchParams().by || 'shouts') diff --git a/src/components/Views/AllTopics.tsx b/src/components/Views/AllTopics.tsx index 2261a10c..d94461f4 100644 --- a/src/components/Views/AllTopics.tsx +++ b/src/components/Views/AllTopics.tsx @@ -7,7 +7,7 @@ import { handleClientRouteLinkClick, useRouter } from '../../stores/router' import { TopicCard } from '../Topic/Card' import styles from '../../styles/AllTopics.module.scss' import { clsx } from 'clsx' -import { useAuth } from '../../context/auth' +import { useSession } from '../../context/session' type AllTopicsPageSearchParams = { by: 'shouts' | 'authors' | 'title' | '' @@ -28,7 +28,7 @@ export const AllTopicsView = (props: AllTopicsViewProps) => { sortBy: searchParams().by || 'shouts' }) - const { session } = useAuth() + const { session } = useSession() createEffect(() => { setTopicsSort(searchParams().by || 'shouts') diff --git a/src/components/Views/Create.tsx b/src/components/Views/Create.tsx index 2281ddf5..636b1386 100644 --- a/src/components/Views/Create.tsx +++ b/src/components/Views/Create.tsx @@ -1,17 +1,11 @@ -import { Show, onMount, createSignal } from 'solid-js' import { Editor } from '../EditorNew/Editor' +import { ClientContainer } from '../_shared/ClientContainer' export const CreateView = () => { - // don't render anything on server - // usage of isServer causing hydration errors - const [isMounted, setIsMounted] = createSignal(false) - - onMount(() => setIsMounted(true)) - return ( - + - + ) } diff --git a/src/components/Views/Feed.tsx b/src/components/Views/Feed.tsx index 36318f12..aca7987b 100644 --- a/src/components/Views/Feed.tsx +++ b/src/components/Views/Feed.tsx @@ -14,7 +14,7 @@ import { useReactionsStore } from '../../stores/zine/reactions' import { useAuthorsStore } from '../../stores/zine/authors' import { useTopicsStore } from '../../stores/zine/topics' import { useTopAuthorsStore } from '../../stores/zine/topAuthors' -import { useAuth } from '../../context/auth' +import { useSession } from '../../context/session' // const AUTHORSHIP_REACTIONS = [ // ReactionKind.Accept, @@ -32,7 +32,7 @@ export const FeedView = () => { const { sortedAuthors } = useAuthorsStore() const { topTopics } = useTopicsStore() const { topAuthors } = useTopAuthorsStore() - const { session } = useAuth() + const { session } = useSession() const topReactions = createMemo(() => sortBy(reactions(), byCreated)) diff --git a/src/components/_shared/ClientContainer.tsx b/src/components/_shared/ClientContainer.tsx new file mode 100644 index 00000000..913efae6 --- /dev/null +++ b/src/components/_shared/ClientContainer.tsx @@ -0,0 +1,12 @@ +import type { JSX } from 'solid-js' +import { createSignal, onMount, Show } from 'solid-js' + +// show children only on client side +// usage of isServer causing hydration errors +export const ClientContainer = (props: { children: JSX.Element }) => { + const [isMounted, setIsMounted] = createSignal(false) + + onMount(() => setIsMounted(true)) + + return {props.children} +} diff --git a/src/components/Nav/Popup.module.scss b/src/components/_shared/Popup.module.scss similarity index 100% rename from src/components/Nav/Popup.module.scss rename to src/components/_shared/Popup.module.scss diff --git a/src/components/Nav/Popup.tsx b/src/components/_shared/Popup.tsx similarity index 100% rename from src/components/Nav/Popup.tsx rename to src/components/_shared/Popup.tsx diff --git a/src/context/auth.tsx b/src/context/session.tsx similarity index 73% rename from src/context/auth.tsx rename to src/context/session.tsx index 71792145..47a51e76 100644 --- a/src/context/auth.tsx +++ b/src/context/session.tsx @@ -4,7 +4,7 @@ import type { AuthResult } from '../graphql/types.gen' import { apiClient } from '../utils/apiClient' import { resetToken, setToken } from '../graphql/privateGraphQLClient' -type AuthContextType = { +type SessionContextType = { session: InitializedResource isAuthenticated: Accessor actions: { @@ -15,7 +15,7 @@ type AuthContextType = { } } -const AuthContext = createContext() +const SessionContext = createContext() const refreshSession = async (): Promise => { try { @@ -32,31 +32,11 @@ const refreshSession = async (): Promise => { } } -export const register = async ({ - name, - email, - password -}: { - name: string - email: string - password: string -}) => { - await apiClient.authRegister({ - name, - email, - password - }) +export function useSession() { + return useContext(SessionContext) } -export const signSendLink = async ({ email, lang }: { email: string; lang: string }) => { - return await apiClient.authSendLink({ email, lang }) -} - -export function useAuth() { - return useContext(AuthContext) -} - -export const AuthProvider = (props: { children: JSX.Element }) => { +export const SessionProvider = (props: { children: JSX.Element }) => { const [session, { refetch: refetchRefreshSession, mutate }] = createResource(refreshSession, { ssrLoadFrom: 'initial', initialValue: null @@ -91,11 +71,11 @@ export const AuthProvider = (props: { children: JSX.Element }) => { confirmEmail } - const value: AuthContextType = { session, isAuthenticated, actions } + const value: SessionContextType = { session, isAuthenticated, actions } onMount(() => { refetchRefreshSession() }) - return {props.children} + return {props.children} } diff --git a/src/stores/auth.ts b/src/stores/auth.ts new file mode 100644 index 00000000..b92814fa --- /dev/null +++ b/src/stores/auth.ts @@ -0,0 +1,21 @@ +import { apiClient } from '../utils/apiClient' + +export const register = async ({ + name, + email, + password +}: { + name: string + email: string + password: string +}) => { + await apiClient.authRegister({ + name, + email, + password + }) +} + +export const signSendLink = async ({ email, lang }: { email: string; lang: string }) => { + return await apiClient.authSendLink({ email, lang }) +} From 59c2283b39f89d0bc1876ce275d6f5b59c0a7973 Mon Sep 17 00:00:00 2001 From: kvakazyambra Date: Mon, 14 Nov 2022 23:32:17 +0300 Subject: [PATCH 30/34] Header minor fixes --- src/components/Nav/Header.module.scss | 12 ++++++------ src/components/Nav/ProfilePopup.tsx | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/Nav/Header.module.scss b/src/components/Nav/Header.module.scss index f683397b..6592658d 100644 --- a/src/components/Nav/Header.module.scss +++ b/src/components/Nav/Header.module.scss @@ -135,6 +135,10 @@ position: relative; @include font-size(1.7rem); + @include media-breakpoint-down(lg) { + margin-right: 5rem; + } + @include media-breakpoint-down(md) { display: none; } @@ -300,7 +304,7 @@ @include font-size(1.4rem); left: 0; - margin: 0.2em; + margin: 0.2em 0; overflow: hidden; position: absolute; text-overflow: ellipsis; @@ -329,7 +333,7 @@ display: flex; justify-content: flex-end; position: absolute; - right: 2rem; + right: 4rem; top: 50%; transform: translateY(-50%); width: 100%; @@ -403,12 +407,10 @@ .userControlItem { align-items: center; - border: 2px solid #f6f6f6; border-radius: 100%; display: flex; height: 2.4em; justify-content: center; - margin-left: divide($container-padding-x, 4); position: relative; transition: margin-left 0.3s; width: 2.4em; @@ -419,8 +421,6 @@ .headerScrolledTop &, .headerScrolledBottom & { - border-color: transparent; - margin-left: 0; transition: none; } diff --git a/src/components/Nav/ProfilePopup.tsx b/src/components/Nav/ProfilePopup.tsx index 807b0cdd..6b196f9b 100644 --- a/src/components/Nav/ProfilePopup.tsx +++ b/src/components/Nav/ProfilePopup.tsx @@ -15,7 +15,7 @@ export const ProfilePopup = (props: ProfilePopupProps) => {