Hello from MCP server
<template>
<div class="search-section">
<h2 v-if="title" class="search-title">{{ title }}</h2>
<div class="search-bar">
<div class="search-input-container">
<ion-input
v-model="searchQuery"
placeholder="Search key words..."
fill="outline"
class="search-input"
@keydown="handleSearchKeydown"
@input="handleSearchInput"
@ionFocus="handleSearchFocus"
@ionBlur="handleSearchBlur"
></ion-input>
<!-- Suggestions dropdown -->
<div v-if="showSuggestions && suggestions.length > 0" class="suggestions-dropdown">
<ion-list>
<ion-item
v-for="(suggestion, index) in suggestions"
:key="`${suggestion.type}-${suggestion.id}`"
button
@click="handleSelectSuggestion(suggestion)"
:class="{ 'suggestion-selected': index === selectedSuggestionIndex }"
class="suggestion-item"
>
<ion-icon
:icon="suggestion.type === 'tag' ? pricetag : briefcase"
slot="start"
:class="suggestion.type === 'tag' ? 'tag-icon' : 'job-icon'"
></ion-icon>
<ion-label>{{ suggestion.name }}</ion-label>
</ion-item>
</ion-list>
</div>
</div>
<ion-button fill="solid" color="secondary" @click="handleApplySearch">
{{ buttonText }}
</ion-button>
<ion-button v-if="showClear" fill="outline" color="tertiary" @click="handleClear">
Clear
</ion-button>
</div>
<div class="search-tags-container">
<!-- Search term chips (without #) -->
<ion-chip
v-for="(term, index) in searchTerms"
:key="'search-term-' + index"
color="tertiary"
class="search-tag-chip"
@click="handleRemoveSearchTerm(index)"
>
{{ term }}
</ion-chip>
<!-- Selected tag chips (without #) -->
<ion-chip
v-for="tag in selectedTags"
:key="`selected-tag-${tag.id}`"
color="tertiary"
class="search-tag-chip"
@click="handleRemoveSelectedTag(tag.id)"
>
{{ tag.name }}
</ion-chip>
<!-- Available tag chips (with #) - only shown when showTags is true -->
<template v-if="showTags">
<ion-chip
v-for="tag in availableTags"
:key="`available-tag-${tag.id}`"
color="secondary"
class="search-tag-chip"
@click="handleTagClick(tag)"
>
{{ '#' + tag.name }}
</ion-chip>
</template>
<!-- Quick access chips - displayed alongside tags -->
<template v-if="quickAccessItems.length > 0">
<ion-chip
v-for="item in quickAccessItems"
:key="`quick-access-${item.id}`"
class="search-tag-chip quick-access-chip"
@click="handleQuickAccessClick(item)"
>
{{ item.name }}
</ion-chip>
</template>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import {
IonInput,
IonButton,
IonChip,
IonIcon,
IonList,
IonItem,
IonLabel,
} from '@ionic/vue';
import { pricetag, briefcase } from 'ionicons/icons';
interface Tag {
id: string;
name: string;
}
interface Suggestion {
id: string;
name: string;
type: 'tag' | 'job';
}
interface QuickAccessItem {
id: string;
name: string;
refId?: string;
}
const props = withDefaults(defineProps<{
title?: string;
buttonText?: string;
searchTerms?: string[];
selectedTags?: Tag[];
availableTags?: Tag[];
suggestions?: Suggestion[];
navigatePath?: string;
showClear?: boolean;
showTags?: boolean;
category?: string | null;
topLevel?: string | null;
quickAccessItems?: QuickAccessItem[];
}>(), {
title: 'What are we doing today?',
buttonText: 'Find Job',
searchTerms: () => [],
selectedTags: () => [],
availableTags: () => [],
suggestions: () => [],
navigatePath: '/find-job',
showClear: false,
showTags: true,
category: null,
topLevel: null,
quickAccessItems: () => []
});
const emit = defineEmits([
'navigate',
'search-input',
'quick-access-click'
]);
// Local UI state
const searchQuery = ref('');
const showSuggestions = ref(false);
const selectedSuggestionIndex = ref(-1);
// Log all props data for debugging
console.log('[SearchBar] Props:', {
title: props.title,
buttonText: props.buttonText,
searchTerms: props.searchTerms,
selectedTags: props.selectedTags,
availableTags: props.availableTags,
suggestions: props.suggestions,
navigatePath: props.navigatePath
});
// Helper to build URL with query params
const buildUrl = (tags: string[], searchTerms: string[], clearFilters = false) => {
const query = new URLSearchParams();
// Always preserve top-level category (never clear it)
if (props.topLevel) {
query.set('top', props.topLevel);
}
// If searching or clearing, don't include category/tags
if (!clearFilters && searchTerms.length === 0) {
// Preserve category if set (only when not searching)
if (props.category) {
query.set('category', props.category);
}
if (tags.length > 0) {
query.set('tags', tags.join(','));
}
}
if (searchTerms.length > 0) {
query.set('q', searchTerms.join(' '));
}
const queryString = query.toString();
const url = queryString ? `${props.navigatePath}?${queryString}` : props.navigatePath;
console.log('[SearchBar] buildUrl:', { tags, searchTerms, category: props.category, topLevel: props.topLevel, clearFilters, url });
return url;
};
// Event handlers
const handleRemoveSearchTerm = (index: number) => {
// Remove the search term at the specified index
const newSearchTerms = props.searchTerms.filter((_, i) => i !== index);
// Keep existing selected tags
const currentTags = props.selectedTags.map(t => t.name);
const url = buildUrl(currentTags, newSearchTerms);
emit('navigate', url);
};
const handleRemoveSelectedTag = (tagId: string) => {
// Remove the selected tag
const newTags = props.selectedTags.filter(t => t.id !== tagId).map(t => t.name);
// Keep existing search terms
const url = buildUrl(newTags, props.searchTerms);
emit('navigate', url);
};
const handleTagClick = (tag: Tag) => {
// Keep existing selected tags and add the new one
const currentTags = [...props.selectedTags.map(t => t.name), tag.name];
// Keep existing search terms
const url = buildUrl(currentTags, props.searchTerms);
emit('navigate', url);
};
const handleApplySearch = () => {
if (searchQuery.value.trim() === "") return;
// Keep existing search terms and add the new one
const newSearchTerms = [...props.searchTerms, searchQuery.value.trim()];
// Keep existing selected tags
const currentTags = props.selectedTags.map(t => t.name);
const url = buildUrl(currentTags, newSearchTerms);
emit('navigate', url);
searchQuery.value = "";
};
const handleSearchKeydown = (event: KeyboardEvent) => {
if (!showSuggestions.value || props.suggestions.length === 0) {
if (event.key === "Enter") {
handleApplySearch();
}
return;
}
if (event.key === "ArrowDown") {
event.preventDefault();
selectedSuggestionIndex.value = Math.min(
selectedSuggestionIndex.value + 1,
props.suggestions.length - 1
);
} else if (event.key === "ArrowUp") {
event.preventDefault();
selectedSuggestionIndex.value = Math.max(selectedSuggestionIndex.value - 1, -1);
} else if (event.key === "Enter") {
event.preventDefault();
if (selectedSuggestionIndex.value >= 0) {
handleSelectSuggestion(props.suggestions[selectedSuggestionIndex.value]);
} else {
handleApplySearch();
}
} else if (event.key === "Escape") {
showSuggestions.value = false;
selectedSuggestionIndex.value = -1;
}
};
const handleSearchInput = () => {
const query = searchQuery.value.trim();
showSuggestions.value = query.length >= 2;
selectedSuggestionIndex.value = -1;
emit('search-input', query);
};
const handleSearchFocus = () => {
if (searchQuery.value.trim().length >= 2) {
showSuggestions.value = true;
}
};
const handleSearchBlur = () => {
setTimeout(() => {
showSuggestions.value = false;
selectedSuggestionIndex.value = -1;
}, 200);
};
const handleSelectSuggestion = (suggestion: Suggestion) => {
if (suggestion.type === 'tag') {
// Keep existing selected tags and add the suggested tag
const currentTags = [...props.selectedTags.map(t => t.name), suggestion.name];
// Keep existing search terms
const url = buildUrl(currentTags, props.searchTerms);
emit('navigate', url);
} else if (suggestion.type === 'job') {
// Navigate directly to view job page
emit('navigate', `/view-job/${suggestion.id}`);
}
searchQuery.value = "";
showSuggestions.value = false;
selectedSuggestionIndex.value = -1;
};
const handleClear = () => {
// Clear all filters (search terms, tags, and category)
const url = buildUrl([], [], true);
emit('navigate', url);
};
const handleQuickAccessClick = (item: QuickAccessItem) => {
emit('quick-access-click', item);
};
</script>
<style scoped>
.search-section {
margin-top: 0;
margin-bottom: 32px;
width: 100%;
max-width: 1000px;
margin-left: auto;
margin-right: auto;
}
.search-title {
margin: 0 0 24px 0;
color: var(--ion-text-color);
font-size: 32px;
font-weight: 600;
text-align: center;
font-variant: small-caps;
}
.search-tags-container {
display: flex;
flex-wrap: wrap;
gap: 6px;
justify-content: center;
align-items: center;
padding: 8px;
background-color: var(--ion-background-color-step-50);
border-radius: 8px;
margin-top: 8px;
}
.search-tag-chip {
cursor: pointer;
margin: 0;
text-transform: lowercase;
font-size: 12px;
font-weight: 600;
height: 24px;
padding: 0 10px;
white-space: nowrap;
}
.search-tag-chip:hover {
filter: brightness(1.1);
transform: scale(1.02);
transition: all 0.2s ease;
}
.search-term-chip {
/* Blue/secondary color chips for search terms */
}
.search-bar {
display: flex;
gap: 8px;
align-items: center;
background-color: var(--ion-background-color-step-50);
padding: 12px;
border-radius: 8px;
}
.search-input-container {
flex: 1;
display: flex;
flex-direction: column;
position: relative;
}
.search-input {
width: 100%;
font-size: 20px;
min-height: 48px;
}
.search-undo-wrapper {
position: relative;
display: inline-block;
}
.search-undo-button {
margin: 0;
min-width: 44px;
}
.search-undo-shadow {
position: absolute;
top: 2px;
left: -2px;
z-index: 0;
}
.search-undo-front {
position: relative;
z-index: 1;
--color: var(--ion-color-tertiary);
}
/* Suggestions dropdown */
.suggestions-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 1000;
margin-top: 4px;
background: var(--ion-color-dark);
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
border: 1px solid var(--ion-color-medium);
max-height: 400px;
overflow-y: auto;
}
.suggestions-dropdown ion-list {
background: transparent;
padding: 0;
}
.suggestion-item {
--background: var(--ion-color-dark);
--border-color: transparent;
cursor: pointer;
transition: background 0.2s ease;
}
.suggestion-item:hover,
.suggestion-item.suggestion-selected {
--background: var(--ion-color-medium);
}
.suggestion-item ion-label {
font-size: 15px;
color: var(--ion-color-light);
}
.suggestion-item .tag-icon {
color: var(--ion-color-primary);
font-size: 20px;
}
.suggestion-item .job-icon {
color: var(--ion-color-success);
font-size: 20px;
}
.quick-access-chip {
font-weight: 500;
text-transform: none;
font-size: 12px;
height: auto;
padding: 4px 10px;
border-radius: 12px;
--background: var(--ion-color-primary);
--color: var(--ion-color-primary-contrast);
}
</style>