Created
October 16, 2025 22:53
-
-
Save blacksandsolutions/845696145b7106214173e75c1b2671cd to your computer and use it in GitHub Desktop.
NTC Trip Grade Calculator demo
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
| <script src="https://cdn.tailwindcss.com"></script> | |
| <body> | |
| <div class="flex min-h-svh w-full items-center justify-center p-6 md:p-10"> | |
| <div class="flex w-full max-w-4xl gap-6"> | |
| <div class="flex flex-col flex-1 gap-6"> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <div> | |
| Trip Grade Calculator | |
| </div> | |
| <div> | |
| Enter values for the hardest day | |
| </div> | |
| </div> | |
| <div class="card-content"> | |
| <form> | |
| <div class="flex flex-col gap-6"> | |
| <div class="grid gap-3"> | |
| <label class="flex items-center gap-2 label" for="distance">Distance (km)</label><input type="number" data-slot="input" id="distance" /> | |
| </div> | |
| <div class="grid gap-3"> | |
| <div class="flex items-center"> | |
| <label class="label" for="height">Height (m)</label> | |
| </div> | |
| <input type="number" id="height" /> | |
| </div> | |
| <div class="grid gap-3"> | |
| <div class="flex items-center"> | |
| <label class="label" for="track">Track Roughness</label> | |
| <a href="#" id="track-info" class="sub-label" tabindex="-1">0.1 (Great Walk) to 1.0 (Very | |
| Rough)</a> | |
| <p id="track-error" class="error"> | |
| Must be between 0.1 and 1.0 | |
| </p> | |
| </div> | |
| <input type="number" step="0.1" min="0.1" max="1.0" id="track" /> | |
| </div> | |
| <div class="space-y-4"> | |
| <!-- SNOW SWITCH --> | |
| <div class="gap-2 flex flex-row items-start justify-between rounded-lg border p-3 shadow-sm"> | |
| <div class="space-y-0.5"> | |
| <label class="switch-label" for="switch-snow">Is there snow?</label> | |
| </div> | |
| <div class="flex flex-col"> | |
| <span id="snowYesNo" class="switch-state"></span> | |
| <button type="button" role="switch" id="switch-snow" aria-checked="false" data-state="unchecked" value="on" class="switch-button"> | |
| <span id="switch-snow-thumb" data-state="unchecked" data-slot="switch-thumb" class="switch-thumb"></span></button><input aria-hidden="true" tabindex="-1" type="checkbox" value="on" class="switch-input" /> | |
| </div> | |
| </div> | |
| <!-- OVERNIGHT PACK SWITCH --> | |
| <div class="gap-2 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm"> | |
| <div class="flex flex-col"> | |
| <div class="space-y-0.5"> | |
| <label class="switch-label" for="switch-pack">Overnight Pack? </label> | |
| </div> | |
| <div class="text-white text-base text-opacity-75 mt-2"> | |
| <p> If the hardest day is climbing a mountain from a hut and return to the hut, no overnight pack is carried</p> | |
| </div> | |
| </div> | |
| <div class="flex flex-col"> | |
| <span id="packYesNo" class="switch-state"></span> | |
| <button type="button" role="switch" id="switch-pack" aria-checked="false" data-state="unchecked" value="on" class="switch-button"> | |
| <span id="switch-pack-thumb" data-state="unchecked" data-slot="switch-thumb" class="switch-thumb"></span></button><input aria-hidden="true" tabindex="-1" type="checkbox" value="on" class="switch-input" /> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </form> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="flex flex-col flex-1 gap-6"> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <div> | |
| GRADE | |
| </div> | |
| <div></div> | |
| </div> | |
| <div class="flex flex-col flex-1 items-center justify-center"> | |
| <span id="result"></span> | |
| <span id="desc"></span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </body> |
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
| document.addEventListener("DOMContentLoaded", function () { | |
| const switchSnowElem = document.getElementById("switch-snow"); | |
| const switchSnowThumbElem = document.getElementById("switch-snow-thumb"); | |
| const switchPackElem = document.getElementById("switch-pack"); | |
| const switchPackThumbElem = document.getElementById("switch-pack-thumb"); | |
| const distanceElem = document.getElementById("distance"); | |
| const heightElem = document.getElementById("height"); | |
| const trackElem = document.getElementById("track"); | |
| const trackInfoElem = document.getElementById("track-info"); | |
| const trackErrorElem = document.getElementById("track-error"); | |
| const result = document.getElementById("result"); | |
| const descElem = document.getElementById("desc"); | |
| const packYesNoElem = document.getElementById("packYesNo"); | |
| const snowYesNoElem = document.getElementById("snowYesNo"); | |
| descElem.textContent = "Enter details on the left to get a grade"; | |
| packYesNoElem.textContent = "NO"; | |
| snowYesNoElem.textContent = "NO"; | |
| document.getElementById("track-error").style.display = "none"; | |
| function updateSum() { | |
| const distance = Number(distanceElem.value) || 0; | |
| const height = Number(heightElem.value) || 0; | |
| const track = Number(trackElem.value) || 0; | |
| const snow = | |
| switchSnowElem.getAttribute("data-state") === "checked" ? 1 : 0; | |
| const distanceFactor = distance / 14; | |
| const heightFactor = height / 400; | |
| const trackFactor = track + snow * 0.5; | |
| const packFactor = | |
| switchPackElem.getAttribute("data-state") === "checked" ? 0.5 : 0; | |
| if (track > 1.0) { | |
| trackErrorElem.style.display = "block"; | |
| trackInfoElem.style.display = "none"; | |
| } else if (track < 0.1) { | |
| trackErrorElem.style.display = "block"; | |
| trackInfoElem.style.display = "none"; | |
| } else { | |
| trackErrorElem.style.display = "none"; | |
| trackInfoElem.style.display = "block"; | |
| } | |
| if (distance === 0 || height === 0 || track === 0) { | |
| return; | |
| } | |
| const tripGradeSum = | |
| distanceFactor + heightFactor + trackFactor + packFactor; | |
| const tripGrade = tripGradeSum / 1.7 + 0.9; | |
| const tripGradeRounded = Math.round(tripGrade * 10) / 10; | |
| console.log(`tripGradeSum ${tripGradeSum}`); | |
| console.log(`tripGrade ${tripGrade}`); | |
| console.log(`tripGradeRounded ${tripGradeRounded}`); | |
| let grade = "Easy"; | |
| let desc = "A walk in the park; suitable for begineers and children"; | |
| if (tripGradeRounded > 1.6 && tripGradeRounded <= 1.9) { | |
| grade = "Easy Medium"; | |
| desc = "You might break a sweat..."; | |
| } else if (tripGradeRounded > 1.9 && tripGradeRounded <= 2.6) { | |
| grade = "Medium"; | |
| desc = "Some puffing might be required"; | |
| } else if (tripGradeRounded > 2.6 && tripGradeRounded <= 2.9) { | |
| grade = "Medium Fit"; | |
| desc = "Can we stop for tea please...?"; | |
| } else if (tripGradeRounded > 2.9 && tripGradeRounded <= 3.9) { | |
| grade = "Fit"; | |
| desc = "I wish I had not skipped breakfast..."; | |
| } else if (tripGradeRounded >= 4.0) { | |
| grade = "Hard"; | |
| desc = | |
| "Not for the faint of heart. May leave you questioning your life choices..."; | |
| } | |
| result.textContent = grade; | |
| descElem.textContent = desc; | |
| } | |
| if (distanceElem) { | |
| distanceElem.addEventListener("input", updateSum); | |
| } | |
| if (heightElem) { | |
| heightElem.addEventListener("input", updateSum); | |
| } | |
| if (trackElem) { | |
| trackElem.addEventListener("input", updateSum); | |
| } | |
| if (switchSnowElem) { | |
| switchSnowElem.addEventListener("click", () => { | |
| const currentState = switchSnowElem.getAttribute("data-state"); | |
| const newValue = currentState === "checked" ? "unchecked" : "checked"; | |
| switchSnowElem.setAttribute("data-state", newValue); | |
| switchSnowThumbElem.setAttribute("data-state", newValue); | |
| snowYesNoElem.textContent = currentState === "checked" ? "NO" : "YES"; | |
| updateSum(); | |
| }); | |
| } | |
| if (switchPackElem) { | |
| switchPackElem.addEventListener("click", () => { | |
| const currentState = switchPackElem.getAttribute("data-state"); | |
| const newValue = currentState === "checked" ? "unchecked" : "checked"; | |
| switchPackElem.setAttribute("data-state", newValue); | |
| switchPackThumbElem.setAttribute("data-state", newValue); | |
| packYesNoElem.textContent = currentState === "checked" ? "NO" : "YES"; | |
| updateSum(); | |
| }); | |
| } | |
| }); |
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
| body { | |
| color: #ffffff; | |
| background-color: #1d4ed8; | |
| } | |
| .card { | |
| display: flex; | |
| padding-top: 1.5rem; | |
| padding-bottom: 1.5rem; | |
| flex-direction: column; | |
| flex-grow: 1; | |
| gap: 1.5rem; | |
| border-radius: 0.75rem; | |
| border-width: 1px; | |
| background-color: #1e40af; | |
| box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); | |
| } | |
| .card-header { | |
| display: grid; | |
| padding-left: 1.5rem; | |
| padding-right: 1.5rem; | |
| grid-auto-rows: min; | |
| gap: 0.375rem; | |
| align-items: flex-start; | |
| } | |
| .card-header div:first-child { | |
| font-weight: 600; | |
| line-height: 1; | |
| } | |
| .card-header div:last-child { | |
| font-size: 1rem; | |
| line-height: 1.5rem; | |
| color: #ffffff; | |
| opacity: 0.75; | |
| } | |
| .card-content { | |
| padding-left: 1.5rem; | |
| padding-right: 1.5rem; | |
| } | |
| .flex { | |
| display: flex; | |
| } | |
| .flex-col { | |
| flex-direction: column; | |
| } | |
| .items-center { | |
| align-items: center; | |
| } | |
| .gap-2 { | |
| gap: 0.5rem; | |
| } | |
| .gap-3 { | |
| gap: 0.75rem; | |
| } | |
| .gap-6 { | |
| gap: 1.5rem; | |
| } | |
| .label { | |
| font-size: 0.875rem; | |
| line-height: 1.25rem; | |
| font-weight: 500; | |
| line-height: 1; | |
| user-select: none; | |
| } | |
| input { | |
| display: flex; | |
| padding-top: 0.25rem; | |
| padding-bottom: 0.25rem; | |
| /* todo work out why important needed */ | |
| padding-left: 0.75rem!important; | |
| padding-right: 0.75rem; | |
| border-radius: 0.375rem; | |
| border-width: 1px; | |
| outline-style: none; | |
| width: 100%; | |
| height: 2.25rem; | |
| min-width: 0; | |
| font-size: 1rem; | |
| line-height: 1.5rem; | |
| background-color: transparent; | |
| box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05); | |
| @media (min-width: 768px) { | |
| font-size: 0.875rem; | |
| line-height: 1.25rem; | |
| } | |
| } | |
| .sub-label { | |
| display: inline-block; | |
| font-size: 0.875rem; | |
| line-height: 1.25rem; | |
| text-underline-offset: 4px; | |
| margin-left: auto; | |
| :hover { | |
| text-decoration: underline; | |
| } | |
| } | |
| .error { | |
| font-size: 0.875rem; | |
| line-height: 1.25rem; | |
| color: #fca5a5; | |
| margin-left: auto; | |
| } | |
| #result { | |
| font-size: 3rem; | |
| line-height: 1; | |
| } | |
| #desc { | |
| margin-top: 0.5rem; | |
| font-size: 1.25rem; | |
| line-height: 1.75rem; | |
| text-align: center; | |
| color: #ffffff; | |
| opacity: 0.75; | |
| } | |
| .switch-label { | |
| display: flex; | |
| gap: 0.5rem; | |
| align-items: center; | |
| font-size: 0.875rem; | |
| line-height: 1.25rem; | |
| font-weight: 500; | |
| line-height: 1; | |
| user-select: none; | |
| } | |
| .switch-state { | |
| margin-bottom: 0.5rem; | |
| color: #ffffff; | |
| opacity: 0.75; | |
| } | |
| .switch-button { | |
| background-color: #1e3a8a; | |
| display: inline-flex; | |
| shrink: 0; | |
| align-items: center; | |
| border-radius: 9999px; | |
| border-width: 1px; | |
| border-color: transparent; | |
| outline-style: none; | |
| width: 2rem; | |
| transition-property: all; | |
| transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); | |
| transition-duration: 300ms; | |
| box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05); | |
| height: 1.15rem; | |
| } | |
| .switch-thumb { | |
| display: block; | |
| border-radius: 9999px; | |
| box-shadow: 0px 0px 0px 0px; | |
| background-color: #ffffff; | |
| transition-property: transform; | |
| transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); | |
| transition-duration: 300ms; | |
| pointer-events: none; | |
| width: 1rem; | |
| height: 1rem; | |
| } | |
| .switch-thumb[data-state="checked"] { | |
| transform: translateX(calc(100% - 2px)); | |
| } | |
| .switch-thumb[data-state="unchecked"] { | |
| transform: translateX(0); | |
| } | |
| .switch-input { | |
| transform: translateX(-100%); | |
| position: absolute; | |
| pointer-events: none; | |
| opacity: 0; | |
| margin: 0px; | |
| width: 32px; | |
| height: 18.3906px; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment