Files
fotospiel-app/resources/js/guest/components/RouteTransition.tsx
Codex Agent 5bdc15d399
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled
Tune guest route transition animations
2026-01-14 11:30:03 +01:00

101 lines
2.8 KiB
TypeScript

import React from 'react';
import { AnimatePresence, motion, useReducedMotion } from 'framer-motion';
import { Outlet, useLocation, useNavigationType } from 'react-router-dom';
const TAB_SECTIONS = new Set(['home', 'tasks', 'achievements', 'gallery']);
export function getTabKey(pathname: string): string | null {
const match = pathname.match(/^\/e\/[^/]+(?:\/([^/]+))?$/);
if (!match) {
return null;
}
const section = match[1];
if (!section) {
return 'home';
}
return TAB_SECTIONS.has(section) ? section : null;
}
export function getTransitionKind(prevPath: string, nextPath: string): 'tab' | 'stack' {
const prevTab = getTabKey(prevPath);
const nextTab = getTabKey(nextPath);
if (prevTab && nextTab && prevTab !== nextTab) {
return 'tab';
}
return 'stack';
}
export function isTransitionDisabled(pathname: string): boolean {
if (pathname.startsWith('/share/')) {
return true;
}
return /^\/e\/[^/]+\/upload(?:\/|$)/.test(pathname);
}
export default function RouteTransition({ children }: { children?: React.ReactNode }) {
const location = useLocation();
const navigationType = useNavigationType();
const prefersReducedMotion = useReducedMotion();
const prevPathRef = React.useRef(location.pathname);
const prevPath = prevPathRef.current;
const direction = navigationType === 'POP' ? 'back' : 'forward';
const kind = getTransitionKind(prevPath, location.pathname);
const disableTransitions = prefersReducedMotion
|| isTransitionDisabled(prevPath)
|| isTransitionDisabled(location.pathname);
React.useEffect(() => {
prevPathRef.current = location.pathname;
}, [location.pathname]);
const content = children ?? <Outlet />;
if (disableTransitions) {
return <>{content}</>;
}
const stackVariants = {
enter: ({ direction }: { direction: 'forward' | 'back' }) => ({
x: direction === 'back' ? -28 : 28,
opacity: 0,
}),
center: { x: 0, opacity: 1 },
exit: ({ direction }: { direction: 'forward' | 'back' }) => ({
x: direction === 'back' ? 28 : -28,
opacity: 0,
}),
};
const tabVariants = {
enter: { opacity: 0, y: 8 },
center: { opacity: 1, y: 0 },
exit: { opacity: 0, y: -8 },
};
const transition = kind === 'tab'
? { duration: 0.22, ease: [0.22, 0.61, 0.36, 1] }
: { duration: 0.28, ease: [0.25, 0.8, 0.25, 1] };
return (
<AnimatePresence initial={false} mode="wait">
<motion.div
key={location.pathname}
custom={{ direction }}
variants={kind === 'tab' ? tabVariants : stackVariants}
initial="enter"
animate="center"
exit="exit"
transition={transition as any}
style={{ willChange: 'transform, opacity' }}
>
{content}
</motion.div>
</AnimatePresence>
);
}