Merge pull request #4 from ciphera-net/staging
PULSE-31: PWA support and offline banner
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -34,3 +34,8 @@ yarn-error.log*
|
|||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
|
|
||||||
|
# PWA
|
||||||
|
public/sw.js
|
||||||
|
public/workbox-*.js
|
||||||
|
public/swe-worker-*.js
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
|
import { OfflineBanner } from '@/components/OfflineBanner'
|
||||||
import { Header, Footer } from '@ciphera-net/ui'
|
import { Header, Footer } from '@ciphera-net/ui'
|
||||||
import { useAuth } from '@/lib/auth/context'
|
import { useAuth } from '@/lib/auth/context'
|
||||||
|
import { useOnlineStatus } from '@/lib/hooks/useOnlineStatus'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { getUserOrganizations, switchContext } from '@/lib/api/organization'
|
import { getUserOrganizations, switchContext } from '@/lib/api/organization'
|
||||||
@@ -11,6 +13,7 @@ import { useRouter } from 'next/navigation'
|
|||||||
export default function LayoutContent({ children }: { children: React.ReactNode }) {
|
export default function LayoutContent({ children }: { children: React.ReactNode }) {
|
||||||
const auth = useAuth()
|
const auth = useAuth()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const isOnline = useOnlineStatus()
|
||||||
const [orgs, setOrgs] = useState<any[]>([])
|
const [orgs, setOrgs] = useState<any[]>([])
|
||||||
|
|
||||||
// * Fetch organizations for the header workspace switcher
|
// * Fetch organizations for the header workspace switcher
|
||||||
@@ -37,8 +40,14 @@ export default function LayoutContent({ children }: { children: React.ReactNode
|
|||||||
router.push('/onboarding')
|
router.push('/onboarding')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showOfflineBar = Boolean(auth.user && !isOnline);
|
||||||
|
const barHeightRem = 2.5;
|
||||||
|
const headerHeightRem = 6;
|
||||||
|
const mainTopPaddingRem = barHeightRem + headerHeightRem;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{auth.user && <OfflineBanner isOnline={isOnline} />}
|
||||||
<Header
|
<Header
|
||||||
auth={auth}
|
auth={auth}
|
||||||
LinkComponent={Link}
|
LinkComponent={Link}
|
||||||
@@ -52,8 +61,12 @@ export default function LayoutContent({ children }: { children: React.ReactNode
|
|||||||
showFaq={false}
|
showFaq={false}
|
||||||
showSecurity={false}
|
showSecurity={false}
|
||||||
showPricing={true}
|
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}
|
{children}
|
||||||
</main>
|
</main>
|
||||||
<Footer
|
<Footer
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export const metadata: Metadata = {
|
|||||||
shortcut: '/favicon.ico',
|
shortcut: '/favicon.ico',
|
||||||
apple: '/pulse_icon_no_margins.png',
|
apple: '/pulse_icon_no_margins.png',
|
||||||
},
|
},
|
||||||
|
manifest: '/manifest.json',
|
||||||
robots: {
|
robots: {
|
||||||
index: true,
|
index: true,
|
||||||
follow: true,
|
follow: true,
|
||||||
|
|||||||
14
components/OfflineBanner.tsx
Normal file
14
components/OfflineBanner.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
24
lib/hooks/useOnlineStatus.ts
Normal file
24
lib/hooks/useOnlineStatus.ts
Normal 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;
|
||||||
|
}
|
||||||
@@ -1,4 +1,10 @@
|
|||||||
import type { NextConfig } from 'next'
|
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 = {
|
const nextConfig: NextConfig = {
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
@@ -17,4 +23,4 @@ const nextConfig: NextConfig = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default nextConfig
|
export default withPWA(nextConfig)
|
||||||
|
|||||||
3257
package-lock.json
generated
3257
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,13 +4,14 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build --webpack",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"type-check": "tsc --noEmit"
|
"type-check": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ciphera-net/ui": "^0.0.38",
|
"@ciphera-net/ui": "^0.0.42",
|
||||||
|
"@ducanh2912/next-pwa": "^10.2.9",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"country-flag-icons": "^1.6.4",
|
"country-flag-icons": "^1.6.4",
|
||||||
"d3-scale": "^4.0.2",
|
"d3-scale": "^4.0.2",
|
||||||
|
|||||||
BIN
public/Icon Padding left & right 192x192.png
Normal file
BIN
public/Icon Padding left & right 192x192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.8 KiB |
BIN
public/Icon Padding left & right 512x512.png
Normal file
BIN
public/Icon Padding left & right 512x512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
BIN
public/icon-192x192.png
Normal file
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
BIN
public/icon-512x512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
21
public/manifest.json
Normal file
21
public/manifest.json
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user