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:
@@ -1,29 +1,69 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
|
||||
type Toast = { id: number; text: string; type?: 'success'|'error' };
|
||||
type ToastAction = { label: string; onClick: () => void };
|
||||
type Toast = {
|
||||
id: number;
|
||||
text: string;
|
||||
type?: 'success' | 'error' | 'info';
|
||||
action?: ToastAction;
|
||||
durationMs?: number;
|
||||
};
|
||||
const Ctx = React.createContext<{ push: (t: Omit<Toast,'id'>) => void } | null>(null);
|
||||
|
||||
export function ToastProvider({ children }: { children: React.ReactNode }) {
|
||||
const [list, setList] = React.useState<Toast[]>([]);
|
||||
const push = React.useCallback((t: Omit<Toast,'id'>) => {
|
||||
const id = Date.now() + Math.random();
|
||||
setList((arr) => [...arr, { id, ...t }]);
|
||||
setTimeout(() => setList((arr) => arr.filter((x) => x.id !== id)), 3000);
|
||||
const durationMs = t.durationMs ?? 3000;
|
||||
setList((arr) => [...arr, { id, ...t, durationMs }]);
|
||||
if (durationMs > 0) {
|
||||
setTimeout(() => setList((arr) => arr.filter((x) => x.id !== id)), durationMs);
|
||||
}
|
||||
}, []);
|
||||
const dismiss = React.useCallback((id: number) => {
|
||||
setList((arr) => arr.filter((x) => x.id !== id));
|
||||
}, []);
|
||||
const contextValue = React.useMemo(() => ({ push }), [push]);
|
||||
React.useEffect(() => {
|
||||
const onEvt = (e: CustomEvent<Omit<Toast, 'id'>>) => push(e.detail);
|
||||
window.addEventListener('guest-toast', onEvt);
|
||||
return () => window.removeEventListener('guest-toast', onEvt);
|
||||
}, [push]);
|
||||
return (
|
||||
<Ctx.Provider value={{ push }}>
|
||||
<Ctx.Provider value={contextValue}>
|
||||
{children}
|
||||
<div className="pointer-events-none fixed inset-x-0 bottom-4 z-50 flex justify-center px-4">
|
||||
<div className="flex w-full max-w-sm flex-col gap-2">
|
||||
{list.map((t) => (
|
||||
<div key={t.id} className={`pointer-events-auto rounded-md border p-3 shadow-sm ${t.type==='error'?'border-red-300 bg-red-50 text-red-700':'border-green-300 bg-green-50 text-green-700'}`}>
|
||||
{t.text}
|
||||
<div
|
||||
key={t.id}
|
||||
className={`pointer-events-auto rounded-md border p-3 shadow-sm ${
|
||||
t.type === 'error'
|
||||
? 'border-red-300 bg-red-50 text-red-700'
|
||||
: t.type === 'info'
|
||||
? 'border-blue-300 bg-blue-50 text-blue-700'
|
||||
: 'border-green-300 bg-green-50 text-green-700'
|
||||
}`}
|
||||
>
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<span className="text-sm">{t.text}</span>
|
||||
{t.action ? (
|
||||
<button
|
||||
type="button"
|
||||
className="pointer-events-auto rounded-full border border-current/30 px-3 py-1 text-xs font-semibold uppercase tracking-wide transition hover:border-current"
|
||||
onClick={() => {
|
||||
try {
|
||||
t.action?.onClick();
|
||||
} finally {
|
||||
dismiss(t.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t.action.label}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user