2
2
import { ref , computed , onMounted } from ' vue'
3
3
import { useI18n } from ' vue-i18n'
4
4
import { useRouter } from ' vue-router'
5
- import { Loader2 , Info , Download } from ' lucide-vue-next'
5
+ import { Loader2 , Info , Download , ChevronDown , PackagePlus } from ' lucide-vue-next'
6
6
import { Button } from ' @/components/ui/button'
7
- import { Input } from ' @/components/ui/input'
8
7
import { McpCatalogService } from ' @/services/mcpCatalogService'
9
8
10
9
// Props and emits
@@ -27,18 +26,38 @@ const servers = ref<any[]>([])
27
26
const searchTerm = ref (' ' )
28
27
const searchQuery = ref (' ' )
29
28
const selectedServerId = ref <string | null >(null )
29
+ const selectedCategory = ref (' all' )
30
+
31
+ // Available categories (you can expand this based on your data)
32
+ const categories = [
33
+ { value: ' all' , label: ' All Categories' },
34
+ { value: ' productivity' , label: ' Productivity' },
35
+ { value: ' development' , label: ' Development' },
36
+ { value: ' ai' , label: ' AI & Machine Learning' },
37
+ { value: ' database' , label: ' Database' },
38
+ { value: ' api' , label: ' API & Integration' }
39
+ ]
30
40
31
41
// Computed
32
42
const filteredServers = computed (() => {
33
43
if (! searchQuery .value .trim ()) return []
34
44
35
45
const term = searchQuery .value .toLowerCase ()
36
- return servers .value .filter (server =>
46
+ let filtered = servers .value .filter (server =>
37
47
server .name .toLowerCase ().includes (term ) ||
38
48
server .description .toLowerCase ().includes (term ) ||
39
49
server .author_name ?.toLowerCase ().includes (term ) ||
40
50
server .category_name ?.toLowerCase ().includes (term )
41
51
)
52
+
53
+ // Filter by category if not 'all'
54
+ if (selectedCategory .value !== ' all' ) {
55
+ filtered = filtered .filter (server =>
56
+ server .category_name ?.toLowerCase () === selectedCategory .value .toLowerCase ()
57
+ )
58
+ }
59
+
60
+ return filtered
42
61
})
43
62
44
63
@@ -88,42 +107,69 @@ onMounted(() => {
88
107
</script >
89
108
90
109
<template >
91
- <div class =" space-y-6" >
92
- <!-- Step Header -->
93
- <div >
94
- <h2 class =" text-xl font-semibold text-gray-900 mb-2" >
95
- {{ t('mcpInstallations.wizard.server.title') }}
96
- </h2 >
97
- <p class =" text-gray-600" >
98
- {{ t('mcpInstallations.wizard.server.description') }}
99
- </p >
100
- </div >
101
-
102
- <!-- Search Input -->
103
- <div class =" space-y-2" >
104
- <label for =" server-search" class =" text-sm font-medium text-gray-700" >
105
- {{ t('mcpInstallations.wizard.server.searchLabel') }}
106
- </label >
107
- <div class =" flex w-full items-center gap-1.5" >
108
- <Input
109
- id =" server-search"
110
- v-model =" searchTerm"
111
- type =" text"
112
- :placeholder =" t('mcpInstallations.wizard.server.searchPlaceholder')"
113
- @keyup.enter =" performSearch"
114
- class =" flex-1"
115
- />
116
- <Button type =" button" @click =" performSearch" >
117
- Search
118
- </Button >
110
+ <div class =" pt-10" >
111
+ <div class =" mx-auto max-w-2xl" >
112
+ <div class =" text-center" >
113
+ <div class =" mx-auto size-16 text-gray-400" >
114
+ <PackagePlus class =" w-full h-full" stroke-width =" 1.25" aria-hidden =" true" />
115
+ </div >
116
+ <h2 class =" mt-2 text-base font-semibold text-gray-900" >
117
+ {{ t('mcpInstallations.wizard.server.title') }}
118
+ </h2 >
119
+ <p class =" mt-1 text-sm text-gray-500" >
120
+ {{ t('mcpInstallations.wizard.server.description') }}
121
+ </p >
119
122
</div >
123
+
124
+ <!-- Search Form -->
125
+ <form class =" mt-6 sm:flex sm:items-center" @submit.prevent =" performSearch" >
126
+ <div class =" flex grow items-center rounded-md bg-white pl-3 outline-1 -outline-offset-1 outline-gray-300 has-[input:focus-within]:outline-2 has-[input:focus-within]:-outline-offset-2 has-[input:focus-within]:outline-primary" >
127
+ <input
128
+ v-model =" searchTerm"
129
+ type =" text"
130
+ name =" search"
131
+ :aria-label =" t('mcpInstallations.wizard.server.searchLabel')"
132
+ class =" block min-w-0 grow py-1.5 pr-3 text-base text-gray-900 placeholder:text-gray-400 focus:outline-none sm:text-sm/6"
133
+ :placeholder =" t('mcpInstallations.wizard.server.searchPlaceholder')"
134
+ @keyup.enter =" performSearch"
135
+ />
136
+ <div class =" grid shrink-0 grid-cols-1 focus-within:relative" >
137
+ <select
138
+ v-model =" selectedCategory"
139
+ name =" category"
140
+ :aria-label =" t('mcpInstallations.wizard.server.categoryLabel')"
141
+ class =" col-start-1 row-start-1 w-full appearance-none rounded-md py-1.5 pr-7 pl-3 text-base text-gray-500 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-primary sm:text-sm/6"
142
+ >
143
+ <option
144
+ v-for =" category in categories"
145
+ :key =" category.value"
146
+ :value =" category.value"
147
+ >
148
+ {{ category.label }}
149
+ </option >
150
+ </select >
151
+ <ChevronDown class =" pointer-events-none col-start-1 row-start-1 mr-2 size-5 self-center justify-self-end text-gray-500 sm:size-4" aria-hidden =" true" />
152
+ </div >
153
+ </div >
154
+ <div class =" mt-3 sm:mt-0 sm:ml-4 sm:shrink-0" >
155
+ <button
156
+ type =" submit"
157
+ class =" block w-full rounded-md bg-primary px-3 py-2 text-center text-sm font-semibold text-primary-foreground shadow-xs hover:bg-primary/90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary"
158
+ :disabled =" isLoading"
159
+ >
160
+ {{ isLoading ? t('mcpInstallations.wizard.server.searching') : t('mcpInstallations.wizard.server.searchButton') }}
161
+ </button >
162
+ </div >
163
+ </form >
120
164
</div >
121
165
122
166
<!-- Error Alert -->
123
- <div v-if =" error" class =" rounded-md bg-red-50 p-4" >
167
+ <div v-if =" error" class =" mt-14 rounded-md bg-red-50 p-4" >
124
168
<div class =" flex" >
125
169
<div class =" ml-3" >
126
- <h3 class =" text-sm font-medium text-red-800" >Error loading servers</h3 >
170
+ <h3 class =" text-sm font-medium text-red-800" >
171
+ {{ t('mcpInstallations.wizard.server.errorTitle') }}
172
+ </h3 >
127
173
<div class =" mt-2 text-sm text-red-700" >
128
174
<p >{{ error }}</p >
129
175
</div >
@@ -143,13 +189,13 @@ onMounted(() => {
143
189
</div >
144
190
145
191
<!-- Loading State -->
146
- <div v-if =" isLoading" class =" flex items-center justify-center py-8" >
192
+ <div v-if =" isLoading" class =" mt-14 flex items-center justify-center py-8" >
147
193
<Loader2 class =" h-6 w-6 animate-spin mr-2 text-gray-400" />
148
194
<span class =" text-gray-600" >{{ t('messages.loading') }}</span >
149
195
</div >
150
196
151
197
<!-- Server List (only show when there's a search query) -->
152
- <div v-else-if =" searchQuery.trim() && filteredServers.length > 0" class =" space-y-4" >
198
+ <div v-else-if =" searchQuery.trim() && filteredServers.length > 0" class =" mt-14 space-y-4" >
153
199
<div
154
200
v-for =" server in filteredServers"
155
201
:key =" server.id"
@@ -205,13 +251,13 @@ onMounted(() => {
205
251
</div >
206
252
207
253
<!-- No Results -->
208
- <div v-else-if =" searchQuery.trim() && filteredServers.length === 0" class =" text-center py-8" >
254
+ <div v-else-if =" searchQuery.trim() && filteredServers.length === 0" class =" mt-14 text-center py-8" >
209
255
<p class =" text-gray-500" >{{ t('mcpInstallations.wizard.server.noServersFound') }}</p >
210
256
</div >
211
257
212
258
<!-- Empty State (when no search performed) -->
213
- <div v-else-if =" !searchQuery.trim() && !isLoading" class =" text-center py-8" >
214
- <p class =" text-gray-500" >Enter a search term and click Search to find MCP servers... </p >
259
+ <div v-else-if =" !searchQuery.trim() && !isLoading" class =" mt-14 text-center py-8" >
260
+ <p class =" text-gray-500" >{{ t('mcpInstallations.wizard.server.emptyStateMessage') }} </p >
215
261
</div >
216
262
</div >
217
263
</template >
0 commit comments