diff --git a/client/pages/Activity.tsx b/client/pages/Activity.tsx index 36b7db44..ea221bf6 100644 --- a/client/pages/Activity.tsx +++ b/client/pages/Activity.tsx @@ -26,6 +26,14 @@ import { X, Send, MessagesSquare, + BarChart3, + Plus, + Vote, + Trophy, + Clock, + ThumbsUp, + Layers, + Eye, } from "lucide-react"; import { motion, AnimatePresence } from "framer-motion"; @@ -682,6 +690,663 @@ function BadgesTab({ userId, openExternalLink }: { userId?: string; openExternal ); } +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; @@ -1147,6 +1812,9 @@ export default function Activity() { 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 }, @@ -1246,6 +1914,9 @@ export default function Activity() { {activeTab === "feed" && } {activeTab === "chat" && } + {activeTab === "polls" && } + {activeTab === "challenges" && } + {activeTab === "projects" && } {activeTab === "realms" && } {activeTab === "quests" && } {activeTab === "top" && }