import { clsx } from 'clsx' import deepEqual from 'fast-deep-equal' import { Accessor, Show, createMemo, createSignal, lazy, onCleanup, onMount } from 'solid-js' import { createStore } from 'solid-js/store' import { ShoutForm, useEditorContext } from '../../../context/editor' import { useLocalize } from '../../../context/localize' import type { Shout, Topic } from '../../../graphql/schema/core.gen' import { LayoutType, MediaItem } from '../../../pages/types' import { useRouter } from '../../../stores/router' import { clone } from '../../../utils/clone' import { getImageUrl } from '../../../utils/getImageUrl' import { isDesktop } from '../../../utils/media-query' import { slugify } from '../../../utils/slugify' import { Editor, Panel } from '../../Editor' import { AudioUploader } from '../../Editor/AudioUploader' import { AutoSaveNotice } from '../../Editor/AutoSaveNotice' import { VideoUploader } from '../../Editor/VideoUploader' import { Modal } from '../../Nav/Modal' import { TableOfContents } from '../../TableOfContents' import { DropArea } from '../../_shared/DropArea' import { Icon } from '../../_shared/Icon' import { InviteMembers } from '../../_shared/InviteMembers' import { Popover } from '../../_shared/Popover' import { EditorSwiper } from '../../_shared/SolidSwiper' import { PublishSettings } from '../PublishSettings' import styles from './EditView.module.scss' const SimplifiedEditor = lazy(() => import('../../Editor/SimplifiedEditor')) const GrowingTextarea = lazy(() => import('../../_shared/GrowingTextarea/GrowingTextarea')) type Props = { shout: Shout } export const MAX_HEADER_LIMIT = 100 export const EMPTY_TOPIC: Topic = { id: -1, slug: '', } const AUTO_SAVE_INTERVAL = 5000 const handleScrollTopButtonClick = (e) => { e.preventDefault() window.scrollTo({ top: 0, behavior: 'smooth', }) } export const EditView = (props: Props) => { const { t } = useLocalize() const [isScrolled, setIsScrolled] = createSignal(false) const { page } = useRouter() const { form, formErrors, setForm, setFormErrors, saveDraft, saveDraftToLocalStorage, getDraftFromLocalStorage, } = useEditorContext() const shoutTopics = props.shout.topics || [] // TODO: проверить сохранение черновика в local storage (не работает) const draft = getDraftFromLocalStorage(props.shout.id) if (draft) { setForm(Object.keys(draft).length !== 0 ? draft : { shoutId: props.shout.id }); } else { setForm({ slug: props.shout.slug, shoutId: props.shout.id, title: props.shout.title, lead: props.shout.lead, description: props.shout.description, subtitle: props.shout.subtitle, selectedTopics: shoutTopics, mainTopic: shoutTopics[0], body: props.shout.body, coverImageUrl: props.shout.cover, media: props.shout.media, layout: props.shout.layout, }) } const subtitleInput: { current: HTMLTextAreaElement } = { current: null } const [prevForm, setPrevForm] = createStore(clone(form)) const [saving, setSaving] = createSignal(false) const [isSubtitleVisible, setIsSubtitleVisible] = createSignal(Boolean(form.subtitle)) const [isLeadVisible, setIsLeadVisible] = createSignal(Boolean(form.lead)) const mediaItems: Accessor = createMemo(() => { return JSON.parse(form.media || '[]') }) onMount(() => { const handleScroll = () => { setIsScrolled(window.scrollY > 0) } window.addEventListener('scroll', handleScroll, { passive: true }) onCleanup(() => { window.removeEventListener('scroll', handleScroll) }) }) onMount(() => { // eslint-disable-next-line unicorn/consistent-function-scoping const handleBeforeUnload = (event) => { if (!deepEqual(prevForm, form)) { event.returnValue = t( 'There are unsaved changes in your publishing settings. Are you sure you want to leave the page without saving?', ) } } window.addEventListener('beforeunload', handleBeforeUnload) onCleanup(() => window.removeEventListener('beforeunload', handleBeforeUnload)) }) const handleTitleInputChange = (value: string) => { setForm('title', value) setForm('slug', slugify(value)) if (value) { setFormErrors('title', '') } } const handleAddMedia = (data) => { const newMedia = [...mediaItems(), ...data] setForm('media', JSON.stringify(newMedia)) } const handleSortedMedia = (data) => { setForm('media', JSON.stringify(data)) } const handleMediaDelete = (index) => { const copy = [...mediaItems()] copy.splice(index, 1) setForm('media', JSON.stringify(copy)) } const handleMediaChange = (index, value) => { const updated = mediaItems().map((item, idx) => (idx === index ? value : item)) setForm('media', JSON.stringify(updated)) } const [baseAudioFields, setBaseAudioFields] = createSignal({ artist: '', date: '', genre: '', }) const handleBaseFieldsChange = (key, value) => { if (mediaItems().length > 0) { const updated = mediaItems().map((media) => ({ ...media, [key]: value })) setForm('media', JSON.stringify(updated)) } else { setBaseAudioFields({ ...baseAudioFields(), [key]: value }) } } const articleTitle = () => { switch (props.shout.layout as LayoutType) { case 'audio': { return t('Album name') } case 'image': { return t('Gallery name') } default: { return t('Header') } } } let autoSaveTimeOutId: number | string | NodeJS.Timeout //TODO: add throttle const autoSaveRecursive = () => { autoSaveTimeOutId = setTimeout(async () => { const hasChanges = !deepEqual(form, prevForm) if (hasChanges) { setSaving(true) if (props.shout?.published_at) { saveDraftToLocalStorage(form) } else { await saveDraft(form) } setPrevForm(clone(form)) setTimeout(() => { setSaving(false) }, 2000) } autoSaveRecursive() }, AUTO_SAVE_INTERVAL) } const stopAutoSave = () => { clearTimeout(autoSaveTimeOutId) } onMount(() => { autoSaveRecursive() }) onCleanup(() => { stopAutoSave() }) const showSubtitleInput = () => { setIsSubtitleVisible(true) subtitleInput.current.focus() } const showLeadInput = () => { setIsLeadVisible(true) } return ( <>
{t('Add subtitle')}
{t('Add intro')}
<>
handleTitleInputChange(value)} class={styles.titleInput} placeholder={articleTitle()} initialValue={form.title} maxLength={MAX_HEADER_LIMIT} />
{formErrors.title}
handleBaseFieldsChange('artist', event.target.value)} /> handleBaseFieldsChange('date', event.target.value)} /> handleBaseFieldsChange('genre', event.target.value)} />
{ subtitleInput.current = el }} allowEnterKey={false} value={(value) => setForm('subtitle', value || '')} class={styles.subtitleInput} placeholder={t('Subheader')} initialValue={form.subtitle || ''} maxLength={MAX_HEADER_LIMIT} /> setForm('lead', value)} />
{t('min. 1400×1400 pix')}
{t('jpg, .png, max. 10 mb.')} } isMultiply={false} fileType={'image'} onUpload={(val) => setForm('coverImageUrl', val[0].url)} /> } >
{(triggerRef: (el) => void) => (
setForm('coverImageUrl', null)} >
)}
handleMediaDelete(index)} onImagesAdd={(value) => handleAddMedia(value)} onImagesSorted={(value) => handleSortedMedia(value)} /> handleAddMedia(data)} onVideoDelete={(index) => handleMediaDelete(index)} /> handleAddMedia(value)} onAudioChange={handleMediaChange} onAudioSorted={(value) => handleSortedMedia(value)} />
setForm('body', body)} />
) } export default EditView