You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
180 lines
5.7 KiB
180 lines
5.7 KiB
<script setup lang="ts">
|
|
import { Minus, Plus, Trash2 } from 'lucide-vue-next'
|
|
import { cn } from '~/lib/utils'
|
|
import type { CartItemWithProduct } from '~/types/cart'
|
|
|
|
interface Props {
|
|
/**
|
|
* Cart item with product details
|
|
*/
|
|
item: CartItemWithProduct
|
|
/**
|
|
* Loading state for update/remove operations
|
|
*/
|
|
loading?: boolean
|
|
/**
|
|
* Additional CSS classes
|
|
*/
|
|
class?: string
|
|
}
|
|
|
|
const props = defineProps<Props>()
|
|
|
|
const emit = defineEmits<{
|
|
'update:quantity': [quantity: number]
|
|
remove: []
|
|
}>()
|
|
|
|
// Format price as EUR currency
|
|
const formatPrice = (price: string | number) => {
|
|
const numPrice = typeof price === 'string' ? Number.parseFloat(price) : price
|
|
return new Intl.NumberFormat('de-DE', {
|
|
style: 'currency',
|
|
currency: 'EUR',
|
|
}).format(numPrice)
|
|
}
|
|
|
|
// Format subtotal
|
|
const formattedSubtotal = computed(() => formatPrice(props.item.subtotal))
|
|
const formattedUnitPrice = computed(() => formatPrice(props.item.product.price))
|
|
|
|
// Local quantity for input binding
|
|
const localQuantity = ref(props.item.quantity)
|
|
|
|
// Watch for prop changes
|
|
watch(
|
|
() => props.item.quantity,
|
|
(newQuantity) => {
|
|
localQuantity.value = newQuantity
|
|
}
|
|
)
|
|
|
|
// Handle quantity decrease
|
|
const decreaseQuantity = () => {
|
|
if (localQuantity.value > 1) {
|
|
localQuantity.value--
|
|
emit('update:quantity', localQuantity.value)
|
|
}
|
|
}
|
|
|
|
// Handle quantity increase
|
|
const increaseQuantity = () => {
|
|
if (localQuantity.value < props.item.product.stockQuantity) {
|
|
localQuantity.value++
|
|
emit('update:quantity', localQuantity.value)
|
|
}
|
|
}
|
|
|
|
// Handle manual quantity input
|
|
const handleQuantityInput = (event: Event) => {
|
|
const input = event.target as HTMLInputElement
|
|
const value = Number.parseInt(input.value, 10)
|
|
|
|
// Validate input
|
|
if (Number.isNaN(value) || value < 1) {
|
|
localQuantity.value = 1
|
|
} else if (value > props.item.product.stockQuantity) {
|
|
localQuantity.value = props.item.product.stockQuantity
|
|
} else {
|
|
localQuantity.value = value
|
|
}
|
|
|
|
// Update input value to match validated value
|
|
input.value = String(localQuantity.value)
|
|
|
|
// Emit update
|
|
emit('update:quantity', localQuantity.value)
|
|
}
|
|
|
|
// Handle remove item
|
|
const handleRemove = () => {
|
|
emit('remove')
|
|
}
|
|
|
|
// Placeholder image for products without images
|
|
const placeholderImage = '/img/makerspace-jk-2025.jpg'
|
|
</script>
|
|
|
|
<template>
|
|
<Card :class="cn('relative overflow-hidden', props.class)">
|
|
<div class="p-4">
|
|
<!-- Top Row: Image + Title/Description + Remove Button -->
|
|
<div class="flex gap-4">
|
|
<!-- Product Image -->
|
|
<div class="flex-shrink-0">
|
|
<div class="relative h-20 w-20 overflow-hidden rounded-lg bg-purple-dark/50 sm:h-24 sm:w-24">
|
|
<img :src="item.product.imageUrl || placeholderImage" :alt="item.product.name"
|
|
class="h-full w-full object-cover" loading="lazy" />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Product Details -->
|
|
<div class="flex flex-1 flex-col gap-1 min-w-0">
|
|
<div class="flex items-start justify-between gap-2">
|
|
<h3 class="font-bold text-white text-sm sm:text-base leading-tight line-clamp-2 flex-1">
|
|
{{ item.product.name }}
|
|
</h3>
|
|
|
|
<!-- Remove Button (Desktop) -->
|
|
<Button variant="ghost" size="icon"
|
|
class="hidden sm:flex text-white/60 hover:text-red hover:bg-red/10 flex-shrink-0 h-8 w-8" :disabled="loading"
|
|
@click="handleRemove">
|
|
<Trash2 class="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
|
|
<p v-if="item.product.description" class="text-xs text-white/70 line-clamp-2">
|
|
{{ item.product.description }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Bottom Row: Quantity Controls (left) + Subtotal (right) -->
|
|
<div class="flex items-center justify-between mt-4">
|
|
<!-- Quantity Controls -->
|
|
<div class="flex items-center gap-2 select-none">
|
|
<!-- Decrease Button -->
|
|
<Button variant="outline" size="icon" class="h-8 w-8 text-white border-white/20 hover:bg-white/10 select-none"
|
|
:disabled="loading || localQuantity <= 1" @click="decreaseQuantity">
|
|
<Minus class="h-4 w-4" />
|
|
</Button>
|
|
|
|
<!-- Quantity Display -->
|
|
<div class="flex h-8 w-16 items-center justify-center rounded-md border border-white/20 bg-white/5 text-sm font-medium text-white select-none">
|
|
{{ localQuantity }}
|
|
</div>
|
|
|
|
<!-- Increase Button -->
|
|
<Button variant="outline" size="icon" class="h-8 w-8 text-white border-white/20 hover:bg-white/10 select-none"
|
|
:disabled="loading || localQuantity >= item.product.stockQuantity" @click="increaseQuantity">
|
|
<Plus class="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
|
|
<!-- Subtotal + Remove Button -->
|
|
<div class="flex items-center gap-2">
|
|
<!-- Subtotal -->
|
|
<div class="flex flex-col text-right select-none">
|
|
<div class="text-xs text-white/60 uppercase tracking-wide">
|
|
Summe
|
|
</div>
|
|
<div class="text-base font-bold text-experimenta-accent sm:text-lg">
|
|
{{ formattedSubtotal }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Remove Button (Mobile) -->
|
|
<Button variant="ghost" size="icon" class="sm:hidden text-white/60 hover:text-red hover:bg-red/10 h-8 w-8 select-none"
|
|
:disabled="loading" @click="handleRemove">
|
|
<Trash2 class="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Loading Overlay -->
|
|
<div v-if="loading" class="absolute inset-0 bg-purple-darkest/80 backdrop-blur-sm flex items-center justify-center">
|
|
<div class="animate-spin rounded-full h-8 w-8 border-4 border-white/20 border-t-experimenta-accent" />
|
|
</div>
|
|
</Card>
|
|
</template>
|
|
|