Skip to content

Commit 93d4318

Browse files
committed
feat(new tool): Running Pace Calculator
Fix #400 (and #413)
1 parent 464e005 commit 93d4318

6 files changed

Lines changed: 191 additions & 1 deletion

File tree

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,7 @@
317317
"riimut": "^0.9.1",
318318
"roboto-base64": "^0.1.2",
319319
"rtf-stream-parser": "^3.8.1",
320+
"run-pace": "^2.2.0",
320321
"safe-license-list": "^0.1.2",
321322
"sandybox": "^1.1.2",
322323
"sanitize-html": "^2.17.0",
@@ -467,7 +468,8 @@
467468
},
468469
"patchedDependencies": {
469470
"node-forge@1.4.0": "patches/node-forge@1.4.0.patch",
470-
"esm-potrace-wasm": "patches/esm-potrace-wasm.patch"
471+
"esm-potrace-wasm": "patches/esm-potrace-wasm.patch",
472+
"run-pace": "patches/run-pace.patch"
471473
}
472474
},
473475
"scarfSettings": {

patches/run-pace.patch

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
diff --git a/lib/run-pace.js b/lib/run-pace.js
2+
index 2b20a32ff4b5712016dd869a734ff25f730cdb12..9b9b72187e6675db6b295a827f2293950d12a111 100644
3+
--- a/lib/run-pace.js
4+
+++ b/lib/run-pace.js
5+
@@ -119,7 +119,3 @@ function removeTrailingZeroes(number) {
6+
function removeLeadingZero(number) {
7+
return number.replace(/^0/, "");
8+
}
9+
-
10+
-function zeroPad(number) {
11+
- return `0${number}`.slice(-2);
12+
-}

pnpm-lock.yaml

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Run } from '@vicons/tabler';
2+
import { defineTool } from '../tool';
3+
4+
export const tool = defineTool({
5+
name: 'Running Pace Calculator',
6+
path: '/running-pace-calculator',
7+
description: 'Calculate your running pace, estimate your finish times for popular races, break down splits, and find your training zones.',
8+
keywords: ['running', 'pace', 'calculator'],
9+
component: () => import('./running-pace-calculator.vue'),
10+
icon: Run,
11+
createdAt: new Date('2026-06-21'),
12+
category: 'Data',
13+
});
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
declare module 'run-pace' {
2+
export function calculatePace(options: {
3+
time: string;
4+
length: string;
5+
imperial?: boolean;
6+
metric?: boolean;
7+
speed?: boolean;
8+
}): string;
9+
export function calculateLength(options: {
10+
time: string;
11+
pace: string;
12+
imperial?: boolean;
13+
metric?: boolean;
14+
}): string;
15+
export function calculateTime(options: {
16+
length: string;
17+
pace: string;
18+
imperial?: boolean;
19+
metric?: boolean;
20+
}): string;
21+
export function paceToSpeed(options: {
22+
pace: string;
23+
imperial?: boolean;
24+
metric?: boolean;
25+
}): string;
26+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
<script setup lang="ts">
2+
import * as runPace from 'run-pace';
3+
4+
// Inputs
5+
const length = ref<string>('');
6+
const time = ref<string>('');
7+
const pace = ref<string>('');
8+
9+
// Options
10+
const unit = ref<'km' | 'mile'>('km');
11+
const speed = ref<boolean>(false);
12+
13+
// Output
14+
const result = ref<string>('');
15+
const resultType = ref<string>('');
16+
const error = ref<string>('');
17+
18+
function calculateStuff() {
19+
result.value = '';
20+
error.value = '';
21+
22+
const options: any = {
23+
length: length.value,
24+
time: time.value,
25+
pace: pace.value,
26+
};
27+
28+
// Unit selection
29+
if (unit.value === 'km') {
30+
options.metric = true;
31+
}
32+
if (unit.value === 'mile') {
33+
options.imperial = true;
34+
}
35+
36+
options.speed = speed.value;
37+
38+
try {
39+
if (options.time && options.length && options.pace) {
40+
throw new Error(
41+
'Only two of "time", "length" and "pace" may be provided at any time',
42+
);
43+
}
44+
else if (options.time && options.pace) {
45+
if (options.speed) {
46+
throw new Error('Speed is only valid when (only) pace is given or calculated');
47+
}
48+
resultType.value = 'Length';
49+
result.value = `${runPace.calculateLength(options)}`;
50+
}
51+
else if (options.time && options.length) {
52+
const label = options.speed ? 'Speed' : 'Pace';
53+
resultType.value = label;
54+
result.value = `${runPace.calculatePace(options)}`;
55+
}
56+
else if (options.pace && options.length) {
57+
if (options.speed) {
58+
throw new Error('Speed is only valid when (only) pace is given or calculated');
59+
}
60+
resultType.value = 'Time';
61+
result.value = `${runPace.calculateTime(options)}`;
62+
}
63+
else if (options.pace && options.speed) {
64+
resultType.value = 'Speed';
65+
result.value = `${runPace.paceToSpeed(options)}`;
66+
}
67+
else {
68+
throw new Error(
69+
'Two of "time", "length" and "pace" must be provided.\nSpeed is only valid when pace is given or calculated.',
70+
);
71+
}
72+
}
73+
catch (e: any) {
74+
error.value = e.message;
75+
}
76+
}
77+
</script>
78+
79+
<template>
80+
<div>
81+
<NForm label-placement="left" label-width="70px">
82+
<NFormItem label="Length:">
83+
<NInput v-model:value="length" placeholder="e.g. 10km or 100m" clearable />
84+
</NFormItem>
85+
86+
<NFormItem label="Time:">
87+
<NInput v-model:value="time" placeholder="e.g. 45:00 or 45min or 1h30..." clearable />
88+
</NFormItem>
89+
90+
<NFormItem label="Pace:">
91+
<NInput v-model:value="pace" placeholder="e.g. 4:30/km" clearable />
92+
</NFormItem>
93+
94+
<n-space justify="center">
95+
<NFormItem label="Unit:">
96+
<NRadioGroup v-model:value="unit">
97+
<NRadio value="km">
98+
km
99+
</NRadio>
100+
<NRadio value="mile">
101+
mile
102+
</NRadio>
103+
</NRadioGroup>
104+
</NFormItem>
105+
<NFormItem label="Output speed in instead of pace:" label-width="auto">
106+
<NSwitch v-model:value="speed" />
107+
</NFormItem>
108+
</n-space>
109+
</NForm>
110+
111+
<n-space justify="center" mb-2>
112+
<NButton type="primary" @click="calculateStuff">
113+
Calculate
114+
</NButton>
115+
</n-space>
116+
117+
<c-alert v-if="error">
118+
{{ error }}
119+
</c-alert>
120+
121+
<c-card v-if="result" title="Result">
122+
<input-copyable :label="`${resultType}:`" label-position="left" :value="result" />
123+
</c-card>
124+
</div>
125+
</template>

0 commit comments

Comments
 (0)