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