Add full PWA support (icons, manifest, service worker, pwa lib and bridge) and introduce install/offline UI. Generate app icons via a new scripts/generate-icons.js and include multiple icon sizes in public/icons; update manifest and layout head meta/preconnect. Revamp global styles (globals.css) with new utilities, animations, spacing for bottom nav, card shadows, skeletons and accessibility tweaks. Polish major screens (login, collections, map, settings) with improved headers, loading states, touch/press feedback, and layout adjustments; add new components (InstallPrompt, OfflineIndicator, PWABridge) and update various components to match new visual/UX patterns. Update Dockerfile to generate icons and use Next.js static export output (/out) for nginx, and bump package setup to support icon generation.
105 lines
No EOL
2.8 KiB
TypeScript
105 lines
No EOL
2.8 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState, useCallback } from 'react';
|
|
|
|
export function useOnlineStatus() {
|
|
const [isOnline, setIsOnline] = useState(
|
|
typeof navigator !== 'undefined' ? navigator.onLine : true
|
|
);
|
|
|
|
useEffect(() => {
|
|
const handleOnline = () => setIsOnline(true);
|
|
const handleOffline = () => setIsOnline(false);
|
|
|
|
window.addEventListener('online', handleOnline);
|
|
window.addEventListener('offline', handleOffline);
|
|
|
|
return () => {
|
|
window.removeEventListener('online', handleOnline);
|
|
window.removeEventListener('offline', handleOffline);
|
|
};
|
|
}, []);
|
|
|
|
return isOnline;
|
|
}
|
|
|
|
export function usePWAInstall() {
|
|
const [deferredPrompt, setDeferredPrompt] = useState<any>(null);
|
|
const [isInstallable, setIsInstallable] = useState(false);
|
|
const [isInstalled, setIsInstalled] = useState(false);
|
|
|
|
useEffect(() => {
|
|
// Check if already installed
|
|
if (window.matchMedia('(display-mode: standalone)').matches) {
|
|
setIsInstalled(true);
|
|
return;
|
|
}
|
|
|
|
const handleBeforeInstall = (e: Event) => {
|
|
e.preventDefault();
|
|
setDeferredPrompt(e as any);
|
|
setIsInstallable(true);
|
|
};
|
|
|
|
const handleAppInstalled = () => {
|
|
setIsInstalled(true);
|
|
setIsInstallable(false);
|
|
setDeferredPrompt(null);
|
|
};
|
|
|
|
window.addEventListener('beforeinstallprompt', handleBeforeInstall);
|
|
window.addEventListener('appinstalled', handleAppInstalled);
|
|
|
|
return () => {
|
|
window.removeEventListener('beforeinstallprompt', handleBeforeInstall);
|
|
window.removeEventListener('appinstalled', handleAppInstalled);
|
|
};
|
|
}, []);
|
|
|
|
const install = useCallback(async () => {
|
|
if (!deferredPrompt) return false;
|
|
|
|
deferredPrompt.prompt();
|
|
const { outcome } = await deferredPrompt.userChoice;
|
|
setDeferredPrompt(null);
|
|
|
|
if (outcome === 'accepted') {
|
|
setIsInstalled(true);
|
|
return true;
|
|
}
|
|
return false;
|
|
}, [deferredPrompt]);
|
|
|
|
const dismiss = useCallback(() => {
|
|
setIsInstallable(false);
|
|
}, []);
|
|
|
|
return { isInstallable, isInstalled, install, dismiss };
|
|
}
|
|
|
|
export async function registerServiceWorker() {
|
|
if (typeof window === 'undefined' || !('serviceWorker' in navigator)) return null;
|
|
|
|
try {
|
|
const registration = await navigator.serviceWorker.register('/sw.js', {
|
|
scope: '/',
|
|
});
|
|
|
|
registration.addEventListener('updatefound', () => {
|
|
const newWorker = registration.installing;
|
|
if (newWorker) {
|
|
newWorker.addEventListener('statechange', () => {
|
|
if (newWorker.state === 'activated') {
|
|
// New SW activated - could notify user
|
|
console.log('[PWA] New service worker activated');
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
return registration;
|
|
} catch (error) {
|
|
console.error('[PWA] SW registration failed:', error);
|
|
return null;
|
|
}
|
|
} |