refactor: rework API loader and caching
This commit removes dependency on rxjs. It also implements better composables to handle data fetching from remote APIs and caching these values more transparently. This commit also switches from yarn to npm It also switches to the official Umami plugin
This commit is contained in:
@@ -1,31 +1,22 @@
|
||||
<template>
|
||||
<Cache
|
||||
:name="props.cacheName"
|
||||
:callback="fetchData"
|
||||
:already-known-data="alreadyKnownData"
|
||||
@cached="processCachedData"
|
||||
/>
|
||||
<slot v-if="loading" name="loader">
|
||||
<Loader />
|
||||
<LoaderAnimation />
|
||||
</slot>
|
||||
<slot v-else-if="error" name="error">
|
||||
<Error :url="props.url" />
|
||||
<FetchError :url="props.url" />
|
||||
</slot>
|
||||
<slot v-else> </slot>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Cache from './Cache.vue';
|
||||
import Loader from './Loader.vue';
|
||||
import Error from './Error.vue';
|
||||
import LoaderAnimation from './LoaderAnimation.vue';
|
||||
import FetchError from './FetchError.vue';
|
||||
|
||||
import { Ref, ref } from 'vue';
|
||||
import { Observable, catchError, switchMap, throwError } from 'rxjs';
|
||||
import { fromFetch } from 'rxjs/fetch';
|
||||
import { useFetchAndCache } from '../composables/fetchAndCache';
|
||||
|
||||
const props = defineProps({
|
||||
url: {
|
||||
default: false,
|
||||
default: '',
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
@@ -35,31 +26,11 @@ const props = defineProps({
|
||||
},
|
||||
alreadyKnownData: Object,
|
||||
});
|
||||
const emits = defineEmits(['dataLoaded', 'dataError', 'loading']);
|
||||
|
||||
const error: Ref<Error> = ref(null);
|
||||
const loading: Ref<boolean> = ref(true);
|
||||
const emits = defineEmits(['loaded', 'error', 'loading']);
|
||||
|
||||
const fetchData = (): Observable<any> => {
|
||||
return fromFetch(props.url).pipe(
|
||||
switchMap((response: Response) => response.json()),
|
||||
catchError((errorResponse: Error) => {
|
||||
error.value = errorResponse;
|
||||
return Error;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const processCachedData = (data: Observable<any>) => {
|
||||
data.subscribe({
|
||||
next: (response: any) => {
|
||||
loading.value = false;
|
||||
emits('dataLoaded', response);
|
||||
},
|
||||
error: (responseError: Error) => {
|
||||
loading.value = false;
|
||||
error.value = responseError;
|
||||
},
|
||||
});
|
||||
};
|
||||
const { loading, error } = useFetchAndCache(props.url, {
|
||||
emits: emits,
|
||||
cacheName: props.cacheName,
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
<template>
|
||||
<slot />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Observable, of, tap } from 'rxjs';
|
||||
|
||||
const props = defineProps({
|
||||
name: {
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
callback: {
|
||||
required: true,
|
||||
type: Function,
|
||||
},
|
||||
lifetime: {
|
||||
default: 1000 * 60 * 60, // one hour
|
||||
required: false,
|
||||
type: Number,
|
||||
},
|
||||
alreadyKnownData: {
|
||||
default: null,
|
||||
type: Object,
|
||||
},
|
||||
});
|
||||
|
||||
const emits = defineEmits(['cached']);
|
||||
|
||||
const isDataOutdated = (name: string): boolean => {
|
||||
const lastUpdated: number = +localStorage.getItem(name + '-timestamp');
|
||||
const elapsedTime: number = Date.now() - lastUpdated;
|
||||
return elapsedTime > props.lifetime;
|
||||
};
|
||||
|
||||
const storeInCache = (
|
||||
callback: Function,
|
||||
data: any,
|
||||
name: string,
|
||||
): Observable<any> => {
|
||||
let response: Observable<any> = data ? of(data) : callback();
|
||||
return response.pipe(
|
||||
tap((response) => {
|
||||
localStorage.setItem(name, JSON.stringify(response));
|
||||
localStorage.setItem(name + '-timestamp', `${Date.now()}`);
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
if (isDataOutdated(props.name)) {
|
||||
emits(
|
||||
'cached',
|
||||
storeInCache(props.callback, props.alreadyKnownData, props.name),
|
||||
);
|
||||
} else {
|
||||
let data = localStorage.getItem(props.name);
|
||||
try {
|
||||
emits('cached', of(JSON.parse(data)));
|
||||
} catch (err) {
|
||||
console.error(`Could not parse data found in cache: ${err}`);
|
||||
emits(
|
||||
'cached',
|
||||
storeInCache(props.callback, props.alreadyKnownData, props.name),
|
||||
);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<ApiLoader :url="fetchUrl" @dataLoaded="filterRepos" cache-name="repos" />
|
||||
<ApiLoader :url="fetchUrl" @loaded="filterRepos" cache-name="repos" />
|
||||
<slot />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PropType } from 'vue';
|
||||
import { GithubRepo } from '../../composables/github';
|
||||
import { PropType, ref } from 'vue';
|
||||
import { GithubRepo } from '../../types/github';
|
||||
const props = defineProps({
|
||||
sortBy: {
|
||||
default: 'none',
|
||||
@@ -23,27 +23,24 @@ const props = defineProps({
|
||||
type: Number,
|
||||
},
|
||||
});
|
||||
|
||||
const emits = defineEmits(['dataLoaded']);
|
||||
|
||||
const emits = defineEmits(['loaded']);
|
||||
const fetchUrl = `https://api.github.com/users/${props.user}/repos?per_page=100`;
|
||||
const repos = ref<GithubRepo[]>([]);
|
||||
|
||||
const filterRepos = (response: GithubRepo[]) => {
|
||||
emits(
|
||||
'dataLoaded',
|
||||
response
|
||||
.sort((a, b) => {
|
||||
if (props.sortBy === 'stars') {
|
||||
return b.stargazers_count - a.stargazers_count;
|
||||
}
|
||||
if (props.sortBy === 'pushed_at') {
|
||||
const dateA = new Date(a.pushed_at);
|
||||
const dateB = new Date(b.pushed_at);
|
||||
return dateB.getTime() - dateA.getTime();
|
||||
}
|
||||
return b.forks_count - a.forks_count;
|
||||
})
|
||||
.slice(0, +props.limit)
|
||||
);
|
||||
repos.value = response
|
||||
.sort((a, b) => {
|
||||
if (props.sortBy === 'stars') {
|
||||
return b.stargazers_count - a.stargazers_count;
|
||||
}
|
||||
if (props.sortBy === 'pushed_at') {
|
||||
const dateA = new Date(a.pushed_at);
|
||||
const dateB = new Date(b.pushed_at);
|
||||
return dateB.getTime() - dateA.getTime();
|
||||
}
|
||||
return b.forks_count - a.forks_count;
|
||||
})
|
||||
.slice(0, +props.limit);
|
||||
emits('loaded', repos.value);
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -6,23 +6,23 @@
|
||||
:cache-name="repoName()"
|
||||
:url="fetchUrl"
|
||||
:already-known-data="props.data"
|
||||
@data-loaded="(repo: GithubRepo) => (repository = repo)"
|
||||
@loaded="(repo: GithubRepo) => (repository = repo)"
|
||||
>
|
||||
<h3>{{ repository.name }}</h3>
|
||||
<h3>{{ repository?.name }}</h3>
|
||||
<div>
|
||||
<p>
|
||||
{{ repository.description }}
|
||||
{{ repository?.description }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex-row flex-start gap-1rem stats">
|
||||
<div class="stars">
|
||||
<Icon name="star" /> {{ repository.stargazers_count }}
|
||||
<Icon name="star" /> {{ repository?.stargazers_count }}
|
||||
</div>
|
||||
<div class="forks">
|
||||
<Icon name="fork" /> {{ repository.forks_count }}
|
||||
<Icon name="fork" /> {{ repository?.forks_count }}
|
||||
</div>
|
||||
<div class="link">
|
||||
<a :href="repository.html_url"><i class="icon phunic-link" /></a>
|
||||
<a :href="repository?.html_url"><i class="icon phunic-link" /></a>
|
||||
</div>
|
||||
</div>
|
||||
</ApiLoader>
|
||||
@@ -32,7 +32,7 @@
|
||||
<script setup lang="ts">
|
||||
import ApiLoader from '../ApiLoader.vue';
|
||||
|
||||
import { GithubRepo } from '../../composables/github';
|
||||
import { GithubRepo } from '../../types/github';
|
||||
import { PropType, Ref, ref } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
@@ -45,7 +45,7 @@ const repoName = (): string => {
|
||||
};
|
||||
|
||||
const fetchUrl = `https://api.github.com/repos/${repoName()}`;
|
||||
const repository: Ref<GithubRepo> = ref(null);
|
||||
const repository: Ref<GithubRepo | null> = ref(null);
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
@@ -78,5 +78,12 @@ const repository: Ref<GithubRepo> = ref(null);
|
||||
gap: 0.3rem;
|
||||
}
|
||||
}
|
||||
|
||||
.link {
|
||||
a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
:sort-by="props.sortBy"
|
||||
:user="props.user"
|
||||
:limit="props.limit"
|
||||
@data-loaded="(response: GithubRepo[]) => (repos = response)"
|
||||
@loaded="(response: GithubRepo[]) => (repos = response)"
|
||||
>
|
||||
<GithubRepository
|
||||
:data="repo"
|
||||
@@ -22,7 +22,7 @@ import FetchRepositories from './FetchRepositories.vue';
|
||||
import GithubRepository from './GithubRepository.vue';
|
||||
|
||||
import { PropType, Ref, ref } from 'vue';
|
||||
import { GithubRepo } from '../../composables/github';
|
||||
import { GithubRepo } from '../../types/github';
|
||||
|
||||
const props = defineProps({
|
||||
sortBy: {
|
||||
|
||||
Reference in New Issue
Block a user