import { useEffect, useState, useCallback, useRef, useMemo, type MouseEvent } from "react"; import { useDiscordActivity } from "@/contexts/DiscordActivityContext"; import LoadingScreen from "@/components/LoadingScreen"; import { Heart, MessageCircle, Zap, Gamepad2, Briefcase, BookOpen, Sparkles, Shield, ExternalLink, Flame, Star, Target, Gift, CheckCircle, AlertCircle, Loader2, ChevronRight, Users, TrendingUp, Calendar, Award, X, Send, MessagesSquare, BarChart3, Plus, Vote, Trophy, Clock, ThumbsUp, Layers, Eye, } from "lucide-react"; import { motion, AnimatePresence } from "framer-motion"; const APP_URL = "https://aethex.dev"; type ArmType = "labs" | "gameforge" | "corp" | "foundation" | "nexus" | "staff"; const ARM_CONFIG: Record = { labs: { label: "Labs", icon: Zap, color: "#facc15", gradient: "from-yellow-500/20 via-amber-500/10 to-transparent", glow: "shadow-yellow-500/30" }, gameforge: { label: "GameForge", icon: Gamepad2, color: "#4ade80", gradient: "from-green-500/20 via-emerald-500/10 to-transparent", glow: "shadow-green-500/30" }, corp: { label: "Corp", icon: Briefcase, color: "#60a5fa", gradient: "from-blue-500/20 via-sky-500/10 to-transparent", glow: "shadow-blue-500/30" }, foundation: { label: "Foundation", icon: BookOpen, color: "#f87171", gradient: "from-red-500/20 via-rose-500/10 to-transparent", glow: "shadow-red-500/30" }, nexus: { label: "Nexus", icon: Sparkles, color: "#c084fc", gradient: "from-purple-500/20 via-violet-500/10 to-transparent", glow: "shadow-purple-500/30" }, staff: { label: "Staff", icon: Shield, color: "#818cf8", gradient: "from-indigo-500/20 via-blue-500/10 to-transparent", glow: "shadow-indigo-500/30" }, }; interface Post { id: string; title: string; content: string; arm_affiliation: ArmType; author_id: string; created_at: string; likes_count: number; comments_count: number; tags?: string[]; user_profiles?: { id: string; username?: string; full_name?: string; avatar_url?: string }; } interface LeaderboardEntry { rank: number; user_id: string; username: string; avatar_url?: string; total_xp: number; level: number; current_streak?: number; } interface UserStats { total_xp: number; level: number; current_streak: number; longest_streak: number; rank?: number; } function XPRing({ xp, level, size = 64, strokeWidth = 4, color }: { xp: number; level: number; size?: number; strokeWidth?: number; color: string }) { const xpForLevel = 1000; const xpInCurrentLevel = xp % xpForLevel; const progress = xpInCurrentLevel / xpForLevel; const radius = (size - strokeWidth) / 2; const circumference = 2 * Math.PI * radius; const strokeDashoffset = circumference * (1 - progress); return (
{level}
); } function XPGainAnimation({ amount, onComplete }: { amount: number; onComplete: () => void }) { useEffect(() => { const timer = setTimeout(onComplete, 2000); return () => clearTimeout(timer); }, [onComplete]); return ( +{amount} XP ); } function ConfettiEffect() { const colors = ["#facc15", "#4ade80", "#60a5fa", "#c084fc", "#f87171"]; return (
{Array.from({ length: 50 }).map((_, i) => ( 0.5 ? 1 : -1), x: (Math.random() - 0.5) * 200 }} transition={{ duration: 2 + Math.random() * 2, delay: Math.random() * 0.5, ease: "easeOut" }} /> ))}
); } function FeedTab({ openExternalLink, userId }: { openExternalLink: (url: string) => Promise; userId?: string }) { const [posts, setPosts] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [likingPost, setLikingPost] = useState(null); const [likedPosts, setLikedPosts] = useState>(new Set()); const fetchPosts = useCallback(async () => { setLoading(true); setError(null); try { const response = await fetch("/api/feed?limit=10"); if (!response.ok) throw new Error("Failed to load"); const data = await response.json(); setPosts(data.data || []); } catch { setError("Couldn't load feed"); } finally { setLoading(false); } }, []); const handleQuickLike = async (postId: string, e: MouseEvent) => { e.stopPropagation(); if (!userId || likingPost) return; setLikingPost(postId); try { const response = await fetch(`/api/feed/${postId}/like`, { method: "POST", headers: { "Content-Type": "application/json" }, }); if (response.ok) { setLikedPosts(prev => new Set(prev).add(postId)); setPosts(prev => prev.map(p => p.id === postId ? { ...p, likes_count: p.likes_count + 1 } : p )); } } catch { // Silent fail for likes } finally { setLikingPost(null); } }; useEffect(() => { fetchPosts(); }, [fetchPosts]); if (loading) return (
); if (error) return (

{error}

); return ( {posts.length === 0 ? ( ) : ( posts.map((post, index) => { const config = ARM_CONFIG[post.arm_affiliation] || ARM_CONFIG.nexus; const isLiked = likedPosts.has(post.id); const isLiking = likingPost === post.id; return ( {post.comments_count} ); }) )} ); } function RealmsTab({ currentRealm, openExternalLink }: { currentRealm: ArmType; openExternalLink: (url: string) => Promise }) { const realms = Object.entries(ARM_CONFIG) as [ArmType, typeof ARM_CONFIG[ArmType]][]; return (
{realms.map(([key, config], index) => { const Icon = config.icon; const isActive = currentRealm === key; return ( openExternalLink(`${APP_URL}/${key}`)} initial={{ opacity: 0, scale: 0.9 }} animate={{ opacity: 1, scale: 1 }} transition={{ delay: index * 0.05 }} whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }} className={`p-3 rounded-xl transition-all flex items-center gap-3 border ${ isActive ? `bg-gradient-to-br ${config.gradient} border-[${config.color}]/30 shadow-lg ${config.glow}` : "bg-[#232428] hover:bg-[#2b2d31] border-[#3f4147]" }`} >
{config.label} {isActive && }
); })}

Tap to explore each realm

); } function LeaderboardTab({ openExternalLink, currentUserId }: { openExternalLink: (url: string) => Promise; currentUserId?: string }) { const leaderboard: LeaderboardEntry[] = [ { rank: 1, user_id: "1", username: "PixelMaster", total_xp: 15420, level: 16, current_streak: 21 }, { rank: 2, user_id: "2", username: "CodeNinja", total_xp: 12800, level: 13, current_streak: 14 }, { rank: 3, user_id: "3", username: "BuilderX", total_xp: 11250, level: 12, current_streak: 7 }, { rank: 4, user_id: "4", username: "GameDev_Pro", total_xp: 9800, level: 10, current_streak: 5 }, { rank: 5, user_id: "5", username: "ForgeHero", total_xp: 8500, level: 9, current_streak: 12 }, { rank: 6, user_id: "6", username: "NexusCreator", total_xp: 7200, level: 8, current_streak: 3 }, { rank: 7, user_id: "7", username: "LabsExplorer", total_xp: 6100, level: 7 }, ]; const medals = ["๐Ÿฅ‡", "๐Ÿฅˆ", "๐Ÿฅ‰"]; return (
Your Rank #12

Keep earning XP to climb!

{( leaderboard.map((entry, index) => ( {medals[index] || `#${index + 1}`} {entry.avatar_url ? ( ) : (
{entry.username?.[0]?.toUpperCase() || "?"}
)}

{entry.username}

Lvl {entry.level} ยท {entry.total_xp.toLocaleString()} XP

{entry.current_streak && entry.current_streak > 0 && ( {entry.current_streak} )}
)) )}
); } function QuestsTab({ userId, onXPGain }: { userId?: string; onXPGain: (amount: number) => void }) { const [dailyClaimed, setDailyClaimed] = useState(() => { try { const today = new Date().toISOString().slice(0, 10); const lastClaim = localStorage.getItem('aethex_daily_claim'); return lastClaim === today; } catch { return false; } }); const [claiming, setClaiming] = useState(false); const claimDailyXP = () => { if (claiming || dailyClaimed) return; setClaiming(true); setTimeout(() => { try { const today = new Date().toISOString().slice(0, 10); localStorage.setItem('aethex_daily_claim', today); } catch {} setDailyClaimed(true); onXPGain(25); setClaiming(false); }, 500); }; const quests = [ { id: "daily", title: "Daily Login", description: "Log in today", xp: 25, icon: Calendar, canClaim: !dailyClaimed, onClaim: claimDailyXP, claiming }, { id: "post", title: "Share Your Work", description: "Create a post", xp: 20, icon: Star, progress: 0, total: 1 }, { id: "like", title: "Show Support", description: "Like 5 posts", xp: 15, icon: Heart, progress: 3, total: 5 }, { id: "explore", title: "Realm Explorer", description: "Visit 3 realms", xp: 30, icon: Sparkles, progress: 2, total: 3 }, ]; return (
Daily Quests Resets at midnight UTC
{quests.map((quest, index) => { const Icon = quest.icon; const isCompleted = quest.progress !== undefined && quest.progress >= (quest.total || 1); return (
{quest.title} +{quest.xp} XP

{quest.description}

{quest.progress !== undefined && quest.total && (
{quest.progress}/{quest.total}
)}
{quest.canClaim !== undefined && ( {quest.claiming ? ( ) : quest.canClaim ? ( "Claim" ) : ( )} )} {isCompleted && !quest.canClaim && ( )}
); })}
); } function JobsTab({ openExternalLink }: { openExternalLink: (url: string) => Promise }) { const [jobs, setJobs] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { const fetchJobs = async () => { try { const response = await fetch("/api/opportunities?limit=5&status=active"); if (response.ok) { const data = await response.json(); setJobs(data.data || []); } } catch { setJobs([]); } finally { setLoading(false); } }; fetchJobs(); }, []); const categories = [ { label: "Full-Time", icon: Briefcase, color: "#60a5fa" }, { label: "Contract", icon: Target, color: "#4ade80" }, { label: "Freelance", icon: Star, color: "#facc15" }, ]; if (loading) return (
); return (
{categories.map(({ label, icon: Icon, color }, index) => ( openExternalLink(`${APP_URL}/opportunities?type=${label.toLowerCase()}`)} initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: index * 0.05 }} whileHover={{ scale: 1.02, y: -2 }} whileTap={{ scale: 0.98 }} className="p-3 rounded-xl bg-[#232428] hover:bg-[#2b2d31] transition-all flex flex-col items-center gap-2 border border-[#3f4147]" > {label} ))}
{jobs.length > 0 && (

Latest Opportunities

{jobs.slice(0, 3).map((job, index) => ( openExternalLink(`${APP_URL}/opportunities/${job.id}`)} initial={{ opacity: 0, x: -10 }} animate={{ opacity: 1, x: 0 }} transition={{ delay: 0.15 + index * 0.05 }} className="w-full p-3 rounded-xl bg-[#232428] hover:bg-[#2b2d31] transition-all text-left border border-[#3f4147] group" >

{job.title}

{job.company_name || "Remote"}

))}
)} openExternalLink(`${APP_URL}/opportunities`)} whileHover={{ scale: 1.01 }} whileTap={{ scale: 0.99 }} className="w-full py-3 rounded-xl bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 transition-all text-white text-sm font-medium shadow-lg shadow-purple-500/20" > Browse All Opportunities
); } function BadgesTab({ userId, openExternalLink }: { userId?: string; openExternalLink: (url: string) => Promise }) { const displayBadges = [ { id: "1", name: "Early Adopter", icon: "๐Ÿš€", description: "Joined during beta", unlocked: true }, { id: "2", name: "First Post", icon: "โœจ", description: "Created your first post", unlocked: true }, { id: "3", name: "Realm Explorer", icon: "๐Ÿ—บ๏ธ", description: "Visited all 6 realms", unlocked: false, progress: 4, total: 6 }, { id: "4", name: "Social Butterfly", icon: "๐Ÿฆ‹", description: "Made 10 connections", unlocked: false, progress: 6, total: 10 }, { id: "5", name: "Top Contributor", icon: "๐Ÿ‘‘", description: "Reach top 10 leaderboard", unlocked: false }, { id: "6", name: "Week Warrior", icon: "โš”๏ธ", description: "7-day login streak", unlocked: false, progress: 3, total: 7 }, ]; return (
Your Badges {displayBadges.filter(b => b.unlocked).length}/{displayBadges.length} unlocked
{displayBadges.map((badge, index) => ( {badge.icon}
{badge.name} {badge.unlocked && }

{badge.description}

{!badge.unlocked && badge.progress !== undefined && badge.total && (
{badge.progress}/{badge.total}
)}
{badge.unlocked && }
))}
); } interface Poll { id: string; question: string; options: { id: string; text: string; votes: number }[]; createdBy: string; createdByName: string; createdAt: number; expiresAt: number; votedUsers: string[]; } function PollsTab({ userId, username }: { userId?: string; username?: string }) { const [polls, setPolls] = useState(() => { try { const saved = localStorage.getItem('aethex_activity_polls'); const parsed = saved ? JSON.parse(saved) : []; return parsed.filter((p: Poll) => p.expiresAt > Date.now()); } catch { return []; } }); const [showCreate, setShowCreate] = useState(false); const [newQuestion, setNewQuestion] = useState(''); const [newOptions, setNewOptions] = useState(['', '']); const [creating, setCreating] = useState(false); useEffect(() => { try { localStorage.setItem('aethex_activity_polls', JSON.stringify(polls)); } catch {} }, [polls]); const createPoll = () => { if (!userId || !newQuestion.trim() || newOptions.filter(o => o.trim()).length < 2) return; setCreating(true); const poll: Poll = { id: `${Date.now()}-${Math.random().toString(36).slice(2)}`, question: newQuestion.trim(), options: newOptions.filter(o => o.trim()).map((text, i) => ({ id: `opt-${i}`, text: text.trim(), votes: 0 })), createdBy: userId, createdByName: username || 'Anonymous', createdAt: Date.now(), expiresAt: Date.now() + 24 * 60 * 60 * 1000, votedUsers: [] }; setPolls(prev => [poll, ...prev]); setNewQuestion(''); setNewOptions(['', '']); setShowCreate(false); setCreating(false); }; const vote = (pollId: string, optionId: string) => { if (!userId) return; setPolls(prev => prev.map(poll => { if (poll.id !== pollId || poll.votedUsers.includes(userId)) return poll; return { ...poll, options: poll.options.map(opt => opt.id === optionId ? { ...opt, votes: opt.votes + 1 } : opt ), votedUsers: [...poll.votedUsers, userId] }; })); }; const deletePoll = (pollId: string) => { setPolls(prev => prev.filter(p => p.id !== pollId)); }; const formatTimeRemaining = (expiresAt: number) => { const remaining = expiresAt - Date.now(); if (remaining <= 0) return 'Expired'; const hours = Math.floor(remaining / (1000 * 60 * 60)); const minutes = Math.floor((remaining % (1000 * 60 * 60)) / (1000 * 60)); if (hours > 0) return `${hours}h ${minutes}m left`; return `${minutes}m left`; }; const samplePolls: Poll[] = [ { id: 'sample1', question: 'What realm are you most excited about?', options: [ { id: 'opt-1', text: 'GameForge', votes: 12 }, { id: 'opt-2', text: 'Labs', votes: 8 }, { id: 'opt-3', text: 'Nexus', votes: 15 }, { id: 'opt-4', text: 'Foundation', votes: 5 }, ], createdBy: 'system', createdByName: 'AeThex', createdAt: Date.now() - 3600000, expiresAt: Date.now() + 20 * 60 * 60 * 1000, votedUsers: [] } ]; const displayPolls = polls.length > 0 ? polls : samplePolls; return ( {showCreate && (
Create Poll
setNewQuestion(e.target.value)} placeholder="Ask a question..." className="w-full bg-[#1e1f22] text-white placeholder-[#949ba4] px-3 py-2 rounded-lg border border-[#3f4147] focus:border-purple-500 focus:outline-none text-sm" />
{newOptions.map((opt, i) => (
{ const updated = [...newOptions]; updated[i] = e.target.value; setNewOptions(updated); }} placeholder={`Option ${i + 1}`} className="flex-1 bg-[#1e1f22] text-white placeholder-[#949ba4] px-3 py-2 rounded-lg border border-[#3f4147] focus:border-purple-500 focus:outline-none text-sm" /> {newOptions.length > 2 && ( )}
))} {newOptions.length < 5 && ( )}
o.trim()).length < 2 || creating} whileHover={{ scale: 1.01 }} whileTap={{ scale: 0.99 }} className="w-full py-2.5 rounded-xl bg-purple-500 hover:bg-purple-600 disabled:bg-[#3f4147] disabled:opacity-50 text-white text-sm font-medium transition-colors" > {creating ? : 'Create Poll'}
)}
{!showCreate && userId && ( setShowCreate(true)} whileHover={{ scale: 1.01 }} whileTap={{ scale: 0.99 }} className="w-full py-3 rounded-xl bg-[#232428] hover:bg-[#2b2d31] border border-[#3f4147] text-[#b5bac1] text-sm font-medium flex items-center justify-center gap-2 transition-colors" > Create a Poll )} {displayPolls.map((poll, pollIndex) => { const totalVotes = poll.options.reduce((sum, opt) => sum + opt.votes, 0); const hasVoted = userId && poll.votedUsers.includes(userId); const isOwner = userId === poll.createdBy; return (

{poll.question}

by {poll.createdByName} ยท {totalVotes} vote{totalVotes !== 1 ? 's' : ''} ยท {formatTimeRemaining(poll.expiresAt)}

{isOwner && ( )}
{poll.options.map((option) => { const percentage = totalVotes > 0 ? Math.round((option.votes / totalVotes) * 100) : 0; return ( vote(poll.id, option.id)} disabled={!userId || hasVoted} whileHover={!hasVoted && userId ? { scale: 1.01 } : {}} whileTap={!hasVoted && userId ? { scale: 0.99 } : {}} className={`w-full relative overflow-hidden rounded-lg text-left transition-all ${ hasVoted ? 'bg-[#1e1f22] cursor-default' : 'bg-[#1e1f22] hover:bg-[#2b2d31] cursor-pointer' }`} > {hasVoted && ( )}
{option.text} {hasVoted && ( {percentage}% )}
); })}
{!userId && (

Sign in to vote

)}
); })} {polls.length === 0 && (

Polls expire after 24 hours. Create one to get opinions!

)}
); } interface Challenge { id: string; title: string; description: string; xpReward: number; type: 'daily' | 'weekly'; requirement: number; icon: string; endsAt: number; } function ChallengesTab({ userId, onXPGain }: { userId?: string; onXPGain: (amount: number) => void }) { const [claimedChallenges, setClaimedChallenges] = useState>(() => { try { const saved = localStorage.getItem('aethex_claimed_challenges'); const parsed = saved ? JSON.parse(saved) : { claimed: [], lastReset: 0 }; const now = Date.now(); const weekStart = now - (now % (7 * 24 * 60 * 60 * 1000)); if (parsed.lastReset < weekStart) { return new Set(); } return new Set(parsed.claimed); } catch { return new Set(); } }); const [progress, setProgress] = useState>(() => { try { const saved = localStorage.getItem('aethex_challenge_progress'); const parsed = saved ? JSON.parse(saved) : { data: {}, lastReset: 0 }; const now = Date.now(); const weekStart = now - (now % (7 * 24 * 60 * 60 * 1000)); if (parsed.lastReset < weekStart) { return {}; } return parsed.data || parsed; } catch { return {}; } }); const [claiming, setClaiming] = useState(null); useEffect(() => { const now = Date.now(); const weekStart = now - (now % (7 * 24 * 60 * 60 * 1000)); localStorage.setItem('aethex_claimed_challenges', JSON.stringify({ claimed: Array.from(claimedChallenges), lastReset: weekStart })); }, [claimedChallenges]); useEffect(() => { const now = Date.now(); const weekStart = now - (now % (7 * 24 * 60 * 60 * 1000)); localStorage.setItem('aethex_challenge_progress', JSON.stringify({ data: progress, lastReset: weekStart })); }, [progress]); const getWeekEnd = () => { const now = new Date(); const dayOfWeek = now.getDay(); const daysUntilSunday = 7 - dayOfWeek; const endOfWeek = new Date(now); endOfWeek.setDate(now.getDate() + daysUntilSunday); endOfWeek.setHours(23, 59, 59, 999); return endOfWeek.getTime(); }; const formatTimeRemaining = (endsAt: number) => { const remaining = endsAt - Date.now(); if (remaining <= 0) return 'Ended'; const days = Math.floor(remaining / (1000 * 60 * 60 * 24)); const hours = Math.floor((remaining % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); if (days > 0) return `${days}d ${hours}h left`; return `${hours}h left`; }; const challenges: Challenge[] = [ { id: 'wc1', title: 'Social Butterfly', description: 'Like 20 posts this week', xpReward: 100, type: 'weekly', requirement: 20, icon: '๐Ÿฆ‹', endsAt: getWeekEnd() }, { id: 'wc2', title: 'Realm Hopper', description: 'Visit all 6 realms', xpReward: 150, type: 'weekly', requirement: 6, icon: '๐ŸŒ€', endsAt: getWeekEnd() }, { id: 'wc3', title: 'Pollster', description: 'Create 3 polls', xpReward: 75, type: 'weekly', requirement: 3, icon: '๐Ÿ“Š', endsAt: getWeekEnd() }, { id: 'wc4', title: 'Chatterbox', description: 'Send 50 messages in Activity', xpReward: 80, type: 'weekly', requirement: 50, icon: '๐Ÿ’ฌ', endsAt: getWeekEnd() }, { id: 'wc5', title: 'Streak Master', description: 'Maintain a 7-day login streak', xpReward: 200, type: 'weekly', requirement: 7, icon: '๐Ÿ”ฅ', endsAt: getWeekEnd() }, ]; const simulateProgress = (challengeId: string) => { const challenge = challenges.find(c => c.id === challengeId); if (!challenge) return; const current = progress[challengeId] || 0; const mockProgress = Math.min(current + Math.floor(Math.random() * 3) + 1, challenge.requirement); setProgress(prev => ({ ...prev, [challengeId]: mockProgress })); }; const claimReward = (challenge: Challenge) => { if (!userId || claimedChallenges.has(challenge.id) || claiming) return; const currentProgress = progress[challenge.id] || 0; if (currentProgress < challenge.requirement) return; setClaiming(challenge.id); setTimeout(() => { setClaimedChallenges(prev => new Set(prev).add(challenge.id)); onXPGain(challenge.xpReward); setClaiming(null); }, 500); }; return (

Weekly Challenges

{formatTimeRemaining(getWeekEnd())}

{challenges.filter(c => claimedChallenges.has(c.id)).length}/{challenges.length}

Completed

{challenges.map((challenge, index) => { const currentProgress = progress[challenge.id] ?? 0; const isCompleted = currentProgress >= challenge.requirement; const isClaimed = claimedChallenges.has(challenge.id); const isClaiming = claiming === challenge.id; const progressPercent = Math.min((currentProgress / challenge.requirement) * 100, 100); return (
{challenge.icon}
{challenge.title} +{challenge.xpReward} XP

{challenge.description}

{currentProgress}/{challenge.requirement}
{isClaimed ? ( ) : isCompleted ? ( claimReward(challenge)} disabled={isClaiming} whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }} className="px-3 py-1.5 rounded-lg bg-amber-500 hover:bg-amber-600 text-white text-xs font-medium transition-colors shrink-0" > {isClaiming ? : 'Claim'} ) : ( )}
); })}

Complete challenges to earn bonus XP and exclusive badges!

); } interface Project { id: string; title: string; description: string; author: string; authorAvatar?: string; realm: ArmType; thumbnail: string; votes: number; views: number; tags: string[]; } function ProjectsTab({ userId, openExternalLink }: { userId?: string; openExternalLink: (url: string) => Promise }) { const [votedProjects, setVotedProjects] = useState>(() => { try { const saved = localStorage.getItem('aethex_voted_projects'); return new Set(saved ? JSON.parse(saved) : []); } catch { return new Set(); } }); const [projects, setProjects] = useState([ { id: 'p1', title: 'Neon Racer', description: 'A cyberpunk racing game with stunning visuals', author: 'PixelDev', realm: 'gameforge', thumbnail: '๐ŸŽ๏ธ', votes: 142, views: 890, tags: ['Game', 'Racing'] }, { id: 'p2', title: 'DevFlow', description: 'AI-powered code review and workflow tool', author: 'CodeMaster', realm: 'labs', thumbnail: '๐Ÿ”ฎ', votes: 98, views: 654, tags: ['Tool', 'AI'] }, { id: 'p3', title: 'EcoTrack', description: 'Track and reduce your carbon footprint', author: 'GreenCoder', realm: 'foundation', thumbnail: '๐ŸŒฑ', votes: 76, views: 432, tags: ['Social', 'Environment'] }, { id: 'p4', title: 'SoundWave', description: 'Collaborative music production platform', author: 'BeatMaker', realm: 'nexus', thumbnail: '๐ŸŽต', votes: 112, views: 780, tags: ['Music', 'Collaboration'] }, { id: 'p5', title: 'PixelForge', description: '2D game asset generator using AI', author: 'ArtistX', realm: 'gameforge', thumbnail: '๐ŸŽจ', votes: 89, views: 567, tags: ['Tool', 'Art'] }, ]); const [filter, setFilter] = useState('all'); useEffect(() => { localStorage.setItem('aethex_voted_projects', JSON.stringify(Array.from(votedProjects))); }, [votedProjects]); const voteProject = (projectId: string) => { if (!userId || votedProjects.has(projectId)) return; setVotedProjects(prev => new Set(prev).add(projectId)); setProjects(prev => prev.map(p => p.id === projectId ? { ...p, votes: p.votes + 1 } : p )); }; const filteredProjects = filter === 'all' ? projects : projects.filter(p => p.realm === filter); const sortedProjects = [...filteredProjects].sort((a, b) => b.votes - a.votes); return (
{(Object.keys(ARM_CONFIG) as ArmType[]).map(realm => { const config = ARM_CONFIG[realm]; return ( ); })}
{sortedProjects.map((project, index) => { const config = ARM_CONFIG[project.realm]; const hasVoted = votedProjects.has(project.id); return (
{project.thumbnail}
{config.label}

{project.description}

by {project.author} {project.views}
{project.tags.map(tag => ( {tag} ))}
voteProject(project.id)} disabled={!userId || hasVoted} whileHover={!hasVoted && userId ? { scale: 1.1 } : {}} whileTap={!hasVoted && userId ? { scale: 0.9 } : {}} className={`p-2 rounded-lg transition-colors ${ hasVoted ? 'bg-green-500/20 text-green-400' : 'bg-[#1e1f22] text-[#949ba4] hover:text-white hover:bg-[#3f4147]' }`} > {project.votes}
); })}
); } interface ChatMessage { id: string; userId: string; username: string; avatar: string | null; content: string; timestamp: number; } function ChatTab({ userId, username, avatar, participants }: { userId?: string; username?: string; avatar?: string | null; participants: any[]; }) { const [messages, setMessages] = useState(() => { try { const saved = localStorage.getItem('aethex_activity_chat'); return saved ? JSON.parse(saved) : []; } catch { return []; } }); const [inputValue, setInputValue] = useState(''); const [sending, setSending] = useState(false); const messagesEndRef = useRef(null); const inputRef = useRef(null); useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]); useEffect(() => { try { localStorage.setItem('aethex_activity_chat', JSON.stringify(messages.slice(-50))); } catch {} }, [messages]); const sendMessage = useCallback(() => { if (!inputValue.trim() || !userId || sending) return; setSending(true); const newMessage: ChatMessage = { id: `${Date.now()}-${Math.random().toString(36).slice(2)}`, userId, username: username || 'Anonymous', avatar: avatar || null, content: inputValue.trim(), timestamp: Date.now(), }; setMessages(prev => [...prev, newMessage]); setInputValue(''); setSending(false); inputRef.current?.focus(); }, [inputValue, userId, username, avatar, sending]); const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } }; const formatTime = (timestamp: number) => { return new Date(timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); }; const mockMessages: ChatMessage[] = [ { id: '1', userId: 'bot', username: 'AeThex Bot', avatar: null, content: 'Welcome to Activity Chat! Say hi to your fellow builders.', timestamp: Date.now() - 300000 }, { id: '2', userId: 'user1', username: 'GameDevPro', avatar: null, content: 'Hey everyone! Working on a new GameForge project ๐ŸŽฎ', timestamp: Date.now() - 180000 }, { id: '3', userId: 'user2', username: 'PixelArtist', avatar: null, content: 'Nice! What genre?', timestamp: Date.now() - 120000 }, ]; const displayMessages = messages.length > 0 ? messages : mockMessages; return (
{participants.length > 0 && (
{participants.length} {participants.length === 1 ? 'person' : 'people'} in this Activity
)} {displayMessages.map((msg, index) => { const isOwn = msg.userId === userId; const showAvatar = index === 0 || displayMessages[index - 1]?.userId !== msg.userId; return ( {showAvatar ? ( msg.avatar ? ( {msg.username} ) : (
{msg.username?.[0]?.toUpperCase() || '?'}
) ) : (
)}
{showAvatar && (
{msg.username} {formatTime(msg.timestamp)}
)}
{msg.content}
); })}
setInputValue(e.target.value)} onKeyDown={handleKeyDown} placeholder="Type a message..." disabled={!userId} className="flex-1 bg-[#1e1f22] text-white placeholder-[#949ba4] px-4 py-2.5 rounded-xl border border-[#3f4147] focus:border-purple-500 focus:outline-none transition-colors text-sm disabled:opacity-50" />

Messages are local to this session

); } interface ParticipantProfile { id: string; username: string; global_name: string | null; avatar: string | null; speaking?: boolean; } function ProfilePreviewModal({ participant, onClose, openExternalLink }: { participant: ParticipantProfile; onClose: () => void; openExternalLink: (url: string) => Promise; }) { const { mockBadges, mockLevel, mockXP } = useMemo(() => { const hash = participant.id.split('').reduce((a, c) => a + c.charCodeAt(0), 0); const level = (hash % 15) + 1; return { mockBadges: [ { icon: "๐Ÿš€", name: "Early Adopter", unlocked: hash % 2 === 0 }, { icon: "โš”๏ธ", name: "Realm Explorer", unlocked: hash % 3 === 0 }, { icon: "๐ŸŽฎ", name: "GameForge Member", unlocked: hash % 4 !== 0 }, { icon: "โœจ", name: "First Post", unlocked: hash % 5 !== 0 }, ], mockLevel: level, mockXP: level * 1000 - (hash % 800), }; }, [participant.id]); return ( e.stopPropagation()} className="bg-[#2b2d31] rounded-2xl w-full max-w-sm overflow-hidden border border-[#3f4147] shadow-xl" >
{participant.avatar ? ( {participant.global_name ) : (
{(participant.global_name || participant.username)?.[0]?.toUpperCase() || "?"}
)} {participant.speaking && ( ๐ŸŽค )}

{participant.global_name || participant.username}

@{participant.username}

{mockLevel}

Level

{mockXP.toLocaleString()}

XP

{mockBadges.filter(b => b.unlocked).length}

Badges

{mockBadges.map((badge, i) => ( {badge.icon} {badge.name} ))}
openExternalLink(`${APP_URL}/passport/${participant.id}`)} whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }} className="w-full py-3 rounded-xl bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 transition-all text-white text-sm font-medium flex items-center justify-center gap-2 shadow-lg shadow-purple-500/20" > View Full Passport
); } function ParticipantsBar({ participants, currentUserId, openExternalLink }: { participants: any[]; currentUserId?: string; openExternalLink: (url: string) => Promise; }) { const [selectedParticipant, setSelectedParticipant] = useState(null); const otherParticipants = participants.filter(p => p.id !== currentUserId); if (otherParticipants.length === 0) return null; return ( <> {selectedParticipant && ( setSelectedParticipant(null)} openExternalLink={openExternalLink} /> )}
{otherParticipants.length} here
{otherParticipants.slice(0, 8).map((p) => ( setSelectedParticipant(p)} className="relative cursor-pointer" > {p.avatar ? ( {p.global_name ) : (
{(p.global_name || p.username)?.[0]?.toUpperCase() || "?"}
)} {p.speaking && ( )}
))} {otherParticipants.length > 8 && (
+{otherParticipants.length - 8}
)}
); } export default function Activity() { const { isActivity, isLoading, user, error, openExternalLink, participants } = useDiscordActivity(); const [activeTab, setActiveTab] = useState("feed"); const [xpGain, setXpGain] = useState(null); const [showConfetti, setShowConfetti] = useState(false); const [userStats, setUserStats] = useState({ total_xp: 0, level: 1, current_streak: 0, longest_streak: 0 }); const currentRealm: ArmType = (user?.primary_arm as ArmType) || "nexus"; useEffect(() => { // Use mock data for user stats (no API endpoint available) setUserStats({ total_xp: 2450, level: 3, current_streak: 5, longest_streak: 12, rank: 12 }); }, [user?.id]); const handleXPGain = useCallback((amount: number) => { setXpGain(amount); setUserStats(prev => ({ ...prev, total_xp: prev.total_xp + amount, level: Math.max(1, Math.floor((prev.total_xp + amount) / 1000) + 1) })); const newLevel = Math.max(1, Math.floor((userStats.total_xp + amount) / 1000) + 1); if (newLevel > userStats.level) { setShowConfetti(true); setTimeout(() => setShowConfetti(false), 3000); } }, [userStats]); if (isLoading) return ; if (error) return (

Connection Error

{error}

); if (!isActivity) return (

AeThex Activity

Launch this within Discord to access the full experience.

Visit aethex.dev
); const tabs = [ { id: "feed", label: "Feed", icon: MessageCircle }, { id: "chat", label: "Chat", icon: MessagesSquare }, { id: "polls", label: "Polls", icon: BarChart3 }, { id: "challenges", label: "Challenges", icon: Trophy }, { id: "projects", label: "Projects", icon: Layers }, { id: "realms", label: "Realms", icon: Sparkles }, { id: "quests", label: "Quests", icon: Target }, { id: "top", label: "Top", icon: TrendingUp }, { id: "jobs", label: "Jobs", icon: Briefcase }, { id: "badges", label: "Badges", icon: Award }, ]; const realmConfig = ARM_CONFIG[currentRealm]; const RealmIcon = realmConfig.icon; return (
{showConfetti && } {xpGain && setXpGain(null)} />} {/* Dynamic Gradient Header */}
{user?.avatar_url ? ( ) : (
{user?.username?.[0]?.toUpperCase() || "?"}
)}

{user?.full_name || user?.username || "Builder"}

{realmConfig.label}
{userStats.current_streak > 0 && (
{userStats.current_streak}d
)}
{userStats.total_xp.toLocaleString()} XP {1000 - (userStats.total_xp % 1000)} XP to Level {userStats.level + 1}
{/* Participants Bar */} {/* Tab Navigation */}
{tabs.map((tab) => { const Icon = tab.icon; const isActive = activeTab === tab.id; return ( ); })}
{/* Tab Content - fills remaining space */}
{activeTab === "feed" && } {activeTab === "chat" && } {activeTab === "polls" && } {activeTab === "challenges" && } {activeTab === "projects" && } {activeTab === "realms" && } {activeTab === "quests" && } {activeTab === "top" && } {activeTab === "jobs" && } {activeTab === "badges" && }
); }