広場投稿追加画面の刷新 (#399) #413
+41
-41
@@ -155,7 +155,7 @@ const PostDetailRoute = ({ user }: { user: User | null }) => {
|
||||
}
|
||||
|
||||
|
||||
const App: FC = () => {
|
||||
const RoutedApp: FC = () => {
|
||||
const [user, setUser] = useState<User | null> (null)
|
||||
const [status, setStatus] = useState (200)
|
||||
const behaviourSettings = useClientBehaviourSettings ()
|
||||
@@ -172,41 +172,6 @@ const App: FC = () => {
|
||||
),
|
||||
[animationMode],
|
||||
)
|
||||
const router = useMemo (
|
||||
() => createBrowserRouter ([{
|
||||
path: '*',
|
||||
element: (
|
||||
<DialogueProvider>
|
||||
<UnsavedChangesGuardProvider>
|
||||
<KeyboardShortcutsProvider>
|
||||
<MotionConfig
|
||||
reducedMotion={
|
||||
animationMode === 'normal'
|
||||
? 'never'
|
||||
: animationMode === 'reduced'
|
||||
? 'user'
|
||||
: 'always'
|
||||
}>
|
||||
<LayoutWrapper>
|
||||
<motion.div
|
||||
layout={animationMode === 'off' ? false : 'position'}
|
||||
transition={{ layout: appLayoutTransition }}
|
||||
className="relative flex h-dvh w-full flex-col overflow-y-hidden">
|
||||
<TopNav user={user}/>
|
||||
<RouteTransitionWrapper
|
||||
animationMode={animationMode}
|
||||
user={user}
|
||||
setUser={setUser}/>
|
||||
</motion.div>
|
||||
</LayoutWrapper>
|
||||
</MotionConfig>
|
||||
|
||||
<Toaster/>
|
||||
</KeyboardShortcutsProvider>
|
||||
</UnsavedChangesGuardProvider>
|
||||
</DialogueProvider>) }]),
|
||||
[LayoutWrapper, animationMode, appLayoutTransition, setUser, user],
|
||||
)
|
||||
|
||||
useEffect (() => {
|
||||
applyClientAppearance ()
|
||||
@@ -289,11 +254,46 @@ const App: FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<RouteBlockerOverlay/>
|
||||
{import.meta.env.DEV && <DevModeWatermark/>}
|
||||
<RouterProvider router={router}/>
|
||||
</>)
|
||||
<DialogueProvider>
|
||||
<UnsavedChangesGuardProvider>
|
||||
<KeyboardShortcutsProvider>
|
||||
<MotionConfig
|
||||
reducedMotion={
|
||||
animationMode === 'normal'
|
||||
? 'never'
|
||||
: animationMode === 'reduced'
|
||||
? 'user'
|
||||
: 'always'
|
||||
}>
|
||||
<LayoutWrapper>
|
||||
<motion.div
|
||||
layout={animationMode === 'off' ? false : 'position'}
|
||||
transition={{ layout: appLayoutTransition }}
|
||||
className="relative flex h-dvh w-full flex-col overflow-y-hidden">
|
||||
<TopNav user={user}/>
|
||||
<RouteTransitionWrapper
|
||||
animationMode={animationMode}
|
||||
user={user}
|
||||
setUser={setUser}/>
|
||||
</motion.div>
|
||||
</LayoutWrapper>
|
||||
</MotionConfig>
|
||||
|
||||
<Toaster/>
|
||||
</KeyboardShortcutsProvider>
|
||||
</UnsavedChangesGuardProvider>
|
||||
</DialogueProvider>)
|
||||
}
|
||||
|
||||
const router = createBrowserRouter ([{
|
||||
path: '*',
|
||||
element: <RoutedApp/> }])
|
||||
|
||||
const App: FC = () => (
|
||||
<>
|
||||
<RouteBlockerOverlay/>
|
||||
{import.meta.env.DEV && <DevModeWatermark/>}
|
||||
<RouterProvider router={router}/>
|
||||
</>)
|
||||
|
||||
export default App
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useBlocker, useLocation } from 'react-router-dom'
|
||||
import { useBlocker } from 'react-router-dom'
|
||||
|
||||
import { useDialogue } from '@/lib/dialogues/useDialogue'
|
||||
|
||||
@@ -34,7 +34,6 @@ const UnsavedChangesGuardContext =
|
||||
|
||||
export const UnsavedChangesGuardProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||
const dialogue = useDialogue ()
|
||||
const location = useLocation ()
|
||||
const sourcesRef = useRef (new Map<symbol, UnsavedChangesSource> ())
|
||||
const bypassNextNavigationRef = useRef (false)
|
||||
const [revision, setRevision] = useState (0)
|
||||
@@ -68,12 +67,19 @@ export const UnsavedChangesGuardProvider: FC<PropsWithChildren> = ({ children })
|
||||
[sources],
|
||||
)
|
||||
const hasUnsavedChanges = dirtySources.length > 0
|
||||
const blocker = useBlocker (
|
||||
useCallback (
|
||||
() => hasUnsavedChanges && !(bypassNextNavigationRef.current),
|
||||
[hasUnsavedChanges],
|
||||
),
|
||||
)
|
||||
const shouldBlock = useCallback (() => {
|
||||
if (!(hasUnsavedChanges))
|
||||
return false
|
||||
|
||||
if (bypassNextNavigationRef.current)
|
||||
{
|
||||
bypassNextNavigationRef.current = false
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}, [hasUnsavedChanges])
|
||||
const blocker = useBlocker (shouldBlock)
|
||||
|
||||
const discardDirtyChanges = useCallback (async (): Promise<boolean> => {
|
||||
if (!(hasUnsavedChanges))
|
||||
@@ -93,8 +99,6 @@ export const UnsavedChangesGuardProvider: FC<PropsWithChildren> = ({ children })
|
||||
if (!(confirmed))
|
||||
return false
|
||||
|
||||
bypassNextNavigationRef.current = true
|
||||
|
||||
try
|
||||
{
|
||||
for (const source of dirtySources)
|
||||
@@ -102,7 +106,6 @@ export const UnsavedChangesGuardProvider: FC<PropsWithChildren> = ({ children })
|
||||
}
|
||||
catch
|
||||
{
|
||||
bypassNextNavigationRef.current = false
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -139,10 +142,6 @@ export const UnsavedChangesGuardProvider: FC<PropsWithChildren> = ({ children })
|
||||
}
|
||||
}, [blocker, discardDirtyChanges])
|
||||
|
||||
useEffect (() => {
|
||||
bypassNextNavigationRef.current = false
|
||||
}, [location.hash, location.pathname, location.search])
|
||||
|
||||
useEffect (() => {
|
||||
if (!(hasUnsavedChanges))
|
||||
return
|
||||
@@ -183,14 +182,16 @@ export const useUnsavedChangesGuard = (
|
||||
if (context == null)
|
||||
throw new Error ('UnsavedChangesGuardProvider が必要です.')
|
||||
|
||||
const { registerUnsavedChangesSource } = context
|
||||
|
||||
useEffect (() => {
|
||||
if (options == null)
|
||||
return
|
||||
|
||||
return context.registerUnsavedChangesSource ({
|
||||
return registerUnsavedChangesSource ({
|
||||
dirty: options.dirty,
|
||||
discard: options.onDiscard ?? (() => undefined) })
|
||||
}, [context, options?.dirty, options?.onDiscard])
|
||||
}, [options?.dirty, options?.onDiscard, registerUnsavedChangesSource])
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
新しい課題から参照
ユーザをブロックする