diff --git a/client/pages/Activity.tsx b/client/pages/Activity.tsx index 684bfc12..36b7db44 100644 --- a/client/pages/Activity.tsx +++ b/client/pages/Activity.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState, useCallback, useRef, type MouseEvent } from "react"; +import { useEffect, useState, useCallback, useRef, useMemo, type MouseEvent } from "react"; import { useDiscordActivity } from "@/contexts/DiscordActivityContext"; import LoadingScreen from "@/components/LoadingScreen"; import { @@ -23,6 +23,9 @@ import { TrendingUp, Calendar, Award, + X, + Send, + MessagesSquare, } from "lucide-react"; import { motion, AnimatePresence } from "framer-motion"; @@ -679,54 +682,393 @@ function BadgesTab({ userId, openExternalLink }: { userId?: string; openExternal ); } -function ParticipantsBar({ participants, currentUserId }: { participants: any[]; currentUserId?: string }) { +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 ( -
- - {otherParticipants.length} here -
- {otherParticipants.slice(0, 8).map((p) => ( - - {p.avatar ? ( - {p.global_name - ) : ( -
- {(p.global_name || p.username)?.[0]?.toUpperCase() || "?"} -
- )} - {p.speaking && ( - - )} - - ))} - {otherParticipants.length > 8 && ( -
- +{otherParticipants.length - 8} -
+ <> + + {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} +
+ )} +
-
+ ); } @@ -804,6 +1146,7 @@ export default function Activity() { const tabs = [ { id: "feed", label: "Feed", icon: MessageCircle }, + { id: "chat", label: "Chat", icon: MessagesSquare }, { id: "realms", label: "Realms", icon: Sparkles }, { id: "quests", label: "Quests", icon: Target }, { id: "top", label: "Top", icon: TrendingUp }, @@ -870,7 +1213,7 @@ export default function Activity() { {/* Participants Bar */} - + {/* Tab Navigation */}
@@ -902,6 +1245,7 @@ export default function Activity() {
{activeTab === "feed" && } + {activeTab === "chat" && } {activeTab === "realms" && } {activeTab === "quests" && } {activeTab === "top" && }