first commit
Made-with: Cursor
This commit is contained in:
173
frontend/app/admin/overview/page.tsx
Normal file
173
frontend/app/admin/overview/page.tsx
Normal file
@@ -0,0 +1,173 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { api } from "@/lib/api";
|
||||
import { formatDate } from "@/lib/utils";
|
||||
import { Calendar, FileText, Tag, User, Plus, Download, FolderOpen } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function OverviewPage() {
|
||||
const { user, loading: authLoading } = useAuth();
|
||||
const router = useRouter();
|
||||
const [meetups, setMeetups] = useState<any[]>([]);
|
||||
const [posts, setPosts] = useState<any[]>([]);
|
||||
const [categories, setCategories] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (!authLoading && !user) {
|
||||
router.push("/admin");
|
||||
}
|
||||
}, [authLoading, user, router]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) return;
|
||||
async function load() {
|
||||
try {
|
||||
const [m, p, c] = await Promise.all([
|
||||
api.getMeetups(),
|
||||
api.getPosts({ limit: 5, all: true }),
|
||||
api.getCategories(),
|
||||
]);
|
||||
setMeetups(Array.isArray(m) ? m : []);
|
||||
setPosts(p.posts || []);
|
||||
setCategories(Array.isArray(c) ? c : []);
|
||||
} catch (err: any) {
|
||||
setError(err.message || "Failed to load dashboard data");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
load();
|
||||
}, [user]);
|
||||
|
||||
if (authLoading || !user) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[60vh]">
|
||||
<div className="text-on-surface/50">Loading...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const shortPubkey = `${user.pubkey.slice(0, 8)}...${user.pubkey.slice(-8)}`;
|
||||
|
||||
const upcomingMeetup = meetups.find(
|
||||
(m) => new Date(m.date) > new Date()
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[60vh]">
|
||||
<div className="text-on-surface/50">Loading dashboard...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-on-surface">Welcome back</h1>
|
||||
<p className="text-on-surface/60 font-mono text-sm mt-1">{shortPubkey}</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="bg-error-container/20 text-error rounded-xl p-4 text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<StatCard icon={Calendar} label="Total Meetups" value={meetups.length} />
|
||||
<StatCard icon={FileText} label="Blog Posts" value={posts.length} />
|
||||
<StatCard icon={Tag} label="Categories" value={categories.length} />
|
||||
<StatCard icon={User} label="Your Role" value={user.role} />
|
||||
</div>
|
||||
|
||||
{upcomingMeetup && (
|
||||
<div className="bg-surface-container-low rounded-xl p-6">
|
||||
<h2 className="text-lg font-semibold text-on-surface mb-3">Next Upcoming Meetup</h2>
|
||||
<p className="text-primary font-semibold">{upcomingMeetup.title}</p>
|
||||
<p className="text-on-surface/60 text-sm mt-1">
|
||||
{formatDate(upcomingMeetup.date)} · {upcomingMeetup.location}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="bg-surface-container-low rounded-xl p-6">
|
||||
<h2 className="text-lg font-semibold text-on-surface mb-4">Recent Posts</h2>
|
||||
{posts.length === 0 ? (
|
||||
<p className="text-on-surface/50 text-sm">No posts yet.</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{posts.slice(0, 5).map((post: any) => (
|
||||
<div
|
||||
key={post.id}
|
||||
className="flex items-center justify-between py-2"
|
||||
>
|
||||
<div>
|
||||
<p className="text-on-surface text-sm font-medium">{post.title}</p>
|
||||
<p className="text-on-surface/50 text-xs">{post.slug}</p>
|
||||
</div>
|
||||
{post.featured && (
|
||||
<span className="rounded-full px-3 py-1 text-xs font-bold bg-primary/20 text-primary">
|
||||
Featured
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-surface-container-low rounded-xl p-6">
|
||||
<h2 className="text-lg font-semibold text-on-surface mb-4">Quick Actions</h2>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Link
|
||||
href="/admin/events"
|
||||
className="flex items-center gap-2 px-4 py-2 rounded-lg bg-gradient-to-r from-primary to-primary-container text-on-primary font-semibold text-sm hover:opacity-90 transition-opacity"
|
||||
>
|
||||
<Plus size={16} />
|
||||
Create Meetup
|
||||
</Link>
|
||||
<Link
|
||||
href="/admin/blog"
|
||||
className="flex items-center gap-2 px-4 py-2 rounded-lg bg-surface-container-highest text-on-surface font-semibold text-sm hover:bg-surface-container-high transition-colors"
|
||||
>
|
||||
<Download size={16} />
|
||||
Import Post
|
||||
</Link>
|
||||
<Link
|
||||
href="/admin/categories"
|
||||
className="flex items-center gap-2 px-4 py-2 rounded-lg bg-surface-container-highest text-on-surface font-semibold text-sm hover:bg-surface-container-high transition-colors"
|
||||
>
|
||||
<FolderOpen size={16} />
|
||||
Manage Categories
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StatCard({
|
||||
icon: Icon,
|
||||
label,
|
||||
value,
|
||||
}: {
|
||||
icon: any;
|
||||
label: string;
|
||||
value: string | number;
|
||||
}) {
|
||||
return (
|
||||
<div className="bg-surface-container-low rounded-xl p-6">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<Icon size={20} className="text-primary" />
|
||||
<span className="text-on-surface/60 text-sm">{label}</span>
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-on-surface">{value}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user