Add image by URL, Upload (choose file by button and D&D)

This commit is contained in:
ilia tapazukk 2023-05-04 04:43:52 +00:00
parent 821fb428de
commit 08cc22b93c
37 changed files with 548 additions and 181 deletions

69
package-lock.json generated
View File

@ -5723,6 +5723,18 @@
"solid-js": ">=1.4.0" "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": { "node_modules/@thisbeyond/solid-select": {
"version": "0.13.0", "version": "0.13.0",
"resolved": "https://registry.npmjs.org/@thisbeyond/solid-select/-/solid-select-0.13.0.tgz", "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", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" "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": { "node_modules/auto-bind": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-4.0.0.tgz", "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-4.0.0.tgz",
@ -8376,8 +8396,7 @@
"node_modules/csstype": { "node_modules/csstype": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz",
"integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==", "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw=="
"dev": true
}, },
"node_modules/damerau-levenshtein": { "node_modules/damerau-levenshtein": {
"version": "1.0.8", "version": "1.0.8",
@ -10149,6 +10168,17 @@
"node": "^10.12.0 || >=12.0.0" "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": { "node_modules/filelist": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
@ -18080,7 +18110,6 @@
"version": "0.5.1", "version": "0.5.1",
"resolved": "https://registry.npmjs.org/seroval/-/seroval-0.5.1.tgz", "resolved": "https://registry.npmjs.org/seroval/-/seroval-0.5.1.tgz",
"integrity": "sha512-ZfhQVB59hmIauJG5Ydynupy8KHyr5imGNtdDhbZG68Ufh1Ynkv9KOYOAABf71oVbQxJ8VkWnMHAjEHE7fWkH5g==", "integrity": "sha512-ZfhQVB59hmIauJG5Ydynupy8KHyr5imGNtdDhbZG68Ufh1Ynkv9KOYOAABf71oVbQxJ8VkWnMHAjEHE7fWkH5g==",
"dev": true,
"engines": { "engines": {
"node": ">=10" "node": ">=10"
} }
@ -24782,6 +24811,15 @@
"dev": true, "dev": true,
"requires": {} "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": { "@thisbeyond/solid-select": {
"version": "0.13.0", "version": "0.13.0",
"resolved": "https://registry.npmjs.org/@thisbeyond/solid-select/-/solid-select-0.13.0.tgz", "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", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" "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": { "auto-bind": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-4.0.0.tgz", "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-4.0.0.tgz",
@ -26717,8 +26760,7 @@
"csstype": { "csstype": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz",
"integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==", "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw=="
"dev": true
}, },
"damerau-levenshtein": { "damerau-levenshtein": {
"version": "1.0.8", "version": "1.0.8",
@ -28031,6 +28073,14 @@
"flat-cache": "^3.0.4" "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": { "filelist": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
@ -33906,8 +33956,7 @@
"seroval": { "seroval": {
"version": "0.5.1", "version": "0.5.1",
"resolved": "https://registry.npmjs.org/seroval/-/seroval-0.5.1.tgz", "resolved": "https://registry.npmjs.org/seroval/-/seroval-0.5.1.tgz",
"integrity": "sha512-ZfhQVB59hmIauJG5Ydynupy8KHyr5imGNtdDhbZG68Ufh1Ynkv9KOYOAABf71oVbQxJ8VkWnMHAjEHE7fWkH5g==", "integrity": "sha512-ZfhQVB59hmIauJG5Ydynupy8KHyr5imGNtdDhbZG68Ufh1Ynkv9KOYOAABf71oVbQxJ8VkWnMHAjEHE7fWkH5g=="
"dev": true
}, },
"set-blocking": { "set-blocking": {
"version": "2.0.0", "version": "2.0.0",
@ -34043,9 +34092,9 @@
} }
}, },
"solid-js": { "solid-js": {
"version": "1.7.3", "version": "1.7.0",
"resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.7.3.tgz", "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.7.0.tgz",
"integrity": "sha512-4hwaF/zV/xbNeBBIYDyu3dcReOZBECbO//mrra6GqOrKy4Soyo+fnKjpZSa0nODm6j1aL0iQRh/7ofYowH+jzw==", "integrity": "sha512-tLG68KWlVRgzYeAW003G3E70emZqTcqCKJR9QoGr0rcuiLIuKrlUoezT8jLME1YSl3Wfu35jzgeY10iLEY4YQQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"csstype": "^3.1.0", "csstype": "^3.1.0",

View 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

View 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

View 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

View File

@ -53,6 +53,7 @@
"Discussion rules": "Discussion rules", "Discussion rules": "Discussion rules",
"Dogma": "Dogma", "Dogma": "Dogma",
"Drafts": "Drafts", "Drafts": "Drafts",
"Drag the image to this area": "Drag the image to this area",
"Edit": "Edit", "Edit": "Edit",
"Editing": "Editing", "Editing": "Editing",
"Email": "Mail", "Email": "Mail",
@ -94,9 +95,11 @@
"I have an account": "I have an account!", "I have an account": "I have an account!",
"I have no account yet": "I don't have an account yet", "I have no account yet": "I don't have an account yet",
"I know the password": "I know the password", "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", "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", "Introduce": "Introduction",
"Invalid email": "Check if your email is correct", "Invalid email": "Check if your email is correct",
"Invalid image link": "Invalid image link",
"Invalid url format": "Invalid url format", "Invalid url format": "Invalid url format",
"Invite co-authors": "Invite co-authors", "Invite co-authors": "Invite co-authors",
"Invite to collab": "Invite to Collab", "Invite to collab": "Invite to Collab",
@ -115,6 +118,7 @@
"Loading": "Loading", "Loading": "Loading",
"Logout": "Logout", "Logout": "Logout",
"Manifest": "Manifest", "Manifest": "Manifest",
"Many files, choose only one": "Many files, choose only one",
"More": "More", "More": "More",
"Most commented": "Commented", "Most commented": "Commented",
"Most read": "Readable", "Most read": "Readable",
@ -128,6 +132,7 @@
"Nothing here yet": "There's nothing here yet", "Nothing here yet": "There's nothing here yet",
"Nothing is here": "There is nothing here", "Nothing is here": "There is nothing here",
"Or continue with social network": "Or continue with social network", "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", "Our regular contributor": "Our regular contributor",
"Paragraphs": "Абзацев", "Paragraphs": "Абзацев",
"Participating": "Participating", "Participating": "Participating",
@ -210,6 +215,7 @@
"Try to find another way": "Try to find another way", "Try to find another way": "Try to find another way",
"Unfollow": "Unfollow", "Unfollow": "Unfollow",
"Unfollow the topic": "Unfollow the topic", "Unfollow the topic": "Unfollow the topic",
"Upload": "Upload",
"Username": "Username", "Username": "Username",
"Userpic": "Userpic", "Userpic": "Userpic",
"Video": "Video", "Video": "Video",

View File

@ -55,6 +55,7 @@
"Discussion rules": "Правила сообществ самиздата в&nbsp;соцсетях", "Discussion rules": "Правила сообществ самиздата в&nbsp;соцсетях",
"Dogma": "Догма", "Dogma": "Догма",
"Drafts": "Черновики", "Drafts": "Черновики",
"Drag the image to this area": "Перетащите изображение в эту область",
"Edit": "Редактировать", "Edit": "Редактировать",
"Edited": "Отредактирован", "Edited": "Отредактирован",
"Editing": "Редактирование", "Editing": "Редактирование",
@ -67,8 +68,8 @@
"Enter your new password": "Введите новый пароль", "Enter your new password": "Введите новый пароль",
"Error": "Ошибка", "Error": "Ошибка",
"Everything is ok, please give us your email address": "Ничего страшного, просто укажите свою почту, чтобы получить ссылку для сброса пароля.", "Everything is ok, please give us your email address": "Ничего страшного, просто укажите свою почту, чтобы получить ссылку для сброса пароля.",
"Favorite": "Избранное",
"FAQ": "Советы и предложения", "FAQ": "Советы и предложения",
"Favorite": "Избранное",
"Favorite topics": "Избранные темы", "Favorite topics": "Избранные темы",
"Feed settings": "Настройки ленты", "Feed settings": "Настройки ленты",
"Feedback": "Обратная связь", "Feedback": "Обратная связь",
@ -99,11 +100,13 @@
"I have an account": "У меня есть аккаунт!", "I have an account": "У меня есть аккаунт!",
"I have no account yet": "У меня еще нет аккаунта", "I have no account yet": "У меня еще нет аккаунта",
"I know the password": "Я знаю пароль", "I know the password": "Я знаю пароль",
"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": "Представление", "Introduce": "Представление",
"Invalid email": "Проверьте правильность ввода почты", "Invalid email": "Проверьте правильность ввода почты",
"Invite co-authors": "Пригласить соавторов", "Invalid image link": "Некорректная ссылка на изображение",
"Invalid url format": "Неверный формат ссылки", "Invalid url format": "Неверный формат ссылки",
"Invite co-authors": "Пригласить соавторов",
"Invite experts": "Пригласить экспертов", "Invite experts": "Пригласить экспертов",
"Invite to collab": "Пригласить к участию", "Invite to collab": "Пригласить к участию",
"It does not look like url": "Это не похоже на ссылку", "It does not look like url": "Это не похоже на ссылку",
@ -122,6 +125,7 @@
"Loading": "Загрузка", "Loading": "Загрузка",
"Logout": "Выход", "Logout": "Выход",
"Manifest": "Манифест", "Manifest": "Манифест",
"Many files, choose only one": "Много файлов, выберете один",
"More": "Ещё", "More": "Ещё",
"Most commented": "Комментируемое", "Most commented": "Комментируемое",
"Most read": "Читаемое", "Most read": "Читаемое",
@ -135,6 +139,7 @@
"Nothing here yet": "Здесь пока ничего нет", "Nothing here yet": "Здесь пока ничего нет",
"Nothing is here": "Здесь ничего нет", "Nothing is here": "Здесь ничего нет",
"Or continue with social network": "Или продолжите через соцсеть", "Or continue with social network": "Или продолжите через соцсеть",
"Or paste a link to an image": "Или вставьте ссылку на изображение",
"Our regular contributor": "Наш постоянный автор", "Our regular contributor": "Наш постоянный автор",
"Paragraphs": "Абзацев", "Paragraphs": "Абзацев",
"Participating": "Участвовать", "Participating": "Участвовать",
@ -159,8 +164,8 @@
"Profile": "Профиль", "Profile": "Профиль",
"Profile settings": "Настройки профиля", "Profile settings": "Настройки профиля",
"Profile successfully saved": "Профиль успешно сохранён", "Profile successfully saved": "Профиль успешно сохранён",
"Publications": "Публикации",
"Publication settings": "Настройки публикации", "Publication settings": "Настройки публикации",
"Publications": "Публикации",
"Publish": "Опубликовать", "Publish": "Опубликовать",
"Quit": "Выйти", "Quit": "Выйти",
"Quotes": "Цитаты", "Quotes": "Цитаты",
@ -223,6 +228,7 @@
"Try to find another way": "Попробуйте найти по-другому", "Try to find another way": "Попробуйте найти по-другому",
"Unfollow": "Отписаться", "Unfollow": "Отписаться",
"Unfollow the topic": "Отписаться от темы", "Unfollow the topic": "Отписаться от темы",
"Upload": "Загрузить",
"Username": "Имя пользователя", "Username": "Имя пользователя",
"Userpic": "Аватар", "Userpic": "Аватар",
"Video": "Видео", "Video": "Видео",

View File

@ -26,10 +26,7 @@ import { Image } from '@tiptap/extension-image'
import { Paragraph } from '@tiptap/extension-paragraph' import { Paragraph } from '@tiptap/extension-paragraph'
import Focus from '@tiptap/extension-focus' import Focus from '@tiptap/extension-focus'
import { TrailingNode } from './extensions/TrailingNode' import { TrailingNode } from './extensions/TrailingNode'
import { EditorBubbleMenu } from './EditorBubbleMenu/EditorBubbleMenu'
import { EditorFloatingMenu } from './EditorFloatingMenu'
import * as Y from 'yjs' import * as Y from 'yjs'
// import { WebrtcProvider } from 'y-webrtc'
import { CollaborationCursor } from '@tiptap/extension-collaboration-cursor' import { CollaborationCursor } from '@tiptap/extension-collaboration-cursor'
import { Collaboration } from '@tiptap/extension-collaboration' import { Collaboration } from '@tiptap/extension-collaboration'
import './Prosemirror.scss' import './Prosemirror.scss'
@ -38,10 +35,12 @@ import { useSession } from '../../context/session'
import uniqolor from 'uniqolor' import uniqolor from 'uniqolor'
import { HocuspocusProvider } from '@hocuspocus/provider' import { HocuspocusProvider } from '@hocuspocus/provider'
import { Embed } from './extensions/embed' import { Embed } from './extensions/embed'
import { EditorBubbleMenu } from './EditorBubbleMenu'
import { EditorFloatingMenu } from './EditorFloatingMenu'
import { useEditorContext } from '../../context/editor' import { useEditorContext } from '../../context/editor'
type EditorProps = { type EditorProps = {
shoutSlug: string shoutId: number
initialContent?: string initialContent?: string
onChange: (text: string) => void onChange: (text: string) => void
} }
@ -54,7 +53,7 @@ export const Editor = (props: EditorProps) => {
const { t } = useLocalize() const { t } = useLocalize()
const { user } = useSession() const { user } = useSession()
const docName = `shout-${props.shoutSlug}` const docName = `shout-${props.shoutId}`
if (!providers[docName]) { if (!providers[docName]) {
providers[docName] = new HocuspocusProvider({ providers[docName] = new HocuspocusProvider({
@ -89,8 +88,6 @@ export const Editor = (props: EditorProps) => {
const editor = createTiptapEditor(() => ({ const editor = createTiptapEditor(() => ({
element: editorElRef.current, element: editorElRef.current,
content: props.initialContent,
//onTransaction: handleEditorTransaction,
extensions: [ extensions: [
Document, Document,
Text, Text,
@ -111,7 +108,6 @@ export const Editor = (props: EditorProps) => {
BulletList, BulletList,
OrderedList, OrderedList,
ListItem, ListItem,
CharacterCount,
Collaboration.configure({ Collaboration.configure({
document: yDoc document: yDoc
}), }),
@ -129,9 +125,15 @@ export const Editor = (props: EditorProps) => {
Gapcursor, Gapcursor,
HardBreak, HardBreak,
Highlight, Highlight,
Image, Image.configure({
HTMLAttributes: {
class: 'uploadedImage'
}
}),
TrailingNode,
Embed, Embed,
TrailingNode, TrailingNode,
CharacterCount,
BubbleMenu.configure({ BubbleMenu.configure({
element: bubbleMenuRef.current element: bubbleMenuRef.current
}), }),

View File

@ -6,7 +6,7 @@ import { clsx } from 'clsx'
import { createEditorTransaction } from 'solid-tiptap' import { createEditorTransaction } from 'solid-tiptap'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { InlineForm } from '../InlineForm' import { InlineForm } from '../InlineForm'
import validateUrl from '../../../utils/validateUrl' import validateImage from '../../../utils/validateUrl'
type BubbleMenuProps = { type BubbleMenuProps = {
editor: Editor editor: Editor
@ -80,12 +80,13 @@ export const EditorBubbleMenu = (props: BubbleMenuProps) => {
<Switch> <Switch>
<Match when={linkEditorOpen()}> <Match when={linkEditorOpen()}>
<InlineForm <InlineForm
variant="inBubble" placeholder={t('Enter URL address')}
initialValue={currentUrl() ?? ''} initialValue={currentUrl() ?? ''}
onClear={handleClearLinkForm} onClear={handleClearLinkForm}
validate={(value) => (validateUrl(value) ? '' : t('Invalid url format'))} validate={(value) => (validateImage(value) ? '' : t('Invalid url format'))}
onSubmit={handleLinkFormSubmit} onSubmit={handleLinkFormSubmit}
onClose={() => setLinkEditorOpen(false)} onClose={() => setLinkEditorOpen(false)}
errorMessage={t('Error')}
/> />
</Match> </Match>
<Match when={!linkEditorOpen()}> <Match when={!linkEditorOpen()}>

View File

@ -0,0 +1 @@
export { EditorBubbleMenu } from './EditorBubbleMenu'

View File

@ -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>
)
}

View File

@ -12,4 +12,12 @@
opacity: 1; 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;
}
} }

View File

@ -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>
</>
)
}

View File

@ -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;
}
}
}

View 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>
)
}

View File

@ -0,0 +1 @@
export { Menu } from './Menu'

View File

@ -0,0 +1 @@
export { EditorFloatingMenu } from './EditorFloatingMenu'

View File

@ -1,32 +1,13 @@
.InlineForm { .InlineForm {
position: relative; position: relative;
width: 100%;
&.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 { .form {
display: flex; display: flex;
flex-flow: row nowrap; flex-direction: row;
flex-wrap: nowrap;
padding: 6px 11px; padding: 6px 11px;
width: 100%;
input { input {
margin: 0 12px 0 0; margin: 0 12px 0 0;
@ -56,7 +37,8 @@
right: 0; right: 0;
height: 0; height: 0;
background: #fff; background: #fff;
box-shadow: 0 4px 10px rgba(#000, 0.25); border: 1px solid #e9e9ee;
border-radius: 2px;
opacity: 0; opacity: 0;
transition: height 0.3s ease-in-out, opacity 0.3s ease-in-out; transition: height 0.3s ease-in-out, opacity 0.3s ease-in-out;

View File

@ -1,35 +1,39 @@
import styles from './InlineForm.module.scss' import styles from './InlineForm.module.scss'
import { Icon } from '../../_shared/Icon' import { Icon } from '../../_shared/Icon'
import { createSignal, Show } from 'solid-js' import { createSignal } from 'solid-js'
import { useLocalize } from '../../../context/localize'
import { clsx } from 'clsx' import { clsx } from 'clsx'
type Props = { type Props = {
onClose: () => void onClose: () => void
onClear?: () => void onClear?: () => void
onSubmit: (value: string) => void onSubmit: (value: string) => void
variant: 'inBubble' | 'inFloating' validate?: (value: string) => string | Promise<string> | Promise<void> | Promise<boolean>
validate?: (value: string) => string | Promise<string>
initialValue?: string initialValue?: string
showInput?: boolean
placeholder: string
errorMessage: string
autoFocus?: boolean
} }
export const InlineForm = (props: Props) => { export const InlineForm = (props: Props) => {
const { t } = useLocalize()
const [formValue, setFormValue] = createSignal(props.initialValue || '') const [formValue, setFormValue] = createSignal(props.initialValue || '')
const [formValueError, setFormValueError] = createSignal('') const [formValueError, setFormValueError] = createSignal<string | undefined>()
const handleFormInput = (value) => { const handleFormInput = (value) => {
setFormValueError()
setFormValue(value) setFormValue(value)
} }
const handleSaveButtonClick = async () => { const handleSaveButtonClick = async () => {
const errorMessage = await props.validate(formValue()) if (props.validate) {
if (errorMessage) { const checkValid = await props.validate(formValue())
setFormValueError(errorMessage) if (checkValid) {
return
}
props.onSubmit(formValue()) props.onSubmit(formValue())
props.onClose() props.onClose()
} else {
setFormValueError(props.errorMessage)
}
}
} }
const handleKeyPress = async (event) => { const handleKeyPress = async (event) => {
@ -46,33 +50,16 @@ export const InlineForm = (props: Props) => {
} }
return ( return (
<div <div class={styles.InlineForm}>
class={clsx(styles.InlineForm, {
// [styles.inBubble]: props.variant === 'inBubble',
[styles.inFloating]: props.variant === 'inFloating'
})}
>
<div class={styles.form}> <div class={styles.form}>
<Show when={props.variant === 'inBubble'}>
<input <input
autofocus={props.autoFocus ?? true}
type="text" type="text"
placeholder={t('Enter URL address')} placeholder={props.placeholder}
autofocus
value={props.initialValue}
onKeyPress={(e) => handleKeyPress(e)} onKeyPress={(e) => handleKeyPress(e)}
onInput={(e) => handleFormInput(e.currentTarget.value)} onInput={(e) => handleFormInput(e.currentTarget.value)}
/> />
</Show> <button type="button" onClick={handleSaveButtonClick} disabled={Boolean(formValueError())}>
<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() !== ''}>
<Icon name="status-done" /> <Icon name="status-done" />
</button> </button>
<button type="button" onClick={props.onClear}> <button type="button" onClick={props.onClear}>

View File

@ -9,11 +9,11 @@ import { useEscKeyDownHandler } from '../../../utils/useEscKeyDownHandler'
import { getPagePath } from '@nanostores/router' import { getPagePath } from '@nanostores/router'
import { router } from '../../../stores/router' import { router } from '../../../stores/router'
type PanelProps = { type Props = {
shoutSlug: string shoutSlug: string
} }
export const Panel = (props: PanelProps) => { export const Panel = (props: Props) => {
const { t } = useLocalize() const { t } = useLocalize()
const { const {
isEditorPanelVisible, isEditorPanelVisible,

View File

@ -61,3 +61,10 @@
overflow: hidden; overflow: hidden;
} }
} }
.uploadedImage {
max-height: 80vh;
margin: auto;
display: block;
width: unset !important;
}

View File

@ -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%);
}
}

View 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>
)
}

View File

@ -0,0 +1 @@
export { UploadModalContent } from './UploadModalContent'

View File

@ -1,7 +1,7 @@
import { createEffect, createSignal, Show } from 'solid-js' import { createEffect, createSignal, Show } from 'solid-js'
import type { JSX } from 'solid-js' import type { JSX } from 'solid-js'
import { hideModal, useModalStore } from '../../stores/ui' import { hideModal, useModalStore } from '../../../stores/ui'
import { useEscKeyDownHandler } from '../../utils/useEscKeyDownHandler' import { useEscKeyDownHandler } from '../../../utils/useEscKeyDownHandler'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import styles from './Modal.module.scss' import styles from './Modal.module.scss'
@ -9,16 +9,18 @@ interface ModalProps {
name: string name: string
variant: 'narrow' | 'wide' variant: 'narrow' | 'wide'
children: JSX.Element children: JSX.Element
onClose?: () => void
} }
export const Modal = (props: ModalProps) => { export const Modal = (props: ModalProps) => {
const { modal } = useModalStore() const { modal } = useModalStore()
const backdropClick = () => { const handleHide = () => {
hideModal() hideModal()
props.onClose && props.onClose()
} }
useEscKeyDownHandler(() => hideModal()) useEscKeyDownHandler(handleHide)
const [visible, setVisible] = createSignal(false) const [visible, setVisible] = createSignal(false)
@ -28,7 +30,7 @@ export const Modal = (props: ModalProps) => {
return ( return (
<Show when={visible()}> <Show when={visible()}>
<div class={styles.backdrop} onClick={backdropClick}> <div class={styles.backdrop} onClick={handleHide}>
<div <div
class={clsx(styles.modal, { class={clsx(styles.modal, {
[styles.narrow]: props.variant === 'narrow' [styles.narrow]: props.variant === 'narrow'
@ -36,7 +38,7 @@ export const Modal = (props: ModalProps) => {
onClick={(event) => event.stopPropagation()} onClick={(event) => event.stopPropagation()}
> >
{props.children} {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"> <svg width="16" height="18" viewBox="0 0 16 18" xmlns="http://www.w3.org/2000/svg">
<path <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" 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"

View File

@ -1,6 +1,6 @@
import type { JSX } from 'solid-js/jsx-runtime' import type { JSX } from 'solid-js/jsx-runtime'
import type { ModalType } from '../../stores/ui' import type { ModalType } from '../../../stores/ui'
import { showModal } from '../../stores/ui' import { showModal } from '../../../stores/ui'
export default (props: { name: ModalType; children: JSX.Element }) => { export default (props: { name: ModalType; children: JSX.Element }) => {
return ( return (

View File

@ -0,0 +1 @@
export { Modal } from './Modal'

View File

@ -128,9 +128,8 @@ export const EditView = (props: EditViewProps) => {
value={form.subtitle} value={form.subtitle}
onChange={(e) => setForm('subtitle', e.currentTarget.value)} onChange={(e) => setForm('subtitle', e.currentTarget.value)}
/> />
<Editor <Editor
shoutSlug={props.shout.slug} shoutId={props.shout.id}
initialContent={props.shout.body} initialContent={props.shout.body}
onChange={(body) => setForm('body', body)} onChange={(body) => setForm('body', body)}
/> />

View File

@ -2,6 +2,7 @@
border-radius: 2px; border-radius: 2px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center;
font-weight: 500; font-weight: 500;
cursor: pointer; cursor: pointer;
@ -44,7 +45,8 @@
} }
} }
&.outline { &.outline,
&.bordered {
border: 3px solid #f2f2f2; border: 3px solid #f2f2f2;
border-radius: 1.2em; border-radius: 1.2em;
cursor: pointer; cursor: pointer;
@ -72,6 +74,13 @@
} }
} }
&.bordered {
border-radius: 2px;
border: 2px solid #000;
font-size: 16px;
font-weight: 500;
}
&:disabled, &:disabled,
&:disabled:hover { &:disabled:hover {
cursor: default; cursor: default;

View File

@ -5,7 +5,7 @@ import styles from './Button.module.scss'
type Props = { type Props = {
value: string | JSX.Element value: string | JSX.Element
size?: 'S' | 'M' | 'L' size?: 'S' | 'M' | 'L'
variant?: 'primary' | 'secondary' | 'inline' | 'outline' variant?: 'primary' | 'secondary' | 'bordered' | 'inline' | 'outline'
type?: 'submit' | 'button' type?: 'submit' | 'button'
loading?: boolean loading?: boolean
disabled?: boolean disabled?: boolean

View File

@ -1,12 +1,12 @@
.icon { .icon {
line-height: 1; line-height: 1;
position: relative; position: relative;
}
img { img {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
}
.notificationsCounter { .notificationsCounter {
background-color: #d00820; background-color: #d00820;

View File

@ -2,7 +2,7 @@
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 50%; left: 50%;
transform: translate(-50%, 0); transform: translate(-50%, -50%);
} }
@keyframes spin { @keyframes spin {

View File

@ -3,7 +3,7 @@ import { PageLayout } from '../../components/_shared/PageLayout'
import { Modal } from '../../components/Nav/Modal' import { Modal } from '../../components/Nav/Modal'
import { Feedback } from '../../components/Discours/Feedback' import { Feedback } from '../../components/Discours/Feedback'
import Subscribe from '../../components/Discours/Subscribe' 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' import { Icon } from '../../components/_shared/Icon'
// title={t('Manifest')} // title={t('Manifest')}

View File

@ -6,23 +6,14 @@ import { clsx } from 'clsx'
import styles from './Settings.module.scss' import styles from './Settings.module.scss'
import { useProfileForm } from '../../context/profile' import { useProfileForm } from '../../context/profile'
import validateUrl from '../../utils/validateUrl' 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 { Loading } from '../../components/_shared/Loading'
import { useSession } from '../../context/session' import { useSession } from '../../context/session'
import { Button } from '../../components/_shared/Button' import { Button } from '../../components/_shared/Button'
import { useSnackbar } from '../../context/snackbar' import { useSnackbar } from '../../context/snackbar'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { Image } from '../../components/_shared/Image' import { Image } from '../../components/_shared/Image'
import { handleFileUpload } from '../../utils/handleFileUpload'
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()
}
export const ProfileSettingsPage = () => { export const ProfileSettingsPage = () => {
const { t } = useLocalize() const { t } = useLocalize()

View File

@ -3,7 +3,14 @@ import { useRouter } from './router'
import type { AuthModalSearchParams, ConfirmEmailSearchParams } from '../components/Nav/AuthModal/types' import type { AuthModalSearchParams, ConfirmEmailSearchParams } from '../components/Nav/AuthModal/types'
import type { RootSearchParams } from '../pages/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' type WarnKind = 'error' | 'warn' | 'info'
export interface Warning { export interface Warning {
@ -18,7 +25,8 @@ export const MODALS: Record<ModalType, ModalType> = {
feedback: 'feedback', feedback: 'feedback',
thank: 'thank', thank: 'thank',
donate: 'donate', donate: 'donate',
inviteToChat: 'inviteToChat' inviteToChat: 'inviteToChat',
uploadImage: 'uploadImage'
} }
const [modal, setModal] = createSignal<ModalType | null>(null) const [modal, setModal] = createSignal<ModalType | null>(null)

View 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
View 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))
}