2023-04-26 02:37:29 +00:00
|
|
|
import type { JSX } from 'solid-js'
|
2023-11-14 15:10:00 +00:00
|
|
|
|
|
|
|
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'
|
2023-05-04 19:57:02 +00:00
|
|
|
import { createStore, SetStoreFunction } from 'solid-js/store'
|
2023-11-14 15:10:00 +00:00
|
|
|
|
2023-12-19 09:34:24 +00:00
|
|
|
import { apiClient } from '../graphql/client/core'
|
2024-02-02 17:29:53 +00:00
|
|
|
import { Topic, TopicInput } from '../graphql/schema/core.gen'
|
2023-11-14 15:10:00 +00:00
|
|
|
import { router, useRouter } from '../stores/router'
|
|
|
|
import { slugify } from '../utils/slugify'
|
2024-02-02 21:52:04 +00:00
|
|
|
import { addArticles } from '../stores/zine/articles'
|
2023-05-05 20:05:50 +00:00
|
|
|
import { useLocalize } from './localize'
|
2023-12-14 11:49:55 +00:00
|
|
|
import { useSnackbar } from './snackbar'
|
2023-04-26 02:37:29 +00:00
|
|
|
|
|
|
|
type WordCounter = {
|
|
|
|
characters: number
|
|
|
|
words: number
|
|
|
|
}
|
|
|
|
|
2023-08-04 15:59:36 +00:00
|
|
|
export type ShoutForm = {
|
2023-06-10 14:10:05 +00:00
|
|
|
layout?: string
|
2023-05-07 15:15:30 +00:00
|
|
|
shoutId: number
|
2023-05-04 19:57:02 +00:00
|
|
|
slug: string
|
|
|
|
title: string
|
2024-01-22 17:37:27 +00:00
|
|
|
subtitle?: string
|
2023-08-22 13:37:54 +00:00
|
|
|
lead?: string
|
|
|
|
description?: string
|
2023-05-04 19:57:02 +00:00
|
|
|
selectedTopics: Topic[]
|
2023-05-10 20:20:53 +00:00
|
|
|
mainTopic?: Topic
|
2023-05-04 19:57:02 +00:00
|
|
|
body: string
|
2023-10-30 11:29:15 +00:00
|
|
|
coverImageUrl?: string
|
2023-06-10 14:10:05 +00:00
|
|
|
media?: string
|
2023-05-04 19:57:02 +00:00
|
|
|
}
|
|
|
|
|
2023-04-26 02:37:29 +00:00
|
|
|
type EditorContextType = {
|
|
|
|
isEditorPanelVisible: Accessor<boolean>
|
|
|
|
wordCounter: Accessor<WordCounter>
|
2023-05-04 19:57:02 +00:00
|
|
|
form: ShoutForm
|
2023-05-10 20:20:53 +00:00
|
|
|
formErrors: Record<keyof ShoutForm, string>
|
2023-05-12 13:03:46 +00:00
|
|
|
editorRef: { current: () => Editor }
|
2023-04-26 02:37:29 +00:00
|
|
|
actions: {
|
2023-08-04 15:59:36 +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>
|
2023-05-08 17:21:06 +00:00
|
|
|
publishShoutById: (shoutId: number) => Promise<void>
|
|
|
|
deleteShout: (shoutId: number) => Promise<boolean>
|
2023-04-26 02:37:29 +00:00
|
|
|
toggleEditorPanel: () => void
|
|
|
|
countWords: (value: WordCounter) => void
|
2023-05-04 19:57:02 +00:00
|
|
|
setForm: SetStoreFunction<ShoutForm>
|
2023-05-10 20:20:53 +00:00
|
|
|
setFormErrors: SetStoreFunction<Record<keyof ShoutForm, string>>
|
2023-05-12 13:03:46 +00:00
|
|
|
setEditor: (editor: () => Editor) => void
|
2023-04-26 02:37:29 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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,
|
2023-11-14 15:10:00 +00:00
|
|
|
title: topic.title,
|
2023-05-11 11:06:29 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-14 14:48:27 +00:00
|
|
|
const saveDraftToLocalStorage = (formToSave: ShoutForm) => {
|
|
|
|
localStorage.setItem(`shout-${formToSave.shoutId}`, JSON.stringify(formToSave))
|
|
|
|
}
|
|
|
|
const getDraftFromLocalStorage = (shoutId: number) => {
|
|
|
|
return JSON.parse(localStorage.getItem(`shout-${shoutId}`))
|
|
|
|
}
|
|
|
|
|
|
|
|
const removeDraftFromLocalStorage = (shoutId: number) => {
|
|
|
|
localStorage.removeItem(`shout-${shoutId}`)
|
|
|
|
}
|
|
|
|
|
2023-04-26 02:37:29 +00:00
|
|
|
export const EditorProvider = (props: { children: JSX.Element }) => {
|
2023-05-05 20:05:50 +00:00
|
|
|
const { t } = useLocalize()
|
2023-05-08 17:21:06 +00:00
|
|
|
const { page } = useRouter()
|
2023-05-05 20:05:50 +00:00
|
|
|
const {
|
2023-11-14 15:10:00 +00:00
|
|
|
actions: { showSnackbar },
|
2023-05-05 20:05:50 +00:00
|
|
|
} = useSnackbar()
|
2023-05-03 16:13:48 +00:00
|
|
|
const [isEditorPanelVisible, setIsEditorPanelVisible] = createSignal<boolean>(false)
|
2023-05-12 13:03:46 +00:00
|
|
|
const editorRef: { current: () => Editor } = { current: null }
|
2023-05-04 19:57:02 +00:00
|
|
|
const [form, setForm] = createStore<ShoutForm>(null)
|
2023-05-10 20:20:53 +00:00
|
|
|
const [formErrors, setFormErrors] = createStore<Record<keyof ShoutForm, string>>(null)
|
2023-04-26 02:37:29 +00:00
|
|
|
const [wordCounter, setWordCounter] = createSignal<WordCounter>({
|
|
|
|
characters: 0,
|
2023-11-14 15:10:00 +00:00
|
|
|
words: 0,
|
2023-04-26 02:37:29 +00:00
|
|
|
})
|
2023-05-03 16:13:48 +00:00
|
|
|
const toggleEditorPanel = () => setIsEditorPanelVisible((value) => !value)
|
2023-04-26 02:37:29 +00:00
|
|
|
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) {
|
|
|
|
setFormErrors('title', t('Required'))
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2023-06-10 14:10:05 +00:00
|
|
|
const parsedMedia = JSON.parse(form.media)
|
|
|
|
if (form.layout === 'video' && !parsedMedia[0]) {
|
|
|
|
showSnackbar({ type: 'error', body: 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) {
|
|
|
|
setFormErrors('selectedTopics', t('Required'))
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2023-08-04 15:59:36 +00:00
|
|
|
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-02 18:03:55 +00:00
|
|
|
...formToUpdate,
|
2023-11-28 13:18:25 +00:00
|
|
|
topics: formToUpdate.selectedTopics.map((topic) => topic2topicInput(topic)), // NOTE: first is main
|
2024-02-02 18:03:55 +00:00
|
|
|
cover: formToUpdate.coverImageUrl,
|
2023-05-10 20:20:53 +00:00
|
|
|
},
|
2023-11-14 15:10:00 +00:00
|
|
|
publish,
|
2023-05-10 20:20:53 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-08-04 15:59:36 +00:00
|
|
|
const saveShout = async (formToSave: ShoutForm) => {
|
2023-05-10 01:46:39 +00:00
|
|
|
if (isEditorPanelVisible()) {
|
|
|
|
toggleEditorPanel()
|
|
|
|
}
|
|
|
|
|
2023-05-10 20:20:53 +00:00
|
|
|
if (page().route === 'edit' && !validate()) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if (page().route === 'editSettings' && !validateSettings()) {
|
2023-05-08 17:21:06 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-05-05 20:05:50 +00:00
|
|
|
try {
|
2023-08-04 15:59:36 +00:00
|
|
|
const shout = await updateShout(formToSave, { publish: false })
|
|
|
|
removeDraftFromLocalStorage(formToSave.shoutId)
|
2023-05-08 17:21:06 +00:00
|
|
|
|
2024-02-02 17:29:53 +00:00
|
|
|
if (shout.published_at) {
|
2023-05-08 17:21:06 +00:00
|
|
|
openPage(router, 'article', { slug: shout.slug })
|
2024-02-02 17:29:53 +00:00
|
|
|
} 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)
|
2023-05-05 20:05:50 +00:00
|
|
|
showSnackbar({ type: 'error', body: t('Error') })
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-04 15:59:36 +00:00
|
|
|
const saveDraft = async (draftForm: ShoutForm) => {
|
|
|
|
await updateShout(draftForm, { publish: false })
|
|
|
|
}
|
|
|
|
|
|
|
|
const publishShout = async (formToPublish: ShoutForm) => {
|
2023-05-10 01:46:39 +00:00
|
|
|
if (isEditorPanelVisible()) {
|
|
|
|
toggleEditorPanel()
|
|
|
|
}
|
2023-05-10 20:20:53 +00:00
|
|
|
|
2023-05-08 17:21:06 +00:00
|
|
|
if (page().route === 'edit') {
|
2023-05-11 11:06:29 +00:00
|
|
|
if (!validate()) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-08-04 15:59:36 +00:00
|
|
|
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 {
|
2023-08-04 15:59:36 +00:00
|
|
|
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)
|
2023-05-08 17:21:06 +00:00
|
|
|
showSnackbar({ type: 'error', body: t('Error') })
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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,
|
2023-11-14 15:10:00 +00:00
|
|
|
publish: true,
|
2023-05-08 17:21:06 +00:00
|
|
|
})
|
2024-02-02 21:52:04 +00:00
|
|
|
addArticles([newShout])
|
2023-05-08 17:21:06 +00:00
|
|
|
openPage(router, 'feed')
|
|
|
|
} catch (error) {
|
2023-05-08 18:01:11 +00:00
|
|
|
console.error('[publishShoutById]', error)
|
2023-05-08 17:21:06 +00:00
|
|
|
showSnackbar({ type: 'error', body: t('Error') })
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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 {
|
|
|
|
showSnackbar({ type: 'error', body: t('Error') })
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-12 13:03:46 +00:00
|
|
|
const setEditor = (editor: () => Editor) => {
|
|
|
|
editorRef.current = editor
|
|
|
|
}
|
|
|
|
|
2023-04-26 02:37:29 +00:00
|
|
|
const actions = {
|
2023-05-05 20:05:50 +00:00
|
|
|
saveShout,
|
2023-08-04 15:59:36 +00:00
|
|
|
saveDraft,
|
|
|
|
saveDraftToLocalStorage,
|
|
|
|
getDraftFromLocalStorage,
|
2023-05-05 20:05:50 +00:00
|
|
|
publishShout,
|
2023-05-08 17:21:06 +00:00
|
|
|
publishShoutById,
|
|
|
|
deleteShout,
|
2023-04-26 02:37:29 +00:00
|
|
|
toggleEditorPanel,
|
2023-05-04 19:57:02 +00:00
|
|
|
countWords,
|
2023-05-05 20:05:50 +00:00
|
|
|
setForm,
|
2023-05-12 13:03:46 +00:00
|
|
|
setFormErrors,
|
2023-11-14 15:10:00 +00:00
|
|
|
setEditor,
|
2023-04-26 02:37:29 +00:00
|
|
|
}
|
|
|
|
|
2023-05-12 13:03:46 +00:00
|
|
|
const value: EditorContextType = {
|
|
|
|
actions,
|
|
|
|
form,
|
|
|
|
formErrors,
|
|
|
|
editorRef,
|
|
|
|
isEditorPanelVisible,
|
2023-11-14 15:10:00 +00:00
|
|
|
wordCounter,
|
2023-05-12 13:03:46 +00:00
|
|
|
}
|
2023-04-26 02:37:29 +00:00
|
|
|
|
|
|
|
return <EditorContext.Provider value={value}>{props.children}</EditorContext.Provider>
|
|
|
|
}
|