fix: new page ProductsView

This commit is contained in:
2026-03-12 10:03:13 +01:00
parent 9fc208192d
commit ea8d05ddce
4 changed files with 354 additions and 0 deletions

View File

@@ -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 -->

View File

@@ -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' },
],
},
{

View 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>

View File

@@ -0,0 +1,94 @@
-- +goose Up
-- Add translations for products and navigation
-- Register new components
INSERT IGNORE INTO b2b_components (id, name) VALUES (304, 'products');
INSERT IGNORE INTO b2b_components (id, name) VALUES (305, 'nav');
-- Component: products (component_id = 304)
INSERT IGNORE b2b_translations (lang_id, scope_id, component_id, `key`, data) VALUES
-- English (lang_id = 1)
(1, 3, 304, 'title', 'Products'),
(1, 3, 304, 'image', 'Image'),
(1, 3, 304, 'product_name', 'Product Name'),
(1, 3, 304, 'product_code', 'Product Code'),
(1, 3, 304, 'in_stock', 'In Stock'),
(1, 3, 304, 'price', 'Price'),
(1, 3, 304, 'count', 'Count'),
(1, 3, 304, 'add_to_cart', 'Add to cart'),
(1, 3, 304, 'search_by_name', 'Search by name'),
(1, 3, 304, 'search_by_code', 'Search by code'),
(1, 3, 304, 'search_name_placeholder', 'Enter product name'),
(1, 3, 304, 'search_code_placeholder', 'Enter product code'),
(1, 3, 304, 'price_from', 'From'),
(1, 3, 304, 'price_to', 'To'),
(1, 3, 304, 'clear_filters', 'Clear'),
(1, 3, 304, 'login_to_view', 'Please login to view products'),
(1, 3, 304, 'showing', 'Showing'),
(1, 3, 304, 'of', 'of'),
(1, 3, 304, 'products', 'products'),
(1, 3, 304, 'yes', 'Yes'),
(1, 3, 304, 'no', 'No'),
(1, 3, 304, 'added_to_cart', 'Added to cart'),
-- Polish (lang_id = 2)
(2, 3, 304, 'title', 'Produkty'),
(2, 3, 304, 'image', 'Obrazek'),
(2, 3, 304, 'product_name', 'Nazwa produktu'),
(2, 3, 304, 'product_code', 'Kod produktu'),
(2, 3, 304, 'in_stock', 'W magazynie'),
(2, 3, 304, 'price', 'Cena'),
(2, 3, 304, 'count', 'Ilość'),
(2, 3, 304, 'add_to_cart', 'Dodaj do koszyka'),
(2, 3, 304, 'search_by_name', 'Szukaj po nazwie'),
(2, 3, 304, 'search_by_code', 'Szukaj po kodzie'),
(2, 3, 304, 'search_name_placeholder', 'Wpisz nazwę produktu'),
(2, 3, 304, 'search_code_placeholder', 'Wpisz kod produktu'),
(2, 3, 304, 'price_from', 'Od'),
(2, 3, 304, 'price_to', 'Do'),
(2, 3, 304, 'clear_filters', 'Wyczyść'),
(2, 3, 304, 'login_to_view', 'Zaloguj się, aby zobaczyć produkty'),
(2, 3, 304, 'showing', 'Wyświetlanie'),
(2, 3, 304, 'of', 'z'),
(2, 3, 304, 'products', 'produktów'),
(2, 3, 304, 'yes', 'Tak'),
(2, 3, 304, 'no', 'Nie'),
(2, 3, 304, 'added_to_cart', 'Dodano do koszyka'),
-- Czech (lang_id = 3)
(3, 3, 304, 'title', 'Produkty'),
(3, 3, 304, 'image', 'Obrázek'),
(3, 3, 304, 'product_name', 'Název produktu'),
(3, 3, 304, 'product_code', 'Kód produktu'),
(3, 3, 304, 'in_stock', 'Skladem'),
(3, 3, 304, 'price', 'Cena'),
(3, 3, 304, 'count', 'Množství'),
(3, 3, 304, 'add_to_cart', 'Přidat do košíku'),
(3, 3, 304, 'search_by_name', 'Hledat podle názvu'),
(3, 3, 304, 'search_by_code', 'Hledat podle kódu'),
(3, 3, 304, 'search_name_placeholder', 'Zadejte název produktu'),
(3, 3, 304, 'search_code_placeholder', 'Zadejte kód produktu'),
(3, 3, 304, 'price_from', 'Od'),
(3, 3, 304, 'price_to', 'Do'),
(3, 3, 304, 'clear_filters', 'Vymazat'),
(3, 3, 304, 'login_to_view', 'Přihlaste se pro zobrazení produktů'),
(3, 3, 304, 'showing', 'Zobrazeno'),
(3, 3, 304, 'of', 'z'),
(3, 3, 304, 'products', 'produktů'),
(3, 3, 304, 'yes', 'Ano'),
(3, 3, 304, 'no', 'Ne'),
(3, 3, 304, 'added_to_cart', 'Přidáno do košíku');
-- Component: nav (component_id = 305)
INSERT IGNORE b2b_translations (lang_id, scope_id, component_id, `key`, data) VALUES
-- English (lang_id = 1)
(1, 3, 305, 'chart', 'Chart'),
(1, 3, 305, 'products', 'Products'),
-- Polish (lang_id = 2)
(2, 3, 305, 'chart', 'Wykres'),
(2, 3, 305, 'products', 'Produkty'),
-- Czech (lang_id = 3)
(3, 3, 305, 'chart', 'Graf'),
(3, 3, 305, 'products', 'Produkty');
-- +goose Down
-- Remove translations for these components
DELETE FROM b2b_translations WHERE scope_id = 3 AND component_id IN (304, 305);