Created
October 17, 2025 00:11
-
-
Save blacksandsolutions/8c941eab9bd3083d20988465dd5c206f 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