1- import { useEffect , useState } from "react"
2- import { VSCodeButton , VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
1+ import { useEffect , useState , useMemo } from "react"
2+ import {
3+ VSCodeButton ,
4+ VSCodeProgressRing ,
5+ VSCodeDropdown ,
6+ VSCodeOption ,
7+ VSCodeTextField ,
8+ } from "@vscode/webview-ui-toolkit/react"
39import { useAppTranslation } from "../../../i18n/TranslationContext"
410import { useExtensionState } from "../../../context/ExtensionStateContext"
511import { vscode } from "../../../utils/vscode"
612import { McpMarketplaceItem } from "../../../../../src/shared/mcp"
713import McpMarketplaceCard from "./McpMarketplaceCard"
814import McpSubmitCard from "./McpSubmitCard"
915
16+ type SortOption = "stars" | "downloads" | "newest" | "updated"
17+
1018const McpMarketplaceView = ( ) => {
1119 const { t } = useAppTranslation ( )
1220 const { mcpServers } = useExtensionState ( )
1321 const [ isLoading , setIsLoading ] = useState ( true )
1422 const [ error , setError ] = useState < string | null > ( null )
1523 const [ items , setItems ] = useState < McpMarketplaceItem [ ] > ( [ ] )
24+ const [ searchQuery , setSearchQuery ] = useState ( "" )
25+ const [ selectedCategory , setSelectedCategory ] = useState < string > ( "all" )
26+ const [ sortBy , setSortBy ] = useState < SortOption > ( "stars" )
1627
1728 useEffect ( ( ) => {
1829 const handleMessage = ( event : MessageEvent ) => {
@@ -31,15 +42,52 @@ const McpMarketplaceView = () => {
3142 }
3243 }
3344 }
34-
3545 window . addEventListener ( "message" , handleMessage )
36-
37- // Request marketplace data when component mounts
3846 vscode . postMessage ( { type : "fetchMcpMarketplace" } )
39-
4047 return ( ) => window . removeEventListener ( "message" , handleMessage )
4148 } , [ ] )
4249
50+ const categories = useMemo ( ( ) => {
51+ const categorySet = new Set ( items . map ( ( item ) => item . category ) )
52+ return [ "all" , ...Array . from ( categorySet ) ]
53+ } , [ items ] )
54+
55+ const filteredAndSortedItems = useMemo ( ( ) => {
56+ let filtered = items
57+
58+ // Apply search filter
59+ if ( searchQuery ) {
60+ const query = searchQuery . toLowerCase ( )
61+ filtered = filtered . filter (
62+ ( item ) =>
63+ item . name . toLowerCase ( ) . includes ( query ) ||
64+ item . description . toLowerCase ( ) . includes ( query ) ||
65+ item . tags . some ( ( tag ) => tag . toLowerCase ( ) . includes ( query ) ) ,
66+ )
67+ }
68+
69+ // Apply category filter
70+ if ( selectedCategory !== "all" ) {
71+ filtered = filtered . filter ( ( item ) => item . category === selectedCategory )
72+ }
73+
74+ // Apply sorting
75+ return [ ...filtered ] . sort ( ( a , b ) => {
76+ switch ( sortBy ) {
77+ case "stars" :
78+ return b . githubStars - a . githubStars
79+ case "downloads" :
80+ return b . downloadCount - a . downloadCount
81+ case "newest" :
82+ return new Date ( b . createdAt ) . getTime ( ) - new Date ( a . createdAt ) . getTime ( )
83+ case "updated" :
84+ return new Date ( b . updatedAt ) . getTime ( ) - new Date ( a . updatedAt ) . getTime ( )
85+ default :
86+ return 0
87+ }
88+ } )
89+ } , [ items , searchQuery , selectedCategory , sortBy ] )
90+
4391 if ( isLoading ) {
4492 return (
4593 < div
@@ -84,7 +132,33 @@ const McpMarketplaceView = () => {
84132
85133 return (
86134 < div style = { { padding : "0 10px" } } >
87- { items . length === 0 ? (
135+ < div style = { { padding : "16px 0" , display : "flex" , gap : "16px" , alignItems : "center" } } >
136+ < VSCodeTextField
137+ placeholder = { t ( "mcp:marketplace.searchPlaceholder" ) }
138+ value = { searchQuery }
139+ onChange = { ( e ) => setSearchQuery ( ( e . target as HTMLInputElement ) . value ) }
140+ style = { { flexGrow : 1 } }
141+ />
142+ < VSCodeDropdown
143+ value = { selectedCategory }
144+ onChange = { ( e ) => setSelectedCategory ( ( e . target as HTMLSelectElement ) . value ) } >
145+ { categories . map ( ( category ) => (
146+ < VSCodeOption key = { category } value = { category } >
147+ { category === "all" ? t ( "mcp:marketplace.allCategories" ) : category }
148+ </ VSCodeOption >
149+ ) ) }
150+ </ VSCodeDropdown >
151+ < VSCodeDropdown
152+ value = { sortBy }
153+ onChange = { ( e ) => setSortBy ( ( e . target as HTMLSelectElement ) . value as SortOption ) } >
154+ < VSCodeOption value = "stars" > { t ( "mcp:marketplace.sortByStars" ) } </ VSCodeOption >
155+ < VSCodeOption value = "downloads" > { t ( "mcp:marketplace.sortByDownloads" ) } </ VSCodeOption >
156+ < VSCodeOption value = "newest" > { t ( "mcp:marketplace.sortByNewest" ) } </ VSCodeOption >
157+ < VSCodeOption value = "updated" > { t ( "mcp:marketplace.sortByUpdated" ) } </ VSCodeOption >
158+ </ VSCodeDropdown >
159+ </ div >
160+
161+ { filteredAndSortedItems . length === 0 ? (
88162 < div
89163 style = { {
90164 display : "flex" ,
@@ -96,7 +170,11 @@ const McpMarketplaceView = () => {
96170 textAlign : "center" ,
97171 } } >
98172 < span className = "codicon codicon-inbox" style = { { fontSize : "48px" , marginBottom : "16px" } } />
99- < p style = { { margin : 0 } } > { t ( "mcp:marketplace.noServers" ) } </ p >
173+ < p style = { { margin : 0 } } >
174+ { searchQuery || selectedCategory !== "all"
175+ ? t ( "mcp:marketplace.noResults" )
176+ : t ( "mcp:marketplace.noServers" ) }
177+ </ p >
100178 </ div >
101179 ) : (
102180 < div
@@ -106,7 +184,7 @@ const McpMarketplaceView = () => {
106184 gap : "16px" ,
107185 padding : "16px 0" ,
108186 } } >
109- { items . map ( ( item ) => (
187+ { filteredAndSortedItems . map ( ( item ) => (
110188 < McpMarketplaceCard key = { item . mcpId } item = { item } installedServers = { mcpServers } />
111189 ) ) }
112190 < McpSubmitCard />
0 commit comments