Make rich text fields autosave to localstorage
This commit is contained in:
		
							parent
							
								
									b81c0b5e0e
								
							
						
					
					
						commit
						881e480819
					
				|  | @ -43,7 +43,7 @@ export function CreateChallengeModal(props: { | ||||||
|   const { user, contract, isOpen, setOpen } = props |   const { user, contract, isOpen, setOpen } = props | ||||||
|   const [challengeSlug, setChallengeSlug] = useState('') |   const [challengeSlug, setChallengeSlug] = useState('') | ||||||
|   const [loading, setLoading] = useState(false) |   const [loading, setLoading] = useState(false) | ||||||
|   const { editor } = useTextEditor({ placeholder: '' }) |   const { editor } = useTextEditor({ key: 'challenge'}) | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <Modal open={isOpen} setOpen={setOpen}> |     <Modal open={isOpen} setOpen={setOpen}> | ||||||
|  |  | ||||||
|  | @ -46,6 +46,7 @@ function RichEditContract(props: { contract: Contract; isAdmin?: boolean }) { | ||||||
|   const [isSubmitting, setIsSubmitting] = useState(false) |   const [isSubmitting, setIsSubmitting] = useState(false) | ||||||
| 
 | 
 | ||||||
|   const { editor, upload } = useTextEditor({ |   const { editor, upload } = useTextEditor({ | ||||||
|  |     // key: `description ${contract.id}`,
 | ||||||
|     max: MAX_DESCRIPTION_LENGTH, |     max: MAX_DESCRIPTION_LENGTH, | ||||||
|     defaultValue: contract.description, |     defaultValue: contract.description, | ||||||
|     disabled: isSubmitting, |     disabled: isSubmitting, | ||||||
|  |  | ||||||
|  | @ -21,6 +21,7 @@ export function CreatePost(props: { group?: Group }) { | ||||||
|   const { group } = props |   const { group } = props | ||||||
| 
 | 
 | ||||||
|   const { editor, upload } = useTextEditor({ |   const { editor, upload } = useTextEditor({ | ||||||
|  |     key: `post ${group?.id || ''}`, | ||||||
|     disabled: isSubmitting, |     disabled: isSubmitting, | ||||||
|   }) |   }) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -13,7 +13,7 @@ import StarterKit from '@tiptap/starter-kit' | ||||||
| import { Image } from '@tiptap/extension-image' | import { Image } from '@tiptap/extension-image' | ||||||
| import { Link } from '@tiptap/extension-link' | import { Link } from '@tiptap/extension-link' | ||||||
| import clsx from 'clsx' | import clsx from 'clsx' | ||||||
| import { useEffect, useState } from 'react' | import { useCallback, useEffect, useState } from 'react' | ||||||
| import { Linkify } from './linkify' | import { Linkify } from './linkify' | ||||||
| import { uploadImage } from 'web/lib/firebase/storage' | import { uploadImage } from 'web/lib/firebase/storage' | ||||||
| import { useMutation } from 'react-query' | import { useMutation } from 'react-query' | ||||||
|  | @ -42,6 +42,12 @@ import ItalicIcon from 'web/lib/icons/italic-icon' | ||||||
| import LinkIcon from 'web/lib/icons/link-icon' | import LinkIcon from 'web/lib/icons/link-icon' | ||||||
| import { getUrl } from 'common/util/parse' | import { getUrl } from 'common/util/parse' | ||||||
| import { TiptapSpoiler } from 'common/util/tiptap-spoiler' | import { TiptapSpoiler } from 'common/util/tiptap-spoiler' | ||||||
|  | import { | ||||||
|  |   storageStore, | ||||||
|  |   usePersistentState, | ||||||
|  | } from 'web/hooks/use-persistent-state' | ||||||
|  | import { safeLocalStorage } from 'web/lib/util/local' | ||||||
|  | import { debounce } from 'lodash' | ||||||
| 
 | 
 | ||||||
| const DisplayImage = Image.configure({ | const DisplayImage = Image.configure({ | ||||||
|   HTMLAttributes: { |   HTMLAttributes: { | ||||||
|  | @ -75,8 +81,20 @@ export function useTextEditor(props: { | ||||||
|   defaultValue?: Content |   defaultValue?: Content | ||||||
|   disabled?: boolean |   disabled?: boolean | ||||||
|   simple?: boolean |   simple?: boolean | ||||||
|  |   key?: string // unique key for this text field for autosave
 | ||||||
| }) { | }) { | ||||||
|   const { placeholder, max, defaultValue = '', disabled, simple } = props |   const { placeholder, max, defaultValue, disabled, simple, key } = props | ||||||
|  | 
 | ||||||
|  |   const [content, saveContent] = usePersistentState<JSONContent | undefined>( | ||||||
|  |     undefined, | ||||||
|  |     { | ||||||
|  |       key: `text ${key}`, | ||||||
|  |       store: storageStore(safeLocalStorage()), | ||||||
|  |     } | ||||||
|  |   ) | ||||||
|  | 
 | ||||||
|  |   // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||||
|  |   const save = useCallback(debounce(saveContent, 500), []) | ||||||
| 
 | 
 | ||||||
|   const editorClass = clsx( |   const editorClass = clsx( | ||||||
|     proseClass, |     proseClass, | ||||||
|  | @ -88,6 +106,7 @@ export function useTextEditor(props: { | ||||||
| 
 | 
 | ||||||
|   const editor = useEditor({ |   const editor = useEditor({ | ||||||
|     editorProps: { attributes: { class: editorClass, spellcheck: 'false' } }, |     editorProps: { attributes: { class: editorClass, spellcheck: 'false' } }, | ||||||
|  |     onUpdate: key ? ({ editor }) => save(editor.getJSON()) : undefined, | ||||||
|     extensions: [ |     extensions: [ | ||||||
|       StarterKit.configure({ |       StarterKit.configure({ | ||||||
|         heading: simple ? false : { levels: [1, 2, 3] }, |         heading: simple ? false : { levels: [1, 2, 3] }, | ||||||
|  | @ -113,7 +132,7 @@ export function useTextEditor(props: { | ||||||
|         spoilerOpenClass: 'rounded-sm bg-greyscale-2', |         spoilerOpenClass: 'rounded-sm bg-greyscale-2', | ||||||
|       }), |       }), | ||||||
|     ], |     ], | ||||||
|     content: defaultValue, |     content: defaultValue ?? (key && content ? content : ''), | ||||||
|   }) |   }) | ||||||
| 
 | 
 | ||||||
|   const upload = useUploadMutation(editor) |   const upload = useUploadMutation(editor) | ||||||
|  |  | ||||||
|  | @ -23,7 +23,6 @@ import { ChoicesToggleGroup } from 'web/components/choices-toggle-group' | ||||||
| import { getGroup, groupPath } from 'web/lib/firebase/groups' | import { getGroup, groupPath } from 'web/lib/firebase/groups' | ||||||
| import { Group } from 'common/group' | import { Group } from 'common/group' | ||||||
| import { useTracking } from 'web/hooks/use-tracking' | import { useTracking } from 'web/hooks/use-tracking' | ||||||
| import { useWarnUnsavedChanges } from 'web/hooks/use-warn-unsaved-changes' |  | ||||||
| import { track } from 'web/lib/service/analytics' | import { track } from 'web/lib/service/analytics' | ||||||
| import { GroupSelector } from 'web/components/groups/group-selector' | import { GroupSelector } from 'web/components/groups/group-selector' | ||||||
| import { User } from 'common/user' | import { User } from 'common/user' | ||||||
|  | @ -228,6 +227,7 @@ export function NewContract(props: { | ||||||
|       : `e.g. I will choose the answer according to...` |       : `e.g. I will choose the answer according to...` | ||||||
| 
 | 
 | ||||||
|   const { editor, upload } = useTextEditor({ |   const { editor, upload } = useTextEditor({ | ||||||
|  |     key: 'create market', | ||||||
|     max: MAX_DESCRIPTION_LENGTH, |     max: MAX_DESCRIPTION_LENGTH, | ||||||
|     placeholder: descriptionPlaceholder, |     placeholder: descriptionPlaceholder, | ||||||
|     disabled: isSubmitting, |     disabled: isSubmitting, | ||||||
|  | @ -236,9 +236,6 @@ export function NewContract(props: { | ||||||
|       : undefined, |       : undefined, | ||||||
|   }) |   }) | ||||||
| 
 | 
 | ||||||
|   const isEditorFilled = editor != null && !editor.isEmpty |  | ||||||
|   useWarnUnsavedChanges(!isSubmitting && (Boolean(question) || isEditorFilled)) |  | ||||||
| 
 |  | ||||||
|   function setCloseDateInDays(days: number) { |   function setCloseDateInDays(days: number) { | ||||||
|     const newCloseDate = dayjs().add(days, 'day').format('YYYY-MM-DD') |     const newCloseDate = dayjs().add(days, 'day').format('YYYY-MM-DD') | ||||||
|     setCloseDate(newCloseDate) |     setCloseDate(newCloseDate) | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user