create-fullstack-scaffold 0.4.2 → 0.4.3
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 +1 -1
- package/template/package.json +1 -1
- package/template/src/client/App.tsx +3 -1
- package/template/src/client/pages/ContentListPage.tsx +36 -4
- package/template/src/client/pages/PluginsPage.tsx +112 -20
- package/template/src/client/pages/TodoPage.tsx +9 -1
- package/template/src/client/preset-ui-config.ts +79 -2
- package/template/src/server/db/init.ts +389 -0
- package/template/src/server/module-content/routes/topics-routes.ts +250 -0
- package/template/src/server/module-content/services/content-service.ts +1 -1
- package/template/src/server/module-todos/services/todo-service.ts +1 -1
package/package.json
CHANGED
package/template/package.json
CHANGED
|
@@ -19,7 +19,9 @@ export const App: React.FC<{ presetId?: string }> = ({ presetId = 'todo' }) => {
|
|
|
19
19
|
mobileTabs={mobileTabs}
|
|
20
20
|
>
|
|
21
21
|
<Routes>
|
|
22
|
-
|
|
22
|
+
{defaultRoute !== '/' && (
|
|
23
|
+
<Route path="/" element={<Navigate to={defaultRoute} replace />} />
|
|
24
|
+
)}
|
|
23
25
|
{routes
|
|
24
26
|
.filter(
|
|
25
27
|
(r): r is RouteDef & { component: NonNullable<RouteDef['component']> } =>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useState, useEffect, useCallback } from 'react'
|
|
2
2
|
import { Link } from 'react-router-dom'
|
|
3
3
|
import { Helmet } from 'react-helmet-async'
|
|
4
|
-
import type { Content, ContentCategory } from '@shared/modules/content'
|
|
4
|
+
import type { Content, ContentCategory, ContentStatus } from '@shared/modules/content'
|
|
5
5
|
import { apiClient } from '@client/services/apiClient'
|
|
6
6
|
|
|
7
7
|
const CATEGORIES: { value: ContentCategory | ''; label: string }[] = [
|
|
@@ -66,6 +66,15 @@ export const ContentListPage: React.FC = () => {
|
|
|
66
66
|
})
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
const statusConfig = (status: ContentStatus) => {
|
|
70
|
+
const configs = {
|
|
71
|
+
draft: { color: 'bg-yellow-100 text-yellow-700', label: '草稿' },
|
|
72
|
+
published: { color: 'bg-green-100 text-green-700', label: '已发布' },
|
|
73
|
+
archived: { color: 'bg-gray-100 text-gray-700', label: '已归档' },
|
|
74
|
+
}
|
|
75
|
+
return configs[status]
|
|
76
|
+
}
|
|
77
|
+
|
|
69
78
|
return (
|
|
70
79
|
<>
|
|
71
80
|
<Helmet>
|
|
@@ -137,16 +146,39 @@ export const ContentListPage: React.FC = () => {
|
|
|
137
146
|
>
|
|
138
147
|
<div className="flex items-start justify-between gap-4">
|
|
139
148
|
<div className="flex-1">
|
|
140
|
-
<div className="flex items-center gap-2 mb-2">
|
|
149
|
+
<div className="flex items-center gap-2 mb-2 flex-wrap">
|
|
141
150
|
<span className="px-2 py-1 bg-blue-100 text-blue-700 text-xs font-medium rounded">
|
|
142
151
|
{categoryLabel(content.category)}
|
|
143
152
|
</span>
|
|
153
|
+
<span
|
|
154
|
+
className={`px-2 py-1 text-xs font-medium rounded ${
|
|
155
|
+
statusConfig(content.status).color
|
|
156
|
+
}`}
|
|
157
|
+
>
|
|
158
|
+
{statusConfig(content.status).label}
|
|
159
|
+
</span>
|
|
144
160
|
<span className="text-sm text-gray-500">{content.author}</span>
|
|
145
161
|
</div>
|
|
146
162
|
<h2 className="text-xl font-semibold text-gray-900 mb-2">{content.title}</h2>
|
|
147
163
|
<p className="text-gray-600 line-clamp-2">{content.content.slice(0, 150)}...</p>
|
|
148
|
-
|
|
149
|
-
|
|
164
|
+
{content.tags && content.tags.length > 0 && (
|
|
165
|
+
<div className="flex gap-1.5 flex-wrap mt-3">
|
|
166
|
+
{content.tags.map(tag => (
|
|
167
|
+
<span
|
|
168
|
+
key={tag}
|
|
169
|
+
className="px-2 py-0.5 bg-gray-100 text-gray-600 text-xs rounded"
|
|
170
|
+
>
|
|
171
|
+
{tag}
|
|
172
|
+
</span>
|
|
173
|
+
))}
|
|
174
|
+
</div>
|
|
175
|
+
)}
|
|
176
|
+
<div className="flex items-center gap-4 mt-3 text-sm text-gray-400 flex-wrap">
|
|
177
|
+
{content.createdAt && <span>创建于 {formatDate(content.createdAt)}</span>}
|
|
178
|
+
{content.updatedAt && content.updatedAt !== content.createdAt && (
|
|
179
|
+
<span>更新于 {formatDate(content.updatedAt)}</span>
|
|
180
|
+
)}
|
|
181
|
+
{content.publishedAt && <span>发布于 {formatDate(content.publishedAt)}</span>}
|
|
150
182
|
<span>👁 {content.viewCount}</span>
|
|
151
183
|
<span>❤ {content.likeCount}</span>
|
|
152
184
|
</div>
|
|
@@ -10,10 +10,27 @@ import {
|
|
|
10
10
|
Sparkles,
|
|
11
11
|
TrendingUp,
|
|
12
12
|
Zap,
|
|
13
|
+
Eye,
|
|
14
|
+
ExternalLink,
|
|
15
|
+
Star,
|
|
16
|
+
Shield,
|
|
13
17
|
} from 'lucide-react'
|
|
14
18
|
import { usePluginStore } from '@client/stores/pluginStore'
|
|
15
19
|
import { LoadingSpinner, EmptyState } from '@client/components'
|
|
16
|
-
import { StatusBadge } from '@client/components'
|
|
20
|
+
import { StatusBadge, type ColorScheme } from '@client/components'
|
|
21
|
+
|
|
22
|
+
const getStatusColor = (status: string): ColorScheme => {
|
|
23
|
+
switch (status?.toLowerCase()) {
|
|
24
|
+
case 'approved':
|
|
25
|
+
return 'green'
|
|
26
|
+
case 'pending':
|
|
27
|
+
return 'yellow'
|
|
28
|
+
case 'rejected':
|
|
29
|
+
return 'red'
|
|
30
|
+
default:
|
|
31
|
+
return 'gray'
|
|
32
|
+
}
|
|
33
|
+
}
|
|
17
34
|
|
|
18
35
|
export const PluginsPage: React.FC = () => {
|
|
19
36
|
const plugins = usePluginStore(state => state.plugins)
|
|
@@ -141,37 +158,112 @@ export const PluginsPage: React.FC = () => {
|
|
|
141
158
|
<Link
|
|
142
159
|
key={plugin.id}
|
|
143
160
|
to={`/plugins/${plugin.slug}`}
|
|
144
|
-
className="group relative
|
|
161
|
+
className="group relative bg-white rounded-2xl border border-gray-100 hover:border-violet-200 hover:shadow-xl hover:shadow-violet-500/5 transition-all duration-300 block overflow-hidden"
|
|
145
162
|
>
|
|
146
163
|
<div className="absolute inset-0 bg-gradient-to-br from-violet-500/0 to-fuchsia-500/0 group-hover:from-violet-500/[0.02] group-hover:to-fuchsia-500/[0.04] transition-all duration-300" />
|
|
147
164
|
<div className="relative">
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
165
|
+
{/* Screenshot thumbnail */}
|
|
166
|
+
<div className="relative">
|
|
167
|
+
{plugin.screenshotUrl ? (
|
|
168
|
+
<img
|
|
169
|
+
src={plugin.screenshotUrl}
|
|
170
|
+
alt={plugin.name}
|
|
171
|
+
className="w-full h-32 object-cover"
|
|
172
|
+
/>
|
|
173
|
+
) : (
|
|
174
|
+
<div className="w-full h-20 bg-gradient-to-br from-violet-100 to-fuchsia-100" />
|
|
175
|
+
)}
|
|
176
|
+
{plugin.featured && (
|
|
177
|
+
<div className="absolute top-2 right-2">
|
|
178
|
+
<div className="p-1.5 bg-amber-400 rounded-full shadow-lg">
|
|
179
|
+
<Star className="w-3.5 h-3.5 text-white fill-white" />
|
|
180
|
+
</div>
|
|
152
181
|
</div>
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
182
|
+
)}
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
<div className="p-5">
|
|
186
|
+
<div className="flex items-start justify-between gap-3 mb-3">
|
|
187
|
+
<div className="flex items-center gap-3">
|
|
188
|
+
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-violet-100 to-fuchsia-100 flex items-center justify-center text-lg font-bold text-violet-600 shrink-0">
|
|
189
|
+
{plugin.name.charAt(0).toUpperCase()}
|
|
190
|
+
</div>
|
|
191
|
+
<div className="min-w-0">
|
|
192
|
+
<h3 className="text-base font-semibold text-gray-900 truncate group-hover:text-violet-700 transition-colors">
|
|
193
|
+
{plugin.name}
|
|
194
|
+
</h3>
|
|
195
|
+
<p className="text-xs text-gray-400">by {plugin.authorName}</p>
|
|
196
|
+
</div>
|
|
158
197
|
</div>
|
|
198
|
+
<StatusBadge label={`v${plugin.version}`} colorScheme="purple" />
|
|
159
199
|
</div>
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
200
|
+
|
|
201
|
+
{/* Status and License */}
|
|
202
|
+
<div className="flex items-center gap-2 mb-3">
|
|
203
|
+
<StatusBadge
|
|
204
|
+
label={plugin.status || 'Unknown'}
|
|
205
|
+
icon={Shield}
|
|
206
|
+
colorScheme={getStatusColor(plugin.status || '')}
|
|
207
|
+
/>
|
|
208
|
+
{plugin.license && (
|
|
209
|
+
<span className="inline-flex items-center px-2 py-0.5 bg-gray-100 text-gray-600 rounded text-xs font-medium">
|
|
210
|
+
{plugin.license}
|
|
211
|
+
</span>
|
|
212
|
+
)}
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
<p className="text-sm text-gray-500 line-clamp-2 mb-4 leading-relaxed">
|
|
216
|
+
{plugin.description}
|
|
217
|
+
</p>
|
|
218
|
+
|
|
219
|
+
{/* Download + View counts */}
|
|
220
|
+
<div className="flex items-center gap-3 text-xs text-gray-400 mb-3">
|
|
167
221
|
<span className="flex items-center gap-1">
|
|
168
222
|
<Download className="w-3.5 h-3.5" />
|
|
169
223
|
{plugin.downloadCount}
|
|
170
224
|
</span>
|
|
225
|
+
{plugin.viewCount !== undefined && (
|
|
226
|
+
<span className="flex items-center gap-1">
|
|
227
|
+
<Eye className="w-3.5 h-3.5" />
|
|
228
|
+
{plugin.viewCount}
|
|
229
|
+
</span>
|
|
230
|
+
)}
|
|
171
231
|
</div>
|
|
232
|
+
|
|
233
|
+
{/* External links */}
|
|
234
|
+
{(plugin.repositoryUrl || plugin.homepageUrl) && (
|
|
235
|
+
<div className="flex items-center gap-1.5 mb-3">
|
|
236
|
+
{plugin.repositoryUrl && (
|
|
237
|
+
<a
|
|
238
|
+
href={plugin.repositoryUrl}
|
|
239
|
+
target="_blank"
|
|
240
|
+
rel="noopener noreferrer"
|
|
241
|
+
onClick={e => e.stopPropagation()}
|
|
242
|
+
className="p-1.5 rounded-lg bg-gray-50 hover:bg-gray-100 text-gray-400 hover:text-violet-600 transition-colors"
|
|
243
|
+
title="Repository"
|
|
244
|
+
>
|
|
245
|
+
<ExternalLink className="w-3.5 h-3.5" />
|
|
246
|
+
</a>
|
|
247
|
+
)}
|
|
248
|
+
{plugin.homepageUrl && (
|
|
249
|
+
<a
|
|
250
|
+
href={plugin.homepageUrl}
|
|
251
|
+
target="_blank"
|
|
252
|
+
rel="noopener noreferrer"
|
|
253
|
+
onClick={e => e.stopPropagation()}
|
|
254
|
+
className="p-1.5 rounded-lg bg-gray-50 hover:bg-gray-100 text-gray-400 hover:text-violet-600 transition-colors"
|
|
255
|
+
title="Homepage"
|
|
256
|
+
>
|
|
257
|
+
<ExternalLink className="w-3.5 h-3.5" />
|
|
258
|
+
</a>
|
|
259
|
+
)}
|
|
260
|
+
</div>
|
|
261
|
+
)}
|
|
262
|
+
|
|
263
|
+
{/* Tags - show all */}
|
|
172
264
|
{plugin.tags && plugin.tags.length > 0 && (
|
|
173
|
-
<div className="flex items-center gap-1">
|
|
174
|
-
{plugin.tags.
|
|
265
|
+
<div className="flex flex-wrap items-center gap-1">
|
|
266
|
+
{plugin.tags.map(tag => (
|
|
175
267
|
<span
|
|
176
268
|
key={tag}
|
|
177
269
|
className="inline-flex items-center px-2 py-0.5 bg-gray-50 text-gray-500 rounded-md text-xs"
|
|
@@ -112,7 +112,10 @@ export const TodoPage: React.FC = () => {
|
|
|
112
112
|
<title>Todo List - Biomimic App</title>
|
|
113
113
|
<meta name="description" content="Manage your todo items with full CRUD operations" />
|
|
114
114
|
<meta property="og:title" content="Todo List - Biomimic App" />
|
|
115
|
-
<meta
|
|
115
|
+
<meta
|
|
116
|
+
property="og:description"
|
|
117
|
+
content="Manage your todo items with full CRUD operations"
|
|
118
|
+
/>
|
|
116
119
|
</Helmet>
|
|
117
120
|
<div className="mb-8">
|
|
118
121
|
<h1
|
|
@@ -275,6 +278,11 @@ export const TodoPage: React.FC = () => {
|
|
|
275
278
|
</select>
|
|
276
279
|
<span className="text-xs text-gray-400">
|
|
277
280
|
{new Date(todo.createdAt).toLocaleString()}
|
|
281
|
+
{todo.updatedAt && todo.updatedAt !== todo.createdAt && (
|
|
282
|
+
<span className="ml-2">
|
|
283
|
+
· Updated {new Date(todo.updatedAt).toLocaleString()}
|
|
284
|
+
</span>
|
|
285
|
+
)}
|
|
278
286
|
</span>
|
|
279
287
|
</div>
|
|
280
288
|
</div>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { lazy, type ComponentType } from 'react'
|
|
2
2
|
|
|
3
|
-
export type PresetType = 'todo' | 'plugin' | 'ecommerce' | 'community' | 'saas'
|
|
3
|
+
export type PresetType = 'todo' | 'plugin' | 'ecommerce' | 'community' | 'forum' | 'saas'
|
|
4
4
|
|
|
5
5
|
export type AppType = 'client' | 'admin'
|
|
6
6
|
export type LayoutType = 'top-nav' | 'minimal'
|
|
@@ -481,10 +481,87 @@ export const PRESET_UI_CONFIGS: Record<PresetType, PresetUIConfig> = {
|
|
|
481
481
|
},
|
|
482
482
|
],
|
|
483
483
|
},
|
|
484
|
+
|
|
485
|
+
// forum is an alias for community — same config, different preset ID
|
|
486
|
+
forum: {
|
|
487
|
+
id: 'community',
|
|
488
|
+
name: 'Community',
|
|
489
|
+
appType: 'client',
|
|
490
|
+
layout: 'top-nav',
|
|
491
|
+
theme: COMMUNITY_THEME,
|
|
492
|
+
navigation: {
|
|
493
|
+
visible: true,
|
|
494
|
+
showLogo: true,
|
|
495
|
+
showSearch: false,
|
|
496
|
+
showCart: false,
|
|
497
|
+
authStyle: 'avatar',
|
|
498
|
+
navItems: 'desktop',
|
|
499
|
+
},
|
|
500
|
+
desktopNav: [
|
|
501
|
+
{ label: 'Topics', icon: 'Hash', path: '/topics' },
|
|
502
|
+
{ label: 'Popular', icon: 'TrendingUp', path: '/popular' },
|
|
503
|
+
{ label: 'Notifications', icon: 'Bell', path: '/notifications' },
|
|
504
|
+
{ label: 'Profile', icon: 'User', path: '/profile' },
|
|
505
|
+
],
|
|
506
|
+
mobileTabs: [
|
|
507
|
+
{ label: 'Topics', icon: 'Hash', path: '/topics' },
|
|
508
|
+
{ label: 'Popular', icon: 'TrendingUp', path: '/popular' },
|
|
509
|
+
{ label: 'Notifications', icon: 'Bell', path: '/notifications' },
|
|
510
|
+
{ label: 'Me', icon: 'User', path: '/profile' },
|
|
511
|
+
],
|
|
512
|
+
defaultRoute: '/topics',
|
|
513
|
+
routes: [
|
|
514
|
+
{
|
|
515
|
+
path: '/topics',
|
|
516
|
+
component: lazy(() => import('./pages/TopicsPage').then(m => ({ default: m.TopicsPage }))),
|
|
517
|
+
label: 'Topics',
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
path: '/popular',
|
|
521
|
+
component: lazy(() => import('./pages/TopicsPage').then(m => ({ default: m.TopicsPage }))),
|
|
522
|
+
label: 'Popular',
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
path: '/notifications',
|
|
526
|
+
component: lazy(() =>
|
|
527
|
+
import('./pages/NotificationPage').then(m => ({ default: m.NotificationPage }))
|
|
528
|
+
),
|
|
529
|
+
label: 'Notifications',
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
path: '/profile',
|
|
533
|
+
component: lazy(() =>
|
|
534
|
+
import('./pages/ProfilePage').then(m => ({ default: m.ProfilePage }))
|
|
535
|
+
),
|
|
536
|
+
label: 'Profile',
|
|
537
|
+
},
|
|
538
|
+
{
|
|
539
|
+
path: '/login',
|
|
540
|
+
component: lazy(() => import('./pages/LoginPage').then(m => ({ default: m.LoginPage }))),
|
|
541
|
+
label: 'Login',
|
|
542
|
+
},
|
|
543
|
+
{
|
|
544
|
+
path: '/register',
|
|
545
|
+
component: lazy(() =>
|
|
546
|
+
import('./pages/RegisterPage').then(m => ({ default: m.RegisterPage }))
|
|
547
|
+
),
|
|
548
|
+
label: 'Register',
|
|
549
|
+
},
|
|
550
|
+
],
|
|
551
|
+
},
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Preset ID aliases: deployed preset IDs may differ from internal config keys
|
|
555
|
+
const PRESET_ALIASES: Record<string, PresetType> = {
|
|
556
|
+
forum: 'community',
|
|
557
|
+
'xbrowser-marketplace': 'plugin',
|
|
558
|
+
'fullstack-admin': 'saas',
|
|
559
|
+
'todo-app': 'todo',
|
|
484
560
|
}
|
|
485
561
|
|
|
486
562
|
export function getPresetUIConfig(id: string): PresetUIConfig {
|
|
487
|
-
|
|
563
|
+
const resolvedId = PRESET_ALIASES[id] ?? (id as PresetType)
|
|
564
|
+
return PRESET_UI_CONFIGS[resolvedId] ?? PRESET_UI_CONFIGS.todo
|
|
488
565
|
}
|
|
489
566
|
|
|
490
567
|
export function getPresetUIConfigs(): Record<PresetType, PresetUIConfig> {
|
|
@@ -571,6 +571,395 @@ async function seedPluginsIfEmpty() {
|
|
|
571
571
|
screenshotUrl: 'https://picsum.photos/seed/notify/800/400',
|
|
572
572
|
tags: '["notification","email","push"]',
|
|
573
573
|
},
|
|
574
|
+
{
|
|
575
|
+
id: generateUUID(),
|
|
576
|
+
slug: 'form-builder',
|
|
577
|
+
name: 'Form Builder',
|
|
578
|
+
description:
|
|
579
|
+
'Drag-and-drop form creation with validation rules, conditional logic, and file uploads.',
|
|
580
|
+
readme:
|
|
581
|
+
'# Form Builder\n\nCreate complex forms with ease using our visual drag-and-drop builder.',
|
|
582
|
+
authorId: '1',
|
|
583
|
+
authorName: 'superadmin',
|
|
584
|
+
repositoryUrl: 'https://github.com/example/form-builder',
|
|
585
|
+
homepageUrl: 'https://form-builder.example.com',
|
|
586
|
+
license: 'MIT',
|
|
587
|
+
version: '3.2.1',
|
|
588
|
+
status: 'approved' as const,
|
|
589
|
+
downloadCount: 3420,
|
|
590
|
+
viewCount: 8900,
|
|
591
|
+
featured: true,
|
|
592
|
+
screenshotUrl: 'https://picsum.photos/seed/formbuilder/800/400',
|
|
593
|
+
tags: '["forms","validation","drag-drop"]',
|
|
594
|
+
},
|
|
595
|
+
{
|
|
596
|
+
id: generateUUID(),
|
|
597
|
+
slug: 'image-optimizer',
|
|
598
|
+
name: 'Image Optimizer',
|
|
599
|
+
description:
|
|
600
|
+
'Automatic image compression and format conversion with lazy loading and responsive srcset support.',
|
|
601
|
+
readme: '# Image Optimizer\n\nOptimize images automatically for faster page loads.',
|
|
602
|
+
authorId: '2',
|
|
603
|
+
authorName: 'customerservice',
|
|
604
|
+
repositoryUrl: 'https://github.com/example/image-optimizer',
|
|
605
|
+
license: 'MIT',
|
|
606
|
+
version: '2.0.0',
|
|
607
|
+
status: 'approved' as const,
|
|
608
|
+
downloadCount: 2890,
|
|
609
|
+
viewCount: 6500,
|
|
610
|
+
featured: false,
|
|
611
|
+
screenshotUrl: 'https://picsum.photos/seed/imgopt/800/400',
|
|
612
|
+
tags: '["image","optimization","performance"]',
|
|
613
|
+
},
|
|
614
|
+
{
|
|
615
|
+
id: generateUUID(),
|
|
616
|
+
slug: 'cache-manager',
|
|
617
|
+
name: 'Cache Manager',
|
|
618
|
+
description:
|
|
619
|
+
'Redis-powered caching layer with TTL management, cache invalidation, and tag-based purging.',
|
|
620
|
+
readme: '# Cache Manager\n\nSmart caching for high-performance applications.',
|
|
621
|
+
authorId: '1',
|
|
622
|
+
authorName: 'superadmin',
|
|
623
|
+
repositoryUrl: 'https://github.com/example/cache-manager',
|
|
624
|
+
homepageUrl: 'https://cache-manager.example.com',
|
|
625
|
+
license: 'Apache-2.0',
|
|
626
|
+
version: '1.8.0',
|
|
627
|
+
status: 'approved' as const,
|
|
628
|
+
downloadCount: 1560,
|
|
629
|
+
viewCount: 4200,
|
|
630
|
+
featured: true,
|
|
631
|
+
screenshotUrl: 'https://picsum.photos/seed/cache/800/400',
|
|
632
|
+
tags: '["cache","redis","performance"]',
|
|
633
|
+
},
|
|
634
|
+
{
|
|
635
|
+
id: generateUUID(),
|
|
636
|
+
slug: 'auth-guard',
|
|
637
|
+
name: 'Auth Guard',
|
|
638
|
+
description:
|
|
639
|
+
'Advanced authentication flows with OAuth2, SAML, and biometric support including session management.',
|
|
640
|
+
readme: '# Auth Guard\n\nEnterprise-grade authentication for modern apps.',
|
|
641
|
+
authorId: '1',
|
|
642
|
+
authorName: 'superadmin',
|
|
643
|
+
repositoryUrl: 'https://github.com/example/auth-guard',
|
|
644
|
+
homepageUrl: 'https://auth-guard.example.com',
|
|
645
|
+
license: 'MIT',
|
|
646
|
+
version: '4.1.0',
|
|
647
|
+
status: 'approved' as const,
|
|
648
|
+
downloadCount: 4210,
|
|
649
|
+
viewCount: 9800,
|
|
650
|
+
featured: true,
|
|
651
|
+
screenshotUrl: 'https://picsum.photos/seed/authguard/800/400',
|
|
652
|
+
tags: '["auth","oauth","security"]',
|
|
653
|
+
},
|
|
654
|
+
{
|
|
655
|
+
id: generateUUID(),
|
|
656
|
+
slug: 'payment-gateway',
|
|
657
|
+
name: 'Payment Gateway',
|
|
658
|
+
description:
|
|
659
|
+
'Stripe and PayPal integration with subscription management, invoicing, and refund handling.',
|
|
660
|
+
readme: '# Payment Gateway\n\nAccept payments globally with multiple providers.',
|
|
661
|
+
authorId: '2',
|
|
662
|
+
authorName: 'customerservice',
|
|
663
|
+
repositoryUrl: 'https://github.com/example/payment-gateway',
|
|
664
|
+
homepageUrl: 'https://payment-gateway.example.com',
|
|
665
|
+
license: 'MIT',
|
|
666
|
+
version: '2.3.0',
|
|
667
|
+
status: 'approved' as const,
|
|
668
|
+
downloadCount: 3100,
|
|
669
|
+
viewCount: 7800,
|
|
670
|
+
featured: true,
|
|
671
|
+
screenshotUrl: 'https://picsum.photos/seed/payment/800/400',
|
|
672
|
+
tags: '["payment","stripe","subscription"]',
|
|
673
|
+
},
|
|
674
|
+
{
|
|
675
|
+
id: generateUUID(),
|
|
676
|
+
slug: 'chat-widget',
|
|
677
|
+
name: 'Chat Widget',
|
|
678
|
+
description:
|
|
679
|
+
'Real-time customer support chat with AI-powered auto-replies and conversation routing.',
|
|
680
|
+
readme: '# Chat Widget\n\nLive chat support for your customers.',
|
|
681
|
+
authorId: '3',
|
|
682
|
+
authorName: 'user1',
|
|
683
|
+
repositoryUrl: 'https://github.com/example/chat-widget',
|
|
684
|
+
license: 'ISC',
|
|
685
|
+
version: '1.4.2',
|
|
686
|
+
status: 'approved' as const,
|
|
687
|
+
downloadCount: 1870,
|
|
688
|
+
viewCount: 5200,
|
|
689
|
+
featured: false,
|
|
690
|
+
screenshotUrl: 'https://picsum.photos/seed/chatwidget/800/400',
|
|
691
|
+
tags: '["chat","support","real-time"]',
|
|
692
|
+
},
|
|
693
|
+
{
|
|
694
|
+
id: generateUUID(),
|
|
695
|
+
slug: 'backup-manager',
|
|
696
|
+
name: 'Backup Manager',
|
|
697
|
+
description:
|
|
698
|
+
'Automated database backups with scheduling, encryption, and cloud storage integration.',
|
|
699
|
+
readme: '# Backup Manager\n\nNever lose data with automated backups.',
|
|
700
|
+
authorId: '1',
|
|
701
|
+
authorName: 'superadmin',
|
|
702
|
+
repositoryUrl: 'https://github.com/example/backup-manager',
|
|
703
|
+
license: 'MIT',
|
|
704
|
+
version: '1.1.0',
|
|
705
|
+
status: 'approved' as const,
|
|
706
|
+
downloadCount: 980,
|
|
707
|
+
viewCount: 2900,
|
|
708
|
+
featured: false,
|
|
709
|
+
screenshotUrl: 'https://picsum.photos/seed/backup/800/400',
|
|
710
|
+
tags: '["backup","database","scheduling"]',
|
|
711
|
+
},
|
|
712
|
+
{
|
|
713
|
+
id: generateUUID(),
|
|
714
|
+
slug: 'rate-limiter',
|
|
715
|
+
name: 'Rate Limiter',
|
|
716
|
+
description:
|
|
717
|
+
'API rate limiting middleware with sliding window, token bucket, and fixed window algorithms.',
|
|
718
|
+
readme: '# Rate Limiter\n\nProtect your APIs from abuse.',
|
|
719
|
+
authorId: '2',
|
|
720
|
+
authorName: 'customerservice',
|
|
721
|
+
repositoryUrl: 'https://github.com/example/rate-limiter',
|
|
722
|
+
homepageUrl: 'https://rate-limiter.example.com',
|
|
723
|
+
license: 'MIT',
|
|
724
|
+
version: '2.0.0',
|
|
725
|
+
status: 'approved' as const,
|
|
726
|
+
downloadCount: 1340,
|
|
727
|
+
viewCount: 3800,
|
|
728
|
+
featured: false,
|
|
729
|
+
screenshotUrl: 'https://picsum.photos/seed/ratelimit/800/400',
|
|
730
|
+
tags: '["rate-limit","api","middleware"]',
|
|
731
|
+
},
|
|
732
|
+
{
|
|
733
|
+
id: generateUUID(),
|
|
734
|
+
slug: 'email-templates',
|
|
735
|
+
name: 'Email Templates',
|
|
736
|
+
description:
|
|
737
|
+
'Responsive email builder with template variables, preview mode, and SMTP integration.',
|
|
738
|
+
readme: '# Email Templates\n\nBeautiful emails that work everywhere.',
|
|
739
|
+
authorId: '3',
|
|
740
|
+
authorName: 'user1',
|
|
741
|
+
repositoryUrl: 'https://github.com/example/email-templates',
|
|
742
|
+
license: 'Apache-2.0',
|
|
743
|
+
version: '1.6.0',
|
|
744
|
+
status: 'approved' as const,
|
|
745
|
+
downloadCount: 2100,
|
|
746
|
+
viewCount: 5600,
|
|
747
|
+
featured: false,
|
|
748
|
+
screenshotUrl: 'https://picsum.photos/seed/emailtpl/800/400',
|
|
749
|
+
tags: '["email","templates","smtp"]',
|
|
750
|
+
},
|
|
751
|
+
{
|
|
752
|
+
id: generateUUID(),
|
|
753
|
+
slug: 'search-engine',
|
|
754
|
+
name: 'Search Engine',
|
|
755
|
+
description:
|
|
756
|
+
'Elasticsearch integration with faceted search, autocomplete, and relevance tuning.',
|
|
757
|
+
readme: '# Search Engine\n\nPowerful full-text search for your application.',
|
|
758
|
+
authorId: '1',
|
|
759
|
+
authorName: 'superadmin',
|
|
760
|
+
repositoryUrl: 'https://github.com/example/search-engine',
|
|
761
|
+
homepageUrl: 'https://search-engine.example.com',
|
|
762
|
+
license: 'MIT',
|
|
763
|
+
version: '3.0.0',
|
|
764
|
+
status: 'approved' as const,
|
|
765
|
+
downloadCount: 2560,
|
|
766
|
+
viewCount: 7100,
|
|
767
|
+
featured: true,
|
|
768
|
+
screenshotUrl: 'https://picsum.photos/seed/searcheng/800/400',
|
|
769
|
+
tags: '["search","elasticsearch","full-text"]',
|
|
770
|
+
},
|
|
771
|
+
{
|
|
772
|
+
id: generateUUID(),
|
|
773
|
+
slug: 'cdn-manager',
|
|
774
|
+
name: 'CDN Manager',
|
|
775
|
+
description:
|
|
776
|
+
'Content delivery network controls with cache purging, URL signing, and geo-routing.',
|
|
777
|
+
readme: '# CDN Manager\n\nDeliver content faster globally.',
|
|
778
|
+
authorId: '2',
|
|
779
|
+
authorName: 'customerservice',
|
|
780
|
+
repositoryUrl: 'https://github.com/example/cdn-manager',
|
|
781
|
+
license: 'MIT',
|
|
782
|
+
version: '1.3.0',
|
|
783
|
+
status: 'approved' as const,
|
|
784
|
+
downloadCount: 870,
|
|
785
|
+
viewCount: 2400,
|
|
786
|
+
featured: false,
|
|
787
|
+
screenshotUrl: 'https://picsum.photos/seed/cdn/800/400',
|
|
788
|
+
tags: '["cdn","caching","delivery"]',
|
|
789
|
+
},
|
|
790
|
+
{
|
|
791
|
+
id: generateUUID(),
|
|
792
|
+
slug: 'logger-pro',
|
|
793
|
+
name: 'Logger Pro',
|
|
794
|
+
description:
|
|
795
|
+
'Structured logging with severity levels, log rotation, and remote aggregation support.',
|
|
796
|
+
readme: '# Logger Pro\n\nEnterprise logging made simple.',
|
|
797
|
+
authorId: '1',
|
|
798
|
+
authorName: 'superadmin',
|
|
799
|
+
repositoryUrl: 'https://github.com/example/logger-pro',
|
|
800
|
+
license: 'BSD-3-Clause',
|
|
801
|
+
version: '2.1.0',
|
|
802
|
+
status: 'approved' as const,
|
|
803
|
+
downloadCount: 1190,
|
|
804
|
+
viewCount: 3300,
|
|
805
|
+
featured: false,
|
|
806
|
+
screenshotUrl: 'https://picsum.photos/seed/logger/800/400',
|
|
807
|
+
tags: '["logging","monitoring","structured"]',
|
|
808
|
+
},
|
|
809
|
+
{
|
|
810
|
+
id: generateUUID(),
|
|
811
|
+
slug: 'task-scheduler',
|
|
812
|
+
name: 'Task Scheduler',
|
|
813
|
+
description:
|
|
814
|
+
'Cron job management with retry policies, concurrency controls, and execution history.',
|
|
815
|
+
readme: '# Task Scheduler\n\nAutomate recurring tasks with confidence.',
|
|
816
|
+
authorId: '3',
|
|
817
|
+
authorName: 'user1',
|
|
818
|
+
repositoryUrl: 'https://github.com/example/task-scheduler',
|
|
819
|
+
homepageUrl: 'https://task-scheduler.example.com',
|
|
820
|
+
license: 'MIT',
|
|
821
|
+
version: '1.5.0',
|
|
822
|
+
status: 'approved' as const,
|
|
823
|
+
downloadCount: 1780,
|
|
824
|
+
viewCount: 4900,
|
|
825
|
+
featured: false,
|
|
826
|
+
screenshotUrl: 'https://picsum.photos/seed/tasksched/800/400',
|
|
827
|
+
tags: '["scheduler","cron","automation"]',
|
|
828
|
+
},
|
|
829
|
+
{
|
|
830
|
+
id: generateUUID(),
|
|
831
|
+
slug: 'ab-testing',
|
|
832
|
+
name: 'A/B Testing',
|
|
833
|
+
description:
|
|
834
|
+
'Experiment framework with statistical significance calculation and variant allocation.',
|
|
835
|
+
readme: '# A/B Testing\n\nData-driven decisions through controlled experiments.',
|
|
836
|
+
authorId: '1',
|
|
837
|
+
authorName: 'superadmin',
|
|
838
|
+
repositoryUrl: 'https://github.com/example/ab-testing',
|
|
839
|
+
license: 'Apache-2.0',
|
|
840
|
+
version: '1.0.0',
|
|
841
|
+
status: 'approved' as const,
|
|
842
|
+
downloadCount: 640,
|
|
843
|
+
viewCount: 2100,
|
|
844
|
+
featured: false,
|
|
845
|
+
screenshotUrl: 'https://picsum.photos/seed/abtest/800/400',
|
|
846
|
+
tags: '["testing","experiment","analytics"]',
|
|
847
|
+
},
|
|
848
|
+
{
|
|
849
|
+
id: generateUUID(),
|
|
850
|
+
slug: 'markdown-editor',
|
|
851
|
+
name: 'Markdown Editor',
|
|
852
|
+
description:
|
|
853
|
+
'Rich text editing with Markdown support, syntax highlighting, and live preview.',
|
|
854
|
+
readme: '# Markdown Editor\n\nWrite beautifully with Markdown.',
|
|
855
|
+
authorId: '2',
|
|
856
|
+
authorName: 'customerservice',
|
|
857
|
+
repositoryUrl: 'https://github.com/example/markdown-editor',
|
|
858
|
+
homepageUrl: 'https://md-editor.example.com',
|
|
859
|
+
license: 'MIT',
|
|
860
|
+
version: '2.4.0',
|
|
861
|
+
status: 'approved' as const,
|
|
862
|
+
downloadCount: 2950,
|
|
863
|
+
viewCount: 7400,
|
|
864
|
+
featured: true,
|
|
865
|
+
screenshotUrl: 'https://picsum.photos/seed/mdeditor/800/400',
|
|
866
|
+
tags: '["markdown","editor","rich-text"]',
|
|
867
|
+
},
|
|
868
|
+
{
|
|
869
|
+
id: generateUUID(),
|
|
870
|
+
slug: 'webhook-manager',
|
|
871
|
+
name: 'Webhook Manager',
|
|
872
|
+
description:
|
|
873
|
+
'Outgoing webhook automation with retry logic, payload templating, and delivery tracking.',
|
|
874
|
+
readme: '# Webhook Manager\n\nReliable webhook delivery for your integrations.',
|
|
875
|
+
authorId: '3',
|
|
876
|
+
authorName: 'user1',
|
|
877
|
+
repositoryUrl: 'https://github.com/example/webhook-manager',
|
|
878
|
+
license: 'MIT',
|
|
879
|
+
version: '1.2.0',
|
|
880
|
+
status: 'pending' as const,
|
|
881
|
+
downloadCount: 520,
|
|
882
|
+
viewCount: 1800,
|
|
883
|
+
featured: false,
|
|
884
|
+
screenshotUrl: 'https://picsum.photos/seed/webhook/800/400',
|
|
885
|
+
tags: '["webhook","automation","integration"]',
|
|
886
|
+
},
|
|
887
|
+
{
|
|
888
|
+
id: generateUUID(),
|
|
889
|
+
slug: 'two-factor-auth',
|
|
890
|
+
name: 'Two-Factor Auth',
|
|
891
|
+
description: '2FA/MFA support with TOTP, SMS OTP, and hardware key authentication methods.',
|
|
892
|
+
readme: '# Two-Factor Auth\n\nAdd an extra layer of security.',
|
|
893
|
+
authorId: '1',
|
|
894
|
+
authorName: 'superadmin',
|
|
895
|
+
repositoryUrl: 'https://github.com/example/two-factor-auth',
|
|
896
|
+
license: 'MIT',
|
|
897
|
+
version: '1.7.0',
|
|
898
|
+
status: 'approved' as const,
|
|
899
|
+
downloadCount: 2340,
|
|
900
|
+
viewCount: 6100,
|
|
901
|
+
featured: false,
|
|
902
|
+
screenshotUrl: 'https://picsum.photos/seed/2fa/800/400',
|
|
903
|
+
tags: '["2fa","security","authentication"]',
|
|
904
|
+
},
|
|
905
|
+
{
|
|
906
|
+
id: generateUUID(),
|
|
907
|
+
slug: 'database-browser',
|
|
908
|
+
name: 'Database Browser',
|
|
909
|
+
description:
|
|
910
|
+
'GUI interface for browsing tables, running queries, and visualizing schema relationships.',
|
|
911
|
+
readme: '# Database Browser\n\nExplore your data visually.',
|
|
912
|
+
authorId: '2',
|
|
913
|
+
authorName: 'customerservice',
|
|
914
|
+
repositoryUrl: 'https://github.com/example/database-browser',
|
|
915
|
+
license: 'GPL-3.0',
|
|
916
|
+
version: '1.0.0',
|
|
917
|
+
status: 'pending' as const,
|
|
918
|
+
downloadCount: 340,
|
|
919
|
+
viewCount: 1200,
|
|
920
|
+
featured: false,
|
|
921
|
+
screenshotUrl: 'https://picsum.photos/seed/dbbrowser/800/400',
|
|
922
|
+
tags: '["database","gui","query"]',
|
|
923
|
+
},
|
|
924
|
+
{
|
|
925
|
+
id: generateUUID(),
|
|
926
|
+
slug: 'performance-monitor',
|
|
927
|
+
name: 'Performance Monitor',
|
|
928
|
+
description:
|
|
929
|
+
'Core Web Vitals tracking with real user monitoring, alerts, and performance budgets.',
|
|
930
|
+
readme: '# Performance Monitor\n\nKeep your app fast and responsive.',
|
|
931
|
+
authorId: '1',
|
|
932
|
+
authorName: 'superadmin',
|
|
933
|
+
repositoryUrl: 'https://github.com/example/performance-monitor',
|
|
934
|
+
homepageUrl: 'https://perf-monitor.example.com',
|
|
935
|
+
license: 'MIT',
|
|
936
|
+
version: '2.0.0',
|
|
937
|
+
status: 'approved' as const,
|
|
938
|
+
downloadCount: 1890,
|
|
939
|
+
viewCount: 5300,
|
|
940
|
+
featured: true,
|
|
941
|
+
screenshotUrl: 'https://picsum.photos/seed/perfmon/800/400',
|
|
942
|
+
tags: '["performance","monitoring","web-vitals"]',
|
|
943
|
+
},
|
|
944
|
+
{
|
|
945
|
+
id: generateUUID(),
|
|
946
|
+
slug: 'i18n-manager',
|
|
947
|
+
name: 'i18n Manager',
|
|
948
|
+
description:
|
|
949
|
+
'Internationalization toolkit with translation memory, pluralization, and RTL support.',
|
|
950
|
+
readme: '# i18n Manager\n\nGo global with multilingual support.',
|
|
951
|
+
authorId: '3',
|
|
952
|
+
authorName: 'user1',
|
|
953
|
+
repositoryUrl: 'https://github.com/example/i18n-manager',
|
|
954
|
+
license: 'MIT',
|
|
955
|
+
version: '1.3.0',
|
|
956
|
+
status: 'approved' as const,
|
|
957
|
+
downloadCount: 1450,
|
|
958
|
+
viewCount: 4100,
|
|
959
|
+
featured: false,
|
|
960
|
+
screenshotUrl: 'https://picsum.photos/seed/i18n/800/400',
|
|
961
|
+
tags: '["i18n","translation","localization"]',
|
|
962
|
+
},
|
|
574
963
|
]
|
|
575
964
|
|
|
576
965
|
await db.insert(plugins).values(samplePlugins)
|
|
@@ -101,6 +101,256 @@ const MOCK_TOPICS = [
|
|
|
101
101
|
author: { name: 'Emma Wilson', initials: 'EW' },
|
|
102
102
|
createdAt: '12h ago',
|
|
103
103
|
},
|
|
104
|
+
{
|
|
105
|
+
id: '7',
|
|
106
|
+
title: 'Optimizing React components with useMemo and useCallback',
|
|
107
|
+
excerpt:
|
|
108
|
+
'Understanding when and how to use React hooks for performance optimization. Real-world examples and benchmarks showing measurable improvements.',
|
|
109
|
+
votes: 45,
|
|
110
|
+
replyCount: 15,
|
|
111
|
+
viewCount: 678,
|
|
112
|
+
status: 'discussion' as const,
|
|
113
|
+
tags: [
|
|
114
|
+
{ label: 'React', color: 'bg-cyan-100 text-cyan-700' },
|
|
115
|
+
{ label: 'Performance', color: 'bg-amber-100 text-amber-700' },
|
|
116
|
+
],
|
|
117
|
+
author: { name: '张伟', initials: 'ZW' },
|
|
118
|
+
createdAt: '1d ago',
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: '8',
|
|
122
|
+
title: 'PostgreSQL query optimization for large datasets',
|
|
123
|
+
excerpt:
|
|
124
|
+
'Strategies for handling millions of rows with efficient indexing, query planning, and materialized views. Includes EXPLAIN ANALYZE walkthrough.',
|
|
125
|
+
votes: 89,
|
|
126
|
+
replyCount: 23,
|
|
127
|
+
viewCount: 1567,
|
|
128
|
+
status: 'hot' as const,
|
|
129
|
+
tags: [
|
|
130
|
+
{ label: 'PostgreSQL', color: 'bg-blue-100 text-blue-700' },
|
|
131
|
+
{ label: 'Database', color: 'bg-indigo-100 text-indigo-700' },
|
|
132
|
+
{ label: 'Performance', color: 'bg-amber-100 text-amber-700' },
|
|
133
|
+
],
|
|
134
|
+
author: { name: 'Raj Sharma', initials: 'RS' },
|
|
135
|
+
createdAt: '1d ago',
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
id: '9',
|
|
139
|
+
title: 'Implementing JWT authentication with refresh tokens',
|
|
140
|
+
excerpt:
|
|
141
|
+
'Secure authentication flow with JWT access tokens and refresh tokens. Best practices for token storage, rotation, and revocation.',
|
|
142
|
+
votes: 72,
|
|
143
|
+
replyCount: 18,
|
|
144
|
+
viewCount: 1245,
|
|
145
|
+
status: 'solved' as const,
|
|
146
|
+
tags: [
|
|
147
|
+
{ label: 'Auth', color: 'bg-rose-100 text-rose-700' },
|
|
148
|
+
{ label: 'Security', color: 'bg-red-100 text-red-700' },
|
|
149
|
+
{ label: 'JWT', color: 'bg-pink-100 text-pink-700' },
|
|
150
|
+
],
|
|
151
|
+
author: { name: '田中太郎', initials: 'TT' },
|
|
152
|
+
createdAt: '2d ago',
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
id: '10',
|
|
156
|
+
title: 'Building microservices with Docker and Kubernetes',
|
|
157
|
+
excerpt:
|
|
158
|
+
'Complete guide to containerizing Node.js applications and orchestrating them with Kubernetes. Includes service discovery, load balancing, and autoscaling.',
|
|
159
|
+
votes: 103,
|
|
160
|
+
replyCount: 31,
|
|
161
|
+
viewCount: 1890,
|
|
162
|
+
status: 'hot' as const,
|
|
163
|
+
tags: [
|
|
164
|
+
{ label: 'Docker', color: 'bg-sky-100 text-sky-700' },
|
|
165
|
+
{ label: 'Kubernetes', color: 'bg-blue-100 text-blue-700' },
|
|
166
|
+
{ label: 'Microservices', color: 'bg-violet-100 text-violet-700' },
|
|
167
|
+
],
|
|
168
|
+
author: { name: 'Marie Dubois', initials: 'MD' },
|
|
169
|
+
createdAt: '2d ago',
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
id: '11',
|
|
173
|
+
title: 'Writing accessible React applications',
|
|
174
|
+
excerpt:
|
|
175
|
+
'Essential ARIA patterns and semantic HTML for building inclusive web apps. Screen reader testing checklist and common accessibility pitfalls.',
|
|
176
|
+
votes: 58,
|
|
177
|
+
replyCount: 14,
|
|
178
|
+
viewCount: 923,
|
|
179
|
+
status: 'open' as const,
|
|
180
|
+
tags: [
|
|
181
|
+
{ label: 'Accessibility', color: 'bg-emerald-100 text-emerald-700' },
|
|
182
|
+
{ label: 'React', color: 'bg-cyan-100 text-cyan-700' },
|
|
183
|
+
{ label: 'A11y', color: 'bg-lime-100 text-lime-700' },
|
|
184
|
+
],
|
|
185
|
+
author: { name: '김민수', initials: 'KM' },
|
|
186
|
+
createdAt: '3d ago',
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
id: '12',
|
|
190
|
+
title: 'TypeScript utility types explained with examples',
|
|
191
|
+
excerpt:
|
|
192
|
+
'Deep dive into TypeScript utility types like Pick, Omit, Partial, Record, and more. Practical examples for everyday use.',
|
|
193
|
+
votes: 81,
|
|
194
|
+
replyCount: 22,
|
|
195
|
+
viewCount: 1456,
|
|
196
|
+
status: 'hot' as const,
|
|
197
|
+
tags: [
|
|
198
|
+
{ label: 'TypeScript', color: 'bg-blue-100 text-blue-700' },
|
|
199
|
+
{ label: 'Guide', color: 'bg-emerald-100 text-emerald-700' },
|
|
200
|
+
],
|
|
201
|
+
author: { name: 'Anna Müller', initials: 'AM' },
|
|
202
|
+
createdAt: '3d ago',
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
id: '13',
|
|
206
|
+
title: 'Redis caching strategies for high-traffic APIs',
|
|
207
|
+
excerpt:
|
|
208
|
+
'Implementing Redis caching layers for REST APIs. Cache invalidation strategies, TTL policies, and handling cache stampedes.',
|
|
209
|
+
votes: 64,
|
|
210
|
+
replyCount: 9,
|
|
211
|
+
viewCount: 1012,
|
|
212
|
+
status: 'solved' as const,
|
|
213
|
+
tags: [
|
|
214
|
+
{ label: 'Redis', color: 'bg-red-100 text-red-700' },
|
|
215
|
+
{ label: 'Caching', color: 'bg-orange-100 text-orange-700' },
|
|
216
|
+
{ label: 'API', color: 'bg-sky-100 text-sky-700' },
|
|
217
|
+
],
|
|
218
|
+
author: { name: 'Carlos Silva', initials: 'CS' },
|
|
219
|
+
createdAt: '3d ago',
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
id: '14',
|
|
223
|
+
title: 'GraphQL Federation for microservices architecture',
|
|
224
|
+
excerpt:
|
|
225
|
+
'Combining multiple GraphQL services into a unified API with Apollo Federation. Schema stitching, type composition, and distributed tracing.',
|
|
226
|
+
votes: 47,
|
|
227
|
+
replyCount: 0,
|
|
228
|
+
viewCount: 623,
|
|
229
|
+
status: 'unanswered' as const,
|
|
230
|
+
tags: [
|
|
231
|
+
{ label: 'GraphQL', color: 'bg-pink-100 text-pink-700' },
|
|
232
|
+
{ label: 'Microservices', color: 'bg-violet-100 text-violet-700' },
|
|
233
|
+
],
|
|
234
|
+
author: { name: '李明', initials: 'LM' },
|
|
235
|
+
createdAt: '4h ago',
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
id: '15',
|
|
239
|
+
title: 'Testing React components with Vitest and Testing Library',
|
|
240
|
+
excerpt:
|
|
241
|
+
'Modern testing setup with Vitest and React Testing Library. Writing maintainable tests, mocking external dependencies, and snapshot testing.',
|
|
242
|
+
votes: 76,
|
|
243
|
+
replyCount: 17,
|
|
244
|
+
viewCount: 1234,
|
|
245
|
+
status: 'discussion' as const,
|
|
246
|
+
tags: [
|
|
247
|
+
{ label: 'Testing', color: 'bg-amber-100 text-amber-700' },
|
|
248
|
+
{ label: 'React', color: 'bg-cyan-100 text-cyan-700' },
|
|
249
|
+
{ label: 'Vitest', color: 'bg-purple-100 text-purple-700' },
|
|
250
|
+
],
|
|
251
|
+
author: { name: 'Elena Popova', initials: 'EP' },
|
|
252
|
+
createdAt: '5h ago',
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
id: '16',
|
|
256
|
+
title: 'RESTful API design best practices',
|
|
257
|
+
excerpt:
|
|
258
|
+
'Comprehensive guide to designing REST APIs. Resource naming, HTTP methods, status codes, pagination, filtering, and versioning strategies.',
|
|
259
|
+
votes: 92,
|
|
260
|
+
replyCount: 28,
|
|
261
|
+
viewCount: 1678,
|
|
262
|
+
status: 'hot' as const,
|
|
263
|
+
tags: [
|
|
264
|
+
{ label: 'API Design', color: 'bg-sky-100 text-sky-700' },
|
|
265
|
+
{ label: 'REST', color: 'bg-emerald-100 text-emerald-700' },
|
|
266
|
+
{ label: 'Architecture', color: 'bg-orange-100 text-orange-700' },
|
|
267
|
+
],
|
|
268
|
+
author: { name: '佐藤健', initials: 'SK' },
|
|
269
|
+
createdAt: '6h ago',
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
id: '17',
|
|
273
|
+
title: 'CSS Grid vs Flexbox — when to use which?',
|
|
274
|
+
excerpt:
|
|
275
|
+
'Practical comparison of CSS Grid and Flexbox layouts. Use cases, browser support, and examples of combining both for complex layouts.',
|
|
276
|
+
votes: 39,
|
|
277
|
+
replyCount: 8,
|
|
278
|
+
viewCount: 567,
|
|
279
|
+
status: 'open' as const,
|
|
280
|
+
tags: [
|
|
281
|
+
{ label: 'CSS', color: 'bg-indigo-100 text-indigo-700' },
|
|
282
|
+
{ label: 'Layout', color: 'bg-purple-100 text-purple-700' },
|
|
283
|
+
],
|
|
284
|
+
author: { name: 'Olga Ivanova', initials: 'OI' },
|
|
285
|
+
createdAt: '8h ago',
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
id: '18',
|
|
289
|
+
title: 'Setting up CI/CD pipeline with GitHub Actions',
|
|
290
|
+
excerpt:
|
|
291
|
+
'Automating testing, building, and deployment with GitHub Actions. Multi-environment configs, secrets management, and deployment strategies.',
|
|
292
|
+
votes: 68,
|
|
293
|
+
replyCount: 13,
|
|
294
|
+
viewCount: 1102,
|
|
295
|
+
status: 'solved' as const,
|
|
296
|
+
tags: [
|
|
297
|
+
{ label: 'CI/CD', color: 'bg-rose-100 text-rose-700' },
|
|
298
|
+
{ label: 'GitHub', color: 'bg-slate-100 text-slate-700' },
|
|
299
|
+
{ label: 'DevOps', color: 'bg-blue-100 text-blue-700' },
|
|
300
|
+
],
|
|
301
|
+
author: { name: '黄志强', initials: 'HZ' },
|
|
302
|
+
createdAt: '30m ago',
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
id: '19',
|
|
306
|
+
title: 'Node.js event loop explained in depth',
|
|
307
|
+
excerpt:
|
|
308
|
+
'Understanding the Node.js event loop, call stack, and asynchronous operations. Performance implications and best practices for event-driven apps.',
|
|
309
|
+
votes: 55,
|
|
310
|
+
replyCount: 11,
|
|
311
|
+
viewCount: 845,
|
|
312
|
+
status: 'discussion' as const,
|
|
313
|
+
tags: [
|
|
314
|
+
{ label: 'Node.js', color: 'bg-green-100 text-green-700' },
|
|
315
|
+
{ label: 'Performance', color: 'bg-amber-100 text-amber-700' },
|
|
316
|
+
],
|
|
317
|
+
author: { name: 'Giovanni Rossi', initials: 'GR' },
|
|
318
|
+
createdAt: '45m ago',
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
id: '20',
|
|
322
|
+
title: 'Handling file uploads in multipart/form-data with Hono',
|
|
323
|
+
excerpt:
|
|
324
|
+
'Implementing secure file upload endpoints in Hono. Validation, storage with R2, handling large files, and progress tracking.',
|
|
325
|
+
votes: 71,
|
|
326
|
+
replyCount: 16,
|
|
327
|
+
viewCount: 1323,
|
|
328
|
+
status: 'solved' as const,
|
|
329
|
+
tags: [
|
|
330
|
+
{ label: 'Hono', color: 'bg-sky-100 text-sky-700' },
|
|
331
|
+
{ label: 'File Upload', color: 'bg-indigo-100 text-indigo-700' },
|
|
332
|
+
{ label: 'R2', color: 'bg-amber-100 text-amber-700' },
|
|
333
|
+
],
|
|
334
|
+
author: { name: 'Priya Patel', initials: 'PP' },
|
|
335
|
+
createdAt: '2h ago',
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
id: '21',
|
|
339
|
+
title: 'Debouncing and throttling in React applications',
|
|
340
|
+
excerpt:
|
|
341
|
+
'Implementing debouncing and throttling for search inputs, scroll events, and API calls. Custom hooks and performance considerations.',
|
|
342
|
+
votes: 33,
|
|
343
|
+
replyCount: 5,
|
|
344
|
+
viewCount: 445,
|
|
345
|
+
status: 'open' as const,
|
|
346
|
+
tags: [
|
|
347
|
+
{ label: 'React', color: 'bg-cyan-100 text-cyan-700' },
|
|
348
|
+
{ label: 'Performance', color: 'bg-amber-100 text-amber-700' },
|
|
349
|
+
{ label: 'Hooks', color: 'bg-pink-100 text-pink-700' },
|
|
350
|
+
],
|
|
351
|
+
author: { name: '山本花子', initials: 'YH' },
|
|
352
|
+
createdAt: '1w ago',
|
|
353
|
+
},
|
|
104
354
|
]
|
|
105
355
|
|
|
106
356
|
const MOCK_PROFILE_ACTIVITY = [
|
|
@@ -42,7 +42,7 @@ export async function seedContentsIfEmpty(): Promise<void> {
|
|
|
42
42
|
JSON.stringify(['最佳实践', '经验']),
|
|
43
43
|
]
|
|
44
44
|
|
|
45
|
-
for (let i = 0; i <
|
|
45
|
+
for (let i = 0; i < 25; i++) {
|
|
46
46
|
const category = randomElement(CATEGORIES)
|
|
47
47
|
const status = randomElement(STATUSES)
|
|
48
48
|
const createdAt = new Date(randomDate(new Date('2024-01-01'), new Date()))
|
|
@@ -45,7 +45,7 @@ export async function seedTodosIfEmpty(): Promise<void> {
|
|
|
45
45
|
const existing = await db.select().from(todos)
|
|
46
46
|
if (existing.length === 0) {
|
|
47
47
|
const STATUS = ['pending', 'in_progress', 'completed'] as const
|
|
48
|
-
for (let i = 0; i <
|
|
48
|
+
for (let i = 0; i < 25; i++) {
|
|
49
49
|
const createdAt = new Date(randomDate(new Date('2024-01-01'), new Date()))
|
|
50
50
|
await db.insert(todos).values({
|
|
51
51
|
title: TODO_TITLES[i % TODO_TITLES.length],
|