Files
hms-licht-ton/frontend/components/EquipmentCard.vue
T
Implementation Engineer e9da21b57e feat(T06): Mietkatalog frontend – equipment list, detail, search/filter, SSR
- composables/useApi.ts: base API client with runtimeConfig
- composables/useEquipment.ts: equipment API wrapper (list, detail, categories)
- components/EquipmentCard.vue: card with image/placeholder, badge, name, price, add-to-cart
- pages/mietkatalog.vue: SSR list with search, category chips, sort, pagination, skeleton, empty/error states
- pages/mietkatalog/[id].vue: SSR detail with specs table, related items, breadcrumb, add-to-cart
- nuxt.config.ts: runtimeConfig with apiBase
- 101 vitest tests passing (5 files)
- Build: 0 errors, 2.09 MB
2026-07-10 00:54:40 +02:00

88 lines
3.0 KiB
Vue

<template>
<article
class="card rounded-lg overflow-hidden border border-border-default hover:border-accent-border transition-colors duration-fast flex flex-col"
data-testid="equipment-card"
>
<!-- Image / Placeholder -->
<div class="relative w-full h-48 bg-surface overflow-hidden">
<img
v-if="equipment.image_url"
:src="equipment.image_url"
:alt="equipment.name"
class="w-full h-full object-cover"
loading="lazy"
/>
<div
v-else
class="w-full h-full flex items-center justify-center"
>
<svg class="w-12 h-12 text-secondary" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909M3.75 3.75h16.5a1.5 1.5 0 011.5 1.5v12a1.5 1.5 0 01-1.5 1.5H3.75a1.5 1.5 0 01-1.5-1.5V5.25a1.5 1.5 0 011.5-1.5z" />
</svg>
</div>
<!-- Category Badge -->
<span
v-if="equipment.category"
class="absolute top-2 left-2 px-2 py-1 text-xs font-medium rounded-sm bg-bg/80 text-text-muted backdrop-blur-sm"
>
{{ equipment.category }}
</span>
<!-- Availability Badge -->
<span
v-if="!equipment.available"
class="absolute top-2 right-2 px-2 py-1 text-xs font-medium rounded-sm bg-error-bg text-error"
>
Nicht verfügbar
</span>
</div>
<!-- Content -->
<div class="flex flex-col flex-grow p-4">
<h3 class="text-base font-bold text-text mb-1 line-clamp-1">
<NuxtLink :to="`/mietkatalog/${equipment.id}`" class="hover:text-accent transition-colors duration-fast">
{{ equipment.name }}
</NuxtLink>
</h3>
<p class="text-sm text-text-muted mb-3 line-clamp-2 flex-grow">
{{ equipment.description || "Keine Beschreibung verfügbar" }}
</p>
<!-- Price & Button -->
<div class="flex items-center justify-between mt-2">
<span v-if="equipment.rental_price != null" class="text-sm font-semibold text-accent">
{{ formatPrice(equipment.rental_price) }} /Tag
</span>
<span v-else class="text-sm text-text-muted">
Preis auf Anfrage
</span>
<button
class="btn-primary text-xs px-3 py-2"
@click="$emit('add-to-cart', { id: equipment.id, name: equipment.name })"
>
Mietanfrage
</button>
</div>
</div>
</article>
</template>
<script setup lang="ts">
import type { EquipmentItem } from "~/composables/useEquipment";
const props = defineProps<{
equipment: EquipmentItem;
}>();
defineEmits<{
"add-to-cart": [{ id: number; name: string }];
}>();
function formatPrice(price: number | null): string {
if (price == null) return "—";
return new Intl.NumberFormat("de-DE", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(price);
}
</script>