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;
+ })
+ );
+});