@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.
- package/package.json +6 -3
- package/.env.example +0 -2
- package/.eslintrc.json +0 -25
- package/.github/workflows/build.yml +0 -51
- package/.github/workflows/release.yml +0 -19
- package/.prettierrc +0 -7
- package/Dockerfile +0 -51
- package/assets/amap-edit.png +0 -0
- package/assets/amap-result.png +0 -0
- package/assets/cherry-mcp.png +0 -0
- package/assets/cursor-mcp.png +0 -0
- package/assets/cursor-query.png +0 -0
- package/assets/cursor-tools.png +0 -0
- package/assets/dashboard.png +0 -0
- package/assets/dashboard.zh.png +0 -0
- package/assets/group.png +0 -0
- package/assets/group.zh.png +0 -0
- package/assets/market.zh.png +0 -0
- package/assets/wegroup.jpg +0 -0
- package/assets/wegroup.png +0 -0
- package/assets/wexin.png +0 -0
- package/doc/intro.md +0 -73
- package/doc/intro2.md +0 -232
- package/entrypoint.sh +0 -10
- package/frontend/favicon.ico +0 -0
- package/frontend/index.html +0 -13
- package/frontend/postcss.config.js +0 -6
- package/frontend/src/App.tsx +0 -44
- package/frontend/src/components/AddGroupForm.tsx +0 -132
- package/frontend/src/components/AddServerForm.tsx +0 -90
- package/frontend/src/components/ChangePasswordForm.tsx +0 -158
- package/frontend/src/components/EditGroupForm.tsx +0 -149
- package/frontend/src/components/EditServerForm.tsx +0 -76
- package/frontend/src/components/GroupCard.tsx +0 -143
- package/frontend/src/components/MarketServerCard.tsx +0 -153
- package/frontend/src/components/MarketServerDetail.tsx +0 -297
- package/frontend/src/components/ProtectedRoute.tsx +0 -27
- package/frontend/src/components/ServerCard.tsx +0 -230
- package/frontend/src/components/ServerForm.tsx +0 -276
- package/frontend/src/components/icons/LucideIcons.tsx +0 -14
- package/frontend/src/components/layout/Content.tsx +0 -17
- package/frontend/src/components/layout/Header.tsx +0 -61
- package/frontend/src/components/layout/Sidebar.tsx +0 -98
- package/frontend/src/components/ui/Badge.tsx +0 -33
- package/frontend/src/components/ui/Button.tsx +0 -0
- package/frontend/src/components/ui/DeleteDialog.tsx +0 -48
- package/frontend/src/components/ui/Pagination.tsx +0 -128
- package/frontend/src/components/ui/Toast.tsx +0 -96
- package/frontend/src/components/ui/ToggleGroup.tsx +0 -134
- package/frontend/src/components/ui/ToolCard.tsx +0 -38
- package/frontend/src/contexts/AuthContext.tsx +0 -159
- package/frontend/src/contexts/ToastContext.tsx +0 -60
- package/frontend/src/hooks/useGroupData.ts +0 -232
- package/frontend/src/hooks/useMarketData.ts +0 -410
- package/frontend/src/hooks/useServerData.ts +0 -306
- package/frontend/src/hooks/useSettingsData.ts +0 -131
- package/frontend/src/i18n.ts +0 -42
- package/frontend/src/index.css +0 -20
- package/frontend/src/layouts/MainLayout.tsx +0 -33
- package/frontend/src/locales/en.json +0 -214
- package/frontend/src/locales/zh.json +0 -214
- package/frontend/src/main.tsx +0 -12
- package/frontend/src/pages/Dashboard.tsx +0 -206
- package/frontend/src/pages/GroupsPage.tsx +0 -116
- package/frontend/src/pages/LoginPage.tsx +0 -104
- package/frontend/src/pages/MarketPage.tsx +0 -356
- package/frontend/src/pages/ServersPage.tsx +0 -144
- package/frontend/src/pages/SettingsPage.tsx +0 -149
- package/frontend/src/services/authService.ts +0 -141
- package/frontend/src/types/index.ts +0 -160
- package/frontend/src/utils/cn.ts +0 -10
- package/frontend/tsconfig.json +0 -31
- package/frontend/tsconfig.node.json +0 -10
- package/frontend/vite.config.ts +0 -26
- package/googled76ca578b6543fbc.html +0 -1
- package/jest.config.js +0 -10
- package/mcp_settings.json +0 -45
- package/servers.json +0 -74722
- package/src/config/index.ts +0 -46
- package/src/controllers/authController.ts +0 -179
- package/src/controllers/groupController.ts +0 -341
- package/src/controllers/marketController.ts +0 -154
- package/src/controllers/serverController.ts +0 -303
- package/src/index.ts +0 -18
- package/src/middlewares/auth.ts +0 -28
- package/src/middlewares/index.ts +0 -43
- package/src/models/User.ts +0 -103
- package/src/routes/index.ts +0 -96
- package/src/server.ts +0 -72
- package/src/services/groupService.ts +0 -232
- package/src/services/marketService.ts +0 -116
- package/src/services/mcpService.ts +0 -385
- package/src/services/sseService.ts +0 -119
- package/src/types/index.ts +0 -129
- package/src/utils/migration.ts +0 -52
- 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;
|