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:
Bastian Masanek
2025-11-03 16:24:00 +01:00
parent 527379a2cd
commit b302411626
10 changed files with 169 additions and 80 deletions

View File

@@ -1,14 +1,15 @@
<script setup lang="ts">
/**
* Checkout Page (/kasse)
* Checkout Page (/checkout)
*
* 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
* - Redirects to homepage if cart is empty
* - "Warenkorb bearbeiten" button opens cart sidebar
* - Creates order on form submit
* - Redirects to /zahlung?orderId={orderId} after order creation
* - Redirects to /payment?orderId={orderId} after order creation
*/
// Type for checkout data (matches server schema)
@@ -30,6 +31,7 @@ definePageMeta({
})
const { items, total, itemCount, fetchCart } = useCart()
const { open: openCart } = useCartUI()
// Loading states
const isLoading = ref(true) // Start as true to prevent premature redirect
@@ -80,7 +82,7 @@ async function handleCheckout(checkoutData: CheckoutData) {
if (response.success) {
// Redirect to payment page with order ID
navigateTo(`/zahlung?orderId=${response.orderId}`)
navigateTo(`/payment?orderId=${response.orderId}`)
}
} catch (err: any) {
console.error('Order creation failed:', err)
@@ -113,9 +115,7 @@ async function handleCheckout(checkoutData: CheckoutData) {
<!-- 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"
/>
<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>
@@ -134,13 +134,8 @@ async function handleCheckout(checkoutData: CheckoutData) {
<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"
/>
<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">
@@ -157,12 +152,12 @@ async function handleCheckout(checkoutData: CheckoutData) {
}}
</p>
</div>
<NuxtLink
to="/warenkorb"
<button
@click="openCart"
class="text-sm text-experimenta-accent hover:underline"
>
Bearbeiten
</NuxtLink>
</button>
</div>
</Card>
</div>
@@ -173,12 +168,9 @@ async function handleCheckout(checkoutData: CheckoutData) {
<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="space-y-3 mb-4 divide-y divide-white/10">
<div v-for="item in items" :key="item.id"
class="flex items-start justify-between pt-4 py-2 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">
@@ -226,9 +218,7 @@ async function handleCheckout(checkoutData: CheckoutData) {
}}
</span>
</div>
<div
class="flex items-center justify-between pt-3 border-t border-white/20"
>
<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">
{{
@@ -243,12 +233,12 @@ async function handleCheckout(checkoutData: CheckoutData) {
<!-- Edit Cart Link -->
<div class="mt-6 pt-6 border-t border-white/20">
<NuxtLink
to="/warenkorb"
<button
@click="openCart"
class="text-sm text-experimenta-accent hover:underline"
>
Warenkorb bearbeiten
</NuxtLink>
</button>
</div>
</Card>
</div>

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
/**
* Order Confirmation Page (/bestellung/bestaetigen/[orderId])
* Order Confirmation Page (/order/confirm/[orderId])
*
* Features:
* - Requires authentication (middleware: auth)
@@ -41,7 +41,7 @@ async function fetchOrder() {
// Check order status
if (order.value.status === 'completed') {
// Order already completed - redirect to success page
navigateTo(`/bestellung/erfolg/${orderId.value}`)
navigateTo(`/order/success/${orderId.value}`)
return
}
@@ -61,7 +61,7 @@ async function fetchOrder() {
// Redirect to cart after 3 seconds
setTimeout(() => {
navigateTo('/warenkorb')
navigateTo('/cart')
}, 3000)
} finally {
isLoading.value = false
@@ -82,7 +82,7 @@ async function confirmOrder() {
if (response.success) {
// Redirect to success page
navigateTo(`/bestellung/erfolg/${orderId.value}`)
navigateTo(`/order/success/${orderId.value}`)
}
} catch (err: any) {
console.error('Failed to confirm order:', err)

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
/**
* Order Success Page (/bestellung/erfolg/[orderId])
* Order Success Page (/order/success/[orderId])
*
* Features:
* - Requires authentication (middleware: auth)
@@ -21,6 +21,9 @@ definePageMeta({
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)
@@ -43,9 +46,12 @@ async function fetchOrder() {
// Redirect to confirmation page if still pending
if (order.value.status === 'pending') {
setTimeout(() => {
navigateTo(`/bestellung/bestaetigen/${orderId.value}`)
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)

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
/**
* Payment Mock Page (/zahlung)
* Payment Mock Page (/payment)
*
* Features:
* - Requires authentication (middleware: auth)
@@ -28,7 +28,7 @@ const error = ref<string | null>(null)
// Redirect to cart if no order ID
watchEffect(() => {
if (!orderId.value) {
navigateTo('/warenkorb')
navigateTo('/cart')
}
})
@@ -47,7 +47,7 @@ onMounted(async () => {
// Redirect to cart after 3 seconds
setTimeout(() => {
navigateTo('/warenkorb')
navigateTo('/cart')
}, 3000)
} finally {
isLoading.value = false
@@ -59,7 +59,7 @@ function handlePaymentSuccess() {
if (!orderId.value) return
// Redirect to order confirmation page
navigateTo(`/bestellung/bestaetigen/${orderId.value}`)
navigateTo(`/order/confirm/${orderId.value}`)
}
</script>
@@ -176,7 +176,7 @@ function handlePaymentSuccess() {
<!-- Back Link -->
<div class="text-center pt-4">
<NuxtLink to="/kasse" class="text-sm text-experimenta-accent hover:underline">
<NuxtLink to="/checkout" class="text-sm text-experimenta-accent hover:underline">
Zurück zur Kasse
</NuxtLink>
</div>

View File

@@ -100,10 +100,8 @@ const handleAddToCart = async () => {
<div class="min-h-screen bg-gradient-primary px-4 py-12 md:px-6 lg:px-8">
<!-- Back Button -->
<div class="mx-auto mb-8 max-w-container-narrow">
<NuxtLink
to="/products"
class="inline-flex items-center gap-2 text-white/80 transition-colors hover:text-white"
>
<NuxtLink to="/products"
class="inline-flex items-center gap-2 text-white/80 transition-colors hover:text-white">
<ArrowLeft :size="20" />
<span>Zurück zur Übersicht</span>
</NuxtLink>
@@ -142,15 +140,9 @@ const handleAddToCart = async () => {
<div class="overflow-hidden rounded-2xl border border-white/20 bg-white/10 shadow-glass backdrop-blur-lg">
<!-- Product Image (no padding, flush with top) -->
<div class="relative aspect-[16/9] w-full overflow-hidden bg-purple-dark">
<img
src="/img/makerspace-jk-2025.jpg"
:alt="product.name"
class="h-full w-full object-cover"
/>
<img src="/img/makerspace-jk-2025.jpg" :alt="product.name" class="h-full w-full object-cover" />
<!-- Gradient overlay -->
<div
class="absolute inset-0 bg-gradient-to-t from-purple-darkest/80 via-transparent to-transparent"
/>
<div class="absolute inset-0 bg-gradient-to-t from-purple-darkest/80 via-transparent to-transparent" />
</div>
<!-- Product Content -->
@@ -178,12 +170,10 @@ const handleAddToCart = async () => {
<!-- Availability Card -->
<div class="rounded-xl bg-white/5 p-4 backdrop-blur-sm">
<span class="mb-1 block text-xs uppercase tracking-wide text-white/60">Verfügbarkeit</span>
<div
:class="[
'flex items-center gap-2 text-xl font-semibold',
product.stockQuantity > 0 ? 'text-green' : 'text-red',
]"
>
<div :class="[
'flex items-center gap-2 text-xl font-semibold',
product.stockQuantity > 0 ? 'text-green' : 'text-red',
]">
<CheckCircle v-if="product.stockQuantity > 0" :size="24" />
<span>{{ product.stockQuantity > 0 ? 'Sofort' : 'Nicht verfügbar' }}</span>
</div>
@@ -217,19 +207,13 @@ const handleAddToCart = async () => {
<!-- Action Buttons -->
<div class="flex flex-col gap-4 sm:flex-row">
<Button
variant="experimenta"
size="experimenta"
class="flex-1 relative"
:disabled="product.stockQuantity === 0 || isAddingToCart"
@click="handleAddToCart"
>
<NuxtLink to="/products" class="btn-secondary flex-1 text-center">
Weitere Produkte ansehen
</NuxtLink>
<Button variant="experimenta" size="experimenta" class="flex-1 relative"
:disabled="product.stockQuantity === 0 || isAddingToCart" @click="handleAddToCart">
<!-- Loading spinner -->
<Loader2
v-if="isAddingToCart"
:size="20"
class="mr-2 animate-spin"
/>
<Loader2 v-if="isAddingToCart" :size="20" class="mr-2 animate-spin" />
<!-- Button text -->
<span v-if="isAddingToCart">Wird hinzugefügt...</span>
@@ -237,16 +221,10 @@ const handleAddToCart = async () => {
<span v-else>In den Warenkorb</span>
</Button>
<NuxtLink to="/products" class="btn-secondary flex-1 text-center">
Weitere Produkte ansehen
</NuxtLink>
</div>
<!-- Already in cart hint -->
<div
v-if="isInCart && product.stockQuantity > 0"
class="mt-2 text-center text-sm text-white/70"
>
<div v-if="isInCart && product.stockQuantity > 0" class="mt-2 text-center text-sm text-white/70">
Dieses Produkt befindet sich bereits in deinem Warenkorb.
</div>
</div>