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:
87
src/App.tsx
87
src/App.tsx
@@ -98,7 +98,7 @@ function NumberSlider({
|
||||
value={Number.isFinite(value) ? value : 0}
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -118,17 +118,21 @@ interface PlayoffRound {
|
||||
export default function BaseballLeagueCostEstimator() {
|
||||
const [fixedCosts, setFixedCosts] = useState<Array<{ id: string; name: string; amount: number }>>([
|
||||
{ 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 [gamesPerTeam, setGamesPerTeam] = useState(10);
|
||||
const [umpiresPerGame, setUmpiresPerGame] = useState(2);
|
||||
const [costPerUmpPerGame, setCostPerUmpPerGame] = useState(70);
|
||||
const [avgTeamSize, setAvgTeamSize] = useState(12);
|
||||
const [teams, setTeams] = useState(5);
|
||||
const [gamesPerTeam, setGamesPerTeam] = useState(28);
|
||||
const [umpiresPerGame, setUmpiresPerGame] = useState(1);
|
||||
const [costPerUmpPerGame, setCostPerUmpPerGame] = useState(90);
|
||||
const [avgTeamSize, setAvgTeamSize] = useState(22);
|
||||
|
||||
const [ballsPerGame, setBallsPerGame] = useState(4);
|
||||
const [costPerDozenBalls, setCostPerDozenBalls] = useState(60);
|
||||
const [ballsPerGame, setBallsPerGame] = useState(5);
|
||||
const [costPerDozenBalls, setCostPerDozenBalls] = useState(80);
|
||||
|
||||
const [startMonth, setStartMonth] = useState(4);
|
||||
const [startWeek, setStartWeek] = useState(1);
|
||||
@@ -136,8 +140,7 @@ export default function BaseballLeagueCostEstimator() {
|
||||
const [endWeek, setEndWeek] = useState(4);
|
||||
|
||||
const [fields, setFields] = useState<FieldRow[]>([
|
||||
{ id: crypto.randomUUID(), name: "Main Park #1", pct: 60, costPerGame: 40 },
|
||||
{ id: crypto.randomUUID(), name: "Riverside #2", pct: 40, costPerGame: 25 },
|
||||
{ id: crypto.randomUUID(), name: "Field #1", pct: 100, costPerGame: 212 },
|
||||
]);
|
||||
|
||||
const newRound = (i: number): PlayoffRound => ({
|
||||
@@ -149,8 +152,7 @@ export default function BaseballLeagueCostEstimator() {
|
||||
ballsPerGame: 5,
|
||||
costPerDozenBalls: 70,
|
||||
fields: [
|
||||
{ id: crypto.randomUUID(), name: "Stadium A", pct: 70, costPerGame: 100 },
|
||||
{ id: crypto.randomUUID(), name: "Stadium B", pct: 30, costPerGame: 80 },
|
||||
{ id: crypto.randomUUID(), name: "Field #1", pct: 100, costPerGame: 0 },
|
||||
],
|
||||
});
|
||||
const [playoffRounds, setPlayoffRounds] = useState<PlayoffRound[]>([newRound(1)]);
|
||||
@@ -444,14 +446,14 @@ export default function BaseballLeagueCostEstimator() {
|
||||
if (f) importFromFile(f);
|
||||
}}
|
||||
/>
|
||||
<button onClick={onClickImport} className="rounded-xl border hover:bg-gray-50">Import</button>
|
||||
<button onClick={exportJSON} className="rounded-xl bg-blue-600 text-white shadow hover:bg-blue-700">Export</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 shadow hover:bg-blue-700">Export</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
||||
<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>
|
||||
<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} />
|
||||
@@ -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`} />
|
||||
</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>Opponents per team: <span className="font-medium">{opponentsPerTeam}</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>.
|
||||
</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 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>
|
||||
<div className="grid grid-cols-1 items-end gap-4 sm:grid-cols-4">
|
||||
<div className="space-y-2">
|
||||
@@ -518,22 +520,22 @@ export default function BaseballLeagueCostEstimator() {
|
||||
</div>
|
||||
</div>
|
||||
<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="text-gray-600">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="text-gray-600">Games vs each opponent/team:</span> <span className="font-medium">{gamesVsEachOther.toFixed(2)}</span></div>
|
||||
<div><span className="">Approx. season length:</span> <span className="font-medium">{approxWeeks} weeks</span></div>
|
||||
<div><span className="">Games/week per team:</span> <span className="font-medium">{gamesPerWeekPerTeam.toFixed(2)}</span></div>
|
||||
<div><span className="">Games/week league-wide:</span> <span className="font-medium">{gamesPerWeekLeague.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 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">
|
||||
<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 className="overflow-x-auto">
|
||||
<table className="w-full table-auto">
|
||||
<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">% of games</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="mb-4 flex items-center justify-between">
|
||||
<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 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">{totalPct}%</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>
|
||||
</tfoot>
|
||||
</table>
|
||||
@@ -637,7 +639,7 @@ export default function BaseballLeagueCostEstimator() {
|
||||
})}
|
||||
</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 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>
|
||||
@@ -647,10 +649,10 @@ export default function BaseballLeagueCostEstimator() {
|
||||
</section>
|
||||
|
||||
<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">
|
||||
<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 className="space-y-3">
|
||||
{fixedCosts.map((fc) => (
|
||||
@@ -667,8 +669,15 @@ export default function BaseballLeagueCostEstimator() {
|
||||
</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>
|
||||
<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="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>
|
||||
@@ -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 text-base"><span className="font-semibold">Grand total</span><span className="font-bold">{currency(grandTotal)}</span></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 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 player (avg team size {avgTeamSize})</span><span className="font-semibold">{currency(costPerPlayer)}</span></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>
|
||||
</section>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user