fix: add page cart

This commit is contained in:
2026-03-25 15:56:52 +01:00
parent 8f37ecbac2
commit fef83eb46b
10 changed files with 548 additions and 101 deletions

View File

@@ -34,11 +34,8 @@ const authStore = useAuthStore()
<RouterLink :to="{ name: 'customer-data' }">
Customer Data
</RouterLink>
<RouterLink :to="{ name: 'cart' }">
Cart
</RouterLink>
<RouterLink :to="{ name: 'cart1' }">
Cart1
<RouterLink :to="{ name: 'carts' }">
Carts
</RouterLink>
<div class="flex items-center gap-2">
<!-- Language Switcher -->

View File

@@ -121,4 +121,5 @@ function goToProduct(productId: number) {
params: { id: productId }
})
}
</script>
</script>

View File

@@ -1,74 +0,0 @@
<template>
<component :is="Default || 'div'">
<div class="container mx-auto mt-20">
<h2
class="font-semibold text-black dark:text-white pb-6 text-2xl">
{{ t('Cart Items') }}
</h2>
<div
class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) overflow-hidden">
<div v-if="cartStore.items.length > 0" class="divide-y divide-(--border-light) dark:divide-(--border-dark)">
<div v-for="item in cartStore.items" :key="item.id" class="flex items-center justify-between p-4 gap-4">
<div class="grid grid-cols-5 w-[100%]">
<div
class="w-20 bg-(--second-light) dark:bg-(--main-dark) rounded flex items-center justify-center overflow-hidden">
<img v-if="item.image" :src="item.image" :alt="item.name" class="w-full h-full object-cover" />
<UIcon v-else name="mdi:package-variant" class="text-xl text-gray-400" />
</div>
<p class="text-black dark:text-white text-sm font-medium truncate">{{ item.name }}</p>
<p class="text-black dark:text-white text-sm font-medium truncate">{{ item.product_number }}</p>
<p class="text-black dark:text-white font-medium">${{ (item.price * item.quantity).toFixed(2) }}</p>
<div class="flex items-center justify-end gap-10">
<UInputNumber v-model="item.quantity" class="text-gray-500 dark:text-gray-400 text-sm" />
<div class="flex justify-center">
<button @click="removeItem(item.id)"
class="p-2 text-red-500 bg-red-100 dark:bg-(--main-dark) rounded transition-colors"
:title="t('Remove')">
<UIcon name="material-symbols:delete" class="text-[20px]" />
</button>
</div>
</div>
</div>
</div>
</div>
<div v-else class="p-8 text-center">
<UIcon name="mdi:cart-outline" class="text-5xl text-gray-300 dark:text-gray-600 mb-4 mx-auto" />
<p class="text-gray-500 dark:text-gray-400">{{ t('Your cart is empty') }}</p>
</div>
</div>
<div v-if="cartStore.items.length > 0" class="flex gap-4 justify-end items-center pt-6">
<UButton color="primary" @click="handleContinueToCheckout"
class="bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) text-white hover:bg-(--accent-blue-dark) dark:hover:bg-(--accent-blue-light)">
{{ t('Continue to Checkout') }}
</UButton>
<UButton variant="outline" color="neutral" @click="handleCancel"
class="text-black dark:text-white border-(--border-light) dark:border-(--border-dark) hover:bg-gray-100 dark:hover:bg-gray-700">
{{ t('Cancel') }}
</UButton>
</div>
</div>
</component>
</template>
<script setup lang="ts">
import { useCartStore } from '@/stores/cart'
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import Default from '@/layouts/default.vue'
const cartStore = useCartStore()
const { t } = useI18n()
const router = useRouter()
function handleCancel() {
router.back()
}
function handleContinueToCheckout() {
router.push({ name: 'cart' })
}
function removeItem(itemId: number) {
cartStore.removeItem(itemId)
}
</script>

View File

@@ -0,0 +1,233 @@
<template>
<div class="relative">
<button
@click="toggleDropdown"
class="flex items-center gap-2 px-4 py-2 bg-(--second-light) dark:bg-(--main-dark) border border-(--border-light) dark:border-(--border-dark) rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
>
<UIcon name="mdi:cart" class="text-lg" />
<span class="text-black dark:text-white font-medium">{{ activeCartName }}</span>
<UIcon :name="isOpen ? 'mdi:chevron-up' : 'mdi:chevron-down'" class="text-lg" />
</button>
<div
v-if="isOpen"
class="absolute top-full right-0 mt-2 w-64 bg-white dark:bg-gray-800 border border-(--border-light) dark:border-(--border-dark) rounded-lg shadow-lg z-50"
>
<div class="divide-y divide-(--border-light) dark:divide-(--border-dark)">
<div
v-for="cart in carts"
:key="cart.id"
class="flex items-center justify-between p-3 hover:bg-gray-50 dark:hover:bg-gray-700"
:class="{ 'bg-blue-50 dark:bg-blue-900/20': cart.id === activeCartId }"
>
<button
@click="selectCart(cart.id)"
class="flex-1 text-left"
>
<span class="text-black dark:text-white">{{ cart.name }}</span>
<span class="text-gray-500 dark:text-gray-400 text-sm ml-2">({{ cart.items.length }})</span>
</button>
<div class="flex items-center gap-1">
<button
@click.stop="startEditing(cart)"
class="p-1.5 text-gray-500 hover:text-(--accent-blue-light) dark:hover:text-(--accent-blue-dark) hover:bg-gray-100 dark:hover:bg-gray-600 rounded"
:title="t('Edit')"
>
<UIcon name="mdi:pencil" class="text-base" />
</button>
<button
@click.stop="confirmDelete(cart)"
class="p-1.5 text-gray-500 hover:text-red-500 hover:bg-gray-100 dark:hover:bg-gray-600 rounded"
:title="t('Delete')"
>
<UIcon name="mdi:delete" class="text-base" />
</button>
</div>
</div>
</div>
<div class="p-3 border-t border-(--border-light) dark:border-(--border-dark)">
<button
@click="showCreateModal = true"
class="w-full flex items-center gap-2 text-(--accent-blue-light) dark:text-(--accent-blue-dark) hover:bg-gray-50 dark:hover:bg-gray-700 p-2 rounded"
>
<UIcon name="mdi:plus" class="text-lg" />
<span>{{ t('Create New Cart') }}</span>
</button>
</div>
</div>
<div
v-if="isOpen"
@click="closeDropdown"
class="fixed inset-0 z-40"
/>
</div>
<div
v-if="showCreateModal"
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
@click.self="showCreateModal = false"
>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl p-6 w-96">
<h3 class="text-lg font-semibold text-black dark:text-white mb-4">{{ t('Create New Cart') }}</h3>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{{ t('Cart Name') }}
</label>
<UInput
v-model="newCartName"
:placeholder="t('Enter cart name')"
class="w-full"
/>
</div>
<div class="flex gap-3 justify-end">
<UButton
variant="outline"
color="neutral"
@click="showCreateModal = false"
>
{{ t('Cancel') }}
</UButton>
<UButton
color="primary"
@click="createNewCart"
:disabled="!newCartName.trim()"
>
{{ t('Create and Continue') }}
</UButton>
</div>
</div>
</div>
<div
v-if="editingCart"
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
@click.self="editingCart = null"
>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl p-6 w-96">
<h3 class="text-lg font-semibold text-black dark:text-white mb-4">{{ t('Edit Cart Name') }}</h3>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{{ t('Cart Name') }}
</label>
<UInput
v-model="editCartName"
:placeholder="t('Enter cart name')"
class="w-full"
/>
</div>
<div class="flex gap-3 justify-end">
<UButton
variant="outline"
color="neutral"
@click="editingCart = null"
>
{{ t('Cancel') }}
</UButton>
<UButton
color="primary"
@click="saveEditedCart"
:disabled="!editCartName.trim()"
>
{{ t('Save') }}
</UButton>
</div>
</div>
</div>
<div
v-if="deletingCart"
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
@click.self="deletingCart = null"
>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl p-6 w-96">
<h3 class="text-lg font-semibold text-black dark:text-white mb-4">{{ t('Delete Cart') }}</h3>
<p class="text-gray-600 dark:text-gray-400 mb-6">
{{ t('Are you sure you want to delete') }} "{{ deletingCart.name }}"?
</p>
<div class="flex gap-3 justify-end">
<UButton
variant="outline"
color="neutral"
@click="deletingCart = null"
>
{{ t('Cancel') }}
</UButton>
<UButton
color="error"
@click="confirmDeleteCart"
>
{{ t('Delete') }}
</UButton>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useCartStore } from '@/stores/cart'
import { useI18n } from 'vue-i18n'
const cartStore = useCartStore()
const { t } = useI18n()
const isOpen = ref(false)
const showCreateModal = ref(false)
const newCartName = ref('')
const editingCart = ref<{ id: string; name: string } | null>(null)
const editCartName = ref('')
const deletingCart = ref<{ id: string; name: string } | null>(null)
const carts = computed(() => cartStore.carts)
const activeCartId = computed(() => cartStore.activeCartId)
const activeCartName = computed(() => {
const cart = cartStore.activeCart
return cart ? cart.name : t('Select Cart')
})
function toggleDropdown() {
isOpen.value = !isOpen.value
}
function closeDropdown() {
isOpen.value = false
}
function selectCart(cartId: string) {
cartStore.setActiveCart(cartId)
closeDropdown()
}
function startEditing(cart: { id: string; name: string }) {
editingCart.value = { id: cart.id, name: cart.name }
editCartName.value = cart.name
}
function saveEditedCart() {
if (editingCart.value && editCartName.value.trim()) {
cartStore.renameCart(editingCart.value.id, editCartName.value.trim())
editingCart.value = null
}
}
function confirmDelete(cart: { id: string; name: string }) {
deletingCart.value = cart
}
function confirmDeleteCart() {
if (deletingCart.value) {
cartStore.deleteCart(deletingCart.value.id)
deletingCart.value = null
}
}
function createNewCart() {
if (newCartName.value.trim()) {
cartStore.createCart(newCartName.value.trim())
newCartName.value = ''
showCreateModal.value = false
}
}
</script>

View File

@@ -188,10 +188,7 @@ function removeItem(itemId: number) {
function placeOrder() {
if (canPlaceOrder.value) {
console.log('Placing order...')
alert(t('Order placed successfully!'))
cartStore.clearCart()
router.push({ name: 'home' })
router.push({ name: 'checkout' })
}
}

View File

@@ -0,0 +1,100 @@
<template>
<component :is="Default || 'div'">
<div class="container mx-auto mt-20">
<div class="flex justify-between items-center pb-6">
<div class="flex gap-2 items-center">
<h2 class="font-semibold text-black dark:text-white text-2xl ">
{{ t('Shopping') }}
</h2>
<p class="font-semibold text-(--accent-blue-light) dark:text-(--accent-blue-dark) text-2xl">{{
cartStore.activeCart?.name }}</p>
</div>
<CartSelector />
</div>
<div
class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) overflow-hidden">
<div v-if="cartStore.items.length > 0"
class="divide-y divide-(--border-light) dark:divide-(--border-dark)">
<div v-for="item in cartStore.items" :key="item.id"
class="flex items-center justify-between p-4 gap-4">
<div class="grid grid-cols-5 w-[100%] flex items-center">
<div
class="w-20 bg-white dark:bg-gray-700 rounded flex items-center justify-center overflow-hidden">
<img v-if="item.image" :src="item.image" :alt="item.name"
class="w-full h-full object-cover" />
<UIcon v-else name="mdi:package-variant" class="text-xl text-gray-400" />
</div>
<p class="text-black dark:text-white text-sm font-medium truncate">
{{ item.name }}
</p>
<p class="text-gray-600 dark:text-gray-400 text-sm truncate">
{{ item.product_number }}
</p>
<p class="text-black dark:text-white font-medium">
${{ item.price.toFixed(2) }}
</p>
<div class="flex items-center justify-end gap-10">
<p class="text-black dark:text-white font-medium">
${{ (item.price * item.quantity).toFixed(2) }}
</p>
<div class="flex items-center justify-end gap-10">
<UInputNumber v-model="item.quantity"
class="text-gray-500 dark:text-gray-400 text-sm" :min="1"
@update:model-value="(val: number) => cartStore.updateQuantity(item.id, val)" />
<div class="flex justify-center">
<button @click="removeItem(item.id)"
class="p-2 text-red-500 bg-red-100 dark:bg-(--main-dark) rounded transition-colors"
:title="t('Remove')">
<UIcon name="material-symbols:delete" class="text-[20px]" />
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div v-else class="p-8 text-center">
<UIcon name="mdi:cart-outline" class="text-5xl text-gray-300 dark:text-gray-600 mb-4 mx-auto" />
<p class="text-gray-500 dark:text-gray-400">{{ t('Your cart is empty') }}</p>
<RouterLink :to="{ name: 'product-card-full' }"
class="inline-block mt-4 text-(--accent-blue-light) dark:text-(--accent-blue-dark) hover:underline">
{{ t('Continue Shopping') }}
</RouterLink>
</div>
</div>
<div v-if="cartStore.items.length > 0" class="flex gap-4 justify-end items-center pt-6">
<UButton color="primary" @click="handleContinueToCart"
class="bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) text-white hover:bg-(--accent-blue-dark) dark:hover:bg-(--accent-blue-light)">
{{ t('Continue to Checkout') }}
</UButton>
<UButton variant="outline" color="neutral" @click="handleCancel"
class="text-black dark:text-white border-(--border-light) dark:border-(--border-dark) hover:bg-gray-100 dark:hover:bg-gray-700">
{{ t('Cancel') }}
</UButton>
</div>
</div>
</component>
</template>
<script setup lang="ts">
import { useCartStore } from '@/stores/cart'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import Default from '@/layouts/default.vue'
import CartSelector from './CartSelector.vue'
const cartStore = useCartStore()
const { t } = useI18n()
const router = useRouter()
function handleCancel() {
router.back()
}
function handleContinueToCart() {
router.push({ name: 'cart' })
}
function removeItem(itemId: number) {
cartStore.removeItem(itemId)
}
</script>

View File

@@ -0,0 +1,79 @@
<template>
<component :is="Default || 'div'">
<div class="container mx-auto mt-20">
<div class="max-w-2xl mx-auto">
<div class="grid grid-cols-3 pb-6">
<button variant="outline" color="neutral" @click="goBackToCart"
class="text-(--accent-blue-light) dark:text-(--accent-blue-dark) flex items-center gap-2">
<UIcon name="mdi:arrow-left" />
{{ t('Back') }}
</button>
<h2 class="font-semibold text-black dark:text-white text-2xl text-center">
{{ t('Checkout') }}
</h2>
</div>
<div
class="bg-(--second-light) dark:bg-(--main-dark) rounded-lg border border-(--border-light) dark:border-(--border-dark) overflow-hidden mb-6">
<div class="p-6">
<h3 class="text-lg font-semibold text-black dark:text-white mb-4">
{{ t('Order Summary') }}
</h3>
<div class="space-y-3 border-b border-(--border-light) dark:border-(--border-dark) pb-4 mb-4">
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">{{ t('Products') }}</span>
<span class="text-black dark:text-white">{{ cartStore.items.length }} {{ t('items')
}}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">{{ t('Products total') }}</span>
<span class="text-black dark:text-white">${{ cartStore.productsTotal.toFixed(2)
}}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">{{ t('VAT') }}</span>
<span class="text-black dark:text-white">${{ cartStore.vatAmount.toFixed(2) }}</span>
</div>
</div>
<div class="flex justify-between mb-6">
<span class="text-black dark:text-white font-semibold text-lg">{{ t('Total') }}</span>
<span class="text-(--accent-blue-light) dark:text-(--accent-blue-dark) font-bold text-lg">
${{ cartStore.orderTotal.toFixed(2) }}
</span>
</div>
<div class="space-y-2 mb-4">
<div v-for="item in cartStore.items" :key="item.id" class="flex items-center gap-3 text-sm">
<div
class="w-10 h-10 bg-white dark:bg-gray-700 rounded flex items-center justify-center overflow-hidden">
<img v-if="item.image" :src="item.image" :alt="item.name"
class="w-full h-full object-cover" />
<UIcon v-else name="mdi:package-variant" class="text-lg text-gray-400" />
</div>
<span class="text-black dark:text-white flex-1 truncate">{{ item.name }}</span>
<span class="text-gray-600 dark:text-gray-400">x{{ item.quantity }}</span>
<span class="text-black dark:text-white">${{ (item.price * item.quantity).toFixed(2)
}}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</component>
</template>
<script setup lang="ts">
import { useCartStore } from '@/stores/cart'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import Default from '@/layouts/default.vue'
const cartStore = useCartStore()
const { t } = useI18n()
const router = useRouter()
function goBackToCart() {
router.push({ name: 'cart' })
}
</script>