betting-prediction-app/components/ChessApp.js

1167 lines
49 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect } from 'react';
import { ChevronDown, ChevronUp, Plus, Settings, Crown, TrendingUp, Users, DollarSign, Target, BarChart3, X } from 'lucide-react';
import { pusherClient, triggerPusherEvent } from '../lib/pusher';
const ChessApp = () => {
const [currentUser, setCurrentUser] = useState(null);
const [users, setUsers] = useState({});
const [platformAccount, setPlatformAccount] = useState({ balance: 0, totalFees: 0, transactionCount: 0 });
const [games, setGames] = useState([]);
const [bets, setBets] = useState({});
const [isAdmin, setIsAdmin] = useState(false);
const [showAdminPanel, setShowAdminPanel] = useState(false);
const [expandedGames, setExpandedGames] = useState({});
const [showGameForm, setShowGameForm] = useState(false);
const [newGameData, setNewGameData] = useState({ player1: '', player2: '' });
const [showBetForm, setShowBetForm] = useState({});
const [showBetConfirmation, setShowBetConfirmation] = useState(false);
const [pendingBet, setPendingBet] = useState(null);
const [betType, setBetType] = useState('hedged');
const [newBetData, setNewBetData] = useState({ gameId: '', betAmount: 10, condition: '', certainty: 50 });
const [showDashboard, setShowDashboard] = useState(false);
const [showAdminLogin, setShowAdminLogin] = useState(false);
const [adminPassword, setAdminPassword] = useState('');
const [showOnlineUsers, setShowOnlineUsers] = useState(false);
const [marketHistory, setMarketHistory] = useState({});
const [selectedWager, setSelectedWager] = useState(null);
const [showExistingBetForm, setShowExistingBetForm] = useState({});
const [existingBetAmount, setExistingBetAmount] = useState(10);
const [existingBetPosition, setExistingBetPosition] = useState('yes');
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// API helper functions
const api = {
async getUsers() {
const response = await fetch('/api/users');
if (!response.ok) throw new Error('Failed to fetch users');
return response.json();
},
async saveUser(user) {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user })
});
if (!response.ok) throw new Error('Failed to save user');
return response.json();
},
async updateUserBalance(userId, balance) {
const response = await fetch('/api/users', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId, updates: { balance } })
});
if (!response.ok) throw new Error('Failed to update user balance');
return response.json();
},
async getGames() {
const response = await fetch('/api/games');
if (!response.ok) throw new Error('Failed to fetch games');
return response.json();
},
async saveGame(game) {
const response = await fetch('/api/games', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ game })
});
if (!response.ok) throw new Error('Failed to save game');
return response.json();
},
async getBets() {
const response = await fetch('/api/bets');
if (!response.ok) throw new Error('Failed to fetch bets');
return response.json();
},
async saveBet(gameId, bet, marketProbability) {
const response = await fetch('/api/bets', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ gameId, bet, marketProbability })
});
if (!response.ok) throw new Error('Failed to save bet');
return response.json();
},
async getPlatformAccount() {
const response = await fetch('/api/platform');
if (!response.ok) throw new Error('Failed to fetch platform account');
return response.json();
},
async savePlatformAccount(platformAccount) {
const response = await fetch('/api/platform', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ platformAccount })
});
if (!response.ok) throw new Error('Failed to save platform account');
return response.json();
}
};
// Initialize app and load data
useEffect(() => {
const initializeApp = async () => {
try {
setError(null);
// Load all data from backend
const [usersData, gamesData, betsData, platformData] = await Promise.all([
api.getUsers(),
api.getGames(),
api.getBets(),
api.getPlatformAccount()
]);
setUsers(usersData);
setGames(gamesData);
setBets(betsData);
setPlatformAccount(platformData);
// Check for existing user in localStorage
const savedUserId = typeof window !== 'undefined' ? localStorage.getItem('chessUserId') : null;
if (savedUserId && usersData[savedUserId]) {
setCurrentUser(savedUserId);
setIsAdmin(usersData[savedUserId].is_admin || false);
} else {
// Create new user
const newUserId = 'user_' + Math.random().toString(36).substr(2, 9);
const myceliumName = generateMyceliumName();
const newUser = {
id: newUserId,
name: myceliumName,
balance: 1000,
is_admin: false
};
await api.saveUser(newUser);
setUsers(prev => ({ ...prev, [newUserId]: newUser }));
setCurrentUser(newUserId);
if (typeof window !== 'undefined') {
localStorage.setItem('chessUserId', newUserId);
}
}
setLoading(false);
} catch (error) {
console.error('Failed to initialize app:', error);
setError('Failed to load application. Please refresh the page.');
setLoading(false);
}
};
initializeApp();
}, []);
// Set up Pusher real-time listeners
useEffect(() => {
if (!currentUser || !pusherClient) return;
const channel = pusherClient.subscribe('chess-tournament');
channel.bind('new-game', (data) => {
setGames(prev => {
const exists = prev.find(g => g.id === data.game.id);
return exists ? prev : [...prev, data.game];
});
setExpandedGames(prev => ({ ...prev, [data.game.id]: true }));
});
channel.bind('new-bet', (data) => {
setBets(prev => ({
...prev,
[data.gameId]: [...(prev[data.gameId] || []), data.bet]
}));
updateMarketHistory(data.gameId, data.bet.condition, data.marketProbability);
});
channel.bind('user-update', (data) => {
setUsers(prev => ({ ...prev, [data.userId]: data.user }));
if (data.userId === currentUser) {
// Update local user data
}
});
channel.bind('platform-update', (data) => {
setPlatformAccount(data.platformAccount);
});
return () => {
channel.unbind_all();
pusherClient.unsubscribe('chess-tournament');
};
}, [currentUser]);
// Auto-expand new games
useEffect(() => {
if (games.length > 0) {
const initialExpanded = {};
games.forEach(game => {
if (!(game.id in expandedGames)) {
initialExpanded[game.id] = true;
}
});
if (Object.keys(initialExpanded).length > 0) {
setExpandedGames(prev => ({ ...prev, ...initialExpanded }));
}
}
}, [games]);
// Update admin status when user changes
useEffect(() => {
if (currentUser && users[currentUser]) {
setIsAdmin(users[currentUser].is_admin || false);
}
}, [currentUser, users]);
const generateMyceliumName = () => {
const titles = ['Spore', 'Network', 'Colony', 'Cluster', 'Node', 'Branch', 'Root', 'Cap'];
const names = ['Shiitake', 'Oyster', 'Chanterelle', 'Morel', 'Porcini', 'Enoki', 'Maitake', 'Reishi', 'Cordyceps', 'Agaricus'];
const suffixes = ['Weaver', 'Connector', 'Spreader', 'Fruiter', 'Decomposer', 'Networker', 'Symbiont', 'Grower'];
const title = titles[Math.floor(Math.random() * titles.length)];
const name = names[Math.floor(Math.random() * names.length)];
const suffix = suffixes[Math.floor(Math.random() * suffixes.length)];
return `${title} ${name} ${suffix}`;
};
const updateMarketHistory = (gameId, condition, probability) => {
const key = `${gameId}-${condition}`;
setMarketHistory(prev => {
const currentHistory = prev[key] || [];
const newEntry = {
timestamp: new Date().toISOString(),
probability: parseFloat(probability)
};
return {
...prev,
[key]: [...currentHistory, newEntry].slice(-10)
};
});
};
const calculateTokenPrices = (gameId, betCondition) => {
const gameBets = bets[gameId] || [];
const relevantBets = gameBets.filter(bet => bet.condition.toLowerCase().trim() === betCondition.toLowerCase().trim());
if (relevantBets.length === 0) {
return { yesPrice: 0.50, noPrice: 0.50, marketProbability: 50 };
}
let totalWeightedCertainty = 0;
let totalAmount = 0;
relevantBets.forEach(bet => {
totalWeightedCertainty += bet.certainty * bet.amount;
totalAmount += bet.amount;
});
const marketProbability = totalWeightedCertainty / totalAmount;
const yesPrice = (marketProbability / 100).toFixed(2);
const noPrice = (1 - (marketProbability / 100)).toFixed(2);
return {
yesPrice: parseFloat(yesPrice),
noPrice: parseFloat(noPrice),
marketProbability: marketProbability.toFixed(1)
};
};
const calculateOdds = (gameId, betCondition) => {
const gameBets = bets[gameId] || [];
const relevantBets = gameBets.filter(bet => bet.condition.toLowerCase().trim() === betCondition.toLowerCase().trim());
if (relevantBets.length === 0) {
return { odds: 'No bets', totalAmount: 0, avgCertainty: 0, betCount: 0 };
}
let totalWeightedCertainty = 0;
let totalAmount = 0;
relevantBets.forEach(bet => {
totalWeightedCertainty += bet.certainty * bet.amount;
totalAmount += bet.amount;
});
const avgCertainty = totalWeightedCertainty / totalAmount;
const impliedProbability = avgCertainty / 100;
const odds = impliedProbability > 0 ? (1 / impliedProbability).toFixed(2) : '∞';
return {
odds: `${odds}:1`,
totalAmount,
avgCertainty: avgCertainty.toFixed(1),
betCount: relevantBets.length
};
};
const getUniqueBetConditions = (gameId) => {
const gameBets = bets[gameId] || [];
const conditions = [...new Set(gameBets.map(bet => bet.condition))];
return conditions;
};
const toggleGameExpansion = (gameId) => {
setExpandedGames(prev => ({
...prev,
[gameId]: !prev[gameId]
}));
};
const addGame = async () => {
if (newGameData.player1 && newGameData.player2) {
try {
const gameId = 'game_' + Math.random().toString(36).substr(2, 9);
const newGame = {
id: gameId,
player1: newGameData.player1,
player2: newGameData.player2,
status: 'upcoming',
created_at: new Date().toISOString()
};
setGames(prev => [...prev, newGame]);
setBets(prev => ({ ...prev, [gameId]: [] }));
setExpandedGames(prev => ({ ...prev, [gameId]: true }));
setNewGameData({ player1: '', player2: '' });
setShowGameForm(false);
await api.saveGame(newGame);
} catch (error) {
console.error('Failed to add game:', error);
}
}
};
const openBetForm = (gameId, condition = '') => {
setNewBetData({ gameId, betAmount: 10, condition, certainty: 50 });
setShowBetForm(prev => ({ ...prev, [gameId]: true }));
};
const closeBetForm = (gameId) => {
setShowBetForm(prev => ({ ...prev, [gameId]: false }));
};
const calculateBetDetails = (amount, certainty, type) => {
const platformFee = amount * 0.01; // 1% fee
const netCost = amount - platformFee;
if (type === 'hedged') {
const yesTokens = Math.floor((netCost * (certainty / 100)) / 0.50 * 100);
const noTokens = Math.floor((netCost * ((100 - certainty) / 100)) / 0.50 * 100);
return { yesTokens, noTokens, platformFee, netCost, actualCost: amount };
} else {
const tokens = Math.floor(netCost / 0.50 * 100);
return { yesTokens: tokens, noTokens: 0, platformFee, netCost, actualCost: amount };
}
};
const prepareBet = () => {
const { gameId, betAmount, condition, certainty } = newBetData;
if (!condition || betAmount <= 0) return;
const userBalance = users[currentUser]?.balance || 0;
if (betAmount > userBalance) {
alert('Insufficient balance!');
return;
}
const betDetails = calculateBetDetails(betAmount, certainty, betType);
const { marketProbability } = calculateTokenPrices(gameId, condition);
setPendingBet({
gameId,
condition,
certainty,
amount: betAmount,
betType,
...betDetails,
marketProbability
});
setShowBetConfirmation(true);
};
const confirmBet = async () => {
if (!pendingBet) return;
try {
const betId = 'bet_' + Math.random().toString(36).substr(2, 9);
const newBet = {
id: betId,
game_id: pendingBet.gameId,
user_id: currentUser,
user_name: users[currentUser].name,
amount: pendingBet.amount,
condition: pendingBet.condition,
certainty: pendingBet.certainty,
yes_tokens: pendingBet.yesTokens,
no_tokens: pendingBet.noTokens,
bet_type: pendingBet.betType,
actual_cost: pendingBet.actualCost,
platform_fee: pendingBet.platformFee,
net_cost: pendingBet.netCost,
created_at: new Date().toISOString()
};
// Optimistically update UI
setBets(prev => ({
...prev,
[pendingBet.gameId]: [...(prev[pendingBet.gameId] || []), newBet]
}));
// Update user balance
const newBalance = users[currentUser].balance - pendingBet.amount;
setUsers(prev => ({
...prev,
[currentUser]: { ...prev[currentUser], balance: newBalance }
}));
// Update platform account
setPlatformAccount(prev => ({
balance: prev.balance + pendingBet.platformFee,
totalFees: prev.totalFees + pendingBet.platformFee,
transactionCount: prev.transactionCount + 1
}));
// Save to backend
await api.saveBet(pendingBet.gameId, newBet, pendingBet.marketProbability);
await api.updateUserBalance(currentUser, newBalance);
// Close forms
setShowBetConfirmation(false);
setPendingBet(null);
setShowBetForm({});
setShowExistingBetForm({});
} catch (error) {
console.error('Failed to place bet:', error);
alert('Failed to place bet. Please try again.');
}
};
const placeBetOnExisting = (gameId, condition, position) => {
const { yesPrice, noPrice, marketProbability } = calculateTokenPrices(gameId, condition);
const certainty = position === 'yes' ? parseFloat(marketProbability) : (100 - parseFloat(marketProbability));
setNewBetData({
gameId,
betAmount: existingBetAmount,
condition,
certainty
});
setBetType('non-hedged');
const betDetails = calculateBetDetails(existingBetAmount, certainty, 'non-hedged');
setPendingBet({
gameId,
condition,
certainty,
amount: existingBetAmount,
betType: 'non-hedged',
position,
...betDetails,
marketProbability
});
setShowBetConfirmation(true);
};
const handleAdminLogin = () => {
if (adminPassword === 'mycelium2024') {
setIsAdmin(true);
setShowAdminLogin(false);
setAdminPassword('');
// Update user in database
const updatedUser = { ...users[currentUser], is_admin: true };
setUsers(prev => ({ ...prev, [currentUser]: updatedUser }));
api.saveUser(updatedUser);
} else {
alert('Invalid password');
}
};
const getUserBets = () => {
const userBets = [];
Object.entries(bets).forEach(([gameId, gameBets]) => {
gameBets.forEach(bet => {
if (bet.user_id === currentUser) {
const game = games.find(g => g.id === gameId);
userBets.push({ ...bet, game });
}
});
});
return userBets;
};
// Loading state
if (loading) {
return (
<div className="min-h-screen bg-gradient-to-b from-green-900 to-gray-900 text-green-100 flex items-center justify-center">
<div className="text-center">
<div className="text-6xl mb-4">🍄</div>
<div>Connecting to the mycelial network...</div>
<div className="text-sm text-gray-400 mt-2">Loading from Supabase...</div>
</div>
</div>
);
}
// Error state
if (error) {
return (
<div className="min-h-screen bg-gradient-to-b from-green-900 to-gray-900 text-green-100 flex items-center justify-center">
<div className="text-center">
<div className="text-6xl mb-4">🚨</div>
<div className="text-xl font-bold text-red-400 mb-2">Connection Error</div>
<div className="text-gray-300 mb-4">{error}</div>
<button
onClick={() => window.location.reload()}
className="bg-green-700 hover:bg-green-600 px-4 py-2 rounded"
>
Retry Connection
</button>
</div>
</div>
);
}
// Not logged in state
if (!currentUser || !users[currentUser]) {
return (
<div className="min-h-screen bg-gradient-to-b from-green-900 to-gray-900 text-green-100 flex items-center justify-center">
<div className="text-center">
<div className="text-6xl mb-4">🍄</div>
<div>Initializing user session...</div>
</div>
</div>
);
}
const currentUserData = users[currentUser];
const userBets = getUserBets();
return (
<div className="min-h-screen bg-gradient-to-b from-green-900 to-gray-900 text-green-100">
{/* Header */}
<div className="bg-black/50 border-b border-green-800/30 p-4">
<div className="max-w-6xl mx-auto flex justify-between items-center">
<div className="flex items-center space-x-3">
<div className="text-3xl">🍄</div>
<div>
<h1 className="text-xl md:text-2xl font-bold text-green-400">Commons Hub Chess</h1>
<p className="text-xs md:text-sm text-green-300/70">Mycelial Prediction Network</p>
</div>
</div>
<div className="flex items-center space-x-4">
<button
onClick={() => setShowDashboard(!showDashboard)}
className="p-2 bg-green-800/50 hover:bg-green-700/50 rounded-lg"
title="Dashboard"
>
<BarChart3 size={20} />
</button>
<button
onClick={() => setShowOnlineUsers(!showOnlineUsers)}
className="p-2 bg-green-800/50 hover:bg-green-700/50 rounded-lg"
title="Online Users"
>
<Users size={20} />
</button>
{!isAdmin && (
<button
onClick={() => setShowAdminLogin(true)}
className="p-2 bg-green-800/50 hover:bg-green-700/50 rounded-lg"
title="Admin Login"
>
<Settings size={20} />
</button>
)}
{isAdmin && (
<button
onClick={() => setShowAdminPanel(!showAdminPanel)}
className="p-2 bg-amber-800/50 hover:bg-amber-700/50 rounded-lg"
title="Admin Panel"
>
<Crown size={20} className="text-amber-400" />
</button>
)}
<div className="text-right">
<div className="text-lg font-bold text-amber-400">
🟫 {currentUserData.balance?.toFixed(0) || 0}
</div>
<div className="text-xs text-green-300">{currentUserData.name}</div>
</div>
</div>
</div>
</div>
<div className="max-w-6xl mx-auto p-4 md:p-6">
{/* Admin Login Modal */}
{showAdminLogin && (
<div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4">
<div className="bg-gray-900 border border-green-600/30 rounded-lg p-6 max-w-sm w-full">
<h3 className="text-xl font-bold text-green-400 mb-4">Admin Login</h3>
<input
type="password"
placeholder="Enter admin password"
value={adminPassword}
onChange={(e) => setAdminPassword(e.target.value)}
className="w-full p-3 bg-gray-800 border border-green-600/30 rounded text-white mb-4"
onKeyPress={(e) => e.key === 'Enter' && handleAdminLogin()}
/>
<div className="flex space-x-3">
<button
onClick={handleAdminLogin}
className="flex-1 bg-green-700 hover:bg-green-600 px-4 py-2 rounded font-semibold"
>
Login
</button>
<button
onClick={() => { setShowAdminLogin(false); setAdminPassword(''); }}
className="flex-1 bg-gray-700 hover:bg-gray-600 px-4 py-2 rounded"
>
Cancel
</button>
</div>
</div>
</div>
)}
{/* Bet Confirmation Modal */}
{showBetConfirmation && pendingBet && (
<div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4">
<div className="bg-gray-900 border border-green-600/30 rounded-lg p-6 max-w-md w-full">
<h3 className="text-xl font-bold text-green-400 mb-4">Confirm Your Bet</h3>
<div className="space-y-3 mb-6">
<div className="flex justify-between">
<span className="text-gray-400">Condition:</span>
<span className="text-white font-semibold">{pendingBet.condition}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-400">Your Certainty:</span>
<span className="text-white">{pendingBet.certainty}%</span>
</div>
<div className="flex justify-between">
<span className="text-gray-400">Market Says:</span>
<span className="text-white">{pendingBet.marketProbability}%</span>
</div>
<div className="flex justify-between">
<span className="text-gray-400">Bet Type:</span>
<span className="text-white capitalize">{pendingBet.betType}</span>
</div>
<hr className="border-green-800/30" />
<div className="flex justify-between">
<span className="text-gray-400">Amount:</span>
<span className="text-white">{pendingBet.amount} tokens</span>
</div>
<div className="flex justify-between">
<span className="text-gray-400">Platform Fee (1%):</span>
<span className="text-amber-400">{pendingBet.platformFee.toFixed(2)} tokens</span>
</div>
<div className="flex justify-between">
<span className="text-gray-400">Net Investment:</span>
<span className="text-green-400">{pendingBet.netCost.toFixed(2)} tokens</span>
</div>
{pendingBet.betType === 'hedged' && (
<>
<div className="flex justify-between">
<span className="text-gray-400">YES Tokens:</span>
<span className="text-green-400">{pendingBet.yesTokens}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-400">NO Tokens:</span>
<span className="text-red-400">{pendingBet.noTokens}</span>
</div>
</>
)}
</div>
<div className="flex space-x-3">
<button
onClick={confirmBet}
className="flex-1 bg-green-700 hover:bg-green-600 px-4 py-3 rounded font-semibold"
>
Confirm Bet
</button>
<button
onClick={() => { setShowBetConfirmation(false); setPendingBet(null); }}
className="flex-1 bg-gray-700 hover:bg-gray-600 px-4 py-3 rounded"
>
Cancel
</button>
</div>
</div>
</div>
)}
{/* Dashboard Panel */}
{showDashboard && (
<div className="bg-gray-900/50 border border-green-600/30 rounded-lg p-6 mb-6">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-bold text-green-400">Your Dashboard</h2>
<button onClick={() => setShowDashboard(false)} className="text-gray-400 hover:text-white">
<X size={20} />
</button>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<div className="bg-green-900/30 rounded-lg p-4 text-center">
<div className="text-2xl font-bold text-green-400">{currentUserData.balance?.toFixed(0) || 0}</div>
<div className="text-xs text-gray-400">Balance</div>
</div>
<div className="bg-purple-900/30 rounded-lg p-4 text-center">
<div className="text-2xl font-bold text-purple-400">{userBets.length}</div>
<div className="text-xs text-gray-400">Active Bets</div>
</div>
<div className="bg-amber-900/30 rounded-lg p-4 text-center">
<div className="text-2xl font-bold text-amber-400">
{userBets.reduce((sum, bet) => sum + (bet.amount || 0), 0).toFixed(0)}
</div>
<div className="text-xs text-gray-400">Total Wagered</div>
</div>
<div className="bg-blue-900/30 rounded-lg p-4 text-center">
<div className="text-2xl font-bold text-blue-400">{games.length}</div>
<div className="text-xs text-gray-400">Games</div>
</div>
</div>
{userBets.length > 0 && (
<div>
<h3 className="text-lg font-semibold mb-3">Your Bets</h3>
<div className="space-y-2 max-h-60 overflow-y-auto">
{userBets.map((bet, idx) => (
<div key={idx} className="bg-gray-800/50 rounded p-3 text-sm">
<div className="flex justify-between">
<span className="text-green-300">{bet.condition}</span>
<span className="text-amber-400">{bet.amount} tokens</span>
</div>
<div className="text-xs text-gray-400 mt-1">
{bet.game?.player1} vs {bet.game?.player2} {bet.certainty}% certainty
</div>
</div>
))}
</div>
</div>
)}
</div>
)}
{/* Online Users Panel */}
{showOnlineUsers && (
<div className="bg-gray-900/50 border border-green-600/30 rounded-lg p-6 mb-6">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-bold text-green-400">Network Participants</h2>
<button onClick={() => setShowOnlineUsers(false)} className="text-gray-400 hover:text-white">
<X size={20} />
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{Object.values(users).map((user) => (
<div key={user.id} className="bg-gray-800/50 rounded-lg p-3 flex justify-between items-center">
<div>
<div className="text-green-300 font-medium">{user.name}</div>
<div className="text-xs text-gray-400">
{user.is_admin && <span className="text-amber-400">👑 Admin </span>}
Balance: {user.balance?.toFixed(0) || 0}
</div>
</div>
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
</div>
))}
</div>
</div>
)}
{/* Admin Panel */}
{showAdminPanel && isAdmin && (
<div className="bg-amber-900/20 border border-amber-600/30 rounded-lg p-6 mb-6">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-bold text-amber-400">Admin Panel</h2>
<button onClick={() => setShowAdminPanel(false)} className="text-gray-400 hover:text-white">
<X size={20} />
</button>
</div>
<div className="grid grid-cols-3 gap-4 mb-6">
<div className="bg-amber-900/30 rounded-lg p-4 text-center">
<div className="text-2xl font-bold text-amber-400">{platformAccount.balance?.toFixed(2) || 0}</div>
<div className="text-xs text-gray-400">Platform Balance</div>
</div>
<div className="bg-amber-900/30 rounded-lg p-4 text-center">
<div className="text-2xl font-bold text-amber-400">{platformAccount.totalFees?.toFixed(2) || 0}</div>
<div className="text-xs text-gray-400">Total Fees Collected</div>
</div>
<div className="bg-amber-900/30 rounded-lg p-4 text-center">
<div className="text-2xl font-bold text-amber-400">{platformAccount.transactionCount || 0}</div>
<div className="text-xs text-gray-400">Transactions</div>
</div>
</div>
<button
onClick={() => setShowGameForm(!showGameForm)}
className="w-full bg-amber-700 hover:bg-amber-600 px-4 py-3 rounded font-semibold flex items-center justify-center space-x-2"
>
<Plus size={20} />
<span>Create New Game</span>
</button>
{showGameForm && (
<div className="mt-4 bg-gray-800/50 rounded-lg p-4">
<h3 className="font-semibold mb-3">New Game</h3>
<div className="grid grid-cols-2 gap-3 mb-3">
<input
type="text"
placeholder="Player 1 Name"
value={newGameData.player1}
onChange={(e) => setNewGameData(prev => ({ ...prev, player1: e.target.value }))}
className="p-3 bg-gray-700 border border-gray-600 rounded text-white"
/>
<input
type="text"
placeholder="Player 2 Name"
value={newGameData.player2}
onChange={(e) => setNewGameData(prev => ({ ...prev, player2: e.target.value }))}
className="p-3 bg-gray-700 border border-gray-600 rounded text-white"
/>
</div>
<button
onClick={addGame}
disabled={!newGameData.player1 || !newGameData.player2}
className="w-full bg-green-700 hover:bg-green-600 disabled:bg-gray-600 disabled:cursor-not-allowed px-4 py-2 rounded font-semibold"
>
Create Game
</button>
</div>
)}
</div>
)}
{/* Games List */}
<div className="space-y-4">
<div className="flex justify-between items-center">
<h2 className="text-xl font-bold text-green-400">Active Games</h2>
<div className="text-sm text-gray-400">
{games.length} game{games.length !== 1 ? 's' : ''} {Object.values(bets).flat().length} bet{Object.values(bets).flat().length !== 1 ? 's' : ''}
</div>
</div>
{games.length === 0 ? (
<div className="bg-gray-900/50 border border-green-600/30 rounded-lg p-8 text-center">
<div className="text-4xl mb-4">🎯</div>
<div className="text-xl text-gray-300 mb-2">No games yet</div>
<div className="text-sm text-gray-500">
{isAdmin ? 'Create a game to get started!' : 'Waiting for admin to create games...'}
</div>
</div>
) : (
games.map((game) => {
const gameBets = bets[game.id] || [];
const conditions = getUniqueBetConditions(game.id);
const isExpanded = expandedGames[game.id];
return (
<div key={game.id} className="bg-gray-900/50 border border-green-600/30 rounded-lg overflow-hidden">
{/* Game Header */}
<div
onClick={() => toggleGameExpansion(game.id)}
className="p-4 cursor-pointer hover:bg-gray-800/30 flex justify-between items-center"
>
<div className="flex items-center space-x-4">
<div className="text-2xl"></div>
<div>
<div className="text-lg font-bold text-white">
{game.player1} <span className="text-gray-500">vs</span> {game.player2}
</div>
<div className="text-xs text-gray-400">
{gameBets.length} bet{gameBets.length !== 1 ? 's' : ''} {conditions.length} market{conditions.length !== 1 ? 's' : ''}
</div>
</div>
</div>
<div className="flex items-center space-x-3">
<span className={`px-2 py-1 rounded text-xs ${
game.status === 'upcoming' ? 'bg-blue-900/50 text-blue-300' :
game.status === 'in_progress' ? 'bg-green-900/50 text-green-300' :
'bg-gray-700 text-gray-300'
}`}>
{game.status?.replace('_', ' ')}
</span>
{isExpanded ? <ChevronUp size={20} /> : <ChevronDown size={20} />}
</div>
</div>
{/* Expanded Game Content */}
{isExpanded && (
<div className="border-t border-green-800/30 p-4">
{/* Default Markets */}
<div className="mb-4">
<h4 className="text-sm font-semibold text-gray-400 mb-3">Quick Markets</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{[`${game.player1} wins`, `${game.player2} wins`].map((condition) => {
const { yesPrice, noPrice, marketProbability } = calculateTokenPrices(game.id, condition);
const odds = calculateOdds(game.id, condition);
const hasMarket = odds.betCount > 0;
return (
<div key={condition} className="bg-gray-800/50 rounded-lg p-4">
<div className="flex justify-between items-start mb-3">
<span className="font-medium text-white">{condition}</span>
{hasMarket && (
<span className="text-xs text-gray-400">{odds.betCount} bets</span>
)}
</div>
<div className="flex space-x-2 mb-3">
<button
onClick={() => {
setExistingBetAmount(10);
setShowExistingBetForm(prev => ({ ...prev, [`${game.id}-${condition}`]: 'yes' }));
}}
className="flex-1 bg-green-700/50 hover:bg-green-600/50 rounded p-2 text-center"
>
<div className="text-xs text-gray-400">YES</div>
<div className="text-lg font-bold text-green-400">{yesPrice.toFixed(2)}</div>
</button>
<button
onClick={() => {
setExistingBetAmount(10);
setShowExistingBetForm(prev => ({ ...prev, [`${game.id}-${condition}`]: 'no' }));
}}
className="flex-1 bg-red-700/50 hover:bg-red-600/50 rounded p-2 text-center"
>
<div className="text-xs text-gray-400">NO</div>
<div className="text-lg font-bold text-red-400">{noPrice.toFixed(2)}</div>
</button>
</div>
{/* Quick bet form */}
{showExistingBetForm[`${game.id}-${condition}`] && (
<div className="bg-gray-700/50 rounded p-3 mt-2">
<div className="flex items-center space-x-2 mb-2">
<input
type="number"
value={existingBetAmount}
onChange={(e) => setExistingBetAmount(Number(e.target.value))}
min="1"
max={currentUserData.balance}
className="flex-1 p-2 bg-gray-600 rounded text-white text-sm"
/>
<span className="text-xs text-gray-400">tokens</span>
</div>
<div className="flex space-x-2">
<button
onClick={() => placeBetOnExisting(game.id, condition, showExistingBetForm[`${game.id}-${condition}`])}
className={`flex-1 py-2 rounded text-sm font-semibold ${
showExistingBetForm[`${game.id}-${condition}`] === 'yes'
? 'bg-green-600 hover:bg-green-500'
: 'bg-red-600 hover:bg-red-500'
}`}
>
Buy {showExistingBetForm[`${game.id}-${condition}`].toUpperCase()}
</button>
<button
onClick={() => setShowExistingBetForm(prev => ({ ...prev, [`${game.id}-${condition}`]: null }))}
className="px-3 py-2 bg-gray-600 hover:bg-gray-500 rounded text-sm"
>
</button>
</div>
</div>
)}
{hasMarket && (
<div className="text-xs text-center text-gray-400">
Market: {marketProbability}% Pool: {odds.totalAmount}
</div>
)}
</div>
);
})}
</div>
</div>
{/* Custom Conditions */}
{conditions.filter(c => !c.includes('wins')).length > 0 && (
<div className="mb-4">
<h4 className="text-sm font-semibold text-gray-400 mb-3">Custom Markets</h4>
<div className="space-y-2">
{conditions.filter(c => !c.includes('wins')).map((condition) => {
const { yesPrice, noPrice, marketProbability } = calculateTokenPrices(game.id, condition);
const odds = calculateOdds(game.id, condition);
return (
<div key={condition} className="bg-gray-800/50 rounded-lg p-3 flex justify-between items-center">
<div>
<span className="text-white">{condition}</span>
<div className="text-xs text-gray-400">
{odds.betCount} bets {marketProbability}% likely
</div>
</div>
<div className="flex space-x-2">
<button
onClick={() => placeBetOnExisting(game.id, condition, 'yes')}
className="px-3 py-1 bg-green-700/50 hover:bg-green-600/50 rounded text-sm"
>
YES {yesPrice.toFixed(2)}
</button>
<button
onClick={() => placeBetOnExisting(game.id, condition, 'no')}
className="px-3 py-1 bg-red-700/50 hover:bg-red-600/50 rounded text-sm"
>
NO {noPrice.toFixed(2)}
</button>
</div>
</div>
);
})}
</div>
</div>
)}
{/* Create Custom Bet */}
<div className="border-t border-green-800/30 pt-4">
{!showBetForm[game.id] ? (
<button
onClick={() => openBetForm(game.id)}
className="w-full bg-green-800/50 hover:bg-green-700/50 px-4 py-3 rounded flex items-center justify-center space-x-2"
>
<Plus size={18} />
<span>Create Custom Prediction</span>
</button>
) : (
<div className="bg-gray-800/50 rounded-lg p-4">
<div className="flex justify-between items-center mb-4">
<h4 className="font-semibold">New Prediction</h4>
<button onClick={() => closeBetForm(game.id)} className="text-gray-400 hover:text-white">
<X size={18} />
</button>
</div>
<div className="space-y-4">
<div>
<label className="block text-sm text-gray-400 mb-1">What are you predicting?</label>
<input
type="text"
placeholder="e.g., Game ends in checkmate"
value={newBetData.condition}
onChange={(e) => setNewBetData(prev => ({ ...prev, condition: e.target.value }))}
className="w-full p-3 bg-gray-700 border border-gray-600 rounded text-white"
/>
</div>
<div>
<label className="block text-sm text-gray-400 mb-1">
How certain are you? {newBetData.certainty}%
</label>
<input
type="range"
min="1"
max="99"
value={newBetData.certainty}
onChange={(e) => setNewBetData(prev => ({ ...prev, certainty: Number(e.target.value) }))}
className="w-full"
/>
<div className="flex justify-between text-xs text-gray-500">
<span>Unlikely</span>
<span>50/50</span>
<span>Very Likely</span>
</div>
</div>
<div>
<label className="block text-sm text-gray-400 mb-1">Bet Amount</label>
<input
type="number"
min="1"
max={currentUserData.balance}
value={newBetData.betAmount}
onChange={(e) => setNewBetData(prev => ({ ...prev, betAmount: Number(e.target.value) }))}
className="w-full p-3 bg-gray-700 border border-gray-600 rounded text-white"
/>
</div>
<div>
<label className="block text-sm text-gray-400 mb-2">Bet Type</label>
<div className="flex space-x-3">
<button
onClick={() => setBetType('hedged')}
className={`flex-1 p-3 rounded border ${
betType === 'hedged'
? 'border-green-500 bg-green-900/30'
: 'border-gray-600 bg-gray-700'
}`}
>
<div className="font-semibold">Hedged</div>
<div className="text-xs text-gray-400">Split YES/NO tokens</div>
</button>
<button
onClick={() => setBetType('non-hedged')}
className={`flex-1 p-3 rounded border ${
betType === 'non-hedged'
? 'border-green-500 bg-green-900/30'
: 'border-gray-600 bg-gray-700'
}`}
>
<div className="font-semibold">All-In</div>
<div className="text-xs text-gray-400">100% one direction</div>
</button>
</div>
</div>
<button
onClick={prepareBet}
disabled={!newBetData.condition || newBetData.betAmount <= 0}
className="w-full bg-green-700 hover:bg-green-600 disabled:bg-gray-600 disabled:cursor-not-allowed px-4 py-3 rounded font-semibold"
>
Review Bet
</button>
</div>
</div>
)}
</div>
{/* Recent Bets on this game */}
{gameBets.length > 0 && (
<div className="mt-4 border-t border-green-800/30 pt-4">
<h4 className="text-sm font-semibold text-gray-400 mb-2">Recent Activity</h4>
<div className="space-y-2 max-h-40 overflow-y-auto">
{gameBets.slice(-5).reverse().map((bet, idx) => (
<div key={idx} className="text-xs bg-gray-800/30 rounded p-2 flex justify-between">
<span className="text-gray-300">
<span className="text-green-400">{bet.user_name}</span> bet on "{bet.condition}"
</span>
<span className="text-amber-400">{bet.amount} tokens</span>
</div>
))}
</div>
</div>
)}
</div>
)}
</div>
);
})
)}
</div>
{/* Footer */}
<div className="mt-8 text-center text-xs text-gray-500">
<div>🍄 Mycelial Prediction Network 1% Platform Fee</div>
<div className="mt-1">Real-time via Pusher Data stored in Supabase</div>
</div>
</div>
</div>
);
};
export default ChessApp;