1167 lines
49 KiB
JavaScript
1167 lines
49 KiB
JavaScript
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;
|