Files
WrestleDesk/AGENTS.md
T
Andrej Spielmann 3fefc550fe Initial commit: WrestleDesk full project
- Django backend with DRF (clubs, wrestlers, trainers, exercises, templates, trainings, homework, locations, leistungstest)
- Next.js 16 frontend with React, Shadcn UI, Tailwind
- JWT authentication
- Full CRUD for all entities
- Calendar view for trainings
- Homework management system
- Leistungstest tracking
2026-03-26 13:24:57 +01:00

36 KiB

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)

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)

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

npx create-next-app@latest frontend --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
cd frontend

Initialize Shadcn UI

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

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

"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

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)

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

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

const [isOpen, setIsOpen] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)

Server State (TanStack Query): Use for all API data

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

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

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

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)

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

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

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):

const [deleteId, setDeleteId] = useState<number | null>(null)
const [isDeleting, setIsDeleting] = useState(false)

Delete handler (sets confirmation state instead of deleting directly):

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):

<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):

<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

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

"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

import dynamic from "next/dynamic"

const DataTable = dynamic(() => import("@/components/data-table"), {
  loading: () => <TableSkeleton />,
  ssr: false,
})

Image Optimization

import Image from "next/image"

<Image
  src={wrestler.photo}
  alt={wrestler.full_name}
  width={200}
  height={200}
  className="object-cover rounded-lg"
/>

Memoization

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

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)

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)

"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

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

{
    "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)

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

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)