diff options
| author | Adam Soltys <[email protected]> | 2026-03-02 21:22:41 -0800 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-03-02 21:22:41 -0800 |
| commit | 7c4d393feaf9c594fa6bdae274abc0edd45c3373 (patch) | |
| tree | 74ea34dbbc9b7399b910ae92af9208f2c787ecde /src | |
| parent | 90f29dce3d675f850e48ede2edb187e6c53b84da (diff) | |
| download | coinos-ui-7c4d393feaf9c594fa6bdae274abc0edd45c3373.tar.gz coinos-ui-7c4d393feaf9c594fa6bdae274abc0edd45c3373.zip | |
offline mode
Diffstat (limited to 'src')
| -rw-r--r-- | src/components/OfflineBanner.svelte | 10 | ||||
| -rw-r--r-- | src/lib/store.ts | 5 | ||||
| -rw-r--r-- | src/routes/(app)/+layout.svelte | 13 | ||||
| -rw-r--r-- | src/routes/(app)/[username]/(profile)/+page.svelte | 8 | ||||
| -rw-r--r-- | src/routes/(app)/payments/[...page]/+page.svelte | 5 | ||||
| -rw-r--r-- | src/service-worker.js | 102 |
6 files changed, 136 insertions, 7 deletions
diff --git a/src/components/OfflineBanner.svelte b/src/components/OfflineBanner.svelte new file mode 100644 index 00000000..5b59513d --- /dev/null +++ b/src/components/OfflineBanner.svelte @@ -0,0 +1,10 @@ +<script lang="ts"> + import { offline } from "$lib/store"; +</script> + +{#if $offline} + <div class="alert alert-warning sticky top-0 z-50 rounded-none justify-center gap-2"> + <iconify-icon icon="ph:wifi-slash-bold" width="20"></iconify-icon> + You're offline — showing cached data + </div> +{/if} diff --git a/src/lib/store.ts b/src/lib/store.ts index 435df4b2..13aa8bd8 100644 --- a/src/lib/store.ts +++ b/src/lib/store.ts @@ -101,3 +101,8 @@ export const paymentSignal = writable(0); export const lastPayment = writable<{ iid: string; amount: number; confirmed: boolean } | null>( null, ); +export const cachedUser = persistLocal("cachedUser"); +export const cachedAccounts = persistLocal("cachedAccounts", []); +export const cachedPayments = persistLocal("cachedPayments", []); +export const cachedRates = persistLocal("cachedRates"); +export const offline = writable(false); diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte index 40f28444..748dfdba 100644 --- a/src/routes/(app)/+layout.svelte +++ b/src/routes/(app)/+layout.svelte @@ -16,11 +16,14 @@ theme as themeStore, fiat, rate as rateStore, + cachedUser, + offline, } from "$lib/store"; import { page } from "$app/stores"; import { browser } from "$app/environment"; import LoadingSplash from "$comp/LoadingSplash.svelte"; import AppHeader from "$comp/AppHeader.svelte"; + import OfflineBanner from "$comp/OfflineBanner.svelte"; import Nostr from "$comp/Nostr.svelte"; import Password from "$comp/Password.svelte"; import { s, f, toFiat, success, getCookie, warning, post } from "$lib/utils"; @@ -41,6 +44,9 @@ $effect(() => { theme = $themeStore; }); + $effect(() => { + if (user) $cachedUser = user; + }); afterNavigate(() => { document.cookie = `pathname=${$page.url.pathname}; path=/; max-age=86400`; @@ -172,6 +178,12 @@ onMount(async () => { if (browser) { + $offline = !navigator.onLine; + const goOnline = () => ($offline = false); + const goOffline = () => ($offline = true); + window.addEventListener("online", goOnline); + window.addEventListener("offline", goOffline); + checkSocket(); $pin = getCookie("pin"); @@ -274,6 +286,7 @@ <SvelteToast options={{ reversed: true, intro: { y: 192 } }} /> <main class="pb-4 min-h-dvh" data-theme={theme}> + <OfflineBanner /> <AppHeader {user} {subject} /> {#if !$loading} {@render children?.()} diff --git a/src/routes/(app)/[username]/(profile)/+page.svelte b/src/routes/(app)/[username]/(profile)/+page.svelte index 2bde3e08..9c0d2203 100644 --- a/src/routes/(app)/[username]/(profile)/+page.svelte +++ b/src/routes/(app)/[username]/(profile)/+page.svelte @@ -5,7 +5,7 @@ import Balance from "$comp/Balance.svelte"; import Spinner from "$comp/Spinner.svelte"; import { t } from "$lib/translations"; - import { installPrompt, password } from "$lib/store"; + import { installPrompt, password, cachedAccounts, cachedRates } from "$lib/store"; import { afterNavigate, goto, invalidate, preloadData } from "$app/navigation"; import { page } from "$app/stores"; import { versions, post, fail, getCookie } from "$lib/utils"; @@ -84,6 +84,12 @@ $effect(() => { if (user) user.savings = 0; }); + $effect(() => { + if (accounts?.length) $cachedAccounts = accounts; + }); + $effect(() => { + if (rates) $cachedRates = rates; + }); let { host } = $derived($page.url); </script> diff --git a/src/routes/(app)/payments/[...page]/+page.svelte b/src/routes/(app)/payments/[...page]/+page.svelte index 1d5a0407..afce7711 100644 --- a/src/routes/(app)/payments/[...page]/+page.svelte +++ b/src/routes/(app)/payments/[...page]/+page.svelte @@ -3,7 +3,7 @@ import Payments from "$comp/Payments.svelte"; import { onMount } from "svelte"; import { format } from "date-fns"; - import { newPayment } from "$lib/store"; + import { newPayment, cachedPayments } from "$lib/store"; import { t } from "$lib/translations"; import { get, f, s, si, sat, loc, sats, types } from "$lib/utils"; import { page } from "$app/stores"; @@ -55,6 +55,9 @@ invalidate(`/users/${user.username}`); } }); + $effect(() => { + if (payments?.length && (!p || p === "1")) $cachedPayments = payments; + }); let path = $derived( $page.params.page diff --git a/src/service-worker.js b/src/service-worker.js index a366b9d8..3c1ae0ac 100644 --- a/src/service-worker.js +++ b/src/service-worker.js @@ -1,18 +1,110 @@ +import { build, files, version } from "$service-worker"; import { Worker } from "@arkade-os/sdk"; const worker = new Worker(); -// Only add message listener - we handle install/activate ourselves worker.start(false).catch(console.error); -self.addEventListener("install", () => { - self.skipWaiting(); +const CACHE_BUILD = `coinos-build-${version}`; +const CACHE_PAGES = `coinos-pages-${version}`; +const CACHE_PUBLIC = `coinos-public-${version}`; + +const ESSENTIAL_STATIC = files.filter( + (f) => + f.endsWith(".woff2") || + f.endsWith(".woff") || + f.endsWith(".png") || + f.endsWith(".webp") || + f.endsWith(".webmanifest") || + f === "/favicon.ico", +); + +self.addEventListener("install", (event) => { + event.waitUntil( + caches + .open(CACHE_BUILD) + .then((cache) => cache.addAll([...build, ...ESSENTIAL_STATIC])) + .then(() => self.skipWaiting()), + ); }); self.addEventListener("activate", (event) => { - event.waitUntil(self.clients.claim()); + event.waitUntil( + caches + .keys() + .then((keys) => + Promise.all( + keys + .filter((k) => k.startsWith("coinos-") && k !== CACHE_BUILD && k !== CACHE_PAGES && k !== CACHE_PUBLIC) + .map((k) => caches.delete(k)), + ), + ) + .then(() => self.clients.claim()), + ); }); -self.addEventListener("fetch", () => {}); +self.addEventListener("fetch", (event) => { + const { request } = event; + const url = new URL(request.url); + + if (request.method !== "GET") return; + if (url.origin !== self.location.origin) return; + + // Build assets — cache-first (fingerprinted/immutable) + if (build.includes(url.pathname)) { + event.respondWith(caches.match(request).then((cached) => cached || fetch(request))); + return; + } + + // Navigation HTML — network-first + if (request.mode === "navigate") { + event.respondWith( + fetch(request) + .then((response) => { + const clone = response.clone(); + caches.open(CACHE_PAGES).then((cache) => cache.put(request, clone)); + return response; + }) + .catch(() => caches.match(request).then((cached) => cached || caches.match("/"))), + ); + return; + } + + // __data.json (SvelteKit client-side navigation) — network-first + if (url.pathname.endsWith("__data.json")) { + event.respondWith( + fetch(request) + .then((response) => { + const clone = response.clone(); + caches.open(CACHE_PAGES).then((cache) => cache.put(request, clone)); + return response; + }) + .catch(() => caches.match(request)), + ); + return; + } + + // Public API (avatars etc) — cache-first + if (url.pathname.startsWith("/api/public/")) { + event.respondWith( + caches.match(request).then( + (cached) => + cached || + fetch(request).then((response) => { + const clone = response.clone(); + caches.open(CACHE_PUBLIC).then((cache) => cache.put(request, clone)); + return response; + }), + ), + ); + return; + } + + // Static files from ESSENTIAL_STATIC — cache-first + if (ESSENTIAL_STATIC.includes(url.pathname)) { + event.respondWith(caches.match(request).then((cached) => cached || fetch(request))); + return; + } +}); self.addEventListener("push", (event) => { const { title, body, url } = event.data.json(); |
