create-fullstack-scaffold 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/dist/cli/index.js +1673 -696
  2. package/dist/cli/index.js.map +1 -1
  3. package/package.json +16 -10
  4. package/template/.husky/pre-commit +1 -1
  5. package/template/modules.config.ts +29 -2
  6. package/template/package.json +12 -12
  7. package/template/patches/{typescript+5.8.3.patch → typescript+5.9.3.patch} +2 -4
  8. package/template/playwright.config.ts +7 -1
  9. package/template/src/admin/App.tsx +2 -2
  10. package/template/src/admin/components/CaptchaModal.tsx +7 -9
  11. package/template/src/admin/layouts/Header.tsx +56 -22
  12. package/template/src/admin/layouts/Layout.tsx +14 -9
  13. package/template/src/admin/layouts/Sidebar.tsx +122 -105
  14. package/template/src/admin/pages/CategoryManagementPage.tsx +241 -0
  15. package/template/src/admin/pages/ContentPage.tsx +2 -0
  16. package/template/src/admin/pages/DashboardPage.tsx +2 -0
  17. package/template/src/admin/pages/DisputesPage.tsx +2 -0
  18. package/template/src/admin/pages/MediaTestPage.tsx +1 -1
  19. package/template/src/admin/pages/OrdersPage.tsx +2 -0
  20. package/template/src/admin/pages/PluginDashboardPage.tsx +297 -0
  21. package/template/src/admin/pages/PluginManagementPage.tsx +340 -0
  22. package/template/src/admin/pages/PluginReviewPage.tsx +255 -0
  23. package/template/src/admin/pages/SystemLogsPage.tsx +11 -2
  24. package/template/src/admin/pages/TicketsPage.tsx +2 -0
  25. package/template/src/admin/pages/UsersPage.tsx +3 -2
  26. package/template/src/admin/stores/adminStore.ts +5 -0
  27. package/template/src/cli/index.ts +17 -19
  28. package/template/src/cli/modules/auth/index.ts +65 -0
  29. package/template/src/cli/modules/config/index.ts +74 -77
  30. package/template/src/cli/modules/index.ts +28 -6
  31. package/template/src/cli/modules/notification/index.ts +95 -79
  32. package/template/src/cli/modules/plugin/index.ts +111 -0
  33. package/template/src/cli/modules/todo/index.ts +99 -51
  34. package/template/src/cli/utils/auto-command.ts +7 -25
  35. package/template/src/cli/utils/index.ts +3 -1
  36. package/template/src/client/App.tsx +36 -16
  37. package/template/src/client/Layout.tsx +67 -10
  38. package/template/src/client/components/AuthButton.tsx +25 -18
  39. package/template/src/client/components/BottomTabBar.tsx +107 -0
  40. package/template/src/client/components/Navigation.tsx +162 -52
  41. package/template/src/client/components/__tests__/App.test.tsx +48 -32
  42. package/template/src/client/components/__tests__/AuthButton.test.tsx +78 -77
  43. package/template/src/client/components/__tests__/Navigation.test.tsx +31 -20
  44. package/template/src/client/components/index.ts +1 -0
  45. package/template/src/client/contexts/PresetContext.tsx +10 -0
  46. package/template/src/client/main.tsx +63 -8
  47. package/template/src/client/pages/CartPage.tsx +244 -0
  48. package/template/src/client/pages/CategoriesPage.tsx +100 -0
  49. package/template/src/client/pages/ContentDetailPage.tsx +9 -14
  50. package/template/src/client/pages/ContentListPage.tsx +4 -11
  51. package/template/src/client/pages/DashboardPage.tsx +261 -0
  52. package/template/src/client/pages/DeveloperDashboardPage.tsx +211 -0
  53. package/template/src/client/pages/LoginPage.tsx +127 -0
  54. package/template/src/client/pages/OrdersPage.tsx +196 -0
  55. package/template/src/client/pages/PluginDetailPage.tsx +345 -0
  56. package/template/src/client/pages/PluginsPage.tsx +223 -0
  57. package/template/src/client/pages/ProfilePage.tsx +206 -0
  58. package/template/src/client/pages/PublishPage.tsx +336 -0
  59. package/template/src/client/pages/RegisterPage.tsx +136 -0
  60. package/template/src/client/pages/SearchPage.tsx +204 -0
  61. package/template/src/client/pages/SettingsPage.tsx +220 -0
  62. package/template/src/client/pages/TopicsPage.tsx +180 -0
  63. package/template/src/client/pages/__tests__/LoginPage.test.tsx +170 -0
  64. package/template/src/client/pages/__tests__/RegisterPage.test.tsx +168 -0
  65. package/template/src/client/preset-ui-config.ts +492 -0
  66. package/template/src/client/services/apiClient.ts +14 -4
  67. package/template/src/client/stores/__tests__/authStore.test.ts +293 -38
  68. package/template/src/client/stores/__tests__/todoStore.test.ts +7 -17
  69. package/template/src/client/stores/authStore.ts +58 -5
  70. package/template/src/client/stores/chatWSStore.ts +9 -0
  71. package/template/src/client/stores/notificationStore.ts +4 -0
  72. package/template/src/client/stores/pluginStore.ts +279 -0
  73. package/template/src/client/stores/todoStore.ts +2 -6
  74. package/template/src/server/core/__tests__/isr-cache.test.ts +117 -0
  75. package/template/src/server/core/__tests__/isr-invalidation.test.ts +72 -0
  76. package/template/src/server/core/__tests__/ssr-renderer.test.ts +89 -0
  77. package/template/src/server/core/isr-cache.ts +239 -0
  78. package/template/src/server/core/isr-invalidation.ts +45 -0
  79. package/template/src/server/core/module-loader.ts +14 -7
  80. package/template/src/server/core/ssr-renderer.ts +240 -0
  81. package/template/src/server/db/init.ts +257 -10
  82. package/template/src/server/db/schema/developers.ts +20 -0
  83. package/template/src/server/db/schema/index.ts +2 -0
  84. package/template/src/server/db/schema/plugins.ts +114 -0
  85. package/template/src/server/db/test-setup.ts +91 -0
  86. package/template/src/server/entries/cloudflare.ts +79 -7
  87. package/template/src/server/entries/node.ts +48 -5
  88. package/template/src/server/middleware/__tests__/captcha.test.ts +23 -13
  89. package/template/src/server/middleware/auth.ts +8 -1
  90. package/template/src/server/middleware/captcha.ts +14 -4
  91. package/template/src/server/middleware/rate-limit.ts +7 -2
  92. package/template/src/server/module-admin/module.ts +9 -5
  93. package/template/src/server/module-admin/routes/admin-notification-routes.ts +43 -14
  94. package/template/src/server/module-admin/routes/admin-routes.ts +0 -2
  95. package/template/src/server/module-admin/routes/client-auth-routes.ts +90 -0
  96. package/template/src/server/module-admin/routes/dashboard-routes.ts +79 -0
  97. package/template/src/server/module-admin/services/admin-service.ts +68 -11
  98. package/template/src/server/module-auth/__tests__/auth-service.test.ts +239 -0
  99. package/template/src/server/module-auth/index.ts +7 -0
  100. package/template/src/server/module-auth/module.ts +40 -0
  101. package/template/src/server/module-auth/routes/auth-routes.ts +94 -0
  102. package/template/src/server/module-auth/routes/profile-routes.ts +31 -0
  103. package/template/src/server/module-auth/services/auth-service.ts +100 -0
  104. package/template/src/server/module-content/module.ts +10 -4
  105. package/template/src/server/module-content/routes/public-content-routes.ts +2 -2
  106. package/template/src/server/module-content/routes/topics-routes.ts +205 -0
  107. package/template/src/server/module-content/services/content-service.ts +18 -2
  108. package/template/src/server/module-dispute/routes/dispute-routes.ts +2 -1
  109. package/template/src/server/module-dispute/services/dispute-service.ts +1 -1
  110. package/template/src/server/module-notifications/__tests__/sse-rpc.test.ts +14 -14
  111. package/template/src/server/module-notifications/routes/notification-routes.ts +34 -8
  112. package/template/src/server/module-order/__tests__/order-route.test.ts +22 -2
  113. package/template/src/server/module-order/module.ts +15 -0
  114. package/template/src/server/module-order/routes/cart-routes.ts +103 -0
  115. package/template/src/server/module-order/routes/orders-mock-routes.ts +67 -0
  116. package/template/src/server/module-order/services/order-service.ts +1 -1
  117. package/template/src/server/module-plugin/__tests__/plugin-query-service.test.ts +203 -0
  118. package/template/src/server/module-plugin/__tests__/plugin-service.test.ts +234 -0
  119. package/template/src/server/module-plugin/index.ts +2 -0
  120. package/template/src/server/module-plugin/module.ts +52 -0
  121. package/template/src/server/module-plugin/routes/plugin-admin-routes.ts +261 -0
  122. package/template/src/server/module-plugin/routes/plugin-routes.ts +354 -0
  123. package/template/src/server/module-plugin/services/admin-category-service.ts +99 -0
  124. package/template/src/server/module-plugin/services/admin-plugin-service.ts +170 -0
  125. package/template/src/server/module-plugin/services/admin-stats-service.ts +41 -0
  126. package/template/src/server/module-plugin/services/plugin-query-service.ts +360 -0
  127. package/template/src/server/module-plugin/services/plugin-review-service.ts +95 -0
  128. package/template/src/server/module-plugin/services/plugin-service.ts +163 -0
  129. package/template/src/server/module-ticket/services/ticket-service.ts +1 -1
  130. package/template/src/server/module-todos/routes/todos-routes.ts +0 -4
  131. package/template/src/server/route-registry.ts +16 -1
  132. package/template/src/server/test-utils/test-client.ts +1 -2
  133. package/template/src/server/utils/auth.ts +6 -0
  134. package/template/src/server/utils/json.ts +13 -0
  135. package/template/src/shared/core/module-manifest.ts +3 -6
  136. package/template/src/shared/modules/auth/index.ts +12 -0
  137. package/template/src/shared/modules/auth/schemas.ts +50 -0
  138. package/template/src/shared/modules/cart/index.ts +1 -0
  139. package/template/src/shared/modules/cart/schemas.ts +41 -0
  140. package/template/src/shared/modules/community/index.ts +1 -0
  141. package/template/src/shared/modules/community/schemas.ts +58 -0
  142. package/template/src/shared/modules/dashboard/index.ts +1 -0
  143. package/template/src/shared/modules/dashboard/schemas.ts +35 -0
  144. package/template/src/shared/modules/index.ts +41 -0
  145. package/template/src/shared/modules/order/schemas.ts +29 -0
  146. package/template/src/shared/modules/plugins/index.ts +48 -0
  147. package/template/src/shared/modules/plugins/schemas.ts +227 -0
  148. package/template/src/shared/schemas/index.ts +130 -0
  149. package/template/tests/e2e/todo.spec.ts +23 -18
  150. package/template/tests/e2e/visual-screenshots.spec.ts +1824 -0
  151. package/template/uploads/.gitkeep +0 -0
  152. package/template/vite.config.ts +2 -1
  153. package/template/vitest.setup.ts +11 -0
  154. package/template/wrangler.toml +4 -3
  155. package/template/package-lock.json +0 -14554
@@ -1,41 +1,48 @@
1
+ import { Link } from 'react-router-dom'
1
2
  import { useAuthStore } from '../stores/authStore'
2
3
 
3
4
  export function AuthButton() {
4
- const { isAuthenticated, logout, setToken } = useAuthStore()
5
-
6
- const handleLogin = () => {
7
- // For development, use a test token
8
- // In production, this should call an actual login API
9
- const testToken = 'user-token'
10
- setToken(testToken)
11
- window.location.reload()
12
- }
5
+ const isAuthenticated = useAuthStore(state => state.isAuthenticated)
6
+ const user = useAuthStore(state => state.user)
7
+ const logout = useAuthStore(state => state.logout)
13
8
 
14
9
  const handleLogout = () => {
15
10
  logout()
16
- window.location.reload()
17
11
  }
18
12
 
19
13
  if (isAuthenticated) {
20
14
  return (
21
15
  <div className="flex items-center gap-2">
22
- <span className="text-sm text-gray-600">已登录</span>
16
+ <span className="text-sm text-gray-600" data-testid="auth-username">
17
+ {user?.username}
18
+ </span>
23
19
  <button
24
20
  onClick={handleLogout}
21
+ data-testid="auth-logout"
25
22
  className="px-3 py-1 text-sm bg-red-500 text-white rounded hover:bg-red-600"
26
23
  >
27
- 退出
24
+ Sign Out
28
25
  </button>
29
26
  </div>
30
27
  )
31
28
  }
32
29
 
33
30
  return (
34
- <button
35
- onClick={handleLogin}
36
- className="px-3 py-1 text-sm bg-blue-500 text-white rounded hover:bg-blue-600"
37
- >
38
- 登录
39
- </button>
31
+ <div className="flex items-center gap-2">
32
+ <Link
33
+ to="/login"
34
+ data-testid="auth-login"
35
+ className="px-3 py-1 text-sm bg-blue-500 text-white rounded hover:bg-blue-600"
36
+ >
37
+ Sign In
38
+ </Link>
39
+ <Link
40
+ to="/register"
41
+ data-testid="auth-register"
42
+ className="px-3 py-1 text-sm border border-gray-300 text-gray-700 rounded hover:bg-gray-50"
43
+ >
44
+ Sign Up
45
+ </Link>
46
+ </div>
40
47
  )
41
48
  }
@@ -0,0 +1,107 @@
1
+ import { useState } from 'react'
2
+ import { NavLink, useLocation } from 'react-router-dom'
3
+ import {
4
+ Sun,
5
+ Calendar,
6
+ FolderKanban,
7
+ Compass,
8
+ Puzzle,
9
+ Tags,
10
+ Code,
11
+ Home,
12
+ ShoppingCart,
13
+ Package,
14
+ Hash,
15
+ TrendingUp,
16
+ Bell,
17
+ User,
18
+ LayoutDashboard,
19
+ Settings,
20
+ Search,
21
+ PlusCircle,
22
+ type LucideIcon,
23
+ } from 'lucide-react'
24
+ import type { TabItem, PresetTheme } from '../preset-ui-config'
25
+
26
+ interface BottomTabBarProps {
27
+ tabs: TabItem[]
28
+ theme?: PresetTheme
29
+ }
30
+
31
+ const ICON_MAP: Record<string, LucideIcon> = {
32
+ Sun,
33
+ Calendar,
34
+ FolderKanban,
35
+ Compass,
36
+ Puzzle,
37
+ Tags,
38
+ Code,
39
+ Home,
40
+ ShoppingCart,
41
+ Package,
42
+ Hash,
43
+ TrendingUp,
44
+ Bell,
45
+ User,
46
+ LayoutDashboard,
47
+ Settings,
48
+ Search,
49
+ PlusCircle,
50
+ }
51
+
52
+ export const BottomTabBar: React.FC<BottomTabBarProps> = ({ tabs, theme }) => {
53
+ const location = useLocation()
54
+ const [activeTab, setActiveTab] = useState(() => {
55
+ const match = tabs.find(tab => location.pathname.startsWith(tab.path))
56
+ return match?.path || tabs[0]?.path || ''
57
+ })
58
+
59
+ const primaryColor = theme?.primaryColor ?? '#6366f1'
60
+
61
+ if (tabs.length === 0) return null
62
+
63
+ return (
64
+ <nav
65
+ className="md:hidden fixed bottom-0 left-0 right-0 z-50 bg-white/95 backdrop-blur-md border-t border-gray-200 safe-area-inset-bottom"
66
+ style={{
67
+ paddingBottom: 'env(safe-area-inset-bottom, 0px)',
68
+ }}
69
+ data-testid="bottom-tab-bar"
70
+ >
71
+ <div className="flex items-center justify-around h-16 px-2">
72
+ {tabs.map(tab => {
73
+ const Icon = ICON_MAP[tab.icon] || LayoutDashboard
74
+ const isActive = activeTab === tab.path || location.pathname.startsWith(tab.path)
75
+
76
+ return (
77
+ <NavLink
78
+ key={tab.path}
79
+ to={tab.path}
80
+ onClick={() => setActiveTab(tab.path)}
81
+ data-testid={`bottom-tab-${tab.label.toLowerCase()}`}
82
+ className={`flex flex-col items-center justify-center gap-0.5 flex-1 py-1 rounded-xl transition-all duration-200 min-w-0 ${
83
+ isActive ? '' : 'text-gray-400 hover:text-gray-600'
84
+ }`}
85
+ style={{ color: isActive ? primaryColor : undefined }}
86
+ >
87
+ <Icon className={`w-5 h-5 transition-transform ${isActive ? 'scale-110' : ''}`} />
88
+ <span
89
+ className={`text-[10px] font-medium leading-tight truncate w-full text-center px-1 ${
90
+ isActive ? 'font-semibold' : ''
91
+ }`}
92
+ >
93
+ {tab.label}
94
+ </span>
95
+ {isActive && (
96
+ <div
97
+ className="absolute top-0 w-8 h-0.5 rounded-full"
98
+ style={{ backgroundColor: primaryColor }}
99
+ />
100
+ )}
101
+ </NavLink>
102
+ )
103
+ })}
104
+ </div>
105
+ </nav>
106
+ )
107
+ }
@@ -1,64 +1,174 @@
1
- import { NavLink } from 'react-router-dom'
2
- import { CheckCircle, Bell, Plug, Rocket, Github, FileText } from 'lucide-react'
3
- import { AuthButton } from './AuthButton'
1
+ import { NavLink, Link } from 'react-router-dom'
2
+ import { Rocket, Sparkles, Search, ShoppingCart, UserCircle, LogIn } from 'lucide-react'
3
+ import { useAuthStore } from '../stores/authStore'
4
+ import type { PresetTheme, NavigationConfig, AuthStyle, ClientNavItem } from '../preset-ui-config'
4
5
 
5
- type RouteKey = 'todos' | 'notifications' | 'websocket' | 'content'
6
+ interface NavigationProps {
7
+ preset?: string
8
+ items?: ClientNavItem[]
9
+ theme?: PresetTheme
10
+ navigation?: NavigationConfig
11
+ }
12
+
13
+ function AuthSection({ style }: { style: AuthStyle }) {
14
+ const isAuthenticated = useAuthStore(state => state.isAuthenticated)
15
+ const user = useAuthStore(state => state.user)
16
+ const logout = useAuthStore(state => state.logout)
17
+
18
+ if (style === 'none') return null
6
19
 
7
- const routes: Record<
8
- RouteKey,
9
- { label: string; icon: React.FC<{ className?: string }>; path: string }
10
- > = {
11
- todos: { label: 'Todo List', icon: CheckCircle, path: '/todos' },
12
- content: { label: 'Content', icon: FileText, path: '/content' },
13
- notifications: { label: 'Notifications', icon: Bell, path: '/notifications' },
14
- websocket: { label: 'WebSocket', icon: Plug, path: '/websocket' },
20
+ if (isAuthenticated) {
21
+ if (style === 'avatar') {
22
+ return (
23
+ <div className="flex items-center gap-2">
24
+ <div className="w-8 h-8 rounded-full bg-gray-300 flex items-center justify-center text-xs font-medium text-gray-700">
25
+ {user?.username?.[0]?.toUpperCase() || 'U'}
26
+ </div>
27
+ <button onClick={logout} className="text-xs text-gray-400 hover:text-gray-600">
28
+ Sign Out
29
+ </button>
30
+ </div>
31
+ )
32
+ }
33
+ return (
34
+ <div className="flex items-center gap-2">
35
+ <span className="text-sm text-gray-600">{user?.username}</span>
36
+ <button onClick={logout} className="text-xs text-gray-400 hover:text-red-500">
37
+ Sign Out
38
+ </button>
39
+ </div>
40
+ )
41
+ }
42
+
43
+ switch (style) {
44
+ case 'text-link':
45
+ return (
46
+ <Link
47
+ to="/login"
48
+ className="text-sm text-gray-500 hover:text-gray-900 flex items-center gap-1"
49
+ >
50
+ <LogIn className="w-4 h-4" />
51
+ Login
52
+ </Link>
53
+ )
54
+ case 'icon':
55
+ return (
56
+ <Link to="/login" className="text-gray-400 hover:text-gray-600">
57
+ <UserCircle className="w-6 h-6" />
58
+ </Link>
59
+ )
60
+ case 'avatar':
61
+ return (
62
+ <Link to="/login" className="text-sm text-gray-500 hover:text-gray-900">
63
+ Join
64
+ </Link>
65
+ )
66
+ case 'buttons':
67
+ default:
68
+ return (
69
+ <div className="flex items-center gap-2">
70
+ <Link
71
+ to="/login"
72
+ className="px-3 py-1 text-sm bg-blue-500 text-white rounded hover:bg-blue-600"
73
+ >
74
+ Sign In
75
+ </Link>
76
+ <Link
77
+ to="/register"
78
+ className="px-3 py-1 text-sm border border-gray-300 text-gray-700 rounded hover:bg-gray-50"
79
+ >
80
+ Sign Up
81
+ </Link>
82
+ </div>
83
+ )
84
+ }
15
85
  }
16
86
 
17
- export const Navigation: React.FC = () => {
87
+ export const Navigation: React.FC<NavigationProps> = ({
88
+ preset: _preset = 'todo',
89
+ items,
90
+ theme,
91
+ navigation,
92
+ }) => {
93
+ const navItems = navigation?.navItems === 'none' ? [] : items ?? []
94
+ const primaryColor = theme?.primaryColor ?? '#6366f1'
95
+ const logoText = theme?.logoText ?? 'Biomimic'
96
+ const showLogo = navigation?.showLogo !== false
97
+ const showSearch = navigation?.showSearch === true
98
+ const showCart = navigation?.showCart === true
99
+ const authStyle = navigation?.authStyle ?? 'buttons'
100
+
18
101
  return (
19
- <nav className="bg-white border-b border-gray-200 sticky top-0 z-50" data-testid="app-nav">
20
- <div className="max-w-6xl mx-auto px-6 h-16 flex items-center justify-between">
21
- <div className="flex items-center gap-8">
22
- <h1
23
- className="text-xl font-bold text-gray-900 flex items-center gap-2"
102
+ <nav
103
+ className="hidden md:block bg-white/80 backdrop-blur-md border-b border-gray-200 sticky top-0 z-50"
104
+ data-testid="app-nav"
105
+ >
106
+ <div className="max-w-7xl mx-auto px-4 h-16 flex items-center justify-between gap-3">
107
+ {showLogo && (
108
+ <NavLink
109
+ to="/"
110
+ className="flex items-center gap-2 group shrink-0"
24
111
  data-testid="app-title"
25
112
  >
26
- <Rocket className="w-6 h-6 text-blue-500" />
27
- Biomimic App
28
- </h1>
29
- <div className="flex items-center gap-1">
30
- {(Object.keys(routes) as RouteKey[]).map(route => {
31
- const Icon = routes[route].icon
32
- return (
33
- <NavLink
34
- key={route}
35
- to={routes[route].path}
36
- data-testid={`nav-${route}-button`}
37
- className={({ isActive }) =>
38
- `flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
39
- isActive ? 'bg-blue-50 text-blue-600' : 'text-gray-600 hover:bg-gray-100'
40
- }`
41
- }
42
- >
43
- <Icon className="w-4 h-4" />
44
- {routes[route].label}
45
- </NavLink>
46
- )
47
- })}
113
+ <div
114
+ className="w-8 h-8 rounded-lg flex items-center justify-center shadow-sm group-hover:shadow-md transition-shadow"
115
+ style={{ backgroundColor: primaryColor }}
116
+ >
117
+ <Rocket className="w-4 h-4 text-white" />
118
+ </div>
119
+ <span className="text-lg font-semibold text-gray-900 tracking-tight whitespace-nowrap">
120
+ {logoText}
121
+ </span>
122
+ <Sparkles className="w-3.5 h-3.5 shrink-0" style={{ color: primaryColor }} />
123
+ </NavLink>
124
+ )}
125
+
126
+ {showSearch && (
127
+ <div className="flex-1 max-w-md mx-4">
128
+ <div className="relative">
129
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
130
+ <input
131
+ type="text"
132
+ placeholder="Search..."
133
+ className="w-full pl-10 pr-4 py-2 bg-gray-50 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-200 focus:border-blue-300"
134
+ />
135
+ </div>
48
136
  </div>
137
+ )}
138
+
139
+ <div className="flex items-center gap-0.5 overflow-x-auto flex-1 min-w-0 scrollbar-hide [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]">
140
+ {navItems.map(item => (
141
+ <NavLink
142
+ key={item.path}
143
+ to={item.path}
144
+ data-testid={`nav-${item.label.toLowerCase().replace(/\s+/g, '-')}-button`}
145
+ className={({ isActive }: { isActive: boolean }) =>
146
+ `px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200 shrink-0 whitespace-nowrap ${
147
+ isActive ? 'text-white' : 'text-gray-500 hover:text-gray-900 hover:bg-gray-50'
148
+ }`
149
+ }
150
+ style={
151
+ (({ isActive }: { isActive: boolean }) =>
152
+ isActive
153
+ ? { backgroundColor: `${primaryColor}15`, color: primaryColor }
154
+ : undefined) as never
155
+ }
156
+ >
157
+ {item.label}
158
+ </NavLink>
159
+ ))}
49
160
  </div>
50
- <div className="flex items-center gap-4">
51
- <AuthButton />
52
- <a
53
- href="https://github.com"
54
- target="_blank"
55
- rel="noopener noreferrer"
56
- data-testid="github-link"
57
- className="text-gray-400 hover:text-gray-600 transition-colors"
58
- >
59
- <Github className="w-5 h-5" />
60
- </a>
61
- </div>
161
+
162
+ {showCart && (
163
+ <Link to="/cart" className="relative text-gray-500 hover:text-gray-700 mr-2">
164
+ <ShoppingCart className="w-5 h-5" />
165
+ <span className="absolute -top-1 -right-1 w-4 h-4 bg-red-500 text-white text-[10px] rounded-full flex items-center justify-center">
166
+ 3
167
+ </span>
168
+ </Link>
169
+ )}
170
+
171
+ <AuthSection style={authStyle} />
62
172
  </div>
63
173
  </nav>
64
174
  )
@@ -15,6 +15,22 @@ vi.mock('@client/pages/WebSocketPage', () => ({
15
15
  WebSocketPage: () => <div data-testid="websocket-page">WebSocket Page</div>,
16
16
  }))
17
17
 
18
+ vi.mock('@client/pages/LoginPage', () => ({
19
+ LoginPage: () => <div data-testid="login-page">Login Page</div>,
20
+ }))
21
+
22
+ vi.mock('@client/pages/RegisterPage', () => ({
23
+ RegisterPage: () => <div data-testid="register-page">Register Page</div>,
24
+ }))
25
+
26
+ vi.mock('@client/pages/DashboardPage', () => ({
27
+ DashboardPage: () => <div data-testid="dashboard-page">Dashboard Page</div>,
28
+ }))
29
+
30
+ vi.mock('@client/pages/SettingsPage', () => ({
31
+ SettingsPage: () => <div data-testid="settings-page">Settings Page</div>,
32
+ }))
33
+
18
34
  describe('App Component', () => {
19
35
  beforeEach(() => {
20
36
  vi.clearAllMocks()
@@ -25,55 +41,55 @@ describe('App Component', () => {
25
41
  })
26
42
 
27
43
  describe('Initial Render', () => {
28
- it('should render title with correct text', () => {
29
- render(<App />)
30
- const titleElement = screen.getByTestId('app-title')
31
- expect(titleElement).toBeInTheDocument()
32
- expect(titleElement).toHaveTextContent('Biomimic App')
33
- })
34
-
35
44
  it('should render navigation', () => {
36
- render(<App />)
45
+ render(<App presetId="todo" />)
37
46
  expect(screen.getByTestId('app-nav')).toBeInTheDocument()
38
47
  })
39
48
 
40
- it('should render footer', () => {
41
- render(<App />)
42
- expect(screen.getByTestId('app-footer')).toBeInTheDocument()
49
+ it('should render main content area', () => {
50
+ render(<App presetId="todo" />)
51
+ expect(screen.getByTestId('app-main')).toBeInTheDocument()
43
52
  })
44
- })
45
53
 
46
- describe('Navigation Links', () => {
47
- it('should render todos nav link', () => {
48
- render(<App />)
49
- expect(screen.getByTestId('nav-todos-button')).toBeInTheDocument()
54
+ it('should render container', () => {
55
+ render(<App presetId="todo" />)
56
+ expect(screen.getByTestId('app-container')).toBeInTheDocument()
50
57
  })
58
+ })
51
59
 
52
- it('should render notifications nav link', () => {
53
- render(<App />)
54
- expect(screen.getByTestId('nav-notifications-button')).toBeInTheDocument()
60
+ describe('Layout', () => {
61
+ it('should render with todo preset', () => {
62
+ render(<App presetId="todo" />)
63
+ expect(screen.getByTestId('app-container')).toBeInTheDocument()
64
+ expect(screen.getByTestId('app-nav')).toBeInTheDocument()
55
65
  })
56
66
 
57
- it('should render websocket nav link', () => {
58
- render(<App />)
59
- expect(screen.getByTestId('nav-websocket-button')).toBeInTheDocument()
67
+ it('should render with plugin preset', () => {
68
+ render(<App presetId="plugin" />)
69
+ expect(screen.getByTestId('app-container')).toBeInTheDocument()
60
70
  })
61
71
 
62
- it('should render github link', () => {
63
- render(<App />)
64
- expect(screen.getByTestId('github-link')).toBeInTheDocument()
72
+ it('should render with ecommerce preset', () => {
73
+ render(<App presetId="ecommerce" />)
74
+ expect(screen.getByTestId('app-container')).toBeInTheDocument()
65
75
  })
66
- })
67
76
 
68
- describe('Layout', () => {
69
- it('should render main content area', () => {
70
- render(<App />)
71
- expect(screen.getByTestId('app-main')).toBeInTheDocument()
77
+ it('should render with community preset', () => {
78
+ render(<App presetId="community" />)
79
+ expect(screen.getByTestId('app-container')).toBeInTheDocument()
72
80
  })
73
81
 
74
- it('should render container', () => {
75
- render(<App />)
82
+ it('should render with saas preset', () => {
83
+ render(<App presetId="saas" />)
76
84
  expect(screen.getByTestId('app-container')).toBeInTheDocument()
77
85
  })
78
86
  })
87
+
88
+ describe('Layout Features', () => {
89
+ it('should render nav and footer for top-nav preset', () => {
90
+ render(<App presetId="todo" />)
91
+ expect(screen.getByTestId('app-nav')).toBeInTheDocument()
92
+ expect(screen.getByTestId('app-footer')).toBeInTheDocument()
93
+ })
94
+ })
79
95
  })