Transform search bar in Quote mode to Airbnb-style segmented input
All checks were successful
Build Docker Image / build (push) Successful in 3m21s

- Remove mode toggle [Explore/Quote] tabs from header
- In Quote mode: show segmented input (Product | Hub | Quantity) + Search button
- In Explore mode: keep regular pill input with chips
- Add productLabel, hubLabel, supplierLabel computed values to useCatalogSearch
- Pass Quote mode props to MainNavigation
This commit is contained in:
Ruslan Bakiev
2026-01-22 20:52:06 +07:00
parent 7465b1d6a2
commit c0f38a25cd
3 changed files with 124 additions and 74 deletions

View File

@@ -9,84 +9,105 @@
</NuxtLink> </NuxtLink>
</div> </div>
<!-- Center: Search input + chips --> <!-- Center: Search input (transforms based on mode) -->
<div class="flex-1 flex flex-col items-center max-w-2xl mx-auto gap-2"> <div class="flex-1 flex flex-col items-center max-w-2xl mx-auto gap-2">
<!-- Big pill input --> <!-- Quote mode: Segmented input like Airbnb -->
<div <template v-if="catalogMode === 'quote'">
class="flex items-center gap-3 w-full px-5 py-3 rounded-full border border-base-300 bg-base-100 focus-within:border-primary focus-within:ring-2 focus-within:ring-primary/20 transition-all cursor-text" <div class="flex items-center gap-3 w-full">
@click="focusInput" <div class="flex items-center flex-1 rounded-full border border-base-300 bg-base-100 shadow-sm divide-x divide-base-300">
> <!-- Product segment -->
<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 <button
class="hover:text-error" class="flex-1 px-4 py-2.5 text-left hover:bg-base-200/50 rounded-l-full transition-colors min-w-0"
@click.stop="$emit('remove-token', token.type)" @click="$emit('edit-token', 'product')"
> >
<Icon name="lucide:x" size="14" /> <div class="text-xs text-base-content/60">{{ $t('catalog.quote.product') }}</div>
<div class="font-medium truncate">{{ productLabel || $t('catalog.quote.selectProduct') }}</div>
</button>
<!-- Hub segment -->
<button
class="flex-1 px-4 py-2.5 text-left hover:bg-base-200/50 transition-colors min-w-0"
@click="$emit('edit-token', 'hub')"
>
<div class="text-xs text-base-content/60">{{ $t('catalog.quote.hub') }}</div>
<div class="font-medium truncate">{{ hubLabel || $t('catalog.quote.selectHub') }}</div>
</button>
<!-- Quantity segment -->
<button
class="flex-1 px-4 py-2.5 text-left hover:bg-base-200/50 rounded-r-full transition-colors min-w-0"
@click="$emit('edit-token', 'quantity')"
>
<div class="text-xs text-base-content/60">{{ $t('catalog.quote.quantity') }}</div>
<div class="font-medium">{{ quantity ? `${quantity} ${$t('units.t')}` : '—' }}</div>
</button> </button>
</div> </div>
<!-- Search button -->
<!-- Search input --> <button
<input class="btn btn-primary btn-circle shadow-lg"
ref="inputRef" :disabled="!canSearch"
v-model="localSearchQuery" @click="$emit('search')"
type="text" >
:placeholder="placeholder" <Icon name="lucide:search" size="20" />
class="flex-1 min-w-32 bg-transparent outline-none text-lg" </button>
@input="$emit('update:search-query', localSearchQuery)"
/>
</div> </div>
</div> </template>
<!-- Chips below --> <!-- Explore mode: Regular pill input + chips -->
<div <template v-else>
v-if="availableChips.length > 0" <!-- Big pill input -->
class="flex items-center justify-center gap-2" <div
> class="flex items-center gap-3 w-full px-5 py-3 rounded-full border border-base-300 bg-base-100 focus-within:border-primary focus-within:ring-2 focus-within:ring-primary/20 transition-all cursor-text"
<button @click="focusInput"
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" /> <Icon name="lucide:search" size="22" class="text-primary flex-shrink-0" />
{{ chip.label }}
</button>
</div>
</div>
<!-- Catalog mode toggle (only on catalog pages) --> <!-- Tokens + input inline -->
<div v-if="showCatalogModeToggle" class="flex-shrink-0 py-2.5"> <div class="flex items-center gap-2 flex-wrap flex-1 min-w-0">
<div class="tabs tabs-boxed tabs-sm bg-base-200"> <!-- Active filter tokens -->
<button <div
class="tab" v-for="token in activeTokens"
:class="{ 'tab-active': catalogMode === 'explore' }" :key="token.type"
@click="$emit('set-catalog-mode', 'explore')" 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 -->
<div
v-if="availableChips.length > 0"
class="flex items-center justify-center gap-2"
> >
<Icon name="lucide:compass" size="14" class="mr-1" /> <button
{{ $t('catalog.modes.explore') }} v-for="chip in availableChips"
</button> :key="chip.type"
<button class="btn btn-xs btn-ghost gap-1"
class="tab" @click="$emit('start-select', chip.type)"
:class="{ 'tab-active': catalogMode === 'quote' }" >
@click="$emit('set-catalog-mode', 'quote')" <Icon name="lucide:plus" size="12" />
> {{ chip.label }}
<Icon name="lucide:search" size="14" class="mr-1" /> </button>
{{ $t('catalog.modes.quote') }} </div>
</button> </template>
</div>
</div> </div>
<!-- Right: AI + Globe + Team + User --> <!-- Right: AI + Globe + Team + User -->
@@ -232,8 +253,12 @@ const props = defineProps<{
selectMode?: SelectMode selectMode?: SelectMode
searchQuery?: string searchQuery?: string
// Catalog mode props // Catalog mode props
showCatalogModeToggle?: boolean
catalogMode?: CatalogMode catalogMode?: CatalogMode
// Quote mode props
productLabel?: string
hubLabel?: string
quantity?: string
canSearch?: boolean
}>() }>()
defineEmits([ defineEmits([
@@ -247,8 +272,8 @@ defineEmits([
'edit-token', 'edit-token',
'remove-token', 'remove-token',
'update:search-query', 'update:search-query',
// Catalog mode // Quote mode
'set-catalog-mode' 'search'
]) ])
const localePath = useLocalePath() const localePath = useLocalePath()

View File

@@ -243,6 +243,11 @@ export function useCatalogSearch() {
return !!(productId.value && (hubId.value || supplierId.value)) return !!(productId.value && (hubId.value || supplierId.value))
}) })
// Labels for Quote mode display
const productLabel = computed(() => getLabel('product', productId.value))
const hubLabel = computed(() => getLabel('hub', hubId.value))
const supplierLabel = computed(() => getLabel('supplier', supplierId.value))
return { return {
// State // State
selectMode, selectMode,
@@ -262,6 +267,9 @@ export function useCatalogSearch() {
activeTokens, activeTokens,
availableChips, availableChips,
canSearch, canSearch,
productLabel,
hubLabel,
supplierLabel,
// Actions // Actions
startSelect, startSelect,

View File

@@ -16,8 +16,11 @@
:available-chips="availableChips" :available-chips="availableChips"
:select-mode="selectMode" :select-mode="selectMode"
:search-query="searchQuery" :search-query="searchQuery"
:show-catalog-mode-toggle="isCatalogSection"
:catalog-mode="catalogMode" :catalog-mode="catalogMode"
:product-label="productLabel"
:hub-label="hubLabel"
:quantity="quantity"
:can-search="canSearch"
@toggle-theme="toggleTheme" @toggle-theme="toggleTheme"
@sign-out="onClickSignOut" @sign-out="onClickSignOut"
@sign-in="signIn()" @sign-in="signIn()"
@@ -27,7 +30,7 @@
@edit-token="editFilter" @edit-token="editFilter"
@remove-token="removeFilter" @remove-token="removeFilter"
@update:search-query="searchQuery = $event" @update:search-query="searchQuery = $event"
@set-catalog-mode="setCatalogMode" @search="onSearch"
/> />
<!-- Sub Navigation (section-specific tabs) - only for non-catalog sections --> <!-- Sub Navigation (section-specific tabs) - only for non-catalog sections -->
@@ -61,7 +64,10 @@ const {
removeFilter, removeFilter,
editFilter, editFilter,
catalogMode, catalogMode,
setCatalogMode productLabel,
hubLabel,
quantity,
canSearch
} = useCatalogSearch() } = useCatalogSearch()
// Collapsible header for catalog pages // Collapsible header for catalog pages
@@ -234,4 +240,15 @@ watch(theme, (value) => applyTheme(value))
const toggleTheme = () => { const toggleTheme = () => {
theme.value = theme.value === 'night' ? 'cupcake' : 'night' theme.value = theme.value === 'night' ? 'cupcake' : 'night'
} }
// Search handler for Quote mode - emits event that page can handle
const localePath = useLocalePath()
const router = useRouter()
const onSearch = () => {
// Navigate to catalog page which will handle the search
if (!route.path.includes('/catalog')) {
router.push({ path: localePath('/catalog'), query: { ...route.query, mode: 'quote' } })
}
// The page component will react to canSearch becoming true and perform the search
}
</script> </script>