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.
- package/dist/cli/index.js +1673 -696
- package/dist/cli/index.js.map +1 -1
- package/package.json +16 -10
- package/template/.husky/pre-commit +1 -1
- package/template/modules.config.ts +29 -2
- package/template/package.json +12 -12
- package/template/patches/{typescript+5.8.3.patch → typescript+5.9.3.patch} +2 -4
- package/template/playwright.config.ts +7 -1
- package/template/src/admin/App.tsx +2 -2
- package/template/src/admin/components/CaptchaModal.tsx +7 -9
- package/template/src/admin/layouts/Header.tsx +56 -22
- package/template/src/admin/layouts/Layout.tsx +14 -9
- package/template/src/admin/layouts/Sidebar.tsx +122 -105
- package/template/src/admin/pages/CategoryManagementPage.tsx +241 -0
- package/template/src/admin/pages/ContentPage.tsx +2 -0
- package/template/src/admin/pages/DashboardPage.tsx +2 -0
- package/template/src/admin/pages/DisputesPage.tsx +2 -0
- package/template/src/admin/pages/MediaTestPage.tsx +1 -1
- package/template/src/admin/pages/OrdersPage.tsx +2 -0
- package/template/src/admin/pages/PluginDashboardPage.tsx +297 -0
- package/template/src/admin/pages/PluginManagementPage.tsx +340 -0
- package/template/src/admin/pages/PluginReviewPage.tsx +255 -0
- package/template/src/admin/pages/SystemLogsPage.tsx +11 -2
- package/template/src/admin/pages/TicketsPage.tsx +2 -0
- package/template/src/admin/pages/UsersPage.tsx +3 -2
- package/template/src/admin/stores/adminStore.ts +5 -0
- package/template/src/cli/index.ts +17 -19
- package/template/src/cli/modules/auth/index.ts +65 -0
- package/template/src/cli/modules/config/index.ts +74 -77
- package/template/src/cli/modules/index.ts +28 -6
- package/template/src/cli/modules/notification/index.ts +95 -79
- package/template/src/cli/modules/plugin/index.ts +111 -0
- package/template/src/cli/modules/todo/index.ts +99 -51
- package/template/src/cli/utils/auto-command.ts +7 -25
- package/template/src/cli/utils/index.ts +3 -1
- package/template/src/client/App.tsx +36 -16
- package/template/src/client/Layout.tsx +67 -10
- package/template/src/client/components/AuthButton.tsx +25 -18
- package/template/src/client/components/BottomTabBar.tsx +107 -0
- package/template/src/client/components/Navigation.tsx +162 -52
- package/template/src/client/components/__tests__/App.test.tsx +48 -32
- package/template/src/client/components/__tests__/AuthButton.test.tsx +78 -77
- package/template/src/client/components/__tests__/Navigation.test.tsx +31 -20
- package/template/src/client/components/index.ts +1 -0
- package/template/src/client/contexts/PresetContext.tsx +10 -0
- package/template/src/client/main.tsx +63 -8
- package/template/src/client/pages/CartPage.tsx +244 -0
- package/template/src/client/pages/CategoriesPage.tsx +100 -0
- package/template/src/client/pages/ContentDetailPage.tsx +9 -14
- package/template/src/client/pages/ContentListPage.tsx +4 -11
- package/template/src/client/pages/DashboardPage.tsx +261 -0
- package/template/src/client/pages/DeveloperDashboardPage.tsx +211 -0
- package/template/src/client/pages/LoginPage.tsx +127 -0
- package/template/src/client/pages/OrdersPage.tsx +196 -0
- package/template/src/client/pages/PluginDetailPage.tsx +345 -0
- package/template/src/client/pages/PluginsPage.tsx +223 -0
- package/template/src/client/pages/ProfilePage.tsx +206 -0
- package/template/src/client/pages/PublishPage.tsx +336 -0
- package/template/src/client/pages/RegisterPage.tsx +136 -0
- package/template/src/client/pages/SearchPage.tsx +204 -0
- package/template/src/client/pages/SettingsPage.tsx +220 -0
- package/template/src/client/pages/TopicsPage.tsx +180 -0
- package/template/src/client/pages/__tests__/LoginPage.test.tsx +170 -0
- package/template/src/client/pages/__tests__/RegisterPage.test.tsx +168 -0
- package/template/src/client/preset-ui-config.ts +492 -0
- package/template/src/client/services/apiClient.ts +14 -4
- package/template/src/client/stores/__tests__/authStore.test.ts +293 -38
- package/template/src/client/stores/__tests__/todoStore.test.ts +7 -17
- package/template/src/client/stores/authStore.ts +58 -5
- package/template/src/client/stores/chatWSStore.ts +9 -0
- package/template/src/client/stores/notificationStore.ts +4 -0
- package/template/src/client/stores/pluginStore.ts +279 -0
- package/template/src/client/stores/todoStore.ts +2 -6
- package/template/src/server/core/__tests__/isr-cache.test.ts +117 -0
- package/template/src/server/core/__tests__/isr-invalidation.test.ts +72 -0
- package/template/src/server/core/__tests__/ssr-renderer.test.ts +89 -0
- package/template/src/server/core/isr-cache.ts +239 -0
- package/template/src/server/core/isr-invalidation.ts +45 -0
- package/template/src/server/core/module-loader.ts +14 -7
- package/template/src/server/core/ssr-renderer.ts +240 -0
- package/template/src/server/db/init.ts +257 -10
- package/template/src/server/db/schema/developers.ts +20 -0
- package/template/src/server/db/schema/index.ts +2 -0
- package/template/src/server/db/schema/plugins.ts +114 -0
- package/template/src/server/db/test-setup.ts +91 -0
- package/template/src/server/entries/cloudflare.ts +79 -7
- package/template/src/server/entries/node.ts +48 -5
- package/template/src/server/middleware/__tests__/captcha.test.ts +23 -13
- package/template/src/server/middleware/auth.ts +8 -1
- package/template/src/server/middleware/captcha.ts +14 -4
- package/template/src/server/middleware/rate-limit.ts +7 -2
- package/template/src/server/module-admin/module.ts +9 -5
- package/template/src/server/module-admin/routes/admin-notification-routes.ts +43 -14
- package/template/src/server/module-admin/routes/admin-routes.ts +0 -2
- package/template/src/server/module-admin/routes/client-auth-routes.ts +90 -0
- package/template/src/server/module-admin/routes/dashboard-routes.ts +79 -0
- package/template/src/server/module-admin/services/admin-service.ts +68 -11
- package/template/src/server/module-auth/__tests__/auth-service.test.ts +239 -0
- package/template/src/server/module-auth/index.ts +7 -0
- package/template/src/server/module-auth/module.ts +40 -0
- package/template/src/server/module-auth/routes/auth-routes.ts +94 -0
- package/template/src/server/module-auth/routes/profile-routes.ts +31 -0
- package/template/src/server/module-auth/services/auth-service.ts +100 -0
- package/template/src/server/module-content/module.ts +10 -4
- package/template/src/server/module-content/routes/public-content-routes.ts +2 -2
- package/template/src/server/module-content/routes/topics-routes.ts +205 -0
- package/template/src/server/module-content/services/content-service.ts +18 -2
- package/template/src/server/module-dispute/routes/dispute-routes.ts +2 -1
- package/template/src/server/module-dispute/services/dispute-service.ts +1 -1
- package/template/src/server/module-notifications/__tests__/sse-rpc.test.ts +14 -14
- package/template/src/server/module-notifications/routes/notification-routes.ts +34 -8
- package/template/src/server/module-order/__tests__/order-route.test.ts +22 -2
- package/template/src/server/module-order/module.ts +15 -0
- package/template/src/server/module-order/routes/cart-routes.ts +103 -0
- package/template/src/server/module-order/routes/orders-mock-routes.ts +67 -0
- package/template/src/server/module-order/services/order-service.ts +1 -1
- package/template/src/server/module-plugin/__tests__/plugin-query-service.test.ts +203 -0
- package/template/src/server/module-plugin/__tests__/plugin-service.test.ts +234 -0
- package/template/src/server/module-plugin/index.ts +2 -0
- package/template/src/server/module-plugin/module.ts +52 -0
- package/template/src/server/module-plugin/routes/plugin-admin-routes.ts +261 -0
- package/template/src/server/module-plugin/routes/plugin-routes.ts +354 -0
- package/template/src/server/module-plugin/services/admin-category-service.ts +99 -0
- package/template/src/server/module-plugin/services/admin-plugin-service.ts +170 -0
- package/template/src/server/module-plugin/services/admin-stats-service.ts +41 -0
- package/template/src/server/module-plugin/services/plugin-query-service.ts +360 -0
- package/template/src/server/module-plugin/services/plugin-review-service.ts +95 -0
- package/template/src/server/module-plugin/services/plugin-service.ts +163 -0
- package/template/src/server/module-ticket/services/ticket-service.ts +1 -1
- package/template/src/server/module-todos/routes/todos-routes.ts +0 -4
- package/template/src/server/route-registry.ts +16 -1
- package/template/src/server/test-utils/test-client.ts +1 -2
- package/template/src/server/utils/auth.ts +6 -0
- package/template/src/server/utils/json.ts +13 -0
- package/template/src/shared/core/module-manifest.ts +3 -6
- package/template/src/shared/modules/auth/index.ts +12 -0
- package/template/src/shared/modules/auth/schemas.ts +50 -0
- package/template/src/shared/modules/cart/index.ts +1 -0
- package/template/src/shared/modules/cart/schemas.ts +41 -0
- package/template/src/shared/modules/community/index.ts +1 -0
- package/template/src/shared/modules/community/schemas.ts +58 -0
- package/template/src/shared/modules/dashboard/index.ts +1 -0
- package/template/src/shared/modules/dashboard/schemas.ts +35 -0
- package/template/src/shared/modules/index.ts +41 -0
- package/template/src/shared/modules/order/schemas.ts +29 -0
- package/template/src/shared/modules/plugins/index.ts +48 -0
- package/template/src/shared/modules/plugins/schemas.ts +227 -0
- package/template/src/shared/schemas/index.ts +130 -0
- package/template/tests/e2e/todo.spec.ts +23 -18
- package/template/tests/e2e/visual-screenshots.spec.ts +1824 -0
- package/template/uploads/.gitkeep +0 -0
- package/template/vite.config.ts +2 -1
- package/template/vitest.setup.ts +11 -0
- package/template/wrangler.toml +4 -3
- 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
|
|
5
|
-
|
|
6
|
-
const
|
|
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"
|
|
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
|
-
<
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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 {
|
|
3
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
<
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
<
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
41
|
-
render(<App />)
|
|
42
|
-
expect(screen.getByTestId('app-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
|
58
|
-
render(<App />)
|
|
59
|
-
expect(screen.getByTestId('
|
|
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
|
|
63
|
-
render(<App />)
|
|
64
|
-
expect(screen.getByTestId('
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
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
|
})
|