defragment
This commit is contained in:
parent
4fe9cb9be4
commit
e5f1daf3d0
150
src/App.vue
150
src/App.vue
@ -17,6 +17,7 @@
|
||||
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { fetchTodos, createTodo, patchTodo, deleteTodo } from './api.js'
|
||||
import TodoList from './components/TodoList.vue'
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// STATE – reaktive Zustandsvariablen
|
||||
@ -355,49 +356,13 @@ watch(page, () => {
|
||||
<div v-if="loading" class="loading">Laden...</div>
|
||||
|
||||
<!-- ── ToDo-Liste ────────────────────────────────────────────────────── -->
|
||||
<!--
|
||||
v-else-if und v-else funktionieren wie if/else, müssen aber
|
||||
direkt nach einem v-if- bzw. v-else-if-Element stehen.
|
||||
-->
|
||||
<div v-else-if="todos.length === 0 && !loading" class="empty">
|
||||
Keine ToDos gefunden.
|
||||
</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>
|
||||
<!-- ToDo-Liste ist als eigene Komponente ausgelagert -->
|
||||
<TodoList
|
||||
:todos="todos"
|
||||
:loading="loading"
|
||||
@toggle-completed="toggleCompleted"
|
||||
@remove-todo="removeTodo"
|
||||
/>
|
||||
|
||||
<!-- ── Pagination ────────────────────────────────────────────────────── -->
|
||||
<!--
|
||||
@ -467,37 +432,27 @@ watch(page, () => {
|
||||
<style>
|
||||
/**
|
||||
* 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 {
|
||||
--color-bg: #f5f5f5;
|
||||
--color-surface: #ffffff;
|
||||
--color-primary: #4a90d9;
|
||||
--color-primary-hover: #357abd;
|
||||
--color-danger: #e05050;
|
||||
--color-danger-hover: #c03030;
|
||||
--color-secondary: #6c757d;
|
||||
--color-border: #ddd;
|
||||
--color-text: #333;
|
||||
--color-text-muted: #888;
|
||||
--color-done: #aaa;
|
||||
--color-error-bg: #fdecea;
|
||||
--color-error-border: #f5c6cb;
|
||||
--radius: 6px;
|
||||
--shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
/* ── Reset & Basis ───────────────────────────────────────────────────────── */
|
||||
*, *::before, *::after {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@ -510,7 +465,6 @@ body {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ── App-Layout ──────────────────────────────────────────────────────────── */
|
||||
.app {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
@ -539,7 +493,6 @@ body {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
/* ── Filterleiste ────────────────────────────────────────────────────────── */
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@ -550,7 +503,6 @@ body {
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
/* ── Formular-Elemente ───────────────────────────────────────────────────── */
|
||||
.input {
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
@ -579,7 +531,6 @@ body {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* ── Buttons ─────────────────────────────────────────────────────────────── */
|
||||
.btn {
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
@ -613,17 +564,6 @@ body {
|
||||
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 {
|
||||
background: none;
|
||||
border: none;
|
||||
@ -633,14 +573,12 @@ body {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
/* ── Statuszeile ─────────────────────────────────────────────────────────── */
|
||||
.status-bar {
|
||||
font-size: 0.85rem;
|
||||
color: var(--color-text-muted);
|
||||
padding: 0 0.25rem;
|
||||
}
|
||||
|
||||
/* ── Alerts ──────────────────────────────────────────────────────────────── */
|
||||
.alert {
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: var(--radius);
|
||||
@ -655,76 +593,13 @@ body {
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
/* ── Ladeanzeige / Leerstate ─────────────────────────────────────────────── */
|
||||
.loading,
|
||||
.empty {
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: var(--color-text-muted);
|
||||
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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -739,7 +614,6 @@ body {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ── Neues ToDo ──────────────────────────────────────────────────────────── */
|
||||
.add-form {
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius);
|
||||
|
||||
62
src/components/TodoList.vue
Normal file
62
src/components/TodoList.vue
Normal 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>
|
||||
105
src/components/TodoListItem.vue
Normal file
105
src/components/TodoListItem.vue
Normal 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>
|
||||
Loading…
x
Reference in New Issue
Block a user