fix: style
This commit is contained in:
@@ -42,6 +42,13 @@ export const uiOptions: NuxtUIOptions = {
|
|||||||
itemLeadingIcon: 'text-(--black)! dark:text-white!'
|
itemLeadingIcon: 'text-(--black)! dark:text-white!'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
table: {
|
||||||
|
slots: {
|
||||||
|
base: 'border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0! bg-(--second-light) dark:bg-(--main-dark)',
|
||||||
|
tr: 'border-b! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0! text-(--black)! dark:text-white!',
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -22,14 +22,16 @@ body {
|
|||||||
--black: #1A1A1A;
|
--black: #1A1A1A;
|
||||||
|
|
||||||
/* gray */
|
/* gray */
|
||||||
--gray: #6B6B6B;
|
--gray: #E5E7EB;
|
||||||
--gray-dark: #A3A3A3;
|
--gray-dark: #6B7280;
|
||||||
|
|
||||||
/* borders */
|
/* borders */
|
||||||
--border-light: #E8E7E0;
|
--border-light: #E8E7E0;
|
||||||
--border-dark: #3F3E3D;
|
--border-dark: #3F3E3D;
|
||||||
|
|
||||||
/* text */
|
/* text */
|
||||||
|
--accent-blue-dark: #3B82F6;
|
||||||
|
--accent-blue-light:#2563EB;
|
||||||
--text-dark: #FFFEFB;
|
--text-dark: #FFFEFB;
|
||||||
|
|
||||||
/* placeholder */
|
/* placeholder */
|
||||||
|
|||||||
@@ -18,25 +18,6 @@ const authStore = useAuthStore()
|
|||||||
</div>
|
</div>
|
||||||
<span class="font-semibold text-gray-900 dark:text-white">TimeTracker</span>
|
<span class="font-semibold text-gray-900 dark:text-white">TimeTracker</span>
|
||||||
</RouterLink>
|
</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 -->
|
<!-- Right Side Actions -->
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<!-- Language Switcher -->
|
<!-- Language Switcher -->
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const authStore = useAuthStore()
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<header
|
<header
|
||||||
class="fixed top-0 left-0 right-0 z-50 bg-white/80 dark:bg-(--black) backdrop-blur-md border-b border-(--border-light) dark:border-(--border-dark)">
|
class="fixed top-0 left-0 right-0 z-50 bg-(--main-light)/80 dark:bg-(--black) backdrop-blur-md border-b border-(--border-light) dark:border-(--border-dark)">
|
||||||
<div class="container px-4 sm:px-6 lg:px-8">
|
<div class="container px-4 sm:px-6 lg:px-8">
|
||||||
<div class="flex items-center justify-between h-14">
|
<div class="flex items-center justify-between h-14">
|
||||||
<!-- Logo -->
|
<!-- Logo -->
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<USelectMenu v-model="locale" :items="langs" class="w-40 bg-white dark:bg-(--black) rounded-md shadow-sm hover:none!"
|
<USelectMenu v-model="locale" :items="langs" class="w-40 bg-(--main-light) dark:bg-(--black) rounded-md shadow-sm hover:none!"
|
||||||
valueKey="iso_code" :searchInput="false">
|
valueKey="iso_code" :searchInput="false">
|
||||||
<template #default="{ modelValue }">
|
<template #default="{ modelValue }">
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
|
|||||||
@@ -6,11 +6,9 @@ import TopBar from '@/components/TopBar.vue';
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-screen grid grid-rows-[auto_1fr_auto]">
|
<div class="h-screen grid grid-rows-[auto_1fr_auto]">
|
||||||
<!-- <header class="w-full bg-gray-100 text-primary shadow border-b-gray-300 p-4 mb-8">Header</header> -->
|
<!-- <header class="w-full bg-gray-100 text-primary shadow border-b-gray-300 p-4 mb-8">Header</header> -->
|
||||||
<UContainer>
|
|
||||||
<main class="p-10">
|
<main class="p-10">
|
||||||
<TopBar/>
|
<TopBar/>
|
||||||
<router-view />
|
<router-view />
|
||||||
</main>
|
</main>
|
||||||
</UContainer>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ const router = createRouter({
|
|||||||
children: [
|
children: [
|
||||||
{ path: '', component: () => import('../views/RepoChartView.vue'), name: 'home' },
|
{ path: '', component: () => import('../views/RepoChartView.vue'), name: 'home' },
|
||||||
{ path: 'products', component: () => import('../views/customer/ProductsView.vue'), name: 'products' },
|
{ path: 'products', component: () => import('../views/customer/ProductsView.vue'), name: 'products' },
|
||||||
|
{ path: 'products/:id', component: () => import('../views/customer/ProductDetailView.vue'), name: 'product-detail' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -105,21 +105,21 @@ const PrivacyComponent = computed(() =>
|
|||||||
|
|
||||||
<div class="flex items-center justify-between w-full dark:text-white text-black">
|
<div class="flex items-center justify-between w-full dark:text-white text-black">
|
||||||
<button variant="link" size="sm" @click="goToPasswordRecovery"
|
<button variant="link" size="sm" @click="goToPasswordRecovery"
|
||||||
class="text-[15px] w-full flex justify-end text-(--color-blue-600) dark:text-(--color-blue-500) cursor-pointer">
|
class="text-[15px] w-full flex justify-end text-(--accent-blue-light) dark:text-(--accent-blue-dark) cursor-pointer">
|
||||||
{{ $t('general.forgot_password') }}?
|
{{ $t('general.forgot_password') }}?
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<UButton type="submit" :loading="authStore.loading"
|
<UButton type="submit" :loading="authStore.loading"
|
||||||
class="w-full flex justify-center text-white bg-(--color-blue-600) dark:bg-(--color-blue-500) cursor-pointer">
|
class="w-full flex justify-center text-white bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) cursor-pointer">
|
||||||
{{ $t('general.sign_in') }}
|
{{ $t('general.sign_in') }}
|
||||||
</UButton>
|
</UButton>
|
||||||
</UForm>
|
</UForm>
|
||||||
|
|
||||||
<!-- Divider -->
|
<!-- Divider -->
|
||||||
<div class="flex items-center gap-3 my-1">
|
<div class="flex items-center gap-3 my-1">
|
||||||
<div class="flex-1 h-px bg-gray-200 dark:bg-gray-700" />
|
<div class="flex-1 h-px bg-gray-200 dark:dark:hover:bg-(--gray-dark)" />
|
||||||
<span class="text-xs text-gray-400 dark:text-gray-500">{{ $t('general.or') }}</span>
|
<span class="text-xs text-gray-400 dark:text-gray-500">{{ $t('general.or') }}</span>
|
||||||
<div class="flex-1 h-px bg-gray-200 dark:bg-gray-700" />
|
<div class="flex-1 h-px bg-gray-200 dark:dark:hover:bg-(--gray-dark)" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Google Sign In -->
|
<!-- Google Sign In -->
|
||||||
@@ -147,18 +147,18 @@ const PrivacyComponent = computed(() =>
|
|||||||
<p class="dark:text-white text-black">
|
<p class="dark:text-white text-black">
|
||||||
{{ $t('general.dont_have_an_account') }}?
|
{{ $t('general.dont_have_an_account') }}?
|
||||||
<button variant="link" size="sm"
|
<button variant="link" size="sm"
|
||||||
class="text-[15px] text-(--color-blue-600) dark:text-(--color-blue-500) cursor-pointer"
|
class="text-[15px] text-(--accent-blue-light) dark:text-(--accent-blue-dark) cursor-pointer"
|
||||||
@click="goToRegister">{{ $t('general.create_account_now') }}</button>
|
@click="goToRegister">{{ $t('general.create_account_now') }}</button>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-8 text-center text-xs dark:text-white text-black">
|
<p class="mt-8 text-center text-xs dark:text-white text-black">
|
||||||
{{ $t('general.by_signing_in_you_agree_to_our') }}
|
{{ $t('general.by_signing_in_you_agree_to_our') }}
|
||||||
<span @click="showTherms = !showTherms"
|
<span @click="showTherms = !showTherms"
|
||||||
class="cursor-pointer underline text-(--color-blue-600) dark:text-(--color-blue-500) cursor-pointer">{{
|
class="cursor-pointer underline text-(--accent-blue-light) dark:text-(--accent-blue-dark) cursor-pointer">{{
|
||||||
$t('general.terms_of_service') }}</span>
|
$t('general.terms_of_service') }}</span>
|
||||||
{{ $t('general.and') }}
|
{{ $t('general.and') }}
|
||||||
<span @click="showPrivacy = !showPrivacy"
|
<span @click="showPrivacy = !showPrivacy"
|
||||||
class="cursor-pointer underline text-(--color-blue-600) dark:text-(--color-blue-500) cursor-pointer">{{
|
class="cursor-pointer underline text-(--accent-blue-light) dark:text-(--accent-blue-dark) cursor-pointer">{{
|
||||||
$t('general.privacy_policy') }}</span>
|
$t('general.privacy_policy') }}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ function validate(): FormError[] {
|
|||||||
</UFormField>
|
</UFormField>
|
||||||
|
|
||||||
<UButton type="submit" block :loading="authStore.loading"
|
<UButton type="submit" block :loading="authStore.loading"
|
||||||
class="text-white bg-(--color-blue-600) dark:bg-(--color-blue-500) cursor-pointer">
|
class="text-white bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) cursor-pointer">
|
||||||
{{ $t('general.send_password_reset_link') }}
|
{{ $t('general.send_password_reset_link') }}
|
||||||
</UButton>
|
</UButton>
|
||||||
</UForm>
|
</UForm>
|
||||||
@@ -88,13 +88,13 @@ function validate(): FormError[] {
|
|||||||
<button color="neutral" variant="outline" :loading="authStore.loading"
|
<button color="neutral" variant="outline" :loading="authStore.loading"
|
||||||
class="w-full flex items-center gap-2 justify-center text-[15px] dark:text-white text-black cursor-pointer"
|
class="w-full flex items-center gap-2 justify-center text-[15px] dark:text-white text-black cursor-pointer"
|
||||||
@click="goToLogin">
|
@click="goToLogin">
|
||||||
<UIcon name="mingcute:arrow-left-line" class="text-(--color-blue-600) dark:text-(--color-blue-500) text-[16px]" />
|
<UIcon name="mingcute:arrow-left-line" class="text-(--accent-blue-light) dark:text-(--accent-blue-dark) text-[16px]" />
|
||||||
{{ $t('general.back_to_sign_in') }}
|
{{ $t('general.back_to_sign_in') }}
|
||||||
</button>
|
</button>
|
||||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
{{ $t('general.dont_have_an_account') }}
|
{{ $t('general.dont_have_an_account') }}
|
||||||
<button variant="link" size="sm" @click="goToRegister"
|
<button variant="link" size="sm" @click="goToRegister"
|
||||||
class=" text-[15px] text-(--color-blue-600) dark:text-(--color-blue-500) cursor-pointer">{{
|
class=" text-[15px] text-(--accent-blue-light) dark:text-(--accent-blue-dark) cursor-pointer">{{
|
||||||
$t('general.create_account_now') }}
|
$t('general.create_account_now') }}
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -82,12 +82,12 @@
|
|||||||
<span class="dark:text-white text-black">
|
<span class="dark:text-white text-black">
|
||||||
{{ $t('general.i_agree_to_the') }}
|
{{ $t('general.i_agree_to_the') }}
|
||||||
<span @click="showTherms = !showTherms"
|
<span @click="showTherms = !showTherms"
|
||||||
class="cursor-pointer underline text-(--color-blue-600) dark:text-(--color-blue-500)">{{
|
class="cursor-pointer underline text-(--accent-blue-light) dark:text-(--accent-blue-dark)">{{
|
||||||
$t('general.terms_of_service')
|
$t('general.terms_of_service')
|
||||||
}}</span>
|
}}</span>
|
||||||
{{ $t('general.and') }}
|
{{ $t('general.and') }}
|
||||||
<span @click="showPrivacy = !showPrivacy"
|
<span @click="showPrivacy = !showPrivacy"
|
||||||
class="cursor-pointer underline text-(--color-blue-600) dark:text-(--color-blue-500)">{{
|
class="cursor-pointer underline text-(--accent-blue-light) dark:text-(--accent-blue-dark)">{{
|
||||||
$t('general.privacy_policy')
|
$t('general.privacy_policy')
|
||||||
}}</span>
|
}}</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -95,7 +95,7 @@
|
|||||||
</UCheckbox>
|
</UCheckbox>
|
||||||
|
|
||||||
<UButton type="submit" block :loading="authStore.loading" :disabled="!acceptTerms"
|
<UButton type="submit" block :loading="authStore.loading" :disabled="!acceptTerms"
|
||||||
class="text-white bg-(--color-blue-600) dark:bg-(--color-blue-500) cursor-pointer">
|
class="text-white bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) cursor-pointer">
|
||||||
{{ $t('general.create_account') }}
|
{{ $t('general.create_account') }}
|
||||||
</UButton>
|
</UButton>
|
||||||
|
|
||||||
@@ -104,7 +104,7 @@
|
|||||||
{{ $t('general.already_have_an_account') }}
|
{{ $t('general.already_have_an_account') }}
|
||||||
</p>
|
</p>
|
||||||
<UButton color="neutral" variant="outline" :loading="authStore.loading"
|
<UButton color="neutral" variant="outline" :loading="authStore.loading"
|
||||||
class="w-full flex justify-center dark:text-white hover:text-white cursor-pointer hover:bg-(--color-blue-600) dark:hover:bg-(--color-blue-500) border border-(--border-light)! dark:border-(--border-dark)!"
|
class="w-full flex justify-center dark:text-white hover:text-white cursor-pointer hover:bg-(--accent-blue-light) dark:hover:bg-(--accent-blue-dark) border border-(--border-light)! dark:border-(--border-dark)!"
|
||||||
@click="goToLogin">{{ $t('general.sign_in') }}</UButton>
|
@click="goToLogin">{{ $t('general.sign_in') }}</UButton>
|
||||||
</div>
|
</div>
|
||||||
</UForm>
|
</UForm>
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ const columns = computed<TableColumn<IssueTimeSummary>[]>(() => [
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="p-6 bg-white dark:bg-(--black) min-h-screen font-sans">
|
<div class="p-6 bg-(--main-light) dark:bg-(--black) font-sans">
|
||||||
<h1 class="text-2xl font-bold mb-6 text-black dark:text-white">{{ $t('repo_chart.repository_work_chart') }}
|
<h1 class="text-2xl font-bold mb-6 text-black dark:text-white">{{ $t('repo_chart.repository_work_chart') }}
|
||||||
</h1>
|
</h1>
|
||||||
<div v-if="error" class="mb-4 p-3 bg-red-100 text-red-700 rounded">{{ error }}</div>
|
<div v-if="error" class="mb-4 p-3 bg-red-100 text-red-700 rounded">{{ error }}</div>
|
||||||
@@ -195,14 +195,14 @@ const columns = computed<TableColumn<IssueTimeSummary>[]>(() => [
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="authStore.isAuthenticated" class="flex flex-wrap gap-4 mb-6">
|
<div v-if="authStore.isAuthenticated" class="flex flex-wrap gap-4 mb-6">
|
||||||
<div class="flex flex-col min-w-[192px]">
|
<div class="flex flex-col">
|
||||||
<label class="mb-1 text-sm font-medium text-black dark:text-white">{{ $t('repo_chart.repository')
|
<label class="mb-1 text-sm font-medium text-black dark:text-white">{{ $t('repo_chart.repository')
|
||||||
}}</label>
|
}}</label>
|
||||||
<USelect v-model="selectedRepo" :items="items" :disabled="loading"
|
<USelect v-model="selectedRepo" :items="items" :disabled="loading"
|
||||||
:placeholder="$t('repo_chart.select_a_repository')" class="dark:text-white text-black " />
|
:placeholder="$t('repo_chart.select_a_repository')" class="dark:text-white text-black " />
|
||||||
<!-- Select a repository -->
|
<!-- Select a repository -->
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col min-w-[160px]">
|
<div class="flex flex-col">
|
||||||
<label class="mb-1 text-sm font-medium text-black dark:text-white">{{ $t('repo_chart.year')
|
<label class="mb-1 text-sm font-medium text-black dark:text-white">{{ $t('repo_chart.year')
|
||||||
}}</label>
|
}}</label>
|
||||||
<USelect v-model="selectedYear" :items="yearItems"
|
<USelect v-model="selectedYear" :items="yearItems"
|
||||||
@@ -210,7 +210,7 @@ const columns = computed<TableColumn<IssueTimeSummary>[]>(() => [
|
|||||||
:placeholder="$t('repo_chart.select_a_year')"
|
:placeholder="$t('repo_chart.select_a_year')"
|
||||||
class="dark:text-white text-black placeholder:text-(--placeholder)" />
|
class="dark:text-white text-black placeholder:text-(--placeholder)" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col min-w-[192px]">
|
<div class="flex flex-col">
|
||||||
<label class="mb-1 text-sm font-medium text-black dark:text-white">{{ $t('repo_chart.quarter')
|
<label class="mb-1 text-sm font-medium text-black dark:text-white">{{ $t('repo_chart.quarter')
|
||||||
}}</label>
|
}}</label>
|
||||||
<USelect v-model="selectedQuarter" :items="quarterItems"
|
<USelect v-model="selectedQuarter" :items="quarterItems"
|
||||||
@@ -241,12 +241,12 @@ const columns = computed<TableColumn<IssueTimeSummary>[]>(() => [
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="selectedQuarter && !loading && authStore.isAuthenticated && !hasIssues"
|
<div v-else-if="selectedQuarter && !loading && authStore.isAuthenticated && !hasIssues"
|
||||||
class="mt-4 p-3 dark:bg-(--black) bg-white border border-(--border-light) dark:border-(--border-dark) dark:text-white! text-black rounded">
|
class="mt-4 p-3 dark:bg-(--black) bg-(--main-light) border border-(--border-light) dark:border-(--border-dark) dark:text-white! text-black rounded">
|
||||||
{{ $t('validate_error.no_issues_for_quarter') }}.
|
{{ $t('validate_error.no_issues_for_quarter') }}.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="!loading && authStore.isAuthenticated"
|
<div v-else-if="!loading && authStore.isAuthenticated"
|
||||||
class="p-3 dark:bg-(--black) bg-white border border-(--border-light) dark:border-(--border-dark) rounded dark:text-white! text-black">
|
class="p-3 dark:bg-(--black) bg-(--main-light) border border-(--border-light) dark:border-(--border-dark) rounded dark:text-white! text-black">
|
||||||
<span v-if="!selectedRepo">{{ $t('repo_chart.select_repo_to_view_data') }}</span>
|
<span v-if="!selectedRepo">{{ $t('repo_chart.select_repo_to_view_data') }}</span>
|
||||||
<span v-else-if="!selectedYear">{{ $t('repo_chart.select_year_to_view_data') }}</span>
|
<span v-else-if="!selectedYear">{{ $t('repo_chart.select_year_to_view_data') }}</span>
|
||||||
<span v-else-if="!selectedQuarter">{{ $t('repo_chart.select_quarter_to_view_issues') }}</span>
|
<span v-else-if="!selectedQuarter">{{ $t('repo_chart.select_quarter_to_view_issues') }}</span>
|
||||||
|
|||||||
@@ -110,13 +110,13 @@ function validate(): FormError[] {
|
|||||||
</UFormField>
|
</UFormField>
|
||||||
|
|
||||||
<UButton type="submit" block :loading="authStore.loading"
|
<UButton type="submit" block :loading="authStore.loading"
|
||||||
class="text-white! bg-(--color-blue-600) dark:bg-(--color-blue-500) cursor-pointer">
|
class="text-white! bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) cursor-pointer">
|
||||||
{{ $t('general.reset_password') }}
|
{{ $t('general.reset_password') }}
|
||||||
</UButton>
|
</UButton>
|
||||||
|
|
||||||
<div class="text-center border-t dark:border-(--border-dark) border-(--border-light) pt-4">
|
<div class="text-center border-t dark:border-(--border-dark) border-(--border-light) pt-4">
|
||||||
<button color="neutral" variant="ghost" @click="goToLogin"
|
<button color="neutral" variant="ghost" @click="goToLogin"
|
||||||
class="text-[15px] flex items-center gap-2 text-(--color-blue-600) dark:text-(--color-blue-500) cursor-pointer">
|
class="text-[15px] flex items-center gap-2 text-(--accent-blue-light) dark:text-(--accent-blue-dark) cursor-pointer">
|
||||||
<UIcon name="mingcute:arrow-left-line" />
|
<UIcon name="mingcute:arrow-left-line" />
|
||||||
{{ $t('general.back_to_sign_in') }}
|
{{ $t('general.back_to_sign_in') }}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ function goToLogin() {
|
|||||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
{{ $t('verify_email.already_registered') }}
|
{{ $t('verify_email.already_registered') }}
|
||||||
<button variant="link" size="sm" @click="goToLogin"
|
<button variant="link" size="sm" @click="goToLogin"
|
||||||
class="cursor-pointer text-(--color-blue-600) dark:text-(--color-blue-500)"> {{ $t('general.sign_in')
|
class="cursor-pointer text-(--accent-blue-light) dark:text-(--accent-blue-dark)"> {{ $t('general.sign_in')
|
||||||
}}
|
}}
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
308
bo/src/views/customer/ProductDetailView.vue
Normal file
308
bo/src/views/customer/ProductDetailView.vue
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import type { TableColumn } from '@nuxt/ui'
|
||||||
|
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 router = useRouter()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
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
|
||||||
|
const productId = computed(() => Number(route.params.id))
|
||||||
|
const product = computed(() => products.value.find(p => p.id === productId.value))
|
||||||
|
|
||||||
|
// Active tab for the four buttons
|
||||||
|
const activeTab = ref<'description' | 'howToUse' | 'productDetails' | 'documents'>('description')
|
||||||
|
|
||||||
|
// Mock variants (same structure as products but with variants)
|
||||||
|
const variants = computed(() => {
|
||||||
|
if (!product.value) return []
|
||||||
|
// Create mock variants based on the product
|
||||||
|
return [
|
||||||
|
{ ...product.value, id: product.value.id * 10 + 1, name: `${product.value.name} - Standard`, priceFrom: product.value.priceFrom, priceTo: product.value.priceFrom + 50, count: product.value.count },
|
||||||
|
{ ...product.value, id: product.value.id * 10 + 2, name: `${product.value.name} - Premium`, priceFrom: product.value.priceFrom + 100, priceTo: product.value.priceTo, count: Math.floor(product.value.count / 2) },
|
||||||
|
{ ...product.value, id: product.value.id * 10 + 3, name: `${product.value.name} - Bundle`, priceFrom: product.value.priceTo + 50, priceTo: product.value.priceTo + 150, count: Math.floor(product.value.count / 3) },
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Pagination for variants table
|
||||||
|
const page = ref(1)
|
||||||
|
const pageSize = 5
|
||||||
|
|
||||||
|
const totalItems = computed(() => variants.value.length)
|
||||||
|
|
||||||
|
const paginatedVariants = computed(() => {
|
||||||
|
const start = (page.value - 1) * pageSize
|
||||||
|
const end = start + pageSize
|
||||||
|
return variants.value.slice(start, end)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Table columns for variants
|
||||||
|
const columns = computed<TableColumn<any>[]>(() => [
|
||||||
|
{
|
||||||
|
accessorKey: 'image',
|
||||||
|
header: () => h('div', { class: 'text-center' }, t('products.image')),
|
||||||
|
cell: ({ row }) => h('img', {
|
||||||
|
src: row.getValue('image'),
|
||||||
|
alt: 'Product',
|
||||||
|
class: 'w-12 h-12 object-cover rounded'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'name',
|
||||||
|
header: t('products.product_name'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'code',
|
||||||
|
header: t('products.product_code'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'inStock',
|
||||||
|
header: t('products.in_stock'),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const inStock = row.getValue('inStock')
|
||||||
|
return h('span', {
|
||||||
|
class: inStock ? 'text-green-600 font-medium' : 'text-red-600 font-medium'
|
||||||
|
}, inStock ? t('products.yes') : t('products.no'))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'price',
|
||||||
|
header: t('products.price'),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const priceFromVal = row.original.priceFrom
|
||||||
|
const priceToVal = row.original.priceTo
|
||||||
|
return `$${priceFromVal} - $${priceToVal}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'count',
|
||||||
|
header: t('products.count'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'actions',
|
||||||
|
header: '',
|
||||||
|
cell: ({ row }) => 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)
|
||||||
|
}, t('products.add_to_cart')),
|
||||||
|
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',
|
||||||
|
onClick: () => incrementCount(row.original)
|
||||||
|
}, '+')
|
||||||
|
])
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
function addToCart(product: Product) {
|
||||||
|
console.log('Add to cart:', product)
|
||||||
|
}
|
||||||
|
|
||||||
|
function incrementCount(product: Product) {
|
||||||
|
product.count++
|
||||||
|
}
|
||||||
|
|
||||||
|
function goBack() {
|
||||||
|
router.push({ name: 'products' })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="container">
|
||||||
|
<div class="p-6 bg-(--main-light) dark:bg-(--black) min-h-screen font-sans">
|
||||||
|
<!-- Back Button -->
|
||||||
|
<button
|
||||||
|
@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"
|
||||||
|
>
|
||||||
|
← {{ t('products.back_to_list') }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div v-if="!authStore.isAuthenticated" class="mb-4 p-3 bg-yellow-100 text-yellow-700 rounded">
|
||||||
|
{{ t('products.login_to_view') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="authStore.isAuthenticated && product">
|
||||||
|
<!-- Product Header: Image and Title -->
|
||||||
|
<div class="flex flex-col md:flex-row gap-8 mb-6">
|
||||||
|
<!-- Product Image -->
|
||||||
|
<div class="w-full md:w-1/3">
|
||||||
|
<img
|
||||||
|
:src="product.image"
|
||||||
|
:alt="product.name"
|
||||||
|
class="w-full h-auto rounded-lg shadow-lg object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Product Info -->
|
||||||
|
<div class="w-full md:w-2/3 flex flex-col justify-center">
|
||||||
|
<h1 class="text-3xl font-bold mb-4 text-black dark:text-white">{{ product.name }}</h1>
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
<p class="text-gray-600 dark:text-gray-300 mb-4">{{ product.description }}</p>
|
||||||
|
|
||||||
|
<!-- Product Code -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ t('products.product_code') }}: </span>
|
||||||
|
<span class="text-lg font-semibold text-black dark:text-white">{{ product.code }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Price -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ t('products.price') }}: </span>
|
||||||
|
<span class="text-2xl font-bold text-primary">${{ product.priceFrom }} - ${{ product.priceTo }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Stock Status -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ t('products.in_stock') }}: </span>
|
||||||
|
<span :class="product.inStock ? 'text-green-600 font-medium' : 'text-red-600 font-medium'">
|
||||||
|
{{ product.inStock ? t('products.yes') : t('products.no') }}
|
||||||
|
</span>
|
||||||
|
<span v-if="product.inStock" class="ml-2 text-gray-500">({{ product.count }} {{ t('products.available') }})</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- First Divider -->
|
||||||
|
<hr class="border-gray-300 dark:border-gray-600 my-6" />
|
||||||
|
|
||||||
|
<!-- Four Buttons -->
|
||||||
|
<div class="flex flex-wrap gap-2 mb-6">
|
||||||
|
<button
|
||||||
|
@click="activeTab = 'description'"
|
||||||
|
:class="[
|
||||||
|
'px-4 py-2 text-sm font-medium rounded-lg transition-colors',
|
||||||
|
activeTab === 'description'
|
||||||
|
? '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'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
{{ t('products.description') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="activeTab = 'howToUse'"
|
||||||
|
:class="[
|
||||||
|
'px-4 py-2 text-sm font-medium rounded-lg transition-colors',
|
||||||
|
activeTab === 'howToUse'
|
||||||
|
? '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'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
{{ t('products.how_to_use') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="activeTab = 'productDetails'"
|
||||||
|
:class="[
|
||||||
|
'px-4 py-2 text-sm font-medium rounded-lg transition-colors',
|
||||||
|
activeTab === 'productDetails'
|
||||||
|
? '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'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
{{ t('products.product_details') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="activeTab = 'documents'"
|
||||||
|
:class="[
|
||||||
|
'px-4 py-2 text-sm font-medium rounded-lg transition-colors',
|
||||||
|
activeTab === 'documents'
|
||||||
|
? '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'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
{{ t('products.documents') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab Content -->
|
||||||
|
<div class="mb-6 p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
||||||
|
<div v-if="activeTab === 'description'" class="text-black dark:text-white">
|
||||||
|
<h3 class="text-lg font-semibold mb-2">{{ t('products.description') }}</h3>
|
||||||
|
<p>{{ product.description }}</p>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="activeTab === 'howToUse'" class="text-black dark:text-white">
|
||||||
|
<h3 class="text-lg font-semibold mb-2">{{ t('products.how_to_use') }}</h3>
|
||||||
|
<p>{{ product.howToUse }}</p>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="activeTab === 'productDetails'" class="text-black dark:text-white">
|
||||||
|
<h3 class="text-lg font-semibold mb-2">{{ t('products.product_details') }}</h3>
|
||||||
|
<p>{{ product.productDetails }}</p>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="activeTab === 'documents'" class="text-black dark:text-white">
|
||||||
|
<h3 class="text-lg font-semibold mb-2">{{ t('products.documents') }}</h3>
|
||||||
|
<ul class="list-disc list-inside">
|
||||||
|
<li><a href="#" class="text-primary hover:underline">User Manual.pdf</a></li>
|
||||||
|
<li><a href="#" class="text-primary hover:underline">Technical Specifications.pdf</a></li>
|
||||||
|
<li><a href="#" class="text-primary hover:underline">Warranty Information.pdf</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Second Divider -->
|
||||||
|
<hr class="border-gray-300 dark:border-gray-600 my-6" />
|
||||||
|
|
||||||
|
<!-- Product Variants Section -->
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-bold mb-4 text-black dark:text-white">{{ t('products.product_variants') }}</h2>
|
||||||
|
|
||||||
|
<!-- Variants Table -->
|
||||||
|
<div class="border border-(--border-light) dark:border-(--border-dark) rounded overflow-hidden">
|
||||||
|
<UTable
|
||||||
|
:data="paginatedVariants"
|
||||||
|
: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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="authStore.isAuthenticated && !product" class="text-center py-10">
|
||||||
|
<p class="text-gray-500 dark:text-gray-400">{{ t('products.product_not_found') }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
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'
|
||||||
|
|
||||||
interface Product {
|
interface Product {
|
||||||
id: number
|
id: number
|
||||||
@@ -15,6 +17,7 @@ interface Product {
|
|||||||
count: number
|
count: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
@@ -35,8 +38,8 @@ const products = ref<Product[]>([
|
|||||||
// Search filters
|
// Search filters
|
||||||
const searchName = ref('')
|
const searchName = ref('')
|
||||||
const searchCode = ref('')
|
const searchCode = ref('')
|
||||||
const priceFrom = ref<number | null>(null)
|
const priceFromFilter = ref<number | null>(null)
|
||||||
const priceTo = ref<number | null>(null)
|
const priceToFilter = ref<number | null>(null)
|
||||||
|
|
||||||
// Pagination
|
// Pagination
|
||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
@@ -47,8 +50,8 @@ const filteredProducts = computed(() => {
|
|||||||
return products.value.filter(product => {
|
return products.value.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 = priceFrom.value === null || product.priceFrom >= priceFrom.value
|
const matchesPriceFrom = priceFromFilter.value === null || product.priceFrom >= priceFromFilter.value
|
||||||
const matchesPriceTo = priceTo.value === null || product.priceTo <= priceTo.value
|
const matchesPriceTo = priceToFilter.value === null || product.priceTo <= priceToFilter.value
|
||||||
|
|
||||||
return matchesName && matchesCode && matchesPriceFrom && matchesPriceTo
|
return matchesName && matchesCode && matchesPriceFrom && matchesPriceTo
|
||||||
})
|
})
|
||||||
@@ -67,40 +70,50 @@ function resetPage() {
|
|||||||
page.value = 1
|
page.value = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Navigate to product detail
|
||||||
|
function goToProduct(product: Product) {
|
||||||
|
router.push({ name: 'product-detail', params: { id: product.id } })
|
||||||
|
}
|
||||||
|
|
||||||
// Table columns
|
// Table columns
|
||||||
const columns = computed<TableColumn<Product>[]>(() => [
|
const columns = computed<TableColumn<Product>[]>(() => [
|
||||||
{
|
{
|
||||||
accessorKey: 'image',
|
accessorKey: 'image',
|
||||||
header: 'Image',
|
header: () => h('div', { class: 'text-center' }, t('Image')),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => h('img', {
|
||||||
return h('img', {
|
src: row.getValue('image'),
|
||||||
src: row.getValue('image'),
|
alt: 'Product',
|
||||||
alt: 'Product',
|
class: 'w-12 h-12 object-cover rounded'
|
||||||
class: 'w-12 h-12 object-cover rounded'
|
})
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'name',
|
accessorKey: 'name',
|
||||||
header: 'Product name',
|
header: 'product name',
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const product = row.original
|
||||||
|
return h('button', {
|
||||||
|
class: 'text-primary hover:underline font-medium text-left',
|
||||||
|
onClick: (e: Event) => { e.stopPropagation(); goToProduct(product) }
|
||||||
|
}, product.name)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'code',
|
accessorKey: 'code',
|
||||||
header:'Product code',
|
header: 'product code',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'inStock',
|
accessorKey: 'inStock',
|
||||||
header: t('In Stock'),
|
header: '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' : 'text-red-600'
|
class: inStock ? 'text-green-600 font-medium' : 'text-red-600 font-medium'
|
||||||
}, inStock ? t('products yes') : t('products no'))
|
}, inStock ? 'products yes' : 'products.no')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'price',
|
accessorKey: 'price',
|
||||||
header: 'Price',
|
header: '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
|
||||||
@@ -109,63 +122,59 @@ const columns = computed<TableColumn<Product>[]>(() => [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'count',
|
accessorKey: 'count',
|
||||||
header: 'Count',
|
header: 'count',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
header: '',
|
header: '',
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
|
const product = row.original
|
||||||
return h('div', { class: 'flex gap-2' }, [
|
return h('div', { class: 'flex gap-2' }, [
|
||||||
h('button', {
|
h('button', {
|
||||||
class: 'px-3 py-1.5 text-sm font-medium bg-primary text-white rounded-lg hover:bg-blue-600 transition-colors',
|
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)
|
onClick: (e: Event) => { e.stopPropagation(); addToCart(product) }
|
||||||
}, '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:bg-gray-700 text-black dark:text-white rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors',
|
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',
|
||||||
onClick: () => incrementCount(row.original)
|
onClick: (e: Event) => { e.stopPropagation(); incrementCount(product) }
|
||||||
}, '+')
|
}, '+')
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
// Helper for render function
|
|
||||||
import { h } from 'vue'
|
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
function addToCart(product: Product) {
|
function addToCart(product: Product) {
|
||||||
console.log('Add to cart:', product)
|
console.log('Add to cart:', product)
|
||||||
alert(`('added_to_cart'): ${product.name}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function incrementCount(product: Product) {
|
function incrementCount(product: Product) {
|
||||||
console.log('Increment:', product)
|
|
||||||
product.count++
|
product.count++
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearFilters() {
|
function clearFilters() {
|
||||||
searchName.value = ''
|
searchName.value = ''
|
||||||
searchCode.value = ''
|
searchCode.value = ''
|
||||||
priceFrom.value = null
|
priceFromFilter.value = null
|
||||||
priceTo.value = null
|
priceToFilter.value = null
|
||||||
resetPage()
|
resetPage()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="p-6 bg-white dark:bg-(--black) min-h-screen font-sans">
|
<div class="bg-(--main-light) mt-10 dark:bg-(--black) 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">title</h1>
|
||||||
|
|
||||||
<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 bg-yellow-100 text-yellow-700 rounded">
|
||||||
login_to_view
|
login to view
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="authStore.isAuthenticated" class="space-y-4">
|
<div v-if="authStore.isAuthenticated" class="space-y-4">
|
||||||
<!-- Search Filters -->
|
<!-- Filter Block -->
|
||||||
<div class="flex flex-wrap gap-4 mb-6 p-4 border border-(--border-light) dark:border-(--border-dark) rounded">
|
<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-col min-w-[200px]">
|
<div class="flex flex-col">
|
||||||
<label class="mb-1 text-sm font-medium text-black dark:text-white">search by name</label>
|
<label class="mb-1 text-sm font-medium text-black dark:text-white">Product name</label>
|
||||||
<UInput
|
<UInput
|
||||||
v-model="searchName"
|
v-model="searchName"
|
||||||
placeholder="search name placeholder"
|
placeholder="search name placeholder"
|
||||||
@@ -173,8 +182,8 @@ function clearFilters() {
|
|||||||
class="dark:text-white text-black"
|
class="dark:text-white text-black"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col min-w-[200px]">
|
<div class="flex flex-col">
|
||||||
<label class="mb-1 text-sm font-medium text-black dark:text-white">search by code</label>
|
<label class="mb-1 text-sm font-medium text-black dark:text-white">Product code</label>
|
||||||
<UInput
|
<UInput
|
||||||
v-model="searchCode"
|
v-model="searchCode"
|
||||||
placeholder="search code placeholder"
|
placeholder="search code placeholder"
|
||||||
@@ -182,33 +191,33 @@ function clearFilters() {
|
|||||||
class="dark:text-white text-black"
|
class="dark:text-white text-black"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col min-w-[150px]">
|
<div class="flex flex-col">
|
||||||
<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">Price from</label>
|
||||||
<UInput
|
<UInput
|
||||||
v-model="priceFrom"
|
v-model="priceFromFilter"
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="price from"
|
placeholder="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 min-w-[150px]">
|
<div class="flex flex-col">
|
||||||
<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">Price to</label>
|
||||||
<UInput
|
<UInput
|
||||||
v-model="priceTo"
|
v-model="priceToFilter"
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="price_to"
|
placeholder="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">
|
||||||
<button
|
<UButton
|
||||||
@click="clearFilters"
|
@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"
|
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"
|
||||||
>
|
>
|
||||||
clear filters
|
clear filters
|
||||||
</button>
|
</UButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -217,7 +226,8 @@ function clearFilters() {
|
|||||||
<UTable
|
<UTable
|
||||||
:data="paginatedProducts"
|
:data="paginatedProducts"
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
class="dark:text-white! text-dark"
|
class="dark:text-white! text-dark"
|
||||||
|
@select="goToProduct"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -232,7 +242,7 @@ function clearFilters() {
|
|||||||
|
|
||||||
<!-- Results count -->
|
<!-- Results count -->
|
||||||
<div class="text-sm text-gray-600 dark:text-gray-400 text-center">
|
<div class="text-sm text-gray-600 dark:text-gray-400 text-center">
|
||||||
showing {{ paginatedProducts.length }} of {{ totalItems }} products
|
showing {{ paginatedProducts.length }} of {{ totalItems }} paginatedProducts
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user