Revise defaults and UI for baseball cost estimator

- Updated initial values for teams, games per team, umpire costs, and team size to reflect a smaller league.
- Expanded fixed cost list with items such as "Umpire Admin", "Digital", and "Trophies".
- Simplified field configuration to a single field covering 100% of games.
- Removed redundant background and text color utility classes for a cleaner and more uniform UI.
- Added derived metrics in the Summary section for improved clarity and budgeting transparency.
This commit is contained in:
2026-01-06 21:22:46 -06:00
parent 1f766846b0
commit 0269fa854a
2 changed files with 46 additions and 61 deletions

View File

@@ -98,7 +98,7 @@ function NumberSlider({
value={Number.isFinite(value) ? value : 0} value={Number.isFinite(value) ? value : 0}
onChange={(e) => onChange(clamp(parseFloat(e.target.value || "0"), min, max))} onChange={(e) => onChange(clamp(parseFloat(e.target.value || "0"), min, max))}
/> />
{help && <p className="text-xs text-gray-500">{help}</p>} {help && <p className="text-xs ">{help}</p>}
</div> </div>
); );
} }
@@ -118,17 +118,21 @@ interface PlayoffRound {
export default function BaseballLeagueCostEstimator() { export default function BaseballLeagueCostEstimator() {
const [fixedCosts, setFixedCosts] = useState<Array<{ id: string; name: string; amount: number }>>([ const [fixedCosts, setFixedCosts] = useState<Array<{ id: string; name: string; amount: number }>>([
{ id: crypto.randomUUID(), name: "Insurance", amount: 1500 }, { id: crypto.randomUUID(), name: "Insurance", amount: 1500 },
{ id: crypto.randomUUID(), name: "Commissioner", amount: 2000 }, { id: crypto.randomUUID(), name: "Commissioner", amount: 0 },
{ id: crypto.randomUUID(), name: "Umpire Admin", amount: 2000 },
{ id: crypto.randomUUID(), name: "Digital", amount: 500 },
{ id: crypto.randomUUID(), name: "Reserve", amount: 3000 },
{ id: crypto.randomUUID(), name: "Trophies", amount: 500 },
]); ]);
const [teams, setTeams] = useState(12); const [teams, setTeams] = useState(5);
const [gamesPerTeam, setGamesPerTeam] = useState(10); const [gamesPerTeam, setGamesPerTeam] = useState(28);
const [umpiresPerGame, setUmpiresPerGame] = useState(2); const [umpiresPerGame, setUmpiresPerGame] = useState(1);
const [costPerUmpPerGame, setCostPerUmpPerGame] = useState(70); const [costPerUmpPerGame, setCostPerUmpPerGame] = useState(90);
const [avgTeamSize, setAvgTeamSize] = useState(12); const [avgTeamSize, setAvgTeamSize] = useState(22);
const [ballsPerGame, setBallsPerGame] = useState(4); const [ballsPerGame, setBallsPerGame] = useState(5);
const [costPerDozenBalls, setCostPerDozenBalls] = useState(60); const [costPerDozenBalls, setCostPerDozenBalls] = useState(80);
const [startMonth, setStartMonth] = useState(4); const [startMonth, setStartMonth] = useState(4);
const [startWeek, setStartWeek] = useState(1); const [startWeek, setStartWeek] = useState(1);
@@ -136,8 +140,7 @@ export default function BaseballLeagueCostEstimator() {
const [endWeek, setEndWeek] = useState(4); const [endWeek, setEndWeek] = useState(4);
const [fields, setFields] = useState<FieldRow[]>([ const [fields, setFields] = useState<FieldRow[]>([
{ id: crypto.randomUUID(), name: "Main Park #1", pct: 60, costPerGame: 40 }, { id: crypto.randomUUID(), name: "Field #1", pct: 100, costPerGame: 212 },
{ id: crypto.randomUUID(), name: "Riverside #2", pct: 40, costPerGame: 25 },
]); ]);
const newRound = (i: number): PlayoffRound => ({ const newRound = (i: number): PlayoffRound => ({
@@ -149,8 +152,7 @@ export default function BaseballLeagueCostEstimator() {
ballsPerGame: 5, ballsPerGame: 5,
costPerDozenBalls: 70, costPerDozenBalls: 70,
fields: [ fields: [
{ id: crypto.randomUUID(), name: "Stadium A", pct: 70, costPerGame: 100 }, { id: crypto.randomUUID(), name: "Field #1", pct: 100, costPerGame: 0 },
{ id: crypto.randomUUID(), name: "Stadium B", pct: 30, costPerGame: 80 },
], ],
}); });
const [playoffRounds, setPlayoffRounds] = useState<PlayoffRound[]>([newRound(1)]); const [playoffRounds, setPlayoffRounds] = useState<PlayoffRound[]>([newRound(1)]);
@@ -444,14 +446,14 @@ export default function BaseballLeagueCostEstimator() {
if (f) importFromFile(f); if (f) importFromFile(f);
}} }}
/> />
<button onClick={onClickImport} className="rounded-xl border hover:bg-gray-50">Import</button> <button onClick={onClickImport} className="rounded-xl border bg-blue-600 shadow hover:bg-blue-700">Import</button>
<button onClick={exportJSON} className="rounded-xl bg-blue-600 text-white shadow hover:bg-blue-700">Export</button> <button onClick={exportJSON} className="rounded-xl bg-blue-600 shadow hover:bg-blue-700">Export</button>
</div> </div>
</header> </header>
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3"> <div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
<section className="lg:col-span-2 space-y-6"> <section className="lg:col-span-2 space-y-6">
<div className="rounded-2xl border bg-white p-5 shadow-sm"> <div className="rounded-2xl border p-5 shadow-sm">
<h2 className="mb-4 text-lg font-semibold">League Basics</h2> <h2 className="mb-4 text-lg font-semibold">League Basics</h2>
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2"> <div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
<NumberSlider label="Number of teams" value={teams} onChange={setTeams} min={0} max={50} /> <NumberSlider label="Number of teams" value={teams} onChange={setTeams} min={0} max={50} />
@@ -475,7 +477,7 @@ export default function BaseballLeagueCostEstimator() {
<NumberSlider label="Cost of baseballs per dozen ($)" value={costPerDozenBalls} onChange={setCostPerDozenBalls} min={0} max={200} step={1} help={`${currency(perBallCost)} per ball • ${currency(baseballsCostPerGame)} per game`} /> <NumberSlider label="Cost of baseballs per dozen ($)" value={costPerDozenBalls} onChange={setCostPerDozenBalls} min={0} max={200} step={1} help={`${currency(perBallCost)} per ball • ${currency(baseballsCostPerGame)} per game`} />
</div> </div>
<div className="mt-4 rounded-xl border bg-gray-50 p-3 text-sm"> <div className="mt-4 rounded-xl border p-3 text-sm">
<div className="flex flex-wrap gap-x-6 gap-y-1"> <div className="flex flex-wrap gap-x-6 gap-y-1">
<div>Opponents per team: <span className="font-medium">{opponentsPerTeam}</span></div> <div>Opponents per team: <span className="font-medium">{opponentsPerTeam}</span></div>
<div>Games per opponent (avg): <span className="font-medium">{gamesPerOpponentExact.toFixed(2)}</span></div> <div>Games per opponent (avg): <span className="font-medium">{gamesPerOpponentExact.toFixed(2)}</span></div>
@@ -485,11 +487,11 @@ export default function BaseballLeagueCostEstimator() {
Distribution (as even as possible): each team plays <span className="font-medium">{remainderOpponents}</span> opponents <span className="font-medium">{gamesPerOpponentFloor + 1}×</span> and <span className="font-medium">{opponentsPerTeam - remainderOpponents}</span> opponents <span className="font-medium">{gamesPerOpponentFloor}×</span>. Distribution (as even as possible): each team plays <span className="font-medium">{remainderOpponents}</span> opponents <span className="font-medium">{gamesPerOpponentFloor + 1}×</span> and <span className="font-medium">{opponentsPerTeam - remainderOpponents}</span> opponents <span className="font-medium">{gamesPerOpponentFloor}×</span>.
</div> </div>
)} )}
<div className="mt-1 text-gray-500">Total league games computed as teams × gamesPerTeam ÷ 2.</div> <div className="mt-1 ">Total league games computed as teams × gamesPerTeam ÷ 2.</div>
</div> </div>
</div> </div>
<div className="rounded-2xl border bg-white p-5 shadow-sm"> <div className="rounded-2xl border p-5 shadow-sm">
<h2 className="mb-4 text-lg font-semibold">Season Window</h2> <h2 className="mb-4 text-lg font-semibold">Season Window</h2>
<div className="grid grid-cols-1 items-end gap-4 sm:grid-cols-4"> <div className="grid grid-cols-1 items-end gap-4 sm:grid-cols-4">
<div className="space-y-2"> <div className="space-y-2">
@@ -518,22 +520,22 @@ export default function BaseballLeagueCostEstimator() {
</div> </div>
</div> </div>
<div className="mt-3 grid grid-cols-1 gap-2 text-sm sm:grid-cols-2 lg:grid-cols-4"> <div className="mt-3 grid grid-cols-1 gap-2 text-sm sm:grid-cols-2 lg:grid-cols-4">
<div><span className="text-gray-600">Approx. season length:</span> <span className="font-medium">{approxWeeks} weeks</span></div> <div><span className="">Approx. season length:</span> <span className="font-medium">{approxWeeks} weeks</span></div>
<div><span className="text-gray-600">Games/week per team:</span> <span className="font-medium">{gamesPerWeekPerTeam.toFixed(2)}</span></div> <div><span className="">Games/week per team:</span> <span className="font-medium">{gamesPerWeekPerTeam.toFixed(2)}</span></div>
<div><span className="text-gray-600">Games/week league-wide:</span> <span className="font-medium">{gamesPerWeekLeague.toFixed(2)}</span></div> <div><span className="">Games/week league-wide:</span> <span className="font-medium">{gamesPerWeekLeague.toFixed(2)}</span></div>
<div><span className="text-gray-600">Games vs each opponent/team:</span> <span className="font-medium">{gamesVsEachOther.toFixed(2)}</span></div> <div><span className="">Games vs each opponent/team:</span> <span className="font-medium">{gamesVsEachOther.toFixed(2)}</span></div>
</div> </div>
</div> </div>
<div className="rounded-2xl border bg-white p-5 shadow-sm"> <div className="rounded-2xl border p-5 shadow-sm">
<div className="mb-4 flex items-center justify-between"> <div className="mb-4 flex items-center justify-between">
<h2 className="text-lg font-semibold">Fields & Game Allocation (Regular Season)</h2> <h2 className="text-lg font-semibold">Fields & Game Allocation (Regular Season)</h2>
<button onClick={addField} className="rounded-xl bg-blue-600 text-white shadow hover:bg-blue-700">+ Add field</button> <button onClick={addField} className="rounded-xl bg-blue-600 shadow hover:bg-blue-700">+ Add field</button>
</div> </div>
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full table-auto"> <table className="w-full table-auto">
<thead> <thead>
<tr className="text-left text-sm text-gray-500"> <tr className="text-left text-sm ">
<th className="p-2">Field name</th> <th className="p-2">Field name</th>
<th className="p-2">% of games</th> <th className="p-2">% of games</th>
<th className="p-2">Cost per game ($)</th> <th className="p-2">Cost per game ($)</th>
@@ -570,7 +572,7 @@ export default function BaseballLeagueCostEstimator() {
<div className="rounded-2xl border p-5 shadow-sm"> <div className="rounded-2xl border p-5 shadow-sm">
<div className="mb-4 flex items-center justify-between"> <div className="mb-4 flex items-center justify-between">
<h2 className="text-lg font-semibold">Playoffs</h2> <h2 className="text-lg font-semibold">Playoffs</h2>
<button onClick={addRound} className="rounded-xl bg-purple-600 text-white shadow hover:bg-purple-700">+ Add round</button> <button onClick={addRound} className="rounded-xl bg-purple-600 shadow hover:bg-purple-700">+ Add round</button>
</div> </div>
<div className="space-y-6"> <div className="space-y-6">
@@ -622,7 +624,7 @@ export default function BaseballLeagueCostEstimator() {
<td className="p-2 font-medium">Totals</td> <td className="p-2 font-medium">Totals</td>
<td className="p-2 font-medium">{totalPct}%</td> <td className="p-2 font-medium">{totalPct}%</td>
<td className="p-2 font-medium"></td> <td className="p-2 font-medium"></td>
<td className="p-2 text-right"><button onClick={() => addRoundField(r.id)} className="text-sm text-purple-700 hover:underline">+ Add field</button></td> <td className="p-2 text-right"><button onClick={() => addRoundField(r.id)} className="text-sm hover:underline">+ Add field</button></td>
</tr> </tr>
</tfoot> </tfoot>
</table> </table>
@@ -637,7 +639,7 @@ export default function BaseballLeagueCostEstimator() {
})} })}
</div> </div>
<div className="mt-4 grid grid-cols-1 gap-3 rounded-xl bg-purple-50 p-3 text-sm sm:grid-cols-2 lg:grid-cols-4"> <div className="mt-4 grid grid-cols-1 gap-3 rounded-xl p-3 text-sm sm:grid-cols-2 lg:grid-cols-4">
<div className="flex justify-between"><span>Playoff games (total)</span><span className="font-medium">{playoffTotals.games}</span></div> <div className="flex justify-between"><span>Playoff games (total)</span><span className="font-medium">{playoffTotals.games}</span></div>
<div className="flex justify-between"><span>Playoff field costs</span><span className="font-medium">{currency(playoffTotals.field)}</span></div> <div className="flex justify-between"><span>Playoff field costs</span><span className="font-medium">{currency(playoffTotals.field)}</span></div>
<div className="flex justify-between"><span>Playoff baseballs</span><span className="font-medium">{currency(playoffTotals.balls)}</span></div> <div className="flex justify-between"><span>Playoff baseballs</span><span className="font-medium">{currency(playoffTotals.balls)}</span></div>
@@ -647,10 +649,10 @@ export default function BaseballLeagueCostEstimator() {
</section> </section>
<section className="space-y-6"> <section className="space-y-6">
<div className="rounded-2xl border bg-white p-5 shadow-sm"> <div className="rounded-2xl border p-5 shadow-sm">
<div className="mb-4 flex items-center justify-between"> <div className="mb-4 flex items-center justify-between">
<h2 className="text-lg font-semibold">Fixed Costs</h2> <h2 className="text-lg font-semibold">Fixed Costs</h2>
<button onClick={addFixed} className="rounded-xl bg-blue-600 text-white shadow hover:bg-blue-700">+ Add item</button> <button onClick={addFixed} className="rounded-xl bg-blue-600 shadow hover:bg-blue-700">+ Add item</button>
</div> </div>
<div className="space-y-3"> <div className="space-y-3">
{fixedCosts.map((fc) => ( {fixedCosts.map((fc) => (
@@ -667,8 +669,15 @@ export default function BaseballLeagueCostEstimator() {
</div> </div>
</div> </div>
<div className="rounded-2xl border bg-white p-5 shadow-sm"> <div className="rounded-2xl border p-5 shadow-sm">
<h2 className="mb-4 text-lg font-semibold">Summary</h2> <h2 className="mb-4 text-lg font-semibold">Summary</h2>
<div className="space-y-2 text-sm">
<div className="flex justify-between"><span>Games Per Team</span><span className="font-medium">{gamesPerTeam}</span></div>
<div className="flex justify-between"><span>Total League Games</span><span className="font-medium">{totalGames}</span></div>
<div className="flex justify-between"><span>Average Games/Week/Team</span><span className="font-medium">{gamesPerWeekPerTeam.toFixed(2)}</span></div>
<div className="flex justify-between"><span>League Games/Week</span><span className="font-medium">{gamesPerWeekLeague.toFixed(2)}</span></div>
<div className="flex justify-between flex justify-between border-t pt-2"><span>League Total Games</span><span className="font-medium">{totalGames}</span></div>
</div>
<div className="space-y-2 text-sm"> <div className="space-y-2 text-sm">
<div className="flex justify-between"><span>Umpire costs (regular)</span><span className="font-medium">{currency(umpireCostTotal)}</span></div> <div className="flex justify-between"><span>Umpire costs (regular)</span><span className="font-medium">{currency(umpireCostTotal)}</span></div>
<div className="flex justify-between"><span>Umpire costs (playoffs)</span><span className="font-medium">{currency(playoffUmpireCostTotal)}</span></div> <div className="flex justify-between"><span>Umpire costs (playoffs)</span><span className="font-medium">{currency(playoffUmpireCostTotal)}</span></div>
@@ -680,27 +689,17 @@ export default function BaseballLeagueCostEstimator() {
<div className="flex justify-between border-t pt-2"><span>League costs subtotal</span><span className="font-medium">{currency(leagueCostsTotal)}</span></div> <div className="flex justify-between border-t pt-2"><span>League costs subtotal</span><span className="font-medium">{currency(leagueCostsTotal)}</span></div>
<div className="flex justify-between border-t pt-2 text-base"><span className="font-semibold">Grand total</span><span className="font-bold">{currency(grandTotal)}</span></div> <div className="flex justify-between border-t pt-2 text-base"><span className="font-semibold">Grand total</span><span className="font-bold">{currency(grandTotal)}</span></div>
</div> </div>
<div className="mt-4 space-y-2 rounded-xl bg-gray-50 p-3 text-sm"> <div className="mt-4 space-y-2 rounded-xl p-3 text-sm">
<div className="flex justify-between"><span>Per team to league</span><span className="font-semibold">{currency(costPerTeamLeague)}</span></div> <div className="flex justify-between"><span>Per team to league</span><span className="font-semibold">{currency(costPerTeamLeague)}</span></div>
<div className="flex justify-between"><span>Per team to umpires</span><span className="font-semibold">{currency(costPerTeamUmpires)}</span></div> <div className="flex justify-between"><span>Per team to umpires</span><span className="font-semibold">{currency(costPerTeamUmpires)}</span></div>
<div className="flex justify-between"><span>Per team (total)</span><span className="font-semibold">{currency(costPerTeam)}</span></div> <div className="flex justify-between"><span>Per team (total)</span><span className="font-semibold">{currency(costPerTeam)}</span></div>
<div className="flex justify-between"><span>Per player (avg team size {avgTeamSize})</span><span className="font-semibold">{currency(costPerPlayer)}</span></div> <div className="flex justify-between"><span>Per player (avg team size {avgTeamSize})</span><span className="font-semibold">{currency(costPerPlayer)}</span></div>
</div> </div>
<div className="mt-4 text-xs text-gray-500">
Notes:
<ul className="list-disc pl-5">
<li>Regular-season league games = teams × games per team ÷ 2.</li>
<li>Field cost = total games × % allocation × per-field cost per game (normalized to 100%).</li>
<li>Playoff rounds accept total games per round. Costs are computed per round then summed.</li>
<li>Autosaves to your browser (localStorage). Use Export for backups or sharing.</li>
</ul>
</div>
</div> </div>
</section> </section>
</div> </div>
<footer className="mt-8 text-center text-xs text-gray-500">Built for quick budgeting. Adjust anything and the math updates instantly.</footer> <footer className="mt-8 text-center text-xs ">Built for quick budgeting. Adjust anything and the math updates instantly.</footer>
</div> </div>
); );
} }

View File

@@ -5,8 +5,6 @@
font-weight: 400; font-weight: 400;
color-scheme: light dark; color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none; font-synthesis: none;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
@@ -37,13 +35,14 @@ h1 {
} }
button { button {
@apply text-white;
border-radius: 8px; border-radius: 8px;
border: 1px solid transparent; border: 1px solid transparent;
padding: 0.6em 1.2em; padding: 0.6em 1.2em;
font-size: 1em; font-size: 1em;
font-weight: 500; font-weight: 500;
font-family: inherit; font-family: inherit;
background-color: #1a1a1a;
cursor: pointer; cursor: pointer;
transition: border-color 0.25s; transition: border-color 0.25s;
} }
@@ -53,17 +52,4 @@ button:hover {
button:focus, button:focus,
button:focus-visible { button:focus-visible {
outline: 4px auto -webkit-focus-ring-color; outline: 4px auto -webkit-focus-ring-color;
} }
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}