generate ChessApp
This commit is contained in:
commit
3bdd7120f9
|
|
@ -0,0 +1,467 @@
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { ChevronDown, ChevronUp, Plus, Settings, Crown } 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({});
|
||||||
|
const [pendingBet, setPendingBet] = useState(null);
|
||||||
|
const [betType, setBetType] = useState('hedged');
|
||||||
|
const [newBetData, setNewBetData] = useState({ gameId: '', betAmount: 0, 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 [showAdvancedAnalysis, setShowAdvancedAnalysis] = useState(false);
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Create user if none exists
|
||||||
|
if (Object.keys(usersData).length === 0) {
|
||||||
|
const newUserId = 'user_' + Math.random().toString(36).substr(2, 9);
|
||||||
|
const myceliumName = generateMyceliumName();
|
||||||
|
const newUser = {
|
||||||
|
id: newUserId,
|
||||||
|
name: myceliumName,
|
||||||
|
balance: 1000,
|
||||||
|
isAdmin: false
|
||||||
|
};
|
||||||
|
|
||||||
|
await api.saveUser(newUser);
|
||||||
|
setUsers({ [newUserId]: newUser });
|
||||||
|
setCurrentUser(newUserId);
|
||||||
|
} else {
|
||||||
|
// Set current user to first user (in production, this would be from auth)
|
||||||
|
setCurrentUser(Object.keys(usersData)[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
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');
|
||||||
|
|
||||||
|
// Listen for new games
|
||||||
|
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 }));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for new bets
|
||||||
|
channel.bind('new-bet', (data) => {
|
||||||
|
setBets(prev => ({
|
||||||
|
...prev,
|
||||||
|
[data.gameId]: [...(prev[data.gameId] || []), data.bet]
|
||||||
|
}));
|
||||||
|
// Update market history
|
||||||
|
updateMarketHistory(data.gameId, data.bet.condition, data.marketProbability);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for user updates
|
||||||
|
channel.bind('user-update', (data) => {
|
||||||
|
setUsers(prev => ({ ...prev, [data.userId]: data.user }));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for platform account updates
|
||||||
|
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].isAdmin || 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)
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatedHistory = [...currentHistory, newEntry].slice(-10);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
[key]: updatedHistory
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Rest of your calculation functions (unchanged)
|
||||||
|
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 updateUserName = async (newName) => {
|
||||||
|
try {
|
||||||
|
const updatedUser = { ...users[currentUser], name: newName };
|
||||||
|
setUsers(prev => ({ ...prev, [currentUser]: updatedUser }));
|
||||||
|
await api.saveUser(updatedUser);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update user name:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateUserBalance = async (userId, newBalance) => {
|
||||||
|
try {
|
||||||
|
await api.updateUserBalance(userId, newBalance);
|
||||||
|
// The real-time update will come through Pusher
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update user balance:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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',
|
||||||
|
createdAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Optimistically update UI
|
||||||
|
setGames(prev => [...prev, newGame]);
|
||||||
|
setBets(prev => ({ ...prev, [gameId]: [] }));
|
||||||
|
setExpandedGames(prev => ({ ...prev, [gameId]: true }));
|
||||||
|
setNewGameData({ player1: '', player2: '' });
|
||||||
|
setShowGameForm(false);
|
||||||
|
|
||||||
|
// Save to backend
|
||||||
|
await api.saveGame(newGame);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to add game:', error);
|
||||||
|
// Revert optimistic update on error
|
||||||
|
setGames(prev => prev.filter(g => g.id !== newGame.id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Continue with rest of component logic...
|
||||||
|
// (I'll create the rest in the next update due to length)
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-b from-green-900 to-gray-900 text-green-100">
|
||||||
|
{/* Backend Status Indicator */}
|
||||||
|
<div className="bg-green-900/20 border-b border-green-800/30 p-2 text-center">
|
||||||
|
<div className="text-xs text-green-300">
|
||||||
|
🌐 <strong>Live Multiplayer Mode</strong> - Connected to Pusher + Supabase
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Basic header and navigation for now */}
|
||||||
|
<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-2xl font-bold text-green-400">Commons Hub Chess Tournament</h1>
|
||||||
|
<p className="text-sm text-green-300/70">Official Mycelial-Betting Network</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-right">
|
||||||
|
<div className="text-lg font-bold text-amber-400">
|
||||||
|
🟫 {currentUserData.balance} Spore Tokens
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-green-300">{currentUserData.name}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="max-w-6xl mx-auto p-6">
|
||||||
|
<div className="bg-blue-900/20 border border-blue-600/30 rounded-lg p-6 text-center">
|
||||||
|
<div className="text-4xl mb-4">🚀</div>
|
||||||
|
<h3 className="text-xl font-bold text-blue-300 mb-2">Backend Successfully Connected!</h3>
|
||||||
|
<p className="text-blue-200 mb-4">Your app is now running with real backend services:</p>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
|
||||||
|
<div className="bg-purple-900/20 rounded p-3">
|
||||||
|
<div className="font-semibold text-purple-300">🔌 Pusher Real-time</div>
|
||||||
|
<div className="text-purple-400">Live updates between users</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-green-900/20 rounded p-3">
|
||||||
|
<div className="font-semibold text-green-300">🗄️ Supabase Database</div>
|
||||||
|
<div className="text-green-400">Persistent data storage</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-amber-900/20 rounded p-3">
|
||||||
|
<div className="font-semibold text-amber-300">⚡ Vercel Hosting</div>
|
||||||
|
<div className="text-amber-400">Production deployment</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 text-sm text-gray-400">
|
||||||
|
<p>👥 <strong>{Object.keys(users).length}</strong> users connected</p>
|
||||||
|
<p>🎮 <strong>{games.length}</strong> games created</p>
|
||||||
|
<p>💰 <strong>{Object.values(bets).flat().length}</strong> bets placed</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChessApp;
|
||||||
|
|
@ -0,0 +1,104 @@
|
||||||
|
-- Run this SQL in your Supabase SQL Editor to create the database schema
|
||||||
|
|
||||||
|
-- Enable the UUID extension if not already enabled
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||||
|
|
||||||
|
-- Create users table
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
balance DECIMAL(10, 2) DEFAULT 1000.00,
|
||||||
|
is_admin BOOLEAN DEFAULT false,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL,
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create games table
|
||||||
|
CREATE TABLE IF NOT EXISTS games (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
player1 TEXT NOT NULL,
|
||||||
|
player2 TEXT NOT NULL,
|
||||||
|
status TEXT DEFAULT 'upcoming' CHECK (status IN ('upcoming', 'in_progress', 'completed')),
|
||||||
|
winner TEXT,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL,
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create bets table
|
||||||
|
CREATE TABLE IF NOT EXISTS bets (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
game_id TEXT NOT NULL REFERENCES games(id) ON DELETE CASCADE,
|
||||||
|
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
user_name TEXT NOT NULL,
|
||||||
|
amount DECIMAL(10, 2) NOT NULL CHECK (amount > 0),
|
||||||
|
condition TEXT NOT NULL,
|
||||||
|
certainty INTEGER NOT NULL CHECK (certainty >= 0 AND certainty <= 100),
|
||||||
|
yes_tokens INTEGER DEFAULT 0,
|
||||||
|
no_tokens INTEGER DEFAULT 0,
|
||||||
|
bet_type TEXT DEFAULT 'hedged' CHECK (bet_type IN ('hedged', 'non-hedged')),
|
||||||
|
actual_cost DECIMAL(10, 2) NOT NULL,
|
||||||
|
platform_fee DECIMAL(10, 2) NOT NULL,
|
||||||
|
net_cost DECIMAL(10, 2) NOT NULL,
|
||||||
|
is_resolved BOOLEAN DEFAULT false,
|
||||||
|
payout_amount DECIMAL(10, 2) DEFAULT 0,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL,
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create platform account table
|
||||||
|
CREATE TABLE IF NOT EXISTS platform_account (
|
||||||
|
id INTEGER PRIMARY KEY DEFAULT 1,
|
||||||
|
balance DECIMAL(10, 2) DEFAULT 0.00,
|
||||||
|
total_fees DECIMAL(10, 2) DEFAULT 0.00,
|
||||||
|
transaction_count INTEGER DEFAULT 0,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL,
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL,
|
||||||
|
CONSTRAINT single_platform_account CHECK (id = 1)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Insert the initial platform account record
|
||||||
|
INSERT INTO platform_account (id, balance, total_fees, transaction_count)
|
||||||
|
VALUES (1, 0.00, 0.00, 0)
|
||||||
|
ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
-- Create indexes for better performance
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_bets_game_id ON bets(game_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_bets_user_id ON bets(user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_bets_condition ON bets(condition);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_games_status ON games(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_users_created_at ON users(created_at);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_bets_created_at ON bets(created_at);
|
||||||
|
|
||||||
|
-- Create updated_at triggers for automatic timestamp updates
|
||||||
|
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.updated_at = timezone('utc'::text, now());
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ language 'plpgsql';
|
||||||
|
|
||||||
|
-- Apply the trigger to all tables
|
||||||
|
CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||||
|
|
||||||
|
CREATE TRIGGER update_games_updated_at BEFORE UPDATE ON games
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||||
|
|
||||||
|
CREATE TRIGGER update_bets_updated_at BEFORE UPDATE ON bets
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||||
|
|
||||||
|
CREATE TRIGGER update_platform_account_updated_at BEFORE UPDATE ON platform_account
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||||
|
|
||||||
|
-- Enable Row Level Security (RLS) for better security
|
||||||
|
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
|
||||||
|
ALTER TABLE games ENABLE ROW LEVEL SECURITY;
|
||||||
|
ALTER TABLE bets ENABLE ROW LEVEL SECURITY;
|
||||||
|
ALTER TABLE platform_account ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
-- Create policies (allow all operations for now, you can restrict later)
|
||||||
|
CREATE POLICY "Allow all operations on users" ON users FOR ALL USING (true);
|
||||||
|
CREATE POLICY "Allow all operations on games" ON games FOR ALL USING (true);
|
||||||
|
CREATE POLICY "Allow all operations on bets" ON bets FOR ALL USING (true);
|
||||||
|
CREATE POLICY "Allow all operations on platform_account" ON platform_account FOR ALL USING (true);
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
import Pusher from 'pusher'
|
||||||
|
import PusherClient from 'pusher-js'
|
||||||
|
|
||||||
|
// Server-side Pusher (for API routes)
|
||||||
|
export const pusher = new Pusher({
|
||||||
|
appId: process.env.PUSHER_APP_ID,
|
||||||
|
key: process.env.NEXT_PUBLIC_PUSHER_APP_KEY,
|
||||||
|
secret: process.env.PUSHER_SECRET,
|
||||||
|
cluster: process.env.NEXT_PUBLIC_PUSHER_CLUSTER,
|
||||||
|
useTLS: true
|
||||||
|
})
|
||||||
|
|
||||||
|
// Client-side Pusher
|
||||||
|
export const pusherClient = typeof window !== 'undefined' ? new PusherClient(
|
||||||
|
process.env.NEXT_PUBLIC_PUSHER_APP_KEY,
|
||||||
|
{
|
||||||
|
cluster: process.env.NEXT_PUBLIC_PUSHER_CLUSTER,
|
||||||
|
forceTLS: true
|
||||||
|
}
|
||||||
|
) : null
|
||||||
|
|
||||||
|
// Helper function to trigger events
|
||||||
|
export const triggerPusherEvent = async (channel, event, data) => {
|
||||||
|
try {
|
||||||
|
await fetch('/api/pusher/trigger', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
channel,
|
||||||
|
event,
|
||||||
|
data
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to trigger Pusher event:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,140 @@
|
||||||
|
import { createClient } from '@supabase/supabase-js'
|
||||||
|
|
||||||
|
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL
|
||||||
|
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
|
||||||
|
|
||||||
|
if (!supabaseUrl || !supabaseKey) {
|
||||||
|
throw new Error('Missing Supabase environment variables')
|
||||||
|
}
|
||||||
|
|
||||||
|
export const supabase = createClient(supabaseUrl, supabaseKey)
|
||||||
|
|
||||||
|
// Helper functions for database operations
|
||||||
|
export const db = {
|
||||||
|
// Users
|
||||||
|
async getUsers() {
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('users')
|
||||||
|
.select('*')
|
||||||
|
|
||||||
|
if (error) throw error
|
||||||
|
|
||||||
|
// Convert array to object with id as key
|
||||||
|
const usersObj = {}
|
||||||
|
data.forEach(user => {
|
||||||
|
usersObj[user.id] = user
|
||||||
|
})
|
||||||
|
return usersObj
|
||||||
|
},
|
||||||
|
|
||||||
|
async saveUser(user) {
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('users')
|
||||||
|
.upsert(user)
|
||||||
|
.select()
|
||||||
|
|
||||||
|
if (error) throw error
|
||||||
|
return data[0]
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateUserBalance(userId, balance) {
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('users')
|
||||||
|
.update({ balance })
|
||||||
|
.eq('id', userId)
|
||||||
|
.select()
|
||||||
|
|
||||||
|
if (error) throw error
|
||||||
|
return data[0]
|
||||||
|
},
|
||||||
|
|
||||||
|
// Games
|
||||||
|
async getGames() {
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('games')
|
||||||
|
.select('*')
|
||||||
|
.order('created_at', { ascending: false })
|
||||||
|
|
||||||
|
if (error) throw error
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
|
||||||
|
async saveGame(game) {
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('games')
|
||||||
|
.insert(game)
|
||||||
|
.select()
|
||||||
|
|
||||||
|
if (error) throw error
|
||||||
|
return data[0]
|
||||||
|
},
|
||||||
|
|
||||||
|
// Bets
|
||||||
|
async getBets() {
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('bets')
|
||||||
|
.select('*')
|
||||||
|
.order('created_at', { ascending: true })
|
||||||
|
|
||||||
|
if (error) throw error
|
||||||
|
|
||||||
|
// Group bets by game_id
|
||||||
|
const betsObj = {}
|
||||||
|
data.forEach(bet => {
|
||||||
|
if (!betsObj[bet.game_id]) {
|
||||||
|
betsObj[bet.game_id] = []
|
||||||
|
}
|
||||||
|
betsObj[bet.game_id].push(bet)
|
||||||
|
})
|
||||||
|
return betsObj
|
||||||
|
},
|
||||||
|
|
||||||
|
async saveBet(bet) {
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('bets')
|
||||||
|
.insert(bet)
|
||||||
|
.select()
|
||||||
|
|
||||||
|
if (error) throw error
|
||||||
|
return data[0]
|
||||||
|
},
|
||||||
|
|
||||||
|
// Platform Account
|
||||||
|
async getPlatformAccount() {
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('platform_account')
|
||||||
|
.select('*')
|
||||||
|
.eq('id', 1)
|
||||||
|
.single()
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
// If no platform account exists, create one
|
||||||
|
if (error.code === 'PGRST116') {
|
||||||
|
return { balance: 0, total_fees: 0, transaction_count: 0 }
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
balance: parseFloat(data.balance),
|
||||||
|
totalFees: parseFloat(data.total_fees),
|
||||||
|
transactionCount: data.transaction_count
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async savePlatformAccount(account) {
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('platform_account')
|
||||||
|
.upsert({
|
||||||
|
id: 1,
|
||||||
|
balance: account.balance,
|
||||||
|
total_fees: account.totalFees,
|
||||||
|
transaction_count: account.transactionCount,
|
||||||
|
updated_at: new Date().toISOString()
|
||||||
|
})
|
||||||
|
.select()
|
||||||
|
|
||||||
|
if (error) throw error
|
||||||
|
return data[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
reactStrictMode: true,
|
||||||
|
swcMinify: true,
|
||||||
|
experimental: {
|
||||||
|
// Enable if you want to use app directory in the future
|
||||||
|
appDir: false
|
||||||
|
},
|
||||||
|
// Environment variables that should be available on the client side
|
||||||
|
env: {
|
||||||
|
NEXT_PUBLIC_PUSHER_APP_KEY: process.env.NEXT_PUBLIC_PUSHER_APP_KEY,
|
||||||
|
NEXT_PUBLIC_PUSHER_CLUSTER: process.env.NEXT_PUBLIC_PUSHER_CLUSTER,
|
||||||
|
NEXT_PUBLIC_SUPABASE_URL: process.env.NEXT_PUBLIC_SUPABASE_URL,
|
||||||
|
NEXT_PUBLIC_SUPABASE_ANON_KEY: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = nextConfig
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
import { db } from '../../lib/supabase'
|
||||||
|
import { triggerPusherEvent } from '../../lib/pusher'
|
||||||
|
|
||||||
|
export default async function handler(req, res) {
|
||||||
|
try {
|
||||||
|
switch (req.method) {
|
||||||
|
case 'GET':
|
||||||
|
const bets = await db.getBets()
|
||||||
|
res.status(200).json(bets)
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'POST':
|
||||||
|
const { gameId, bet, marketProbability } = req.body
|
||||||
|
if (!gameId || !bet || !bet.id) {
|
||||||
|
return res.status(400).json({ error: 'Invalid bet data' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const savedBet = await db.saveBet({
|
||||||
|
id: bet.id,
|
||||||
|
game_id: gameId,
|
||||||
|
user_id: bet.userId,
|
||||||
|
user_name: bet.userName,
|
||||||
|
amount: bet.amount,
|
||||||
|
condition: bet.condition,
|
||||||
|
certainty: bet.certainty,
|
||||||
|
yes_tokens: bet.yesTokens || 0,
|
||||||
|
no_tokens: bet.noTokens || 0,
|
||||||
|
bet_type: bet.betType || 'hedged',
|
||||||
|
actual_cost: bet.actualCost,
|
||||||
|
platform_fee: bet.platformFee,
|
||||||
|
net_cost: bet.netCost,
|
||||||
|
created_at: bet.createdAt || new Date().toISOString()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Convert database format back to app format
|
||||||
|
const appBet = {
|
||||||
|
id: savedBet.id,
|
||||||
|
userId: savedBet.user_id,
|
||||||
|
userName: savedBet.user_name,
|
||||||
|
amount: parseFloat(savedBet.amount),
|
||||||
|
condition: savedBet.condition,
|
||||||
|
certainty: savedBet.certainty,
|
||||||
|
yesTokens: savedBet.yes_tokens,
|
||||||
|
noTokens: savedBet.no_tokens,
|
||||||
|
betType: savedBet.bet_type,
|
||||||
|
actualCost: parseFloat(savedBet.actual_cost),
|
||||||
|
platformFee: parseFloat(savedBet.platform_fee),
|
||||||
|
netCost: parseFloat(savedBet.net_cost),
|
||||||
|
createdAt: savedBet.created_at
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger real-time update
|
||||||
|
await triggerPusherEvent('chess-tournament', 'new-bet', {
|
||||||
|
gameId: gameId,
|
||||||
|
bet: appBet,
|
||||||
|
marketProbability: marketProbability || 50
|
||||||
|
})
|
||||||
|
|
||||||
|
res.status(200).json(savedBet)
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
res.status(405).json({ error: 'Method not allowed' })
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Bets API error:', error)
|
||||||
|
res.status(500).json({ error: 'Internal server error' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { db } from '../../lib/supabase'
|
||||||
|
import { triggerPusherEvent } from '../../lib/pusher'
|
||||||
|
|
||||||
|
export default async function handler(req, res) {
|
||||||
|
try {
|
||||||
|
switch (req.method) {
|
||||||
|
case 'GET':
|
||||||
|
const games = await db.getGames()
|
||||||
|
res.status(200).json(games)
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'POST':
|
||||||
|
const { game } = req.body
|
||||||
|
if (!game || !game.id || !game.player1 || !game.player2) {
|
||||||
|
return res.status(400).json({ error: 'Invalid game data' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const savedGame = await db.saveGame({
|
||||||
|
id: game.id,
|
||||||
|
player1: game.player1,
|
||||||
|
player2: game.player2,
|
||||||
|
status: game.status || 'upcoming',
|
||||||
|
created_at: game.createdAt || new Date().toISOString()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Trigger real-time update
|
||||||
|
await triggerPusherEvent('chess-tournament', 'new-game', {
|
||||||
|
game: {
|
||||||
|
id: savedGame.id,
|
||||||
|
player1: savedGame.player1,
|
||||||
|
player2: savedGame.player2,
|
||||||
|
status: savedGame.status,
|
||||||
|
createdAt: savedGame.created_at
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
res.status(200).json(savedGame)
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
res.status(405).json({ error: 'Method not allowed' })
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Games API error:', error)
|
||||||
|
res.status(500).json({ error: 'Internal server error' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { db } from '../../lib/supabase'
|
||||||
|
import { triggerPusherEvent } from '../../lib/pusher'
|
||||||
|
|
||||||
|
export default async function handler(req, res) {
|
||||||
|
try {
|
||||||
|
switch (req.method) {
|
||||||
|
case 'GET':
|
||||||
|
const account = await db.getPlatformAccount()
|
||||||
|
res.status(200).json(account)
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'POST':
|
||||||
|
const { platformAccount } = req.body
|
||||||
|
if (!platformAccount) {
|
||||||
|
return res.status(400).json({ error: 'Invalid platform account data' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const savedAccount = await db.savePlatformAccount(platformAccount)
|
||||||
|
|
||||||
|
// Trigger real-time update
|
||||||
|
await triggerPusherEvent('chess-tournament', 'platform-update', {
|
||||||
|
platformAccount: {
|
||||||
|
balance: parseFloat(savedAccount.balance),
|
||||||
|
totalFees: parseFloat(savedAccount.total_fees),
|
||||||
|
transactionCount: savedAccount.transaction_count
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
res.status(200).json(savedAccount)
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
res.status(405).json({ error: 'Method not allowed' })
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Platform API error:', error)
|
||||||
|
res.status(500).json({ error: 'Internal server error' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { pusher } from '../../../lib/pusher'
|
||||||
|
|
||||||
|
export default async function handler(req, res) {
|
||||||
|
if (req.method !== 'POST') {
|
||||||
|
return res.status(405).json({ error: 'Method not allowed' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const { channel, event, data } = req.body
|
||||||
|
|
||||||
|
if (!channel || !event || !data) {
|
||||||
|
return res.status(400).json({ error: 'Missing required fields' })
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await pusher.trigger(channel, event, data)
|
||||||
|
res.status(200).json({ success: true })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Pusher trigger error:', error)
|
||||||
|
res.status(500).json({ error: 'Failed to trigger event' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
import { db } from '../../lib/supabase'
|
||||||
|
import { triggerPusherEvent } from '../../lib/pusher'
|
||||||
|
|
||||||
|
export default async function handler(req, res) {
|
||||||
|
try {
|
||||||
|
switch (req.method) {
|
||||||
|
case 'GET':
|
||||||
|
const users = await db.getUsers()
|
||||||
|
res.status(200).json(users)
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'POST':
|
||||||
|
const { user } = req.body
|
||||||
|
if (!user || !user.id) {
|
||||||
|
return res.status(400).json({ error: 'Invalid user data' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const savedUser = await db.saveUser({
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
|
balance: user.balance,
|
||||||
|
is_admin: user.isAdmin || false
|
||||||
|
})
|
||||||
|
|
||||||
|
// Trigger real-time update
|
||||||
|
await triggerPusherEvent('chess-tournament', 'user-update', {
|
||||||
|
userId: user.id,
|
||||||
|
user: {
|
||||||
|
id: savedUser.id,
|
||||||
|
name: savedUser.name,
|
||||||
|
balance: parseFloat(savedUser.balance),
|
||||||
|
isAdmin: savedUser.is_admin
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
res.status(200).json(savedUser)
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'PUT':
|
||||||
|
const { userId, updates } = req.body
|
||||||
|
if (!userId || !updates) {
|
||||||
|
return res.status(400).json({ error: 'Missing userId or updates' })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updates.balance !== undefined) {
|
||||||
|
const updatedUser = await db.updateUserBalance(userId, updates.balance)
|
||||||
|
|
||||||
|
// Trigger real-time update
|
||||||
|
await triggerPusherEvent('chess-tournament', 'user-update', {
|
||||||
|
userId: userId,
|
||||||
|
user: {
|
||||||
|
id: updatedUser.id,
|
||||||
|
name: updatedUser.name,
|
||||||
|
balance: parseFloat(updatedUser.balance),
|
||||||
|
isAdmin: updatedUser.is_admin
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
res.status(200).json(updatedUser)
|
||||||
|
} else {
|
||||||
|
res.status(400).json({ error: 'No valid updates provided' })
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
res.status(405).json({ error: 'Method not allowed' })
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Users API error:', error)
|
||||||
|
res.status(500).json({ error: 'Internal server error' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
import Head from 'next/head'
|
||||||
|
import dynamic from 'next/dynamic'
|
||||||
|
|
||||||
|
// Dynamically import the chess app to avoid SSR issues with Pusher
|
||||||
|
const ChessApp = dynamic(() => import('../components/ChessApp'), {
|
||||||
|
ssr: false,
|
||||||
|
loading: () => (
|
||||||
|
<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 application...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>Commons Hub Chess Tournament</title>
|
||||||
|
<meta name="description" content="Official Mycelial-Betting Network for Chess Tournaments" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
</Head>
|
||||||
|
<ChessApp />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,172 @@
|
||||||
|
# Commons Hub Chess Tournament 🍄
|
||||||
|
|
||||||
|
Official Mycelial-Betting Network for Chess Tournaments with real-time multiplayer functionality.
|
||||||
|
|
||||||
|
## 🚀 Quick Deploy to Vercel
|
||||||
|
|
||||||
|
### 1. Clone Repository
|
||||||
|
```bash
|
||||||
|
git clone [your-repo-url]
|
||||||
|
cd chess-tournament-betting
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Set Up Services
|
||||||
|
|
||||||
|
#### Supabase Setup
|
||||||
|
1. Go to [supabase.com](https://supabase.com) and create a new project
|
||||||
|
2. Go to SQL Editor and run the schema from `database/schema.sql`
|
||||||
|
3. Get your project URL and anon key from Settings > API
|
||||||
|
|
||||||
|
#### Pusher Setup
|
||||||
|
1. Go to [pusher.com](https://pusher.com) and create a new app
|
||||||
|
2. Choose your region (e.g., us-east-1)
|
||||||
|
3. Get your App ID, Key, Secret, and Cluster from App Keys
|
||||||
|
|
||||||
|
### 3. Environment Variables
|
||||||
|
|
||||||
|
Create `.env.local` file in your project root:
|
||||||
|
```bash
|
||||||
|
# Copy from .env.local.example and fill in your values
|
||||||
|
NEXT_PUBLIC_PUSHER_APP_KEY=your_pusher_key
|
||||||
|
NEXT_PUBLIC_PUSHER_CLUSTER=us2
|
||||||
|
PUSHER_APP_ID=your_pusher_app_id
|
||||||
|
PUSHER_SECRET=your_pusher_secret
|
||||||
|
NEXT_PUBLIC_SUPABASE_URL=your_supabase_url
|
||||||
|
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Deploy to Vercel
|
||||||
|
|
||||||
|
#### Option A: GitHub Integration (Recommended)
|
||||||
|
1. Push your code to GitHub
|
||||||
|
2. Go to [vercel.com](https://vercel.com) and import your GitHub repo
|
||||||
|
3. Add environment variables in Vercel dashboard
|
||||||
|
4. Deploy! 🚀
|
||||||
|
|
||||||
|
#### Option B: Vercel CLI
|
||||||
|
```bash
|
||||||
|
npm install -g vercel
|
||||||
|
vercel
|
||||||
|
# Follow prompts and add environment variables
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🏗️ Local Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Run development server
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# Open http://localhost:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📁 Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
chess-tournament-betting/
|
||||||
|
├── components/
|
||||||
|
│ └── ChessApp.js # Main React component
|
||||||
|
├── lib/
|
||||||
|
│ ├── supabase.js # Database client & helpers
|
||||||
|
│ └── pusher.js # Real-time client & helpers
|
||||||
|
├── pages/
|
||||||
|
│ ├── api/
|
||||||
|
│ │ ├── pusher/trigger.js # Pusher webhook endpoint
|
||||||
|
│ │ ├── users.js # User management API
|
||||||
|
│ │ ├── games.js # Game management API
|
||||||
|
│ │ ├── bets.js # Betting system API
|
||||||
|
│ │ └── platform.js # Platform account API
|
||||||
|
│ └── index.js # Main page
|
||||||
|
├── database/
|
||||||
|
│ └── schema.sql # Database schema
|
||||||
|
├── package.json
|
||||||
|
├── next.config.js
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎮 Features
|
||||||
|
|
||||||
|
- **Real-time Multiplayer**: Live updates via Pusher WebSockets
|
||||||
|
- **Prediction Markets**: YES/NO token betting with dynamic pricing
|
||||||
|
- **Advanced Analytics**: Market efficiency, contrarian analysis
|
||||||
|
- **Platform Economy**: 1% commons fee system
|
||||||
|
- **Admin Dashboard**: User management and platform controls
|
||||||
|
- **Mobile Responsive**: Works on all devices
|
||||||
|
|
||||||
|
## 🔧 Tech Stack
|
||||||
|
|
||||||
|
- **Frontend**: Next.js + React
|
||||||
|
- **Database**: Supabase (PostgreSQL)
|
||||||
|
- **Real-time**: Pusher WebSockets
|
||||||
|
- **Hosting**: Vercel
|
||||||
|
- **Styling**: Tailwind CSS (CDN)
|
||||||
|
|
||||||
|
## 🚨 Important Notes
|
||||||
|
|
||||||
|
### Vercel Environment Variables
|
||||||
|
When deploying to Vercel, add these environment variables in your Vercel dashboard:
|
||||||
|
|
||||||
|
1. Go to your project in Vercel
|
||||||
|
2. Navigate to Settings > Environment Variables
|
||||||
|
3. Add each variable from your `.env.local` file
|
||||||
|
|
||||||
|
### Database Schema
|
||||||
|
Make sure to run the SQL schema in Supabase **before** deploying:
|
||||||
|
1. Open Supabase dashboard
|
||||||
|
2. Go to SQL Editor
|
||||||
|
3. Copy and paste contents of `database/schema.sql`
|
||||||
|
4. Run the query
|
||||||
|
|
||||||
|
### Pusher Configuration
|
||||||
|
Ensure your Pusher app is configured for your deployment domain:
|
||||||
|
1. Go to Pusher dashboard
|
||||||
|
2. Navigate to App Settings
|
||||||
|
3. Add your Vercel domain to allowed origins
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
**"Failed to connect to Pusher"**
|
||||||
|
- Check your Pusher environment variables
|
||||||
|
- Ensure cluster matches your Pusher app region
|
||||||
|
|
||||||
|
**"Database connection failed"**
|
||||||
|
- Verify Supabase URL and anon key
|
||||||
|
- Check if database schema has been applied
|
||||||
|
|
||||||
|
**"Build failed on Vercel"**
|
||||||
|
- Ensure all environment variables are set
|
||||||
|
- Check that dependencies are correctly specified in package.json
|
||||||
|
|
||||||
|
### Testing Locally
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test API endpoints
|
||||||
|
curl http://localhost:3000/api/users
|
||||||
|
curl http://localhost:3000/api/games
|
||||||
|
curl http://localhost:3000/api/platform
|
||||||
|
|
||||||
|
# Check environment variables
|
||||||
|
echo $NEXT_PUBLIC_PUSHER_APP_KEY
|
||||||
|
echo $NEXT_PUBLIC_SUPABASE_URL
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 License
|
||||||
|
|
||||||
|
MIT License - Feel free to use for your chess tournaments!
|
||||||
|
|
||||||
|
## 🤝 Contributing
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a feature branch
|
||||||
|
3. Commit your changes
|
||||||
|
4. Push to the branch
|
||||||
|
5. Create a Pull Request
|
||||||
|
|
||||||
|
## 🍄 Happy Betting!
|
||||||
|
|
||||||
|
Your mycelial network awaits. May the spores be with you! 🕸️
|
||||||
|
│
|
||||||
Loading…
Reference in New Issue