599514c0db
- 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
77 lines
2.3 KiB
TypeScript
77 lines
2.3 KiB
TypeScript
"use client"
|
|
|
|
import { useEffect, useState } from "react"
|
|
import { useTranslations } from "next-intl"
|
|
import { Download, X } from "lucide-react"
|
|
|
|
import { Button } from "@/components/ui/button"
|
|
|
|
interface BeforeInstallPromptEvent extends Event {
|
|
prompt: () => Promise<void>
|
|
userChoice: Promise<{ outcome: "accepted" | "dismissed" }>
|
|
}
|
|
|
|
export function PwaInstallPrompt() {
|
|
const t = useTranslations("pwa")
|
|
const [deferredPrompt, setDeferredPrompt] =
|
|
useState<BeforeInstallPromptEvent | null>(null)
|
|
const [dismissed, setDismissed] = useState(false)
|
|
|
|
useEffect(() => {
|
|
// Check if already dismissed permanently
|
|
if (localStorage.getItem("pwa-install-dismissed") === "true") {
|
|
setDismissed(true)
|
|
return
|
|
}
|
|
|
|
const handler = (e: Event) => {
|
|
e.preventDefault()
|
|
setDeferredPrompt(e as BeforeInstallPromptEvent)
|
|
}
|
|
|
|
window.addEventListener("beforeinstallprompt", handler)
|
|
return () => window.removeEventListener("beforeinstallprompt", handler)
|
|
}, [])
|
|
|
|
const handleInstall = async () => {
|
|
if (!deferredPrompt) return
|
|
await deferredPrompt.prompt()
|
|
const { outcome } = await deferredPrompt.userChoice
|
|
if (outcome === "accepted") {
|
|
setDeferredPrompt(null)
|
|
}
|
|
}
|
|
|
|
const handleDismiss = () => {
|
|
setDismissed(true)
|
|
localStorage.setItem("pwa-install-dismissed", "true")
|
|
setDeferredPrompt(null)
|
|
}
|
|
|
|
if (!deferredPrompt || dismissed) return null
|
|
|
|
return (
|
|
<div className="fixed bottom-4 left-4 right-4 z-50 mx-auto max-w-md animate-in slide-in-from-bottom-4 rounded-lg border bg-card p-4 shadow-lg">
|
|
<div className="flex items-start gap-3">
|
|
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-primary/10">
|
|
<Download className="h-5 w-5 text-primary" />
|
|
</div>
|
|
<div className="flex-1">
|
|
<p className="text-sm font-medium">{t("install")}</p>
|
|
<p className="mt-1 text-xs text-muted-foreground">
|
|
{t("installDesc")}
|
|
</p>
|
|
<div className="mt-3 flex gap-2">
|
|
<Button size="sm" onClick={handleInstall}>
|
|
{t("install")}
|
|
</Button>
|
|
<Button size="sm" variant="ghost" onClick={handleDismiss}>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|