174 lines
5.7 KiB
TypeScript
174 lines
5.7 KiB
TypeScript
"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>
|
|
);
|
|
}
|