import Layout from "@/components/Layout"; import LoadingScreen from "@/components/LoadingScreen"; import { useAuth } from "@/contexts/AuthContext"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useNavigate } from "react-router-dom"; import { aethexToast } from "@/lib/aethex-toast"; import { aethexUserService, type AethexUserProfile, } from "@/lib/aethex-database-adapter"; import { Card, CardContent, CardHeader, CardTitle, CardDescription, } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import AdminStatCard from "@/components/admin/AdminStatCard"; import AdminMemberManager from "@/components/admin/AdminMemberManager"; import AdminAchievementManager from "@/components/admin/AdminAchievementManager"; import { Shield, Users, Rocket, PenTool, Command, Activity, UserCog, Settings, ExternalLink, ClipboardList, Loader2, RefreshCw, } from "lucide-react"; export default function Admin() { const { user, loading, roles } = useAuth(); const navigate = useNavigate(); const ownerEmail = "mrpiglr@gmail.com"; const normalizedEmail = user?.email?.toLowerCase() ?? ""; const isOwner = (Array.isArray(roles) && roles.includes("owner")) || normalizedEmail === ownerEmail; const [managedProfiles, setManagedProfiles] = useState( [], ); type Studio = { name: string; tagline?: string; metrics?: string; specialties?: string[]; }; type ProjectApplication = { id: string; status?: string | null; applicant_email?: string | null; applicant_name?: string | null; created_at?: string | null; notes?: string | null; projects?: { id?: string | null; title?: string | null; user_id?: string | null; } | null; }; const [studios, setStudios] = useState([ { name: "Lone Star Studio", tagline: "Indie craftsmanship with AAA polish", metrics: "Top-rated indie hits", specialties: ["Unity", "Unreal", "Pixel Art"], }, { name: "AeThex | GameForge", tagline: "High-performance cross-platform experiences", metrics: "Billions of player sessions", specialties: ["Roblox", "Backend", "LiveOps"], }, { name: "Gaming Control", tagline: "Strategy, simulation, and systems-first design", metrics: "Award-winning franchises", specialties: ["Simulation", "AI/ML", "Economy"], }, ]); const [projectApplications, setProjectApplications] = useState([]); const [projectApplicationsLoading, setProjectApplicationsLoading] = useState(false); type OpportunityApplication = { id: string; type?: string | null; full_name?: string | null; email?: string | null; status?: string | null; availability?: string | null; role_interest?: string | null; primary_skill?: string | null; experience_level?: string | null; submitted_at?: string | null; message?: string | null; }; const [opportunityApplications, setOpportunityApplications] = useState([]); const [opportunityApplicationsLoading, setOpportunityApplicationsLoading] = useState(false); const [selectedMemberId, setSelectedMemberId] = useState(null); const loadProfiles = useCallback(async () => { try { const list = await aethexUserService.listProfiles(200); setManagedProfiles(list); } catch (error) { console.warn("Failed to load managed profiles:", error); setManagedProfiles([]); } }, []); useEffect(() => { loadProfiles().catch(() => undefined); }, [loadProfiles]); useEffect(() => { fetch("/api/featured-studios") .then((r) => (r.ok ? r.json() : [])) .then((data) => { if (Array.isArray(data) && data.length) setStudios(data); }) .catch(() => void 0); }, []); const loadProjectApplications = useCallback(async () => { if (!user?.id) return; setProjectApplicationsLoading(true); try { const response = await fetch( `/api/applications?owner=${encodeURIComponent(user.id)}`, ); if (response.ok) { const data = await response.json(); setProjectApplications(Array.isArray(data) ? data : []); } else { setProjectApplications([]); } } catch (error) { console.warn("Failed to load project applications:", error); setProjectApplications([]); } finally { setProjectApplicationsLoading(false); } }, [user?.id]); useEffect(() => { loadProjectApplications().catch(() => undefined); }, [loadProjectApplications]); const loadOpportunityApplications = useCallback(async () => { const email = user?.email?.toLowerCase(); if (!email) return; setOpportunityApplicationsLoading(true); try { const response = await fetch( `/api/opportunities/applications?email=${encodeURIComponent(email)}`, ); if (response.ok) { const data = await response.json(); setOpportunityApplications(Array.isArray(data) ? data : []); } else { const message = await response.text().catch(() => ""); if (response.status === 403) { aethexToast.error({ title: "Access denied", description: "You must be signed in as the owner to view opportunity applications.", }); } else { console.warn("Opportunity applications request failed:", message); } setOpportunityApplications([]); } } catch (error) { console.warn("Failed to load opportunity applications:", error); setOpportunityApplications([]); } finally { setOpportunityApplicationsLoading(false); } }, [user?.email]); useEffect(() => { loadOpportunityApplications().catch(() => undefined); }, [loadOpportunityApplications]); useEffect(() => { if (!loading) { if (!user) { navigate("/login", { replace: true }); } } }, [user, loading, navigate]); useEffect(() => { if (!selectedMemberId && managedProfiles.length) { setSelectedMemberId(managedProfiles[0].id); } }, [managedProfiles, selectedMemberId]); if (loading || !user) { return ( ); } if (!isOwner) { return (
Access denied This panel is restricted to {ownerEmail}. If you need access, contact the site owner.
); } const [blogPosts, setBlogPosts] = useState([]); const [loadingPosts, setLoadingPosts] = useState(false); const [activeTab, setActiveTab] = useState("overview"); const selectedMember = useMemo( () => managedProfiles.find((profile) => profile.id === selectedMemberId) ?? null, [managedProfiles, selectedMemberId], ); const totalMembers = managedProfiles.length; const publishedPosts = blogPosts.length; const featuredStudios = studios.length; const pendingProjectApplications = projectApplications.filter((app) => { const status = (app.status ?? "").toLowerCase(); return ( status !== "approved" && status !== "completed" && status !== "closed" ); }).length; const overviewStats = useMemo( () => [ { title: "Total members", value: totalMembers ? totalMembers.toString() : "—", description: "Profiles synced from AeThex identity service.", trend: totalMembers ? `${totalMembers} active profiles` : "Awaiting sync", icon: Users, tone: "blue" as const, }, { title: "Published posts", value: publishedPosts ? publishedPosts.toString() : "0", description: "Blog entries stored in Supabase content tables.", trend: loadingPosts ? "Refreshing content…" : "Latest sync up to date", icon: PenTool, tone: "purple" as const, }, { title: "Featured studios", value: featuredStudios ? featuredStudios.toString() : "0", description: "Studios highlighted on community landing pages.", trend: "Synced nightly from partner directory", icon: Rocket, tone: "green" as const, }, { title: "Pending project applications", value: projectApplicationsLoading ? "���" : pendingProjectApplications.toString(), description: "Project collaboration requests awaiting review.", trend: projectApplicationsLoading ? "Fetching submissions…" : `${projectApplications.length} total submissions`, icon: ClipboardList, tone: "orange" as const, }, { title: "Opportunity pipeline", value: opportunityApplicationsLoading ? "…" : opportunityApplications.length.toString(), description: "Contributor & career submissions captured via Opportunities.", trend: opportunityApplicationsLoading ? "Syncing applicant data…" : `${opportunityApplications.filter((app) => (app.status ?? "new").toLowerCase() === "new").length} new this week`, icon: Rocket, tone: "teal" as const, }, ], [ applications.length, applicationsLoading, featuredStudios, loadingPosts, pendingApplications, publishedPosts, totalMembers, ], ); const quickActions = useMemo( () => [ { label: "Review dashboard", description: "Jump to the live product dashboard and KPIs.", icon: Activity, action: () => navigate("/dashboard"), }, { label: "Manage content", description: "Create, edit, and publish new blog updates.", icon: PenTool, action: () => setActiveTab("content"), }, { label: "Member directory", description: "Audit profiles, roles, and onboarding progress.", icon: Users, action: () => setActiveTab("community"), }, { label: "Operations runbook", description: "Review featured studios and partner programs.", icon: Settings, action: () => setActiveTab("operations"), }, { label: "Review applications", description: "Approve partnership or project requests.", icon: ClipboardList, action: () => setActiveTab("operations"), }, { label: "Open Builder CMS", description: "Edit marketing pages and landing content in Builder.io.", icon: ExternalLink, action: () => { if (typeof window !== "undefined") { window.open("https://builder.io", "_blank", "noopener"); } }, }, { label: "Invite teammates", description: "Send access links and assign admin roles.", icon: UserCog, action: () => setActiveTab("community"), }, ], [navigate], ); useEffect(() => { (async () => { try { setLoadingPosts(true); const res = await fetch("/api/blog?limit=100"); const data = res.ok ? await res.json() : []; if (Array.isArray(data)) setBlogPosts(data); } catch (e) { console.warn("Failed to load blog posts:", e); } finally { setLoadingPosts(false); } })(); }, []); const savePost = async (idx: number) => { const p = blogPosts[idx]; const payload = { ...p, slug: (p.slug || p.title || "") .toLowerCase() .trim() .replace(/[^a-z0-9\s-]/g, "") .replace(/\s+/g, "-"), }; const res = await fetch("/api/blog", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); if (!res.ok) return aethexToast.error({ title: "Save failed", description: await res.text().catch(() => ""), }); const saved = await res.json(); const next = blogPosts.slice(); next[idx] = saved; setBlogPosts(next); aethexToast.success({ title: "Saved", description: saved.title }); }; const deletePost = async (idx: number) => { const p = blogPosts[idx]; const res = await fetch(`/api/blog/${encodeURIComponent(p.slug)}`, { method: "DELETE", }); if (!res.ok) return aethexToast.error({ title: "Delete failed", description: await res.text().catch(() => ""), }); setBlogPosts(blogPosts.filter((_, i) => i !== idx)); aethexToast.info({ title: "Deleted", description: p.title }); }; return (

Admin Control Center

Unified oversight for AeThex operations, content, and community.

Owner Admin Founder

Signed in as{" "} {normalizedEmail || ownerEmail}

Overview Content Community Operations
{overviewStats.map((stat) => ( ))}
Quick actions
Launch frequent administrative workflows.
{quickActions.map( ({ label, description, icon: ActionIcon, action }) => ( ), )}
Access control
Owner-only access enforced via Supabase roles.
  • Owner email:{" "} {ownerEmail}
  • Roles are provisioned automatically on owner sign-in.
  • Grant additional admins by updating Supabase role assignments.
Content overview
{publishedPosts} published{" "} {publishedPosts === 1 ? "post" : "posts"} ·{" "} {loadingPosts ? "refreshing content…" : "latest Supabase sync"}

Drafts and announcements appear instantly on the public blog after saving. Use scheduled releases for major updates and keep thumbnails optimised for 1200×630.

Blog posts
Manage blog content stored in Supabase
{blogPosts.length === 0 && (

No posts loaded yet. Use “Refresh” or “Add post” to start managing content.

)} {blogPosts.map((p, i) => (
{ const next = blogPosts.slice(); next[i] = { ...next[i], title: e.target.value }; setBlogPosts(next); }} /> { const next = blogPosts.slice(); next[i] = { ...next[i], slug: e.target.value }; setBlogPosts(next); }} />
{ const n = blogPosts.slice(); n[i] = { ...n[i], author: e.target.value }; setBlogPosts(n); }} /> { const n = blogPosts.slice(); n[i] = { ...n[i], date: e.target.value }; setBlogPosts(n); }} />
{ const n = blogPosts.slice(); n[i] = { ...n[i], read_time: e.target.value }; setBlogPosts(n); }} /> { const n = blogPosts.slice(); n[i] = { ...n[i], category: e.target.value }; setBlogPosts(n); }} /> { const n = blogPosts.slice(); n[i] = { ...n[i], image: e.target.value }; setBlogPosts(n); }} />