PULSE-31: PWA support and offline banner #4
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
@@ -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
@@ -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);
|
||||||
|
Prompt To Fix With AI`window.addEventListener` is called without checking if `window` exists (like line 8 does), which will cause SSR errors in Next.js.
```suggestion
if (typeof window !== 'undefined') {
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
}
```
<details><summary>Prompt To Fix With AI</summary>
`````markdown
This is a comment left during a code review.
Path: lib/hooks/useOnlineStatus.ts
Line: 15:16
Comment:
`window.addEventListener` is called without checking if `window` exists (like line 8 does), which will cause SSR errors in Next.js.
```suggestion
if (typeof window !== 'undefined') {
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
}
```
How can I resolve this? If you propose a fix, please make it concise.
`````
</details>
|
|||||||
|
|
||||||
|
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
@@ -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
|
After Width: | Height: | Size: 7.8 KiB |
BIN
public/Icon Padding left & right 512x512.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
public/icon-192x192.png
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
public/icon-512x512.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
21
public/manifest.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
Icon paths use URL encoding ( Prompt To Fix With AIIcon paths use URL encoding (`%20` and `%26`) which may cause issues with some browsers or PWA installers. Use hyphens or underscores in filenames instead.
```suggestion
{
"src": "/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
```
<details><summary>Prompt To Fix With AI</summary>
`````markdown
This is a comment left during a code review.
Path: public/manifest.json
Line: 11:18
Comment:
Icon paths use URL encoding (`%20` and `%26`) which may cause issues with some browsers or PWA installers. Use hyphens or underscores in filenames instead.
```suggestion
{
"src": "/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
```
How can I resolve this? If you propose a fix, please make it concise.
`````
</details>
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time! Prompt To Fix With AI`theme_color` is `#000000` but `layout.tsx:19` sets `themeColor: '#FD5E0F'`. These should match for consistency.
<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>
<details><summary>Prompt To Fix With AI</summary>
`````markdown
This is a comment left during a code review.
Path: public/manifest.json
Line: 8:8
Comment:
`theme_color` is `#000000` but `layout.tsx:19` sets `themeColor: '#FD5E0F'`. These should match for consistency.
<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>
How can I resolve this? If you propose a fix, please make it concise.
`````
</details>
|
|||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Moved auth check to parent instead of keeping it in
OfflineBanner- the banner now only handles online/offline state while this component controls when it shows based on authentication.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Prompt To Fix With AI
barHeightRemis defined but not used in thept-[8.5rem]calculation on line 64. The hardcoded value could get out of sync if the banner height changes.Prompt To Fix With AI