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

<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">
<!-- Decrease Button -->
<Button variant="outline" size="icon" class="h-8 w-8 text-white border-white/20 hover:bg-white/10"
: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">
{{ localQuantity }}
</div>
<!-- Increase Button -->
<Button variant="outline" size="icon" class="h-8 w-8 text-white border-white/20 hover:bg-white/10"
: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">
<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"
: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>