feat(T07): Warenkorb & Mietanfrage frontend – cart store, drawer, pages, form, tests
This commit is contained in:
@@ -21,7 +21,7 @@
|
||||
<NuxtLink to="/kontakt" class="nav-link">Kontakt</NuxtLink>
|
||||
</nav>
|
||||
|
||||
<!-- Desktop Right: Phone + Social -->
|
||||
<!-- Desktop Right: Phone + Social + Cart -->
|
||||
<div class="hidden md:flex items-center gap-4">
|
||||
<a
|
||||
href="tel:+491726264796"
|
||||
@@ -34,6 +34,23 @@
|
||||
<span>+49 172 6264796</span>
|
||||
</a>
|
||||
|
||||
<!-- Cart Icon with Counter Badge -->
|
||||
<button
|
||||
class="relative flex items-center justify-center w-10 h-10 text-text-muted hover:text-accent transition-colors duration-fast min-h-44px"
|
||||
aria-label="Warenkorb öffnen"
|
||||
data-testid="header-cart-button"
|
||||
@click="cartDrawerOpen = true"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 3h1.386c.51 0 .955.343 1.087.835l.383 1.437M7.5 14.25a3 3 0 00-3 3h15.75m-12.75-3h11.218c1.121-2.3 2.1-4.684 2.924-7.138a60.114 60.114 0 00-16.536-1.84M7.5 14.25L5.106 5.272M6 20.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm12.75 0a.75.75 0 11-1.5 0 .75.75 0 011.5 0z" />
|
||||
</svg>
|
||||
<span
|
||||
v-if="cartCount > 0"
|
||||
class="absolute -top-1 -right-1 bg-accent text-white text-xs font-bold rounded-full w-5 h-5 flex items-center justify-center"
|
||||
data-testid="header-cart-count"
|
||||
>{{ cartCount }}</span>
|
||||
</button>
|
||||
|
||||
<a
|
||||
href="https://www.facebook.com/hms.licht.ton"
|
||||
target="_blank"
|
||||
@@ -61,6 +78,23 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Cart Button -->
|
||||
<button
|
||||
class="md:hidden relative flex items-center justify-center w-11 h-11 text-text-muted hover:text-accent transition-colors duration-fast"
|
||||
aria-label="Warenkorb öffnen"
|
||||
data-testid="header-cart-button-mobile"
|
||||
@click="cartDrawerOpen = true"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 3h1.386c.51 0 .955.343 1.087.835l.383 1.437M7.5 14.25a3 3 0 00-3 3h15.75m-12.75-3h11.218c1.121-2.3 2.1-4.684 2.924-7.138a60.114 60.114 0 00-16.536-1.84M7.5 14.25L5.106 5.272M6 20.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm12.75 0a.75.75 0 11-1.5 0 .75.75 0 011.5 0z" />
|
||||
</svg>
|
||||
<span
|
||||
v-if="cartCount > 0"
|
||||
class="absolute -top-1 -right-1 bg-accent text-white text-xs font-bold rounded-full w-5 h-5 flex items-center justify-center"
|
||||
data-testid="header-cart-count-mobile"
|
||||
>{{ cartCount }}</span>
|
||||
</button>
|
||||
|
||||
<!-- Mobile Burger Button -->
|
||||
<button
|
||||
class="md:hidden flex items-center justify-center w-11 h-11 text-text-muted hover:text-accent transition-colors duration-fast"
|
||||
@@ -116,10 +150,18 @@
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<!-- Cart Drawer -->
|
||||
<CartDrawer v-model="cartDrawerOpen" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useCart } from "~/composables/useCart";
|
||||
|
||||
const { totalCount: cartCount } = useCart();
|
||||
|
||||
const mobileMenuOpen = ref(false);
|
||||
const cartDrawerOpen = ref(false);
|
||||
|
||||
function toggleMobileMenu(): void {
|
||||
mobileMenuOpen.value = !mobileMenuOpen.value;
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<!-- Backdrop -->
|
||||
<Transition name="cart-backdrop">
|
||||
<div
|
||||
v-if="modelValue"
|
||||
class="fixed inset-0 z-[60] bg-black/60"
|
||||
data-testid="cart-drawer-backdrop"
|
||||
@click="close"
|
||||
/>
|
||||
</Transition>
|
||||
|
||||
<!-- Drawer Panel -->
|
||||
<Transition name="cart-drawer">
|
||||
<aside
|
||||
v-if="modelValue"
|
||||
class="fixed z-[61] bg-panel border-l border-border-default shadow-xl flex flex-col"
|
||||
:class="isMobile ? 'bottom-0 left-0 right-0 rounded-t-lg max-h-[80vh]' : 'top-0 right-0 bottom-0 w-96'"
|
||||
role="dialog"
|
||||
aria-label="Warenkorb"
|
||||
data-testid="cart-drawer"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between px-4 py-3 border-b border-border-default shrink-0">
|
||||
<h2 class="text-lg font-bold text-text" data-testid="cart-drawer-title">
|
||||
Warenkorb
|
||||
<span v-if="totalCount > 0" class="text-sm font-normal text-text-muted ml-1">({{ totalCount }})</span>
|
||||
</h2>
|
||||
<button
|
||||
class="text-text-muted hover:text-accent transition-colors duration-fast w-8 h-8 flex items-center justify-center"
|
||||
aria-label="Warenkorb schließen"
|
||||
@click="close"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-if="isEmpty" class="flex-1 flex flex-col items-center justify-center px-6 py-12 text-center">
|
||||
<div class="w-16 h-16 mb-4 flex items-center justify-center rounded-full bg-surface">
|
||||
<svg class="w-8 h-8 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 3h1.386c.51 0 .955.343 1.087.835l.383 1.437M7.5 14.25a3 3 0 00-3 3h15.75m-12.75-3h11.218c1.121-2.3 2.1-4.684 2.924-7.138a60.114 60.114 0 00-16.536-1.84M7.5 14.25L5.106 5.272M6 20.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm12.75 0a.75.75 0 11-1.5 0 .75.75 0 011.5 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<p class="text-text-muted text-sm mb-4">Ihr Warenkorb ist leer.</p>
|
||||
<button class="btn-primary" @click="close">
|
||||
Weiter einkaufen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Items List -->
|
||||
<div v-else class="flex-1 overflow-y-auto px-4 py-3 space-y-3">
|
||||
<div
|
||||
v-for="item in items"
|
||||
:key="item.equipment_id"
|
||||
class="flex items-center gap-3 bg-surface rounded-md p-3"
|
||||
data-testid="cart-drawer-item"
|
||||
>
|
||||
<!-- Item Image -->
|
||||
<div class="w-12 h-12 rounded-md overflow-hidden bg-card shrink-0">
|
||||
<img
|
||||
v-if="item.image_url"
|
||||
:src="item.image_url"
|
||||
:alt="item.name"
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
<div v-else class="w-full h-full flex items-center justify-center">
|
||||
<svg class="w-6 h-6 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>
|
||||
</div>
|
||||
|
||||
<!-- Item Info -->
|
||||
<div class="flex-grow min-w-0">
|
||||
<NuxtLink :to="`/mietkatalog/${item.equipment_id}`" class="text-sm font-medium text-text hover:text-accent transition-colors duration-fast line-clamp-1" @click="close">
|
||||
{{ item.name }}
|
||||
</NuxtLink>
|
||||
<p class="text-xs text-text-muted">
|
||||
{{ item.quantity }}×
|
||||
<span v-if="item.rental_price != null">{{ formatPrice(item.rental_price) }} €/Tag</span>
|
||||
<span v-else>Preis auf Anfrage</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Remove Button -->
|
||||
<button
|
||||
class="text-text-muted hover:text-error transition-colors duration-fast w-8 h-8 flex items-center justify-center shrink-0"
|
||||
:aria-label="`${item.name} entfernen`"
|
||||
data-testid="cart-drawer-remove"
|
||||
@click="removeItem(item.equipment_id)"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div v-if="!isEmpty" class="shrink-0 border-t border-border-default px-4 py-3 space-y-3">
|
||||
<NuxtLink to="/warenkorb" class="btn-secondary w-full" @click="close" data-testid="cart-drawer-view-cart">
|
||||
Warenkorb ansehen
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/mietanfrage" class="btn-primary w-full" @click="close" data-testid="cart-drawer-checkout">
|
||||
Zur Mietanfrage
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</aside>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useCart } from "~/composables/useCart";
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
"update:modelValue": [boolean];
|
||||
}>();
|
||||
|
||||
const { items, totalCount, isEmpty, removeItem } = useCart();
|
||||
|
||||
const isMobile = ref(false);
|
||||
|
||||
function checkViewport(): void {
|
||||
if (import.meta.client) {
|
||||
isMobile.value = window.innerWidth < 768;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
checkViewport();
|
||||
window.addEventListener("resize", checkViewport);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (import.meta.client) {
|
||||
window.removeEventListener("resize", checkViewport);
|
||||
}
|
||||
});
|
||||
|
||||
function close(): void {
|
||||
emit("update:modelValue", false);
|
||||
}
|
||||
|
||||
function formatPrice(price: number | null): string {
|
||||
if (price == null) return "—";
|
||||
return new Intl.NumberFormat("de-DE", {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
}).format(price);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.cart-backdrop-enter-active,
|
||||
.cart-backdrop-leave-active {
|
||||
transition: opacity 0.25s ease;
|
||||
}
|
||||
.cart-backdrop-enter-from,
|
||||
.cart-backdrop-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.cart-drawer-enter-active,
|
||||
.cart-drawer-leave-active {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
.cart-drawer-enter-from,
|
||||
.cart-drawer-leave-to {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user