defragment
This commit is contained in:
parent
df119dcc03
commit
3660c941ac
18
README.md
18
README.md
@ -23,9 +23,21 @@ WebDev-Angular-ToDo-Example/
|
|||||||
├── main.ts ← App-Bootstrap (Startpunkt)
|
├── main.ts ← App-Bootstrap (Startpunkt)
|
||||||
├── styles.css ← Globale CSS-Basis-Styles
|
├── styles.css ← Globale CSS-Basis-Styles
|
||||||
└── app/
|
└── app/
|
||||||
├── app.component.ts ← Root-Komponente (UI + Logik)
|
├── app.component.ts / .html / .css
|
||||||
├── todo.model.ts ← TypeScript-Interfaces (Datentypen)
|
├── core/
|
||||||
└── todo.service.ts ← Service für API-Kommunikation
|
│ ├── 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
|
## Installation & Start
|
||||||
|
|||||||
@ -20,162 +20,3 @@
|
|||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
margin: 0.25rem 0 0;
|
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;
|
|
||||||
}
|
|
||||||
|
|||||||
@ -5,105 +5,35 @@
|
|||||||
<p class="subtitle">Angular 21 · Vite · REST-API</p>
|
<p class="subtitle">Angular 21 · Vite · REST-API</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- ======================================================
|
<app-error-message
|
||||||
FEHLERMELDUNG
|
*ngIf="error"
|
||||||
*ngIf zeigt das Element nur an, wenn "error" einen Wert hat.
|
[message]="error"
|
||||||
====================================================== -->
|
(dismissed)="error = ''"
|
||||||
<div class="error-box" *ngIf="error">
|
|
||||||
<strong>Fehler:</strong> {{ error }}
|
|
||||||
<button class="close-btn" (click)="error = ''">✕</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ======================================================
|
|
||||||
FORMULAR – Neues ToDo hinzufügen
|
|
||||||
(ngSubmit): Ruft addTodo() beim Absenden auf.
|
|
||||||
[(ngModel)]: Two-Way-Binding – verbindet Input mit "newTitle".
|
|
||||||
[disabled]: Deaktiviert Button, wenn Feld leer ist.
|
|
||||||
====================================================== -->
|
|
||||||
<form class="add-form" (ngSubmit)="addTodo()">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
[(ngModel)]="newTitle"
|
|
||||||
name="newTitle"
|
|
||||||
placeholder="Neues ToDo eingeben..."
|
|
||||||
autocomplete="off"
|
|
||||||
/>
|
|
||||||
<button type="submit" [disabled]="!newTitle.trim() || saving">
|
|
||||||
{{ saving ? 'Speichert…' : '+ Hinzufügen' }}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<!-- ======================================================
|
|
||||||
LADEINDIKATOR
|
|
||||||
Wird angezeigt, während Daten von der API geladen werden.
|
|
||||||
====================================================== -->
|
|
||||||
<div class="loading" *ngIf="loading">Daten werden geladen…</div>
|
|
||||||
|
|
||||||
<!-- ======================================================
|
|
||||||
TODO-LISTE
|
|
||||||
*ngFor iteriert über das todos-Array und rendert ein <li> pro Eintrag.
|
|
||||||
"let todo of todos" → todo ist die aktuelle Variable im Loop.
|
|
||||||
[class.completed]: Fügt CSS-Klasse hinzu, wenn todo.completed === true.
|
|
||||||
====================================================== -->
|
|
||||||
<ul class="todo-list" *ngIf="!loading">
|
|
||||||
|
|
||||||
<!-- Meldung, wenn keine Todos vorhanden -->
|
|
||||||
<li class="empty-state" *ngIf="todos.length === 0">
|
|
||||||
Keine ToDos auf dieser Seite.
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li
|
|
||||||
*ngFor="let todo of todos"
|
|
||||||
class="todo-item"
|
|
||||||
[class.completed]="todo.completed"
|
|
||||||
>
|
|
||||||
<!-- Checkbox: (change) reagiert auf Änderung und ruft toggleTodo() auf -->
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
[checked]="todo.completed"
|
|
||||||
(change)="toggleTodo(todo)"
|
|
||||||
[id]="'todo-' + todo.id"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Label verknüpft Text mit der Checkbox über das id-Attribut -->
|
<app-todo-form
|
||||||
<label [for]="'todo-' + todo.id" class="todo-title">
|
[model]="newTitle"
|
||||||
{{ todo.title }}
|
(modelChange)="newTitle = $event"
|
||||||
</label>
|
[saving]="saving"
|
||||||
|
(submitted)="addTodo()"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Badge zeigt die User-ID an -->
|
<app-loading-indicator *ngIf="loading" />
|
||||||
<span class="user-badge">User {{ todo.userId }}</span>
|
|
||||||
|
|
||||||
<!-- Löschen-Button: (click) ruft deleteTodo() mit der ID auf -->
|
<app-todo-list
|
||||||
<button
|
*ngIf="!loading"
|
||||||
class="delete-btn"
|
[todos]="todos"
|
||||||
(click)="deleteTodo(todo.id)"
|
(toggled)="toggleTodo($event)"
|
||||||
title="ToDo löschen"
|
(deleted)="deleteTodo($event)"
|
||||||
>
|
/>
|
||||||
✕
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<!-- ======================================================
|
<app-todo-pagination
|
||||||
PAGINATION
|
*ngIf="!loading"
|
||||||
[disabled]: Deaktiviert Buttons an den Rändern der Liste.
|
[currentPage]="currentPage"
|
||||||
====================================================== -->
|
[canGoPrevious]="currentPage > 1"
|
||||||
<div class="pagination" *ngIf="!loading">
|
[canGoNext]="todos.length === pageSize"
|
||||||
<button
|
(previous)="prevPage()"
|
||||||
(click)="prevPage()"
|
(next)="nextPage()"
|
||||||
[disabled]="currentPage === 1"
|
/>
|
||||||
>
|
|
||||||
← Zurück
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<span class="page-info">Seite {{ currentPage }}</span>
|
|
||||||
|
|
||||||
<button
|
|
||||||
(click)="nextPage()"
|
|
||||||
[disabled]="todos.length < pageSize"
|
|
||||||
>
|
|
||||||
Weiter →
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -11,9 +11,13 @@
|
|||||||
|
|
||||||
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
|
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common'; // *ngIf, *ngFor, etc.
|
import { CommonModule } from '@angular/common'; // *ngIf, *ngFor, etc.
|
||||||
import { FormsModule } from '@angular/forms'; // [(ngModel)] Two-Way-Binding
|
import { Todo } from './core/models/todo.model';
|
||||||
import { TodoService } from './todo.service';
|
import { TodoService } from './core/services/todo.service';
|
||||||
import { Todo } from './todo.model';
|
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({
|
@Component({
|
||||||
// selector: Wie diese Komponente im HTML eingebunden wird (<app-root>)
|
// selector: Wie diese Komponente im HTML eingebunden wird (<app-root>)
|
||||||
@ -23,7 +27,14 @@ import { Todo } from './todo.model';
|
|||||||
standalone: true,
|
standalone: true,
|
||||||
|
|
||||||
// imports: Andere Angular-Module/Komponenten, die im Template verwendet werden
|
// 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
|
// Template und Styles sind ausgelagert für bessere Wartbarkeit
|
||||||
templateUrl: './app.component.html',
|
templateUrl: './app.component.html',
|
||||||
|
|||||||
14
src/app/core/models/todo.model.ts
Normal file
14
src/app/core/models/todo.model.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
@ -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 { 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 API_BASE = 'https://webdev.iten-web.ch/10003/api';
|
||||||
const REQUEST_TIMEOUT_MS = 10000;
|
const REQUEST_TIMEOUT_MS = 10000;
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class TodoService {
|
export class TodoService {
|
||||||
|
|
||||||
private buildUrl(path: string, query: Record<string, string | number | boolean> = {}): string {
|
private buildUrl(path: string, query: Record<string, string | number | boolean> = {}): string {
|
||||||
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
||||||
const url = new URL(`${API_BASE}${normalizedPath}`);
|
const url = new URL(`${API_BASE}${normalizedPath}`);
|
||||||
@ -54,8 +40,6 @@ export class TodoService {
|
|||||||
return response.json() as Promise<T>;
|
return response.json() as Promise<T>;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// Promise.race garantiert, dass der Aufruf auch dann endet,
|
|
||||||
// wenn fetch in seltenen Browser-/Extension-Faellen hängen bleibt.
|
|
||||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||||
timeoutId = window.setTimeout(() => {
|
timeoutId = window.setTimeout(() => {
|
||||||
controller.abort();
|
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<Todo[]> {
|
async getTodos(page = 1, limit = 10): Promise<Todo[]> {
|
||||||
return this.request<Todo[]>('/todos', undefined, { page, limit });
|
return this.request<Todo[]>('/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<Todo> {
|
async createTodo(data: TodoCreate): Promise<Todo> {
|
||||||
return this.request<Todo>('/todos', {
|
return this.request<Todo>('/todos', {
|
||||||
method: 'POST',
|
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<Todo>): Promise<Todo> {
|
async patchTodo(id: number, changes: Partial<Todo>): Promise<Todo> {
|
||||||
return this.request<Todo>(`/todos/${id}`, {
|
return this.request<Todo>(`/todos/${id}`, {
|
||||||
method: 'PATCH',
|
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<void> {
|
async deleteTodo(id: number): Promise<void> {
|
||||||
await this.request<void>(`/todos/${id}`, {
|
await this.request<void>(`/todos/${id}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
<div class="error-box">
|
||||||
|
<strong>Fehler:</strong> {{ message }}
|
||||||
|
<button class="close-btn" (click)="dismissed.emit()">X</button>
|
||||||
|
</div>
|
||||||
@ -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<void>();
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
color: #6b7280;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
<div class="loading">{{ text }}</div>
|
||||||
@ -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...';
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
<form class="add-form" (ngSubmit)="onSubmit()">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
[ngModel]="model"
|
||||||
|
(ngModelChange)="modelChange.emit($event)"
|
||||||
|
name="newTitle"
|
||||||
|
placeholder="Neues ToDo eingeben..."
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
<button type="submit" [disabled]="!model.trim() || saving">
|
||||||
|
{{ saving ? 'Speichert...' : '+ Hinzufuegen' }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
@ -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<string>();
|
||||||
|
@Output() submitted = new EventEmitter<void>();
|
||||||
|
|
||||||
|
onSubmit(): void {
|
||||||
|
this.submitted.emit();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
<div class="todo-item" [class.completed]="todo.completed">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
[checked]="todo.completed"
|
||||||
|
(change)="toggled.emit(todo)"
|
||||||
|
[id]="'todo-' + todo.id"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label [for]="'todo-' + todo.id" class="todo-title">
|
||||||
|
{{ todo.title }}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<span class="user-badge">User {{ todo.userId }}</span>
|
||||||
|
|
||||||
|
<button class="delete-btn" (click)="deleted.emit(todo.id)" title="ToDo loeschen">
|
||||||
|
X
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
@ -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<Todo>();
|
||||||
|
@Output() deleted = new EventEmitter<number>();
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
.todo-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
color: #9ca3af;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
<div class="todo-list">
|
||||||
|
<div class="empty-state" *ngIf="todos.length === 0">
|
||||||
|
Keine ToDos auf dieser Seite.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<app-todo-item
|
||||||
|
*ngFor="let todo of todos"
|
||||||
|
[todo]="todo"
|
||||||
|
(toggled)="toggled.emit($event)"
|
||||||
|
(deleted)="deleted.emit($event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
@ -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<Todo>();
|
||||||
|
@Output() deleted = new EventEmitter<number>();
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
<div class="pagination">
|
||||||
|
<button (click)="previous.emit()" [disabled]="!canGoPrevious">
|
||||||
|
Zurueck
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<span class="page-info">Seite {{ currentPage }}</span>
|
||||||
|
|
||||||
|
<button (click)="next.emit()" [disabled]="!canGoNext">
|
||||||
|
Weiter
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
@ -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<void>();
|
||||||
|
@Output() next = new EventEmitter<void>();
|
||||||
|
}
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user