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:
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,
};