running-tools

A collection of tools for runners and their coaches
git clone https://git.ashermorgan.net/running-tools/
Log | Files | Refs | README

commit bfd85b24ed19608c23f7a2d4e9f5c2e7fd516ed6
parent 77a65ef6ecf0e5683914bd467e4aa99b83c2e656
Author: Asher Morgan <59518073+ashermorgan@users.noreply.github.com>
Date:   Sat, 28 Jun 2025 12:31:00 -0700

Convert remaining components to TypeScript

Diffstat:
Msrc/components/SplitOutputTable.vue | 42+++++++++++++++++++++++++++---------------
Msrc/components/TargetEditor.vue | 84+++++++++++++++++++++++++++++++++++++------------------------------------------
Msrc/components/TargetSetSelector.vue | 46+++++++++++++++++++++++-----------------------
Msrc/utils/targets.ts | 50+++++++++++++++++++++++++++++++++++++++++++++-----
4 files changed, 134 insertions(+), 88 deletions(-)

diff --git a/src/components/SplitOutputTable.vue b/src/components/SplitOutputTable.vue @@ -46,43 +46,55 @@ </table> </template> -<script setup> +<script setup lang="ts"> import { computed } from 'vue'; +import type { PropType } from 'vue'; import { formatDuration, formatNumber } from '@/utils/format'; -import { DistanceUnitData, convertDistance, getDefaultDistanceUnit } from '@/utils/units'; +import type { SplitTarget } from '@/utils/targets'; +import { DistanceUnits, DistanceUnitData, UnitSystems, convertDistance, + getDefaultDistanceUnit } from '@/utils/units'; import TimeInput from '@/components/TimeInput.vue'; import useObjectModel from '@/composables/useObjectModel'; -const props = defineProps({ +interface SplitTargetResult { + distance: number, + distanceValue: number, + distanceUnit: DistanceUnits, + time: number, + splitTime: number, + pace: number, +}; + +interface Props { /** * The unit system to use when showing result paces */ - defaultUnitSystem: { - type: String, - default: 'metric', - }, + defaultUnitSystem?: UnitSystems, /** * The split targets */ - modelValue: { - type: Array, - default: () => [], - }, + modelValue?: Array<SplitTarget>, +}; + +const props = withDefaults(defineProps<Props>(), { + defaultUnitSystem: UnitSystems.Metric, + modelValue: [], }); // Generate internal ref tied to modelValue prop const emit = defineEmits(['update:modelValue']); -const model = useObjectModel(() => props.modelValue, (x) => emit('update:modelValue', x)); +const model = useObjectModel<Array<SplitTarget>>(() => props.modelValue, + (x) => emit('update:modelValue', x)); /** * The target table results */ const results = computed(() => { // Initialize results array - const results = []; + const results: Array<SplitTargetResult> = []; for (let i = 0; i < model.value.length; i += 1) { // Calculate split and total times @@ -92,12 +104,12 @@ const results = computed(() => { // Calculate split and total distances const totalDistance = convertDistance( model.value[i].distanceValue, - model.value[i].distanceUnit, 'meters', + model.value[i].distanceUnit, DistanceUnits.Meters, ); const splitDistance = i === 0 ? totalDistance : totalDistance - results[i - 1].distance; // Calculate pace - const pace = splitTime / convertDistance(splitDistance, 'meters', + const pace = splitTime / convertDistance(splitDistance, DistanceUnits.Meters, getDefaultDistanceUnit(props.defaultUnitSystem)); // Add row to results array diff --git a/src/components/TargetEditor.vue b/src/components/TargetEditor.vue @@ -24,14 +24,14 @@ <tr v-for="(item, index) in model.targets" :key="index"> <td> <span v-if="setType === 'workout' && customWorkoutNames"> - <input v-model="item.customName" :placeholder="workoutTargetToString(item)" - aria-label="Custom target name"/>: + <input v-model="(item as WorkoutTarget).customName" aria-label="Custom target name" + :placeholder="workoutTargetToString(item as WorkoutTarget)"/>: </span> <span v-if="setType === 'workout'"> - <decimal-input v-model="item.splitValue" aria-label="Split distance value" - :min="0" :digits="2"/> - <select v-model="item.splitUnit" aria-label="Split distance unit"> + <decimal-input v-model="(item as WorkoutTarget).splitValue" + aria-label="Split distance value" :min="0" :digits="2"/> + <select v-model="(item as WorkoutTarget).splitUnit" aria-label="Split distance unit"> <option v-for="(value, key) in DistanceUnitData" :key="key" :value="key"> {{ value.name }} </option> @@ -86,82 +86,76 @@ </table> </template> -<script setup> +<script setup lang="ts"> import VueFeather from 'vue-feather'; +import type { PropType } from 'vue'; -import { workoutTargetToString } from '@/utils/targets'; -import { DistanceUnitData, getDefaultDistanceUnit } from '@/utils/units'; +import { TargetType, TargetSetType, workoutTargetToString } from '@/utils/targets'; +import type { StandardTargetSet, TargetSet, WorkoutTarget, WorkoutTargetSet } from '@/utils/targets'; +import { DistanceUnitData, UnitSystems, getDefaultDistanceUnit } from '@/utils/units'; import DecimalInput from '@/components/DecimalInput.vue'; import TimeInput from '@/components/TimeInput.vue'; import useObjectModel from '@/composables/useObjectModel'; -const props = defineProps({ +interface Props { /** * Whether to allow custom names for workout targets */ - customWorkoutNames: { - type: Boolean, - default: false, - }, - + customWorkoutNames?: boolean, /** * The unit system to use when creating distance targets */ - defaultUnitSystem: { - type: String, - default: 'metric', - }, + defaultUnitSystem?: UnitSystems, /** * Whether the target set is a custom or default set */ - isCustomSet: { - type: Boolean, - default: false, - }, + isCustomSet?: boolean, /** * The component value */ - modelValue: { - type: Object, - default: () => ({ - name: 'New target set', - targets: [], - }), - }, + modelValue?: TargetSet, /** * The target set type ('standard', 'split', or 'workout') */ - setType: { - type: String, - default: 'standard' + setType?: TargetSetType, +} + +const props = withDefaults(defineProps<Props>(), { + customWorkoutNames: false, + defaultUnitSystem: UnitSystems.Metric, + isCustomSet: false, + modelValue: { + name: 'New target set', + targets: [], }, + setType: TargetSetType.Standard, }); // Declare emitted events const emit = defineEmits(['close', 'revert', 'update:modelValue']); // Generate internal ref tied to modelValue prop -const model = useObjectModel(() => props.modelValue, (x) => emit('update:modelValue', x)); +const model = useObjectModel<TargetSet>(() => props.modelValue, (x) => emit('update:modelValue', x)); /** * Add a new distance based target */ function addDistanceTarget() { - if (props.setType === 'workout') { - model.value.targets.push({ - type: 'distance', + if (props.setType === TargetSetType.Workout) { + (model.value as WorkoutTargetSet).targets.push({ + type: TargetType.Distance, distanceValue: 1, distanceUnit: getDefaultDistanceUnit(props.defaultUnitSystem), splitValue: 1, splitUnit: getDefaultDistanceUnit(props.defaultUnitSystem), }); } else { - model.value.targets.push({ - type: 'distance', + (model.value as StandardTargetSet).targets.push({ + type: TargetType.Distance, distanceValue: 1, distanceUnit: getDefaultDistanceUnit(props.defaultUnitSystem), }); @@ -172,16 +166,16 @@ function addDistanceTarget() { * Add a new time based target */ function addTimeTarget() { - if (props.setType === 'workout') { - model.value.targets.push({ - type: 'time', + if (props.setType === TargetSetType.Workout) { + (model.value as WorkoutTargetSet).targets.push({ + type: TargetType.Time, time: 600, splitValue: 1, splitUnit: getDefaultDistanceUnit(props.defaultUnitSystem), }); } else { - model.value.targets.push({ - type: 'time', + (model.value as StandardTargetSet).targets.push({ + type: TargetType.Time, time: 600, }); } @@ -189,9 +183,9 @@ function addTimeTarget() { /** * Remove a target - * @param {Number} index The index of the target + * @param {number} index The index of the target */ -function removeTarget(index) { +function removeTarget(index: number) { model.value.targets.splice(index, 1); } </script> diff --git a/src/components/TargetSetSelector.vue b/src/components/TargetSetSelector.vue @@ -20,12 +20,16 @@ </span> </template> -<script setup> +<script setup lang="ts"> import { computed, nextTick, ref } from 'vue'; +import type { PropType } from 'vue'; import VueFeather from 'vue-feather'; -import { sort, defaultTargetSets } from '@/utils/targets'; +import { deepCopy } from '@/utils/misc'; +import { TargetSetType, sort, defaultTargetSets } from '@/utils/targets'; +import type { TargetSet, TargetSets } from '@/utils/targets'; +import { UnitSystems } from '@/utils/units'; import TargetEditor from '@/components/TargetEditor.vue'; import useObjectModel from '@/composables/useObjectModel'; @@ -38,48 +42,44 @@ const model = defineModel('selectedTargetSet', { default: '_new', }); -const props = defineProps({ +interface Props { /** * Whether to allow custom names for workout targets */ - customWorkoutNames: { - type: Boolean, - default: false, - }, + customWorkoutNames?: Boolean, /** * The unit system to use when creating distance targets */ - defaultUnitSystem: { - type: String, - default: 'metric', - }, + defaultUnitSystem?: UnitSystems, /** * The target set type ('standard', 'split', or 'workout') */ - setType: { - type: String, - default: 'standard' - }, + setType?: TargetSetType, /** * The target sets */ - targetSets: { - type: Object, - default: () => ({}), - }, + targetSets?: TargetSets, +}; + + +const props = withDefaults(defineProps<Props>(), { + customWorkoutNames: false, + defaultUnitSystem: UnitSystems.Metric, + setType: TargetSetType.Standard, + targetSets: {} }); // Generate internal ref tied to modelValue prop const emit = defineEmits(['update:targetSets']); -const targetSets = useObjectModel(() => props.targetSets, (x) => emit('update:targetSets', x)); +const targetSets = useObjectModel<TargetSets>(() => props.targetSets, (x) => emit('update:targetSets', x)); /** * The dialog element */ -const dialogElement = ref(null); +const dialogElement = ref(); /** * The internal value @@ -91,7 +91,7 @@ const internalValue = computed({ } return model.value; }, - set: async (newValue) => { + set: async (newValue: string) => { if (newValue == '_new') { await nextTick(); // <select> won't update if value changed immediately newTargetSet(); @@ -134,7 +134,7 @@ function revertTargetSet() { if (internalValue.value.startsWith('_')) { // Revert default set targetSets.value[internalValue.value] = - JSON.parse(JSON.stringify(defaultTargetSets[internalValue.value])); + deepCopy<TargetSet>(defaultTargetSets[internalValue.value]); sortTargetSet(); } else { // Remove custom set diff --git a/src/utils/targets.ts b/src/utils/targets.ts @@ -40,6 +40,13 @@ export interface StandardTargetSet { } /* + * Type for a collection of pace and race calculator target sets + */ +export interface StandardTargetSets { + [key: string]: StandardTargetSet, +} + +/* * Type for split calculator targets */ export type SplitTarget = DistanceTarget & { @@ -55,6 +62,13 @@ export interface SplitTargetSet { } /* + * Type for a collection of split calculator target sets + */ +export interface SplitTargetSets { + [key: string]: SplitTargetSet, +} + +/* * Type for workout calculator targets */ export type WorkoutTarget = StandardTarget & { @@ -72,10 +86,36 @@ export interface WorkoutTargetSet { } /* + * Type for a collection of workout calculator target sets + */ +export interface WorkoutTargetSets { + [key: string]: WorkoutTargetSet, +} + +/* + * Enumeration for the three types of targets sets: standard (pace & race), split, and workout + */ +export enum TargetSetType { + Standard = 'standard', + Split = 'split', + Workout = 'workout', +}; + +/* * Type for generic targets */ export type Target = StandardTarget | SplitTarget | WorkoutTarget; +/* + * Type for generic target sets + */ +export type TargetSet = StandardTargetSet | SplitTargetSet | WorkoutTargetSet; + +/* + * Type for generic collection of target sets + */ +export type TargetSets = StandardTargetSets | SplitTargetSets | WorkoutTargetSets; + /** * Sort an array of targets * @param {Array<Target>} targets The array of targets @@ -217,9 +257,9 @@ const common_workout_targets: WorkoutTargetSet = { ], }; -export const defaultTargetSets = { - _pace_targets: common_pace_targets, - _race_targets: common_race_targets, - _split_targets: five_k_mile_splits, - _workout_targets: common_workout_targets, +export const defaultTargetSets: TargetSets = { + '_pace_targets': common_pace_targets, + '_race_targets': common_race_targets, + '_split_targets': five_k_mile_splits, + '_workout_targets': common_workout_targets, };