"use client" import type React from "react" import { useEffect, useState, useRef } from "react" import { motion } from "framer-motion" import { Stethoscope, Shield, Heart, Zap, TrendingUp, Activity, Users } from "lucide-react" interface RegistrationEntry { id: string name: string type: "Guardian" | "Healer" | "CHW" | "Health Facility" location: string timestamp: string flbEarned: number verified: boolean } interface CommunityStats { healers: { total: number verified: number pending: number specializations: { nurses: number doctors: number midwives: number pharmacists: number } } guardians: { total: number active: number totalContributions: number } soulbound: { total: number resonanceHigh: number ancestralVerified: number } codex: { scrollKeepers: number proverbContributors: number codeContributors: number totalScrolls: number } testnet: { activeNodes: number newJoinsToday: number transactionsToday: number } mainnet: { activeNodes: number newJoinsToday: number transactionsToday: number } regions: { westAfrica: number eastAfrica: number southernAfrica: number northAfrica: number centralAfrica: number } impact: { totalPatientsServed: number communitiesReached: number donationsReceived: number flbTokensEarned: number } growth: { thisMonth: number thisWeek: number today: number } lastUpdated: string liveRegistrations: RegistrationEntry[] } interface BubbleConfig { id: string label: string count: number icon: React.ElementType category: | "testnet" | "mainnet" | "healers" | "guardians" | "soulbound" | "codex" | "verified" | "active" | "live-person" size: "xs" | "sm" | "md" | "lg" | "xl" position: { x: number; y: number } priority: number velocity: { x: number; y: number } isLive?: boolean personData?: RegistrationEntry isNewJoin?: boolean } export function DynamicBubbleField() { const [stats, setStats] = useState(null) const [loading, setLoading] = useState(true) const [bubbles, setBubbles] = useState([]) const [liveRegistrations, setLiveRegistrations] = useState([]) const containerRef = useRef(null) const animationRef = useRef() // Fetch live registration data from Google Sheets const fetchLiveRegistrations = async (): Promise => { try { // In a real implementation, this would fetch from Google Sheets API // For now, we'll simulate live data that updates const mockRegistrations: RegistrationEntry[] = [ { id: `${Date.now()}-1`, name: "Dr. Amara Kone", type: "Healer", location: "Lagos, Nigeria", timestamp: new Date(Date.now() - Math.random() * 300000).toISOString(), flbEarned: 250, verified: true, }, { id: `${Date.now()}-2`, name: "Kwame Asante", type: "Guardian", location: "Accra, Ghana", timestamp: new Date(Date.now() - Math.random() * 600000).toISOString(), flbEarned: 150, verified: true, }, { id: `${Date.now()}-3`, name: "Sarah Okafor", type: "CHW", location: "Kano, Nigeria", timestamp: new Date(Date.now() - Math.random() * 900000).toISOString(), flbEarned: 100, verified: false, }, { id: `${Date.now()}-4`, name: "Nairobi Community Health Center", type: "Health Facility", location: "Nairobi, Kenya", timestamp: new Date(Date.now() - Math.random() * 1200000).toISOString(), flbEarned: 500, verified: true, }, { id: `${Date.now()}-5`, name: "Fatima Al-Rashid", type: "Guardian", location: "Cairo, Egypt", timestamp: new Date(Date.now() - Math.random() * 1500000).toISOString(), flbEarned: 200, verified: true, }, { id: `${Date.now()}-6`, name: "Dr. Kofi Mensah", type: "Healer", location: "Kumasi, Ghana", timestamp: new Date(Date.now() - Math.random() * 300000).toISOString(), flbEarned: 300, verified: true, }, { id: `${Date.now()}-7`, name: "Aisha Mwangi", type: "CHW", location: "Mombasa, Kenya", timestamp: new Date(Date.now() - Math.random() * 400000).toISOString(), flbEarned: 120, verified: false, }, { id: `${Date.now()}-8`, name: "Ubuntu Health Collective", type: "Health Facility", location: "Cape Town, South Africa", timestamp: new Date(Date.now() - Math.random() * 500000).toISOString(), flbEarned: 450, verified: true, }, ] // Simulate new registrations appearing const recentRegistrations = mockRegistrations.filter((reg) => { const regTime = new Date(reg.timestamp).getTime() const now = Date.now() return now - regTime < 1800000 // Last 30 minutes }) return recentRegistrations } catch (error) { console.error("Error fetching live registrations:", error) return [] } } useEffect(() => { const fetchStats = async () => { try { // Fetch live registrations const registrations = await fetchLiveRegistrations() setLiveRegistrations(registrations) // Try to fetch community stats const response = await fetch("/api/community-stats") let statsData: CommunityStats if (response.ok) { statsData = await response.json() } else { // Generate mock stats based on live registrations statsData = generateMockStats(registrations) } // Add live registrations to stats statsData.liveRegistrations = registrations setStats(statsData) generateBubbles(statsData) updateCSSVariables(statsData) } catch (error) { console.error("Error fetching community stats:", error) // Use mock data as fallback const registrations = await fetchLiveRegistrations() const mockStats = generateMockStats(registrations) mockStats.liveRegistrations = registrations setStats(mockStats) generateBubbles(mockStats) updateCSSVariables(mockStats) } finally { setLoading(false) } } fetchStats() const interval = setInterval(fetchStats, 10000) // Update every 10 seconds for live feel return () => clearInterval(interval) }, []) const generateMockStats = (registrations: RegistrationEntry[]): CommunityStats => { const healers = registrations.filter((r) => r.type === "Healer" || r.type === "CHW").length const guardians = registrations.filter((r) => r.type === "Guardian").length const facilities = registrations.filter((r) => r.type === "Health Facility").length return { healers: { total: 1247 + healers, verified: 892 + registrations.filter((r) => (r.type === "Healer" || r.type === "CHW") && r.verified).length, pending: 355, specializations: { nurses: 456, doctors: 234, midwives: 189, pharmacists: 123, }, }, guardians: { total: 2156 + guardians, active: 1834 + guardians, totalContributions: 45678, }, soulbound: { total: 567, resonanceHigh: 234, ancestralVerified: 345, }, codex: { scrollKeepers: 89, proverbContributors: 234, codeContributors: 156, totalScrolls: 1234, }, testnet: { activeNodes: 45 + Math.floor(Math.random() * 10), newJoinsToday: registrations.length + Math.floor(Math.random() * 5), transactionsToday: 234 + Math.floor(Math.random() * 50), }, mainnet: { activeNodes: 128 + Math.floor(Math.random() * 20), newJoinsToday: Math.floor(registrations.length / 2) + Math.floor(Math.random() * 3), transactionsToday: 567 + Math.floor(Math.random() * 100), }, regions: { westAfrica: 1234, eastAfrica: 987, southernAfrica: 654, northAfrica: 432, centralAfrica: 321, }, impact: { totalPatientsServed: 45678, communitiesReached: 234, donationsReceived: 123456, flbTokensEarned: 987654 + registrations.reduce((sum, r) => sum + r.flbEarned, 0), }, growth: { thisMonth: 234, thisWeek: 67, today: registrations.length, }, lastUpdated: new Date().toISOString(), liveRegistrations: registrations, } } const updateCSSVariables = (stats: CommunityStats) => { const root = document.documentElement root.style.setProperty("--testnet-nodes", stats.testnet.activeNodes.toString()) root.style.setProperty("--mainnet-nodes", stats.mainnet.activeNodes.toString()) root.style.setProperty("--healers-count", stats.healers.total.toString()) root.style.setProperty("--guardians-count", stats.guardians.total.toString()) root.style.setProperty("--live-registrations", stats.liveRegistrations.length.toString()) } const generateBubbles = (stats: CommunityStats) => { const bubbleConfigs: Omit[] = [ // Network bubbles (highest priority) { id: "testnet", label: "Testnet Nodes", count: stats.testnet.activeNodes, icon: Activity, category: "testnet", size: "xl", priority: 1, isLive: true, }, { id: "mainnet", label: "Mainnet Nodes", count: stats.mainnet.activeNodes, icon: Zap, category: "mainnet", size: "xl", priority: 1, isLive: true, }, // Primary categories { id: "healers", label: "Healers", count: stats.healers.total, icon: Stethoscope, category: "healers", size: "lg", priority: 2, }, { id: "guardians", label: "Guardians", count: stats.guardians.total, icon: Shield, category: "guardians", size: "lg", priority: 2, }, // Network activity { id: "testnet-joins", label: "Testnet Joins Today", count: stats.testnet.newJoinsToday, icon: TrendingUp, category: "testnet", size: "md", priority: 3, isLive: true, }, { id: "mainnet-joins", label: "Mainnet Joins Today", count: stats.mainnet.newJoinsToday, icon: TrendingUp, category: "mainnet", size: "md", priority: 3, isLive: true, }, ] // Add live person bubbles for recent registrations const personBubbles = stats.liveRegistrations.slice(0, 8).map((registration, index) => ({ id: `person-${registration.id}`, label: registration.name.split(" ")[0], // First name only for bubble count: registration.flbEarned, icon: registration.type === "Guardian" ? Shield : registration.type === "Healer" ? Stethoscope : registration.type === "CHW" ? Users : Heart, category: "live-person" as const, size: "sm" as const, priority: 4, isLive: true, personData: registration, isNewJoin: Date.now() - new Date(registration.timestamp).getTime() < 600000, // Last 10 minutes })) const allBubbles = [...bubbleConfigs, ...personBubbles] // Generate positions and velocities using improved distribution const generatedBubbles = allBubbles.map((config, index) => { const angle = index * 137.5 * (Math.PI / 180) // Golden angle const radius = Math.sqrt(index + 1) * 12 const centerX = 50 const centerY = 50 return { ...config, position: { x: Math.max(10, Math.min(90, centerX + Math.cos(angle) * radius)), y: Math.max(10, Math.min(90, centerY + Math.sin(angle) * radius)), }, velocity: { x: (Math.random() - 0.5) * 0.5, y: (Math.random() - 0.5) * 0.5, }, } }) setBubbles(generatedBubbles) } // Animate bubbles continuously useEffect(() => { if (bubbles.length === 0) return const animate = () => { setBubbles((prevBubbles) => prevBubbles.map((bubble) => { let newX = bubble.position.x + bubble.velocity.x let newY = bubble.position.y + bubble.velocity.y let newVx = bubble.velocity.x let newVy = bubble.velocity.y // Bounce off edges if (newX <= 5 || newX >= 95) { newVx = -newVx * 0.8 newX = Math.max(5, Math.min(95, newX)) } if (newY <= 5 || newY >= 95) { newVy = -newVy * 0.8 newY = Math.max(5, Math.min(95, newY)) } // Add slight random movement for live bubbles if (bubble.isLive) { newVx += (Math.random() - 0.5) * 0.1 newVy += (Math.random() - 0.5) * 0.1 } // Extra movement for new joins if (bubble.isNewJoin) { newVx += (Math.random() - 0.5) * 0.2 newVy += (Math.random() - 0.5) * 0.2 } // Apply friction newVx *= 0.99 newVy *= 0.99 return { ...bubble, position: { x: newX, y: newY }, velocity: { x: newVx, y: newVy }, } }), ) animationRef.current = requestAnimationFrame(animate) } animate() return () => { if (animationRef.current) { cancelAnimationFrame(animationRef.current) } } }, [bubbles.length]) const getBubbleClasses = (bubble: BubbleConfig) => { const baseClasses = `data-bubble bubble-${bubble.size} bubble-${bubble.category}` const liveClass = bubble.isLive ? "bubble-live" : "" const newJoinClass = bubble.isNewJoin ? "bubble-new-join" : "" const personClass = bubble.category === "live-person" ? "bubble-person" : "" return `${baseClasses} ${liveClass} ${newJoinClass} ${personClass}`.trim() } const getPersonTypeColor = (type: string) => { switch (type) { case "Guardian": return "from-blue-400 to-indigo-600" case "Healer": return "from-green-400 to-emerald-600" case "CHW": return "from-purple-400 to-violet-600" case "Health Facility": return "from-orange-400 to-red-600" default: return "from-gray-400 to-gray-600" } } if (loading) { return (
) } return (
{/* Title */}

Live Network & Community Pulse

Real-time visualization of testnet, mainnet, and live registrations from Google Forms

{stats && (
🔥 {stats.liveRegistrations.length} people joined recently • Last update:{" "} {new Date(stats.lastUpdated).toLocaleTimeString()}
)}
{/* Bubble Field */}
{/* Background pattern */}
{/* Dynamic Bubbles */} {bubbles.map((bubble) => { const Icon = bubble.icon return ( {/* Live indicator */} {bubble.isLive && (
)} {/* New join indicator */} {bubble.isNewJoin && (
)} {/* Person bubble styling */} {bubble.category === "live-person" && bubble.personData && (
)} {/* Icon */} {/* Count with live animation */} {bubble.category === "live-person" ? bubble.count : bubble.count} {/* Label */} {bubble.size !== "xs" && (
{bubble.label}
)} {/* Enhanced tooltip for persons */}
{bubble.personData ? (
{bubble.personData.name}
{bubble.personData.type}
{bubble.personData.location}
+{bubble.personData.flbEarned} FLB
{bubble.personData.verified &&
✓ Verified
}
{new Date(bubble.personData.timestamp).toLocaleTimeString()}
) : (
{bubble.label}: {bubble.count.toLocaleString()} {bubble.isLive &&
● LIVE
}
)}
) })} {/* Network Status */} {stats && (
LIVE REGISTRATIONS
{stats.liveRegistrations.length} recent joins
Testnet: {stats.testnet.activeNodes} | Mainnet: {stats.mainnet.activeNodes}
)}
{/* Enhanced Legend */}
Testnet
Mainnet
Healers
Guardians
CHWs
Live Joins
{/* Live Registration Summary */} {stats && stats.liveRegistrations.length > 0 && (

Recent Live Registrations

{stats.liveRegistrations.slice(0, 4).map((reg) => (
{reg.name.split(" ")[0]}
{reg.type} • {reg.location.split(",")[0]}
+{reg.flbEarned} FLB
))}
)}
) }