mirror of
https://github.com/maciejpedzich/spotifyplaylistarchive.com.git
synced 2024-09-19 18:16:19 +02:00
Create a snapshot calendar page
This commit is contained in:
parent
7528491f03
commit
1afac8235a
87
pages/playlists/[playlistId].vue
Normal file
87
pages/playlists/[playlistId].vue
Normal 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>
|
139
pages/playlists/[playlistId]/snapshots/index.vue
Normal file
139
pages/playlists/[playlistId]/snapshots/index.vue
Normal 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>
|
Loading…
Reference in New Issue
Block a user