Skip to content

Commit 6012b83

Browse files
committed
fix(admin-web): repair broken admin features, silent data truncation, and error handling across dashboard
1 parent de48164 commit 6012b83

103 files changed

Lines changed: 2254 additions & 2722 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

admin-web/app/(dashboard)/admin-management/page.tsx

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export default function AdminManagementPage() {
3434
const [searchQuery, setSearchQuery] = useState('')
3535
const [searchResults, setSearchResults] = useState<SearchResult[]>([])
3636
const [isSearching, setIsSearching] = useState(false)
37+
const [searchError, setSearchError] = useState<string | null>(null)
3738
const [confirmDialog, setConfirmDialog] = useState<{
3839
userId: string
3940
userName: string
@@ -85,14 +86,18 @@ export default function AdminManagementPage() {
8586
const handleSearch = async () => {
8687
if (!searchQuery.trim()) return
8788
setIsSearching(true)
89+
setSearchError(null)
8890
try {
8991
const res = await fetch('/api/admin/search-users', {
9092
method: 'POST',
9193
headers: { 'Content-Type': 'application/json' },
9294
credentials: 'include',
9395
body: JSON.stringify({ query: searchQuery }),
9496
})
95-
const data = await res.json()
97+
const data = await res.json().catch(() => ({}))
98+
if (!res.ok) {
99+
throw new Error(data.error || `Search failed (${res.status})`)
100+
}
96101
setSearchResults(
97102
(data.users || []).map((u: any) => ({
98103
id: u.id,
@@ -101,8 +106,11 @@ export default function AdminManagementPage() {
101106
is_admin: u.is_admin ?? false,
102107
}))
103108
)
104-
} catch {
105-
toast.error('Search failed')
109+
} catch (err) {
110+
const message = err instanceof Error ? err.message : 'Search failed'
111+
setSearchResults([])
112+
setSearchError(message)
113+
toast.error(message)
106114
} finally {
107115
setIsSearching(false)
108116
}
@@ -306,7 +314,13 @@ export default function AdminManagementPage() {
306314
</div>
307315
)}
308316

309-
{searchResults.length === 0 && searchQuery && !isSearching && (
317+
{searchError && !isSearching && (
318+
<p className="mt-4 text-sm text-red-600 dark:text-red-400">
319+
{searchError}
320+
</p>
321+
)}
322+
323+
{!searchError && searchResults.length === 0 && searchQuery && !isSearching && (
310324
<p className="mt-4 text-sm text-gray-500 dark:text-gray-400">
311325
No users found. Try a different search term.
312326
</p>

admin-web/app/(dashboard)/analytics/page.tsx

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ function AnalyticsEventsTab() {
5656
const { resolvedTheme } = useTheme()
5757
const isDark = resolvedTheme === 'dark'
5858

59-
const { data, isLoading } = useQuery({
59+
const { data, isLoading, error, refetch } = useQuery({
6060
queryKey: ['analytics-events', rangeFilter],
6161
queryFn: async () => {
6262
const params = new URLSearchParams()
@@ -136,6 +136,16 @@ function AnalyticsEventsTab() {
136136
<div className="flex h-96 items-center justify-center">
137137
<div className="text-gray-500 dark:text-gray-400">Loading analytics...</div>
138138
</div>
139+
) : error ? (
140+
<div className="flex h-96 flex-col items-center justify-center gap-3">
141+
<p className="text-sm text-red-600 dark:text-red-400">Failed to load analytics events. Please try again.</p>
142+
<button
143+
onClick={() => refetch()}
144+
className="rounded-lg border border-gray-300 px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700"
145+
>
146+
Retry
147+
</button>
148+
</div>
139149
) : data ? (
140150
<div className="grid gap-6 md:grid-cols-2">
141151
{/* Timeline Chart */}
@@ -182,7 +192,7 @@ function UserEngagementTab() {
182192
const { resolvedTheme } = useTheme()
183193
const isDark = resolvedTheme === 'dark'
184194

185-
const { data, isLoading } = useQuery({
195+
const { data, isLoading, error, refetch } = useQuery({
186196
queryKey: ['analytics-engagement', rangeFilter],
187197
queryFn: async () => {
188198
const params = new URLSearchParams()
@@ -260,6 +270,16 @@ function UserEngagementTab() {
260270
<div className="flex h-96 items-center justify-center">
261271
<div className="text-gray-500 dark:text-gray-400">Loading engagement data...</div>
262272
</div>
273+
) : error ? (
274+
<div className="flex h-96 flex-col items-center justify-center gap-3">
275+
<p className="text-sm text-red-600 dark:text-red-400">Failed to load engagement metrics. Please try again.</p>
276+
<button
277+
onClick={() => refetch()}
278+
className="rounded-lg border border-gray-300 px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700"
279+
>
280+
Retry
281+
</button>
282+
</div>
263283
) : data ? (
264284
<div className="grid gap-6 md:grid-cols-2">
265285
{/* Daily Active Users */}
@@ -341,7 +361,7 @@ function FeatureAdoptionTab() {
341361
const { resolvedTheme } = useTheme()
342362
const isDark = resolvedTheme === 'dark'
343363

344-
const { data, isLoading } = useQuery({
364+
const { data, isLoading, error, refetch } = useQuery({
345365
queryKey: ['analytics-features', rangeFilter],
346366
queryFn: async () => {
347367
const params = new URLSearchParams()
@@ -384,6 +404,16 @@ function FeatureAdoptionTab() {
384404
<div className="flex h-96 items-center justify-center">
385405
<div className="text-gray-500 dark:text-gray-400">Loading feature adoption...</div>
386406
</div>
407+
) : error ? (
408+
<div className="flex h-96 flex-col items-center justify-center gap-3">
409+
<p className="text-sm text-red-600 dark:text-red-400">Failed to load feature adoption. Please try again.</p>
410+
<button
411+
onClick={() => refetch()}
412+
className="rounded-lg border border-gray-300 px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700"
413+
>
414+
Retry
415+
</button>
416+
</div>
387417
) : data ? (
388418
<div className="space-y-6">
389419
{/* Feature Adoption Chart */}

admin-web/app/(dashboard)/blogs/[id]/page.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,22 @@ export default function EditBlogPostPage({ params }: { params: Promise<{ id: str
6060

6161
const markDirty = () => setIsDirty(true)
6262

63+
// Warn before losing unsaved changes on tab close / hard navigation
64+
useEffect(() => {
65+
if (!isDirty) return
66+
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
67+
e.preventDefault()
68+
e.returnValue = ''
69+
}
70+
window.addEventListener('beforeunload', handleBeforeUnload)
71+
return () => window.removeEventListener('beforeunload', handleBeforeUnload)
72+
}, [isDirty])
73+
74+
const handleBack = () => {
75+
if (isDirty && !confirm('You have unsaved changes. Leave without saving?')) return
76+
router.push('/blogs')
77+
}
78+
6379
const handleSave = async () => {
6480
if (!title.trim()) { toast.error('Title is required'); return }
6581
if (!content.trim()) { toast.error('Content is required'); return }
@@ -171,7 +187,7 @@ export default function EditBlogPostPage({ params }: { params: Promise<{ id: str
171187
{showPanel ? 'Hide panel ›' : '‹ Options'}
172188
</button>
173189
<button
174-
onClick={() => router.push('/blogs')}
190+
onClick={handleBack}
175191
className="text-sm text-indigo-400/70 hover:text-white transition-colors"
176192
>
177193
← Back to Posts

admin-web/app/(dashboard)/blogs/new/page.tsx

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client'
22

3-
import { useState } from 'react'
3+
import { useState, useEffect } from 'react'
44
import { useRouter } from 'next/navigation'
55
import { toast } from 'sonner'
66
import { PageHeader } from '@/components/ui/page-header'
@@ -36,6 +36,25 @@ export default function NewBlogPostPage() {
3636
const [showSchedule, setShowSchedule] = useState(false)
3737
const [showPanel, setShowPanel] = useState(true)
3838

39+
// Dirty when the user has entered any content that would be lost
40+
const isDirty = Boolean(title.trim() || excerpt.trim() || content.trim() || tagsInput.trim())
41+
42+
// Warn before losing unsaved changes on tab close / hard navigation
43+
useEffect(() => {
44+
if (!isDirty || isSaving) return
45+
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
46+
e.preventDefault()
47+
e.returnValue = ''
48+
}
49+
window.addEventListener('beforeunload', handleBeforeUnload)
50+
return () => window.removeEventListener('beforeunload', handleBeforeUnload)
51+
}, [isDirty, isSaving])
52+
53+
const handleBack = () => {
54+
if (isDirty && !confirm('You have unsaved changes. Leave without saving?')) return
55+
router.push('/blogs')
56+
}
57+
3958
const handleTitleChange = (v: string) => {
4059
setTitle(v)
4160
if (!slugManual) setSlug(slugify(v) + (locale !== 'en' ? `-${locale}` : ''))
@@ -102,7 +121,7 @@ export default function NewBlogPostPage() {
102121
{showPanel ? 'Hide panel ›' : '‹ Options'}
103122
</button>
104123
<button
105-
onClick={() => router.push('/blogs')}
124+
onClick={handleBack}
106125
className="text-sm text-indigo-400/70 hover:text-white transition-colors"
107126
>
108127
← Back to Posts

admin-web/app/(dashboard)/blogs/page.tsx

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -35,27 +35,51 @@ export default function BlogsPage() {
3535
const [togglingId, setTogglingId] = useState<string | null>(null)
3636
const [triggeringCron, setTriggeringCron] = useState(false)
3737
const [page, setPage] = useState(1)
38+
const [total, setTotal] = useState(0)
39+
const [stats, setStats] = useState({ total: 0, published: 0, drafts: 0 })
3840

3941
const loadPosts = useCallback(async () => {
4042
setIsLoading(true)
4143
setError(null)
4244
try {
43-
const data = await listBlogPosts({ limit: 100 })
45+
const data = await listBlogPosts({
46+
page,
47+
limit: PAGE_SIZE,
48+
locale: localeFilter !== 'all' ? localeFilter : undefined,
49+
status: statusFilter !== 'all' ? statusFilter : undefined,
50+
})
4451
setPosts(data.posts)
52+
setTotal(data.total)
4553
} catch (err) {
4654
console.error(err)
4755
setError('Failed to load blog posts.')
4856
} finally {
4957
setIsLoading(false)
5058
}
59+
}, [page, localeFilter, statusFilter])
60+
61+
const loadStats = useCallback(async () => {
62+
try {
63+
const [all, published, drafts] = await Promise.all([
64+
listBlogPosts({ limit: 1 }),
65+
listBlogPosts({ status: 'published', limit: 1 }),
66+
listBlogPosts({ status: 'draft', limit: 1 }),
67+
])
68+
setStats({ total: all.total, published: published.total, drafts: drafts.total })
69+
} catch (err) {
70+
console.error('Failed to load blog stats:', err)
71+
}
5172
}, [])
5273

5374
useEffect(() => { loadPosts() }, [loadPosts])
75+
useEffect(() => { loadStats() }, [loadStats])
76+
77+
// Reset to first page when filters change
78+
useEffect(() => { setPage(1) }, [localeFilter, statusFilter])
5479

80+
// Search filters the currently loaded page client-side
5581
useEffect(() => {
5682
let result = posts
57-
if (localeFilter !== 'all') result = result.filter(p => p.locale === localeFilter)
58-
if (statusFilter !== 'all') result = result.filter(p => p.status === statusFilter)
5983
if (search.trim()) {
6084
const q = search.toLowerCase()
6185
result = result.filter(p =>
@@ -65,8 +89,7 @@ export default function BlogsPage() {
6589
)
6690
}
6791
setFiltered(result)
68-
setPage(1)
69-
}, [posts, localeFilter, statusFilter, search])
92+
}, [posts, search])
7093

7194
const handleToggleStatus = async (post: BlogPostListItem) => {
7295
setTogglingId(post.id)
@@ -79,6 +102,7 @@ export default function BlogsPage() {
79102
toast.success(`"${post.title}" published`)
80103
}
81104
await loadPosts()
105+
loadStats()
82106
} catch (err) {
83107
toast.error(err instanceof Error ? err.message : 'Failed to update status')
84108
} finally {
@@ -93,6 +117,7 @@ export default function BlogsPage() {
93117
await deleteBlogPost(post.id)
94118
toast.success('Post deleted')
95119
await loadPosts()
120+
loadStats()
96121
} catch (err) {
97122
toast.error(err instanceof Error ? err.message : 'Failed to delete post')
98123
} finally {
@@ -112,10 +137,7 @@ export default function BlogsPage() {
112137
}
113138
}
114139

115-
const published = posts.filter(p => p.status === 'published').length
116-
const drafts = posts.filter(p => p.status === 'draft').length
117-
const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE))
118-
const paginatedFiltered = filtered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE)
140+
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE))
119141

120142
return (
121143
<div className="space-y-6">
@@ -144,9 +166,9 @@ export default function BlogsPage() {
144166
{/* Stats */}
145167
<div className="grid grid-cols-3 gap-4">
146168
{[
147-
{ label: 'Total Posts', value: posts.length, color: 'text-indigo-300' },
148-
{ label: 'Published', value: published, color: 'text-emerald-300' },
149-
{ label: 'Drafts', value: drafts, color: 'text-amber-300' },
169+
{ label: 'Total Posts', value: stats.total, color: 'text-indigo-300' },
170+
{ label: 'Published', value: stats.published, color: 'text-emerald-300' },
171+
{ label: 'Drafts', value: stats.drafts, color: 'text-amber-300' },
150172
].map(stat => (
151173
<div key={stat.label} className="rounded-xl border border-white/10 bg-white/5 p-4">
152174
<p className="text-xs text-indigo-400/70">{stat.label}</p>
@@ -184,7 +206,7 @@ export default function BlogsPage() {
184206
<option value="scheduled">Scheduled</option>
185207
<option value="draft">Draft</option>
186208
</select>
187-
<span className="text-xs text-indigo-400/60">{filtered.length} posts</span>
209+
<span className="text-xs text-indigo-400/60">{total} posts</span>
188210
</div>
189211

190212
{/* Table */}
@@ -213,7 +235,7 @@ export default function BlogsPage() {
213235
</tr>
214236
</thead>
215237
<tbody className="divide-y divide-white/5">
216-
{paginatedFiltered.map(post => (
238+
{filtered.map(post => (
217239
<tr key={post.id} className="group transition-colors hover:bg-white/5">
218240
<td className="px-4 py-3">
219241
<div className="flex items-start gap-2">
@@ -309,7 +331,7 @@ export default function BlogsPage() {
309331
{totalPages > 1 && (
310332
<div className="flex items-center justify-between text-sm">
311333
<span className="text-indigo-400/60">
312-
{(page - 1) * PAGE_SIZE + 1}{Math.min(page * PAGE_SIZE, filtered.length)} of {filtered.length}
334+
{(page - 1) * PAGE_SIZE + 1}{Math.min(page * PAGE_SIZE, total)} of {total}
313335
</span>
314336
<div className="flex gap-2">
315337
<button

0 commit comments

Comments
 (0)