Skip to content

Commit

Permalink
feat: autosave
Browse files Browse the repository at this point in the history
Signed-off-by: Innei <i@innei.in>
  • Loading branch information
Innei committed Jan 10, 2024
1 parent 0826096 commit 6daaf06
Show file tree
Hide file tree
Showing 6 changed files with 131 additions and 22 deletions.
17 changes: 10 additions & 7 deletions src/app/(dashboard)/dashboard/notes/edit/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ import {
useNoteModelSetModelData,
} from '~/components/modules/dashboard/note-editing'
import { NoteNid } from '~/components/modules/dashboard/note-editing/NoteNid'
import { BaseWritingProvider } from '~/components/modules/dashboard/writing/BaseWritingProvider'
import {
BaseWritingProvider,
useAutoSaver,
} from '~/components/modules/dashboard/writing/BaseWritingProvider'
import { EditorLayer } from '~/components/modules/dashboard/writing/EditorLayer'
import { ImportMarkdownButton } from '~/components/modules/dashboard/writing/ImportMarkdownButton'
import { PreviewButton } from '~/components/modules/dashboard/writing/PreviewButton'
Expand All @@ -27,8 +30,7 @@ import {
Writing,
} from '~/components/modules/dashboard/writing/Writing'
import { LoadingButtonWrapper, StyledButton } from '~/components/ui/button'
import { EmitKeyMap } from '~/constants/keys'
import { PublishEvent } from '~/events'
import { PublishEvent, WriteEditEvent } from '~/events'
import { useEventCallback } from '~/hooks/common/use-event-callback'
import { cloneDeep } from '~/lib/_'
import { dayOfYear } from '~/lib/datetime'
Expand Down Expand Up @@ -74,25 +76,26 @@ const createInitialEditingData = (): NoteDto => {
const EditPage: FC<{
initialData?: NoteDto
}> = (props) => {
const [editingData] = useState<NoteDto>(() =>
const [editingData, setEditingData] = useState<NoteDto>(() =>
props.initialData
? cloneDeep(props.initialData)
: createInitialEditingData(),
)
const [forceUpdateKey] = useAutoSaver([editingData, setEditingData])

const editingAtom = useMemo(() => atom(editingData), [editingData])
const created = editingData.created ? new Date(editingData.created) : null

const store = useStore()
useEffect(() => {
return store.sub(editingAtom, () => {
window.dispatchEvent(new CustomEvent(EmitKeyMap.EditDataUpdate))
window.dispatchEvent(new WriteEditEvent(store.get(editingAtom)))
})
}, [editingAtom, store])

const isMobile = useIsMobile()
return (
<NoteModelDataAtomProvider overrideAtom={editingAtom}>
<NoteModelDataAtomProvider overrideAtom={editingAtom} key={forceUpdateKey}>
<BaseWritingProvider atom={editingAtom}>
<EditorLayer>
{isMobile ? (
Expand Down Expand Up @@ -169,8 +172,8 @@ const ActionButtonGroup = ({ initialData }: { initialData?: NoteDto }) => {
<PreviewButton
getData={() => {
return {
id: 'preview',
...getData(),
id: 'preview',
}
}}
/>
Expand Down
17 changes: 10 additions & 7 deletions src/app/(dashboard)/dashboard/posts/edit/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ import {
usePostModelGetModelData,
usePostModelSetModelData,
} from '~/components/modules/dashboard/post-editing'
import { BaseWritingProvider } from '~/components/modules/dashboard/writing/BaseWritingProvider'
import {
BaseWritingProvider,
useAutoSaver,
} from '~/components/modules/dashboard/writing/BaseWritingProvider'
import { EditorLayer } from '~/components/modules/dashboard/writing/EditorLayer'
import { ImportMarkdownButton } from '~/components/modules/dashboard/writing/ImportMarkdownButton'
import { PreviewButton } from '~/components/modules/dashboard/writing/PreviewButton'
Expand All @@ -27,8 +30,7 @@ import {
Writing,
} from '~/components/modules/dashboard/writing/Writing'
import { LoadingButtonWrapper, StyledButton } from '~/components/ui/button'
import { EmitKeyMap } from '~/constants/keys'
import { PublishEvent } from '~/events'
import { PublishEvent, WriteEditEvent } from '~/events'
import { useEventCallback } from '~/hooks/common/use-event-callback'
import { cloneDeep } from '~/lib/_'
import { toast } from '~/lib/toast'
Expand Down Expand Up @@ -79,23 +81,24 @@ const createInitialEditingData = (): PostDto => {
const EditPage: FC<{
initialData?: PostDto
}> = (props) => {
const [editingData] = useState<PostDto>(() =>
const [editingData, setEditingData] = useState<PostDto>(() =>
props.initialData
? cloneDeep(props.initialData)
: createInitialEditingData(),
)

const [forceUpdateKey] = useAutoSaver([editingData, setEditingData])
const editingAtom = useMemo(() => atom(editingData), [editingData])
const store = useStore()
useEffect(() => {
return store.sub(editingAtom, () => {
window.dispatchEvent(new CustomEvent(EmitKeyMap.EditDataUpdate))
window.dispatchEvent(new WriteEditEvent(store.get(editingAtom)))
})
}, [editingAtom, store])

const isMobile = useIsMobile()
return (
<PostModelDataAtomProvider overrideAtom={editingAtom}>
<PostModelDataAtomProvider overrideAtom={editingAtom} key={forceUpdateKey}>
<BaseWritingProvider atom={editingAtom}>
<EditorLayer>
{isMobile ? (
Expand Down Expand Up @@ -164,8 +167,8 @@ const ActionButtonGroup = ({ initialData }: { initialData?: PostDto }) => {
<PreviewButton
getData={() => {
return {
id: 'preview',
...getData(),
id: 'preview',
}
}}
/>
Expand Down
106 changes: 99 additions & 7 deletions src/components/modules/dashboard/writing/BaseWritingProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import { createContext, useContext, useEffect, useMemo, useState } from 'react'
import { useForceUpdate } from 'framer-motion'
import { produce } from 'immer'
import { atom, useAtom } from 'jotai'
import type { WriteEditEvent } from '~/events'
import type { PrimitiveAtom } from 'jotai'
import type { PropsWithChildren } from 'react'
import type { Dispatch, FC, PropsWithChildren, SetStateAction } from 'react'

import { StyledButton } from '~/components/ui/button'
import { useModalStack } from '~/components/ui/modal'
import { EmitKeyMap } from '~/constants/keys'
import { useBeforeUnload } from '~/hooks/common/use-before-unload'
import { throttle } from '~/lib/_'
import { buildNSKey } from '~/lib/ns'

const BaseWritingContext = createContext<PrimitiveAtom<BaseModelType>>(null!)

Expand Down Expand Up @@ -33,16 +39,102 @@ export const BaseWritingProvider = <T extends BaseModelType>(
}, [])
useBeforeUnload(isFormDirty)

useBeforeUnload.forceRoute(() => {
console.log('forceRoute')
})
return (
<BaseWritingContext.Provider value={props.atom as any}>
{props.children}
</BaseWritingContext.Provider>
<AutoSaverProvider>
<BaseWritingContext.Provider value={props.atom as any}>
{props.children}
</BaseWritingContext.Provider>
</AutoSaverProvider>
)
}

const AutoSaverContext = createContext({
reset(type: 'note' | 'post', id?: string) {},
})

const AutoSaverProvider: FC<PropsWithChildren> = ({ children }) => {
useEffect(() => {
const handler = throttle((e: any) => {
const ev = e as WriteEditEvent

const dto = ev.data
const id = dto.id || ('categoryId' in dto ? 'new-post' : 'new-note')
const nsKey = buildNSKey(`auto-save-${id}`)

console.debug('auto save', dto)
localStorage.setItem(nsKey, JSON.stringify(dto))
}, 300)
window.addEventListener(EmitKeyMap.EditDataUpdate, handler)

return () => {
window.removeEventListener(EmitKeyMap.EditDataUpdate, handler)
}
}, [])

return (
<AutoSaverContext.Provider
value={useMemo(() => {
return {
reset(type, nsKey?: string) {
const id = nsKey || (type === 'note' ? 'new-note' : 'new-post')
nsKey = buildNSKey(`auto-save-${id}`)
localStorage.removeItem(nsKey)
},
}
}, [])}
>
{children}
</AutoSaverContext.Provider>
)
}

export const useResetAutoSaverData = () => useContext(AutoSaverContext).reset

export const useAutoSaver = <T extends { id: string }>([
editingData,
setEditingData,
]: [T, Dispatch<SetStateAction<T>>]) => {
const { present } = useModalStack()
const [forceUpdate, forceUpdateKey] = useForceUpdate()

useEffect(() => {
const id =
editingData.id || ('categoryId' in editingData ? 'new-post' : 'new-note')
const nsKey = buildNSKey(`auto-save-${id}`)

console.log('recovery key', nsKey)
const autoSavedDataString = localStorage.getItem(nsKey)

if (!autoSavedDataString) return
const autoSavedData = JSON.parse(autoSavedDataString)
if (!autoSavedData) return

console.log('recovery data', autoSavedData)

setTimeout(() => {
present({
title: '存在为保存的数据,需要恢复吗?',
content: ({ dismiss }) => (
<div className="flex justify-end">
<StyledButton
onClick={() => {
dismiss()
setEditingData(autoSavedData)
forceUpdate()
localStorage.removeItem(nsKey)
}}
>
恢复
</StyledButton>
</div>
),
})
}, 100)
}, [editingData?.id, forceUpdate, present])

return [forceUpdateKey]
}

export const useBaseWritingContext = () => {
return useContext(BaseWritingContext)
}
Expand Down
1 change: 1 addition & 0 deletions src/events/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './publish'
export * from './write-edit'
10 changes: 10 additions & 0 deletions src/events/write-edit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { NoteDto, PostDto } from '~/models/writing'

import { EmitKeyMap } from '~/constants/keys'

export class WriteEditEvent extends Event {
static readonly type = EmitKeyMap.EditDataUpdate
constructor(public readonly data: NoteDto | PostDto) {
super(EmitKeyMap.EditDataUpdate)
}
}
2 changes: 1 addition & 1 deletion src/models/writing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export type WriteBaseType = {
text: string
allowComment: boolean

id?: string
id: string
images: Image[]
created?: string
modified?: string
Expand Down

1 comment on commit 6daaf06

@vercel
Copy link

@vercel vercel bot commented on 6daaf06 Jan 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

shiro – ./

innei.in
springtide.vercel.app
shiro-innei.vercel.app
shiro-git-main-innei.vercel.app

Please sign in to comment.