// ============================================ // COINRUSH CHAT - React Component // Modern, responsive chat with channels // ============================================ const { useState, useEffect, useRef, useCallback, useMemo } = React; // ============================================ // SVG ICONS // ============================================ const ChatIcons = { send: ( ), users: ( ), crown: ( ), lock: ( ), chat: ( ), minimize: ( ), close: ( ), wifi: ( ), wifiOff: ( ), star: ( ), sparkles: ( ), chevronDown: ( ), }; // ============================================ // UTILITY FUNCTIONS // ============================================ // Generate consistent color for username function generateUserColor(username) { if (!username) return '#94a3b8'; const colors = [ '#f472b6', '#fb7185', '#f97316', '#facc15', '#a3e635', '#4ade80', '#2dd4bf', '#22d3ee', '#60a5fa', '#818cf8', '#a78bfa', '#c084fc', '#e879f9', '#fb923c', '#fbbf24', '#34d399' ]; let hash = 0; for (let i = 0; i < username.length; i++) { hash = username.charCodeAt(i) + ((hash << 5) - hash); } return colors[Math.abs(hash) % colors.length]; } // Format timestamp for display function formatTime(timestamp) { if (!timestamp) return ''; const date = new Date(timestamp); const now = new Date(); const diffMs = now - date; const diffMins = Math.floor(diffMs / 60000); if (diffMins < 1) return 'now'; if (diffMins < 60) return `${diffMins}m`; const diffHours = Math.floor(diffMins / 60); if (diffHours < 24) return `${diffHours}h`; return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); } // Get level badge info function getLevelBadge(level) { if (!level || level < 1) return { class: 'newbie', label: 'LV1', title: null, color: '#94a3b8' }; if (level >= 50) { return { class: 'legendary', label: `LV${level}`, title: 'LEGEND', color: '#f472b6' }; } else if (level >= 30) { return { class: 'master', label: `LV${level}`, title: 'MASTER', color: '#a78bfa' }; } else if (level >= 20) { return { class: 'expert', label: `LV${level}`, title: 'EXPERT', color: '#60a5fa' }; } else if (level >= 10) { return { class: 'vip', label: `LV${level}`, title: 'VIP', color: '#fbbf24' }; } else if (level >= 5) { return { class: 'regular', label: `LV${level}`, title: null, color: '#4ade80' }; } return { class: 'newbie', label: `LV${level}`, title: null, color: '#94a3b8' }; } // Get avatar border class based on level function getAvatarBorderClass(level) { if (!level || level < 5) return 'avatar-rookie'; if (level < 10) return 'avatar-bronze'; if (level < 15) return 'avatar-silver'; if (level < 20) return 'avatar-gold'; if (level < 30) return 'avatar-platinum'; // Ruby/Platinum if (level < 40) return 'avatar-diamond'; return 'avatar-challenger'; // Mythic/Legendary } // Storage keys const STORAGE_KEYS = { coinrush: 'cr_chat_coinrush_v2', vip: 'cr_chat_vip_v2', }; // ============================================ // MESSAGE COMPONENT // ============================================ function ChatMessage({ message, isOwnMessage, currentUser }) { const userColor = useMemo(() => generateUserColor(message.username), [message.username]); const levelBadge = useMemo(() => getLevelBadge(message.level), [message.level]); const timeAgo = useMemo(() => formatTime(message.timestamp), [message.timestamp]); const borderClass = useMemo(() => getAvatarBorderClass(message.level), [message.level]); const isSystem = message.username === 'system' || message.type === 'system'; if (isSystem) { return (
{ChatIcons.sparkles} {message.message}
); } return (
{message.username?.charAt(0).toUpperCase() || '?'}
{message.username} {levelBadge && ( {levelBadge.label} )} {levelBadge && levelBadge.title && ( {levelBadge.title} )} {timeAgo}
{message.message}
); } // ============================================ // MAIN CHAT COMPONENT // ============================================ function CoinRushChatApp({ currentUser, theme = 'gold' }) { const [messages, setMessages] = useState({ coinrush: [], vip: [] }); const [currentChannel, setCurrentChannel] = useState('coinrush'); const [inputValue, setInputValue] = useState(''); const [isConnected, setIsConnected] = useState(false); const [onlineCount, setOnlineCount] = useState(0); const [isMinimized, setIsMinimized] = useState(false); const [showNewMessage, setShowNewMessage] = useState(false); const [banInfo, setBanInfo] = useState(null); const wsRef = useRef(null); const messagesContainerRef = useRef(null); const inputRef = useRef(null); const reconnectTimeoutRef = useRef(null); const reconnectAttemptsRef = useRef(0); const handleWSMessageRef = useRef(null); const userLevel = currentUser?.level || currentUser?.level_info?.current_level || 0; const isVIPUnlocked = userLevel >= 10; // Load messages from localStorage const loadStoredMessages = useCallback((channel) => { try { const key = STORAGE_KEYS[channel]; const stored = localStorage.getItem(key); if (!stored) return []; const msgs = JSON.parse(stored); const cutoff = Date.now() - (4 * 60 * 60 * 1000); // 4 hours return msgs.filter(m => { const ts = m.timestamp ? new Date(m.timestamp).getTime() : 0; return ts >= cutoff; }).slice(-100); } catch { return []; } }, []); // Save messages to localStorage const saveMessages = useCallback((channel, msgs) => { try { const key = STORAGE_KEYS[channel]; const cutoff = Date.now() - (4 * 60 * 60 * 1000); const filtered = msgs.filter(m => { const ts = m.timestamp ? new Date(m.timestamp).getTime() : 0; return ts >= cutoff; }).slice(-100); localStorage.setItem(key, JSON.stringify(filtered)); } catch (e) { console.error('Failed to save chat messages:', e); } }, []); // Initialize messages from storage useEffect(() => { setMessages({ coinrush: loadStoredMessages('coinrush'), vip: loadStoredMessages('vip'), }); }, [loadStoredMessages]); // Scroll to bottom when new messages arrive const scrollToBottom = useCallback(() => { if (messagesContainerRef.current) { const { scrollHeight, clientHeight } = messagesContainerRef.current; messagesContainerRef.current.scrollTo({ top: scrollHeight, behavior: 'smooth' }); } }, []); // Handle incoming WebSocket messages const handleWSMessage = useCallback((event) => { try { const data = JSON.parse(event.data); if (data.type === 'chat') { const channel = data.channel || 'coinrush'; const newMessage = { id: Date.now() + Math.random(), username: data.username, message: data.message, level: data.level, timestamp: data.timestamp || new Date().toISOString(), }; setMessages(prev => { const currentList = prev[channel] || []; // Prevent duplicate messages (same content/user within 500ms) const isDuplicate = currentList.some(m => m.username === newMessage.username && m.message === newMessage.message && Math.abs(new Date(m.timestamp).getTime() - new Date(newMessage.timestamp).getTime()) < 500 ); if (isDuplicate) return prev; const updated = { ...prev, [channel]: [...currentList, newMessage].slice(-100), }; saveMessages(channel, updated[channel]); return updated; }); // Show notification if minimized or different channel if (isMinimized || channel !== currentChannel) { setShowNewMessage(true); } // Scroll if on current channel if (channel === currentChannel && !isMinimized) { setTimeout(scrollToBottom, 50); } } else if (data.type === 'online_count') { setOnlineCount(data.count || 0); } else if (data.type === 'chat_moderation') { if (data.status === 'banned' || data.action === 'ban') { setBanInfo({ until: data.until, hours: data.hours, }); } } else if (data.type === 'system') { // System messages go to coinrush channel const sysMessage = { id: Date.now() + Math.random(), username: 'system', message: data.msg || data.message, type: 'system', timestamp: data.ts || new Date().toISOString(), }; setMessages(prev => ({ ...prev, coinrush: [...prev.coinrush, sysMessage].slice(-100), })); } } catch (e) { console.error('Failed to parse chat message:', e); } }, [currentChannel, isMinimized, saveMessages, scrollToBottom]); // Update ref whenever handler changes useEffect(() => { handleWSMessageRef.current = handleWSMessage; }, [handleWSMessage]); // WebSocket connection const connectWS = useCallback(() => { if (wsRef.current?.readyState === WebSocket.OPEN) return; const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const ws = new WebSocket(`${protocol}//${window.location.host}/ws/lobby`); ws.onopen = () => { console.log('Chat connected'); setIsConnected(true); reconnectAttemptsRef.current = 0; }; ws.onmessage = (event) => { if (handleWSMessageRef.current) { handleWSMessageRef.current(event); } }; ws.onerror = () => { console.error('Chat connection error'); setIsConnected(false); }; ws.onclose = () => { console.log('Chat disconnected'); setIsConnected(false); wsRef.current = null; // Reconnect with exponential backoff if (reconnectAttemptsRef.current < 5) { const delay = Math.min(1000 * Math.pow(2, reconnectAttemptsRef.current), 30000); reconnectAttemptsRef.current++; reconnectTimeoutRef.current = setTimeout(connectWS, delay); } }; wsRef.current = ws; }, []); // Connect on mount useEffect(() => { connectWS(); return () => { if (reconnectTimeoutRef.current) { clearTimeout(reconnectTimeoutRef.current); } if (wsRef.current) { wsRef.current.close(); } }; }, [connectWS]); // Send message const sendMessage = useCallback(() => { if (!inputValue.trim() || !currentUser || !wsRef.current) return; // Check if banned if (banInfo) { const banEnd = new Date(banInfo.until); if (banEnd > new Date()) { return; // Still banned } setBanInfo(null); } // Check VIP access if (currentChannel === 'vip' && !isVIPUnlocked) { return; } const messageData = { type: 'chat', channel: currentChannel, message: inputValue.trim(), }; try { wsRef.current.send(JSON.stringify(messageData)); setInputValue(''); inputRef.current?.focus(); } catch (e) { console.error('Failed to send message:', e); } }, [inputValue, currentUser, currentChannel, banInfo, isVIPUnlocked]); // Handle Enter key const handleKeyPress = useCallback((e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } }, [sendMessage]); // Switch channel const switchChannel = useCallback((channel) => { if (channel === 'vip' && !isVIPUnlocked) return; setCurrentChannel(channel); setShowNewMessage(false); setTimeout(scrollToBottom, 50); }, [isVIPUnlocked, scrollToBottom]); // Toggle minimize const toggleMinimize = useCallback(() => { setIsMinimized(prev => !prev); setShowNewMessage(false); if (isMinimized) { setTimeout(scrollToBottom, 100); } }, [isMinimized, scrollToBottom]); const currentMessages = messages[currentChannel] || []; // Render minimized state if (isMinimized) { return (
{ChatIcons.chat}
Chat {showNewMessage &&
}
{onlineCount}
); } return (
{/* Header */}
{ChatIcons.chat}
LIVE CHAT
{onlineCount} online
{/* Channel Tabs */}
{/* Messages Area */}
{currentChannel === 'vip' && !isVIPUnlocked ? (
{ChatIcons.crown}

VIP Lounge

Exclusive chat for dedicated players

Reach Level 10 to unlock {currentUser && Current: LVL {userLevel}}
) : currentMessages.length === 0 ? (
{ChatIcons.chat}

{currentChannel === 'vip' ? 'VIP Lounge' : 'Welcome to Chat'}

{currentChannel === 'vip' ? 'Exclusive room for VIP members' : 'Be the first to say something!'}

) : ( <> {currentMessages.map((msg) => ( ))} )}
{/* Input Area */}
{banInfo && new Date(banInfo.until) > new Date() ? (
{ChatIcons.lock} Chat banned until {new Date(banInfo.until).toLocaleTimeString()}
) : !currentUser ? (
Login to chat
) : (
setInputValue(e.target.value)} onKeyPress={handleKeyPress} maxLength={200} disabled={currentChannel === 'vip' && !isVIPUnlocked} />
)}
{isConnected ? ChatIcons.wifi : ChatIcons.wifiOff} {isConnected ? 'Connected' : 'Reconnecting...'}
); } // ============================================ // FLOATING CHAT WRAPPER // ============================================ function FloatingChat({ currentUser, theme = 'gold' }) { const [isOpen, setIsOpen] = useState(false); const [hasNewMessage, setHasNewMessage] = useState(false); return ( <> {/* Floating Button */} {!isOpen && ( )} {/* Chat Panel */} {isOpen && (
)} ); } // ============================================ // RENDER FUNCTIONS // ============================================ window.renderCoinRushChat = function(containerId, currentUser, theme = 'gold') { const container = document.getElementById(containerId); if (!container) { console.error(`Chat container #${containerId} not found`); return; } const root = ReactDOM.createRoot(container); root.render(); }; window.renderFloatingChat = function(containerId, currentUser, theme = 'gold') { const container = document.getElementById(containerId); if (!container) { console.error(`Floating chat container #${containerId} not found`); return; } const root = ReactDOM.createRoot(container); root.render(); }; // Export for use window.CoinRushChatApp = CoinRushChatApp; window.FloatingChat = FloatingChat;