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
|
@@ -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
|
+
}
|