Compare commits

..

No commits in common. "0a0769115bc2967f69154ab12c8043d738599953" and "8303b0981b23469ff7088d83201bd0f79d10c4cd" have entirely different histories.

24 changed files with 217 additions and 103 deletions

126
public/sw.js Normal file
View file

@ -0,0 +1,126 @@
// Custom service worker for push notifications
// This will be merged with Workbox generated SW
import { precacheAndRoute, cleanupOutdatedCaches } from 'workbox-precaching'
import { clientsClaim, skipWaiting } from 'workbox-core'
// Precache and route static assets
precacheAndRoute(self.__WB_MANIFEST)
cleanupOutdatedCaches()
// Take control of all pages immediately
skipWaiting()
clientsClaim()
// Push notification event handler
self.addEventListener('push', (event) => {
console.log('Push event received:', event)
let notificationData = {
title: 'New Announcement',
body: 'You have a new admin announcement',
icon: '/pwa-192x192.png',
badge: '/pwa-192x192.png',
data: {
url: '/',
timestamp: Date.now()
},
tag: 'admin-announcement',
requireInteraction: true,
actions: [
{
action: 'view',
title: 'View',
icon: '/pwa-192x192.png'
},
{
action: 'dismiss',
title: 'Dismiss'
}
]
}
// Parse push data if available
if (event.data) {
try {
const pushData = event.data.json()
notificationData = {
...notificationData,
...pushData
}
} catch (error) {
console.warn('Failed to parse push data:', error)
// Use default notification data
}
}
event.waitUntil(
self.registration.showNotification(notificationData.title, notificationData)
)
})
// Notification click handler
self.addEventListener('notificationclick', (event) => {
console.log('Notification clicked:', event)
event.notification.close()
const action = event.action
const notificationData = event.notification.data || {}
if (action === 'dismiss') {
return // Just close the notification
}
// Default action or 'view' action - open the app
const urlToOpen = notificationData.url || '/'
event.waitUntil(
clients.matchAll({ type: 'window', includeUncontrolled: true })
.then((clientList) => {
// Try to find an existing window with the app
for (const client of clientList) {
if (client.url.includes(self.location.origin) && 'focus' in client) {
client.focus()
// Navigate to the notification URL if different
if (client.url !== urlToOpen) {
client.navigate(urlToOpen)
}
return
}
}
// If no existing window, open a new one
if (clients.openWindow) {
return clients.openWindow(urlToOpen)
}
})
)
})
// Background sync for offline notification queue (future enhancement)
self.addEventListener('sync', (event) => {
if (event.tag === 'notification-queue') {
event.waitUntil(
// Process any queued notifications when back online
console.log('Background sync: notification-queue')
)
}
})
// Message handler for communication with main app
self.addEventListener('message', (event) => {
console.log('Service worker received message:', event.data)
if (event.data && event.data.type === 'SHOW_NOTIFICATION') {
const { title, body, data } = event.data.payload
self.registration.showNotification(title, {
body,
icon: '/pwa-192x192.png',
badge: '/pwa-192x192.png',
data,
tag: 'manual-notification',
requireInteraction: false
})
}
})

View file

@ -26,8 +26,8 @@ async function handleLoginSuccess() {
</script>
<template>
<div class="min-h-dvh bg-background font-sans antialiased">
<div class="relative flex min-h-dvh flex-col"
<div class="min-h-screen bg-background font-sans antialiased">
<div class="relative flex min-h-screen flex-col"
style="padding-top: env(safe-area-inset-top); padding-bottom: env(safe-area-inset-bottom)">
<div v-if="!isLoginPage && !isAuthenticated" class="fixed top-0 right-0 z-50 p-3" style="padding-top: env(safe-area-inset-top)">

View file

@ -49,8 +49,8 @@ async function handleLoginSuccess() {
</script>
<template>
<div class="min-h-dvh bg-background font-sans antialiased">
<div class="relative flex min-h-dvh flex-col"
<div class="min-h-screen bg-background font-sans antialiased">
<div class="relative flex min-h-screen flex-col"
style="padding-top: env(safe-area-inset-top); padding-bottom: env(safe-area-inset-bottom)">
<!-- Top bar with login -->

View file

@ -46,8 +46,8 @@ async function handleLoginSuccess() {
</script>
<template>
<div class="min-h-dvh bg-background font-sans antialiased">
<div class="relative flex min-h-dvh flex-col"
<div class="min-h-screen bg-background font-sans antialiased">
<div class="relative flex min-h-screen flex-col"
style="padding-top: env(safe-area-inset-top); padding-bottom: env(safe-area-inset-bottom)">
<!-- Main content (with bottom padding for nav bar) -->

View file

@ -177,31 +177,4 @@
body {
@apply bg-background text-foreground;
}
/* Match shadcn ScrollArea look on native scrollbars (full-page scroll,
textareas, anything not wrapped in <ScrollArea>). */
* {
scrollbar-width: thin;
scrollbar-color: var(--color-border) transparent;
}
*::-webkit-scrollbar {
width: 10px;
height: 10px;
}
*::-webkit-scrollbar-track {
background: transparent;
}
*::-webkit-scrollbar-thumb {
background-color: var(--color-border);
border-radius: 9999px;
border: 2px solid transparent;
background-clip: content-box;
}
*::-webkit-scrollbar-thumb:hover {
background-color: var(--color-muted-foreground);
background-clip: content-box;
}
*::-webkit-scrollbar-corner {
background: transparent;
}
}

View file

@ -26,8 +26,8 @@ async function handleLoginSuccess() {
</script>
<template>
<div class="min-h-dvh bg-background font-sans antialiased">
<div class="relative flex min-h-dvh flex-col"
<div class="min-h-screen bg-background font-sans antialiased">
<div class="relative flex min-h-screen flex-col"
style="padding-top: env(safe-area-inset-top); padding-bottom: env(safe-area-inset-bottom)">
<div v-if="!isLoginPage && !isAuthenticated" class="fixed top-0 right-0 z-50 p-3" style="padding-top: env(safe-area-inset-top)">

View file

@ -12,7 +12,7 @@ const openSidebar = () => {
</script>
<template>
<div class="min-h-dvh bg-background">
<div class="min-h-screen bg-background">
<!-- Mobile Drawer -->
<MobileDrawer v-model:open="sidebarOpen" />

View file

@ -59,8 +59,8 @@ async function handleLoginSuccess() {
</script>
<template>
<div class="min-h-dvh bg-background font-sans antialiased">
<div class="relative flex min-h-dvh flex-col"
<div class="min-h-screen bg-background font-sans antialiased">
<div class="relative flex min-h-screen flex-col"
style="padding-top: env(safe-area-inset-top); padding-bottom: env(safe-area-inset-bottom)">
<div v-if="!isLoginPage && !isAuthenticated" class="fixed top-0 right-0 z-50 p-3" style="padding-top: env(safe-area-inset-top)">

View file

@ -1,38 +0,0 @@
/**
* Unregister the legacy hub service worker (closes #41).
*
* Pre-#41 the hub shipped a Workbox SW at `${origin}/sw.js` with scope `/`,
* which claimed the whole origin and blocked Chrome from offering installs
* for the path-mounted standalones at /libra/, /market/, etc. The hub is no
* longer a PWA; this helper proactively cleans up that stale registration
* for users who installed the hub before this change shipped.
*
* Only touches SWs whose scope is the origin root. Standalone SWs at
* /libra/, /market/, etc. live at deeper scopes and are left alone they
* are still legitimate PWAs.
*
* Idempotent: once the legacy registration is gone, future calls are
* cheap no-ops. Safe to leave in place permanently.
*/
export async function decommissionHubServiceWorker(): Promise<void> {
if (!('serviceWorker' in navigator)) return
try {
const regs = await navigator.serviceWorker.getRegistrations()
const originRoot = `${location.origin}/`
const legacy = regs.filter(r => r.scope === originRoot)
if (legacy.length === 0) return
console.warn(
`[decommission-hub-sw] Unregistering ${legacy.length} legacy hub service worker(s).`
)
await Promise.all(legacy.map(r => r.unregister()))
if ('caches' in window) {
const keys = await caches.keys()
await Promise.all(keys.map(k => caches.delete(k)))
}
} catch (err) {
console.warn('[decommission-hub-sw] failed to unregister:', err)
}
}

View file

@ -1,15 +1,24 @@
// New modular application entry point
import { startApp } from './app'
import { registerSW } from 'virtual:pwa-register'
import { cleanupStaleDevServiceWorkers } from '@/lib/dev-sw-cleanup'
import { decommissionHubServiceWorker } from '@/lib/decommission-hub-sw'
import 'vue-sonner/style.css'
// Clean up any leftover dev-mode service workers from a previous session
cleanupStaleDevServiceWorkers()
// Hub is no longer a PWA (#41) — unregister any legacy hub SW left behind
// on users who installed the old hub PWA before this change shipped.
void decommissionHubServiceWorker()
// Simple periodic service worker updates
const intervalMS = 60 * 60 * 1000 // 1 hour
registerSW({
onRegistered(r) {
r && setInterval(() => {
r.update()
}, intervalMS)
},
onOfflineReady() {
console.log('App ready to work offline')
}
})
// Start the modular application
startApp()

View file

@ -69,8 +69,8 @@ async function handleLoginSuccess() {
</script>
<template>
<div class="min-h-dvh bg-background font-sans antialiased">
<div class="relative flex min-h-dvh flex-col"
<div class="min-h-screen bg-background font-sans antialiased">
<div class="relative flex min-h-screen flex-col"
style="padding-top: env(safe-area-inset-top); padding-bottom: env(safe-area-inset-bottom)">
<main class="flex-1" :class="{ 'pb-16': !isLoginPage }">

View file

@ -26,7 +26,7 @@ onMounted(() => {
</script>
<template>
<div class="flex flex-col h-[calc(100dvh-3.5rem)]">
<div class="flex flex-col h-[calc(100vh-3.5rem)]">
<!-- Loading overlay -->
<div v-if="isLoading && geoActivities.length === 0" class="flex-1 flex items-center justify-center">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />

View file

@ -1,6 +1,6 @@
<template>
<!-- Loading State -->
<div v-if="isLoading" class="flex flex-col items-center justify-center min-h-dvh">
<div v-if="isLoading" class="flex flex-col items-center justify-center min-h-screen">
<div class="flex flex-col items-center space-y-4">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-primary"></div>
<div class="text-center space-y-2">
@ -11,7 +11,7 @@
</div>
<!-- Error State -->
<div v-else-if="error" class="flex flex-col items-center justify-center min-h-dvh">
<div v-else-if="error" class="flex flex-col items-center justify-center min-h-screen">
<div class="text-center space-y-4">
<h2 class="text-xl font-semibold text-red-600">Failed to load chat</h2>
<p class="text-muted-foreground">{{ error }}</p>
@ -22,7 +22,7 @@
</div>
<!-- Chat Content - Only render when module is ready -->
<div v-else class="h-[calc(100dvh-3.5rem)] lg:h-[calc(100dvh-4rem)] xl:h-[calc(100dvh-5rem)] w-full">
<div v-else class="h-[calc(100vh-3.5rem)] lg:h-[calc(100vh-4rem)] xl:h-[calc(100vh-5rem)] w-full">
<ChatComponent />
</div>
</template>

View file

@ -275,7 +275,7 @@ watch(comments, (newComments) => {
</script>
<template>
<div class="min-h-dvh bg-background">
<div class="min-h-screen bg-background">
<!-- Header -->
<header class="sticky top-0 z-30 bg-background/95 backdrop-blur border-b">
<div class="max-w-4xl mx-auto px-4 py-3">

View file

@ -11,7 +11,7 @@ function onSubmissionClick(submission: SubmissionWithMeta) {
</script>
<template>
<div class="flex flex-col h-dvh bg-background">
<div class="flex flex-col h-screen bg-background">
<div class="sticky top-0 z-30 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 border-b">
<div class="max-w-4xl mx-auto flex items-center justify-between px-4 py-2 sm:px-6">
<h1 class="text-lg font-semibold">Forum</h1>

View file

@ -275,7 +275,7 @@ watch(comments, (newComments) => {
</script>
<template>
<div class="min-h-dvh bg-background">
<div class="min-h-screen bg-background">
<!-- Header -->
<header class="sticky top-0 z-30 bg-background/95 backdrop-blur border-b">
<div class="max-w-4xl mx-auto px-4 py-3">

View file

@ -1,5 +1,5 @@
<template>
<div class="flex flex-col h-dvh bg-background">
<div class="flex flex-col h-screen bg-background">
<!-- Header -->
<div class="sticky top-0 z-40 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 border-b">
<div class="flex items-center justify-between px-4 py-2 sm:px-6">

View file

@ -1,5 +1,5 @@
<template>
<div class="flex flex-col h-dvh bg-background">
<div class="flex flex-col h-screen bg-background">
<PWAInstallPrompt auto-show />
<!-- Header -->

View file

@ -96,7 +96,7 @@ function notImplemented() {
</script>
<template>
<div class="relative h-dvh flex flex-col text-foreground overflow-hidden bg-background"
<div class="relative h-screen flex flex-col text-foreground overflow-hidden bg-background"
style="
background-image:
linear-gradient(to bottom,

View file

@ -1,5 +1,5 @@
<template>
<div class="min-h-dvh flex items-center justify-center p-4 bg-gradient-to-br from-background via-background to-muted/20">
<div class="min-h-screen flex items-center justify-center p-4 bg-gradient-to-br from-background via-background to-muted/20">
<div class="w-full max-w-md space-y-8">
<!-- Logo and Title -->
<div class="text-center space-y-6">

View file

@ -1,6 +1,6 @@
<template>
<div
class="min-h-dvh flex items-start sm:items-center justify-center px-4 sm:px-6 lg:px-8 xl:px-12 2xl:px-16 py-8 sm:py-12">
class="min-h-screen flex items-start sm:items-center justify-center px-4 sm:px-6 lg:px-8 xl:px-12 2xl:px-16 py-8 sm:py-12">
<div class="flex flex-col items-center justify-center space-y-3 sm:space-y-6 max-w-4xl mx-auto w-full mt-8 sm:mt-0">
<!-- Welcome Section -->
<div class="text-center space-y-2 sm:space-y-4">

View file

@ -26,8 +26,8 @@ async function handleLoginSuccess() {
</script>
<template>
<div class="min-h-dvh bg-background font-sans antialiased">
<div class="relative flex min-h-dvh flex-col"
<div class="min-h-screen bg-background font-sans antialiased">
<div class="relative flex min-h-screen flex-col"
style="padding-top: env(safe-area-inset-top); padding-bottom: env(safe-area-inset-bottom)">
<div v-if="!isLoginPage && !isAuthenticated" class="fixed top-0 right-0 z-50 p-3" style="padding-top: env(safe-area-inset-top)">

View file

@ -26,8 +26,8 @@ async function handleLoginSuccess() {
</script>
<template>
<div class="min-h-dvh bg-background font-sans antialiased">
<div class="relative flex min-h-dvh flex-col"
<div class="min-h-screen bg-background font-sans antialiased">
<div class="relative flex min-h-screen flex-col"
style="padding-top: env(safe-area-inset-top); padding-bottom: env(safe-area-inset-bottom)">
<!-- Top bar with login -->

View file

@ -2,16 +2,12 @@ import { fileURLToPath, URL } from 'node:url'
import vue from '@vitejs/plugin-vue'
import tailwindcss from '@tailwindcss/vite'
import { defineConfig } from 'vite'
import { VitePWA } from 'vite-plugin-pwa'
import Inspect from 'vite-plugin-inspect'
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
import { visualizer } from 'rollup-plugin-visualizer'
// https://vite.dev/config/
//
// The hub is intentionally NOT a PWA (closes #41). Its scope `/` claimed
// the entire origin and blocked Chrome from offering installs for the
// path-mounted standalones at /libra/, /market/, etc. The hub is a
// launcher page; users install the standalones they actually use.
export default defineConfig(({ mode }) => ({
// Per-app dep cache so concurrent dev servers don't race on .vite/deps
cacheDir: 'node_modules/.vite-hub',
@ -22,6 +18,54 @@ export default defineConfig(({ mode }) => ({
plugins: [
vue(),
tailwindcss(),
VitePWA({
registerType: 'autoUpdate',
devOptions: {
// SW disabled in dev — was caching stale bundles across restarts.
// Run `npm run preview` to test PWA behaviour against a real build.
enabled: false
},
// strategies: 'injectManifest',
srcDir: 'public',
filename: 'sw.js',
workbox: {
globPatterns: [
'**/*.{js,css,html,ico,png,svg}'
],
// Don't intercept standalone app paths — they have their own service workers
navigateFallbackDenylist: [/^\/sortir\//, /^\/libra\//, /^\/wallet\//, /^\/chat\//, /^\/market\//, /^\/cart\//, /^\/checkout\//, /^\/tasks\//, /^\/forum\//, /^\/submit\//, /^\/submission\//],
},
includeAssets: [
'favicon.ico',
'apple-touch-icon.png',
'mask-icon.svg',
// optional: include the icon PNGs explicitly if you also reference them directly
'icon-192.png',
'icon-512.png',
'icon-maskable-192.png',
'icon-maskable-512.png',
],
manifest: {
name: 'AIO - Community Hub',
short_name: 'AIO',
description: 'Nostr-based community platform with Lightning Network integration for events, market and announcements',
theme_color: '#1f2937',
background_color: '#ffffff',
display: 'standalone',
orientation: 'portrait-primary',
start_url: '/',
scope: '/',
id: 'aio-community-hub',
categories: ['social', 'utilities'],
lang: 'en',
"icons": [
{ "src": "/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" },
{ "src": "/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" },
{ "src": "/icon-maskable-192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable" },
{ "src": "/icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
],
}
}),
Inspect(),
ViteImageOptimizer({
jpg: {