# Trainings Calendar View - Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Add a calendar month view to the Trainings page as an alternative to grid/list views using react-big-calendar. **Architecture:** Install react-big-calendar, create a CalendarView component with custom styling to match Shadcn theme, integrate with existing view toggle system, and add day-click popover for training details. **Tech Stack:** react-big-calendar, date-fns, existing Shadcn UI components --- ## File Structure ``` frontend/ ├── src/app/(dashboard)/trainings/page.tsx # Modify: add calendar view toggle ├── src/components/ui/calendar-view.tsx # Create: CalendarView component ├── src/components/ui/calendar.css # Create: Calendar custom styles ├── package.json # Modify: add react-big-calendar, date-fns ``` --- ## Task 1: Install Dependencies **Files:** - Modify: `frontend/package.json` - [ ] **Step 1: Add dependencies** ```bash cd /Volumes/T3/Opencode/WrestleDesk/frontend npm install react-big-calendar date-fns npm install -D @types/react-big-calendar ``` - [ ] **Step 2: Verify install** Run: `npm list react-big-calendar date-fns` Expected: Both packages listed --- ## Task 2: Create Calendar CSS **Files:** - Create: `frontend/src/components/ui/calendar.css` - [ ] **Step 1: Write calendar styles** ```css .rbc-calendar { font-family: inherit; background: transparent; } .rbc-toolbar { padding: 1rem 0; margin-bottom: 1rem; flex-wrap: wrap; gap: 0.5rem; } .rbc-toolbar button { color: hsl(var(--foreground)); border: 1px solid hsl(var(--border)); background: transparent; border-radius: 0.375rem; padding: 0.5rem 1rem; font-size: 0.875rem; transition: all 0.2s; } .rbc-toolbar button:hover { background: hsl(var(--muted)); } .rbc-toolbar button.rbc-active { background: hsl(var(--primary)); color: hsl(var(--primary-foreground)); border-color: hsl(var(--primary)); } .rbc-toolbar-label { font-weight: 600; font-size: 1.125rem; } .rbc-header { padding: 0.75rem 0; font-weight: 500; font-size: 0.875rem; color: hsl(var(--muted-foreground)); border-bottom: 1px solid hsl(var(--border)); } .rbc-month-view { border: 1px solid hsl(var(--border)); border-radius: 0.75rem; overflow: hidden; } .rbc-month-row { border-bottom: 1px solid hsl(var(--border)); } .rbc-month-row:last-child { border-bottom: none; } .rbc-day-bg { transition: background-color 0.2s; } .rbc-day-bg:hover { background: hsl(var(--muted)); } .rbc-today { background: hsl(var(--primary) / 0.1); } .rbc-off-range-bg { background: hsl(var(--muted) / 0.3); } .rbc-date-cell { padding: 0.5rem; text-align: right; font-size: 0.875rem; } .rbc-date-cell > a { color: inherit; text-decoration: none; } .rbc-event { background: hsl(var(--primary)); border: none; border-radius: 0.25rem; padding: 0.125rem 0.375rem; font-size: 0.75rem; color: hsl(var(--primary-foreground)); } .rbc-event.rbc-selected { background: hsl(var(--primary) / 0.8); } .rbc-show-more { font-size: 0.75rem; color: hsl(var(--primary)); font-weight: 500; } .rbc-overlay { background: hsl(var(--background)); border: 1px solid hsl(var(--border)); border-radius: 0.5rem; box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1); padding: 0.5rem; min-width: 200px; } .rbc-overlay-header { border-bottom: 1px solid hsl(var(--border)); padding: 0.5rem; font-weight: 600; font-size: 0.875rem; } ``` --- ## Task 3: Create CalendarView Component **Files:** - Create: `frontend/src/components/ui/calendar-view.tsx` - [ ] **Step 1: Write CalendarView component** ```tsx "use client" import { useEffect, useState, useMemo, useCallback } from "react" import { Calendar, dateFnsLocalizer, Views, SlotInfo } from "react-big-calendar" import { format, parse, startOfWeek, getDay, startOfMonth, endOfMonth, addMonths, subMonths } from "date-fns" import { de } from "date-fns/locale" import "react-big-calendar/lib/css/react-big-calendar.css" import "./calendar.css" import { ITraining } from "@/lib/api" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Clock, MapPin, Users, Eye, Pencil, Trash2, X } from "lucide-react" import { useAuth } from "@/lib/auth" import { apiFetch, PaginatedResponse } from "@/lib/api" import { toast } from "sonner" const groupConfig = { kids: { label: "Kinder", class: "bg-primary/10 text-primary border-primary/20" }, youth: { label: "Jugend", class: "bg-secondary/10 text-secondary border-secondary/20" }, adults: { label: "Erwachsene", class: "bg-accent/10 text-accent border-accent/20" }, all: { label: "Alle", class: "bg-muted text-muted-foreground" }, } const locales = { "de": de } const localizer = dateFnsLocalizer({ format, parse, startOfWeek: () => startOfWeek(new Date(), { locale: de }), getDay, locales, }) interface CalendarViewProps { onEdit: (training: ITraining) => void onDelete: (id: number) => void onView: (training: ITraining) => void refreshTrigger: number } interface CalendarEvent { id: number title: string start: Date end: Date resource: ITraining } export function CalendarView({ onEdit, onDelete, onView, refreshTrigger }: CalendarViewProps) { const { token } = useAuth() const [currentDate, setCurrentDate] = useState(new Date()) const [trainings, setTrainings] = useState([]) const [isLoading, setIsLoading] = useState(true) const [selectedDayTrainings, setSelectedDayTrainings] = useState([]) const [selectedDay, setSelectedDay] = useState(null) const [popoverOpen, setPopoverOpen] = useState(false) const fetchTrainings = useCallback(async (date: Date) => { if (!token) return setIsLoading(true) try { const start = startOfMonth(subMonths(date, 1)) const end = endOfMonth(addMonths(date, 1)) const params = new URLSearchParams() params.set("date_from", format(start, "yyyy-MM-dd")) params.set("date_to", format(end, "yyyy-MM-dd")) params.set("page_size", "100") const data = await apiFetch>(`/trainings/?${params.toString()}`, { token }) setTrainings(data.results || []) } catch { toast.error("Fehler beim Laden der Trainingseinheiten") } finally { setIsLoading(false) } }, [token]) useEffect(() => { fetchTrainings(currentDate) }, [fetchTrainings, currentDate, refreshTrigger]) const events: CalendarEvent[] = useMemo(() => { return trainings .filter(t => t.date) .map(t => ({ id: t.id, title: `${t.start_time || ""} - ${t.group ? groupConfig[t.group as keyof typeof groupConfig]?.label : ""}`, start: new Date(`${t.date}T${t.start_time || "00:00"}`), end: new Date(`${t.date}T${t.end_time || "23:59"}`), resource: t, })) }, [trainings]) const handleSelectEvent = useCallback((event: CalendarEvent) => { onView(event.resource) }, [onView]) const handleSelectSlot = useCallback((slotInfo: SlotInfo) => { const dayTrainings = trainings.filter(t => { const trainingDate = new Date(t.date) return format(trainingDate, "yyyy-MM-dd") === format(slotInfo.start, "yyyy-MM-dd") }) if (dayTrainings.length > 0) { setSelectedDay(slotInfo.start) setSelectedDayTrainings(dayTrainings) setPopoverOpen(true) } }, [trainings]) const eventStyleGetter = useCallback((event: CalendarEvent) => { const group = event.resource.group const config = groupConfig[group as keyof typeof groupConfig] || groupConfig.all return { style: { backgroundColor: `hsl(var(--primary))`, borderRadius: "0.25rem", border: "none", color: `hsl(var(--primary-foreground))`, } } }, []) const CustomEvent = ({ event }: { event: CalendarEvent }) => (
{event.resource.start_time}
) const CustomToolbar = ({ label, onNavigate, onView, view }: any) => (
{label}
) return (
{popoverOpen && selectedDay && (
setPopoverOpen(false)}>
{format(selectedDay, "EEEE, d. MMMM", { locale: de })}
{selectedDayTrainings.map(training => (
{ onView(training) setPopoverOpen(false) }} >
{training.start_time} - {training.end_time}
{groupConfig[training.group as keyof typeof groupConfig]?.label}
{training.attendance_count || 0}
))}
)}
) } ``` --- ## Task 4: Integrate Calendar into Trainings Page **Files:** - Modify: `frontend/src/app/(dashboard)/trainings/page.tsx` - [ ] **Step 1: Add imports** Add to imports section: ```tsx import { CalendarView } from "@/components/ui/calendar-view" import { Calendar } from "lucide-react" ``` - [ ] **Step 2: Update viewMode state** Change line ~50: ```tsx const [viewMode, setViewMode] = useState<"grid" | "list" | "calendar">("grid") ``` - [ ] **Step 3: Update toggle buttons** Find the existing toggle (around line 420-440) and update to: ```tsx
``` - [ ] **Step 4: Add CalendarView rendering** After the existing grid/list views (around line 525), add: ```tsx {viewMode === "calendar" && ( setDeleteId(id)} onView={(training) => router.push(`/trainings/${training.id}`)} refreshTrigger={trainings.length} /> )} ``` - [ ] **Step 5: Hide pagination in calendar view** Find pagination section and update: ```tsx {totalPages > 1 && viewMode !== "calendar" && ( )} ``` --- ## Task 5: Build and Test - [ ] **Step 1: Run build** Run: `cd /Volumes/T3/Opencode/WrestleDesk/frontend && npm run build` Expected: SUCCESS - [ ] **Step 2: Test in browser** Run: `cd /Volumes/T3/Opencode/WrestleDesk/frontend && npm run dev` Navigate to: http://localhost:3000/trainings - Click calendar icon in toggle - Verify calendar renders with month navigation - Click on a day to see trainings popover - Navigate between months - Click "Heute" to return to current month --- ## Task 6: Commit ```bash git add -A git commit -m "feat(trainings): add calendar month view with react-big-calendar" ```