Redesign header - move search bar into main navigation
All checks were successful
Build Docker Image / build (push) Successful in 3m10s

- Move search input with tokens into center of header
- Remove tabs (Search, Catalog, Orders, Seller)
- Icons (bot, globe, user) remain on right side
- Chips for filter selection below input
- Delete GlobalSearchBar.vue and UnifiedSearchBar.vue
- Share searchQuery via useState across composable calls
- Simplify main page to just show hero
This commit is contained in:
Ruslan Bakiev
2026-01-22 11:22:44 +07:00
parent 13325825d7
commit 584a423e86
7 changed files with 160 additions and 407 deletions

View File

@@ -1,37 +1,80 @@
<template>
<header class="bg-base-100 shadow-md">
<div class="relative flex items-center h-16 px-4 lg:px-6">
<!-- Top row: Logo + Icons -->
<div class="flex items-center h-14 px-4 lg:px-6">
<!-- Left: Logo -->
<div class="flex items-center">
<div class="flex items-center flex-shrink-0">
<NuxtLink :to="localePath('/')" class="flex items-center gap-2">
<span class="font-bold text-xl">Optovia</span>
</NuxtLink>
</div>
<!-- Center: Main tabs (absolutely centered on page) -->
<nav class="hidden md:flex items-center gap-1 absolute left-1/2 -translate-x-1/2">
<NuxtLink
v-for="tab in visibleTabs"
:key="tab.key"
:to="localePath(tab.path)"
class="px-4 py-2 rounded-full font-medium text-sm transition-colors hover:bg-base-200"
:class="{ 'bg-base-200 text-primary': isActiveTab(tab.key) }"
<!-- Center: Search input with tokens -->
<div class="flex-1 flex justify-center px-4 max-w-2xl mx-auto">
<div
class="flex items-center gap-2 w-full px-3 py-1.5 border border-base-300 rounded-lg bg-base-100 focus-within:border-primary focus-within:ring-1 focus-within:ring-primary cursor-text"
@click="focusInput"
>
{{ tab.label }}
</NuxtLink>
</nav>
<Icon name="lucide:search" size="16" class="text-base-content/50 flex-shrink-0" />
<!-- Tokens + input inline -->
<div class="flex items-center gap-1.5 flex-wrap flex-1 min-w-0">
<!-- Active filter tokens -->
<div
v-for="token in activeTokens"
:key="token.type"
class="badge badge-sm gap-1 cursor-pointer hover:badge-primary transition-colors flex-shrink-0"
@click.stop="$emit('edit-token', token.type)"
>
<Icon :name="token.icon" size="12" />
<span class="max-w-20 truncate">{{ token.label }}</span>
<button
class="hover:text-error"
@click.stop="$emit('remove-token', token.type)"
>
<Icon name="lucide:x" size="10" />
</button>
</div>
<!-- Active selection mode indicator -->
<div
v-if="selectMode"
class="badge badge-sm badge-outline badge-primary gap-1 flex-shrink-0"
>
<Icon :name="selectModeIcon" size="12" />
{{ selectModeLabel }}:
<button
class="hover:text-error"
@click.stop="$emit('cancel-select')"
>
<Icon name="lucide:x" size="10" />
</button>
</div>
<!-- Search input -->
<input
ref="inputRef"
v-model="localSearchQuery"
type="text"
:placeholder="placeholder"
class="flex-1 min-w-24 bg-transparent outline-none text-sm"
@input="$emit('update:search-query', localSearchQuery)"
/>
</div>
</div>
</div>
<!-- Right: AI + Globe + Team + User -->
<div class="flex items-center gap-2 ml-auto">
<div class="flex items-center gap-1 flex-shrink-0">
<!-- AI Assistant button -->
<NuxtLink :to="localePath('/clientarea/ai')" class="btn btn-ghost btn-circle">
<Icon name="lucide:bot" size="20" />
<NuxtLink :to="localePath('/clientarea/ai')" class="btn btn-ghost btn-circle btn-sm">
<Icon name="lucide:bot" size="18" />
</NuxtLink>
<!-- Globe (language/currency) dropdown -->
<div class="dropdown dropdown-end">
<button tabindex="0" class="btn btn-ghost btn-circle">
<Icon name="lucide:globe" size="20" />
<button tabindex="0" class="btn btn-ghost btn-circle btn-sm">
<Icon name="lucide:globe" size="18" />
</button>
<div tabindex="0" class="dropdown-content bg-base-100 rounded-box z-50 w-52 p-4 shadow-lg border border-base-300">
<div class="font-semibold mb-2">{{ $t('common.language') }}</div>
@@ -60,12 +103,12 @@
<!-- Team dropdown -->
<template v-if="loggedIn && userData?.teams?.length">
<div class="dropdown dropdown-end">
<button tabindex="0" class="btn btn-ghost gap-2">
<Icon name="lucide:building-2" size="18" />
<span class="hidden sm:inline max-w-32 truncate">
<button tabindex="0" class="btn btn-ghost btn-sm gap-1">
<Icon name="lucide:building-2" size="16" />
<span class="hidden lg:inline max-w-24 truncate text-xs">
{{ userData?.activeTeam?.name || $t('common.selectTeam') }}
</span>
<Icon name="lucide:chevron-down" size="14" />
<Icon name="lucide:chevron-down" size="12" />
</button>
<ul tabindex="0" class="dropdown-content menu bg-base-100 rounded-box z-50 w-56 p-2 shadow-lg border border-base-300">
<li class="menu-title"><span>{{ $t('common.teams') }}</span></li>
@@ -92,12 +135,12 @@
<template v-if="sessionChecked">
<template v-if="loggedIn">
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-ghost btn-circle avatar">
<div class="w-10 rounded-full">
<div tabindex="0" role="button" class="btn btn-ghost btn-circle btn-sm avatar">
<div class="w-8 rounded-full">
<div v-if="userAvatarSvg" v-html="userAvatarSvg" class="w-full h-full" />
<div
v-else
class="w-full h-full bg-primary flex items-center justify-center text-primary-content font-bold text-sm"
class="w-full h-full bg-primary flex items-center justify-center text-primary-content font-bold text-xs"
>
{{ userInitials }}
</div>
@@ -135,22 +178,27 @@
</div>
</div>
<!-- Mobile tabs (shown below header on small screens) -->
<nav class="md:hidden flex items-center justify-center gap-1 py-2 border-t border-base-300 overflow-x-auto">
<NuxtLink
v-for="tab in visibleTabs"
:key="tab.key"
:to="localePath(tab.path)"
class="px-4 py-2 rounded-full text-sm font-medium hover:bg-base-200"
:class="{ 'bg-base-200 text-primary': isActiveTab(tab.key) }"
<!-- Bottom row: Quick filter chips -->
<div
v-if="availableChips.length > 0"
class="flex items-center justify-center gap-2 px-4 py-1.5 border-t border-base-200"
>
<button
v-for="chip in availableChips"
:key="chip.type"
class="btn btn-xs btn-ghost gap-1"
@click="$emit('start-select', chip.type)"
>
{{ tab.label }}
</NuxtLink>
</nav>
<Icon name="lucide:plus" size="12" />
{{ chip.label }}
</button>
</div>
</header>
</template>
<script setup lang="ts">
import type { SelectMode } from '~/composables/useCatalogSearch'
const props = defineProps<{
sessionChecked?: boolean
loggedIn?: boolean
@@ -165,38 +213,62 @@ const props = defineProps<{
teams?: Array<{ id?: string; name?: string; logtoOrgId?: string }>
} | null
isSeller?: boolean
// Search props
activeTokens?: Array<{ type: string; id: string; label: string; icon: string }>
availableChips?: Array<{ type: string; label: string }>
selectMode?: SelectMode
searchQuery?: string
}>()
defineEmits(['toggle-theme', 'sign-out', 'sign-in', 'switch-team'])
defineEmits([
'toggle-theme',
'sign-out',
'sign-in',
'switch-team',
// Search events
'start-select',
'cancel-select',
'edit-token',
'remove-token',
'update:search-query'
])
const localePath = useLocalePath()
const { locale, locales } = useI18n()
const switchLocalePath = useSwitchLocalePath()
const route = useRoute()
const { t } = useI18n()
const tabs = computed(() => [
{ key: 'search', label: t('cabinetNav.search'), path: '/', auth: false },
{ key: 'catalog', label: t('cabinetNav.catalog'), path: '/catalog', auth: false },
{ key: 'orders', label: t('cabinetNav.orders'), path: '/clientarea/orders', auth: true },
{ key: 'seller', label: t('cabinetNav.seller'), path: '/clientarea/offers', auth: true, seller: true },
])
const inputRef = ref<HTMLInputElement>()
const localSearchQuery = ref(props.searchQuery || '')
const visibleTabs = computed(() => {
return tabs.value.filter(tab => {
if (tab.auth && !props.loggedIn) return false
if (tab.seller && !props.isSeller) return false
return true
})
watch(() => props.searchQuery, (val) => {
localSearchQuery.value = val || ''
})
const isActiveTab = (key: string) => {
const path = route.path
if (key === 'search') return path === '/' || path === '/en' || path === '/ru'
if (key === 'catalog') return path.startsWith('/catalog') || path.includes('/en/catalog') || path.includes('/ru/catalog')
if (key === 'orders') return path.includes('/clientarea/orders') || path.includes('/clientarea/addresses') || path.includes('/clientarea/billing')
if (key === 'seller') return path.includes('/clientarea/offers')
return false
const focusInput = () => {
inputRef.value?.focus()
}
const placeholder = computed(() => {
if (props.selectMode === 'product') return t('catalog.search.searchProducts')
if (props.selectMode === 'supplier') return t('catalog.search.searchSuppliers')
if (props.selectMode === 'hub') return t('catalog.search.searchHubs')
if (!props.activeTokens?.length) return t('catalog.search.placeholder')
return t('catalog.search.refine')
})
const selectModeLabel = computed(() => {
if (props.selectMode === 'product') return t('catalog.filters.product')
if (props.selectMode === 'supplier') return t('catalog.filters.supplier')
if (props.selectMode === 'hub') return t('catalog.filters.hub')
return ''
})
const selectModeIcon = computed(() => {
if (props.selectMode === 'product') return 'lucide:package'
if (props.selectMode === 'supplier') return 'lucide:factory'
if (props.selectMode === 'hub') return 'lucide:map-pin'
return 'lucide:search'
})
</script>