commit 314b19d89ed8dd2baaa2758a1f782deb20a0f5cd
parent 147f47f096469669d4925a172f91ff35feba94f2
Author: Asher Morgan <59518073+ashermorgan@users.noreply.github.com>
Date: Sat, 28 Jun 2025 13:19:36 -0700
Convert all views to TypeScript
Diffstat:
23 files changed, 504 insertions(+), 350 deletions(-)
diff --git a/package-lock.json b/package-lock.json
@@ -17,7 +17,7 @@
"@eslint/js": "^9.22.0",
"@playwright/test": "^1.53.1",
"@tsconfig/node22": "^22.0.2",
- "@types/node": "^24.0.3",
+ "@types/node": "^24.0.7",
"@vitejs/plugin-vue": "^5.2.4",
"@vue/eslint-config-typescript": "^14.5.1",
"@vue/test-utils": "^2.4.6",
@@ -3051,9 +3051,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
- "version": "24.0.3",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.3.tgz",
- "integrity": "sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg==",
+ "version": "24.0.7",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.7.tgz",
+ "integrity": "sha512-YIEUUr4yf8q8oQoXPpSlnvKNVKDQlPMWrmOcgzoduo7kvA2UF0/BwJ/eMKFTiTtkNL17I0M6Xe2tvwFU7be6iw==",
"dev": true,
"license": "MIT",
"dependencies": {
diff --git a/package.json b/package.json
@@ -28,7 +28,7 @@
"@eslint/js": "^9.22.0",
"@playwright/test": "^1.53.1",
"@tsconfig/node22": "^22.0.2",
- "@types/node": "^24.0.3",
+ "@types/node": "^24.0.7",
"@vitejs/plugin-vue": "^5.2.4",
"@vue/eslint-config-typescript": "^14.5.1",
"@vue/test-utils": "^2.4.6",
diff --git a/src/components/RaceOptions.vue b/src/components/RaceOptionsInput.vue
diff --git a/src/components/TargetEditor.vue b/src/components/TargetEditor.vue
@@ -89,7 +89,7 @@
<script setup lang="ts">
import VueFeather from 'vue-feather';
-import { TargetType, TargetSetType, workoutTargetToString } from '@/utils/targets';
+import { TargetTypes, TargetSetTypes, workoutTargetToString } from '@/utils/targets';
import type { StandardTargetSet, TargetSet, WorkoutTarget, WorkoutTargetSet } from '@/utils/targets';
import { DistanceUnitData, UnitSystems, getDefaultDistanceUnit } from '@/utils/units';
@@ -120,14 +120,14 @@ interface Props {
/**
* The target set type (Standard, Split, or Workout, defaults to Standard)
*/
- setType?: TargetSetType,
+ setType?: TargetSetTypes,
}
const props = withDefaults(defineProps<Props>(), {
customWorkoutNames: false,
defaultUnitSystem: UnitSystems.Metric,
isCustomSet: false,
- setType: TargetSetType.Standard,
+ setType: TargetSetTypes.Standard,
});
// Declare emitted events
@@ -140,9 +140,9 @@ const model = useObjectModel<TargetSet>(() => props.modelValue, (x) => emit('upd
* Add a new distance based target
*/
function addDistanceTarget() {
- if (props.setType === TargetSetType.Workout) {
+ if (props.setType === TargetSetTypes.Workout) {
(model.value as WorkoutTargetSet).targets.push({
- type: TargetType.Distance,
+ type: TargetTypes.Distance,
distanceValue: 1,
distanceUnit: getDefaultDistanceUnit(props.defaultUnitSystem),
splitValue: 1,
@@ -150,7 +150,7 @@ function addDistanceTarget() {
});
} else {
(model.value as StandardTargetSet).targets.push({
- type: TargetType.Distance,
+ type: TargetTypes.Distance,
distanceValue: 1,
distanceUnit: getDefaultDistanceUnit(props.defaultUnitSystem),
});
@@ -161,16 +161,16 @@ function addDistanceTarget() {
* Add a new time based target
*/
function addTimeTarget() {
- if (props.setType === TargetSetType.Workout) {
+ if (props.setType === TargetSetTypes.Workout) {
(model.value as WorkoutTargetSet).targets.push({
- type: TargetType.Time,
+ type: TargetTypes.Time,
time: 600,
splitValue: 1,
splitUnit: getDefaultDistanceUnit(props.defaultUnitSystem),
});
} else {
(model.value as StandardTargetSet).targets.push({
- type: TargetType.Time,
+ type: TargetTypes.Time,
time: 600,
});
}
diff --git a/src/components/TargetSetSelector.vue b/src/components/TargetSetSelector.vue
@@ -26,7 +26,7 @@ import { computed, nextTick, ref } from 'vue';
import VueFeather from 'vue-feather';
import { deepCopy } from '@/utils/misc';
-import { TargetSetType, sort, defaultTargetSets } from '@/utils/targets';
+import { TargetSetTypes, sort, defaultTargetSets } from '@/utils/targets';
import type { TargetSet, TargetSets } from '@/utils/targets';
import { UnitSystems } from '@/utils/units';
@@ -55,7 +55,7 @@ interface Props {
/**
* The target set type (Standard, Split, or Workout, defaults to Standard)
*/
- setType?: TargetSetType,
+ setType?: TargetSetTypes,
/**
* The target sets
@@ -67,7 +67,7 @@ interface Props {
const props = withDefaults(defineProps<Props>(), {
customWorkoutNames: false,
defaultUnitSystem: UnitSystems.Metric,
- setType: TargetSetType.Standard,
+ setType: TargetSetTypes.Standard,
});
// Generate internal ref tied to modelValue prop
diff --git a/src/composables/useStorage.ts b/src/composables/useStorage.ts
@@ -6,16 +6,16 @@ import * as storage from '@/utils/storage';
/*
* Create a reactive value that is synced with a localStorage item
* @param {string} key The localStorage item's key
- * @param {object} defaultValue The default value
- * @returns {Ref<object>} The synchronized ref
+ * @param {Type} defaultValue The default value
+ * @returns {Ref<Type>} The synchronized ref
*/
-export default function useStorage(key: string, defaultValue: object): Ref<object> {
- const clonedDefault = JSON.parse(JSON.stringify(defaultValue));
- const value = ref(clonedDefault);
+export default function useStorage<Type>(key: string, defaultValue: Type): Ref<Type> {
+ const clonedDefault: Type = JSON.parse(JSON.stringify(defaultValue));
+ const value: Ref<Type> = ref<Type>(clonedDefault) as Ref<Type>;
// (Re)load value from localStorage
function updateValue() {
- const parsedValue = storage.get(key);
+ const parsedValue = storage.get<Type>(key);
if (parsedValue !== null) value.value = parsedValue;
}
updateValue();
@@ -24,7 +24,7 @@ export default function useStorage(key: string, defaultValue: object): Ref<objec
// Save value to localStorage when modified
watchEffect(() => {
if (typeof localStorage !== 'undefined') {
- storage.set(key, value.value);
+ storage.set<Type>(key, value.value);
}
})
diff --git a/src/utils/calculators.ts b/src/utils/calculators.ts
@@ -1,7 +1,7 @@
import { formatDuration, formatNumber } from '@/utils/format';
import * as paceUtils from '@/utils/paces';
import * as raceUtils from '@/utils/races';
-import { TargetType, workoutTargetToString } from '@/utils/targets';
+import { TargetTypes, workoutTargetToString } from '@/utils/targets';
import type { StandardTarget, WorkoutTarget } from '@/utils/targets';
import { DistanceUnits, DistanceUnitData, UnitSystems, convertDistance,
getDefaultDistanceUnit } from '@/utils/units';
@@ -15,7 +15,7 @@ export enum ResultType {
interface PreResult {
distanceValue: number,
distanceUnit: DistanceUnits,
- result: TargetType,
+ result: TargetTypes,
time: number,
};
@@ -69,7 +69,7 @@ export function formatTargetResult(result: PreResult, defaultUnitSystem: UnitSys
+ DistanceUnitData[getDefaultDistanceUnit(defaultUnitSystem)].symbol,
// Convert dist/time result to key/value
- result: result.result === TargetType.Time ? ResultType.Value : ResultType.Key,
+ result: result.result === TargetTypes.Time ? ResultType.Value : ResultType.Key,
// Use time (in seconds) as sort key
sort: result.time,
@@ -91,7 +91,7 @@ export function calculatePaceResults(input: DistanceTime, target: StandardTarget
distanceValue: 0,
distanceUnit: DistanceUnits.Meters,
time: 0,
- result: target.type === TargetType.Distance ? TargetType.Time : TargetType.Distance,
+ result: target.type === TargetTypes.Distance ? TargetTypes.Time : TargetTypes.Distance,
};
const d1 = convertDistance(input.distanceValue, input.distanceUnit, DistanceUnits.Meters);
@@ -141,7 +141,7 @@ export function calculateRaceResults(input: DistanceTime, target: StandardTarget
distanceValue: 0,
distanceUnit: DistanceUnits.Meters,
time: 0,
- result: target.type === TargetType.Distance ? TargetType.Time : TargetType.Distance,
+ result: target.type === TargetTypes.Distance ? TargetTypes.Time : TargetTypes.Distance,
};
const d1 = convertDistance(input.distanceValue, input.distanceUnit, DistanceUnits.Meters);
diff --git a/src/utils/storage.ts b/src/utils/storage.ts
@@ -4,9 +4,9 @@ const prefix = 'running-tools';
/**
* Read an object from a localStorage item
* @param {string} key The localStorage item's key
- * @returns {object} The object
+ * @returns {Type} The object
*/
-export function get(key: string): object | null {
+export function get<Type>(key: string): Type | null {
try {
return JSON.parse(localStorage.getItem(`${prefix}.${key}`) || '');
} catch {
@@ -17,9 +17,9 @@ export function get(key: string): object | null {
/**
* Write an object to a localStorage item
* @param {string} key The localStorage item's key
- * @param {object} value The object to write
+ * @param {Type} value The object to write
*/
-export function set(key: string, value: object) {
+export function set<Type>(key: string, value: Type) {
localStorage.setItem(`${prefix}.${key}`, JSON.stringify(value));
}
diff --git a/src/utils/targets.ts b/src/utils/targets.ts
@@ -4,7 +4,7 @@ import { DistanceUnits, DistanceUnitData, convertDistance } from '@/utils/units'
/*
* Enumeration for the two basic types of targets: those defined by distance vs time
*/
-export enum TargetType {
+export enum TargetTypes {
Distance = 'distance',
Time = 'time',
};
@@ -13,7 +13,7 @@ export enum TargetType {
* Type for basic distance-defined targets
*/
interface DistanceTarget {
- type: TargetType.Distance,
+ type: TargetTypes.Distance,
distanceValue: number,
distanceUnit: DistanceUnits,
};
@@ -22,7 +22,7 @@ interface DistanceTarget {
* Type for basic time-defined targets
*/
interface TimeTarget {
- type: TargetType.Time,
+ type: TargetTypes.Time,
time: number,
};
@@ -95,7 +95,7 @@ export interface WorkoutTargetSets {
/*
* Enumeration for the three types of targets sets: standard (pace & race), split, and workout
*/
-export enum TargetSetType {
+export enum TargetSetTypes {
Standard = 'standard',
Split = 'split',
Workout = 'workout',
@@ -123,11 +123,11 @@ export type TargetSets = StandardTargetSets | SplitTargetSets | WorkoutTargetSet
*/
export function sort(targets: Array<Target>): Array<Target> {
return [
- ...targets.filter((item) => item.type === TargetType.Distance)
+ ...targets.filter((item) => item.type === TargetTypes.Distance)
.sort((a, b) => convertDistance(a.distanceValue, a.distanceUnit, DistanceUnits.Meters)
- convertDistance(b.distanceValue, b.distanceUnit, DistanceUnits.Meters)),
- ...targets.filter((item) => item.type === TargetType.Time)
+ ...targets.filter((item) => item.type === TargetTypes.Time)
.sort((a, b) => a.time - b.time),
];
}
@@ -140,7 +140,7 @@ export function sort(targets: Array<Target>): Array<Target> {
export function workoutTargetToString(target: WorkoutTarget): string {
let result = formatNumber(target.splitValue, 0, 2, false) + ' ' +
DistanceUnitData[target.splitUnit].symbol;
- if (target.type === TargetType.Time) {
+ if (target.type === TargetTypes.Time) {
result += ' @ ' + formatDuration(target.time, 3, 2, false);
} else if (target.distanceValue != target.splitValue || target.distanceUnit != target.splitUnit) {
result += ' @ ' + formatNumber(target.distanceValue, 0, 2, false) + ' ' +
@@ -155,40 +155,40 @@ export function workoutTargetToString(target: WorkoutTarget): string {
const common_pace_targets: StandardTargetSet = {
name: 'Common Pace Targets',
targets: sort([
- { type: TargetType.Distance, distanceValue: 100, distanceUnit: DistanceUnits.Meters },
- { type: TargetType.Distance, distanceValue: 200, distanceUnit: DistanceUnits.Meters },
- { type: TargetType.Distance, distanceValue: 300, distanceUnit: DistanceUnits.Meters },
- { type: TargetType.Distance, distanceValue: 400, distanceUnit: DistanceUnits.Meters },
- { type: TargetType.Distance, distanceValue: 600, distanceUnit: DistanceUnits.Meters },
- { type: TargetType.Distance, distanceValue: 800, distanceUnit: DistanceUnits.Meters },
- { type: TargetType.Distance, distanceValue: 1000, distanceUnit: DistanceUnits.Meters },
- { type: TargetType.Distance, distanceValue: 1200, distanceUnit: DistanceUnits.Meters },
- { type: TargetType.Distance, distanceValue: 1500, distanceUnit: DistanceUnits.Meters },
- { type: TargetType.Distance, distanceValue: 1600, distanceUnit: DistanceUnits.Meters },
- { type: TargetType.Distance, distanceValue: 3200, distanceUnit: DistanceUnits.Meters },
+ { type: TargetTypes.Distance, distanceValue: 100, distanceUnit: DistanceUnits.Meters },
+ { type: TargetTypes.Distance, distanceValue: 200, distanceUnit: DistanceUnits.Meters },
+ { type: TargetTypes.Distance, distanceValue: 300, distanceUnit: DistanceUnits.Meters },
+ { type: TargetTypes.Distance, distanceValue: 400, distanceUnit: DistanceUnits.Meters },
+ { type: TargetTypes.Distance, distanceValue: 600, distanceUnit: DistanceUnits.Meters },
+ { type: TargetTypes.Distance, distanceValue: 800, distanceUnit: DistanceUnits.Meters },
+ { type: TargetTypes.Distance, distanceValue: 1000, distanceUnit: DistanceUnits.Meters },
+ { type: TargetTypes.Distance, distanceValue: 1200, distanceUnit: DistanceUnits.Meters },
+ { type: TargetTypes.Distance, distanceValue: 1500, distanceUnit: DistanceUnits.Meters },
+ { type: TargetTypes.Distance, distanceValue: 1600, distanceUnit: DistanceUnits.Meters },
+ { type: TargetTypes.Distance, distanceValue: 3200, distanceUnit: DistanceUnits.Meters },
- { type: TargetType.Distance, distanceValue: 2, distanceUnit: DistanceUnits.Kilometers },
- { type: TargetType.Distance, distanceValue: 3, distanceUnit: DistanceUnits.Kilometers },
- { type: TargetType.Distance, distanceValue: 4, distanceUnit: DistanceUnits.Kilometers },
- { type: TargetType.Distance, distanceValue: 5, distanceUnit: DistanceUnits.Kilometers },
- { type: TargetType.Distance, distanceValue: 6, distanceUnit: DistanceUnits.Kilometers },
- { type: TargetType.Distance, distanceValue: 8, distanceUnit: DistanceUnits.Kilometers },
- { type: TargetType.Distance, distanceValue: 10, distanceUnit: DistanceUnits.Kilometers },
+ { type: TargetTypes.Distance, distanceValue: 2, distanceUnit: DistanceUnits.Kilometers },
+ { type: TargetTypes.Distance, distanceValue: 3, distanceUnit: DistanceUnits.Kilometers },
+ { type: TargetTypes.Distance, distanceValue: 4, distanceUnit: DistanceUnits.Kilometers },
+ { type: TargetTypes.Distance, distanceValue: 5, distanceUnit: DistanceUnits.Kilometers },
+ { type: TargetTypes.Distance, distanceValue: 6, distanceUnit: DistanceUnits.Kilometers },
+ { type: TargetTypes.Distance, distanceValue: 8, distanceUnit: DistanceUnits.Kilometers },
+ { type: TargetTypes.Distance, distanceValue: 10, distanceUnit: DistanceUnits.Kilometers },
- { type: TargetType.Distance, distanceValue: 1, distanceUnit: DistanceUnits.Miles },
- { type: TargetType.Distance, distanceValue: 2, distanceUnit: DistanceUnits.Miles },
- { type: TargetType.Distance, distanceValue: 3, distanceUnit: DistanceUnits.Miles },
- { type: TargetType.Distance, distanceValue: 5, distanceUnit: DistanceUnits.Miles },
- { type: TargetType.Distance, distanceValue: 6, distanceUnit: DistanceUnits.Miles },
- { type: TargetType.Distance, distanceValue: 8, distanceUnit: DistanceUnits.Miles },
- { type: TargetType.Distance, distanceValue: 10, distanceUnit: DistanceUnits.Miles },
+ { type: TargetTypes.Distance, distanceValue: 1, distanceUnit: DistanceUnits.Miles },
+ { type: TargetTypes.Distance, distanceValue: 2, distanceUnit: DistanceUnits.Miles },
+ { type: TargetTypes.Distance, distanceValue: 3, distanceUnit: DistanceUnits.Miles },
+ { type: TargetTypes.Distance, distanceValue: 5, distanceUnit: DistanceUnits.Miles },
+ { type: TargetTypes.Distance, distanceValue: 6, distanceUnit: DistanceUnits.Miles },
+ { type: TargetTypes.Distance, distanceValue: 8, distanceUnit: DistanceUnits.Miles },
+ { type: TargetTypes.Distance, distanceValue: 10, distanceUnit: DistanceUnits.Miles },
- { type: TargetType.Distance, distanceValue: 0.5, distanceUnit: DistanceUnits.Marathons },
- { type: TargetType.Distance, distanceValue: 1, distanceUnit: DistanceUnits.Marathons },
+ { type: TargetTypes.Distance, distanceValue: 0.5, distanceUnit: DistanceUnits.Marathons },
+ { type: TargetTypes.Distance, distanceValue: 1, distanceUnit: DistanceUnits.Marathons },
- { type: TargetType.Time, time: 600 },
- { type: TargetType.Time, time: 1800 },
- { type: TargetType.Time, time: 3600 },
+ { type: TargetTypes.Time, time: 600 },
+ { type: TargetTypes.Time, time: 1800 },
+ { type: TargetTypes.Time, time: 3600 },
]),
};
@@ -198,24 +198,24 @@ const common_pace_targets: StandardTargetSet = {
const common_race_targets: StandardTargetSet = {
name: 'Common Race Targets',
targets: sort([
- { type: TargetType.Distance, distanceValue: 400, distanceUnit: DistanceUnits.Meters },
- { type: TargetType.Distance, distanceValue: 800, distanceUnit: DistanceUnits.Meters },
- { type: TargetType.Distance, distanceValue: 1500, distanceUnit: DistanceUnits.Meters },
- { type: TargetType.Distance, distanceValue: 1600, distanceUnit: DistanceUnits.Meters },
- { type: TargetType.Distance, distanceValue: 1, distanceUnit: DistanceUnits.Miles },
- { type: TargetType.Distance, distanceValue: 3000, distanceUnit: DistanceUnits.Meters },
- { type: TargetType.Distance, distanceValue: 3200, distanceUnit: DistanceUnits.Meters },
- { type: TargetType.Distance, distanceValue: 2, distanceUnit: DistanceUnits.Miles },
+ { type: TargetTypes.Distance, distanceValue: 400, distanceUnit: DistanceUnits.Meters },
+ { type: TargetTypes.Distance, distanceValue: 800, distanceUnit: DistanceUnits.Meters },
+ { type: TargetTypes.Distance, distanceValue: 1500, distanceUnit: DistanceUnits.Meters },
+ { type: TargetTypes.Distance, distanceValue: 1600, distanceUnit: DistanceUnits.Meters },
+ { type: TargetTypes.Distance, distanceValue: 1, distanceUnit: DistanceUnits.Miles },
+ { type: TargetTypes.Distance, distanceValue: 3000, distanceUnit: DistanceUnits.Meters },
+ { type: TargetTypes.Distance, distanceValue: 3200, distanceUnit: DistanceUnits.Meters },
+ { type: TargetTypes.Distance, distanceValue: 2, distanceUnit: DistanceUnits.Miles },
- { type: TargetType.Distance, distanceValue: 3, distanceUnit: DistanceUnits.Miles },
- { type: TargetType.Distance, distanceValue: 5, distanceUnit: DistanceUnits.Kilometers },
- { type: TargetType.Distance, distanceValue: 6, distanceUnit: DistanceUnits.Kilometers },
- { type: TargetType.Distance, distanceValue: 8, distanceUnit: DistanceUnits.Kilometers },
- { type: TargetType.Distance, distanceValue: 10, distanceUnit: DistanceUnits.Kilometers },
- { type: TargetType.Distance, distanceValue: 15, distanceUnit: DistanceUnits.Kilometers },
+ { type: TargetTypes.Distance, distanceValue: 3, distanceUnit: DistanceUnits.Miles },
+ { type: TargetTypes.Distance, distanceValue: 5, distanceUnit: DistanceUnits.Kilometers },
+ { type: TargetTypes.Distance, distanceValue: 6, distanceUnit: DistanceUnits.Kilometers },
+ { type: TargetTypes.Distance, distanceValue: 8, distanceUnit: DistanceUnits.Kilometers },
+ { type: TargetTypes.Distance, distanceValue: 10, distanceUnit: DistanceUnits.Kilometers },
+ { type: TargetTypes.Distance, distanceValue: 15, distanceUnit: DistanceUnits.Kilometers },
- { type: TargetType.Distance, distanceValue: 0.5, distanceUnit: DistanceUnits.Marathons },
- { type: TargetType.Distance, distanceValue: 1, distanceUnit: DistanceUnits.Marathons },
+ { type: TargetTypes.Distance, distanceValue: 0.5, distanceUnit: DistanceUnits.Marathons },
+ { type: TargetTypes.Distance, distanceValue: 1, distanceUnit: DistanceUnits.Marathons },
]),
};
@@ -226,9 +226,9 @@ const common_race_targets: StandardTargetSet = {
const five_k_mile_splits: SplitTargetSet = {
name: '5K Mile Splits',
targets: [
- { type: TargetType.Distance, distanceValue: 1, distanceUnit: DistanceUnits.Miles },
- { type: TargetType.Distance, distanceValue: 2, distanceUnit: DistanceUnits.Miles },
- { type: TargetType.Distance, distanceValue: 5, distanceUnit: DistanceUnits.Kilometers },
+ { type: TargetTypes.Distance, distanceValue: 1, distanceUnit: DistanceUnits.Miles },
+ { type: TargetTypes.Distance, distanceValue: 2, distanceUnit: DistanceUnits.Miles },
+ { type: TargetTypes.Distance, distanceValue: 5, distanceUnit: DistanceUnits.Kilometers },
],
};
@@ -240,19 +240,19 @@ const common_workout_targets: WorkoutTargetSet = {
targets: [
{
splitValue: 400, splitUnit: DistanceUnits.Meters,
- type: TargetType.Distance, distanceValue: 1, distanceUnit: DistanceUnits.Miles,
+ type: TargetTypes.Distance, distanceValue: 1, distanceUnit: DistanceUnits.Miles,
},
{
splitValue: 800, splitUnit: DistanceUnits.Meters,
- type: TargetType.Distance, distanceValue: 5, distanceUnit: DistanceUnits.Kilometers,
+ type: TargetTypes.Distance, distanceValue: 5, distanceUnit: DistanceUnits.Kilometers,
},
{
splitValue: 1600, splitUnit: DistanceUnits.Meters,
- type: TargetType.Time, time: 3600,
+ type: TargetTypes.Time, time: 3600,
},
{
splitValue: 1, splitUnit: DistanceUnits.Miles,
- type: TargetType.Distance, distanceValue: 1, distanceUnit: DistanceUnits.Marathons,
+ type: TargetTypes.Distance, distanceValue: 1, distanceUnit: DistanceUnits.Marathons,
},
],
};
diff --git a/src/utils/units.ts b/src/utils/units.ts
@@ -1,4 +1,13 @@
/**
+ * The data included for each unit
+ */
+export interface UnitData {
+ name: string,
+ symbol: string,
+ value: number,
+};
+
+/**
* The supported time units
*/
export enum TimeUnits {
@@ -6,7 +15,7 @@ export enum TimeUnits {
Minutes = 'minutes',
Hours = 'hours',
}
-export const TimeUnitData = {
+export const TimeUnitData: { [key in TimeUnits]: UnitData } = {
[TimeUnits.Seconds]: {
name: 'Seconds',
symbol: 's',
@@ -34,7 +43,7 @@ export enum DistanceUnits {
Miles = 'miles',
Marathons = 'marathons',
}
-export const DistanceUnitData = {
+export const DistanceUnitData: { [key in DistanceUnits]: UnitData } = {
[DistanceUnits.Meters]: {
name: 'Meters',
symbol: 'm',
@@ -70,18 +79,18 @@ export enum SpeedUnits {
KilometersPerHour = 'kilometers_per_hour',
MilesPerHour = 'miles_per_hour',
}
-export const SpeedUnitData = {
- meters_per_second: {
+export const SpeedUnitData: { [key in SpeedUnits]: UnitData } = {
+ [SpeedUnits.MetersPerSecond]: {
name: 'Meters per Second',
symbol: 'm/s',
value: 1,
},
- kilometers_per_hour: {
+ [SpeedUnits.KilometersPerHour]: {
name: 'Kilometers per Hour',
symbol: 'kph',
value: DistanceUnitData[DistanceUnits.Kilometers].value / TimeUnitData[TimeUnits.Hours].value,
},
- miles_per_hour: {
+ [SpeedUnits.MilesPerHour]: {
name: 'Miles per Hour',
symbol: 'mph',
value: DistanceUnitData[DistanceUnits.Miles].value / TimeUnitData[TimeUnits.Hours].value,
@@ -96,7 +105,7 @@ export enum PaceUnits {
TimePerKilometer = 'seconds_per_kilometer',
TimePerMile = 'seconds_per_mile',
}
-export const PaceUnitData = {
+export const PaceUnitData: { [key in PaceUnits]: UnitData } = {
[PaceUnits.SecondsPerMeter]: {
name: 'Seconds per Meter',
symbol: 's/m',
@@ -114,6 +123,11 @@ export const PaceUnitData = {
},
};
+/**
+ * The supported speed and pace units
+ */
+export type SpeedPaceUnits = SpeedUnits | PaceUnits;
+
export enum UnitSystems {
Metric = 'metric',
Imperial = 'imperial',
@@ -183,8 +197,8 @@ export function convertPace(inputValue: number, inputUnit: PaceUnits,
* @param {string} outputUnit The unit of the output
* @returns {number} The output
*/
-export function convertSpeedPace(inputValue: number, inputUnit: SpeedUnits | PaceUnits,
- outputUnit: SpeedUnits | PaceUnits): number {
+export function convertSpeedPace(inputValue: number, inputUnit: SpeedPaceUnits,
+ outputUnit: SpeedPaceUnits): number {
// Calculate input speed
let speed;
if (inputUnit in PaceUnitData) {
diff --git a/src/views/AboutPage.vue b/src/views/AboutPage.vue
@@ -161,13 +161,13 @@
</div>
</template>
-<script setup>
+<script setup lang="ts">
import { repository, version } from '/package.json';
import VueFeather from 'vue-feather';
-const development = process.env.NODE_ENV === 'development';
-const git_url = repository.url.slice(4);
+const development: boolean = process.env.NODE_ENV === 'development';
+const git_url: string = repository.url.slice(4);
</script>
<style scoped>
diff --git a/src/views/BatchCalculator.vue b/src/views/BatchCalculator.vue
@@ -37,18 +37,22 @@
<div>
Target Set:
<target-set-selector v-model:selectedTargetSet="selectedTargetSet"
- :setType="options.calculator === 'workout' ? 'workout' : 'standard'"
- :customWorkoutNames="advancedOptions.customTargetNames" v-model:targetSets="targetSets"
+ :set-type="options.calculator === BatchCompatableCalculators.Workout ?
+ TargetSetTypes.Workout : TargetSetTypes.Standard" v-model:targetSets="targetSets"
+ :customWorkoutNames="options.calculator === BatchCompatableCalculators.Workout ?
+ (advancedOptions as WorkoutOptions).customTargetNames : false"
:default-unit-system="defaultUnitSystem"/>
</div>
<div v-if="options.calculator === 'workout'">
Target Name Customization:
- <select v-model="advancedOptions.customTargetNames" aria-label="Target name customization">
+ <select v-model="(advancedOptions as WorkoutOptions).customTargetNames"
+ aria-label="Target name customization">
<option :value="false">Disabled</option>
<option :value="true">Enabled</option>
</select>
</div>
- <race-options v-if="options.calculator !== 'pace'" v-model="advancedOptions"/>
+ <race-options-input v-if="options.calculator !== BatchCompatableCalculators.Pace"
+ v-model="advancedOptions as RaceOptions"/>
</details>
<h2>Batch Results</h2>
@@ -58,36 +62,59 @@
</div>
</template>
-<script setup>
+<script setup lang="ts">
import { computed } from 'vue';
import * as calcUtils from '@/utils/calculators';
-import { defaultTargetSets } from '@/utils/targets';
-import { detectDefaultUnitSystem } from '@/utils/units';
+import type { TargetResult, RaceOptions, WorkoutOptions } from '@/utils/calculators';
+import { RacePredictionModel } from '@/utils/races';
+import { TargetSetTypes, defaultTargetSets } from '@/utils/targets';
+import type { Target, TargetSets, StandardTargetSet, StandardTargetSets, WorkoutTarget,
+ WorkoutTargetSet, WorkoutTargetSets } from '@/utils/targets';
+import { DistanceUnits, UnitSystems, detectDefaultUnitSystem } from '@/utils/units';
+import type { Distance, DistanceTime } from '@/utils/units';
import DoubleOutputTable from '@/components/DoubleOutputTable.vue';
import IntegerInput from '@/components/IntegerInput.vue';
import PaceInput from '@/components/PaceInput.vue';
-import RaceOptions from '@/components/RaceOptions.vue';
+import RaceOptionsInput from '@/components/RaceOptionsInput.vue';
import TargetSetSelector from '@/components/TargetSetSelector.vue';
import TimeInput from '@/components/TimeInput.vue';
import useStorage from '@/composables/useStorage';
/**
+ * The calculators that may be used from within the batch calculator
+ */
+enum BatchCompatableCalculators {
+ Pace = 'pace',
+ Race = 'race',
+ Workout = 'workout',
+}
+
+/**
+ * The batch calculator settings type
+ */
+interface BatchCalculatorOptions {
+ calculator: BatchCompatableCalculators,
+ increment: number,
+ rows: number,
+}
+
+/**
* The input pace
*/
-const input = useStorage('batch-calculator-input', {
+const input = useStorage<DistanceTime>('batch-calculator-input', {
distanceValue: 5,
- distanceUnit: 'kilometers',
+ distanceUnit: DistanceUnits.Kilometers,
time: 1200,
});
/**
* The batch input options
*/
-const options = useStorage('batch-calculator-options', {
- calculator: 'workout',
+const options = useStorage<BatchCalculatorOptions>('batch-calculator-options', {
+ calculator: BatchCompatableCalculators.Workout,
increment: 15,
rows: 20,
});
@@ -95,45 +122,46 @@ const options = useStorage('batch-calculator-options', {
/**
* The default unit system
*/
-const defaultUnitSystem = useStorage('default-unit-system', detectDefaultUnitSystem());
+const defaultUnitSystem = useStorage<UnitSystems>('default-unit-system', detectDefaultUnitSystem());
/**
* The current selected target sets for each calculator
*/
-const selectedPaceTargetSet = useStorage('pace-calculator-target-set', '_pace_targets');
-const selectedRaceTargetSet = useStorage('race-calculator-target-set', '_race_targets');
-const selectedWorkoutTargetSet = useStorage('workout-calculator-target-set', '_workout_targets');
+const selectedPaceTargetSet = useStorage<string>('pace-calculator-target-set', '_pace_targets');
+const selectedRaceTargetSet = useStorage<string>('race-calculator-target-set', '_race_targets');
+const selectedWorkoutTargetSet = useStorage<string>('workout-calculator-target-set',
+ '_workout_targets');
/**
* The target sets for each calculator
*/
-const paceTargetSets = useStorage('pace-calculator-target-sets', {
- _pace_targets: defaultTargetSets._pace_targets
+const paceTargetSets = useStorage<StandardTargetSets>('pace-calculator-target-sets', {
+ _pace_targets: defaultTargetSets._pace_targets as StandardTargetSet
});
-const raceTargetSets = useStorage('race-calculator-target-sets', {
- _race_targets: defaultTargetSets._race_targets
+const raceTargetSets = useStorage<StandardTargetSets>('race-calculator-target-sets', {
+ _race_targets: defaultTargetSets._race_targets as StandardTargetSet
});
-const workoutTargetSets = useStorage('workout-calculator-target-sets', {
- _workout_targets: defaultTargetSets._workout_targets
+const workoutTargetSets = useStorage<WorkoutTargetSets>('workout-calculator-target-sets', {
+ _workout_targets: defaultTargetSets._workout_targets as WorkoutTargetSet
});
/**
* The advanced options for each calculator
*/
-const raceOptions = useStorage('race-calculator-options', {
- model: 'AverageModel',
+const raceOptions = useStorage<RaceOptions>('race-calculator-options', {
+ model: RacePredictionModel.AverageModel,
riegelExponent: 1.06,
});
-const workoutOptions = useStorage('workout-calculator-options', {
+const workoutOptions = useStorage<WorkoutOptions>('workout-calculator-options', {
customTargetNames: false,
- model: 'AverageModel',
+ model: RacePredictionModel.AverageModel,
riegelExponent: 1.06,
});
/**
* The input distance
*/
-const inputDistance = computed(() => ({
+const inputDistance = computed<Distance>(() => ({
distanceValue: input.value.distanceValue,
distanceUnit: input.value.distanceUnit,
}));
@@ -141,8 +169,8 @@ const inputDistance = computed(() => ({
/**
* The set of input times
*/
-const inputTimes = computed(() => {
- let results = [];
+const inputTimes = computed<Array<number>>(() => {
+ const results = [];
for (let i = 0; i < options.value.rows; i++) {
results.push(input.value.time + options.value.increment * i);
}
@@ -152,23 +180,36 @@ const inputTimes = computed(() => {
/**
* The selected target set for the current calculator
*/
-const selectedTargetSet = computed({
- get: () => {
- if (options.value.calculator === 'pace') {
- return selectedPaceTargetSet.value;
- } else if (options.value.calculator === 'race') {
- return selectedRaceTargetSet.value;
- } else {
- return selectedWorkoutTargetSet.value;
+const selectedTargetSet = computed<string>({
+ get: (): string => {
+ switch (options.value.calculator) {
+ case (BatchCompatableCalculators.Pace): {
+ return selectedPaceTargetSet.value;
+ }
+ case (BatchCompatableCalculators.Race): {
+ return selectedRaceTargetSet.value;
+ }
+ default:
+ case (BatchCompatableCalculators.Workout): {
+ return selectedWorkoutTargetSet.value;
+ }
}
},
- set: (newValue) => {
- if (options.value.calculator === 'pace') {
- selectedPaceTargetSet.value = newValue;
- } else if (options.value.calculator === 'race') {
- selectedRaceTargetSet.value = newValue;
- } else {
- selectedWorkoutTargetSet.value = newValue;
+ set: (newValue: string) => {
+ switch (options.value.calculator) {
+ case (BatchCompatableCalculators.Pace): {
+ selectedPaceTargetSet.value = newValue;
+ break;
+ }
+ case (BatchCompatableCalculators.Race): {
+ selectedRaceTargetSet.value = newValue;
+ break;
+ }
+ default:
+ case (BatchCompatableCalculators.Workout): {
+ selectedWorkoutTargetSet.value = newValue;
+ break;
+ }
}
},
});
@@ -176,23 +217,36 @@ const selectedTargetSet = computed({
/**
* The target sets for the current calculator
*/
-const targetSets = computed({
+const targetSets = computed<TargetSets>({
get: () => {
- if (options.value.calculator === 'pace') {
- return paceTargetSets.value;
- } else if (options.value.calculator === 'race') {
- return raceTargetSets.value;
- } else {
- return workoutTargetSets.value;
+ switch (options.value.calculator) {
+ case (BatchCompatableCalculators.Pace): {
+ return paceTargetSets.value;
+ }
+ case (BatchCompatableCalculators.Race): {
+ return raceTargetSets.value;
+ }
+ default:
+ case (BatchCompatableCalculators.Workout): {
+ return workoutTargetSets.value;
+ }
}
},
- set: (newValue) => {
- if (options.value.calculator === 'pace') {
- paceTargetSets.value = newValue;
- } else if (options.value.calculator === 'race') {
- raceTargetSets.value = newValue;
- } else {
- workoutTargetSets.value = newValue;
+ set: (newValue: TargetSets) => {
+ switch (options.value.calculator) {
+ case (BatchCompatableCalculators.Pace): {
+ paceTargetSets.value = newValue as StandardTargetSets;
+ break;
+ }
+ case (BatchCompatableCalculators.Race): {
+ raceTargetSets.value = newValue as StandardTargetSets;
+ break;
+ }
+ default:
+ case (BatchCompatableCalculators.Workout): {
+ workoutTargetSets.value = newValue as WorkoutTargetSets;
+ break;
+ }
}
},
});
@@ -200,23 +254,36 @@ const targetSets = computed({
/**
* The advanced options for the current calculator
*/
-const advancedOptions = computed({
+const advancedOptions = computed<null | RaceOptions | WorkoutOptions>({
get: () => {
- if (options.value.calculator === 'pace') {
- return {};
- } else if (options.value.calculator === 'race') {
- return raceOptions.value;
- } else {
- return workoutOptions.value;
+ switch (options.value.calculator) {
+ case (BatchCompatableCalculators.Pace): {
+ return null;
+ }
+ case (BatchCompatableCalculators.Race): {
+ return raceOptions.value;
+ }
+ default:
+ case (BatchCompatableCalculators.Workout): {
+ return workoutOptions.value;
+ }
}
},
- set: (newValue) => {
- if (options.value.calculator === 'pace') {
- // do nothing
- } else if (options.value.calculator === 'race') {
- raceOptions.value = newValue;
- } else {
- workoutOptions.value = newValue;
+ set: (newValue: null | RaceOptions | WorkoutOptions) => {
+ switch(options.value.calculator) {
+ case (BatchCompatableCalculators.Pace): {
+ // do nothing
+ break;
+ }
+ case (BatchCompatableCalculators.Race): {
+ raceOptions.value = newValue as RaceOptions;
+ break;
+ }
+ default:
+ case (BatchCompatableCalculators.Workout): {
+ workoutOptions.value = newValue as WorkoutOptions;
+ break;
+ }
}
},
});
@@ -224,14 +291,20 @@ const advancedOptions = computed({
/**
* The appropriate calculate_results function for the current calculator
*/
-const calculateResult = computed(() => {
- if (options.value.calculator === 'pace') {
- return (x,y) => calcUtils.calculatePaceResults(x, y, defaultUnitSystem.value, false);
- } else if (options.value.calculator === 'race') {
- return (x,y) => calcUtils.calculateRaceResults(x, y, raceOptions.value, defaultUnitSystem.value,
- false);
- } else {
- return (x,y) => calcUtils.calculateWorkoutResults(x, y, workoutOptions.value, false);
+const calculateResult = computed<(x: DistanceTime, y: Target) => TargetResult>(() => {
+ switch(options.value.calculator) {
+ case (BatchCompatableCalculators.Pace): {
+ return (x,y) => calcUtils.calculatePaceResults(x, y, defaultUnitSystem.value, false);
+ }
+ case (BatchCompatableCalculators.Race): {
+ return (x,y) => calcUtils.calculateRaceResults(x, y, raceOptions.value,
+ defaultUnitSystem.value, false);
+ }
+ default:
+ case (BatchCompatableCalculators.Workout): {
+ return (x,y) => calcUtils.calculateWorkoutResults(x, y as WorkoutTarget, workoutOptions.value,
+ false);
+ }
}
});
</script>
diff --git a/src/views/PaceCalculator.vue b/src/views/PaceCalculator.vue
@@ -30,10 +30,12 @@
</div>
</template>
-<script setup>
+<script setup lang="ts">
import { calculatePaceResults } from '@/utils/calculators';
import { defaultTargetSets } from '@/utils/targets';
-import { detectDefaultUnitSystem } from '@/utils/units';
+import type { StandardTargetSets } from '@/utils/targets';
+import { DistanceUnits, UnitSystems, detectDefaultUnitSystem } from '@/utils/units';
+import type { DistanceTime } from '@/utils/units';
import PaceInput from '@/components/PaceInput.vue';
import SingleOutputTable from '@/components/SingleOutputTable.vue';
@@ -44,26 +46,26 @@ import useStorage from '@/composables/useStorage';
/**
* The input pace
*/
-const input = useStorage('pace-calculator-input', {
+const input = useStorage<DistanceTime>('pace-calculator-input', {
distanceValue: 5,
- distanceUnit: 'kilometers',
+ distanceUnit: DistanceUnits.Kilometers,
time: 1200,
});
/**
* The default unit system
*/
-const defaultUnitSystem = useStorage('default-unit-system', detectDefaultUnitSystem());
+const defaultUnitSystem = useStorage<UnitSystems>('default-unit-system', detectDefaultUnitSystem());
/**
* The current selected target set
*/
-const selectedTargetSet = useStorage('pace-calculator-target-set', '_pace_targets');
+const selectedTargetSet = useStorage<string>('pace-calculator-target-set', '_pace_targets');
/**
* The target sets
*/
-const targetSets = useStorage('pace-calculator-target-sets', {
+const targetSets = useStorage<StandardTargetSets>('pace-calculator-target-sets', {
_pace_targets: defaultTargetSets._pace_targets
});
</script>
diff --git a/src/views/RaceCalculator.vue b/src/views/RaceCalculator.vue
@@ -38,7 +38,7 @@
<target-set-selector v-model:selectedTargetSet="selectedTargetSet"
v-model:targetSets="targetSets" :default-unit-system="defaultUnitSystem"/>
</div>
- <race-options v-model="options"/>
+ <race-options-input v-model="options"/>
</details>
<h2>Equivalent Race Results</h2>
@@ -48,16 +48,20 @@
</div>
</template>
-<script setup>
+<script setup lang="ts">
import { computed } from 'vue';
import { calculateRaceResults, calculateRaceStats } from '@/utils/calculators';
+import type { RaceOptions } from '@/utils/calculators';
import { formatNumber } from '@/utils/format';
+import { RacePredictionModel } from '@/utils/races';
import { defaultTargetSets } from '@/utils/targets';
-import { detectDefaultUnitSystem } from '@/utils/units';
+import type { StandardTargetSets } from '@/utils/targets';
+import { DistanceUnits, UnitSystems, detectDefaultUnitSystem } from '@/utils/units';
+import type { DistanceTime } from '@/utils/units';
import PaceInput from '@/components/PaceInput.vue';
-import RaceOptions from '@/components/RaceOptions.vue';
+import RaceOptionsInput from '@/components/RaceOptionsInput.vue';
import SingleOutputTable from '@/components/SingleOutputTable.vue';
import TargetSetSelector from '@/components/TargetSetSelector.vue';
@@ -66,34 +70,34 @@ import useStorage from '@/composables/useStorage';
/**
* The input race
*/
-const input = useStorage('race-calculator-input', {
+const input = useStorage<DistanceTime>('race-calculator-input', {
distanceValue: 5,
- distanceUnit: 'kilometers',
+ distanceUnit: DistanceUnits.Kilometers,
time: 1200,
});
/**
* The default unit system
*/
-const defaultUnitSystem = useStorage('default-unit-system', detectDefaultUnitSystem());
+const defaultUnitSystem = useStorage<UnitSystems>('default-unit-system', detectDefaultUnitSystem());
/**
* The race prediction options
*/
-const options = useStorage('race-calculator-options', {
- model: 'AverageModel',
+const options = useStorage<RaceOptions>('race-calculator-options', {
+ model: RacePredictionModel.AverageModel,
riegelExponent: 1.06,
});
/**
* The current selected target set
*/
-const selectedTargetSet = useStorage('race-calculator-target-set', '_race_targets');
+const selectedTargetSet = useStorage<string>('race-calculator-target-set', '_race_targets');
/**
* The target sets
*/
-let targetSets = useStorage('race-calculator-target-sets', {
+const targetSets = useStorage<StandardTargetSets>('race-calculator-target-sets', {
_race_targets: defaultTargetSets._race_targets
});
diff --git a/src/views/SplitCalculator.vue b/src/views/SplitCalculator.vue
@@ -11,8 +11,9 @@
<div class="target-set">
Target Set:
- <target-set-selector v-model:selectedTargetSet="selectedTargetSet" setType="split"
- v-model:targetSets="targetSets" :default-unit-system="defaultUnitSystem"/>
+ <target-set-selector v-model:selectedTargetSet="selectedTargetSet"
+ :set-type="TargetSetTypes.Split" v-model:targetSets="targetSets"
+ :default-unit-system="defaultUnitSystem"/>
</div>
</div>
@@ -22,11 +23,13 @@
</div>
</template>
-<script setup>
+<script setup lang="ts">
import { computed } from 'vue';
import { defaultTargetSets } from '@/utils/targets';
-import { detectDefaultUnitSystem } from '@/utils/units';
+import { TargetSetTypes } from '@/utils/targets';
+import type { SplitTargetSet, SplitTargetSets } from '@/utils/targets';
+import { UnitSystems, detectDefaultUnitSystem } from '@/utils/units';
import SplitOutputTable from '@/components/SplitOutputTable.vue';
import TargetSetSelector from '@/components/TargetSetSelector.vue';
@@ -36,18 +39,18 @@ import useStorage from '@/composables/useStorage';
/**
* The default unit system
*/
-const defaultUnitSystem = useStorage('default-unit-system', detectDefaultUnitSystem());
+const defaultUnitSystem = useStorage<UnitSystems>('default-unit-system', detectDefaultUnitSystem());
/**
* The current selected target set
*/
-const selectedTargetSet = useStorage('split-calculator-target-set', '_split_targets');
+const selectedTargetSet = useStorage<string>('split-calculator-target-set', '_split_targets');
/**
* The default output targets
*/
-const targetSets = useStorage('split-calculator-target-sets', {
- _split_targets: defaultTargetSets._split_targets
+const targetSets = useStorage<SplitTargetSets>('split-calculator-target-sets', {
+ _split_targets: defaultTargetSets._split_targets as SplitTargetSet
});
/**
diff --git a/src/views/UnitCalculator.vue b/src/views/UnitCalculator.vue
@@ -6,20 +6,20 @@
<option value="speed_and_pace">Speed & Pace</option>
</select>
- <time-input v-if="getUnitType(input.inputUnit) === 'time'" class="input-value"
+ <time-input v-if="isTimeUnit(input.inputUnit)" class="input-value"
label="Input time" v-model="input.inputValue"/>
<decimal-input v-else class="input-value" aria-label="Input value"
v-model="input.inputValue" :min="0" :digits="2"/>
<select v-model="input.inputUnit" class="input-units" aria-label="Input units">
<option v-for="(value, key) in units" :key="key" :value="key">
- {{ value.name }}
+ {{ value?.name }}
</option>
</select>
<span class="equals"> = </span>
- <span v-if="getUnitType(input.outputUnit) === 'time'" class="output-value" aria-label="Output value">
+ <span v-if="isTimeUnit(input.outputUnit)" class="output-value" aria-label="Output value">
{{ formatDuration(outputValue, 6, 3, true) }}
</span>
<span v-else class="output-value" aria-label="Output value">
@@ -28,18 +28,19 @@
<select v-model="input.outputUnit" class="output-units" aria-label="Output units">
<option v-for="(value, key) in units" :key="key" :value="key">
- {{ value.name }}
+ {{ value?.name }}
</option>
</select>
</div>
</template>
-<script setup>
+<script setup lang="ts">
import { computed, ref } from 'vue';
import { formatDuration, formatNumber } from '@/utils/format';
-import { DistanceUnitData, TimeUnitData, SpeedUnitData, PaceUnitData, convertDistance, convertTime,
-convertSpeedPace } from '@/utils/units';
+import { DistanceUnits, DistanceUnitData, TimeUnits, TimeUnitData, PaceUnits, SpeedUnits, SpeedUnitData,
+ PaceUnitData, convertDistance, convertTime, convertSpeedPace } from '@/utils/units';
+import type { SpeedPaceUnits, UnitData } from '@/utils/units';
import DecimalInput from '@/components/DecimalInput.vue';
import TimeInput from '@/components/TimeInput.vue';
@@ -47,116 +48,166 @@ import TimeInput from '@/components/TimeInput.vue';
import useStorage from '@/composables/useStorage';
/**
+ * The supported time units: Hours, Minutes, Seconds, and 'hh:mm:ss'
+ */
+type ExtendedTimeUnits = TimeUnits | 'hh:mm:ss';
+
+/**
+ * All supported distance, time, speed, and pace units
+ */
+type AllUnits = DistanceUnits | ExtendedTimeUnits | SpeedPaceUnits;
+
+/**
+ * The three categories of units
+ */
+enum UnitTypes {
+ Distance = 'distance',
+ Time = 'time',
+ SpeedPace = 'speed_and_pace',
+}
+
+/**
+ * The type of the calculator inputs
+ */
+interface UnitCalculatorInputs {
+ [UnitTypes.Distance]: {
+ inputValue: number,
+ inputUnit: DistanceUnits,
+ outputUnit: DistanceUnits,
+ },
+ [UnitTypes.Time]: {
+ inputValue: number,
+ inputUnit: ExtendedTimeUnits,
+ outputUnit: ExtendedTimeUnits,
+ },
+ [UnitTypes.SpeedPace]: {
+ inputValue: number,
+ inputUnit: SpeedPaceUnits,
+ outputUnit: SpeedPaceUnits,
+ },
+};
+
+/**
* The calculator inputs
*/
-const inputs = useStorage('unit-calculator-inputs', {
- distance: {
+const inputs = useStorage<UnitCalculatorInputs>('unit-calculator-inputs', {
+ [UnitTypes.Distance]: {
inputValue: 1,
- inputUnit: 'miles',
- outputUnit: 'kilometers',
+ inputUnit: DistanceUnits.Miles,
+ outputUnit: DistanceUnits.Kilometers,
},
- time: {
+ [UnitTypes.Time]: {
inputValue: 1,
- inputUnit: 'seconds',
+ inputUnit: TimeUnits.Seconds,
outputUnit: 'hh:mm:ss',
},
- speed_and_pace: {
+ [UnitTypes.SpeedPace]: {
inputValue: 600,
- inputUnit: 'seconds_per_mile',
- outputUnit: 'miles_per_hour',
+ inputUnit: PaceUnits.TimePerMile,
+ outputUnit: SpeedUnits.MilesPerHour,
},
});
/**
* The unit category
*/
-const category = ref('distance');
+const category = ref<UnitTypes>(UnitTypes.Distance);
/**
* The inputs for the current category
*/
-const input = computed({
+const input = computed<{ inputValue: number, inputUnit: AllUnits, outputUnit: AllUnits }>({
get() {
return inputs.value[category.value];
},
set(newValue) {
- inputs.value[category.value] = newValue;
- },
+ switch (category.value) {
+ default:
+ case UnitTypes.Distance: {
+ inputs.value[category.value] = {
+ inputValue: newValue.inputValue,
+ inputUnit: newValue.inputUnit as DistanceUnits,
+ outputUnit: newValue.outputUnit as DistanceUnits,
+ };
+ break;
+ }
+ case UnitTypes.Time: {
+ inputs.value[category.value] = {
+ inputValue: newValue.inputValue,
+ inputUnit: newValue.inputUnit as ExtendedTimeUnits,
+ outputUnit: newValue.outputUnit as ExtendedTimeUnits,
+ };
+ break;
+ }
+ case UnitTypes.SpeedPace: {
+ inputs.value[category.value] = {
+ inputValue: newValue.inputValue,
+ inputUnit: newValue.inputUnit as SpeedPaceUnits,
+ outputUnit: newValue.outputUnit as SpeedPaceUnits,
+ };
+ break;
+ }
+ }
+ }
});
/**
* The names of the units in the current category
*/
-const units = computed(() => {
+const units = computed<{ [key in AllUnits]?: UnitData }>(() => {
switch (category.value) {
- case 'distance': {
+ default:
+ case UnitTypes.Distance: {
return DistanceUnitData;
}
- case 'time': {
+ case UnitTypes.Time: {
return {
...TimeUnitData,
'hh:mm:ss': {
name: 'hh:mm:ss',
symbol: '',
- value: null,
+ value: 1,
},
};
}
- case 'speed_and_pace': {
+ case UnitTypes.SpeedPace: {
return { ...PaceUnitData, ...SpeedUnitData };
}
- default: {
- return {};
- }
}
});
/**
* The output value
*/
-const outputValue = computed(() => {
+const outputValue = computed<number>(() => {
switch (category.value) {
- case 'distance': {
- return convertDistance(input.value.inputValue, input.value.inputUnit,
- input.value.outputUnit);
+ default:
+ case UnitTypes.Distance: {
+ return convertDistance(input.value.inputValue, input.value.inputUnit as DistanceUnits,
+ input.value.outputUnit as DistanceUnits);
}
- case 'time': {
+ case UnitTypes.Time: {
// Correct input and output units for 'hh:mm:ss' unit
const realInput = input.value.inputUnit === 'hh:mm:ss' ? 'seconds' : input.value.inputUnit;
const realOutput = input.value.outputUnit === 'hh:mm:ss' ? 'seconds' : input.value.outputUnit;
// Calculate conversion
- return convertTime(input.value.inputValue, realInput, realOutput);
- }
- case 'speed_and_pace': {
- return convertSpeedPace(input.value.inputValue, input.value.inputUnit,
- input.value.outputUnit);
+ return convertTime(input.value.inputValue, realInput as TimeUnits, realOutput as TimeUnits);
}
- default: {
- return null;
+ case UnitTypes.SpeedPace: {
+ return convertSpeedPace(input.value.inputValue, input.value.inputUnit as SpeedPaceUnits,
+ input.value.outputUnit as SpeedPaceUnits);
}
}
});
/**
- * Get the type of a unit
- * @param {String} unit The unit
- * @returns {String} The type ('decimal' or 'time')
+ * Determine whether a unit should be represented as a time
+ * @param {AllUnits} unit The unit
+ * @returns {boolean} Whether the unit should be represented as a time
*/
-function getUnitType(unit) {
- if (unit in DistanceUnitData) {
- return 'decimal';
- }
- if (unit in TimeUnitData) {
- return 'decimal';
- }
- if (unit === 'hh:mm:ss') {
- return 'time';
- }
- if (['seconds_per_kilometer', 'seconds_per_mile'].includes(unit)) {
- return 'time';
- }
- return 'decimal';
+function isTimeUnit(unit: AllUnits): boolean {
+ return unit in PaceUnitData || unit === 'hh:mm:ss';
}
</script>
diff --git a/src/views/WorkoutCalculator.vue b/src/views/WorkoutCalculator.vue
@@ -18,9 +18,9 @@
</div>
<div>
Target Set:
- <target-set-selector v-model:selectedTargetSet="selectedTargetSet" setType="workout"
- :customWorkoutNames="options.customTargetNames" v-model:targetSets="targetSets"
- :default-unit-system="defaultUnitSystem"/>
+ <target-set-selector v-model:selectedTargetSet="selectedTargetSet"
+ :set-type="TargetSetTypes.Workout" :customWorkoutNames="options.customTargetNames"
+ v-model:targetSets="targetSets" :default-unit-system="defaultUnitSystem"/>
</div>
<div>
Target Name Customization:
@@ -29,23 +29,27 @@
<option :value="true">Enabled</option>
</select>
</div>
- <race-options v-model="options"/>
+ <race-options-input v-model="options"/>
</details>
<h2>Workout Splits</h2>
<single-output-table class="output"
- :calculate-result="x => calculateWorkoutResults(input, x, options, true)"
+ :calculate-result="x => calculateWorkoutResults(input, x as WorkoutTarget, options, true)"
:targets="targetSets[selectedTargetSet] ? targetSets[selectedTargetSet].targets : []"/>
</div>
</template>
-<script setup>
+<script setup lang="ts">
import { calculateWorkoutResults } from '@/utils/calculators';
-import { defaultTargetSets } from '@/utils/targets';
-import { detectDefaultUnitSystem } from '@/utils/units';
+import type { WorkoutOptions } from '@/utils/calculators';
+import { RacePredictionModel } from '@/utils/races';
+import { TargetSetTypes, defaultTargetSets } from '@/utils/targets';
+import type { WorkoutTarget, WorkoutTargetSet, WorkoutTargetSets } from '@/utils/targets';
+import { DistanceUnits, UnitSystems, detectDefaultUnitSystem } from '@/utils/units';
+import type { DistanceTime } from '@/utils/units';
import PaceInput from '@/components/PaceInput.vue';
-import RaceOptions from '@/components/RaceOptions.vue';
+import RaceOptionsInput from '@/components/RaceOptionsInput.vue';
import SingleOutputTable from '@/components/SingleOutputTable.vue';
import TargetSetSelector from '@/components/TargetSetSelector.vue';
@@ -54,36 +58,36 @@ import useStorage from '@/composables/useStorage';
/**
* The input race
*/
-const input = useStorage('workout-calculator-input', {
+const input = useStorage<DistanceTime>('workout-calculator-input', {
distanceValue: 5,
- distanceUnit: 'kilometers',
+ distanceUnit: DistanceUnits.Kilometers,
time: 1200,
});
/**
* The default unit system
*/
-const defaultUnitSystem = useStorage('default-unit-system', detectDefaultUnitSystem());
+const defaultUnitSystem = useStorage<UnitSystems>('default-unit-system', detectDefaultUnitSystem());
/**
* The race prediction options
*/
-const options = useStorage('workout-calculator-options', {
+const options = useStorage<WorkoutOptions>('workout-calculator-options', {
customTargetNames: false,
- model: 'AverageModel',
+ model: RacePredictionModel.AverageModel,
riegelExponent: 1.06,
});
/**
* The current selected target set
*/
-const selectedTargetSet = useStorage('workout-calculator-target-set', '_workout_targets');
+const selectedTargetSet = useStorage<string>('workout-calculator-target-set', '_workout_targets');
/**
* The target sets
*/
-let targetSets = useStorage('workout-calculator-target-sets', {
- _workout_targets: defaultTargetSets._workout_targets
+const targetSets = useStorage<WorkoutTargetSets>('workout-calculator-target-sets', {
+ _workout_targets: defaultTargetSets._workout_targets as WorkoutTargetSet
});
</script>
diff --git a/tests/unit/components/RaceOptions.spec.js b/tests/unit/components/RaceOptions.spec.js
@@ -1,53 +0,0 @@
-import { test, expect } from 'vitest';
-import { shallowMount } from '@vue/test-utils';
-import RaceOptions from '@/components/RaceOptions.vue';
-
-test('should be initialized to modelValue', () => {
- // Initialize component
- const wrapper = shallowMount(RaceOptions, {
- propsData: {
- modelValue: {
- model: 'PurdyPointsModel',
- riegelExponent: 1.2,
- }
- },
- });
-
- // Assert input fields are correct
- expect(wrapper.find('select').element.value).to.equal('PurdyPointsModel');
- expect(wrapper.findComponent({ name: 'decimal-input' }).vm.modelValue).to.equal(1.2);
-});
-
-test('should emit event when inputs are modified', async () => {
- // Initialize component
- const wrapper = shallowMount(RaceOptions, {
- propsData: {
- modelValue: {
- model: 'AverageModel',
- riegelExponent: 1.06,
- },
- },
- });
-
- // Update model
- await wrapper.find('select').setValue('CameronModel');
- expect(wrapper.emitted()['update:modelValue']).to.deep.equal([
- [{
- model: 'CameronModel',
- riegelExponent: 1.06,
- }],
- ]);
-
- // Update Riegel exponent
- await wrapper.findComponent({ name: 'decimal-input' }).setValue(1.3);
- expect(wrapper.emitted()['update:modelValue']).to.deep.equal([
- [{
- model: 'CameronModel',
- riegelExponent: 1.06,
- }],
- [{
- model: 'CameronModel',
- riegelExponent: 1.3,
- }],
- ]);
-});
diff --git a/tests/unit/components/RaceOptionsInput.spec.js b/tests/unit/components/RaceOptionsInput.spec.js
@@ -0,0 +1,53 @@
+import { test, expect } from 'vitest';
+import { shallowMount } from '@vue/test-utils';
+import RaceOptionsInput from '@/components/RaceOptionsInput.vue';
+
+test('should be initialized to modelValue', () => {
+ // Initialize component
+ const wrapper = shallowMount(RaceOptionsInput, {
+ propsData: {
+ modelValue: {
+ model: 'PurdyPointsModel',
+ riegelExponent: 1.2,
+ }
+ },
+ });
+
+ // Assert input fields are correct
+ expect(wrapper.find('select').element.value).to.equal('PurdyPointsModel');
+ expect(wrapper.findComponent({ name: 'decimal-input' }).vm.modelValue).to.equal(1.2);
+});
+
+test('should emit event when inputs are modified', async () => {
+ // Initialize component
+ const wrapper = shallowMount(RaceOptionsInput, {
+ propsData: {
+ modelValue: {
+ model: 'AverageModel',
+ riegelExponent: 1.06,
+ },
+ },
+ });
+
+ // Update model
+ await wrapper.find('select').setValue('CameronModel');
+ expect(wrapper.emitted()['update:modelValue']).to.deep.equal([
+ [{
+ model: 'CameronModel',
+ riegelExponent: 1.06,
+ }],
+ ]);
+
+ // Update Riegel exponent
+ await wrapper.findComponent({ name: 'decimal-input' }).setValue(1.3);
+ expect(wrapper.emitted()['update:modelValue']).to.deep.equal([
+ [{
+ model: 'CameronModel',
+ riegelExponent: 1.06,
+ }],
+ [{
+ model: 'CameronModel',
+ riegelExponent: 1.3,
+ }],
+ ]);
+});
diff --git a/tests/unit/views/BatchCalculator.spec.js b/tests/unit/views/BatchCalculator.spec.js
@@ -298,14 +298,14 @@ test('should load advanced model options from localStorage', async () => {
// Assert race prediction options are loaded
await wrapper.find('select[aria-label="Calculator"]').setValue('race');
- expect(wrapper.findComponent({ name: 'RaceOptions' }).vm.modelValue).to.deep.equal({
+ expect(wrapper.findComponent({ name: 'RaceOptionsInput' }).vm.modelValue).to.deep.equal({
model: 'PurdyPointsModel',
riegelExponent: 1.2,
});
// Assert workout prediction options are loaded
await wrapper.find('select[aria-label="Calculator"]').setValue('workout');
- expect(wrapper.findComponent({ name: 'RaceOptions' }).vm.modelValue).to.deep.equal({
+ expect(wrapper.findComponent({ name: 'RaceOptionsInput' }).vm.modelValue).to.deep.equal({
model: 'RiegelModel',
riegelExponent: 1.1,
});
diff --git a/tests/unit/views/RaceCalculator.spec.js b/tests/unit/views/RaceCalculator.spec.js
@@ -135,7 +135,7 @@ test('should correctly calculate results according to advanced model options', a
});
// Switch model
- await wrapper.findComponent({ name: 'RaceOptions' }).setValue({
+ await wrapper.findComponent({ name: 'RaceOptionsInput' }).setValue({
model: 'RiegelModel',
riegelExponent: 1.06, // default value
});
@@ -152,7 +152,7 @@ test('should correctly calculate results according to advanced model options', a
expect(result.value).to.equal('41:41.92');
// Update Riegel Exponent
- await wrapper.findComponent({ name: 'RaceOptions' }).setValue({
+ await wrapper.findComponent({ name: 'RaceOptionsInput' }).setValue({
model: 'RiegelModel', // existing value
riegelExponent: 1,
});
@@ -275,7 +275,7 @@ test('should load advanced model options from localStorage', async () => {
const wrapper = shallowMount(RaceCalculator);
// Assert data loaded
- expect(wrapper.findComponent({ name: 'RaceOptions' }).vm.modelValue).to.deep.equal({
+ expect(wrapper.findComponent({ name: 'RaceOptionsInput' }).vm.modelValue).to.deep.equal({
model: 'PurdyPointsModel',
riegelExponent: 1.2,
});
@@ -286,7 +286,7 @@ test('should save advanced model options to localStorage when modified', async (
const wrapper = shallowMount(RaceCalculator);
// Update advanced model options
- await wrapper.findComponent({ name: 'RaceOptions' }).setValue({
+ await wrapper.findComponent({ name: 'RaceOptionsInput' }).setValue({
model: 'CameronModel',
riegelExponent: 1.30,
});
diff --git a/tests/unit/views/WorkoutCalculator.spec.js b/tests/unit/views/WorkoutCalculator.spec.js
@@ -65,7 +65,7 @@ test('should correctly calculate results according to advanced model options', a
});
// Update model and Riegel Exponent
- await wrapper.findComponent({ name: 'RaceOptions' }).setValue({
+ await wrapper.findComponent({ name: 'RaceOptionsInput' }).setValue({
model: 'RiegelModel',
riegelExponent: 1.10,
});
@@ -215,7 +215,7 @@ test('should load advanced model options from localStorage', async () => {
const wrapper = shallowMount(WorkoutCalculator);
// Assert data loaded
- expect(wrapper.findComponent({ name: 'RaceOptions' }).vm.modelValue).to.deep.equal({
+ expect(wrapper.findComponent({ name: 'RaceOptionsInput' }).vm.modelValue).to.deep.equal({
model: 'PurdyPointsModel',
riegelExponent: 1.2,
});
@@ -226,7 +226,7 @@ test('should save advanced model options to localStorage when modified', async (
const wrapper = shallowMount(WorkoutCalculator);
// Update advanced model options
- await wrapper.findComponent({ name: 'RaceOptions' }).setValue({
+ await wrapper.findComponent({ name: 'RaceOptionsInput' }).setValue({
model: 'CameronModel',
riegelExponent: 1.30,
});
diff --git a/tsconfig.app.json b/tsconfig.app.json
@@ -1,11 +1,14 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
- "include": ["src/**/*", "src/**/*.vue"],
+ "include": ["src/**/*"],
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+ "resolveJsonModule": true,
"paths": {
- "@/*": ["./src/*"]
- }
+ "@/*": ["./src/*"],
+ "/*": ["./*"]
+ },
+ "types": ["node"]
}
}