// ============================================
// 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 &&
}
);
}
return (
{/* Header */}
{ChatIcons.chat}
LIVE CHAT
{/* 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;