defragment
This commit is contained in:
parent
4fe9cb9be4
commit
e5f1daf3d0
148
src/App.vue
148
src/App.vue
@ -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);
|
||||||
|
|||||||
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