fix: api
This commit is contained in:
85
bo/src/stores/product.ts
Normal file
85
bo/src/stores/product.ts
Normal 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,
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user