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)
This commit is contained in:
Andrej Spielmann
2026-03-26 13:40:54 +01:00
parent 3fefc550fe
commit 824191ce81
6 changed files with 191 additions and 10 deletions
+2
View File
@@ -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}
</div>
</main>
<InstallPrompt />
</div>
)
}
+66 -9
View File
@@ -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%;
}
}
}
+25 -1
View File
@@ -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({
@@ -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 (
<div className="fixed bottom-0 left-0 right-0 z-50 p-4 bg-card border-t shadow-lg safe-area-bottom">
<div className="flex items-center justify-between max-w-lg mx-auto">
<div className="flex-1">
<p className="text-sm font-medium">
WrestleDesk als App installieren
</p>
{isIOS ? (
<p className="text-xs text-muted-foreground mt-0.5">
Tippe auf Teilen "Zum Home Screen hinzufügen"
</p>
) : (
<p className="text-xs text-muted-foreground mt-0.5">
Installieren für schnellen Zugriff
</p>
)}
</div>
<div className="flex items-center gap-2">
<Button size="sm" variant="ghost" onClick={handleDismiss}>
<X className="w-4 h-4" />
</Button>
</div>
</div>
</div>
)
}