Implemented guest-only PWA using vite-plugin-pwa (the actual published package; @vite-pwa/plugin isn’t on npm) with
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.
This commit is contained in:
100
resources/js/guest/components/RouteTransition.tsx
Normal file
100
resources/js/guest/components/RouteTransition.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user