Files
WrestleDesk/AGENTS.md
T
Andrej Spielmann 7611533718 docs: add PWA implementation guide and git workflow notes
- Document PWA feature with testing instructions
- Add Git workflow: push after every task completion
- Document iPhone testing steps
2026-03-26 15:15:48 +01:00

1242 lines
38 KiB
Markdown

# AGENTS.md - WrestleDesk
## Project Overview
**Backend-First Development**: This project uses a backend-first approach. Agents should focus on Django backend development before frontend work.
### Tech Stack
- **Backend**: Django + Django REST Framework + PostgreSQL
- **Frontend**: Next.js 16 + React + Shadcn UI + Tailwind CSS
- **Auth**: JWT via djangorestframework-simplejwt
---
## Development Commands
### Backend (Django)
```bash
cd backend
pip install -r requirements.txt
python manage.py migrate
python manage.py makemigrations
python manage.py runserver
python manage.py test
python manage.py shell
python manage.py createsuperuser
python manage.py seed_data
```
### Frontend (Next.js)
```bash
cd frontend
npm install
npm run dev
npm run build
npm run start
npm run lint
npm run typecheck
```
---
## Frontend Setup
### Create New Next.js Project
```bash
npx create-next-app@latest frontend --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
cd frontend
```
### Initialize Shadcn UI
```bash
npx shadcn@latest init
# When prompted:
# - Style: default
# - Base color: zinc (or slate)
# - CSS file: src/app/globals.css
# - CSS variables: yes
# - Dark mode: class
# - SSR: yes
```
### Add Shadcn Components
```bash
npx shadcn@latest add button card dialog sheet table badge input select dropdown-menu toast tooltip avatar separator tabs accordion alert skeleton progress switch checkbox label scroll-area
```
---
## Frontend Project Structure
```
frontend/
├── src/
│ ├── app/
│ │ ├── (auth)/
│ │ │ └── login/
│ │ │ └── page.tsx
│ │ ├── (dashboard)/
│ │ │ ├── layout.tsx
│ │ │ ├── page.tsx # Dashboard
│ │ │ ├── wrestlers/
│ │ │ │ └── page.tsx # Wrestlers CRUD
│ │ │ ├── trainers/
│ │ │ │ └── page.tsx # Trainers CRUD
│ │ │ ├── exercises/
│ │ │ │ └── page.tsx # Exercises CRUD
│ │ │ ├── trainings/
│ │ │ │ ├── page.tsx # Trainings list (Grid/List/Calendar)
│ │ │ │ └── [id]/
│ │ │ │ └── page.tsx # Training detail + attendance + homework
│ │ │ ├── templates/
│ │ │ │ └── page.tsx # Templates CRUD
│ │ │ ├── homework/
│ │ │ │ └── page.tsx # Homework assignments overview
│ │ │ └── clubs/
│ │ │ └── page.tsx # Clubs CRUD
│ │ ├── api/
│ │ │ └── [...slug]/
│ │ │ └── route.ts # API proxy
│ │ ├── layout.tsx
│ │ ├── globals.css
│ │ └── providers.tsx
│ ├── components/
│ │ ├── ui/ # Shadcn components
│ │ ├── layout/ # Sidebar
│ │ ├── modal.tsx # Reusable modal
│ │ ├── animations.tsx # FadeIn wrapper
│ │ ├── skeletons.tsx # Loading states
│ │ ├── empty-state.tsx # Empty state
│ │ └── pagination.tsx # Pagination
│ ├── lib/
│ │ ├── api.ts # API client + types
│ │ ├── auth.ts # Zustand auth store
│ │ └── utils.ts # cn() helper
│ └── hooks/ # React hooks
├── next.config.js
├── tailwind.config.ts
└── package.json
```
---
## Implemented Features Status
### ✅ Fully Working
| Feature | Backend | Frontend | Notes |
|---------|---------|----------|-------|
| **Authentication** | ✅ JWT login/register | ✅ Login page with zustand | Rate limited, persistent storage |
| **Dashboard** | ✅ Stats endpoints | ✅ Stats cards + animations | Shows wrestlers, trainers, trainings, open homework |
| **Wrestlers CRUD** | ✅ Full API | ✅ Full UI with photo upload | Filters, pagination, FormData |
| **Trainers CRUD** | ✅ Full API | ✅ Full UI with photo upload | Filters, pagination |
| **Exercises CRUD** | ✅ Full API | ✅ Full UI with categories | Categories: warmup, kraft, technik, ausdauer, spiele, cool_down |
| **Clubs CRUD** | ✅ Full API | ✅ Full UI with logo upload | Wrestler count displayed |
| **Trainings** | ✅ Full API | ✅ Grid/List/Calendar view | Attendance management working |
| **Training Detail** | ✅ Full API | ✅ 2-column layout | Participants + Homework side-by-side |
| **Homework System** | ✅ Complete | ✅ Full implementation | Training-based homework with completion tracking |
| **Templates** | ✅ Full API | ✅ Basic CRUD | Exercise association via through table |
### ⚠️ Known Issues
| Issue | Status | Priority |
|-------|--------|----------|
| **Wrestlers FormData** | 🔴 Bug in handleSubmit - references undefined variables | High |
| **Locations Page** | 🟡 Backend exists, frontend missing | Medium |
| **Settings Page** | 🟡 Sidebar links to /settings but no page | Low |
---
## Architecture Decisions
### ❌ NOT Required: Club-Level Data Isolation
**Important**: Club-level data isolation is NOT a requirement for this project. All users can see all data across all clubs.
- The `ClubFilterBackend` and `ClubLevelPermission` exist in `utils/permissions.py` but are NOT enforced
- ViewSets use only `IsAuthenticated` permission
- This is intentional - the system is designed for open data access
### Multi-Club Training Feature
Trainers and wrestlers from OTHER clubs CAN be added to training sessions:
- `GET /api/v1/trainers/available_for_training/` - Returns ALL trainers
- `GET /api/v1/wrestlers/available_for_training/` - Returns ALL wrestlers
This allows cross-club collaboration while maintaining separate club entities.
---
## React Patterns
### Component Structure
```tsx
"use client"
import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
interface Props {
title: string
}
export function ComponentName({ title }: Props) {
const [loading, setLoading] = useState(false)
return (
<Card>
<CardHeader>
<CardTitle>{title}</CardTitle>
</CardHeader>
<CardContent>
<Button disabled={loading}>
{loading ? "Loading..." : "Submit"}
</Button>
</CardContent>
</Card>
)
}
```
### Using Shadcn Components
```tsx
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
```
### Toast Notifications (Sonner)
```tsx
import { toast } from "sonner"
toast.success("Ringer erstellt")
toast.error("Fehler beim Speichern")
```
---
## Next.js/React Development Guidelines
#### Core Principles
- **Server Components First**: Use Server Components by default; add "use client" only when interactivity is needed
- **TypeScript Strict Mode**: Enable strict TypeScript everywhere; avoid `any` type
- **Component Composition**: Prefer small, reusable components over monolithic ones
- **Data Fetching**: Use React Query (TanStack Query) for client-side data fetching; Server Components for initial data
- **Performance**: Lazy load routes and heavy components; use `next/image` for optimized images
#### Project Structure Best Practices
**File Organization**
```
src/
├── app/ # App Router (file-based routing)
│ ├── (auth)/ # Route groups (no URL prefix)
│ ├── (dashboard)/ # Dashboard routes
│ ├── api/ # API routes (proxy to backend)
│ └── layout.tsx # Root layout
├── components/
│ ├── ui/ # Shadcn/ui components (ALWAYS manually managed)
│ ├── forms/ # Form components with react-hook-form + zod
│ ├── data-display/ # Tables, lists, cards
│ └── layout/ # Sidebar, header, navigation
├── hooks/ # Custom React hooks
├── lib/ # Utilities, API client, stores
├── types/ # Shared TypeScript types
└── styles/ # Global styles
```
**Naming Conventions**
- Components: `PascalCase.tsx` (e.g., `WrestlerCard.tsx`)
- Hooks: `camelCase.ts` with `use` prefix (e.g., `useWrestlers.ts`)
- Utils: `camelCase.ts` (e.g., `formatDate.ts`)
- Types: `PascalCase.ts` or inline interfaces
- Constants: `UPPERCASE_WITH_UNDERSCORE`
#### Component Development
**Component Anatomy**
```tsx
// 1. "use client" ONLY if needed for interactivity
"use client"
import { useState } from "react"
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { z } from "zod"
import { Button } from "@/components/ui/button"
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { toast } from "sonner"
// 2. Define schema with zod
const formSchema = z.object({
firstName: z.string().min(1, "Vorname ist erforderlich"),
lastName: z.string().min(1, "Nachname ist erforderlich"),
})
type FormValues = z.infer<typeof formSchema>
// 3. Define interface BEFORE component
interface WrestlerFormProps {
onSubmit: (values: FormValues) => Promise<void>
defaultValues?: Partial<FormValues>
}
// 4. Component with explicit prop types
export function WrestlerForm({ onSubmit, defaultValues }: WrestlerFormProps) {
const [isLoading, setIsLoading] = useState(false)
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: defaultValues ?? {
firstName: "",
lastName: "",
},
})
async function handleSubmit(values: FormValues) {
setIsLoading(true)
try {
await onSubmit(values)
toast.success("Ringer gespeichert")
} catch (error) {
toast.error("Fehler beim Speichern")
} finally {
setIsLoading(false)
}
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
<FormField
control={form.control}
name="firstName"
render={({ field }) => (
<FormItem>
<FormLabel>Vorname</FormLabel>
<FormControl>
<Input placeholder="Max" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" disabled={isLoading}>
{isLoading ? "Speichern..." : "Speichern"}
</Button>
</form>
</Form>
)
}
```
**When to Use "use client"**
| Use Client | Server Component |
|------------|-----------------|
| useState, useEffect | Data fetching (Server Components) |
| Event handlers | Static UI rendering |
| Browser APIs | SEO metadata |
| Third-party hooks | Static forms (no validation) |
| Real-time subscriptions | Static pages |
#### State Management Strategy
**Local State**: Use `useState` for component-specific UI state
```tsx
const [isOpen, setIsOpen] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
```
**Server State (TanStack Query)**: Use for all API data
```tsx
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
import { apiFetch } from "@/lib/api"
export function useWrestlers(filters?: Record<string, string>) {
return useQuery({
queryKey: ["wrestlers", filters],
queryFn: () => apiFetch<{ results: Wrestler[] }>("/wrestlers/", { params: filters }),
})
}
export function useCreateWrestler() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (data: CreateWrestlerInput) =>
apiFetch<Wrestler>("/wrestlers/", { method: "POST", body: data }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["wrestlers"] })
toast.success("Ringer erstellt")
},
})
}
```
**Global UI State (Zustand)**: Use for auth, theme, modals
```tsx
// lib/auth-store.ts
import { create } from "zustand"
import { persist } from "zustand/middleware"
interface AuthState {
token: string | null
user: User | null
setAuth: (token: string, user: User) => void
logout: () => void
}
export const useAuthStore = create<AuthState>()(
persist(
(set) => ({
token: null,
user: null,
setAuth: (token, user) => set({ token, user }),
logout: () => set({ token: null, user: null }),
}),
{ name: "auth-storage" }
)
)
```
#### Form Handling
**Use react-hook-form + zod for all forms**
```tsx
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { z } from "zod"
const schema = z.object({
email: z.string().email("Ungültige E-Mail"),
password: z.string().min(8, "Mindestens 8 Zeichen"),
})
type FormData = z.infer<typeof schema>
export function LoginForm() {
const form = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: { email: "", password: "" },
})
// ... form implementation
}
```
**FormData for File Uploads**
```tsx
const formData = new FormData()
formData.append("first_name", data.firstName)
formData.append("photo", photoFile) // File object directly
await apiFetch("/wrestlers/", {
method: "POST",
body: formData,
// Don't set Content-Type header - browser sets it with boundary
})
```
#### Data Fetching Patterns
**Next.js App Router - Server Components (Preferred)**
```tsx
// app/(dashboard)/wrestlers/page.tsx
import { apiFetch } from "@/lib/api"
import { WrestlerList } from "./wrestler-list"
export default async function WrestlersPage() {
const data = await apiFetch<{ results: Wrestler[] }>("/wrestlers/")
return <WrestlerList wrestlers={data.results} />
}
```
**Client Components with TanStack Query**
```tsx
// components/wrestler-list.tsx
"use client"
import { useQuery } from "@tanstack/react-query"
import { apiFetch } from "@/lib/api"
export function WrestlerList() {
const { data, isLoading, error } = useQuery({
queryKey: ["wrestlers"],
queryFn: () => apiFetch<{ results: Wrestler[] }>("/wrestlers/"),
})
if (isLoading) return <WrestlerListSkeleton />
if (error) return <div>Fehler beim Laden</div>
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{data?.results.map((wrestler) => (
<WrestlerCard key={wrestler.id} wrestler={wrestler} />
))}
</div>
)
}
```
#### Reactive Filter + Table Pattern
Pages with filters (search, dropdowns) and a data table must use a **two-component split** to prevent full-page flashes when typing:
```
PageNamePage → Header + Filter bar (stable, mounted once)
PageNameTable → Table + Pagination (reactive, updates independently)
```
**The Problem:** If `isLoading` is shared state at the page level, typing in the search input triggers a fetch which sets `isLoading = true`, and if that renders `<PageSkeleton />` at the top of the return statement, the entire page goes blank on every keystroke.
**The Fix:**
1. **Main page component** holds: header, filter bar, modal state, total count
2. **Table sub-component** holds: table data state, `isTableLoading` state, fetch logic
3. Filter changes → only the table component re-renders, not the whole page
4. Table sub-component shows its own skeleton rows during loading, never replaces the page
**exercises/page.tsx pattern:**
```tsx
function ExerciseTable({ filters, token, onEdit, onDelete }) {
const [exercises, setExercises] = useState([])
const [isTableLoading, setIsTableLoading] = useState(false)
useEffect(() => {
setIsTableLoading(true)
fetchExercises(filters).then(data => {
setExercises(data.results)
setIsTableLoading(false)
})
}, [filters])
if (isTableLoading) {
return (
<div className="border rounded-xl">
<TableSkeleton rows={5} />
</div>
)
}
return <Table data={exercises} onEdit={onEdit} onDelete={onDelete} />
}
export default function ExercisesPage() {
const [filters, setFilters] = useState(DEFAULT_FILTERS)
const [totalCount, setTotalCount] = useState(0)
if (isLoading) return <PageSkeleton /> // initial load only
return (
<div className="space-y-8">
<FadeIn>Header + Add Button</FadeIn>
<FadeIn delay={0.05}>Filter Bar</FadeIn>
<FadeIn delay={0.1}>
<ExerciseTable filters={filters} ... />
</FadeIn>
</div>
)
}
```
**Key rules:**
- `isLoading` (initial page load) → `<PageSkeleton />` returned BEFORE the main layout
- `isTableLoading` (filter/pagination changes) → table shows inline skeleton, page layout stays
- NEVER `return <PageSkeleton />` inside the main return's JSX tree — it must be an early return before the layout renders
- Table component is a separate function component, not inline JSX
- Parent page's `isLoading` initializes to `true`, becomes `false` after first mount effect
#### Delete Confirmation Modal Pattern
For destructive actions (delete) on list pages, always show a confirmation modal before executing the delete. This prevents accidental deletions.
**State (in the page component):**
```tsx
const [deleteId, setDeleteId] = useState<number | null>(null)
const [isDeleting, setIsDeleting] = useState(false)
```
**Delete handler (sets confirmation state instead of deleting directly):**
```tsx
const handleDelete = (id: number) => {
setDeleteId(id)
}
const confirmDelete = async () => {
if (!deleteId) return
setIsDeleting(true)
try {
await apiFetch(`/resource/${deleteId}/`, { method: "DELETE", token: token! })
toast.success("Erfolgreich gelöscht")
setDeleteId(null)
fetchData()
} catch {
toast.error("Fehler beim Löschen")
} finally {
setIsDeleting(false)
}
}
```
**Modal (at bottom of page, before closing tag):**
```tsx
<Modal
open={!!deleteId}
onOpenChange={(open) => !open && setDeleteId(null)}
title="[Resource] löschen"
description="Bist du sicher, dass du diesen Eintrag löschen möchtest?"
size="sm"
footer={
<>
<Button variant="outline" onClick={() => setDeleteId(null)} disabled={isDeleting}>
Abbrechen
</Button>
<Button variant="destructive" onClick={confirmDelete} disabled={isDeleting}>
{isDeleting ? "..." : "Löschen"}
</Button>
</>
}
>
<div />
</Modal>
```
**Delete button in table (calls handleDelete, not the API directly):**
```tsx
<Button
variant="ghost"
size="icon"
onClick={() => handleDelete(item.id)}
className="hover:bg-destructive/10 hover:text-destructive"
>
<Trash2 className="w-4 h-4" />
</Button>
```
**Key rules:**
- Never call the API directly from a delete button — always go through the confirmation flow
- The modal shows `size="sm"` for simple confirmations (just a title + description)
- The modal body is empty (`<div />`) — the description text is sufficient
- `isDeleting` prevents double-submit while the API call is in flight
#### Error Handling
**API Error Handling**
```tsx
async function handleSubmit(values: FormValues) {
try {
await createWrestler(values)
toast.success("Erfolg!")
} catch (error) {
if (error instanceof ApiError) {
// Handle field errors
if (error.status === 422) {
form.setError("firstName", { message: error.errors.first_name?.[0] })
return
}
}
toast.error("Ein Fehler ist aufgetreten")
}
}
```
**Error Boundaries**
```tsx
"use client"
import { Component, type ReactNode } from "react"
interface Props {
children: ReactNode
fallback: ReactNode
}
interface State {
hasError: boolean
}
export class ErrorBoundary extends Component<Props, State> {
state: State = { hasError: false }
static getDerivedStateFromError(): State {
return { hasError: true }
}
render() {
if (this.state.hasError) {
return this.props.fallback
}
return this.props.children
}
}
```
#### Performance Optimization
**Code Splitting**
```tsx
import dynamic from "next/dynamic"
const DataTable = dynamic(() => import("@/components/data-table"), {
loading: () => <TableSkeleton />,
ssr: false,
})
```
**Image Optimization**
```tsx
import Image from "next/image"
<Image
src={wrestler.photo}
alt={wrestler.full_name}
width={200}
height={200}
className="object-cover rounded-lg"
/>
```
**Memoization**
```tsx
import { useMemo } from "react"
const sortedWrestlers = useMemo(() => {
return wrestlers
.filter(w => w.isActive)
.sort((a, b) => a.lastName.localeCompare(b.lastName))
}, [wrestlers])
```
#### Testing Guidelines
**Component Testing**
```tsx
import { render, screen, fireEvent } from "@testing-library/react"
import { describe, it, expect, vi } from "vitest"
import { WrestlerCard } from "./wrestler-card"
describe("WrestlerCard", () => {
it("renders wrestler name", () => {
render(<WrestlerCard wrestler={{ id: "1", firstName: "Max", lastName: "Mustermann" }} />)
expect(screen.getByText("Max Mustermann")).toBeInTheDocument()
})
it("calls onEdit when edit button is clicked", async () => {
const onEdit = vi.fn()
render(<WrestlerCard wrestler={mockWrestler} onEdit={onEdit} />)
fireEvent.click(screen.getByRole("button", { name: /bearbeiten/i }))
expect(onEdit).toHaveBeenCalledWith(mockWrestler.id)
})
})
```
#### Accessibility (a11y)
- Use semantic HTML elements (`<button>`, `<nav>`, `<main>`, `<article>`)
- Add `aria-label` for icon-only buttons
- Ensure color contrast ratio ≥ 4.5:1
- Support keyboard navigation (Tab, Enter, Escape)
- Use `aria-invalid` and `aria-describedby` for form errors
---
## API Integration
### API Client (lib/api.ts)
```typescript
const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000/api/v1"
interface FetchOptions extends RequestInit {
token?: string
}
export async function apiFetch<T>(
endpoint: string,
options: FetchOptions = {}
): Promise<T> {
const { token, ...fetchOptions } = options
const isFormData = options.body instanceof FormData
const headers: HeadersInit = isFormData ? {
...(token ? { Authorization: `Bearer ${token}` } : {}),
...options.headers,
} : {
"Content-Type": "application/json",
...(token ? { Authorization: `Bearer ${token}` } : {}),
...options.headers,
}
const response = await fetch(`${API_URL}${endpoint}`, {
...fetchOptions,
headers,
})
if (response.status === 401) {
if (typeof window !== "undefined") {
localStorage.removeItem("auth-storage")
window.location.href = "/login"
}
throw new Error("Session expired")
}
if (!response.ok) {
const error = await response.json().catch(() => ({}))
throw new Error(error.detail || `API Error: ${response.status}`)
}
if (response.status === 204) {
return {} as T
}
return response.json()
}
```
### Auth Store (lib/auth.ts)
```typescript
"use client"
import { create } from "zustand"
import { persist } from "zustand/middleware"
interface AuthState {
token: string | null
user: any | null
setAuth: (token: string, user: any) => void
logout: () => void
}
export const useAuth = create<AuthState>()(
persist(
(set) => ({
token: null,
user: null,
setAuth: (token, user) => set({ token, user }),
logout: () => set({ token: null, user: null }),
}),
{ name: "auth-storage" }
)
)
```
---
## Backend (Django)
### Code Style Guidelines
- Use Black for code formatting (line length: 88)
- Use isort for import sorting
- 4 spaces indentation
- Models: PascalCase
- Model fields: snake_case
- Methods: snake_case
- Boolean fields: prefix with `is_`, `has_`, `can_`
### Django Model Patterns
```python
class Wrestler(models.Model):
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
club = models.ForeignKey('clubs.Club', on_delete=models.CASCADE)
group = models.CharField(max_length=20, choices=[
('kids', 'Kids'),
('youth', 'Youth'),
('adults', 'Adults'),
])
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
is_active = models.BooleanField(default=True)
class Meta:
ordering = ['last_name', 'first_name']
indexes = [
models.Index(fields=['club', 'group']),
]
```
### Django/Python Development Guidelines
#### Core Principles
- **Django-First Approach**: Use Django's built-in features and tools wherever possible to leverage its full capabilities
- **Code Quality**: Prioritize readability and maintainability; follow Django's coding style guide (PEP 8 compliance)
- **Naming Conventions**: Use descriptive variable and function names; adhere to naming conventions (lowercase with underscores for functions and variables)
- **Modular Architecture**: Structure your project in a modular way using Django apps to promote reusability and separation of concerns
- **Performance Awareness**: Always consider scalability and performance implications in your design decisions
#### Project Structure
**Application Structure**
```
app_name/
├── migrations/ # Database migration files
├── admin.py # Django admin configuration
├── apps.py # App configuration
├── models.py # Database models
├── managers.py # Custom model managers
├── signals.py # Django signals
├── tasks.py # Celery tasks (if applicable)
└── __init__.py # Package initialization
```
**API Structure**
```
api/
└── v1/
├── app_name/
│ ├── urls.py # URL routing
│ ├── serializers.py # Data serialization
│ ├── views.py # API views
│ ├── permissions.py # Custom permissions
│ ├── filters.py # Custom filters
│ └── validators.py # Custom validators
└── urls.py # Main API URL configuration
```
**Core Structure**
```
core/
├── responses.py # Unified response structures
├── pagination.py # Custom pagination classes
├── permissions.py # Base permission classes
├── exceptions.py # Custom exception handlers
├── middleware.py # Custom middleware
├── logging.py # Structured logging utilities
└── validators.py # Reusable validators
```
**Configuration Structure**
```
config/
├── settings/
│ ├── base.py # Base settings
│ ├── development.py # Development settings
│ ├── staging.py # Staging settings
│ └── production.py # Production settings
├── urls.py # Main URL configuration
└── wsgi.py # WSGI configuration
```
#### Views and API Design
- **Use Class-Based Views**: Leverage Django's class-based views (CBVs) with DRF's APIViews
- **RESTful Design**: Follow RESTful principles strictly with proper HTTP methods and status codes
- **Keep Views Light**: Focus views on request handling; keep business logic in models, managers, and services
- **Consistent Response Format**: Use unified response structure for both success and error cases
#### Models and Database
- **ORM First**: Leverage Django's ORM for database interactions; avoid raw SQL queries unless necessary for performance
- **Business Logic in Models**: Keep business logic in models and custom managers
- **Query Optimization**: Use `select_related` and `prefetch_related` for related object fetching
- **Database Indexing**: Implement proper database indexing for frequently queried fields
- **Transactions**: Use `transaction.atomic()` for data consistency in critical operations
#### Serializers and Validation
- **DRF Serializers**: Use Django REST Framework serializers for data validation and serialization
- **Custom Validation**: Implement custom validators for complex business rules
- **Field-Level Validation**: Use serializer field validation for input sanitization
- **Nested Serializers**: Properly handle nested relationships with appropriate serializers
#### Authentication and Permissions
- **JWT Authentication**: Use `djangorestframework-simplejwt` for JWT token-based authentication
- **Custom Permissions**: Implement granular permission classes for different user roles
- **Security Best Practices**: Implement proper CSRF protection, CORS configuration, and input sanitization
#### URL Configuration
- **URL Patterns**: Use `urlpatterns` to define clean URL patterns with each `path()` mapping routes to views
- **Nested Routing**: Use `include()` for modular URL organization
- **API Versioning**: Implement proper API versioning strategy (URL-based versioning recommended)
#### Query Optimization
- **N+1 Problem Prevention**: Always use `select_related` and `prefetch_related` appropriately
- **Query Monitoring**: Monitor query counts and execution time in development
- **Database Connection Pooling**: Implement connection pooling for high-traffic applications
- **Caching Strategy**: Use Django's cache framework with Redis/Memcached for frequently accessed data
#### Response Optimization
- **Pagination**: Standardize pagination across all list endpoints
- **Field Selection**: Allow clients to specify required fields to reduce payload size
- **Compression**: Enable response compression for large payloads
#### Unified Error Responses
```json
{
"success": false,
"message": "Error description",
"errors": {
"field_name": ["Specific error details"]
},
"error_code": "SPECIFIC_ERROR_CODE"
}
```
#### Exception Handling
- **Custom Exception Handler**: Implement global exception handling for consistent error responses
- **Django Signals**: Use Django signals to decouple error handling and post-model activities
- **Proper HTTP Status Codes**: Use appropriate HTTP status codes (400, 401, 403, 404, 422, 500, etc.)
#### Logging Strategy
- **Structured Logging**: Implement structured logging for API monitoring and debugging
- **Request/Response Logging**: Log API calls with execution time, user info, and response status
- **Performance Monitoring**: Log slow queries and performance bottlenecks
---
## API Endpoints
```
# Auth
POST /api/v1/auth/login/ - Login (rate limited)
POST /api/v1/auth/register/ - Register (rate limited)
POST /api/v1/auth/refresh/ - Refresh token (rate limited)
GET /api/v1/auth/me/ - Current user
GET/PATCH /api/v1/auth/preferences/ - User preferences
# Clubs
GET /api/v1/clubs/ - List clubs
POST /api/v1/clubs/ - Create club
GET /api/v1/clubs/{id}/ - Get club
PATCH /api/v1/clubs/{id}/ - Update club
DELETE /api/v1/clubs/{id}/ - Delete club
# Wrestlers
GET /api/v1/wrestlers/ - List wrestlers
POST /api/v1/wrestlers/ - Create wrestler
GET /api/v1/wrestlers/{id}/ - Get wrestler
PATCH /api/v1/wrestlers/{id}/ - Update wrestler
DELETE /api/v1/wrestlers/{id}/ - Delete wrestler
GET /api/v1/wrestlers/available_for_training/ - List ALL wrestlers
# Trainers
GET /api/v1/trainers/ - List trainers
POST /api/v1/trainers/ - Create trainer
GET /api/v1/trainers/{id}/ - Get trainer
PATCH /api/v1/trainers/{id}/ - Update trainer
DELETE /api/v1/trainers/{id}/ - Delete trainer
GET /api/v1/trainers/available_for_training/ - List ALL trainers
# Exercises
GET /api/v1/exercises/ - List exercises
POST /api/v1/exercises/ - Create exercise
GET /api/v1/exercises/{id}/ - Get exercise
PATCH /api/v1/exercises/{id}/ - Update exercise
DELETE /api/v1/exercises/{id}/ - Delete exercise
# Locations
GET /api/v1/locations/ - List locations
POST /api/v1/locations/ - Create location
GET /api/v1/locations/{id}/ - Get location
PATCH /api/v1/locations/{id}/ - Update location
DELETE /api/v1/locations/{id}/ - Delete location
# Trainings
GET /api/v1/trainings/ - List trainings
POST /api/v1/trainings/ - Create training
GET /api/v1/trainings/{id}/ - Get training detail
PATCH /api/v1/trainings/{id}/ - Update training
DELETE /api/v1/trainings/{id}/ - Delete training
# Attendances
GET /api/v1/attendances/?training={id} - List attendances
POST /api/v1/attendances/ - Add wrestler to training
DELETE /api/v1/attendances/{id}/ - Remove wrestler
# Training Exercises
GET /api/v1/training-exercises/ - List training exercises
POST /api/v1/training-exercises/ - Add exercise to training
# Templates
GET /api/v1/templates/ - List templates
POST /api/v1/templates/ - Create template
GET /api/v1/templates/{id}/ - Get template
PATCH /api/v1/templates/{id}/ - Update template
DELETE /api/v1/templates/{id}/ - Delete template
# Template Exercises
GET /api/v1/template-exercises/ - List template exercises
POST /api/v1/template-exercises/ - Add exercise to template
# Homework (Legacy - Templates)
GET /api/v1/homework/ - List homework templates
POST /api/v1/homework/ - Create homework template
GET /api/v1/homework/{id}/ - Get homework
PATCH /api/v1/homework/{id}/ - Update homework
DELETE /api/v1/homework/{id}/ - Delete homework
POST /api/v1/homework/{id}/assign/ - Assign to wrestlers
# Homework Exercise Items
GET /api/v1/homework-exercise-items/ - List exercise items
POST /api/v1/homework-exercise-items/ - Add exercise to homework
# Homework Assignments (Legacy)
GET /api/v1/homework-assignments/ - List assignments
POST /api/v1/homework-assignments/ - Create assignment
GET /api/v1/homework-assignments/{id}/ - Get assignment
POST /api/v1/homework-assignments/{id}/complete-item/ - Mark exercise complete
# Training Homework Assignments (NEW - Primary)
GET /api/v1/homework/training-assignments/ - List training assignments
POST /api/v1/homework/training-assignments/ - Create assignment
GET /api/v1/homework/training-assignments/{id}/ - Get assignment
PATCH /api/v1/homework/training-assignments/{id}/ - Update assignment
DELETE /api/v1/homework/training-assignments/{id}/ - Delete assignment
POST /api/v1/homework/training-assignments/{id}/complete/ - Mark complete
POST /api/v1/homework/training-assignments/{id}/uncomplete/ - Mark uncomplete
```
---
## Backend Environment
### Backend (.env)
```env
SECRET_KEY=your-secret-key-here
DEBUG=True
ALLOWED_HOSTS=localhost,127.0.0.1,testserver
CORS_ALLOWED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000
```
---
## Current Status Summary
### Backend (COMPLETED)
- Django project with 9 apps: auth_app, clubs, wrestlers, trainers, exercises, locations, templates, trainings, homework
- All models with proper fields, relationships, indexes
- DRF ViewSets with pagination, filtering, search
- JWT authentication with rate limiting
- File Upload Validation with FileExtensionValidator
- Django admin with django-unfold
- Seed data command
### Frontend (COMPLETED)
- Next.js 16 with App Router
- React with TypeScript
- Shadcn UI components
- Tailwind CSS
- All main features implemented
- Dashboard with stats
- Full CRUD for wrestlers, trainers, exercises, clubs, templates
- Trainings with Grid/List/Calendar views
- Training detail with attendance and homework assignment
- Homework overview page
- Reactive filter pattern: header + filter (stable) separate from table (reactive update)
### Known Issues
1. ~~**Wrestlers page**: FormData bug in handleSubmit (needs fix)~~ — Fixed
2. **Locations page**: Missing frontend page (backend exists)
3. ~~**Settings page**: Sidebar link exists but page missing~~ — Fixed
4. ~~**Flash bug on /exercises**: Full page flashed white when typing in search~~ — Fixed with reactive table pattern
5. ~~**Flash bug on /trainers**: Full page flashed when filter changed~~ — Fixed with reactive table pattern
---
## Git Workflow
**IMPORTANT: Push after EVERY task completion**
When implementing features, push to Gitea after completing each individual task:
```bash
# After each task completion
git add -A
git commit -m "feat(scope): description"
git push origin feature/branch-name
```
**Why:**
- Prevents loss of work
- Enables testing on multiple devices (e.g., iPhone testing requires network-accessible code)
- Allows rollback if issues arise
- Keeps Gitea in sync with local progress
---
## PWA Implementation (COMPLETED)
**Branch:** `feature/pwa`
### What was implemented:
1. **manifest.json** - PWA configuration with icons, theme colors, display mode
2. **App Icons** - 192x192, 512x512, Apple Touch (180x180), Maskable
3. **Meta Tags** - viewport-fit: cover, theme-color, apple-web-app-capable
4. **Mobile CSS** - Safe areas, touch targets (44x44px), standalone mode styles
5. **InstallPrompt Component** - "Add to Home Screen" banner for iOS/Android
6. **dev:host Script** - `npm run dev:host` for network testing
7. **Network Config** - `.env.local` with API URL on 192.168.168.101.111
### Testing PWA on iPhone:
1. Start backend: `python manage.py runserver 0.0.0.0:8000`
2. Start frontend: `npm run dev:host` (or `npm start` after build)
3. Open Safari → http://192.168.101.111:3000
4. Tap "Teilen" → "Zum Home Screen hinzufügen"
5. App runs standalone without Safari UI
---
## Security Notes
- Rate limiting on auth endpoints (5/minute)
- JWT token expiration handled with automatic redirect to login
- File uploads validated with FileExtensionValidator
- FormData automatically detected in apiFetch (no Content-Type header)
---
## Color Scheme
- Primary: `#1B1A55` (navy)
- Secondary: `#535C91` (blue)
- Accent: `#9290C3` (lavender)
- Background: `#070F2B` (dark sidebar)