running-tools

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

UnitCalculator.vue (7368B)


      1 <template>
      2   <div class="unit-calculator">
      3     <select class="category" v-model="category" aria-label="Selected unit category">
      4       <option value="distance">Distance</option>
      5       <option value="time">Time</option>
      6       <option value="speed_and_pace">Speed &amp; Pace</option>
      7     </select>
      8 
      9     <time-input v-if="isTimeUnit(input.inputUnit)" class="input-value"
     10       label="Input time" v-model="input.inputValue"/>
     11     <decimal-input v-else class="input-value" aria-label="Input value"
     12       v-model="input.inputValue" :min="0" :digits="2"/>
     13 
     14     <select v-model="input.inputUnit" class="input-units" aria-label="Input units">
     15       <option v-for="(value, key) in categoryUnits" :key="key" :value="key">
     16         {{ value?.name }}
     17       </option>
     18     </select>
     19 
     20     <span class="equals"> = </span>
     21 
     22     <span v-if="isTimeUnit(input.outputUnit)" class="output-value" aria-label="Output value">
     23       {{ units.formatDuration(outputValue, 6, 3, true) }}
     24     </span>
     25     <span v-else class="output-value" aria-label="Output value">
     26       {{ units.formatNumber(outputValue, 0, 3, true) }}
     27     </span>
     28 
     29     <select v-model="input.outputUnit" class="output-units" aria-label="Output units">
     30       <option v-for="(value, key) in categoryUnits" :key="key" :value="key">
     31         {{ value?.name }}
     32       </option>
     33     </select>
     34   </div>
     35 </template>
     36 
     37 <script setup lang="ts">
     38 import { computed } from 'vue';
     39 
     40 import * as units from '@/core/units';
     41 
     42 import DecimalInput from '@/components/DecimalInput.vue';
     43 import TimeInput from '@/components/TimeInput.vue';
     44 
     45 import useStorage from '@/composables/useStorage';
     46 
     47 /*
     48  * The three categories of units
     49  */
     50 enum UnitTypes {
     51   Distance = 'distance',
     52   Time = 'time',
     53   SpeedPace = 'speed_and_pace',
     54 }
     55 
     56 /*
     57  * The supported time units: Hours, Minutes, Seconds, and 'hh:mm:ss'
     58  */
     59 type ExtendedTimeUnits = units.TimeUnits | 'hh:mm:ss';
     60 
     61 /*
     62  * The support units from all categories
     63  */
     64 type AllUnits = units.DistanceUnits | ExtendedTimeUnits | units.SpeedPaceUnits;
     65 
     66 /*
     67  * The type of the calculator inputs
     68  */
     69 interface UnitCalculatorInputs {
     70   [UnitTypes.Distance]: {
     71     inputValue: number,
     72     inputUnit: units.DistanceUnits,
     73     outputUnit: units.DistanceUnits,
     74   },
     75   [UnitTypes.Time]: {
     76     inputValue: number,
     77     inputUnit: ExtendedTimeUnits,
     78     outputUnit: ExtendedTimeUnits,
     79   },
     80   [UnitTypes.SpeedPace]: {
     81     inputValue: number,
     82     inputUnit: units.SpeedPaceUnits,
     83     outputUnit: units.SpeedPaceUnits,
     84   },
     85 };
     86 
     87 /*
     88  * The calculator inputs
     89  */
     90 const inputs = useStorage<UnitCalculatorInputs>('unit-calculator-inputs', {
     91   [UnitTypes.Distance]: {
     92     inputValue: 1,
     93     inputUnit: units.DistanceUnits.Miles,
     94     outputUnit: units.DistanceUnits.Kilometers,
     95   },
     96   [UnitTypes.Time]: {
     97     inputValue: 1,
     98     inputUnit: units.TimeUnits.Seconds,
     99     outputUnit: 'hh:mm:ss',
    100   },
    101   [UnitTypes.SpeedPace]: {
    102     inputValue: 600,
    103     inputUnit: units.PaceUnits.TimePerMile,
    104     outputUnit: units.SpeedUnits.MilesPerHour,
    105   },
    106 });
    107 
    108 /*
    109  * The unit category
    110  */
    111 const category = useStorage<UnitTypes>('unit-calculator-category', UnitTypes.Distance);
    112 
    113 /*
    114  * The inputs for the current category
    115  */
    116 const input = computed<{ inputValue: number, inputUnit: AllUnits, outputUnit: AllUnits }>({
    117   get() {
    118     return inputs.value[category.value];
    119   },
    120   set(newValue) {
    121     switch (category.value) {
    122       default:
    123       case UnitTypes.Distance: {
    124         inputs.value[category.value] = {
    125           inputValue: newValue.inputValue,
    126           inputUnit: newValue.inputUnit as units.DistanceUnits,
    127           outputUnit: newValue.outputUnit as units.DistanceUnits,
    128         };
    129         break;
    130       }
    131       case UnitTypes.Time: {
    132         inputs.value[category.value] = {
    133           inputValue: newValue.inputValue,
    134           inputUnit: newValue.inputUnit as ExtendedTimeUnits,
    135           outputUnit: newValue.outputUnit as ExtendedTimeUnits,
    136         };
    137         break;
    138       }
    139       case UnitTypes.SpeedPace: {
    140         inputs.value[category.value] = {
    141           inputValue: newValue.inputValue,
    142           inputUnit: newValue.inputUnit as units.SpeedPaceUnits,
    143           outputUnit: newValue.outputUnit as units.SpeedPaceUnits,
    144         };
    145         break;
    146       }
    147     }
    148   },
    149 });
    150 
    151 /*
    152  * The data for the units in the current category
    153  */
    154 const categoryUnits = computed<{ [key in AllUnits]?: units.UnitData }>(() => {
    155   switch (category.value) {
    156     default:
    157     case UnitTypes.Distance: {
    158       return units.DistanceUnitData;
    159     }
    160     case UnitTypes.Time: {
    161       return {
    162         ...units.TimeUnitData,
    163         'hh:mm:ss': {
    164           name: 'hh:mm:ss',
    165           symbol: '',
    166           value: 1,
    167         },
    168       };
    169     }
    170     case UnitTypes.SpeedPace: {
    171       return { ...units.PaceUnitData, ...units.SpeedUnitData };
    172     }
    173   }
    174 });
    175 
    176 /*
    177  * The output value
    178  */
    179 const outputValue = computed<number>(() => {
    180   switch (category.value) {
    181     default:
    182     case UnitTypes.Distance: {
    183       return units.convertDistance(input.value.inputValue,
    184         input.value.inputUnit as units.DistanceUnits,
    185         input.value.outputUnit as units.DistanceUnits);
    186     }
    187     case UnitTypes.Time: {
    188       // Correct input and output units for 'hh:mm:ss' unit
    189       const realInput = input.value.inputUnit === 'hh:mm:ss' ? 'seconds' : input.value.inputUnit;
    190       const realOutput = input.value.outputUnit === 'hh:mm:ss' ? 'seconds' : input.value.outputUnit;
    191 
    192       // Calculate conversion
    193       return units.convertTime(input.value.inputValue, realInput as units.TimeUnits,
    194         realOutput as units.TimeUnits);
    195     }
    196     case UnitTypes.SpeedPace: {
    197       return units.convertSpeedPace(input.value.inputValue,
    198         input.value.inputUnit as units.SpeedPaceUnits,
    199         input.value.outputUnit as units.SpeedPaceUnits);
    200     }
    201   }
    202 });
    203 
    204 /**
    205  * Determine whether a unit should be represented as a time
    206  * @param {AllUnits} unit The unit
    207  * @returns {boolean} Whether the unit should be represented as a time
    208  */
    209 function isTimeUnit(unit: AllUnits): boolean {
    210   return unit in units.PaceUnitData || unit === 'hh:mm:ss';
    211 }
    212 </script>
    213 
    214 <style scoped>
    215 .unit-calculator {
    216   margin: 0px auto;
    217   display: grid;
    218   grid-template-columns: 1fr auto 1fr;
    219   grid-template-rows: auto auto auto;
    220   width: 450px;
    221   grid-gap: 0.4em;
    222 }
    223 .unit-calculator .category {
    224   grid-row: 1;
    225   grid-column: 1 / 4;
    226 }
    227 .unit-calculator .input-value {
    228   grid-row: 2;
    229   grid-column: 1;
    230 
    231   width: 100%;
    232   text-align: center;
    233 }
    234 .unit-calculator .input-units {
    235   grid-row: 3;
    236   grid-column: 1;
    237 }
    238 .unit-calculator .equals {
    239   grid-row: 2 / 4;
    240   grid-column: 2;
    241 
    242   text-align: center;
    243   padding: 0em 0.5em;
    244   font-size: 2em;
    245 }
    246 .unit-calculator .output-value {
    247   grid-row: 2;
    248   grid-column: 3;
    249 
    250   width: 100%;
    251   text-align: center;
    252 }
    253 .unit-calculator .output-units {
    254   grid-row: 3;
    255   grid-column: 3;
    256 }
    257 
    258 @media only screen and (max-width: 500px) {
    259   /* switch to mobile friendly layout */
    260   .unit-calculator {
    261     grid-template-columns: 1fr;
    262     grid-template-rows: auto auto auto auto auto auto;
    263     width: 100%;
    264   }
    265   .unit-calculator * {
    266     grid-column: 1 !important;
    267   }
    268   .unit-calculator .category {
    269     grid-row: 1;
    270   }
    271   .unit-calculator .input-value {
    272     grid-row: 2;
    273   }
    274   .unit-calculator .input-units {
    275     grid-row: 3;
    276   }
    277   .unit-calculator .equals {
    278     grid-row: 4;
    279   }
    280   .unit-calculator .output-value {
    281     grid-row: 5;
    282   }
    283   .unit-calculator .output-units {
    284     grid-row: 6;
    285   }
    286 }
    287 </style>