fix: 1:1 prototype port – exact hms-* CSS, real logo, all components from app.js
This commit is contained in:
+68
-125
@@ -1,120 +1,45 @@
|
||||
<template>
|
||||
<div class="max-w-5xl mx-auto px-4 py-8">
|
||||
<!-- Breadcrumb -->
|
||||
<nav class="flex items-center gap-2 text-sm text-text-muted mb-6" aria-label="Breadcrumb">
|
||||
<NuxtLink to="/" class="hover:text-accent transition-colors duration-fast">Home</NuxtLink>
|
||||
<span class="text-secondary">/</span>
|
||||
<span class="text-text font-medium">Warenkorb</span>
|
||||
</nav>
|
||||
|
||||
<h1 class="text-2xl font-bold text-text mb-6" data-testid="warenkorb-title">Warenkorb</h1>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-if="isEmpty" class="flex flex-col items-center justify-center py-16 text-center" data-testid="warenkorb-empty">
|
||||
<div class="w-20 h-20 mb-4 flex items-center justify-center rounded-full bg-surface">
|
||||
<svg class="w-10 h-10 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>
|
||||
<h2 class="text-lg font-bold text-text mb-2">Ihr Warenkorb ist leer</h2>
|
||||
<p class="text-text-muted text-sm mb-6 max-w-sm">Sie haben noch keine Artikel zum Ausleihen ausgewählt.</p>
|
||||
<NuxtLink to="/mietkatalog" class="btn-primary" data-testid="warenkorb-empty-cta">
|
||||
Zum Mietkatalog
|
||||
</NuxtLink>
|
||||
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<h1 class="text-3xl sm:text-4xl font-bold mb-2" style="color: var(--text)">Mietanfrage</h1>
|
||||
<p class="mb-8" style="color: var(--secondary)">Überprüfen Sie Ihre Geräteauswahl und senden Sie die Anfrage ab.</p>
|
||||
<div v-if="submitted" class="hms-card p-8 text-center" role="status">
|
||||
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full mb-4" :style="{ background: 'var(--color-success-bg)', color: 'var(--color-success)' }"><svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg></div>
|
||||
<h2 class="text-xl font-semibold mb-2" style="color: var(--text)">Mietanfrage übermittelt</h2>
|
||||
<p class="mb-6" style="color: var(--secondary)">Vielen Dank! Wir melden uns innerhalb von 24 Stunden mit einem unverbindlichen Angebot.</p>
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center"><button @click="navigate('/mietkatalog')" class="hms-btn hms-btn-secondary">Weiter stöbern</button><button @click="navigate('/')" class="hms-btn hms-btn-primary">Zur Startseite</button></div>
|
||||
</div>
|
||||
|
||||
<!-- Cart Items -->
|
||||
<div v-else class="space-y-4">
|
||||
<div
|
||||
v-for="item in items"
|
||||
:key="item.equipment_id"
|
||||
class="card rounded-lg border border-border-default p-4 flex flex-col sm:flex-row gap-4"
|
||||
data-testid="warenkorb-item"
|
||||
>
|
||||
<!-- Item Image -->
|
||||
<div class="w-full sm:w-24 h-24 rounded-md overflow-hidden bg-surface 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-10 h-10 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>
|
||||
<EmptyState v-else-if="cartItems.length === 0" icon="🛒" title="Warenkorb ist leer" message="Fügen Sie Geräte aus dem Mietkatalog hinzu, um eine Mietanfrage zu stellen." action-label="Zum Mietkatalog" @action="navigate('/mietkatalog')" />
|
||||
<div v-else class="grid lg:grid-cols-3 gap-8">
|
||||
<div class="lg:col-span-2 space-y-4">
|
||||
<div v-for="(item, index) in cartItems" :key="index" class="hms-card p-4 sm:p-6">
|
||||
<div class="flex gap-4">
|
||||
<div class="w-20 h-20 rounded-lg flex items-center justify-center flex-shrink-0 text-2xl" style="background: var(--surface); color: var(--secondary)">📦</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-start justify-between gap-2"><div><h3 class="font-semibold text-sm sm:text-base" style="color: var(--text)">{{ item.name }}</h3><div class="text-xs mt-0.5" style="color: var(--secondary)">{{ item.equipment_id }}</div></div>
|
||||
<button @click="removeItem(item.equipment_id)" class="p-1" style="color: var(--secondary)" :aria-label="item.name + ' entfernen'"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg></button>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-3 mt-3"><div class="flex items-center gap-2"><button @click="decrementQuantity(item.equipment_id)" class="w-7 h-7 rounded border flex items-center justify-center" style="border-color: var(--border-strong); color: var(--text-muted)" aria-label="Anzahl verringern">−</button><span class="w-8 text-center text-sm font-medium" style="color: var(--text)">{{ item.quantity }}</span><button @click="incrementQuantity(item.equipment_id)" class="w-7 h-7 rounded border flex items-center justify-center" style="border-color: var(--border-strong); color: var(--text-muted)" aria-label="Anzahl erhöhen">+</button></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Item Info -->
|
||||
<div class="flex-grow flex flex-col justify-between">
|
||||
<div>
|
||||
<NuxtLink :to="`/mietkatalog/${item.equipment_id}`" class="text-base font-bold text-text hover:text-accent transition-colors duration-fast" data-testid="warenkorb-item-name">
|
||||
{{ item.name }}
|
||||
</NuxtLink>
|
||||
<p class="text-sm text-text-muted mt-1">
|
||||
<span v-if="item.rental_price != null" class="text-accent font-semibold">{{ formatPrice(item.rental_price) }} €/Tag</span>
|
||||
<span v-else>Preis auf Anfrage</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quantity Stepper -->
|
||||
<div class="flex items-center gap-3 shrink-0" data-testid="warenkorb-item-stepper">
|
||||
<button
|
||||
class="w-9 h-9 rounded-md bg-surface border border-border-default text-text-muted hover:text-accent hover:border-accent-border transition-colors duration-fast flex items-center justify-center"
|
||||
:aria-label="`Menge von ${item.name} verringern`"
|
||||
data-testid="warenkorb-item-decrement"
|
||||
@click="decrementQuantity(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="M5 12h14" />
|
||||
</svg>
|
||||
</button>
|
||||
<span class="text-base font-semibold text-text w-8 text-center" data-testid="warenkorb-item-quantity">{{ item.quantity }}</span>
|
||||
<button
|
||||
class="w-9 h-9 rounded-md bg-surface border border-border-default text-text-muted hover:text-accent hover:border-accent-border transition-colors duration-fast flex items-center justify-center"
|
||||
:aria-label="`Menge von ${item.name} erhöhen`"
|
||||
data-testid="warenkorb-item-increment"
|
||||
@click="incrementQuantity(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="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Remove Button -->
|
||||
<div class="flex items-center shrink-0">
|
||||
<button
|
||||
class="text-text-muted hover:text-error transition-colors duration-fast w-9 h-9 flex items-center justify-center"
|
||||
:aria-label="`${item.name} aus Warenkorb entfernen`"
|
||||
data-testid="warenkorb-item-remove"
|
||||
@click="removeItem(item.equipment_id)"
|
||||
>
|
||||
<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="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<button @click="clearCart" class="text-sm hover:text-[var(--color-error)]" style="color: var(--secondary)">Warenkorb leeren</button>
|
||||
</div>
|
||||
|
||||
<!-- Summary & Actions -->
|
||||
<div class="flex flex-col sm:flex-row items-center justify-between gap-4 pt-4 border-t border-border-default">
|
||||
<p class="text-sm text-text-muted">
|
||||
Insgesamt <span class="text-text font-bold">{{ totalCount }}</span> Artikel
|
||||
</p>
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
class="btn-secondary"
|
||||
data-testid="warenkorb-clear"
|
||||
@click="clearCart"
|
||||
>
|
||||
Warenkorb leeren
|
||||
</button>
|
||||
<NuxtLink to="/mietanfrage" class="btn-primary" data-testid="warenkorb-checkout">
|
||||
Zur Mietanfrage
|
||||
</NuxtLink>
|
||||
<div>
|
||||
<div class="hms-card p-6 sticky top-20">
|
||||
<h2 class="text-lg font-semibold mb-4" style="color: var(--text)">Anfrage senden</h2>
|
||||
<div class="text-sm mb-4 pb-4 border-b" :style="{ borderColor: 'var(--border)', color: 'var(--secondary)' }"><div class="flex justify-between mb-1"><span>Geräte gesamt:</span><span class="font-medium" style="color: var(--text)">{{ totalCount }}</span></div><div class="flex justify-between"><span>Positionen:</span><span class="font-medium" style="color: var(--text)">{{ cartItems.length }}</span></div></div>
|
||||
<form @submit.prevent="submitRequest" novalidate>
|
||||
<div class="space-y-3">
|
||||
<div><label for="req-name" class="block text-xs font-medium mb-1" style="color: var(--secondary)">Name <span style="color: var(--color-error)">*</span></label><input id="req-name" v-model="form.name" type="text" :class="['hms-input text-sm', errors.name ? 'hms-input-error' : '']" placeholder="Ihr Name" /><p v-if="errors.name" class="text-xs mt-1" style="color: var(--color-error)">{{ errors.name }}</p></div>
|
||||
<div><label for="req-email" class="block text-xs font-medium mb-1" style="color: var(--secondary)">E-Mail <span style="color: var(--color-error)">*</span></label><input id="req-email" v-model="form.email" type="email" :class="['hms-input text-sm', errors.email ? 'hms-input-error' : '']" placeholder="ihre@email.de" /><p v-if="errors.email" class="text-xs mt-1" style="color: var(--color-error)">{{ errors.email }}</p></div>
|
||||
<div><label for="req-phone" class="block text-xs font-medium mb-1" style="color: var(--secondary)">Telefon</label><input id="req-phone" v-model="form.phone" type="tel" class="hms-input text-sm" placeholder="+49 ..." /></div>
|
||||
<div><label for="req-date" class="block text-xs font-medium mb-1" style="color: var(--secondary)">Veranstaltungsdatum <span style="color: var(--color-error)">*</span></label><input id="req-date" v-model="form.event_date" type="date" :class="['hms-input text-sm', errors.event_date ? 'hms-input-error' : '']" /><p v-if="errors.event_date" class="text-xs mt-1" style="color: var(--color-error)">{{ errors.event_date }}</p></div>
|
||||
<div><label for="req-location" class="block text-xs font-medium mb-1" style="color: var(--secondary)">Veranstaltungsort <span style="color: var(--color-error)">*</span></label><input id="req-location" v-model="form.event_location" type="text" :class="['hms-input text-sm', errors.event_location ? 'hms-input-error' : '']" placeholder="Ort / Location" /><p v-if="errors.event_location" class="text-xs mt-1" style="color: var(--color-error)">{{ errors.event_location }}</p></div>
|
||||
<div><label for="req-message" class="block text-xs font-medium mb-1" style="color: var(--secondary)">Anmerkung</label><textarea id="req-message" v-model="form.message" rows="3" class="hms-input text-sm" placeholder="Zusätzliche Informationen..."></textarea></div>
|
||||
<button type="submit" :disabled="submitting" class="hms-btn hms-btn-primary w-full py-3"><span v-if="submitting" class="hms-spinner" style="width:18px;height:18px;border-width:2px"></span><span v-else>Anfrage absenden</span></button>
|
||||
<p class="text-xs text-center" style="color: var(--secondary)">Unverbindlich – Angebot innerhalb 24 Std.</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -122,20 +47,38 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useCart } from "~/composables/useCart";
|
||||
import { reactive, ref } from 'vue'
|
||||
import { useCart } from '~/composables/useCart'
|
||||
|
||||
const { items, totalCount, isEmpty, removeItem, incrementQuantity, decrementQuantity, clearCart } = useCart();
|
||||
const { items: cartItems, totalCount, removeItem, incrementQuantity, decrementQuantity, clearCart } = useCart()
|
||||
|
||||
function formatPrice(price: number | null): string {
|
||||
if (price == null) return "—";
|
||||
return new Intl.NumberFormat("de-DE", {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
}).format(price);
|
||||
const submitting = ref(false)
|
||||
const submitted = ref(false)
|
||||
const form = reactive({ name: '', email: '', phone: '', event_date: '', event_location: '', message: '' })
|
||||
const errors = reactive<Record<string, string>>({})
|
||||
|
||||
function navigate(route: string) {
|
||||
navigateTo(route)
|
||||
if (import.meta.client) window.scrollTo(0, 0)
|
||||
}
|
||||
function validate(): boolean {
|
||||
const e: Record<string, string> = {}
|
||||
if (!form.name.trim()) e.name = 'Name ist erforderlich'
|
||||
if (!form.email.trim()) e.email = 'E-Mail ist erforderlich'
|
||||
else if (!/^\S+@\S+\.\S+$/.test(form.email)) e.email = 'Ungültige E-Mail'
|
||||
if (!form.event_date) e.event_date = 'Veranstaltungsdatum erforderlich'
|
||||
if (!form.event_location.trim()) e.event_location = 'Veranstaltungsort erforderlich'
|
||||
Object.keys(errors).forEach(k => delete errors[k])
|
||||
Object.assign(errors, e)
|
||||
return Object.keys(e).length === 0
|
||||
}
|
||||
function submitRequest() {
|
||||
if (!validate()) return
|
||||
submitting.value = true
|
||||
setTimeout(() => {
|
||||
submitting.value = false
|
||||
submitted.value = true
|
||||
clearCart()
|
||||
}, 1800)
|
||||
}
|
||||
|
||||
useHead({
|
||||
title: "Warenkorb – HMS Licht & Ton",
|
||||
meta: [{ name: "robots", content: "noindex, nofollow, noarchive, nosnippet" }],
|
||||
});
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user