Skip to content

Commit 97bfc32

Browse files
authored
Merge pull request #56 from codeunia-dev/enhance/admindashboard
Feature: Enhance Admin Dashboard with page views tracking
2 parents 53cfa7f + 6ea00c5 commit 97bfc32

File tree

5 files changed

+131
-73
lines changed

5 files changed

+131
-73
lines changed

app/admin/page.tsx

Lines changed: 90 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client"
22

3-
import { useState, useEffect } from "react"
3+
import { useState, useEffect, useRef } from "react"
44
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
55
import { Badge } from "@/components/ui/badge"
66
import { Button } from "@/components/ui/button"
@@ -19,6 +19,9 @@ import {
1919
ArrowUpRight,
2020
ArrowDownRight,
2121
} from "lucide-react"
22+
import { createClient } from "@/lib/supabase/client"
23+
import type { BlogPost } from "@/components/data/blog-posts"
24+
import { RealtimeChannel } from "@supabase/supabase-js"
2225

2326
type SupabaseUser = {
2427
created_at: string;
@@ -94,37 +97,6 @@ const recentActivities = [
9497
}
9598
]
9699

97-
const topContent = [
98-
{
99-
title: "Building Scalable APIs with Node.js",
100-
views: "12.5K",
101-
engagement: "94%",
102-
author: "Akshay Kumar",
103-
status: "published",
104-
},
105-
{
106-
title: "React 18 Features You Should Know",
107-
views: "8.2K",
108-
engagement: "87%",
109-
author: "Akshay Kumar",
110-
status: "published",
111-
},
112-
{
113-
title: "Advanced TypeScript Patterns",
114-
views: "6.8K",
115-
engagement: "91%",
116-
author: "Akshay Kumar",
117-
status: "draft",
118-
},
119-
{
120-
title: "Machine Learning with Python",
121-
views: "9.1K",
122-
engagement: "89%",
123-
author: "Akshay Kumar",
124-
status: "published",
125-
},
126-
]
127-
128100
const systemHealth = [
129101
{
130102
service: "API Server",
@@ -158,6 +130,12 @@ export default function AdminDashboard() {
158130
const [totalUsers, setTotalUsers] = useState<string | null>(null)
159131
const [totalUsersChange, setTotalUsersChange] = useState<string>("")
160132
const [totalUsersTrend, setTotalUsersTrend] = useState<"up" | "down">("up")
133+
const [pageViews, setPageViews] = useState<string | null>(null)
134+
const [pageViewsChange, setPageViewsChange] = useState<string>("")
135+
const [pageViewsTrend, setPageViewsTrend] = useState<"up" | "down">("up")
136+
const [topContent, setTopContent] = useState<BlogPost[]>([])
137+
const supabaseRef = useRef<ReturnType<typeof createClient> | null>(null)
138+
const likesChannelRef = useRef<RealtimeChannel | null>(null)
161139

162140
useEffect(() => {
163141
// Fetch real total users and previous month users
@@ -193,6 +171,70 @@ export default function AdminDashboard() {
193171
}
194172
}
195173
})
174+
// Fetch total page views and previous month views
175+
fetch("/api/admin-page-views")
176+
.then(res => res.json())
177+
.then(data => {
178+
if (typeof data.totalViews === "number") {
179+
setPageViews(data.totalViews.toLocaleString());
180+
// Optionally, you can fetch previous month views for change/trend
181+
// For now, just set as N/A
182+
setPageViewsChange("N/A");
183+
setPageViewsTrend("up");
184+
}
185+
})
186+
// Fetch top performing content (top 4 by views)
187+
const fetchTopContentWithLikes = async () => {
188+
const supabase = createClient();
189+
// Get top 4 blogs by views
190+
const { data: blogs, error } = await supabase
191+
.from("blogs")
192+
.select("id, title, author, views, slug")
193+
.order("views", { ascending: false })
194+
.limit(4);
195+
if (!error && blogs) {
196+
const slugs: string[] = (blogs as Pick<BlogPost, "slug">[])?.map((b) => b.slug)
197+
const { data: likesData } = await supabase
198+
.from("blog_likes")
199+
.select("blog_slug")
200+
.in("blog_slug", slugs)
201+
const likesCount: Record<string, number> = {}
202+
if (Array.isArray(likesData)) {
203+
likesData.forEach((like: { blog_slug: string }) => {
204+
likesCount[like.blog_slug] = (likesCount[like.blog_slug] || 0) + 1
205+
})
206+
}
207+
setTopContent(
208+
(blogs as (Pick<BlogPost, "id" | "title" | "author" | "views" | "slug">)[]).map((b) => ({
209+
...b,
210+
excerpt: "",
211+
content: "",
212+
date: "",
213+
readTime: "",
214+
category: "",
215+
tags: [],
216+
featured: false,
217+
image: "",
218+
likes: likesCount[b.slug] || 0,
219+
}))
220+
)
221+
}
222+
}
223+
fetchTopContentWithLikes()
224+
// Setup realtime subscription
225+
const supabase = createClient()
226+
supabaseRef.current = supabase
227+
const channel = supabase.channel('realtime:blog_likes')
228+
.on('postgres_changes', { event: '*', schema: 'public', table: 'blog_likes' }, () => {
229+
fetchTopContentWithLikes()
230+
})
231+
.subscribe()
232+
likesChannelRef.current = channel
233+
return () => {
234+
if (likesChannelRef.current) {
235+
supabase.removeChannel(likesChannelRef.current)
236+
}
237+
}
196238
}, [])
197239

198240
useEffect(() => {
@@ -241,12 +283,16 @@ export default function AdminDashboard() {
241283
}
242284
}
243285

244-
// When rendering dashboardStats, override Total Users value if totalUsers is available
245-
const statsToShow = dashboardStats.map(stat =>
246-
stat.title === "Total Users"
247-
? { ...stat, value: totalUsers ?? stat.value, change: totalUsersChange, trend: totalUsersTrend }
248-
: stat
249-
)
286+
// When rendering dashboardStats, override Total Users and Page Views value if available
287+
const statsToShow = dashboardStats.map(stat => {
288+
if (stat.title === "Total Users") {
289+
return { ...stat, value: totalUsers ?? stat.value, change: totalUsersChange, trend: totalUsersTrend }
290+
}
291+
if (stat.title === "Page Views") {
292+
return { ...stat, value: pageViews ?? stat.value, change: pageViewsChange, trend: pageViewsTrend }
293+
}
294+
return stat
295+
})
250296

251297
return (
252298
<div className="bg-black space-y-8 md:space-y-14 min-h-screen px-4 py-8 md:px-8 lg:px-16 relative overflow-x-hidden">
@@ -312,6 +358,8 @@ export default function AdminDashboard() {
312358
<div className="text-2xl sm:text-3xl font-extrabold text-zinc-900 dark:text-white flex items-end gap-2 tracking-tight">
313359
{stat.title === "Total Users" && totalUsers ? (
314360
<span>{totalUsers}</span>
361+
) : stat.title === "Page Views" && pageViews ? (
362+
<span>{pageViews}</span>
315363
) : stat.value.match(/\d/) ? (
316364
<span>{animatedStats[i].toLocaleString()}</span>
317365
) : (
@@ -421,14 +469,12 @@ export default function AdminDashboard() {
421469
<TableHead className="text-zinc-700 dark:text-zinc-200 font-semibold text-xs sm:text-sm">Title</TableHead>
422470
<TableHead className="text-zinc-700 dark:text-zinc-200 font-semibold text-xs sm:text-sm hidden sm:table-cell">Author</TableHead>
423471
<TableHead className="text-zinc-700 dark:text-zinc-200 font-semibold text-xs sm:text-sm">Views</TableHead>
424-
<TableHead className="text-zinc-700 dark:text-zinc-200 font-semibold text-xs sm:text-sm hidden md:table-cell">Engagement</TableHead>
425-
<TableHead className="text-zinc-700 dark:text-zinc-200 font-semibold text-xs sm:text-sm">Status</TableHead>
426-
<TableHead className="text-right text-zinc-700 dark:text-zinc-200 font-semibold text-xs sm:text-sm">Actions</TableHead>
472+
<TableHead className="text-zinc-700 dark:text-zinc-200 font-semibold text-xs sm:text-sm hidden md:table-cell">Likes</TableHead>
427473
</TableRow>
428474
</TableHeader>
429475
<TableBody>
430-
{topContent.map((content, index) => (
431-
<TableRow key={index} className="hover:bg-purple-700/10 transition-colors">
476+
{topContent.map((content) => (
477+
<TableRow key={content.id} className="hover:bg-purple-700/10 transition-colors">
432478
<TableCell className="font-semibold text-zinc-900 dark:text-zinc-100 text-xs sm:text-sm">
433479
<div className="max-w-[150px] sm:max-w-none">
434480
<div className="truncate">{content.title}</div>
@@ -437,24 +483,7 @@ export default function AdminDashboard() {
437483
</TableCell>
438484
<TableCell className="text-zinc-800 dark:text-zinc-200 text-xs sm:text-sm hidden sm:table-cell">{content.author}</TableCell>
439485
<TableCell className="text-zinc-800 dark:text-zinc-200 text-xs sm:text-sm">{content.views}</TableCell>
440-
<TableCell className="text-zinc-800 dark:text-zinc-200 text-xs sm:text-sm hidden md:table-cell">{content.engagement}</TableCell>
441-
<TableCell>
442-
<Badge
443-
variant={content.status === "published" ? "default" : "secondary"}
444-
className={
445-
content.status === "published"
446-
? "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200 text-xs"
447-
: "bg-zinc-700 text-zinc-200 text-xs"
448-
}
449-
>
450-
{content.status}
451-
</Badge>
452-
</TableCell>
453-
<TableCell className="text-right">
454-
<Button variant="ghost" size="sm" className="hover:bg-purple-700/20 text-purple-400 font-semibold text-xs sm:text-sm h-8 px-2 sm:px-3">
455-
Edit
456-
</Button>
457-
</TableCell>
486+
<TableCell className="text-zinc-800 dark:text-zinc-200 text-xs sm:text-sm hidden md:table-cell">{content.likes}</TableCell>
458487
</TableRow>
459488
))}
460489
</TableBody>

app/api/admin-page-views/route.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { NextResponse } from 'next/server';
2+
import { createClient } from '@/lib/supabase/server';
3+
4+
export async function GET() {
5+
const supabase = await createClient();
6+
const { data, error } = await supabase
7+
.from('blogs')
8+
.select('views');
9+
10+
if (error) {
11+
return NextResponse.json({ error: error.message }, { status: 500 });
12+
}
13+
14+
const totalViews = (data || []).reduce((sum, blog) => sum + (blog.views || 0), 0);
15+
return NextResponse.json({ totalViews });
16+
}

app/blog/[slug]/page.tsx

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import ReactMarkdown from 'react-markdown'
1414
import rehypeRaw from 'rehype-raw'
1515
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip"
1616
import { ShareButton } from "@/components/ui/share-button"
17+
import Image from "next/image";
1718

1819
import Header from "@/components/header";
1920
import Footer from "@/components/footer";
@@ -329,18 +330,30 @@ export default function BlogPostPage() {
329330
transition={{ duration: 0.5, delay: 0.2 }}
330331
className="mb-12"
331332
>
332-
<div className="aspect-video bg-gradient-to-br from-muted to-muted/50 rounded-2xl overflow-hidden shadow-2xl relative">
333-
<div className="absolute inset-0 bg-gradient-to-br from-primary/10 via-transparent to-purple-500/10"></div>
334-
<div className="w-full h-full flex items-center justify-center relative z-10">
335-
<div className="text-center space-y-4">
336-
<div className="relative">
337-
<BookOpen className="h-16 w-16 text-muted-foreground mx-auto" />
338-
<div className="absolute inset-0 w-16 h-16 bg-primary/10 rounded-full blur-xl animate-pulse mx-auto"></div>
339-
</div>
340-
<p className="text-muted-foreground">Blog post image placeholder</p>
341-
</div>
342-
</div>
343-
</div>
333+
<div className="h-[900px] bg-gradient-to-br from-muted to-muted/50 rounded-2xl overflow-hidden shadow-2xl relative">
334+
<div className="absolute inset-0 bg-gradient-to-br from-primary/10 via-transparent to-purple-500/10"></div>
335+
<div className="w-full h-full flex items-center justify-center relative z-10">
336+
{post?.image ? (
337+
<Image
338+
src={post.image}
339+
alt={post.title || 'Blog post image'}
340+
fill
341+
className="object-cover w-full h-full"
342+
style={{ objectFit: 'cover' }}
343+
priority
344+
/>
345+
) : (
346+
<div className="text-center space-y-4">
347+
<div className="relative">
348+
<BookOpen className="h-16 w-16 text-muted-foreground mx-auto" />
349+
<div className="absolute inset-0 w-16 h-16 bg-primary/10 rounded-full blur-xl animate-pulse mx-auto"></div>
350+
</div>
351+
<p className="text-muted-foreground">Blog post image placeholder</p>
352+
</div>
353+
)}
354+
</div>
355+
</div>
356+
344357
</motion.div>
345358

346359
{/* article content */}

public/images/blog/figure1.webp

-27 KB
Binary file not shown.

public/images/blog/nextjs.jpeg

182 KB
Loading

0 commit comments

Comments
 (0)