injectManifest, a new typed SW source, runtime caching, and a non‑blocking update toast with an action button. The
guest shell now links a dedicated manifest and theme color, and background upload sync is managed in a single
PwaManager component.
Key changes (where/why)
- vite.config.ts: added VitePWA injectManifest config, guest manifest, and output to /public so the SW can control /
scope.
- resources/js/guest/guest-sw.ts: new Workbox SW (precache + runtime caching for guest navigation, GET /api/v1/*,
images, fonts) and preserves push/sync/notification logic.
- resources/js/guest/components/PwaManager.tsx: registers SW, shows update/offline toasts, and processes the upload
queue on sync/online.
- resources/js/guest/components/ToastHost.tsx: action-capable toasts so update prompts can include a CTA.
- resources/js/guest/i18n/messages.ts: added common.updateAvailable, common.updateAction, common.offlineReady.
- resources/views/guest.blade.php: manifest + theme color + apple touch icon.
- .gitignore: ignore generated public/guest-sw.js and public/guest.webmanifest; public/guest-sw.js removed since it’s
now build output.
101 lines
2.8 KiB
TypeScript
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, scale: 0.985 },
|
|
center: { opacity: 1, scale: 1 },
|
|
exit: { opacity: 0, scale: 0.985 },
|
|
};
|
|
|
|
const transition = kind === 'tab'
|
|
? { duration: 0.18, ease: [0.22, 0.61, 0.36, 1] }
|
|
: { duration: 0.24, ease: [0.25, 0.8, 0.25, 1] };
|
|
|
|
return (
|
|
<AnimatePresence initial={false}>
|
|
<motion.div
|
|
key={location.pathname}
|
|
custom={{ direction }}
|
|
variants={kind === 'tab' ? tabVariants : stackVariants}
|
|
initial="enter"
|
|
animate="center"
|
|
exit="exit"
|
|
transition={transition}
|
|
style={{ willChange: 'transform, opacity' }}
|
|
>
|
|
{content}
|
|
</motion.div>
|
|
</AnimatePresence>
|
|
);
|
|
}
|