Fortgeschritten252025-01-15

TypeScript Final Project - Todo App mit Type Safety

Baue eine vollständige Todo-App mit TypeScript, lokalem Storage, und professionellen Patterns.

#typescript#project#todo-app#practice

TypeScript Final Project - Todo App mit Type Safety

Zeit für ein echtes Projekt! Wir bauen eine vollständige Todo-App mit allem was du gelernt hast.

Projekt-Übersicht

Was wir bauen:

  • ✅ Todo hinzufügen, bearbeiten, löschen
  • ✅ Kategorien und Prioritäten
  • ✅ Filter und Suche
  • ✅ LocalStorage Persistence
  • ✅ 100% Type-Safe

Tech Stack:

  • TypeScript (strict mode)
  • Vanilla JavaScript (DOM)
  • LocalStorage API
  • CSS für Styling

Projekt-Setup

mkdir typescript-todo-app
cd typescript-todo-app
npm init -y
npm install --save-dev typescript
npx tsc --init

tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ES2020",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

Types definieren

// src/types.ts

export enum Priority {
  Low = "low",
  Medium = "medium",
  High = "high"
}

export enum Category {
  Work = "work",
  Personal = "personal",
  Shopping = "shopping",
  Health = "health"
}

export interface Todo {
  id: string
  title: string
  description?: string
  completed: boolean
  priority: Priority
  category: Category
  createdAt: Date
  updatedAt: Date
  dueDate?: Date
}

export interface TodoFilter {
  category?: Category
  priority?: Priority
  completed?: boolean
  searchTerm?: string
}

export type TodoUpdate = Partial<Omit<Todo, "id" | "createdAt">>

Storage Service

// src/storage.ts

import { Todo } from "./types"

const STORAGE_KEY = "typescript-todos"

export class StorageService {
  static saveTodos(todos: Todo[]): void {
    try {
      const serialized = JSON.stringify(todos)
      localStorage.setItem(STORAGE_KEY, serialized)
    } catch (error) {
      console.error("Failed to save todos:", error)
    }
  }

  static loadTodos(): Todo[] {
    try {
      const serialized = localStorage.getItem(STORAGE_KEY)
      if (!serialized) return []

      const parsed = JSON.parse(serialized)

      // Convert date strings back to Date objects
      return parsed.map((todo: any) => ({
        ...todo,
        createdAt: new Date(todo.createdAt),
        updatedAt: new Date(todo.updatedAt),
        dueDate: todo.dueDate ? new Date(todo.dueDate) : undefined
      }))
    } catch (error) {
      console.error("Failed to load todos:", error)
      return []
    }
  }

  static clearTodos(): void {
    localStorage.removeItem(STORAGE_KEY)
  }
}

Todo Manager

// src/todoManager.ts

import { Todo, TodoFilter, TodoUpdate, Priority, Category } from "./types"
import { StorageService } from "./storage"

export class TodoManager {
  private todos: Todo[] = []

  constructor() {
    this.todos = StorageService.loadTodos()
  }

  addTodo(
    title: string,
    priority: Priority = Priority.Medium,
    category: Category = Category.Personal
  ): Todo {
    const todo: Todo = {
      id: this.generateId(),
      title,
      description: undefined,
      completed: false,
      priority,
      category,
      createdAt: new Date(),
      updatedAt: new Date()
    }

    this.todos.push(todo)
    this.save()
    return todo
  }

  updateTodo(id: string, updates: TodoUpdate): Todo | null {
    const todo = this.findById(id)
    if (!todo) return null

    Object.assign(todo, {
      ...updates,
      updatedAt: new Date()
    })

    this.save()
    return todo
  }

  deleteTodo(id: string): boolean {
    const index = this.todos.findIndex(t => t.id === id)
    if (index === -1) return false

    this.todos.splice(index, 1)
    this.save()
    return true
  }

  toggleComplete(id: string): Todo | null {
    const todo = this.findById(id)
    if (!todo) return null

    todo.completed = !todo.completed
    todo.updatedAt = new Date()
    this.save()
    return todo
  }

  getTodos(filter?: TodoFilter): Todo[] {
    let filtered = [...this.todos]

    if (filter) {
      if (filter.category) {
        filtered = filtered.filter(t => t.category === filter.category)
      }

      if (filter.priority) {
        filtered = filtered.filter(t => t.priority === filter.priority)
      }

      if (filter.completed !== undefined) {
        filtered = filtered.filter(t => t.completed === filter.completed)
      }

      if (filter.searchTerm) {
        const term = filter.searchTerm.toLowerCase()
        filtered = filtered.filter(t =>
          t.title.toLowerCase().includes(term) ||
          t.description?.toLowerCase().includes(term)
        )
      }
    }

    return filtered.sort((a, b) =>
      b.createdAt.getTime() - a.createdAt.getTime()
    )
  }

  getStats() {
    return {
      total: this.todos.length,
      completed: this.todos.filter(t => t.completed).length,
      pending: this.todos.filter(t => !t.completed).length,
      byCategory: {
        work: this.todos.filter(t => t.category === Category.Work).length,
        personal: this.todos.filter(t => t.category === Category.Personal).length,
        shopping: this.todos.filter(t => t.category === Category.Shopping).length,
        health: this.todos.filter(t => t.category === Category.Health).length
      }
    }
  }

  private findById(id: string): Todo | undefined {
    return this.todos.find(t => t.id === id)
  }

  private generateId(): string {
    return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
  }

  private save(): void {
    StorageService.saveTodos(this.todos)
  }
}

UI Renderer

// src/ui.ts

import { TodoManager } from "./todoManager"
import { Todo, Priority, Category } from "./types"

export class UIRenderer {
  private todoManager: TodoManager
  private filterCategory?: Category
  private filterCompleted?: boolean

  constructor(todoManager: TodoManager) {
    this.todoManager = todoManager
    this.setupEventListeners()
    this.render()
  }

  private setupEventListeners(): void {
    // Add Todo Form
    const form = document.getElementById("add-todo-form") as HTMLFormElement
    form?.addEventListener("submit", (e) => {
      e.preventDefault()
      this.handleAddTodo()
    })

    // Filter Buttons
    document.querySelectorAll("[data-filter]").forEach(btn => {
      btn.addEventListener("click", () => {
        const filter = btn.getAttribute("data-filter")
        this.handleFilter(filter)
      })
    })
  }

  private handleAddTodo(): void {
    const titleInput = document.getElementById("todo-title") as HTMLInputElement
    const prioritySelect = document.getElementById("todo-priority") as HTMLSelectElement
    const categorySelect = document.getElementById("todo-category") as HTMLSelectElement

    const title = titleInput.value.trim()
    if (!title) return

    this.todoManager.addTodo(
      title,
      prioritySelect.value as Priority,
      categorySelect.value as Category
    )

    titleInput.value = ""
    this.render()
  }

  private handleFilter(filter: string | null): void {
    switch (filter) {
      case "all":
        this.filterCompleted = undefined
        break
      case "active":
        this.filterCompleted = false
        break
      case "completed":
        this.filterCompleted = true
        break
    }
    this.render()
  }

  private render(): void {
    this.renderTodos()
    this.renderStats()
  }

  private renderTodos(): void {
    const container = document.getElementById("todo-list")
    if (!container) return

    const todos = this.todoManager.getTodos({
      category: this.filterCategory,
      completed: this.filterCompleted
    })

    if (todos.length === 0) {
      container.innerHTML = `
        <div class="empty-state">
          <p>Keine Todos gefunden</p>
        </div>
      `
      return
    }

    container.innerHTML = todos.map(todo => this.todoTemplate(todo)).join("")

    // Event Listeners für Todo-Items
    container.querySelectorAll("[data-todo-id]").forEach(el => {
      const id = el.getAttribute("data-todo-id")!

      el.querySelector("[data-action='toggle']")?.addEventListener("click", () => {
        this.todoManager.toggleComplete(id)
        this.render()
      })

      el.querySelector("[data-action='delete']")?.addEventListener("click", () => {
        if (confirm("Todo löschen?")) {
          this.todoManager.deleteTodo(id)
          this.render()
        }
      })
    })
  }

  private todoTemplate(todo: Todo): string {
    const priorityClass = `priority-${todo.priority}`
    const completedClass = todo.completed ? "completed" : ""

    return `
      <div class="todo-item ${priorityClass} ${completedClass}" data-todo-id="${todo.id}">
        <div class="todo-checkbox">
          <input type="checkbox" ${todo.completed ? "checked" : ""} data-action="toggle">
        </div>
        <div class="todo-content">
          <h3>${this.escapeHtml(todo.title)}</h3>
          <div class="todo-meta">
            <span class="category">${todo.category}</span>
            <span class="priority">${todo.priority}</span>
            <span class="date">${this.formatDate(todo.createdAt)}</span>
          </div>
        </div>
        <button class="btn-delete" data-action="delete">×</button>
      </div>
    `
  }

  private renderStats(): void {
    const stats = this.todoManager.getStats()
    const container = document.getElementById("stats")
    if (!container) return

    container.innerHTML = `
      <div class="stat">
        <span class="stat-value">${stats.total}</span>
        <span class="stat-label">Gesamt</span>
      </div>
      <div class="stat">
        <span class="stat-value">${stats.pending}</span>
        <span class="stat-label">Offen</span>
      </div>
      <div class="stat">
        <span class="stat-value">${stats.completed}</span>
        <span class="stat-label">Erledigt</span>
      </div>
    `
  }

  private escapeHtml(text: string): string {
    const div = document.createElement("div")
    div.textContent = text
    return div.innerHTML
  }

  private formatDate(date: Date): string {
    return date.toLocaleDateString("de-DE", {
      day: "2-digit",
      month: "2-digit",
      year: "numeric"
    })
  }
}

Main Entry Point

// src/main.ts

import { TodoManager } from "./todoManager"
import { UIRenderer } from "./ui"

// Initialize App
const todoManager = new TodoManager()
const ui = new UIRenderer(todoManager)

console.log("Todo App started!")

HTML Structure

<!-- index.html -->
<!DOCTYPE html>
<html lang="de">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>TypeScript Todo App</title>
  <link rel="stylesheet" href="styles.css">
</head>
<body>
  <div class="container">
    <header>
      <h1>📝 Todo App</h1>
      <div id="stats"></div>
    </header>

    <main>
      <form id="add-todo-form">
        <input
          type="text"
          id="todo-title"
          placeholder="Neues Todo..."
          required
        >
        <select id="todo-priority">
          <option value="low">Niedrig</option>
          <option value="medium" selected>Mittel</option>
          <option value="high">Hoch</option>
        </select>
        <select id="todo-category">
          <option value="personal">Privat</option>
          <option value="work">Arbeit</option>
          <option value="shopping">Einkauf</option>
          <option value="health">Gesundheit</option>
        </select>
        <button type="submit">Hinzufügen</button>
      </form>

      <div class="filters">
        <button data-filter="all">Alle</button>
        <button data-filter="active">Offen</button>
        <button data-filter="completed">Erledigt</button>
      </div>

      <div id="todo-list"></div>
    </main>
  </div>

  <script type="module" src="dist/main.js"></script>
</body>
</html>

Build & Run

package.json Scripts:

{
  "scripts": {
    "build": "tsc",
    "watch": "tsc --watch",
    "dev": "tsc --watch"
  }
}

Build:

npm run build

Run: Öffne index.html im Browser

🎯

Zusammenfassung

Du hast gebaut:

  • ✅ Type-Safe Todo App
  • ✅ CRUD Operations
  • ✅ LocalStorage Persistence
  • ✅ Filter & Search
  • ✅ Stats Dashboard
  • ✅ Clean Architecture

Verwendete TypeScript Features:

  • Enums für Konstanten
  • Interfaces für Daten-Strukturen
  • Utility Types (Partial, Omit)
  • Type Guards
  • Generics
  • Classes mit private/public

Nächste Schritte:

  • ✨ Drag & Drop für Sortierung
  • ✨ Due Dates mit Notifications
  • ✨ Tags System
  • ✨ Export/Import JSON
  • ✨ Dark Mode
  • ✨ React/Vue Version

Herzlichen Glückwunsch! Du hast eine vollständige TypeScript-App gebaut! 🎉

TypeScriptLektion 13 von 15
87% abgeschlossen
Lektion abgeschlossen!

Gut gemacht! 🎉

Du hast "TypeScript Final Project - Todo App mit Type Safety" abgeschlossen

Artikel bewerten

0.0 (0 Bewertungen)

Bitte einloggen um zu bewerten