Skip to content

Instantly share code, notes, and snippets.

@fhk
Created November 21, 2025 17:22
Show Gist options
  • Select an option

  • Save fhk/ff0d13719203c22ee08da13e7b184b5b to your computer and use it in GitHub Desktop.

Select an option

Save fhk/ff0d13719203c22ee08da13e7b184b5b to your computer and use it in GitHub Desktop.
broadband calculator
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Fiber Construction Cost Calculator</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Inter', sans-serif;
}
.card {
background-color: white;
border-radius: 0.75rem;
padding: 2rem;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease-in-out;
}
.card:hover {
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
.result-card {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
}
.btn-primary {
background-color: #4f46e5;
color: white;
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
font-weight: 500;
transition: background-color 0.3s ease;
}
.btn-primary:hover {
background-color: #4338ca;
}
input, select {
border-radius: 0.5rem;
border: 1px solid #d1d5db;
padding: 0.5rem 0.75rem;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 20px;
height: 20px;
background: #4f46e5;
cursor: pointer;
border-radius: 50%;
}
input[type="range"]::-moz-range-thumb {
width: 20px;
height: 20px;
background: #4f46e5;
cursor: pointer;
border-radius: 50%;
}
</style>
</head>
<body class="bg-gray-100 min-h-screen flex items-center justify-center p-4">
<div class="max-w-4xl w-full grid grid-cols-1 md:grid-cols-2 gap-8">
<!-- Input Card -->
<div class="card">
<h1 class="text-2xl font-bold text-gray-800 mb-6 text-center">Cost Estimator</h1>
<div class="space-y-6">
<!-- State Selection -->
<div>
<label for="state" class="block text-sm font-medium text-gray-700 mb-1">State</label>
<select id="state" class="w-full">
<!-- Options will be populated by JS -->
</select>
</div>
<!-- Total Miles -->
<div>
<label for="miles" class="block text-sm font-medium text-gray-700 mb-1">Total Miles to Build</label>
<input type="number" id="miles" value="100" min="1" class="w-full">
</div>
<!-- Aerial Percentage Slider -->
<div>
<label for="aerialPct" class="block text-sm font-medium text-gray-700 mb-1">
Aerial Construction Percentage: <span id="aerialPctValue" class="font-bold text-indigo-600">60%</span>
</label>
<input type="range" id="aerialPct" min="0" max="100" value="60" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
</div>
<!-- Aerial Cost Slider -->
<div>
<label for="aerialCost" class="block text-sm font-medium text-gray-700 mb-1">
Aerial Cost / Mile: <span id="aerialCostValue" class="font-bold text-indigo-600">$0</span>
</label>
<input type="range" id="aerialCost" min="20000" max="100000" step="1000" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
</div>
<!-- Underground Cost Slider -->
<div>
<label for="undergroundCost" class="block text-sm font-medium text-gray-700 mb-1">
Underground Cost / Mile: <span id="undergroundCostValue" class="font-bold text-indigo-600">$0</span>
</label>
<input type="range" id="undergroundCost" min="60000" max="200000" step="1000" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
</div>
</div>
</div>
<!-- Results Card -->
<div id="resultsCard" class="card result-card hidden flex flex-col justify-center">
<h2 class="text-2xl font-bold text-gray-800 mb-6 text-center">Estimated Project Cost</h2>
<div class="space-y-4">
<div id="totalCost" class="text-center text-4xl font-extrabold text-indigo-700 py-4">
$0.00
</div>
<div class="border-t border-gray-300 pt-4 space-y-3 text-gray-600">
<p class="flex justify-between"><span>Selected State:</span> <strong id="resultState" class="text-gray-800">N/A</strong></p>
<p class="flex justify-between"><span>Aerial Cost/Mile:</span> <strong id="resultAerialCost" class="text-gray-800">$0</strong></p>
<p class="flex justify-between"><span>Underground Cost/Mile:</span> <strong id="resultUndergroundCost" class="text-gray-800">$0</strong></p>
<p class="flex justify-between"><span>Aerial Miles:</span> <strong id="resultAerialMiles" class="text-gray-800">0 mi</strong></p>
<p class="flex justify-between"><span>Underground Miles:</span> <strong id="resultUndergroundMiles" class="text-gray-800">0 mi</strong></p>
</div>
</div>
</div>
</div>
<script>
const stateConstructionCosts = {
'AK': { aerial: 75000, underground: 180000, aerialPct: 40, avgMix: 140000 }, 'HI': { aerial: 72000, underground: 170000, aerialPct: 35, avgMix: 135000 }, 'CA': { aerial: 55000, underground: 115000, aerialPct: 45, avgMix: 88000 }, 'NY': { aerial: 58000, underground: 125000, aerialPct: 40, avgMix: 95000 }, 'NJ': { aerial: 56000, underground: 120000, aerialPct: 38, avgMix: 93000 }, 'CT': { aerial: 54000, underground: 118000, aerialPct: 42, avgMix: 91000 }, 'MA': { aerial: 55000, underground: 120000, aerialPct: 40, avgMix: 92000 }, 'RI': { aerial: 53000, underground: 115000, aerialPct: 41, avgMix: 88000 }, 'NH': { aerial: 50000, underground: 105000, aerialPct: 55, avgMix: 80000 }, 'VT': { aerial: 48000, underground: 100000, aerialPct: 60, avgMix: 75000 }, 'ME': { aerial: 47000, underground: 98000, aerialPct: 62, avgMix: 73000 }, 'CO': { aerial: 48000, underground: 105000, aerialPct: 52, avgMix: 78000 }, 'WY': { aerial: 45000, underground: 95000, aerialPct: 65, avgMix: 69000 }, 'MT': { aerial: 46000, underground: 98000, aerialPct: 68, avgMix: 70000 }, 'ID': { aerial: 44000, underground: 92000, aerialPct: 60, avgMix: 70000 }, 'UT': { aerial: 45000, underground: 96000, aerialPct: 55, avgMix: 72000 }, 'NV': { aerial: 47000, underground: 100000, aerialPct: 50, avgMix: 75000 }, 'AZ': { aerial: 46000, underground: 98000, aerialPct: 48, avgMix: 76000 }, 'NM': { aerial: 42000, underground: 88000, aerialPct: 58, avgMix: 67000 }, 'WA': { aerial: 52000, underground: 110000, aerialPct: 48, avgMix: 82000 }, 'OR': { aerial: 50000, underground: 105000, aerialPct: 52, avgMix: 78000 }, 'PA': { aerial: 48000, underground: 100000, aerialPct: 50, avgMix: 76000 }, 'MD': { aerial: 50000, underground: 108000, aerialPct: 45, avgMix: 82000 }, 'DE': { aerial: 49000, underground: 105000, aerialPct: 46, avgMix: 80000 }, 'VA': { aerial: 46000, underground: 98000, aerialPct: 52, avgMix: 74000 }, 'WV': { aerial: 43000, underground: 90000, aerialPct: 65, avgMix: 65000 }, 'MI': { aerial: 44000, underground: 92000, aerialPct: 55, avgMix: 70000 }, 'OH': { aerial: 43000, underground: 90000, aerialPct: 56, avgMix: 68000 }, 'IN': { aerial: 41000, underground: 86000, aerialPct: 60, avgMix: 64000 }, 'IL': { aerial: 46000, underground: 96000, aerialPct: 48, avgMix: 73000 }, 'WI': { aerial: 42000, underground: 88000, aerialPct: 58, avgMix: 66000 }, 'MN': { aerial: 43000, underground: 90000, aerialPct: 55, avgMix: 68000 }, 'ND': { aerial: 40000, underground: 82000, aerialPct: 68, avgMix: 59000 }, 'SD': { aerial: 39000, underground: 80000, aerialPct: 70, avgMix: 57000 }, 'NE': { aerial: 38000, underground: 78000, aerialPct: 72, avgMix: 55000 }, 'KS': { aerial: 37000, underground: 76000, aerialPct: 73, avgMix: 54000 }, 'IA': { aerial: 38000, underground: 78000, aerialPct: 70, avgMix: 56000 }, 'MO': { aerial: 39000, underground: 80000, aerialPct: 68, avgMix: 58000 }, 'OK': { aerial: 36000, underground: 74000, aerialPct: 75, avgMix: 52000 }, 'TX': { aerial: 40000, underground: 82000, aerialPct: 65, avgMix: 59000 }, 'LA': { aerial: 38000, underground: 80000, aerialPct: 62, avgMix: 59000 }, 'AR': { aerial: 36000, underground: 74000, aerialPct: 72, avgMix: 53000 }, 'MS': { aerial: 34000, underground: 70000, aerialPct: 75, avgMix: 49000 }, 'AL': { aerial: 35000, underground: 72000, aerialPct: 73, avgMix: 51000 }, 'TN': { aerial: 37000, underground: 76000, aerialPct: 70, avgMix: 54000 }, 'KY': { aerial: 38000, underground: 78000, aerialPct: 68, avgMix: 56000 }, 'GA': { aerial: 38000, underground: 80000, aerialPct: 65, avgMix: 58000 }, 'FL': { aerial: 42000, underground: 88000, aerialPct: 58, avgMix: 64000 }, 'SC': { aerial: 36000, underground: 75000, aerialPct: 70, avgMix: 54000 }, 'NC': { aerial: 38000, underground: 80000, aerialPct: 66, avgMix: 57000 }, 'DEFAULT': { aerial: 40000, underground: 85000, aerialPct: 60, avgMix: 63000 }
};
// --- Element References ---
const stateSelect = document.getElementById('state');
const milesInput = document.getElementById('miles');
const aerialPctSlider = document.getElementById('aerialPct');
const aerialPctValue = document.getElementById('aerialPctValue');
const aerialCostSlider = document.getElementById('aerialCost');
const aerialCostValue = document.getElementById('aerialCostValue');
const undergroundCostSlider = document.getElementById('undergroundCost');
const undergroundCostValue = document.getElementById('undergroundCostValue');
const resultsCard = document.getElementById('resultsCard');
const totalCostEl = document.getElementById('totalCost');
const resultState = document.getElementById('resultState');
const resultAerialCost = document.getElementById('resultAerialCost');
const resultUndergroundCost = document.getElementById('resultUndergroundCost');
const resultAerialMiles = document.getElementById('resultAerialMiles');
const resultUndergroundMiles = document.getElementById('resultUndergroundMiles');
// --- Event Listeners Setup ---
function setupEventListeners() {
window.addEventListener('DOMContentLoaded', () => {
const states = Object.keys(stateConstructionCosts).filter(key => key !== 'DEFAULT').sort();
states.forEach(state => {
const option = new Option(state, state);
stateSelect.add(option);
});
stateSelect.value = 'CA';
updateInputsBasedOnState();
calculateCost();
});
stateSelect.addEventListener('change', () => {
updateInputsBasedOnState();
calculateCost();
});
milesInput.addEventListener('input', calculateCost);
aerialPctSlider.addEventListener('input', () => {
aerialPctValue.textContent = `${aerialPctSlider.value}%`;
calculateCost();
});
aerialCostSlider.addEventListener('input', () => {
aerialCostValue.textContent = formatCurrency(aerialCostSlider.value);
calculateCost();
});
undergroundCostSlider.addEventListener('input', () => {
undergroundCostValue.textContent = formatCurrency(undergroundCostSlider.value);
calculateCost();
});
}
// --- Functions ---
function updateInputsBasedOnState() {
const stateData = stateConstructionCosts[stateSelect.value] || stateConstructionCosts['DEFAULT'];
aerialPctSlider.value = stateData.aerialPct;
aerialPctValue.textContent = `${stateData.aerialPct}%`;
aerialCostSlider.value = stateData.aerial;
aerialCostValue.textContent = formatCurrency(stateData.aerial);
undergroundCostSlider.value = stateData.underground;
undergroundCostValue.textContent = formatCurrency(stateData.underground);
}
// Formats currency for slider values (no cents)
function formatCurrency(amount) {
const numericAmount = parseFloat(amount);
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 0, maximumFractionDigits: 0 }).format(numericAmount);
}
// Formats currency for final total (with cents)
function formatCurrencyWithCents(amount) {
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(amount);
}
function calculateCost() {
const miles = parseFloat(milesInput.value) || 0;
if (miles <= 0) {
resultsCard.classList.add('hidden');
return;
}
const aerialPct = parseInt(aerialPctSlider.value, 10);
const aerialCostPerMile = parseFloat(aerialCostSlider.value);
const undergroundCostPerMile = parseFloat(undergroundCostSlider.value);
const aerialMiles = miles * (aerialPct / 100);
const undergroundMiles = miles * (100 - aerialPct) / 100;
const totalAerialCost = aerialMiles * aerialCostPerMile;
const totalUndergroundCost = undergroundMiles * undergroundCostPerMile;
const grandTotal = totalAerialCost + totalUndergroundCost;
resultsCard.classList.remove('hidden');
totalCostEl.textContent = formatCurrencyWithCents(grandTotal);
resultState.textContent = stateSelect.value;
resultAerialCost.textContent = formatCurrency(aerialCostPerMile);
resultUndergroundCost.textContent = formatCurrency(undergroundCostPerMile);
resultAerialMiles.textContent = `${aerialMiles.toFixed(1)} mi`;
resultUndergroundMiles.textContent = `${undergroundMiles.toFixed(1)} mi`;
}
// Initialize the app
setupEventListeners();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment