diff --git a/src/components/modules/dashboard/home/Version.tsx b/src/components/modules/dashboard/home/Version.tsx
index 6af7fef3bd..0d0ed9cfde 100644
--- a/src/components/modules/dashboard/home/Version.tsx
+++ b/src/components/modules/dashboard/home/Version.tsx
@@ -18,7 +18,6 @@ export const Version = () => {
)
- console.log(version, 'a')
return (
diff --git a/src/components/modules/dashboard/writing/BaseWritingProvider.tsx b/src/components/modules/dashboard/writing/BaseWritingProvider.tsx
index f58b6cddcf..a49e578707 100644
--- a/src/components/modules/dashboard/writing/BaseWritingProvider.tsx
+++ b/src/components/modules/dashboard/writing/BaseWritingProvider.tsx
@@ -1,9 +1,12 @@
-import { createContext, useContext, useMemo } from 'react'
+import { createContext, useContext, useEffect, useMemo, useState } from 'react'
import { produce } from 'immer'
import { atom, useAtom } from 'jotai'
import type { PrimitiveAtom } from 'jotai'
import type { PropsWithChildren } from 'react'
+import { EmitKeyMap } from '~/constants/keys'
+import { useBeforeUnload } from '~/hooks/common/use-before-unload'
+
const BaseWritingContext = createContext>(null!)
type BaseModelType = {
@@ -17,6 +20,22 @@ type BaseModelType = {
export const BaseWritingProvider = (
props: { atom: PrimitiveAtom } & PropsWithChildren,
) => {
+ const [isFormDirty, setIsDirty] = useState(false)
+ useEffect(() => {
+ const handler = () => {
+ setIsDirty(true)
+ }
+ window.addEventListener(EmitKeyMap.EditDataUpdate, handler)
+
+ return () => {
+ window.removeEventListener(EmitKeyMap.EditDataUpdate, handler)
+ }
+ }, [])
+ useBeforeUnload(isFormDirty)
+
+ useBeforeUnload.forceRoute(() => {
+ console.log('forceRoute')
+ })
return (
{props.children}
diff --git a/src/hooks/common/use-before-unload.ts b/src/hooks/common/use-before-unload.ts
new file mode 100644
index 0000000000..975acda7b4
--- /dev/null
+++ b/src/hooks/common/use-before-unload.ts
@@ -0,0 +1,148 @@
+/// useBeforeUnload.ts
+'use client'
+
+import { useEffect, useId } from 'react'
+import { useRouter } from 'next/navigation'
+
+let isForceRouting = false
+const activeIds: string[] = []
+let lastKnownHref: string
+
+export const useBeforeUnload = (isActive = true) => {
+ const id = useId()
+
+ // Handle clicks & onbeforeunload(attemptimg to close/refresh browser)
+ useEffect(() => {
+ if (!isActive) return
+ lastKnownHref = window.location.href
+
+ activeIds.push(id)
+
+ const handleAnchorClick = (e: Event) => {
+ const targetUrl = (e.currentTarget as HTMLAnchorElement).href,
+ currentUrl = window.location.href
+
+ if (targetUrl !== currentUrl) {
+ const res = beforeUnloadFn()
+ if (!res) e.preventDefault()
+ lastKnownHref = window.location.href
+ }
+ }
+
+ let anchorElements: HTMLAnchorElement[] = []
+
+ const disconnectAnchors = () => {
+ anchorElements.forEach((anchor) => {
+ anchor.removeEventListener('click', handleAnchorClick)
+ })
+ }
+
+ const handleMutation = () => {
+ disconnectAnchors()
+
+ anchorElements = Array.from(document.querySelectorAll('a[href]'))
+ anchorElements.forEach((anchor) => {
+ anchor.addEventListener('click', handleAnchorClick)
+ })
+ }
+
+ const mutationObserver = new MutationObserver(handleMutation)
+ mutationObserver.observe(document.body, { childList: true, subtree: true })
+ addEventListener('beforeunload', beforeUnloadFn)
+
+ return () => {
+ removeEventListener('beforeunload', beforeUnloadFn)
+ disconnectAnchors()
+ mutationObserver.disconnect()
+
+ activeIds.splice(activeIds.indexOf(id), 1)
+ }
+ }, [isActive, id])
+}
+
+const beforeUnloadFn = (event?: BeforeUnloadEvent) => {
+ if (isForceRouting) return true
+
+ const message = 'Discard unsaved changes?'
+
+ if (event) {
+ event.returnValue = message
+ return message
+ } else {
+ return confirm(message)
+ }
+}
+
+const BeforeUnloadProvider = ({ children }: React.PropsWithChildren) => {
+ const router = useRouter()
+ useEffect(() => {
+ lastKnownHref = window.location.href
+ })
+
+ // Hack nextjs13 popstate impl, so it will include route cancellation.
+ // This Provider has to be rendered in the layout phase wrapping the page.
+ useEffect(() => {
+ let nextjsPopStateHandler: (...args: any[]) => void
+
+ function popStateHandler(...args: any[]) {
+ useBeforeUnload.ensureSafeNavigation(
+ () => {
+ nextjsPopStateHandler(...args)
+ lastKnownHref = window.location.href
+ },
+ () => {
+ router.replace(lastKnownHref, { scroll: false })
+ },
+ )
+ }
+
+ addEventListener('popstate', popStateHandler)
+ const originalAddEventListener = window.addEventListener
+ window.addEventListener = (...args: any[]) => {
+ if (args[0] === 'popstate') {
+ nextjsPopStateHandler = args[1]
+ window.addEventListener = originalAddEventListener
+ } else {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ originalAddEventListener(...args)
+ }
+ }
+
+ originalAddEventListener('popstate', (e) => {
+ e.preventDefault()
+ })
+
+ // window.addEventListener('popstate', () => {
+ // history.pushState(null, '', null)
+ // })
+ return () => {
+ window.addEventListener = originalAddEventListener
+ removeEventListener('popstate', popStateHandler)
+ }
+ }, [])
+
+ return children
+}
+
+useBeforeUnload.Provider = BeforeUnloadProvider
+
+useBeforeUnload.forceRoute = async (cb: () => void | Promise) => {
+ try {
+ isForceRouting = true
+ await cb()
+ } finally {
+ isForceRouting = false
+ }
+}
+
+useBeforeUnload.ensureSafeNavigation = (
+ onPerformRoute: () => void,
+ onRouteRejected?: () => void,
+) => {
+ if (activeIds.length === 0 || beforeUnloadFn()) {
+ onPerformRoute()
+ } else {
+ onRouteRejected?.()
+ }
+}
diff --git a/src/providers/root/index.tsx b/src/providers/root/index.tsx
index 85c360aa68..6b44967bed 100644
--- a/src/providers/root/index.tsx
+++ b/src/providers/root/index.tsx
@@ -11,6 +11,7 @@ import type { PropsWithChildren } from 'react'
import { PeekPortal } from '~/components/modules/peek/PeekPortal'
import { ModalStackProvider } from '~/components/ui/modal'
+import { useBeforeUnload } from '~/hooks/common/use-before-unload'
import { ProviderComposer } from '../../components/common/ProviderComposer'
import { AuthProvider } from './auth-provider'
@@ -58,6 +59,7 @@ export function WebAppProviders({ children }: PropsWithChildren) {
const dashboardContexts: JSX.Element[] = baseContexts.concat(
,
,
+ ,
)
export function DashboardAppProviders({ children }: PropsWithChildren) {
return (
diff --git a/src/queries/definition/note.ts b/src/queries/definition/note.ts
index 5a230ece66..8b72121d2b 100644
--- a/src/queries/definition/note.ts
+++ b/src/queries/definition/note.ts
@@ -1,5 +1,6 @@
import { useMutation } from '@tanstack/react-query'
import dayjs from 'dayjs'
+import { revalidateTag } from 'next/cache'
import type { NoteModel, NoteWrappedPayload } from '@mx-space/api-client'
import type { NoteDto } from '~/models/writing'
@@ -92,6 +93,7 @@ export const useCreateNote = () =>
})
},
onSuccess: () => {
+ revalidateTag('note')
toast.success('εε»Ίζε')
},
})