From 824191ce81737183efc655aeeede1a533f4938ba Mon Sep 17 00:00:00 2001 From: Andrej Spielmann Date: Thu, 26 Mar 2026 13:40:54 +0100 Subject: [PATCH] Add PWA support and mobile optimizations - Add manifest.json with PWA configuration - Add viewport settings for iOS (viewport-fit: cover) - Add meta tags for iOS Safari (apple-mobile-web-app-capable) - Add mobile CSS optimizations: * iOS Safe Area support * Minimum 44x44px touch targets * Disable zoom on input focus * Remove scrollbars on mobile * Disable hover effects on touch devices * Standalone mode styles - Add InstallPrompt component for Add to Home Screen - Add SVG icon (needs PNG conversion) --- frontend/public/icon-192.svg | 4 + frontend/public/manifest.json | 31 ++++++++ frontend/src/app/(dashboard)/layout.tsx | 2 + frontend/src/app/globals.css | 75 ++++++++++++++++--- frontend/src/app/layout.tsx | 26 ++++++- frontend/src/components/ui/install-prompt.tsx | 63 ++++++++++++++++ 6 files changed, 191 insertions(+), 10 deletions(-) create mode 100644 frontend/public/icon-192.svg create mode 100644 frontend/public/manifest.json create mode 100644 frontend/src/components/ui/install-prompt.tsx diff --git a/frontend/public/icon-192.svg b/frontend/public/icon-192.svg new file mode 100644 index 0000000..8aff20c --- /dev/null +++ b/frontend/public/icon-192.svg @@ -0,0 +1,4 @@ + + + W + diff --git a/frontend/public/manifest.json b/frontend/public/manifest.json new file mode 100644 index 0000000..294b22e --- /dev/null +++ b/frontend/public/manifest.json @@ -0,0 +1,31 @@ +{ + "name": "WrestleDesk", + "short_name": "WrestleDesk", + "description": "Wrestling Club Management System", + "start_url": "/", + "display": "standalone", + "background_color": "#070F2B", + "theme_color": "#1B1A55", + "orientation": "portrait", + "scope": "/", + "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.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/frontend/src/app/(dashboard)/layout.tsx b/frontend/src/app/(dashboard)/layout.tsx index 23115f7..fd9ece0 100644 --- a/frontend/src/app/(dashboard)/layout.tsx +++ b/frontend/src/app/(dashboard)/layout.tsx @@ -5,6 +5,7 @@ import { useRouter, usePathname } from "next/navigation" import { useAuth } from "@/lib/auth" import { Loader2 } from "lucide-react" import { Sidebar } from "@/components/layout/Sidebar" +import { InstallPrompt } from "@/components/ui/install-prompt" export default function DashboardLayout({ children, @@ -46,6 +47,7 @@ export default function DashboardLayout({ {children} + ) } diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index faa4546..b229f58 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -126,23 +126,80 @@ } html { @apply font-sans; -} + } -::-webkit-scrollbar { + ::-webkit-scrollbar { width: 6px; height: 6px; -} + } -::-webkit-scrollbar-track { + ::-webkit-scrollbar-track { background: transparent; -} + } -::-webkit-scrollbar-thumb { + ::-webkit-scrollbar-thumb { background: oklch(0.5 0 0 / 20%); border-radius: 3px; -} + } -::-webkit-scrollbar-thumb:hover { + ::-webkit-scrollbar-thumb:hover { background: oklch(0.5 0 0 / 35%); -} + } + + /* Mobile/PWA Optimierungen */ + html { + -webkit-tap-highlight-color: transparent; + touch-action: manipulation; + } + + /* iOS Safe Areas */ + body { + padding-top: env(safe-area-inset-top); + padding-bottom: env(safe-area-inset-bottom); + padding-left: env(safe-area-inset-left); + padding-right: env(safe-area-inset-right); + } + + /* Kein Zoom bei Input-Fokus auf iOS */ + input, select, textarea { + font-size: 16px; + } + + /* Scrollbar auf Mobile ausblenden */ + @media (max-width: 768px) { + ::-webkit-scrollbar { + display: none; + } + body { + scrollbar-width: none; + } + } + + /* Minimum Touch Target 44x44px */ + button, a, input, select, textarea, [role="button"] { + min-height: 44px; + min-width: 44px; + } + + /* Kein Hover-Effekt auf Touch-Geräten */ + @media (hover: none) { + *:hover { + -webkit-transform: none !important; + transform: none !important; + } + } + + /* PWA App-Look im Standalone-Modus */ + @media (display-mode: standalone) { + html { + height: 100vh; + height: 100dvh; + } + body { + overflow: hidden; + position: fixed; + width: 100%; + height: 100%; + } + } } \ No newline at end of file diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index cc90595..e97ad78 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -1,4 +1,4 @@ -import type { Metadata } from "next" +import type { Metadata, Viewport } from "next" import { Syne, DM_Sans } from "next/font/google" import "./globals.css" import { Providers } from "./providers" @@ -15,9 +15,33 @@ const dmSans = DM_Sans({ weight: ["400", "500", "600", "700"], }) +export const viewport: Viewport = { + width: "device-width", + initialScale: 1, + maximumScale: 1, + userScalable: false, + themeColor: "#1B1A55", + viewportFit: "cover", +} + export const metadata: Metadata = { title: "WrestleDesk", description: "Wrestling Club Management System", + manifest: "/manifest.json", + appleWebApp: { + capable: true, + statusBarStyle: "black-translucent", + title: "WrestleDesk", + }, + icons: { + icon: [ + { url: "/icon-192.png", sizes: "192x192", type: "image/png" }, + { url: "/icon-512.png", sizes: "512x512", type: "image/png" }, + ], + apple: [ + { url: "/icon-192.png", sizes: "192x192", type: "image/png" }, + ], + }, } export default function RootLayout({ diff --git a/frontend/src/components/ui/install-prompt.tsx b/frontend/src/components/ui/install-prompt.tsx new file mode 100644 index 0000000..008a8b1 --- /dev/null +++ b/frontend/src/components/ui/install-prompt.tsx @@ -0,0 +1,63 @@ +"use client" + +import { useState, useEffect } from "react" +import { Button } from "@/components/ui/button" +import { X } from "lucide-react" + +export function InstallPrompt() { + const [show, setShow] = useState(false) + const [isIOS, setIsIOS] = useState(false) + const [isStandalone, setIsStandalone] = useState(false) + + useEffect(() => { + // Prüfe ob bereits als PWA installiert + const standalone = window.matchMedia('(display-mode: standalone)').matches || + (window.navigator as any).standalone || + document.referrer.includes('android-app://') + setIsStandalone(standalone) + + // Prüfe iOS + const isIOSDevice = /iPad|iPhone|iPod/.test(navigator.userAgent) && !(window as any).MSStream + setIsIOS(isIOSDevice) + + // Zeige Prompt nur wenn nicht bereits installiert und nicht bereits geschlossen + const dismissed = localStorage.getItem('install-prompt-dismissed') + if (!standalone && !dismissed) { + // Verzögert anzeigen + setTimeout(() => setShow(true), 3000) + } + }, []) + + const handleDismiss = () => { + setShow(false) + localStorage.setItem('install-prompt-dismissed', 'true') + } + + if (!show || isStandalone) return null + + return ( +
+
+
+

+ WrestleDesk als App installieren +

+ {isIOS ? ( +

+ Tippe auf Teilen → "Zum Home Screen hinzufügen" +

+ ) : ( +

+ Installieren für schnellen Zugriff +

+ )} +
+
+ +
+
+
+ ) +}