feat: publication management with cover images, PDFs, and fruit images (story #04)

Full CRUD for publications; link fruits to publications; upload per-fruit
PDF descriptions and fruit images stored as BYTEA; fruit detail page shows
publication descriptions and images. ImageOverlayDrawer for cover thumbnail
full-size view.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-18 10:48:08 +02:00
co-authored by Claude Sonnet 4.6
parent 2e3f0f366c
commit fce4d67cbe
18 changed files with 3116 additions and 0 deletions
+41
View File
@@ -4,6 +4,8 @@ import { useRouter } from 'vue-router'
import { useFruitStore } from '../stores/fruitStore'
import { addImage, deleteImage, FRUIT_TYPES } from '../api/fruits'
import type { Fruit } from '../api/fruits'
import { getFruitDescriptions, getFruitPublicationImages } from '../api/publications'
import type { PublicationDescription, PublicationFruitImage } from '../api/publications'
const props = defineProps<{ id: string }>()
const router = useRouter()
@@ -26,9 +28,19 @@ const imageTitle = ref('')
const uploading = ref(false)
const uploadError = ref<string | null>(null)
const pubDescriptions = ref<PublicationDescription[]>([])
const pubFruitImages = ref<PublicationFruitImage[]>([])
onMounted(async () => {
await store.fetchFruit(Number(props.id))
if (store.current) startEdit(store.current)
const id = Number(props.id)
const [descs, imgs] = await Promise.all([
getFruitDescriptions(id),
getFruitPublicationImages(id),
])
pubDescriptions.value = descs
pubFruitImages.value = imgs
})
function startEdit(f: Fruit) {
@@ -258,6 +270,35 @@ async function removeImage(imageId: number) {
</div>
</div>
<!-- Publication descriptions -->
<div v-if="pubDescriptions.length > 0" class="mt-8">
<h2 class="text-lg font-semibold text-gray-800 mb-3">Beschreibungen in Publikationen</h2>
<ul class="space-y-1">
<li v-for="d in pubDescriptions" :key="d.id">
<a
:href="d.url"
target="_blank"
class="text-blue-600 hover:underline text-sm"
>
{{ d.publication_title }}
</a>
</li>
</ul>
</div>
<!-- Publication fruit images -->
<div v-if="pubFruitImages.length > 0" class="mt-8">
<h2 class="text-lg font-semibold text-gray-800 mb-3">Bilder aus Publikationen</h2>
<div class="grid grid-cols-2 md:grid-cols-3 gap-4">
<div v-for="img in pubFruitImages" :key="img.id" class="border rounded overflow-hidden">
<img :src="img.url" :alt="img.filename ?? 'Fruchtbild'" class="w-full h-32 object-cover" />
<div class="p-1 text-xs text-gray-500">
{{ img.publication_title }}{{ img.publication_author ? ' · ' + img.publication_author : '' }}
</div>
</div>
</div>
</div>
<div class="mt-6">
<RouterLink to="/fruits" class="text-green-700 hover:underline text-sm"> Zurück zur Liste</RouterLink>
</div>