fix: new page ProductsView
This commit is contained in:
@@ -18,6 +18,25 @@ const authStore = useAuthStore()
|
||||
</div>
|
||||
<span class="font-semibold text-gray-900 dark:text-white">TimeTracker</span>
|
||||
</RouterLink>
|
||||
|
||||
<!-- Navigation Tabs (only when authenticated) -->
|
||||
<nav v-if="authStore.isAuthenticated" class="hidden md:flex items-center gap-1">
|
||||
<RouterLink
|
||||
:to="{ name: 'home' }"
|
||||
class="px-3 py-1.5 text-sm font-medium text-black dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||
active-class="bg-gray-100 dark:bg-gray-700!"
|
||||
>
|
||||
{{ $t('nav.chart') }}
|
||||
</RouterLink>
|
||||
<RouterLink
|
||||
:to="{ name: 'products' }"
|
||||
class="px-3 py-1.5 text-sm font-medium text-black dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||
active-class="bg-gray-100 dark:bg-gray-700!"
|
||||
>
|
||||
{{ $t('nav.products') }}
|
||||
</RouterLink>
|
||||
</nav>
|
||||
|
||||
<!-- Right Side Actions -->
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- Language Switcher -->
|
||||
|
||||
@@ -31,6 +31,7 @@ const router = createRouter({
|
||||
component: Default,
|
||||
children: [
|
||||
{ path: '', component: () => import('../views/RepoChartView.vue'), name: 'home' },
|
||||
{ path: 'products', component: () => import('../views/customer/ProductsView.vue'), name: 'products' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
240
bo/src/views/customer/ProductsView.vue
Normal file
240
bo/src/views/customer/ProductsView.vue
Normal file
@@ -0,0 +1,240 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { TableColumn } from '@nuxt/ui'
|
||||
|
||||
interface Product {
|
||||
id: number
|
||||
image: string
|
||||
name: string
|
||||
code: string
|
||||
inStock: boolean
|
||||
priceFrom: number
|
||||
priceTo: number
|
||||
count: number
|
||||
}
|
||||
|
||||
const authStore = useAuthStore()
|
||||
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
|
||||
const searchName = ref('')
|
||||
const searchCode = ref('')
|
||||
const priceFrom = ref<number | null>(null)
|
||||
const priceTo = ref<number | null>(null)
|
||||
|
||||
// Pagination
|
||||
const page = ref(1)
|
||||
const pageSize = 5
|
||||
|
||||
// Filtered products
|
||||
const filteredProducts = computed(() => {
|
||||
return products.value.filter(product => {
|
||||
const matchesName = product.name.toLowerCase().includes(searchName.value.toLowerCase())
|
||||
const matchesCode = product.code.toLowerCase().includes(searchCode.value.toLowerCase())
|
||||
const matchesPriceFrom = priceFrom.value === null || product.priceFrom >= priceFrom.value
|
||||
const matchesPriceTo = priceTo.value === null || product.priceTo <= priceTo.value
|
||||
|
||||
return matchesName && matchesCode && matchesPriceFrom && matchesPriceTo
|
||||
})
|
||||
})
|
||||
|
||||
const totalItems = computed(() => filteredProducts.value.length)
|
||||
|
||||
const paginatedProducts = computed(() => {
|
||||
const start = (page.value - 1) * pageSize
|
||||
const end = start + pageSize
|
||||
return filteredProducts.value.slice(start, end)
|
||||
})
|
||||
|
||||
// Reset page when filters change
|
||||
function resetPage() {
|
||||
page.value = 1
|
||||
}
|
||||
|
||||
// Table columns
|
||||
const columns = computed<TableColumn<Product>[]>(() => [
|
||||
{
|
||||
accessorKey: 'image',
|
||||
header: 'Image',
|
||||
cell: ({ row }) => {
|
||||
return h('img', {
|
||||
src: row.getValue('image'),
|
||||
alt: 'Product',
|
||||
class: 'w-12 h-12 object-cover rounded'
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: 'Product name',
|
||||
},
|
||||
{
|
||||
accessorKey: 'code',
|
||||
header:'Product code',
|
||||
},
|
||||
{
|
||||
accessorKey: 'inStock',
|
||||
header: t('In Stock'),
|
||||
cell: ({ row }) => {
|
||||
const inStock = row.getValue('inStock')
|
||||
return h('span', {
|
||||
class: inStock ? 'text-green-600' : 'text-red-600'
|
||||
}, inStock ? t('products yes') : t('products no'))
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'price',
|
||||
header: 'Price',
|
||||
cell: ({ row }) => {
|
||||
const priceFromVal = row.original.priceFrom
|
||||
const priceToVal = row.original.priceTo
|
||||
return `$${priceFromVal} - $${priceToVal}`
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'count',
|
||||
header: 'Count',
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: '',
|
||||
cell: ({ row }) => {
|
||||
return h('div', { class: 'flex gap-2' }, [
|
||||
h('button', {
|
||||
class: 'px-3 py-1.5 text-sm font-medium bg-primary text-white rounded-lg hover:bg-blue-600 transition-colors',
|
||||
onClick: () => addToCart(row.original)
|
||||
}, 'add to cart'),
|
||||
h('button', {
|
||||
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)
|
||||
}, '+')
|
||||
])
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
// Helper for render function
|
||||
import { h } from 'vue'
|
||||
|
||||
// Actions
|
||||
function addToCart(product: Product) {
|
||||
console.log('Add to cart:', product)
|
||||
alert(`('added_to_cart'): ${product.name}`)
|
||||
}
|
||||
|
||||
function incrementCount(product: Product) {
|
||||
console.log('Increment:', product)
|
||||
product.count++
|
||||
}
|
||||
|
||||
function clearFilters() {
|
||||
searchName.value = ''
|
||||
searchCode.value = ''
|
||||
priceFrom.value = null
|
||||
priceTo.value = null
|
||||
resetPage()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container">
|
||||
<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>
|
||||
|
||||
<div v-if="!authStore.isAuthenticated" class="mb-4 p-3 bg-yellow-100 text-yellow-700 rounded">
|
||||
login_to_view
|
||||
</div>
|
||||
|
||||
<div v-if="authStore.isAuthenticated" class="space-y-4">
|
||||
<!-- Search Filters -->
|
||||
<div class="flex flex-wrap gap-4 mb-6 p-4 border border-(--border-light) dark:border-(--border-dark) rounded">
|
||||
<div class="flex flex-col min-w-[200px]">
|
||||
<label class="mb-1 text-sm font-medium text-black dark:text-white">search by name</label>
|
||||
<UInput
|
||||
v-model="searchName"
|
||||
placeholder="search name placeholder"
|
||||
@update:model-value="resetPage"
|
||||
class="dark:text-white text-black"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col min-w-[200px]">
|
||||
<label class="mb-1 text-sm font-medium text-black dark:text-white">search by code</label>
|
||||
<UInput
|
||||
v-model="searchCode"
|
||||
placeholder="search code placeholder"
|
||||
@update:model-value="resetPage"
|
||||
class="dark:text-white text-black"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col min-w-[150px]">
|
||||
<label class="mb-1 text-sm font-medium text-black dark:text-white">price from</label>
|
||||
<UInput
|
||||
v-model="priceFrom"
|
||||
type="number"
|
||||
placeholder="price from"
|
||||
@update:model-value="resetPage"
|
||||
class="dark:text-white text-black"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col min-w-[150px]">
|
||||
<label class="mb-1 text-sm font-medium text-black dark:text-white">price to</label>
|
||||
<UInput
|
||||
v-model="priceTo"
|
||||
type="number"
|
||||
placeholder="price_to"
|
||||
@update:model-value="resetPage"
|
||||
class="dark:text-white text-black"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-end">
|
||||
<button
|
||||
@click="clearFilters"
|
||||
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
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Products Table -->
|
||||
<div class="border border-(--border-light) dark:border-(--border-dark) rounded overflow-hidden">
|
||||
<UTable
|
||||
:data="paginatedProducts"
|
||||
:columns="columns"
|
||||
class="dark:text-white! text-dark"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="pt-4 flex justify-center items-center dark:text-white! text-dark">
|
||||
<UPagination
|
||||
v-model:page="page"
|
||||
:page-count="pageSize"
|
||||
:total="totalItems"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Results count -->
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400 text-center">
|
||||
showing {{ paginatedProducts.length }} of {{ totalItems }} products
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user