diff --git a/app/layout.tsx b/app/layout.tsx index 80c2999..f70e274 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -40,6 +40,12 @@ export default function RootLayout({ + + + + + + {isProd && umamiScript && umamiId && ( )} diff --git a/app/manifest.ts b/app/manifest.ts new file mode 100644 index 0000000..3b438ca --- /dev/null +++ b/app/manifest.ts @@ -0,0 +1,29 @@ +import { MetadataRoute } from 'next'; + +export const dynamic = 'force-static'; + +export default function manifest(): MetadataRoute.Manifest { + return { + name: 'Kit - Creative Toolkit', + short_name: 'Kit', + description: 'A curated collection of creative and utility tools for developers and creators.', + start_url: '/', + display: 'standalone', + background_color: '#0a0a0f', + theme_color: '#8b5cf6', + icons: [ + { + src: '/icon.png', + sizes: '512x512', + type: 'image/png', + purpose: 'any', + }, + { + src: '/icon.png', + sizes: '512x512', + type: 'image/png', + purpose: 'maskable', + }, + ], + }; +} diff --git a/components/providers/Providers.tsx b/components/providers/Providers.tsx index bac11dc..338f9be 100644 --- a/components/providers/Providers.tsx +++ b/components/providers/Providers.tsx @@ -5,6 +5,7 @@ import { Toaster } from 'sonner'; import { useState } from 'react'; import { ThemeProvider } from './ThemeProvider'; import { TooltipProvider } from '@/components/ui/tooltip'; +import { SWRegistration } from './SWRegistration'; export function Providers({ children }: { children: React.ReactNode }) { const [queryClient] = useState( @@ -23,6 +24,7 @@ export function Providers({ children }: { children: React.ReactNode }) { + {children} diff --git a/components/providers/SWRegistration.tsx b/components/providers/SWRegistration.tsx new file mode 100644 index 0000000..d96f459 --- /dev/null +++ b/components/providers/SWRegistration.tsx @@ -0,0 +1,22 @@ +'use client'; + +import { useEffect } from 'react'; + +export function SWRegistration() { + useEffect(() => { + if ('serviceWorker' in navigator && process.env.NODE_ENV === 'production') { + window.addEventListener('load', () => { + navigator.serviceWorker + .register('/sw.js') + .then((registration) => { + console.log('SW registered:', registration); + }) + .catch((error) => { + console.log('SW registration failed:', error); + }); + }); + } + }, []); + + return null; +} diff --git a/public/sw.js b/public/sw.js new file mode 100644 index 0000000..4b5e29f --- /dev/null +++ b/public/sw.js @@ -0,0 +1,70 @@ +const CACHE_NAME = 'kit-ui-v1'; +const ASSETS_TO_CACHE = [ + '/', + '/manifest.webmanifest', + '/icon.png', + '/wasm/magick.wasm', + '/wasm/ffmpeg-core.wasm', + '/wasm/ffmpeg-core.js', +]; + +// Install Event - Pre-cache core assets +self.addEventListener('install', (event) => { + event.waitUntil( + caches.open(CACHE_NAME).then((cache) => { + console.log('Pre-caching assets'); + return cache.addAll(ASSETS_TO_CACHE); + }) + ); + self.skipWaiting(); +}); + +// Activate Event - Clean up old caches +self.addEventListener('activate', (event) => { + event.waitUntil( + caches.keys().then((cacheNames) => { + return Promise.all( + cacheNames.map((cacheName) => { + if (cacheName !== CACHE_NAME) { + console.log('Deleting old cache:', cacheName); + return caches.delete(cacheName); + } + }) + ); + }) + ); + self.clients.claim(); +}); + +// Fetch Event - Stale-while-revalidate strategy +self.addEventListener('fetch', (event) => { + // Only handle GET requests + if (event.request.method !== 'GET') return; + + // Skip browser extensions and non-http(s) requests + if (!event.request.url.startsWith('http')) return; + + event.respondWith( + caches.match(event.request).then((cachedResponse) => { + const fetchPromise = fetch(event.request).then((networkResponse) => { + // Only cache valid responses + if (networkResponse && networkResponse.status === 200 && networkResponse.type === 'basic') { + const responseToCache = networkResponse.clone(); + caches.open(CACHE_NAME).then((cache) => { + cache.put(event.request, responseToCache); + }); + } + return networkResponse; + }).catch((err) => { + // If offline and not in cache, and it's a page navigation, return the root page + if (event.request.mode === 'navigate') { + return caches.match('/'); + } + throw err; + }); + + // Return cached response if available, else wait for network + return cachedResponse || fetchPromise; + }) + ); +});