Created
November 21, 2025 17:22
-
-
Save fhk/ff0d13719203c22ee08da13e7b184b5b to your computer and use it in GitHub Desktop.
broadband calculator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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