Browse Source

adds api access

main
koehr 4 months ago
parent
commit
38e1ab60f2
  1. 99
      src/App.vue
  2. 5
      src/app.css
  3. 140
      src/composables/useAPI.ts
  4. 10
      src/types.d.ts
  5. 3
      tsconfig.json
  6. 1
      vite.config.ts

99
src/App.vue

@ -1,93 +1,10 @@ @@ -1,93 +1,10 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import TableOptions from './components/TableOptions.vue'
import SortableTable from './components/SortableTable.vue'
import TableOptions from '!component/TableOptions.vue'
import SortableTable from '!component/SortableTable.vue'
import useAPI, { HTTPError } from '!composable/useAPI'
const apiResult: IPeople = {
'people/1': {
"name": "Luke Skywalker",
"height": "172",
"mass": "77",
"hair_color": "blond",
"skin_color": "fair",
"eye_color": "blue",
"birth_year": "19BBY",
"gender": "male",
"homeworld": "https://swapi.dev/api/planets/1/",
"films": [
"https://swapi.dev/api/films/1/",
"https://swapi.dev/api/films/2/",
"https://swapi.dev/api/films/3/",
"https://swapi.dev/api/films/6/"
],
"species": [],
"vehicles": [
"https://swapi.dev/api/vehicles/14/",
"https://swapi.dev/api/vehicles/30/"
],
"starships": [
"https://swapi.dev/api/starships/12/",
"https://swapi.dev/api/starships/22/"
],
"created": "2014-12-09T13:50:51.644000Z",
"edited": "2014-12-20T21:17:56.891000Z",
"url": "https://swapi.dev/api/people/1/"
},
'people/2': {
"name": "C-3PO",
"height": "167",
"mass": "75",
"hair_color": "n/a",
"skin_color": "gold",
"eye_color": "yellow",
"birth_year": "112BBY",
"gender": "n/a",
"homeworld": "https://swapi.dev/api/planets/1/",
"films": [
"https://swapi.dev/api/films/1/",
"https://swapi.dev/api/films/2/",
"https://swapi.dev/api/films/3/",
"https://swapi.dev/api/films/4/",
"https://swapi.dev/api/films/5/",
"https://swapi.dev/api/films/6/"
],
"species": [
"https://swapi.dev/api/species/2/"
],
"vehicles": [],
"starships": [],
"created": "2014-12-10T15:10:51.357000Z",
"edited": "2014-12-20T21:17:50.309000Z",
"url": "https://swapi.dev/api/people/2/"
},
'people/3': {
"name": "R2-D2",
"height": "96",
"mass": "32",
"hair_color": "n/a",
"skin_color": "white, blue",
"eye_color": "red",
"birth_year": "33BBY",
"gender": "n/a",
"homeworld": "https://swapi.dev/api/planets/8/",
"films": [
"https://swapi.dev/api/films/1/",
"https://swapi.dev/api/films/2/",
"https://swapi.dev/api/films/3/",
"https://swapi.dev/api/films/4/",
"https://swapi.dev/api/films/5/",
"https://swapi.dev/api/films/6/"
],
"species": [
"https://swapi.dev/api/species/2/"
],
"vehicles": [],
"starships": [],
"created": "2014-12-10T15:11:50.376000Z",
"edited": "2014-12-20T21:17:50.311000Z",
"url": "https://swapi.dev/api/people/3/"
}
}
const dataset = useAPI()
const fields: IFieldDefinition[] = [
{ field: 'url', label: 'ID', format: 'id' },
@ -102,9 +19,9 @@ const fields: IFieldDefinition[] = [ @@ -102,9 +19,9 @@ const fields: IFieldDefinition[] = [
const limit = ref(10)
const sortBy = ref('id')
const filter = ref('')
const data = computed<IPerson[]>(() => {
const people = Object.values(apiResult)
return people.filter(person => {
const people = computed<IPerson[]>(() => {
return dataset.people.filter(person => {
const name = person.name.toLowerCase()
const filter_ = filter.value.toLowerCase()
return name.indexOf(filter_) >= 0
@ -115,5 +32,5 @@ const data = computed<IPerson[]>(() => { @@ -115,5 +32,5 @@ const data = computed<IPerson[]>(() => {
<template>
<h1>Star Wars Characters</h1>
<TableOptions v-model:filter="filter" v-model:limit="limit" />
<SortableTable v-bind="{ data, limit, fields }" v-model:sortBy="sortBy" />
<SortableTable v-bind="{ data: people, limit, fields }" v-model:sortBy="sortBy" />
</template>

5
src/app.css

@ -55,6 +55,11 @@ input, select { @@ -55,6 +55,11 @@ input, select {
border: 1px solid var(--page-fg);
padding: .5em 1em;
color: var(--hilight);
font-weight: bold;
}
input::placeholder {
color: var(--page-fg);
opacity: .5;
}
input:focus {
outline: 1px solid var(--hilight);

140
src/composables/useAPI.ts

@ -0,0 +1,140 @@ @@ -0,0 +1,140 @@
import { reactive } from 'vue'
type KnownCategories = 'people'|'planets';
const API_PATH = 'https://swapi.dev/api/'
export class HTTPError extends Error {
constructor (response: Response) {
const { status, statusText, url } = response
super(`Fetching ${url} failed with status ${status}`)
this.name = 'HTTPError'
this.status = status
this.statusText = statusText
this.url = url
}
}
function paramsToString (params: GenericData) {
const keys = Object.keys(params)
const pairs = keys.map(key => `${key}=${params[key]}`)
return `?${pairs.join('&')}`
}
/* fetch item(s) from SWAPI
* category: string "people" or "planets"
* id: optional string if a single item is to be requested
*
* returns fetched data in JSON format
* throws HTTPError when request fails
*/
async function fetchFromApi (category: KnownCategories, id = '', params = {}): IPlanet|IPerson[] {
const paramString = paramsToString(params)
const url = `${API_PATH}${category}/${id}${paramString}`
let response
try {
response = await fetch(url)
} catch (err) {
// fetch throws very rarely, only in cases where the browser probably
// already intervenes but in case we catch one of the rare exceptions
// we generate a HTTPError, because implementing yet another Error type
// for such a rare occasion is not worth the effort (in this application)
console.error('Network Error', err)
throw new HTTPError({
status: 500,
statusText: 'Network Error, even if it looks like a server issue, it is not.',
url,
})
}
// Much more common: HTTP error codes
if (!response.ok) throw new HTTPError(response)
const jsonData = await response.json()
return jsonData
}
const cache = reactive({
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
})
/* fetchAllPeople, recursively fetches all entries from SWAPI/people
* page: number, API page to fetch
*
* returns first batch of people
*/
async function fetchAllPeople (page: number = 1): IPerson[]|HTTPError {
console.log('fetching people, page', page)
// avoid being called twice unless it is inside the recursive chain
if (cache.loading && page === 1) return cache.people
cache.loading = true
const response = await fetchFromApi('people', '', { page })
if (response instanceof HTTPError) {
if (!cache.error) { // no error yet, so we try again
if (page === 1) cache.loading = false // we need to restart
fetchAllPeople(page)
}
cache.error = true // set error so that we don't end up in a retry loop
return response
}
const newPeople: IPerson[] = response.results
// store result in cache to make component code simpler
cache.people = [...cache.people, ...newPeople]
if (response.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
}
/* fetch a planet from SWAPI
* id: number, id of planet to fetch
*
* returns fetched data in JSON format or, in case the request failed, a HTTPError object
*/
async function getPlanet (id: number): IPlanet|HTTPError { // we always fetch *single* planets
if (planets[id]) return planets[id] // yeah, cached already
const planet = await fetchFromApi('planets', `${id}`)
if (planet instanceof HTTPError) return planet
// see getPeople why results are cached
// important: internally, IDs are numbers but the fetchFromApi function
// wants a string because it uses an empty string to fetch all entries
cache.planets[id] = planet
return planet
}
/* useAPI
* prefetch: boolean, fetch people immediately, defaults to true
*
* returns {
async getPlanet (id: number): IPlanet|HTTPError
async getPeople (): IPerson[]|HTTPError
* }
*/
export default function useAPI () {
fetchAllPeople() // start fetching immediately
return cache
}

10
src/types.d.ts vendored

@ -43,7 +43,11 @@ interface IPlanet { @@ -43,7 +43,11 @@ interface IPlanet {
}
interface IFieldDefinition {
field: string,
label: string,
format: string,
field: string;
label: string;
format: string;
}
interface GenericData {
[key: string]: string|number;
}

3
tsconfig.json

@ -22,9 +22,6 @@ @@ -22,9 +22,6 @@
],
"!composable/*": [
"./src/composables/*"
],
"!api/*": [
"./src/api/*"
]
}
},

1
vite.config.ts

@ -14,7 +14,6 @@ export default defineConfig({ @@ -14,7 +14,6 @@ export default defineConfig({
"@": fileURLToPath(new URL("./src", import.meta.url)),
"!component": fileURLToPath(new URL("./src/components", import.meta.url)),
"!composable": fileURLToPath(new URL("./src/composables", import.meta.url)),
"!api": fileURLToPath(new URL("./src/api", import.meta.url)),
}
}
})

Loading…
Cancel
Save