Files
donetick/src/components/animations/PageTransition.jsx
2026-08-16 11:52:40 -04:00

135 lines
3.5 KiB
JavaScript

import './PageTransition.css'
import { useLayoutEffect, useRef, useState } from 'react'
import { flushSync } from 'react-dom'
import { useLocation } from 'react-router-dom'
// Route hierarchy for determining navigation direction
const routeHierarchy = {
'/': 0,
'/my/chores': 0,
'/chores': 1,
'/chores/create': 2,
'/settings': 1,
'/things': 1,
'/activities': 1,
'/points': 1,
'/labels': 1,
'/projects': 1,
'/login': 0,
'/signup': 1,
'/landing': 0,
'/archived': 1,
}
const getRouteLevel = pathname => {
// Check for exact matches first
if (routeHierarchy[pathname] !== undefined) {
return routeHierarchy[pathname]
}
// Check for dynamic routes (e.g., /chores/123/edit)
if (pathname.includes('/chores/') && pathname.includes('/edit')) {
return 3
}
if (pathname.includes('/chores/') && pathname.includes('/history')) {
return 3
}
if (pathname.includes('/chores/') && pathname.includes('/timer')) {
return 3
}
if (
pathname.includes('/chores/') &&
!pathname.includes('/edit') &&
!pathname.includes('/history') &&
!pathname.includes('/timer')
) {
return 2
}
if (pathname.includes('/things/')) {
return 2
}
if (pathname.includes('/settings/')) {
return 2
}
// Default level
return 1
}
const PageTransition = ({ children }) => {
const location = useLocation()
const prevLevel = useRef(0)
const [displayLocation, setDisplayLocation] = useState(location)
const isFirstRender = useRef(true)
useLayoutEffect(() => {
// Skip transition on first render
if (isFirstRender.current) {
isFirstRender.current = false
setDisplayLocation(location)
prevLevel.current = getRouteLevel(location.pathname)
return
}
// Don't transition if location hasn't actually changed
if (location.pathname === displayLocation.pathname) {
return
}
const currentLevel = getRouteLevel(location.pathname)
const previousLevel = prevLevel.current
// Determine navigation direction
const isBack = currentLevel < previousLevel
const isFade =
location.pathname.includes('/login') ||
location.pathname.includes('/signup') ||
location.pathname.includes('/landing') ||
location.pathname.includes('/onboarding') ||
location.pathname.includes('/get-started') ||
location.pathname.includes('/ready') ||
location.pathname.includes('/auth/')
// Apply transition type as data attribute for CSS
document.documentElement.dataset.transition = isFade
? 'fade'
: isBack
? 'back'
: 'forward'
// Trigger View Transition if supported
if (document.startViewTransition) {
document.startViewTransition(() => {
// flushSync forces React to update the DOM synchronously
// This ensures the View Transition captures the actual DOM change
flushSync(() => {
setDisplayLocation(location)
})
// Scroll to top after DOM update
window.scrollTo({ top: 0, left: 0, behavior: 'instant' })
})
} else {
// Fallback for browsers without View Transitions API
setDisplayLocation(location)
window.scrollTo({ top: 0, left: 0, behavior: 'instant' })
}
// Update refs
prevLevel.current = currentLevel
}, [location, displayLocation.pathname])
return (
<div
className='page-wrapper'
style={{
paddingBottom: `var(--safe-area-inset-bottom, 0px)`,
}}
>
{displayLocation.pathname === location.pathname ? children : null}
</div>
)
}
export default PageTransition