Merge pull request #4 from ciphera-net/staging

PULSE-31: PWA support and offline banner
This commit is contained in:
Usman
2026-02-04 12:46:26 +01:00
committed by GitHub
13 changed files with 3179 additions and 171 deletions

5
.gitignore vendored
View File

@@ -34,3 +34,8 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
# PWA
public/sw.js
public/workbox-*.js
public/swe-worker-*.js

View File

@@ -1,7 +1,9 @@
'use client'
import { OfflineBanner } from '@/components/OfflineBanner'
import { Header, Footer } from '@ciphera-net/ui'
import { useAuth } from '@/lib/auth/context'
import { useOnlineStatus } from '@/lib/hooks/useOnlineStatus'
import Link from 'next/link'
import { useEffect, useState } from 'react'
import { getUserOrganizations, switchContext } from '@/lib/api/organization'
@@ -11,6 +13,7 @@ import { useRouter } from 'next/navigation'
export default function LayoutContent({ children }: { children: React.ReactNode }) {
const auth = useAuth()
const router = useRouter()
const isOnline = useOnlineStatus()
const [orgs, setOrgs] = useState<any[]>([])
// * Fetch organizations for the header workspace switcher
@@ -37,8 +40,14 @@ export default function LayoutContent({ children }: { children: React.ReactNode
router.push('/onboarding')
}
const showOfflineBar = Boolean(auth.user && !isOnline);
const barHeightRem = 2.5;
const headerHeightRem = 6;
const mainTopPaddingRem = barHeightRem + headerHeightRem;
return (
<>
{auth.user && <OfflineBanner isOnline={isOnline} />}
<Header
auth={auth}
LinkComponent={Link}
@@ -52,8 +61,12 @@ export default function LayoutContent({ children }: { children: React.ReactNode
showFaq={false}
showSecurity={false}
showPricing={true}
topOffset={showOfflineBar ? `${barHeightRem}rem` : undefined}
/>
<main className="flex-1 pt-24 pb-8">
<main
className={`flex-1 pb-8 ${showOfflineBar ? '' : 'pt-24'}`}
style={showOfflineBar ? { paddingTop: `${mainTopPaddingRem}rem` } : undefined}
>
{children}
</main>
<Footer

View File

@@ -31,6 +31,7 @@ export const metadata: Metadata = {
shortcut: '/favicon.ico',
apple: '/pulse_icon_no_margins.png',
},
manifest: '/manifest.json',
robots: {
index: true,
follow: true,

View File

@@ -0,0 +1,14 @@
'use client';
import { FiWifiOff } from 'react-icons/fi';
export function OfflineBanner({ isOnline }: { isOnline: boolean }) {
if (isOnline) return null;
return (
<div className="fixed top-0 left-0 right-0 z-[100] rounded-b-xl bg-yellow-500/15 dark:bg-yellow-500/25 border-b border-yellow-500/30 dark:border-yellow-500/40 text-yellow-700 dark:text-yellow-300 px-4 sm:px-8 py-2.5 text-sm flex items-center justify-center gap-2 font-medium shadow-md">
<FiWifiOff className="w-4 h-4 shrink-0" />
<span>You are currently offline. Changes may not be saved.</span>
</div>
);
}

View File

@@ -0,0 +1,24 @@
import { useState, useEffect } from 'react';
export function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
if (typeof window === 'undefined') return;
setIsOnline(navigator.onLine);
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return isOnline;
}

View File

@@ -1,4 +1,10 @@
import type { NextConfig } from 'next'
const withPWA = require("@ducanh2912/next-pwa").default({
dest: "public",
register: true,
skipWaiting: true,
disable: process.env.NODE_ENV === "development",
});
const nextConfig: NextConfig = {
reactStrictMode: true,
@@ -17,4 +23,4 @@ const nextConfig: NextConfig = {
},
}
export default nextConfig
export default withPWA(nextConfig)

3257
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,13 +4,14 @@
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"build": "next build --webpack",
"start": "next start",
"lint": "next lint",
"type-check": "tsc --noEmit"
},
"dependencies": {
"@ciphera-net/ui": "^0.0.38",
"@ciphera-net/ui": "^0.0.42",
"@ducanh2912/next-pwa": "^10.2.9",
"axios": "^1.13.2",
"country-flag-icons": "^1.6.4",
"d3-scale": "^4.0.2",

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
public/icon-192x192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

BIN
public/icon-512x512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

21
public/manifest.json Normal file
View File

@@ -0,0 +1,21 @@
{
"name": "Pulse",
"short_name": "Pulse",
"description": "Privacy-friendly website analytics",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#FD5E0F",
"icons": [
{
"src": "/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}