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
@@ -0,0 +1,223 @@
1
+ import { useEffect } from 'react'
2
+ import { Link } from 'react-router-dom'
3
+ import { Helmet } from 'react-helmet-async'
4
+ import {
5
+ Package,
6
+ ChevronLeft,
7
+ ChevronRight,
8
+ Tag,
9
+ Download,
10
+ Sparkles,
11
+ TrendingUp,
12
+ Zap,
13
+ } from 'lucide-react'
14
+ import { usePluginStore } from '@client/stores/pluginStore'
15
+ import { LoadingSpinner, EmptyState } from '@client/components'
16
+ import { StatusBadge } from '@client/components'
17
+
18
+ export const PluginsPage: React.FC = () => {
19
+ const plugins = usePluginStore(state => state.plugins)
20
+ const loading = usePluginStore(state => state.loading)
21
+ const error = usePluginStore(state => state.error)
22
+ const categories = usePluginStore(state => state.categories)
23
+ const selectedCategory = usePluginStore(state => state.selectedCategory)
24
+ const pagination = usePluginStore(state => state.pagination)
25
+ const fetchPlugins = usePluginStore(state => state.fetchPlugins)
26
+ const fetchCategories = usePluginStore(state => state.fetchCategories)
27
+ const setSelectedCategory = usePluginStore(state => state.setSelectedCategory)
28
+
29
+ useEffect(() => {
30
+ fetchPlugins(1)
31
+ fetchCategories()
32
+ }, [fetchPlugins, fetchCategories])
33
+
34
+ useEffect(() => {
35
+ fetchPlugins(1)
36
+ }, [selectedCategory, fetchPlugins])
37
+
38
+ const totalPages = Math.ceil(pagination.total / pagination.limit)
39
+
40
+ return (
41
+ <div className="min-h-screen" data-testid="plugins-page">
42
+ <Helmet>
43
+ <title>Plugin Marketplace - Biomimic App</title>
44
+ <meta name="description" content="Browse and discover plugins for your application" />
45
+ </Helmet>
46
+
47
+ <div className="bg-gradient-to-br from-slate-900 via-purple-900 to-slate-900 pt-12 pb-16 px-6">
48
+ <div className="max-w-6xl mx-auto">
49
+ <div className="flex items-center gap-3 mb-4">
50
+ <div className="p-2.5 bg-gradient-to-br from-violet-500 to-fuchsia-500 rounded-xl shadow-lg shadow-violet-500/25">
51
+ <Package className="w-6 h-6 text-white" />
52
+ </div>
53
+ <h1 className="text-3xl font-bold text-white tracking-tight">Plugin Marketplace</h1>
54
+ </div>
55
+ <p className="text-purple-200/70 text-lg max-w-xl">
56
+ Extend your application with powerful plugins. Browse, install, and publish
57
+ community-built extensions.
58
+ </p>
59
+ <div className="mt-6 flex items-center gap-4">
60
+ <Link
61
+ to="/publish"
62
+ className="group flex items-center gap-2 px-5 py-2.5 bg-gradient-to-r from-violet-500 to-fuchsia-500 text-white rounded-xl hover:from-violet-400 hover:to-fuchsia-400 transition-all shadow-lg shadow-violet-500/25 hover:shadow-violet-500/40 text-sm font-semibold"
63
+ >
64
+ <Sparkles className="w-4 h-4 group-hover:rotate-12 transition-transform" />
65
+ Publish Plugin
66
+ </Link>
67
+ <Link
68
+ to="/search"
69
+ className="flex items-center gap-2 px-5 py-2.5 bg-white/10 text-white rounded-xl hover:bg-white/20 transition-all text-sm font-medium backdrop-blur-sm border border-white/10"
70
+ >
71
+ Search Plugins
72
+ </Link>
73
+ </div>
74
+ </div>
75
+ </div>
76
+
77
+ <div className="max-w-6xl mx-auto px-6 -mt-8">
78
+ {error && (
79
+ <div className="mb-6 p-4 bg-red-500/10 border border-red-500/20 text-red-300 rounded-2xl backdrop-blur-sm">
80
+ {error}
81
+ </div>
82
+ )}
83
+
84
+ <div className="flex gap-8">
85
+ <aside className="w-60 flex-shrink-0 hidden lg:block">
86
+ <div className="bg-white rounded-2xl shadow-lg shadow-black/5 border border-gray-100 p-4 sticky top-6">
87
+ <h3 className="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-3 px-2">
88
+ Categories
89
+ </h3>
90
+ <div className="space-y-0.5">
91
+ <button
92
+ onClick={() => setSelectedCategory(null)}
93
+ className={`w-full text-left px-3 py-2.5 text-sm rounded-xl transition-all flex items-center gap-2 ${
94
+ !selectedCategory
95
+ ? 'bg-gradient-to-r from-violet-50 to-fuchsia-50 text-violet-700 font-semibold shadow-sm'
96
+ : 'text-gray-600 hover:bg-gray-50'
97
+ }`}
98
+ >
99
+ <Zap className="w-4 h-4" />
100
+ All Plugins
101
+ </button>
102
+ {categories.map(cat => (
103
+ <button
104
+ key={cat.id}
105
+ onClick={() => setSelectedCategory(cat.slug)}
106
+ className={`w-full text-left px-3 py-2.5 text-sm rounded-xl transition-all flex items-center gap-2 ${
107
+ selectedCategory === cat.slug
108
+ ? 'bg-gradient-to-r from-violet-50 to-fuchsia-50 text-violet-700 font-semibold shadow-sm'
109
+ : 'text-gray-600 hover:bg-gray-50'
110
+ }`}
111
+ >
112
+ <span className="text-base">{cat.icon || '📁'}</span>
113
+ {cat.name}
114
+ </button>
115
+ ))}
116
+ </div>
117
+ </div>
118
+ </aside>
119
+
120
+ <div className="flex-1 min-w-0">
121
+ {loading && plugins.length === 0 ? (
122
+ <div className="flex items-center justify-center py-20">
123
+ <LoadingSpinner size="lg" />
124
+ </div>
125
+ ) : plugins.length === 0 ? (
126
+ <EmptyState icon={Package} title="No plugins found" />
127
+ ) : (
128
+ <>
129
+ <div className="flex items-center justify-between mb-5">
130
+ <p className="text-sm text-gray-500">
131
+ <span className="font-semibold text-gray-700">{pagination.total}</span> plugins
132
+ available
133
+ </p>
134
+ <div className="flex items-center gap-1 text-xs text-gray-400">
135
+ <TrendingUp className="w-3.5 h-3.5" />
136
+ Sorted by popularity
137
+ </div>
138
+ </div>
139
+ <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
140
+ {plugins.map(plugin => (
141
+ <Link
142
+ key={plugin.id}
143
+ 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"
145
+ >
146
+ <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
+ <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()}
152
+ </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>
158
+ </div>
159
+ </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">
167
+ <span className="flex items-center gap-1">
168
+ <Download className="w-3.5 h-3.5" />
169
+ {plugin.downloadCount}
170
+ </span>
171
+ </div>
172
+ {plugin.tags && plugin.tags.length > 0 && (
173
+ <div className="flex items-center gap-1">
174
+ {plugin.tags.slice(0, 2).map(tag => (
175
+ <span
176
+ key={tag}
177
+ className="inline-flex items-center px-2 py-0.5 bg-gray-50 text-gray-500 rounded-md text-xs"
178
+ >
179
+ <Tag className="w-2.5 h-2.5 mr-0.5 opacity-50" />
180
+ {tag}
181
+ </span>
182
+ ))}
183
+ </div>
184
+ )}
185
+ </div>
186
+ </div>
187
+ </Link>
188
+ ))}
189
+ </div>
190
+
191
+ {totalPages > 1 && (
192
+ <div className="mt-10 flex items-center justify-center gap-3">
193
+ <button
194
+ onClick={() => fetchPlugins(pagination.page - 1)}
195
+ disabled={pagination.page <= 1}
196
+ className="p-2.5 rounded-xl bg-white border border-gray-200 hover:border-violet-300 hover:shadow-md disabled:opacity-30 disabled:cursor-not-allowed transition-all"
197
+ >
198
+ <ChevronLeft className="w-5 h-5 text-gray-600" />
199
+ </button>
200
+ <div className="flex items-center gap-1.5 px-4 py-2 bg-white rounded-xl border border-gray-200">
201
+ <span className="text-sm text-gray-600">
202
+ <span className="font-semibold text-gray-900">{pagination.page}</span>
203
+ <span className="mx-1.5 text-gray-300">/</span>
204
+ {totalPages}
205
+ </span>
206
+ </div>
207
+ <button
208
+ onClick={() => fetchPlugins(pagination.page + 1)}
209
+ disabled={pagination.page >= totalPages}
210
+ className="p-2.5 rounded-xl bg-white border border-gray-200 hover:border-violet-300 hover:shadow-md disabled:opacity-30 disabled:cursor-not-allowed transition-all"
211
+ >
212
+ <ChevronRight className="w-5 h-5 text-gray-600" />
213
+ </button>
214
+ </div>
215
+ )}
216
+ </>
217
+ )}
218
+ </div>
219
+ </div>
220
+ </div>
221
+ </div>
222
+ )
223
+ }
@@ -0,0 +1,206 @@
1
+ import { useState, useEffect } from 'react'
2
+ import { Helmet } from 'react-helmet-async'
3
+ import { MessageSquare, FileText, ThumbsUp, Pencil } from 'lucide-react'
4
+ import { apiClient } from '@client/services/apiClient'
5
+ import { LoadingSpinner } from '@client/components'
6
+ import type { ProfileStats, ProfileActivity } from '@shared/schemas'
7
+
8
+ type ProfileTab = 'activity' | 'topics' | 'replies'
9
+
10
+ interface ProfileData {
11
+ name: string
12
+ initials: string
13
+ joinedDate: string
14
+ bio: string
15
+ }
16
+
17
+ const DEFAULT_PROFILE: ProfileData = {
18
+ name: 'Jane Doe',
19
+ initials: 'JD',
20
+ joinedDate: 'March 2025',
21
+ bio: 'Full-stack developer passionate about real-time web apps, type safety, and building developer tools.',
22
+ }
23
+
24
+ const DEFAULT_STATS: ProfileStats = {
25
+ topics: 0,
26
+ replies: 0,
27
+ likes: 0,
28
+ }
29
+
30
+ const TABS: { value: ProfileTab; label: string }[] = [
31
+ { value: 'activity', label: 'Activity' },
32
+ { value: 'topics', label: 'Topics' },
33
+ { value: 'replies', label: 'Replies' },
34
+ ]
35
+
36
+ const activityIcon = (type: ProfileActivity['type']) => {
37
+ switch (type) {
38
+ case 'topic':
39
+ return <FileText className="w-4 h-4" />
40
+ case 'reply':
41
+ return <MessageSquare className="w-4 h-4" />
42
+ case 'like':
43
+ return <ThumbsUp className="w-4 h-4" />
44
+ }
45
+ }
46
+
47
+ const activityColor = (type: ProfileActivity['type']) => {
48
+ switch (type) {
49
+ case 'topic':
50
+ return 'bg-emerald-100 text-emerald-600'
51
+ case 'reply':
52
+ return 'bg-blue-100 text-blue-600'
53
+ case 'like':
54
+ return 'bg-rose-100 text-rose-500'
55
+ }
56
+ }
57
+
58
+ export const ProfilePage: React.FC = () => {
59
+ const [activeTab, setActiveTab] = useState<ProfileTab>('activity')
60
+ const [profile, setProfile] = useState<ProfileData>(DEFAULT_PROFILE)
61
+ const [stats, setStats] = useState<ProfileStats>(DEFAULT_STATS)
62
+ const [activity, setActivity] = useState<ProfileActivity[]>([])
63
+ const [loading, setLoading] = useState(true)
64
+
65
+ useEffect(() => {
66
+ async function fetchProfile() {
67
+ try {
68
+ const res = await apiClient.api.profile.$get()
69
+ const result = await res.json()
70
+ if (result.success) {
71
+ const d = result.data as Record<string, unknown>
72
+ if (d.profile) {
73
+ setProfile(d.profile as ProfileData)
74
+ }
75
+ if (d.stats) {
76
+ setStats(d.stats as ProfileStats)
77
+ }
78
+ if (d.activity) {
79
+ setActivity(d.activity as ProfileActivity[])
80
+ }
81
+ }
82
+ } finally {
83
+ setLoading(false)
84
+ }
85
+ }
86
+ fetchProfile()
87
+ }, [])
88
+
89
+ const filteredActivity =
90
+ activeTab === 'activity'
91
+ ? activity
92
+ : activity.filter(a => {
93
+ if (activeTab === 'topics') return a.type === 'topic'
94
+ if (activeTab === 'replies') return a.type === 'reply'
95
+ return true
96
+ })
97
+
98
+ if (loading) {
99
+ return (
100
+ <div className="flex items-center justify-center py-20" data-testid="profile-loading">
101
+ <LoadingSpinner size="lg" />
102
+ </div>
103
+ )
104
+ }
105
+
106
+ return (
107
+ <div className="min-h-screen bg-white" data-testid="profile-page">
108
+ <Helmet>
109
+ <title>Profile — Community</title>
110
+ <meta name="description" content="Community user profile" />
111
+ </Helmet>
112
+
113
+ <div className="bg-gradient-to-b from-emerald-50 to-white pt-12 pb-16 px-4 sm:px-6">
114
+ <div className="max-w-3xl mx-auto">
115
+ <div className="flex flex-col sm:flex-row items-center sm:items-start gap-5">
116
+ <div className="w-20 h-20 rounded-full bg-emerald-500 text-white text-2xl font-bold flex items-center justify-center shadow-lg shadow-emerald-500/25 shrink-0">
117
+ {profile.initials}
118
+ </div>
119
+ <div className="text-center sm:text-left flex-1">
120
+ <h1 className="text-2xl font-bold text-gray-900">{profile.name}</h1>
121
+ <p className="text-sm text-gray-500 mt-0.5">Joined {profile.joinedDate}</p>
122
+ <p className="text-gray-600 mt-2 text-sm leading-relaxed max-w-md">{profile.bio}</p>
123
+ </div>
124
+ <button className="inline-flex items-center gap-2 px-4 py-2 bg-emerald-500 hover:bg-emerald-600 text-white text-sm font-medium rounded-xl shadow-sm shadow-emerald-500/25 transition-colors self-center sm:self-start shrink-0">
125
+ <Pencil className="w-3.5 h-3.5" />
126
+ Edit Profile
127
+ </button>
128
+ </div>
129
+
130
+ <div className="flex justify-center sm:justify-start gap-6 mt-6">
131
+ <div className="text-center">
132
+ <div className="text-xl font-bold text-gray-900">{stats.topics}</div>
133
+ <div className="text-xs text-gray-500">Topics</div>
134
+ </div>
135
+ <div className="w-px bg-gray-200" />
136
+ <div className="text-center">
137
+ <div className="text-xl font-bold text-gray-900">{stats.replies}</div>
138
+ <div className="text-xs text-gray-500">Replies</div>
139
+ </div>
140
+ <div className="w-px bg-gray-200" />
141
+ <div className="text-center">
142
+ <div className="text-xl font-bold text-gray-900">{stats.likes}</div>
143
+ <div className="text-xs text-gray-500">Likes</div>
144
+ </div>
145
+ </div>
146
+ </div>
147
+ </div>
148
+
149
+ <div className="max-w-3xl mx-auto px-4 sm:px-6 -mt-6 pb-16">
150
+ <div className="flex gap-1 bg-gray-100 p-1 rounded-xl mb-6">
151
+ {TABS.map(tab => (
152
+ <button
153
+ key={tab.value}
154
+ onClick={() => setActiveTab(tab.value)}
155
+ className={`flex-1 py-2 text-sm font-medium rounded-lg transition-colors ${
156
+ activeTab === tab.value
157
+ ? 'bg-white text-gray-900 shadow-sm'
158
+ : 'text-gray-500 hover:text-gray-700'
159
+ }`}
160
+ >
161
+ {tab.label}
162
+ </button>
163
+ ))}
164
+ </div>
165
+
166
+ {filteredActivity.length === 0 ? (
167
+ <div className="text-center py-16">
168
+ <div className="inline-flex items-center justify-center w-14 h-14 bg-gray-100 rounded-full mb-3">
169
+ <FileText className="w-6 h-6 text-gray-400" />
170
+ </div>
171
+ <p className="text-gray-500 text-sm">No items yet</p>
172
+ </div>
173
+ ) : (
174
+ <div className="space-y-1">
175
+ {filteredActivity.map((item, index) => (
176
+ <div
177
+ key={item.id}
178
+ className="flex items-start gap-3 p-3.5 rounded-xl hover:bg-gray-50 transition-colors"
179
+ >
180
+ <div className="relative">
181
+ <div
182
+ className={`w-8 h-8 rounded-lg flex items-center justify-center ${activityColor(
183
+ item.type
184
+ )}`}
185
+ >
186
+ {activityIcon(item.type)}
187
+ </div>
188
+ {index < filteredActivity.length - 1 && (
189
+ <div className="absolute top-8 left-1/2 -translate-x-px w-0.5 h-[calc(100%+4px)] bg-gray-100" />
190
+ )}
191
+ </div>
192
+ <div className="flex-1 min-w-0 pt-0.5">
193
+ <p className="text-sm text-gray-700 leading-relaxed">
194
+ <span className="text-gray-500">{item.text}</span>{' '}
195
+ <span className="font-medium text-gray-900">{item.target}</span>
196
+ </p>
197
+ <p className="text-xs text-gray-400 mt-0.5">{item.time}</p>
198
+ </div>
199
+ </div>
200
+ ))}
201
+ </div>
202
+ )}
203
+ </div>
204
+ </div>
205
+ )
206
+ }