feat: add PWA support with service worker and update prompt

- Updated package.json to include vite-plugin-pwa and workbox-window.
- Added icon SVGs for PWA: icon-512.svg and maskable-512.svg.
- Created a manifest.json for PWA configuration.
- Implemented PwaUpdatePrompt component to notify users of available updates.
- Enhanced CookieConsent and SiteAssistant components for better layout and responsiveness.
- Updated global CSS for safe-area insets and mobile-first enhancements.
- Registered service worker in usePwaRegistration hook for managing updates.
- Modified Vite configuration to integrate PWA features and caching strategies.
This commit is contained in:
Your Name
2026-04-06 08:12:32 +02:00
parent c483e8508b
commit a539ad43af
23 changed files with 4393 additions and 28 deletions

View File

@@ -70,7 +70,7 @@ export default function CookieConsent() {
<div
role="dialog"
aria-label={t('cookie.title', 'Cookie Consent')}
className="fixed inset-x-0 bottom-0 z-50 p-4 sm:p-6"
className="fixed inset-x-0 bottom-0 z-50 p-4 pb-[max(1rem,env(safe-area-inset-bottom))] sm:p-6 sm:pb-[max(1.5rem,env(safe-area-inset-bottom))]"
>
<div className="mx-auto max-w-3xl rounded-2xl border border-slate-200 bg-white p-5 shadow-2xl dark:border-slate-700 dark:bg-slate-800 sm:flex sm:items-start sm:gap-4">
<div className="mb-3 flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-amber-100 text-amber-600 dark:bg-amber-900/30 dark:text-amber-400 sm:mb-0">

View File

@@ -98,7 +98,7 @@ export default function Header() {
].join(' ');
return (
<header className="sticky top-0 z-50 border-b border-slate-200/70 bg-white/78 backdrop-blur-2xl dark:border-slate-700/60 dark:bg-slate-950/78">
<header className="sticky-header-safe sticky top-0 z-50 border-b border-slate-200/70 bg-white/78 backdrop-blur-2xl dark:border-slate-700/60 dark:bg-slate-950/78">
<div className="mx-auto flex h-20 max-w-7xl items-center justify-between gap-4 px-4 sm:px-6 lg:px-8">
<div className="flex items-center gap-8">
<Link to="/" className="group flex items-center gap-3">

View File

@@ -0,0 +1,47 @@
import { RefreshCw, X } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { usePwaRegistration } from '../../hooks/usePwaRegistration';
/**
* Renders a bottom-right toast when a new service-worker version is available.
* The user can choose to reload immediately or dismiss.
*/
export default function PwaUpdatePrompt() {
const { needRefresh, acceptUpdate, dismissUpdate } = usePwaRegistration();
const { t } = useTranslation();
if (!needRefresh) return null;
return (
<div
role="alert"
className="fixed bottom-4 right-4 z-50 flex max-w-sm items-start gap-3 rounded-2xl border border-slate-200 bg-white p-4 shadow-2xl dark:border-slate-700 dark:bg-slate-800 sm:bottom-6 sm:right-6"
>
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-primary-100 text-primary-600 dark:bg-primary-900/30 dark:text-primary-400">
<RefreshCw className="h-5 w-5" />
</div>
<div className="flex-1">
<p className="text-sm font-semibold text-slate-900 dark:text-slate-100">
{t('pwa.updateAvailable', 'Update available')}
</p>
<p className="mt-0.5 text-xs text-slate-500 dark:text-slate-400">
{t('pwa.updateDescription', 'A new version is ready. Reload to get the latest features.')}
</p>
<button
onClick={acceptUpdate}
className="mt-2 inline-flex items-center gap-1.5 rounded-lg bg-primary-600 px-3 py-1.5 text-xs font-semibold text-white transition-colors hover:bg-primary-700 active:scale-[0.98]"
>
<RefreshCw className="h-3.5 w-3.5" />
{t('pwa.reload', 'Reload')}
</button>
</div>
<button
onClick={dismissUpdate}
className="rounded-lg p-1 text-slate-400 transition-colors hover:bg-slate-100 hover:text-slate-600 dark:hover:bg-slate-700 dark:hover:text-slate-300"
aria-label="Dismiss"
>
<X className="h-4 w-4" />
</button>
</div>
);
}

View File

@@ -195,7 +195,7 @@ export default function SiteAssistant() {
};
return (
<div className="pointer-events-none fixed inset-x-4 bottom-4 z-40 flex justify-end sm:bottom-6 sm:right-6 sm:left-auto">
<div className="pointer-events-none fixed inset-x-4 bottom-[max(1rem,env(safe-area-inset-bottom))] z-40 flex justify-end sm:bottom-6 sm:right-6 sm:left-auto">
<div className="pointer-events-auto w-full max-w-sm">
{open && (
<div className="mb-3 overflow-hidden rounded-[28px] border border-slate-200/80 bg-white/95 shadow-[0_20px_80px_rgba(15,23,42,0.16)] backdrop-blur dark:border-slate-700/80 dark:bg-slate-950/95">
@@ -225,7 +225,7 @@ export default function SiteAssistant() {
</p>
</div>
<div ref={scrollRef} className="max-h-[26rem] space-y-3 overflow-y-auto px-4 py-4">
<div ref={scrollRef} className="max-h-[50dvh] space-y-3 overflow-y-auto overscroll-contain px-4 py-4 sm:max-h-[26rem]">
{messages.length === 0 && (
<div className="rounded-3xl border border-sky-100 bg-sky-50/80 p-4 text-sm text-slate-700 dark:border-sky-900/50 dark:bg-slate-900 dark:text-slate-200">
<p className="font-medium text-slate-900 dark:text-slate-100">