28222d634d
- 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
485 lines
12 KiB
Markdown
485 lines
12 KiB
Markdown
# 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
|
|
<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**
|
|
|
|
```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 (
|
|
<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**
|
|
|
|
```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 (
|
|
<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**
|
|
|
|
```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
|
|
<InstallPrompt />
|
|
</div>
|
|
```
|
|
|
|
- [ ] **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
|