diff --git a/public/icons/editor-blockquote.svg b/public/icons/editor-blockquote.svg index b6e52b40..67c1ae6c 100644 --- a/public/icons/editor-blockquote.svg +++ b/public/icons/editor-blockquote.svg @@ -1,3 +1,5 @@ - + diff --git a/public/icons/editor-squib.svg b/public/icons/editor-squib.svg index 519a3975..db5e4a79 100644 --- a/public/icons/editor-squib.svg +++ b/public/icons/editor-squib.svg @@ -1,3 +1,5 @@ - + diff --git a/public/icons/hide-table-of-contents-2.svg b/public/icons/hide-table-of-contents-2.svg new file mode 100644 index 00000000..f51dbe12 --- /dev/null +++ b/public/icons/hide-table-of-contents-2.svg @@ -0,0 +1,5 @@ + + + + diff --git a/public/icons/hide-table-of-contents.svg b/public/icons/hide-table-of-contents.svg index 6c8b9a0e..fd50f43e 100644 --- a/public/icons/hide-table-of-contents.svg +++ b/public/icons/hide-table-of-contents.svg @@ -1,3 +1,4 @@ - - + + + diff --git a/public/icons/show-table-of-contents.svg b/public/icons/show-table-of-contents.svg index d887df58..37075c24 100644 --- a/public/icons/show-table-of-contents.svg +++ b/public/icons/show-table-of-contents.svg @@ -1,4 +1,4 @@ - - - + + + diff --git a/public/icons/user-link-behance.svg b/public/icons/user-link-behance.svg new file mode 100644 index 00000000..f9ccd2cc --- /dev/null +++ b/public/icons/user-link-behance.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/icons/user-link-default.svg b/public/icons/user-link-default.svg index d1e03c91..06b47b48 100644 --- a/public/icons/user-link-default.svg +++ b/public/icons/user-link-default.svg @@ -1,5 +1,4 @@ - - + + + diff --git a/public/icons/user-link-dribbble.svg b/public/icons/user-link-dribbble.svg new file mode 100644 index 00000000..759ccd9d --- /dev/null +++ b/public/icons/user-link-dribbble.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/icons/user-link-dzen.svg b/public/icons/user-link-dzen.svg new file mode 100644 index 00000000..e730bde0 --- /dev/null +++ b/public/icons/user-link-dzen.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/icons/user-link-facebook.svg b/public/icons/user-link-facebook.svg new file mode 100644 index 00000000..47632b3c --- /dev/null +++ b/public/icons/user-link-facebook.svg @@ -0,0 +1,4 @@ + + + diff --git a/public/icons/user-link-github.svg b/public/icons/user-link-github.svg new file mode 100644 index 00000000..b7c1ef2d --- /dev/null +++ b/public/icons/user-link-github.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/icons/user-link-instagram.svg b/public/icons/user-link-instagram.svg new file mode 100644 index 00000000..bce6d6a2 --- /dev/null +++ b/public/icons/user-link-instagram.svg @@ -0,0 +1,4 @@ + + + diff --git a/public/icons/user-link-linkedin.svg b/public/icons/user-link-linkedin.svg new file mode 100644 index 00000000..973dadd8 --- /dev/null +++ b/public/icons/user-link-linkedin.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/icons/user-link-medium.svg b/public/icons/user-link-medium.svg new file mode 100644 index 00000000..38cc100c --- /dev/null +++ b/public/icons/user-link-medium.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/icons/user-link-ok.svg b/public/icons/user-link-ok.svg new file mode 100644 index 00000000..99faa68b --- /dev/null +++ b/public/icons/user-link-ok.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/icons/user-link-pinterest.svg b/public/icons/user-link-pinterest.svg new file mode 100644 index 00000000..f9602b73 --- /dev/null +++ b/public/icons/user-link-pinterest.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/icons/user-link-reddit.svg b/public/icons/user-link-reddit.svg new file mode 100644 index 00000000..ddfce137 --- /dev/null +++ b/public/icons/user-link-reddit.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/icons/user-link-telegram.svg b/public/icons/user-link-telegram.svg new file mode 100644 index 00000000..9320b127 --- /dev/null +++ b/public/icons/user-link-telegram.svg @@ -0,0 +1,5 @@ + + + + diff --git a/public/icons/user-link-tiktok.svg b/public/icons/user-link-tiktok.svg new file mode 100644 index 00000000..8741f6c9 --- /dev/null +++ b/public/icons/user-link-tiktok.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/icons/user-link-twitter.svg b/public/icons/user-link-twitter.svg new file mode 100644 index 00000000..8cdfcc67 --- /dev/null +++ b/public/icons/user-link-twitter.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/icons/user-link-vk.svg b/public/icons/user-link-vk.svg new file mode 100644 index 00000000..d2a49375 --- /dev/null +++ b/public/icons/user-link-vk.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/icons/user-link-youtube.svg b/public/icons/user-link-youtube.svg new file mode 100644 index 00000000..25a90f08 --- /dev/null +++ b/public/icons/user-link-youtube.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 0cafd13d..83ff92f5 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -3,14 +3,18 @@ "About myself": "About myself", "About the project": "About the project", "Add a few topics so that the reader knows what your content is about and can find it on pages of topics that interest them. Topics can be swapped, the first topic becomes the title": "Add a few topics so that the reader knows what your content is about and can find it on pages of topics that interest them. Topics can be swapped, the first topic becomes the title", + "Add a link or click plus to embed media": "Add a link or click plus to embed media", + "Add an embed widget": "Add an embed widget", "Add another image": "Add another image", "Add audio": "Add audio", + "Add blockquote": "Add blockquote", "Add comment": "Comment", "Add cover": "Add cover", "Add image": "Add image", "Add images": "Add images", "Add intro": "Add intro", "Add link": "Add link", + "Add rule": "Add rule", "Add signature": "Add signature", "Add subtitle": "Add subtitle", "Add url": "Add url", @@ -61,6 +65,7 @@ "Collaborate": "Help Edit", "Come up with a subtitle for your story": "Come up with a subtitle for your story", "Come up with a title for your story": "Come up with a title for your story", + "Comment successfully deleted": "Comment successfully deleted", "Comments": "Comments", "Communities": "Communities", "Confirm": "Confirm", @@ -86,7 +91,7 @@ "Delete cover": "Delete cover", "Description": "Description", "Discours": "Discours", - "Discours is an intellectual environment, a web space and tools that allows authors to collaborate with readers and come together to co-create publications and media projects": "Discours is an intellectual environment, a web space and tools that allows authors to collaborate with readers and come together to co-create publications and media projects", + "Discours is an intellectual environment, a web space and tools that allows authors to collaborate with readers and come together to co-create publications and media projects": "Discours is an intellectual environment, a web space and tools that allows authors to collaborate with readers and come together to co-create publications and media projects.
We are convinced that one voice is good, but many is better. We create the most amazing stories together", "Discours is created with our common effort": "Discours exists because of our common effort", "Discussing": "Discussing", "Discussion rules": "Discussion rules", @@ -100,6 +105,7 @@ "Email": "Mail", "Enter": "Enter", "Enter URL address": "Enter URL address", + "Enter footnote text": "Enter footnote text", "Enter image description": "Enter image description", "Enter image title": "Enter image title", "Enter text": "Enter text", @@ -139,6 +145,7 @@ "Help": "Помощь", "Help to edit": "Help to edit", "Here you can customize your profile the way you want.": "Here you can customize your profile the way you want.", + "Hide table of contents": "Hide table of contents", "Highlight": "Highlight", "Hooray! Welcome!": "Hooray! Welcome!", "Horizontal collaborative journalistic platform": "Horizontal collaborative journalism platform", @@ -258,9 +265,9 @@ "Send link again": "Send link again", "Settings": "Settings", "Share": "Share", - "Short opening": "Short opening", "Show": "Show", - "Show lyrics": "Текст песни", + "Show lyrics": "Show lyrics", + "Show table of contents": "Show table of contents", "Slug": "Slug", "Social networks": "Social networks", "Something went wrong, check email and password": "Something went wrong. Check your email and password", @@ -318,9 +325,7 @@ "Video": "Video", "Video format not supported": "Video format not supported", "Views": "Views", - "We are convinced that one voice is good, but many is better": "We are convinced that one voice is good, but many is better", "We can't find you, check email or": "We can't find you, check email or", - "We create the most amazing stories together": "We create the most amazing stories together", "We know you, please try to login": "This email address is already registered, please try to login", "We've sent you a message with a link to enter our website.": "We've sent you an email with a link to your email. Follow the link in the email to enter our website.", "Where": "From", @@ -388,5 +393,12 @@ "user already exist": "user already exists", "video": "video", "view": "view", - "zine": "zine" + "zine": "zine", + "subscriber": "subscriber", + "subscriber_rp": "subscriber", + "subscribers": "subscribers", + "subscription": "subscription", + "subscription_rp": "subscription", + "subscriptions": "subscriptions", + "Users": "Users" } diff --git a/public/locales/ru/translation.json b/public/locales/ru/translation.json index 5dc7bc6d..96559f15 100644 --- a/public/locales/ru/translation.json +++ b/public/locales/ru/translation.json @@ -1,18 +1,22 @@ { "...subscribing": "...подписываем", - "A short introduction to keep the reader interested": "Небольшое вступление, чтобы заинтересовать читателя", + "A short introduction to keep the reader interested": "Добавьте вступление, чтобы заинтересовать читателя", "About myself": "О себе", "About the project": "О проекте", "Accomplices": "Соучастники", "Add a few topics so that the reader knows what your content is about and can find it on pages of topics that interest them. Topics can be swapped, the first topic becomes the title": "Добавьте несколько тем, чтобы читатель знал, о чем ваш материал, и мог найти его на страницах интересных ему тем. Темы можно менять местами, первая тема становится заглавной", + "Add a link or click plus to embed media": "Добавьте ссылку или нажмите плюс для вставки медиа", + "Add an embed widget": "Добавить embed-виджет", "Add another image": "Добавить другое изображение", "Add audio": "Добавить аудио", + "Add blockquote": "Добавить цитату", "Add comment": "Комментировать", "Add cover": "Добавить обложку", "Add image": "Добавить изображение", "Add images": "Добавить изображения", "Add intro": "Добавить вступление", "Add link": "Добавить ссылку", + "Add rule": "Добавить разделитель", "Add signature": "Добавить подпись", "Add subtitle": "Добавить подзаголовок", "Add to bookmarks": "Добавить в закладки", @@ -65,6 +69,7 @@ "Collaborate": "Помочь редактировать", "Come up with a subtitle for your story": "Придумайте подзаголовок вашей истории", "Come up with a title for your story": "Придумайте заголовок вашей истории", + "Comment successfully deleted": "Комментарий успешно удален", "Comments": "Комментарии", "Communities": "Сообщества", "Confirm": "Подтвердить", @@ -90,7 +95,7 @@ "Delete cover": "Удалить обложку", "Description": "Описание", "Discours": "Дискурс", - "Discours is an intellectual environment, a web space and tools that allows authors to collaborate with readers and come together to co-create publications and media projects": "Дискурс — это интеллектуальная среда, веб-пространство и инструменты, которые позволяют авторам сотрудничать с читателями и объединяться для совместного создания публикаций и медиапроектов", + "Discours is an intellectual environment, a web space and tools that allows authors to collaborate with readers and come together to co-create publications and media projects": "Дискурс — это интеллектуальная среда, веб-пространство и инструменты, которые позволяют авторам сотрудничать с читателями и объединяться для совместного создания публикаций и медиапроектов.
Мы убеждены, один голос хорошо, а много — лучше. Самые потрясающиe истории мы создаём вместе.", "Discours is created with our common effort": "Дискурс существует благодаря нашему общему вкладу", "Discussing": "Обсуждаемое", "Discussion rules": "Правила сообществ самиздата в соцсетях", @@ -147,9 +152,10 @@ "Help": "Помощь", "Help to edit": "Помочь редактировать", "Here you can customize your profile the way you want.": "Здесь можно настроить свой профиль так, как вы хотите.", + "Hide table of contents": "Скрыть главление", "Highlight": "Подсветка", "Hooray! Welcome!": "Ура! Добро пожаловать!", - "Horizontal collaborative journalistic platform": "Горизонтальная платформа для коллаборативной журналистики", + "Horizontal collaborative journalistic platform": "Открытая платформа
для независимой журналистики", "Hot topics": "Горячие темы", "Hotkeys": "Горячие клавиши", "How can I help/skills": "Чем могу помочь/навыки", @@ -222,7 +228,7 @@ "Password again": "Пароль ещё раз", "Password should be at least 8 characters": "Пароль должен быть не менее 8 символов", "Password should contain at least one number": "Пароль должен содержать хотя бы одну цифру", - "Password should contain at least one special character: !@#$%^&*": "Пароль должен содержать хотя бы один специальный символ: !@#$%^&*", + "Password should contain at least one special character: !@#$%^&*": "Пароль должен содержать хотя бы один спецсимвол: !@#$%^&*", "Passwords are not equal": "Пароли не совпадают", "Paste Embed code": "Вставьте embed код", "Personal": "Личные", @@ -274,9 +280,10 @@ "Send link again": "Прислать ссылку ещё раз", "Settings": "Настройки", "Share": "Поделиться", - "Short opening": "Небольшое вступление, чтобы заинтересовать читателя", + "Short opening": "Расскажите вашу историю...", "Show": "Показать", "Show lyrics": "Текст песни", + "Show table of contents": "Показать главление", "Slug": "Постоянная ссылка", "Social networks": "Социальные сети", "Something went wrong, check email and password": "Что-то пошло не так. Проверьте адрес электронной почты и пароль", @@ -293,6 +300,7 @@ "Subscribe what you like to tune your personal feed": "Подпишитесь на интересующие вас темы, чтобы настроить вашу персональную ленту и моментально узнавать о новых публикациях и обсуждениях", "Subscribe who you like to tune your personal feed": "Подпишитесь на интересующих вас авторов, чтобы настроить вашу персональную ленту и моментально узнавать о новых публикациях и обсуждениях", "Subscription": "Подписка", + "subscription": "подписка", "Subscriptions": "Подписки", "Substrate": "Подложка", "Success": "Успешно", @@ -315,7 +323,7 @@ "Top authors": "Рейтинг авторов", "Top commented": "Самое комментируемое", "Top discussed": "Обсуждаемое", - "Top month articles": "Лучшее за месяц", + "Top month articles": "Лучшие материалы месяца", "Top rated": "Популярное", "Top recent": "Самое новое", "Top topics": "Интересные темы", @@ -335,9 +343,7 @@ "Video": "Видео", "Video format not supported": "Тип видео не поддерживается", "Views": "Просмотры", - "We are convinced that one voice is good, but many is better": "Мы убеждены, один голос хорошо, а много — лучше", "We can't find you, check email or": "Не можем вас найти, проверьте адрес электронной почты или", - "We create the most amazing stories together": "Самые потрясающиe истории мы создаём вместе", "We know you, please try to login": "Такой адрес почты уже зарегистрирован, попробуйте залогиниться", "We've sent you a message with a link to enter our website.": "Мы выслали вам письмо с ссылкой на почту. Перейдите по ссылке в письме, чтобы войти на сайт.", "Welcome!": "Добро пожаловать!", @@ -382,7 +388,6 @@ "email not confirmed": "email не подтвержден", "enter": "войдите", "feed": "лента", - "follower": "подписчик", "general feed": "Общая лента", "header 1": "заголовок 1", "header 2": "заголовок 2", @@ -413,5 +418,13 @@ "user already exist": "пользователь уже существует", "video": "видео", "view": "просмотр", - "zine": "журнал" + "zine": "журнал", + "Enter footnote text": "Введите текст сноски", + "follower": "подписчик", + "subscriber": "подписчик", + "subscriber_rp": "подписчика", + "subscribers": "подписчиков", + "subscription_rp": "подписки", + "subscriptions": "подписок", + "Users": "Пользователи" } diff --git a/src/components/Article/Article.module.scss b/src/components/Article/Article.module.scss index efe5996f..c36d2df2 100644 --- a/src/components/Article/Article.module.scss +++ b/src/components/Article/Article.module.scss @@ -540,13 +540,13 @@ a[data-toggle='tooltip'] { vertical-align: text-top; justify-content: center; position: relative; - width: 10px; - height: 10px; + width: 14px; + height: 14px; border-radius: 50%; margin: -2px 0 0 1px; border: unset; - background-size: 10px; - background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/PjxzdmcgY2xhc3M9ImJpIGJpLWluZm8tY2lyY2xlIiBmaWxsPSJjdXJyZW50Q29sb3IiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgd2lkdGg9IjE2IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGQ9Ik04IDE1QTcgNyAwIDEgMSA4IDFhNyA3IDAgMCAxIDAgMTR6bTAgMUE4IDggMCAxIDAgOCAwYTggOCAwIDAgMCAwIDE2eiIvPjxwYXRoIGQ9Im04LjkzIDYuNTg4LTIuMjkuMjg3LS4wODIuMzguNDUuMDgzYy4yOTQuMDcuMzUyLjE3Ni4yODguNDY5bC0uNzM4IDMuNDY4Yy0uMTk0Ljg5Ny4xMDUgMS4zMTkuODA4IDEuMzE5LjU0NSAwIDEuMTc4LS4yNTIgMS40NjUtLjU5OGwuMDg4LS40MTZjLS4yLjE3Ni0uNDkyLjI0Ni0uNjg2LjI0Ni0uMjc1IDAtLjM3NS0uMTkzLS4zMDQtLjUzM0w4LjkzIDYuNTg4ek05IDQuNWExIDEgMCAxIDEtMiAwIDEgMSAwIDAgMSAyIDB6Ii8+PC9zdmc+'); + background-size: contain; + background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQiIGhlaWdodD0iMTQiIHZpZXdCb3g9IjAgMCAxNCAxNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTcgMTRDMTAuODY2IDE0IDE0IDEwLjg2NiAxNCA3QzE0IDMuMTM0MDEgMTAuODY2IDAgNyAwQzMuMTM0MDEgMCAwIDMuMTM0MDEgMCA3QzAgMTAuODY2IDMuMTM0MDEgMTQgNyAxNFpNNy44ODQzNSAzLjQyMzA3QzcuODg0MzUgMi45MTQyNCA3LjQ3MDExIDIuNSA2Ljk2MTI4IDIuNUM2LjQ1MjM2IDIuNSA2LjAyNjMyIDIuOTE0MjQgNi4wMzgyIDMuNDIzMDdDNi4wMzgyIDMuOTMxOTEgNi40NTI0NSA0LjM0NjE1IDYuOTYxMjggNC4zNDYxNUM3LjQ3MDExIDQuMzQ2MTUgNy44ODQzNSAzLjkzMTkxIDcuODg0MzUgMy40MjMwN1pNOC40NDA1NiAxMC41QzguNTU4OTUgMTAuNSA4LjY2NTQ0IDEwLjM5MzUgOC42NjU0NCAxMC4yNzUxTDguNjY1NDYgOS4xMDM1NUM4LjY2NTQ2IDguOTczMzYgOC41NTg5NiA4Ljg3ODY4IDguNDQwNTggOC44Nzg2OEg3Ljk0MzU0VjUuMjIxODhDNy45NDM1NCA1LjEwMzUgNy44MzcwNSA0Ljk5NzAxIDcuNzE4NjcgNC45OTcwMUg1LjU1M0M1LjQzNDYxIDQuOTk3MDEgNS4zMjgxMiA1LjA5MTcgNS4zMjgxMiA1LjIyMTg4VjYuMzkzNUM1LjMyODEyIDYuNTIzNjkgNS40MzQ2MiA2LjYxODM3IDUuNTUzIDYuNjE4MzdINi4wNTAwNFY4Ljg3ODcyTDUuNTUzIDguODc4NjRDNS40MzQ2MSA4Ljg3ODY0IDUuMzI4MTIgOC45NzMzMyA1LjMyODEyIDkuMTAzNTFWMTAuMjc1MUM1LjMyODEyIDEwLjM5MzUgNS40MzQ2MiAxMC41IDUuNTUzIDEwLjVIOC40NDA1NloiIGZpbGw9IiM0MDQwNDAiLz48L3N2Zz4='); &:hover { background-color: unset; @@ -554,9 +554,50 @@ a[data-toggle='tooltip'] { } .tooltip { + @include font-size(1.4rem); + + position: relative; padding: 8px; - background: #141414; - font-size: 12px; - color: white; + border-radius: 4px; max-width: 400px; + box-sizing: border-box; + background: var(--black-500); + + .tooltipContent { + max-height: 300px; + overflow: auto; + color: var(--default-color-invert); + + a { + text-decoration: underline; + } + + p:last-child { + margin: 0; + } + } + + &::after { + content: ''; + position: absolute; + left: 50%; + bottom: -4px; + transform: translateX(-50%); + width: 0; + height: 0; + border-style: solid; + border-width: 4px 4px 0 4px; + border-color: var(--black-500) transparent transparent transparent; + } +} + +.lead { + @include font-size(1.8rem); + + font-weight: 600; + + b, + strong { + font-weight: 700; + } } diff --git a/src/components/Article/AudioPlayer/AudioPlayer.tsx b/src/components/Article/AudioPlayer/AudioPlayer.tsx index 3954965d..265f55d3 100644 --- a/src/components/Article/AudioPlayer/AudioPlayer.tsx +++ b/src/components/Article/AudioPlayer/AudioPlayer.tsx @@ -35,7 +35,8 @@ export const AudioPlayer = (props: Props) => { () => currentTrackIndex(), () => { setCurrentTrackDuration(0) - } + }, + { defer: true } ) ) diff --git a/src/components/Article/FullArticle.tsx b/src/components/Article/FullArticle.tsx index 2d9e8499..84dfaebb 100644 --- a/src/components/Article/FullArticle.tsx +++ b/src/components/Article/FullArticle.tsx @@ -1,19 +1,14 @@ -import { createEffect, For, createMemo, onMount, Show, createSignal } from 'solid-js' +import { createEffect, For, createMemo, onMount, Show, createSignal, onCleanup } from 'solid-js' import { Title } from '@solidjs/meta' import { clsx } from 'clsx' import { getPagePath } from '@nanostores/router' - import MD from './MD' - import type { Author, Shout } from '../../graphql/types.gen' import { useSession } from '../../context/session' import { useLocalize } from '../../context/localize' import { useReactions } from '../../context/reactions' - import { MediaItem } from '../../pages/types' - import { router, useRouter } from '../../stores/router' - import { formatDate } from '../../utils' import { getDescription } from '../../utils/meta' import { imageProxy } from '../../utils/imageProxy' @@ -24,9 +19,8 @@ import { AudioPlayer } from './AudioPlayer' import { SharePopup } from './SharePopup' import { ShoutRatingControl } from './ShoutRatingControl' import { CommentsTree } from './CommentsTree' -import stylesHeader from '../Nav/Header.module.scss' +import stylesHeader from '../Nav/Header/Header.module.scss' import { AudioHeader } from './AudioHeader' - import { Popover } from '../_shared/Popover' import { VideoPlayer } from '../_shared/VideoPlayer' import { Icon } from '../_shared/Icon' @@ -47,6 +41,7 @@ export const FullArticle = (props: Props) => { isAuthenticated, actions: { requireAuthentication } } = useSession() + const [isReactionsLoaded, setIsReactionsLoaded] = createSignal(false) const formattedDate = createMemo(() => formatDate(new Date(props.article.createdAt))) @@ -128,23 +123,78 @@ export const FullArticle = (props: Props) => { setIsReactionsLoaded(true) }) - onMount(() => { - const tooltipElements: NodeListOf = - document.querySelectorAll('[data-toggle="tooltip"]') + const clickHandlers = [] + const documentClickHandlers = [] + + createEffect(() => { + if (!body()) { + return + } + + const tooltipElements: NodeListOf = document.querySelectorAll( + '[data-toggle="tooltip"], footnote' + ) if (!tooltipElements) return tooltipElements.forEach((element) => { const tooltip = document.createElement('div') tooltip.classList.add(styles.tooltip) - tooltip.textContent = element.dataset.originalTitle + const tooltipContent = document.createElement('div') + tooltipContent.classList.add(styles.tooltipContent) + tooltipContent.innerHTML = element.dataset.originalTitle || element.dataset.value + + tooltip.appendChild(tooltipContent) + document.body.appendChild(tooltip) - createPopper(element, tooltip, { placement: 'top' }) + + if (element.hasAttribute('href')) { + element.setAttribute('href', 'javascript: void(0);') + } + createPopper(element, tooltip, { + placement: 'top', + modifiers: [ + { + name: 'offset', + options: { + offset: [0, 8] + } + } + ] + }) + tooltip.style.visibility = 'hidden' - element.addEventListener('mouseenter', () => { - tooltip.style.visibility = 'visible' - }) - element.addEventListener('mouseleave', () => { - tooltip.style.visibility = 'hidden' - }) + let isTooltipVisible = false + + const handleClick = () => { + if (isTooltipVisible) { + tooltip.style.visibility = 'hidden' + isTooltipVisible = false + } else { + tooltip.style.visibility = 'visible' + isTooltipVisible = true + } + } + + const handleDocumentClick = (e) => { + if (isTooltipVisible && e.target !== element && e.target !== tooltip) { + tooltip.style.visibility = 'hidden' + isTooltipVisible = false + } + } + + element.addEventListener('click', handleClick) + document.addEventListener('click', handleDocumentClick) + + clickHandlers.push({ element, handler: handleClick }) + documentClickHandlers.push(handleDocumentClick) + }) + }) + + onCleanup(() => { + clickHandlers.forEach(({ element, handler }) => { + element.removeEventListener('click', handler) + }) + documentClickHandlers.forEach((handler) => { + document.removeEventListener('click', handler) }) }) @@ -190,6 +240,9 @@ export const FullArticle = (props: Props) => { + +
+ { + - +
+ +
diff --git a/src/components/Author/AuthorCard.tsx b/src/components/Author/AuthorCard.tsx deleted file mode 100644 index 3b1e4004..00000000 --- a/src/components/Author/AuthorCard.tsx +++ /dev/null @@ -1,237 +0,0 @@ -import type { Author } from '../../graphql/types.gen' -import { Userpic } from './Userpic' -import { Icon } from '../_shared/Icon' -import styles from './AuthorCard.module.scss' -import { createMemo, createSignal, For, Show } from 'solid-js' -import { translit } from '../../utils/ru2en' -import { follow, unfollow } from '../../stores/zine/common' -import { clsx } from 'clsx' -import { useSession } from '../../context/session' -import { StatMetrics } from '../_shared/StatMetrics' -import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient' -import { FollowingEntity } from '../../graphql/types.gen' -import { router, useRouter } from '../../stores/router' -import { openPage } from '@nanostores/router' -import { useLocalize } from '../../context/localize' - -interface AuthorCardProps { - caption?: string - hideWriteButton?: boolean - hideDescription?: boolean - hideFollow?: boolean - hasLink?: boolean - subscribed?: boolean - author: Author - isAuthorPage?: boolean - noSocialButtons?: boolean - isAuthorsList?: boolean - truncateBio?: boolean - liteButtons?: boolean - isTextButton?: boolean - isComments?: boolean - isFeedMode?: boolean - isNowrap?: boolean - class?: string -} - -export const AuthorCard = (props: AuthorCardProps) => { - const { t, lang } = useLocalize() - - const { - session, - isSessionLoaded, - actions: { loadSession, requireAuthentication } - } = useSession() - - const [isSubscribing, setIsSubscribing] = createSignal(false) - - const subscribed = createMemo(() => { - return session()?.news?.authors?.some((u) => u === props.author.slug) || false - }) - - const subscribe = async (really = true) => { - setIsSubscribing(true) - - await (really - ? follow({ what: FollowingEntity.Author, slug: props.author.slug }) - : unfollow({ what: FollowingEntity.Author, slug: props.author.slug })) - - await loadSession() - setIsSubscribing(false) - } - - const canFollow = createMemo(() => !props.hideFollow && session()?.user?.slug !== props.author.slug) - - const name = createMemo(() => { - if (lang() !== 'ru') { - if (props.author.name === 'Дискурс') { - return 'Discours' - } - - return translit(props.author.name) - } - - return props.author.name - }) - - // TODO: reimplement AuthorCard - const { changeSearchParam } = useRouter() - const initChat = () => { - requireAuthentication(() => { - openPage(router, `inbox`) - changeSearchParam('initChat', `${props.author.id}`) - }, 'discussions') - } - - const handleSubscribe = () => { - requireAuthentication(() => { - subscribe(true) - }, 'subscribe') - } - - return ( -
- - -
-
- - - - -
{name()}
-
- - - {props.isAuthorsList} -
- - - - - -
- - - -
- - - - - - - {t('Follow')} - - - - } - > - - - - - - - -
- {(link) => } -
-
-
-
-
-
-
-
-
- ) -} diff --git a/src/components/Author/AuthorCard.module.scss b/src/components/Author/AuthorCard/AuthorCard.module.scss similarity index 65% rename from src/components/Author/AuthorCard.module.scss rename to src/components/Author/AuthorCard/AuthorCard.module.scss index fe8e9d29..fa6574c4 100644 --- a/src/components/Author/AuthorCard.module.scss +++ b/src/components/Author/AuthorCard/AuthorCard.module.scss @@ -14,10 +14,17 @@ } .authorDetails { - align-items: baseline; - display: flex; flex: 1; + @include media-breakpoint-up(sm) { + align-items: baseline; + display: flex; + } + + &.authorDetailsShrinked { + flex: 0 0 auto; + } + @include media-breakpoint-down(sm) { flex-wrap: wrap; } @@ -58,29 +65,28 @@ .authorAbout { color: rgb(0 0 0 / 60%); - font-size: 1.5rem; + font-size: 1.4rem; line-height: 1.4; word-break: break-word; } .authorSubscribe { align-items: center; - display: flex; + //display: flex; @include media-breakpoint-down(md) { flex-wrap: wrap; } a { - background: #f7f7f7; border: none; display: inline-block; - height: 32px; + height: 24px; margin-right: 0.4rem; position: relative; transition: background-color 0.2s; vertical-align: middle; - width: 32px; + width: 24px; &::before { background-image: url(/icons/user-link-default.svg); @@ -88,59 +94,131 @@ background-position: 50% 50%; background-size: contain; content: ''; - filter: invert(1); - height: 18px; + height: 100%; left: 50%; position: absolute; top: 50%; transform: translate(-50%, -50%); transition: filter 0.2s; - width: 18px; + width: 100%; } &:hover { background: #000; &::before { - filter: invert(0); + filter: invert(1); } } } a[href*='facebook.com/'] { &::before { - background-image: url(/icons/facebook-white.svg); + background-image: url(/icons/user-link-facebook.svg); } } a[href*='twitter.com/'] { &::before { - background-image: url(/icons/twitter-white.svg); + background-image: url(/icons/user-link-twitter.svg); } } a[href*='telegram.com/'] { &::before { - background-image: url(/icons/telegram-white.svg); + background-image: url(/icons/user-link-telegram.svg); } } a[href*='vk.cc/'], a[href*='vk.com/'] { &::before { - background-image: url(/icons/vk-white.svg); + background-image: url(/icons/user-link-vk.svg); } } a[href*='tumblr.com/'] { &::before { - background-image: url(/icons/tumblr-white.svg); + background-image: url(/icons/user-link-tumblr.svg); } } a[href*='instagram.com/'] { &::before { - background-image: url(/icons/instagram-white.svg); + background-image: url(/icons/user-link-instagram.svg); + } + } + + a[href*='behance.net/'] { + &::before { + background-image: url(/icons/user-link-behance.svg); + } + } + + a[href*='dribbble.com/'] { + &::before { + background-image: url(/icons/user-link-dribbble.svg); + } + } + + a[href*='github.com/'] { + &::before { + background-image: url(/icons/user-link-github.svg); + } + } + + a[href*='linkedin.com/'] { + &::before { + background-image: url(/icons/user-link-linkedin.svg); + } + } + + a[href*='medium.com/'] { + &::before { + background-image: url(/icons/user-link-medium.svg); + } + } + + a[href*='ok.ru/'] { + &::before { + background-image: url(/icons/user-link-ok.svg); + } + } + + a[href*='pinterest.com/'] { + &::before { + background-image: url(/icons/user-link-pinterest.svg); + } + } + + a[href*='reddit.com/'] { + &::before { + background-image: url(/icons/user-link-reddit.svg); + } + } + + a[href*='tiktok.com/'] { + &::before { + background-image: url(/icons/user-link-tiktok.svg); + } + } + + a[href*='vk.com/'] { + &::before { + background-image: url(/icons/user-link-vk.svg); + } + } + + a[href*='youtube.com/'], + a[href*='youtu.be/'] { + &::before { + background-image: url(/icons/user-link-youtube.svg); + } + } + + a[href*='dzen.ru/'] { + &::before { + background-image: url(/icons/user-link-dzen.svg); } } @@ -171,6 +249,7 @@ .authorSubscribeSocial { align-items: center; display: flex; + margin: 0 0.8rem 1.6rem; @include media-breakpoint-down(sm) { flex: 1 100%; @@ -237,8 +316,7 @@ } .authorAbout { - @include font-size(1.7rem); - + @include font-size(2rem); color: #696969; } @@ -366,3 +444,38 @@ width: 1.6rem; } } + +.subscribersContainer { + display: flex; + flex-wrap: wrap; + font-size: 1.4rem; + margin-top: 1em; +} + +.subscribers { + align-items: center; + cursor: pointer; + display: inline-flex; + margin-right: 3rem; + vertical-align: top; + + .userpic { + background: var(--background-color); + box-shadow: 0 0 0 2px var(--background-color); + vertical-align: top; + + &:not(:first-child) { + margin-left: -2.2rem; + } + } +} + +.subscribersCounter { + margin-left: -0.6rem; +} + +.listWrapper { + max-height: 70vh; + overflow: auto; + padding-right: 2rem; +} diff --git a/src/components/Author/AuthorCard/AuthorCard.tsx b/src/components/Author/AuthorCard/AuthorCard.tsx new file mode 100644 index 00000000..8bd7bc79 --- /dev/null +++ b/src/components/Author/AuthorCard/AuthorCard.tsx @@ -0,0 +1,417 @@ +import type { Author } from '../../../graphql/types.gen' +import { Userpic } from '../Userpic' +import { Icon } from '../../_shared/Icon' +import styles from './AuthorCard.module.scss' +import { createEffect, createMemo, createSignal, For, Show } from 'solid-js' +import { translit } from '../../../utils/ru2en' +import { follow, unfollow } from '../../../stores/zine/common' +import { clsx } from 'clsx' +import { useSession } from '../../../context/session' +import { ShowOnlyOnClient } from '../../_shared/ShowOnlyOnClient' +import { FollowingEntity, Topic } from '../../../graphql/types.gen' +import { router, useRouter } from '../../../stores/router' +import { openPage } from '@nanostores/router' +import { useLocalize } from '../../../context/localize' +import { ConditionalWrapper } from '../../_shared/ConditionalWrapper' +import { Modal } from '../../Nav/Modal' +import { showModal } from '../../../stores/ui' +import { TopicCard } from '../../Topic/Card' +import { getNumeralsDeclension } from '../../../utils/getNumeralsDeclension' + +type SubscriptionFilter = 'all' | 'users' | 'topics' +type AuthorCardProps = { + caption?: string + hideWriteButton?: boolean + hideDescription?: boolean + hideFollow?: boolean + hasLink?: boolean + subscribed?: boolean + author: Author + isAuthorPage?: boolean + noSocialButtons?: boolean + isAuthorsList?: boolean + truncateBio?: boolean + liteButtons?: boolean + isTextButton?: boolean + isComments?: boolean + isFeedMode?: boolean + isNowrap?: boolean + class?: string + followers?: Author[] + subscriptions?: Array + showPublicationsCounter?: boolean +} + +function isAuthor(value: Author | Topic): value is Author { + return 'name' in value +} + +export const AuthorCard = (props: AuthorCardProps) => { + const { t, lang } = useLocalize() + + const { + session, + isSessionLoaded, + actions: { loadSession, requireAuthentication } + } = useSession() + + const [isSubscribing, setIsSubscribing] = createSignal(false) + const [subscriptions, setSubscriptions] = createSignal>(props.subscriptions) + const [subscriptionFilter, setSubscriptionFilter] = createSignal('all') + + const subscribed = createMemo(() => { + return session()?.news?.authors?.some((u) => u === props.author.slug) || false + }) + + const subscribe = async (really = true) => { + setIsSubscribing(true) + + await (really + ? follow({ what: FollowingEntity.Author, slug: props.author.slug }) + : unfollow({ what: FollowingEntity.Author, slug: props.author.slug })) + + await loadSession() + setIsSubscribing(false) + } + + const canFollow = createMemo(() => !props.hideFollow && session()?.user?.slug !== props.author.slug) + + const name = createMemo(() => { + if (lang() !== 'ru') { + if (props.author.name === 'Дискурс') { + return 'Discours' + } + + return translit(props.author.name) + } + + return props.author.name + }) + + // TODO: reimplement AuthorCard + const { changeSearchParam } = useRouter() + const initChat = () => { + requireAuthentication(() => { + openPage(router, `inbox`) + changeSearchParam('initChat', `${props.author.id}`) + }, 'discussions') + } + + const handleSubscribe = () => { + requireAuthentication(() => { + subscribe(true) + }, 'subscribe') + } + + createEffect(() => { + if (props.subscriptions) { + if (subscriptionFilter() === 'users') { + setSubscriptions(props.subscriptions.filter((s) => 'name' in s)) + } else if (subscriptionFilter() === 'topics') { + setSubscriptions(props.subscriptions.filter((s) => 'title' in s)) + } else { + setSubscriptions(props.subscriptions) + } + } + }) + + return ( + <> +
+ + } + > +
+ +
+
+ +
+
+
+ ( + + {children} + + )} + > + {name()} + +
+ {/*TODO: implement plurals by i18n*/} + {props.author.stat?.shouts} публикаций
+ ) : ( + '' + ) + } + > +
+ + + 0) || + (props.subscriptions && props.subscriptions.length > 0) + } + > +
+ 0}> +
showModal('followers')}> + + {(f) => } + +
+ {props.followers.length}  + {getNumeralsDeclension(props.followers.length, [ + t('subscriber'), + t('subscriber_rp'), + t('subscribers') + ])} +
+
+
+ 0}> +
showModal('subscriptions')}> + + {(f) => { + if ('name' in f) { + return + } else if ('title' in f) { + return + } + return null + }} + +
+ {props.subscriptions.length}  + {getNumeralsDeclension(props.subscriptions.length, [ + t('subscription'), + t('subscription_rp'), + t('subscriptions') + ])} +
+
+
+
+
+
+ + + + + + + +
+
+ + + + <> +

{t('Followers')}

+
+
+
+ + {(follower: Author) => ( + + )} + +
+
+
+ +
+
+ + + + <> +

{t('Subscriptions')}

+
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+
+
+
+
+ + {(subscription: Author | Topic) => + isAuthor(subscription) ? ( + + ) : ( + + ) + } + +
+
+
+ +
+
+ + ) +} diff --git a/src/components/Author/AuthorCard/index.ts b/src/components/Author/AuthorCard/index.ts new file mode 100644 index 00000000..ceaabe6d --- /dev/null +++ b/src/components/Author/AuthorCard/index.ts @@ -0,0 +1 @@ +export { AuthorCard } from './AuthorCard' diff --git a/src/components/Author/Full.scss b/src/components/Author/Full.scss deleted file mode 100644 index d4a8c16c..00000000 --- a/src/components/Author/Full.scss +++ /dev/null @@ -1,30 +0,0 @@ -.user-details { - margin: 0 0 5.4rem; - - @include media-breakpoint-up(md) { - margin-left: 160px; - } - - @include media-breakpoint-up(lg) { - margin-left: 235px; - } - - @include media-breakpoint-down(md) { - text-align: center; - } -} - -.author-page { - .view-switcher { - margin-top: 0; - - button { - font-size: 100%; - } - } - - .group__controls { - margin-bottom: 2em !important; - margin-top: 0 !important; - } -} diff --git a/src/components/Author/Full.tsx b/src/components/Author/Full.tsx deleted file mode 100644 index 4b280250..00000000 --- a/src/components/Author/Full.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import type { Author } from '../../graphql/types.gen' -import { AuthorCard } from './AuthorCard' -import './Full.scss' - -export const AuthorFull = (props: { author: Author }) => { - return ( -
-
- -
-
- ) -} diff --git a/src/components/Author/Userpic/Userpic.module.scss b/src/components/Author/Userpic/Userpic.module.scss index 1dc106ac..7e3d1edd 100644 --- a/src/components/Author/Userpic/Userpic.module.scss +++ b/src/components/Author/Userpic/Userpic.module.scss @@ -55,19 +55,23 @@ } &.big { - margin-right: 0; + aspect-ratio: 1/1; + margin: 0 auto; max-width: 168px; - min-width: 168px; - height: 168px; - width: 168px; + height: auto; + width: 100%; @include media-breakpoint-up(md) { - margin-right: 4.8rem; + margin: 0; + max-width: 100%; } .letters { + align-items: center; + display: flex; font-size: 2em; - line-height: 168px; + justify-content: center; + line-height: normal; max-width: 100%; width: 100%; } diff --git a/src/components/Discours/Hero.scss b/src/components/Discours/Hero.module.scss similarity index 51% rename from src/components/Discours/Hero.scss rename to src/components/Discours/Hero.module.scss index 10a45736..1d293ef0 100644 --- a/src/components/Discours/Hero.scss +++ b/src/components/Discours/Hero.module.scss @@ -1,34 +1,30 @@ -.about-discours { - @include font-size(1.7rem); - - background: #000; - color: #fff; - font-weight: 400; +.aboutDiscours { + @include font-size(1.6rem); + background: #fef2f2; + font-weight: 500; margin-bottom: 6.4rem; - padding: 3.6rem 0; + padding: 8rem 0 6.4rem; text-align: center; h4 { - margin-bottom: 4rem; + @include font-size(4rem); + font-weight: bold; + line-height: 1.1; + margin-bottom: 2rem; } em { font-weight: inherit; } - - ::selection { - background: #fff; - color: #000; - } } -.about-discours__actions { +.aboutDiscoursActions { margin-top: 4.8rem; - .button { - border: 3px solid; - border-radius: 1.2em; - color: inherit; + :global(.button) { + border: 3px solid #000; + border-radius: 0.8rem; + color: #fff; cursor: pointer; display: inline-block; font-weight: bold; @@ -39,7 +35,6 @@ &:hover { background: #fff; - border-color: #fff; color: #000; } } diff --git a/src/components/Discours/Hero.tsx b/src/components/Discours/Hero.tsx index 18dff663..f4b0701c 100644 --- a/src/components/Discours/Hero.tsx +++ b/src/components/Discours/Hero.tsx @@ -1,38 +1,37 @@ -import './Hero.scss' +import styles from './Hero.module.scss' import { showModal } from '../../stores/ui' import { useLocalize } from '../../context/localize' +import { useRouter } from '../../stores/router' +import { AuthModalSearchParams } from '../Nav/AuthModal/types' export default () => { const { t } = useLocalize() + const { changeSearchParam } = useRouter() + return ( -
+
-
-

{t('Horizontal collaborative journalistic platform')}

-

- {t( +

+

+

- - {t('We are convinced that one voice is good, but many is better') + - '. ' + - t('We create the most amazing stories together')} - . - -

-
- showModal('auth')}> - {t('Join the community')} - + /> +
- {t('Become an author')} + {t('Create post')} - - {t('About the project')} + { + showModal('auth') + changeSearchParam('mode', 'register') + }} + > + {t('Join the community')} {t('Support us')} diff --git a/src/components/Discours/Subscribe.module.scss b/src/components/Discours/Subscribe.module.scss index a6424f38..7cbba980 100644 --- a/src/components/Discours/Subscribe.module.scss +++ b/src/components/Discours/Subscribe.module.scss @@ -1,22 +1,8 @@ -@mixin input-placeholder-overflow($direction: 'down') { - @if $direction == 'down' { - @media (width <= 1410px) { - @content; - } - } @else if $direction == 'up' { - @media (width > 1410px) { - @content; - } - } @else { - @error "Unknown direction #{$direction}."; - } -} - .form { display: flex; flex-direction: column; - @include input-placeholder-overflow(down) { + @include media-breakpoint-down(xxl) { margin-bottom: 2.4rem; } } @@ -25,13 +11,12 @@ display: flex; width: 100%; - @include input-placeholder-overflow(down) { + @include media-breakpoint-down(xxl) { flex-direction: column; } .input { @include font-size(2rem); - background: none; color: #fff; font-family: inherit; @@ -45,11 +30,11 @@ border-radius: 0; height: 4rem; - @include input-placeholder-overflow(up) { + @include media-breakpoint-up(xxl) { border-right: none; } - @include input-placeholder-overflow(down) { + @include media-breakpoint-down(xxl) { border-bottom: none; } @@ -62,7 +47,7 @@ border-radius: 0; flex-shrink: 0; - @include input-placeholder-overflow(down) { + @include media-breakpoint-down(xxl) { width: 100%; } } diff --git a/src/components/Editor/BubbleMenu/IncutBubbleMenu.tsx b/src/components/Editor/BubbleMenu/IncutBubbleMenu.tsx index af526852..120e1502 100644 --- a/src/components/Editor/BubbleMenu/IncutBubbleMenu.tsx +++ b/src/components/Editor/BubbleMenu/IncutBubbleMenu.tsx @@ -17,13 +17,6 @@ export const IncutBubbleMenu = (props: Props) => { const [substratBubbleOpen, setSubstratBubbleOpen] = createSignal(false) return (
- - - -
- - + + {(triggerRef: (el) => void) => ( + + )} + + + {(triggerRef: (el) => void) => ( + + )} + + + {(triggerRef: (el) => void) => ( + + )} +
) } diff --git a/src/components/Editor/Prosemirror.scss b/src/components/Editor/Prosemirror.scss index 6c36272a..91367d48 100644 --- a/src/components/Editor/Prosemirror.scss +++ b/src/components/Editor/Prosemirror.scss @@ -9,79 +9,11 @@ float: left; height: 0; pointer-events: none; - font-weight: 500; - font-size: 20px; - line-height: 30px; opacity: 0.3; } // Keeping the cursor active when moving outside the editable area -.articleEditor p, -.articleEditor ul, -.articleEditor h4, -.articleEditor ol { - box-sizing: content-box; - flex: 0 0 auto; - - @media (width >= 768px) { - padding-left: calc(21.9% + 3px); - max-width: 72.7%; - } - - @media (width >= 1200px) { - padding-left: calc(21.5% + 3px); - max-width: 64.9%; - } -} - -.articleEditor blockquote, -.articleEditor figure, -.articleEditor article[data-type='incut'] { - @media (width >= 768px) { - margin-left: calc(21.9% + 3px) !important; - max-width: 73.6%; - } - - @media (width >= 1200px) { - margin-left: calc(21.4% + 3px) !important; - max-width: 65.3%; - } -} - -.articleEditor h2 { - @media (width >= 768px) { - padding-left: calc(21.9% + 2px); - max-width: 72.7%; - } - - @media (width >= 1200px) { - padding-left: 21.5%; - max-width: 87.1%; - } -} - -.articleEditor h3 { - @media (width >= 768px) { - padding-left: calc(21.9% + 2px); - } - - @media (width >= 1200px) { - padding-left: 21.5%; - max-width: 87.1%; - } -} - -.articleEditor * p, -.articleEditor * h2, -.articleEditor * h3, -.articleEditor * h4 { - @media (width >= 768px) { - padding-left: unset; - max-width: unset; - } -} - /* Give a remote user a caret */ .collaboration-cursor__caret { border-left: 1px solid #0d0d0d; @@ -326,3 +258,27 @@ mark.highlight { figure[data-type='capturedImage'] { flex-direction: column-reverse; } + +footnote { + display: inline-flex; + position: relative; + cursor: pointer; + width: 0.8rem; + height: 1em; + + &:before { + content: ''; + position: absolute; + width: 10px; + height: 10px; + border-radius: 50%; + top: -2px; + border: unset; + background-size: 10px; + background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/PjxzdmcgY2xhc3M9ImJpIGJpLWluZm8tY2lyY2xlIiBmaWxsPSJjdXJyZW50Q29sb3IiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgd2lkdGg9IjE2IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGQ9Ik04IDE1QTcgNyAwIDEgMSA4IDFhNyA3IDAgMCAxIDAgMTR6bTAgMUE4IDggMCAxIDAgOCAwYTggOCAwIDAgMCAwIDE2eiIvPjxwYXRoIGQ9Im04LjkzIDYuNTg4LTIuMjkuMjg3LS4wODIuMzguNDUuMDgzYy4yOTQuMDcuMzUyLjE3Ni4yODguNDY5bC0uNzM4IDMuNDY4Yy0uMTk0Ljg5Ny4xMDUgMS4zMTkuODA4IDEuMzE5LjU0NSAwIDEuMTc4LS4yNTIgMS40NjUtLjU5OGwuMDg4LS40MTZjLS4yLjE3Ni0uNDkyLjI0Ni0uNjg2LjI0Ni0uMjc1IDAtLjM3NS0uMTkzLS4zMDQtLjUzM0w4LjkzIDYuNTg4ek05IDQuNWExIDEgMCAxIDEtMiAwIDEgMSAwIDAgMSAyIDB6Ii8+PC9zdmc+'); + } + + &:hover { + background-color: unset; + } +} diff --git a/src/components/Editor/SimplifiedEditor.module.scss b/src/components/Editor/SimplifiedEditor.module.scss index 7f15041b..81312dc0 100644 --- a/src/components/Editor/SimplifiedEditor.module.scss +++ b/src/components/Editor/SimplifiedEditor.module.scss @@ -5,6 +5,7 @@ background: var(--black-50); border-radius: 16px; padding: 16px 16px 8px; + position: relative; .simplifiedEditorField { @include font-size(1.4rem); @@ -54,6 +55,11 @@ bottom: -1rem; transition: 0.3s ease-in-out; + &.alwaysVisible { + opacity: unset; + bottom: unset; + } + .actions { display: flex; align-items: center; @@ -92,4 +98,48 @@ bottom: 0; } } + + &.minimal { + background: unset; + padding: 0; + + & div[contenteditable] { + font-size: 1.6rem; + font-weight: 500; + } + } + + &.bordered { + box-sizing: border-box; + padding: 16px 12px 6px 12px; + border-radius: 2px; + border: 2px solid var(--black-100); + background: var(--white-500); + + & div[contenteditable] { + font-size: 1.6rem; + font-weight: 500; + } + } + + &.labelVisible { + padding-top: 22px; + } + + .limit { + position: absolute; + right: 1rem; + bottom: 0.5rem; + font-weight: 500; + font-size: 1.2rem; + } + + .label { + @include font-size(1.2rem); + + position: absolute; + top: 6px; + left: 12px; + color: var(--black-400); + } } diff --git a/src/components/Editor/SimplifiedEditor.tsx b/src/components/Editor/SimplifiedEditor.tsx index afed92d2..4dad4d35 100644 --- a/src/components/Editor/SimplifiedEditor.tsx +++ b/src/components/Editor/SimplifiedEditor.tsx @@ -1,4 +1,5 @@ -import { createEffect, onCleanup, onMount, Show } from 'solid-js' +import { createEffect, createSignal, onCleanup, onMount, Show } from 'solid-js' +import { Portal } from 'solid-js/web' import { createEditorTransaction, createTiptapEditor, @@ -30,13 +31,20 @@ import { UploadedFile } from '../../pages/types' import { Figure } from './extensions/Figure' import { Image } from '@tiptap/extension-image' import { Figcaption } from './extensions/Figcaption' -import { useOutsideClickHandler } from '../../utils/useOutsideClickHandler' +import { TextBubbleMenu } from './TextBubbleMenu' +import { BubbleMenu } from '@tiptap/extension-bubble-menu' +import { CharacterCount } from '@tiptap/extension-character-count' type Props = { - initialContent?: string - onSubmit?: (text: string) => void - onChange?: (text: string) => void placeholder: string + initialContent?: string + label?: string + onSubmit?: (text: string) => void + onCancel?: () => void + onChange?: (text: string) => void + variant?: 'minimal' | 'bordered' + maxLength?: number + maxHeight?: number submitButtonText?: string quoteEnabled?: boolean imageEnabled?: boolean @@ -44,10 +52,15 @@ type Props = { smallHeight?: boolean submitByEnter?: boolean submitByShiftEnter?: boolean + onlyBubbleControls?: boolean + controlsAlwaysVisible?: boolean } +export const MAX_DESCRIPTION_LIMIT = 400 + const SimplifiedEditor = (props: Props) => { const { t } = useLocalize() + const [counter, setCounter] = createSignal() const wrapperEditorElRef: { current: HTMLElement @@ -61,6 +74,12 @@ const SimplifiedEditor = (props: Props) => { current: null } + const textBubbleMenuRef: { + current: HTMLDivElement + } = { + current: null + } + const { actions: { setEditor } } = useEditorContext() @@ -70,6 +89,7 @@ const SimplifiedEditor = (props: Props) => { content: 'figcaption image' }) + const content = props.initialContent const editor = createTiptapEditor(() => ({ element: editorElRef.current, editorProps: { @@ -86,11 +106,25 @@ const SimplifiedEditor = (props: Props) => { Link.configure({ openOnClick: false }), + + CharacterCount.configure({ + limit: MAX_DESCRIPTION_LIMIT + }), Blockquote.configure({ HTMLAttributes: { class: styles.blockQuote } }), + BubbleMenu.configure({ + pluginKey: 'textBubbleMenu', + element: textBubbleMenuRef.current, + shouldShow: ({ view, state }) => { + if (!props.onlyBubbleControls) return + const { selection } = state + const { empty } = selection + return view.hasFocus() && !empty + } + }), ImageFigure, Image, Figcaption, @@ -99,7 +133,7 @@ const SimplifiedEditor = (props: Props) => { placeholder: props.placeholder }) ], - content: props.initialContent ?? null + content: content ?? null })) setEditor(editor) @@ -149,6 +183,9 @@ const SimplifiedEditor = (props: Props) => { } const handleClear = () => { + if (props.onCancel) { + props.onCancel() + } editor().commands.clearContent(true) } @@ -174,7 +211,7 @@ const SimplifiedEditor = (props: Props) => { if (event.code === 'KeyK' && (event.metaKey || event.ctrlKey) && !editor().state.selection.empty) { event.preventDefault() - showModal('editorInsertLink') + showModal('simplifiedEditorInsertLink') } } @@ -183,6 +220,7 @@ const SimplifiedEditor = (props: Props) => { }) onCleanup(() => { + editor().destroy() window.removeEventListener('keydown', handleKeyDown) }) @@ -192,107 +230,141 @@ const SimplifiedEditor = (props: Props) => { }) } - const handleInsertLink = () => !editor().state.selection.empty && showModal('editorInsertLink') + const handleInsertLink = () => !editor().state.selection.empty && showModal('simplifiedEditorInsertLink') + + createEffect(() => { + if (html()) { + setCounter(editor().storage.characterCount.characters()) + } + }) + + const maxHeightStyle = { + overflow: 'auto', + 'max-height': `${props.maxHeight}px` + } return (
(wrapperEditorElRef.current = el)} class={clsx(styles.SimplifiedEditor, { [styles.smallHeight]: props.smallHeight, - [styles.isFocused]: isFocused() || !isEmpty() + [styles.minimal]: props.variant === 'minimal', + [styles.bordered]: props.variant === 'bordered', + [styles.isFocused]: isFocused() || !isEmpty(), + [styles.labelVisible]: props.label && counter() > 0 })} > -
(editorElRef.current = el)} /> -
-
- - {(triggerRef: (el) => void) => ( - - )} - - - {(triggerRef: (el) => void) => ( - - )} - - - {(triggerRef: (el) => void) => ( - - )} - - - + +
{MAX_DESCRIPTION_LIMIT - counter()}
+
+ 0}> +
{props.label}
+
+
(editorElRef.current = el)} /> + +
+
+ {(triggerRef: (el) => void) => ( )} - - - + {(triggerRef: (el) => void) => ( )} + + {(triggerRef: (el) => void) => ( + + )} + + + + {(triggerRef: (el) => void) => ( + + )} + + + + + {(triggerRef: (el) => void) => ( + + )} + + +
+ +
+
- -
-
-
-
- - hideModal()} /> - - - - { - renderImage(value) - }} - /> + + + + hideModal()} /> + + + + + { + renderImage(value) + }} + /> + + + + + (textBubbleMenuRef.current = el)} + />
) diff --git a/src/components/Editor/TextBubbleMenu/TextBubbleMenu.module.scss b/src/components/Editor/TextBubbleMenu/TextBubbleMenu.module.scss index f9f613e5..9f022771 100644 --- a/src/components/Editor/TextBubbleMenu/TextBubbleMenu.module.scss +++ b/src/components/Editor/TextBubbleMenu/TextBubbleMenu.module.scss @@ -2,6 +2,10 @@ background: var(--editor-bubble-menu-background); box-shadow: 0 4px 10px rgba(#000, 0.25); + &.growWidth { + min-width: 460px; + } + .bubbleMenuButton { display: inline-flex; align-items: center; @@ -86,4 +90,8 @@ height: 0; color: transparent; } + + .noWrap { + white-space: nowrap; + } } diff --git a/src/components/Editor/TextBubbleMenu/TextBubbleMenu.tsx b/src/components/Editor/TextBubbleMenu/TextBubbleMenu.tsx index eeb67c5c..052410ab 100644 --- a/src/components/Editor/TextBubbleMenu/TextBubbleMenu.tsx +++ b/src/components/Editor/TextBubbleMenu/TextBubbleMenu.tsx @@ -1,4 +1,4 @@ -import { Switch, Match, createSignal, Show, onMount, onCleanup } from 'solid-js' +import { Switch, Match, createSignal, Show, onMount, onCleanup, createEffect } from 'solid-js' import type { Editor } from '@tiptap/core' import styles from './TextBubbleMenu.module.scss' import { Icon } from '../../_shared/Icon' @@ -7,11 +7,13 @@ import { createEditorTransaction } from 'solid-tiptap' import { useLocalize } from '../../../context/localize' import { Popover } from '../../_shared/Popover' import { InsertLinkForm } from '../InsertLinkForm' +import SimplifiedEditor from '../SimplifiedEditor' type BubbleMenuProps = { editor: Editor isCommonMarkup: boolean ref: (el: HTMLDivElement) => void + shouldShow: boolean } export const TextBubbleMenu = (props: BubbleMenuProps) => { @@ -20,24 +22,35 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => { const isActive = (name: string, attributes?: unknown) => createEditorTransaction( () => props.editor, - (editor) => { - return editor && editor.isActive(name, attributes) - } + (editor) => editor && editor.isActive(name, attributes) ) + const [textSizeBubbleOpen, setTextSizeBubbleOpen] = createSignal(false) const [listBubbleOpen, setListBubbleOpen] = createSignal(false) const [linkEditorOpen, setLinkEditorOpen] = createSignal(false) + const [footnoteEditorOpen, setFootnoteEditorOpen] = createSignal(false) + const [footNote, setFootNote] = createSignal() + + createEffect(() => { + if (!props.shouldShow) { + setFootNote() + setFootnoteEditorOpen(false) + } + }) const isBold = isActive('bold') const isItalic = isActive('italic') const isH1 = isActive('heading', { level: 2 }) const isH2 = isActive('heading', { level: 3 }) const isH3 = isActive('heading', { level: 4 }) - const isBlockQuote = isActive('blockquote') + const isQuote = isActive('blockquote', { 'data-type': 'quote' }) + const isPunchLine = isActive('blockquote', { 'data-type': 'punchline' }) const isOrderedList = isActive('isOrderedList') const isBulletList = isActive('isBulletList') const isLink = isActive('link') const isHighlight = isActive('highlight') + const isFootnote = isActive('footnote') + const isIncut = isActive('article') const toggleTextSizePopup = () => { if (listBubbleOpen()) { @@ -58,6 +71,47 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => { } } + const updateCurrentFootnoteValue = createEditorTransaction( + () => props.editor, + (ed) => { + if (!isFootnote()) { + return + } + const value = ed.getAttributes('footnote').value + setFootNote(value) + } + ) + + const handleAddFootnote = (footnote) => { + if (footNote()) { + props.editor.chain().focus().updateFootnote(footnote).run() + } else { + props.editor.chain().focus().setFootnote({ value: footnote }).run() + } + setFootNote() + setFootnoteEditorOpen(false) + } + + const handleOpenFootnoteEditor = () => { + updateCurrentFootnoteValue() + setFootnoteEditorOpen(true) + } + + const handleSetPunchline = () => { + if (isPunchLine()) { + props.editor.chain().focus().toggleBlockquote('punchline').run() + } + props.editor.chain().focus().toggleBlockquote('quote').run() + toggleTextSizePopup() + } + const handleSetQuote = () => { + if (isQuote()) { + props.editor.chain().focus().toggleBlockquote('quote').run() + } + props.editor.chain().focus().toggleBlockquote('punchline').run() + toggleTextSizePopup() + } + onMount(() => { window.addEventListener('keydown', handleKeyDown) }) @@ -67,12 +121,27 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => { }) return ( -
+
setLinkEditorOpen(false)} /> - + + handleAddFootnote(value)} + variant={'bordered'} + initialContent={footNote()} + onCancel={() => { + setFootnoteEditorOpen(false) + }} + submitButtonText={t('Send')} + /> + + <> <> @@ -151,12 +220,9 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => { ref={triggerRef} type="button" class={clsx(styles.bubbleMenuButton, { - [styles.bubbleMenuButtonActive]: isBlockQuote() + [styles.bubbleMenuButtonActive]: isQuote() })} - onClick={() => { - props.editor.chain().focus().toggleBlockquote('quote').run() - toggleTextSizePopup() - }} + onClick={handleSetPunchline} > @@ -168,12 +234,9 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => { ref={triggerRef} type="button" class={clsx(styles.bubbleMenuButton, { - [styles.bubbleMenuButtonActive]: isBlockQuote() + [styles.bubbleMenuButtonActive]: isPunchLine() })} - onClick={() => { - props.editor.chain().focus().toggleBlockquote('punchline').run() - toggleTextSizePopup() - }} + onClick={handleSetQuote} > @@ -188,7 +251,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => { ref={triggerRef} type="button" class={clsx(styles.bubbleMenuButton, { - [styles.bubbleMenuButtonActive]: isBlockQuote() + [styles.bubbleMenuButtonActive]: isIncut() })} onClick={() => { props.editor.chain().focus().toggleArticle().run() @@ -252,7 +315,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
- + {t('Add url')}
}> {(triggerRef: (el) => void) => ( )} diff --git a/src/components/Editor/extensions/Article.ts b/src/components/Editor/extensions/Article.ts index fc16dac0..0f5b1ba0 100644 --- a/src/components/Editor/extensions/Article.ts +++ b/src/components/Editor/extensions/Article.ts @@ -4,7 +4,7 @@ declare module '@tiptap/core' { interface Commands { Article: { toggleArticle: () => ReturnType - setArticleFloat: (float: null | 'left' | 'half-left' | 'right' | 'half-right') => ReturnType + setArticleFloat: (float: null | 'half-left' | 'half-right') => ReturnType setArticleBg: (bg: null | string) => ReturnType } } @@ -12,7 +12,6 @@ declare module '@tiptap/core' { export default Node.create({ name: 'article', - defaultOptions: { HTMLAttributes: { 'data-type': 'incut' diff --git a/src/components/Editor/extensions/CustomBlockquote.ts b/src/components/Editor/extensions/CustomBlockquote.ts index 51b77796..8f436e4b 100644 --- a/src/components/Editor/extensions/CustomBlockquote.ts +++ b/src/components/Editor/extensions/CustomBlockquote.ts @@ -14,10 +14,10 @@ declare module '@tiptap/core' { export const CustomBlockquote = Blockquote.extend({ name: 'blockquote', defaultOptions: { - HTMLAttributes: {}, - group: 'block', - content: 'block+' + HTMLAttributes: {} }, + group: 'block', + content: 'block+', addAttributes() { return { 'data-float': { diff --git a/src/components/Editor/extensions/Footnote.ts b/src/components/Editor/extensions/Footnote.ts new file mode 100644 index 00000000..e72cb02c --- /dev/null +++ b/src/components/Editor/extensions/Footnote.ts @@ -0,0 +1,97 @@ +import { mergeAttributes, Node } from '@tiptap/core' + +declare module '@tiptap/core' { + interface Commands { + Footnote: { + setFootnote: (options: { value: string }) => ReturnType + updateFootnote: (options: { value: string }) => ReturnType + deleteFootnote: () => ReturnType + } + } +} + +export const Footnote = Node.create({ + name: 'footnote', + addOptions() { + return { + HTMLAttributes: {} + } + }, + group: 'inline', + content: 'text*', + inline: true, + isolating: true, + + addAttributes() { + return { + value: { + default: null, + parseHTML: (element) => element.dataset.value || null, + renderHTML: (attributes) => { + return { + 'data-value': attributes.value + } + } + } + } + }, + + parseHTML() { + return [ + { + tag: 'footnote' + } + ] + }, + + renderHTML({ HTMLAttributes }) { + return ['footnote', mergeAttributes(HTMLAttributes), 0] + }, + + addCommands() { + return { + setFootnote: + (attributes) => + ({ tr, state }) => { + const { selection } = state + const position = selection.$to.pos + const node = this.type.create(attributes) + tr.insert(position, node) + return true + }, + updateFootnote: + (newValue) => + ({ tr, state }) => { + const { selection } = state + const { $from, $to } = selection + + if ($from.parent.type.name === 'footnote' || $to.parent.type.name === 'footnote') { + const node = $from.parent.type.name === 'footnote' ? $from.parent : $to.parent + const pos = $from.parent.type.name === 'footnote' ? $from.pos - 1 : $to.pos - 1 + + const newNode = node.type.create({ value: newValue }) + tr.setNodeMarkup(pos, null, newNode.attrs) + + return true + } + + return false + }, + deleteFootnote: + () => + ({ tr, state }) => { + const { selection } = state + const { $from, $to } = selection + + if ($from.parent.type.name === 'footnote' || $to.parent.type.name === 'footnote') { + const startPos = $from.start($from.depth) + const endPos = $to.end($to.depth) + tr.delete(startPos, endPos) + return true + } + + return false + } + } + } +}) diff --git a/src/components/Feed/ArticleCard.module.scss b/src/components/Feed/ArticleCard.module.scss index b61190bf..b7761634 100644 --- a/src/components/Feed/ArticleCard.module.scss +++ b/src/components/Feed/ArticleCard.module.scss @@ -71,6 +71,7 @@ &::before { content: ''; + height: 100%; left: 0; position: absolute; top: 0; @@ -180,12 +181,10 @@ } } -.shoutCardLead { +.shoutCardDescription { @include font-size(1.6rem); - color: var(--secondary-color); - font-weight: 400; - line-height: 1.3; + color: var(--default-color); margin-bottom: 1.4rem; } @@ -399,8 +398,10 @@ padding: 0 2.4rem; width: 100%; - @include media-breakpoint-down(sm) { - padding-top: 100%; + @include media-breakpoint-down(xl) { + aspect-ratio: auto; + height: 100%; + padding-top: 30%; } &.swiper-slide { @@ -451,6 +452,12 @@ justify-content: end; padding: 2.4rem; z-index: 1; + + @include media-breakpoint-down(xl) { + padding-left: 0; + padding-right: 0; + position: relative; + } } .shoutCardCover { diff --git a/src/components/Feed/ArticleCard.tsx b/src/components/Feed/ArticleCard.tsx index 9c32b6c6..3b4ed6ff 100644 --- a/src/components/Feed/ArticleCard.tsx +++ b/src/components/Feed/ArticleCard.tsx @@ -7,7 +7,7 @@ import { clsx } from 'clsx' import { CardTopic } from './CardTopic' import { ShoutRatingControl } from '../Article/ShoutRatingControl' import { getShareUrl, SharePopup } from '../Article/SharePopup' -import stylesHeader from '../Nav/Header.module.scss' +import stylesHeader from '../Nav/Header/Header.module.scss' import { getDescription } from '../../utils/meta' import { FeedArticlePopup } from './FeedArticlePopup' import { useLocalize } from '../../context/localize' @@ -164,10 +164,6 @@ export const ArticleCard = (props: ArticleCardProps) => {
- - -
{props.article.lead}
-
@@ -196,7 +192,9 @@ export const ArticleCard = (props: ArticleCardProps) => {
- + +
+
diff --git a/src/components/Nav/AuthModal/AuthModal.module.scss b/src/components/Nav/AuthModal/AuthModal.module.scss index 361cebbe..1e097b54 100644 --- a/src/components/Nav/AuthModal/AuthModal.module.scss +++ b/src/components/Nav/AuthModal/AuthModal.module.scss @@ -141,9 +141,10 @@ } .authInfo { - min-height: 5em; font-weight: 400; font-size: smaller; + margin-top: -2em; + position: absolute; .warn { color: #a00; @@ -158,6 +159,7 @@ h4 { font-weight: bold; + margin-bottom: 1em; } } @@ -166,11 +168,11 @@ } .validationError { - position: relative; - top: -8px; + position: absolute; + top: 100%; font-size: 12px; line-height: 16px; - margin-bottom: 8px; + margin-top: 0.3em; &.registerPassword { margin-bottom: -32px; diff --git a/src/components/Nav/AuthModal/ForgotPasswordForm.tsx b/src/components/Nav/AuthModal/ForgotPasswordForm.tsx index c18e2092..48d58603 100644 --- a/src/components/Nav/AuthModal/ForgotPasswordForm.tsx +++ b/src/components/Nav/AuthModal/ForgotPasswordForm.tsx @@ -80,31 +80,7 @@ export const ForgotPasswordForm = () => {

{t('Forgot password?')}

{t('Everything is ok, please give us your email address')}
- -
-
    -
  • {submitError()}
  • -
-
-
- -
- {/*TODO: text*/} - {t("We can't find you, check email or")}{' '} - { - event.preventDefault() - changeSearchParam('mode', 'register') - }} - > - {t('register')} - -
-
- -
{validationErrors().email}
-
+
{
+ +
+
    +
  • {submitError()}
  • +
+
+
+ + +
+ {/*TODO: text*/} + {t("We can't find you, check email or")}{' '} + { + event.preventDefault() + changeSearchParam('mode', 'register') + }} + > + {t('register')} + + +
{validationErrors().email}
+
+
+
+
- -
{validationErrors().email}
-
{ > + +
{validationErrors().password}
+
- -
{validationErrors().password}
-
- -
{validationErrors().fullName}
-
+
{ onBlur={handleEmailBlur} /> + +
{validationErrors().email}
+
+ + +
- -
{validationErrors().email}
-
- - - +
{ id="password" name="password" autocomplete="current-password" - type="password" + type={showPassword() ? 'text' : 'password'} placeholder={t('Password')} onInput={(event) => handlePasswordInput(event.currentTarget.value)} /> + + +
+ {validationErrors().password} +
+
- -
- {validationErrors().password} -
-
diff --git a/src/components/Topic/Card.module.scss b/src/components/Topic/Card.module.scss index 2dee7902..c0d58d03 100644 --- a/src/components/Topic/Card.module.scss +++ b/src/components/Topic/Card.module.scss @@ -19,6 +19,7 @@ align-items: center; display: flex; justify-content: space-between; + margin-top: 0; button { margin-top: 0; diff --git a/src/components/Views/Author/Author.module.scss b/src/components/Views/Author/Author.module.scss index 5098043d..6f176203 100644 --- a/src/components/Views/Author/Author.module.scss +++ b/src/components/Views/Author/Author.module.scss @@ -1,13 +1,26 @@ +.authorPage { + :global(.view-switcher) { + margin-top: 0; + + button { + font-size: 100%; + } + } + + .groupControls { + margin-bottom: 2em !important; + margin-top: 0 !important; + } +} + .ratingContainer { @include font-size(1.5rem); - display: inline-flex; vertical-align: top; } .ratingControl { @include font-size(1.5rem); - display: inline-flex; margin-left: 1em; vertical-align: middle; @@ -21,57 +34,6 @@ } } -.userpic { - background: #fff; - box-shadow: 0 0 0 2px #fff; - display: inline-block; - margin-right: -1.2rem; - vertical-align: top; -} - -.subscribers { - cursor: pointer; - display: inline-block; - margin: -0.4rem 2em 0 0; - vertical-align: top; -} - -.subscribersCounter { - @include font-size(1rem); - - background: #fff; - border: 2px solid #000; - border-radius: 100%; - font-weight: bold; - height: 32px; - line-height: 30px; - position: relative; - text-align: center; - width: 32px; - z-index: 1; -} - -.subscribersList { - max-height: 15em; - overflow: auto; - position: relative; - - .subscriber { - white-space: nowrap; - display: flex; - flex-direction: row; - align-items: center; - margin: 0; - border-radius: 4px; - padding: 8px 4px; - transition: background 0.2s ease-in-out; - - &:hover { - background: #f7f7f7; - } - } -} - .loadingWrapper { position: relative; min-height: 40vh; diff --git a/src/components/Views/Author/Author.tsx b/src/components/Views/Author/Author.tsx index 171e2e2c..6f6dff8b 100644 --- a/src/components/Views/Author/Author.tsx +++ b/src/components/Views/Author/Author.tsx @@ -2,8 +2,7 @@ import { Show, createMemo, createSignal, Switch, onMount, For, Match, createEffe import type { Author, Shout, Topic } from '../../../graphql/types.gen' import { Row1 } from '../../Feed/Row1' import { Row2 } from '../../Feed/Row2' -import { AuthorFull } from '../../Author/Full' - +import { Row3 } from '../../Feed/Row3' import { useAuthorsStore } from '../../../stores/zine/authors' import { loadShouts, useArticlesStore } from '../../../stores/zine/articles' import { useRouter } from '../../../stores/router' @@ -12,15 +11,12 @@ import { splitToPages } from '../../../utils/splitToPages' import styles from './Author.module.scss' import stylesArticle from '../../Article/Article.module.scss' import { clsx } from 'clsx' -import { Userpic } from '../../Author/Userpic' -import { Popup } from '../../_shared/Popup' import { AuthorCard } from '../../Author/AuthorCard' import { apiClient } from '../../../utils/apiClient' import { Comment } from '../../Article/Comment' import { useLocalize } from '../../../context/localize' import { AuthorRatingControl } from '../../Author/AuthorRatingControl' -import { TopicCard } from '../../Topic/Card' -import { Loading } from '../../_shared/Loading' +import { hideModal } from '../../../stores/ui' type AuthorProps = { shouts: Shout[] @@ -29,24 +25,12 @@ type AuthorProps = { } export type AuthorPageSearchParams = { - by: - | '' - | 'viewed' - | 'rating' - | 'commented' - | 'recent' - | 'subscriptions' - | 'followers' - | 'about' - | 'popular' + by: '' | 'viewed' | 'rating' | 'commented' | 'recent' | 'about' | 'popular' } export const PRERENDERED_ARTICLES_COUNT = 12 const LOAD_MORE_PAGE_SIZE = 9 -function isAuthor(value: Author | Topic): value is Author { - return 'name' in value -} export const AuthorView = (props: AuthorProps) => { const { t } = useLocalize() const { sortedArticles } = useArticlesStore({ shouts: props.shouts }) @@ -58,7 +42,6 @@ export const AuthorView = (props: AuthorProps) => { const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false) const [followers, setFollowers] = createSignal([]) const [subscriptions, setSubscriptions] = createSignal>([]) - const [isLoaded, setIsLoaded] = createSignal() const fetchSubscriptions = async (): Promise<{ authors: Author[]; topics: Topic[] }> => { try { @@ -76,6 +59,7 @@ export const AuthorView = (props: AuthorProps) => { } onMount(async () => { + hideModal() try { const userSubscribers = await apiClient.getAuthorFollowers({ slug: props.authorSlug }) setFollowers(userSubscribers) @@ -88,6 +72,8 @@ export const AuthorView = (props: AuthorProps) => { if (sortedArticles().length === PRERENDERED_ARTICLES_COUNT) { await loadMore() } + const { authors, topics } = await fetchSubscriptions() + setSubscriptions([...authors, ...topics]) }) const loadMore = async () => { @@ -117,13 +103,6 @@ export const AuthorView = (props: AuthorProps) => { const [commented, setCommented] = createSignal([]) createEffect(async () => { - if (searchParams().by === 'subscriptions') { - setIsLoaded(false) - const { authors, topics } = await fetchSubscriptions() - setSubscriptions([...authors, ...topics]) - setIsLoaded(true) - } - if (searchParams().by === 'commented') { try { const data = await apiClient.getReactionsBy({ @@ -136,12 +115,17 @@ export const AuthorView = (props: AuthorProps) => { } }) return ( -
+
- + -
+
  • @@ -149,16 +133,6 @@ export const AuthorView = (props: AuthorProps) => { {t('Publications')}
  • -
  • - -
  • -
  • - -
- - - - - {(f) => } - - - 3}> - - {(f) => } - -
- {followers().length} -
-
-
-
- } - variant="tiny" - > -
    - - {(item: Author) => ( -
  • - -
  • - )} -
    -
- -
{t('Karma')} @@ -238,72 +173,41 @@ export const AuthorView = (props: AuthorProps) => {
- -
-
- - {(follower: Author) => ( -
- -
- )} -
-
-
-
- -
-
- - -
- } - > - - {(subscription: Author | Topic) => ( -
- {isAuthor(subscription) ? ( -
- -
- ) : ( - - )} -
- )} -
- -
-
- - - - - - - - - - {(page) => ( - <> - - - - - - - - )} - + + + + + + + + + + + + + + 3}> + + + + + + + + + {(page) => ( + <> + + + + + + + + )} + +

diff --git a/src/components/Views/Edit.module.scss b/src/components/Views/Edit.module.scss index b1fb8d2a..6f77ab2d 100644 --- a/src/components/Views/Edit.module.scss +++ b/src/components/Views/Edit.module.scss @@ -133,7 +133,7 @@ pointer-events: none; user-select: none; cursor: pointer; - left: 2rem; + left: 0; position: sticky; top: calc(100vh - 40px); width: 2.8rem; @@ -151,7 +151,7 @@ } @include media-breakpoint-up(xl) { - left: 4rem; + left: 0; } &:hover { @@ -215,3 +215,11 @@ background-repeat: no-repeat; } } + +.wrapperTableOfContents { + position: fixed; + left: 40px; + top: 106px; + width: 240px; + padding-top: 100px; +} diff --git a/src/components/Views/Edit.tsx b/src/components/Views/Edit.tsx index a19ba586..d3ed8179 100644 --- a/src/components/Views/Edit.tsx +++ b/src/components/Views/Edit.tsx @@ -15,19 +15,21 @@ import { AudioUploader } from '../Editor/AudioUploader' import { slugify } from '../../utils/slugify' import { SolidSwiper } from '../_shared/SolidSwiper' import { DropArea } from '../_shared/DropArea' -import { LayoutType, MediaItem, UploadedFile } from '../../pages/types' +import { LayoutType, MediaItem } from '../../pages/types' import { clone } from '../../utils/clone' import deepEqual from 'fast-deep-equal' import { AutoSaveNotice } from '../Editor/AutoSaveNotice' import { PublishSettings } from './PublishSettings' import { createStore } from 'solid-js/store' +import SimplifiedEditor from '../Editor/SimplifiedEditor' +import { isDesktop } from '../../utils/media-query' +import { TableOfContents } from '../TableOfContents' type Props = { shout: Shout } export const MAX_HEADER_LIMIT = 100 -export const MAX_LEAD_LIMIT = 400 export const EMPTY_TOPIC: Topic = { id: -1, slug: '' @@ -64,6 +66,8 @@ export const EditView = (props: Props) => { slug: props.shout.slug, shoutId: props.shout.id, title: props.shout.title, + lead: props.shout.lead, + description: props.shout.description, subtitle: props.shout.subtitle, selectedTopics: shoutTopics, mainTopic: shoutTopics.find((topic) => topic.slug === props.shout.mainTopic) || EMPTY_TOPIC, @@ -75,7 +79,6 @@ export const EditView = (props: Props) => { } const subtitleInput: { current: HTMLTextAreaElement } = { current: null } - const leadInput: { current: HTMLTextAreaElement } = { current: null } const [prevForm, setPrevForm] = createStore(clone(form)) const [saving, setSaving] = createSignal(false) @@ -226,7 +229,6 @@ export const EditView = (props: Props) => { } const showLeadInput = () => { setIsLeadVisible(true) - leadInput.current.focus() } return ( @@ -235,7 +237,6 @@ export const EditView = (props: Props) => { {pageTitle()}

- + + +
+ + + +
+
@@ -320,16 +329,13 @@ export const EditView = (props: Props) => { /> - { - leadInput.current = el - }} - allowEnterKey={true} - value={(value) => setForm('lead', value)} - class={styles.leadInput} - placeholder={t('Description')} - initialValue={form.subtitle} - maxLength={MAX_LEAD_LIMIT} + setForm('lead', value)} /> diff --git a/src/components/Views/Feed.tsx b/src/components/Views/Feed.tsx index c763f9da..a37dd66a 100644 --- a/src/components/Views/Feed.tsx +++ b/src/components/Views/Feed.tsx @@ -55,13 +55,18 @@ export const FeedView = () => { actions: { loadReactionsBy } } = useReactions() + onMount(() => { + loadMore() + }) + createEffect( on( () => page().route + searchParams().by, () => { resetSortedArticles() loadMore() - } + }, + { defer: true } ) ) diff --git a/src/components/Views/FourOuFour.tsx b/src/components/Views/FourOuFour.tsx index e35a6448..0f362fe1 100644 --- a/src/components/Views/FourOuFour.tsx +++ b/src/components/Views/FourOuFour.tsx @@ -8,7 +8,7 @@ export const FourOuFourView = (_props) => { return (
-
+
diff --git a/src/components/Views/PublishSettings/PublishSettings.tsx b/src/components/Views/PublishSettings/PublishSettings.tsx index 287b67f5..b9f62582 100644 --- a/src/components/Views/PublishSettings/PublishSettings.tsx +++ b/src/components/Views/PublishSettings/PublishSettings.tsx @@ -10,7 +10,7 @@ import { useLocalize } from '../../../context/localize' import { Modal } from '../../Nav/Modal' import { Topic } from '../../../graphql/types.gen' import { apiClient } from '../../../utils/apiClient' -import { EMPTY_TOPIC, MAX_LEAD_LIMIT } from '../Edit' +import { EMPTY_TOPIC } from '../Edit' import { useSession } from '../../../context/session' import { Icon } from '../../_shared/Icon' import stylesBeside from '../../Feed/Beside.module.scss' @@ -19,6 +19,7 @@ import { router } from '../../../stores/router' import { GrowingTextarea } from '../../_shared/GrowingTextarea' import { createStore } from 'solid-js/store' import { UploadedFile } from '../../../pages/types' +import SimplifiedEditor, { MAX_DESCRIPTION_LIMIT } from '../../Editor/SimplifiedEditor' type Props = { shoutId: number @@ -35,12 +36,13 @@ export const PublishSettings = (props: Props) => { const { t } = useLocalize() const { user } = useSession() - const composeLead = () => { - if (!props.form.lead) { - const leadText = props.form.body.replaceAll(/<\/?[^>]+(>|$)/gi, ' ') - return shorten(leadText, MAX_LEAD_LIMIT).trim() + const composeDescription = () => { + if (!props.form.description) { + const cleanFootnotes = props.form.body.replaceAll(/.*?<\/footnote>/g, '') + const leadText = cleanFootnotes.replaceAll(/<\/?[^>]+(>|$)/gi, ' ') + return shorten(leadText, MAX_DESCRIPTION_LIMIT).trim() } - return props.form.lead + return props.form.description } const initialData: Partial = { @@ -49,7 +51,7 @@ export const PublishSettings = (props: Props) => { slug: props.form.slug, title: props.form.title, subtitle: props.form.subtitle, - lead: composeLead() + description: composeDescription() } const { @@ -183,15 +185,15 @@ export const PublishSettings = (props: Props) => { allowEnterKey={false} maxLength={100} /> - setSettingsForm('lead', value)} - allowEnterKey={false} - maxLength={MAX_LEAD_LIMIT} + label={t('Description')} + initialContent={composeDescription()} + onChange={(value) => setForm('description', value)} + maxLength={MAX_DESCRIPTION_LIMIT} />
diff --git a/src/components/_shared/Button/Button.tsx b/src/components/_shared/Button/Button.tsx index ac8f744e..42d7d5ec 100644 --- a/src/components/_shared/Button/Button.tsx +++ b/src/components/_shared/Button/Button.tsx @@ -9,7 +9,7 @@ type Props = { type?: 'submit' | 'button' loading?: boolean disabled?: boolean - onClick?: () => void + onClick?: (event?: MouseEvent) => void class?: string ref?: HTMLButtonElement | ((el: HTMLButtonElement) => void) } diff --git a/src/components/_shared/DarkModeToggle/DarkModeToggle.tsx b/src/components/_shared/DarkModeToggle/DarkModeToggle.tsx index e68438ab..ab7ee6f5 100644 --- a/src/components/_shared/DarkModeToggle/DarkModeToggle.tsx +++ b/src/components/_shared/DarkModeToggle/DarkModeToggle.tsx @@ -3,7 +3,6 @@ import styles from './DarkModeToggle.module.scss' import { Icon } from '../Icon' import { useLocalize } from '../../../context/localize' import { createSignal, onCleanup, onMount } from 'solid-js' -import { createPrefersDark } from '@solid-primitives/media' type Props = { class?: string @@ -14,7 +13,6 @@ const editorDarkModeAttr = document.documentElement.getAttribute('editorDarkMode export const DarkModeToggle = (props: Props) => { const { t } = useLocalize() - const prefersDark = createPrefersDark() const [editorDarkMode, setEditorDarkMode] = createSignal(false) onMount(() => { @@ -27,9 +25,8 @@ export const DarkModeToggle = (props: Props) => { } if (!editorDarkModeAttr && !editorDarkModeSelected) { - setEditorDarkMode(prefersDark()) - localStorage.setItem('editorDarkMode', prefersDark() ? 'true' : 'false') - document.documentElement.dataset.editorDarkMode = prefersDark() ? 'true' : 'false' + localStorage.setItem('editorDarkMode', 'false') + document.documentElement.dataset.editorDarkMode = 'false' } onCleanup(() => { diff --git a/src/components/_shared/FloatingPanel/FloatingPanel.module.scss b/src/components/_shared/FloatingPanel/FloatingPanel.module.scss new file mode 100644 index 00000000..b21e0326 --- /dev/null +++ b/src/components/_shared/FloatingPanel/FloatingPanel.module.scss @@ -0,0 +1,32 @@ +.PanelWrapper { + position: fixed; + bottom: 20px; + left: 0; + right: 0; + + display: none; + align-items: center; + justify-content: space-between; + + max-width: 430px; + width: auto; + height: auto; + + margin: 0 auto; + padding: 14px; + + background-color: var(--background-color); + border: 2px solid black; + + @include media-breakpoint-down(sm) { + flex-wrap: wrap; + + input { + min-width: 250px; + } + } +} + +.PanelWrapperVisible { + display: flex; +} diff --git a/src/components/_shared/FloatingPanel/FloatingPanel.tsx b/src/components/_shared/FloatingPanel/FloatingPanel.tsx new file mode 100644 index 00000000..3bc7de66 --- /dev/null +++ b/src/components/_shared/FloatingPanel/FloatingPanel.tsx @@ -0,0 +1,35 @@ +import { clsx } from 'clsx' + +import { Button } from '../Button' + +import styles from './FloatingPanel.module.scss' + +type Props = { + isVisible: boolean + confirmTitle: string + confirmAction: () => void + declineTitle: string + declineAction: () => void +} + +export default (props: Props) => { + return ( +
+
+ ) +} diff --git a/src/components/_shared/GrowingTextarea/GrowingTextarea.module.scss b/src/components/_shared/GrowingTextarea/GrowingTextarea.module.scss index 967f3d1d..db819c9e 100644 --- a/src/components/_shared/GrowingTextarea/GrowingTextarea.module.scss +++ b/src/components/_shared/GrowingTextarea/GrowingTextarea.module.scss @@ -9,7 +9,7 @@ padding: 16px 12px; border-radius: 2px; border: 2px solid var(--black-100); - background: var(--white-500, #fff); + background: var(--white-500); } &.hasFieldName { diff --git a/src/components/_shared/Popover/Popover.tsx b/src/components/_shared/Popover/Popover.tsx index 2ebb2bf2..4a937b1f 100644 --- a/src/components/_shared/Popover/Popover.tsx +++ b/src/components/_shared/Popover/Popover.tsx @@ -4,7 +4,7 @@ import styles from './Popover.module.scss' type Props = { children: (setTooltipEl: (el: HTMLElement | null) => void) => JSX.Element - content: string + content: string | JSX.Element disabled?: boolean } diff --git a/src/components/_shared/SolidSwiper/SolidSwiper.tsx b/src/components/_shared/SolidSwiper/SolidSwiper.tsx index 3037127f..0c670873 100644 --- a/src/components/_shared/SolidSwiper/SolidSwiper.tsx +++ b/src/components/_shared/SolidSwiper/SolidSwiper.tsx @@ -63,7 +63,8 @@ export const SolidSwiper = (props: Props) => { () => { mainSwipeRef.current?.swiper.update() thumbSwipeRef.current?.swiper.update() - } + }, + { defer: true } ) ) @@ -95,7 +96,7 @@ export const SolidSwiper = (props: Props) => { const results: UploadedFile[] = [] for (const file of selectedFiles) { const result = await handleFileUpload(file) - results.push(result.url) + results.push(result) } props.onImagesAdd(composeMediaItems(results)) setLoading(false) @@ -316,18 +317,18 @@ export const SolidSwiper = (props: Props) => { type="text" class={clsx(styles.input, styles.title)} placeholder={t('Enter image title')} - value={props.images[slideIndex()].title} + value={props.images[slideIndex()]?.title} onChange={(event) => handleSlideDescriptionChange(slideIndex(), 'title', event.target.value)} /> handleSlideDescriptionChange(slideIndex(), 'source', event.target.value)} /> setSlideBody(value)} diff --git a/src/components/_shared/SolidSwiper/Swiper.module.scss b/src/components/_shared/SolidSwiper/Swiper.module.scss index 02044bb0..33a4eb1e 100644 --- a/src/components/_shared/SolidSwiper/Swiper.module.scss +++ b/src/components/_shared/SolidSwiper/Swiper.module.scss @@ -93,8 +93,9 @@ $navigation-reserve: 32px; position: absolute; top: 16px; right: 16px; - background: rgba(#000, 0.3); + background: rgba(var(--default-color), 0.3); cursor: pointer; + z-index: 12; display: none; .icon { @@ -129,14 +130,14 @@ $navigation-reserve: 32px; background: var(--placeholder-color-semi); position: relative; - &:hover .action { - display: flex; - } - img { max-height: 100%; } } + + &:hover .action { + display: flex; + } } &.editorMode { .holder { diff --git a/src/context/editor.tsx b/src/context/editor.tsx index 28316a7d..75812c53 100644 --- a/src/context/editor.tsx +++ b/src/context/editor.tsx @@ -21,12 +21,13 @@ export type ShoutForm = { slug: string title: string subtitle: string + lead?: string + description?: string selectedTopics: Topic[] mainTopic?: Topic body: string coverImageUrl: string media?: string - lead?: string } type EditorContextType = { @@ -91,15 +92,12 @@ export const EditorProvider = (props: { children: JSX.Element }) => { const [form, setForm] = createStore(null) const [formErrors, setFormErrors] = createStore>(null) - const [wordCounter, setWordCounter] = createSignal({ characters: 0, words: 0 }) - const toggleEditorPanel = () => setIsEditorPanelVisible((value) => !value) const countWords = (value) => setWordCounter(value) - const validate = () => { if (!form.title) { setFormErrors('title', t('Required')) @@ -136,6 +134,8 @@ export const EditorProvider = (props: { children: JSX.Element }) => { slug: formToUpdate.slug, subtitle: formToUpdate.subtitle, title: formToUpdate.title, + lead: formToUpdate.lead, + description: formToUpdate.description, cover: formToUpdate.coverImageUrl, media: formToUpdate.media }, diff --git a/src/graphql/mutation/article-update.ts b/src/graphql/mutation/article-update.ts index da53afe7..a41c5c1a 100644 --- a/src/graphql/mutation/article-update.ts +++ b/src/graphql/mutation/article-update.ts @@ -9,6 +9,8 @@ export default gql` slug title subtitle + lead + description body visibility } diff --git a/src/graphql/query/article-load.ts b/src/graphql/query/article-load.ts index 1fac1b12..d16589ba 100644 --- a/src/graphql/query/article-load.ts +++ b/src/graphql/query/article-load.ts @@ -5,6 +5,8 @@ export default gql` loadShout(slug: $slug, shout_id: $shoutId) { id title + lead + description visibility subtitle slug diff --git a/src/graphql/query/articles-load-by.ts b/src/graphql/query/articles-load-by.ts index 9e8dd616..5db2edca 100644 --- a/src/graphql/query/articles-load-by.ts +++ b/src/graphql/query/articles-load-by.ts @@ -5,6 +5,8 @@ export default gql` loadShouts(options: $options) { id title + lead + description subtitle slug layout diff --git a/src/graphql/query/author-followers.ts b/src/graphql/query/author-followers.ts index 13cf82b0..d300c385 100644 --- a/src/graphql/query/author-followers.ts +++ b/src/graphql/query/author-followers.ts @@ -7,6 +7,10 @@ export default gql` slug name userpic + bio + stat { + shouts + } } } ` diff --git a/src/graphql/query/author-following-users.ts b/src/graphql/query/author-following-users.ts index 9b7df27e..c62de1eb 100644 --- a/src/graphql/query/author-following-users.ts +++ b/src/graphql/query/author-following-users.ts @@ -7,6 +7,10 @@ export default gql` slug name userpic + bio + stat { + shouts + } } } ` diff --git a/src/graphql/types.gen.ts b/src/graphql/types.gen.ts index ccf57ba9..61516a6b 100644 --- a/src/graphql/types.gen.ts +++ b/src/graphql/types.gen.ts @@ -554,6 +554,7 @@ export type Shout = { createdAt: Scalars['DateTime'] deletedAt?: Maybe deletedBy?: Maybe + description?: Maybe id: Scalars['Int'] lang?: Maybe layout?: Maybe @@ -577,7 +578,9 @@ export type ShoutInput = { body?: InputMaybe community?: InputMaybe cover?: InputMaybe + description?: InputMaybe layout?: InputMaybe + lead?: InputMaybe mainTopic?: InputMaybe media?: InputMaybe slug?: InputMaybe diff --git a/src/pages/about/dogma.page.tsx b/src/pages/about/dogma.page.tsx index 229cf16a..a700c532 100644 --- a/src/pages/about/dogma.page.tsx +++ b/src/pages/about/dogma.page.tsx @@ -10,13 +10,13 @@ export const DogmaPage = () => {

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

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

  1. - На первое место ставим факты. Наша задача - не судить, а наблюдать и непредвзято + На первое место ставим факты. Наша задача — не судить, а наблюдать и непредвзято фиксировать происходящее. Все утверждения и выводы, которые мы делаем, подтверждаются фактами, цифрами, мнениями экспертов или ссылками на авторитетные источники.
  2. @@ -39,7 +39,7 @@ export const DogmaPage = () => {
  3. Всегда исправляем ошибки, если мы их допустили. - Никто не безгрешен, иногда и мы ошибаемся. Заметили ошибку - отправьте{' '} + Никто не безгрешен, иногда и мы ошибаемся. Заметили ошибку — отправьте{' '} ремарку автору или напишите нам на{' '} welcome@discours.io diff --git a/src/pages/author.page.tsx b/src/pages/author.page.tsx index 7346f789..c0533f19 100644 --- a/src/pages/author.page.tsx +++ b/src/pages/author.page.tsx @@ -17,14 +17,15 @@ export const AuthorPage = (props: PageProps) => { Boolean(props.authorShouts) && Boolean(props.author) && props.author.slug === slug() ) - const preload = () => - Promise.all([ + const preload = () => { + return Promise.all([ loadShouts({ filters: { author: slug(), visibility: 'community' }, limit: PRERENDERED_ARTICLES_COUNT }), loadAuthor({ slug: slug() }) ]) + } onMount(async () => { if (isLoaded()) { @@ -44,7 +45,8 @@ export const AuthorPage = (props: PageProps) => { resetSortedArticles() await preload() setIsLoaded(true) - } + }, + { defer: true } ) ) diff --git a/src/pages/profile/profileSettings.page.tsx b/src/pages/profile/profileSettings.page.tsx index 6b086435..838d50d6 100644 --- a/src/pages/profile/profileSettings.page.tsx +++ b/src/pages/profile/profileSettings.page.tsx @@ -9,7 +9,7 @@ import { useProfileForm } from '../../context/profile' import { validateUrl } from '../../utils/validateUrl' import { createFileUploader } from '@solid-primitives/upload' import { useSession } from '../../context/session' -import { Button } from '../../components/_shared/Button' +import FloatingPanel from '../../components/_shared/FloatingPanel/FloatingPanel' import { useSnackbar } from '../../context/snackbar' import { useLocalize } from '../../context/localize' import { handleFileUpload } from '../../utils/handleFileUpload' @@ -21,8 +21,8 @@ export const ProfileSettingsPage = () => { const { t } = useLocalize() const [addLinkForm, setAddLinkForm] = createSignal(false) const [incorrectUrl, setIncorrectUrl] = createSignal(false) - const [isSubmitting, setIsSubmitting] = createSignal(false) const [isUserpicUpdating, setIsUserpicUpdating] = createSignal(false) + const [isFloatingPanelVisible, setIsFloatingPanelVisible] = createSignal(false) const { actions: { showSnackbar } @@ -31,6 +31,7 @@ export const ProfileSettingsPage = () => { const { actions: { loadSession } } = useSession() + const { form, updateFormField, submit, slugError } = useProfileForm() const [prevForm, setPrevForm] = createStore(clone(form)) @@ -45,8 +46,6 @@ export const ProfileSettingsPage = () => { const handleSubmit = async (event: Event) => { event.preventDefault() - setIsSubmitting(true) - try { await submit(form) setPrevForm(clone(form)) @@ -56,7 +55,6 @@ export const ProfileSettingsPage = () => { } loadSession() - setIsSubmitting(false) } const { selectFiles } = createFileUploader({ multiple: false, accept: 'image/*' }) @@ -68,6 +66,7 @@ export const ProfileSettingsPage = () => { const result = await handleFileUpload(uploadFile) updateFormField('userpic', result.url) setIsUserpicUpdating(false) + setIsFloatingPanelVisible(true) } catch (error) { console.error('[upload avatar] error', error) } @@ -92,6 +91,11 @@ export const ProfileSettingsPage = () => { onCleanup(() => window.removeEventListener('beforeunload', handleBeforeUnload)) }) + const handleSaveProfile = () => { + setIsFloatingPanelVisible(false) + setPrevForm(clone(form)) + } + return ( @@ -107,7 +111,15 @@ export const ProfileSettingsPage = () => {

    {t('Profile settings')}

    {t('Here you can customize your profile the way you want.')}

    - + { + if (!deepEqual(form, prevForm)) { + setIsFloatingPanelVisible(true) + } + }} + enctype="multipart/form-data" + >

    {t('Userpic')}

    {

    -
diff --git a/src/pages/topic.page.tsx b/src/pages/topic.page.tsx index c1f3d3c9..8988a719 100644 --- a/src/pages/topic.page.tsx +++ b/src/pages/topic.page.tsx @@ -41,7 +41,8 @@ export const TopicPage = (props: PageProps) => { resetSortedArticles() await preload() setIsLoaded(true) - } + }, + { defer: true } ) ) diff --git a/src/stores/ui.ts b/src/stores/ui.ts index ed376d7a..1477d57d 100644 --- a/src/stores/ui.ts +++ b/src/stores/ui.ts @@ -16,8 +16,12 @@ export type ModalType = | 'donate' | 'inviteToChat' | 'uploadImage' + | 'simplifiedEditorUploadImage' | 'uploadCoverImage' | 'editorInsertLink' + | 'simplifiedEditorInsertLink' + | 'followers' + | 'subscriptions' type WarnKind = 'error' | 'warn' | 'info' @@ -36,8 +40,12 @@ export const MODALS: Record = { donate: 'donate', inviteToChat: 'inviteToChat', uploadImage: 'uploadImage', + simplifiedEditorUploadImage: 'simplifiedEditorUploadImage', uploadCoverImage: 'uploadCoverImage', - editorInsertLink: 'editorInsertLink' + editorInsertLink: 'editorInsertLink', + simplifiedEditorInsertLink: 'simplifiedEditorInsertLink', + followers: 'followers', + subscriptions: 'subscriptions' } const [modal, setModal] = createSignal(null) diff --git a/src/stores/zine/articles.ts b/src/stores/zine/articles.ts index b333aa99..f680353a 100644 --- a/src/stores/zine/articles.ts +++ b/src/stores/zine/articles.ts @@ -1,7 +1,6 @@ -import type { Author, Shout, ShoutInput, Topic, LoadShoutsOptions } from '../../graphql/types.gen' +import type { Author, Shout, ShoutInput, LoadShoutsOptions } from '../../graphql/types.gen' import { apiClient } from '../../utils/apiClient' import { addAuthorsByTopic } from './authors' -import { addTopicsByAuthor } from './topics' import { byStat } from '../../utils/sortby' import { createSignal } from 'solid-js' import { createLazyMemo } from '@solid-primitives/memo' @@ -97,26 +96,6 @@ const addArticles = (...args: Shout[][]) => { }, {} as { [topicSlug: string]: Author[] }) addAuthorsByTopic(authorsByTopic) - - const topicsByAuthor = allArticles.reduce((acc, article) => { - const { authors, topics } = article - - authors.forEach((author) => { - if (!acc[author.slug]) { - acc[author.slug] = [] - } - - topics.forEach((topic) => { - if (!acc[author.slug].some((t) => t.slug === topic.slug)) { - acc[author.slug].push(topic) - } - }) - }) - - return acc - }, {} as { [authorSlug: string]: Topic[] }) - - addTopicsByAuthor(topicsByAuthor) } const addSortedArticles = (articles: Shout[]) => { diff --git a/src/stores/zine/authors.ts b/src/stores/zine/authors.ts index c133b8ec..9c3abd30 100644 --- a/src/stores/zine/authors.ts +++ b/src/stores/zine/authors.ts @@ -38,12 +38,18 @@ const addAuthors = (authors: Author[]) => { return acc }, {} as Record) - setAuthorEntities((prevAuthorEntities) => { - return { - ...prevAuthorEntities, - ...newAuthorEntities - } - }) + setAuthorEntities((prevAuthorEntities) => + Object.keys(newAuthorEntities).reduce( + (acc, authorSlug) => { + acc[authorSlug] = { + ...acc[authorSlug], + ...newAuthorEntities[authorSlug] + } + return acc + }, + { ...prevAuthorEntities } + ) + ) } export const loadAuthor = async ({ slug }: { slug: string }): Promise => { diff --git a/src/stores/zine/topics.ts b/src/stores/zine/topics.ts index 568695b0..c8d81caf 100644 --- a/src/stores/zine/topics.ts +++ b/src/stores/zine/topics.ts @@ -12,7 +12,6 @@ export const setTopicsSort = (sortBy: TopicsSortBy) => setSortAllBy(sortBy) const [topicEntities, setTopicEntities] = createSignal<{ [topicSlug: string]: Topic }>({}) const [randomTopics, setRandomTopics] = createSignal([]) -const [topicsByAuthor, setTopicByAuthor] = createSignal<{ [authorSlug: string]: Topic[] }>({}) const sortedTopics = createLazyMemo(() => { const topics = Object.values(topicEntities()) @@ -68,27 +67,6 @@ const addTopics = (...args: Topic[][]) => { }) } -export const addTopicsByAuthor = (newTopicsByAuthors: { [authorSlug: string]: Topic[] }) => { - const allTopics = Object.values(newTopicsByAuthors).flat() - addTopics(allTopics) - - setTopicByAuthor((prevTopicsByAuthor) => { - return Object.entries(newTopicsByAuthors).reduce((acc, [authorSlug, topics]) => { - if (!acc[authorSlug]) { - acc[authorSlug] = [] - } - - topics.forEach((topic) => { - if (!acc[authorSlug].some((t) => t.slug === topic.slug)) { - acc[authorSlug].push(topic) - } - }) - - return acc - }, prevTopicsByAuthor) - }) -} - export const loadAllTopics = async (): Promise => { const topics = await apiClient.getAllTopics() addTopics(topics) @@ -121,5 +99,5 @@ export const useTopicsStore = (initialState: InitialState = {}) => { setRandomTopics(initialState.randomTopics) } - return { topicEntities, sortedTopics, randomTopics, topTopics, topicsByAuthor } + return { topicEntities, sortedTopics, randomTopics, topTopics } } diff --git a/src/styles/FourOuFour.module.scss b/src/styles/FourOuFour.module.scss index b8a01d3e..09a87b79 100644 --- a/src/styles/FourOuFour.module.scss +++ b/src/styles/FourOuFour.module.scss @@ -1,12 +1,13 @@ .errorPageWrapper { height: 100vh; + margin: -120px 0 -2em; padding-top: 100px; } .errorPage { position: relative; top: 35%; - transform: translateY(-45%); + transform: translateY(-50%); .image-link:hover { background: none; @@ -36,13 +37,14 @@ .errorImage { display: block; margin: auto; + max-height: 60vh; width: 85%; } .errorText { font-weight: 300; left: 52px; - margin-bottom: 1em; + margin: 0 60px 1em 0; position: relative; top: -2.25em; diff --git a/src/styles/_globals.scss b/src/styles/_globals.scss index 3649585e..fb18331c 100644 --- a/src/styles/_globals.scss +++ b/src/styles/_globals.scss @@ -10,7 +10,7 @@ $grid-breakpoints: ( md: 768px, lg: 992px, xl: 1200px, - // xxl: 1500px, + xxl: 1400px ) !default; $default-color: #141414; $link-color: #2638d9; diff --git a/src/styles/app.scss b/src/styles/app.scss index 5fdd71b8..a6d21e12 100644 --- a/src/styles/app.scss +++ b/src/styles/app.scss @@ -478,6 +478,7 @@ form { } .pretty-form__item { + margin-bottom: 2em; position: relative; input { diff --git a/src/utils/getNumeralsDeclension.ts b/src/utils/getNumeralsDeclension.ts new file mode 100644 index 00000000..dafc15c0 --- /dev/null +++ b/src/utils/getNumeralsDeclension.ts @@ -0,0 +1,3 @@ +// Usage in tsx: {getNumeralsDeclension(NUMBER, ['яблоко', 'яблока', 'яблок'])} +export const getNumeralsDeclension = (number: number, words: string[], cases = [2, 0, 1, 1, 1, 2]) => + words[number % 100 > 4 && number % 100 < 20 ? 2 : cases[number % 10 < 5 ? number % 10 : 5]] diff --git a/src/utils/handleFileUpload.ts b/src/utils/handleFileUpload.ts index c9c925d4..ddf5d4ef 100644 --- a/src/utils/handleFileUpload.ts +++ b/src/utils/handleFileUpload.ts @@ -1,9 +1,10 @@ import { UploadFile } from '@solid-primitives/upload' import { apiBaseUrl } from './config' +import { UploadedFile } from '../pages/types' const apiUrl = `${apiBaseUrl}/upload` -export const handleFileUpload = async (uploadFile: UploadFile) => { +export const handleFileUpload = async (uploadFile: UploadFile): Promise => { const formData = new FormData() formData.append('file', uploadFile.file, uploadFile.name) const response = await fetch(apiUrl, {