Files
osdb-1-claude-opusplan/frontend/src/views/FruitList.vue
T
juliaandClaude Sonnet 4.6 1b889ac112 feat: manage fruits — full CRUD with images and synonyms (story #02)
Backend: domain structs, FruitRepository interface + pg implementation,
9 Echo v4 handlers (list/get/create/update/delete, image sub-resources),
migration 000002 (fruit_type ENUM, fruits, fruit_synonyms, fruit_images),
route-scoped BodyLimit("5M") for uploads, http.DetectContentType for serving.

Frontend: typed fetch API layer, Pinia setup-style fruitStore, FruitList
(paginated), FruitCreate, and FruitDetail (edit + synonyms editor + image
gallery). 25 backend unit tests + integration test; 18 frontend tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 10:18:01 +02:00

101 lines
3.4 KiB
Vue

<script setup lang="ts">
import { onMounted, computed } from 'vue'
import { RouterLink } from 'vue-router'
import { useFruitStore } from '../stores/fruitStore'
const store = useFruitStore()
onMounted(async () => {
await store.fetchFruits()
})
const hasPrev = computed(() => store.offset > 0)
const hasNext = computed(() => store.offset + store.limit < store.total)
async function prev() {
await store.fetchFruits(store.limit, Math.max(0, store.offset - store.limit))
}
async function next() {
await store.fetchFruits(store.limit, store.offset + store.limit)
}
</script>
<template>
<div class="p-6 max-w-4xl mx-auto">
<div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-bold text-gray-900">Früchte</h1>
<RouterLink
to="/fruits/new"
class="bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700 transition-colors"
>
Neue Frucht
</RouterLink>
</div>
<!-- Search placeholder (story #06) -->
<div class="mb-4">
<input
type="text"
placeholder="Suche... (kommt in Story #06)"
disabled
class="w-full border border-gray-300 rounded px-3 py-2 bg-gray-100 text-gray-400 cursor-not-allowed"
/>
</div>
<div v-if="store.loading" class="text-gray-500">Wird geladen...</div>
<div v-else-if="store.error" class="text-red-600">{{ store.error }}</div>
<div v-else>
<table class="w-full border-collapse border border-gray-200 rounded">
<thead class="bg-gray-50">
<tr>
<th class="border border-gray-200 px-4 py-2 text-left text-sm font-medium text-gray-700">Name</th>
<th class="border border-gray-200 px-4 py-2 text-left text-sm font-medium text-gray-700">OSDB-Kürzel</th>
<th class="border border-gray-200 px-4 py-2 text-left text-sm font-medium text-gray-700">Typ</th>
</tr>
</thead>
<tbody>
<tr v-if="store.fruits.length === 0">
<td colspan="3" class="border border-gray-200 px-4 py-8 text-center text-gray-400">
Keine Früchte vorhanden.
</td>
</tr>
<tr
v-for="fruit in store.fruits"
:key="fruit.id"
class="hover:bg-gray-50 cursor-pointer"
>
<td class="border border-gray-200 px-4 py-2">
<RouterLink :to="`/fruits/${fruit.id}`" class="text-green-700 hover:underline">
{{ fruit.name }}
</RouterLink>
</td>
<td class="border border-gray-200 px-4 py-2 text-sm text-gray-600">{{ fruit.osdb_number }}</td>
<td class="border border-gray-200 px-4 py-2 text-sm text-gray-600">{{ fruit.fruit_type }}</td>
</tr>
</tbody>
</table>
<div class="flex items-center justify-between mt-4 text-sm text-gray-600">
<span>{{ store.total }} Früchte gesamt</span>
<div class="flex gap-2">
<button
@click="prev"
:disabled="!hasPrev"
class="px-3 py-1 border rounded disabled:opacity-40 hover:bg-gray-100 transition-colors"
>
Zurück
</button>
<button
@click="next"
:disabled="!hasNext"
class="px-3 py-1 border rounded disabled:opacity-40 hover:bg-gray-100 transition-colors"
>
Weiter
</button>
</div>
</div>
</div>
</div>
</template>