fix: page serchUsers
This commit is contained in:
@@ -1,25 +1,245 @@
|
||||
<template>
|
||||
<component :is="Default || 'div'">
|
||||
<component :is="Default">
|
||||
<div class="pt-70! flex flex-col items-center justify-center bg-gray-50 dark:bg-(--main-dark)">
|
||||
<h1 class="text-6xl font-bold text-black dark:text-white mb-14">Search User</h1>
|
||||
<h1 class="text-6xl font-bold text-black dark:text-white mb-14">Search Users</h1>
|
||||
|
||||
<div class="w-full max-w-4xl">
|
||||
<input type="text" placeholder="Type user name or ID..."
|
||||
class="w-full border border-gray-300 rounded-full px-6 py-3 text-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" />
|
||||
<UInput icon="i-lucide-search" type="text" placeholder="Type user name or ID..." v-model="searchQuery"
|
||||
class="w-full!" :ui="{ base: 'py-4! rounded-full!' }" />
|
||||
</div>
|
||||
|
||||
<p v-if="loading">Loading...</p>
|
||||
<p v-else-if="error" class="text-red-600">{{ error }}</p>
|
||||
<div v-else-if="clients.length" class="w-full max-w-4xl mt-7">
|
||||
<UTable :columns="columns" :data="clients" :ui="{ root: 'w-full!' }" />
|
||||
</div>
|
||||
|
||||
<p v-else-if="searchQuery.length" class="pt-4 text-gray-700">
|
||||
No users found with that name or ID
|
||||
</p>
|
||||
</div>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, resolveComponent, h } from 'vue'
|
||||
import Default from '@/layouts/default.vue';
|
||||
import type { TableColumn } from '@nuxt/ui';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useFetchJson } from '@/composable/useFetchJson';
|
||||
|
||||
|
||||
interface Customer {
|
||||
user_id: number;
|
||||
email: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
}
|
||||
|
||||
|
||||
const searchQuery = ref('');
|
||||
const clients = ref<Customer[]>([]);
|
||||
const loading = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
const limit = ref(10);
|
||||
const page = ref(1);
|
||||
|
||||
|
||||
function debounce(fn: Function, delay = 400) {
|
||||
let t: any;
|
||||
return (...args: any[]) => {
|
||||
clearTimeout(t);
|
||||
t = setTimeout(() => fn(...args), delay);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
const searchClients = async () => {
|
||||
if (!searchQuery.value) {
|
||||
clients.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const result = await useFetchJson(
|
||||
`/api/v1/restricted/customer/list?search=${encodeURIComponent(searchQuery.value)}&elems=${limit.value}&page=${page.value}`
|
||||
);
|
||||
|
||||
clients.value = result.items?.items || [];
|
||||
} catch (err: unknown) {
|
||||
error.value = err instanceof Error ? err.message : 'Failed to fetch clients';
|
||||
clients.value = [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
watch(searchQuery, debounce(searchClients, 300));
|
||||
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
|
||||
const UInput = resolveComponent('UInput')
|
||||
const UIcon = resolveComponent('UIcon')
|
||||
const UButton = resolveComponent('UButton')
|
||||
|
||||
|
||||
const sortField = computed({
|
||||
get: () => [
|
||||
route.query.sort as string | undefined,
|
||||
route.query.direction as 'asc' | 'desc' | undefined
|
||||
],
|
||||
set: ([sort]: [string, 'asc' | 'desc']) => {
|
||||
const currentSort = route.query.sort as string | undefined
|
||||
const currentDirection = route.query.direction as 'asc' | 'desc' | undefined
|
||||
const query = { ...route.query }
|
||||
|
||||
if (currentSort === sort) {
|
||||
if (currentDirection === 'asc') query.direction = 'desc'
|
||||
else if (currentDirection === 'desc') {
|
||||
delete query.sort
|
||||
delete query.direction
|
||||
} else {
|
||||
query.direction = 'asc'
|
||||
query.sort = sort
|
||||
}
|
||||
} else {
|
||||
query.sort = sort
|
||||
query.direction = 'asc'
|
||||
}
|
||||
router.push({ query })
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
function getIcon(name: string) {
|
||||
if (sortField.value[0] === name) {
|
||||
if (sortField.value[1] === 'asc') return 'i-lucide-arrow-up-narrow-wide'
|
||||
if (sortField.value[1] === 'desc') return 'i-lucide-arrow-down-wide-narrow'
|
||||
}
|
||||
return 'i-lucide-arrow-up-down'
|
||||
}
|
||||
|
||||
|
||||
const filters = computed<Record<string, string>>({
|
||||
get: () => {
|
||||
const q = { ...route.query }
|
||||
delete q.page
|
||||
delete q.sort
|
||||
delete q.direction
|
||||
return q as Record<string, string>
|
||||
},
|
||||
set: (val) => {
|
||||
const baseQuery = { ...route.query }
|
||||
Object.keys(baseQuery).forEach(k => {
|
||||
if (!['page', 'sort', 'direction'].includes(k)) delete baseQuery[k]
|
||||
})
|
||||
router.push({ query: { ...baseQuery, ...val, page: 1 } })
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const updateFilter = debounce((columnId: string, val: string) => {
|
||||
const newFilters = { ...filters.value }
|
||||
if (val) newFilters[columnId] = val
|
||||
else delete newFilters[columnId]
|
||||
filters.value = newFilters
|
||||
}, 400)
|
||||
|
||||
|
||||
const columns: TableColumn<Customer>[] = [
|
||||
{
|
||||
accessorKey: 'user_id',
|
||||
header: ({ column }) => h('div', { class: 'flex flex-col gap-1' }, [
|
||||
h('div', {
|
||||
class: 'flex items-center gap-2 cursor-pointer',
|
||||
onClick: () => (sortField.value = ['user_id', 'asc'])
|
||||
}, [h('span', 'Client ID'), h(UIcon, { name: getIcon('user_id') })]),
|
||||
h(UInput, {
|
||||
placeholder: 'Search...',
|
||||
modelValue: filters.value[column.id] ?? '',
|
||||
'onUpdate:modelValue': (val: string) => updateFilter(column.id, val),
|
||||
size: 'xs'
|
||||
})
|
||||
]),
|
||||
cell: ({ row }) => `#${row.getValue('user_id')}`
|
||||
},
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: ({ column }) => h('div', { class: 'flex flex-col gap-1' }, [
|
||||
h('div', {
|
||||
class: 'flex items-center gap-2 cursor-pointer',
|
||||
onClick: () => (sortField.value = ['last_name, first_name', 'asc'])
|
||||
}, [h('span', 'Name/Surname'), h(UIcon, { name: getIcon('name') })]),
|
||||
h(UInput, {
|
||||
placeholder: 'Search...',
|
||||
modelValue: filters.value[column.id] ?? '',
|
||||
'onUpdate:modelValue': (val: string) => updateFilter(column.id, val),
|
||||
size: 'xs'
|
||||
})
|
||||
]),
|
||||
cell: ({ row }) => `${row.original.first_name} ${row.original.last_name}`
|
||||
},
|
||||
{
|
||||
accessorKey: 'email',
|
||||
header: ({ column }) => h('div', { class: 'flex flex-col gap-1' }, [
|
||||
h('div', {
|
||||
class: 'flex items-center gap-2 cursor-pointer',
|
||||
onClick: () => (sortField.value = ['email', 'asc'])
|
||||
}, [h('span', 'Email'), h(UIcon, { name: getIcon('email') })]),
|
||||
h(UInput, {
|
||||
placeholder: 'Search...',
|
||||
modelValue: filters.value[column.id] ?? '',
|
||||
'onUpdate:modelValue': (val: string) => updateFilter(column.id, val),
|
||||
size: 'xs'
|
||||
})
|
||||
]),
|
||||
cell: ({ row }) => row.getValue('email')
|
||||
},
|
||||
{
|
||||
accessorKey: 'count',
|
||||
header: '',
|
||||
cell: ({ row }) => {
|
||||
return h(UButton, {
|
||||
// onClick: () => {
|
||||
// goToProduct(row.original.product_id, row.original.link_rewrite)
|
||||
// },
|
||||
class: 'cursor-pointer',
|
||||
color: 'info',
|
||||
variant: 'soft'
|
||||
}, () => 'Manage')
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'profile',
|
||||
header: '',
|
||||
cell: ({ row }) => {
|
||||
const userId = row.original.user_id
|
||||
return h(UButton, {
|
||||
color: 'primary',
|
||||
size: 'sm',
|
||||
variant: 'soft',
|
||||
onClick: () => {
|
||||
router.push({
|
||||
name: 'customer-management-profile',
|
||||
params: { user_id: userId }
|
||||
})
|
||||
}
|
||||
}, () => 'Go to profile')
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
input::placeholder {
|
||||
color: #9ca3af;
|
||||
/* Tailwind gray-400 */
|
||||
}
|
||||
</style>
|
||||
@@ -16,7 +16,6 @@
|
||||
</template>
|
||||
|
||||
<template #default="{ state }">
|
||||
{{ menu }}
|
||||
<UNavigationMenu :key="state" :items="menuItems" orientation="vertical"
|
||||
:ui="{ link: 'p-1.5 overflow-hidden' }" />
|
||||
</template>
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
</template>
|
||||
|
||||
<template #default="{ state }">
|
||||
{{ menu }}
|
||||
<UNavigationMenu :key="state" :items="menuItems" orientation="vertical"
|
||||
:ui="{ link: 'p-1.5 overflow-hidden' }" />
|
||||
</template>
|
||||
@@ -39,7 +38,7 @@
|
||||
<div class="flex items-center gap-2">
|
||||
<UButton icon="i-lucide-panel-left" color="neutral" variant="ghost" aria-label="Toggle sidebar"
|
||||
@click="open = !open" />
|
||||
<p class="font-bold text-2xl">Customer-Management: <span class="text-[24px] font-bold">{{ pageTitle }}</span></p>
|
||||
<p class="font-bold text-xl">Customer-Management: <span class="text-[20px] font-medium">{{ pageTitle }}</span></p>
|
||||
</div>
|
||||
<div class="hidden md:flex items-center gap-12">
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -71,7 +70,6 @@ import { defineShortcuts, extractShortcuts } from '@nuxt/ui/runtime/composables/
|
||||
import { useAuthStore } from '../stores/customer/auth'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
|
||||
const route = useRoute()
|
||||
const pageTitle = computed(() => route.meta.name ?? 'Default Page')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user