diff --git a/public/icons/editor-unlink.svg b/public/icons/editor-unlink.svg new file mode 100644 index 00000000..df9325d0 --- /dev/null +++ b/public/icons/editor-unlink.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index acc42485..5968c4b4 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -2,24 +2,22 @@ "...subscribing": "...subscribing", "About myself": "About myself", "About the project": "About the project", - "actions": "actions", "Add comment": "Comment", "Address on Discourse": "Address on Discourse", "All": "All", "All authors": "All authors", "All posts": "All posts", - "all topics": "all topics", "All topics": "All topics", "Almost done! Check your email.": "Almost done! Just checking your email.", "Artworks": "Artworks", "Audio": "Audio", - "author": "author", "Author": "Author", - "authors": "authors", + "Author subscriptions": "Подписки на авторов", "Authors": "Authors", "Back to main page": "Back to main page", "Become an author": "Become an author", "Bookmarked": "Saved", + "Bookmarks": "Bookmarks", "By alphabet": "By alphabet", "By authors": "By authors", "By name": "By name", @@ -28,23 +26,23 @@ "By relevance": "By relevance", "By shouts": "By publications", "By signing up you agree with our": "By signing up you agree with our", + "By time": "By time", "By title": "By title", "By updates": "By updates", "By views": "By views", - "cancel": "Cancel", + "Characters": "Знаков", "Chat Title": "Chat Title", "Choose who you want to write to": "Choose who you want to write to", "Collaborate": "Help Edit", - "collections": "collections", "Comments": "Comments", "Communities": "Communities", - "community": "community", "Cooperate": "Cooperate", "Copy": "Copy", "Copy link": "Copy link", - "Create account": "Create an account", + "Corrections history": "Corrections history", "Create Chat": "Create Chat", "Create Group": "Create a group", + "Create account": "Create an account", "Create post": "Create post", "Date of Birth": "Date of Birth", "Delete": "Delete", @@ -52,30 +50,28 @@ "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 created with our common effort": "Discours exists because of our common effort", "Discussing": "Discussing", - "discussion": "discourse", "Discussion rules": "Discussion rules", "Dogma": "Dogma", "Drafts": "Drafts", "Edit": "Edit", + "Editing": "Editing", "Email": "Mail", - "email not confirmed": "email not confirmed", - "enter": "enter", "Enter": "Enter", + "Enter URL address": "Enter URL address", "Enter text": "Enter text", - "Enter the code or click the link from email to confirm": "Enter the code from the email or follow the link in the email to confirm registration", "Enter the Discours": "Enter the Discours", + "Enter the code or click the link from email to confirm": "Enter the code from the email or follow the link in the email to confirm registration", "Enter your new password": "Enter your new password", "Error": "Error", "Everything is ok, please give us your email address": "It's okay, just enter your email address to receive a password reset link.", + "FAQ": "Tips and suggestions", "Favorite": "Favorites", "Favorite topics": "Favorite topics", - "feed": "feed", "Feed settings": "Feed settings", "Feedback": "Feedback", "Fill email": "Fill email", "Follow": "Follow", "Follow the topic": "Follow the topic", - "follower": "follower", "Followers": "Followers", "Forgot password?": "Forgot your password?", "Forward": "Forward", @@ -84,12 +80,16 @@ "Go to main page": "Go to main page", "Group Chat": "Group Chat", "Groups": "Groups", + "Headers": "Headers", + "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.", "Hooray! Welcome!": "Hooray! Welcome!", "Horizontal collaborative journalistic platform": "Horizontal collaborative journalism platform", + "Hotkeys": "Горячие клавиши", "How can I help/skills": "How can I help/skills", "How it works": "How it works", + "How to write a good article": "Как написать хорошую статью", "How to write an article": "How to write an article", "I have an account": "I have an account!", "I have no account yet": "I don't have an account yet", @@ -97,7 +97,8 @@ "Independant magazine with an open horizontal cooperation about culture, science and society": "Independant magazine with an open horizontal cooperation about culture, science and society", "Introduce": "Introduction", "Invalid email": "Check if your email is correct", - "invalid password": "invalid password", + "Invalid url format": "Invalid url format", + "Invite co-authors": "Invite co-authors", "Invite to collab": "Invite to Collab", "It does not look like url": "It doesn't look like a link", "Join": "Join", @@ -106,10 +107,13 @@ "Join the global community of authors!": "Join the global community of authors from all over the world!", "Just start typing...": "Just start typing...", "Knowledge base": "Knowledge base", + "Last rev.": "Посл. изм.", "Link sent, check your email": "Link sent, check your email", + "Lists": "Lists", "Literature": "Literature", "Load more": "Show more", "Loading": "Loading", + "Logout": "Logout", "Manifest": "Manifest", "More": "More", "Most commented": "Commented", @@ -117,6 +121,7 @@ "My feed": "My feed", "My subscriptions": "Subscriptions", "Name": "Name", + "New only": "New only", "New password": "New password", "New stories every day and even more!": "New stories and more are waiting for you every day!", "No such account, please try to register": "No such account found, please try to register", @@ -124,13 +129,14 @@ "Nothing is here": "There is nothing here", "Or continue with social network": "Or continue with social network", "Our regular contributor": "Our regular contributor", + "Paragraphs": "Абзацев", "Participating": "Participating", "Partners": "Partners", "Password": "Password", "Password again": "Password again", "Passwords are not equal": "Passwords are not equal", + "Paste Embed code": "Paste Embed code", "Personal": "Personal", - "personal data usage and email notifications": "to process personal data and receive email notifications", "Pin": "Pin", "Please check your email address": "Please check your email address", "Please confirm your email to finish": "Confirm your email and the action will complete", @@ -141,19 +147,19 @@ "Please, confirm email": "Please confirm email", "Popular": "Popular", "Popular authors": "Popular authors", - "post": "post", "Principles": "Community principles", "Profile": "Profile", "Profile settings": "Profile settings", "Publications": "Publications", "Quit": "Quit", + "Quotes": "Quotes", "Reason uknown": "Reason unknown", "Recent": "Fresh", - "register": "register", "Reply": "Reply", "Report": "Complain", "Resend code": "Send confirmation", "Restore password": "Restore password", + "Save draft": "Save draft", "Save settings": "Save settings", "Search": "Search", "Search author": "Search author", @@ -165,10 +171,8 @@ "Send link again": "Send link again", "Settings": "Settings", "Share": "Share", - "shout": "post", + "Short opening": "Short opening", "Show": "Show", - "sign up or sign in": "sign up or sign in", - "slug is used by another user": "Slug is already taken by another user", "Social networks": "Social networks", "Something went wrong, check email and password": "Something went wrong. Check your email and password", "Something went wrong, please try again": "Something went wrong, please try again", @@ -184,10 +188,11 @@ "Successfully authorized": "Authorization successful", "Suggest an idea": "Suggest an idea", "Support us": "Help the magazine", - "terms of use": "terms of use", "Terms of use": "Site rules", "Thank you": "Thank you", + "This comment has not yet been rated": "This comment has not yet been rated", "This email is already taken. If it's you": "This email is already taken. If it's you", + "This post has not been rated yet": "This post has not been rated yet", "To leave a comment please": "To leave a comment please", "To write a comment, you must": "To write a comment, you must", "Top authors": "Authors rating", @@ -199,17 +204,15 @@ "Top topics": "Interesting topics", "Top viewed": "Most viewed", "Topic is supported by": "Topic is supported by", - "topics": "topics", + "Topic subscriptions": "Подписки на темы", "Topics": "Topics", "Topics which supported by author": "Topics which supported by author", "Try to find another way": "Try to find another way", "Unfollow": "Unfollow", "Unfollow the topic": "Unfollow the topic", - "user already exist": "user already exists", "Username": "Username", "Userpic": "Userpic", "Video": "Video", - "view": "view", "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", @@ -217,10 +220,12 @@ "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", + "Words": "Слов", "Work with us": "Cooperate with Discourse", "Write": "Write", "Write a comment...": "Write a comment...", "Write about the topic": "Write about the topic", + "Write an article": "Write an article", "Write comment": "Write comment", "Write message": "Write a message", "Write to us": "Write to us", @@ -229,18 +234,35 @@ "You've confirmed email": "You've confirmed email", "You've reached a non-existed page": "You've reached a non-existed page", "Your name will appear on your profile page and as your signature in publications, comments and responses.": "Your name will appear on your profile page and as your signature in publications, comments and responses", - "zine": "zine", - "By time": "By time", - "New only": "New only", - "Short opening": "Short opening", - "Write an article": "Write an article", - "Enter URL address": "Enter URL address", - "Invalid url format": "Invalid url format", - "Headers": "Headers", - "Quotes": "Quotes", - "Lists": "Lists", - "Bookmarks": "Bookmarks", - "Logout": "Logout", - "This comment has not yet been rated": "This comment has not yet been rated", - "This post has not been rated yet": "This post has not been rated yet" + "accomplices": "accomplices", + "actions": "actions", + "all topics": "all topics", + "author": "author", + "authors": "authors", + "bookmarks": "bookmarks", + "cancel": "Cancel", + "collections": "collections", + "community": "community", + "discussion": "discourse", + "discussions": "discussions", + "drafts": "drafts", + "email not confirmed": "email not confirmed", + "enter": "enter", + "feed": "feed", + "follower": "follower", + "general feed": "general tape", + "invalid password": "invalid password", + "my feed": "my ribbon", + "notifications": "notifications", + "personal data usage and email notifications": "to process personal data and receive email notifications", + "post": "post", + "register": "register", + "shout": "post", + "sign up or sign in": "sign up or sign in", + "slug is used by another user": "Slug is already taken by another user", + "terms of use": "terms of use", + "topics": "topics", + "user already exist": "user already exists", + "view": "view", + "zine": "zine" } diff --git a/public/locales/ru/translation.json b/public/locales/ru/translation.json index 83d9c750..99560546 100644 --- a/public/locales/ru/translation.json +++ b/public/locales/ru/translation.json @@ -14,10 +14,12 @@ "Artworks": "Артворки", "Audio": "Аудио", "Author": "Автор", + "Author subscriptions": "Подписки на авторов", "Authors": "Авторы", "Back to main page": "Вернуться на главную", "Become an author": "Стать автором", "Bookmarked": "Сохранено", + "Bookmarks": "Закладки", "By alphabet": "По алфавиту", "By authors": "По авторам", "By name": "По имени", @@ -26,9 +28,11 @@ "By relevance": "По релевантности", "By shouts": "По публикациям", "By signing up you agree with our": "Регистрируясь, вы соглашаетесь с", + "By time": "По порядку", "By title": "По названию", "By updates": "По обновлениям", "By views": "По просмотрам", + "Characters": "Знаков", "Chat Title": "Тема дискурса", "Choose who you want to write to": "Выберите кому хотите написать", "Collaborate": "Помочь редактировать", @@ -56,6 +60,7 @@ "Editing": "Редактирование", "Email": "Почта", "Enter": "Войти", + "Enter URL address": "Введите адрес ссылки", "Enter text": "Введите текст", "Enter the Discours": "Войти в Дискурс", "Enter the code or click the link from email to confirm": "Введите код из письма или пройдите по ссылке в письме для подтверждения регистрации", @@ -80,12 +85,16 @@ "Group Chat": "Общий чат", "Groups": "Группы", "Header": "Заголовок", + "Headers": "Заголовки", + "Help": "Помощь", "Help to edit": "Помочь редактировать", "Here you can customize your profile the way you want.": "Здесь можно настроить свой профиль так, как вы хотите.", "Hooray! Welcome!": "Ура! Добро пожаловать!", "Horizontal collaborative journalistic platform": "Горизонтальная платформа для коллаборативной журналистики", + "Hotkeys": "Горячие клавиши", "How can I help/skills": "Чем могу помочь/навыки", "How it works": "Как это работает", + "How to write a good article": "Как написать хорошую статью", "How to write an article": "Как написать статью", "I have an account": "У меня есть аккаунт!", "I have no account yet": "У меня еще нет аккаунта", @@ -94,6 +103,7 @@ "Introduce": "Представление", "Invalid email": "Проверьте правильность ввода почты", "Invite co-authors": "Пригласить соавторов", + "Invalid url format": "Неверный формат ссылки", "Invite experts": "Пригласить экспертов", "Invite to collab": "Пригласить к участию", "It does not look like url": "Это не похоже на ссылку", @@ -104,10 +114,13 @@ "Just start typing...": "Просто начните печатать...", "Karma": "Карма", "Knowledge base": "База знаний", + "Last rev.": "Посл. изм.", "Link sent, check your email": "Ссылка отправлена, проверьте почту", + "Lists": "Списки", "Literature": "Литература", "Load more": "Показать ещё", "Loading": "Загрузка", + "Logout": "Выход", "Manifest": "Манифест", "More": "Ещё", "Most commented": "Комментируемое", @@ -115,6 +128,7 @@ "My feed": "Моя лента", "My subscriptions": "Подписки", "Name": "Имя", + "New only": "Только новые", "New password": "Новый пароль", "New stories every day and even more!": "Каждый день вас ждут новые истории и ещё много всего интересного!", "No such account, please try to register": "Такой адрес не найден, попробуйте зарегистрироваться", @@ -122,11 +136,13 @@ "Nothing is here": "Здесь ничего нет", "Or continue with social network": "Или продолжите через соцсеть", "Our regular contributor": "Наш постоянный автор", + "Paragraphs": "Абзацев", "Participating": "Участвовать", "Partners": "Партнёры", "Password": "Пароль", "Password again": "Пароль ещё раз", "Passwords are not equal": "Пароли не совпадают", + "Paste Embed code": "Вставьте embed код", "Personal": "Личные", "Pin": "Закрепить", "Please check your email address": "Пожалуйста, проверьте введенный адрес почты", @@ -147,6 +163,7 @@ "Publication settings": "Настройки публикации", "Publish": "Опубликовать", "Quit": "Выйти", + "Quotes": "Цитаты", "Reason uknown": "Причина неизвестна", "Recent": "Свежее", "Reply": "Ответить", @@ -154,8 +171,8 @@ "Resend code": "Выслать подтверждение", "Restore password": "Восстановить пароль", "Save": "Сохранить", - "Save settings": "Сохранить настройки", "Save draft": "Сохранить черновик", + "Save settings": "Сохранить настройки", "Search": "Поиск", "Search author": "Поиск автора", "Search topic": "Поиск темы", @@ -166,6 +183,7 @@ "Send link again": "Прислать ссылку ещё раз", "Settings": "Настройки", "Share": "Поделиться", + "Short opening": "Небольшое вступление, чтобы заинтересовать читателя", "Show": "Показать", "Social networks": "Социальные сети", "Something went wrong, check email and password": "Что-то пошло не так. Проверьте адрес электронной почты и пароль", @@ -185,7 +203,9 @@ "Support us": "Помочь журналу", "Terms of use": "Правила сайта", "Thank you": "Благодарности", + "This comment has not yet been rated": "Этот комментарий еще пока никто не оценил", "This email is already taken. If it's you": "Такой email уже зарегистрирован. Если это вы", + "This post has not been rated yet": "Эту публикацию еще пока никто не оценил", "To leave a comment please": "Чтобы оставить комментарий, необходимо", "To write a comment, you must": "Чтобы написать комментарий, необходимо", "Top authors": "Рейтинг авторов", @@ -197,6 +217,7 @@ "Top topics": "Интересные темы", "Top viewed": "Самое читаемое", "Topic is supported by": "Тему поддерживают", + "Topic subscriptions": "Подписки на темы", "Topics": "Темы", "Topics which supported by author": "Автор поддерживает темы", "Try to find another way": "Попробуйте найти по-другому", @@ -213,10 +234,12 @@ "We've sent you a message with a link to enter our website.": "Мы выслали вам письмо с ссылкой на почту. Перейдите по ссылке в письме, чтобы войти на сайт.", "Welcome!": "Добро пожаловать!", "Where": "Откуда", + "Words": "Слов", "Work with us": "Сотрудничать с Дискурсом", "Write": "Написать", "Write a comment...": "Написать комментарий...", "Write about the topic": "Написать в тему", + "Write an article": "Написать статью", "Write comment": "Написать комментарий", "Write message": "Написать сообщение", "Write to us": "Напишите нам", @@ -226,10 +249,12 @@ "You've reached a non-existed page": "Вы попали на несуществующую страницу", "You've successfully logged out": "Вы успешно вышли из аккаунта", "Your name will appear on your profile page and as your signature in publications, comments and responses.": "Ваше имя появится на странице вашего профиля и как ваша подпись в публикациях, комментариях и откликах", + "accomplices": "соучастники", "actions": "действия", "all topics": "все темы", "author": "автор", "authors": "авторы", + "bookmarks": "закладки", "cancel": "Отмена", "collections": "коллекции", "community": "сообщество", @@ -237,11 +262,16 @@ "create_group": "Создать группу", "discourse_theme": "Тема дискурса", "discussion": "дискурс", + "discussions": "дискуссии", + "drafts": "черновики", "email not confirmed": "email не подтвержден", "enter": "войдите", "feed": "лента", "follower": "подписчик", + "general feed": "общая лента", "invalid password": "некорректный пароль", + "my feed": "моя лента", + "notifications": "уведомления", "or": "или", "personal data usage and email notifications": "на обработку персональных данных и на получение почтовых уведомлений", "post": "пост", @@ -255,18 +285,5 @@ "topics": "темы", "user already exist": "пользователь уже существует", "view": "просмотр", - "zine": "журнал", - "By time": "По порядку", - "New only": "Только новые", - "Short opening": "Небольшое вступление, чтобы заинтересовать читателя", - "Write an article": "Написать статью", - "Enter URL address": "Введите адрес ссылки", - "Invalid url format": "Неверный формат ссылки", - "Headers": "Заголовки", - "Quotes": "Цитаты", - "Lists": "Списки", - "Bookmarks": "Закладки", - "Logout": "Выход", - "This comment has not yet been rated": "Этот комментарий еще пока никто не оценил", - "This post has not been rated yet": "Эту публикацию еще пока никто не оценил" + "zine": "журнал" } diff --git a/src/components/App.tsx b/src/components/App.tsx index 6b3416fa..a9b984c0 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -37,6 +37,7 @@ import { ProfileSecurityPage } from '../pages/profile/profileSecurity.page' import { ProfileSubscriptionsPage } from '../pages/profile/profileSubscriptions.page' import { SnackbarProvider } from '../context/snackbar' import { LocalizeProvider } from '../context/localize' +import { EditorProvider } from '../context/editor' // TODO: lazy load // const SomePage = lazy(() => import('./Pages/SomePage')) @@ -93,11 +94,13 @@ export const App = (props: PageProps) => { return ( - - - - - + + + + + + + ) } diff --git a/src/components/Article/Article.module.scss b/src/components/Article/Article.module.scss index 35442400..9066b8d8 100644 --- a/src/components/Article/Article.module.scss +++ b/src/components/Article/Article.module.scss @@ -186,9 +186,27 @@ img { } } } +} + +.shoutStatsItemInner { + cursor: pointer; + margin: -0.3em -0.3em 0; + padding: 0.3em; + + .icon { + margin-right: 0; + } + + .iconEdit { + margin-right: 0.3em; + } &:hover { + background: #000; cursor: pointer; + img { + filter: invert(1); + } } } diff --git a/src/components/Article/FullArticle.tsx b/src/components/Article/FullArticle.tsx index 1dc9ec90..1994eb4a 100644 --- a/src/components/Article/FullArticle.tsx +++ b/src/components/Article/FullArticle.tsx @@ -34,6 +34,7 @@ interface MediaItem { const MediaView = (props: { media: MediaItem; kind: Shout['layout'] }) => { const { t } = useLocalize() + return ( <> {t('Cannot show this media type')}}> @@ -84,12 +85,11 @@ export const FullArticle = (props: ArticleProps) => { const body = createMemo(() => props.article.body) const media = createMemo(() => { const mi = JSON.parse(props.article.media || '[]') - console.debug(mi) + console.debug('!!! media items', mi) return mi }) const commentsRef: { current: HTMLDivElement } = { current: null } - const scrollToComments = () => { window.scrollTo({ top: commentsRef.current.offsetTop - 96, @@ -97,12 +97,20 @@ export const FullArticle = (props: ArticleProps) => { behavior: 'smooth' }) } + const { searchParams } = useRouter() createEffect(() => { if (props.scrollToComments) { scrollToComments() } }) + const { changeSearchParam } = useRouter() + createEffect(() => { + if (searchParams()?.scrollTo === 'comments' && commentsRef.current) { + scrollToComments() + changeSearchParam('scrollTo', null) + } + }) const { actions: { loadReactionsBy } diff --git a/src/components/Editor/Editor.tsx b/src/components/Editor/Editor.tsx index b2039eec..0bd66635 100644 --- a/src/components/Editor/Editor.tsx +++ b/src/components/Editor/Editor.tsx @@ -1,3 +1,4 @@ +import { createEffect } from 'solid-js' import { createTiptapEditor, useEditorHTML } from 'solid-tiptap' import { useLocalize } from '../../context/localize' import { Blockquote } from '@tiptap/extension-blockquote' @@ -19,7 +20,6 @@ import { HardBreak } from '@tiptap/extension-hard-break' import { Heading } from '@tiptap/extension-heading' import { Highlight } from '@tiptap/extension-highlight' import { Link } from '@tiptap/extension-link' -import { Youtube } from '@tiptap/extension-youtube' import { Document } from '@tiptap/extension-document' import { Text } from '@tiptap/extension-text' import { Image } from '@tiptap/extension-image' @@ -37,6 +37,8 @@ import { IndexeddbPersistence } from 'y-indexeddb' import { useSession } from '../../context/session' import uniqolor from 'uniqolor' import { HocuspocusProvider } from '@hocuspocus/provider' +import { Embed } from './extensions/embed' +import { useEditorContext } from '../../context/editor' type EditorProps = { shoutSlug: string @@ -128,7 +130,7 @@ export const Editor = (props: EditorProps) => { HardBreak, Highlight, Image, - Youtube, + Embed, TrailingNode, BubbleMenu.configure({ element: bubbleMenuRef.current @@ -142,6 +144,22 @@ export const Editor = (props: EditorProps) => { ] })) + const html = useEditorHTML(() => editor()) + + const { + actions: { countWords } + } = useEditorContext() + + createEffect(() => { + props.onChange(html()) + if (html()) { + countWords({ + characters: editor().storage.characterCount.characters(), + words: editor().storage.characterCount.words() + }) + } + }) + return ( <>
(editorElRef.current = el)} /> diff --git a/src/components/Editor/EditorBubbleMenu/EditorBubbleMenu.tsx b/src/components/Editor/EditorBubbleMenu/EditorBubbleMenu.tsx index cf095243..9d1dc83b 100644 --- a/src/components/Editor/EditorBubbleMenu/EditorBubbleMenu.tsx +++ b/src/components/Editor/EditorBubbleMenu/EditorBubbleMenu.tsx @@ -5,7 +5,8 @@ import { Icon } from '../../_shared/Icon' import { clsx } from 'clsx' import { createEditorTransaction } from 'solid-tiptap' import { useLocalize } from '../../../context/localize' -import { LinkForm } from './LinkForm' +import { InlineForm } from '../InlineForm' +import validateUrl from '../../../utils/validateUrl' type BubbleMenuProps = { editor: Editor @@ -55,12 +56,37 @@ export const EditorBubbleMenu = (props: BubbleMenuProps) => { setListBubbleOpen((prev) => !prev) } + const handleLinkFormSubmit = (value: string) => { + props.editor.chain().focus().setLink({ href: value }).run() + } + + const currentUrl = createEditorTransaction( + () => props.editor, + (editor) => { + return (editor && editor.getAttributes('link').href) || '' + } + ) + + const handleClearLinkForm = () => { + if (currentUrl()) { + props.editor.chain().focus().unsetLink().run() + } + setLinkEditorOpen(false) + } + return ( <>
- setLinkEditorOpen(false)} /> + (validateUrl(value) ? '' : t('Invalid url format'))} + onSubmit={handleLinkFormSubmit} + onClose={() => setLinkEditorOpen(false)} + /> <> diff --git a/src/components/Editor/EditorFloatingMenu.module.scss b/src/components/Editor/EditorFloatingMenu.module.scss index e6fd6cee..6df8d7ed 100644 --- a/src/components/Editor/EditorFloatingMenu.module.scss +++ b/src/components/Editor/EditorFloatingMenu.module.scss @@ -6,5 +6,10 @@ button { opacity: 0.3; vertical-align: text-bottom; + transition: opacity 0.3s ease-in-out; + + &:hover { + opacity: 1; + } } } diff --git a/src/components/Editor/EditorFloatingMenu.tsx b/src/components/Editor/EditorFloatingMenu.tsx index edf216fd..38c0764e 100644 --- a/src/components/Editor/EditorFloatingMenu.tsx +++ b/src/components/Editor/EditorFloatingMenu.tsx @@ -1,19 +1,51 @@ +import { createSignal, Show } from 'solid-js' import type { Editor } from '@tiptap/core' -import styles from './EditorFloatingMenu.module.scss' import { Icon } from '../_shared/Icon' -import { createSignal } from 'solid-js' +import { InlineForm } from './InlineForm' +import styles from './EditorFloatingMenu.module.scss' +import HTMLParser from 'html-to-json-parser' type FloatingMenuProps = { editor: Editor ref: (el: HTMLDivElement) => void } +const embedData = async (data) => { + const result = await HTMLParser(data, false) + if (result && 'type' in result && result.type === 'iframe') { + return result.attributes + } +} + export const EditorFloatingMenu = (props: FloatingMenuProps) => { + const [inlineEditorOpen, setInlineEditorOpen] = createSignal(false) + + const handleEmbedFormSubmit = async (value: string) => { + // TODO: add support instagram embed (blockquote) + const emb = await embedData(value) + props.editor.chain().focus().setIframe(emb).run() + } + + const validateEmbed = async (value) => { + const iframeData = await HTMLParser(value, false) + if (iframeData && iframeData.type !== 'iframe') { + return + } + } + return (
- + + setInlineEditorOpen(false)} + validate={validateEmbed} + onSubmit={handleEmbedFormSubmit} + /> +
) } diff --git a/src/components/Editor/InlineForm/InlineForm.module.scss b/src/components/Editor/InlineForm/InlineForm.module.scss new file mode 100644 index 00000000..00f76969 --- /dev/null +++ b/src/components/Editor/InlineForm/InlineForm.module.scss @@ -0,0 +1,67 @@ +.InlineForm { + position: relative; + + &.inBubble { + //... + } + + &.inFloating { + position: absolute; + left: calc(100% + 1rem); + top: -0.8rem; + min-width: 64vw; + background: #fff; + box-shadow: 0 4px 10px rgba(#000, 0.25); + + button { + opacity: 1; + &:disabled, + &:disabled:hover { + opacity: 0.3; + } + } + } + + .form { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + padding: 6px 11px; + + input { + margin: 0 12px 0 0; + padding: 0; + flex: 1; + border: none; + min-width: 200px; + display: block; + + &::placeholder { + color: rgba(#000, 0.3); + } + &:focus { + outline: none; + } + } + } + + .linkError { + padding: 6px 11px; + color: red; + font-size: 0.7em; + position: absolute; + bottom: -3rem; + left: 0; + right: 0; + height: 0; + background: #fff; + box-shadow: 0 4px 10px rgba(#000, 0.25); + opacity: 0; + transition: height 0.3s ease-in-out, opacity 0.3s ease-in-out; + + &.visible { + height: 32px; + opacity: 1; + } + } +} diff --git a/src/components/Editor/InlineForm/InlineForm.tsx b/src/components/Editor/InlineForm/InlineForm.tsx new file mode 100644 index 00000000..d3950294 --- /dev/null +++ b/src/components/Editor/InlineForm/InlineForm.tsx @@ -0,0 +1,90 @@ +import styles from './InlineForm.module.scss' +import { Icon } from '../../_shared/Icon' +import { createEditorTransaction } from 'solid-tiptap' +import type { Editor } from '@tiptap/core' +import { createSignal, Show } from 'solid-js' +import { useLocalize } from '../../../context/localize' +import { clsx } from 'clsx' + +type Props = { + onClose: () => void + onClear?: () => void + onSubmit: (value: string) => void + variant: 'inBubble' | 'inFloating' + validate?: (value: string) => string | Promise + initialValue?: string +} + +export const InlineForm = (props: Props) => { + const { t } = useLocalize() + const [formValue, setFormValue] = createSignal(props.initialValue || '') + const [formValueError, setFormValueError] = createSignal('') + + const handleFormInput = (value) => { + setFormValue(value) + } + + const handleSaveButtonClick = async () => { + const errorMessage = await props.validate(formValue()) + if (errorMessage) { + setFormValueError(errorMessage) + return + } + props.onSubmit(formValue()) + props.onClose() + } + + const handleKeyPress = async (event) => { + setFormValueError('') + const key = event.key + + if (key === 'Enter') { + await handleSaveButtonClick() + } + + if (key === 'Esc') { + props.onClear + } + } + + return ( +
+
+ + handleKeyPress(e)} + onInput={(e) => handleFormInput(e.currentTarget.value)} + /> + + + handleKeyPress(e)} + onInput={(e) => handleFormInput(e.currentTarget.value)} + /> + + + +
+ +
+ {formValueError()} +
+
+ ) +} diff --git a/src/components/Editor/InlineForm/index.ts b/src/components/Editor/InlineForm/index.ts new file mode 100644 index 00000000..cfb5d1de --- /dev/null +++ b/src/components/Editor/InlineForm/index.ts @@ -0,0 +1 @@ +export { InlineForm } from './InlineForm' diff --git a/src/components/Editor/Panel/Panel.module.scss b/src/components/Editor/Panel/Panel.module.scss new file mode 100644 index 00000000..ad3b12b9 --- /dev/null +++ b/src/components/Editor/Panel/Panel.module.scss @@ -0,0 +1,93 @@ +.Panel { + background: #1f1f1f; + color: rgb(255 255 255 / 0.35); + display: flex; + flex-direction: column; + font-size: 1.7rem; + justify-content: flex-start; + height: 100%; + line-height: 1.4; + padding: $grid-gutter-width $grid-gutter-width / 2; + position: fixed; + transition: transform 0.3s; + right: 0; + top: 0; + z-index: 10; + + .close { + filter: invert(1); + margin: -1.6rem 0 0 -1.6rem; + } + + .actionsHolder { + padding: 0 $grid-gutter-width / 2; + + &.scrolled { + overflow-y: auto; + scroll-behavior: smooth; + } + } + + section { + border-bottom: 2px solid rgb(255 255 255 / 0.1); + padding: 1.8rem 0; + + &:first-child { + padding-top: 0; + } + + p { + margin: 0.6em 0; + + &:last-child { + margin-bottom: 0; + } + } + } + + .button { + font-weight: normal; + margin-left: -1.6rem; + text-align: left; + + &:hover { + color: #fff; + text-decoration: none; + } + } + + .buttonWithIcon { + margin-left: -1.6rem; + + .icon { + filter: invert(0.5); + margin-right: 0.3em; + width: 1em; + } + + img { + vertical-align: middle; + } + } + + .stats { + display: flex; + flex: 1; + flex-direction: column; + justify-content: flex-end; + margin-top: 3em; + } + + a { + color: rgb(255 255 255 / 0.35); + font-weight: normal !important; + + &:hover { + background: none; + color: #fff; + } + } + &.hidden { + transform: translateX(100%); + } +} diff --git a/src/components/Editor/Panel/Panel.tsx b/src/components/Editor/Panel/Panel.tsx new file mode 100644 index 00000000..c043135b --- /dev/null +++ b/src/components/Editor/Panel/Panel.tsx @@ -0,0 +1,107 @@ +import { Show } from 'solid-js' +import { clsx } from 'clsx' +import { Button } from '../../_shared/Button' +import { Icon } from '../../_shared/Icon' +import { useLocalize } from '../../../context/localize' +import styles from './Panel.module.scss' +import { useEditorContext } from '../../../context/editor' + +type Props = { + // isVisible: boolean +} + +export const Panel = (props: Props) => { + const { t } = useLocalize() + const { + isEditorPanelVisible, + wordCounter, + actions: { toggleEditorPanel } + } = useEditorContext() + + return ( + + ) +} diff --git a/src/components/Editor/Panel/index.ts b/src/components/Editor/Panel/index.ts new file mode 100644 index 00000000..7dcefa28 --- /dev/null +++ b/src/components/Editor/Panel/index.ts @@ -0,0 +1 @@ +export { Panel } from './Panel' diff --git a/src/components/Editor/Prosemirror.scss b/src/components/Editor/Prosemirror.scss index 03ca073c..eaea214c 100644 --- a/src/components/Editor/Prosemirror.scss +++ b/src/components/Editor/Prosemirror.scss @@ -47,3 +47,17 @@ user-select: none; white-space: nowrap; } + +.embed-wrapper { + display: flex; + align-items: center; + justify-content: center; + padding: 1rem; + background: #f1f1f1; + margin: 4rem 0; + + iframe { + border: none; + overflow: hidden; + } +} diff --git a/src/components/Editor/extensions/embed.ts b/src/components/Editor/extensions/embed.ts new file mode 100644 index 00000000..a905621b --- /dev/null +++ b/src/components/Editor/extensions/embed.ts @@ -0,0 +1,73 @@ +import { mergeAttributes, Node } from '@tiptap/core' +import { NodeRange } from 'prosemirror-model' +import { insert } from 'solid-js/web' +import { TextSelection } from 'prosemirror-state' + +export interface IframeOptions { + allowFullscreen: boolean + HTMLAttributes: { + [key: string]: any + } +} + +declare module '@tiptap/core' { + interface Commands { + iframe: { + setIframe: (options: { src: string }) => ReturnType + } + } +} + +export const Embed = Node.create({ + name: 'embed', + group: 'block', + selectable: true, + atom: true, + draggable: true, + addAttributes() { + return { + src: { default: null }, + width: { default: null }, + height: { default: null } + } + }, + parseHTML() { + return [ + { + tag: 'iframe' + } + ] + }, + renderHTML({ HTMLAttributes }) { + return ['iframe', mergeAttributes(HTMLAttributes)] + }, + addNodeView() { + return ({ node }) => { + const div = document.createElement('div') + div.className = 'embed-wrapper' + const iframe = document.createElement('iframe') + iframe.width = node.attrs.width + iframe.height = node.attrs.height + iframe.allowfullscreen = node.attrs.allowfullscreen + iframe.src = node.attrs.src + div.append(iframe) + return { + dom: div + } + } + }, + addCommands() { + return { + setIframe: + (options) => + ({ tr, dispatch }) => { + const { selection } = tr + const node = this.type.create(options) + if (dispatch) { + tr.replaceRangeWith(selection.from, selection.to, node) + } + return true + } + } + } +}) diff --git a/src/components/Feed/Card.tsx b/src/components/Feed/Card.tsx index 4424badf..ff651bf0 100644 --- a/src/components/Feed/Card.tsx +++ b/src/components/Feed/Card.tsx @@ -12,6 +12,8 @@ import stylesHeader from '../Nav/Header.module.scss' import { getDescription } from '../../utils/meta' import { FeedArticlePopup } from './FeedArticlePopup' import { useLocalize } from '../../context/localize' +import { openPage } from '@nanostores/router' +import { router, useRouter } from '../../stores/router' interface ArticleCardProps { settings?: { @@ -35,6 +37,7 @@ interface ArticleCardProps { isBeside?: boolean } article: Shout + scrollTo: 'comments' } const getTitleAndSubtitle = (article: Shout): { title: string; subtitle: string } => { @@ -75,6 +78,13 @@ export const ArticleCard = (props: ArticleCardProps) => { const { cover, layout, slug, authors, stat, body } = props.article + const { changeSearchParam } = useRouter() + const scrollToComments = (event) => { + event.preventDefault() + openPage(router, 'article', { slug: slug }) + changeSearchParam('scrollTo', 'comments') + } + return (
{
- + scrollToComments(event)}> {stat?.commented || t('Add comment')} diff --git a/src/components/Feed/Sidebar.module.scss b/src/components/Feed/Sidebar.module.scss deleted file mode 100644 index 718e56a2..00000000 --- a/src/components/Feed/Sidebar.module.scss +++ /dev/null @@ -1,83 +0,0 @@ -.sidebar { - max-height: calc(100vh - 120px); - overflow: auto; - position: sticky; - top: 120px; -} - -.sidebarItemName { - margin-right: 0.5em; - position: relative; - overflow: hidden; - text-overflow: ellipsis; -} - -.counter { - @include font-size(1.2rem); - align-items: center; - align-self: flex-start; - background: #f6f6f6; - border-radius: 0.8rem; - display: flex; - font-weight: bold; - justify-content: center; - min-width: 2em; - margin-left: 0.5em; - padding: 0.25em 0.5em 0.15em; - transition: background-color 0.2s; -} - -.unread { - position: relative; - - &::after { - background: #2638d9; - border-radius: 100%; - content: ''; - display: inline-block; - height: 0.5em; - left: 100%; - margin-left: 0.3em; - position: absolute; - top: 0.5em; - width: 0.5em; - } -} - -.settings { - display: flex; - justify-content: space-between; - margin-bottom: 2em; -} - -a { - img { - transition: filter 0.3s; - } - - &:hover { - img { - filter: invert(1); - } - - .counter { - background: #000; - } - } -} - -.icon { - display: inline-block; - line-height: 1; - height: 2rem; - margin-right: 0.5em; - vertical-align: middle; - width: 2.2rem; - - img { - height: 100%; - object-fit: contain; - object-position: center; - width: 100%; - } -} diff --git a/src/components/Feed/Sidebar.tsx b/src/components/Feed/Sidebar.tsx deleted file mode 100644 index 6c0d6771..00000000 --- a/src/components/Feed/Sidebar.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import { For } from 'solid-js' -import type { Author } from '../../graphql/types.gen' -import { useAuthorsStore } from '../../stores/zine/authors' - -import { Icon } from '../_shared/Icon' -import { useTopicsStore } from '../../stores/zine/topics' -import { useArticlesStore } from '../../stores/zine/articles' -import { useSeenStore } from '../../stores/zine/seen' -import { useSession } from '../../context/session' -import styles from './Sidebar.module.scss' -import { useLocalize } from '../../context/localize' - -type FeedSidebarProps = { - authors: Author[] -} - -export const FeedSidebar = (props: FeedSidebarProps) => { - const { t } = useLocalize() - const { seen } = useSeenStore() - const { session } = useSession() - const { authorEntities } = useAuthorsStore({ authors: props.authors }) - const { articlesByTopic } = useArticlesStore() - const { topicEntities } = useTopicsStore() - - const checkTopicIsSeen = (topicSlug: string) => { - return articlesByTopic()[topicSlug]?.every((article) => Boolean(seen()[article.slug])) - } - - const checkAuthorIsSeen = (authorSlug: string) => { - return Boolean(seen()[authorSlug]) - } - - return ( - - ) -} diff --git a/src/components/Feed/Sidebar/Sidebar.module.scss b/src/components/Feed/Sidebar/Sidebar.module.scss new file mode 100644 index 00000000..0cd74b44 --- /dev/null +++ b/src/components/Feed/Sidebar/Sidebar.module.scss @@ -0,0 +1,89 @@ +.sidebar { + max-height: calc(100vh - 120px); + overflow: auto; + position: sticky; + top: 120px; + + ul > li { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .sidebarItemName { + margin-right: 0.5em; + position: relative; + overflow: hidden; + text-overflow: ellipsis; + } + + .counter { + @include font-size(1.2rem); + align-items: center; + align-self: flex-start; + background: #f6f6f6; + border-radius: 0.8rem; + display: inline-flex; + font-weight: bold; + justify-content: center; + min-width: 2em; + margin-left: 0.5em; + padding: 0.25em 0.5em 0.15em; + transition: background-color 0.2s; + } + + .unread { + position: relative; + + &::after { + background: #2638d9; + border-radius: 100%; + content: ''; + display: inline-block; + height: 0.5em; + left: 100%; + margin-left: 0.3em; + position: absolute; + top: 0.5em; + width: 0.5em; + } + } + + .settings { + display: flex; + justify-content: space-between; + margin-bottom: 2em; + } + + a { + img { + transition: filter 0.3s; + } + + &:hover { + img { + filter: invert(1); + } + + .counter { + background: #000; + } + } + } + + .icon { + display: inline-block; + line-height: 1; + height: 2rem; + margin-right: 0.5em; + vertical-align: middle; + width: 2.2rem; + + img { + height: 100%; + object-fit: contain; + object-position: center; + width: 100%; + } + } +} diff --git a/src/components/Feed/Sidebar/Sidebar.tsx b/src/components/Feed/Sidebar/Sidebar.tsx new file mode 100644 index 00000000..fd507d97 --- /dev/null +++ b/src/components/Feed/Sidebar/Sidebar.tsx @@ -0,0 +1,130 @@ +import { createSignal, For } from 'solid-js' +import type { Author } from '../../../graphql/types.gen' +import { useAuthorsStore } from '../../../stores/zine/authors' +import { Icon } from '../../_shared/Icon' +import { useTopicsStore } from '../../../stores/zine/topics' +import { useArticlesStore } from '../../../stores/zine/articles' +import { useSeenStore } from '../../../stores/zine/seen' +import { useSession } from '../../../context/session' +import { useLocalize } from '../../../context/localize' +import styles from './Sidebar.module.scss' + +type FeedSidebarProps = { + authors: Author[] +} + +type ListItem = { + title: string + icon?: string + counter?: number + href?: string + isBold?: boolean +} + +export const Sidebar = (props: FeedSidebarProps) => { + const { t } = useLocalize() + const { seen } = useSeenStore() + const { session } = useSession() + const { authorEntities } = useAuthorsStore({ authors: props.authors }) + const { articlesByTopic } = useArticlesStore() + const { topicEntities } = useTopicsStore() + + createSignal(() => { + console.log('!!! topicEntities:', topicEntities()) + }) + + const checkTopicIsSeen = (topicSlug: string) => { + return articlesByTopic()[topicSlug]?.every((article) => Boolean(seen()[article.slug])) + } + + const checkAuthorIsSeen = (authorSlug: string) => { + return Boolean(seen()[authorSlug]) + } + + const menuItems: ListItem[] = [ + { + icon: 'feed-all', + title: t('general feed') + }, + { + icon: 'feed-my', + title: t('my feed') + }, + { + icon: 'feed-collaborate', + title: t('accomplices') + }, + { + icon: 'feed-discussion', + title: t('discussions'), + counter: 4 + }, + { + icon: 'feed-drafts', + title: t('drafts'), + counter: 14 + }, + { + icon: 'bookmark', + title: t('bookmarks'), + counter: 6 + }, + { + icon: 'feed-notifications', + title: t('notifications') + }, + { + href: '/feed?by=subscribed', + title: t('My subscriptions'), + isBold: true + } + ] + + return ( +
+ + +
+ ) +} diff --git a/src/components/Feed/Sidebar/index.ts b/src/components/Feed/Sidebar/index.ts new file mode 100644 index 00000000..70fb43ef --- /dev/null +++ b/src/components/Feed/Sidebar/index.ts @@ -0,0 +1 @@ +export { Sidebar } from './Sidebar' diff --git a/src/components/Nav/Header.tsx b/src/components/Nav/Header.tsx index 0407cb3b..87de7880 100644 --- a/src/components/Nav/Header.tsx +++ b/src/components/Nav/Header.tsx @@ -80,7 +80,8 @@ export const Header = (props: Props) => { }) }) - const scrollToComments = (value) => { + const scrollToComments = (event, value) => { + event.preventDefault() props.scrollToComments(value) } @@ -110,7 +111,6 @@ export const Header = (props: Props) => {
{props.title}
-