mirror of
https://github.com/maciejpedzich/spotifyplaylistarchive.com.git
synced 2024-09-20 02:26:20 +02:00
Make calendar fetch and highlight capture dates
This commit is contained in:
parent
2526221095
commit
2f8e6ee7ac
@ -1,63 +1,111 @@
|
||||
<script setup lang="ts">
|
||||
import { onBeforeMount, ref } from 'vue';
|
||||
import { computed, onBeforeMount, onMounted, ref } from 'vue';
|
||||
|
||||
import Datepicker from '@vuepic/vue-datepicker';
|
||||
import '@vuepic/vue-datepicker/dist/main.css';
|
||||
|
||||
import type { SnapshotMeta } from '@/models/snapshot-meta';
|
||||
import type { UpdateMonthYearPayload } from '@/models/update-month-year-payload';
|
||||
|
||||
const today = new Date();
|
||||
import { queryParamsToDate } from '@/utils/queryParamsToDate';
|
||||
|
||||
const props = defineProps<{ playlistId: string }>();
|
||||
|
||||
const queryParams = ref<URLSearchParams | null>(null);
|
||||
const minDate = ref(new Date('2021-01-01'));
|
||||
const maxDate = ref(new Date());
|
||||
const startDate = ref(new Date());
|
||||
|
||||
const getStartDateArgs = () => {
|
||||
const queryParams = new URLSearchParams(location.search);
|
||||
const loadingSnapshots = ref(true);
|
||||
const errorOccurred = ref(false);
|
||||
const snapshots = ref<SnapshotMeta[]>([]);
|
||||
|
||||
const year = queryParams.has('year')
|
||||
? Math.max(
|
||||
Math.min(
|
||||
Number(queryParams.get('year')) || today.getFullYear(),
|
||||
today.getFullYear()
|
||||
),
|
||||
2021
|
||||
)
|
||||
: today.getFullYear();
|
||||
const allowedDates = computed(() =>
|
||||
snapshots.value.map(({ dateCaptured }) => new Date(dateCaptured))
|
||||
);
|
||||
|
||||
const normalisedMonthParam = Math.min(
|
||||
// Query param months aren't zero-based, but JS Date months are
|
||||
Math.max((Number(queryParams.get('month')) || 1) - 1, 0),
|
||||
11
|
||||
);
|
||||
const dateToCommitShaMap = computed<Record<string, string>>(() =>
|
||||
snapshots.value.reduce((obj, { dateCaptured, commitSha }) => {
|
||||
obj[dateCaptured.substring(8, 10)] = commitSha;
|
||||
|
||||
const month = queryParams.has('month')
|
||||
? year === today.getFullYear()
|
||||
? Math.min(normalisedMonthParam, today.getMonth())
|
||||
: normalisedMonthParam
|
||||
: today.getMonth();
|
||||
return obj;
|
||||
}, {} as Record<string, string>)
|
||||
);
|
||||
|
||||
return { month, year };
|
||||
const loadSnapshots = async () => {
|
||||
try {
|
||||
loadingSnapshots.value = true;
|
||||
errorOccurred.value = false;
|
||||
|
||||
const queryString = queryParams.value?.toString();
|
||||
const apiResponse = await fetch(
|
||||
`/playlists/${props.playlistId}/snapshots.json?${queryString}`
|
||||
);
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
throw new Error(`API ${apiResponse.status}`);
|
||||
}
|
||||
|
||||
const data = await apiResponse.json();
|
||||
snapshots.value = data;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
errorOccurred.value = true;
|
||||
} finally {
|
||||
loadingSnapshots.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const updateSearchParams = ({ month, year }: UpdateMonthYearPayload) => {
|
||||
const isDisplayedMonth = (date: Date) =>
|
||||
date.getMonth() === startDate.value.getMonth();
|
||||
|
||||
const getCommitSha = (day: number) =>
|
||||
dateToCommitShaMap.value[day.toString().padStart(2, '0')];
|
||||
|
||||
const getSnapshotHref = (day: number) =>
|
||||
`/playlists/${props.playlistId}/snapshots/${getCommitSha(day)}`;
|
||||
|
||||
const updateQueryAndReloadSnapshots = async ({
|
||||
month,
|
||||
year
|
||||
}: UpdateMonthYearPayload) => {
|
||||
const url = new URL(location.href);
|
||||
|
||||
url.searchParams.set('month', (month + 1).toString());
|
||||
url.searchParams.set('year', year.toString());
|
||||
queryParams.value?.set('month', (month + 1).toString());
|
||||
queryParams.value?.set('year', year.toString());
|
||||
startDate.value = queryParamsToDate(queryParams.value as URLSearchParams);
|
||||
|
||||
const queryString = queryParams.value?.toString();
|
||||
url.search = `?${queryString}`;
|
||||
|
||||
window.history.pushState({}, '', url);
|
||||
await loadSnapshots();
|
||||
};
|
||||
|
||||
onBeforeMount(() => {
|
||||
const { month, year } = getStartDateArgs();
|
||||
startDate.value = new Date(year, month, 1);
|
||||
queryParams.value = new URLSearchParams(location.search);
|
||||
startDate.value = queryParamsToDate(queryParams.value);
|
||||
});
|
||||
|
||||
onMounted(loadSnapshots);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<i v-if="loadingSnapshots" class="fa-solid fa-spinner fa-spin text-5xl"></i>
|
||||
<div v-else-if="errorOccurred" class="alert alert-error shadow-lg max-w-sm">
|
||||
<div>
|
||||
<span>Failed to load playlist registry</span>
|
||||
</div>
|
||||
<div class="flex-none">
|
||||
<button class="btn btn-sm btn-ghost" @click="loadSnapshots">Retry</button>
|
||||
</div>
|
||||
</div>
|
||||
<Datepicker
|
||||
v-show="!(loadingSnapshots || errorOccurred)"
|
||||
:min-date="minDate"
|
||||
:max-date="today"
|
||||
:max-date="maxDate"
|
||||
:start-date="startDate"
|
||||
:allowed-dates="allowedDates"
|
||||
:enable-time-picker="false"
|
||||
:month-change-on-arrows="false"
|
||||
:month-change-on-scroll="false"
|
||||
@ -65,8 +113,20 @@ onBeforeMount(() => {
|
||||
no-today
|
||||
prevent-min-max-navigation
|
||||
inline
|
||||
@update-month-year="updateSearchParams"
|
||||
@update-month-year="updateQueryAndReloadSnapshots"
|
||||
>
|
||||
<template #day="{ day, date }">
|
||||
<a
|
||||
v-if="isDisplayedMonth(date) && getCommitSha(day)"
|
||||
class="w-full h-full flex justify-center items-center bg-primary text-primary-content"
|
||||
:href="getSnapshotHref(day)"
|
||||
>
|
||||
{{ day }}
|
||||
</a>
|
||||
<template v-else>
|
||||
{{ day }}
|
||||
</template>
|
||||
</template>
|
||||
</Datepicker>
|
||||
</template>
|
||||
|
||||
@ -109,6 +169,10 @@ onBeforeMount(() => {
|
||||
@apply md:mx-2 md:my-1.5 mx-1.5 my-1 p-0 hover:bg-transparent hover:text-inherit;
|
||||
}
|
||||
|
||||
:deep(div.dp__cell_inner > a) {
|
||||
@apply hover:opacity-75 hover:text-primary-content focus:text-primary-content active:text-primary-content rounded-full;
|
||||
}
|
||||
|
||||
:deep(div.dp__cell_disabled) {
|
||||
@apply text-base-content opacity-30;
|
||||
}
|
||||
|
6
src/models/snapshot-meta.ts
Normal file
6
src/models/snapshot-meta.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export interface SnapshotMeta {
|
||||
snapshotId: string;
|
||||
commitSha: string;
|
||||
dateCaptured: string;
|
||||
numFollowers: number;
|
||||
}
|
@ -4,6 +4,7 @@ import SnapshotCalendar from '@/components/vue/SnapshotCalendar.vue';
|
||||
|
||||
import { getPlaylistLayoutProps } from '@/utils/getPlaylistLayoutProps';
|
||||
|
||||
const playlistId = Astro.params.playlistId as string;
|
||||
const layoutProps = await getPlaylistLayoutProps(Astro);
|
||||
---
|
||||
|
||||
@ -11,5 +12,5 @@ const layoutProps = await getPlaylistLayoutProps(Astro);
|
||||
<p class="text-lg mb-3">
|
||||
Click on a highlighted date to show a snapshot captured that day.
|
||||
</p>
|
||||
<SnapshotCalendar client:load />
|
||||
<SnapshotCalendar playlistId={playlistId} client:load />
|
||||
</PlaylistPageTab>
|
||||
|
78
src/pages/playlists/[playlistId]/snapshots.json.ts
Normal file
78
src/pages/playlists/[playlistId]/snapshots.json.ts
Normal file
@ -0,0 +1,78 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import type { PlaylistSnapshot } from '@/models/playlist-snapshot';
|
||||
|
||||
import { Octokit } from '@octokit/rest';
|
||||
import { queryParamsToDate } from '@/utils/queryParamsToDate';
|
||||
|
||||
export const get: APIRoute = async ({ request, params }) => {
|
||||
try {
|
||||
const playlistId = params.playlistId;
|
||||
const queryParams = new URLSearchParams(new URL(request.url).search);
|
||||
|
||||
const sinceDate = queryParamsToDate(queryParams);
|
||||
const untilDate = new Date(
|
||||
sinceDate.getFullYear(),
|
||||
sinceDate.getMonth() + 1
|
||||
);
|
||||
|
||||
const octokit = new Octokit();
|
||||
const { data: commits } = await octokit.rest.repos.listCommits({
|
||||
owner: 'mackorone',
|
||||
repo: 'spotify-playlist-archive',
|
||||
path: `playlists/pretty/${playlistId}.json`,
|
||||
since: sinceDate.toISOString(),
|
||||
until: untilDate.toISOString()
|
||||
});
|
||||
|
||||
const possiblyDuplicateSnapshots = await Promise.all(
|
||||
commits.map(async ({ sha, commit }) => {
|
||||
const githubResponse = await fetch(
|
||||
`https://raw.githubusercontent.com/mackorone/spotify-playlist-archive/${sha}/playlists/pretty/${playlistId}.json`
|
||||
);
|
||||
|
||||
if (!githubResponse.ok) {
|
||||
throw new Error(`GitHub ${githubResponse.status}`);
|
||||
}
|
||||
|
||||
const { snapshot_id, num_followers } =
|
||||
(await githubResponse.json()) as PlaylistSnapshot;
|
||||
|
||||
return {
|
||||
snapshotId: snapshot_id,
|
||||
commitSha: sha,
|
||||
dateCaptured: commit.author?.date,
|
||||
numFollowers: num_followers
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const body = JSON.stringify(
|
||||
queryParams.get('allowDuplicates') === 'yes'
|
||||
? possiblyDuplicateSnapshots
|
||||
: // Since commits are sorted by the latest date_captured first,
|
||||
// the following code will preserve the last item with a duplicate snapshot_id value.
|
||||
// Therefore, we'll be left with entries containing the earliest date_captured.
|
||||
[
|
||||
...new Map(
|
||||
possiblyDuplicateSnapshots.map((snapshot) => [
|
||||
snapshot.snapshotId,
|
||||
snapshot
|
||||
])
|
||||
).values()
|
||||
]
|
||||
);
|
||||
|
||||
return {
|
||||
headers: {
|
||||
'Cache-Control': 'max-age=86400'
|
||||
},
|
||||
body
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return new Response(null, {
|
||||
status: 500,
|
||||
statusText: 'Failed to load snapshots'
|
||||
});
|
||||
}
|
||||
};
|
27
src/utils/queryParamsToDate.ts
Normal file
27
src/utils/queryParamsToDate.ts
Normal file
@ -0,0 +1,27 @@
|
||||
export const queryParamsToDate = (queryParams: URLSearchParams) => {
|
||||
const today = new Date();
|
||||
|
||||
const year = queryParams.has('year')
|
||||
? Math.max(
|
||||
Math.min(
|
||||
Number(queryParams.get('year')) || today.getFullYear(),
|
||||
today.getFullYear()
|
||||
),
|
||||
2021
|
||||
)
|
||||
: today.getFullYear();
|
||||
|
||||
const normalisedMonthParam = Math.min(
|
||||
// Query param months aren't zero-based, but JS Date months are
|
||||
Math.max((Number(queryParams.get('month')) || 1) - 1, 0),
|
||||
11
|
||||
);
|
||||
|
||||
const month = queryParams.has('month')
|
||||
? year === today.getFullYear()
|
||||
? Math.min(normalisedMonthParam, today.getMonth())
|
||||
: normalisedMonthParam
|
||||
: today.getMonth();
|
||||
|
||||
return new Date(year, month);
|
||||
};
|
Loading…
Reference in New Issue
Block a user