mirror of
https://github.com/maciejpedzich/spotifyplaylistarchive.com.git
synced 2024-09-19 18:16:19 +02:00
Create "Show Statistics" tab
This commit is contained in:
parent
2d3acccae2
commit
7034fbae6c
103
components/stats/FollowerGrowth.vue
Normal file
103
components/stats/FollowerGrowth.vue
Normal file
@ -0,0 +1,103 @@
|
||||
<script setup lang="ts">
|
||||
import { $fetch } from 'ohmyfetch';
|
||||
|
||||
import ProgressSpinner from 'primevue/progressspinner';
|
||||
import Dropdown from 'primevue/dropdown';
|
||||
import Chart from 'primevue/chart';
|
||||
|
||||
import { Snapshot } from '~~/models/snapshot';
|
||||
|
||||
const route = useRoute();
|
||||
const playlistId = route.params.playlistId as string;
|
||||
|
||||
const chartPeriodOptions = ['Last week', 'Last month'];
|
||||
const chartPeriod = useState('chartPeriod', () => 'Last week');
|
||||
|
||||
const sinceDate = computed(() => {
|
||||
const baseDate = new Date();
|
||||
|
||||
if (chartPeriod.value === 'Last week') {
|
||||
baseDate.setDate(baseDate.getDate() - 7);
|
||||
} else {
|
||||
baseDate.setMonth(baseDate.getMonth() - 1);
|
||||
}
|
||||
|
||||
return baseDate.toISOString();
|
||||
});
|
||||
|
||||
const chartOptions = {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const dateFormatter = new Intl.DateTimeFormat('en-US', {
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
|
||||
const { pending, error, data, refresh } = await useAsyncData(
|
||||
`playlist-${playlistId}-follower-growth`,
|
||||
async () => {
|
||||
const snapshots = (
|
||||
await $fetch<Snapshot[]>(
|
||||
`/api/playlists/${playlistId}/snapshots?sinceDate=${sinceDate.value}`
|
||||
)
|
||||
).reverse();
|
||||
|
||||
const labels = snapshots.map((snapshot) =>
|
||||
dateFormatter.format(Date.parse(snapshot.dateCaptured))
|
||||
);
|
||||
const numFollowersData = snapshots.map(({ numFollowers }) => numFollowers);
|
||||
|
||||
return {
|
||||
labels,
|
||||
datasets: [
|
||||
{
|
||||
type: 'line',
|
||||
label: 'Followers',
|
||||
borderColor: '#90cd93',
|
||||
data: numFollowersData
|
||||
}
|
||||
]
|
||||
};
|
||||
},
|
||||
{ server: false }
|
||||
);
|
||||
|
||||
watch(chartPeriod, async () => await refresh());
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NuxtLayout name="centered-content">
|
||||
<ClientOnly>
|
||||
<p class="mt-0 mb-3 text-lg">
|
||||
Period:
|
||||
<Dropdown
|
||||
v-model="chartPeriod"
|
||||
:options="chartPeriodOptions"
|
||||
:disabled="pending"
|
||||
/>
|
||||
</p>
|
||||
<ProgressSpinner v-if="pending" />
|
||||
<p v-else-if="error">
|
||||
Something went wrong while fetching follwer growth data
|
||||
</p>
|
||||
<template v-else-if="data">
|
||||
<Chart class="mt-3" type="line" :options="chartOptions" :data="data" />
|
||||
</template>
|
||||
</ClientOnly>
|
||||
</NuxtLayout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@media only screen and (min-width: 768px) {
|
||||
:deep(div.p-chart) {
|
||||
width: 100%;
|
||||
padding: 0 10rem;
|
||||
}
|
||||
}
|
||||
</style>
|
136
components/stats/TrackRetention.vue
Normal file
136
components/stats/TrackRetention.vue
Normal file
@ -0,0 +1,136 @@
|
||||
<script setup lang="ts">
|
||||
import { $fetch } from 'ohmyfetch';
|
||||
import { intervalToDuration, formatDuration } from 'date-fns';
|
||||
|
||||
import ProgressSpinner from 'primevue/progressspinner';
|
||||
import DataTable from 'primevue/datatable';
|
||||
import Column from 'primevue/column';
|
||||
|
||||
import { Cumulative } from '~~/models/cumulative';
|
||||
|
||||
const route = useRoute();
|
||||
const playlistId = route.params.playlistId as string;
|
||||
|
||||
const dateFormatter = new Intl.DateTimeFormat('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
|
||||
const formatDate = (date: string | null) =>
|
||||
date ? dateFormatter.format(new Date(date)) : 'N/A';
|
||||
|
||||
const displayRetentionText = (retention: number) => {
|
||||
const durationObject = intervalToDuration({ start: 0, end: retention });
|
||||
const displayText = formatDuration(durationObject, {
|
||||
format: ['years', 'months', 'days']
|
||||
});
|
||||
|
||||
return displayText;
|
||||
};
|
||||
|
||||
const {
|
||||
pending,
|
||||
error,
|
||||
data: tracks
|
||||
} = await useAsyncData(`longest-standing-tracks-${playlistId}`, async () => {
|
||||
const { tracks } = await $fetch<Cumulative>(
|
||||
`https://raw.githubusercontent.com/mackorone/spotify-playlist-archive/main/playlists/cumulative/${playlistId}.json`,
|
||||
{ parseResponse: JSON.parse }
|
||||
);
|
||||
const now = new Date().toISOString();
|
||||
|
||||
return tracks
|
||||
.map((track) => {
|
||||
track.retention =
|
||||
Date.parse(track.date_removed || now) - Date.parse(track.date_added);
|
||||
|
||||
return track;
|
||||
})
|
||||
.sort((a, b) => b.retention - a.retention)
|
||||
.map((track, index) => {
|
||||
track.position = `${index + 1}.`;
|
||||
|
||||
return track;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NuxtLayout name="centered-content">
|
||||
<ClientOnly>
|
||||
<ProgressSpinner v-if="pending" />
|
||||
<p v-else-if="error">
|
||||
Something went wrong while fetching the longest standing tracks
|
||||
</p>
|
||||
<DataTable
|
||||
class="w-full mx-5 mt-3 mb-5"
|
||||
:value="tracks"
|
||||
paginator
|
||||
:rows="10"
|
||||
:rows-per-page-options="[10, 20, 50, 100]"
|
||||
paginator-template="CurrentPageReport FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown"
|
||||
current-page-report-template="Showing {first} to {last} of {totalRecords}"
|
||||
responsive-layout="scroll"
|
||||
>
|
||||
<Column field="position" header="No."></Column>
|
||||
<Column field="name" header="Title">
|
||||
<template #body="{ data: track }">
|
||||
<NuxtLink :to="track.url" target="_blank">
|
||||
{{ track.name }}
|
||||
</NuxtLink>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="artists" header="Artist(s)">
|
||||
<template #body="{ data: track }">
|
||||
<div v-for="(artist, index) of track.artists" :key="artist.url">
|
||||
<NuxtLink :to="artist.url" target="_blank">
|
||||
{{ artist.name }}
|
||||
</NuxtLink>
|
||||
<span v-if="index !== track.artists.length - 1" class="md:mr-1"
|
||||
>,</span
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="album.name" header="Album">
|
||||
<template #body="{ data: track }">
|
||||
<NuxtLink :to="track.album.url" target="_blank">
|
||||
{{ track.album.name }}
|
||||
</NuxtLink>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="date_added" header="Date added">
|
||||
<template #body="{ data: track }">
|
||||
<span>
|
||||
{{ formatDate(track.date_added) }}
|
||||
</span>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="date_removed" header="Date removed">
|
||||
<template #body="{ data: track }">
|
||||
<span>
|
||||
{{ formatDate(track.date_removed) }}
|
||||
</span>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="retention" header="Retention">
|
||||
<template #body="{ data: track }">
|
||||
<span>
|
||||
{{ displayRetentionText(track.retention) }}
|
||||
</span>
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
</ClientOnly>
|
||||
</NuxtLayout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@media only screen and (min-width: 768px) {
|
||||
:deep(div.p-datatable) {
|
||||
width: 100%;
|
||||
padding: 0 8rem;
|
||||
}
|
||||
}
|
||||
</style>
|
8
pages/playlists/[playlistId]/stats.vue
Normal file
8
pages/playlists/[playlistId]/stats.vue
Normal file
@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<div class="w-full text-lg text-center">
|
||||
<h2 class="mt-0 mb-3">Number of followers</h2>
|
||||
<StatsFollowerGrowth />
|
||||
<h2 class="mt-4 mb-2">Track retention</h2>
|
||||
<StatsTrackRetention />
|
||||
</div>
|
||||
</template>
|
Loading…
Reference in New Issue
Block a user