webapp/src/context/editor.tsx

273 lines
7.4 KiB
TypeScript
Raw Normal View History

import type { JSX } from 'solid-js'
import { openPage } from '@nanostores/router'
import { Editor } from '@tiptap/core'
2023-12-19 09:34:24 +00:00
import { Accessor, createContext, createSignal, useContext } from 'solid-js'
2024-02-04 11:25:21 +00:00
import { SetStoreFunction, createStore } from 'solid-js/store'
2023-12-19 09:34:24 +00:00
import { apiClient } from '../graphql/client/core'
import { Topic, TopicInput } from '../graphql/schema/core.gen'
import { router, useRouter } from '../stores/router'
2024-02-02 21:52:04 +00:00
import { addArticles } from '../stores/zine/articles'
2024-02-03 08:16:47 +00:00
import { slugify } from '../utils/slugify'
2023-05-05 20:05:50 +00:00
import { useLocalize } from './localize'
2024-02-17 13:25:25 +00:00
import { useSnackbar } from './snackbar'
type WordCounter = {
characters: number
words: number
}
export type ShoutForm = {
layout?: string
shoutId: number
slug: string
title: string
2024-01-22 17:37:27 +00:00
subtitle?: string
lead?: string
description?: string
selectedTopics: Topic[]
2023-05-10 20:20:53 +00:00
mainTopic?: Topic
body: string
2023-10-30 11:29:15 +00:00
coverImageUrl?: string
media?: string
}
type EditorContextType = {
isEditorPanelVisible: Accessor<boolean>
wordCounter: Accessor<WordCounter>
form: ShoutForm
2023-05-10 20:20:53 +00:00
formErrors: Record<keyof ShoutForm, string>
2024-02-17 14:22:11 +00:00
editorRef: { current: () => Editor | null }
2024-02-04 17:40:15 +00:00
saveShout: (form: ShoutForm) => Promise<void>
saveDraft: (form: ShoutForm) => Promise<void>
saveDraftToLocalStorage: (form: ShoutForm) => void
getDraftFromLocalStorage: (shoutId: number) => ShoutForm
publishShout: (form: ShoutForm) => Promise<void>
publishShoutById: (shoutId: number) => Promise<void>
deleteShout: (shoutId: number) => Promise<boolean>
toggleEditorPanel: () => void
countWords: (value: WordCounter) => void
setForm: SetStoreFunction<ShoutForm>
setFormErrors: SetStoreFunction<Record<keyof ShoutForm, string>>
setEditor: (editor: () => Editor) => void
}
const EditorContext = createContext<EditorContextType>()
export function useEditorContext() {
return useContext(EditorContext)
}
2023-05-11 11:06:29 +00:00
const topic2topicInput = (topic: Topic): TopicInput => {
return {
id: topic.id,
slug: topic.slug,
title: topic.title,
2023-05-11 11:06:29 +00:00
}
}
const saveDraftToLocalStorage = (formToSave: ShoutForm) => {
localStorage.setItem(`shout-${formToSave.shoutId}`, JSON.stringify(formToSave))
}
const getDraftFromLocalStorage = (shoutId: number) => {
2024-02-17 14:28:57 +00:00
return JSON.parse(localStorage.getItem(`shout-${shoutId}`) || '{}')
}
const removeDraftFromLocalStorage = (shoutId: number) => {
localStorage.removeItem(`shout-${shoutId}`)
}
export const EditorProvider = (props: { children: JSX.Element }) => {
2024-02-17 14:22:11 +00:00
const localize = useLocalize()
2023-05-08 17:21:06 +00:00
const { page } = useRouter()
2024-02-17 14:22:11 +00:00
const snackbar = useSnackbar()
2023-05-03 16:13:48 +00:00
const [isEditorPanelVisible, setIsEditorPanelVisible] = createSignal<boolean>(false)
2024-02-17 14:22:11 +00:00
const editorRef: { current: () => Editor | null } = { current: () => null }
const [form, setForm] = createStore<ShoutForm>({
body: '',
slug: '',
shoutId: 0,
title: '',
selectedTopics: [],
})
const [formErrors, setFormErrors] = createStore({} as Record<keyof ShoutForm, string>)
const [wordCounter, setWordCounter] = createSignal<WordCounter>({
characters: 0,
words: 0,
})
2023-05-03 16:13:48 +00:00
const toggleEditorPanel = () => setIsEditorPanelVisible((value) => !value)
const countWords = (value) => setWordCounter(value)
2023-05-08 17:21:06 +00:00
const validate = () => {
2023-05-05 20:05:50 +00:00
if (!form.title) {
2024-02-17 14:22:11 +00:00
setFormErrors('title', localize?.t('Please, set the article title') || '')
2023-05-05 20:05:50 +00:00
return false
}
2024-02-17 14:28:57 +00:00
const parsedMedia = JSON.parse(form.media || '[]')
if (form.layout === 'video' && !parsedMedia[0]) {
2024-02-17 14:22:11 +00:00
snackbar?.showSnackbar({
type: 'error',
body: localize?.t('Looks like you forgot to upload the video'),
})
return false
}
2023-05-08 17:21:06 +00:00
return true
}
2023-05-10 20:20:53 +00:00
const validateSettings = () => {
if (form.selectedTopics.length === 0) {
2024-02-17 14:22:11 +00:00
setFormErrors('selectedTopics', localize?.t('Required') || '')
2023-05-10 20:20:53 +00:00
return false
}
return true
}
const updateShout = async (formToUpdate: ShoutForm, { publish }: { publish: boolean }) => {
2023-12-19 09:34:24 +00:00
return await apiClient.updateArticle({
2024-02-02 21:19:58 +00:00
shout_id: formToUpdate.shoutId,
shout_input: {
2024-02-03 18:48:44 +00:00
body: formToUpdate.body,
2023-11-28 13:18:25 +00:00
topics: formToUpdate.selectedTopics.map((topic) => topic2topicInput(topic)), // NOTE: first is main
2024-02-03 18:48:44 +00:00
// authors?: InputMaybe<Array<InputMaybe<Scalars['String']>>>
// community?: InputMaybe<Scalars['Int']>
// mainTopic: topic2topicInput(formToUpdate.mainTopic),
slug: formToUpdate.slug,
subtitle: formToUpdate.subtitle,
title: formToUpdate.title,
lead: formToUpdate.lead,
description: formToUpdate.description,
2024-02-02 18:03:55 +00:00
cover: formToUpdate.coverImageUrl,
2024-02-17 13:25:25 +00:00
media: formToUpdate.media,
2023-05-10 20:20:53 +00:00
},
publish,
2023-05-10 20:20:53 +00:00
})
}
const saveShout = async (formToSave: ShoutForm) => {
if (isEditorPanelVisible()) {
toggleEditorPanel()
}
2024-02-17 14:22:11 +00:00
if (page()?.route === 'edit' && !validate()) {
2023-05-10 20:20:53 +00:00
return
}
2024-02-17 14:22:11 +00:00
if (page()?.route === 'editSettings' && !validateSettings()) {
2023-05-08 17:21:06 +00:00
return
}
2023-05-05 20:05:50 +00:00
try {
const shout = await updateShout(formToSave, { publish: false })
removeDraftFromLocalStorage(formToSave.shoutId)
2023-05-08 17:21:06 +00:00
2024-02-17 14:40:10 +00:00
if (shout?.published_at) {
2023-05-08 17:21:06 +00:00
openPage(router, 'article', { slug: shout.slug })
} else {
openPage(router, 'drafts')
2023-05-08 17:21:06 +00:00
}
2023-05-05 20:05:50 +00:00
} catch (error) {
2023-05-08 18:01:11 +00:00
console.error('[saveShout]', error)
2024-02-17 14:22:11 +00:00
snackbar?.showSnackbar({ type: 'error', body: localize?.t('Error') || '' })
2023-05-05 20:05:50 +00:00
}
}
const saveDraft = async (draftForm: ShoutForm) => {
await updateShout(draftForm, { publish: false })
}
const publishShout = async (formToPublish: ShoutForm) => {
if (isEditorPanelVisible()) {
toggleEditorPanel()
}
2023-05-10 20:20:53 +00:00
2024-02-17 14:22:11 +00:00
if (page()?.route === 'edit') {
2023-05-11 11:06:29 +00:00
if (!validate()) {
return
}
await updateShout(formToPublish, { publish: false })
2023-05-11 11:06:29 +00:00
2023-05-10 20:20:53 +00:00
const slug = slugify(form.title)
2023-05-08 17:21:06 +00:00
setForm('slug', slug)
openPage(router, 'editSettings', { shoutId: form.shoutId.toString() })
return
}
2023-05-11 11:06:29 +00:00
if (!validateSettings()) {
return
}
2023-05-05 20:05:50 +00:00
try {
await updateShout(formToPublish, { publish: true })
2023-05-08 18:26:42 +00:00
openPage(router, 'feed')
2023-05-08 17:21:06 +00:00
} catch (error) {
2023-05-08 18:01:11 +00:00
console.error('[publishShout]', error)
2024-02-17 14:22:11 +00:00
snackbar?.showSnackbar({ type: 'error', body: localize?.t('Error') || '' })
2023-05-08 17:21:06 +00:00
}
}
2024-02-02 21:19:58 +00:00
const publishShoutById = async (shout_id: number) => {
2023-05-08 17:21:06 +00:00
try {
2024-02-02 21:52:04 +00:00
const newShout = await apiClient.updateArticle({
2024-02-02 21:19:58 +00:00
shout_id,
publish: true,
2023-05-08 17:21:06 +00:00
})
2024-02-05 08:59:21 +00:00
if (newShout) {
addArticles([newShout])
openPage(router, 'feed')
} else {
console.error('[publishShoutById] no shout returned:', newShout)
}
2023-05-08 17:21:06 +00:00
} catch (error) {
2023-05-08 18:01:11 +00:00
console.error('[publishShoutById]', error)
2024-02-17 14:22:11 +00:00
snackbar?.showSnackbar({ type: 'error', body: localize?.t('Error') })
2023-05-08 17:21:06 +00:00
}
}
2024-01-31 18:41:20 +00:00
const deleteShout = async (shout_id: number) => {
2023-05-08 17:21:06 +00:00
try {
2023-12-19 09:34:24 +00:00
await apiClient.deleteShout({
2024-01-31 18:41:20 +00:00
shout_id,
2023-05-05 20:05:50 +00:00
})
return true
} catch {
2024-02-17 14:22:11 +00:00
snackbar?.showSnackbar({ type: 'error', body: localize?.t('Error') || '' })
2023-05-05 20:05:50 +00:00
return false
}
}
const setEditor = (editor: () => Editor) => {
editorRef.current = editor
}
const actions = {
2023-05-05 20:05:50 +00:00
saveShout,
saveDraft,
saveDraftToLocalStorage,
getDraftFromLocalStorage,
2023-05-05 20:05:50 +00:00
publishShout,
2023-05-08 17:21:06 +00:00
publishShoutById,
deleteShout,
toggleEditorPanel,
countWords,
2023-05-05 20:05:50 +00:00
setForm,
setFormErrors,
setEditor,
}
const value: EditorContextType = {
2024-02-04 17:40:15 +00:00
...actions,
form,
formErrors,
editorRef,
isEditorPanelVisible,
wordCounter,
}
return <EditorContext.Provider value={value}>{props.children}</EditorContext.Provider>
}