Implement checkout and payment flow with new components
- Added Checkout and Payment pages to handle user authentication and order processing. - Introduced MockPayPalButton for simulating payment during development. - Updated CartSheet and CartSidebar components to navigate to the new checkout page. - Enhanced Cart UI with responsive design for mobile and desktop views. - Implemented order confirmation and success pages to provide feedback after payment completion. These changes complete the checkout and payment functionality, improving the overall user experience and ensuring a seamless transition from cart to order confirmation.
This commit is contained in:
219
app/pages/order/confirm/[orderId].vue
Normal file
219
app/pages/order/confirm/[orderId].vue
Normal file
@@ -0,0 +1,219 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* Order Confirmation Page (/order/confirm/[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(`/order/success/${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('/cart')
|
||||
}, 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(`/order/success/${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>
|
||||
245
app/pages/order/success/[orderId].vue
Normal file
245
app/pages/order/success/[orderId].vue
Normal file
@@ -0,0 +1,245 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* Order Success Page (/order/success/[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)
|
||||
|
||||
// Get cart composable to refresh cart state
|
||||
const { fetchCart } = useCart()
|
||||
|
||||
// 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(`/order/confirm/${orderId.value}`)
|
||||
}, 2000)
|
||||
}
|
||||
} else {
|
||||
// Order completed successfully - refresh cart to show it's empty
|
||||
await fetchCart()
|
||||
}
|
||||
} 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>
|
||||
Reference in New Issue
Block a user