Back to all posts

Battery Distance Calculator

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width,initial-scale=1" />
  <title>Golf Cart Range Estimator</title>
  <style>
    /* Base */
    #gc-range-widget * { box-sizing: border-box; }
    #gc-range-widget {
      font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji";
      color:#111827; max-width:940px; margin:24px auto; padding:0 12px;
    }
    #gc-range-widget .gc-card{
      background:#fff; border:1px solid #e5e7eb; border-radius:14px; padding:16px; margin:10px 0 18px;
      box-shadow:0 1px 2px rgba(0,0,0,.04);
    }
    #gc-range-widget h3{ margin:0 0 10px; font-size:1.05rem; line-height:1.2; display:flex; align-items:center; gap:10px; }
    #gc-range-widget .gc-stepnum{
      display:inline-flex; width:28px; height:28px; border-radius:999px; align-items:center; justify-content:center;
      background:#111827; color:#fff; font-weight:700; font-size:.9rem;
    }
    .muted{ color:#6b7280; font-size:.9rem; }

    /* Grid + battery cards */
    .gc-grid{ display:grid; gap:12px; grid-template-columns:repeat(1,minmax(0,1fr)); }
    @media(min-width:640px){ .gc-grid{ grid-template-columns:repeat(2,minmax(0,1fr)); } }
    @media(min-width:1024px){ .gc-grid{ grid-template-columns:repeat(3,minmax(0,1fr)); } }

    .gc-batt-card{
      display:flex; gap:12px; border:1px solid #e5e7eb; border-radius:12px; padding:12px;
      transition:box-shadow .15s, border-color .15s; cursor:pointer; background:#fff;
    }
    .gc-batt-card:hover{ box-shadow:0 2px 8px rgba(0,0,0,.06); border-color:#d1d5db; }
    .gc-batt-media{ width:92px; min-width:92px; height:92px; border-radius:10px; overflow:hidden; background:#f3f4f6; display:flex; align-items:center; justify-content:center; }
    .gc-batt-media img{ width:100%; height:100%; object-fit:cover; display:block; }
    .gc-batt-body{ flex:1; min-width:0; }
    .gc-batt-head{ display:flex; align-items:center; gap:8px; margin-bottom:6px; }
    .gc-batt-name{ font-weight:700; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }

    /* Shared controls / results */
    .gc-flex{ display:flex; gap:10px; align-items:center; flex-wrap:wrap; }
    .cap-opt{ flex:1 1 160px; display:flex; gap:10px; align-items:center; border:1px solid #e5e7eb; border-radius:10px; padding:10px; background:#fff; transition:box-shadow .15s, border-color .15s; }
    select, button{ font:inherit; }
    select{ width:100%; padding:10px 12px; border:1px solid #e5e7eb; border-radius:10px; background:#fff; transition: box-shadow .15s, border-color .15s; }
    input[type="radio"]{ accent-color:#111827; }

    /* Selection highlight */
    .is-selected{
      border-color:#111827 !important;
      box-shadow:0 0 0 3px rgba(17,24,39,.12);
    }
    select.is-selected{
      border-color:#111827 !important;
      box-shadow:0 0 0 2px rgba(17,24,39,.12);
    }

    /* Result */
    .gc-result{ background:#f9fafb; border:1px dashed #e5e7eb; border-radius:14px; padding:16px; text-align:center; }
    .gc-miles{ font-weight:800; font-size:2.2rem; }
    .gc-actions{ display:flex; gap:10px; flex-wrap:wrap; justify-content:center; margin-top:12px; }
    .gc-btn{ border:1px solid #111827; background:#111827; color:#fff; border-radius:999px; padding:10px 16px; cursor:pointer; font-weight:700; }
    .gc-btn.secondary{ background:#fff; color:#111827; }

    /* Big power lines (20% smaller than miles) */
    .gc-metric{ font-weight:800; font-size:1.76rem; line-height:1.15; margin-top:8px; }
  </style>
</head>
<body>
  <div id="gc-range-widget">
    <!-- STEP 1 -->
    <div class="gc-card" id="gc-step1">
      <h3><span class="gc-stepnum">1</span> Choose your battery (photo, voltage & capacity)</h3>
      <div class="muted" style="margin-bottom:8px">Pick your battery below.</div>
      <div class="gc-grid" id="gc-battery-list"></div>
    </div>

    <!-- STEP 2 -->
    <div class="gc-card" id="gc-step2">
      <h3><span class="gc-stepnum">2</span> Golf Cart Size</h3>
      <fieldset aria-labelledby="gc-step2" class="gc-flex">
        <label class="cap-opt"><input type="radio" name="gc-capacity" value="2"> <span><strong>2 Seater</strong></span></label>
        <label class="cap-opt"><input type="radio" name="gc-capacity" value="4"> <span><strong>4 Seater</strong></span></label>
        <label class="cap-opt"><input type="radio" name="gc-capacity" value="6"> <span><strong>6 Seater</strong></span></label>
      </fieldset>
    </div>

    <!-- STEP 3 -->
    <div class="gc-card" id="gc-step3">
      <h3><span class="gc-stepnum">3</span> Total passengers riding (including driver)</h3>
      <select id="gc-passengers" aria-label="Total passengers"></select>
      <div class="muted" id="gc-passenger-hint">Choose capacity in Step 2 to lock max passengers.</div>
    </div>

    <!-- STEP 4 -->
    <div class="gc-card" id="gc-step4">
      <h3><span class="gc-stepnum">4</span> Check all that apply</h3>
      <div class="gc-grid modifiers">
        <label class="cap-opt"><input type="checkbox" id="gc-windshield" data-factor="0.95"> <span><strong>Windshield up</strong><br><span class="muted">Slight aerodynamic penalty</span></span></label>
        <label class="cap-opt"><input type="checkbox" id="gc-windy" data-factor="0.90"> <span><strong>Windy conditions</strong><br><span class="muted">Headwinds reduce efficiency</span></span></label>
        <label class="cap-opt"><input type="checkbox" id="gc-hilly" data-factor="0.80"> <span><strong>Hilly terrain</strong><br><span class="muted">Climbing consumes more energy</span></span></label>
        <label class="cap-opt"><input type="checkbox" id="gc-modified" data-factor="0.85"> <span><strong>Modified motor and/or controller</strong><br><span class="muted">Higher performance often lowers range</span></span></label>
      </div>
    </div>

    <!-- RESULT -->
    <div class="gc-card">
      <div class="gc-result" role="status" aria-live="polite">
        <div class="muted" style="margin-bottom:6px;">Estimated range on a full charge</div>
        <div class="gc-miles" id="gc-miles">β€”</div>

        <!-- Big power lines (20% smaller than miles) -->
        <div class="gc-metric" id="gc-continuous"> </div>
        <div class="gc-metric" id="gc-instant"> </div>

        <!-- Smaller breakdown -->
        <div class="muted" id="gc-breakdown" style="margin-top:6px;">Make selections to see your result.</div>

        <div class="gc-actions">
          <button class="gc-btn" id="gc-calc" type="button">Calculate</button>
          <button class="gc-btn secondary" id="gc-reset" type="button">Reset</button>
        </div>
        <div class="muted" style="margin-top:8px;">
          Estimates only. Real-world range varies with speed, payload, tires, temperature, maintenance, and route.
        </div>
      </div>
    </div>
  </div>

  <script>
    (function(){
      // ---------- CONFIG ----------
      // Set your real A (Amps) specs; kW are not calculated or shown.
      const CONFIG = {
        capacityMultiplier: { 2: 1.00, 4: 0.92, 6: 0.88 },
        perPassengerPenalty: 0.05,
        minMilesFloor: 1,
        roundTo: 1,
        batteries: [
          { id:"modelA", name:"36V 105Ah",
            imageUrl:"https://cdn.shopify.com/s/files/1/0251/0484/2798/files/reduceImage_20250625235400_2.png?v=1755737490",
            voltage:36, capacityAh:105, baseRange:35,
            maxContinuousA:200,  // TODO: replace with official spec
            maxInstantaneousA:700 // TODO: replace with official spec
          },
          { id:"modelB", name:"48V 105Ah MINI",
            imageUrl:"https://cdn.shopify.com/s/files/1/0251/0484/2798/files/redImage_20250625235425_2.png?v=1755737489",
            voltage:48, capacityAh:105, baseRange:50,
            maxContinuousA:200,  // TODO
            maxInstantaneousA:700 // TODO
          },
          { id:"modelC", name:"48V 105Ah",
            imageUrl:"https://cdn.shopify.com/s/files/1/0251/0484/2798/files/Image_20241121224213_small_9bbd4a12-2ac2-4f25-bf61-4659d34d4c92_3.png?v=1755737490",
            voltage:48, capacityAh:105, baseRange:50,
            maxContinuousA:200,  // TODO
            maxInstantaneousA:700 // TODO
          },
          { id:"modelD", name:"48V 160Ah",
            imageUrl:"https://cdn.shopify.com/s/files/1/0251/0484/2798/files/untitled.82_1518fdc3-3507-4f38-a3d9-4f5b5a62c6cb.png?v=1755737490",
            voltage:48, capacityAh:160, baseRange:75,
            maxContinuousA:200,  // TODO
            maxInstantaneousA:700 // TODO
          },
          { id:"modelE", name:"72V 105Ah",
            imageUrl:"https://cdn.shopify.com/s/files/1/0251/0484/2798/files/12412412343124_2.png?v=1755737490",
            voltage:72, capacityAh:105, baseRange:80,
            maxContinuousA:250,  // TODO
            maxInstantaneousA:1000 // TODO
          }
        ]
      };

      // ---------- Utilities ----------
      const $ = (sel, root=document) => root.querySelector(sel);
      const $$ = (sel, root=document) => Array.from(root.querySelectorAll(sel));
      const roundTo = (n, step) => { const s=Math.max(step,1); return Math.round(n/s)*s; };

      // Passenger select
      function fillPassengerOptions(max=6){
        const sel = $('#gc-passengers'); sel.innerHTML = '';
        for(let i=1;i<=max;i++){
          const opt = document.createElement('option');
          opt.value = i; opt.textContent = i + (i===1 ? ' passenger' : ' passengers');
          sel.appendChild(opt);
        }
      }
      fillPassengerOptions(6);

      // Render batteries
      function renderBatteryCards(){
        const holder = $('#gc-battery-list'); holder.innerHTML = '';
        CONFIG.batteries.forEach((batt, idx)=>{
          const card = document.createElement('label');
          card.className = 'gc-batt-card';
          card.setAttribute('data-id', batt.id);

          const radioId = 'gc-batt-' + batt.id;

          card.innerHTML = `
            <div class="gc-batt-media">
              <img src="${batt.imageUrl}" alt="${batt.name}">
            </div>
            <div class="gc-batt-body">
              <div class="gc-batt-head">
                <input type="radio" name="gc-battery" id="${radioId}" value="${batt.id}" ${idx===0 ? 'checked' : ''}>
                <div class="gc-batt-name">${batt.name}</div>
              </div>
              <div class="muted">${batt.voltage}V β€’ ${batt.capacityAh}Ah (base ${batt.baseRange} mi)</div>
            </div>
          `;
          holder.appendChild(card);
        });

        // Recalculate + highlight on battery change
        $$('input[name="gc-battery"]').forEach(r=>{
          r.addEventListener('change', ()=>{
            updateHighlights();
            render();
          });
        });
      }
      renderBatteryCards();

      // Capacity changes clamp passenger select
      $$('#gc-step2 input[name="gc-capacity"]').forEach(r=>{
        r.addEventListener('change', ()=>{
          const cap = parseInt(r.value,10);
          fillPassengerOptions(cap);
          $('#gc-passenger-hint').textContent = `Max set to ${cap} based on your cart capacity.`;
          updateHighlights();
          render();
        });
      });

      // Step 4 (checkboxes) highlight on change
      $$('#gc-step4 input[type="checkbox"]').forEach(cb=>{
        cb.addEventListener('change', ()=>{
          updateHighlights();
          render();
        });
      });

      // Step 3 select highlight on change
      $('#gc-passengers').addEventListener('change', ()=>{
        updateHighlights();
        render();
      });

      function getSelectedBattery(){
        const selectedRadio = $('input[name="gc-battery"]:checked');
        if(!selectedRadio) return null;
        return CONFIG.batteries.find(b=> b.id === selectedRadio.value) || null;
      }

      function getSelectedCapacity(){ const el = $('input[name="gc-capacity"]:checked'); return el ? parseInt(el.value,10) : null; }
      function getPassengers(){ return parseInt($('#gc-passengers').value, 10) || 1; }
      function getModifiers(){
        return $$('#gc-step4 input[type="checkbox"]:checked').map(cb => ({
          id: cb.id,
          factor: parseFloat(cb.dataset.factor || '1'),
          label: cb.closest('label')?.innerText.split('\n')[0].trim() || cb.id
        }));
      }

      function calculate(){
        const battery = getSelectedBattery();
        const capacity = getSelectedCapacity();
        const passengers = getPassengers();
        const modifiers = getModifiers();

        if(!battery){ return { ok:false, message:'Select a battery in Step 1.' }; }
        if(!capacity){ return { ok:false, message:'Select your cart size in Step 2.' }; }

        const p = Math.min(Math.max(1, passengers), capacity);

        let miles = battery.baseRange;
        miles *= (CONFIG.capacityMultiplier[capacity] ?? 1);

        const extra = Math.max(0, p - 1);
        const passengerFactor = Math.max(0, 1 - (CONFIG.perPassengerPenalty * extra));
        miles *= passengerFactor;

        const modsText = [];
        modifiers.forEach(m=>{ miles *= m.factor; modsText.push(m.label); });

        miles = Math.max(CONFIG.minMilesFloor, miles);
        miles = roundTo(miles, CONFIG.roundTo);

        const breakdown = [
          `<strong>Battery:</strong> ${battery.name} β€” ${battery.voltage}V / ${battery.capacityAh}Ah (base ${battery.baseRange} mi)`,
          `<strong>Capacity:</strong> ${capacity}-seat (${Math.round((CONFIG.capacityMultiplier[capacity] ?? 1)*100)}% baseline)`,
          `<strong>Riders:</strong> ${p} (${Math.round((1 - (CONFIG.perPassengerPenalty * Math.max(0,p-1)))*100)}% of baseline)`,
          `<strong>Conditions:</strong> ${modsText.length ? modsText.join(', ') : 'none'}`
        ].filter(Boolean).join(' Β· ');

        // Prepare big power lines (Amps only, no kW)
        const contA = battery.maxContinuousA;
        const instA = battery.maxInstantaneousA;

        return { ok:true, miles, breakdown, contA, instA };
      }

      function updateHighlights(){
        // Step 1 (battery cards)
        $$('#gc-battery-list .gc-batt-card').forEach(card=>{
          const input = card.querySelector('input[name="gc-battery"]');
          card.classList.toggle('is-selected', !!(input && input.checked));
        });

        // Step 2 (capacity radios)
        $$('#gc-step2 label.cap-opt').forEach(lbl=>{
          const inp = lbl.querySelector('input[type="radio"]');
          lbl.classList.toggle('is-selected', !!(inp && inp.checked));
        });

        // Step 3 (passenger select)
        $('#gc-passengers').classList.add('is-selected');

        // Step 4 (checkboxes)
        $$('#gc-step4 label.cap-opt').forEach(lbl=>{
          const cb = lbl.querySelector('input[type="checkbox"]');
          lbl.classList.toggle('is-selected', !!(cb && cb.checked));
        });
      }

      function render(){
        const res = calculate();
        const milesEl = $('#gc-miles');
        const breakdownEl = $('#gc-breakdown');
        const contEl = $('#gc-continuous');
        const instEl = $('#gc-instant');

        if(!res.ok){
          milesEl.textContent = 'β€”';
          breakdownEl.innerHTML = res.message || '';
          contEl.textContent = '';
          instEl.textContent = '';
          return;
        }

        milesEl.textContent = res.miles + ' miles';
        breakdownEl.innerHTML = res.breakdown;

        // Show Amps (no kW) in big text (20% smaller than miles)
        contEl.textContent = res.contA ? `Max Continuous: ${Math.round(res.contA)} A` : '';
        instEl.textContent = res.instA ? `Max Instantaneous: ${Math.round(res.instA)} A` : '';
      }

      // Buttons
      $('#gc-calc').addEventListener('click', ()=>{ updateHighlights(); render(); });
      $('#gc-reset').addEventListener('click', ()=>{
        $$('input[name="gc-battery"]').forEach((i,idx)=> i.checked = idx===0);
        $$('input[name="gc-capacity"]').forEach(i=> i.checked=false);
        $$('#gc-step4 input[type="checkbox"]').forEach(i=> i.checked=false);
        fillPassengerOptions(6);
        $('#gc-passenger-hint').textContent = 'Choose capacity in Step 2 to lock max passengers.';
        $('#gc-miles').textContent = 'β€”';
        $('#gc-continuous').textContent = '';
        $('#gc-instant').textContent = '';
        $('#gc-breakdown').textContent = 'Make selections to see your result.';
        updateHighlights();
      });

      // Initial render + highlight
      updateHighlights();
      render();

      // Live update
      ['change','input'].forEach(evt=>{
        document.getElementById('gc-range-widget').addEventListener(evt, (e)=>{
          if(e.target && (e.target.matches('input, select'))) {
            updateHighlights();
            render();
          }
        }, true);
      });

      // Accessibility: Enter triggers calculate
      document.getElementById('gc-range-widget').addEventListener('keydown', (e)=>{
        if(e.key === 'Enter') { updateHighlights(); render(); }
      });
    })();
  </script>
</body>
</html>

Explore more articles

Discover more insights and resources from our blog.

View all posts