feat: Add PWA support

- PWA_MOBILE_SPEC.md documentation
- manifest.json with app metadata
- Service Worker with caching strategies
- Offline page
- PWA registration in layout
- App icons (placeholder)
This commit is contained in:
Apple
2025-11-26 13:56:22 -08:00
parent e0d534ea87
commit a3e632b9e7
12 changed files with 655 additions and 10 deletions

View File

@@ -3,6 +3,7 @@ import { Inter } from 'next/font/google'
import './globals.css'
import { Navigation } from '@/components/Navigation'
import { AuthProvider } from '@/context/AuthContext'
import { PWAProvider } from '@/components/PWAProvider'
const inter = Inter({
subsets: ['latin', 'cyrillic'],
@@ -15,6 +16,16 @@ export const metadata: Metadata = {
description: 'Децентралізована платформа для мікро-спільнот з AI-агентами',
keywords: ['DAARION', 'DAO', 'AI', 'agents', 'community', 'decentralized'],
authors: [{ name: 'DAARION Team' }],
manifest: '/manifest.json',
appleWebApp: {
capable: true,
statusBarStyle: 'black-translucent',
title: 'DAARION',
},
icons: {
icon: '/icons/icon-192x192.png',
apple: '/icons/apple-touch-icon.png',
},
}
export const viewport: Viewport = {
@@ -33,16 +44,18 @@ export default function RootLayout({
<html lang="uk" className={inter.variable}>
<body className={`${inter.className} antialiased`}>
<AuthProvider>
{/* Ambient background effect */}
<div className="ambient-bg" />
{/* Navigation */}
<Navigation />
{/* Main content */}
<main className="min-h-screen pt-16">
{children}
</main>
<PWAProvider>
{/* Ambient background effect */}
<div className="ambient-bg" />
{/* Navigation */}
<Navigation />
{/* Main content */}
<main className="min-h-screen pt-16">
{children}
</main>
</PWAProvider>
</AuthProvider>
</body>
</html>

View File

@@ -0,0 +1,53 @@
'use client'
import { WifiOff, RefreshCw, Sparkles } from 'lucide-react'
export default function OfflinePage() {
const handleRetry = () => {
window.location.reload()
}
return (
<div className="min-h-screen flex items-center justify-center px-4">
<div className="text-center max-w-md">
{/* Logo */}
<div className="flex justify-center mb-6">
<div className="w-20 h-20 rounded-2xl bg-slate-800/50 flex items-center justify-center">
<Sparkles className="w-10 h-10 text-cyan-400" />
</div>
</div>
{/* Offline Icon */}
<div className="flex justify-center mb-6">
<div className="w-16 h-16 rounded-full bg-amber-500/20 flex items-center justify-center">
<WifiOff className="w-8 h-8 text-amber-400" />
</div>
</div>
{/* Message */}
<h1 className="text-2xl font-bold text-white mb-3">
Ви офлайн
</h1>
<p className="text-slate-400 mb-8">
Перевірте підключення до інтернету та спробуйте ще раз.
Деякі раніше відвідані сторінки можуть бути доступні з кешу.
</p>
{/* Retry Button */}
<button
onClick={handleRetry}
className="inline-flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-cyan-500 to-blue-600 text-white font-medium rounded-xl hover:from-cyan-400 hover:to-blue-500 transition-all shadow-lg shadow-cyan-500/20"
>
<RefreshCw className="w-5 h-5" />
Спробувати знову
</button>
{/* Info */}
<p className="mt-8 text-sm text-slate-500">
DAARION.city працює краще з інтернет-з&apos;єднанням
</p>
</div>
</div>
)
}

View File

@@ -0,0 +1,13 @@
'use client'
import { useEffect } from 'react'
import { registerServiceWorker } from '@/lib/pwa'
export function PWAProvider({ children }: { children: React.ReactNode }) {
useEffect(() => {
registerServiceWorker()
}, [])
return <>{children}</>
}

83
apps/web/src/lib/pwa.ts Normal file
View File

@@ -0,0 +1,83 @@
/**
* PWA utilities for DAARION
*/
export function registerServiceWorker(): void {
if (typeof window === 'undefined') return;
if (!('serviceWorker' in navigator)) {
console.log('[PWA] Service Worker not supported');
return;
}
// Only register in production
if (process.env.NODE_ENV !== 'production') {
console.log('[PWA] Skipping SW registration in development');
return;
}
window.addEventListener('load', async () => {
try {
const registration = await navigator.serviceWorker.register('/sw.js', {
scope: '/'
});
console.log('[PWA] Service Worker registered:', registration.scope);
// Check for updates
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
if (newWorker) {
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
// New content available
console.log('[PWA] New content available, refresh to update');
// Could show a toast notification here
}
});
}
});
} catch (error) {
console.error('[PWA] Service Worker registration failed:', error);
}
});
}
export function unregisterServiceWorker(): void {
if (typeof window === 'undefined') return;
if (!('serviceWorker' in navigator)) return;
navigator.serviceWorker.ready.then((registration) => {
registration.unregister().then((success) => {
if (success) {
console.log('[PWA] Service Worker unregistered');
}
});
});
}
/**
* Check if app is running in standalone mode (installed PWA)
*/
export function isStandalone(): boolean {
if (typeof window === 'undefined') return false;
return (
window.matchMedia('(display-mode: standalone)').matches ||
(window.navigator as any).standalone === true
);
}
/**
* Check if app can be installed
*/
export function canInstall(): boolean {
if (typeof window === 'undefined') return false;
// Check if already installed
if (isStandalone()) return false;
// Check for beforeinstallprompt support
return 'BeforeInstallPromptEvent' in window ||
'onbeforeinstallprompt' in window;
}