feat(cache): add blog cache, blog to controlled inputs
This commit is contained in:
parent
30a87cc347
commit
60773ec446
@ -2,14 +2,7 @@ import { NDKContextType } from 'contexts/NDKContext'
|
|||||||
import { ActionFunctionArgs, redirect } from 'react-router-dom'
|
import { ActionFunctionArgs, redirect } from 'react-router-dom'
|
||||||
import { getBlogPageRoute } from 'routes'
|
import { getBlogPageRoute } from 'routes'
|
||||||
import { BlogFormErrors, BlogEventSubmitForm, BlogEventEditForm } from 'types'
|
import { BlogFormErrors, BlogEventSubmitForm, BlogEventEditForm } from 'types'
|
||||||
import {
|
import { isReachable, isValidImageUrl, log, LogType, now } from 'utils'
|
||||||
isReachable,
|
|
||||||
isValidImageUrl,
|
|
||||||
log,
|
|
||||||
LogType,
|
|
||||||
now,
|
|
||||||
parseFormData
|
|
||||||
} from 'utils'
|
|
||||||
import { kinds, UnsignedEvent, Event, nip19 } from 'nostr-tools'
|
import { kinds, UnsignedEvent, Event, nip19 } from 'nostr-tools'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { NDKEvent } from '@nostr-dev-kit/ndk'
|
import { NDKEvent } from '@nostr-dev-kit/ndk'
|
||||||
@ -43,12 +36,9 @@ export const writeRouteAction =
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get the form data from submit request
|
// Get the form data from submit request
|
||||||
const formData = await request.formData()
|
const formSubmit = (await request.json()) as
|
||||||
|
| BlogEventSubmitForm
|
||||||
// Parse the the data
|
| BlogEventEditForm
|
||||||
const formSubmit = parseFormData<BlogEventSubmitForm | BlogEventEditForm>(
|
|
||||||
formData
|
|
||||||
)
|
|
||||||
|
|
||||||
// Check for errors
|
// Check for errors
|
||||||
const formErrors = await validateFormData(formSubmit)
|
const formErrors = await validateFormData(formSubmit)
|
||||||
@ -80,7 +70,7 @@ export const writeRouteAction =
|
|||||||
const tTags = formSubmit
|
const tTags = formSubmit
|
||||||
.tags!.toLowerCase()
|
.tags!.toLowerCase()
|
||||||
.split(',')
|
.split(',')
|
||||||
.map((t) => ['t', t])
|
.map((t) => ['t', t.trim()])
|
||||||
|
|
||||||
const tags = [
|
const tags = [
|
||||||
['d', uuid],
|
['d', uuid],
|
||||||
@ -95,7 +85,7 @@ export const writeRouteAction =
|
|||||||
|
|
||||||
// Add NSFW tag, L label namespace standardized tag
|
// Add NSFW tag, L label namespace standardized tag
|
||||||
// https://github.com/nostr-protocol/nips/blob/2838e3bd51ac00bd63c4cef1601ae09935e7dd56/README.md#standardized-tags
|
// https://github.com/nostr-protocol/nips/blob/2838e3bd51ac00bd63c4cef1601ae09935e7dd56/README.md#standardized-tags
|
||||||
if (formSubmit.nsfw === 'on') tags.push(['L', 'content-warning'])
|
if (formSubmit.nsfw) tags.push(['L', 'content-warning'])
|
||||||
|
|
||||||
const unsignedEvent: UnsignedEvent = {
|
const unsignedEvent: UnsignedEvent = {
|
||||||
kind: kinds.LongFormArticle,
|
kind: kinds.LongFormArticle,
|
||||||
|
@ -1,60 +1,123 @@
|
|||||||
import { useRef, useState } from 'react'
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import {
|
import {
|
||||||
Form,
|
|
||||||
useActionData,
|
useActionData,
|
||||||
useLoaderData,
|
useLoaderData,
|
||||||
useNavigation
|
useNavigation,
|
||||||
|
useSubmit
|
||||||
} from 'react-router-dom'
|
} from 'react-router-dom'
|
||||||
import {
|
import {
|
||||||
CheckboxFieldUncontrolled,
|
CheckboxField,
|
||||||
InputFieldUncontrolled,
|
InputField,
|
||||||
InputFieldWithImageUpload
|
InputFieldWithImageUpload
|
||||||
} from '../../components/Inputs'
|
} from 'components/Inputs'
|
||||||
import { ProfileSection } from '../../components/ProfileSection'
|
import { ProfileSection } from 'components/ProfileSection'
|
||||||
import { useAppSelector } from '../../hooks'
|
import { useAppSelector, useLocalCache } from 'hooks'
|
||||||
import { BlogFormErrors, BlogPageLoaderResult } from 'types'
|
import {
|
||||||
import '../../styles/innerPage.css'
|
BlogEventEditForm,
|
||||||
import '../../styles/styles.css'
|
BlogEventSubmitForm,
|
||||||
import '../../styles/write.css'
|
BlogFormErrors,
|
||||||
|
BlogPageLoaderResult
|
||||||
|
} from 'types'
|
||||||
import { LoadingSpinner } from 'components/LoadingSpinner'
|
import { LoadingSpinner } from 'components/LoadingSpinner'
|
||||||
import { AlertPopup } from 'components/AlertPopup'
|
import { AlertPopup } from 'components/AlertPopup'
|
||||||
import { Editor, EditorRef } from 'components/Markdown/Editor'
|
import { Editor, EditorRef } from 'components/Markdown/Editor'
|
||||||
import { InputError } from 'components/Inputs/Error'
|
import { InputError } from 'components/Inputs/Error'
|
||||||
|
import { initializeBlogForm } from 'utils'
|
||||||
|
import 'styles/innerPage.css'
|
||||||
|
import 'styles/styles.css'
|
||||||
|
import 'styles/write.css'
|
||||||
|
|
||||||
export const WritePage = () => {
|
export const WritePage = () => {
|
||||||
const userState = useAppSelector((state) => state.user)
|
const userState = useAppSelector((state) => state.user)
|
||||||
const data = useLoaderData() as BlogPageLoaderResult
|
const data = useLoaderData() as BlogPageLoaderResult
|
||||||
|
|
||||||
const formErrors = useActionData() as BlogFormErrors
|
const formErrors = useActionData() as BlogFormErrors
|
||||||
const navigation = useNavigation()
|
const navigation = useNavigation()
|
||||||
|
const submit = useSubmit()
|
||||||
|
|
||||||
const blog = data?.blog
|
const blog = data?.blog
|
||||||
const title = data?.blog ? 'Edit blog post' : 'Submit a blog post'
|
|
||||||
const [content, setContent] = useState(blog?.content || '')
|
|
||||||
const [image, setImage] = useState(blog?.image || '')
|
|
||||||
|
|
||||||
const formRef = useRef<HTMLFormElement>(null)
|
// Enable cache for the new blog
|
||||||
|
const isEditing = typeof data?.blog !== 'undefined'
|
||||||
|
const [cache, setCache, clearCache] =
|
||||||
|
useLocalCache<BlogEventSubmitForm>('draft-blog')
|
||||||
|
|
||||||
|
const title = isEditing ? 'Edit blog post' : 'Submit a blog post'
|
||||||
|
const [formState, setFormState] = useState<
|
||||||
|
BlogEventSubmitForm | BlogEventEditForm
|
||||||
|
>(isEditing ? initializeBlogForm(blog) : cache ? cache : initializeBlogForm())
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
!isEditing && setCache(formState)
|
||||||
|
}, [formState, isEditing, setCache])
|
||||||
|
|
||||||
const editorRef = useRef<EditorRef>(null)
|
const editorRef = useRef<EditorRef>(null)
|
||||||
|
|
||||||
const [showConfirmPopup, setShowConfirmPopup] = useState<boolean>(false)
|
const [showConfirmPopup, setShowConfirmPopup] = useState<boolean>(false)
|
||||||
const handleReset = () => {
|
const handleReset = useCallback(() => {
|
||||||
setShowConfirmPopup(true)
|
setShowConfirmPopup(true)
|
||||||
}
|
}, [])
|
||||||
const handleResetConfirm = (confirm: boolean) => {
|
const handleResetConfirm = useCallback(
|
||||||
setShowConfirmPopup(false)
|
(confirm: boolean) => {
|
||||||
|
setShowConfirmPopup(false)
|
||||||
|
|
||||||
// Cancel if not confirmed
|
// Cancel if not confirmed
|
||||||
if (!confirm) return
|
if (!confirm) return
|
||||||
|
|
||||||
// Reset featured image
|
const initialState = initializeBlogForm(blog)
|
||||||
setImage(blog?.image || '')
|
|
||||||
|
|
||||||
// Reset editor
|
// Reset editor
|
||||||
if (blog?.content) {
|
editorRef.current?.setMarkdown(initialState.content)
|
||||||
editorRef.current?.setMarkdown(blog?.content)
|
setFormState(initialState)
|
||||||
}
|
|
||||||
|
|
||||||
formRef.current?.reset()
|
// Clear cache
|
||||||
}
|
!isEditing && clearCache()
|
||||||
|
},
|
||||||
|
[blog, clearCache, isEditing]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleImageChange = useCallback((_name: string, value: string) => {
|
||||||
|
setFormState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
image: value
|
||||||
|
}))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleInputChange = useCallback((name: string, value: string) => {
|
||||||
|
setFormState((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
[name]: value
|
||||||
|
}))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleEditorChange = useCallback(
|
||||||
|
(md: string) => {
|
||||||
|
handleInputChange('content', md)
|
||||||
|
},
|
||||||
|
[handleInputChange]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleCheckboxChange = useCallback(
|
||||||
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const { name, checked } = e.target
|
||||||
|
setFormState((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
[name]: checked
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleFormSubmit = useCallback(
|
||||||
|
(e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
submit(JSON.stringify(formState), {
|
||||||
|
method: isEditing ? 'put' : 'post',
|
||||||
|
encType: 'application/json'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[formState, isEditing, submit]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='InnerBodyMain'>
|
<div className='InnerBodyMain'>
|
||||||
@ -71,82 +134,63 @@ export const WritePage = () => {
|
|||||||
{navigation.state === 'submitting' && (
|
{navigation.state === 'submitting' && (
|
||||||
<LoadingSpinner desc='Publishing blog to relays' />
|
<LoadingSpinner desc='Publishing blog to relays' />
|
||||||
)}
|
)}
|
||||||
<Form
|
<form className='IBMSMSMBS_Write' onSubmit={handleFormSubmit}>
|
||||||
ref={formRef}
|
<InputField
|
||||||
className='IBMSMSMBS_Write'
|
|
||||||
method={blog ? 'put' : 'post'}
|
|
||||||
>
|
|
||||||
<InputFieldUncontrolled
|
|
||||||
label='Title'
|
label='Title'
|
||||||
name='title'
|
name='title'
|
||||||
defaultValue={blog?.title}
|
value={formState.title}
|
||||||
error={formErrors?.title}
|
error={formErrors?.title}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
placeholder='Blog title'
|
||||||
/>
|
/>
|
||||||
<div className='inputLabelWrapperMain'>
|
<div className='inputLabelWrapperMain'>
|
||||||
<label className='form-label labelMain'>Content</label>
|
<label className='form-label labelMain'>Content</label>
|
||||||
<div className='inputMain'>
|
<div className='inputMain'>
|
||||||
<Editor
|
<Editor
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
markdown={content}
|
markdown={formState.content}
|
||||||
onChange={(md) => {
|
onChange={handleEditorChange}
|
||||||
setContent(md)
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{typeof formErrors?.content !== 'undefined' && (
|
{typeof formErrors?.content !== 'undefined' && (
|
||||||
<InputError message={formErrors?.content} />
|
<InputError message={formErrors?.content} />
|
||||||
)}
|
)}
|
||||||
{/* encode to keep the markdown formatting */}
|
|
||||||
<input
|
|
||||||
name='content'
|
|
||||||
hidden
|
|
||||||
value={encodeURIComponent(content)}
|
|
||||||
readOnly
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<InputFieldWithImageUpload
|
<InputFieldWithImageUpload
|
||||||
label='Featured Image URL'
|
label='Featured Image URL'
|
||||||
name='image'
|
name='image'
|
||||||
inputMode='url'
|
inputMode='url'
|
||||||
value={image}
|
value={formState.image}
|
||||||
error={formErrors?.image}
|
error={formErrors?.image}
|
||||||
onInputChange={(_, value) => setImage(value)}
|
onInputChange={handleImageChange}
|
||||||
placeholder='Image URL'
|
placeholder='Image URL'
|
||||||
/>
|
/>
|
||||||
<InputFieldUncontrolled
|
<InputField
|
||||||
label='Summary'
|
label='Summary'
|
||||||
name='summary'
|
name='summary'
|
||||||
type='textarea'
|
type='textarea'
|
||||||
defaultValue={blog?.summary}
|
value={formState.summary}
|
||||||
error={formErrors?.summary}
|
error={formErrors?.summary}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
placeholder={'This is a quick description of my blog'}
|
||||||
/>
|
/>
|
||||||
<InputFieldUncontrolled
|
<InputField
|
||||||
label='Tags'
|
label='Tags'
|
||||||
description='Separate each tag with a comma. (Example: tag1, tag2, tag3)'
|
description='Separate each tag with a comma. (Example: tag1, tag2, tag3)'
|
||||||
placeholder='Tags'
|
placeholder='Tags'
|
||||||
name='tags'
|
name='tags'
|
||||||
defaultValue={blog?.tTags?.join(', ')}
|
value={formState.tags}
|
||||||
error={formErrors?.tags}
|
error={formErrors?.tags}
|
||||||
|
onChange={handleInputChange}
|
||||||
/>
|
/>
|
||||||
<CheckboxFieldUncontrolled
|
<CheckboxField
|
||||||
label='This post is not safe for work (NSFW)'
|
label='This post is not safe for work (NSFW)'
|
||||||
name='nsfw'
|
name='nsfw'
|
||||||
defaultChecked={blog?.nsfw}
|
isChecked={formState.nsfw}
|
||||||
|
handleChange={handleCheckboxChange}
|
||||||
|
type='stylized'
|
||||||
/>
|
/>
|
||||||
{typeof blog?.dTag !== 'undefined' && (
|
|
||||||
<input name='dTag' hidden value={blog.dTag} readOnly />
|
|
||||||
)}
|
|
||||||
{typeof blog?.rTag !== 'undefined' && (
|
|
||||||
<input name='rTag' hidden value={blog.rTag} readOnly />
|
|
||||||
)}
|
|
||||||
{typeof blog?.published_at !== 'undefined' && (
|
|
||||||
<input
|
|
||||||
name='published_at'
|
|
||||||
hidden
|
|
||||||
value={blog.published_at}
|
|
||||||
readOnly
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div className='IBMSMSMBS_WriteAction'>
|
<div className='IBMSMSMBS_WriteAction'>
|
||||||
<button
|
<button
|
||||||
className='btn btnMain'
|
className='btn btnMain'
|
||||||
@ -157,7 +201,7 @@ export const WritePage = () => {
|
|||||||
navigation.state === 'submitting'
|
navigation.state === 'submitting'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{blog ? 'Reset' : 'Clear fields'}
|
{isEditing ? 'Reset' : 'Clear fields'}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className='btn btnMain'
|
className='btn btnMain'
|
||||||
@ -178,13 +222,13 @@ export const WritePage = () => {
|
|||||||
handleClose={() => setShowConfirmPopup(false)}
|
handleClose={() => setShowConfirmPopup(false)}
|
||||||
header={'Are you sure?'}
|
header={'Are you sure?'}
|
||||||
label={
|
label={
|
||||||
blog
|
isEditing
|
||||||
? `Are you sure you want to clear all changes?`
|
? `Are you sure you want to clear all changes?`
|
||||||
: `Are you sure you want to clear all field data?`
|
: `Are you sure you want to clear all field data?`
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{userState.auth && userState.user?.pubkey && (
|
{userState.auth && userState.user?.pubkey && (
|
||||||
<ProfileSection pubkey={userState.user.pubkey as string} />
|
<ProfileSection pubkey={userState.user.pubkey as string} />
|
||||||
|
@ -20,9 +20,7 @@ export interface BlogDetails extends BlogForm {
|
|||||||
tTags: string[]
|
tTags: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BlogEventSubmitForm extends Omit<BlogForm, 'nsfw'> {
|
export interface BlogEventSubmitForm extends BlogForm {}
|
||||||
nsfw: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BlogEventEditForm extends BlogEventSubmitForm {
|
export interface BlogEventEditForm extends BlogEventSubmitForm {
|
||||||
dTag: string
|
dTag: string
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
import { NDKEvent } from '@nostr-dev-kit/ndk'
|
import { NDKEvent } from '@nostr-dev-kit/ndk'
|
||||||
import { BlogCardDetails, BlogDetails } from 'types'
|
import {
|
||||||
|
BlogCardDetails,
|
||||||
|
BlogDetails,
|
||||||
|
BlogEventEditForm,
|
||||||
|
BlogEventSubmitForm
|
||||||
|
} from 'types'
|
||||||
import { getFirstTagValue, getFirstTagValueAsInt, getTagValues } from './nostr'
|
import { getFirstTagValue, getFirstTagValueAsInt, getTagValues } from './nostr'
|
||||||
import { kinds, nip19 } from 'nostr-tools'
|
import { kinds, nip19 } from 'nostr-tools'
|
||||||
|
|
||||||
@ -50,3 +55,23 @@ export const extractBlogCardDetails = (
|
|||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const initializeBlogForm = (
|
||||||
|
blog?: Partial<BlogDetails>
|
||||||
|
): BlogEventSubmitForm | BlogEventEditForm => ({
|
||||||
|
content: blog?.content || '',
|
||||||
|
image: blog?.image || '',
|
||||||
|
nsfw: blog?.nsfw || false,
|
||||||
|
summary: blog?.summary || '',
|
||||||
|
title: blog?.title || '',
|
||||||
|
tags: blog?.tTags?.join(', ') || '',
|
||||||
|
...(blog?.aTag && {
|
||||||
|
aTag: blog.aTag
|
||||||
|
}),
|
||||||
|
...(blog?.dTag && {
|
||||||
|
dTag: blog.dTag
|
||||||
|
}),
|
||||||
|
...(blog?.published_at && {
|
||||||
|
published_at: blog.published_at
|
||||||
|
})
|
||||||
|
})
|
||||||
|
Loading…
x
Reference in New Issue
Block a user