Cache forms #187

Merged
enes merged 9 commits from 166-caching-fields into staging 2025-01-09 13:45:45 +00:00
4 changed files with 150 additions and 93 deletions
Showing only changes of commit 60773ec446 - Show all commits

View File

@ -2,14 +2,7 @@ import { NDKContextType } from 'contexts/NDKContext'
import { ActionFunctionArgs, redirect } from 'react-router-dom'
import { getBlogPageRoute } from 'routes'
import { BlogFormErrors, BlogEventSubmitForm, BlogEventEditForm } from 'types'
import {
isReachable,
isValidImageUrl,
log,
LogType,
now,
parseFormData
} from 'utils'
import { isReachable, isValidImageUrl, log, LogType, now } from 'utils'
import { kinds, UnsignedEvent, Event, nip19 } from 'nostr-tools'
import { toast } from 'react-toastify'
import { NDKEvent } from '@nostr-dev-kit/ndk'
@ -43,12 +36,9 @@ export const writeRouteAction =
}
// Get the form data from submit request
const formData = await request.formData()
// Parse the the data
const formSubmit = parseFormData<BlogEventSubmitForm | BlogEventEditForm>(
formData
)
const formSubmit = (await request.json()) as
| BlogEventSubmitForm
| BlogEventEditForm
// Check for errors
const formErrors = await validateFormData(formSubmit)
@ -80,7 +70,7 @@ export const writeRouteAction =
const tTags = formSubmit
.tags!.toLowerCase()
.split(',')
.map((t) => ['t', t])
.map((t) => ['t', t.trim()])
const tags = [
['d', uuid],
@ -95,7 +85,7 @@ export const writeRouteAction =
// Add NSFW tag, L label namespace standardized tag
// 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 = {
kind: kinds.LongFormArticle,

View File

@ -1,60 +1,123 @@
import { useRef, useState } from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
import {
Form,
useActionData,
useLoaderData,
useNavigation
useNavigation,
useSubmit
} from 'react-router-dom'
import {
CheckboxFieldUncontrolled,
InputFieldUncontrolled,
CheckboxField,
InputField,
InputFieldWithImageUpload
} from '../../components/Inputs'
import { ProfileSection } from '../../components/ProfileSection'
import { useAppSelector } from '../../hooks'
import { BlogFormErrors, BlogPageLoaderResult } from 'types'
import '../../styles/innerPage.css'
import '../../styles/styles.css'
import '../../styles/write.css'
} from 'components/Inputs'
import { ProfileSection } from 'components/ProfileSection'
import { useAppSelector, useLocalCache } from 'hooks'
import {
BlogEventEditForm,
BlogEventSubmitForm,
BlogFormErrors,
BlogPageLoaderResult
} from 'types'
import { LoadingSpinner } from 'components/LoadingSpinner'
import { AlertPopup } from 'components/AlertPopup'
import { Editor, EditorRef } from 'components/Markdown/Editor'
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 = () => {
const userState = useAppSelector((state) => state.user)
const data = useLoaderData() as BlogPageLoaderResult
const formErrors = useActionData() as BlogFormErrors
const navigation = useNavigation()
const submit = useSubmit()
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 [showConfirmPopup, setShowConfirmPopup] = useState<boolean>(false)
const handleReset = () => {
const handleReset = useCallback(() => {
setShowConfirmPopup(true)
}
const handleResetConfirm = (confirm: boolean) => {
}, [])
const handleResetConfirm = useCallback(
(confirm: boolean) => {
setShowConfirmPopup(false)
// Cancel if not confirmed
if (!confirm) return
// Reset featured image
setImage(blog?.image || '')
const initialState = initializeBlogForm(blog)
// Reset editor
if (blog?.content) {
editorRef.current?.setMarkdown(blog?.content)
}
editorRef.current?.setMarkdown(initialState.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 (
<div className='InnerBodyMain'>
@ -71,82 +134,63 @@ export const WritePage = () => {
{navigation.state === 'submitting' && (
<LoadingSpinner desc='Publishing blog to relays' />
)}
<Form
ref={formRef}
className='IBMSMSMBS_Write'
method={blog ? 'put' : 'post'}
>
<InputFieldUncontrolled
<form className='IBMSMSMBS_Write' onSubmit={handleFormSubmit}>
<InputField
label='Title'
name='title'
defaultValue={blog?.title}
value={formState.title}
error={formErrors?.title}
onChange={handleInputChange}
placeholder='Blog title'
/>
<div className='inputLabelWrapperMain'>
<label className='form-label labelMain'>Content</label>
<div className='inputMain'>
<Editor
ref={editorRef}
markdown={content}
onChange={(md) => {
setContent(md)
}}
markdown={formState.content}
onChange={handleEditorChange}
/>
</div>
{typeof formErrors?.content !== 'undefined' && (
<InputError message={formErrors?.content} />
)}
{/* encode to keep the markdown formatting */}
<input
name='content'
hidden
value={encodeURIComponent(content)}
readOnly
/>
</div>
<InputFieldWithImageUpload
label='Featured Image URL'
name='image'
inputMode='url'
value={image}
value={formState.image}
error={formErrors?.image}
onInputChange={(_, value) => setImage(value)}
onInputChange={handleImageChange}
placeholder='Image URL'
/>
<InputFieldUncontrolled
<InputField
label='Summary'
name='summary'
type='textarea'
defaultValue={blog?.summary}
value={formState.summary}
error={formErrors?.summary}
onChange={handleInputChange}
placeholder={'This is a quick description of my blog'}
/>
<InputFieldUncontrolled
<InputField
label='Tags'
description='Separate each tag with a comma. (Example: tag1, tag2, tag3)'
placeholder='Tags'
name='tags'
defaultValue={blog?.tTags?.join(', ')}
value={formState.tags}
error={formErrors?.tags}
onChange={handleInputChange}
/>
<CheckboxFieldUncontrolled
<CheckboxField
label='This post is not safe for work (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'>
<button
className='btn btnMain'
@ -157,7 +201,7 @@ export const WritePage = () => {
navigation.state === 'submitting'
}
>
{blog ? 'Reset' : 'Clear fields'}
{isEditing ? 'Reset' : 'Clear fields'}
</button>
<button
className='btn btnMain'
@ -178,13 +222,13 @@ export const WritePage = () => {
handleClose={() => setShowConfirmPopup(false)}
header={'Are you sure?'}
label={
blog
isEditing
? `Are you sure you want to clear all changes?`
: `Are you sure you want to clear all field data?`
}
/>
)}
</Form>
</form>
</div>
{userState.auth && userState.user?.pubkey && (
<ProfileSection pubkey={userState.user.pubkey as string} />

View File

@ -20,9 +20,7 @@ export interface BlogDetails extends BlogForm {
tTags: string[]
}
export interface BlogEventSubmitForm extends Omit<BlogForm, 'nsfw'> {
nsfw: string
}
export interface BlogEventSubmitForm extends BlogForm {}
export interface BlogEventEditForm extends BlogEventSubmitForm {
dTag: string

View File

@ -1,5 +1,10 @@
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 { kinds, nip19 } from 'nostr-tools'
@ -50,3 +55,23 @@ export const extractBlogCardDetails = (
: 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
})
})