Files
WrestleDesk/docs/superpowers/plans/2025-03-26-pwa-implementation.md
Andrej Spielmann 28222d634d feat: implement user management system
- Add role field to UserProfile (superadmin/admin/trainer)
- Add role-based permission classes
- Create UserManagementViewSet with CRUD and password change
- Add API types and components for user management
- Create users management page in settings
- Only superadmins can manage users
2026-03-26 16:42:08 +01:00

12 KiB

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

{
  "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
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 xmlns="http://www.w3.org/2000/svg" width="192" height="192" viewBox="0 0 192 192">
  <rect width="192" height="192" rx="24" fill="#1B1A55"/>
  <text x="96" y="100" font-family="Arial, sans-serif" font-size="80" font-weight="bold" fill="#9290C3" text-anchor="middle">W</text>
</svg>
  • Step 2: Create icon generator script
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
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

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 (
    <html lang="de" suppressHydrationWarning>
      <body className={`${syne.variable} ${dmSans.variable} min-h-screen bg-background font-sans antialiased`}>
        <Providers>{children}</Providers>
      </body>
    </html>
  )
}
  • Step 2: Commit
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

/* 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
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

"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 (
    <div className="fixed bottom-0 left-0 right-0 z-50 p-4 bg-card border-t shadow-lg">
      <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">
              Tippe auf Teilen  "Zum Home Screen hinzufügen"
            </p>
          ) : (
            <p className="text-xs text-muted-foreground">
              Installieren für schnellen Zugriff
            </p>
          )}
        </div>
        <Button size="sm" variant="ghost" onClick={handleDismiss}>
          <X className="w-4 h-4" />
        </Button>
      </div>
    </div>
  )
}
  • Step 2: Commit
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:

import { InstallPrompt } from "@/components/ui/install-prompt"

Add before closing div:

      <InstallPrompt />
    </div>
  • Step 2: Commit
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

"scripts": {
  "dev": "next dev",
  "dev:host": "next dev --hostname 192.168.101.111",
  "build": "next build",
  "start": "next start",
  "lint": "eslint"
},
  • Step 2: Commit
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
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:

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