diff --git a/src/pages/write/action.ts b/src/pages/write/action.ts index e0f33d3..daefbb4 100644 --- a/src/pages/write/action.ts +++ b/src/pages/write/action.ts @@ -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( - 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, diff --git a/src/pages/write/index.tsx b/src/pages/write/index.tsx index fe4f586..0153465 100644 --- a/src/pages/write/index.tsx +++ b/src/pages/write/index.tsx @@ -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(null) + // Enable cache for the new blog + const isEditing = typeof data?.blog !== 'undefined' + const [cache, setCache, clearCache] = + useLocalCache('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(null) const [showConfirmPopup, setShowConfirmPopup] = useState(false) - const handleReset = () => { + const handleReset = useCallback(() => { setShowConfirmPopup(true) - } - const handleResetConfirm = (confirm: boolean) => { - setShowConfirmPopup(false) + }, []) + const handleResetConfirm = useCallback( + (confirm: boolean) => { + setShowConfirmPopup(false) - // Cancel if not confirmed - if (!confirm) return + // 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) - } + // Reset editor + 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) => { + 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 (
@@ -71,82 +134,63 @@ export const WritePage = () => { {navigation.state === 'submitting' && ( )} -
- +
{ - setContent(md) - }} + markdown={formState.content} + onChange={handleEditorChange} />
{typeof formErrors?.content !== 'undefined' && ( )} - {/* encode to keep the markdown formatting */} -
setImage(value)} + onInputChange={handleImageChange} placeholder='Image URL' /> - - - - {typeof blog?.dTag !== 'undefined' && ( - - )} - {typeof blog?.rTag !== 'undefined' && ( - - )} - {typeof blog?.published_at !== 'undefined' && ( - - )} +
{userState.auth && userState.user?.pubkey && ( diff --git a/src/types/blog.ts b/src/types/blog.ts index 075a63c..813c76c 100644 --- a/src/types/blog.ts +++ b/src/types/blog.ts @@ -20,9 +20,7 @@ export interface BlogDetails extends BlogForm { tTags: string[] } -export interface BlogEventSubmitForm extends Omit { - nsfw: string -} +export interface BlogEventSubmitForm extends BlogForm {} export interface BlogEventEditForm extends BlogEventSubmitForm { dTag: string diff --git a/src/utils/blog.ts b/src/utils/blog.ts index 089e058..b937522 100644 --- a/src/utils/blog.ts +++ b/src/utils/blog.ts @@ -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 +): 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 + }) +})