From 463cb99888ab48ee192f0470eab9ee1c2cb1510d Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Sun, 15 Feb 2026 08:36:32 -0700 Subject: [PATCH] feat: add PWA support with install-to-homescreen banner - manifest.json with app metadata and icons - Service worker (network-first for pages, skip APIs) - PWAInstall component with native install prompt + fallback instructions - Generated 192/512 PNG icons and apple-touch-icon - PWA meta tags in layout (apple-web-app, theme-color, manifest link) Co-Authored-By: Claude Opus 4.6 --- public/apple-touch-icon.png | Bin 0 -> 2367 bytes public/icon-192.png | Bin 0 -> 2492 bytes public/icon-512.png | Bin 0 -> 6866 bytes public/manifest.json | 39 +++++++++ public/sw.js | 43 ++++++++++ src/app/layout.tsx | 17 +++- src/components/PWAInstall.tsx | 144 ++++++++++++++++++++++++++++++++++ 7 files changed, 242 insertions(+), 1 deletion(-) create mode 100644 public/apple-touch-icon.png create mode 100644 public/icon-192.png create mode 100644 public/icon-512.png create mode 100644 public/manifest.json create mode 100644 public/sw.js create mode 100644 src/components/PWAInstall.tsx diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..663a1d95a33ddf3c446d29ce16f2b318a4187525 GIT binary patch literal 2367 zcmZvedpr|t8^;%~?Mz2c@!bE7IQwP6{>5rdSx+92+6;JfHXd>v^xw=eqCD{kiVzd*9dn$Mw4tFWFg2h#wXQ000tJ z7t9=lbM21*8z>wNyMS5%z;2+G**T|(+<6w}5!6Q>w7~zt=5@2#t0G$@l2}fV-`5KP zQWY^}>DO#3fx8DEWKHewMdqQk6rT%E@Skos`-X zW`syU?VAULUW(857%x?$JNr(dN3iKFW2xp>4FKS zJLG{%V|_FIU?)P0Dz=C*Uo`t3qz7ggN2_pqz}ktI@KRi1SQR`R=|@=iBGe*nNz#OD z>QWkvnWy@sS8dXvS|xzvnW-x2w$7-|h`Vr5HGoWOM# zKC|eP>LT5=#+Y!y>bDK>Fj(Q=F9d|%r9sLKfN~EB8B~qm+!SUE_w1@I>sU295&^3EF z(&RdYzr+7qr$RZ>SagWEsIA@g!cVHXLWuA{Y2bgkLdXvHT7WXc*#2#gwx-Pbdh)dI z!nA<_{&VL`)aWG%QA$}+ejh8nsbk7cUktsk!4wpgD|pCVWys_+kwZuVZyr*DO@+qG zLdzytu*O2OHWRriBueB~Ocj{|_CMVNyc2|x;5#u(OTK~wXk%vwxYqX)@?Uey%>`#T z;Q9PNc;%D*5K5RUxVETs6)+m#k{6A z_R94_2>~Y-=3zM8kBHZWpj+OEczzDV`dt>5LMRScN^X%_n`9^%g(W^SKT=Z_G2Xt> zNRr1m#ji`O9(P=3^`P=E&{eOI1K%ffb zn!Pl3qb_5JV79mVwqj|6g5^^|K?v-{wY`;GqVtb24Wkm^Az10Nh@2IESs}lApN+v} zZe5Mj%|_{bsDBY3zj*2T;5^MZJ7Oy6U5UfUGp!y{yJ!X5DAiQCxj@p9Q7?l$MU~@4 zlaqXH&h1aVfG8|a0i69^`)J6b%c`w?fVzM<5o0*eN|y|ZCS^z^lJxYyY`2SS|R3K@^ey?WcXxZEixed&?-l+NL0x zNZqxf{$)bDLBE3Q*Rny;a_#w6bP#|^7p)JePsF`GyU^dXI~VF0Y?-3aPs4>ZYBcgkz2cRP&1j9BZ|v45sQP8QGRaGOLD8H`014|{2Msg?1=!2jqjEJUx+UJg_KWn7A39nV)nzLZZgt_AeVu;8dJ8nnBAm2o>ylB|`)l-)12_G;9!J zw(`wpB*+xQe)r)y)ecC@@NcE8ID+Q%BI;jy|Et>X4-(n*b))rWZD!yQBEKcc+-rO^ z4#3oERnIFkLBYS)xJ-o+_jei&2_F|>skG30`3>s&MPiwWMpl72s_DmYWSMjFXytRu zO;cv0G7AB7)?QiSRRrtqs%hlWbld#s1pDdR7@dVN{O;1#%=$FPfuewhwaR*fWI1xj zp6#gZOKnVZ*RtXF2?_@gYd4C!{G+_Z1cOadws4$Y-q~HxZgOT$MOX|fE`6|>@>%@u zGl4?$j*tr(k6a(~aG&v}7u(rD3BbE@y5+f`)vgZo9((E&>dx&)124M(<<5_lt&TKy zy~k*&f@^)wkWUmVVAhK){=AEtQPIvw)O}Fl??FkxL|7b)~7<}#9c<; zUXck7rgW;mx;q~722u7!HR9c*RK1N4>}M+U`5@G}L-5|`K{LfEjPGu!0OE4qM!uW7 ziRHe8pp$Vvk-Lapwlisk?tjJBxw-?HV;|LVov`W~$A?Oh66)^x*zr*DJ^z|LR5GEs4WNh?AzT8Z-`JKtSd}5;&Mx@sCw^{wx_Xr*vIE~RL2W6 zGTvSl$4Sgl7H&$aRE`WN*TVv+(0Gb+hwOp=^H1m4Zu=XLC#CO&C=_;iB<1g?Eb_%Q-l MncJC>&wKv$AN+5(rvLx| literal 0 HcmV?d00001 diff --git a/public/icon-192.png b/public/icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..df14322cd29dba495a31ba0ff77b8420b26e819c GIT binary patch literal 2492 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 literal 0 HcmV?d00001 diff --git a/public/icon-512.png b/public/icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..0a2c4e848f7114825e3cb5ce1c63920ed8a329d5 GIT binary patch literal 6866 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 literal 0 HcmV?d00001 diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..127797d --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,39 @@ +{ + "name": "rNotes - Universal Knowledge Capture", + "short_name": "rNotes", + "description": "Capture notes, clips, bookmarks, code, audio, and files. Organize in notebooks, tag freely.", + "start_url": "/", + "scope": "/", + "id": "/", + "display": "standalone", + "background_color": "#0a0a0a", + "theme_color": "#0a0a0a", + "orientation": "portrait-primary", + "categories": ["productivity", "utilities"], + "icons": [ + { + "src": "/icon-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/icon-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "/icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/public/sw.js b/public/sw.js new file mode 100644 index 0000000..3fbbb0d --- /dev/null +++ b/public/sw.js @@ -0,0 +1,43 @@ +const CACHE_NAME = 'rnotes-v1'; +const PRECACHE = [ + '/', + '/manifest.json', + '/icon-192.png', + '/icon-512.png', +]; + +self.addEventListener('install', (event) => { + event.waitUntil( + caches.open(CACHE_NAME).then((cache) => cache.addAll(PRECACHE)) + ); + self.skipWaiting(); +}); + +self.addEventListener('activate', (event) => { + event.waitUntil( + caches.keys().then((names) => + Promise.all(names.filter((n) => n !== CACHE_NAME).map((n) => caches.delete(n))) + ) + ); + self.clients.claim(); +}); + +self.addEventListener('fetch', (event) => { + const url = new URL(event.request.url); + + // Always go to network for API calls + if (url.pathname.startsWith('/api/')) return; + + // Network-first for pages, cache-first for static assets + event.respondWith( + fetch(event.request) + .then((response) => { + if (response.status === 200) { + const clone = response.clone(); + caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone)); + } + return response; + }) + .catch(() => caches.match(event.request)) + ); +}); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index ecb2bdb..df465bc 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,7 +1,8 @@ -import type { Metadata } from 'next' +import type { Metadata, Viewport } from 'next' import { Inter } from 'next/font/google' import './globals.css' import { AuthProvider } from '@/components/AuthProvider' +import { PWAInstall } from '@/components/PWAInstall' const inter = Inter({ subsets: ['latin'], @@ -11,6 +12,12 @@ const inter = Inter({ export const metadata: Metadata = { title: 'rNotes - Universal Knowledge Capture', description: 'Capture notes, clips, bookmarks, code, and files. Organize in notebooks, tag freely, and collaborate on a visual canvas shared across r*Spaces.', + manifest: '/manifest.json', + appleWebApp: { + capable: true, + statusBarStyle: 'black-translucent', + title: 'rNotes', + }, openGraph: { title: 'rNotes - Universal Knowledge Capture', description: 'Capture notes, clips, bookmarks, code, and files with a collaborative canvas.', @@ -19,6 +26,10 @@ export const metadata: Metadata = { }, } +export const viewport: Viewport = { + themeColor: '#0a0a0a', +} + export default function RootLayout({ children, }: Readonly<{ @@ -26,9 +37,13 @@ export default function RootLayout({ }>) { return ( + + + {children} + diff --git a/src/components/PWAInstall.tsx b/src/components/PWAInstall.tsx new file mode 100644 index 0000000..4ea7b69 --- /dev/null +++ b/src/components/PWAInstall.tsx @@ -0,0 +1,144 @@ +'use client'; + +import { useEffect, useState, useCallback } from 'react'; + +interface BeforeInstallPromptEvent extends Event { + prompt(): Promise; + userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>; +} + +export function PWAInstall() { + const [deferredPrompt, setDeferredPrompt] = useState(null); + const [showBanner, setShowBanner] = useState(false); + const [isIOS, setIsIOS] = useState(false); + const [showInstructions, setShowInstructions] = useState(false); + + useEffect(() => { + // Register service worker + if ('serviceWorker' in navigator) { + navigator.serviceWorker.register('/sw.js').catch(console.error); + } + + // Check if already installed + const isStandalone = + window.matchMedia('(display-mode: standalone)').matches || + (navigator as unknown as { standalone?: boolean }).standalone === true; + + if (isStandalone) return; + + // Detect iOS + const ios = /iPad|iPhone|iPod/.test(navigator.userAgent); + setIsIOS(ios); + + // Check dismiss cooldown (24h) + const dismissedAt = localStorage.getItem('pwa_dismissed'); + if (dismissedAt && Date.now() - parseInt(dismissedAt) < 86400000) return; + + setShowBanner(true); + + // Capture the install prompt + const handler = (e: Event) => { + e.preventDefault(); + setDeferredPrompt(e as BeforeInstallPromptEvent); + }; + + window.addEventListener('beforeinstallprompt', handler); + window.addEventListener('appinstalled', () => { + setShowBanner(false); + setDeferredPrompt(null); + }); + + return () => window.removeEventListener('beforeinstallprompt', handler); + }, []); + + const handleInstall = useCallback(async () => { + if (deferredPrompt) { + deferredPrompt.prompt(); + const { outcome } = await deferredPrompt.userChoice; + if (outcome === 'accepted') { + setShowBanner(false); + } + setDeferredPrompt(null); + } else { + // No native prompt available — show manual instructions + setShowInstructions(true); + } + }, [deferredPrompt]); + + const handleDismiss = useCallback(() => { + setShowBanner(false); + localStorage.setItem('pwa_dismissed', Date.now().toString()); + }, []); + + if (!showBanner) return null; + + return ( +
+
+ 📲 +
+ {showInstructions ? ( + isIOS ? ( +
+

Add to Home Screen

+

+ Tap{' '} + + ⎋ Share + {' '} + then{' '} + + Add to Home Screen + +

+
+ ) : ( +
+

Install rNotes

+

+ 1. Tap{' '} + + ⋮ + {' '} + (three dots) at top-right +
+ 2. Tap{' '} + + Add to Home screen + +
+ 3. Tap{' '} + + Install + +

+
+ ) + ) : ( +
+

Install rNotes

+

Add to your home screen for quick access

+
+ )} +
+
+ {!showInstructions && ( + + )} + +
+
+
+ ); +}