Create a snapshot calendar page

This commit is contained in:
Maciej Pędzich 2022-07-02 15:21:04 +02:00
parent 7528491f03
commit 1afac8235a
2 changed files with 226 additions and 0 deletions

View File

@ -0,0 +1,87 @@
<script setup lang="ts">
import TabMenu from 'primevue/tabmenu';
import { Playlist } from '~~/models/playlist';
const route = useRoute();
const playlistId = route.params.playlistId;
const tabItems = [
{
label: 'Browse snapshots',
icon: 'pi pi-calendar',
to: `/playlists/${playlistId}/snapshots`
},
{
label: 'Compare snapshots',
icon: 'pi pi-sort-alt',
to: `/playlists/${playlistId}/snapshots/compare`
},
{
label: 'Show statistics',
icon: 'pi pi-chart-bar',
to: `/playlists/${playlistId}/statistics`
}
];
const { error, data } = await useFetch<Playlist & { notFound?: true }>(
() =>
`https://raw.githubusercontent.com/mackorone/spotify-playlist-archive/master/playlists/pretty/${playlistId}.json`,
{
parseResponse: (body) =>
body.includes('404') ? { notFound: true } : JSON.parse(body),
key: `playlist-${playlistId}`
}
);
const isNotFoundError = computed(
() => typeof error.value !== 'boolean' && error.value.message.includes('404')
);
</script>
<template>
<div class="flex flex-column align-items-center mt-7">
<p v-if="error" class="text-xl">
<span v-if="isNotFoundError">
This playlist was not found in the registry
</span>
<span v-else>Something went wrong while fetching playlist's data</span>
</p>
<template v-else>
<h1 class="text-3xl mb-3">
<NuxtLink :to="data.url" target="_blank">
{{ data.unique_name }}
<span v-if="data.unique_name !== data.original_name">
({{ data.original_name }})
</span>
</NuxtLink>
by
<NuxtLink :to="data.owner.url" target="_blank">
{{ data.owner.name }}
</NuxtLink>
</h1>
<TabMenu class="w-full mb-5" :model="tabItems" />
<NuxtPage />
</template>
</div>
</template>
<style scoped>
:deep(ul.p-tabmenu-nav) {
margin-left: 5rem;
margin-right: 5rem;
justify-content: center;
}
:deep(li.p-tabmenuitem) {
margin-left: 1rem;
}
:deep(li.p-tabmenuitem:first-of-type) {
margin-left: 0;
}
:deep(.p-tabmenu .p-tabmenu-nav .p-tabmenuitem a.p-menuitem-link) {
background-color: transparent;
}
</style>

View File

@ -0,0 +1,139 @@
<script setup lang="ts">
import { useMediaQuery } from '@vueuse/core';
import ProgressSpinner from 'primevue/progressspinner';
import Calendar from 'primevue/calendar';
import { CalendarEntry } from '~~/models/calendar-entry';
// Moving these interfaces to separate files makes TypeScript scream at you
interface PrimeVueDate {
day: number;
month: number;
year: number;
today: boolean;
otherMonth: boolean;
}
interface DateChangePayload {
month: number;
year: number;
}
const route = useRoute();
const playlistId = route.params.playlistId;
const isPortableScreen = useMediaQuery('(max-width: 768px)');
const calendarDisplayDate = ref(new Date());
const queryMonth = ref(calendarDisplayDate.value.getMonth());
const queryYear = ref(calendarDisplayDate.value.getFullYear());
const sinceDateParam = computed(() =>
new Date(queryYear.value, queryMonth.value, 1).toISOString().substring(0, 10)
);
const untilDateParam = computed(() =>
new Date(queryYear.value, queryMonth.value + 1, 1)
.toISOString()
.substring(0, 10)
);
const queryString = computed(() => {
const queryParamsObj = new URLSearchParams();
queryParamsObj.set('sinceDate', sinceDateParam.value);
queryParamsObj.set('untilDate', untilDateParam.value);
return queryParamsObj.toString();
});
const {
pending: loadingCalendarEntries,
error: calendarEntriesLoadError,
data: calendarEntries,
refresh: reloadCalendarEntries
} = useFetch<CalendarEntry[]>(
() => `/api/playlists/${playlistId}/snapshots?${queryString.value}`,
{
key: `snapshots-calendar-of-${playlistId}`,
server: false
}
);
const snapshotLinkMap = computed<Record<string, string>>(() =>
(calendarEntries.value || []).reduce((map, entry) => {
const dateCapturedKey = entry.dateCaptured.substring(0, 10);
map[dateCapturedKey] = `./snapshots/${entry.commitSha}`;
return map;
}, {})
);
const primeVueDateToString = ({ year, month, day }: PrimeVueDate) =>
// PrimeVue Calendar dates' months are zero-based
// Just like in JavaScript date objects
[year, month + 1, day]
.map((num) => num.toString().padStart(2, '0'))
.join('-');
const getSnapshotLinkFromDate = (calendarDate: PrimeVueDate) =>
snapshotLinkMap.value[primeVueDateToString(calendarDate)];
const isDateCaptured = (calendarDate: PrimeVueDate) =>
!!getSnapshotLinkFromDate(calendarDate);
const updateQueryAndReload = async ({ month, year }: DateChangePayload) => {
queryMonth.value = month;
queryYear.value = year;
await reloadCalendarEntries();
};
</script>
<template>
<div>
<ClientOnly>
<ProgressSpinner
v-if="loadingCalendarEntries"
class="abosulte top-0 right-0"
/>
<p
v-else-if="!loadingCalendarEntries && calendarEntriesLoadError"
class="text-2xl"
>
Something went wrong while fetching archive entries
</p>
<Calendar
v-show="!loadingCalendarEntries"
v-model="calendarDisplayDate"
:disabled="loadingCalendarEntries"
:touch-u-i="isPortableScreen"
inline
@month-change="updateQueryAndReload"
@year-change="updateQueryAndReload"
>
<template #date="{ date }">
<NuxtLink
v-if="isDateCaptured(date as PrimeVueDate)"
:to="getSnapshotLinkFromDate(date as PrimeVueDate)"
>
<span class="bg-primary hover:bg-green-400 text-0 p-3 border-round">
{{ (date as PrimeVueDate).day }}
</span>
</NuxtLink>
<span v-else>
{{ (date as PrimeVueDate).day }}
</span>
</template>
</Calendar>
</ClientOnly>
</div>
</template>
<style scoped>
:deep(.p-datepicker table td.p-datepicker-today > span.p-highlight) {
background-color: transparent;
}
:deep(.p-datepicker table.p-datepicker-calendar td > span.p-highlight) {
background-color: transparent;
}
</style>