Skip to content

Instantly share code, notes, and snippets.

@blacksandsolutions
Created October 16, 2025 22:53
Show Gist options
  • Save blacksandsolutions/845696145b7106214173e75c1b2671cd to your computer and use it in GitHub Desktop.
Save blacksandsolutions/845696145b7106214173e75c1b2671cd to your computer and use it in GitHub Desktop.
NTC Trip Grade Calculator demo
<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>
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();
});
}
});
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