diff --git a/client/public/images/mcp_server_categories.png b/client/public/images/mcp_server_categories.png new file mode 100644 index 00000000..a40af9da Binary files /dev/null and b/client/public/images/mcp_server_categories.png differ diff --git a/client/src/components/CategoriesDropdown.tsx b/client/src/components/CategoriesDropdown.tsx new file mode 100644 index 00000000..8cc42ed6 --- /dev/null +++ b/client/src/components/CategoriesDropdown.tsx @@ -0,0 +1,247 @@ +import { useState, useRef, useEffect } from 'react'; +import { Link } from 'react-router-dom'; +import { useLanguage } from '../contexts/LanguageContext'; + +type CategoryProps = { + isMobile?: boolean; + onSelectMobile?: () => void; +}; + +export function CategoriesDropdown({ isMobile, onSelectMobile }: CategoryProps) { + const [isCategoriesMenuOpen, setIsCategoriesMenuOpen] = useState(false); + const categoriesMenuRef = useRef(null); + const { t } = useLanguage(); + const [categoryKeys, setCategoryKeys] = useState([]); + const [isLoading, setIsLoading] = useState(true); + + // Fetch categories from API + useEffect(() => { + const fetchCategories = async () => { + try { + setIsLoading(true); + const response = await fetch('/v1/hub/server_categories'); + if (!response.ok) { + throw new Error('Failed to fetch categories'); + } + const data = await response.json(); + setCategoryKeys(data); + } catch (error) { + console.error('Error fetching categories:', error); + // Show an empty menu if the API call fails + setCategoryKeys([]); + } finally { + setIsLoading(false); + } + }; + + fetchCategories(); + }, []); + + // Map category keys to their respective SVG icons + const categoryIcons: Record = { + "browser-automation": ( + + + + ), + "cloud-platforms": ( + + + + ), + "communication": ( + + + + ), + "databases": ( + + + + ), + "file-systems": ( + + + + ), + "knowledge-memory": ( + + + + ), + "location-services": ( + + + + + ), + "monitoring": ( + + + + ), + "search": ( + + + + ), + "version-control": ( + + + + ), + "integrations": ( + + + + ), + "other-tools": ( + + + + ), + "developer-tools": ( + + + + ) + }; + + // Close the dropdown menu when selecting a category on mobile + const handleCategoryClick = () => { + if (isMobile && onSelectMobile) { + onSelectMobile(); + } + setIsCategoriesMenuOpen(false); + }; + + // Handle clicks outside the menu to close it + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + isCategoriesMenuOpen && + categoriesMenuRef.current && + !categoriesMenuRef.current.contains(event.target as Node) && + !(event.target as Element).closest('button.categories-toggle') + ) { + setIsCategoriesMenuOpen(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [isCategoriesMenuOpen]); + + if (isMobile) { + // Mobile version + return ( +
+
+ + + + {t('nav.categories')} +
+
+ {isLoading ? ( +
Loading categories...
+ ) : ( + categoryKeys.map((key, index) => ( + + {categoryIcons[key]} + {t(`category.${key}`)} + + )) + )} +
+
+ ); + } + + // Desktop version + return ( +
+ + + {/* Categories dropdown menu */} + {isCategoriesMenuOpen && ( +
+
+ {isLoading ? ( +
Loading categories...
+ ) : ( + categoryKeys.map((key, index) => ( + +
+ {categoryIcons[key]} + {t(`category.${key}`)} +
+ + )) + )} +
+
+ )} +
+ ); +} diff --git a/client/src/components/Header.tsx b/client/src/components/Header.tsx index 2941ebc6..a4c78383 100644 --- a/client/src/components/Header.tsx +++ b/client/src/components/Header.tsx @@ -1,6 +1,7 @@ import { useState, useRef, useEffect } from 'react'; import { Link } from 'react-router-dom'; import { useLanguage, SupportedLanguage } from '../contexts/LanguageContext'; +import { CategoriesDropdown } from './CategoriesDropdown'; export function Header() { const [isMenuOpen, setIsMenuOpen] = useState(false); @@ -119,133 +120,137 @@ export function Header() { {/* Mobile menu - absolutely positioned for better layout */}
- {/* Language switcher for mobile */} -
-
{t('language.label')} / 语言 / 言語 / Idioma
-
- - - - - - + {/* Language switcher for mobile */} +
+
{t('language.label')} / 语言 / 言語 / Idioma
+
+ + + + + + +
-
- - {/* Mobile menu navigation items */} - - - - - {t('nav.home')} - - - + + + {t('nav.home')} + + + {/* Categories dropdown for mobile */} + + + - - - {t('nav.submit')} - - - + + + {t('nav.submit')} + + - - - {t('nav.docs')} - - - + + + {t('nav.docs')} + + - - - {t('nav.about')} - -
+ + + + {t('nav.about')} + +
{/* Desktop menu */} @@ -268,6 +273,10 @@ export function Header() { {t('nav.home')} + + {/* Categories dropdown for desktop */} + + (undefined // Translation dictionary with imported JSON files const translations: Record = { - en: enTranslations, - 'zh-hans': zhHansTranslations, - 'zh-hant': zhHantTranslations, - ja: jaTranslations, - es: esTranslations, - de: deTranslations + en: { ...enTranslations, category: enCategoryTranslations }, + 'zh-hans': { ...zhHansTranslations, category: zhHansCategoryTranslations }, + 'zh-hant': { ...zhHantTranslations, category: zhHantCategoryTranslations }, + ja: { ...jaTranslations, category: jaCategoryTranslations }, + es: { ...esTranslations, category: esCategoryTranslations }, + de: { ...deTranslations, category: deCategoryTranslations } }; // Helper function to detect browser language diff --git a/client/src/locale/category-de.json b/client/src/locale/category-de.json new file mode 100644 index 00000000..4c9f350b --- /dev/null +++ b/client/src/locale/category-de.json @@ -0,0 +1,15 @@ +{ + "browser-automation": "Browser-Automatisierung", + "cloud-platforms": "Cloud-Plattformen", + "communication": "Kommunikation", + "databases": "Datenbanken", + "file-systems": "Dateisysteme", + "knowledge-memory": "Wissen & Speicher", + "location-services": "Ortungsdienste", + "monitoring": "Überwachung", + "search": "Suche", + "version-control": "Versionskontrolle", + "integrations": "Integrationen", + "other-tools": "Andere Werkzeuge", + "developer-tools": "Entwicklertools" +} diff --git a/client/src/locale/category-en.json b/client/src/locale/category-en.json new file mode 100644 index 00000000..a785acb0 --- /dev/null +++ b/client/src/locale/category-en.json @@ -0,0 +1,15 @@ +{ + "browser-automation": "Browser Automation", + "cloud-platforms": "Cloud Platforms", + "communication": "Communication", + "databases": "Databases", + "file-systems": "File Systems", + "knowledge-memory": "Knowledge & Memory", + "location-services": "Location Services", + "monitoring": "Monitoring", + "search": "Search", + "version-control": "Version Control", + "integrations": "Integrations", + "other-tools": "Other Tools", + "developer-tools": "Developer Tools" +} diff --git a/client/src/locale/category-es.json b/client/src/locale/category-es.json new file mode 100644 index 00000000..9aa7d4bd --- /dev/null +++ b/client/src/locale/category-es.json @@ -0,0 +1,15 @@ +{ + "browser-automation": "Automatización de navegador", + "cloud-platforms": "Plataformas en la nube", + "communication": "Comunicación", + "databases": "Bases de datos", + "file-systems": "Sistemas de archivos", + "knowledge-memory": "Conocimiento y memoria", + "location-services": "Servicios de ubicación", + "monitoring": "Monitoreo", + "search": "Búsqueda", + "version-control": "Control de versiones", + "integrations": "Integraciones", + "other-tools": "Otras herramientas", + "developer-tools": "Herramientas para desarrolladores" +} diff --git a/client/src/locale/category-ja.json b/client/src/locale/category-ja.json new file mode 100644 index 00000000..ac6bf105 --- /dev/null +++ b/client/src/locale/category-ja.json @@ -0,0 +1,15 @@ +{ + "browser-automation": "ブラウザ自動化", + "cloud-platforms": "クラウドプラットフォーム", + "communication": "コミュニケーション", + "databases": "データベース", + "file-systems": "ファイルシステム", + "knowledge-memory": "知識とメモリ", + "location-services": "位置情報サービス", + "monitoring": "モニタリング", + "search": "検索", + "version-control": "バージョン管理", + "integrations": "統合", + "other-tools": "その他のツール", + "developer-tools": "開発ツール" +} diff --git a/client/src/locale/category-zh-hans.json b/client/src/locale/category-zh-hans.json new file mode 100644 index 00000000..ef2840ef --- /dev/null +++ b/client/src/locale/category-zh-hans.json @@ -0,0 +1,15 @@ +{ + "browser-automation": "浏览器自动化", + "cloud-platforms": "云平台", + "communication": "通信", + "databases": "数据库", + "file-systems": "文件系统", + "knowledge-memory": "知识与记忆", + "location-services": "位置服务", + "monitoring": "监控", + "search": "搜索", + "version-control": "版本控制", + "integrations": "集成", + "other-tools": "其他工具", + "developer-tools": "开发者工具" +} diff --git a/client/src/locale/category-zh-hant.json b/client/src/locale/category-zh-hant.json new file mode 100644 index 00000000..d9bf251f --- /dev/null +++ b/client/src/locale/category-zh-hant.json @@ -0,0 +1,15 @@ +{ + "browser-automation": "瀏覽器自動化", + "cloud-platforms": "雲平台", + "communication": "通訊", + "databases": "數據庫", + "file-systems": "檔案系統", + "knowledge-memory": "知識與記憶", + "location-services": "位置服務", + "monitoring": "監控", + "search": "搜尋", + "version-control": "版本控制", + "integrations": "整合", + "other-tools": "其他工具", + "developer-tools": "開發者工具" +} diff --git a/client/src/locale/de.json b/client/src/locale/de.json index 56e36df3..0b15b481 100644 --- a/client/src/locale/de.json +++ b/client/src/locale/de.json @@ -4,7 +4,8 @@ "docs": "Dokumentation", "about": "Über uns", "github": "GitHub", - "submit": "Einreichen" + "submit": "Einreichen", + "categories": "Kategorien" }, "language": { "english": "English", diff --git a/client/src/locale/en.json b/client/src/locale/en.json index e0e19f9b..c8914c2d 100644 --- a/client/src/locale/en.json +++ b/client/src/locale/en.json @@ -4,7 +4,8 @@ "docs": "Docs", "about": "About", "github": "GitHub", - "submit": "Submit" + "submit": "Submit", + "categories": "Categories" }, "language": { "english": "English", diff --git a/client/src/locale/es.json b/client/src/locale/es.json index 085e8394..4f9da86b 100644 --- a/client/src/locale/es.json +++ b/client/src/locale/es.json @@ -4,7 +4,8 @@ "docs": "Documentación", "about": "Acerca de", "github": "GitHub", - "submit": "Enviar" + "submit": "Enviar", + "categories": "Categorías" }, "language": { "english": "English", diff --git a/client/src/locale/ja.json b/client/src/locale/ja.json index 5e27001d..b630452a 100644 --- a/client/src/locale/ja.json +++ b/client/src/locale/ja.json @@ -4,7 +4,8 @@ "docs": "ドキュメント", "about": "概要", "github": "GitHub", - "submit": "提出" + "submit": "提出", + "categories": "カテゴリ" }, "language": { "english": "English", diff --git a/client/src/locale/zh-hans.json b/client/src/locale/zh-hans.json index cf8b1fd7..fa0ae8a4 100644 --- a/client/src/locale/zh-hans.json +++ b/client/src/locale/zh-hans.json @@ -4,7 +4,8 @@ "docs": "文档", "about": "关于", "github": "GitHub", - "submit": "提交" + "submit": "提交", + "categories": "分类" }, "language": { "english": "English (英文)", diff --git a/client/src/locale/zh-hant.json b/client/src/locale/zh-hant.json index d1f6caa4..ce2f1aa9 100644 --- a/client/src/locale/zh-hant.json +++ b/client/src/locale/zh-hant.json @@ -4,7 +4,8 @@ "docs": "文檔", "about": "關於", "github": "GitHub", - "submit": "提交" + "submit": "提交", + "categories": "分類" }, "language": { "english": "English (英文)", diff --git a/server/src/lib/mcpCategories.ts b/server/src/lib/mcpCategories.ts new file mode 100644 index 00000000..3efcc1b3 --- /dev/null +++ b/server/src/lib/mcpCategories.ts @@ -0,0 +1,16 @@ +// MCP server categories +export const MCP_SERVER_CATEGORIES = [ + "browser-automation", + "cloud-platforms", + "communication", + "databases", + "file-systems", + "knowledge-memory", + "location-services", + "monitoring", + "search", + "version-control", + "integrations", + "other-tools", + "developer-tools" +]; diff --git a/server/src/routes/hub.ts b/server/src/routes/hub.ts index 85ce3117..cf39f109 100644 --- a/server/src/routes/hub.ts +++ b/server/src/routes/hub.ts @@ -1,6 +1,7 @@ import { Router, Request, Response } from 'express'; import { refreshCacheIfNeeded, forceRefreshCache } from '../lib/mcpServers.js'; import { enrichServerData, fetchReadmeContent, extractInfoFromReadme } from '../lib/githubEnrichment.js'; +import { MCP_SERVER_CATEGORIES } from '../lib/mcpCategories.js'; import { v4 as uuidv4 } from 'uuid'; import path from 'path'; import fs from 'fs/promises'; @@ -254,4 +255,16 @@ router.post('/servers/submit', async (req: Request, res: Response): Promise { + try { + console.log('api /server_categories called'); + res.json(MCP_SERVER_CATEGORIES); + console.log(`v1/hub/server_categories Served categories at ${new Date().toISOString()}`); + } catch (error) { + console.error('Error serving categories:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + export default router; \ No newline at end of file