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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-fullstack-scaffold",
3
- "version": "0.4.1",
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",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "biomimic-todo-app",
3
3
  "private": true,
4
- "version": "0.1.0",
4
+ "version": "0.1.1",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "biomimic": "./dist/cli/index.js"
@@ -19,7 +19,9 @@ export const App: React.FC<{ presetId?: string }> = ({ presetId = 'todo' }) => {
19
19
  mobileTabs={mobileTabs}
20
20
  >
21
21
  <Routes>
22
- <Route path="/" element={<Navigate to={defaultRoute} replace />} />
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
- <div className="flex items-center gap-4 mt-3 text-sm text-gray-400">
149
- {content.publishedAt && <span>{formatDate(content.publishedAt)}</span>}
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 p-5 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"
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
- <div className="flex items-start justify-between gap-3 mb-3">
149
- <div className="flex items-center gap-3">
150
- <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">
151
- {plugin.name.charAt(0).toUpperCase()}
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
- <div className="min-w-0">
154
- <h3 className="text-base font-semibold text-gray-900 truncate group-hover:text-violet-700 transition-colors">
155
- {plugin.name}
156
- </h3>
157
- <p className="text-xs text-gray-400">by {plugin.authorName}</p>
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
- <StatusBadge label={`v${plugin.version}`} colorScheme="purple" />
161
- </div>
162
- <p className="text-sm text-gray-500 line-clamp-2 mb-4 leading-relaxed">
163
- {plugin.description}
164
- </p>
165
- <div className="flex items-center justify-between">
166
- <div className="flex items-center gap-3 text-xs text-gray-400">
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.slice(0, 2).map(tag => (
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 property="og:description" content="Manage your todo items with full CRUD operations" />
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
- return PRESET_UI_CONFIGS[id as PresetType] ?? PRESET_UI_CONFIGS.todo
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> {