diff --git a/README.md b/README.md
index aef515e..4c65441 100644
--- a/README.md
+++ b/README.md
@@ -23,9 +23,21 @@ WebDev-Angular-ToDo-Example/
├── main.ts ← App-Bootstrap (Startpunkt)
├── styles.css ← Globale CSS-Basis-Styles
└── app/
- ├── app.component.ts ← Root-Komponente (UI + Logik)
- ├── todo.model.ts ← TypeScript-Interfaces (Datentypen)
- └── todo.service.ts ← Service für API-Kommunikation
+ ├── app.component.ts / .html / .css
+ ├── core/
+ │ ├── models/
+ │ │ └── todo.model.ts
+ │ └── services/
+ │ └── todo.service.ts
+ └── features/
+ └── todos/
+ └── components/
+ ├── error-message/
+ ├── loading-indicator/
+ ├── todo-form/
+ ├── todo-item/
+ ├── todo-list/
+ └── todo-pagination/
```
## Installation & Start
diff --git a/src/app/app.component.css b/src/app/app.component.css
index 585d441..8b0fa46 100644
--- a/src/app/app.component.css
+++ b/src/app/app.component.css
@@ -20,162 +20,3 @@
font-size: 0.85rem;
margin: 0.25rem 0 0;
}
-
-/* ── Fehlermeldung ───────────────────────────────────── */
-.error-box {
- display: flex;
- justify-content: space-between;
- align-items: center;
- background: #fee2e2;
- border: 1px solid #fca5a5;
- color: #b91c1c;
- padding: 0.75rem 1rem;
- border-radius: 8px;
- margin-bottom: 1rem;
-}
-.close-btn {
- background: none;
- border: none;
- cursor: pointer;
- color: #b91c1c;
- font-size: 1rem;
-}
-
-/* ── Formular ────────────────────────────────────────── */
-.add-form {
- display: flex;
- gap: 0.5rem;
- margin-bottom: 1.5rem;
-}
-.add-form input {
- flex: 1;
- padding: 0.6rem 0.8rem;
- border: 1px solid #d1d5db;
- border-radius: 8px;
- font-size: 1rem;
- outline: none;
- transition: border-color 0.2s;
-}
-.add-form input:focus {
- border-color: #2563eb;
-}
-.add-form button {
- padding: 0.6rem 1.2rem;
- background: #2563eb;
- color: #fff;
- border: none;
- border-radius: 8px;
- font-size: 1rem;
- cursor: pointer;
- white-space: nowrap;
- transition: background 0.2s;
-}
-.add-form button:hover:not(:disabled) {
- background: #1d4ed8;
-}
-.add-form button:disabled {
- opacity: 0.5;
- cursor: default;
-}
-
-/* ── Ladeindikator ───────────────────────────────────── */
-.loading {
- text-align: center;
- color: #6b7280;
- padding: 2rem;
-}
-
-/* ── ToDo-Liste ──────────────────────────────────────── */
-.todo-list {
- list-style: none;
- margin: 0;
- padding: 0;
- display: flex;
- flex-direction: column;
- gap: 0.5rem;
-}
-.empty-state {
- text-align: center;
- color: #9ca3af;
- padding: 2rem;
-}
-.todo-item {
- display: flex;
- align-items: center;
- gap: 0.75rem;
- padding: 0.75rem 1rem;
- background: #fff;
- border: 1px solid #e5e7eb;
- border-radius: 8px;
- transition: background 0.15s;
-}
-.todo-item:hover {
- background: #f9fafb;
-}
-
-/* Abgehaktes ToDo: Text wird durchgestrichen und ausgegraut */
-.todo-item.completed .todo-title {
- text-decoration: line-through;
- color: #9ca3af;
-}
-
-.todo-item input[type="checkbox"] {
- width: 1.1rem;
- height: 1.1rem;
- cursor: pointer;
- flex-shrink: 0;
-}
-.todo-title {
- flex: 1;
- cursor: pointer;
-}
-.user-badge {
- font-size: 0.7rem;
- color: #6b7280;
- background: #f3f4f6;
- border-radius: 99px;
- padding: 0.15rem 0.5rem;
- white-space: nowrap;
-}
-.delete-btn {
- background: none;
- border: none;
- color: #d1d5db;
- font-size: 1rem;
- cursor: pointer;
- padding: 0.25rem;
- line-height: 1;
- border-radius: 4px;
- transition: color 0.15s;
-}
-.delete-btn:hover {
- color: #ef4444;
-}
-
-/* ── Pagination ──────────────────────────────────────── */
-.pagination {
- display: flex;
- justify-content: center;
- align-items: center;
- gap: 1rem;
- margin-top: 1.5rem;
-}
-.pagination button {
- padding: 0.5rem 1rem;
- border: 1px solid #d1d5db;
- background: #fff;
- border-radius: 8px;
- cursor: pointer;
- transition: background 0.15s;
-}
-.pagination button:hover:not(:disabled) {
- background: #f3f4f6;
-}
-.pagination button:disabled {
- opacity: 0.4;
- cursor: default;
-}
-.page-info {
- color: #6b7280;
- font-size: 0.9rem;
-}
diff --git a/src/app/app.component.html b/src/app/app.component.html
index 2f530b8..67398f5 100644
--- a/src/app/app.component.html
+++ b/src/app/app.component.html
@@ -5,105 +5,35 @@
Angular 21 · Vite · REST-API
-
-
- Fehler: {{ error }}
- ✕
-
+
-
-
+
-
- Daten werden geladen…
+
-
-
-
-
-
+ 1"
+ [canGoNext]="todos.length === pageSize"
+ (previous)="prevPage()"
+ (next)="nextPage()"
+ />
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index 07b5cf8..61f56be 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -11,9 +11,13 @@
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common'; // *ngIf, *ngFor, etc.
-import { FormsModule } from '@angular/forms'; // [(ngModel)] Two-Way-Binding
-import { TodoService } from './todo.service';
-import { Todo } from './todo.model';
+import { Todo } from './core/models/todo.model';
+import { TodoService } from './core/services/todo.service';
+import { ErrorMessageComponent } from './features/todos/components/error-message/error-message.component';
+import { LoadingIndicatorComponent } from './features/todos/components/loading-indicator/loading-indicator.component';
+import { TodoFormComponent } from './features/todos/components/todo-form/todo-form.component';
+import { TodoListComponent } from './features/todos/components/todo-list/todo-list.component';
+import { TodoPaginationComponent } from './features/todos/components/todo-pagination/todo-pagination.component';
@Component({
// selector: Wie diese Komponente im HTML eingebunden wird ()
@@ -23,7 +27,14 @@ import { Todo } from './todo.model';
standalone: true,
// imports: Andere Angular-Module/Komponenten, die im Template verwendet werden
- imports: [CommonModule, FormsModule],
+ imports: [
+ CommonModule,
+ ErrorMessageComponent,
+ TodoFormComponent,
+ LoadingIndicatorComponent,
+ TodoListComponent,
+ TodoPaginationComponent,
+ ],
// Template und Styles sind ausgelagert für bessere Wartbarkeit
templateUrl: './app.component.html',
diff --git a/src/app/core/models/todo.model.ts b/src/app/core/models/todo.model.ts
new file mode 100644
index 0000000..d5cecea
--- /dev/null
+++ b/src/app/core/models/todo.model.ts
@@ -0,0 +1,14 @@
+// todo.model.ts – TypeScript-Interfaces für die ToDo-Datenstrukturen
+
+export interface Todo {
+ id: number;
+ userId: number;
+ title: string;
+ completed: boolean;
+}
+
+export interface TodoCreate {
+ userId: number;
+ title: string;
+ completed?: boolean;
+}
diff --git a/src/app/todo.service.ts b/src/app/core/services/todo.service.ts
similarity index 66%
rename from src/app/todo.service.ts
rename to src/app/core/services/todo.service.ts
index 051616e..20a6f9b 100644
--- a/src/app/todo.service.ts
+++ b/src/app/core/services/todo.service.ts
@@ -1,25 +1,11 @@
-// todo.service.ts – Service für die Kommunikation mit der REST-API
-//
-// Ein Angular-Service ist eine Klasse, die wiederverwendbare Logik enthält.
-// @Injectable({ providedIn: 'root' }) registriert den Service als Singleton
-// im gesamten App-Kontext (nur eine Instanz für die ganze Anwendung).
-//
-// API-Dokumentation (OpenAPI): https://webdev.iten-web.ch/10003/api/
-//
-// Für diese App verwenden wir REST-Pfade im Best-Practice-Stil:
-// Basis-URL: https://webdev.iten-web.ch/10003/api
-// Beispiel: https://webdev.iten-web.ch/10003/api/todos?page=1&limit=10
-
import { Injectable } from '@angular/core';
-import { Todo, TodoCreate } from './todo.model';
+import { Todo, TodoCreate } from '../models/todo.model';
-// Basis-URL der API.
const API_BASE = 'https://webdev.iten-web.ch/10003/api';
const REQUEST_TIMEOUT_MS = 10000;
@Injectable({ providedIn: 'root' })
export class TodoService {
-
private buildUrl(path: string, query: Record = {}): string {
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
const url = new URL(`${API_BASE}${normalizedPath}`);
@@ -54,8 +40,6 @@ export class TodoService {
return response.json() as Promise;
})();
- // Promise.race garantiert, dass der Aufruf auch dann endet,
- // wenn fetch in seltenen Browser-/Extension-Faellen hängen bleibt.
const timeoutPromise = new Promise((_, reject) => {
timeoutId = window.setTimeout(() => {
controller.abort();
@@ -76,18 +60,10 @@ export class TodoService {
}
}
- /**
- * Alle ToDos abrufen – GET /todos
- * Unterstützt serverseitige Pagination (Seite + Einträge pro Seite).
- */
async getTodos(page = 1, limit = 10): Promise {
return this.request('/todos', undefined, { page, limit });
}
- /**
- * Neues ToDo anlegen – POST /todos
- * Der Request-Body enthält userId und title (completed ist optional).
- */
async createTodo(data: TodoCreate): Promise {
return this.request('/todos', {
method: 'POST',
@@ -96,11 +72,6 @@ export class TodoService {
});
}
- /**
- * ToDo teilweise aktualisieren – PATCH /todos/:id
- * Nur die angegebenen Felder werden geändert (z. B. nur "completed").
- * Im Gegensatz zu PUT wird das Objekt nicht vollständig ersetzt.
- */
async patchTodo(id: number, changes: Partial): Promise {
return this.request(`/todos/${id}`, {
method: 'PATCH',
@@ -109,10 +80,6 @@ export class TodoService {
});
}
- /**
- * ToDo löschen – DELETE /todos/:id
- * Die API antwortet mit HTTP 204 (No Content) – kein Body.
- */
async deleteTodo(id: number): Promise {
await this.request(`/todos/${id}`, {
method: 'DELETE',
diff --git a/src/app/features/todos/components/error-message/error-message.component.css b/src/app/features/todos/components/error-message/error-message.component.css
new file mode 100644
index 0000000..b069ff5
--- /dev/null
+++ b/src/app/features/todos/components/error-message/error-message.component.css
@@ -0,0 +1,19 @@
+.error-box {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ background: #fee2e2;
+ border: 1px solid #fca5a5;
+ color: #b91c1c;
+ padding: 0.75rem 1rem;
+ border-radius: 8px;
+ margin-bottom: 1rem;
+}
+
+.close-btn {
+ background: none;
+ border: none;
+ cursor: pointer;
+ color: #b91c1c;
+ font-size: 1rem;
+}
diff --git a/src/app/features/todos/components/error-message/error-message.component.html b/src/app/features/todos/components/error-message/error-message.component.html
new file mode 100644
index 0000000..706f78a
--- /dev/null
+++ b/src/app/features/todos/components/error-message/error-message.component.html
@@ -0,0 +1,4 @@
+
+ Fehler: {{ message }}
+ X
+
diff --git a/src/app/features/todos/components/error-message/error-message.component.ts b/src/app/features/todos/components/error-message/error-message.component.ts
new file mode 100644
index 0000000..7176230
--- /dev/null
+++ b/src/app/features/todos/components/error-message/error-message.component.ts
@@ -0,0 +1,12 @@
+import { Component, EventEmitter, Input, Output } from '@angular/core';
+
+@Component({
+ selector: 'app-error-message',
+ standalone: true,
+ templateUrl: './error-message.component.html',
+ styleUrl: './error-message.component.css',
+})
+export class ErrorMessageComponent {
+ @Input({ required: true }) message = '';
+ @Output() dismissed = new EventEmitter();
+}
diff --git a/src/app/features/todos/components/loading-indicator/loading-indicator.component.css b/src/app/features/todos/components/loading-indicator/loading-indicator.component.css
new file mode 100644
index 0000000..df00efb
--- /dev/null
+++ b/src/app/features/todos/components/loading-indicator/loading-indicator.component.css
@@ -0,0 +1,5 @@
+.loading {
+ text-align: center;
+ color: #6b7280;
+ padding: 2rem;
+}
diff --git a/src/app/features/todos/components/loading-indicator/loading-indicator.component.html b/src/app/features/todos/components/loading-indicator/loading-indicator.component.html
new file mode 100644
index 0000000..7ca7961
--- /dev/null
+++ b/src/app/features/todos/components/loading-indicator/loading-indicator.component.html
@@ -0,0 +1 @@
+{{ text }}
diff --git a/src/app/features/todos/components/loading-indicator/loading-indicator.component.ts b/src/app/features/todos/components/loading-indicator/loading-indicator.component.ts
new file mode 100644
index 0000000..b1efaaf
--- /dev/null
+++ b/src/app/features/todos/components/loading-indicator/loading-indicator.component.ts
@@ -0,0 +1,11 @@
+import { Component, Input } from '@angular/core';
+
+@Component({
+ selector: 'app-loading-indicator',
+ standalone: true,
+ templateUrl: './loading-indicator.component.html',
+ styleUrl: './loading-indicator.component.css',
+})
+export class LoadingIndicatorComponent {
+ @Input() text = 'Daten werden geladen...';
+}
diff --git a/src/app/features/todos/components/todo-form/todo-form.component.css b/src/app/features/todos/components/todo-form/todo-form.component.css
new file mode 100644
index 0000000..ce3ca98
--- /dev/null
+++ b/src/app/features/todos/components/todo-form/todo-form.component.css
@@ -0,0 +1,40 @@
+.add-form {
+ display: flex;
+ gap: 0.5rem;
+ margin-bottom: 1.5rem;
+}
+
+.add-form input {
+ flex: 1;
+ padding: 0.6rem 0.8rem;
+ border: 1px solid #d1d5db;
+ border-radius: 8px;
+ font-size: 1rem;
+ outline: none;
+ transition: border-color 0.2s;
+}
+
+.add-form input:focus {
+ border-color: #2563eb;
+}
+
+.add-form button {
+ padding: 0.6rem 1.2rem;
+ background: #2563eb;
+ color: #fff;
+ border: none;
+ border-radius: 8px;
+ font-size: 1rem;
+ cursor: pointer;
+ white-space: nowrap;
+ transition: background 0.2s;
+}
+
+.add-form button:hover:not(:disabled) {
+ background: #1d4ed8;
+}
+
+.add-form button:disabled {
+ opacity: 0.5;
+ cursor: default;
+}
diff --git a/src/app/features/todos/components/todo-form/todo-form.component.html b/src/app/features/todos/components/todo-form/todo-form.component.html
new file mode 100644
index 0000000..43669c6
--- /dev/null
+++ b/src/app/features/todos/components/todo-form/todo-form.component.html
@@ -0,0 +1,13 @@
+
diff --git a/src/app/features/todos/components/todo-form/todo-form.component.ts b/src/app/features/todos/components/todo-form/todo-form.component.ts
new file mode 100644
index 0000000..61b107f
--- /dev/null
+++ b/src/app/features/todos/components/todo-form/todo-form.component.ts
@@ -0,0 +1,21 @@
+import { Component, EventEmitter, Input, Output } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+
+@Component({
+ selector: 'app-todo-form',
+ standalone: true,
+ imports: [FormsModule],
+ templateUrl: './todo-form.component.html',
+ styleUrl: './todo-form.component.css',
+})
+export class TodoFormComponent {
+ @Input() model = '';
+ @Input() saving = false;
+
+ @Output() modelChange = new EventEmitter();
+ @Output() submitted = new EventEmitter();
+
+ onSubmit(): void {
+ this.submitted.emit();
+ }
+}
diff --git a/src/app/features/todos/components/todo-item/todo-item.component.css b/src/app/features/todos/components/todo-item/todo-item.component.css
new file mode 100644
index 0000000..6ce472c
--- /dev/null
+++ b/src/app/features/todos/components/todo-item/todo-item.component.css
@@ -0,0 +1,56 @@
+.todo-item {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ padding: 0.75rem 1rem;
+ background: #fff;
+ border: 1px solid #e5e7eb;
+ border-radius: 8px;
+ transition: background 0.15s;
+}
+
+.todo-item:hover {
+ background: #f9fafb;
+}
+
+.todo-item.completed .todo-title {
+ text-decoration: line-through;
+ color: #9ca3af;
+}
+
+.todo-item input[type="checkbox"] {
+ width: 1.1rem;
+ height: 1.1rem;
+ cursor: pointer;
+ flex-shrink: 0;
+}
+
+.todo-title {
+ flex: 1;
+ cursor: pointer;
+}
+
+.user-badge {
+ font-size: 0.7rem;
+ color: #6b7280;
+ background: #f3f4f6;
+ border-radius: 99px;
+ padding: 0.15rem 0.5rem;
+ white-space: nowrap;
+}
+
+.delete-btn {
+ background: none;
+ border: none;
+ color: #d1d5db;
+ font-size: 1rem;
+ cursor: pointer;
+ padding: 0.25rem;
+ line-height: 1;
+ border-radius: 4px;
+ transition: color 0.15s;
+}
+
+.delete-btn:hover {
+ color: #ef4444;
+}
diff --git a/src/app/features/todos/components/todo-item/todo-item.component.html b/src/app/features/todos/components/todo-item/todo-item.component.html
new file mode 100644
index 0000000..e37af85
--- /dev/null
+++ b/src/app/features/todos/components/todo-item/todo-item.component.html
@@ -0,0 +1,18 @@
+
+
+
+
+ {{ todo.title }}
+
+
+ User {{ todo.userId }}
+
+
+ X
+
+
diff --git a/src/app/features/todos/components/todo-item/todo-item.component.ts b/src/app/features/todos/components/todo-item/todo-item.component.ts
new file mode 100644
index 0000000..16b0a65
--- /dev/null
+++ b/src/app/features/todos/components/todo-item/todo-item.component.ts
@@ -0,0 +1,17 @@
+import { CommonModule } from '@angular/common';
+import { Component, EventEmitter, Input, Output } from '@angular/core';
+import { Todo } from '../../../../core/models/todo.model';
+
+@Component({
+ selector: 'app-todo-item',
+ standalone: true,
+ imports: [CommonModule],
+ templateUrl: './todo-item.component.html',
+ styleUrl: './todo-item.component.css',
+})
+export class TodoItemComponent {
+ @Input({ required: true }) todo!: Todo;
+
+ @Output() toggled = new EventEmitter();
+ @Output() deleted = new EventEmitter();
+}
diff --git a/src/app/features/todos/components/todo-list/todo-list.component.css b/src/app/features/todos/components/todo-list/todo-list.component.css
new file mode 100644
index 0000000..6473013
--- /dev/null
+++ b/src/app/features/todos/components/todo-list/todo-list.component.css
@@ -0,0 +1,11 @@
+.todo-list {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.empty-state {
+ text-align: center;
+ color: #9ca3af;
+ padding: 2rem;
+}
diff --git a/src/app/features/todos/components/todo-list/todo-list.component.html b/src/app/features/todos/components/todo-list/todo-list.component.html
new file mode 100644
index 0000000..724a107
--- /dev/null
+++ b/src/app/features/todos/components/todo-list/todo-list.component.html
@@ -0,0 +1,12 @@
+
+
+ Keine ToDos auf dieser Seite.
+
+
+
+
diff --git a/src/app/features/todos/components/todo-list/todo-list.component.ts b/src/app/features/todos/components/todo-list/todo-list.component.ts
new file mode 100644
index 0000000..eff3823
--- /dev/null
+++ b/src/app/features/todos/components/todo-list/todo-list.component.ts
@@ -0,0 +1,18 @@
+import { CommonModule } from '@angular/common';
+import { Component, EventEmitter, Input, Output } from '@angular/core';
+import { Todo } from '../../../../core/models/todo.model';
+import { TodoItemComponent } from '../todo-item/todo-item.component';
+
+@Component({
+ selector: 'app-todo-list',
+ standalone: true,
+ imports: [CommonModule, TodoItemComponent],
+ templateUrl: './todo-list.component.html',
+ styleUrl: './todo-list.component.css',
+})
+export class TodoListComponent {
+ @Input() todos: Todo[] = [];
+
+ @Output() toggled = new EventEmitter();
+ @Output() deleted = new EventEmitter();
+}
diff --git a/src/app/features/todos/components/todo-pagination/todo-pagination.component.css b/src/app/features/todos/components/todo-pagination/todo-pagination.component.css
new file mode 100644
index 0000000..611b2a5
--- /dev/null
+++ b/src/app/features/todos/components/todo-pagination/todo-pagination.component.css
@@ -0,0 +1,30 @@
+.pagination {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 1rem;
+ margin-top: 1.5rem;
+}
+
+.pagination button {
+ padding: 0.5rem 1rem;
+ border: 1px solid #d1d5db;
+ background: #fff;
+ border-radius: 8px;
+ cursor: pointer;
+ transition: background 0.15s;
+}
+
+.pagination button:hover:not(:disabled) {
+ background: #f3f4f6;
+}
+
+.pagination button:disabled {
+ opacity: 0.4;
+ cursor: default;
+}
+
+.page-info {
+ color: #6b7280;
+ font-size: 0.9rem;
+}
diff --git a/src/app/features/todos/components/todo-pagination/todo-pagination.component.html b/src/app/features/todos/components/todo-pagination/todo-pagination.component.html
new file mode 100644
index 0000000..8c80262
--- /dev/null
+++ b/src/app/features/todos/components/todo-pagination/todo-pagination.component.html
@@ -0,0 +1,11 @@
+
diff --git a/src/app/features/todos/components/todo-pagination/todo-pagination.component.ts b/src/app/features/todos/components/todo-pagination/todo-pagination.component.ts
new file mode 100644
index 0000000..bb71384
--- /dev/null
+++ b/src/app/features/todos/components/todo-pagination/todo-pagination.component.ts
@@ -0,0 +1,16 @@
+import { Component, EventEmitter, Input, Output } from '@angular/core';
+
+@Component({
+ selector: 'app-todo-pagination',
+ standalone: true,
+ templateUrl: './todo-pagination.component.html',
+ styleUrl: './todo-pagination.component.css',
+})
+export class TodoPaginationComponent {
+ @Input() currentPage = 1;
+ @Input() canGoPrevious = false;
+ @Input() canGoNext = false;
+
+ @Output() previous = new EventEmitter();
+ @Output() next = new EventEmitter();
+}
diff --git a/src/app/todo.model.ts b/src/app/todo.model.ts
deleted file mode 100644
index 6f6db63..0000000
--- a/src/app/todo.model.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-// todo.model.ts – TypeScript-Interfaces für die ToDo-Datenstrukturen
-//
-// Interfaces beschreiben den Aufbau von Objekten zur Compile-Zeit.
-// Sie erzeugen keinen JavaScript-Code, sondern dienen nur der Typprüfung.
-
-/** Ein vollständiges ToDo-Objekt, wie es die API zurückgibt */
-export interface Todo {
- id: number; // Eindeutige ID (von der API vergeben)
- userId: number; // ID des Benutzers, dem das ToDo gehört
- title: string; // Titel / Beschreibung des ToDos
- completed: boolean; // Erledigt-Status
-}
-
-/** Daten zum Erstellen eines neuen ToDos (id wird von der API gesetzt) */
-export interface TodoCreate {
- userId: number;
- title: string;
- completed?: boolean; // Optional: Standard ist false
-}