Merge branch 'editor_settings/upload_cover_image' into 'dev'

Editor settings/upload cover image

See merge request discoursio/discoursio-webapp!74
This commit is contained in:
Igor 2023-05-09 13:07:53 +00:00
commit 2c252b8dce
11 changed files with 148 additions and 47 deletions

View File

@ -3,6 +3,8 @@
"About myself": "About myself",
"About the project": "About the project",
"Add comment": "Comment",
"Add image": "Add image",
"Add another image": "Add another image",
"Address on Discourse": "Address on Discourse",
"All": "All",
"All authors": "All authors",
@ -32,6 +34,7 @@
"By views": "By views",
"Characters": "Знаков",
"Chat Title": "Chat Title",
"Choose a title image for the article. You can immediately see how the publication card will look like.": "Choose a title image for the article. You can immediately see how the publication card will look like.",
"Choose who you want to write to": "Choose who you want to write to",
"Collaborate": "Help Edit",
"Comments": "Comments",
@ -119,6 +122,7 @@
"Logout": "Logout",
"Manifest": "Manifest",
"Many files, choose only one": "Many files, choose only one",
"Material card": "Material card",
"More": "More",
"Most commented": "Commented",
"Most read": "Readable",
@ -162,6 +166,7 @@
"Recent": "Fresh",
"Reply": "Reply",
"Report": "Complain",
"Required": "Required",
"Resend code": "Send confirmation",
"Restore password": "Restore password",
"Save draft": "Save draft",
@ -272,6 +277,6 @@
"user already exist": "user already exists",
"view": "view",
"zine": "zine",
"Required": "Required",
"Unnamed draft": "Unnamed draft"
"Unnamed draft": "Unnamed draft",
"Publish Settings": "Publish Settings"
}

View File

@ -4,6 +4,8 @@
"About myself": "О себе",
"About the project": "О проекте",
"Add comment": "Комментировать",
"Add image": "Добавить изображение",
"Add another image": "Добавить другое изображение",
"Add to bookmarks": "Добавить в закладки",
"Address on Discourse": "Адрес на Дискурсе",
"All": "Все",
@ -34,6 +36,7 @@
"By views": "По просмотрам",
"Characters": "Знаков",
"Chat Title": "Тема дискурса",
"Choose a title image for the article. You can immediately see how the publication card will look like.": "Выберите заглавное изображение для статьи. Тут же сразу можно увидеть как будет выглядеть карточка публикации.",
"Choose who you want to write to": "Выберите кому хотите написать",
"Collaborate": "Помочь редактировать",
"Comments": "Комментарии",
@ -126,6 +129,7 @@
"Logout": "Выход",
"Manifest": "Манифест",
"Many files, choose only one": "Много файлов, выберете один",
"Material card": "Карточка материала",
"More": "Ещё",
"Most commented": "Комментируемое",
"Most read": "Читаемое",
@ -173,11 +177,13 @@
"Recent": "Свежее",
"Reply": "Ответить",
"Report": "Пожаловаться",
"Required": "Поле обязательно для заполнения",
"Resend code": "Выслать подтверждение",
"Restore password": "Восстановить пароль",
"Save": "Сохранить",
"Save draft": "Сохранить черновик",
"Save settings": "Сохранить настройки",
"Scroll up": "Наверх",
"Search": "Поиск",
"Search author": "Поиск автора",
"Search topic": "Поиск темы",
@ -187,7 +193,6 @@
"Send": "Отправить",
"Send link again": "Прислать ссылку ещё раз",
"Settings": "Настройки",
"Scroll up": "Наверх",
"Share": "Поделиться",
"Short opening": "Небольшое вступление, чтобы заинтересовать читателя",
"Show": "Показать",
@ -293,6 +298,6 @@
"user already exist": "пользователь уже существует",
"view": "просмотр",
"zine": "журнал",
"Required": "Поле обязательно для заполнения",
"Publish Settings": "Настройки публикации",
"Unnamed draft": "Unnamed draft"
}

View File

@ -8,9 +8,10 @@ import { useLocalize } from '../../../context/localize'
import { Modal } from '../../Nav/Modal'
import { Menu } from './Menu'
import type { MenuItem } from './Menu/Menu'
import { showModal } from '../../../stores/ui'
import { UploadModalContent } from '../UploadModal'
import { hideModal, showModal } from '../../../stores/ui'
import { UploadModalContent } from '../UploadModalContent'
import { useOutsideClickHandler } from '../../../utils/useOutsideClickHandler'
import { imageProxy } from '../../../utils/imageProxy'
type FloatingMenuProps = {
editor: Editor
@ -35,6 +36,7 @@ export const EditorFloatingMenu = (props: FloatingMenuProps) => {
const { t } = useLocalize()
const [selectedMenuItem, setSelectedMenuItem] = createSignal<MenuItem | undefined>()
const [menuOpen, setMenuOpen] = createSignal<boolean>(false)
const menuRef: { current: HTMLDivElement } = { current: null }
const handleEmbedFormSubmit = async (value: string) => {
// TODO: add support instagram embed (blockquote)
const emb = await embedData(value)
@ -58,8 +60,6 @@ export const EditorFloatingMenu = (props: FloatingMenuProps) => {
setMenuOpen(false)
}
const menuRef: { current: HTMLDivElement } = { current: null }
useOutsideClickHandler({
containerRef: menuRef,
handler: () => {
@ -69,6 +69,15 @@ export const EditorFloatingMenu = (props: FloatingMenuProps) => {
}
})
const renderImage = (src: string) => {
props.editor
.chain()
.focus()
.setImage({ src: imageProxy(src) })
.run()
hideModal()
}
return (
<>
<div ref={props.ref} class={styles.editorFloatingMenu}>
@ -100,7 +109,12 @@ export const EditorFloatingMenu = (props: FloatingMenuProps) => {
</Show>
</div>
<Modal variant="narrow" name="uploadImage" onClose={closeUploadModalHandler}>
<UploadModalContent closeCallback={() => setSelectedMenuItem()} editor={props.editor} />
<UploadModalContent
onClose={(value) => {
renderImage(value)
setSelectedMenuItem()
}}
/>
</Modal>
</>
)

View File

@ -33,6 +33,9 @@ export const InlineForm = (props: Props) => {
} else {
setFormValueError(props.errorMessage)
}
} else {
props.onSubmit(formValue())
props.onClose()
}
}

View File

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

View File

@ -8,14 +8,11 @@ import { hideModal } from '../../../stores/ui'
import { createDropzone, createFileUploader, UploadFile } 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'
import { imageProxy } from '../../../utils/imageProxy'
type Props = {
editor: Editor
closeCallback: () => void
onClose: (imgUrl?: string) => void
}
export const UploadModalContent = (props: Props) => {
@ -25,27 +22,17 @@ export const UploadModalContent = (props: Props) => {
const [dragActive, setDragActive] = createSignal(false)
const [dragError, setDragError] = createSignal<string | undefined>()
const renderImage = (src: string) => {
props.editor
.chain()
.focus()
.setImage({ src: imageProxy(src) })
.run()
hideModal()
}
const { selectFiles } = createFileUploader({ multiple: false, accept: 'image/*' })
const runUpload = async (file) => {
try {
setIsUploading(true)
const fileUrl = await handleFileUpload(file)
setIsUploading(false)
props.closeCallback()
renderImage(fileUrl)
props.onClose(fileUrl)
} catch (error) {
console.error('[upload image] error', error)
setIsUploading(false)
setUploadError(t('Error'))
console.error('[runUpload]', error)
}
}
@ -53,7 +40,7 @@ export const UploadModalContent = (props: Props) => {
try {
const data = await fetch(value)
const blob = await data.blob()
const file = new File([blob], 'convertedFromUrl', { type: data.headers.get('Content-Type') })
const file = await new File([blob], 'convertedFromUrl', { type: data.headers.get('Content-Type') })
const fileToUpload: UploadFile = {
source: blob.toString(),
name: file.name,
@ -124,7 +111,7 @@ export const UploadModalContent = (props: Props) => {
showInput={true}
onClose={() => {
hideModal()
props.closeCallback()
props.onClose()
}}
validate={(value) => verifyImg(value)}
onSubmit={handleImageFormSubmit}

View File

@ -0,0 +1,4 @@
export { Editor } from './Editor'
export { Panel } from './Panel'
export { TopicSelect } from './TopicSelect'
export { UploadModalContent } from './UploadModalContent'

View File

@ -4,8 +4,62 @@
.articlePreview {
border: 2px solid #e8e8e8;
min-height: 10em;
padding: 1rem 1.2rem;
display: flex;
flex-direction: column;
min-height: 300px;
align-items: flex-start;
box-sizing: border-box;
.shoutCardCoverContainer {
position: relative;
width: 100%;
.shoutCardCover {
height: 0;
overflow: hidden;
position: relative;
margin: 1.6rem 0;
padding-bottom: 56.2%;
img {
height: 100%;
object-fit: cover;
position: absolute;
transform-origin: 50% 50%;
transition: transform 1s ease-in-out;
width: 100%;
}
&:hover img {
transform: scale(1.1);
}
}
}
.shoutCardTitle {
@include font-size(2.2rem);
font-weight: 700;
line-height: 1.25;
margin: auto 0 0.8rem;
}
.shoutCardSubtitle {
@include font-size(1.7rem);
color: #696969;
font-weight: 400;
line-height: 1.3;
margin-bottom: 0.8rem;
transition: color 0.2s, background-color 0.2s, box-shadow 0.2s;
}
.shoutAuthor {
@include font-size(1.2rem);
margin-right: 1.6rem;
color: rgb(0 0 0 / 70%);
}
}
.formHolder {
@ -60,7 +114,7 @@
.close {
filter: invert(1);
margin: -1.6rem 0 0 -1.6rem;
margin: -1.6rem 0 0 -2.8rem;
}
section {

View File

@ -1,34 +1,43 @@
import { createSignal, onCleanup, onMount, Show } from 'solid-js'
import { useLocalize } from '../../context/localize'
import { clsx } from 'clsx'
import styles from './Edit.module.scss'
import { Title } from '@solidjs/meta'
import type { Shout, Topic } from '../../graphql/types.gen'
import { apiClient } from '../../utils/apiClient'
import { TopicSelect } from '../Editor/TopicSelect/TopicSelect'
import { useRouter } from '../../stores/router'
import { Editor } from '../Editor/Editor'
import { Panel } from '../Editor/Panel'
import { useEditorContext } from '../../context/editor'
import { Editor, Panel, TopicSelect, UploadModalContent } from '../Editor'
import { Icon } from '../_shared/Icon'
import { Button } from '../_shared/Button'
import styles from './Edit.module.scss'
import { useSession } from '../../context/session'
import { Modal } from '../Nav/Modal'
import { hideModal, showModal } from '../../stores/ui'
type EditViewProps = {
shout: Shout
}
const scrollTop = () => {
window.scrollTo({
top: 0,
behavior: 'smooth'
})
}
export const EditView = (props: EditViewProps) => {
const { t } = useLocalize()
const { user } = useSession()
const [isScrolled, setIsScrolled] = createSignal(false)
const [topics, setTopics] = createSignal<Topic[]>(null)
const [coverImage, setCoverImage] = createSignal<string>(null)
const { page } = useRouter()
const {
form,
formErrors,
actions: { setForm, setFormErrors }
} = useEditorContext()
const [isSlugChanged, setIsSlugChanged] = createSignal(false)
setForm({
@ -76,11 +85,10 @@ export const EditView = (props: EditViewProps) => {
setForm('slug', slug)
}
const scrollTop = () => {
window.scrollTo({
top: 0,
behavior: 'smooth'
})
const handleUploadModalContentCloseSetCover = (imgUrl: string) => {
hideModal()
setCoverImage(imgUrl)
setForm('coverImageUrl', imgUrl)
}
return (
@ -112,7 +120,7 @@ export const EditView = (props: EditViewProps) => {
type="text"
name="title"
id="title"
placeholder="Заголовок"
placeholder={t('Header')}
autocomplete="off"
value={form.title}
onInput={handleTitleInputChange}
@ -128,7 +136,7 @@ export const EditView = (props: EditViewProps) => {
name="subtitle"
id="subtitle"
autocomplete="off"
placeholder="Подзаголовок"
placeholder={t('Subheader')}
value={form.subtitle}
onChange={(e) => setForm('subtitle', e.currentTarget.value)}
/>
@ -143,7 +151,7 @@ export const EditView = (props: EditViewProps) => {
[styles.visible]: page().route === 'editSettings'
})}
>
<h1>Настройки публикации</h1>
<h1>{t('Publish Settings')}</h1>
<h4>Slug</h4>
<div class="pretty-form__item">
@ -209,18 +217,38 @@ export const EditView = (props: EditViewProps) => {
{/* </div>*/}
{/*</div>*/}
<h4>Карточка материала на&nbsp;главной</h4>
<h4>{t('Material card')}</h4>
<p class="description">
Выберите заглавное изображение для статьи, тут сразу можно увидеть как карточка будет
выглядеть на&nbsp;главной странице
{t(
'Choose a title image for the article. You can immediately see how the publication card will look like.'
)}
</p>
<div class={styles.articlePreview} />
<div class={styles.articlePreview}>
<Button
variant="primary"
onClick={() => showModal('uploadImage')}
value={coverImage() ? t('Add another image') : t('Add image')}
/>
<Show when={coverImage() ?? form.coverImageUrl}>
<div class={styles.shoutCardCoverContainer}>
<div class={styles.shoutCardCover}>
<img src={coverImage() || form.coverImageUrl} alt={form.title} loading="lazy" />
</div>
</div>
</Show>
<div class={styles.shoutCardTitle}>{form.title}</div>
<div class={styles.shoutCardSubtitle}>{form.subtitle}</div>
<div class={styles.shoutAuthor}>{user().name}</div>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
<Modal variant="narrow" name="uploadImage">
<UploadModalContent onClose={(value) => handleUploadModalContentCloseSetCover(value)} />
</Modal>
<Panel shoutId={props.shout.id} />
</>
)