Browse Source

fixes number sorting and typing

main
koehr 4 months ago
parent
commit
9936b71049
  1. 8
      src/components/PlanetDetails.vue
  2. 36
      src/components/SortableTable.vue
  3. 10
      src/components/TableOptions.vue
  4. 73
      src/composables/useAPI.ts
  5. 6
      src/composables/useNumberFormatting.ts
  6. 19
      src/types.d.ts
  7. 3
      src/util.ts

8
src/components/PlanetDetails.vue

@ -17,8 +17,9 @@ const climateMap = { @@ -17,8 +17,9 @@ const climateMap = {
superheated: '#CC6',
frigid: '#AAF',
}
type TClimate = keyof typeof climateMap;
function climateToColor (climate: string): string {
function climateToColor (climate: TClimate): string {
return climateMap[climate] || 'black'
}
@ -27,13 +28,14 @@ const planetStyle = computed(() => { @@ -27,13 +28,14 @@ const planetStyle = computed(() => {
let unknown = true
if (props.planet.diameter !== 'unknown') {
diameterPerc = (props.planet.diameter / MAX_PLANET_SIZE) * 100
const diameter = parseFloat(props.planet.diameter.replace(',', ''))
diameterPerc = (diameter / MAX_PLANET_SIZE) * 100
if (diameterPerc < 10) diameterPerc = 10
unknown = false
}
const climate = props.planet.climate.split(', ')[0]
return {
backgroundColor: climateToColor(climate),
backgroundColor: climateToColor(climate as TClimate),
width: `${diameterPerc}%`,
height: `${diameterPerc}%`,
borderStyle: unknown ? 'dashed' : 'solid',

36
src/components/SortableTable.vue

@ -13,7 +13,7 @@ const { @@ -13,7 +13,7 @@ const {
limit = 10,
loading = true,
} = defineProps<{
entries: GenericData[];
entries: IPerson[];
fields: IFieldDefinition[];
filter: string;
limit: number;
@ -32,13 +32,13 @@ function setSortBy (field: string) { @@ -32,13 +32,13 @@ function setSortBy (field: string) {
sortBy.value = field
}
function formatValue (value: string|number, format: string) {
function formatValue (value: string, format: string): string {
switch (format) {
case 'height': return formatHeight(value)
case 'mass': return formatMass(value)
case 'date': return formatDate(value)
// id is actually an URL like https://swapi.api/people/3/
case 'id': return getIdFromUrl(value)
case 'id': return `${getIdFromUrl(value)}`
default: return value
}
}
@ -48,22 +48,28 @@ const sortedEntries = computed<IPerson[]>(() => { @@ -48,22 +48,28 @@ const sortedEntries = computed<IPerson[]>(() => {
copy.sort((a, b) => {
const key = sortBy.value
const keyType = fields.find(field => field.field === key)
let va = a[key]
let vb = b[key]
const va = a[key]
const vb = b[key]
switch (keyType.format) {
switch (keyType?.format) {
case 'id':
va = getIdFromUrl(va)
vb - getIdFromUrl(vb)
return va - vb
const ida = getIdFromUrl(va as string)
const idb = getIdFromUrl(vb as string)
return ida - idb
case 'date': // same for iso date strings
case 'string':
return va.localeCompare(vb, [], { sensitivity: 'base' })
return (va as string).localeCompare((vb as string), [], { sensitivity: 'base' })
case 'height':
case 'mass':
return parseFloat(va.replace(',', '')) - parseFloat(vb.replace(',', ''))
const ma = (va as string).replace(',', '')
const mb = (vb as string).replace(',', '')
if (ma === 'unknown') return 1 // sort "unknowns" last
if (mb === 'unknown') return -1
return parseFloat(ma) - parseFloat(mb)
case 'link': // assuming a planet object, might still be a string though // TODO
return va.name.localeCompare(vb.name, [], { sensitivity: 'base' })
const pa = ((va as unknown) as IPlanet).name // thank you typescript for
const pb = ((vb as unknown) as IPlanet).name // bringing beauty into my life
return pa.localeCompare(pb, [], { sensitivity: 'base' })
default:
return 0 // whatever this might be
}
@ -127,11 +133,13 @@ function closeModal () { @@ -127,11 +133,13 @@ function closeModal () {
<div class="loading-indicator" />
</template>
<template v-else>
<button @click="selectPlanet(entry[field])">{{ entry[field].name }}</button>
<button @click="selectPlanet(entry[field] as IPlanet)">
{{ (entry[field] as IPlanet).name }}
</button>
</template>
</template>
<template v-else>
{{ formatValue(entry[field], format) }}
{{ formatValue(entry[field] as string, format) }}
</template>
</td>
</tr>

10
src/components/TableOptions.vue

@ -6,12 +6,14 @@ const { filter = '', limit = 100 } = defineProps<{ @@ -6,12 +6,14 @@ const { filter = '', limit = 100 } = defineProps<{
const emit = defineEmits(['update:filter', 'update:limit'])
function updateFilter (event) {
emit('update:filter', event.target.value)
function updateFilter (event: Event) {
const target = event.target as HTMLInputElement
emit('update:filter', target.value)
}
function updateLimit (event) {
emit('update:limit', parseInt(event.target.value))
function updateLimit (event: Event) {
const target = event.target as HTMLSelectElement
emit('update:limit', parseInt(target.value))
}
</script>

73
src/composables/useAPI.ts

@ -5,7 +5,12 @@ type KnownCategories = 'people'|'planets'; @@ -5,7 +5,12 @@ type KnownCategories = 'people'|'planets';
const API_PATH = 'https://swapi.dev/api/'
export class HTTPError extends Error {
constructor (response: Response) {
name: 'HTTPError'
status: number
statusText: string
url: string
constructor (response: { status: number; statusText: string; url: string; }) {
const { status, statusText, url } = response
super(`Fetching ${url} failed with status ${status}`)
@ -27,7 +32,7 @@ function paramsToString (params: GenericData) { @@ -27,7 +32,7 @@ function paramsToString (params: GenericData) {
*
* returns fetched data in JSON format or HTTPError
*/
async function fetchFromApi (url): IPlanet|IPerson[]|HTTPError {
async function fetchFromApi (url: string): Promise<IPlanet|IPeopleResponse|HTTPError> {
let response
try {
@ -58,31 +63,46 @@ async function fetchFromApi (url): IPlanet|IPerson[]|HTTPError { @@ -58,31 +63,46 @@ async function fetchFromApi (url): IPlanet|IPerson[]|HTTPError {
*
* returns fetched data in JSON format or HTTPError
*/
async function fetchByCategory (category: KnownCategories, id = '', params = {}): IPlanet|IPerson[] {
async function fetchByCategory (category: KnownCategories, id = '', params = {}): Promise<IPlanet|IPeopleResponse|HTTPError> {
const paramString = paramsToString(params)
const url = `${API_PATH}${category}/${id}${paramString}`
const response = fetchFromApi(url)
const response = await fetchFromApi(url)
return response
}
const cache = reactive({
type CachedData = {
error: boolean;
loading: boolean;
people: IPerson[];
planets: { [key: number]: IPlanet };
}
const cache = reactive<CachedData>({
error: false, // generic error state. TODO: is that enough?
loading: false, // still fetching?
people: [], // people are filled in batches of 10 (SWAPI default)
planets: {}, // use object here to avoid a sparse array
})
async function fetchPlanet (url: string): IPlanet {
/* fetchPlanet, fetches a planet by its URL
* url: string, the API URL to the planet
*
* returns IPlanet or HTTPError
*/
async function fetchPlanet (url: string): Promise<IPlanet|HTTPError> {
const id = getIdFromUrl(url)
// Planet already fetched, yeah!
if (cache.planets[id] !== undefined) return cache.planets[id]
// Planet not fetched yet
const response = fetchFromApi(url)
cache.planets[id] = response
const response = await fetchFromApi(url)
if (response instanceof HTTPError) {
console.error('unhandled HTTPError', response)
}
cache.planets[id] = response as IPlanet // TODO: breaks when network request failed
return response
return response as IPlanet
}
/* fetchPeoplesHomes, fetches all missing planets from given list of people
@ -90,12 +110,13 @@ async function fetchPlanet (url: string): IPlanet { @@ -90,12 +110,13 @@ async function fetchPlanet (url: string): IPlanet {
*
* doesn't return but instead works in the shadows
*/
async function fetchPeoplesHomes (people: IPerson[]) {
async function fetchPeoplesHomes (people: IPerson[]): Promise<undefined> {
for (let person of people) {
if (typeof person.homeworld === 'string') {
person.homeworld = await fetchPlanet(person.homeworld)
person.homeworld = await fetchPlanet(person.homeworld) as IPlanet
}
}
return
}
/* fetchAllPeople, recursively fetches all entries from SWAPI/people
@ -103,7 +124,7 @@ async function fetchPeoplesHomes (people: IPerson[]) { @@ -103,7 +124,7 @@ async function fetchPeoplesHomes (people: IPerson[]) {
*
* returns first batch of people
*/
async function fetchAllPeople (page: number = 1): IPerson[]|HTTPError {
async function fetchAllPeople (page: number = 1): Promise<IPerson[]|HTTPError> {
// avoid being called twice unless it is inside the recursive chain
if (cache.loading && page === 1) return cache.people
@ -120,41 +141,27 @@ async function fetchAllPeople (page: number = 1): IPerson[]|HTTPError { @@ -120,41 +141,27 @@ async function fetchAllPeople (page: number = 1): IPerson[]|HTTPError {
cache.error = false // looks like we overcame the error
}
const newPeople: IPerson[] = response.results
const peopleResponse = response as IPeopleResponse
const newPeople: IPerson[] = peopleResponse.results
// store result in cache to make component code simpler
cache.people = [...cache.people, ...newPeople]
fetchPeoplesHomes(newPeople)
if (response.next) { // more people to fetch
if (peopleResponse.next) { // more people to fetch
fetchAllPeople(page + 1)
} else {
cache.loading = false // all done!
}
return response.results
}
/* fetch all people from SWAPI
*
* returns fetched data in JSON format or, in case the request failed, a HTTPError object
*/
async function getPeople (): IPerson[]|HTTPError {
if (cache.people.length > 0) return cache.people // yeah, cached already
const people = await fetchAllPeople()
if (people instanceof HTTPError) return people
return cache.people
return peopleResponse.results
}
/* useAPI
* prefetch: boolean, fetch people immediately, defaults to true
* no parameters
*
* returns {
async getPlanet (id: number): IPlanet|HTTPError
async getPeople (): IPerson[]|HTTPError
* }
* returns CachedData
*/
export default function useAPI () {
export default function useAPI (): CachedData {
fetchAllPeople() // start fetching immediately
return cache
}

6
src/composables/useNumberFormatting.ts

@ -3,14 +3,14 @@ export default function useNumberFormatting ( @@ -3,14 +3,14 @@ export default function useNumberFormatting (
unit?: string,
style: string = 'unit',
locales: string[] = [],
): (value: number) => string {
): (value: string) => string {
const formatter = new Intl.NumberFormat(locales, { style, unit, unitDisplay: 'narrow' })
return (value: string|number): string => {
return (value: string): string => {
if (typeof value === 'string') value = value.replace(',', '')
const numberValue = parseFloat(value)
if (isNaN(numberValue)) return `${value}`
return formatter.format(value)
return formatter.format(numberValue)
}
}

19
src/types.d.ts vendored

@ -1,15 +1,9 @@ @@ -1,15 +1,9 @@
interface IPeople {
[key: string]: IPerson;
}
interface IPlanets {
[key: string]: IPlanet;
}
interface IPerson {
// allow string keys, like person[key]
[key: string]: string|string[]|Date|IPlanet;
birth_year: string;
eye_color: string;
films: string[] | IFilm[];
films: string[];
gender: string;
hair_color: string;
height: string;
@ -25,6 +19,13 @@ interface IPerson { @@ -25,6 +19,13 @@ interface IPerson {
vehicles: string[];
}
interface IPeopleResponse {
count: number;
next: string|null;
prev: string|null;
results: IPerson[];
}
interface IPlanet {
climate: string;
created: Date;

3
src/util.ts

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
export function getIdFromUrl (url: string): number {
// remove leading slash to avoid empty last element
const parts = url.replace(/\/$/, '').split('/')
return parts[parts.length - 1]
const id = parseInt(parts[parts.length - 1])
return id
}

Loading…
Cancel
Save