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 & 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>