feat: Add dashboard for managing portfolio content (blog, experience, projects) and restructure public portfolio routes.
This commit is contained in:
260
src/app/dashboard/page.tsx
Normal file
260
src/app/dashboard/page.tsx
Normal file
@@ -0,0 +1,260 @@
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import {
|
||||
Users,
|
||||
Eye,
|
||||
MousePointer2,
|
||||
TrendingUp,
|
||||
Clock,
|
||||
ArrowUpRight,
|
||||
ArrowDownRight,
|
||||
Monitor,
|
||||
Calendar,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
async function getStats() {
|
||||
const [totalVisitors, totalPageViews, recentSessions] = await Promise.all([
|
||||
prisma.session.count(),
|
||||
prisma.pageView.count(),
|
||||
prisma.session.findMany({
|
||||
take: 5,
|
||||
orderBy: { lastSeenAt: "desc" },
|
||||
include: {
|
||||
_count: {
|
||||
select: { pageViews: true },
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
totalVisitors,
|
||||
totalPageViews,
|
||||
recentSessions,
|
||||
};
|
||||
}
|
||||
|
||||
export default async function DashboardPage() {
|
||||
const stats = await getStats();
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-8 max-w-7xl mx-auto">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h1 className="text-3xl font-display font-bold text-foreground">
|
||||
Dashboard Overview
|
||||
</h1>
|
||||
<p className="text-foreground-muted">
|
||||
Monitor your portfolio performance and user engagement.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Stats Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<StatCard
|
||||
label="Total Visitors"
|
||||
value={stats.totalVisitors.toLocaleString()}
|
||||
icon={Users}
|
||||
trend={+12.5}
|
||||
description="Total unique sessions"
|
||||
/>
|
||||
<StatCard
|
||||
label="Page Views"
|
||||
value={stats.totalPageViews.toLocaleString()}
|
||||
icon={Eye}
|
||||
trend={+8.2}
|
||||
description="Total pages viewed"
|
||||
/>
|
||||
<StatCard
|
||||
label="Avg. Session"
|
||||
value="4m 32s"
|
||||
icon={Clock}
|
||||
trend={-2.4}
|
||||
description="Time spent on site"
|
||||
/>
|
||||
<StatCard
|
||||
label="Bounce Rate"
|
||||
value="42%"
|
||||
icon={MousePointer2}
|
||||
trend={+1.2}
|
||||
description="Single page sessions"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{/* Recent Activity */}
|
||||
<div className="lg:col-span-2 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-xl font-display font-semibold text-foreground">
|
||||
Recent Visitor Activity
|
||||
</h2>
|
||||
<button className="text-sm font-medium text-primary hover:underline">
|
||||
View all
|
||||
</button>
|
||||
</div>
|
||||
<div className="bg-surface border border-border/50 rounded-xl overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left">
|
||||
<thead>
|
||||
<tr className="border-b border-border/50 bg-surface-elevated/50">
|
||||
<th className="px-6 py-4 text-xs font-semibold text-foreground-muted uppercase tracking-wider">
|
||||
Visitor ID
|
||||
</th>
|
||||
<th className="px-6 py-4 text-xs font-semibold text-foreground-muted uppercase tracking-wider">
|
||||
Page Views
|
||||
</th>
|
||||
<th className="px-6 py-4 text-xs font-semibold text-foreground-muted uppercase tracking-wider">
|
||||
Last Active
|
||||
</th>
|
||||
<th className="px-6 py-4 text-xs font-semibold text-foreground-muted uppercase tracking-wider">
|
||||
Status
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border/50">
|
||||
{stats.recentSessions.map((session) => (
|
||||
<tr
|
||||
key={session.id}
|
||||
className="hover:bg-surface-elevated/30 transition-colors"
|
||||
>
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<Monitor size={14} className="text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-foreground">
|
||||
{session.visitorId.slice(0, 8)}...
|
||||
</div>
|
||||
<div className="text-xs text-foreground-muted capitalize">
|
||||
ID: {session.id.slice(0, 6)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="text-sm text-foreground">
|
||||
{session._count.pageViews} pages
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="text-sm text-foreground">
|
||||
{new Date(session.lastSeenAt).toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
</div>
|
||||
<div className="text-xs text-foreground-muted">
|
||||
{new Date(session.lastSeenAt).toLocaleDateString()}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<span className="inline-flex items-center px-2 py-1 rounded-full text-[10px] font-medium bg-green-500/10 text-green-500 border border-green-500/20">
|
||||
Active
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{stats.recentSessions.length === 0 && (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={4}
|
||||
className="px-6 py-12 text-center text-foreground-muted"
|
||||
>
|
||||
No recent activity found.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Stats / Info */}
|
||||
<div className="space-y-6">
|
||||
<div className="bg-surface border border-border/50 rounded-xl p-6 space-y-4">
|
||||
<h3 className="text-lg font-display font-semibold text-foreground flex items-center gap-2">
|
||||
<Calendar size={18} className="text-primary" />
|
||||
Total Period
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-foreground-muted">Period Started</span>
|
||||
<span className="text-foreground font-medium">Jan 01, 2024</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-foreground-muted">Today's Visits</span>
|
||||
<span className="text-foreground font-medium">84</span>
|
||||
</div>
|
||||
<div className="pt-2 border-t border-border/50">
|
||||
<div className="flex items-center gap-2 text-xs text-green-500 font-medium">
|
||||
<TrendingUp size={12} />
|
||||
<span>Growth is up 12% this week</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-primary/5 border border-primary/20 rounded-xl p-6">
|
||||
<h3 className="text-primary font-semibold text-sm uppercase tracking-wider mb-2">
|
||||
Pro Tip
|
||||
</h3>
|
||||
<p className="text-sm text-foreground-muted leading-relaxed">
|
||||
Updating your project descriptions with relevant keywords can help
|
||||
improve your portfolio's search engine visibility and attract more
|
||||
visitors.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StatCard({
|
||||
label,
|
||||
value,
|
||||
icon: Icon,
|
||||
trend,
|
||||
description,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
icon: any;
|
||||
trend: number;
|
||||
description: string;
|
||||
}) {
|
||||
const isPositive = trend > 0;
|
||||
|
||||
return (
|
||||
<div className="bg-surface border border-border/50 rounded-xl p-5 hover:border-primary/30 transition-all group">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="p-2.5 rounded-lg bg-surface-elevated/50 text-foreground-muted group-hover:text-primary group-hover:bg-primary/10 transition-colors">
|
||||
<Icon size={20} />
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-0.5 text-xs font-medium px-2 py-1 rounded-full",
|
||||
isPositive
|
||||
? "text-green-500 bg-green-500/10 border border-green-500/20"
|
||||
: "text-red-500 bg-red-500/10 border border-red-500/20",
|
||||
)}
|
||||
>
|
||||
{isPositive ? (
|
||||
<ArrowUpRight size={12} />
|
||||
) : (
|
||||
<ArrowDownRight size={12} />
|
||||
)}
|
||||
{Math.abs(trend)}%
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 space-y-1">
|
||||
<h3 className="text-sm font-medium text-foreground-muted">{label}</h3>
|
||||
<p className="text-2xl font-display font-bold text-foreground">
|
||||
{value}
|
||||
</p>
|
||||
</div>
|
||||
<p className="mt-2 text-xs text-foreground-muted">{description}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user