Zum Inhalt springen
React Anfänger 90 min

Projekt: Dashboard-App

Baue eine komplette Dashboard-Anwendung mit React. Datenvisualisierung, Tabellen, Authentifizierung und geschützte Routen.

Aktualisiert:

Projekt: Dashboard-App

Dies ist das Abschlussprojekt des React-Kurses. Du baust eine vollstaendige Dashboard-Anwendung, die alles zusammenfuehrt: Routing, State Management, Authentifizierung, Datenlade-Patterns und geschuetzte Routen.

Was wir bauen

  • Login-System mit geschuetzten Routen
  • Dashboard-Uebersicht mit Statistik-Karten
  • Datentabelle mit Sortierung, Filter und Suche
  • Benutzer-Verwaltung (CRUD-Operationen)
  • Dark Mode mit Context
  • Responsive Sidebar-Navigation

Projekt aufsetzen

npm create vite@latest react-dashboard -- --template react
cd react-dashboard
npm install react-router-dom
npm run dev

Projektstruktur

src/
├── components/
│   ├── Sidebar.jsx
│   ├── StatsCard.jsx
│   ├── DataTable.jsx
│   ├── UserForm.jsx
│   └── ProtectedRoute.jsx
├── context/
│   ├── AuthContext.jsx
│   └── ThemeContext.jsx
├── data/
│   └── mockData.js
├── hooks/
│   └── useFetch.js
├── pages/
│   ├── Login.jsx
│   ├── Dashboard.jsx
│   ├── Users.jsx
│   ├── UserDetail.jsx
│   └── Settings.jsx
├── App.jsx
└── main.jsx

Schritt 1: Mock-Daten

// src/data/mockData.js
export const mockUsers = [
  { id: 1, name: 'Max Mustermann', email: 'max@example.com', role: 'Admin', status: 'active', joined: '2024-01-15' },
  { id: 2, name: 'Anna Schmidt', email: 'anna@example.com', role: 'Editor', status: 'active', joined: '2024-03-22' },
  { id: 3, name: 'Tom Weber', email: 'tom@example.com', role: 'User', status: 'inactive', joined: '2024-06-10' },
  { id: 4, name: 'Lisa Mueller', email: 'lisa@example.com', role: 'Editor', status: 'active', joined: '2024-07-05' },
  { id: 5, name: 'Jan Koch', email: 'jan@example.com', role: 'User', status: 'active', joined: '2024-09-18' },
  { id: 6, name: 'Sarah Braun', email: 'sarah@example.com', role: 'Admin', status: 'active', joined: '2024-11-01' },
  { id: 7, name: 'Paul Richter', email: 'paul@example.com', role: 'User', status: 'inactive', joined: '2025-01-12' },
  { id: 8, name: 'Maria Wagner', email: 'maria@example.com', role: 'Editor', status: 'active', joined: '2025-02-28' }
];

export const mockStats = {
  totalUsers: 1234,
  activeUsers: 892,
  revenue: 45678,
  orders: 356,
  growth: 12.5,
  conversionRate: 3.2
};

Schritt 2: Theme-Context

// src/context/ThemeContext.jsx
import { createContext, useContext, useState, useEffect } from 'react';

const ThemeContext = createContext();

export function ThemeProvider({ children }) {
  const [isDark, setIsDark] = useState(() => {
    return localStorage.getItem('theme') === 'dark';
  });

  useEffect(() => {
    localStorage.setItem('theme', isDark ? 'dark' : 'light');
    document.body.style.backgroundColor = isDark ? '#0f172a' : '#f8fafc';
    document.body.style.color = isDark ? '#e2e8f0' : '#1e293b';
  }, [isDark]);

  function toggleTheme() {
    setIsDark(prev => !prev);
  }

  const theme = {
    isDark,
    toggleTheme,
    colors: isDark ? {
      bg: '#0f172a',
      bgCard: '#1e293b',
      bgSidebar: '#1e293b',
      text: '#e2e8f0',
      textSecondary: '#94a3b8',
      border: '#334155',
      primary: '#3b82f6',
      hover: '#334155'
    } : {
      bg: '#f8fafc',
      bgCard: '#ffffff',
      bgSidebar: '#ffffff',
      text: '#1e293b',
      textSecondary: '#64748b',
      border: '#e2e8f0',
      primary: '#2563eb',
      hover: '#f1f5f9'
    }
  };

  return (
    <ThemeContext.Provider value={theme}>
      {children}
    </ThemeContext.Provider>
  );
}

export function useTheme() {
  return useContext(ThemeContext);
}

Schritt 3: Auth-Context

// src/context/AuthContext.jsx
import { createContext, useContext, useState } from 'react';

const AuthContext = createContext();

export function AuthProvider({ children }) {
  const [user, setUser] = useState(() => {
    const saved = localStorage.getItem('user');
    return saved ? JSON.parse(saved) : null;
  });

  function login(email, password) {
    if (email === 'admin@demo.com' && password === 'demo123') {
      const userData = { id: 1, name: 'Admin User', email, role: 'admin' };
      setUser(userData);
      localStorage.setItem('user', JSON.stringify(userData));
      return { success: true };
    }
    return { success: false, error: 'Ungueltige Zugangsdaten' };
  }

  function logout() {
    setUser(null);
    localStorage.removeItem('user');
  }

  return (
    <AuthContext.Provider value={{ user, isLoggedIn: !!user, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

export function useAuth() {
  return useContext(AuthContext);
}

Schritt 4: ProtectedRoute

// src/components/ProtectedRoute.jsx
import { Navigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';

function ProtectedRoute({ children }) {
  const { isLoggedIn } = useAuth();

  if (!isLoggedIn) {
    return <Navigate to="/login" replace />;
  }

  return children;
}

export default ProtectedRoute;

Schritt 5: Sidebar-Navigation

// src/components/Sidebar.jsx
import { NavLink } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
import { useTheme } from '../context/ThemeContext';

function Sidebar() {
  const { user, logout } = useAuth();
  const { colors, isDark, toggleTheme } = useTheme();

  const navItems = [
    { to: '/dashboard', label: 'Dashboard', icon: '📊' },
    { to: '/users', label: 'Benutzer', icon: '👥' },
    { to: '/settings', label: 'Einstellungen', icon: '⚙️' }
  ];

  return (
    <aside style={{
      width: '250px',
      backgroundColor: colors.bgSidebar,
      borderRight: `1px solid ${colors.border}`,
      display: 'flex',
      flexDirection: 'column',
      height: '100vh',
      position: 'sticky',
      top: 0
    }}>
      <div style={{ padding: '1.5rem', borderBottom: `1px solid ${colors.border}` }}>
        <h2 style={{ fontSize: '1.25rem', color: colors.primary }}>
          Dashboard
        </h2>
      </div>

      <nav style={{ flex: 1, padding: '1rem 0' }}>
        {navItems.map(item => (
          <NavLink
            key={item.to}
            to={item.to}
            style={({ isActive }) => ({
              display: 'flex',
              alignItems: 'center',
              gap: '0.75rem',
              padding: '0.75rem 1.5rem',
              textDecoration: 'none',
              color: isActive ? colors.primary : colors.text,
              backgroundColor: isActive ? (isDark ? '#1e3a5f' : '#eff6ff') : 'transparent',
              borderRight: isActive ? `3px solid ${colors.primary}` : '3px solid transparent',
              fontWeight: isActive ? 600 : 400,
              transition: 'all 0.15s'
            })}
          >
            <span>{item.icon}</span>
            {item.label}
          </NavLink>
        ))}
      </nav>

      <div style={{ padding: '1rem 1.5rem', borderTop: `1px solid ${colors.border}` }}>
        <button
          onClick={toggleTheme}
          style={{
            width: '100%',
            padding: '8px',
            marginBottom: '0.5rem',
            backgroundColor: 'transparent',
            border: `1px solid ${colors.border}`,
            borderRadius: '6px',
            color: colors.text,
            cursor: 'pointer'
          }}
        >
          {isDark ? 'Light Mode' : 'Dark Mode'}
        </button>
        <div style={{
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'space-between',
          marginTop: '0.5rem'
        }}>
          <div>
            <p style={{ fontWeight: 600, fontSize: '0.9rem' }}>{user?.name}</p>
            <p style={{ color: colors.textSecondary, fontSize: '0.8rem' }}>{user?.role}</p>
          </div>
          <button onClick={logout} style={{
            padding: '4px 10px',
            border: `1px solid ${colors.border}`,
            borderRadius: '4px',
            backgroundColor: 'transparent',
            color: colors.textSecondary,
            cursor: 'pointer',
            fontSize: '0.8rem'
          }}>
            Logout
          </button>
        </div>
      </div>
    </aside>
  );
}

export default Sidebar;

Schritt 6: StatsCard-Komponente

// src/components/StatsCard.jsx
import { useTheme } from '../context/ThemeContext';

function StatsCard({ title, value, change, icon }) {
  const { colors } = useTheme();
  const isPositive = change >= 0;

  return (
    <div style={{
      backgroundColor: colors.bgCard,
      border: `1px solid ${colors.border}`,
      borderRadius: '12px',
      padding: '1.5rem'
    }}>
      <div style={{
        display: 'flex',
        justifyContent: 'space-between',
        alignItems: 'flex-start'
      }}>
        <div>
          <p style={{
            color: colors.textSecondary,
            fontSize: '0.85rem',
            marginBottom: '0.5rem'
          }}>
            {title}
          </p>
          <p style={{ fontSize: '1.75rem', fontWeight: 700 }}>{value}</p>
        </div>
        <span style={{ fontSize: '1.5rem' }}>{icon}</span>
      </div>
      {change !== undefined && (
        <p style={{
          marginTop: '0.75rem',
          fontSize: '0.85rem',
          color: isPositive ? '#16a34a' : '#dc2626'
        }}>
          {isPositive ? '+' : ''}{change}% gegenueber Vormonat
        </p>
      )}
    </div>
  );
}

export default StatsCard;

Schritt 7: DataTable-Komponente

// src/components/DataTable.jsx
import { useState, useMemo } from 'react';
import { useTheme } from '../context/ThemeContext';

function DataTable({ data, columns, onRowClick }) {
  const { colors } = useTheme();
  const [sortField, setSortField] = useState(null);
  const [sortOrder, setSortOrder] = useState('asc');
  const [search, setSearch] = useState('');
  const [currentPage, setCurrentPage] = useState(1);
  const rowsPerPage = 5;

  function handleSort(field) {
    if (sortField === field) {
      setSortOrder(prev => prev === 'asc' ? 'desc' : 'asc');
    } else {
      setSortField(field);
      setSortOrder('asc');
    }
  }

  const filteredData = useMemo(() => {
    let result = data.filter(row =>
      columns.some(col =>
        String(row[col.key]).toLowerCase().includes(search.toLowerCase())
      )
    );

    if (sortField) {
      result = [...result].sort((a, b) => {
        const valA = a[sortField];
        const valB = b[sortField];
        const comparison = typeof valA === 'string'
          ? valA.localeCompare(valB)
          : valA - valB;
        return sortOrder === 'asc' ? comparison : -comparison;
      });
    }

    return result;
  }, [data, search, sortField, sortOrder, columns]);

  const totalPages = Math.ceil(filteredData.length / rowsPerPage);
  const paginatedData = filteredData.slice(
    (currentPage - 1) * rowsPerPage,
    currentPage * rowsPerPage
  );

  return (
    <div>
      <input
        value={search}
        onChange={(e) => { setSearch(e.target.value); setCurrentPage(1); }}
        placeholder="Suchen..."
        style={{
          padding: '8px 12px',
          border: `1px solid ${colors.border}`,
          borderRadius: '6px',
          marginBottom: '1rem',
          width: '300px',
          backgroundColor: colors.bgCard,
          color: colors.text
        }}
      />

      <div style={{
        border: `1px solid ${colors.border}`,
        borderRadius: '8px',
        overflow: 'hidden'
      }}>
        <table style={{ width: '100%', borderCollapse: 'collapse' }}>
          <thead>
            <tr style={{ backgroundColor: colors.hover }}>
              {columns.map(col => (
                <th
                  key={col.key}
                  onClick={() => handleSort(col.key)}
                  style={{
                    padding: '12px 16px',
                    textAlign: 'left',
                    cursor: 'pointer',
                    fontWeight: 600,
                    fontSize: '0.85rem',
                    color: colors.textSecondary,
                    userSelect: 'none',
                    borderBottom: `1px solid ${colors.border}`
                  }}
                >
                  {col.label}
                  {sortField === col.key && (sortOrder === 'asc' ? ' ▲' : ' ▼')}
                </th>
              ))}
            </tr>
          </thead>
          <tbody>
            {paginatedData.map(row => (
              <tr
                key={row.id}
                onClick={() => onRowClick?.(row)}
                style={{
                  cursor: onRowClick ? 'pointer' : 'default',
                  borderBottom: `1px solid ${colors.border}`,
                  transition: 'background-color 0.1s'
                }}
                onMouseEnter={(e) => e.currentTarget.style.backgroundColor = colors.hover}
                onMouseLeave={(e) => e.currentTarget.style.backgroundColor = 'transparent'}
              >
                {columns.map(col => (
                  <td key={col.key} style={{
                    padding: '12px 16px',
                    fontSize: '0.9rem'
                  }}>
                    {col.render ? col.render(row[col.key], row) : row[col.key]}
                  </td>
                ))}
              </tr>
            ))}
          </tbody>
        </table>
      </div>

      {/* Pagination */}
      <div style={{
        display: 'flex',
        justifyContent: 'space-between',
        alignItems: 'center',
        marginTop: '1rem',
        fontSize: '0.85rem',
        color: colors.textSecondary
      }}>
        <span>
          Zeige {((currentPage - 1) * rowsPerPage) + 1}-
          {Math.min(currentPage * rowsPerPage, filteredData.length)} von {filteredData.length}
        </span>
        <div style={{ display: 'flex', gap: '0.5rem' }}>
          <button
            onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
            disabled={currentPage === 1}
            style={{
              padding: '6px 12px',
              border: `1px solid ${colors.border}`,
              borderRadius: '4px',
              backgroundColor: colors.bgCard,
              color: colors.text,
              cursor: currentPage === 1 ? 'not-allowed' : 'pointer',
              opacity: currentPage === 1 ? 0.5 : 1
            }}
          >
            Zurueck
          </button>
          <button
            onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
            disabled={currentPage === totalPages}
            style={{
              padding: '6px 12px',
              border: `1px solid ${colors.border}`,
              borderRadius: '4px',
              backgroundColor: colors.bgCard,
              color: colors.text,
              cursor: currentPage === totalPages ? 'not-allowed' : 'pointer',
              opacity: currentPage === totalPages ? 0.5 : 1
            }}
          >
            Weiter
          </button>
        </div>
      </div>
    </div>
  );
}

export default DataTable;

Schritt 8: Login-Seite

// src/pages/Login.jsx
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';

function Login() {
  const [email, setEmail] = useState('admin@demo.com');
  const [password, setPassword] = useState('demo123');
  const [error, setError] = useState('');
  const { login } = useAuth();
  const navigate = useNavigate();

  function handleSubmit(e) {
    e.preventDefault();
    const result = login(email, password);
    if (result.success) {
      navigate('/dashboard');
    } else {
      setError(result.error);
    }
  }

  return (
    <div style={{
      minHeight: '100vh',
      display: 'flex',
      alignItems: 'center',
      justifyContent: 'center',
      backgroundColor: '#f1f5f9'
    }}>
      <div style={{
        backgroundColor: 'white',
        padding: '2.5rem',
        borderRadius: '12px',
        boxShadow: '0 4px 6px rgba(0,0,0,0.07)',
        width: '100%',
        maxWidth: '400px'
      }}>
        <h1 style={{ textAlign: 'center', marginBottom: '0.5rem' }}>Dashboard Login</h1>
        <p style={{ textAlign: 'center', color: '#64748b', marginBottom: '1.5rem' }}>
          Demo: admin@demo.com / demo123
        </p>

        {error && (
          <div style={{
            padding: '0.75rem',
            backgroundColor: '#fef2f2',
            color: '#dc2626',
            borderRadius: '6px',
            marginBottom: '1rem',
            fontSize: '0.9rem'
          }}>
            {error}
          </div>
        )}

        <form onSubmit={handleSubmit}>
          <div style={{ marginBottom: '1rem' }}>
            <label style={{ display: 'block', marginBottom: '0.25rem', fontWeight: 500 }}>E-Mail</label>
            <input type="email" value={email} onChange={(e) => setEmail(e.target.value)}
              style={{ width: '100%', padding: '10px', border: '1px solid #e2e8f0',
                borderRadius: '6px', fontSize: '1rem' }} />
          </div>
          <div style={{ marginBottom: '1.5rem' }}>
            <label style={{ display: 'block', marginBottom: '0.25rem', fontWeight: 500 }}>Passwort</label>
            <input type="password" value={password} onChange={(e) => setPassword(e.target.value)}
              style={{ width: '100%', padding: '10px', border: '1px solid #e2e8f0',
                borderRadius: '6px', fontSize: '1rem' }} />
          </div>
          <button type="submit" style={{
            width: '100%', padding: '12px', backgroundColor: '#2563eb',
            color: 'white', border: 'none', borderRadius: '6px',
            fontSize: '1rem', fontWeight: 600, cursor: 'pointer'
          }}>
            Anmelden
          </button>
        </form>
      </div>
    </div>
  );
}

export default Login;

Schritt 9: Dashboard-Seite

// src/pages/Dashboard.jsx
import { mockStats } from '../data/mockData';
import StatsCard from '../components/StatsCard';
import { useTheme } from '../context/ThemeContext';

function Dashboard() {
  const { colors } = useTheme();

  return (
    <div>
      <h1 style={{ marginBottom: '0.5rem' }}>Dashboard</h1>
      <p style={{ color: colors.textSecondary, marginBottom: '2rem' }}>
        Willkommen zurueck! Hier ist deine Uebersicht.
      </p>

      <div style={{
        display: 'grid',
        gridTemplateColumns: 'repeat(auto-fit, minmax(240px, 1fr))',
        gap: '1.5rem',
        marginBottom: '2rem'
      }}>
        <StatsCard
          title="Benutzer gesamt"
          value={mockStats.totalUsers.toLocaleString('de-DE')}
          change={mockStats.growth}
          icon="👥"
        />
        <StatsCard
          title="Aktive Benutzer"
          value={mockStats.activeUsers.toLocaleString('de-DE')}
          change={8.3}
          icon="✓"
        />
        <StatsCard
          title="Umsatz"
          value={`${mockStats.revenue.toLocaleString('de-DE')} €`}
          change={mockStats.growth}
          icon="💰"
        />
        <StatsCard
          title="Bestellungen"
          value={mockStats.orders.toLocaleString('de-DE')}
          change={-2.4}
          icon="📦"
        />
      </div>

      {/* Aktivitaets-Uebersicht */}
      <div style={{
        backgroundColor: colors.bgCard,
        border: `1px solid ${colors.border}`,
        borderRadius: '12px',
        padding: '1.5rem'
      }}>
        <h2 style={{ marginBottom: '1rem', fontSize: '1.25rem' }}>
          Letzte Aktivitaeten
        </h2>
        {[
          { action: 'Neuer Benutzer registriert', user: 'Maria Wagner', time: 'vor 5 Min.' },
          { action: 'Bestellung aufgegeben', user: 'Jan Koch', time: 'vor 12 Min.' },
          { action: 'Profil aktualisiert', user: 'Anna Schmidt', time: 'vor 25 Min.' },
          { action: 'Support-Ticket erstellt', user: 'Tom Weber', time: 'vor 1 Std.' },
          { action: 'Neue Bewertung geschrieben', user: 'Paul Richter', time: 'vor 2 Std.' }
        ].map((activity, index) => (
          <div key={index} style={{
            display: 'flex',
            justifyContent: 'space-between',
            alignItems: 'center',
            padding: '0.75rem 0',
            borderBottom: index < 4 ? `1px solid ${colors.border}` : 'none'
          }}>
            <div>
              <p style={{ fontWeight: 500 }}>{activity.action}</p>
              <p style={{ fontSize: '0.85rem', color: colors.textSecondary }}>{activity.user}</p>
            </div>
            <span style={{ fontSize: '0.8rem', color: colors.textSecondary }}>{activity.time}</span>
          </div>
        ))}
      </div>
    </div>
  );
}

export default Dashboard;

Schritt 10: Benutzer-Seite mit DataTable

// src/pages/Users.jsx
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { mockUsers } from '../data/mockData';
import DataTable from '../components/DataTable';
import { useTheme } from '../context/ThemeContext';

function Users() {
  const { colors } = useTheme();
  const navigate = useNavigate();
  const [users] = useState(mockUsers);

  const columns = [
    { key: 'name', label: 'Name' },
    { key: 'email', label: 'E-Mail' },
    { key: 'role', label: 'Rolle',
      render: (value) => (
        <span style={{
          padding: '2px 10px',
          borderRadius: '12px',
          fontSize: '0.8rem',
          fontWeight: 500,
          backgroundColor: value === 'Admin' ? '#dbeafe' :
                          value === 'Editor' ? '#dcfce7' : '#f3f4f6',
          color: value === 'Admin' ? '#1d4ed8' :
                 value === 'Editor' ? '#16a34a' : '#4b5563'
        }}>
          {value}
        </span>
      )
    },
    { key: 'status', label: 'Status',
      render: (value) => (
        <span style={{
          display: 'inline-flex',
          alignItems: 'center',
          gap: '0.25rem',
          fontSize: '0.85rem'
        }}>
          <span style={{
            width: '8px',
            height: '8px',
            borderRadius: '50%',
            backgroundColor: value === 'active' ? '#16a34a' : '#94a3b8'
          }} />
          {value === 'active' ? 'Aktiv' : 'Inaktiv'}
        </span>
      )
    },
    { key: 'joined', label: 'Beigetreten' }
  ];

  return (
    <div>
      <div style={{
        display: 'flex',
        justifyContent: 'space-between',
        alignItems: 'center',
        marginBottom: '2rem'
      }}>
        <div>
          <h1>Benutzer</h1>
          <p style={{ color: colors.textSecondary }}>{users.length} Benutzer gesamt</p>
        </div>
        <button style={{
          padding: '10px 20px',
          backgroundColor: colors.primary,
          color: 'white',
          border: 'none',
          borderRadius: '6px',
          cursor: 'pointer',
          fontWeight: 600
        }}>
          Neuer Benutzer
        </button>
      </div>

      <DataTable
        data={users}
        columns={columns}
        onRowClick={(user) => navigate(`/users/${user.id}`)}
      />
    </div>
  );
}

export default Users;

Schritt 11: App zusammensetzen

// src/App.jsx
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { AuthProvider } from './context/AuthContext';
import { ThemeProvider, useTheme } from './context/ThemeContext';
import ProtectedRoute from './components/ProtectedRoute';
import Sidebar from './components/Sidebar';
import Login from './pages/Login';
import Dashboard from './pages/Dashboard';
import Users from './pages/Users';

function AppLayout() {
  const { colors } = useTheme();

  return (
    <div style={{ display: 'flex', minHeight: '100vh' }}>
      <Sidebar />
      <main style={{
        flex: 1,
        padding: '2rem',
        backgroundColor: colors.bg,
        overflow: 'auto'
      }}>
        <Routes>
          <Route path="/dashboard" element={<Dashboard />} />
          <Route path="/users" element={<Users />} />
          <Route path="/settings" element={<div><h1>Einstellungen</h1><p>Kommt bald...</p></div>} />
          <Route path="*" element={<Navigate to="/dashboard" replace />} />
        </Routes>
      </main>
    </div>
  );
}

function App() {
  return (
    <BrowserRouter>
      <ThemeProvider>
        <AuthProvider>
          <Routes>
            <Route path="/login" element={<Login />} />
            <Route path="/*" element={
              <ProtectedRoute>
                <AppLayout />
              </ProtectedRoute>
            } />
          </Routes>
        </AuthProvider>
      </ThemeProvider>
    </BrowserRouter>
  );
}

export default App;

Uebungen zur Erweiterung

  1. Erstelle eine Benutzer-Detailseite mit useParams, die alle Infos eines Users zeigt
  2. Implementiere CRUD-Operationen – Benutzer erstellen, bearbeiten und loeschen
  3. Fuege ein Benachrichtigungs-System hinzu mit einem Notification-Context
  4. Baue eine Einstellungs-Seite mit Profil-Bearbeitung und Theme-Auswahl
  5. Ersetze Mock-Daten durch echte API-Aufrufe mit dem useFetch-Hook
  6. Fuege Charts hinzu mit einer Bibliothek wie recharts

Was kommt als Naechstes?

Herzlichen Glueckwunsch – du hast den React-Kurs abgeschlossen! Du hast jetzt das Wissen, um professionelle React-Anwendungen zu bauen. Moegliche naechste Schritte:

  • TypeScript lernen fuer typsicheres React
  • Next.js fuer Server-Side Rendering
  • TanStack Query fuer professionelles Daten-Management
  • Testing mit Vitest und React Testing Library

Zusammenfassung

  • Ein Dashboard vereint alle gelernten Konzepte: Routing, State, Context, Hooks
  • Authentifizierung mit Context und geschuetzten Routen schuetzt sensible Bereiche
  • DataTable mit Sortierung, Filter und Paginierung ist ein reales Anwendungsmuster
  • Dark Mode zeigt, wie Theme-Systeme mit Context funktionieren
  • Sidebar-Navigation mit React Router erstellt professionelle Layouts
  • Komponentenarchitektur haelt auch komplexe Apps wartbar und uebersichtlich

Pro-Tipp: Dieses Dashboard ist ein hervorragendes Portfolio-Stueck! Erweitere es um echte API-Anbindung, fuege Tests hinzu und deploye es auf Vercel oder Netlify. Recruitern zeigt ein funktionierendes Dashboard-Projekt, dass du reale Anwendungen bauen kannst – das ist mehr wert als zehn Todo-Apps!

Zurück zum React Kurs