running-tools

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

commit 583c500e154d0a25e26c579e4760bdadae4235ba
parent e4436e7e7951ecb5b60731401729bc7d87f1eb68
Author: ashermorgan <59518073+ashermorgan@users.noreply.github.com>
Date:   Sun, 22 Aug 2021 13:57:24 -0700

Implement TimeTable component

Diffstat:
Asrc/components/TimeTable.vue | 222+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/views/PaceCalculator.vue | 197+++++++++++--------------------------------------------------------------------
Mtests/unit/PaceCalculator.spec.js | 55++++++++++---------------------------------------------
Atests/unit/TimeTable.spec.js | 34++++++++++++++++++++++++++++++++++
4 files changed, 292 insertions(+), 216 deletions(-)

diff --git a/src/components/TimeTable.vue b/src/components/TimeTable.vue @@ -0,0 +1,222 @@ +<template> + <div class="time-table"> + <table class="results" v-show="!inEditMode"> + <thead> + <tr> + <th colspan="2">Distance</th> + + <th>Time</th> + + <th> + <button class="icon" title="Edit Targets" @click="inEditMode=true"> + <img alt="" src="@/assets/edit.svg"> + </button> + </th> + </tr> + </thead> + + <tbody> + <tr v-for="(item, index) in results" :key="index"> + <td> + {{ item.distanceValue.toFixed(2) }} + {{ distanceSymbols[item.distanceUnit] }} + </td> + + <td>in</td> + + <td colspan="2"> + {{ formatDuration(item.time, 0, 2) }} + </td> + </tr> + + <tr v-if="results.length === 0" class="empty-message"> + <td colspan="4"> + There aren't any targets,<br> + click + <img alt="Edit Targets" src="@/assets/edit.svg"> + to add one + </td> + </tr> + </tbody> + </table> + + <table class="targets" v-show="inEditMode"> + <thead> + <tr> + <th>Edit Targets</th> + + <th> + <button class="icon" title="Close" @click="inEditMode=false"> + <img alt="" src="@/assets/x.svg"> + </button> + </th> + </tr> + </thead> + + <tbody> + <tr v-for="(item, index) in targets" :key="index"> + <td> + <decimal-input v-model="item.distanceValue" aria-label="Distance Value" + :min="0" :digits="2"/> + <select v-model="item.distanceUnit" aria-label="Distance Unit"> + <option v-for="(value, key) in distanceUnits" :key="key" :value="key"> + {{ value }} + </option> + </select> + </td> + + <td> + <button class="icon" title="Remove Target" @click="targets.splice(index, 1)"> + <img alt="" src="@/assets/trash-2.svg"> + </button> + </td> + </tr> + + <tr v-if="targets.length === 0" class="empty-message"> + <td colspan="4"> + There aren't any targets,<br> + click + <img alt="Add Target" src="@/assets/plus-circle.svg"> + to add one + </td> + </tr> + + <tr class="add-target"> + <td colspan="4"> + <button class="icon" title="Add Target" @click="targets.push({distanceValue: 1, + distanceUnit: 'miles'})"> + <img alt="" src="@/assets/plus-circle.svg"> + </button> + </td> + </tr> + </tbody> + </table> + </div> +</template> + +<script> +import unitUtils from '@/utils/units'; + +import DecimalInput from '@/components/DecimalInput.vue'; + +export default { + name: 'TimeTable', + + components: { + DecimalInput, + }, + + props: { + /** + * The method that generates the time table rows + */ + calculateResult: { + type: Function, + required: true, + }, + + /** + * The default time table targets + */ + defaultTargets: { + type: Array, + default: () => [], + }, + }, + + data() { + return { + /** + * The names of the distance units + */ + distanceUnits: unitUtils.DISTANCE_UNIT_NAMES, + + /** + * The symbols of the distance units + */ + distanceSymbols: unitUtils.DISTANCE_UNIT_SYMBOLS, + + /** + * The formatDuration method + */ + formatDuration: unitUtils.formatDuration, + + /** + * Whether the table is in edit mode + */ + inEditMode: false, + + /** + * The time table targets + */ + targets: this.defaultTargets, + }; + }, + + computed: { + /** + * The time table results + */ + results() { + // Calculate results + const result = []; + this.targets.forEach((row) => { + // Add result + result.push(this.calculateResult(row)); + }); + + // Sort results by time + result.sort((a, b) => a.time - b.time); + + // Return results + return result; + }, + }, +}; +</script> + +<style scoped> +/* time table */ +.results th:last-child { + text-align: right; +} + +/* edit targets table */ +.targets th:last-child, .targets td:last-child { + text-align: right; +} +.targets td select { + margin-left: 0.2em; +} +.targets .add-target td { + text-align: center; + padding: 0.5em 0.2em; +} + +/* general table styles */ +table { + border-collapse: collapse; + min-width: 300px; + width: 100%; + text-align: left; +} +table tr { + border: 0.1em solid #000000; +} +table th, table td { + padding: 0.2em; +} +table button.icon { + height: 2em; + width: 2em; +} + +/* empty table message */ +.empty-message td { + text-align: center !important; +} +.empty-message img { + height: 1em; + width: 1em; +} +</style> diff --git a/src/views/PaceCalculator.vue b/src/views/PaceCalculator.vue @@ -15,94 +15,7 @@ <p>is the same pace as running</p> - <table class="output" v-if="!inEditMode"> - <thead> - <tr> - <th colspan="2">Distance</th> - <th>Time</th> - <th> - <button class="icon" title="Edit Targets" @click="inEditMode=true"> - <img alt="" src="@/assets/edit.svg"> - </button> - </th> - </tr> - </thead> - <tbody> - <tr v-for="(item, index) in results" :key="index"> - <td> - {{ item.distanceValue.toFixed(2) }} - {{ distanceSymbols[item.distanceUnit] }} - </td> - - <td>in</td> - - <td colspan="2"> - {{ formatDuration(item.time, 0, 2) }} - </td> - </tr> - - <tr v-if="results.length === 0" class="empty-message"> - <td colspan="4"> - There aren't any targets,<br> - click - <img alt="Edit Targets" src="@/assets/edit.svg"> - to add one - </td> - </tr> - </tbody> - </table> - - <table class="edit-targets" v-if="inEditMode"> - <thead> - <tr> - <th> - Edit Targets - </th> - <th> - <button class="icon" title="Close" @click="inEditMode=false"> - <img alt="" src="@/assets/x.svg"> - </button> - </th> - </tr> - </thead> - <tbody> - <tr v-for="(item, index) in targets" :key="index"> - <td> - <decimal-input v-model="item.distanceValue" aria-label="Distance Value" - :min="0" :digits="2"/> - <select v-model="item.distanceUnit" aria-label="Distance Unit"> - <option v-for="(value, key) in distanceUnits" :key="key" :value="key"> - {{ value }} - </option> - </select> - </td> - - <td> - <button class="icon" title="Remove Target" @click="targets.splice(index, 1)"> - <img alt="" src="@/assets/trash-2.svg"> - </button> - </td> - </tr> - - <tr v-if="targets.length === 0" class="empty-message"> - <td colspan="4"> - There aren't any targets,<br> - click - <img alt="Add Target" src="@/assets/plus-circle.svg"> - to add one - </td> - </tr> - - <tr class="add-target"> - <td colspan="4"> - <button class="icon" title="Add Target" @click="targets.push({distanceValue: 1, - distanceUnit: 'miles'})"> - <img alt="" src="@/assets/plus-circle.svg"> - </button> - </td> - </tr> - </tbody> - </table> + <time-table class="output" :calculate-result="calculatePace" :default-targets="defaultTargets"/> </div> </template> @@ -112,6 +25,7 @@ import unitUtils from '@/utils/units'; import DecimalInput from '@/components/DecimalInput.vue'; import TimeInput from '@/components/TimeInput.vue'; +import TimeTable from '@/components/TimeTable.vue'; export default { name: 'PaceCalculator', @@ -119,6 +33,7 @@ export default { components: { DecimalInput, TimeInput, + TimeTable, }, data() { @@ -144,24 +59,9 @@ export default { distanceUnits: unitUtils.DISTANCE_UNIT_NAMES, /** - * The symbols of the distance units + * The default output targets */ - distanceSymbols: unitUtils.DISTANCE_UNIT_SYMBOLS, - - /** - * The formatDuration method - */ - formatDuration: unitUtils.formatDuration, - - /** - * Whether the calculator is in edit targets mode - */ - inEditMode: false, - - /** - * The output targets - */ - targets: [ + defaultTargets: [ { distanceValue: 100, distanceUnit: 'meters' }, { distanceValue: 200, distanceUnit: 'meters' }, { distanceValue: 300, distanceUnit: 'meters' }, @@ -204,34 +104,28 @@ export default { this.inputUnit, unitUtils.DISTANCE_UNITS.meters); return paceUtils.getPace(distance, this.inputTime); }, + }, + methods: { /** - * The output results + * Calculate paces from a target + * @param {Object} target The target + * @returns {Object} The result */ - results() { - // Calculate results - const result = []; - this.targets.forEach((row) => { - // Convert distance into meters - const distance = unitUtils.convertDistance(row.distanceValue, - row.distanceUnit, unitUtils.DISTANCE_UNITS.meters); - - // Calculate time to travel distance at input pace - const time = paceUtils.getTime(this.pace, distance); - - // Add result - result.push({ - distanceValue: row.distanceValue, - distanceUnit: row.distanceUnit, - time, - }); - }); - - // Sort results by time - result.sort((a, b) => a.time - b.time); - - // Return results - return result; + calculatePace(target) { + // Convert distance into meters + const distance = unitUtils.convertDistance(target.distanceValue, target.distanceUnit, + unitUtils.DISTANCE_UNITS.meters); + + // Calculate time to travel distance at input pace + const time = paceUtils.getTime(this.pace, distance); + + // Return result + return { + distanceValue: target.distanceValue, + distanceUnit: target.distanceUnit, + time, + }; }, }, }; @@ -258,52 +152,13 @@ export default { } /* calculator output */ -.output th:last-child { - text-align: right; -} - -/* edit targets table */ -.edit-targets th:last-child, .edit-targets td:last-child { - text-align: right; -} -.edit-targets td select { - margin-left: 0.2em; -} -.edit-targets .add-target td { - text-align: center; - padding: 0.5em 0.2em; -} - -/* general table styles */ -table { +.output { margin-top: 10px; - border-collapse: collapse; - min-width: 300px; - text-align: left; -} -table tr { - border: 0.1em solid #000000; -} -table th, table td { - padding: 0.2em; -} -table button.icon { - height: 2em; - width: 2em; } @media only screen and (max-width: 500px) { - table { + .output { width: 100%; min-width: 0px; } } - -/* empty table message */ -.empty-message td { - text-align: center !important; -} -.empty-message img { - height: 1em; - width: 1em; -} </style> diff --git a/tests/unit/PaceCalculator.spec.js b/tests/unit/PaceCalculator.spec.js @@ -5,7 +5,7 @@ import { shallowMount } from '@vue/test-utils'; import PaceCalculator from '@/views/PaceCalculator.vue'; describe('PaceCalculator.vue', () => { - it('results should be correct', async () => { + it('should correctly calculate paces', async () => { // Initialize component const wrapper = shallowMount(PaceCalculator); @@ -16,52 +16,17 @@ describe('PaceCalculator.vue', () => { inputTime: 100, }); - // Override targets - await wrapper.setData({ - targets: [ - { distanceValue: 10, distanceUnit: 'meters' }, - { distanceValue: 20, distanceUnit: 'meters' }, - { distanceValue: 100, distanceUnit: 'meters' }, - { distanceValue: 1, distanceUnit: 'kilometers' }, - ], + // Calculate paces + const result = wrapper.vm.calculatePace({ + distanceValue: 20, + distanceUnit: 'meters', }); - // Assert results are correct - expect(wrapper.vm._computedWatchers.results.value).to.deep.equal([ - { distanceValue: 10, distanceUnit: 'meters', time: 1 }, - { distanceValue: 20, distanceUnit: 'meters', time: 2 }, - { distanceValue: 100, distanceUnit: 'meters', time: 10 }, - { distanceValue: 1, distanceUnit: 'kilometers', time: 100 }, - ]); - }); - - it('results should be sorted by time', async () => { - // Initialize component - const wrapper = shallowMount(PaceCalculator); - - // Override input values - await wrapper.setData({ - inputDistance: 1, - inputUnit: 'kilometers', - inputTime: 100, + // Assert result is correct + expect(result).to.deep.equal({ + distanceValue: 20, + distanceUnit: 'meters', + time: 2, }); - - // Override targets - await wrapper.setData({ - targets: [ - { distanceValue: 20, distanceUnit: 'meters' }, - { distanceValue: 100, distanceUnit: 'meters' }, - { distanceValue: 1, distanceUnit: 'kilometers' }, - { distanceValue: 10, distanceUnit: 'meters' }, - ], - }); - - // Assert results are correct - expect(wrapper.vm._computedWatchers.results.value).to.deep.equal([ - { distanceValue: 10, distanceUnit: 'meters', time: 1 }, - { distanceValue: 20, distanceUnit: 'meters', time: 2 }, - { distanceValue: 100, distanceUnit: 'meters', time: 10 }, - { distanceValue: 1, distanceUnit: 'kilometers', time: 100 }, - ]); }); }); diff --git a/tests/unit/TimeTable.spec.js b/tests/unit/TimeTable.spec.js @@ -0,0 +1,34 @@ +/* eslint-disable no-underscore-dangle */ + +import { expect } from 'chai'; +import { shallowMount } from '@vue/test-utils'; +import TimeTable from '@/components/TimeTable.vue'; + +describe('TimeTable.vue', () => { + it('results should be correct and sorted by time', () => { + // Initialize component + const wrapper = shallowMount(TimeTable, { + propsData: { + calculateResult: (row) => ({ + distanceValue: row.distanceValue, + distanceUnit: row.distanceUnit, + time: row.distanceValue + 1, + }), + defaultTargets: [ + { distanceValue: 20, distanceUnit: 'meters' }, + { distanceValue: 100, distanceUnit: 'meters' }, + { distanceValue: 1, distanceUnit: 'kilometers' }, + { distanceValue: 10, distanceUnit: 'meters' }, + ], + }, + }); + + // Assert results are correct + expect(wrapper.vm._computedWatchers.results.value).to.deep.equal([ + { distanceValue: 1, distanceUnit: 'kilometers', time: 2 }, + { distanceValue: 10, distanceUnit: 'meters', time: 11 }, + { distanceValue: 20, distanceUnit: 'meters', time: 21 }, + { distanceValue: 100, distanceUnit: 'meters', time: 101 }, + ]); + }); +});