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:
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
47
frontend/src/components/layout/PwaUpdatePrompt.tsx
Normal file
47
frontend/src/components/layout/PwaUpdatePrompt.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user