Compare commits

9 Commits

8 changed files with 83 additions and 3 deletions

View File

@@ -61,6 +61,31 @@ Am einfachsten via **Intergram**:
2. Sende `/start`, um deine **Chat ID** zu erhalten.
3. Webhook-URL: `https://www.intergram.xyz/msg/DEINE_CHAT_ID`
---
---
## 📲 Push Notifications (PWA)
Die App funktioniert als **Progressive Web App (PWA)** und unterstützt **Push-Benachrichtigungen** direkt auf dem Smartphone oder Desktop.
### Setup für Selbsthoster (VAPID Keys)
Damit Push funktioniert, müssen VAPID Keys in der Umgebung hinterlegt werden.
1. Generiere Keys: `npx web-push generate-vapid-keys`
2. Setze die Environment Variables (z.B. in `.env` oder Docker):
```env
# Public Key (Wird vom Browser benötigt)
NEXT_PUBLIC_VAPID_PUBLIC_KEY="<Dein Public Key>"
# Private Key (Bleibt auf dem Server!)
VAPID_PRIVATE_KEY="<Dein Private Key>"
# Kontakt-Email für den Push-Service
VAPID_SUBJECT="mailto:admin@example.com"
```
Der Benutzer kann Push-Benachrichtigungen dann direkt im Dashboard über die **Einstellungen** aktivieren.
---
## 🐳 Deployment (Docker)
@@ -97,6 +122,17 @@ volumes:
- /pfad/zum/host/uploads:/app/public/uploads
```
---
## 🏥 System Status
Die App stellt einen einfachen Healthcheck-Endpoint bereit, der von Docker oder externen Monitoring-Tools genutzt werden kann:
- **Endpoint**: `/health`
- **Method**: `GET`
- **Response**: `200 OK` `{"status":"ok"}`
---
*Erstellt mit ❤️ für alle Dosenöffner.*

View File

@@ -3,7 +3,7 @@
import { useState, useEffect } from "react"
import { format, eachDayOfInterval, isSameDay } from "date-fns"
import { de, enUS } from "date-fns/locale"
import { CalendarIcon, User, Home, X, Info, Utensils, Trash2, Check } from "lucide-react"
import { CalendarIcon, User, Home, X, Info, Utensils, Trash2, Check, Share2 } from "lucide-react"
import { toast } from "sonner"
import { Button } from "@/components/ui/button"
@@ -220,6 +220,25 @@ export function PlanDashboard({ plan, dict, settingsDict, lang }: PlanDashboardP
dict={settingsDict}
lang={lang}
/>
<Button variant="outline" size="sm" onClick={() => {
if (navigator.share) {
navigator.share({
title: plan.title,
text: dict.shareTitle,
url: window.location.href,
}).catch(() => {
// Fallback if share fails / is cancelled
navigator.clipboard.writeText(window.location.href)
toast.success(dict.copySuccess)
})
} else {
navigator.clipboard.writeText(window.location.href)
toast.success(dict.copySuccess)
}
}}>
<Share2 className="w-4 h-4 mr-2" />
{dict.share}
</Button>
</div>
</div>

5
app/health/route.ts Normal file
View File

@@ -0,0 +1,5 @@
import { NextResponse } from 'next/server';
export async function GET() {
return NextResponse.json({ status: 'ok' }, { status: 200 });
}

View File

@@ -63,7 +63,12 @@ export function PushSubscriptionSettings({ planId }: { planId: string }) {
async function registerServiceWorker() {
try {
const registration = await navigator.serviceWorker.ready
// Explicitly register the service worker
const registration = await navigator.serviceWorker.register('/push-sw.js')
// Wait for it to be ready
await navigator.serviceWorker.ready
const sub = await registration.pushManager.getSubscription()
setSubscription(sub)
} catch (e: any) {

View File

@@ -38,6 +38,10 @@
"instructionsTitle": "Katzenpflege-Anleitungen",
"export": "Exportieren",
"settings": "Einstellungen",
"share": "Teilen",
"shareTitle": "Plan teilen",
"copySuccess": "Link kopiert!",
"shareError": "Fehler beim Teilen.",
"feeding": "Füttern",
"litter": "Klo reinigen",
"ownerHome": "Besitzer zu Hause",

View File

@@ -38,6 +38,10 @@
"instructionsTitle": "Cat Care Instructions",
"export": "Export",
"settings": "Settings",
"share": "Share",
"shareTitle": "Share Plan",
"copySuccess": "Link copied to clipboard!",
"shareError": "Could not share plan.",
"feeding": "Feeding",
"litter": "Clean litter",
"ownerHome": "Owner Home",

View File

@@ -9,3 +9,8 @@ services:
- ./data:/app/data
- ./public/uploads:/app/public/uploads
restart: always
healthcheck:
test: [ "CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/health" ]
interval: 30s
timeout: 10s
retries: 3

View File

@@ -14,6 +14,8 @@ export function middleware(request: NextRequest) {
if (pathnameHasLocale) return;
if (pathname.startsWith("/api") || pathname.includes("sw.js") || pathname.includes("workbox")) return NextResponse.next();
// Redirect if there is no locale
const locale = defaultLocale; // For simplicity, we default to 'de' as requested
request.nextUrl.pathname = `/${locale}${pathname}`;
@@ -25,6 +27,6 @@ export function middleware(request: NextRequest) {
export const config = {
matcher: [
// Skip all internal paths (_next)
"/((?!_next|api|public|manifest|icon|file|globe|next|vercel|window).*)",
"/((?!_next|api|public|manifest|icon|file|globe|next|vercel|window|push-sw.js|sw.js|workbox|health).*)",
],
};