Files
meldestelle/docs/06_Frontend/FIGMA/Vision_03/docs/FRONTEND.md
T
stefan 7702574904 feat(ui): introduce PferdReiterEingabe, NennungenTabelle, and NennungsMaske components
- Added `PferdReiterEingabe` for horse and rider selection with search functionality and keyboard navigation.
- Implemented `NennungenTabelle` to display filtered registrations based on selected horse or rider.
- Introduced `NennungsMaske` to combine search, table, and competition views for streamlined user interaction.
- Extended types with `Veranstalter` interface and mock data for better context and integration.
- Documented ÖTO-compliant tournament structure for frontend reference.

Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
2026-03-24 13:49:21 +01:00

26 KiB

Frontend-Dokumentation - Frontend Developer

🎯 Frontend-Übersicht

React 18+ basierte Single Page Application mit TypeScript, Material-UI und React Router.

Tech Stack:

  • React 18+ (Function Components + Hooks)
  • TypeScript 5+
  • Material-UI v6 (Material Design 3)
  • React Router v7 (Data Mode)
  • Tailwind CSS v4
  • Vite (Build Tool)

📁 Projekt-Struktur

/src
├── /app
│   ├── App.tsx                          # Main Component (RouterProvider)
│   ├── routes.tsx                       # React Router Configuration
│   │
│   └── /components
│       ├── Login.tsx                    # Login Page
│       ├── Dashboard.tsx                # Admin Dashboard
│       │
│       ├── VeranstalterVerwaltung.tsx   # Veranstalter Management
│       ├── VeranstalterAuswahl.tsx      # Veranstalter Selection
│       │
│       ├── TurnierErstellen.tsx         # Veranstaltung Overview
│       ├── TurnierAnsicht.tsx           # Turnier View (8 Tabs)
│       │
│       ├── NennungsMaske.tsx            # Registration Mask
│       ├── PferdReiterEingabe.tsx       # Horse/Rider Input
│       ├── NennungenTabelle.tsx         # Registrations Table
│       ├── VerkaufBuchungen.tsx         # Sales/Bookings
│       ├── Bewerbsliste.tsx             # Contest List
│       │
│       └── /turnier
│           ├── VeranstaltungUebersicht.tsx  # Event Overview
│           ├── StammdatenTab.tsx            # Master Data Tab
│           ├── OrganisationTab.tsx          # Organization Tab
│           ├── BewerbeTab.tsx               # Contests Tab
│           ├── ArtikelTab.tsx               # Articles/Pricing Tab
│           ├── AbrechnungTab.tsx            # Billing Tab
│           ├── NennungenTab.tsx             # Registrations Tab
│           ├── StartlistenTab.tsx           # Start Lists Tab
│           └── ErgebnislistenTab.tsx        # Results Tab
│
├── /styles
│   ├── theme.css                        # Tailwind Theme + CSS Variables
│   └── fonts.css                        # Font Imports
│
└── main.tsx                             # Vite Entry Point

🔄 Component-Hierarchie

App
├── RouterProvider
    ├── Login (/)
    │
    ├── Dashboard (/admin)
    │   └── Veranstaltungs-Liste
    │       └── Veranstaltungs-Karten
    │
    ├── VeranstalterVerwaltung (/veranstalter)
    │   └── Veranstalter-Tabelle
    │       ├── Filter-Controls
    │       └── CRUD-Buttons
    │
    ├── TurnierErstellen (/veranstaltung/:id)
    │   └── VeranstaltungUebersicht
    │       └── Turnier-Karten
    │           └── Status-Badge
    │
    └── TurnierAnsicht (/turnier/:veranstaltungId/:nr)
        ├── Navigation Breadcrumbs
        ├── Tab Navigation
        └── Tab Content
            ├── StammdatenTab
            │   ├── Turnier-Konfiguration
            │   ├── Turnier-Beschreibung
            │   └── Sponsoren
            │
            ├── OrganisationTab
            │   ├── Zeitplan
            │   └── Kontakte
            │
            ├── BewerbeTab
            │   └── Bewerbs-Tabelle
            │
            ├── ArtikelTab
            │   ├── Nennungs-/Startgebühren
            │   ├── Stallungen & Boxen
            │   └── Zusatzleistungen
            │
            ├── AbrechnungTab
            │   ├── Buchungstabelle (70%)
            │   │   ├── Haupt-Tabs
            │   │   ├── Aktions-Buttons
            │   │   └── Buchungs-Table
            │   └── Aktionsbereich (30%)
            │       ├── Teilnehmer-Auswahl
            │       ├── Buchen-Panel
            │       ├── Direkt Drucken
            │       └── Zahlungsart
            │
            ├── NennungenTab
            │   └── NennungsMaske
            │       ├── Obere Hälfte (50%)
            │       │   ├── PferdReiterEingabe (60%)
            │       │   └── VerkaufBuchungen (40%)
            │       ├── Navigation Buttons (5%)
            │       └── Untere Hälfte (45%)
            │           ├── NennungenTabelle (60%)
            │           └── Bewerbsliste (40%)
            │
            ├── StartlistenTab
            ├── ErgebnislistenTab

🎨 Styling System

Material-UI Theme

// App.tsx
import { createTheme, ThemeProvider } from '@mui/material/styles';

const theme = createTheme({
  palette: {
    primary: {
      main: '#3F51B5',  // Indigo
      light: '#7986CB',
      dark: '#303F9F',
    },
    secondary: {
      main: '#FF5722',
    },
  },
  typography: {
    fontSize: 11,  // Kompakt für Desktop
  },
  components: {
    MuiButton: {
      styleOverrides: {
        root: {
          textTransform: 'none',  // Keine ALL CAPS
        },
      },
    },
  },
});

<ThemeProvider theme={theme}>
  <RouterProvider router={router} />
</ThemeProvider>

Tailwind CSS v4

/* src/styles/theme.css */
@import "tailwindcss";

@theme {
  /* Colors */
  --color-primary: #3f51b5;
  --color-primary-light: #7986cb;
  --color-primary-dark: #303f9f;
  
  /* Font Sizes (Kompakt) */
  --font-size-xs: 10px;
  --font-size-sm: 11px;
  --font-size-base: 13px;
  --font-size-lg: 15px;
  
  /* Spacing */
  --spacing-tight: 8px;
  --spacing-normal: 16px;
  --spacing-loose: 24px;
}

/* Base Styles */
body {
  font-family: 'Roboto', sans-serif;
  font-size: var(--font-size-base);
}

/* Material-UI Overrides */
.MuiTab-root {
  font-size: 11px !important;
  min-height: 36px !important;
}

.MuiTableCell-root {
  font-size: 10px !important;
  padding: 8px !important;
}

Styling Best Practices

1. MUI Components mit sx Prop

<Button
  variant="contained"
  sx={{
    fontSize: '11px',
    py: 0.5,
    px: 2,
    minWidth: 120
  }}
>
  Speichern
</Button>

2. Tailwind Utility Classes

<div className="flex items-center gap-2 p-4">
  <span className="text-sm font-semibold">Status:</span>
  <Badge className="bg-green-500">Aktiv</Badge>
</div>

3. Hybrid Approach (empfohlen)

<Box sx={{ p: 2, bgcolor: 'background.paper' }}>
  <Typography className="text-xs font-medium">
    Turnier-Name
  </Typography>
</Box>

🧩 Component Patterns

1. Container Component (Smart)

// BewerbeTab.tsx
import { useState, useEffect } from 'react';

export function BewerbeTab() {
  const [bewerbe, setBewerbe] = useState<Bewerb[]>([]);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    // Daten laden
    fetchBewerbe();
  }, []);
  
  const fetchBewerbe = async () => {
    try {
      setLoading(true);
      const data = await api.getBewerbe();
      setBewerbe(data);
    } catch (error) {
      console.error(error);
    } finally {
      setLoading(false);
    }
  };
  
  const handleCreate = (bewerb: Bewerb) => {
    setBewerbe([...bewerbe, bewerb]);
  };
  
  if (loading) return <Loading />;
  
  return (
    <Box>
      <BewerbeToolbar onCreate={handleCreate} />
      <BewerbeTable bewerbe={bewerbe} />
    </Box>
  );
}

2. Presentational Component (Dumb)

// BewerbeTable.tsx
interface Props {
  bewerbe: Bewerb[];
  onEdit?: (bewerb: Bewerb) => void;
  onDelete?: (id: number) => void;
}

export function BewerbeTable({ bewerbe, onEdit, onDelete }: Props) {
  return (
    <Table>
      <TableHead>
        <TableRow>
          <TableCell>Nr.</TableCell>
          <TableCell>Name</TableCell>
          <TableCell>Klasse</TableCell>
        </TableRow>
      </TableHead>
      <TableBody>
        {bewerbe.map((bewerb) => (
          <TableRow key={bewerb.id}>
            <TableCell>{bewerb.nr}</TableCell>
            <TableCell>{bewerb.name}</TableCell>
            <TableCell>{bewerb.klasse}</TableCell>
          </TableRow>
        ))}
      </TableBody>
    </Table>
  );
}

3. Custom Hook

// hooks/useTurnier.ts
import { useState, useEffect } from 'react';

export function useTurnier(id: string) {
  const [turnier, setTurnier] = useState<Turnier | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);
  
  useEffect(() => {
    const fetchTurnier = async () => {
      try {
        setLoading(true);
        const data = await api.getTurnier(id);
        setTurnier(data);
      } catch (err) {
        setError(err as Error);
      } finally {
        setLoading(false);
      }
    };
    
    fetchTurnier();
  }, [id]);
  
  return { turnier, loading, error };
}

// Usage
const { turnier, loading, error } = useTurnier('123');

🔄 State Management

Aktuell: Local State (useState)

// TurnierAnsicht.tsx
export function TurnierAnsicht() {
  const [activeTab, setActiveTab] = useState(0);
  const [turnier, setTurnier] = useState<Turnier | null>(null);
  
  return (
    <Box>
      <Tabs value={activeTab} onChange={(_, v) => setActiveTab(v)}>
        <Tab label="Stammdaten" />
        <Tab label="Bewerbe" />
      </Tabs>
      
      {activeTab === 0 && <StammdatenTab turnier={turnier} />}
      {activeTab === 1 && <BewerbeTab turnierId={turnier?.id} />}
    </Box>
  );
}

Empfohlen: React Context (für geteilten State)

// context/TurnierContext.tsx
import { createContext, useContext, useState, ReactNode } from 'react';

interface TurnierContextType {
  turnier: Turnier | null;
  setTurnier: (turnier: Turnier) => void;
  bewerbe: Bewerb[];
  setBewerbe: (bewerbe: Bewerb[]) => void;
}

const TurnierContext = createContext<TurnierContextType | undefined>(undefined);

export function TurnierProvider({ children }: { children: ReactNode }) {
  const [turnier, setTurnier] = useState<Turnier | null>(null);
  const [bewerbe, setBewerbe] = useState<Bewerb[]>([]);
  
  return (
    <TurnierContext.Provider value={{ turnier, setTurnier, bewerbe, setBewerbe }}>
      {children}
    </TurnierContext.Provider>
  );
}

export function useTurnierContext() {
  const context = useContext(TurnierContext);
  if (!context) {
    throw new Error('useTurnierContext must be used within TurnierProvider');
  }
  return context;
}

// Usage in TurnierAnsicht.tsx
<TurnierProvider>
  <TurnierAnsicht />
</TurnierProvider>

// Usage in Child Component
const { turnier, setTurnier } = useTurnierContext();

Alternative: Zustand (für komplexe Apps)

// store/useTurnierStore.ts
import { create } from 'zustand';

interface TurnierStore {
  turnier: Turnier | null;
  bewerbe: Bewerb[];
  nennungen: Nennung[];
  
  setTurnier: (turnier: Turnier) => void;
  addBewerb: (bewerb: Bewerb) => void;
  addNennung: (nennung: Nennung) => void;
  clearStore: () => void;
}

export const useTurnierStore = create<TurnierStore>((set) => ({
  turnier: null,
  bewerbe: [],
  nennungen: [],
  
  setTurnier: (turnier) => set({ turnier }),
  
  addBewerb: (bewerb) => 
    set((state) => ({ bewerbe: [...state.bewerbe, bewerb] })),
  
  addNennung: (nennung) => 
    set((state) => ({ nennungen: [...state.nennungen, nennung] })),
  
  clearStore: () => set({ turnier: null, bewerbe: [], nennungen: [] }),
}));

// Usage
const { turnier, addBewerb } = useTurnierStore();

🌐 Routing

Route Konfiguration

// routes.tsx
import { createBrowserRouter } from 'react-router';
import { Login } from './components/Login';
import { Dashboard } from './components/Dashboard';
import { VeranstalterVerwaltung } from './components/VeranstalterVerwaltung';
import { TurnierErstellen } from './components/TurnierErstellen';
import { TurnierAnsicht } from './components/TurnierAnsicht';

export const router = createBrowserRouter([
  {
    path: '/',
    element: <Login />,
  },
  {
    path: '/admin',
    element: <Dashboard />,
  },
  {
    path: '/veranstalter',
    element: <VeranstalterVerwaltung />,
  },
  {
    path: '/veranstaltung/:id',
    element: <TurnierErstellen />,
  },
  {
    path: '/turnier/:veranstaltungId/:nr',
    element: <TurnierAnsicht />,
  },
  {
    path: '*',
    element: <NotFound />,
  },
]);

Navigation

import { useNavigate, useParams } from 'react-router';

export function TurnierAnsicht() {
  const navigate = useNavigate();
  const params = useParams();
  
  const veranstaltungId = params.veranstaltungId;
  const turnierNr = params.nr;
  
  const handleZurueck = () => {
    navigate(`/veranstaltung/${veranstaltungId}`);
  };
  
  const handleToAdmin = () => {
    navigate('/admin');
  };
  
  return (
    <Box>
      <Breadcrumbs>
        <Link onClick={handleToAdmin}>Admin - Verwaltung</Link>
        <Link onClick={handleZurueck}>Veranstaltung</Link>
        <Typography>Turnier {turnierNr}</Typography>
      </Breadcrumbs>
    </Box>
  );
}

📡 API Integration

API Client Setup

// api/client.ts
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/api';

class ApiClient {
  private token: string | null = null;
  
  setToken(token: string) {
    this.token = token;
    localStorage.setItem('token', token);
  }
  
  clearToken() {
    this.token = null;
    localStorage.removeItem('token');
  }
  
  async request<T>(
    endpoint: string,
    options: RequestInit = {}
  ): Promise<T> {
    const headers: HeadersInit = {
      'Content-Type': 'application/json',
      ...(this.token && { Authorization: `Bearer ${this.token}` }),
      ...options.headers,
    };
    
    const response = await fetch(`${API_BASE_URL}${endpoint}`, {
      ...options,
      headers,
    });
    
    if (!response.ok) {
      const error = await response.json();
      throw new Error(error.error?.message || 'Request failed');
    }
    
    return response.json();
  }
  
  get<T>(endpoint: string): Promise<T> {
    return this.request<T>(endpoint, { method: 'GET' });
  }
  
  post<T>(endpoint: string, data: any): Promise<T> {
    return this.request<T>(endpoint, {
      method: 'POST',
      body: JSON.stringify(data),
    });
  }
  
  put<T>(endpoint: string, data: any): Promise<T> {
    return this.request<T>(endpoint, {
      method: 'PUT',
      body: JSON.stringify(data),
    });
  }
  
  delete<T>(endpoint: string): Promise<T> {
    return this.request<T>(endpoint, { method: 'DELETE' });
  }
}

export const apiClient = new ApiClient();

API Service Layer

// api/turnierService.ts
import { apiClient } from './client';

export interface Turnier {
  id: number;
  veranstaltungId: number;
  nr: string;
  name: string;
  // ...
}

export const turnierService = {
  async getTurniere(veranstaltungId?: number): Promise<Turnier[]> {
    const params = veranstaltungId ? `?veranstaltungId=${veranstaltungId}` : '';
    const response = await apiClient.get<{ data: Turnier[] }>(`/turniere${params}`);
    return response.data;
  },
  
  async getTurnierById(id: number): Promise<Turnier> {
    const response = await apiClient.get<{ data: Turnier }>(`/turniere/${id}`);
    return response.data;
  },
  
  async createTurnier(turnier: Partial<Turnier>): Promise<Turnier> {
    const response = await apiClient.post<{ data: Turnier }>('/turniere', turnier);
    return response.data;
  },
  
  async updateTurnier(id: number, turnier: Partial<Turnier>): Promise<Turnier> {
    const response = await apiClient.put<{ data: Turnier }>(`/turniere/${id}`, turnier);
    return response.data;
  },
  
  async deleteTurnier(id: number): Promise<void> {
    await apiClient.delete(`/turniere/${id}`);
  },
};

React Query Integration (empfohlen)

// hooks/useTurniere.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { turnierService } from '../api/turnierService';

export function useTurniere(veranstaltungId?: number) {
  return useQuery({
    queryKey: ['turniere', veranstaltungId],
    queryFn: () => turnierService.getTurniere(veranstaltungId),
  });
}

export function useTurnier(id: number) {
  return useQuery({
    queryKey: ['turnier', id],
    queryFn: () => turnierService.getTurnierById(id),
    enabled: !!id,
  });
}

export function useCreateTurnier() {
  const queryClient = useQueryClient();
  
  return useMutation({
    mutationFn: turnierService.createTurnier,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['turniere'] });
    },
  });
}

// Usage in Component
const { data: turniere, isLoading } = useTurniere(veranstaltungId);
const createMutation = useCreateTurnier();

const handleCreate = (turnier: Partial<Turnier>) => {
  createMutation.mutate(turnier);
};

📝 Form Handling

React Hook Form

// components/TurnierForm.tsx
import { useForm, Controller } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

const turnierSchema = z.object({
  nr: z.string().min(1, 'Nummer ist erforderlich'),
  name: z.string().min(1, 'Name ist erforderlich'),
  znsDaten: z.string().optional(),
  oetoTyp: z.enum(['national', 'international']),
});

type TurnierFormData = z.infer<typeof turnierSchema>;

export function TurnierForm({ onSubmit }: { onSubmit: (data: TurnierFormData) => void }) {
  const {
    control,
    handleSubmit,
    formState: { errors },
  } = useForm<TurnierFormData>({
    resolver: zodResolver(turnierSchema),
    defaultValues: {
      nr: '',
      name: '',
      oetoTyp: 'national',
    },
  });
  
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <Controller
        name="nr"
        control={control}
        render={({ field }) => (
          <TextField
            {...field}
            label="Turnier-Nummer"
            error={!!errors.nr}
            helperText={errors.nr?.message}
            size="small"
            fullWidth
          />
        )}
      />
      
      <Controller
        name="name"
        control={control}
        render={({ field }) => (
          <TextField
            {...field}
            label="Turnier-Name"
            error={!!errors.name}
            helperText={errors.name?.message}
            size="small"
            fullWidth
          />
        )}
      />
      
      <Controller
        name="oetoTyp"
        control={control}
        render={({ field }) => (
          <FormControl fullWidth size="small">
            <InputLabel>ÖTO-Typ</InputLabel>
            <Select {...field} label="ÖTO-Typ">
              <MenuItem value="national">National</MenuItem>
              <MenuItem value="international">International</MenuItem>
            </Select>
          </FormControl>
        )}
      />
      
      <Button type="submit" variant="contained">
        Speichern
      </Button>
    </form>
  );
}

🎯 TypeScript Types

Type Definitions

// types/index.ts

export interface Veranstalter {
  id: number;
  name: string;
  adresse: string;
  plz: string;
  ort: string;
  land: string;
  telefon: string;
  email: string;
  website: string;
  vereinsnummer: string;
}

export interface Veranstaltung {
  id: number;
  veranstalterId: number;
  name: string;
  ort: string;
  startDatum: string;
  endDatum: string;
  status: 'geplant' | 'laufend' | 'abgeschlossen';
  turniere: Turnier[];
}

export interface Turnier {
  id: number;
  veranstaltungId: number;
  nr: string;
  name: string;
  znsDaten: string;
  oetoTyp: 'national' | 'international';
  feiTyp?: string;
  titel: string;
  subTitel: string;
  sponsoren: Sponsor[];
}

export interface Sponsor {
  name: string;
  logo: string;
}

export interface Bewerb {
  id: number;
  turnierId: number;
  nr: string;
  name: string;
  klasse: string;
  tag: number;
  datum: string;
  beginn: string;
  platz: string;
  typ: string;
  richter: string;
  maxTeilnehmer: number;
  startgebuehr: number;
}

export interface Reiter {
  id: number;
  vorname: string;
  nachname: string;
  geburtsdatum: string;
  ort: string;
  land: string;
  verein: string;
  lizenznummer: string;
}

export interface Pferd {
  id: number;
  name: string;
  geschlecht: 'Hengst' | 'Stute' | 'Wallach';
  geburtsjahr: number;
  rasse: string;
  farbe: string;
  besitzer: string;
  lebensnummer: string;
}

export interface Nennung {
  id: number;
  turnierId: number;
  bewerbId: number;
  reiterId: number;
  pferdId: number;
  startnummer?: number;
  startwunsch?: 'vorne' | 'hinten';
  status: 'offen' | 'bestätigt' | 'gestartet' | 'abgeschlossen';
}

export interface Buchung {
  id: number;
  turnierId: number;
  reiterId?: number;
  pferdId?: number;
  buchungstext: string;
  soll: number;
  haben: number;
  saldo: number;
  zahlungsart: 'bar' | 'scheck' | 'bankomat' | 'kreditkarte';
  status: 'offen' | 'bezahlt' | 'storniert';
}

🧪 Testing

Component Testing (React Testing Library)

// __tests__/BewerbeTab.test.tsx
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { BewerbeTab } from '../components/turnier/BewerbeTab';

describe('BewerbeTab', () => {
  it('renders bewerbe table', () => {
    render(<BewerbeTab />);
    
    expect(screen.getByText('Bewerbs-Übersicht')).toBeInTheDocument();
    expect(screen.getByRole('button', { name: /Neuer Bewerb/i })).toBeInTheDocument();
  });
  
  it('opens dialog when "Neuer Bewerb" is clicked', async () => {
    render(<BewerbeTab />);
    
    const button = screen.getByRole('button', { name: /Neuer Bewerb/i });
    fireEvent.click(button);
    
    await waitFor(() => {
      expect(screen.getByText('Bewerb erstellen')).toBeInTheDocument();
    });
  });
});

Hook Testing

// __tests__/useTurnier.test.tsx
import { renderHook, waitFor } from '@testing-library/react';
import { useTurnier } from '../hooks/useTurnier';

describe('useTurnier', () => {
  it('fetches turnier data', async () => {
    const { result } = renderHook(() => useTurnier('123'));
    
    expect(result.current.loading).toBe(true);
    
    await waitFor(() => {
      expect(result.current.loading).toBe(false);
    });
    
    expect(result.current.turnier).toBeDefined();
    expect(result.current.error).toBeNull();
  });
});

Performance Optimierung

1. Code Splitting (Lazy Loading)

// App.tsx
import { lazy, Suspense } from 'react';

const Dashboard = lazy(() => import('./components/Dashboard'));
const TurnierAnsicht = lazy(() => import('./components/TurnierAnsicht'));

function App() {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      <RouterProvider router={router} />
    </Suspense>
  );
}

2. Memoization

import { memo, useMemo, useCallback } from 'react';

// Component Memoization
export const BewerbeTable = memo(({ bewerbe }: { bewerbe: Bewerb[] }) => {
  return <Table>...</Table>;
});

// Value Memoization
const sortedBewerbe = useMemo(() => {
  return bewerbe.sort((a, b) => a.nr.localeCompare(b.nr));
}, [bewerbe]);

// Function Memoization
const handleDelete = useCallback((id: number) => {
  deleteBewerb(id);
}, []);

3. Virtual Scrolling (für große Listen)

import { FixedSizeList } from 'react-window';

function BewerbeList({ bewerbe }: { bewerbe: Bewerb[] }) {
  return (
    <FixedSizeList
      height={600}
      itemCount={bewerbe.length}
      itemSize={50}
      width="100%"
    >
      {({ index, style }) => (
        <div style={style}>
          {bewerbe[index].name}
        </div>
      )}
    </FixedSizeList>
  );
}

🔨 Build & Development

Vite Configuration

// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  server: {
    port: 3000,
    proxy: {
      '/api': {
        target: 'http://localhost:3000',
        changeOrigin: true,
      },
    },
  },
  build: {
    outDir: 'dist',
    sourcemap: true,
    rollupOptions: {
      output: {
        manualChunks: {
          'react-vendor': ['react', 'react-dom', 'react-router'],
          'mui-vendor': ['@mui/material', '@mui/icons-material'],
        },
      },
    },
  },
});

Package.json Scripts

{
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview",
    "test": "vitest",
    "test:ui": "vitest --ui",
    "lint": "eslint src --ext ts,tsx",
    "type-check": "tsc --noEmit"
  }
}

📦 Key Dependencies

{
  "dependencies": {
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "react-router": "^7.1.3",
    "@mui/material": "^6.3.0",
    "@mui/icons-material": "^6.3.0",
    "@emotion/react": "^11.13.3",
    "@emotion/styled": "^11.13.0",
    "tailwindcss": "^4.0.0",
    "@tanstack/react-query": "^5.62.15",
    "react-hook-form": "^7.55.0",
    "zod": "^3.24.1",
    "zustand": "^5.0.3"
  },
  "devDependencies": {
    "@types/react": "^18.3.17",
    "@types/react-dom": "^18.3.5",
    "@vitejs/plugin-react": "^4.3.4",
    "typescript": "^5.7.3",
    "vite": "^6.0.7",
    "vitest": "^3.0.0",
    "@testing-library/react": "^16.1.0",
    "@testing-library/jest-dom": "^6.6.3",
    "eslint": "^9.18.0"
  }
}

🚀 Deployment

Environment Variables

# .env.development
VITE_API_BASE_URL=http://localhost:3000/api

# .env.production
VITE_API_BASE_URL=https://api.turnierverwaltung.at/api

Vercel Deployment

# Install Vercel CLI
npm i -g vercel

# Deploy
vercel --prod

Dokumentiert von: Frontend Developer
Version: 1.0
Datum: 2026-03-24