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:
@@ -9,12 +9,20 @@
|
|||||||
// 15px, which shifted the entire page content 15px to the left. This prop prevents
|
// 15px, which shifted the entire page content 15px to the left. This prop prevents
|
||||||
// that unwanted padding injection and layout shift.
|
// that unwanted padding injection and layout shift.
|
||||||
import { ConfigProvider } from 'reka-ui'
|
import { ConfigProvider } from 'reka-ui'
|
||||||
|
|
||||||
|
// Import cart UI components
|
||||||
|
const { isMobile } = useCartUI()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ConfigProvider :scroll-body="false">
|
<ConfigProvider :scroll-body="false">
|
||||||
<div>
|
<div>
|
||||||
<NuxtPage />
|
<NuxtPage />
|
||||||
|
|
||||||
|
<!-- Cart UI: Render appropriate component based on screen size -->
|
||||||
|
<!-- Mobile: Bottom sheet, Desktop: Right sidebar -->
|
||||||
|
<CartSheet v-if="isMobile" />
|
||||||
|
<CartSidebar v-else />
|
||||||
</div>
|
</div>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ async function handleRemoveItem(itemId: string) {
|
|||||||
// Navigate to checkout
|
// Navigate to checkout
|
||||||
function handleCheckout() {
|
function handleCheckout() {
|
||||||
close()
|
close()
|
||||||
navigateTo('/kasse')
|
navigateTo('/checkout')
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ async function handleRemoveItem(itemId: string) {
|
|||||||
// Navigate to checkout
|
// Navigate to checkout
|
||||||
function handleCheckout() {
|
function handleCheckout() {
|
||||||
close()
|
close()
|
||||||
navigateTo('/kasse')
|
navigateTo('/checkout')
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
106
app/components/MockPayPalButton.vue
Normal file
106
app/components/MockPayPalButton.vue
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* Mock PayPal Button Component
|
||||||
|
*
|
||||||
|
* Simulates a PayPal payment for development/testing purposes.
|
||||||
|
* In production, this would be replaced with the real PayPal SDK integration.
|
||||||
|
*
|
||||||
|
* Features:
|
||||||
|
* - Displays mock PayPal button with branding
|
||||||
|
* - Simulates 2-second payment processing
|
||||||
|
* - Emits success event after "payment" completes
|
||||||
|
* - Shows loading state during processing
|
||||||
|
* - Displays amount to be paid
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
orderId: string
|
||||||
|
amount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
success: []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const isProcessing = ref(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simulate PayPal payment
|
||||||
|
* In production, this would integrate with PayPal SDK
|
||||||
|
*/
|
||||||
|
async function handlePayment() {
|
||||||
|
isProcessing.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Simulate payment processing delay (2 seconds)
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 2000))
|
||||||
|
|
||||||
|
// In production: Call PayPal API to process payment
|
||||||
|
// const result = await processPayPalPayment(props.orderId, props.amount)
|
||||||
|
|
||||||
|
// Emit success event
|
||||||
|
emit('success')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Mock payment failed:', error)
|
||||||
|
// In production: Show error message to user
|
||||||
|
} finally {
|
||||||
|
isProcessing.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<!-- Payment Amount Info -->
|
||||||
|
<div class="flex items-center justify-between p-4 bg-white/5 rounded-lg border border-white/10">
|
||||||
|
<span class="text-white/80">Zu zahlen:</span>
|
||||||
|
<span class="text-xl font-bold text-experimenta-accent">
|
||||||
|
{{
|
||||||
|
new Intl.NumberFormat('de-DE', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'EUR',
|
||||||
|
}).format(amount)
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mock PayPal Button -->
|
||||||
|
<button
|
||||||
|
@click="handlePayment"
|
||||||
|
:disabled="isProcessing"
|
||||||
|
class="w-full relative overflow-hidden bg-[#0070ba] hover:bg-[#005ea6] disabled:bg-[#0070ba]/50 disabled:cursor-not-allowed text-white font-bold py-4 px-6 rounded-lg transition-colors shadow-lg"
|
||||||
|
>
|
||||||
|
<span v-if="!isProcessing" class="flex items-center justify-center gap-3">
|
||||||
|
<!-- PayPal Logo SVG -->
|
||||||
|
<svg
|
||||||
|
class="w-6 h-6"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M20.067 8.478c.492.88.556 2.014.3 3.327-.74 3.806-3.276 5.12-6.514 5.12h-.5a.805.805 0 00-.794.683l-.94 5.96-.267 1.69a.404.404 0 01-.4.342H7.55a.48.48 0 01-.474-.555l.912-5.782.916-5.816a.959.959 0 01.948-.812h1.964c4.332 0 7.299-1.775 8.06-6.145a4.803 4.803 0 00-.808-4.014 6.186 6.186 0 00-1.636-1.293A7.943 7.943 0 0013.784 0H6.732A.959.959 0 005.784.812L2.076 23.235a.48.48 0 00.474.555h3.952a.959.959 0 00.948-.812l.912-5.782h1.74c4.919 0 8.74-2.016 9.965-7.718z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span>Mit PayPal bezahlen (Mock)</span>
|
||||||
|
</span>
|
||||||
|
<span v-else class="flex items-center justify-center gap-2">
|
||||||
|
<div
|
||||||
|
class="animate-spin rounded-full h-5 w-5 border-2 border-white/20 border-t-white"
|
||||||
|
/>
|
||||||
|
Zahlung wird verarbeitet...
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Mock Info -->
|
||||||
|
<div class="text-center">
|
||||||
|
<p class="text-xs text-white/40 italic">
|
||||||
|
Dies ist ein Mock-PayPal-Button für Entwicklungszwecke.
|
||||||
|
<br />
|
||||||
|
Die Zahlung wird simuliert und ist nicht echt.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/**
|
/**
|
||||||
* Checkout Page (/kasse)
|
* Checkout Page (/checkout)
|
||||||
*
|
*
|
||||||
* Features:
|
* Features:
|
||||||
* - Requires authentication (middleware: auth)
|
* - Requires authentication (middleware: auth)
|
||||||
* - Shows CheckoutForm with billing address
|
* - Shows CheckoutForm with billing address
|
||||||
* - Shows CartSummary in right sidebar (desktop) / top (mobile)
|
* - 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
|
* - 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)
|
// Type for checkout data (matches server schema)
|
||||||
@@ -30,6 +31,7 @@ definePageMeta({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const { items, total, itemCount, fetchCart } = useCart()
|
const { items, total, itemCount, fetchCart } = useCart()
|
||||||
|
const { open: openCart } = useCartUI()
|
||||||
|
|
||||||
// Loading states
|
// Loading states
|
||||||
const isLoading = ref(true) // Start as true to prevent premature redirect
|
const isLoading = ref(true) // Start as true to prevent premature redirect
|
||||||
@@ -80,7 +82,7 @@ async function handleCheckout(checkoutData: CheckoutData) {
|
|||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
// Redirect to payment page with order ID
|
// Redirect to payment page with order ID
|
||||||
navigateTo(`/zahlung?orderId=${response.orderId}`)
|
navigateTo(`/payment?orderId=${response.orderId}`)
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Order creation failed:', err)
|
console.error('Order creation failed:', err)
|
||||||
@@ -113,9 +115,7 @@ async function handleCheckout(checkoutData: CheckoutData) {
|
|||||||
|
|
||||||
<!-- Loading State -->
|
<!-- Loading State -->
|
||||||
<div v-if="isLoading" class="text-center py-12">
|
<div v-if="isLoading" class="text-center py-12">
|
||||||
<div
|
<div class="animate-spin rounded-full h-12 w-12 border-4 border-white/20 border-t-white mx-auto mb-4" />
|
||||||
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>
|
<p class="text-white/60">Lade Warenkorb...</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -134,13 +134,8 @@ async function handleCheckout(checkoutData: CheckoutData) {
|
|||||||
<div class="lg:sticky lg:top-4">
|
<div class="lg:sticky lg:top-4">
|
||||||
<!-- Mobile: Show summary at top, Desktop: Show in sidebar -->
|
<!-- Mobile: Show summary at top, Desktop: Show in sidebar -->
|
||||||
<div class="lg:hidden mb-8">
|
<div class="lg:hidden mb-8">
|
||||||
<CartSummary
|
<CartSummary :items="items" :total="total" :loading="isCreatingOrder" @checkout="() => { }"
|
||||||
:items="items"
|
class="hidden" />
|
||||||
:total="total"
|
|
||||||
:loading="isCreatingOrder"
|
|
||||||
@checkout="() => {}"
|
|
||||||
class="hidden"
|
|
||||||
/>
|
|
||||||
<!-- Simplified summary for mobile -->
|
<!-- Simplified summary for mobile -->
|
||||||
<Card class="p-4 bg-white/5 border-white/10">
|
<Card class="p-4 bg-white/5 border-white/10">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
@@ -157,12 +152,12 @@ async function handleCheckout(checkoutData: CheckoutData) {
|
|||||||
}}
|
}}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<NuxtLink
|
<button
|
||||||
to="/warenkorb"
|
@click="openCart"
|
||||||
class="text-sm text-experimenta-accent hover:underline"
|
class="text-sm text-experimenta-accent hover:underline"
|
||||||
>
|
>
|
||||||
Bearbeiten
|
Bearbeiten
|
||||||
</NuxtLink>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
@@ -173,12 +168,9 @@ async function handleCheckout(checkoutData: CheckoutData) {
|
|||||||
<h2 class="text-xl font-bold text-white mb-4">Deine Bestellung</h2>
|
<h2 class="text-xl font-bold text-white mb-4">Deine Bestellung</h2>
|
||||||
|
|
||||||
<!-- Items List -->
|
<!-- Items List -->
|
||||||
<div class="space-y-3 mb-6">
|
<div class="space-y-3 mb-4 divide-y divide-white/10">
|
||||||
<div
|
<div v-for="item in items" :key="item.id"
|
||||||
v-for="item in items"
|
class="flex items-start justify-between pt-4 py-2 border-white/10">
|
||||||
:key="item.id"
|
|
||||||
class="flex items-start justify-between gap-3 pb-3 border-b border-white/10"
|
|
||||||
>
|
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<p class="text-sm font-medium text-white">{{ item.product.name }}</p>
|
<p class="text-sm font-medium text-white">{{ item.product.name }}</p>
|
||||||
<p class="text-xs text-white/60">
|
<p class="text-xs text-white/60">
|
||||||
@@ -226,9 +218,7 @@ async function handleCheckout(checkoutData: CheckoutData) {
|
|||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="flex items-center justify-between pt-3 border-t border-white/20">
|
||||||
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-lg font-bold text-white">Gesamt</span>
|
||||||
<span class="text-2xl font-bold text-experimenta-accent">
|
<span class="text-2xl font-bold text-experimenta-accent">
|
||||||
{{
|
{{
|
||||||
@@ -243,12 +233,12 @@ async function handleCheckout(checkoutData: CheckoutData) {
|
|||||||
|
|
||||||
<!-- Edit Cart Link -->
|
<!-- Edit Cart Link -->
|
||||||
<div class="mt-6 pt-6 border-t border-white/20">
|
<div class="mt-6 pt-6 border-t border-white/20">
|
||||||
<NuxtLink
|
<button
|
||||||
to="/warenkorb"
|
@click="openCart"
|
||||||
class="text-sm text-experimenta-accent hover:underline"
|
class="text-sm text-experimenta-accent hover:underline"
|
||||||
>
|
>
|
||||||
Warenkorb bearbeiten
|
Warenkorb bearbeiten
|
||||||
</NuxtLink>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/**
|
/**
|
||||||
* Order Confirmation Page (/bestellung/bestaetigen/[orderId])
|
* Order Confirmation Page (/order/confirm/[orderId])
|
||||||
*
|
*
|
||||||
* Features:
|
* Features:
|
||||||
* - Requires authentication (middleware: auth)
|
* - Requires authentication (middleware: auth)
|
||||||
@@ -41,7 +41,7 @@ async function fetchOrder() {
|
|||||||
// Check order status
|
// Check order status
|
||||||
if (order.value.status === 'completed') {
|
if (order.value.status === 'completed') {
|
||||||
// Order already completed - redirect to success page
|
// Order already completed - redirect to success page
|
||||||
navigateTo(`/bestellung/erfolg/${orderId.value}`)
|
navigateTo(`/order/success/${orderId.value}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ async function fetchOrder() {
|
|||||||
|
|
||||||
// Redirect to cart after 3 seconds
|
// Redirect to cart after 3 seconds
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
navigateTo('/warenkorb')
|
navigateTo('/cart')
|
||||||
}, 3000)
|
}, 3000)
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
@@ -82,7 +82,7 @@ async function confirmOrder() {
|
|||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
// Redirect to success page
|
// Redirect to success page
|
||||||
navigateTo(`/bestellung/erfolg/${orderId.value}`)
|
navigateTo(`/order/success/${orderId.value}`)
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Failed to confirm order:', err)
|
console.error('Failed to confirm order:', err)
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/**
|
/**
|
||||||
* Order Success Page (/bestellung/erfolg/[orderId])
|
* Order Success Page (/order/success/[orderId])
|
||||||
*
|
*
|
||||||
* Features:
|
* Features:
|
||||||
* - Requires authentication (middleware: auth)
|
* - Requires authentication (middleware: auth)
|
||||||
@@ -21,6 +21,9 @@ definePageMeta({
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const orderId = computed(() => route.params.orderId as string)
|
const orderId = computed(() => route.params.orderId as string)
|
||||||
|
|
||||||
|
// Get cart composable to refresh cart state
|
||||||
|
const { fetchCart } = useCart()
|
||||||
|
|
||||||
// Order data
|
// Order data
|
||||||
const order = ref<any>(null)
|
const order = ref<any>(null)
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
@@ -43,9 +46,12 @@ async function fetchOrder() {
|
|||||||
// Redirect to confirmation page if still pending
|
// Redirect to confirmation page if still pending
|
||||||
if (order.value.status === 'pending') {
|
if (order.value.status === 'pending') {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
navigateTo(`/bestellung/bestaetigen/${orderId.value}`)
|
navigateTo(`/order/confirm/${orderId.value}`)
|
||||||
}, 2000)
|
}, 2000)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Order completed successfully - refresh cart to show it's empty
|
||||||
|
await fetchCart()
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Failed to fetch order:', err)
|
console.error('Failed to fetch order:', err)
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/**
|
/**
|
||||||
* Payment Mock Page (/zahlung)
|
* Payment Mock Page (/payment)
|
||||||
*
|
*
|
||||||
* Features:
|
* Features:
|
||||||
* - Requires authentication (middleware: auth)
|
* - Requires authentication (middleware: auth)
|
||||||
@@ -28,7 +28,7 @@ const error = ref<string | null>(null)
|
|||||||
// Redirect to cart if no order ID
|
// Redirect to cart if no order ID
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
if (!orderId.value) {
|
if (!orderId.value) {
|
||||||
navigateTo('/warenkorb')
|
navigateTo('/cart')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
// Redirect to cart after 3 seconds
|
// Redirect to cart after 3 seconds
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
navigateTo('/warenkorb')
|
navigateTo('/cart')
|
||||||
}, 3000)
|
}, 3000)
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
@@ -59,7 +59,7 @@ function handlePaymentSuccess() {
|
|||||||
if (!orderId.value) return
|
if (!orderId.value) return
|
||||||
|
|
||||||
// Redirect to order confirmation page
|
// Redirect to order confirmation page
|
||||||
navigateTo(`/bestellung/bestaetigen/${orderId.value}`)
|
navigateTo(`/order/confirm/${orderId.value}`)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -176,7 +176,7 @@ function handlePaymentSuccess() {
|
|||||||
|
|
||||||
<!-- Back Link -->
|
<!-- Back Link -->
|
||||||
<div class="text-center pt-4">
|
<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
|
← Zurück zur Kasse
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
@@ -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">
|
<div class="min-h-screen bg-gradient-primary px-4 py-12 md:px-6 lg:px-8">
|
||||||
<!-- Back Button -->
|
<!-- Back Button -->
|
||||||
<div class="mx-auto mb-8 max-w-container-narrow">
|
<div class="mx-auto mb-8 max-w-container-narrow">
|
||||||
<NuxtLink
|
<NuxtLink to="/products"
|
||||||
to="/products"
|
class="inline-flex items-center gap-2 text-white/80 transition-colors hover:text-white">
|
||||||
class="inline-flex items-center gap-2 text-white/80 transition-colors hover:text-white"
|
|
||||||
>
|
|
||||||
<ArrowLeft :size="20" />
|
<ArrowLeft :size="20" />
|
||||||
<span>Zurück zur Übersicht</span>
|
<span>Zurück zur Übersicht</span>
|
||||||
</NuxtLink>
|
</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">
|
<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) -->
|
<!-- Product Image (no padding, flush with top) -->
|
||||||
<div class="relative aspect-[16/9] w-full overflow-hidden bg-purple-dark">
|
<div class="relative aspect-[16/9] w-full overflow-hidden bg-purple-dark">
|
||||||
<img
|
<img src="/img/makerspace-jk-2025.jpg" :alt="product.name" class="h-full w-full object-cover" />
|
||||||
src="/img/makerspace-jk-2025.jpg"
|
|
||||||
:alt="product.name"
|
|
||||||
class="h-full w-full object-cover"
|
|
||||||
/>
|
|
||||||
<!-- Gradient overlay -->
|
<!-- Gradient overlay -->
|
||||||
<div
|
<div class="absolute inset-0 bg-gradient-to-t from-purple-darkest/80 via-transparent to-transparent" />
|
||||||
class="absolute inset-0 bg-gradient-to-t from-purple-darkest/80 via-transparent to-transparent"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Product Content -->
|
<!-- Product Content -->
|
||||||
@@ -178,12 +170,10 @@ const handleAddToCart = async () => {
|
|||||||
<!-- Availability Card -->
|
<!-- Availability Card -->
|
||||||
<div class="rounded-xl bg-white/5 p-4 backdrop-blur-sm">
|
<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>
|
<span class="mb-1 block text-xs uppercase tracking-wide text-white/60">Verfügbarkeit</span>
|
||||||
<div
|
<div :class="[
|
||||||
:class="[
|
'flex items-center gap-2 text-xl font-semibold',
|
||||||
'flex items-center gap-2 text-xl font-semibold',
|
product.stockQuantity > 0 ? 'text-green' : 'text-red',
|
||||||
product.stockQuantity > 0 ? 'text-green' : 'text-red',
|
]">
|
||||||
]"
|
|
||||||
>
|
|
||||||
<CheckCircle v-if="product.stockQuantity > 0" :size="24" />
|
<CheckCircle v-if="product.stockQuantity > 0" :size="24" />
|
||||||
<span>{{ product.stockQuantity > 0 ? 'Sofort' : 'Nicht verfügbar' }}</span>
|
<span>{{ product.stockQuantity > 0 ? 'Sofort' : 'Nicht verfügbar' }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -217,19 +207,13 @@ const handleAddToCart = async () => {
|
|||||||
|
|
||||||
<!-- Action Buttons -->
|
<!-- Action Buttons -->
|
||||||
<div class="flex flex-col gap-4 sm:flex-row">
|
<div class="flex flex-col gap-4 sm:flex-row">
|
||||||
<Button
|
<NuxtLink to="/products" class="btn-secondary flex-1 text-center">
|
||||||
variant="experimenta"
|
Weitere Produkte ansehen
|
||||||
size="experimenta"
|
</NuxtLink>
|
||||||
class="flex-1 relative"
|
<Button variant="experimenta" size="experimenta" class="flex-1 relative"
|
||||||
:disabled="product.stockQuantity === 0 || isAddingToCart"
|
:disabled="product.stockQuantity === 0 || isAddingToCart" @click="handleAddToCart">
|
||||||
@click="handleAddToCart"
|
|
||||||
>
|
|
||||||
<!-- Loading spinner -->
|
<!-- Loading spinner -->
|
||||||
<Loader2
|
<Loader2 v-if="isAddingToCart" :size="20" class="mr-2 animate-spin" />
|
||||||
v-if="isAddingToCart"
|
|
||||||
:size="20"
|
|
||||||
class="mr-2 animate-spin"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Button text -->
|
<!-- Button text -->
|
||||||
<span v-if="isAddingToCart">Wird hinzugefügt...</span>
|
<span v-if="isAddingToCart">Wird hinzugefügt...</span>
|
||||||
@@ -237,16 +221,10 @@ const handleAddToCart = async () => {
|
|||||||
<span v-else>In den Warenkorb</span>
|
<span v-else>In den Warenkorb</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<NuxtLink to="/products" class="btn-secondary flex-1 text-center">
|
|
||||||
Weitere Produkte ansehen
|
|
||||||
</NuxtLink>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Already in cart hint -->
|
<!-- Already in cart hint -->
|
||||||
<div
|
<div v-if="isInCart && product.stockQuantity > 0" class="mt-2 text-center text-sm text-white/70">
|
||||||
v-if="isInCart && product.stockQuantity > 0"
|
|
||||||
class="mt-2 text-center text-sm text-white/70"
|
|
||||||
>
|
|
||||||
Dieses Produkt befindet sich bereits in deinem Warenkorb.
|
Dieses Produkt befindet sich bereits in deinem Warenkorb.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { H3Event } from 'h3'
|
import type { H3Event } from 'h3'
|
||||||
import { and, eq, inArray } from 'drizzle-orm'
|
import { and, eq, inArray, asc } from 'drizzle-orm'
|
||||||
import { carts, cartItems, products } from '../database/schema'
|
import { carts, cartItems, products } from '../database/schema'
|
||||||
|
|
||||||
// Re-export shared types
|
// Re-export shared types
|
||||||
@@ -89,6 +89,7 @@ export async function getCartWithItems(cartId: string): Promise<CartSummary> {
|
|||||||
with: {
|
with: {
|
||||||
product: true,
|
product: true,
|
||||||
},
|
},
|
||||||
|
orderBy: asc(cartItems.addedAt), // Sort by addedAt to maintain stable order
|
||||||
})
|
})
|
||||||
|
|
||||||
// Separate available and unavailable items
|
// Separate available and unavailable items
|
||||||
|
|||||||
Reference in New Issue
Block a user