create-fullstack-scaffold 0.4.1 → 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/dist/cli/index.js +34 -18
- package/dist/cli/index.js.map +1 -1
- package/package.json +2 -2
- 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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-fullstack-scaffold",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"create-fullstack-scaffold": "dist/cli/index.js"
|
|
@@ -107,6 +107,7 @@
|
|
|
107
107
|
"react-dom": "^18.3.1",
|
|
108
108
|
"react-router-dom": "^7.13.1",
|
|
109
109
|
"ws": "^8.18.3",
|
|
110
|
+
"tsx": "^4.21.0",
|
|
110
111
|
"zod": "^4.4.3",
|
|
111
112
|
"zustand": "^5.0.2"
|
|
112
113
|
},
|
|
@@ -150,7 +151,6 @@
|
|
|
150
151
|
"tailwindcss": "^4.2.1",
|
|
151
152
|
"ts-import": "^5.0.0-beta.1",
|
|
152
153
|
"tsup": "^8.5.0",
|
|
153
|
-
"tsx": "^4.21.0",
|
|
154
154
|
"typescript": "^5.8.3",
|
|
155
155
|
"typescript-eslint": "^8.25.0",
|
|
156
156
|
"vite": "^6.2.3",
|
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> {
|