extract html & css

This commit is contained in:
Marc-Alexander Iten 2026-05-14 15:26:01 +02:00
parent 479b41cb8f
commit df119dcc03
3 changed files with 293 additions and 297 deletions

181
src/app/app.component.css Normal file
View File

@ -0,0 +1,181 @@
/* ── Layout ──────────────────────────────────────────── */
.container {
max-width: 640px;
margin: 2rem auto;
padding: 0 1rem;
font-family: inherit; /* übernimmt Font aus styles.css */
}
/* ── Header ──────────────────────────────────────────── */
.app-header {
text-align: center;
margin-bottom: 2rem;
}
.app-header h1 {
font-size: 2rem;
margin: 0;
}
.subtitle {
color: #888;
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;
}

109
src/app/app.component.html Normal file
View File

@ -0,0 +1,109 @@
<div class="container">
<header class="app-header">
<h1>ToDo-App</h1>
<p class="subtitle">Angular 21 · Vite · REST-API</p>
</header>
<!-- ======================================================
FEHLERMELDUNG
*ngIf zeigt das Element nur an, wenn "error" einen Wert hat.
====================================================== -->
<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 -->
<label [for]="'todo-' + todo.id" class="todo-title">
{{ todo.title }}
</label>
<!-- Badge zeigt die User-ID an -->
<span class="user-badge">User {{ todo.userId }}</span>
<!-- Löschen-Button: (click) ruft deleteTodo() mit der ID auf -->
<button
class="delete-btn"
(click)="deleteTodo(todo.id)"
title="ToDo löschen"
>
</button>
</li>
</ul>
<!-- ======================================================
PAGINATION
[disabled]: Deaktiviert Buttons an den Rändern der Liste.
====================================================== -->
<div class="pagination" *ngIf="!loading">
<button
(click)="prevPage()"
[disabled]="currentPage === 1"
>
← Zurück
</button>
<span class="page-info">Seite {{ currentPage }}</span>
<button
(click)="nextPage()"
[disabled]="todos.length < pageSize"
>
Weiter →
</button>
</div>
</div>

View File

@ -25,303 +25,9 @@ import { Todo } from './todo.model';
// imports: Andere Angular-Module/Komponenten, die im Template verwendet werden
imports: [CommonModule, FormsModule],
// template: Das HTML-Template der Komponente (inline, kein separates File)
template: `
<div class="container">
<header class="app-header">
<h1>ToDo-App</h1>
<p class="subtitle">Angular 21 · Vite · REST-API</p>
</header>
<!-- ======================================================
FEHLERMELDUNG
*ngIf zeigt das Element nur an, wenn "error" einen Wert hat.
====================================================== -->
<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 -->
<label [for]="'todo-' + todo.id" class="todo-title">
{{ todo.title }}
</label>
<!-- Badge zeigt die User-ID an -->
<span class="user-badge">User {{ todo.userId }}</span>
<!-- Löschen-Button: (click) ruft deleteTodo() mit der ID auf -->
<button
class="delete-btn"
(click)="deleteTodo(todo.id)"
title="ToDo löschen"
>
</button>
</li>
</ul>
<!-- ======================================================
PAGINATION
[disabled]: Deaktiviert Buttons an den Rändern der Liste.
====================================================== -->
<div class="pagination" *ngIf="!loading">
<button
(click)="prevPage()"
[disabled]="currentPage === 1"
>
Zurück
</button>
<span class="page-info">Seite {{ currentPage }}</span>
<button
(click)="nextPage()"
[disabled]="todos.length < pageSize"
>
Weiter
</button>
</div>
</div>
`,
// styles: Komponenten-spezifisches CSS (ist automatisch auf diese Komponente begrenzt)
styles: [`
/* ── Layout ──────────────────────────────────────────── */
.container {
max-width: 640px;
margin: 2rem auto;
padding: 0 1rem;
font-family: inherit; /* übernimmt Font aus styles.css */
}
/* ── Header ──────────────────────────────────────────── */
.app-header {
text-align: center;
margin-bottom: 2rem;
}
.app-header h1 {
font-size: 2rem;
margin: 0;
}
.subtitle {
color: #888;
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;
}
`],
// Template und Styles sind ausgelagert für bessere Wartbarkeit
templateUrl: './app.component.html',
styleUrl: './app.component.css',
})
export class AppComponent implements OnInit {