feat(sprint-6): Phase 6 — Notifications (WebSocket) + PWA
- WebSocket: Spring STOMP + SockJS, NotificationService, persistent notifications table - NotificationController: GET/PUT endpoints for notification management - Frontend: notification bell with unread badge, dropdown panel, real-time via STOMP - PWA: manifest.json, service worker (manual sw.js), offline page, install prompt - PWA icons (192+512), dark theme colors, standalone display - Full i18n (de/en) for notifications and PWA - Flyway V10 migration for notifications table - spring-boot-starter-websocket dependency added
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 69 B |
Binary file not shown.
|
After Width: | Height: | Size: 69 B |
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "CannaManage",
|
||||
"short_name": "CannaManage",
|
||||
"description": "Cannabis-Club Verwaltung",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#0D1117",
|
||||
"theme_color": "#2ECC71",
|
||||
"orientation": "portrait-primary",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icons/icon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/icons/icon-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/// <reference lib="webworker" />
|
||||
|
||||
const CACHE_NAME = "cannamanage-v1"
|
||||
const OFFLINE_URL = "/offline"
|
||||
|
||||
// Assets to pre-cache
|
||||
const PRECACHE_ASSETS = [
|
||||
"/offline",
|
||||
"/manifest.json",
|
||||
"/icons/icon-192.png",
|
||||
"/icons/icon-512.png",
|
||||
]
|
||||
|
||||
self.addEventListener("install", (event) => {
|
||||
event.waitUntil(
|
||||
caches.open(CACHE_NAME).then((cache) => {
|
||||
return cache.addAll(PRECACHE_ASSETS)
|
||||
})
|
||||
)
|
||||
self.skipWaiting()
|
||||
})
|
||||
|
||||
self.addEventListener("activate", (event) => {
|
||||
event.waitUntil(
|
||||
caches.keys().then((cacheNames) => {
|
||||
return Promise.all(
|
||||
cacheNames
|
||||
.filter((name) => name !== CACHE_NAME)
|
||||
.map((name) => caches.delete(name))
|
||||
)
|
||||
})
|
||||
)
|
||||
self.clients.claim()
|
||||
})
|
||||
|
||||
self.addEventListener("fetch", (event) => {
|
||||
// Only handle GET requests
|
||||
if (event.request.method !== "GET") return
|
||||
|
||||
// Skip API requests — let them fail naturally
|
||||
if (event.request.url.includes("/api/")) return
|
||||
|
||||
// Network-first for navigation requests
|
||||
if (event.request.mode === "navigate") {
|
||||
event.respondWith(
|
||||
fetch(event.request).catch(() => {
|
||||
return caches.match(OFFLINE_URL)
|
||||
})
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Stale-while-revalidate for static assets
|
||||
if (
|
||||
event.request.destination === "style" ||
|
||||
event.request.destination === "script" ||
|
||||
event.request.destination === "image"
|
||||
) {
|
||||
event.respondWith(
|
||||
caches.match(event.request).then((cachedResponse) => {
|
||||
const fetchPromise = fetch(event.request).then((networkResponse) => {
|
||||
if (networkResponse && networkResponse.status === 200) {
|
||||
const cache = caches.open(CACHE_NAME)
|
||||
cache.then((c) => c.put(event.request, networkResponse.clone()))
|
||||
}
|
||||
return networkResponse
|
||||
})
|
||||
return cachedResponse || fetchPromise
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user