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:
@@ -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>
|
||||
|
||||
53
apps/web/src/app/offline/page.tsx
Normal file
53
apps/web/src/app/offline/page.tsx
Normal 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 працює краще з інтернет-з'єднанням
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
13
apps/web/src/components/PWAProvider.tsx
Normal file
13
apps/web/src/components/PWAProvider.tsx
Normal 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
83
apps/web/src/lib/pwa.ts
Normal 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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user