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:
@@ -4,6 +4,7 @@ import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { motion } from 'framer-motion';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -38,6 +39,7 @@ import { resolveUploadErrorDialog, type UploadErrorDialog } from '../lib/uploadE
|
||||
import { useEventStats } from '../context/EventStatsContext';
|
||||
import { useEventBranding } from '../context/EventBrandingContext';
|
||||
import { compressPhoto, formatBytes } from '../lib/image';
|
||||
import { FADE_SCALE, FADE_UP, prefersReducedMotion } from '../lib/motion';
|
||||
import { useGuestIdentity } from '../context/GuestIdentityContext';
|
||||
import { useEventData } from '../hooks/useEventData';
|
||||
import { isTaskModeEnabled } from '../lib/engagement';
|
||||
@@ -147,6 +149,9 @@ export default function UploadPage() {
|
||||
const uploadsRequireApproval =
|
||||
(event?.guest_upload_visibility as 'immediate' | 'review' | undefined) !== 'immediate';
|
||||
const demoReadOnly = Boolean(event?.demo_read_only);
|
||||
const motionEnabled = !prefersReducedMotion();
|
||||
const overlayMotion = motionEnabled ? { initial: 'hidden', animate: 'show', variants: FADE_SCALE } : {};
|
||||
const fadeUpMotion = motionEnabled ? { initial: 'hidden', animate: 'show', variants: FADE_UP } : {};
|
||||
|
||||
const taskIdParam = searchParams.get('task');
|
||||
const emotionSlug = searchParams.get('emotion') || '';
|
||||
@@ -958,10 +963,11 @@ const [canUpload, setCanUpload] = useState(true);
|
||||
}, [beginCapture, isCameraActive, startCamera]);
|
||||
|
||||
const taskFloatingCard = showTaskOverlay && task ? (
|
||||
<button
|
||||
<motion.button
|
||||
type="button"
|
||||
onClick={() => setTaskDetailsExpanded((prev) => !prev)}
|
||||
className="absolute left-4 right-4 top-6 z-30 rounded-3xl border border-white/40 bg-black/60 p-4 text-left text-white shadow-2xl backdrop-blur transition hover:bg-black/70 focus:outline-none focus:ring-2 focus:ring-white/60 sm:left-6 sm:right-6 sm:top-8"
|
||||
{...overlayMotion}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Badge variant="secondary" className="flex items-center gap-2 rounded-full text-[11px]">
|
||||
@@ -1001,11 +1007,11 @@ const [canUpload, setCanUpload] = useState(true);
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</button>
|
||||
</motion.button>
|
||||
) : null;
|
||||
|
||||
const heroOverlay = !task && showHeroOverlay && mode !== 'uploading' ? (
|
||||
<div className="absolute left-4 right-4 top-4 z-30 rounded-2xl border border-white/25 bg-black/60 px-4 py-3 text-white shadow-2xl backdrop-blur sm:left-6 sm:right-6 sm:top-5">
|
||||
<motion.div className="absolute left-4 right-4 top-4 z-30 rounded-2xl border border-white/25 bg-black/60 px-4 py-3 text-white shadow-2xl backdrop-blur sm:left-6 sm:right-6 sm:top-5" {...fadeUpMotion}>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="space-y-1">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-white/70">Bereit für dein Foto?</p>
|
||||
@@ -1015,7 +1021,7 @@ const [canUpload, setCanUpload] = useState(true);
|
||||
Live
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
) : null;
|
||||
|
||||
const dialogToneIconClass: Record<Exclude<UploadErrorDialog['tone'], undefined>, string> = {
|
||||
@@ -1079,7 +1085,10 @@ const renderWithDialog = (content: ReactNode, wrapperClassName = 'space-y-6 pb-[
|
||||
|
||||
const renderPrimer = () => (
|
||||
showPrimer && (
|
||||
<div className="rounded-[28px] border border-pink-200/60 bg-white/90 p-4 text-sm text-pink-900 shadow-lg dark:border-pink-500/40 dark:bg-pink-500/10 dark:text-pink-50">
|
||||
<motion.div
|
||||
className="rounded-[28px] border border-pink-200/60 bg-white/90 p-4 text-sm text-pink-900 shadow-lg dark:border-pink-500/40 dark:bg-pink-500/10 dark:text-pink-50"
|
||||
{...fadeUpMotion}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<Info className="mt-0.5 h-5 w-5 flex-shrink-0" />
|
||||
<div className="text-left">
|
||||
@@ -1093,7 +1102,7 @@ const renderWithDialog = (content: ReactNode, wrapperClassName = 'space-y-6 pb-[
|
||||
{t('upload.primer.dismiss')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1133,9 +1142,10 @@ const renderWithDialog = (content: ReactNode, wrapperClassName = 'space-y-6 pb-[
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div
|
||||
<motion.div
|
||||
className="rounded-[32px] border border-white/15 bg-white/85 p-5 text-slate-900 shadow-lg dark:border-white/10 dark:bg-white/5 dark:text-white"
|
||||
style={{ borderRadius: radius, fontFamily: bodyFont }}
|
||||
{...fadeUpMotion}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-slate-900/10 dark:bg-white/10">
|
||||
@@ -1179,7 +1189,7 @@ const renderWithDialog = (content: ReactNode, wrapperClassName = 'space-y-6 pb-[
|
||||
{t('upload.galleryButton')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user