Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/quarterback/SimBaseballCalc. A fantasy baseball calculator for OOTP using CSVs
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399import React, { useState } from 'react';import Papa from 'papaparse';
const FantasyCalculator = () => { const [hittingStats, setHittingStats] = useState([]); const [pitchingStats, setPitchingStats] = useState([]); const [hittingSearchQuery, setHittingSearchQuery] = useState(''); const [pitchingSearchQuery, setPitchingSearchQuery] = useState(''); const [scoringSystem, setScoringSystem] = useState('draftKingsDFS'); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState('');
const scoringSystems = { draftKingsDFS: { name: 'DraftKings DFS', hitting: { '1B': 3, '2B': 5, '3B': 8, 'HR': 10, 'R': 2, 'RBI': 2, 'BB': 2, 'SB': 5, 'CS': -2, 'HBP': 2 }, pitching: { 'IP': 2.25, 'K': 2, 'W': 4, 'ER': -2, 'H': -0.6, 'BB': -0.6, 'HBP': -0.6, 'CG': 2.5, 'CGSO': 2.5, 'NH': 5 } }, fanduelDFS: { name: 'FanDuel DFS', hitting: { '1B': 3, '2B': 6, '3B': 9, 'HR': 12, 'R': 3.2, 'RBI': 3.5, 'BB': 3, 'SB': 6, 'CS': -3, 'HBP': 3 }, pitching: { 'IP': 3, 'K': 3, 'W': 6, 'ER': -3, 'H': -0.6, 'BB': -0.6, 'HBP': -0.6, 'CG': 3, 'CGSO': 3, 'NH': 6 } }, sabermetricDFS: { name: 'Sabermetric DFS', hitting: { '1B': 2, '2B': 4, '3B': 6, 'HR': 8, 'R': 1.5, 'RBI': 1.2, 'BB': 3, 'SB': 4, 'CS': -4, 'HBP': 3, 'AVG': 10, 'OBP': 15, 'SLG': 12 }, pitching: { 'IP': 3, 'K': 2.5, 'BB': -2, 'HR': -4, 'ERA': -5, 'WHIP': -8, 'K/9': 2, 'K/BB': 3, 'FIP': -5 } }, powerHitter: { name: 'Power Hitter League', hitting: { '1B': 1, '2B': 3, '3B': 5, 'HR': 15, 'R': 2, 'RBI': 3, 'BB': 1, 'SB': 1, 'SLG': 10, 'ISO': 12 }, pitching: { 'IP': 2, 'K': 3, 'W': 5, 'ER': -1, 'H': -0.5, 'BB': -0.5, 'HR': -3 } }, smallBall: { name: 'Small Ball League', hitting: { '1B': 4, '2B': 6, '3B': 8, 'HR': 8, 'R': 3, 'RBI': 2, 'BB': 3, 'SB': 8, 'CS': -1, 'AVG': 15 }, pitching: { 'IP': 4, 'K': 2, 'W': 5, 'SV': 8, 'HLD': 6, 'ER': -2, 'H': -0.5, 'BB': -1 } }, roto5x5: { name: 'Rotisserie 5x5', hitting: ['R', 'HR', 'RBI', 'SB', 'AVG'], pitching: ['W', 'SV', 'ERA', 'WHIP', 'K'] }, statcastEra: { name: 'Statcast Era', hitting: { 'HR': 4, 'BB': 3, 'K': -2, 'OBP': 15, 'SLG': 10, 'ISO': 8, 'BABIP': 5, 'OPS': 12, 'WAR': 10 }, pitching: { 'K/9': 5, 'BB/9': -3, 'HR/9': -5, 'WHIP': -8, 'FIP': -6, 'ERA+': 4, 'WAR': 8 } }, backwardsBaseball: { name: 'Backwards Baseball', hitting: { 'AB': 1, 'H': -2, 'HR': -10, 'RBI': -2, 'BB': -3, 'K': 3, 'GIDP': 5, 'CS': 4 }, pitching: { 'IP': 2, 'ER': 3, 'H': 1, 'BB': 2, 'K': -2, 'HR': 5, 'L': 10 } }};
const calculatePoints = (player, type) => { const scoring = scoringSystems[scoringSystem][type]; return Object.entries(scoring).reduce((total, [stat, points]) => { if (player[stat]) return total + player[stat] * points; return total; }, 0); };
const getPlayerScores = (type) => { const stats = type === 'hitting' ? hittingStats : pitchingStats; return stats .map((player) => ({ ...player, FantasyPoints: calculatePoints(player, type), })) .sort((a, b) => b.FantasyPoints - a.FantasyPoints); };
const processFile = async (file, type) => { setIsLoading(true); setError(''); try { const text = await file.text(); Papa.parse(text, { header: true, dynamicTyping: true, skipEmptyLines: true, complete: (results) => { if (results.errors.length > 0) { setError('Error parsing CSV file'); setIsLoading(false); return; }
const stats = results.data.map((player) => ({ ...player, '1B': type === 'hitting' ? player.H - (player['2B'] + player['3B'] + player.HR) : undefined, })); if (type === 'hitting') { setHittingStats(stats); } else { setPitchingStats(stats); } setIsLoading(false); }, error: (parseError) => { setError(`Error processing file: ${parseError.message}`); setIsLoading(false); } }); } catch (fileError) { setError(`Error reading file: ${fileError.message}`); setIsLoading(false); } };
return ( <div className="space-y-6 p-6 bg-gray-50 min-h-screen"> <h1 className="text-3xl font-bold text-center text-gray-800"> OOTP Fantasy Calculator </h1>
{error && ( <div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative"> {error} </div> )}
{isLoading && ( <div className="bg-blue-100 border border-blue-400 text-blue-700 px-4 py-3 rounded relative"> Processing file... </div> )}
<div className="mb-6"> <label className="block text-sm font-medium text-gray-700 mb-2"> Scoring System: </label> <select value={scoringSystem} onChange={(e) => setScoringSystem(e.target.value)} disabled={isLoading} className="w-full p-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50" > {Object.entries(scoringSystems).map(([key, system]) => ( <option key={key} value={key}> {system.name} </option> ))} </select> </div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> {/* Hitting Stats */} <div className="bg-white rounded-lg shadow p-4"> <h2 className="text-xl font-bold mb-4">Hitting Stats</h2> <input type="text" placeholder="Search hitting stats..." value={hittingSearchQuery} onChange={(e) => setHittingSearchQuery(e.target.value)} disabled={isLoading} className="w-full p-2 mb-4 border border-gray-300 rounded-lg disabled:opacity-50" /> <input type="file" accept=".csv" onChange={(e) => e.target.files?.[0] && processFile(e.target.files[0], 'hitting')} disabled={isLoading} className="block w-full p-2 border rounded-lg mb-4 disabled:opacity-50" /> {hittingStats.length > 0 ? ( <div className={`transition-opacity ${isLoading ? 'opacity-50' : 'opacity-100'}`}> <table className="min-w-full border border-gray-200 rounded-lg"> <thead className="bg-gray-100"> <tr> <th className="p-2">Name</th> <th className="p-2">Position</th> <th className="p-2">Points</th> </tr> </thead> <tbody> {getPlayerScores('hitting') .filter(player => player.Name?.toLowerCase().includes(hittingSearchQuery.toLowerCase()) || player.POS?.toLowerCase().includes(hittingSearchQuery.toLowerCase()) ) .map((player, idx) => ( <tr key={idx} className={idx % 2 === 0 ? 'bg-gray-50' : 'bg-white'}> <td className="p-2">{player.Name}</td> <td className="p-2">{player.POS}</td> <td className="p-2 text-right"> {player.FantasyPoints.toFixed(1)} </td> </tr> ))} </tbody> </table> </div> ) : ( <p className="text-gray-500 mt-4">No hitting stats uploaded yet.</p> )} </div>
{/* Pitching Stats */} <div className="bg-white rounded-lg shadow p-4"> <h2 className="text-xl font-bold mb-4">Pitching Stats</h2> <input type="text" placeholder="Search pitching stats..." value={pitchingSearchQuery} onChange={(e) => setPitchingSearchQuery(e.target.value)} disabled={isLoading} className="w-full p-2 mb-4 border border-gray-300 rounded-lg disabled:opacity-50" /> <input type="file" accept=".csv" onChange={(e) => e.target.files?.[0] && processFile(e.target.files[0], 'pitching')} disabled={isLoading} className="block w-full p-2 border rounded-lg mb-4 disabled:opacity-50" /> {pitchingStats.length > 0 ? ( <div className={`transition-opacity ${isLoading ? 'opacity-50' : 'opacity-100'}`}> <table className="min-w-full border border-gray-200 rounded-lg"> <thead className="bg-gray-100"> <tr> <th className="p-2">Name</th> <th className="p-2">Position</th> <th className="p-2">Points</th> </tr> </thead> <tbody> {getPlayerScores('pitching') .filter(player => player.Name?.toLowerCase().includes(pitchingSearchQuery.toLowerCase()) || player.POS?.toLowerCase().includes(pitchingSearchQuery.toLowerCase()) ) .map((player, idx) => ( <tr key={idx} className={idx % 2 === 0 ? 'bg-gray-50' : 'bg-white'}> <td className="p-2">{player.Name}</td> <td className="p-2">{player.POS}</td> <td className="p-2 text-right"> {player.FantasyPoints.toFixed(1)} </td> </tr> ))} </tbody> </table> </div> ) : ( <p className="text-gray-500 mt-4">No pitching stats uploaded yet.</p> )} </div> </div> </div> );};
export default FantasyCalculator;