From 08cc22b93ce03c393649d6da7e06f14d10a29f81 Mon Sep 17 00:00:00 2001 From: ilia tapazukk Date: Thu, 4 May 2023 04:43:52 +0000 Subject: [PATCH] Add image by URL, Upload (choose file by button and D&D) --- package-lock.json | 69 +++++++++-- public/icons/editor-embed.svg | 5 + public/icons/editor-image-dd.svg | 3 + public/icons/editor-image.svg | 3 + public/locales/en/translation.json | 6 + public/locales/ru/translation.json | 12 +- src/components/Editor/Editor.tsx | 20 +-- .../EditorBubbleMenu/EditorBubbleMenu.tsx | 7 +- .../Editor/EditorBubbleMenu/index.ts | 1 + src/components/Editor/EditorFloatingMenu.tsx | 61 --------- .../EditorFloatingMenu.module.scss | 8 ++ .../EditorFloatingMenu/EditorFloatingMenu.tsx | 89 ++++++++++++++ .../EditorFloatingMenu/Menu/Menu.module.scss | 13 ++ .../Editor/EditorFloatingMenu/Menu/Menu.tsx | 19 +++ .../Editor/EditorFloatingMenu/Menu/index.ts | 1 + .../Editor/EditorFloatingMenu/index.ts | 1 + .../Editor/InlineForm/InlineForm.module.scss | 30 +---- .../Editor/InlineForm/InlineForm.tsx | 63 ++++------ src/components/Editor/Panel/Panel.tsx | 4 +- src/components/Editor/Prosemirror.scss | 7 ++ .../UploadModalContent.module.scss | 95 ++++++++++++++ .../Editor/UploadModal/UploadModalContent.tsx | 116 ++++++++++++++++++ src/components/Editor/UploadModal/index.ts | 1 + .../Nav/{ => Modal}/Modal.module.scss | 0 src/components/Nav/{ => Modal}/Modal.tsx | 14 ++- src/components/Nav/{ => Modal}/Opener.tsx | 4 +- src/components/Nav/Modal/index.ts | 1 + src/components/Views/Edit.tsx | 3 +- .../_shared/Button/Button.module.scss | 11 +- src/components/_shared/Button/Button.tsx | 2 +- src/components/_shared/Icon/Icon.module.scss | 8 +- src/components/_shared/Loading.module.scss | 2 +- src/pages/about/manifest.page.tsx | 2 +- src/pages/profile/profileSettings.page.tsx | 13 +- src/stores/ui.ts | 12 +- src/utils/handleFileUpload.ts | 13 ++ src/utils/verifyImg.ts | 10 ++ 37 files changed, 548 insertions(+), 181 deletions(-) create mode 100644 public/icons/editor-embed.svg create mode 100644 public/icons/editor-image-dd.svg create mode 100644 public/icons/editor-image.svg create mode 100644 src/components/Editor/EditorBubbleMenu/index.ts delete mode 100644 src/components/Editor/EditorFloatingMenu.tsx rename src/components/Editor/{ => EditorFloatingMenu}/EditorFloatingMenu.module.scss (56%) create mode 100644 src/components/Editor/EditorFloatingMenu/EditorFloatingMenu.tsx create mode 100644 src/components/Editor/EditorFloatingMenu/Menu/Menu.module.scss create mode 100644 src/components/Editor/EditorFloatingMenu/Menu/Menu.tsx create mode 100644 src/components/Editor/EditorFloatingMenu/Menu/index.ts create mode 100644 src/components/Editor/EditorFloatingMenu/index.ts create mode 100644 src/components/Editor/UploadModal/UploadModalContent.module.scss create mode 100644 src/components/Editor/UploadModal/UploadModalContent.tsx create mode 100644 src/components/Editor/UploadModal/index.ts rename src/components/Nav/{ => Modal}/Modal.module.scss (100%) rename src/components/Nav/{ => Modal}/Modal.tsx (76%) rename src/components/Nav/{ => Modal}/Opener.tsx (69%) create mode 100644 src/components/Nav/Modal/index.ts create mode 100644 src/utils/handleFileUpload.ts create mode 100644 src/utils/verifyImg.ts diff --git a/package-lock.json b/package-lock.json index 004d0735..65582b2f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5723,6 +5723,18 @@ "solid-js": ">=1.4.0" } }, + "node_modules/@soorria/solid-dropzone": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@soorria/solid-dropzone/-/solid-dropzone-0.0.5.tgz", + "integrity": "sha512-lIuCz33UuHZ/34jMLlhspzUZfpZyPvquJvUIZ4zDFZeaxIvgsspwDblKlk347K/qKu3+WNKhiDoIUodMpM7Yug==", + "dependencies": { + "attr-accept": "^2.2.2", + "file-selector": "^0.6.0" + }, + "peerDependencies": { + "solid-js": ">=1.0.0" + } + }, "node_modules/@thisbeyond/solid-select": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@thisbeyond/solid-select/-/solid-select-0.13.0.tgz", @@ -7175,6 +7187,14 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, + "node_modules/attr-accept": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz", + "integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==", + "engines": { + "node": ">=4" + } + }, "node_modules/auto-bind": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-4.0.0.tgz", @@ -8376,8 +8396,7 @@ "node_modules/csstype": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", - "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==", - "dev": true + "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==" }, "node_modules/damerau-levenshtein": { "version": "1.0.8", @@ -10149,6 +10168,17 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/file-selector": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.6.0.tgz", + "integrity": "sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==", + "dependencies": { + "tslib": "^2.4.0" + }, + "engines": { + "node": ">= 12" + } + }, "node_modules/filelist": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", @@ -18080,7 +18110,6 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/seroval/-/seroval-0.5.1.tgz", "integrity": "sha512-ZfhQVB59hmIauJG5Ydynupy8KHyr5imGNtdDhbZG68Ufh1Ynkv9KOYOAABf71oVbQxJ8VkWnMHAjEHE7fWkH5g==", - "dev": true, "engines": { "node": ">=10" } @@ -24782,6 +24811,15 @@ "dev": true, "requires": {} }, + "@soorria/solid-dropzone": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@soorria/solid-dropzone/-/solid-dropzone-0.0.5.tgz", + "integrity": "sha512-lIuCz33UuHZ/34jMLlhspzUZfpZyPvquJvUIZ4zDFZeaxIvgsspwDblKlk347K/qKu3+WNKhiDoIUodMpM7Yug==", + "requires": { + "attr-accept": "^2.2.2", + "file-selector": "^0.6.0" + } + }, "@thisbeyond/solid-select": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@thisbeyond/solid-select/-/solid-select-0.13.0.tgz", @@ -25828,6 +25866,11 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, + "attr-accept": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz", + "integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==" + }, "auto-bind": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-4.0.0.tgz", @@ -26717,8 +26760,7 @@ "csstype": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", - "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==", - "dev": true + "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==" }, "damerau-levenshtein": { "version": "1.0.8", @@ -28031,6 +28073,14 @@ "flat-cache": "^3.0.4" } }, + "file-selector": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.6.0.tgz", + "integrity": "sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==", + "requires": { + "tslib": "^2.4.0" + } + }, "filelist": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", @@ -33906,8 +33956,7 @@ "seroval": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/seroval/-/seroval-0.5.1.tgz", - "integrity": "sha512-ZfhQVB59hmIauJG5Ydynupy8KHyr5imGNtdDhbZG68Ufh1Ynkv9KOYOAABf71oVbQxJ8VkWnMHAjEHE7fWkH5g==", - "dev": true + "integrity": "sha512-ZfhQVB59hmIauJG5Ydynupy8KHyr5imGNtdDhbZG68Ufh1Ynkv9KOYOAABf71oVbQxJ8VkWnMHAjEHE7fWkH5g==" }, "set-blocking": { "version": "2.0.0", @@ -34043,9 +34092,9 @@ } }, "solid-js": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.7.3.tgz", - "integrity": "sha512-4hwaF/zV/xbNeBBIYDyu3dcReOZBECbO//mrra6GqOrKy4Soyo+fnKjpZSa0nODm6j1aL0iQRh/7ofYowH+jzw==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.7.0.tgz", + "integrity": "sha512-tLG68KWlVRgzYeAW003G3E70emZqTcqCKJR9QoGr0rcuiLIuKrlUoezT8jLME1YSl3Wfu35jzgeY10iLEY4YQQ==", "dev": true, "requires": { "csstype": "^3.1.0", diff --git a/public/icons/editor-embed.svg b/public/icons/editor-embed.svg new file mode 100644 index 00000000..5cb05402 --- /dev/null +++ b/public/icons/editor-embed.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/icons/editor-image-dd.svg b/public/icons/editor-image-dd.svg new file mode 100644 index 00000000..03042b34 --- /dev/null +++ b/public/icons/editor-image-dd.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/icons/editor-image.svg b/public/icons/editor-image.svg new file mode 100644 index 00000000..98586da6 --- /dev/null +++ b/public/icons/editor-image.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 5968c4b4..087436bb 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -53,6 +53,7 @@ "Discussion rules": "Discussion rules", "Dogma": "Dogma", "Drafts": "Drafts", + "Drag the image to this area": "Drag the image to this area", "Edit": "Edit", "Editing": "Editing", "Email": "Mail", @@ -94,9 +95,11 @@ "I have an account": "I have an account!", "I have no account yet": "I don't have an account yet", "I know the password": "I know the password", + "Image format not supported": "Image format not supported", "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 image link": "Invalid image link", "Invalid url format": "Invalid url format", "Invite co-authors": "Invite co-authors", "Invite to collab": "Invite to Collab", @@ -115,6 +118,7 @@ "Loading": "Loading", "Logout": "Logout", "Manifest": "Manifest", + "Many files, choose only one": "Many files, choose only one", "More": "More", "Most commented": "Commented", "Most read": "Readable", @@ -128,6 +132,7 @@ "Nothing here yet": "There's nothing here yet", "Nothing is here": "There is nothing here", "Or continue with social network": "Or continue with social network", + "Or paste a link to an image": "Or paste a link to an image", "Our regular contributor": "Our regular contributor", "Paragraphs": "Абзацев", "Participating": "Participating", @@ -210,6 +215,7 @@ "Try to find another way": "Try to find another way", "Unfollow": "Unfollow", "Unfollow the topic": "Unfollow the topic", + "Upload": "Upload", "Username": "Username", "Userpic": "Userpic", "Video": "Video", diff --git a/public/locales/ru/translation.json b/public/locales/ru/translation.json index 99560546..d11e92d0 100644 --- a/public/locales/ru/translation.json +++ b/public/locales/ru/translation.json @@ -55,6 +55,7 @@ "Discussion rules": "Правила сообществ самиздата в соцсетях", "Dogma": "Догма", "Drafts": "Черновики", + "Drag the image to this area": "Перетащите изображение в эту область", "Edit": "Редактировать", "Edited": "Отредактирован", "Editing": "Редактирование", @@ -67,8 +68,8 @@ "Enter your new password": "Введите новый пароль", "Error": "Ошибка", "Everything is ok, please give us your email address": "Ничего страшного, просто укажите свою почту, чтобы получить ссылку для сброса пароля.", - "Favorite": "Избранное", "FAQ": "Советы и предложения", + "Favorite": "Избранное", "Favorite topics": "Избранные темы", "Feed settings": "Настройки ленты", "Feedback": "Обратная связь", @@ -99,11 +100,13 @@ "I have an account": "У меня есть аккаунт!", "I have no account yet": "У меня еще нет аккаунта", "I know the password": "Я знаю пароль", + "Image format not supported": "Тип изображения не поддерживается", "Independant magazine with an open horizontal cooperation about culture, science and society": "Независимый журнал с открытой горизонтальной редакцией о культуре, науке и обществе", "Introduce": "Представление", "Invalid email": "Проверьте правильность ввода почты", - "Invite co-authors": "Пригласить соавторов", + "Invalid image link": "Некорректная ссылка на изображение", "Invalid url format": "Неверный формат ссылки", + "Invite co-authors": "Пригласить соавторов", "Invite experts": "Пригласить экспертов", "Invite to collab": "Пригласить к участию", "It does not look like url": "Это не похоже на ссылку", @@ -122,6 +125,7 @@ "Loading": "Загрузка", "Logout": "Выход", "Manifest": "Манифест", + "Many files, choose only one": "Много файлов, выберете один", "More": "Ещё", "Most commented": "Комментируемое", "Most read": "Читаемое", @@ -135,6 +139,7 @@ "Nothing here yet": "Здесь пока ничего нет", "Nothing is here": "Здесь ничего нет", "Or continue with social network": "Или продолжите через соцсеть", + "Or paste a link to an image": "Или вставьте ссылку на изображение", "Our regular contributor": "Наш постоянный автор", "Paragraphs": "Абзацев", "Participating": "Участвовать", @@ -159,8 +164,8 @@ "Profile": "Профиль", "Profile settings": "Настройки профиля", "Profile successfully saved": "Профиль успешно сохранён", - "Publications": "Публикации", "Publication settings": "Настройки публикации", + "Publications": "Публикации", "Publish": "Опубликовать", "Quit": "Выйти", "Quotes": "Цитаты", @@ -223,6 +228,7 @@ "Try to find another way": "Попробуйте найти по-другому", "Unfollow": "Отписаться", "Unfollow the topic": "Отписаться от темы", + "Upload": "Загрузить", "Username": "Имя пользователя", "Userpic": "Аватар", "Video": "Видео", diff --git a/src/components/Editor/Editor.tsx b/src/components/Editor/Editor.tsx index 0bd66635..926a9955 100644 --- a/src/components/Editor/Editor.tsx +++ b/src/components/Editor/Editor.tsx @@ -26,10 +26,7 @@ import { Image } from '@tiptap/extension-image' import { Paragraph } from '@tiptap/extension-paragraph' import Focus from '@tiptap/extension-focus' import { TrailingNode } from './extensions/TrailingNode' -import { EditorBubbleMenu } from './EditorBubbleMenu/EditorBubbleMenu' -import { EditorFloatingMenu } from './EditorFloatingMenu' import * as Y from 'yjs' -// import { WebrtcProvider } from 'y-webrtc' import { CollaborationCursor } from '@tiptap/extension-collaboration-cursor' import { Collaboration } from '@tiptap/extension-collaboration' import './Prosemirror.scss' @@ -38,10 +35,12 @@ import { useSession } from '../../context/session' import uniqolor from 'uniqolor' import { HocuspocusProvider } from '@hocuspocus/provider' import { Embed } from './extensions/embed' +import { EditorBubbleMenu } from './EditorBubbleMenu' +import { EditorFloatingMenu } from './EditorFloatingMenu' import { useEditorContext } from '../../context/editor' type EditorProps = { - shoutSlug: string + shoutId: number initialContent?: string onChange: (text: string) => void } @@ -54,7 +53,7 @@ export const Editor = (props: EditorProps) => { const { t } = useLocalize() const { user } = useSession() - const docName = `shout-${props.shoutSlug}` + const docName = `shout-${props.shoutId}` if (!providers[docName]) { providers[docName] = new HocuspocusProvider({ @@ -89,8 +88,6 @@ export const Editor = (props: EditorProps) => { const editor = createTiptapEditor(() => ({ element: editorElRef.current, - content: props.initialContent, - //onTransaction: handleEditorTransaction, extensions: [ Document, Text, @@ -111,7 +108,6 @@ export const Editor = (props: EditorProps) => { BulletList, OrderedList, ListItem, - CharacterCount, Collaboration.configure({ document: yDoc }), @@ -129,9 +125,15 @@ export const Editor = (props: EditorProps) => { Gapcursor, HardBreak, Highlight, - Image, + Image.configure({ + HTMLAttributes: { + class: 'uploadedImage' + } + }), + TrailingNode, Embed, TrailingNode, + CharacterCount, BubbleMenu.configure({ element: bubbleMenuRef.current }), diff --git a/src/components/Editor/EditorBubbleMenu/EditorBubbleMenu.tsx b/src/components/Editor/EditorBubbleMenu/EditorBubbleMenu.tsx index 9d1dc83b..5a8aad2e 100644 --- a/src/components/Editor/EditorBubbleMenu/EditorBubbleMenu.tsx +++ b/src/components/Editor/EditorBubbleMenu/EditorBubbleMenu.tsx @@ -6,7 +6,7 @@ import { clsx } from 'clsx' import { createEditorTransaction } from 'solid-tiptap' import { useLocalize } from '../../../context/localize' import { InlineForm } from '../InlineForm' -import validateUrl from '../../../utils/validateUrl' +import validateImage from '../../../utils/validateUrl' type BubbleMenuProps = { editor: Editor @@ -80,12 +80,13 @@ export const EditorBubbleMenu = (props: BubbleMenuProps) => { (validateUrl(value) ? '' : t('Invalid url format'))} + validate={(value) => (validateImage(value) ? '' : t('Invalid url format'))} onSubmit={handleLinkFormSubmit} onClose={() => setLinkEditorOpen(false)} + errorMessage={t('Error')} /> diff --git a/src/components/Editor/EditorBubbleMenu/index.ts b/src/components/Editor/EditorBubbleMenu/index.ts new file mode 100644 index 00000000..c4d82a89 --- /dev/null +++ b/src/components/Editor/EditorBubbleMenu/index.ts @@ -0,0 +1 @@ +export { EditorBubbleMenu } from './EditorBubbleMenu' diff --git a/src/components/Editor/EditorFloatingMenu.tsx b/src/components/Editor/EditorFloatingMenu.tsx deleted file mode 100644 index 67e39c43..00000000 --- a/src/components/Editor/EditorFloatingMenu.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { createSignal, Show } from 'solid-js' -import type { Editor } from '@tiptap/core' -import { Icon } from '../_shared/Icon' -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 (typeof result === 'string') { - return - } - - if (result && 'type' in result && result.type === 'iframe') { - return result.attributes - } -} - -const validateEmbed = async (value: string): Promise => { - const iframeData = await HTMLParser(value, false) - - if (typeof iframeData === 'string') { - return - } - - if (iframeData && iframeData.type !== 'iframe') { - return - } -} - -export const EditorFloatingMenu = (props: FloatingMenuProps) => { - const [inlineEditorOpen, setInlineEditorOpen] = createSignal(false) - - const handleEmbedFormSubmit = async (value: string) => { - // TODO: add support instagram embed (blockquote) - const { src } = (await embedData(value)) as { src: string } - props.editor.chain().focus().setIframe({ src }).run() - } - - return ( -
- - - setInlineEditorOpen(false)} - validate={validateEmbed} - onSubmit={handleEmbedFormSubmit} - /> - -
- ) -} diff --git a/src/components/Editor/EditorFloatingMenu.module.scss b/src/components/Editor/EditorFloatingMenu/EditorFloatingMenu.module.scss similarity index 56% rename from src/components/Editor/EditorFloatingMenu.module.scss rename to src/components/Editor/EditorFloatingMenu/EditorFloatingMenu.module.scss index 6df8d7ed..e6078156 100644 --- a/src/components/Editor/EditorFloatingMenu.module.scss +++ b/src/components/Editor/EditorFloatingMenu/EditorFloatingMenu.module.scss @@ -12,4 +12,12 @@ opacity: 1; } } + .menuHolder { + background: #fff; + left: calc(100% + 1rem); + box-shadow: 0 4px 10px rgba(#000, 0.25); + position: absolute; + top: -0.8rem; + min-width: 64vw; + } } diff --git a/src/components/Editor/EditorFloatingMenu/EditorFloatingMenu.tsx b/src/components/Editor/EditorFloatingMenu/EditorFloatingMenu.tsx new file mode 100644 index 00000000..cd903404 --- /dev/null +++ b/src/components/Editor/EditorFloatingMenu/EditorFloatingMenu.tsx @@ -0,0 +1,89 @@ +import { createEffect, createSignal, Show } from 'solid-js' +import type { Editor, JSONContent } from '@tiptap/core' +import { Icon } from '../../_shared/Icon' +import { InlineForm } from '../InlineForm' +import styles from './EditorFloatingMenu.module.scss' +import HTMLParser from 'html-to-json-parser' +import { useLocalize } from '../../../context/localize' +import { Modal } from '../../Nav/Modal' +import { Menu } from './Menu' +import { showModal } from '../../../stores/ui' +import { UploadModalContent } from '../UploadModal' + +type FloatingMenuProps = { + editor: Editor + ref: (el: HTMLDivElement) => void +} + +const embedData = async (data) => { + const result = (await HTMLParser(data, false)) as JSONContent + if ('type' in result && result.type === 'iframe') { + return result.attributes + } +} + +const validateEmbed = async (value) => { + const iframeData = (await HTMLParser(value, false)) as JSONContent + if (iframeData.type !== 'iframe') { + return + } +} + +export const EditorFloatingMenu = (props: FloatingMenuProps) => { + const { t } = useLocalize() + const [selectedMenuItem, setSelectedMenuItem] = createSignal(null) + const [menuOpen, setMenuOpen] = 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() + } + + createEffect(() => { + if (selectedMenuItem() === 'image') { + showModal('uploadImage') + } + }) + const closeUploadModalHandler = () => { + setSelectedMenuItem(null) + setMenuOpen(false) + } + + return ( + <> +
+ + +
+ + setSelectedMenuItem(value)} /> + + + setSelectedMenuItem(null)} + validate={validateEmbed} + onSubmit={handleEmbedFormSubmit} + errorMessage={t('Error')} + /> + +
+
+
+ + + + + ) +} diff --git a/src/components/Editor/EditorFloatingMenu/Menu/Menu.module.scss b/src/components/Editor/EditorFloatingMenu/Menu/Menu.module.scss new file mode 100644 index 00000000..b9c4d52e --- /dev/null +++ b/src/components/Editor/EditorFloatingMenu/Menu/Menu.module.scss @@ -0,0 +1,13 @@ +.Menu { + display: flex; + flex-direction: row; + .icon { + opacity: 0.5; + display: block; + transition: opacity 0.3s ease-in-out; + + &:hover { + opacity: 1; + } + } +} diff --git a/src/components/Editor/EditorFloatingMenu/Menu/Menu.tsx b/src/components/Editor/EditorFloatingMenu/Menu/Menu.tsx new file mode 100644 index 00000000..257ea729 --- /dev/null +++ b/src/components/Editor/EditorFloatingMenu/Menu/Menu.tsx @@ -0,0 +1,19 @@ +import styles from './Menu.module.scss' +import { Icon } from '../../../_shared/Icon' + +type Props = { + selectedItem: (value: string) => void +} + +export const Menu = (props: Props) => { + return ( +
+ + +
+ ) +} diff --git a/src/components/Editor/EditorFloatingMenu/Menu/index.ts b/src/components/Editor/EditorFloatingMenu/Menu/index.ts new file mode 100644 index 00000000..8a8c93b5 --- /dev/null +++ b/src/components/Editor/EditorFloatingMenu/Menu/index.ts @@ -0,0 +1 @@ +export { Menu } from './Menu' diff --git a/src/components/Editor/EditorFloatingMenu/index.ts b/src/components/Editor/EditorFloatingMenu/index.ts new file mode 100644 index 00000000..6af80faa --- /dev/null +++ b/src/components/Editor/EditorFloatingMenu/index.ts @@ -0,0 +1 @@ +export { EditorFloatingMenu } from './EditorFloatingMenu' diff --git a/src/components/Editor/InlineForm/InlineForm.module.scss b/src/components/Editor/InlineForm/InlineForm.module.scss index b49506a4..33109c7a 100644 --- a/src/components/Editor/InlineForm/InlineForm.module.scss +++ b/src/components/Editor/InlineForm/InlineForm.module.scss @@ -1,32 +1,13 @@ .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; - } - } - } + width: 100%; .form { display: flex; - flex-flow: row nowrap; + flex-direction: row; + flex-wrap: nowrap; padding: 6px 11px; + width: 100%; input { margin: 0 12px 0 0; @@ -56,7 +37,8 @@ right: 0; height: 0; background: #fff; - box-shadow: 0 4px 10px rgba(#000, 0.25); + border: 1px solid #e9e9ee; + border-radius: 2px; opacity: 0; transition: height 0.3s ease-in-out, opacity 0.3s ease-in-out; diff --git a/src/components/Editor/InlineForm/InlineForm.tsx b/src/components/Editor/InlineForm/InlineForm.tsx index 8a244e58..3a5ea88d 100644 --- a/src/components/Editor/InlineForm/InlineForm.tsx +++ b/src/components/Editor/InlineForm/InlineForm.tsx @@ -1,35 +1,39 @@ import styles from './InlineForm.module.scss' import { Icon } from '../../_shared/Icon' -import { createSignal, Show } from 'solid-js' -import { useLocalize } from '../../../context/localize' +import { createSignal } from 'solid-js' import { clsx } from 'clsx' type Props = { onClose: () => void onClear?: () => void onSubmit: (value: string) => void - variant: 'inBubble' | 'inFloating' - validate?: (value: string) => string | Promise + validate?: (value: string) => string | Promise | Promise | Promise initialValue?: string + showInput?: boolean + placeholder: string + errorMessage: string + autoFocus?: boolean } export const InlineForm = (props: Props) => { - const { t } = useLocalize() const [formValue, setFormValue] = createSignal(props.initialValue || '') - const [formValueError, setFormValueError] = createSignal('') + const [formValueError, setFormValueError] = createSignal() const handleFormInput = (value) => { + setFormValueError() setFormValue(value) } const handleSaveButtonClick = async () => { - const errorMessage = await props.validate(formValue()) - if (errorMessage) { - setFormValueError(errorMessage) - return + if (props.validate) { + const checkValid = await props.validate(formValue()) + if (checkValid) { + props.onSubmit(formValue()) + props.onClose() + } else { + setFormValueError(props.errorMessage) + } } - props.onSubmit(formValue()) - props.onClose() } const handleKeyPress = async (event) => { @@ -46,33 +50,16 @@ export const InlineForm = (props: Props) => { } return ( -
+
- - handleKeyPress(e)} - onInput={(e) => handleFormInput(e.currentTarget.value)} - /> - - - handleKeyPress(e)} - onInput={(e) => handleFormInput(e.currentTarget.value)} - /> - -
+ ) +} diff --git a/src/components/Editor/UploadModal/index.ts b/src/components/Editor/UploadModal/index.ts new file mode 100644 index 00000000..9e38ddda --- /dev/null +++ b/src/components/Editor/UploadModal/index.ts @@ -0,0 +1 @@ +export { UploadModalContent } from './UploadModalContent' diff --git a/src/components/Nav/Modal.module.scss b/src/components/Nav/Modal/Modal.module.scss similarity index 100% rename from src/components/Nav/Modal.module.scss rename to src/components/Nav/Modal/Modal.module.scss diff --git a/src/components/Nav/Modal.tsx b/src/components/Nav/Modal/Modal.tsx similarity index 76% rename from src/components/Nav/Modal.tsx rename to src/components/Nav/Modal/Modal.tsx index 9e28aed6..d07f6ed2 100644 --- a/src/components/Nav/Modal.tsx +++ b/src/components/Nav/Modal/Modal.tsx @@ -1,7 +1,7 @@ import { createEffect, createSignal, Show } from 'solid-js' import type { JSX } from 'solid-js' -import { hideModal, useModalStore } from '../../stores/ui' -import { useEscKeyDownHandler } from '../../utils/useEscKeyDownHandler' +import { hideModal, useModalStore } from '../../../stores/ui' +import { useEscKeyDownHandler } from '../../../utils/useEscKeyDownHandler' import { clsx } from 'clsx' import styles from './Modal.module.scss' @@ -9,16 +9,18 @@ interface ModalProps { name: string variant: 'narrow' | 'wide' children: JSX.Element + onClose?: () => void } export const Modal = (props: ModalProps) => { const { modal } = useModalStore() - const backdropClick = () => { + const handleHide = () => { hideModal() + props.onClose && props.onClose() } - useEscKeyDownHandler(() => hideModal()) + useEscKeyDownHandler(handleHide) const [visible, setVisible] = createSignal(false) @@ -28,7 +30,7 @@ export const Modal = (props: ModalProps) => { return ( -
+
{ onClick={(event) => event.stopPropagation()} > {props.children} -
+
{ return ( diff --git a/src/components/Nav/Modal/index.ts b/src/components/Nav/Modal/index.ts new file mode 100644 index 00000000..41e7f5e8 --- /dev/null +++ b/src/components/Nav/Modal/index.ts @@ -0,0 +1 @@ +export { Modal } from './Modal' diff --git a/src/components/Views/Edit.tsx b/src/components/Views/Edit.tsx index 22d8be81..bc0fb617 100644 --- a/src/components/Views/Edit.tsx +++ b/src/components/Views/Edit.tsx @@ -128,9 +128,8 @@ export const EditView = (props: EditViewProps) => { value={form.subtitle} onChange={(e) => setForm('subtitle', e.currentTarget.value)} /> - setForm('body', body)} /> diff --git a/src/components/_shared/Button/Button.module.scss b/src/components/_shared/Button/Button.module.scss index 70e0047f..e10965b5 100644 --- a/src/components/_shared/Button/Button.module.scss +++ b/src/components/_shared/Button/Button.module.scss @@ -2,6 +2,7 @@ border-radius: 2px; display: flex; align-items: center; + justify-content: center; font-weight: 500; cursor: pointer; @@ -44,7 +45,8 @@ } } - &.outline { + &.outline, + &.bordered { border: 3px solid #f2f2f2; border-radius: 1.2em; cursor: pointer; @@ -72,6 +74,13 @@ } } + &.bordered { + border-radius: 2px; + border: 2px solid #000; + font-size: 16px; + font-weight: 500; + } + &:disabled, &:disabled:hover { cursor: default; diff --git a/src/components/_shared/Button/Button.tsx b/src/components/_shared/Button/Button.tsx index b8711880..9980a2f3 100644 --- a/src/components/_shared/Button/Button.tsx +++ b/src/components/_shared/Button/Button.tsx @@ -5,7 +5,7 @@ import styles from './Button.module.scss' type Props = { value: string | JSX.Element size?: 'S' | 'M' | 'L' - variant?: 'primary' | 'secondary' | 'inline' | 'outline' + variant?: 'primary' | 'secondary' | 'bordered' | 'inline' | 'outline' type?: 'submit' | 'button' loading?: boolean disabled?: boolean diff --git a/src/components/_shared/Icon/Icon.module.scss b/src/components/_shared/Icon/Icon.module.scss index 64609e56..b5602d55 100644 --- a/src/components/_shared/Icon/Icon.module.scss +++ b/src/components/_shared/Icon/Icon.module.scss @@ -1,11 +1,11 @@ .icon { line-height: 1; position: relative; -} -img { - width: 100%; - height: 100%; + img { + width: 100%; + height: 100%; + } } .notificationsCounter { diff --git a/src/components/_shared/Loading.module.scss b/src/components/_shared/Loading.module.scss index 5783271c..de09f09b 100644 --- a/src/components/_shared/Loading.module.scss +++ b/src/components/_shared/Loading.module.scss @@ -2,7 +2,7 @@ position: absolute; top: 50%; left: 50%; - transform: translate(-50%, 0); + transform: translate(-50%, -50%); } @keyframes spin { diff --git a/src/pages/about/manifest.page.tsx b/src/pages/about/manifest.page.tsx index 6fa36ce6..fbdbe7b6 100644 --- a/src/pages/about/manifest.page.tsx +++ b/src/pages/about/manifest.page.tsx @@ -3,7 +3,7 @@ import { PageLayout } from '../../components/_shared/PageLayout' import { Modal } from '../../components/Nav/Modal' import { Feedback } from '../../components/Discours/Feedback' import Subscribe from '../../components/Discours/Subscribe' -import Opener from '../../components/Nav/Opener' +import Opener from '../../components/Nav/Modal/Opener' import { Icon } from '../../components/_shared/Icon' // title={t('Manifest')} diff --git a/src/pages/profile/profileSettings.page.tsx b/src/pages/profile/profileSettings.page.tsx index 84fcb7cb..3519c3ab 100644 --- a/src/pages/profile/profileSettings.page.tsx +++ b/src/pages/profile/profileSettings.page.tsx @@ -6,23 +6,14 @@ import { clsx } from 'clsx' import styles from './Settings.module.scss' import { useProfileForm } from '../../context/profile' import validateUrl from '../../utils/validateUrl' -import { createFileUploader, UploadFile } from '@solid-primitives/upload' +import { createFileUploader } from '@solid-primitives/upload' import { Loading } from '../../components/_shared/Loading' import { useSession } from '../../context/session' import { Button } from '../../components/_shared/Button' import { useSnackbar } from '../../context/snackbar' import { useLocalize } from '../../context/localize' import { Image } from '../../components/_shared/Image' - -const handleFileUpload = async (uploadFile: UploadFile) => { - const formData = new FormData() - formData.append('file', uploadFile.file, uploadFile.name) - const response = await fetch('/api/upload', { - method: 'POST', - body: formData - }) - return response.json() -} +import { handleFileUpload } from '../../utils/handleFileUpload' export const ProfileSettingsPage = () => { const { t } = useLocalize() diff --git a/src/stores/ui.ts b/src/stores/ui.ts index e187a726..fa97affa 100644 --- a/src/stores/ui.ts +++ b/src/stores/ui.ts @@ -3,7 +3,14 @@ import { useRouter } from './router' import type { AuthModalSearchParams, ConfirmEmailSearchParams } from '../components/Nav/AuthModal/types' import type { RootSearchParams } from '../pages/types' -export type ModalType = 'auth' | 'subscribe' | 'feedback' | 'thank' | 'donate' | 'inviteToChat' +export type ModalType = + | 'auth' + | 'subscribe' + | 'feedback' + | 'thank' + | 'donate' + | 'inviteToChat' + | 'uploadImage' type WarnKind = 'error' | 'warn' | 'info' export interface Warning { @@ -18,7 +25,8 @@ export const MODALS: Record = { feedback: 'feedback', thank: 'thank', donate: 'donate', - inviteToChat: 'inviteToChat' + inviteToChat: 'inviteToChat', + uploadImage: 'uploadImage' } const [modal, setModal] = createSignal(null) diff --git a/src/utils/handleFileUpload.ts b/src/utils/handleFileUpload.ts new file mode 100644 index 00000000..2094b5a7 --- /dev/null +++ b/src/utils/handleFileUpload.ts @@ -0,0 +1,13 @@ +import { UploadFile } from '@solid-primitives/upload' +import { isDev } from './config' + +const api = isDev ? 'https://new.discours.io/api/upload' : '/api/upload' +export const handleFileUpload = async (uploadFile: UploadFile) => { + const formData = new FormData() + formData.append('file', uploadFile.file, uploadFile.name) + const response = await fetch(api, { + method: 'POST', + body: formData + }) + return response.json() +} diff --git a/src/utils/verifyImg.ts b/src/utils/verifyImg.ts new file mode 100644 index 00000000..306ef949 --- /dev/null +++ b/src/utils/verifyImg.ts @@ -0,0 +1,10 @@ +export const verifyImg = (url: string) => { + return fetch(url, { method: 'HEAD' }).then((res) => { + return res.headers.get('Content-Type').startsWith('image') + }) +} + +const supportedExtensions = ['png', 'jpg', 'jpeg', 'gif', 'tiff', 'bpg'] +export const isImageExtension = (value: string) => { + return supportedExtensions.some((extension) => value.includes(extension)) +}