Skip to content

Kapitel 14: Projekt - TodoList

In diesem Kapitel bauen wir eine vollständige TodoList-Anwendung.


14.1 Anforderungsanalyse

✅ Funktionen

  1. ✅ Aufgaben hinzufügen
  2. ✅ Aufgaben löschen
  3. ✅ Aufgaben als erledigt markieren
  4. ✅ Aufgaben filtern (alle / aktiv / erledigt)
  5. ✅ LocalStorage-Persistenz

🎨 UI-Struktur

┌─────────────────────────────┐
│        TodoList             │
│  ┌─────────────────────┐  │
│  │  [Input] [Hinzufügen]│  │
│  └─────────────────────┘  │
│  ┌─────────────────────┐  │
│  │ ☑ Aufgabe 1          │  │
│  │ ☑ Aufgabe 2          │  │
│  └─────────────────────┘  │
│  [Alle] [Aktiv] [Erledigt]│
└─────────────────────────────┘

14.2 Komponenten-Struktur

📂 Dateistruktur

src/
├── App.jsx
├── components/
│   ├── TodoHeader.jsx
│   ├── TodoInput.jsx
│   ├── TodoList.jsx
│   ├── TodoItem.jsx
│   └── TodoFooter.jsx

🔗 Komponenten-Hierarchie

App
├── TodoHeader
├── TodoInput
├── TodoList
│   └── TodoItem (mehrere)
└── TodoFooter

14.3 State verwalten

📦 Hauptstate in App.jsx

jsx
function App() {
  const [todos, setTodos] = useState([]);
  const [filter, setFilter] = useState('all'); // 'all', 'active', 'completed'

  // Aufgabe hinzufügen
  const addTodo = (text) => {
    const newTodo = {
      id: Date.now(),
      text: text,
      completed: false
    };
    setTodos([...todos, newTodo]);
  };

  // Aufgabe löschen
  const deleteTodo = (id) => {
    setTodos(todos.filter(todo => todo.id !== id));
  };

  // Aufgabe umschalten (erledigt/aktiv)
  const toggleTodo = (id) => {
    setTodos(
      todos.map(todo =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    );
  };

  // Gefilterte Aufgaben
  const filteredTodos = todos.filter(todo => {
    if (filter === 'active') return !todo.completed;
    if (filter === 'completed') return todo.completed;
    return true; // 'all'
  });

  return (
    <div className="app">
      <TodoHeader />
      <TodoInput onAdd={addTodo} />
      <TodoList 
        todos={filteredTodos} 
        onDelete={deleteTodo} 
        onToggle={toggleTodo} 
      />
      <TodoFooter filter={filter} setFilter={setFilter} />
    </div>
  );
}

export default App;

14.4 Komponenten implementieren

1️⃣ TodoHeader.jsx

jsx
function TodoHeader() {
  return (
    <header className="header">
      <h1>TodoList</h1>
      <p>Verwalten Sie Ihre Aufgaben effizient</p>
    </header>
  );
}

export default TodoHeader;

2️⃣ TodoInput.jsx

jsx
import { useState } from 'react';

function TodoInput({ onAdd }) {
  const [text, setText] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    if (!text.trim()) return;

    onAdd(text);
    setText('');
  };

  return (
    <form className="todo-input" onSubmit={handleSubmit}>
      <input 
        type="text" 
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="Neue Aufgabe..."
      />
      <button type="submit">Hinzufügen</button>
    </form>
  );
}

export default TodoInput;

3️⃣ TodoList.jsx & TodoItem.jsx

TodoList.jsx:

jsx
function TodoList({ todos, onDelete, onToggle }) {
  if (todos.length === 0) {
    return <p className="empty">Keine Aufgaben vorhanden</p>;
  }

  return (
    <ul className="todo-list">
      {todos.map(todo => (
        <TodoItem 
          key={todo.id}
          todo={todo}
          onDelete={onDelete}
          onToggle={onToggle}
        />
      ))}
    </ul>
  );
}

export default TodoList;

TodoItem.jsx:

jsx
function TodoItem({ todo, onDelete, onToggle }) {
  return (
    <li className={`todo-item ${todo.completed ? 'completed' : ''}`}>
      <input 
        type="checkbox"
        checked={todo.completed}
        onChange={() => onToggle(todo.id)}
      />
      <span>{todo.text}</span>
      <button onClick={() => onDelete(todo.id)}>Löschen</button>
    </li>
  );
}

export default TodoItem;

4️⃣ TodoFooter.jsx

jsx
function TodoFooter({ filter, setFilter }) {
  return (
    <div className="todo-footer">
      <button 
        className={filter === 'all' ? 'active' : ''}
        onClick={() => setFilter('all')}
      >
        Alle
      </button>
      <button 
        className={filter === 'active' ? 'active' : ''}
        onClick={() => setFilter('active')}
      >
        Aktiv
      </button>
      <button 
        className={filter === 'completed' ? 'active' : ''}
        onClick={() => setFilter('completed')}
      >
        Erledigt
      </button>
    </div>
  );
}

export default TodoFooter;

14.5 LocalStorage-Persistenz

💾 State in LocalStorage speichern

Benutzerdefinierter Hook: useLocalStorage.js

jsx
import { useState, useEffect } from 'react';

function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    const stored = localStorage.getItem(key);
    return stored ? JSON.parse(stored) : initialValue;
  });

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue];
}

export default useLocalStorage;

🔄 In App.jsx verwenden

jsx
import useLocalStorage from './hooks/useLocalStorage';

function App() {
  const [todos, setTodos] = useLocalStorage('todos', []);
  
  // ... restliche Logik bleibt gleich
}

14.6 Stile optimieren

🎨 Einfache CSS-Datei (App.css)

css
.app {
  max-width: 600px;
  margin: 0 auto;
  padding: 20px;
}

.header {
  text-align: center;
  margin-bottom: 30px;
}

.todo-input {
  display: flex;
  gap: 10px;
  margin-bottom: 20px;
}

.todo-input input {
  flex: 1;
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 4px;
}

.todo-input button {
  padding: 10px 20px;
  background-color: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.todo-input button:hover {
  background-color: #0056b3;
}

.todo-list {
  list-style: none;
  padding: 0;
}

.todo-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px;
  border-bottom: 1px solid #eee;
}

.todo-item.completed span {
  text-decoration: line-through;
  color: #999;
}

.todo-item button {
  margin-left: auto;
  padding: 5px 10px;
  background-color: #dc3545;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.todo-footer {
  display: flex;
  justify-content: center;
  gap: 10px;
  margin-top: 20px;
}

.todo-footer button {
  padding: 8px 16px;
  border: 1px solid #ddd;
  background-color: white;
  cursor: pointer;
  border-radius: 4px;
}

.todo-footer button.active {
  background-color: #007bff;
  color: white;
  border-color: #007bff;
}

.empty {
  text-align: center;
  color: #999;
  margin-top: 20px;
}

14.7 Vollständiger Code

Alle Dateien zusammen:

  • App.jsx (Hauptkomponente mit State)
  • components/TodoHeader.jsx
  • components/TodoInput.jsx
  • components/TodoList.jsx
  • components/TodoItem.jsx
  • components/TodoFooter.jsx
  • hooks/useLocalStorage.js
  • App.css (Stile)

📝 Zusammenfassung

In diesem Kapitel haben wir gelernt:

  • ✅ Eine vollständige TodoList-Anwendung zu bauen
  • ✅ Komponenten-Struktur planen
  • ✅ State verwalten (Hinzufügen, Löschen, Umschalten)
  • ✅ LocalStorage-Persistenz implementieren
  • ✅ Einfache Stile hinzufügen

🎯 Nächste Schritte

Im nächsten Kapitel werden wir:

  • Counter-Projekt erstellen (Hooks weiter vertiefen)
  • Benutzerdefinierte Hooks üben
  • Performance-Optimierung lernen

Bereit für das nächste Projekt? → Kapitel 15: Projekt - Counter

Frei für alle Anfänger