Add image by URL, Upload (choose file by button and D&D)
This commit is contained in:
parent
821fb428de
commit
08cc22b93c
69
package-lock.json
generated
69
package-lock.json
generated
|
@ -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",
|
||||
|
|
5
public/icons/editor-embed.svg
Normal file
5
public/icons/editor-embed.svg
Normal file
|
@ -0,0 +1,5 @@
|
|||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.6642 15.2662L10.5312 19.4724C10.2967 19.7094 10.2967 20.0648 10.5312 20.3018L14.6642 24.4784C14.8987 24.7153 14.8987 25.0708 14.6642 25.3078L13.8435 26.1372C13.609 26.3741 13.2572 26.3741 13.0227 26.1372L7.21884 20.3018C6.98434 20.0648 6.98434 19.7094 7.21884 19.4724L13.0227 13.6074C13.2572 13.3704 13.609 13.3704 13.8435 13.6074L14.6642 14.4368C14.8987 14.6738 14.8987 15.0589 14.6642 15.2662Z" fill="currentColor"/>
|
||||
<path d="M25.3946 24.508L29.5277 20.3314C29.7622 20.0944 29.7622 19.739 29.5277 19.502L25.3946 15.2662C25.1601 15.0292 25.1601 14.6738 25.3946 14.4368L26.2154 13.6074C26.4499 13.3704 26.8016 13.3704 27.0361 13.6074L32.84 19.4724C33.0745 19.7094 33.0745 20.0648 32.84 20.3018L27.0361 26.1668C26.8016 26.4037 26.4499 26.4037 26.2154 26.1668L25.3946 25.3374C25.1601 25.1004 25.1601 24.7153 25.3946 24.508Z" fill="currentColor"/>
|
||||
<path d="M21.9333 11.8003L23.0472 12.1558C23.3696 12.2446 23.5162 12.6001 23.4282 12.8963L18.7675 27.5884C18.6796 27.9142 18.3278 28.0623 18.0347 27.9734L16.9208 27.5884C16.5984 27.4995 16.4518 27.144 16.5398 26.8478L21.2005 12.1854C21.3177 11.8892 21.6402 11.7114 21.9333 11.8003Z" fill="currentColor"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
3
public/icons/editor-image-dd.svg
Normal file
3
public/icons/editor-image-dd.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path opacity="0.3" d="M64 56.8889V7.11111C64 3.2 60.8 0 56.8889 0H7.11111C3.2 0 0 3.2 0 7.11111V56.8889C0 60.8 3.2 64 7.11111 64H56.8889C60.8 64 64 60.8 64 56.8889ZM19.5556 37.3333L28.4444 48.0356L40.8889 32L56.8889 53.3333H7.11111L19.5556 37.3333Z" fill="black"/>
|
||||
</svg>
|
After Width: | Height: | Size: 369 B |
3
public/icons/editor-image.svg
Normal file
3
public/icons/editor-image.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M31 28.5556V11.4444C31 10.1 29.9 9 28.5556 9H11.4444C10.1 9 9 10.1 9 11.4444V28.5556C9 29.9 10.1 31 11.4444 31H28.5556C29.9 31 31 29.9 31 28.5556ZM15.7222 21.8333L18.7778 25.5122L23.0556 20L28.5556 27.3333H11.4444L15.7222 21.8333Z" fill="currentColor"/>
|
||||
</svg>
|
After Width: | Height: | Size: 366 B |
|
@ -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",
|
||||
|
|
|
@ -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": "Видео",
|
||||
|
|
|
@ -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
|
||||
}),
|
||||
|
|
|
@ -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) => {
|
|||
<Switch>
|
||||
<Match when={linkEditorOpen()}>
|
||||
<InlineForm
|
||||
variant="inBubble"
|
||||
placeholder={t('Enter URL address')}
|
||||
initialValue={currentUrl() ?? ''}
|
||||
onClear={handleClearLinkForm}
|
||||
validate={(value) => (validateUrl(value) ? '' : t('Invalid url format'))}
|
||||
validate={(value) => (validateImage(value) ? '' : t('Invalid url format'))}
|
||||
onSubmit={handleLinkFormSubmit}
|
||||
onClose={() => setLinkEditorOpen(false)}
|
||||
errorMessage={t('Error')}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={!linkEditorOpen()}>
|
||||
|
|
1
src/components/Editor/EditorBubbleMenu/index.ts
Normal file
1
src/components/Editor/EditorBubbleMenu/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { EditorBubbleMenu } from './EditorBubbleMenu'
|
|
@ -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<string> => {
|
||||
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<boolean>(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 (
|
||||
<div ref={props.ref} class={styles.editorFloatingMenu}>
|
||||
<button type="button" onClick={() => setInlineEditorOpen(true)}>
|
||||
<Icon name="editor-plus" />
|
||||
</button>
|
||||
<Show when={inlineEditorOpen()}>
|
||||
<InlineForm
|
||||
variant="inFloating"
|
||||
onClose={() => setInlineEditorOpen(false)}
|
||||
validate={validateEmbed}
|
||||
onSubmit={handleEmbedFormSubmit}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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<string | null>(null)
|
||||
const [menuOpen, setMenuOpen] = createSignal<boolean>(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 (
|
||||
<>
|
||||
<div ref={props.ref} class={styles.editorFloatingMenu}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
console.log('!!! selectedMenuItem:', selectedMenuItem())
|
||||
|
||||
setMenuOpen(!menuOpen())
|
||||
}}
|
||||
>
|
||||
<Icon name="editor-plus" />
|
||||
</button>
|
||||
<Show when={menuOpen()}>
|
||||
<div class={styles.menuHolder}>
|
||||
<Show when={!selectedMenuItem()}>
|
||||
<Menu selectedItem={(value) => setSelectedMenuItem(value)} />
|
||||
</Show>
|
||||
<Show when={selectedMenuItem() === 'embed'}>
|
||||
<InlineForm
|
||||
placeholder={t('Paste Embed code')}
|
||||
showInput={true}
|
||||
onClose={closeUploadModalHandler}
|
||||
onClear={() => setSelectedMenuItem(null)}
|
||||
validate={validateEmbed}
|
||||
onSubmit={handleEmbedFormSubmit}
|
||||
errorMessage={t('Error')}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
<Modal variant="narrow" name="uploadImage" onClose={closeUploadModalHandler}>
|
||||
<UploadModalContent editor={props.editor} />
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
19
src/components/Editor/EditorFloatingMenu/Menu/Menu.tsx
Normal file
19
src/components/Editor/EditorFloatingMenu/Menu/Menu.tsx
Normal file
|
@ -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 (
|
||||
<div class={styles.Menu}>
|
||||
<button type="button" onClick={() => props.selectedItem('image')}>
|
||||
<Icon class={styles.icon} name="editor-image" />
|
||||
</button>
|
||||
<button type="button" onClick={() => props.selectedItem('embed')}>
|
||||
<Icon class={styles.icon} name="editor-embed" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
1
src/components/Editor/EditorFloatingMenu/Menu/index.ts
Normal file
1
src/components/Editor/EditorFloatingMenu/Menu/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { Menu } from './Menu'
|
1
src/components/Editor/EditorFloatingMenu/index.ts
Normal file
1
src/components/Editor/EditorFloatingMenu/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { EditorFloatingMenu } from './EditorFloatingMenu'
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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<string>
|
||||
validate?: (value: string) => string | Promise<string> | Promise<void> | Promise<boolean>
|
||||
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<string | undefined>()
|
||||
|
||||
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 (
|
||||
<div
|
||||
class={clsx(styles.InlineForm, {
|
||||
// [styles.inBubble]: props.variant === 'inBubble',
|
||||
[styles.inFloating]: props.variant === 'inFloating'
|
||||
})}
|
||||
>
|
||||
<div class={styles.InlineForm}>
|
||||
<div class={styles.form}>
|
||||
<Show when={props.variant === 'inBubble'}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t('Enter URL address')}
|
||||
autofocus
|
||||
value={props.initialValue}
|
||||
onKeyPress={(e) => handleKeyPress(e)}
|
||||
onInput={(e) => handleFormInput(e.currentTarget.value)}
|
||||
/>
|
||||
</Show>
|
||||
<Show when={props.variant === 'inFloating'}>
|
||||
<input
|
||||
autofocus
|
||||
type="text"
|
||||
placeholder={t('Paste Embed code')}
|
||||
onKeyPress={(e) => handleKeyPress(e)}
|
||||
onInput={(e) => handleFormInput(e.currentTarget.value)}
|
||||
/>
|
||||
</Show>
|
||||
<button type="button" onClick={handleSaveButtonClick} disabled={formValueError() !== ''}>
|
||||
<input
|
||||
autofocus={props.autoFocus ?? true}
|
||||
type="text"
|
||||
placeholder={props.placeholder}
|
||||
onKeyPress={(e) => handleKeyPress(e)}
|
||||
onInput={(e) => handleFormInput(e.currentTarget.value)}
|
||||
/>
|
||||
<button type="button" onClick={handleSaveButtonClick} disabled={Boolean(formValueError())}>
|
||||
<Icon name="status-done" />
|
||||
</button>
|
||||
<button type="button" onClick={props.onClear}>
|
||||
|
|
|
@ -9,11 +9,11 @@ import { useEscKeyDownHandler } from '../../../utils/useEscKeyDownHandler'
|
|||
import { getPagePath } from '@nanostores/router'
|
||||
import { router } from '../../../stores/router'
|
||||
|
||||
type PanelProps = {
|
||||
type Props = {
|
||||
shoutSlug: string
|
||||
}
|
||||
|
||||
export const Panel = (props: PanelProps) => {
|
||||
export const Panel = (props: Props) => {
|
||||
const { t } = useLocalize()
|
||||
const {
|
||||
isEditorPanelVisible,
|
||||
|
|
|
@ -61,3 +61,10 @@
|
|||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.uploadedImage {
|
||||
max-height: 80vh;
|
||||
margin: auto;
|
||||
display: block;
|
||||
width: unset !important;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
.uploadModalContent {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 33px;
|
||||
font-size: 18px;
|
||||
min-height: 335px;
|
||||
|
||||
.dropZone {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 24px;
|
||||
gap: 24px;
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.3s ease-in-out;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
background-color: #e9e9ee;
|
||||
overflow: hidden;
|
||||
|
||||
&.active {
|
||||
background-color: #e9e9ee;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
top: 0;
|
||||
transform: translateX(100%);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
animation: slide 1.8s infinite;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
rgba(#fff, 0) 0%,
|
||||
rgba(#fff, 0.8) 50%,
|
||||
rgba(128, 186, 232, 0) 99%,
|
||||
rgba(125, 185, 232, 0) 100%
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
&.error {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.input {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.uploadButton {
|
||||
margin: 24px 0;
|
||||
width: 100%;
|
||||
max-width: 233px;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.formHolder {
|
||||
width: 100%;
|
||||
margin-top: 24px;
|
||||
border-bottom: 1px solid #000;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slide {
|
||||
0% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
116
src/components/Editor/UploadModal/UploadModalContent.tsx
Normal file
116
src/components/Editor/UploadModal/UploadModalContent.tsx
Normal file
|
@ -0,0 +1,116 @@
|
|||
import styles from './UploadModalContent.module.scss'
|
||||
import { clsx } from 'clsx'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
import { Button } from '../../_shared/Button'
|
||||
import { createSignal, Show } from 'solid-js'
|
||||
import { InlineForm } from '../InlineForm'
|
||||
import { hideModal } from '../../../stores/ui'
|
||||
import { createDropzone, createFileUploader } from '@solid-primitives/upload'
|
||||
import { handleFileUpload } from '../../../utils/handleFileUpload'
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { Editor } from '@tiptap/core'
|
||||
import { Loading } from '../../_shared/Loading'
|
||||
import { verifyImg } from '../../../utils/verifyImg'
|
||||
|
||||
type Props = {
|
||||
editor: Editor
|
||||
}
|
||||
|
||||
export const UploadModalContent = (props: Props) => {
|
||||
const { t } = useLocalize()
|
||||
const [isUploading, setIsUploading] = createSignal(false)
|
||||
const [uploadError, setUploadError] = createSignal<string | undefined>()
|
||||
const [dragActive, setDragActive] = createSignal(false)
|
||||
const [dragError, setDragError] = createSignal<string | undefined>()
|
||||
|
||||
const renderImage = (src: string) => {
|
||||
props.editor.chain().focus().extendMarkRange('link').setImage({ src: src }).run()
|
||||
hideModal()
|
||||
}
|
||||
const handleImageFormSubmit = async (value: string) => {
|
||||
renderImage(value)
|
||||
}
|
||||
|
||||
const { selectFiles } = createFileUploader({ multiple: false, accept: 'image/*' })
|
||||
const runUpload = async (file) => {
|
||||
try {
|
||||
setIsUploading(true)
|
||||
const fileUrl = await handleFileUpload(file)
|
||||
setIsUploading(false)
|
||||
renderImage(fileUrl)
|
||||
} catch (error) {
|
||||
console.error('[upload image] error', error)
|
||||
setIsUploading(false)
|
||||
setUploadError(t('Error'))
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpload = async () => {
|
||||
await selectFiles(async ([uploadFile]) => {
|
||||
await runUpload(uploadFile)
|
||||
})
|
||||
}
|
||||
|
||||
const { setRef: dropzoneRef, files: droppedFiles } = createDropzone({
|
||||
onDrop: async () => {
|
||||
setDragActive(false)
|
||||
if (droppedFiles().length > 1) {
|
||||
setDragError(t('Many files, choose only one'))
|
||||
} else if (droppedFiles()[0].file.type.startsWith('image/')) {
|
||||
await runUpload(droppedFiles()[0])
|
||||
} else {
|
||||
setDragError(t('Image format not supported'))
|
||||
}
|
||||
}
|
||||
})
|
||||
const handleDrag = (event) => {
|
||||
if (event.type === 'dragenter' || event.type === 'dragover') {
|
||||
setDragActive(true)
|
||||
} else if (event.type === 'dragleave') {
|
||||
setDragActive(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div class={styles.uploadModalContent}>
|
||||
<Show when={!isUploading()} fallback={<Loading />}>
|
||||
<>
|
||||
<div
|
||||
onDragEnter={handleDrag}
|
||||
onDragLeave={handleDrag}
|
||||
onDragOver={handleDrag}
|
||||
ref={dropzoneRef}
|
||||
class={clsx(styles.dropZone, { [styles.active]: dragActive() })}
|
||||
>
|
||||
<Icon class={styles.icon} name="editor-image-dd" />
|
||||
<div class={clsx(styles.text, { [styles.error]: dragError() })}>
|
||||
{dragError() ?? t('Drag the image to this area')}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
value={t('Upload')}
|
||||
variant="bordered"
|
||||
onClick={handleUpload}
|
||||
class={styles.uploadButton}
|
||||
/>
|
||||
<Show when={uploadError()}>
|
||||
<div class={styles.error}>{uploadError()}</div>
|
||||
</Show>
|
||||
<div class={styles.formHolder}>
|
||||
<InlineForm
|
||||
autoFocus={false}
|
||||
placeholder={t('Or paste a link to an image')}
|
||||
showInput={true}
|
||||
onClose={() => {
|
||||
hideModal()
|
||||
}}
|
||||
validate={(value) => verifyImg(value)}
|
||||
onSubmit={handleImageFormSubmit}
|
||||
errorMessage={t('Invalid image link')}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
1
src/components/Editor/UploadModal/index.ts
Normal file
1
src/components/Editor/UploadModal/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { UploadModalContent } from './UploadModalContent'
|
|
@ -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 (
|
||||
<Show when={visible()}>
|
||||
<div class={styles.backdrop} onClick={backdropClick}>
|
||||
<div class={styles.backdrop} onClick={handleHide}>
|
||||
<div
|
||||
class={clsx(styles.modal, {
|
||||
[styles.narrow]: props.variant === 'narrow'
|
||||
|
@ -36,7 +38,7 @@ export const Modal = (props: ModalProps) => {
|
|||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
{props.children}
|
||||
<div class={styles.close} onClick={hideModal}>
|
||||
<div class={styles.close} onClick={handleHide}>
|
||||
<svg width="16" height="18" viewBox="0 0 16 18" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M7.99987 7.52552L14.1871 0.92334L15.9548 2.80968L9.76764 9.41185L15.9548 16.014L14.1871 17.9004L7.99987 11.2982L1.81269 17.9004L0.0449219 16.014L6.23211 9.41185L0.0449225 2.80968L1.81269 0.92334L7.99987 7.52552Z"
|
|
@ -1,6 +1,6 @@
|
|||
import type { JSX } from 'solid-js/jsx-runtime'
|
||||
import type { ModalType } from '../../stores/ui'
|
||||
import { showModal } from '../../stores/ui'
|
||||
import type { ModalType } from '../../../stores/ui'
|
||||
import { showModal } from '../../../stores/ui'
|
||||
|
||||
export default (props: { name: ModalType; children: JSX.Element }) => {
|
||||
return (
|
1
src/components/Nav/Modal/index.ts
Normal file
1
src/components/Nav/Modal/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { Modal } from './Modal'
|
|
@ -128,9 +128,8 @@ export const EditView = (props: EditViewProps) => {
|
|||
value={form.subtitle}
|
||||
onChange={(e) => setForm('subtitle', e.currentTarget.value)}
|
||||
/>
|
||||
|
||||
<Editor
|
||||
shoutSlug={props.shout.slug}
|
||||
shoutId={props.shout.id}
|
||||
initialContent={props.shout.body}
|
||||
onChange={(body) => setForm('body', body)}
|
||||
/>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
.icon {
|
||||
line-height: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.notificationsCounter {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
|
|
|
@ -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')}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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<ModalType, ModalType> = {
|
|||
feedback: 'feedback',
|
||||
thank: 'thank',
|
||||
donate: 'donate',
|
||||
inviteToChat: 'inviteToChat'
|
||||
inviteToChat: 'inviteToChat',
|
||||
uploadImage: 'uploadImage'
|
||||
}
|
||||
|
||||
const [modal, setModal] = createSignal<ModalType | null>(null)
|
||||
|
|
13
src/utils/handleFileUpload.ts
Normal file
13
src/utils/handleFileUpload.ts
Normal file
|
@ -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()
|
||||
}
|
10
src/utils/verifyImg.ts
Normal file
10
src/utils/verifyImg.ts
Normal file
|
@ -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))
|
||||
}
|
Loading…
Reference in New Issue
Block a user