diff --git a/client/pages/Admin.tsx b/client/pages/Admin.tsx index 4bdaa649..683ad971 100644 --- a/client/pages/Admin.tsx +++ b/client/pages/Admin.tsx @@ -53,43 +53,66 @@ import { Wifi, Zap, Heart, - Lightbulb, - Briefcase, + BarChart3, + Grid3x3, + Gauge, + MessageSquare, + Lock, + Globe, } from "lucide-react"; +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; +}; + +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; +}; + export default function Admin() { const { user, loading } = useAuth(); const navigate = useNavigate(); - const normalizedEmail = user?.email?.toLowerCase() ?? ""; - // Redirect to login if not authenticated + const normalizedEmail = user?.email?.toLowerCase() ?? ""; + const ownerEmail = "admin@aethex.tech"; + const isOwner = normalizedEmail === ownerEmail.toLowerCase(); + useEffect(() => { if (!loading && !user) { navigate("/login", { replace: true }); } }, [user, loading, navigate]); + 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", @@ -115,25 +138,15 @@ export default function Admin() { >([]); 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< OpportunityApplication[] >([]); const [opportunityApplicationsLoading, setOpportunityApplicationsLoading] = useState(false); const [selectedMemberId, setSelectedMemberId] = useState(null); + const [blogPosts, setBlogPosts] = useState([]); + const [loadingPosts, setLoadingPosts] = useState(false); + const [activeTab, setActiveTab] = useState("overview"); const loadProfiles = useCallback(async () => { try { @@ -163,7 +176,7 @@ export default function Admin() { setProjectApplicationsLoading(true); try { const response = await fetch( - `/api/applications?owner=${encodeURIComponent(user.id)}`, + `/api/applications?owner=${encodeURIComponent(user.id)}` ); if (response.ok) { const data = await response.json(); @@ -189,7 +202,7 @@ export default function Admin() { setOpportunityApplicationsLoading(true); try { const response = await fetch( - `/api/opportunities/applications?email=${encodeURIComponent(email)}`, + `/api/opportunities/applications?email=${encodeURIComponent(email)}` ); if (response.ok) { const data = await response.json(); @@ -219,10 +232,6 @@ export default function Admin() { loadOpportunityApplications().catch(() => undefined); }, [loadOpportunityApplications]); - useEffect(() => { - // Do not redirect unauthenticated users; show inline access UI instead - }, [user, loading, navigate]); - useEffect(() => { if (!selectedMemberId && managedProfiles.length) { setSelectedMemberId(managedProfiles[0].id); @@ -281,106 +290,12 @@ export default function Admin() { ); } - const [blogPosts, setBlogPosts] = useState([]); - const [loadingPosts, setLoadingPosts] = useState(false); - const [activeTab, setActiveTab] = useState("overview"); - const resolvedBlogPosts = blogPosts.length ? blogPosts : blogSeedPosts; - - const blogHighlights = useMemo( - () => - resolvedBlogPosts.slice(0, 4).map((post) => ({ - slug: post.slug || String(post.id || "post"), - title: post.title || "Untitled", - category: post.category || "General", - date: post.date || post.published_at || null, - })), - [resolvedBlogPosts], - ); - - type ChangelogEntry = (typeof changelogEntries)[number]; - - const latestChangelog = useMemo( - () => changelogEntries.slice(0, 3), - [], - ); - - const statusSnapshot = useMemo( - () => [ - { - name: "Core API", - status: "operational" as const, - uptime: "99.98%", - responseTime: 145, - icon: Server, - }, - { - name: "Database", - status: "operational" as const, - uptime: "99.99%", - responseTime: 89, - icon: Database, - }, - { - name: "Realtime", - status: "operational" as const, - uptime: "99.95%", - responseTime: 112, - icon: Wifi, - }, - { - name: "Deploy & CDN", - status: "operational" as const, - uptime: "99.94%", - responseTime: 76, - icon: Zap, - }, - ], - [], - ); - - const overallStatus = useMemo(() => { - const base = { - label: "All systems operational", - accentClass: "text-emerald-300", - badgeClass: "border-emerald-500/40 bg-emerald-500/10 text-emerald-200", - Icon: CheckCircle, - } as const; - - if (!statusSnapshot.length) return base; - - if (statusSnapshot.some((service) => service.status === "outage")) { - return { - label: "Service disruption", - accentClass: "text-red-300", - badgeClass: "border-red-500/40 bg-red-500/10 text-red-200", - Icon: XCircle, - }; - } - - if (statusSnapshot.some((service) => service.status === "degraded")) { - return { - label: "Partial degradation", - accentClass: "text-yellow-300", - badgeClass: "border-yellow-500/40 bg-yellow-500/10 text-yellow-200", - Icon: AlertTriangle, - }; - } - - return base; - }, [statusSnapshot]); - - const blogReach = useMemo( - () => - resolvedBlogPosts.reduce((total, post) => total + (post.likes ?? 0), 0), - [resolvedBlogPosts], - ); - const selectedMember = useMemo( () => managedProfiles.find((profile) => profile.id === selectedMemberId) ?? null, - [managedProfiles, selectedMemberId], + [managedProfiles, selectedMemberId] ); const totalMembers = managedProfiles.length; @@ -393,234 +308,15 @@ export default function Admin() { ); }).length; - const infrastructureMetrics = useMemo(() => { - if (!statusSnapshot.length) { - return { - averageResponseTime: null as number | null, - averageUptime: null as number | null, - degradedServices: 0, - healthyServices: 0, - totalServices: 0, - }; - } - - const totalServices = statusSnapshot.length; - const degradedServices = statusSnapshot.filter( - (service) => service.status !== "operational", - ).length; - const averageResponseTime = Math.round( - statusSnapshot.reduce((sum, service) => sum + service.responseTime, 0) / - totalServices, - ); - const uptimeAccumulator = statusSnapshot.reduce( - (acc, service) => { - const numeric = Number.parseFloat(service.uptime); - if (Number.isFinite(numeric)) { - return { total: acc.total + numeric, count: acc.count + 1 }; - } - return acc; - }, - { total: 0, count: 0 }, - ); - const averageUptime = uptimeAccumulator.count - ? uptimeAccumulator.total / uptimeAccumulator.count - : null; - - return { - averageResponseTime, - averageUptime, - degradedServices, - healthyServices: totalServices - degradedServices, - totalServices, - }; - }, [statusSnapshot]); - - 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…" - : blogHighlights.length - ? `Latest: ${blogHighlights[0].title}` - : "Curate new stories", - icon: PenTool, - tone: "purple" as const, - }, - { - title: "Blog engagement", - value: blogReach ? `${blogReach.toLocaleString()} applause` : "—", - description: "Aggregate reactions across highlighted AeThex posts.", - trend: - blogHighlights.length > 1 - ? `Next up: ${blogHighlights[1].title}` - : "Share a new update", - icon: Activity, - tone: "red" as const, - }, - { - title: "Average latency", - value: - infrastructureMetrics.averageResponseTime !== null - ? `${infrastructureMetrics.averageResponseTime} ms` - : "—", - description: - "Mean response time across monitored infrastructure services.", - trend: - infrastructureMetrics.degradedServices > 0 - ? `${infrastructureMetrics.degradedServices} service${infrastructureMetrics.degradedServices === 1 ? "" : "s"} above SLA target` - : "All services meeting SLA", - icon: Zap, - tone: "purple" as const, - }, - { - title: "Reliability coverage", - value: - infrastructureMetrics.totalServices > 0 - ? `${infrastructureMetrics.healthyServices}/${infrastructureMetrics.totalServices} healthy` - : "—", - description: "Operational services within the AeThex platform stack.", - trend: - infrastructureMetrics.averageUptime !== null - ? `Avg uptime ${infrastructureMetrics.averageUptime.toFixed(2)}%` - : "Awaiting uptime telemetry", - icon: Shield, - tone: "green" 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} awaiting review`, - icon: Rocket, - tone: "green" as const, - }, - ], - [ - projectApplications.length, - projectApplicationsLoading, - opportunityApplications.length, - opportunityApplicationsLoading, - featuredStudios, - loadingPosts, - pendingProjectApplications, - publishedPosts, - totalMembers, - blogReach, - blogHighlights, - infrastructureMetrics, - ], - ); - - 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: "Opportunity applicants", - description: - "Review contributor and career applications from Opportunities.", - icon: Users, - action: () => { - setActiveTab("operations"); - if (typeof window !== "undefined") { - setTimeout(() => { - document - .getElementById("opportunity-applications") - ?.scrollIntoView({ behavior: "smooth", block: "start" }); - }, 75); - } - }, - }, - { - label: "System status", - description: "Monitor uptime and live incidents for AeThex services.", - icon: Server, - action: () => navigate("/status"), - }, - { - 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, setActiveTab], + const blogHighlights = useMemo( + () => + resolvedBlogPosts.slice(0, 4).map((post) => ({ + slug: post.slug || String(post.id || "post"), + title: post.title || "Untitled", + category: post.category || "General", + date: post.date || post.published_at || null, + })), + [resolvedBlogPosts] ); useEffect(() => { @@ -638,52 +334,11 @@ export default function Admin() { })(); }, []); - 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} - -

-
-
- - - -
+
+
+
+

+ Control Center +

+

+ Manage platform, users, content, and integrations +

- + Overview System Map Roadmap @@ -758,135 +370,170 @@ export default function Admin() { Community Mentorship Arm Metrics - Discord Management + Discord Operations -
- {overviewStats.map((stat) => ( - - - -
- ) : undefined - } - /> - ))} -
- -
- navigate("/status")} - /> - +
- -
- - Quick actions -
- - Launch frequent administrative workflows. - + + + Total Members + - - {quickActions.map( - ({ label, description, icon: ActionIcon, action }) => ( - - ), - )} + +
+ {totalMembers || "—"} +
+

+ Active profiles synced +

- navigate("/changelog")} - /> + + + + Published Posts + + + +
+ {publishedPosts || "0"} +
+

+ Blog entries available +

+
+
- -
- - Access control -
- - Owner-only access enforced via Supabase roles. - + + + Featured Studios + - -
    -
  • - Owner email:{" "} - {ownerEmail} -
  • -
  • - Roles are provisioned automatically on owner sign-in. -
  • -
  • - Grant additional admins by updating Supabase role - assignments. -
  • -
-
- - + +
+ {featuredStudios}
+

+ Highlighted partners +

+
+ + + + + + Pending Applications + + + +
+ {pendingProjectApplications} +
+

+ Awaiting review +

+ + + +
+ + Quick Actions +
+
+ + + + + + + + + + + + + +
@@ -902,7 +549,7 @@ export default function Admin() {
- Content overview + Blog Management
{publishedPosts} published{" "} @@ -912,232 +559,40 @@ export default function Admin() { : "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 after saving.

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