Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import type { Metadata } from "next";
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { OfflineBanner } from "@/src/components/common/OfflineBanner";

const baseUrl = process.env.NEXT_PUBLIC_BASE_URL ?? "http://localhost:3000";

const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});

const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
Expand All @@ -22,6 +21,7 @@ export const metadata: Metadata = {
},
description:
"Utility metering and billing dashboard for monitoring meters, managing gas buffers, and tracking usage on Stellar Soroban.",
manifest: "/manifest.json",
openGraph: {
title: "EquipChain Dashboard",
description:
Expand Down Expand Up @@ -64,6 +64,7 @@ export default function RootLayout({
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<OfflineBanner />
{children}
</body>
</html>
Expand Down
37 changes: 37 additions & 0 deletions app/offline/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
'use client';

import { useEffect, useState } from 'react';
import { useOnlineStatus } from '@/src/lib/hooks/useOnlineStatus';

export default function OfflinePage() {
const { isOnline } = useOnlineStatus();
const [lastUpdated, setLastUpdated] = useState<string | null>(null);

useEffect(() => {
setLastUpdated(new Date().toLocaleString());
}, []);

const handleRetry = () => {
window.location.href = '/';
};

return (
<div className="min-h-screen flex flex-col items-center justify-center px-6 text-center gap-4">
<h1 className="text-2xl font-semibold">You&apos;re offline</h1>
<p className="text-sm opacity-70 max-w-md">
{isOnline
? "You're back online — you can return to the dashboard."
: "We can't reach EquipChain right now. Showing your last cached data where available."}
</p>
{lastUpdated && (
<p className="text-xs opacity-50">Last updated: {lastUpdated}</p>
)}
<button
onClick={handleRetry}
className="mt-2 px-4 py-2 rounded-md bg-black text-white dark:bg-white dark:text-black text-sm font-medium"
>
{isOnline ? 'Return to dashboard' : 'Retry'}
</button>
</div>
);
}
46 changes: 46 additions & 0 deletions app/sw.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/// <reference lib="webworker" />
import { defaultCache } from '@serwist/next/worker';
import type { PrecacheEntry, SerwistGlobalConfig } from 'serwist';
import { Serwist, NetworkFirst, StaleWhileRevalidate } from 'serwist';

declare global {
interface WorkerGlobalScope extends SerwistGlobalConfig {
__SW_MANIFEST: (PrecacheEntry | string)[] | undefined;
}
}

declare const self: ServiceWorkerGlobalScope;

const serwist = new Serwist({
precacheEntries: self.__SW_MANIFEST,
skipWaiting: true,
clientsClaim: true,
navigationPreload: true,
runtimeCaching: [
{
matcher: /^\/api\/.*/,
handler: new StaleWhileRevalidate({
cacheName: 'api-cache',
}),
},
{
matcher: /\/offline$/,
handler: new NetworkFirst({
cacheName: 'offline-fallback',
}),
},
...defaultCache,
],
fallbacks: {
entries: [
{
url: '/offline',
matcher({ request }) {
return request.destination === 'document';
},
},
],
},
});

serwist.addEventListeners();
10 changes: 8 additions & 2 deletions next.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import type { NextConfig } from "next";
import type { NextConfig } from "next";
import withSerwistInit from "@serwist/next";

const withSerwist = withSerwistInit({
swSrc: "app/sw.ts",
swDest: "public/sw.js",
});

const nextConfig: NextConfig = {
async rewrites() {
Expand All @@ -11,4 +17,4 @@ const nextConfig: NextConfig = {
},
};

export default nextConfig;
export default withSerwist(nextConfig);
Loading
Loading