defragment

This commit is contained in:
Marc-Alexander Iten 2026-05-14 15:37:59 +02:00
parent 4fe9cb9be4
commit e5f1daf3d0
3 changed files with 179 additions and 138 deletions

View File

@ -17,6 +17,7 @@
import { ref, computed, onMounted, onUnmounted, watch } from 'vue' import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import { fetchTodos, createTodo, patchTodo, deleteTodo } from './api.js' import { fetchTodos, createTodo, patchTodo, deleteTodo } from './api.js'
import TodoList from './components/TodoList.vue'
// //
// STATE reaktive Zustandsvariablen // STATE reaktive Zustandsvariablen
@ -355,50 +356,14 @@ watch(page, () => {
<div v-if="loading" class="loading">Laden...</div> <div v-if="loading" class="loading">Laden...</div>
<!-- ToDo-Liste --> <!-- ToDo-Liste -->
<!-- <!-- ToDo-Liste ist als eigene Komponente ausgelagert -->
v-else-if und v-else funktionieren wie if/else, müssen aber <TodoList
direkt nach einem v-if- bzw. v-else-if-Element stehen. :todos="todos"
--> :loading="loading"
<div v-else-if="todos.length === 0 && !loading" class="empty"> @toggle-completed="toggleCompleted"
Keine ToDos gefunden. @remove-todo="removeTodo"
</div>
<ul v-else class="todo-list">
<!--
v-for iteriert über das todos-Array.
:key ist zwingend erforderlich: Vue nutzt es, um DOM-Elemente
effizient zu aktualisieren (kein unnötiges Re-Rendern).
:class wendet eine CSS-Klasse bedingt an:
{ 'todo-done': todo.completed }
Klasse 'todo-done' wird gesetzt, wenn todo.completed true ist
-->
<li
v-for="todo in todos"
:key="todo.id"
:class="{ 'todo-done': todo.completed }"
class="todo-item"
>
<!-- Checkbox zum Umschalten des Erledigungsstatus -->
<input
type="checkbox"
:checked="todo.completed"
@change="toggleCompleted(todo)"
class="todo-checkbox"
/> />
<span class="todo-content">
<span class="todo-title">{{ todo.title }}</span>
<span class="todo-meta">ID: {{ todo.id }} · User: {{ todo.userId }}</span>
</span>
<!-- Löschen-Button: @click ruft removeTodo mit der todo.id auf -->
<button @click="removeTodo(todo.id)" class="btn btn-danger" title="Löschen">
</button>
</li>
</ul>
<!-- Pagination --> <!-- Pagination -->
<!-- <!--
:disabled deaktiviert den Button (HTML-Attribut disabled setzen), :disabled deaktiviert den Button (HTML-Attribut disabled setzen),
@ -467,37 +432,27 @@ watch(page, () => {
<style> <style>
/** /**
* Globale CSS-Styles (kein "scoped", damit auch body/html erfasst werden) * Globale CSS-Styles (kein "scoped", damit auch body/html erfasst werden)
*
* Designprinzipien:
* - Kein externes CSS-Framework
* - CSS Custom Properties (Variablen) für einfache Anpassbarkeit
* - Schlichte, übersichtliche Gestaltung
*/ */
/* CSS-Variablen (Custom Properties)
Definiert im :root-Selektor, damit sie global verfügbar sind.
Vorteil: Farben und Abstände zentral ändern.
*/
:root { :root {
--color-bg: #f5f5f5; --color-bg: #f5f5f5;
--color-surface: #ffffff; --color-surface: #ffffff;
--color-primary: #4a90d9; --color-primary: #4a90d9;
--color-primary-hover: #357abd; --color-primary-hover: #357abd;
--color-danger: #e05050; --color-danger: #e05050;
--color-danger-hover: #c03030;
--color-secondary: #6c757d; --color-secondary: #6c757d;
--color-border: #ddd; --color-border: #ddd;
--color-text: #333; --color-text: #333;
--color-text-muted: #888; --color-text-muted: #888;
--color-done: #aaa;
--color-error-bg: #fdecea; --color-error-bg: #fdecea;
--color-error-border: #f5c6cb; --color-error-border: #f5c6cb;
--radius: 6px; --radius: 6px;
--shadow: 0 2px 8px rgba(0, 0, 0, 0.08); --shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
} }
/* ── Reset & Basis ───────────────────────────────────────────────────────── */ *,
*, *::before, *::after { *::before,
*::after {
box-sizing: border-box; box-sizing: border-box;
margin: 0; margin: 0;
padding: 0; padding: 0;
@ -510,7 +465,6 @@ body {
line-height: 1.5; line-height: 1.5;
} }
/* ── App-Layout ──────────────────────────────────────────────────────────── */
.app { .app {
max-width: 800px; max-width: 800px;
margin: 0 auto; margin: 0 auto;
@ -539,7 +493,6 @@ body {
gap: 1rem; gap: 1rem;
} }
/* ── Filterleiste ────────────────────────────────────────────────────────── */
.filter-bar { .filter-bar {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@ -550,7 +503,6 @@ body {
box-shadow: var(--shadow); box-shadow: var(--shadow);
} }
/* ── Formular-Elemente ───────────────────────────────────────────────────── */
.input { .input {
flex: 1; flex: 1;
min-width: 120px; min-width: 120px;
@ -579,7 +531,6 @@ body {
cursor: pointer; cursor: pointer;
} }
/* ── Buttons ─────────────────────────────────────────────────────────────── */
.btn { .btn {
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
border: none; border: none;
@ -613,17 +564,6 @@ body {
background: #d4d9de; background: #d4d9de;
} }
.btn-danger {
background: transparent;
color: var(--color-danger);
padding: 0.25rem 0.5rem;
font-size: 1rem;
}
.btn-danger:hover {
background: var(--color-error-bg);
}
.btn-close { .btn-close {
background: none; background: none;
border: none; border: none;
@ -633,14 +573,12 @@ body {
margin-left: 0.5rem; margin-left: 0.5rem;
} }
/* ── Statuszeile ─────────────────────────────────────────────────────────── */
.status-bar { .status-bar {
font-size: 0.85rem; font-size: 0.85rem;
color: var(--color-text-muted); color: var(--color-text-muted);
padding: 0 0.25rem; padding: 0 0.25rem;
} }
/* ── Alerts ──────────────────────────────────────────────────────────────── */
.alert { .alert {
padding: 0.75rem 1rem; padding: 0.75rem 1rem;
border-radius: var(--radius); border-radius: var(--radius);
@ -655,76 +593,13 @@ body {
color: #721c24; color: #721c24;
} }
/* ── Ladeanzeige / Leerstate ─────────────────────────────────────────────── */ .loading {
.loading,
.empty {
text-align: center; text-align: center;
padding: 2rem; padding: 2rem;
color: var(--color-text-muted); color: var(--color-text-muted);
font-style: italic; font-style: italic;
} }
/* ── ToDo-Liste ──────────────────────────────────────────────────────────── */
.todo-list {
list-style: none;
background: var(--color-surface);
border-radius: var(--radius);
box-shadow: var(--shadow);
overflow: hidden;
}
.todo-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--color-border);
transition: background 0.15s;
}
.todo-item:last-child {
border-bottom: none;
}
.todo-item:hover {
background: #fafafa;
}
/* Erledigte ToDos werden ausgegraut und durchgestrichen */
.todo-done .todo-title {
text-decoration: line-through;
color: var(--color-done);
}
.todo-checkbox {
flex-shrink: 0;
width: 1.1rem;
height: 1.1rem;
cursor: pointer;
accent-color: var(--color-primary);
}
.todo-content {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
}
.todo-title {
font-size: 0.95rem;
/* Langer Text wird abgeschnitten statt umzubrechen */
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.todo-meta {
font-size: 0.75rem;
color: var(--color-text-muted);
}
/* ── Pagination ──────────────────────────────────────────────────────────── */
.pagination { .pagination {
display: flex; display: flex;
align-items: center; align-items: center;
@ -739,7 +614,6 @@ body {
text-align: center; text-align: center;
} }
/* ── Neues ToDo ──────────────────────────────────────────────────────────── */
.add-form { .add-form {
background: var(--color-surface); background: var(--color-surface);
border-radius: var(--radius); border-radius: var(--radius);

View File

@ -0,0 +1,62 @@
<script setup>
/**
* ToDo-Liste als eigene UI-Komponente.
* Verantwortlich fuer die Darstellung, nicht fuer Datenzugriff.
*/
import TodoListItem from './TodoListItem.vue'
defineProps({
todos: {
type: Array,
required: true,
},
loading: {
type: Boolean,
required: true,
},
})
const emit = defineEmits(['toggle-completed', 'remove-todo'])
function onToggleCompleted(todo) {
emit('toggle-completed', todo)
}
function onRemoveTodo(id) {
emit('remove-todo', id)
}
</script>
<template>
<div v-if="todos.length === 0 && !loading" class="empty">
Keine ToDos gefunden.
</div>
<ul v-else class="todo-list">
<TodoListItem
v-for="todo in todos"
:key="todo.id"
:todo="todo"
@toggle-completed="onToggleCompleted(todo)"
@remove-todo="onRemoveTodo(todo.id)"
/>
</ul>
</template>
<style scoped>
.empty {
text-align: center;
padding: 2rem;
color: var(--color-text-muted);
font-style: italic;
}
.todo-list {
list-style: none;
background: var(--color-surface);
border-radius: var(--radius);
box-shadow: var(--shadow);
overflow: hidden;
}
</style>

View File

@ -0,0 +1,105 @@
<script setup>
/**
* Einzelner Listeneintrag fuer ein Todo.
* Emittiert Events nach oben, damit die Business-Logik in App.vue bleibt.
*/
defineProps({
todo: {
type: Object,
required: true,
},
})
const emit = defineEmits(['toggle-completed', 'remove-todo'])
function onToggle() {
emit('toggle-completed')
}
function onRemove() {
emit('remove-todo')
}
</script>
<template>
<li :class="{ 'todo-done': todo.completed }" class="todo-item">
<input
type="checkbox"
:checked="todo.completed"
@change="onToggle"
class="todo-checkbox"
/>
<span class="todo-content">
<span class="todo-title">{{ todo.title }}</span>
<span class="todo-meta">ID: {{ todo.id }} · User: {{ todo.userId }}</span>
</span>
<button @click="onRemove" class="btn btn-danger" title="Löschen">
</button>
</li>
</template>
<style scoped>
.todo-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--color-border);
transition: background 0.15s;
}
.todo-item:last-child {
border-bottom: none;
}
.todo-item:hover {
background: #fafafa;
}
.todo-done .todo-title {
text-decoration: line-through;
color: var(--color-done);
}
.todo-checkbox {
flex-shrink: 0;
width: 1.1rem;
height: 1.1rem;
cursor: pointer;
accent-color: var(--color-primary);
}
.todo-content {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
}
.todo-title {
font-size: 0.95rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.todo-meta {
font-size: 0.75rem;
color: var(--color-text-muted);
}
.btn-danger {
background: transparent;
color: var(--color-danger);
padding: 0.25rem 0.5rem;
font-size: 1rem;
}
.btn-danger:hover {
background: var(--color-error-bg);
}
</style>