This commit is contained in:
2026-03-12 12:07:40 +01:00
parent 3943614abb
commit 2d5d6a7487
3 changed files with 178 additions and 101 deletions

85
bo/src/stores/product.ts Normal file
View File

@@ -0,0 +1,85 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { useFetchJson } from '@/composable/useFetchJson'
export interface Product {
id: number
image: string
name: string
code: string
inStock: boolean
priceFrom: number
priceTo: number
count: number
description?: string
howToUse?: string
productDetails?: string
}
export interface ProductResponse {
items: Product[]
items_count: number
}
export const useProductStore = defineStore('product', () => {
const products = ref<Product[]>([])
const currentProduct = ref<Product | null>(null)
const loading = ref(false)
const error = ref<string | null>(null)
// Fetch all products
async function fetchProducts() {
loading.value = true
error.value = null
try {
const data = await useFetchJson<ProductResponse>('/api/v1/restricted/product-description', {
method: 'GET',
})
console.log(data)
const response = (data as any).items || data
products.value = response.items || response || []
} catch (e: any) {
error.value = e?.message || 'Failed to load products'
console.error('Failed to fetch products:', e)
} finally {
loading.value = false
}
}
// Fetch single product by ID
async function fetchProductById(id: number) {
loading.value = true
error.value = null
currentProduct.value = null
try {
const data = await useFetchJson<{ items: Product }>(`/api/v1/restricted/product-description?id=${id}`, {
method: 'GET',
})
const response = (data as any).items || data
currentProduct.value = response.items?.[0] || response
} catch (e: any) {
error.value = e?.message || 'Failed to load product'
console.error('Failed to fetch product:', e)
} finally {
loading.value = false
}
}
// Clear current product
function clearCurrentProduct() {
currentProduct.value = null
}
return {
products,
currentProduct,
loading,
error,
fetchProducts,
fetchProductById,
clearCurrentProduct,
}
})

View File

@@ -1,52 +1,38 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
import { useProductStore, type Product } from '@/stores/product'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import type { TableColumn } from '@nuxt/ui' import type { TableColumn } from '@nuxt/ui'
import { h } from 'vue' import { h } from 'vue'
interface Product {
id: number
image: string
name: string
code: string
inStock: boolean
priceFrom: number
priceTo: number
count: number
description: string
howToUse: string
productDetails: string
}
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const authStore = useAuthStore() const authStore = useAuthStore()
const productStore = useProductStore()
const { t } = useI18n() const { t } = useI18n()
// Mock product data (same as ProductsView)
const products = ref<Product[]>([
{ id: 1, image: 'https://picsum.photos/seed/product1/400/400', name: 'Laptop Pro 15', code: 'LP-001', inStock: true, priceFrom: 999, priceTo: 1299, count: 15, description: 'High-performance laptop for professionals', howToUse: 'Open the lid and press the power button', productDetails: '15-inch display, 16GB RAM, 512GB SSD' },
{ id: 2, image: 'https://picsum.photos/seed/product2/400/400', name: 'Wireless Mouse', code: 'WM-002', inStock: true, priceFrom: 29, priceTo: 49, count: 150, description: 'Ergonomic wireless mouse with precision tracking', howToUse: 'Connect via Bluetooth or USB receiver', productDetails: '3000 DPI, 2.4GHz wireless, 12-month battery' },
{ id: 3, image: 'https://picsum.photos/seed/product3/400/400', name: 'Mechanical Keyboard', code: 'MK-003', inStock: true, priceFrom: 89, priceTo: 159, count: 45, description: 'Premium mechanical keyboard with RGB lighting', howToUse: 'Connect via USB-C cable', productDetails: 'Cherry MX switches, RGB backlight, anti-ghosting' },
{ id: 4, image: 'https://picsum.photos/seed/product4/400/400', name: 'USB-C Hub', code: 'UH-004', inStock: false, priceFrom: 39, priceTo: 59, count: 0, description: 'Multi-port USB-C hub for connectivity', howToUse: 'Connect to laptop USB-C port', productDetails: 'HDMI 4K, 3x USB-A, SD card reader, PD 100W' },
{ id: 5, image: 'https://picsum.photos/seed/product5/400/400', name: 'Monitor 27 inch', code: 'MN-005', inStock: true, priceFrom: 299, priceTo: 449, count: 23, description: '27-inch 4K IPS monitor with HDR support', howToUse: 'Connect via HDMI or DisplayPort', productDetails: '3840x2160, 60Hz, HDR400, built-in speakers' },
{ id: 6, image: 'https://picsum.photos/seed/product6/400/400', name: 'Webcam HD', code: 'WC-006', inStock: true, priceFrom: 59, priceTo: 89, count: 67, description: 'Full HD webcam for video conferencing', howToUse: 'Mount on monitor or use tripod stand', productDetails: '1080p 30fps, autofocus, noise-canceling mic' },
{ id: 7, image: 'https://picsum.photos/seed/product7/400/400', name: 'Headphones Wireless', code: 'HW-007', inStock: true, priceFrom: 149, priceTo: 249, count: 89, description: 'Premium wireless headphones with ANC', howToUse: 'Pair via Bluetooth or use included cable', productDetails: '30-hour battery, ANC, 40mm drivers' },
{ id: 8, image: 'https://picsum.photos/seed/product8/400/400', name: 'External SSD 1TB', code: 'ES-008', inStock: true, priceFrom: 109, priceTo: 149, count: 120, description: 'Portable external SSD with fast speeds', howToUse: 'Connect via USB-C cable', productDetails: '1TB, 1050MB/s read, compact design' },
{ id: 9, image: 'https://picsum.photos/seed/product9/400/400', name: 'Desk Lamp LED', code: 'DL-009', inStock: false, priceFrom: 35, priceTo: 55, count: 0, description: 'Adjustable LED desk lamp with multiple brightness levels', howToUse: 'Plug in and touch controls', productDetails: '5 brightness levels, color temperature control, USB port' },
{ id: 10, image: 'https://picsum.photos/seed/product10/400/400', name: 'Cable Organizer', code: 'CO-010', inStock: true, priceFrom: 15, priceTo: 25, count: 200, description: 'Desk cable management solution', howToUse: 'Stick to desk or use clamps', productDetails: '10 slots, adhesive backing,白色' },
])
// Get product from route params // Get product from route params
const productId = computed(() => Number(route.params.id)) const productId = computed(() => Number(route.params.id))
const product = computed(() => products.value.find(p => p.id === productId.value)) const product = computed(() => productStore.currentProduct)
// Fetch product on mount
onMounted(() => {
if (productId.value) {
productStore.fetchProductById(productId.value)
}
})
// Clear product when leaving
onUnmounted(() => {
productStore.clearCurrentProduct()
})
// Active tab for the four buttons // Active tab for the four buttons
const activeTab = ref<'description' | 'howToUse' | 'productDetails' | 'documents'>('description') const activeTab = ref<'description' | 'howToUse' | 'productDetails' | 'documents'>('description')
// Mock variants (same structure as products but with variants) // Mock variants (in real app, this would come from API)
const variants = computed(() => { const variants = computed(() => {
if (!product.value) return [] if (!product.value) return []
// Create mock variants based on the product // Create mock variants based on the product
@@ -120,7 +106,7 @@ const columns = computed<TableColumn<any>[]>(() => [
onClick: () => addToCart(row.original) onClick: () => addToCart(row.original)
}, t('products.add_to_cart')), }, t('products.add_to_cart')),
h('button', { h('button', {
class: 'px-3 py-1.5 text-sm font-medium bg-gray-200 dark:dark:hover:bg-(--gray-dark) text-black dark:text-white rounded-lg hover:bg-(--gray) dark:hover:bg-gray-600 transition-colors', class: 'px-3 py-1.5 text-sm font-medium bg-gray-200 dark:bg-gray-700 text-black dark:text-white rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors',
onClick: () => incrementCount(row.original) onClick: () => incrementCount(row.original)
}, '+') }, '+')
]) ])
@@ -143,20 +129,30 @@ function goBack() {
<template> <template>
<div class="container"> <div class="container">
<div class="p-6 bg-(--main-light) dark:bg-(--black) min-h-screen font-sans"> <div class="p-6 bg-white dark:bg-(--black) min-h-screen font-sans">
<!-- Back Button --> <!-- Back Button -->
<button <button
@click="goBack" @click="goBack"
class="mb-4 px-4 py-2 text-sm font-medium text-black dark:text-white bg-gray-200 dark:dark:hover:bg-(--gray-dark) rounded-lg hover:bg-(--gray) dark:hover:bg-gray-600 transition-colors" class="mb-4 px-4 py-2 text-sm font-medium text-black dark:text-white bg-gray-200 dark:bg-gray-700 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors"
> >
{{ t('products.back_to_list') }} {{ t('products.back_to_list') }}
</button> </button>
<!-- Loading State -->
<div v-if="productStore.loading" class="mb-4 p-3 bg-blue-100 text-blue-700 rounded">
{{ t('products.loading') }}...
</div>
<!-- Error State -->
<div v-if="productStore.error" class="mb-4 p-3 bg-red-100 text-red-700 rounded">
{{ productStore.error }}
</div>
<div v-if="!authStore.isAuthenticated" class="mb-4 p-3 bg-yellow-100 text-yellow-700 rounded"> <div v-if="!authStore.isAuthenticated" class="mb-4 p-3 bg-yellow-100 text-yellow-700 rounded">
{{ t('products.login_to_view') }} {{ t('products.login_to_view') }}
</div> </div>
<div v-if="authStore.isAuthenticated && product"> <div v-if="authStore.isAuthenticated && product && !productStore.loading">
<!-- Product Header: Image and Title --> <!-- Product Header: Image and Title -->
<div class="flex flex-col md:flex-row gap-8 mb-6"> <div class="flex flex-col md:flex-row gap-8 mb-6">
<!-- Product Image --> <!-- Product Image -->
@@ -209,7 +205,7 @@ function goBack() {
'px-4 py-2 text-sm font-medium rounded-lg transition-colors', 'px-4 py-2 text-sm font-medium rounded-lg transition-colors',
activeTab === 'description' activeTab === 'description'
? 'bg-primary text-white' ? 'bg-primary text-white'
: 'bg-gray-200 dark:dark:hover:bg-(--gray-dark) text-black dark:text-white hover:bg-(--gray) dark:hover:bg-gray-600' : 'bg-gray-200 dark:bg-gray-700 text-black dark:text-white hover:bg-gray-300 dark:hover:bg-gray-600'
]" ]"
> >
{{ t('products.description') }} {{ t('products.description') }}
@@ -220,7 +216,7 @@ function goBack() {
'px-4 py-2 text-sm font-medium rounded-lg transition-colors', 'px-4 py-2 text-sm font-medium rounded-lg transition-colors',
activeTab === 'howToUse' activeTab === 'howToUse'
? 'bg-primary text-white' ? 'bg-primary text-white'
: 'bg-gray-200 dark:dark:hover:bg-(--gray-dark) text-black dark:text-white hover:bg-(--gray) dark:hover:bg-gray-600' : 'bg-gray-200 dark:bg-gray-700 text-black dark:text-white hover:bg-gray-300 dark:hover:bg-gray-600'
]" ]"
> >
{{ t('products.how_to_use') }} {{ t('products.how_to_use') }}
@@ -231,7 +227,7 @@ function goBack() {
'px-4 py-2 text-sm font-medium rounded-lg transition-colors', 'px-4 py-2 text-sm font-medium rounded-lg transition-colors',
activeTab === 'productDetails' activeTab === 'productDetails'
? 'bg-primary text-white' ? 'bg-primary text-white'
: 'bg-gray-200 dark:dark:hover:bg-(--gray-dark) text-black dark:text-white hover:bg-(--gray) dark:hover:bg-gray-600' : 'bg-gray-200 dark:bg-gray-700 text-black dark:text-white hover:bg-gray-300 dark:hover:bg-gray-600'
]" ]"
> >
{{ t('products.product_details') }} {{ t('products.product_details') }}
@@ -242,7 +238,7 @@ function goBack() {
'px-4 py-2 text-sm font-medium rounded-lg transition-colors', 'px-4 py-2 text-sm font-medium rounded-lg transition-colors',
activeTab === 'documents' activeTab === 'documents'
? 'bg-primary text-white' ? 'bg-primary text-white'
: 'bg-gray-200 dark:dark:hover:bg-(--gray-dark) text-black dark:text-white hover:bg-(--gray) dark:hover:bg-gray-600' : 'bg-gray-200 dark:bg-gray-700 text-black dark:text-white hover:bg-gray-300 dark:hover:bg-gray-600'
]" ]"
> >
{{ t('products.documents') }} {{ t('products.documents') }}
@@ -300,7 +296,7 @@ function goBack() {
</div> </div>
</div> </div>
<div v-else-if="authStore.isAuthenticated && !product" class="text-center py-10"> <div v-else-if="authStore.isAuthenticated && !product && !productStore.loading && !productStore.error" class="text-center py-10">
<p class="text-gray-500 dark:text-gray-400">{{ t('products.product_not_found') }}</p> <p class="text-gray-500 dark:text-gray-400">{{ t('products.product_not_found') }}</p>
</div> </div>
</div> </div>

View File

@@ -1,40 +1,17 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from 'vue' import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
import { useProductStore, type Product } from '@/stores/product'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import type { TableColumn } from '@nuxt/ui' import type { TableColumn } from '@nuxt/ui'
import { h } from 'vue' import { h } from 'vue'
interface Product {
id: number
image: string
name: string
code: string
inStock: boolean
priceFrom: number
priceTo: number
count: number
}
const router = useRouter() const router = useRouter()
const authStore = useAuthStore() const authStore = useAuthStore()
const productStore = useProductStore()
const { t } = useI18n() const { t } = useI18n()
// Mock product data
const products = ref<Product[]>([
{ id: 1, image: 'https://picsum.photos/seed/product1/100/100', name: 'Laptop Pro 15', code: 'LP-001', inStock: true, priceFrom: 999, priceTo: 1299, count: 15 },
{ id: 2, image: 'https://picsum.photos/seed/product2/100/100', name: 'Wireless Mouse', code: 'WM-002', inStock: true, priceFrom: 29, priceTo: 49, count: 150 },
{ id: 3, image: 'https://picsum.photos/seed/product3/100/100', name: 'Mechanical Keyboard', code: 'MK-003', inStock: true, priceFrom: 89, priceTo: 159, count: 45 },
{ id: 4, image: 'https://picsum.photos/seed/product4/100/100', name: 'USB-C Hub', code: 'UH-004', inStock: false, priceFrom: 39, priceTo: 59, count: 0 },
{ id: 5, image: 'https://picsum.photos/seed/product5/100/100', name: 'Monitor 27 inch', code: 'MN-005', inStock: true, priceFrom: 299, priceTo: 449, count: 23 },
{ id: 6, image: 'https://picsum.photos/seed/product6/100/100', name: 'Webcam HD', code: 'WC-006', inStock: true, priceFrom: 59, priceTo: 89, count: 67 },
{ id: 7, image: 'https://picsum.photos/seed/product7/100/100', name: 'Headphones Wireless', code: 'HW-007', inStock: true, priceFrom: 149, priceTo: 249, count: 89 },
{ id: 8, image: 'https://picsum.photos/seed/product8/100/100', name: 'External SSD 1TB', code: 'ES-008', inStock: true, priceFrom: 109, priceTo: 149, count: 120 },
{ id: 9, image: 'https://picsum.photos/seed/product9/100/100', name: 'Desk Lamp LED', code: 'DL-009', inStock: false, priceFrom: 35, priceTo: 55, count: 0 },
{ id: 10, image: 'https://picsum.photos/seed/product10/100/100', name: 'Cable Organizer', code: 'CO-010', inStock: true, priceFrom: 15, priceTo: 25, count: 200 },
])
// Search filters // Search filters
const searchName = ref('') const searchName = ref('')
const searchCode = ref('') const searchCode = ref('')
@@ -45,9 +22,14 @@ const priceToFilter = ref<number | null>(null)
const page = ref(1) const page = ref(1)
const pageSize = 5 const pageSize = 5
// Fetch products on mount
onMounted(() => {
productStore.fetchProducts()
})
// Filtered products // Filtered products
const filteredProducts = computed(() => { const filteredProducts = computed(() => {
return products.value.filter(product => { return productStore.products.filter(product => {
const matchesName = product.name.toLowerCase().includes(searchName.value.toLowerCase()) const matchesName = product.name.toLowerCase().includes(searchName.value.toLowerCase())
const matchesCode = product.code.toLowerCase().includes(searchCode.value.toLowerCase()) const matchesCode = product.code.toLowerCase().includes(searchCode.value.toLowerCase())
const matchesPriceFrom = priceFromFilter.value === null || product.priceFrom >= priceFromFilter.value const matchesPriceFrom = priceFromFilter.value === null || product.priceFrom >= priceFromFilter.value
@@ -79,7 +61,7 @@ function goToProduct(product: Product) {
const columns = computed<TableColumn<Product>[]>(() => [ const columns = computed<TableColumn<Product>[]>(() => [
{ {
accessorKey: 'image', accessorKey: 'image',
header: () => h('div', { class: 'text-center' }, t('Image')), header: () => h('div', { class: 'text-center' }, t('products.image')),
cell: ({ row }) => h('img', { cell: ({ row }) => h('img', {
src: row.getValue('image'), src: row.getValue('image'),
alt: 'Product', alt: 'Product',
@@ -88,7 +70,7 @@ const columns = computed<TableColumn<Product>[]>(() => [
}, },
{ {
accessorKey: 'name', accessorKey: 'name',
header: 'product name', header: t('products.product_name'),
cell: ({ row }) => { cell: ({ row }) => {
const product = row.original const product = row.original
return h('button', { return h('button', {
@@ -99,21 +81,21 @@ const columns = computed<TableColumn<Product>[]>(() => [
}, },
{ {
accessorKey: 'code', accessorKey: 'code',
header: 'product code', header: t('products.product_code'),
}, },
{ {
accessorKey: 'inStock', accessorKey: 'inStock',
header: 'in stock', header: t('products.in_stock'),
cell: ({ row }) => { cell: ({ row }) => {
const inStock = row.getValue('inStock') const inStock = row.getValue('inStock')
return h('span', { return h('span', {
class: inStock ? 'text-green-600 font-medium' : 'text-red-600 font-medium' class: inStock ? 'text-green-600 font-medium' : 'text-red-600 font-medium'
}, inStock ? 'products yes' : 'products.no') }, inStock ? t('products.yes') : t('products.no'))
} }
}, },
{ {
accessorKey: 'price', accessorKey: 'price',
header: 'price', header: t('products.price'),
cell: ({ row }) => { cell: ({ row }) => {
const priceFromVal = row.original.priceFrom const priceFromVal = row.original.priceFrom
const priceToVal = row.original.priceTo const priceToVal = row.original.priceTo
@@ -122,7 +104,7 @@ const columns = computed<TableColumn<Product>[]>(() => [
}, },
{ {
accessorKey: 'count', accessorKey: 'count',
header: 'count', header: t('products.count'),
}, },
{ {
id: 'actions', id: 'actions',
@@ -135,7 +117,7 @@ const columns = computed<TableColumn<Product>[]>(() => [
onClick: (e: Event) => { e.stopPropagation(); addToCart(product) } onClick: (e: Event) => { e.stopPropagation(); addToCart(product) }
}, t('products.add_to_cart')), }, t('products.add_to_cart')),
h('button', { h('button', {
class: 'px-3 py-1.5 text-sm font-medium bg-gray-200 dark:dark:hover:bg-(--gray-dark) text-black dark:text-white rounded-lg hover:bg-(--gray) dark:hover:bg-gray-600 transition-colors', class: 'px-3 py-1.5 text-sm font-medium bg-gray-200 dark:bg-gray-700 text-black dark:text-white rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors',
onClick: (e: Event) => { e.stopPropagation(); incrementCount(product) } onClick: (e: Event) => { e.stopPropagation(); incrementCount(product) }
}, '+') }, '+')
]) ])
@@ -163,61 +145,71 @@ function clearFilters() {
<template> <template>
<div class="container"> <div class="container">
<div class="bg-(--main-light) mt-10 dark:bg-(--black) font-sans"> <div class="p-6 bg-white dark:bg-(--black) min-h-screen font-sans">
<h1 class="text-2xl font-bold mb-6 text-black dark:text-white">title</h1> <h1 class="text-2xl font-bold mb-6 text-black dark:text-white">{{ t('products.title') }}</h1>
<div v-if="!authStore.isAuthenticated" class="mb-4 bg-yellow-100 text-yellow-700 rounded"> <div v-if="!authStore.isAuthenticated" class="mb-4 p-3 bg-yellow-100 text-yellow-700 rounded">
login to view {{ t('products.login_to_view') }}
</div> </div>
<div v-if="authStore.isAuthenticated" class="space-y-4"> <!-- Loading State -->
<div v-if="productStore.loading" class="mb-4 p-3 bg-blue-100 text-blue-700 rounded">
{{ t('products.loading') }}...
</div>
<!-- Error State -->
<div v-if="productStore.error" class="mb-4 p-3 bg-red-100 text-red-700 rounded">
{{ productStore.error }}
</div>
<div v-if="authStore.isAuthenticated && !productStore.loading && !productStore.error" class="space-y-4">
<!-- Filter Block --> <!-- Filter Block -->
<div class="flex flex-wrap gap-4 mb-4 p-4 border border-(--border-light) dark:border-(--border-dark) rounded bg-(--second-light) dark:bg-(--main-dark) min-w-[50%]"> <div class="flex flex-wrap gap-4 mb-4 p-4 border border-(--border-light) dark:border-(--border-dark) rounded bg-gray-50 dark:bg-gray-800">
<div class="flex flex-col"> <div class="flex flex-col min-w-[180px]">
<label class="mb-1 text-sm font-medium text-black dark:text-white">Product name</label> <label class="mb-1 text-sm font-medium text-black dark:text-white">{{ t('products.search_by_name') }}</label>
<UInput <UInput
v-model="searchName" v-model="searchName"
placeholder="search name placeholder" :placeholder="t('products.search_name_placeholder')"
@update:model-value="resetPage" @update:model-value="resetPage"
class="dark:text-white text-black" class="dark:text-white text-black"
/> />
</div> </div>
<div class="flex flex-col"> <div class="flex flex-col min-w-[180px]">
<label class="mb-1 text-sm font-medium text-black dark:text-white">Product code</label> <label class="mb-1 text-sm font-medium text-black dark:text-white">{{ t('products.search_by_code') }}</label>
<UInput <UInput
v-model="searchCode" v-model="searchCode"
placeholder="search code placeholder" :placeholder="t('products.search_code_placeholder')"
@update:model-value="resetPage" @update:model-value="resetPage"
class="dark:text-white text-black" class="dark:text-white text-black"
/> />
</div> </div>
<div class="flex flex-col"> <div class="flex flex-col min-w-[120px]">
<label class="mb-1 text-sm font-medium text-black dark:text-white">Price from</label> <label class="mb-1 text-sm font-medium text-black dark:text-white">{{ t('products.price_from') }}</label>
<UInput <UInput
v-model="priceFromFilter" v-model="priceFromFilter"
type="number" type="number"
placeholder="price from" :placeholder="t('products.price_from')"
@update:model-value="resetPage" @update:model-value="resetPage"
class="dark:text-white text-black" class="dark:text-white text-black"
/> />
</div> </div>
<div class="flex flex-col"> <div class="flex flex-col min-w-[120px]">
<label class="mb-1 text-sm font-medium text-black dark:text-white">Price to</label> <label class="mb-1 text-sm font-medium text-black dark:text-white">{{ t('products.price_to') }}</label>
<UInput <UInput
v-model="priceToFilter" v-model="priceToFilter"
type="number" type="number"
placeholder="price to" :placeholder="t('products.price_to')"
@update:model-value="resetPage" @update:model-value="resetPage"
class="dark:text-white text-black" class="dark:text-white text-black"
/> />
</div> </div>
<div class="flex items-end"> <div class="flex items-end">
<UButton <button
@click="clearFilters" @click="clearFilters"
class="px-4 py-2 text-sm font-medium text-white bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) rounded-lg transition-colors cursor-pointer" class="px-4 py-2 text-sm font-medium text-black dark:text-white bg-gray-200 dark:bg-gray-700 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors"
> >
clear filters {{ t('products.clear_filters') }}
</UButton> </button>
</div> </div>
</div> </div>
@@ -227,12 +219,16 @@ function clearFilters() {
:data="paginatedProducts" :data="paginatedProducts"
:columns="columns" :columns="columns"
class="dark:text-white! text-dark" class="dark:text-white! text-dark"
@select="goToProduct"
/> />
</div> </div>
<!-- Empty State -->
<div v-if="filteredProducts.length === 0" class="text-center py-10 text-gray-500 dark:text-gray-400">
{{ t('products.no_products') }}
</div>
<!-- Pagination --> <!-- Pagination -->
<div class="pt-4 flex justify-center items-center dark:text-white! text-dark"> <div v-if="filteredProducts.length > 0" class="pt-4 flex justify-center items-center dark:text-white! text-dark">
<UPagination <UPagination
v-model:page="page" v-model:page="page"
:page-count="pageSize" :page-count="pageSize"
@@ -241,8 +237,8 @@ function clearFilters() {
</div> </div>
<!-- Results count --> <!-- Results count -->
<div class="text-sm text-gray-600 dark:text-gray-400 text-center"> <div v-if="filteredProducts.length > 0" class="text-sm text-gray-600 dark:text-gray-400 text-center">
showing {{ paginatedProducts.length }} of {{ totalItems }} paginatedProducts {{ t('products.showing') }} {{ paginatedProducts.length }} {{ t('products.of') }} {{ totalItems }} {{ t('products.products') }}
</div> </div>
</div> </div>
</div> </div>