@samanhappy/mcphub 0.0.7 → 0.0.9

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 (96) hide show
  1. package/package.json +6 -3
  2. package/.env.example +0 -2
  3. package/.eslintrc.json +0 -25
  4. package/.github/workflows/build.yml +0 -51
  5. package/.github/workflows/release.yml +0 -19
  6. package/.prettierrc +0 -7
  7. package/Dockerfile +0 -51
  8. package/assets/amap-edit.png +0 -0
  9. package/assets/amap-result.png +0 -0
  10. package/assets/cherry-mcp.png +0 -0
  11. package/assets/cursor-mcp.png +0 -0
  12. package/assets/cursor-query.png +0 -0
  13. package/assets/cursor-tools.png +0 -0
  14. package/assets/dashboard.png +0 -0
  15. package/assets/dashboard.zh.png +0 -0
  16. package/assets/group.png +0 -0
  17. package/assets/group.zh.png +0 -0
  18. package/assets/market.zh.png +0 -0
  19. package/assets/wegroup.jpg +0 -0
  20. package/assets/wegroup.png +0 -0
  21. package/assets/wexin.png +0 -0
  22. package/doc/intro.md +0 -73
  23. package/doc/intro2.md +0 -232
  24. package/entrypoint.sh +0 -10
  25. package/frontend/favicon.ico +0 -0
  26. package/frontend/index.html +0 -13
  27. package/frontend/postcss.config.js +0 -6
  28. package/frontend/src/App.tsx +0 -44
  29. package/frontend/src/components/AddGroupForm.tsx +0 -132
  30. package/frontend/src/components/AddServerForm.tsx +0 -90
  31. package/frontend/src/components/ChangePasswordForm.tsx +0 -158
  32. package/frontend/src/components/EditGroupForm.tsx +0 -149
  33. package/frontend/src/components/EditServerForm.tsx +0 -76
  34. package/frontend/src/components/GroupCard.tsx +0 -143
  35. package/frontend/src/components/MarketServerCard.tsx +0 -153
  36. package/frontend/src/components/MarketServerDetail.tsx +0 -297
  37. package/frontend/src/components/ProtectedRoute.tsx +0 -27
  38. package/frontend/src/components/ServerCard.tsx +0 -230
  39. package/frontend/src/components/ServerForm.tsx +0 -276
  40. package/frontend/src/components/icons/LucideIcons.tsx +0 -14
  41. package/frontend/src/components/layout/Content.tsx +0 -17
  42. package/frontend/src/components/layout/Header.tsx +0 -61
  43. package/frontend/src/components/layout/Sidebar.tsx +0 -98
  44. package/frontend/src/components/ui/Badge.tsx +0 -33
  45. package/frontend/src/components/ui/Button.tsx +0 -0
  46. package/frontend/src/components/ui/DeleteDialog.tsx +0 -48
  47. package/frontend/src/components/ui/Pagination.tsx +0 -128
  48. package/frontend/src/components/ui/Toast.tsx +0 -96
  49. package/frontend/src/components/ui/ToggleGroup.tsx +0 -134
  50. package/frontend/src/components/ui/ToolCard.tsx +0 -38
  51. package/frontend/src/contexts/AuthContext.tsx +0 -159
  52. package/frontend/src/contexts/ToastContext.tsx +0 -60
  53. package/frontend/src/hooks/useGroupData.ts +0 -232
  54. package/frontend/src/hooks/useMarketData.ts +0 -410
  55. package/frontend/src/hooks/useServerData.ts +0 -306
  56. package/frontend/src/hooks/useSettingsData.ts +0 -131
  57. package/frontend/src/i18n.ts +0 -42
  58. package/frontend/src/index.css +0 -20
  59. package/frontend/src/layouts/MainLayout.tsx +0 -33
  60. package/frontend/src/locales/en.json +0 -214
  61. package/frontend/src/locales/zh.json +0 -214
  62. package/frontend/src/main.tsx +0 -12
  63. package/frontend/src/pages/Dashboard.tsx +0 -206
  64. package/frontend/src/pages/GroupsPage.tsx +0 -116
  65. package/frontend/src/pages/LoginPage.tsx +0 -104
  66. package/frontend/src/pages/MarketPage.tsx +0 -356
  67. package/frontend/src/pages/ServersPage.tsx +0 -144
  68. package/frontend/src/pages/SettingsPage.tsx +0 -149
  69. package/frontend/src/services/authService.ts +0 -141
  70. package/frontend/src/types/index.ts +0 -160
  71. package/frontend/src/utils/cn.ts +0 -10
  72. package/frontend/tsconfig.json +0 -31
  73. package/frontend/tsconfig.node.json +0 -10
  74. package/frontend/vite.config.ts +0 -26
  75. package/googled76ca578b6543fbc.html +0 -1
  76. package/jest.config.js +0 -10
  77. package/mcp_settings.json +0 -45
  78. package/servers.json +0 -74722
  79. package/src/config/index.ts +0 -46
  80. package/src/controllers/authController.ts +0 -179
  81. package/src/controllers/groupController.ts +0 -341
  82. package/src/controllers/marketController.ts +0 -154
  83. package/src/controllers/serverController.ts +0 -303
  84. package/src/index.ts +0 -18
  85. package/src/middlewares/auth.ts +0 -28
  86. package/src/middlewares/index.ts +0 -43
  87. package/src/models/User.ts +0 -103
  88. package/src/routes/index.ts +0 -96
  89. package/src/server.ts +0 -72
  90. package/src/services/groupService.ts +0 -232
  91. package/src/services/marketService.ts +0 -116
  92. package/src/services/mcpService.ts +0 -385
  93. package/src/services/sseService.ts +0 -119
  94. package/src/types/index.ts +0 -129
  95. package/src/utils/migration.ts +0 -52
  96. package/tsconfig.json +0 -17
@@ -1,356 +0,0 @@
1
- import React, { useState, useEffect } from 'react';
2
- import { useTranslation } from 'react-i18next';
3
- import { useNavigate, useParams, useLocation } from 'react-router-dom';
4
- import { MarketServer } from '@/types';
5
- import { useMarketData } from '@/hooks/useMarketData';
6
- import { useToast } from '@/contexts/ToastContext';
7
- import MarketServerCard from '@/components/MarketServerCard';
8
- import MarketServerDetail from '@/components/MarketServerDetail';
9
- import Pagination from '@/components/ui/Pagination';
10
-
11
- const MarketPage: React.FC = () => {
12
- const { t } = useTranslation();
13
- const navigate = useNavigate();
14
- const location = useLocation();
15
- const { serverName } = useParams<{ serverName?: string }>();
16
- const { showToast } = useToast();
17
-
18
- const {
19
- servers,
20
- allServers,
21
- categories,
22
- tags,
23
- loading,
24
- error,
25
- setError,
26
- searchServers,
27
- filterByCategory,
28
- filterByTag,
29
- selectedCategory,
30
- selectedTag,
31
- installServer,
32
- fetchServerByName,
33
- isServerInstalled,
34
- // Pagination
35
- currentPage,
36
- totalPages,
37
- changePage,
38
- serversPerPage,
39
- changeServersPerPage
40
- } = useMarketData();
41
-
42
- const [selectedServer, setSelectedServer] = useState<MarketServer | null>(null);
43
- const [searchQuery, setSearchQuery] = useState('');
44
- const [installing, setInstalling] = useState(false);
45
- const [showTags, setShowTags] = useState(false);
46
-
47
- // Load server details if a server name is in the URL
48
- useEffect(() => {
49
- const loadServerDetails = async () => {
50
- if (serverName) {
51
- const server = await fetchServerByName(serverName);
52
- if (server) {
53
- setSelectedServer(server);
54
- } else {
55
- // If server not found, navigate back to market page
56
- navigate('/market');
57
- }
58
- } else {
59
- setSelectedServer(null);
60
- }
61
- };
62
-
63
- loadServerDetails();
64
- }, [serverName, fetchServerByName, navigate]);
65
-
66
- const handleSearch = (e: React.FormEvent) => {
67
- e.preventDefault();
68
- searchServers(searchQuery);
69
- };
70
-
71
- const handleCategoryClick = (category: string) => {
72
- filterByCategory(category);
73
- };
74
-
75
- const handleTagClick = (tag: string) => {
76
- filterByTag(tag);
77
- };
78
-
79
- const handleClearFilters = () => {
80
- setSearchQuery('');
81
- filterByCategory('');
82
- filterByTag('');
83
- };
84
-
85
- const handleServerClick = (server: MarketServer) => {
86
- navigate(`/market/${server.name}`);
87
- };
88
-
89
- const handleBackToList = () => {
90
- navigate('/market');
91
- };
92
-
93
- const handleInstall = async (server: MarketServer) => {
94
- try {
95
- setInstalling(true);
96
- const success = await installServer(server);
97
- if (success) {
98
- // Show success message using toast instead of alert
99
- showToast(t('market.installSuccess', { serverName: server.display_name }), 'success');
100
- }
101
- } finally {
102
- setInstalling(false);
103
- }
104
- };
105
-
106
- const handlePageChange = (page: number) => {
107
- changePage(page);
108
- // Scroll to top of page when changing pages
109
- window.scrollTo({ top: 0, behavior: 'smooth' });
110
- };
111
-
112
- const handleChangeItemsPerPage = (e: React.ChangeEvent<HTMLSelectElement>) => {
113
- const newValue = parseInt(e.target.value, 10);
114
- changeServersPerPage(newValue);
115
- };
116
-
117
- const toggleTagsVisibility = () => {
118
- setShowTags(!showTags);
119
- };
120
-
121
- // Render detailed view if a server is selected
122
- if (selectedServer) {
123
- return (
124
- <MarketServerDetail
125
- server={selectedServer}
126
- onBack={handleBackToList}
127
- onInstall={handleInstall}
128
- installing={installing}
129
- isInstalled={isServerInstalled(selectedServer.name)}
130
- />
131
- );
132
- }
133
-
134
- return (
135
- <div>
136
- <div className="flex justify-between items-center mb-8">
137
- <div>
138
- <h1 className="text-2xl font-bold text-gray-900 flex items-center">
139
- {t('market.title')}
140
- <span className="text-sm text-gray-500 font-normal ml-2">{t('pages.market.title').split(' - ')[1]}</span>
141
- </h1>
142
- </div>
143
- </div>
144
-
145
- {error && (
146
- <div className="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mb-6">
147
- <div className="flex items-center justify-between">
148
- <p>{error}</p>
149
- <button
150
- onClick={() => setError(null)}
151
- className="text-red-700 hover:text-red-900"
152
- >
153
- <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
154
- <path fillRule="evenodd" d="M4.293 4.293a1 1 011.414 0L10 8.586l4.293-4.293a1 1 01.414 1.414L11.414 10l4.293 4.293a1 1 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 01-1.414-1.414L8.586 10 4.293 5.707a1 1 010-1.414z" clipRule="evenodd" />
155
- </svg>
156
- </button>
157
- </div>
158
- </div>
159
- )}
160
-
161
- {/* Search bar at the top */}
162
- <div className="bg-white shadow rounded-lg p-6 mb-6">
163
- <form onSubmit={handleSearch} className="flex space-x-4 mb-0">
164
- <div className="flex-grow">
165
- <input
166
- type="text"
167
- value={searchQuery}
168
- onChange={(e) => setSearchQuery(e.target.value)}
169
- placeholder={t('market.searchPlaceholder')}
170
- className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
171
- />
172
- </div>
173
- <button
174
- type="submit"
175
- className="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded"
176
- >
177
- {t('market.search')}
178
- </button>
179
- {(searchQuery || selectedCategory || selectedTag) && (
180
- <button
181
- type="button"
182
- onClick={handleClearFilters}
183
- className="border border-gray-300 text-gray-700 font-medium py-2 px-4 rounded hover:bg-gray-50"
184
- >
185
- {t('market.clearFilters')}
186
- </button>
187
- )}
188
- </form>
189
- </div>
190
-
191
- <div className="flex flex-col md:flex-row gap-6">
192
- {/* Left sidebar for filters (without search) */}
193
- <div className="md:w-48 flex-shrink-0">
194
- <div className="bg-white shadow rounded-lg p-4 mb-6 sticky top-4">
195
- {/* Categories */}
196
- {categories.length > 0 ? (
197
- <div className="mb-6">
198
- <div className="flex justify-between items-center mb-3">
199
- <h3 className="font-medium text-gray-900">{t('market.categories')}</h3>
200
- {selectedCategory && (
201
- <span className="text-xs text-blue-600 cursor-pointer hover:underline" onClick={() => filterByCategory('')}>
202
- {t('market.clearCategoryFilter')}
203
- </span>
204
- )}
205
- </div>
206
- <div className="flex flex-col gap-2">
207
- {categories.map((category) => (
208
- <button
209
- key={category}
210
- onClick={() => handleCategoryClick(category)}
211
- className={`px-3 py-2 rounded text-sm text-left ${selectedCategory === category
212
- ? 'bg-blue-100 text-blue-800 font-medium'
213
- : 'bg-gray-100 text-gray-800 hover:bg-gray-200'
214
- }`}
215
- >
216
- {category}
217
- </button>
218
- ))}
219
- </div>
220
- </div>
221
- ) : loading ? (
222
- <div className="mb-6">
223
- <div className="mb-3">
224
- <h3 className="font-medium text-gray-900">{t('market.categories')}</h3>
225
- </div>
226
- <div className="flex flex-col gap-2 items-center py-4">
227
- <svg className="animate-spin h-6 w-6 text-blue-500 mb-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
228
- <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
229
- <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>
230
- </svg>
231
- <p className="text-sm text-gray-600">{t('app.loading')}</p>
232
- </div>
233
- </div>
234
- ) : (
235
- <div className="mb-6">
236
- <div className="mb-3">
237
- <h3 className="font-medium text-gray-900">{t('market.categories')}</h3>
238
- </div>
239
- <p className="text-sm text-gray-600 py-2">{t('market.noCategories')}</p>
240
- </div>
241
- )}
242
-
243
- {/* Tags */}
244
- {/* {tags.length > 0 && (
245
- <div className="mb-4">
246
- <div className="flex justify-between items-center mb-3">
247
- <div className="flex items-center">
248
- <h3 className="font-medium text-gray-900">{t('market.tags')}</h3>
249
- <button
250
- onClick={toggleTagsVisibility}
251
- className="ml-2 p-1 text-gray-600 hover:text-blue-600 hover:bg-gray-100 rounded-full"
252
- aria-label={showTags ? t('market.hideTags') : t('market.showTags')}
253
- >
254
- <svg xmlns="http://www.w3.org/2000/svg" className={`h-5 w-5 transition-transform ${showTags ? 'rotate-180' : ''}`} viewBox="0 0 20 20" fill="currentColor">
255
- <path fillRule="evenodd" d="M5.293 7.293a1 1 011.414 0L10 10.586l3.293-3.293a1 1 011.414 1.414l-4 4a1 1 01-1.414 0l-4-4a1 1 010-1.414z" clipRule="evenodd" />
256
- </svg>
257
- </button>
258
- </div>
259
- {selectedTag && (
260
- <span className="text-xs text-blue-600 cursor-pointer hover:underline" onClick={() => filterByTag('')}>
261
- {t('market.clearTagFilter')}
262
- </span>
263
- )}
264
- </div>
265
- {showTags && (
266
- <div className="flex flex-wrap gap-2 max-h-48 overflow-y-auto pr-2">
267
- {tags.map((tag) => (
268
- <button
269
- key={tag}
270
- onClick={() => handleTagClick(tag)}
271
- className={`px-2 py-1 rounded text-xs ${selectedTag === tag
272
- ? 'bg-green-100 text-green-800 font-medium'
273
- : 'bg-gray-100 text-gray-700 hover:bg-gray-200'
274
- }`}
275
- >
276
- #{tag}
277
- </button>
278
- ))}
279
- </div>
280
- )}
281
- </div>
282
- )} */}
283
- </div>
284
- </div>
285
-
286
- {/* Main content area */}
287
- <div className="flex-grow">
288
- {loading ? (
289
- <div className="bg-white shadow rounded-lg p-6 flex items-center justify-center">
290
- <div className="flex flex-col items-center">
291
- <svg className="animate-spin h-10 w-10 text-blue-500 mb-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
292
- <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
293
- <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>
294
- </svg>
295
- <p className="text-gray-600">{t('app.loading')}</p>
296
- </div>
297
- </div>
298
- ) : servers.length === 0 ? (
299
- <div className="bg-white shadow rounded-lg p-6">
300
- <p className="text-gray-600">{t('market.noServers')}</p>
301
- </div>
302
- ) : (
303
- <>
304
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3 gap-6">
305
- {servers.map((server, index) => (
306
- <MarketServerCard
307
- key={index}
308
- server={server}
309
- onClick={handleServerClick}
310
- />
311
- ))}
312
- </div>
313
-
314
- <div className="flex justify-between items-center mb-4">
315
- <div className="text-sm text-gray-500">
316
- {t('market.showing', {
317
- from: (currentPage - 1) * serversPerPage + 1,
318
- to: Math.min(currentPage * serversPerPage, allServers.length),
319
- total: allServers.length
320
- })}
321
- </div>
322
- <Pagination
323
- currentPage={currentPage}
324
- totalPages={totalPages}
325
- onPageChange={handlePageChange}
326
- />
327
- <div className="flex items-center space-x-2">
328
- <label htmlFor="perPage" className="text-sm text-gray-600">
329
- {t('market.perPage')}:
330
- </label>
331
- <select
332
- id="perPage"
333
- value={serversPerPage}
334
- onChange={handleChangeItemsPerPage}
335
- className="border rounded p-1 text-sm"
336
- >
337
- <option value="6">6</option>
338
- <option value="9">9</option>
339
- <option value="12">12</option>
340
- <option value="24">24</option>
341
- </select>
342
- </div>
343
- </div>
344
-
345
- <div className="mt-6">
346
-
347
- </div>
348
- </>
349
- )}
350
- </div>
351
- </div>
352
- </div>
353
- );
354
- };
355
-
356
- export default MarketPage;
@@ -1,144 +0,0 @@
1
- import React, { useState } from 'react';
2
- import { useTranslation } from 'react-i18next';
3
- import { useNavigate } from 'react-router-dom';
4
- import { Server } from '@/types';
5
- import ServerCard from '@/components/ServerCard';
6
- import AddServerForm from '@/components/AddServerForm';
7
- import EditServerForm from '@/components/EditServerForm';
8
- import { useServerData } from '@/hooks/useServerData';
9
-
10
- const ServersPage: React.FC = () => {
11
- const { t } = useTranslation();
12
- const navigate = useNavigate();
13
- const {
14
- servers,
15
- error,
16
- setError,
17
- isLoading,
18
- handleServerAdd,
19
- handleServerEdit,
20
- handleServerRemove,
21
- handleServerToggle,
22
- triggerRefresh
23
- } = useServerData();
24
- const [editingServer, setEditingServer] = useState<Server | null>(null);
25
- const [isRefreshing, setIsRefreshing] = useState(false);
26
-
27
- const handleEditClick = async (server: Server) => {
28
- const fullServerData = await handleServerEdit(server);
29
- if (fullServerData) {
30
- setEditingServer(fullServerData);
31
- }
32
- };
33
-
34
- const handleEditComplete = () => {
35
- setEditingServer(null);
36
- triggerRefresh();
37
- };
38
-
39
- const handleRefresh = async () => {
40
- setIsRefreshing(true);
41
- try {
42
- triggerRefresh();
43
- // Add a slight delay to make the spinner visible
44
- await new Promise(resolve => setTimeout(resolve, 500));
45
- } finally {
46
- setIsRefreshing(false);
47
- }
48
- };
49
-
50
- return (
51
- <div>
52
- <div className="flex justify-between items-center mb-8">
53
- <h1 className="text-2xl font-bold text-gray-900">{t('pages.servers.title')}</h1>
54
- <div className="flex space-x-4">
55
- <button
56
- onClick={() => navigate('/market')}
57
- className="px-4 py-2 bg-emerald-100 text-emerald-800 rounded hover:bg-emerald-200 flex items-center"
58
- >
59
- <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 mr-2" viewBox="0 0 20 20" fill="currentColor">
60
- <path d="M3 1a1 1 0 000 2h1.22l.305 1.222a.997.997 0 00.01.042l1.358 5.43-.893.892C3.74 11.846 4.632 14 6.414 14H15a1 1 0 000-2H6.414l1-1H14a1 1 0 00.894-.553l3-6A1 1 0 0017 3H6.28l-.31-1.243A1 1 0 005 1H3z" />
61
- </svg>
62
- {t('nav.market')}
63
- </button>
64
- <AddServerForm onAdd={handleServerAdd} />
65
- <button
66
- onClick={handleRefresh}
67
- disabled={isRefreshing}
68
- className={`px-4 py-2 bg-blue-100 text-blue-800 rounded hover:bg-blue-200 flex items-center ${isRefreshing ? 'opacity-70 cursor-not-allowed' : ''}`}
69
- >
70
- {isRefreshing ? (
71
- <svg className="animate-spin h-4 w-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
72
- <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
73
- <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>
74
- </svg>
75
- ) : (
76
- <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 mr-2" viewBox="0 0 20 20" fill="currentColor">
77
- <path fillRule="evenodd" d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z" clipRule="evenodd" />
78
- </svg>
79
- )}
80
- {t('common.refresh')}
81
- </button>
82
- </div>
83
- </div>
84
-
85
- {error && (
86
- <div className="mb-6 bg-red-50 border-l-4 border-red-500 p-4 rounded shadow-sm">
87
- <div className="flex items-center justify-between">
88
- <div>
89
- <h3 className="text-red-600 text-lg font-medium">{t('app.error')}</h3>
90
- <p className="text-gray-600 mt-1">{error}</p>
91
- </div>
92
- <button
93
- onClick={() => setError(null)}
94
- className="ml-4 text-gray-500 hover:text-gray-700"
95
- aria-label={t('app.closeButton')}
96
- >
97
- <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
98
- <path fillRule="evenodd" d="M4.293 4.293a1 1 011.414 0L10 8.586l4.293-4.293a1 1 111.414 1.414L11.414 10l4.293 4.293a1 1 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 01-1.414-1.414L8.586 10 4.293 5.707a1 1 010-1.414z" clipRule="evenodd" />
99
- </svg>
100
- </button>
101
- </div>
102
- </div>
103
- )}
104
-
105
- {isLoading ? (
106
- <div className="bg-white shadow rounded-lg p-6 flex items-center justify-center">
107
- <div className="flex flex-col items-center">
108
- <svg className="animate-spin h-10 w-10 text-blue-500 mb-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
109
- <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
110
- <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>
111
- </svg>
112
- <p className="text-gray-600">{t('app.loading')}</p>
113
- </div>
114
- </div>
115
- ) : servers.length === 0 ? (
116
- <div className="bg-white shadow rounded-lg p-6">
117
- <p className="text-gray-600">{t('app.noServers')}</p>
118
- </div>
119
- ) : (
120
- <div className="space-y-6">
121
- {servers.map((server, index) => (
122
- <ServerCard
123
- key={index}
124
- server={server}
125
- onRemove={handleServerRemove}
126
- onEdit={handleEditClick}
127
- onToggle={handleServerToggle}
128
- />
129
- ))}
130
- </div>
131
- )}
132
-
133
- {editingServer && (
134
- <EditServerForm
135
- server={editingServer}
136
- onEdit={handleEditComplete}
137
- onCancel={() => setEditingServer(null)}
138
- />
139
- )}
140
- </div>
141
- );
142
- };
143
-
144
- export default ServersPage;
@@ -1,149 +0,0 @@
1
- import React, { useState, useEffect } from 'react';
2
- import { useTranslation } from 'react-i18next';
3
- import { useNavigate } from 'react-router-dom';
4
- import ChangePasswordForm from '@/components/ChangePasswordForm';
5
- import { Switch } from '@/components/ui/ToggleGroup';
6
- import { useSettingsData } from '@/hooks/useSettingsData';
7
- import { useToast } from '@/contexts/ToastContext';
8
-
9
- const SettingsPage: React.FC = () => {
10
- const { t, i18n } = useTranslation();
11
- const navigate = useNavigate();
12
- const { showToast } = useToast();
13
- const [currentLanguage, setCurrentLanguage] = useState(i18n.language);
14
-
15
- // Update current language when it changes
16
- useEffect(() => {
17
- setCurrentLanguage(i18n.language);
18
- }, [i18n.language]);
19
-
20
- const {
21
- routingConfig,
22
- loading,
23
- updateRoutingConfig
24
- } = useSettingsData();
25
-
26
- const [sectionsVisible, setSectionsVisible] = useState({
27
- routingConfig: false,
28
- password: false
29
- });
30
-
31
- const toggleSection = (section: 'routingConfig' | 'password') => {
32
- setSectionsVisible(prev => ({
33
- ...prev,
34
- [section]: !prev[section]
35
- }));
36
- };
37
-
38
- const handleRoutingConfigChange = async (key: 'enableGlobalRoute' | 'enableGroupNameRoute', value: boolean) => {
39
- await updateRoutingConfig(key, value);
40
- };
41
-
42
- const handlePasswordChangeSuccess = () => {
43
- setTimeout(() => {
44
- navigate('/');
45
- }, 2000);
46
- };
47
-
48
- const handleLanguageChange = (lang: string) => {
49
- localStorage.setItem('i18nextLng', lang);
50
- window.location.reload();
51
- };
52
-
53
- return (
54
- <div className="max-w-4xl mx-auto">
55
- <h1 className="text-2xl font-bold text-gray-900 mb-8">{t('pages.settings.title')}</h1>
56
-
57
- {/* Language Settings */}
58
- <div className="bg-white shadow rounded-lg p-6 mb-6">
59
- <div className="flex items-center justify-between">
60
- <h2 className="text-xl font-semibold text-gray-800">{t('pages.settings.language')}</h2>
61
- <div className="flex space-x-3">
62
- <button
63
- className={`px-3 py-1.5 rounded-md transition-colors text-sm ${
64
- currentLanguage.startsWith('en')
65
- ? 'bg-blue-500 text-white'
66
- : 'bg-blue-100 text-blue-800 hover:bg-blue-200'
67
- }`}
68
- onClick={() => handleLanguageChange('en')}
69
- >
70
- English
71
- </button>
72
- <button
73
- className={`px-3 py-1.5 rounded-md transition-colors text-sm ${
74
- currentLanguage.startsWith('zh')
75
- ? 'bg-blue-500 text-white'
76
- : 'bg-blue-100 text-blue-800 hover:bg-blue-200'
77
- }`}
78
- onClick={() => handleLanguageChange('zh')}
79
- >
80
- 中文
81
- </button>
82
- </div>
83
- </div>
84
- </div>
85
-
86
- {/* Route Configuration Settings */}
87
- <div className="bg-white shadow rounded-lg p-6 mb-6">
88
- <div
89
- className="flex justify-between items-center cursor-pointer"
90
- onClick={() => toggleSection('routingConfig')}
91
- >
92
- <h2 className="text-xl font-semibold text-gray-800">{t('pages.settings.routeConfig')}</h2>
93
- <span className="text-gray-500">
94
- {sectionsVisible.routingConfig ? '▼' : '►'}
95
- </span>
96
- </div>
97
-
98
- {sectionsVisible.routingConfig && (
99
- <div className="space-y-4 mt-4">
100
- <div className="flex items-center justify-between p-3 bg-gray-50 rounded-md">
101
- <div>
102
- <h3 className="font-medium text-gray-700">{t('settings.enableGlobalRoute')}</h3>
103
- <p className="text-sm text-gray-500">{t('settings.enableGlobalRouteDescription')}</p>
104
- </div>
105
- <Switch
106
- disabled={loading}
107
- checked={routingConfig.enableGlobalRoute}
108
- onCheckedChange={(checked) => handleRoutingConfigChange('enableGlobalRoute', checked)}
109
- />
110
- </div>
111
-
112
- <div className="flex items-center justify-between p-3 bg-gray-50 rounded-md">
113
- <div>
114
- <h3 className="font-medium text-gray-700">{t('settings.enableGroupNameRoute')}</h3>
115
- <p className="text-sm text-gray-500">{t('settings.enableGroupNameRouteDescription')}</p>
116
- </div>
117
- <Switch
118
- disabled={loading}
119
- checked={routingConfig.enableGroupNameRoute}
120
- onCheckedChange={(checked) => handleRoutingConfigChange('enableGroupNameRoute', checked)}
121
- />
122
- </div>
123
- </div>
124
- )}
125
- </div>
126
-
127
- {/* Change Password */}
128
- <div className="bg-white shadow rounded-lg p-6 mb-6">
129
- <div
130
- className="flex justify-between items-center cursor-pointer"
131
- onClick={() => toggleSection('password')}
132
- >
133
- <h2 className="text-xl font-semibold text-gray-800">{t('auth.changePassword')}</h2>
134
- <span className="text-gray-500">
135
- {sectionsVisible.password ? '▼' : '►'}
136
- </span>
137
- </div>
138
-
139
- {sectionsVisible.password && (
140
- <div className="max-w-lg mt-4">
141
- <ChangePasswordForm onSuccess={handlePasswordChangeSuccess} />
142
- </div>
143
- )}
144
- </div>
145
- </div>
146
- );
147
- };
148
-
149
- export default SettingsPage;