- 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
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
ClubFilterBackendandClubLevelPermissionexist inutils/permissions.pybut are NOT enforced - ViewSets use only
IsAuthenticatedpermission - 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 trainersGET /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
anytype - 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/imagefor 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.tswithuseprefix (e.g.,useWrestlers.ts) - Utils:
camelCase.ts(e.g.,formatDate.ts) - Types:
PascalCase.tsor 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:
- Main page component holds: header, filter bar, modal state, total count
- Table sub-component holds: table data state,
isTableLoadingstate, fetch logic - Filter changes → only the table component re-renders, not the whole page
- 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 layoutisTableLoading(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
isLoadinginitializes totrue, becomesfalseafter 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 isDeletingprevents 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-labelfor icon-only buttons - Ensure color contrast ratio ≥ 4.5:1
- Support keyboard navigation (Tab, Enter, Escape)
- Use
aria-invalidandaria-describedbyfor 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_relatedandprefetch_relatedfor 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-simplejwtfor 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
urlpatternsto define clean URL patterns with eachpath()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_relatedandprefetch_relatedappropriately - 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
Wrestlers page: FormData bug in handleSubmit (needs fix)— Fixed- Locations page: Missing frontend page (backend exists)
Settings page: Sidebar link exists but page missing— FixedFlash bug on /exercises: Full page flashed white when typing in search— Fixed with reactive table patternFlash 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)