Files
webapp/app/components/navigation/MainNavigation.vue
Ruslan Bakiev a8612c20b5
Some checks failed
Build Docker Image / build (push) Has been cancelled
Fix header: remove selectMode indicator, align logo and icons
- Remove 'Товар:' badge during selection - tags appear only after selection
- Align logo and icons vertically with input (h-12)
- Simplify search input styling
2026-01-22 11:49:21 +07:00

268 lines
9.7 KiB
Vue

<template>
<header class="bg-base-100 shadow-md">
<!-- Single row: Logo + Search + Icons -->
<div class="flex items-start px-4 lg:px-6 py-3 gap-4">
<!-- Left: Logo (centered with input) -->
<div class="flex items-center flex-shrink-0 h-12">
<NuxtLink :to="localePath('/')" class="flex items-center gap-2">
<span class="font-bold text-xl">Optovia</span>
</NuxtLink>
</div>
<!-- Center: Search input + chips -->
<div class="flex-1 flex flex-col items-center px-4 max-w-2xl mx-auto gap-2">
<!-- Big rounded input -->
<div
class="flex items-center gap-3 w-full px-5 py-3 border border-base-300 rounded-full bg-base-100 shadow-md hover:shadow-lg focus-within:border-primary focus-within:ring-2 focus-within:ring-primary/20 transition-all cursor-text"
@click="focusInput"
>
<Icon name="lucide:search" size="22" class="text-primary flex-shrink-0" />
<!-- Tokens + input inline -->
<div class="flex items-center gap-2 flex-wrap flex-1 min-w-0">
<!-- Active filter tokens -->
<div
v-for="token in activeTokens"
:key="token.type"
class="badge badge-lg gap-1.5 cursor-pointer hover:opacity-80 transition-all flex-shrink-0 text-white"
:style="{ backgroundColor: getTokenColor(token.type) }"
@click.stop="$emit('edit-token', token.type)"
>
<Icon :name="token.icon" size="14" />
<span class="max-w-28 truncate">{{ token.label }}</span>
<button
class="hover:text-error"
@click.stop="$emit('remove-token', token.type)"
>
<Icon name="lucide:x" size="14" />
</button>
</div>
<!-- Search input -->
<input
ref="inputRef"
v-model="localSearchQuery"
type="text"
:placeholder="placeholder"
class="flex-1 min-w-32 bg-transparent outline-none text-lg"
@input="$emit('update:search-query', localSearchQuery)"
/>
</div>
</div>
<!-- Chips below input -->
<div
v-if="availableChips.length > 0"
class="flex items-center justify-center gap-2"
>
<button
v-for="chip in availableChips"
:key="chip.type"
class="btn btn-xs btn-ghost gap-1"
@click="$emit('start-select', chip.type)"
>
<Icon name="lucide:plus" size="12" />
{{ chip.label }}
</button>
</div>
</div>
<!-- Right: AI + Globe + Team + User (centered with input) -->
<div class="flex items-center gap-1 flex-shrink-0 h-12">
<!-- AI Assistant button -->
<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 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>
<div class="flex gap-2 mb-4">
<NuxtLink
v-for="loc in locales"
:key="loc.code"
:to="switchLocalePath(loc.code)"
class="btn btn-sm"
:class="locale === loc.code ? 'btn-primary' : 'btn-ghost'"
>
{{ loc.code.toUpperCase() }}
</NuxtLink>
</div>
<div class="font-semibold mb-2">{{ $t('common.theme') }}</div>
<button
class="btn btn-sm btn-ghost w-full justify-start"
@click="$emit('toggle-theme')"
>
<Icon :name="theme === 'night' ? 'lucide:sun' : 'lucide:moon'" size="16" />
{{ theme === 'night' ? $t('common.theme_light') : $t('common.theme_dark') }}
</button>
</div>
</div>
<!-- Team dropdown -->
<template v-if="loggedIn && userData?.teams?.length">
<div class="dropdown dropdown-end">
<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="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>
<li v-for="team in userData?.teams" :key="team.id">
<a
@click="$emit('switch-team', team)"
:class="{ active: team.id === userData?.activeTeamId }"
>
{{ team.name }}
</a>
</li>
<div class="divider my-1"></div>
<li>
<NuxtLink :to="localePath('/clientarea/team')">
<Icon name="lucide:settings" size="16" />
{{ $t('cabinetNav.teamSettings') }}
</NuxtLink>
</li>
</ul>
</div>
</template>
<!-- User menu -->
<template v-if="sessionChecked">
<template v-if="loggedIn">
<div class="dropdown dropdown-end">
<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-xs"
>
{{ userInitials }}
</div>
</div>
</div>
<ul
tabindex="0"
class="dropdown-content menu bg-base-100 rounded-box z-50 w-52 p-2 shadow-lg border border-base-300"
>
<li class="menu-title">
<span>{{ userName }}</span>
</li>
<li>
<NuxtLink :to="localePath('/clientarea/profile')">
<Icon name="lucide:user" size="16" />
{{ $t('dashboard.profile') }}
</NuxtLink>
</li>
<div class="divider my-1"></div>
<li>
<a @click="$emit('sign-out')" class="text-error">
<Icon name="lucide:log-out" size="16" />
{{ $t('auth.logout') }}
</a>
</li>
</ul>
</div>
</template>
<template v-else>
<button @click="$emit('sign-in')" class="btn btn-primary btn-sm">
{{ $t('auth.login') }}
</button>
</template>
</template>
</div>
</div>
</header>
</template>
<script setup lang="ts">
import type { SelectMode } from '~/composables/useCatalogSearch'
import { entityColors } from '~/composables/useCatalogSearch'
const props = defineProps<{
sessionChecked?: boolean
loggedIn?: boolean
userAvatarSvg?: string
userName?: string
userInitials?: string
theme?: 'cupcake' | 'night'
userData?: {
id?: string
activeTeam?: { name?: string; teamType?: string }
activeTeamId?: string
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',
// Search events
'start-select',
'cancel-select',
'edit-token',
'remove-token',
'update:search-query'
])
const localePath = useLocalePath()
const { locale, locales } = useI18n()
const switchLocalePath = useSwitchLocalePath()
const { t } = useI18n()
const inputRef = ref<HTMLInputElement>()
const localSearchQuery = ref(props.searchQuery || '')
watch(() => props.searchQuery, (val) => {
localSearchQuery.value = val || ''
})
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'
})
const getTokenColor = (type: string) => {
return entityColors[type as keyof typeof entityColors] || entityColors.product
}
</script>