Skip to content

Commit f9964ed

Browse files
x0sinaImMohammad20000
authored andcommitted
refactor(page-transition): simplify transition logic
1 parent 1470437 commit f9964ed

File tree

1 file changed

+87
-188
lines changed

1 file changed

+87
-188
lines changed
Lines changed: 87 additions & 188 deletions
Original file line numberDiff line numberDiff line change
@@ -1,237 +1,136 @@
1-
import { ReactNode, useEffect, useState, useRef } from 'react'
1+
import { ReactNode, useEffect, useState, useRef, memo } from 'react'
22
import { useLocation, useNavigationType } from 'react-router'
33
import { cn } from '@/lib/utils'
44

55
interface PageTransitionProps {
66
children: ReactNode
7-
duration?: number // in milliseconds
8-
delay?: number // in milliseconds
9-
isContentTransition?: boolean // Flag to indicate if this is an inner content transition
7+
duration?: number
8+
delay?: number
9+
isContentTransition?: boolean
1010
}
1111

12-
export default function PageTransition({
12+
let mobileCache: boolean | null = null
13+
let motionCache: boolean | null = null
14+
15+
const getMobile = () => {
16+
if (typeof window === 'undefined') return false
17+
if (mobileCache === null) {
18+
mobileCache = window.innerWidth < 768
19+
window.addEventListener('resize', () => { mobileCache = window.innerWidth < 768 }, { passive: true })
20+
}
21+
return mobileCache
22+
}
23+
24+
const getMotion = () => {
25+
if (typeof window === 'undefined') return false
26+
if (motionCache === null) {
27+
const mq = window.matchMedia('(prefers-reduced-motion: reduce)')
28+
motionCache = mq.matches
29+
mq.addEventListener('change', (e) => { motionCache = e.matches })
30+
}
31+
return motionCache
32+
}
33+
34+
const isTab = (a: string, b: string) =>
35+
(a.startsWith('/settings') && b.startsWith('/settings')) || (a.startsWith('/nodes') && b.startsWith('/nodes'))
36+
37+
export default memo(function PageTransition({
1338
children,
1439
duration = 300,
1540
delay = 0,
16-
isContentTransition = false, // Default to false for backward compatibility
41+
isContentTransition = false,
1742
}: PageTransitionProps) {
1843
const location = useLocation()
19-
const navigationType = useNavigationType()
44+
const navType = useNavigationType()
2045
const [displayChildren, setDisplayChildren] = useState(children)
21-
const [isPageTransitioning, setIsPageTransitioning] = useState(false)
46+
const [opacity, setOpacity] = useState(1)
2247
const [isShaking, setIsShaking] = useState(false)
23-
const previousLocationRef = useRef({
24-
pathname: location.pathname,
25-
search: location.search,
26-
hash: location.hash,
27-
key: location.key,
28-
state: location.state,
29-
})
30-
const isFirstRenderRef = useRef(true)
31-
const hasNavigatedRef = useRef(false)
32-
const transitionTimeoutRef = useRef<number | null>(null)
33-
const shakeTimeoutRef = useRef<number | null>(null)
34-
35-
// Clean up timeouts on unmount
36-
useEffect(() => {
37-
return () => {
38-
if (transitionTimeoutRef.current) {
39-
window.clearTimeout(transitionTimeoutRef.current)
40-
}
41-
if (shakeTimeoutRef.current) {
42-
window.clearTimeout(shakeTimeoutRef.current)
43-
}
44-
}
45-
}, [])
46-
47-
// Update children when they change, but only if not transitioning
48-
useEffect(() => {
49-
if (!isShaking && !isPageTransitioning) {
50-
setDisplayChildren(children)
51-
}
52-
}, [children, isShaking, isPageTransitioning])
53-
54-
// For hash router, we need to consider the full URL including the hash
55-
const getPathWithHash = () => {
56-
return `${location.pathname}${location.hash}`
57-
}
58-
59-
const previousPathWithHash = () => {
60-
return `${previousLocationRef.current.pathname}${previousLocationRef.current.hash}`
61-
}
62-
63-
// Detect actual location change for hash router
64-
const isSameLocation = () => {
65-
// In hash router, the location.key changes for each navigation attempt
66-
// even if the URL is the same, so we can use it to detect navigation attempts
67-
return getPathWithHash() === previousPathWithHash() && location.key !== previousLocationRef.current.key
68-
}
69-
70-
// Check if we're coming from login page
71-
const isComingFromLogin = () => {
72-
const prevPath = previousPathWithHash()
73-
const currentPath = getPathWithHash()
74-
return prevPath.includes('/login') && (currentPath === '/' || currentPath === '/#/')
75-
}
76-
77-
// Check if this is a tab navigation within dashboard sections
78-
const isTabNavigation = () => {
79-
// Check if navigation is between tabs in settings or nodes sections
80-
const currentPath = location.pathname
81-
const previousPath = previousLocationRef.current.pathname
82-
83-
// Check for tab navigation patterns
84-
return (
85-
// Settings tab navigation
86-
(currentPath.startsWith('/settings') && previousPath.startsWith('/settings')) ||
87-
// Nodes tab navigation
88-
(currentPath.startsWith('/nodes') && previousPath.startsWith('/nodes'))
89-
)
90-
}
48+
const prev = useRef({ pathname: location.pathname, hash: location.hash, key: location.key })
49+
const first = useRef(true)
50+
const timeout = useRef<number | null>(null)
9151

92-
// Reset location ref without animation
93-
const resetLocationRef = () => {
94-
previousLocationRef.current = {
95-
pathname: location.pathname,
96-
search: location.search,
97-
hash: location.hash,
98-
key: location.key,
99-
state: location.state,
100-
}
101-
}
52+
useEffect(() => () => { if (timeout.current) clearTimeout(timeout.current) }, [])
10253

103-
// Handle navigation effects
10454
useEffect(() => {
105-
// Skip on first render
106-
if (isFirstRenderRef.current) {
107-
isFirstRenderRef.current = false
55+
if (first.current) {
56+
first.current = false
57+
prev.current = { pathname: location.pathname, hash: location.hash, key: location.key }
10858
return
10959
}
11060

111-
// Clear any existing timeouts
112-
if (transitionTimeoutRef.current) {
113-
window.clearTimeout(transitionTimeoutRef.current)
114-
transitionTimeoutRef.current = null
115-
}
116-
if (shakeTimeoutRef.current) {
117-
window.clearTimeout(shakeTimeoutRef.current)
118-
shakeTimeoutRef.current = null
119-
}
61+
if (timeout.current) clearTimeout(timeout.current)
12062

121-
// Skip animations on browser actions like refresh (POP)
122-
if (navigationType === 'POP') {
63+
if (navType === 'POP') {
12364
setDisplayChildren(children)
124-
resetLocationRef()
125-
return
126-
}
127-
128-
// Check if we're navigating to the same location
129-
const isSameLocationAttempt = isSameLocation()
130-
131-
// Special case: Tab navigation - handle differently based on isContentTransition
132-
if (isTabNavigation()) {
133-
if (isContentTransition) {
134-
// For content inside tabs
135-
if (isSameLocationAttempt) {
136-
// Same page navigation inside tabs - trigger shake for content
137-
setIsShaking(true)
138-
139-
// Reset shake after animation completes
140-
shakeTimeoutRef.current = window.setTimeout(() => {
141-
setIsShaking(false)
142-
}, duration)
143-
} else {
144-
// Different tab - use transition
145-
setIsPageTransitioning(true)
146-
147-
transitionTimeoutRef.current = window.setTimeout(() => {
148-
setDisplayChildren(children)
149-
// Small delay before removing transition class to ensure smooth animation
150-
requestAnimationFrame(() => {
151-
requestAnimationFrame(() => {
152-
setIsPageTransitioning(false)
153-
})
154-
})
155-
resetLocationRef()
156-
}, 100) // Shorter transition for better responsiveness
157-
}
158-
} else {
159-
// For the main page wrapper, skip animations
160-
setDisplayChildren(children)
161-
resetLocationRef()
162-
}
65+
setOpacity(1)
66+
prev.current = { pathname: location.pathname, hash: location.hash, key: location.key }
16367
return
16468
}
16569

166-
// Special case: coming from login - no shake, just fade
167-
if (isComingFromLogin()) {
168-
// Just do a simple fade transition
169-
setIsPageTransitioning(true)
170-
171-
transitionTimeoutRef.current = window.setTimeout(() => {
172-
setDisplayChildren(children)
173-
// Small delay before removing transition class
174-
requestAnimationFrame(() => {
175-
requestAnimationFrame(() => {
176-
setIsPageTransitioning(false)
177-
})
178-
})
179-
resetLocationRef()
180-
hasNavigatedRef.current = true
181-
}, 100)
70+
const mobile = getMobile()
71+
const noMotion = getMotion()
72+
const current = `${location.pathname}${location.hash}`
73+
const prevKey = `${prev.current.pathname}${prev.current.hash}`
74+
const same = current === prevKey && location.key !== prev.current.key
75+
const tabNav = isTab(location.pathname, prev.current.pathname)
18276

77+
if ((tabNav && !isContentTransition) || noMotion) {
78+
setDisplayChildren(children)
79+
setOpacity(1)
80+
prev.current = { pathname: location.pathname, hash: location.hash, key: location.key }
18381
return
18482
}
18583

186-
// Same path but different navigation attempt - trigger shake
187-
if (isSameLocationAttempt) {
188-
setIsShaking(true)
84+
const ms = isContentTransition && mobile ? 200 : mobile ? 150 : 120
18985

190-
// Reset shake after animation completes
191-
shakeTimeoutRef.current = window.setTimeout(() => {
86+
if (same) {
87+
setIsShaking(true)
88+
timeout.current = window.setTimeout(() => {
19289
setIsShaking(false)
193-
}, duration)
194-
90+
prev.current = { pathname: location.pathname, hash: location.hash, key: location.key }
91+
}, Math.min(duration, 200))
19592
return
19693
}
19794

198-
// Different location - fade transition
199-
const isRealLocationChange = getPathWithHash() !== previousPathWithHash()
200-
201-
if (isRealLocationChange) {
202-
// Different page navigation - fade transition
203-
setIsPageTransitioning(true)
204-
205-
// Wait for fade-out, then update content
206-
transitionTimeoutRef.current = window.setTimeout(() => {
95+
if (current !== prevKey) {
96+
setOpacity(0)
97+
requestAnimationFrame(() => {
20798
setDisplayChildren(children)
208-
// Small delay before removing transition class
20999
requestAnimationFrame(() => {
210-
requestAnimationFrame(() => {
211-
setIsPageTransitioning(false)
212-
})
100+
setOpacity(1)
101+
timeout.current = window.setTimeout(() => {
102+
prev.current = { pathname: location.pathname, hash: location.hash, key: location.key }
103+
}, ms)
213104
})
214-
resetLocationRef()
215-
hasNavigatedRef.current = true
216-
}, 100)
105+
})
217106
} else {
218-
// Same location but different children - update without transition
219107
setDisplayChildren(children)
220-
resetLocationRef()
108+
prev.current = { pathname: location.pathname, hash: location.hash, key: location.key }
221109
}
222-
}, [location, navigationType, children, duration, isContentTransition])
110+
}, [location, navType, children, isContentTransition, duration])
111+
112+
useEffect(() => {
113+
if (opacity === 1 && !isShaking) setDisplayChildren(children)
114+
}, [children, opacity, isShaking])
115+
116+
const noMotion = getMotion()
117+
const ms = isContentTransition && getMobile() ? 200 : getMobile() ? 150 : 120
223118

224119
return (
225120
<div
226-
className={cn('will-change-opacity w-full will-change-transform', isShaking ? 'animate-telegram-shake' : '', isPageTransitioning ? 'translate-y-2 opacity-0' : 'translate-y-0 opacity-100')}
121+
className={cn('w-full', isShaking && !noMotion && 'animate-telegram-shake')}
227122
style={{
228-
animationDuration: isShaking ? `${duration}ms` : undefined,
229-
animationDelay: isShaking && delay > 0 ? `${delay}ms` : undefined,
230-
animationFillMode: isShaking ? 'both' : undefined,
231-
transition: 'opacity 100ms cubic-bezier(0.4, 0, 0.2, 1), transform 100ms cubic-bezier(0.4, 0, 0.2, 1)',
123+
opacity,
124+
transform: 'translate3d(0, 0, 0)',
125+
...(isShaking && !noMotion && {
126+
animationDuration: `${Math.min(duration, 200)}ms`,
127+
...(delay > 0 && { animationDelay: `${delay}ms` }),
128+
animationFillMode: 'both',
129+
}),
130+
...(!noMotion && { transition: `opacity ${ms}ms cubic-bezier(0.4, 0, 0.2, 1)` }),
232131
}}
233132
>
234133
{displayChildren}
235134
</div>
236135
)
237-
}
136+
})

0 commit comments

Comments
 (0)