# PWA Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Implementiere PWA (Progressive Web App) Features für WrestleDesk: App-Icon auf Home Screen, bessere Mobile-Optimierung, Install-Prompt für iOS/Android **Architecture:** Manifest.json für PWA-Konfiguration, Meta-Tags in HTML für iOS/Safari, CSS-Anpassungen für Mobile/Safe Areas, InstallPrompt-Component für "Add to Home Screen" **Tech Stack:** Next.js 16, Tailwind CSS, Zustand, SVG-to-PNG für Icons --- ## File Structure | File | Purpose | |------|---------| | `frontend/public/manifest.json` | PWA Manifest (Icons, Theme, Display Mode) | | `frontend/public/icon-192.png` | PWA Icon 192x192 | | `frontend/public/icon-512.png` | PWA Icon 512x512 | | `frontend/public/apple-touch-icon.png` | iOS Icon 180x180 | | `frontend/public/icon-maskable.png` | Maskable Icon für Android | | `frontend/src/app/layout.tsx` | Meta-Tags für PWA/iOS | | `frontend/src/app/globals.css` | Mobile-Optimierungen (Safe Areas, Touch Targets) | | `frontend/src/components/ui/install-prompt.tsx` | "Add to Home Screen" Banner Component | | `frontend/src/app/(dashboard)/layout.tsx` | InstallPrompt einbinden | | `frontend/package.json` | Script für dev:host hinzufügen | | `frontend/generate-icons.js` | Icon-Generierung aus SVG | | `frontend/.env.local` | API-URL auf Netzwerk-IP | --- ## Task 1: Create PWA Manifest **Files:** - Create: `frontend/public/manifest.json` - [ ] **Step 1: Create manifest.json with PWA config** ```json { "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" } ] } ``` - [ ] **Step 2: Verify manifest.json exists** Run: `ls -la /Volumes/T3/Opencode/WrestleDesk/frontend/public/manifest.json` Expected: File exists - [ ] **Step 3: Commit** ```bash git add frontend/public/manifest.json git commit -m "feat(pwa): add manifest.json for PWA configuration" ``` --- ## Task 2: Create App Icons **Files:** - Create: `frontend/public/icon-192.svg` (SVG template) - Create: `frontend/generate-icons.js` (Icon generator script) - Create: `frontend/public/icon-192.png` - Create: `frontend/public/icon-512.png` - Create: `frontend/public/apple-touch-icon.png` - Create: `frontend/public/icon-maskable.png` - [ ] **Step 1: Create SVG template** ```svg W ``` - [ ] **Step 2: Create icon generator script** ```javascript const sharp = require('sharp'); const fs = require('fs'); const path = require('path'); const svgBuffer = fs.readFileSync(path.join(__dirname, 'public/icon-192.svg')); sharp(svgBuffer) .resize(192, 192) .png() .toFile('public/icon-192.png') .then(() => console.log('Created icon-192.png')); sharp(svgBuffer) .resize(512, 512) .png() .toFile('public/icon-512.png') .then(() => console.log('Created icon-512.png')); sharp(svgBuffer) .resize(180, 180) .png() .toFile('public/apple-touch-icon.png') .then(() => console.log('Created apple-touch-icon.png')); sharp(svgBuffer) .resize(384, 384) .extend({ top: 64, bottom: 64, left: 64, right: 64, background: { r: 27, g: 26, b: 85, alpha: 1 } }) .png() .toFile('public/icon-maskable.png') .then(() => console.log('Created icon-maskable.png')); ``` - [ ] **Step 3: Run icon generator** Run: `cd /Volumes/T3/Opencode/WrestleDesk/frontend && node generate-icons.js` Expected: All 4 PNG files created - [ ] **Step 4: Verify icons exist** Run: `ls -la /Volumes/T3/Opencode/WrestleDesk/frontend/public/*.png` Expected: icon-192.png, icon-512.png, apple-touch-icon.png, icon-maskable.png - [ ] **Step 5: Commit** ```bash git add frontend/public/icon-192.svg frontend/public/*.png frontend/generate-icons.js git commit -m "feat(pwa): add app icons (192x192, 512x512, apple-touch, maskable)" ``` --- ## Task 3: Update Layout with PWA Meta Tags **Files:** - Modify: `frontend/src/app/layout.tsx` - [ ] **Step 1: Add viewport export and PWA meta tags** ```typescript import type { Metadata, Viewport } from "next" import { Syne, DM_Sans } from "next/font/google" import "./globals.css" import { Providers } from "./providers" const syne = Syne({ variable: "--font-heading", subsets: ["latin"], weight: ["400", "500", "600", "700", "800"], }) const dmSans = DM_Sans({ variable: "--font-sans", subsets: ["latin"], 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({ children, }: Readonly<{ children: React.ReactNode }>) { return ( {children} ) } ``` - [ ] **Step 2: Commit** ```bash git add frontend/src/app/layout.tsx git commit -m "feat(pwa): add viewport and PWA meta tags" ``` --- ## Task 4: Add Mobile CSS Optimizations **Files:** - Modify: `frontend/src/app/globals.css` - [ ] **Step 1: Add mobile/PWA CSS at end of file** ```css /* Mobile/PWA Optimizations */ 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); } /* Prevent zoom on input focus */ input, select, textarea { font-size: 16px; } /* Hide scrollbar on mobile */ @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; } /* Disable hover effects on touch devices */ @media (hover: none) { *:hover { transform: none !important; } } /* PWA standalone mode styles */ @media (display-mode: standalone) { html { height: 100vh; height: 100dvh; } body { overflow: hidden; position: fixed; width: 100%; height: 100%; } } ``` - [ ] **Step 2: Commit** ```bash git add frontend/src/app/globals.css git commit -m "feat(pwa): add mobile optimizations (safe areas, touch targets, standalone mode)" ``` --- ## Task 5: Create Install Prompt Component **Files:** - Create: `frontend/src/components/ui/install-prompt.tsx` - [ ] **Step 1: Create InstallPrompt component** ```typescript "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(() => { const standalone = window.matchMedia('(display-mode: standalone)').matches || (window.navigator as any).standalone || document.referrer.includes('android-app://') setIsStandalone(standalone) const isIOSDevice = /iPad|iPhone|iPod/.test(navigator.userAgent) && !(window as any).MSStream setIsIOS(isIOSDevice) const dismissed = localStorage.getItem('install-prompt-dismissed') if (!standalone && !dismissed) { 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

)}
) } ``` - [ ] **Step 2: Commit** ```bash git add frontend/src/components/ui/install-prompt.tsx git commit -m "feat(pwa): add InstallPrompt component for Add to Home Screen" ``` --- ## Task 6: Add InstallPrompt to Dashboard Layout **Files:** - Modify: `frontend/src/app/(dashboard)/layout.tsx` - [ ] **Step 1: Import and add InstallPrompt** Add import: ```typescript import { InstallPrompt } from "@/components/ui/install-prompt" ``` Add before closing div: ```tsx ``` - [ ] **Step 2: Commit** ```bash git add frontend/src/app/(dashboard)/layout.tsx git commit -m "feat(pwa): integrate InstallPrompt in dashboard layout" ``` --- ## Task 7: Add dev:host Script to package.json **Files:** - Modify: `frontend/package.json` - [ ] **Step 1: Add dev:host script** ```json "scripts": { "dev": "next dev", "dev:host": "next dev --hostname 192.168.101.111", "build": "next build", "start": "next start", "lint": "eslint" }, ``` - [ ] **Step 2: Commit** ```bash git add frontend/package.json git commit -m "chore: add dev:host script for network testing" ``` --- ## Task 8: Create .env.local for Network API **Files:** - Create: `frontend/.env.local` - [ ] **Step 1: Create env file** ``` NEXT_PUBLIC_API_URL=http://192.168.101.111:8000/api/v1 ``` - [ ] **Step 2: Commit** ```bash git add frontend/.env.local git commit -m "chore: add .env.local with network API URL" ``` --- ## Final Verification - [ ] **Step 1: Build frontend to verify no errors** Run: `cd /Volumes/T3/Opencode/WrestleDesk/frontend && npm run build` Expected: Build successful - [ ] **Step 2: Push to Gitea** Run: ```bash cd /Volumes/T3/Opencode/WrestleDesk git push ``` - [ ] **Step 3: Test on iPhone** 1. Start servers: `npm run dev:host` (frontend) + `python manage.py runserver 0.0.0.0:8000` (backend) 2. Open Safari → http://192.168.101.111:3000 3. Verify App-Icon in Tab 4. Tap "Teilen" → "Zum Home Screen hinzufügen" 5. Open from Home Screen → should be standalone (no Safari UI) 6. Verify InstallPrompt appears