Compare commits
No commits in common. "0a0769115bc2967f69154ab12c8043d738599953" and "8303b0981b23469ff7088d83201bd0f79d10c4cd" have entirely different histories.
0a0769115b
...
8303b0981b
24 changed files with 217 additions and 103 deletions
126
public/sw.js
Normal file
126
public/sw.js
Normal 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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -26,8 +26,8 @@ async function handleLoginSuccess() {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="min-h-dvh bg-background font-sans antialiased">
|
<div class="min-h-screen bg-background font-sans antialiased">
|
||||||
<div class="relative flex min-h-dvh flex-col"
|
<div class="relative flex min-h-screen flex-col"
|
||||||
style="padding-top: env(safe-area-inset-top); padding-bottom: env(safe-area-inset-bottom)">
|
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)">
|
<div v-if="!isLoginPage && !isAuthenticated" class="fixed top-0 right-0 z-50 p-3" style="padding-top: env(safe-area-inset-top)">
|
||||||
|
|
|
||||||
|
|
@ -49,8 +49,8 @@ async function handleLoginSuccess() {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="min-h-dvh bg-background font-sans antialiased">
|
<div class="min-h-screen bg-background font-sans antialiased">
|
||||||
<div class="relative flex min-h-dvh flex-col"
|
<div class="relative flex min-h-screen flex-col"
|
||||||
style="padding-top: env(safe-area-inset-top); padding-bottom: env(safe-area-inset-bottom)">
|
style="padding-top: env(safe-area-inset-top); padding-bottom: env(safe-area-inset-bottom)">
|
||||||
|
|
||||||
<!-- Top bar with login -->
|
<!-- Top bar with login -->
|
||||||
|
|
|
||||||
|
|
@ -46,8 +46,8 @@ async function handleLoginSuccess() {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="min-h-dvh bg-background font-sans antialiased">
|
<div class="min-h-screen bg-background font-sans antialiased">
|
||||||
<div class="relative flex min-h-dvh flex-col"
|
<div class="relative flex min-h-screen flex-col"
|
||||||
style="padding-top: env(safe-area-inset-top); padding-bottom: env(safe-area-inset-bottom)">
|
style="padding-top: env(safe-area-inset-top); padding-bottom: env(safe-area-inset-bottom)">
|
||||||
|
|
||||||
<!-- Main content (with bottom padding for nav bar) -->
|
<!-- Main content (with bottom padding for nav bar) -->
|
||||||
|
|
|
||||||
|
|
@ -177,31 +177,4 @@
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,8 @@ async function handleLoginSuccess() {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="min-h-dvh bg-background font-sans antialiased">
|
<div class="min-h-screen bg-background font-sans antialiased">
|
||||||
<div class="relative flex min-h-dvh flex-col"
|
<div class="relative flex min-h-screen flex-col"
|
||||||
style="padding-top: env(safe-area-inset-top); padding-bottom: env(safe-area-inset-bottom)">
|
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)">
|
<div v-if="!isLoginPage && !isAuthenticated" class="fixed top-0 right-0 z-50 p-3" style="padding-top: env(safe-area-inset-top)">
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ const openSidebar = () => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="min-h-dvh bg-background">
|
<div class="min-h-screen bg-background">
|
||||||
<!-- Mobile Drawer -->
|
<!-- Mobile Drawer -->
|
||||||
<MobileDrawer v-model:open="sidebarOpen" />
|
<MobileDrawer v-model:open="sidebarOpen" />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,8 +59,8 @@ async function handleLoginSuccess() {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="min-h-dvh bg-background font-sans antialiased">
|
<div class="min-h-screen bg-background font-sans antialiased">
|
||||||
<div class="relative flex min-h-dvh flex-col"
|
<div class="relative flex min-h-screen flex-col"
|
||||||
style="padding-top: env(safe-area-inset-top); padding-bottom: env(safe-area-inset-bottom)">
|
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)">
|
<div v-if="!isLoginPage && !isAuthenticated" class="fixed top-0 right-0 z-50 p-3" style="padding-top: env(safe-area-inset-top)">
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
17
src/main.ts
17
src/main.ts
|
|
@ -1,15 +1,24 @@
|
||||||
// New modular application entry point
|
// New modular application entry point
|
||||||
import { startApp } from './app'
|
import { startApp } from './app'
|
||||||
|
import { registerSW } from 'virtual:pwa-register'
|
||||||
import { cleanupStaleDevServiceWorkers } from '@/lib/dev-sw-cleanup'
|
import { cleanupStaleDevServiceWorkers } from '@/lib/dev-sw-cleanup'
|
||||||
import { decommissionHubServiceWorker } from '@/lib/decommission-hub-sw'
|
|
||||||
import 'vue-sonner/style.css'
|
import 'vue-sonner/style.css'
|
||||||
|
|
||||||
// Clean up any leftover dev-mode service workers from a previous session
|
// Clean up any leftover dev-mode service workers from a previous session
|
||||||
cleanupStaleDevServiceWorkers()
|
cleanupStaleDevServiceWorkers()
|
||||||
|
|
||||||
// Hub is no longer a PWA (#41) — unregister any legacy hub SW left behind
|
// Simple periodic service worker updates
|
||||||
// on users who installed the old hub PWA before this change shipped.
|
const intervalMS = 60 * 60 * 1000 // 1 hour
|
||||||
void decommissionHubServiceWorker()
|
registerSW({
|
||||||
|
onRegistered(r) {
|
||||||
|
r && setInterval(() => {
|
||||||
|
r.update()
|
||||||
|
}, intervalMS)
|
||||||
|
},
|
||||||
|
onOfflineReady() {
|
||||||
|
console.log('App ready to work offline')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Start the modular application
|
// Start the modular application
|
||||||
startApp()
|
startApp()
|
||||||
|
|
|
||||||
|
|
@ -69,8 +69,8 @@ async function handleLoginSuccess() {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="min-h-dvh bg-background font-sans antialiased">
|
<div class="min-h-screen bg-background font-sans antialiased">
|
||||||
<div class="relative flex min-h-dvh flex-col"
|
<div class="relative flex min-h-screen flex-col"
|
||||||
style="padding-top: env(safe-area-inset-top); padding-bottom: env(safe-area-inset-bottom)">
|
style="padding-top: env(safe-area-inset-top); padding-bottom: env(safe-area-inset-bottom)">
|
||||||
|
|
||||||
<main class="flex-1" :class="{ 'pb-16': !isLoginPage }">
|
<main class="flex-1" :class="{ 'pb-16': !isLoginPage }">
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ onMounted(() => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col h-[calc(100dvh-3.5rem)]">
|
<div class="flex flex-col h-[calc(100vh-3.5rem)]">
|
||||||
<!-- Loading overlay -->
|
<!-- Loading overlay -->
|
||||||
<div v-if="isLoading && geoActivities.length === 0" class="flex-1 flex items-center justify-center">
|
<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" />
|
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<!-- Loading State -->
|
<!-- 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="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="animate-spin rounded-full h-12 w-12 border-b-2 border-primary"></div>
|
||||||
<div class="text-center space-y-2">
|
<div class="text-center space-y-2">
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Error State -->
|
<!-- 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">
|
<div class="text-center space-y-4">
|
||||||
<h2 class="text-xl font-semibold text-red-600">Failed to load chat</h2>
|
<h2 class="text-xl font-semibold text-red-600">Failed to load chat</h2>
|
||||||
<p class="text-muted-foreground">{{ error }}</p>
|
<p class="text-muted-foreground">{{ error }}</p>
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Chat Content - Only render when module is ready -->
|
<!-- 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 />
|
<ChatComponent />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -275,7 +275,7 @@ watch(comments, (newComments) => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="min-h-dvh bg-background">
|
<div class="min-h-screen bg-background">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<header class="sticky top-0 z-30 bg-background/95 backdrop-blur border-b">
|
<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">
|
<div class="max-w-4xl mx-auto px-4 py-3">
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ function onSubmissionClick(submission: SubmissionWithMeta) {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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="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">
|
<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>
|
<h1 class="text-lg font-semibold">Forum</h1>
|
||||||
|
|
|
||||||
|
|
@ -275,7 +275,7 @@ watch(comments, (newComments) => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="min-h-dvh bg-background">
|
<div class="min-h-screen bg-background">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<header class="sticky top-0 z-30 bg-background/95 backdrop-blur border-b">
|
<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">
|
<div class="max-w-4xl mx-auto px-4 py-3">
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col h-dvh bg-background">
|
<div class="flex flex-col h-screen bg-background">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="sticky top-0 z-40 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 border-b">
|
<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">
|
<div class="flex items-center justify-between px-4 py-2 sm:px-6">
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col h-dvh bg-background">
|
<div class="flex flex-col h-screen bg-background">
|
||||||
<PWAInstallPrompt auto-show />
|
<PWAInstallPrompt auto-show />
|
||||||
|
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,7 @@ function notImplemented() {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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="
|
style="
|
||||||
background-image:
|
background-image:
|
||||||
linear-gradient(to bottom,
|
linear-gradient(to bottom,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<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">
|
<div class="w-full max-w-md space-y-8">
|
||||||
<!-- Logo and Title -->
|
<!-- Logo and Title -->
|
||||||
<div class="text-center space-y-6">
|
<div class="text-center space-y-6">
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<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">
|
<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 -->
|
<!-- Welcome Section -->
|
||||||
<div class="text-center space-y-2 sm:space-y-4">
|
<div class="text-center space-y-2 sm:space-y-4">
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,8 @@ async function handleLoginSuccess() {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="min-h-dvh bg-background font-sans antialiased">
|
<div class="min-h-screen bg-background font-sans antialiased">
|
||||||
<div class="relative flex min-h-dvh flex-col"
|
<div class="relative flex min-h-screen flex-col"
|
||||||
style="padding-top: env(safe-area-inset-top); padding-bottom: env(safe-area-inset-bottom)">
|
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)">
|
<div v-if="!isLoginPage && !isAuthenticated" class="fixed top-0 right-0 z-50 p-3" style="padding-top: env(safe-area-inset-top)">
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,8 @@ async function handleLoginSuccess() {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="min-h-dvh bg-background font-sans antialiased">
|
<div class="min-h-screen bg-background font-sans antialiased">
|
||||||
<div class="relative flex min-h-dvh flex-col"
|
<div class="relative flex min-h-screen flex-col"
|
||||||
style="padding-top: env(safe-area-inset-top); padding-bottom: env(safe-area-inset-bottom)">
|
style="padding-top: env(safe-area-inset-top); padding-bottom: env(safe-area-inset-bottom)">
|
||||||
|
|
||||||
<!-- Top bar with login -->
|
<!-- Top bar with login -->
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,12 @@ import { fileURLToPath, URL } from 'node:url'
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
import tailwindcss from '@tailwindcss/vite'
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
|
import { VitePWA } from 'vite-plugin-pwa'
|
||||||
import Inspect from 'vite-plugin-inspect'
|
import Inspect from 'vite-plugin-inspect'
|
||||||
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
|
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
|
||||||
import { visualizer } from 'rollup-plugin-visualizer'
|
import { visualizer } from 'rollup-plugin-visualizer'
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// 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 }) => ({
|
export default defineConfig(({ mode }) => ({
|
||||||
// Per-app dep cache so concurrent dev servers don't race on .vite/deps
|
// Per-app dep cache so concurrent dev servers don't race on .vite/deps
|
||||||
cacheDir: 'node_modules/.vite-hub',
|
cacheDir: 'node_modules/.vite-hub',
|
||||||
|
|
@ -22,6 +18,54 @@ export default defineConfig(({ mode }) => ({
|
||||||
plugins: [
|
plugins: [
|
||||||
vue(),
|
vue(),
|
||||||
tailwindcss(),
|
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(),
|
Inspect(),
|
||||||
ViteImageOptimizer({
|
ViteImageOptimizer({
|
||||||
jpg: {
|
jpg: {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue