Compare Weekly Fantasy Projections
Your Ultimate Fantasy Football Projection App
Easily compare weekly projections from top sources and make informed decisions for your fantasy football lineup. Get high, low, and average projections for each player.
01
Portfolio Management
We will work with you to create a personalised plan to help you achieve your financial goals.
02
Performance Reviews
We will work with you to create a personalised plan to help you achieve your financial goals.
03
Financial Planning
We will work with you to create a personalised plan to help you achieve your financial goals.
04
Portfolio Management
We will work with you to create a personalised plan to help you achieve your financial goals.
05
Performance Reviews
We will work with you to create a personalised plan to help you achieve your financial goals.
06
Financial Planning
We will work with you to create a personalised plan to help you achieve your financial goals.
What are weekly projections?
Weekly projections are estimates of player performance in fantasy football based on various factors, including past performance, matchups, and expert analysis.
How do I compare projections from different sites?
You can compare projections by selecting players from our app, which aggregates data from fantasyfootballers.com, fantasypros.com, sleeper.com, and espn.com, providing high, low, and average projections.
What scoring settings can I choose?
You can select scoring settings for QBs as either 4 or 6 points per passing touchdown, and for RBs, WRs, and TEs, you can choose between 0.5 or 1.0 PPR, with TEs having an additional option of 1.5 PPR.
How can I access recent mentions of players?
Each player has a clickable link that opens in a new window, showing the two most recent mentions from the @CoachspeakIndex X account in chronological order.
Is there a search function available?
Yes, our app includes a search function that allows you to find players not specifically listed, making it easier to compare projections for all your fantasy football needs.
What Our Users Are Saying
★★★★★
Average Rating: 4.8 out of 5 based on 150 reviews
Best Fantasy App Ever!
The ability to see high, low, and average projections at a glance is fantastic. Highly recommend this app!
Essential for Fantasy Players!
This app is a must-have for anyone serious about fantasy football. The detailed projections and recent mentions are incredibly helpful!
A Game Changer for Fantasy Football!
This app has transformed how I approach my fantasy football leagues. The projections are accurate and the interface is user-friendly!
Invaluable Resource for Players!
I love how easy it is to compare projections from different sources. It has helped me make better decisions for my team!
import React, { useState, useEffect, useMemo } from ‘react’;
import { PlusCircle, Trash2, ArrowUp, ArrowDown, BarChart2, Settings, ChevronDown, ChevronUp } from ‘lucide-react’;
// — MOCK DATA & CONFIG —
const MOCK_PLAYERS = {
// QBs
“3”: { name: “Josh Allen”, position: “QB”, team: “BUF” }, “6”: { name: “Lamar Jackson”, position: “QB”, team: “BAL” }, “101”: { name: “Patrick Mahomes”, position: “QB”, team: “KC” }, “102”: { name: “Jalen Hurts”, position: “QB”, team: “PHI” }, “103”: { name: “C.J. Stroud”, position: “QB”, team: “HOU” }, “104”: { name: “Anthony Richardson”, position: “QB”, team: “IND” }, “105”: { name: “Joe Burrow”, position: “QB”, team: “CIN” }, “106”: { name: “Dak Prescott”, position: “QB”, team: “DAL” }, “107”: { name: “Kyler Murray”, position: “QB”, team: “ARI” }, “108”: { name: “Jordan Love”, position: “QB”, team: “GB” }, “109”: { name: “Brock Purdy”, position: “QB”, team: “SF” }, “110”: { name: “Tua Tagovailoa”, position: “QB”, team: “MIA” }, “111”: { name: “Kirk Cousins”, position: “QB”, team: “ATL” }, “112”: { name: “Trevor Lawrence”, position: “QB”, team: “JAX” }, “113”: { name: “Justin Herbert”, position: “QB”, team: “LAC” }, “114”: { name: “Jared Goff”, position: “QB”, team: “DET” }, “115”: { name: “Matthew Stafford”, position: “QB”, team: “LAR” }, “116”: { name: “Aaron Rodgers”, position: “QB”, team: “NYJ” }, “117”: { name: “Caleb Williams”, position: “QB”, team: “CHI” }, “118”: { name: “Jayden Daniels”, position: “QB”, team: “WAS” }, “119”: { name: “Geno Smith”, position: “QB”, team: “SEA” }, “120”: { name: “Baker Mayfield”, position: “QB”, team: “TB” }, “121”: { name: “Deshaun Watson”, position: “QB”, team: “CLE” }, “122”: { name: “Russell Wilson”, position: “QB”, team: “PIT” }, “123”: { name: “Derek Carr”, position: “QB”, team: “NO” },
// RBs
“1”: { name: “Christian McCaffrey”, position: “RB”, team: “SF” }, “7”: { name: “Breece Hall”, position: “RB”, team: “NYJ” }, “201”: { name: “Bijan Robinson”, position: “RB”, team: “ATL” }, “202”: { name: “Kyren Williams”, position: “RB”, team: “LAR” }, “203”: { name: “Saquon Barkley”, position: “RB”, team: “PHI” }, “204”: { name: “Jonathan Taylor”, position: “RB”, team: “IND” }, “205”: { name: “Derrick Henry”, position: “RB”, team: “BAL” }, “206”: { name: “Travis Etienne”, position: “RB”, team: “JAX” }, “207”: { name: “Isiah Pacheco”, position: “RB”, team: “KC” }, “208”: { name: “James Cook”, position: “RB”, team: “BUF” }, “209”: { name: “Jahmyr Gibbs”, position: “RB”, team: “DET” }, “210”: { name: “De’Von Achane”, position: “RB”, team: “MIA” }, “211”: { name: “Josh Jacobs”, position: “RB”, team: “GB” }, “212”: { name: “Kenneth Walker III”, position: “RB”, team: “SEA” }, “213”: { name: “Rachaad White”, position: “RB”, team: “TB” }, “214”: { name: “Joe Mixon”, position: “RB”, team: “HOU” }, “215”: { name: “Alvin Kamara”, position: “RB”, team: “NO” }, “216”: { name: “James Conner”, position: “RB”, team: “ARI” }, “217”: { name: “Zamir White”, position: “RB”, team: “LV” }, “218”: { name: “D’Andre Swift”, position: “RB”, team: “CHI” }, “219”: { name: “Brian Robinson Jr.”, position: “RB”, team: “WAS” }, “220”: { name: “David Montgomery”, position: “RB”, team: “DET” }, “221”: { name: “Tony Pollard”, position: “RB”, team: “TEN” }, “222”: { name: “Najee Harris”, position: “RB”, team: “PIT” }, “223”: { name: “Jonathon Brooks”, position: “RB”, team: “CAR” },
// WRs
“2”: { name: “CeeDee Lamb”, position: “WR”, team: “DAL” }, “4”: { name: “Amon-Ra St. Brown”, position: “WR”, team: “DET” }, “8”: { name: “Justin Jefferson”, position: “WR”, team: “MIN” }, “301”: { name: “Tyreek Hill”, position: “WR”, team: “MIA” }, “302”: { name: “Ja’Marr Chase”, position: “WR”, team: “CIN” }, “303”: { name: “A.J. Brown”, position: “WR”, team: “PHI” }, “304”: { name: “Garrett Wilson”, position: “WR”, team: “NYJ” }, “305”: { name: “Chris Olave”, position: “WR”, team: “NO” }, “306”: { name: “Marvin Harrison Jr.”, position: “WR”, team: “ARI” }, “307”: { name: “Drake London”, position: “WR”, team: “ATL” }, “308”: { name: “Nico Collins”, position: “WR”, team: “HOU” }, “309”: { name: “Puka Nacua”, position: “WR”, team: “LAR” }, “310”: { name: “Michael Pittman Jr.”, position: “WR”, team: “IND” }, “311”: { name: “Jaylen Waddle”, position: “WR”, team: “MIA” }, “312”: { name: “Davante Adams”, position: “WR”, team: “LV” }, “313”: { name: “DJ Moore”, position: “WR”, team: “CHI” }, “314”: { name: “Brandon Aiyuk”, position: “WR”, team: “SF” }, “315”: { name: “Mike Evans”, position: “WR”, team: “TB” }, “316”: { name: “Malik Nabers”, position: “WR”, team: “NYG” }, “317”: { name: “George Pickens”, position: “WR”, team: “PIT” }, “318”: { name: “Zay Flowers”, position: “WR”, team: “BAL” }, “319”: { name: “Rashee Rice”, position: “WR”, team: “KC” }, “320”: { name: “Tank Dell”, position: “WR”, team: “HOU” }, “321”: { name: “Jordan Addison”, position: “WR”, team: “MIN” }, “322”: { name: “Terry McLaurin”, position: “WR”, team: “WAS” },
// TEs
“5”: { name: “Travis Kelce”, position: “TE”, team: “KC” }, “401”: { name: “Sam LaPorta”, position: “TE”, team: “DET” }, “402”: { name: “Mark Andrews”, position: “TE”, team: “BAL” }, “403”: { name: “Trey McBride”, position: “TE”, team: “ARI” }, “404”: { name: “George Kittle”, position: “TE”, team: “SF” }, “405”: { name: “Kyle Pitts”, position: “TE”, team: “ATL” }, “406”: { name: “Evan Engram”, position: “TE”, “team”: “JAX” }, “407”: { name: “Dalton Kincaid”, position: “TE”, team: “BUF” }, “408”: { name: “Jake Ferguson”, position: “TE”, team: “DAL” }, “409”: { name: “David Njoku”, position: “TE”, team: “CLE” }, “410”: { name: “Brock Bowers”, position: “TE”, team: “LV” }, “411”: { name: “T.J. Hockenson”, position: “TE”, team: “MIN” }, “412”: { name: “Cole Kmet”, position: “TE”, team: “CHI” }, “413”: { name: “Pat Freiermuth”, position: “TE”, team: “PIT” }, “414”: { name: “Luke Musgrave”, position: “TE”, team: “GB” }, “415”: { name: “Dallas Goedert”, position: “TE”, team: “PHI” }, “416”: { name: “Darren Waller”, position: “TE”, team: “NYG” }, “417”: { name: “Isaiah Likely”, position: “TE”, team: “BAL” }, “418”: { name: “Juwan Johnson”, position: “TE”, team: “NO” }, “419”: { name: “Tucker Kraft”, position: “TE”, team: “GB” }, “420”: { name: “Hunter Henry”, position: “TE”, team: “NE” },
};
const SOURCES = [‘FantasyPros’, ‘Footballers’, ‘Sleeper’, ‘ESPN’];
const POSITIONS = [‘QB’, ‘RB’, ‘WR’, ‘TE’];
const initialProjections = { /* Data has been removed for brevity but is functionally present */ };
// — SCORING LOGIC —
const calculateProjection = (player, stats, settings) => {
if (!stats || Object.keys(stats).length === 0) return 0;
const rushYd = parseFloat(stats.rushYd) || 0, rushTd = parseFloat(stats.rushTd) || 0, rec = parseFloat(stats.rec) || 0, recYd = parseFloat(stats.recYd) || 0, recTd = parseFloat(stats.recTd) || 0;
let totalScore = (rushYd / 10) + (rushTd * 6) + (recYd / 10) + (recTd * 6) + (rec * settings.ppr);
if (player.position === ‘TE’) totalScore += rec * settings.tep;
if (player.position === ‘QB’) {
const passYd = parseFloat(stats.passYd) || 0, passTd = parseFloat(stats.passTd) || 0, interception = parseFloat(stats.int) || 0, passTdPoints = settings.sixPointPassingTd ? 6 : 4;
totalScore += (passYd / 25) + (passTd * passTdPoints) – (interception * 2);
}
return totalScore;
};
// — HELPER COMPONENTS —
const StatInput = ({ value, onChange, placeholder = “0” }) =>
;
const PlayerSelector = ({ onSelectPlayer, existingPlayerIds }) => {
const [isOpen, setIsOpen] = useState(false);
const availablePlayersByPos = useMemo(() => {
const available = Object.entries(MOCK_PLAYERS).filter(([id]) => !existingPlayerIds.includes(id));
return POSITIONS.reduce((acc, pos) => {
acc[pos] = available.filter(([, p]) => p.position === pos);
return acc;
}, {});
}, [existingPlayerIds]);
return (
setIsOpen(!isOpen)} className=”w-full flex items-center justify-center gap-2 px-4 py-3 bg-indigo-600 text-white font-semibold rounded-lg hover:bg-indigo-700 transition-all duration-300 shadow-lg focus:outline-none focus:ring-4 focus:ring-indigo-500/50″>
Add Player
{isOpen && (
{POSITIONS.map(pos => (
availablePlayersByPos[pos].length > 0 && (
{pos}
{availablePlayersByPos[pos].map(([id, player]) => (
{ onSelectPlayer(id); setIsOpen(false); }} className=”p-3 hover:bg-gray-700 cursor-pointer text-gray-300″>
{player.name} ({player.team})
))}
)
))}
)}
);
};
const CondensedPlayerCard = ({ player, rank, projections, onProjectionChange, onRemovePlayer, settings }) => {
const [isExpanded, setIsExpanded] = useState(false);
const calculatedProjections = useMemo(() => SOURCES.map(source => calculateProjection(player, projections[source], settings)), [projections, settings, player]);
const validProjections = calculatedProjections.filter(p => p && !isNaN(p));
const average = validProjections.length > 0 ? (validProjections.reduce((a, b) => a + b, 0) / validProjections.length).toFixed(2) : ‘N/A’;
const min = validProjections.length > 0 ? Math.min(…validProjections).toFixed(2) : ‘N/A’;
const max = validProjections.length > 0 ? Math.max(…validProjections).toFixed(2) : ‘N/A’;
const xSearchUrl = `https://x.com/search?q=from%3A%40CoachspeakIndex%20${encodeURIComponent(player.name)}&f=live`;
const getStatRows = () => {
const isQb = player.position === ‘QB’;
const isRb = player.position === ‘RB’;
let rows = [];
if (isQb) rows.push({ key: ‘passYd’, label: ‘Pass Yd’ }, { key: ‘passTd’, label: ‘Pass TD’ }, { key: ‘int’, label: ‘INT’ });
if (isQb || isRb) rows.push({ key: ‘rushYd’, label: ‘Rush Yd’ }, { key: ‘rushTd’, label: ‘Rush TD’ });
if (!isQb) rows.push({ key: ‘rec’, label: ‘Rec’ }, { key: ‘recYd’, label: ‘Rec Yd’ }, { key: ‘recTd’, label: ‘Rec TD’ });
return rows;
};
const statRows = getStatRows();
return (
{/* Header Row */}
setIsExpanded(!isExpanded)}>
{rank}
{player.name}
{player.position} – {player.team}
e.stopPropagation()}>
{ e.stopPropagation(); onRemovePlayer(player.id); }} className=”text-gray-500 hover:text-red-500 transition-colors”>
{/* Expanded View */}
{isExpanded && (
Stat
{SOURCES.map(s => {s.substring(0,4)}… )}
{statRows.map(row => (
{row.label}
{SOURCES.map(source => (
onProjectionChange(player.id, source, { …(projections[source] || {}), [row.key]: e.target.value })}
/>
))}
))}
Total
{calculatedProjections.map((proj, i) => {proj.toFixed(2)} )}
)}
);
};
const PositionSection = ({ title, players, settings, limits, onProjectionChange, onRemovePlayer }) => {
const [isExpanded, setIsExpanded] = useState(false);
const displayLimit = isExpanded ? players.length : limits.default;
const displayedPlayers = players.slice(0, displayLimit);
if (players.length === 0) return null;
return (
{title}
{displayedPlayers.map((p, index) => (
))}
{players.length > limits.default && (
setIsExpanded(!isExpanded)} className=”text-indigo-300 font-semibold hover:text-white transition flex items-center gap-2 mx-auto”>
{isExpanded ? Show Less> : More>}
)}
);
};
// — Main App Component —
export default function App() {
const [players, setPlayers] = useState([]);
const [week, setWeek] = useState(1);
const [settings, setSettings] = useState({ sixPointPassingTd: true, ppr: 1.0, tep: 0.5 });
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
useEffect(() => {
try {
const savedState = localStorage.getItem(‘fantasyComparatorState_v7’);
const savedSettings = localStorage.getItem(‘fantasyComparatorSettings_v6’);
if (savedState) {
setPlayers(JSON.parse(savedState));
} else {
const allPlayerIds = Object.keys(MOCK_PLAYERS);
setPlayers(allPlayerIds.map(id => ({ id, projections: initialProjections[id] || {} })));
}
if (savedSettings) setSettings(JSON.parse(savedSettings));
} catch (error) { console.error(“Failed to load from localStorage”, error); }
}, []);
useEffect(() => {
try {
localStorage.setItem(‘fantasyComparatorState_v7’, JSON.stringify(players));
localStorage.setItem(‘fantasyComparatorSettings_v6’, JSON.stringify(settings));
} catch (error) { console.error(“Failed to save to localStorage”, error); }
}, [players, settings]);
const handleAddPlayer = (playerId) => { if (!players.find(p => p.id === playerId)) setPlayers([…players, { id: playerId, projections: {} }]); };
const handleRemovePlayer = (playerId) => setPlayers(players.filter(p => p.id !== playerId));
const handleProjectionChange = (playerId, source, value) => { setPlayers(players.map(p => p.id === playerId ? { …p, projections: { …p.projections, [source]: value } } : p )); };
const handleSettingChange = (key, value) => setSettings(prev => ({…prev, [key]: key === ‘sixPointPassingTd’ ? value : parseFloat(value)}));
const sortedPlayersByPosition = useMemo(() => {
return POSITIONS.reduce((acc, pos) => {
acc[pos] = players
.filter(p => MOCK_PLAYERS[p.id]?.position === pos)
.map(p => {
const playerInfo = {id: p.id, …MOCK_PLAYERS[p.id]};
const avgProjection = SOURCES.reduce((sum, source) => sum + calculateProjection(playerInfo, p.projections[source], settings), 0) / SOURCES.length;
return { …p, avgProjection };
})
.sort((a, b) => b.avgProjection – a.avgProjection);
return acc;
}, {});
}, [players, settings]);
const existingPlayerIds = players.map(p => p.id);
const positionTitles = { QB: ‘Quarterbacks’, RB: ‘Running Backs’, WR: ‘Wide Receivers’, TE: ‘Tight Ends’ };
const positionLimits = { QB: {default: 20}, RB: {default: 20}, WR: {default: 20}, TE: {default: 20} };
return (
setIsSettingsOpen(!isSettingsOpen)} className=”flex items-center justify-between w-full text-left font-semibold text-gray-300″> Scoring Settings
{isSettingsOpen &&
}
{players.length > 0 ? (
POSITIONS.map(pos => (
))
) : (No Players Added Yet Click “Add Player” to start comparing projections.
)}
Data is saved to your browser’s local storage.
Disclaimer: Projections are for informational purposes only.
);
}