All checks were successful
Build Docker Image / build (push) Successful in 3m11s
- Restore full landing with How it works + Who it's for sections - Make search input bigger and rounder (rounded-full, shadow) - Remove border between input and chips - Bigger badges and icons
275 lines
9.8 KiB
Vue
275 lines
9.8 KiB
Vue
<template>
|
|
<header class="bg-base-100 shadow-md">
|
|
<!-- Top row: Logo + Icons -->
|
|
<div class="flex items-center h-14 px-4 lg:px-6">
|
|
<!-- Left: Logo -->
|
|
<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: Search input with tokens -->
|
|
<div class="flex-1 flex justify-center px-4 max-w-2xl mx-auto">
|
|
<div
|
|
class="flex items-center gap-3 w-full px-5 py-2.5 border border-base-300 rounded-full bg-base-100 shadow-sm hover:shadow-md focus-within:border-primary focus-within:ring-2 focus-within:ring-primary/20 transition-all cursor-text"
|
|
@click="focusInput"
|
|
>
|
|
<Icon name="lucide:search" size="20" class="text-base-content/50 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-md gap-1.5 cursor-pointer hover:badge-primary transition-colors flex-shrink-0"
|
|
@click.stop="$emit('edit-token', token.type)"
|
|
>
|
|
<Icon :name="token.icon" size="14" />
|
|
<span class="max-w-24 truncate">{{ token.label }}</span>
|
|
<button
|
|
class="hover:text-error"
|
|
@click.stop="$emit('remove-token', token.type)"
|
|
>
|
|
<Icon name="lucide:x" size="12" />
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Active selection mode indicator -->
|
|
<div
|
|
v-if="selectMode"
|
|
class="badge badge-md badge-outline badge-primary gap-1.5 flex-shrink-0"
|
|
>
|
|
<Icon :name="selectModeIcon" size="14" />
|
|
{{ selectModeLabel }}:
|
|
<button
|
|
class="hover:text-error"
|
|
@click.stop="$emit('cancel-select')"
|
|
>
|
|
<Icon name="lucide:x" size="12" />
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Search input -->
|
|
<input
|
|
ref="inputRef"
|
|
v-model="localSearchQuery"
|
|
type="text"
|
|
:placeholder="placeholder"
|
|
class="flex-1 min-w-32 bg-transparent outline-none"
|
|
@input="$emit('update:search-query', localSearchQuery)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right: AI + Globe + Team + User -->
|
|
<div class="flex items-center gap-1 flex-shrink-0">
|
|
<!-- 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>
|
|
|
|
<!-- Bottom row: Quick filter chips -->
|
|
<div
|
|
v-if="availableChips.length > 0"
|
|
class="flex items-center justify-center gap-3 px-4 py-2"
|
|
>
|
|
<button
|
|
v-for="chip in availableChips"
|
|
:key="chip.type"
|
|
class="btn btn-sm btn-ghost gap-1.5"
|
|
@click="$emit('start-select', chip.type)"
|
|
>
|
|
<Icon name="lucide:plus" size="14" />
|
|
{{ chip.label }}
|
|
</button>
|
|
</div>
|
|
</header>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type { SelectMode } 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'
|
|
})
|
|
</script>
|
|
|