@samanhappy/mcphub 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/.env.example +2 -0
  2. package/.eslintrc.json +25 -0
  3. package/.github/workflows/build.yml +51 -0
  4. package/.github/workflows/release.yml +19 -0
  5. package/.prettierrc +7 -0
  6. package/Dockerfile +51 -0
  7. package/assets/amap-edit.png +0 -0
  8. package/assets/amap-result.png +0 -0
  9. package/assets/cherry-mcp.png +0 -0
  10. package/assets/cursor-mcp.png +0 -0
  11. package/assets/cursor-query.png +0 -0
  12. package/assets/cursor-tools.png +0 -0
  13. package/assets/dashboard.png +0 -0
  14. package/assets/dashboard.zh.png +0 -0
  15. package/assets/group.png +0 -0
  16. package/assets/group.zh.png +0 -0
  17. package/assets/market.zh.png +0 -0
  18. package/assets/wegroup.jpg +0 -0
  19. package/assets/wegroup.png +0 -0
  20. package/assets/wexin.png +0 -0
  21. package/doc/intro.md +73 -0
  22. package/doc/intro2.md +232 -0
  23. package/entrypoint.sh +10 -0
  24. package/frontend/favicon.ico +0 -0
  25. package/frontend/index.html +13 -0
  26. package/frontend/postcss.config.js +6 -0
  27. package/frontend/src/App.tsx +44 -0
  28. package/frontend/src/components/AddGroupForm.tsx +132 -0
  29. package/frontend/src/components/AddServerForm.tsx +90 -0
  30. package/frontend/src/components/ChangePasswordForm.tsx +158 -0
  31. package/frontend/src/components/EditGroupForm.tsx +149 -0
  32. package/frontend/src/components/EditServerForm.tsx +76 -0
  33. package/frontend/src/components/GroupCard.tsx +143 -0
  34. package/frontend/src/components/MarketServerCard.tsx +153 -0
  35. package/frontend/src/components/MarketServerDetail.tsx +297 -0
  36. package/frontend/src/components/ProtectedRoute.tsx +27 -0
  37. package/frontend/src/components/ServerCard.tsx +230 -0
  38. package/frontend/src/components/ServerForm.tsx +276 -0
  39. package/frontend/src/components/icons/LucideIcons.tsx +14 -0
  40. package/frontend/src/components/layout/Content.tsx +17 -0
  41. package/frontend/src/components/layout/Header.tsx +61 -0
  42. package/frontend/src/components/layout/Sidebar.tsx +98 -0
  43. package/frontend/src/components/ui/Badge.tsx +33 -0
  44. package/frontend/src/components/ui/Button.tsx +0 -0
  45. package/frontend/src/components/ui/DeleteDialog.tsx +48 -0
  46. package/frontend/src/components/ui/Pagination.tsx +128 -0
  47. package/frontend/src/components/ui/Toast.tsx +96 -0
  48. package/frontend/src/components/ui/ToggleGroup.tsx +134 -0
  49. package/frontend/src/components/ui/ToolCard.tsx +38 -0
  50. package/frontend/src/contexts/AuthContext.tsx +159 -0
  51. package/frontend/src/contexts/ToastContext.tsx +60 -0
  52. package/frontend/src/hooks/useGroupData.ts +232 -0
  53. package/frontend/src/hooks/useMarketData.ts +410 -0
  54. package/frontend/src/hooks/useServerData.ts +306 -0
  55. package/frontend/src/hooks/useSettingsData.ts +131 -0
  56. package/frontend/src/i18n.ts +42 -0
  57. package/frontend/src/index.css +20 -0
  58. package/frontend/src/layouts/MainLayout.tsx +33 -0
  59. package/frontend/src/locales/en.json +214 -0
  60. package/frontend/src/locales/zh.json +214 -0
  61. package/frontend/src/main.tsx +12 -0
  62. package/frontend/src/pages/Dashboard.tsx +206 -0
  63. package/frontend/src/pages/GroupsPage.tsx +116 -0
  64. package/frontend/src/pages/LoginPage.tsx +104 -0
  65. package/frontend/src/pages/MarketPage.tsx +356 -0
  66. package/frontend/src/pages/ServersPage.tsx +144 -0
  67. package/frontend/src/pages/SettingsPage.tsx +149 -0
  68. package/frontend/src/services/authService.ts +141 -0
  69. package/frontend/src/types/index.ts +160 -0
  70. package/frontend/src/utils/cn.ts +10 -0
  71. package/frontend/tsconfig.json +31 -0
  72. package/frontend/tsconfig.node.json +10 -0
  73. package/frontend/vite.config.ts +26 -0
  74. package/googled76ca578b6543fbc.html +1 -0
  75. package/jest.config.js +10 -0
  76. package/mcp_settings.json +45 -0
  77. package/package.json +3 -18
  78. package/servers.json +74722 -0
  79. package/src/config/index.ts +46 -0
  80. package/src/controllers/authController.ts +179 -0
  81. package/src/controllers/groupController.ts +341 -0
  82. package/src/controllers/marketController.ts +154 -0
  83. package/src/controllers/serverController.ts +303 -0
  84. package/src/index.ts +17 -0
  85. package/src/middlewares/auth.ts +28 -0
  86. package/src/middlewares/index.ts +43 -0
  87. package/src/models/User.ts +103 -0
  88. package/src/routes/index.ts +96 -0
  89. package/src/server.ts +72 -0
  90. package/src/services/groupService.ts +232 -0
  91. package/src/services/marketService.ts +116 -0
  92. package/src/services/mcpService.ts +385 -0
  93. package/src/services/sseService.ts +119 -0
  94. package/src/types/index.ts +129 -0
  95. package/src/utils/migration.ts +52 -0
  96. package/tsconfig.json +17 -0
  97. package/bin/cli.js +0 -45
@@ -0,0 +1,132 @@
1
+ import { useState, useEffect } from 'react'
2
+ import { useTranslation } from 'react-i18next'
3
+ import { useGroupData } from '@/hooks/useGroupData'
4
+ import { useServerData } from '@/hooks/useServerData'
5
+ import { GroupFormData, Server } from '@/types'
6
+ import { ToggleGroup } from './ui/ToggleGroup'
7
+
8
+ interface AddGroupFormProps {
9
+ onAdd: () => void
10
+ onCancel: () => void
11
+ }
12
+
13
+ const AddGroupForm = ({ onAdd, onCancel }: AddGroupFormProps) => {
14
+ const { t } = useTranslation()
15
+ const { createGroup } = useGroupData()
16
+ const { servers } = useServerData()
17
+ const [availableServers, setAvailableServers] = useState<Server[]>([])
18
+ const [error, setError] = useState<string | null>(null)
19
+ const [isSubmitting, setIsSubmitting] = useState(false)
20
+
21
+ const [formData, setFormData] = useState<GroupFormData>({
22
+ name: '',
23
+ description: '',
24
+ servers: []
25
+ })
26
+
27
+ useEffect(() => {
28
+ // Filter available servers (enabled only)
29
+ setAvailableServers(servers.filter(server => server.enabled !== false))
30
+ }, [servers])
31
+
32
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
33
+ const { name, value } = e.target
34
+ setFormData(prev => ({
35
+ ...prev,
36
+ [name]: value
37
+ }))
38
+ }
39
+
40
+ const handleSubmit = async (e: React.FormEvent) => {
41
+ e.preventDefault()
42
+ setIsSubmitting(true)
43
+ setError(null)
44
+
45
+ try {
46
+ if (!formData.name.trim()) {
47
+ setError(t('groups.nameRequired'))
48
+ setIsSubmitting(false)
49
+ return
50
+ }
51
+
52
+ const result = await createGroup(formData.name, formData.description, formData.servers)
53
+
54
+ if (!result) {
55
+ setError(t('groups.createError'))
56
+ setIsSubmitting(false)
57
+ return
58
+ }
59
+
60
+ onAdd()
61
+ } catch (err) {
62
+ setError(err instanceof Error ? err.message : String(err))
63
+ setIsSubmitting(false)
64
+ }
65
+ }
66
+
67
+ return (
68
+ <div className="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
69
+ <div className="bg-white rounded-lg shadow-lg max-w-md w-full">
70
+ <div className="p-6">
71
+ <h2 className="text-xl font-semibold text-gray-800 mb-4">{t('groups.addNew')}</h2>
72
+
73
+ {error && (
74
+ <div className="mb-4 p-3 bg-red-100 text-red-700 rounded">
75
+ {error}
76
+ </div>
77
+ )}
78
+
79
+ <form onSubmit={handleSubmit}>
80
+ <div className="mb-4">
81
+ <label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="name">
82
+ {t('groups.name')} *
83
+ </label>
84
+ <input
85
+ type="text"
86
+ id="name"
87
+ name="name"
88
+ value={formData.name}
89
+ onChange={handleChange}
90
+ className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
91
+ placeholder={t('groups.namePlaceholder')}
92
+ required
93
+ />
94
+ </div>
95
+
96
+ <ToggleGroup
97
+ className="mb-6"
98
+ label={t('groups.servers')}
99
+ noOptionsText={t('groups.noServerOptions')}
100
+ values={formData.servers}
101
+ options={availableServers.map(server => ({
102
+ value: server.name,
103
+ label: server.name
104
+ }))}
105
+ onChange={(servers) => setFormData(prev => ({ ...prev, servers }))}
106
+ />
107
+
108
+ <div className="flex justify-end space-x-3">
109
+ <button
110
+ type="button"
111
+ onClick={onCancel}
112
+ className="px-4 py-2 text-gray-600 hover:text-gray-800"
113
+ disabled={isSubmitting}
114
+ >
115
+ {t('common.cancel')}
116
+ </button>
117
+ <button
118
+ type="submit"
119
+ className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50"
120
+ disabled={isSubmitting}
121
+ >
122
+ {isSubmitting ? t('common.submitting') : t('common.create')}
123
+ </button>
124
+ </div>
125
+ </form>
126
+ </div>
127
+ </div>
128
+ </div>
129
+ )
130
+ }
131
+
132
+ export default AddGroupForm
@@ -0,0 +1,90 @@
1
+ import { useState } from 'react'
2
+ import { useTranslation } from 'react-i18next'
3
+ import ServerForm from './ServerForm'
4
+
5
+ interface AddServerFormProps {
6
+ onAdd: () => void
7
+ }
8
+
9
+ const AddServerForm = ({ onAdd }: AddServerFormProps) => {
10
+ const { t } = useTranslation()
11
+ const [modalVisible, setModalVisible] = useState(false)
12
+ const [error, setError] = useState<string | null>(null)
13
+
14
+ const toggleModal = () => {
15
+ setModalVisible(!modalVisible)
16
+ setError(null) // Clear any previous errors when toggling modal
17
+ }
18
+
19
+ const handleSubmit = async (payload: any) => {
20
+ try {
21
+ setError(null)
22
+ const token = localStorage.getItem('mcphub_token');
23
+ const response = await fetch('/api/servers', {
24
+ method: 'POST',
25
+ headers: {
26
+ 'Content-Type': 'application/json',
27
+ 'x-auth-token': token || ''
28
+ },
29
+ body: JSON.stringify(payload),
30
+ })
31
+
32
+ const result = await response.json()
33
+
34
+ if (!response.ok) {
35
+ // Use specific error message from the response if available
36
+ if (result && result.message) {
37
+ setError(result.message)
38
+ } else if (response.status === 400) {
39
+ setError(t('server.invalidData'))
40
+ } else if (response.status === 409) {
41
+ setError(t('server.alreadyExists', { serverName: payload.name }))
42
+ } else {
43
+ setError(t('server.addError'))
44
+ }
45
+ return
46
+ }
47
+
48
+ setModalVisible(false)
49
+ onAdd()
50
+ } catch (err) {
51
+ console.error('Error adding server:', err)
52
+
53
+ // Use friendly error messages based on error type
54
+ if (!navigator.onLine) {
55
+ setError(t('errors.network'))
56
+ } else if (err instanceof TypeError && (
57
+ err.message.includes('NetworkError') ||
58
+ err.message.includes('Failed to fetch')
59
+ )) {
60
+ setError(t('errors.serverConnection'))
61
+ } else {
62
+ setError(t('errors.serverAdd'))
63
+ }
64
+ }
65
+ }
66
+
67
+ return (
68
+ <div>
69
+ <button
70
+ onClick={toggleModal}
71
+ className="w-full bg-blue-500 hover:bg-blue-600 text-white font-medium py-2 px-4 rounded"
72
+ >
73
+ {t('server.addServer')}
74
+ </button>
75
+
76
+ {modalVisible && (
77
+ <div className="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
78
+ <ServerForm
79
+ onSubmit={handleSubmit}
80
+ onCancel={toggleModal}
81
+ modalTitle={t('server.addServer')}
82
+ formError={error}
83
+ />
84
+ </div>
85
+ )}
86
+ </div>
87
+ )
88
+ }
89
+
90
+ export default AddServerForm
@@ -0,0 +1,158 @@
1
+ import React, { useState } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { ChangePasswordCredentials } from '../types';
4
+ import { changePassword } from '../services/authService';
5
+
6
+ interface ChangePasswordFormProps {
7
+ onSuccess?: () => void;
8
+ onCancel?: () => void;
9
+ }
10
+
11
+ const ChangePasswordForm: React.FC<ChangePasswordFormProps> = ({ onSuccess, onCancel }) => {
12
+ const { t } = useTranslation();
13
+ const [formData, setFormData] = useState<ChangePasswordCredentials>({
14
+ currentPassword: '',
15
+ newPassword: '',
16
+ });
17
+ const [confirmPassword, setConfirmPassword] = useState('');
18
+ const [isLoading, setIsLoading] = useState(false);
19
+ const [error, setError] = useState<string | null>(null);
20
+ const [success, setSuccess] = useState(false);
21
+
22
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
23
+ const { name, value } = e.target;
24
+ if (name === 'confirmPassword') {
25
+ setConfirmPassword(value);
26
+ } else {
27
+ setFormData(prev => ({ ...prev, [name]: value }));
28
+ }
29
+ };
30
+
31
+ const handleSubmit = async (e: React.FormEvent) => {
32
+ e.preventDefault();
33
+ setError(null);
34
+
35
+ // Validate passwords match
36
+ if (formData.newPassword !== confirmPassword) {
37
+ setError(t('auth.passwordsNotMatch'));
38
+ return;
39
+ }
40
+
41
+ setIsLoading(true);
42
+ try {
43
+ const response = await changePassword(formData);
44
+
45
+ if (response.success) {
46
+ setSuccess(true);
47
+ if (onSuccess) {
48
+ onSuccess();
49
+ }
50
+ } else {
51
+ setError(response.message || t('auth.changePasswordError'));
52
+ }
53
+ } catch (err) {
54
+ setError(t('auth.changePasswordError'));
55
+ } finally {
56
+ setIsLoading(false);
57
+ }
58
+ };
59
+
60
+ return (
61
+ <div className="p-6 bg-white rounded-lg shadow-md">
62
+ <h2 className="text-xl font-bold mb-4">{t('auth.changePassword')}</h2>
63
+
64
+ {success ? (
65
+ <div className="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4">
66
+ {t('auth.changePasswordSuccess')}
67
+ </div>
68
+ ) : (
69
+ <form onSubmit={handleSubmit}>
70
+ {error && (
71
+ <div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
72
+ {error}
73
+ </div>
74
+ )}
75
+
76
+ <div className="mb-4">
77
+ <label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="currentPassword">
78
+ {t('auth.currentPassword')}
79
+ </label>
80
+ <input
81
+ type="password"
82
+ id="currentPassword"
83
+ name="currentPassword"
84
+ className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
85
+ value={formData.currentPassword}
86
+ onChange={handleChange}
87
+ required
88
+ />
89
+ </div>
90
+
91
+ <div className="mb-4">
92
+ <label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="newPassword">
93
+ {t('auth.newPassword')}
94
+ </label>
95
+ <input
96
+ type="password"
97
+ id="newPassword"
98
+ name="newPassword"
99
+ className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
100
+ value={formData.newPassword}
101
+ onChange={handleChange}
102
+ required
103
+ minLength={6}
104
+ />
105
+ </div>
106
+
107
+ <div className="mb-6">
108
+ <label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="confirmPassword">
109
+ {t('auth.confirmPassword')}
110
+ </label>
111
+ <input
112
+ type="password"
113
+ id="confirmPassword"
114
+ name="confirmPassword"
115
+ className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
116
+ value={confirmPassword}
117
+ onChange={handleChange}
118
+ required
119
+ minLength={6}
120
+ />
121
+ </div>
122
+
123
+ <div className="flex justify-end space-x-2">
124
+ {onCancel && (
125
+ <button
126
+ type="button"
127
+ onClick={onCancel}
128
+ disabled={isLoading}
129
+ className="py-2 px-4 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
130
+ >
131
+ {t('common.cancel')}
132
+ </button>
133
+ )}
134
+ <button
135
+ type="submit"
136
+ disabled={isLoading}
137
+ className="py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
138
+ >
139
+ {isLoading ? (
140
+ <span className="flex items-center">
141
+ <svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
142
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
143
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
144
+ </svg>
145
+ {t('common.save')}
146
+ </span>
147
+ ) : (
148
+ t('common.save')
149
+ )}
150
+ </button>
151
+ </div>
152
+ </form>
153
+ )}
154
+ </div>
155
+ );
156
+ };
157
+
158
+ export default ChangePasswordForm;
@@ -0,0 +1,149 @@
1
+ import { useState, useEffect } from 'react'
2
+ import { useTranslation } from 'react-i18next'
3
+ import { Group, GroupFormData, Server } from '@/types'
4
+ import { useGroupData } from '@/hooks/useGroupData'
5
+ import { useServerData } from '@/hooks/useServerData'
6
+ import { ToggleGroup } from './ui/ToggleGroup'
7
+
8
+ interface EditGroupFormProps {
9
+ group: Group
10
+ onEdit: () => void
11
+ onCancel: () => void
12
+ }
13
+
14
+ const EditGroupForm = ({ group, onEdit, onCancel }: EditGroupFormProps) => {
15
+ const { t } = useTranslation()
16
+ const { updateGroup } = useGroupData()
17
+ const { servers } = useServerData()
18
+ const [availableServers, setAvailableServers] = useState<Server[]>([])
19
+ const [error, setError] = useState<string | null>(null)
20
+ const [isSubmitting, setIsSubmitting] = useState(false)
21
+
22
+ const [formData, setFormData] = useState<GroupFormData>({
23
+ name: group.name,
24
+ description: group.description || '',
25
+ servers: group.servers || []
26
+ })
27
+
28
+ useEffect(() => {
29
+ // Filter available servers (enabled only)
30
+ setAvailableServers(servers.filter(server => server.enabled !== false))
31
+ }, [servers])
32
+
33
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
34
+ const { name, value } = e.target
35
+ setFormData(prev => ({
36
+ ...prev,
37
+ [name]: value
38
+ }))
39
+ }
40
+
41
+ const handleServerToggle = (serverName: string) => {
42
+ setFormData(prev => {
43
+ const isSelected = prev.servers.includes(serverName)
44
+ return {
45
+ ...prev,
46
+ servers: isSelected
47
+ ? prev.servers.filter(name => name !== serverName)
48
+ : [...prev.servers, serverName]
49
+ }
50
+ })
51
+ }
52
+
53
+ const handleSubmit = async (e: React.FormEvent) => {
54
+ e.preventDefault()
55
+ setIsSubmitting(true)
56
+ setError(null)
57
+
58
+ try {
59
+ if (!formData.name.trim()) {
60
+ setError(t('groups.nameRequired'))
61
+ setIsSubmitting(false)
62
+ return
63
+ }
64
+
65
+ const result = await updateGroup(group.id, {
66
+ name: formData.name,
67
+ description: formData.description,
68
+ servers: formData.servers
69
+ })
70
+
71
+ if (!result) {
72
+ setError(t('groups.updateError'))
73
+ setIsSubmitting(false)
74
+ return
75
+ }
76
+
77
+ onEdit()
78
+ } catch (err) {
79
+ setError(err instanceof Error ? err.message : String(err))
80
+ setIsSubmitting(false)
81
+ }
82
+ }
83
+
84
+ return (
85
+ <div className="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
86
+ <div className="bg-white rounded-lg shadow-lg max-w-md w-full">
87
+ <div className="p-6">
88
+ <h2 className="text-xl font-semibold text-gray-800 mb-4">{t('groups.edit')}</h2>
89
+
90
+ {error && (
91
+ <div className="mb-4 p-3 bg-red-100 text-red-700 rounded">
92
+ {error}
93
+ </div>
94
+ )}
95
+
96
+ <form onSubmit={handleSubmit}>
97
+ <div className="mb-4">
98
+ <label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="name">
99
+ {t('groups.name')} *
100
+ </label>
101
+ <input
102
+ type="text"
103
+ id="name"
104
+ name="name"
105
+ value={formData.name}
106
+ onChange={handleChange}
107
+ className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
108
+ placeholder={t('groups.namePlaceholder')}
109
+ required
110
+ />
111
+ </div>
112
+
113
+ <ToggleGroup
114
+ className="mb-6"
115
+ label={t('groups.servers')}
116
+ noOptionsText={t('groups.noServerOptions')}
117
+ values={formData.servers}
118
+ options={availableServers.map(server => ({
119
+ value: server.name,
120
+ label: server.name
121
+ }))}
122
+ onChange={(servers) => setFormData(prev => ({ ...prev, servers }))}
123
+ />
124
+
125
+ <div className="flex justify-end space-x-3">
126
+ <button
127
+ type="button"
128
+ onClick={onCancel}
129
+ className="px-4 py-2 text-gray-600 hover:text-gray-800"
130
+ disabled={isSubmitting}
131
+ >
132
+ {t('common.cancel')}
133
+ </button>
134
+ <button
135
+ type="submit"
136
+ className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50"
137
+ disabled={isSubmitting}
138
+ >
139
+ {isSubmitting ? t('common.submitting') : t('common.save')}
140
+ </button>
141
+ </div>
142
+ </form>
143
+ </div>
144
+ </div>
145
+ </div>
146
+ )
147
+ }
148
+
149
+ export default EditGroupForm
@@ -0,0 +1,76 @@
1
+ import { useState } from 'react'
2
+ import { useTranslation } from 'react-i18next'
3
+ import { Server } from '@/types'
4
+ import ServerForm from './ServerForm'
5
+
6
+ interface EditServerFormProps {
7
+ server: Server
8
+ onEdit: () => void
9
+ onCancel: () => void
10
+ }
11
+
12
+ const EditServerForm = ({ server, onEdit, onCancel }: EditServerFormProps) => {
13
+ const { t } = useTranslation()
14
+ const [error, setError] = useState<string | null>(null)
15
+
16
+ const handleSubmit = async (payload: any) => {
17
+ try {
18
+ setError(null)
19
+ const token = localStorage.getItem('mcphub_token');
20
+ const response = await fetch(`/api/servers/${server.name}`, {
21
+ method: 'PUT',
22
+ headers: {
23
+ 'Content-Type': 'application/json',
24
+ 'x-auth-token': token || ''
25
+ },
26
+ body: JSON.stringify(payload),
27
+ })
28
+
29
+ const result = await response.json()
30
+
31
+ if (!response.ok) {
32
+ // Use specific error message from the response if available
33
+ if (result && result.message) {
34
+ setError(result.message)
35
+ } else if (response.status === 404) {
36
+ setError(t('server.notFound', { serverName: server.name }))
37
+ } else if (response.status === 400) {
38
+ setError(t('server.invalidData'))
39
+ } else {
40
+ setError(t('server.updateError', { serverName: server.name }))
41
+ }
42
+ return
43
+ }
44
+
45
+ onEdit()
46
+ } catch (err) {
47
+ console.error('Error updating server:', err)
48
+
49
+ // Use friendly error messages based on error type
50
+ if (!navigator.onLine) {
51
+ setError(t('errors.network'))
52
+ } else if (err instanceof TypeError && (
53
+ err.message.includes('NetworkError') ||
54
+ err.message.includes('Failed to fetch')
55
+ )) {
56
+ setError(t('errors.serverConnection'))
57
+ } else {
58
+ setError(t('errors.serverUpdate', { serverName: server.name }))
59
+ }
60
+ }
61
+ }
62
+
63
+ return (
64
+ <div className="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
65
+ <ServerForm
66
+ onSubmit={handleSubmit}
67
+ onCancel={onCancel}
68
+ initialData={server}
69
+ modalTitle={t('server.editTitle', { serverName: server.name })}
70
+ formError={error}
71
+ />
72
+ </div>
73
+ )
74
+ }
75
+
76
+ export default EditServerForm