Enhance checkout flow with new components and validation

- Added AddressForm and CheckoutForm components for user input during checkout.
- Implemented validation using Zod and VeeValidate for billing address fields.
- Created OrderSummary and MockPayPalButton components for order confirmation and payment simulation.
- Updated CartSheet and CartSidebar to navigate to the new checkout page at '/kasse'.
- Introduced new API endpoints for validating checkout data and creating orders.
- Enhanced user experience with responsive design and error handling.

These changes complete the checkout functionality, allowing users to enter billing information, simulate payment, and confirm orders.
This commit is contained in:
Bastian Masanek
2025-11-03 15:38:16 +01:00
parent 47fe14c6cc
commit 527379a2cd
44 changed files with 4957 additions and 142 deletions

View File

@@ -0,0 +1,219 @@
<script setup lang="ts">
/**
* Order Confirmation Page (/bestellung/bestaetigen/[orderId])
*
* Features:
* - Requires authentication (middleware: auth)
* - Fetches order details from /api/orders/[orderId]
* - Validates order belongs to user (server-side)
* - Validates order status is 'pending'
* - Shows OrderSummary component
* - Shows billing address
* - Warning text before final confirmation
* - "Jetzt verbindlich bestellen" button
* - Redirects to success page after confirmation
*/
definePageMeta({
middleware: 'auth',
layout: 'default',
})
const route = useRoute()
const orderId = computed(() => route.params.orderId as string)
// Order data
const order = ref<any>(null)
const isLoading = ref(false)
const isConfirming = ref(false)
const error = ref<string | null>(null)
// Fetch order details
async function fetchOrder() {
if (!orderId.value) return
isLoading.value = true
error.value = null
try {
order.value = await $fetch(`/api/orders/${orderId.value}`)
// Check order status
if (order.value.status === 'completed') {
// Order already completed - redirect to success page
navigateTo(`/bestellung/erfolg/${orderId.value}`)
return
}
if (order.value.status !== 'pending') {
error.value = `Bestellung kann nicht bestätigt werden. Status: ${order.value.status}`
}
} catch (err: any) {
console.error('Failed to fetch order:', err)
if (err.statusCode === 404) {
error.value = 'Bestellung nicht gefunden'
} else if (err.statusCode === 403) {
error.value = 'Du hast keine Berechtigung, diese Bestellung zu sehen'
} else {
error.value = 'Fehler beim Laden der Bestellung'
}
// Redirect to cart after 3 seconds
setTimeout(() => {
navigateTo('/warenkorb')
}, 3000)
} finally {
isLoading.value = false
}
}
// Confirm order
async function confirmOrder() {
if (!orderId.value) return
isConfirming.value = true
error.value = null
try {
const response = await $fetch(`/api/orders/confirm/${orderId.value}`, {
method: 'POST',
})
if (response.success) {
// Redirect to success page
navigateTo(`/bestellung/erfolg/${orderId.value}`)
}
} catch (err: any) {
console.error('Failed to confirm order:', err)
error.value =
err.data?.message ||
'Fehler beim Bestätigen der Bestellung. Bitte versuche es erneut.'
} finally {
isConfirming.value = false
}
}
// Fetch order on mount
onMounted(() => {
fetchOrder()
})
</script>
<template>
<div>
<CommonHeader />
<div class="container mx-auto px-4 py-8 max-w-4xl">
<!-- Page Header -->
<div class="mb-8 text-center">
<h1 class="text-4xl font-bold text-white mb-2">Bestellung bestätigen</h1>
<p class="text-white/70">Bitte überprüfe deine Bestellung vor der finalen Bestätigung</p>
</div>
<!-- Error Alert -->
<Alert v-if="error" variant="destructive" class="mb-6">
<AlertTitle>Fehler</AlertTitle>
<AlertDescription>{{ error }}</AlertDescription>
</Alert>
<!-- Loading State -->
<div v-if="isLoading" class="text-center py-12">
<div
class="animate-spin rounded-full h-12 w-12 border-4 border-white/20 border-t-white mx-auto mb-4"
/>
<p class="text-white/60">Lade Bestellung...</p>
</div>
<!-- Order Content -->
<div v-else-if="order" class="space-y-6">
<!-- Order Summary Card -->
<Card class="p-6">
<OrderSummary :order="order" :show-address="true" />
</Card>
<!-- Warning Notice -->
<Alert class="border-yellow-500/50 bg-yellow-500/10">
<div class="flex items-start gap-3">
<svg
class="w-5 h-5 text-yellow-500 mt-0.5 flex-shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
></path>
</svg>
<div>
<AlertTitle class="text-yellow-500">Wichtiger Hinweis</AlertTitle>
<AlertDescription class="text-yellow-100/90">
Bitte überprüfe alle Angaben sorgfältig. Nach der Bestätigung ist die
Bestellung verbindlich und kann nicht mehr geändert werden.
</AlertDescription>
</div>
</div>
</Alert>
<!-- Confirmation Button -->
<Card class="p-6">
<div class="space-y-4">
<Button
@click="confirmOrder"
:disabled="isConfirming"
class="w-full bg-gradient-button bg-size-300 bg-left hover:bg-right transition-all duration-300 font-bold text-white shadow-lg hover:shadow-2xl"
size="lg"
>
<span v-if="!isConfirming" class="flex items-center justify-center gap-2">
<svg
class="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
<span>Jetzt verbindlich bestellen</span>
</span>
<span v-else class="flex items-center gap-2">
<div
class="animate-spin rounded-full h-4 w-4 border-2 border-white/20 border-t-white"
/>
Bestätigung läuft...
</span>
</Button>
<p class="text-xs text-white/60 text-center">
Mit dem Klick auf "Jetzt verbindlich bestellen" akzeptierst du unsere
<NuxtLink to="/agb" class="text-experimenta-accent hover:underline">
AGB
</NuxtLink>
und
<NuxtLink to="/datenschutz" class="text-experimenta-accent hover:underline">
Datenschutzerklärung
</NuxtLink>
.
</p>
</div>
</Card>
<!-- Back Link -->
<div class="text-center pt-4">
<NuxtLink to="/warenkorb" class="text-sm text-experimenta-accent hover:underline">
Zurück zum Warenkorb
</NuxtLink>
</div>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,239 @@
<script setup lang="ts">
/**
* Order Success Page (/bestellung/erfolg/[orderId])
*
* Features:
* - Requires authentication (middleware: auth)
* - Fetches order details from /api/orders/[orderId]
* - Validates order belongs to user (server-side)
* - Validates order status is 'completed'
* - Shows success message and animation
* - Shows order number
* - Shows OrderSummary component (read-only)
* - Links to homepage and product pages
*/
definePageMeta({
middleware: 'auth',
layout: 'default',
})
const route = useRoute()
const orderId = computed(() => route.params.orderId as string)
// Order data
const order = ref<any>(null)
const isLoading = ref(false)
const error = ref<string | null>(null)
// Fetch order details
async function fetchOrder() {
if (!orderId.value) return
isLoading.value = true
error.value = null
try {
order.value = await $fetch(`/api/orders/${orderId.value}`)
// Check order status
if (order.value.status !== 'completed') {
error.value = `Diese Bestellung wurde noch nicht abgeschlossen. Status: ${order.value.status}`
// Redirect to confirmation page if still pending
if (order.value.status === 'pending') {
setTimeout(() => {
navigateTo(`/bestellung/bestaetigen/${orderId.value}`)
}, 2000)
}
}
} catch (err: any) {
console.error('Failed to fetch order:', err)
if (err.statusCode === 404) {
error.value = 'Bestellung nicht gefunden'
} else if (err.statusCode === 403) {
error.value = 'Du hast keine Berechtigung, diese Bestellung zu sehen'
} else {
error.value = 'Fehler beim Laden der Bestellung'
}
// Redirect to homepage after 3 seconds
setTimeout(() => {
navigateTo('/')
}, 3000)
} finally {
isLoading.value = false
}
}
// Fetch order on mount
onMounted(() => {
fetchOrder()
})
</script>
<template>
<div>
<CommonHeader />
<div class="container mx-auto px-4 py-8 max-w-4xl">
<!-- Error Alert -->
<Alert v-if="error" variant="destructive" class="mb-6">
<AlertTitle>Fehler</AlertTitle>
<AlertDescription>{{ error }}</AlertDescription>
</Alert>
<!-- Loading State -->
<div v-if="isLoading" class="text-center py-12">
<div
class="animate-spin rounded-full h-12 w-12 border-4 border-white/20 border-t-white mx-auto mb-4"
/>
<p class="text-white/60">Lade Bestellung...</p>
</div>
<!-- Success Content -->
<div v-else-if="order && order.status === 'completed'" class="space-y-8">
<!-- Success Header with Animation -->
<div class="text-center space-y-4 py-8">
<!-- Success Icon (animated checkmark) -->
<div class="flex justify-center mb-6">
<div
class="rounded-full bg-green-500/20 p-6 border-4 border-green-500/50 animate-pulse"
>
<svg
class="w-16 h-16 text-green-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 13l4 4L19 7"
></path>
</svg>
</div>
</div>
<!-- Success Message -->
<h1 class="text-4xl font-bold text-white mb-2">
Vielen Dank für deine Bestellung!
</h1>
<p class="text-xl text-white/70">Deine Bestellung wurde erfolgreich abgeschlossen.</p>
<!-- Order Number -->
<div class="inline-block mt-4 px-6 py-3 bg-white/5 rounded-lg border border-white/10">
<p class="text-sm text-white/60">Bestellnummer</p>
<p class="text-2xl font-mono font-bold text-experimenta-accent">
{{ order.orderNumber }}
</p>
</div>
</div>
<!-- Next Steps Info -->
<Alert class="border-blue-500/50 bg-blue-500/10">
<div class="flex items-start gap-3">
<svg
class="w-5 h-5 text-blue-400 mt-0.5 flex-shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
<div>
<AlertTitle class="text-blue-400">Wie geht es weiter?</AlertTitle>
<AlertDescription class="text-blue-100/90 space-y-2">
<p>
Du erhältst in Kürze eine Bestätigungs-E-Mail mit allen Details zu deiner
Bestellung.
</p>
<p>
Deine Makerspace-Jahreskarte wird bearbeitet und steht dir bald zur
Verfügung.
</p>
</AlertDescription>
</div>
</div>
</Alert>
<!-- Order Summary Card -->
<Card class="p-6">
<OrderSummary :order="order" :show-address="true" />
</Card>
<!-- Action Buttons -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<NuxtLink to="/">
<Button
variant="outline"
class="w-full border-white/20 hover:bg-white/10 text-white"
size="lg"
>
<svg
class="w-5 h-5 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
></path>
</svg>
Zurück zur Startseite
</Button>
</NuxtLink>
<NuxtLink to="/experimenta">
<Button
class="w-full bg-gradient-button bg-size-300 bg-left hover:bg-right transition-all duration-300 font-bold text-white shadow-lg hover:shadow-2xl"
size="lg"
>
<svg
class="w-5 h-5 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z"
></path>
</svg>
Weitere Produkte kaufen
</Button>
</NuxtLink>
</div>
<!-- Support Info -->
<div class="text-center pt-4 space-y-2">
<p class="text-sm text-white/60">
Fragen zu deiner Bestellung? Kontaktiere uns gerne:
</p>
<a
href="mailto:info@experimenta.science"
class="text-sm text-experimenta-accent hover:underline"
>
info@experimenta.science
</a>
</div>
</div>
</div>
</div>
</template>

260
app/pages/kasse.vue Normal file
View File

@@ -0,0 +1,260 @@
<script setup lang="ts">
/**
* Checkout Page (/kasse)
*
* Features:
* - Requires authentication (middleware: auth)
* - Shows CheckoutForm with billing address
* - Shows CartSummary in right sidebar (desktop) / top (mobile)
* - Redirects to /warenkorb if cart is empty
* - Creates order on form submit
* - Redirects to /zahlung?orderId={orderId} after order creation
*/
// Type for checkout data (matches server schema)
type CheckoutData = {
salutation: 'male' | 'female' | 'other'
firstName: string
lastName: string
dateOfBirth: string
street: string
postCode: string
city: string
countryCode: string
saveAddress?: boolean
}
definePageMeta({
middleware: 'auth',
layout: 'default',
})
const { items, total, itemCount, fetchCart } = useCart()
// Loading states
const isLoading = ref(true) // Start as true to prevent premature redirect
const isCreatingOrder = ref(false)
const cartLoaded = ref(false) // Track if initial cart fetch completed
// Error state
const error = ref<string | null>(null)
// Fetch cart on mount
onMounted(async () => {
isLoading.value = true
try {
await fetchCart()
cartLoaded.value = true // Mark cart as loaded
} catch (err) {
console.error('Failed to fetch cart:', err)
error.value = 'Fehler beim Laden des Warenkorbs'
cartLoaded.value = true // Mark as loaded even on error to allow redirect
} finally {
isLoading.value = false
}
})
// Redirect to homepage if cart is empty (only after cart is loaded)
watchEffect(() => {
if (cartLoaded.value && !isLoading.value && itemCount.value === 0) {
navigateTo('/')
}
})
// Handle checkout form submission
async function handleCheckout(checkoutData: CheckoutData) {
isCreatingOrder.value = true
error.value = null
try {
// Create order via API
const response = await $fetch<{
success: boolean
orderId: string
orderNumber: string
message: string
}>('/api/orders/create', {
method: 'POST',
body: checkoutData,
})
if (response.success) {
// Redirect to payment page with order ID
navigateTo(`/zahlung?orderId=${response.orderId}`)
}
} catch (err: any) {
console.error('Order creation failed:', err)
error.value =
err.data?.message || 'Fehler beim Erstellen der Bestellung. Bitte versuche es erneut.'
} finally {
isCreatingOrder.value = false
}
}
</script>
<template>
<div>
<CommonHeader />
<div class="container mx-auto px-4 py-8 max-w-7xl">
<!-- Page Header -->
<div class="mb-8">
<h1 class="text-4xl font-bold text-white mb-2">Zur Kasse</h1>
<p class="text-white/70">
Bitte gib deine Rechnungsadresse ein, um die Bestellung abzuschließen.
</p>
</div>
<!-- Error Alert -->
<Alert v-if="error" variant="destructive" class="mb-6">
<AlertTitle>Fehler</AlertTitle>
<AlertDescription>{{ error }}</AlertDescription>
</Alert>
<!-- Loading State -->
<div v-if="isLoading" class="text-center py-12">
<div
class="animate-spin rounded-full h-12 w-12 border-4 border-white/20 border-t-white mx-auto mb-4"
/>
<p class="text-white/60">Lade Warenkorb...</p>
</div>
<!-- Checkout Layout -->
<div v-else class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- Left Column: Checkout Form (2/3 width on desktop) -->
<div class="lg:col-span-2">
<Card class="p-6">
<h2 class="text-2xl font-bold text-white mb-6">Rechnungsadresse</h2>
<CheckoutForm :loading="isCreatingOrder" @submit="handleCheckout" />
</Card>
</div>
<!-- Right Column: Cart Summary (1/3 width on desktop, sticky) -->
<div class="lg:col-span-1">
<div class="lg:sticky lg:top-4">
<!-- Mobile: Show summary at top, Desktop: Show in sidebar -->
<div class="lg:hidden mb-8">
<CartSummary
:items="items"
:total="total"
:loading="isCreatingOrder"
@checkout="() => {}"
class="hidden"
/>
<!-- Simplified summary for mobile -->
<Card class="p-4 bg-white/5 border-white/10">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-white/60">
{{ itemCount }} {{ itemCount === 1 ? 'Artikel' : 'Artikel' }}
</p>
<p class="text-2xl font-bold text-experimenta-accent">
{{
new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR',
}).format(total)
}}
</p>
</div>
<NuxtLink
to="/warenkorb"
class="text-sm text-experimenta-accent hover:underline"
>
Bearbeiten
</NuxtLink>
</div>
</Card>
</div>
<!-- Desktop: Full cart summary -->
<div class="hidden lg:block">
<Card class="p-6">
<h2 class="text-xl font-bold text-white mb-4">Deine Bestellung</h2>
<!-- Items List -->
<div class="space-y-3 mb-6">
<div
v-for="item in items"
:key="item.id"
class="flex items-start justify-between gap-3 pb-3 border-b border-white/10"
>
<div class="flex-1">
<p class="text-sm font-medium text-white">{{ item.product.name }}</p>
<p class="text-xs text-white/60">
{{ item.quantity }}x
{{
new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR',
}).format(Number.parseFloat(item.product.price))
}}
</p>
</div>
<p class="text-sm font-semibold text-white">
{{
new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR',
}).format(item.subtotal)
}}
</p>
</div>
</div>
<!-- Totals -->
<div class="space-y-2 pt-4 border-t border-white/20">
<div class="flex items-center justify-between text-white/80">
<span class="text-sm">Zwischensumme</span>
<span class="font-medium">
{{
new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR',
}).format(total)
}}
</span>
</div>
<div class="flex items-center justify-between text-white/60 text-sm">
<span>inkl. MwSt. (7%)</span>
<span>
{{
new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR',
}).format(total * (0.07 / 1.07))
}}
</span>
</div>
<div
class="flex items-center justify-between pt-3 border-t border-white/20"
>
<span class="text-lg font-bold text-white">Gesamt</span>
<span class="text-2xl font-bold text-experimenta-accent">
{{
new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR',
}).format(total)
}}
</span>
</div>
</div>
<!-- Edit Cart Link -->
<div class="mt-6 pt-6 border-t border-white/20">
<NuxtLink
to="/warenkorb"
class="text-sm text-experimenta-accent hover:underline"
>
Warenkorb bearbeiten
</NuxtLink>
</div>
</Card>
</div>
</div>
</div>
</div>
</div>
</div>
</template>

186
app/pages/zahlung.vue Normal file
View File

@@ -0,0 +1,186 @@
<script setup lang="ts">
/**
* Payment Mock Page (/zahlung)
*
* Features:
* - Requires authentication (middleware: auth)
* - Shows order total
* - Shows MockPayPalButton component
* - Redirects to order confirmation after "payment" success
* - Query param: orderId (required)
*/
definePageMeta({
middleware: 'auth',
layout: 'default',
})
const route = useRoute()
// Get order ID from query params
const orderId = computed(() => route.query.orderId as string | undefined)
// Order data
const order = ref<any>(null)
const isLoading = ref(false)
const error = ref<string | null>(null)
// Redirect to cart if no order ID
watchEffect(() => {
if (!orderId.value) {
navigateTo('/warenkorb')
}
})
// Fetch order details
onMounted(async () => {
if (!orderId.value) return
isLoading.value = true
error.value = null
try {
order.value = await $fetch(`/api/orders/${orderId.value}`)
} catch (err: any) {
console.error('Failed to fetch order:', err)
error.value = 'Bestellung nicht gefunden'
// Redirect to cart after 3 seconds
setTimeout(() => {
navigateTo('/warenkorb')
}, 3000)
} finally {
isLoading.value = false
}
})
// Handle payment success
function handlePaymentSuccess() {
if (!orderId.value) return
// Redirect to order confirmation page
navigateTo(`/bestellung/bestaetigen/${orderId.value}`)
}
</script>
<template>
<div>
<CommonHeader />
<div class="container mx-auto px-4 py-8 max-w-2xl">
<!-- Page Header -->
<div class="mb-8 text-center">
<h1 class="text-4xl font-bold text-white mb-2">Zahlung</h1>
<p class="text-white/70">Schließe deine Bestellung mit PayPal ab</p>
</div>
<!-- Error Alert -->
<Alert v-if="error" variant="destructive" class="mb-6">
<AlertTitle>Fehler</AlertTitle>
<AlertDescription>
{{ error }}. Du wirst zum Warenkorb weitergeleitet...
</AlertDescription>
</Alert>
<!-- Loading State -->
<div v-if="isLoading" class="text-center py-12">
<div
class="animate-spin rounded-full h-12 w-12 border-4 border-white/20 border-t-white mx-auto mb-4"
/>
<p class="text-white/60">Lade Bestellung...</p>
</div>
<!-- Payment Content -->
<div v-else-if="order" class="space-y-6">
<!-- Order Summary Card -->
<Card class="p-6">
<div class="space-y-4">
<div class="flex items-center justify-between pb-4 border-b border-white/20">
<div>
<h2 class="text-lg font-semibold text-white">Bestellnummer</h2>
<p class="text-sm font-mono text-white/80">{{ order.orderNumber }}</p>
</div>
<div class="text-right">
<p class="text-sm text-white/60">Zu zahlen</p>
<p class="text-2xl font-bold text-experimenta-accent">
{{
new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR',
}).format(Number.parseFloat(order.totalAmount))
}}
</p>
</div>
</div>
<!-- Order Items Summary -->
<div class="space-y-2">
<h3 class="text-sm font-medium text-white/80">Artikel in dieser Bestellung:</h3>
<div class="space-y-1">
<div
v-for="item in order.items"
:key="item.id"
class="flex items-center justify-between text-sm"
>
<span class="text-white/70">
{{ item.quantity }}x
{{ item.productSnapshot?.name || item.product?.name }}
</span>
<span class="text-white/80 font-medium">
{{
new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR',
}).format(item.subtotal)
}}
</span>
</div>
</div>
</div>
</div>
</Card>
<!-- Payment Button Card -->
<Card class="p-6">
<h2 class="text-lg font-semibold text-white mb-4">Zahlungsmethode</h2>
<MockPayPalButton
:order-id="orderId!"
:amount="Number.parseFloat(order.totalAmount)"
@success="handlePaymentSuccess"
/>
</Card>
<!-- Security Note -->
<div class="text-center space-y-2 pt-4">
<div class="flex items-center justify-center gap-2 text-sm text-white/60">
<svg
class="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
></path>
</svg>
<span>Sichere Verbindung</span>
</div>
<p class="text-xs text-white/40">
Deine Zahlung wird über eine sichere SSL-Verbindung verarbeitet.
</p>
</div>
<!-- Back Link -->
<div class="text-center pt-4">
<NuxtLink to="/kasse" class="text-sm text-experimenta-accent hover:underline">
Zurück zur Kasse
</NuxtLink>
</div>
</div>
</div>
</div>
</template>