first commit

Made-with: Cursor
This commit is contained in:
Michilis
2026-04-01 02:46:53 +00:00
commit 76210db03d
126 changed files with 20208 additions and 0 deletions

View 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>
);
}