Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions components/playlists/AddPlaylistsModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<script setup lang="ts">
import UserSpotifyPlaylistsView from './UserSpotifyPlaylistsView.vue';


const emit = defineEmits(['close-modal', 'refresh']);

const playlistError = ref('')

</script>

<template>
<div class="fixed top-0 bottom-0 left-0 right-0 flex justify-center z-50 bg-black bg-opacity-85">
<div class="mt-[10%] md:mt-[5%] w-full sm:w-3/6 xl:w-2/6 2xl:w-2/6 max-h-[80vh] md:max-h-[50vh] rounded-3xl flex flex-col align-center text-center bg-white px-5 pt-10">
<p class="text-3xl font-bold my-3">Your Playlists</p>
<UserSpotifyPlaylistsView @refresh="emit('refresh')"/>
<p v-if="playlistError" class="error-message">{{ playlistError }}</p>
<p class="px-8">If you can't see your playlist here, make sure to add it to your Spotify profile.</p>
<button class="bg-indigo-600 hover:bg-indigo-800 my-5 text-white mx-auto block" @click="emit('close-modal')">Close</button>
</div>
</div>
</template>

<style scoped>
button {
width: 150px;
height: 40px;
font-size: 14px;
border-radius: 16px;
}
</style>
10 changes: 6 additions & 4 deletions components/playlists/PlaylistBox.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ const props = defineProps({
default: 'Playlist',
required: true
},
controlElement: {
type: String,
default: null
userPlaylist : {
type: Boolean,
default: false
}
});

const emit = defineEmits(['refresh']);

const showModal = ref(false);
</script>

Expand Down Expand Up @@ -48,7 +50,7 @@ const showModal = ref(false);
<!-- Modal for (un)following playlists -->
<PlaylistsPlaylistModal
v-show="showModal" :playlist-id="props.playlistId" :playlist-name="props.name"
:playlist-cover="props.cover" @close-modal="showModal = false" />
:playlist-cover="props.cover" :user-playlist="props.userPlaylist" @close-modal="showModal = false" @refresh="emit('refresh')"/>
</template>

<style scoped>
Expand Down
14 changes: 13 additions & 1 deletion components/playlists/PlaylistContainer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ const props = defineProps({
playlistIds: {
type: Array,
required: true
},
onAction: {
type: Function as PropType<() => void>,
default: () => {}
},
actionLabel: {
type: String,
default: ''
}
});

Expand All @@ -25,8 +33,12 @@ const filteredPlaylists = computed(() =>
<template>
<div class="w-full bg-gray-200 px-3 pb-1 mt-auto rounded-3xl mb-3 flex flex-col flex-grow-0 overflow-hidden">
<!-- Header with genre name -->
<div class="mb-1 text-xs md:text-base mt-2 pt-2">
<div class="flex mb-1 text-xs md:text-base mt-2 pt-2 justify-between">
<p>{{ genre }}</p>
<button v-if="props.onAction && props.actionLabel" class="flex items-center text-gray-700" @click="props.onAction()">
<Icon name="mdi:plus"/>
{{ props.actionLabel }}
</button>
</div>

<!-- Boxes with the name and cover of the playlist in the genre -->
Expand Down
47 changes: 40 additions & 7 deletions components/playlists/PlaylistModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,57 @@ const props = defineProps({
playlistCover: {
type: String,
required: true
},
userPlaylist: {
type: Boolean,
default: false
}
});

onUpdated(async () => { playlistStatus.value = await getPlaylistStatus()});
onUpdated(async () => { playlistStatus.value = await getPlaylistStatus() });

const emit = defineEmits(['close-modal']);
const emit = defineEmits(['close-modal', 'refresh']);

const { playlistStatus, getPlaylistStatus, followPlaylist, unfollowPlaylist } = useSpotify(props.playlistId);

async function addPlaylist() {
emit('close-modal')
await $fetch('/api/v1/playlist', {
method: 'POST',
body: {
id: props.playlistId,
name: props.playlistName,
spotifyId: props.playlistId,
categories: ["User created"],
enabled: true
}
})
emit('refresh')
}
</script>

<template>
<div class="fixed top-0 bottom-0 left-0 right-0 flex justify-center bg-black bg-opacity-85 z-50">
<div class="mt-[10%] w-5/6 md:w-1/6 h-5/6 md:h-3/6 rounded-3xl flex flex-col items-center justify-center text-center bg-white">
<p class="text-xl mb-2">{{ playlistName }}</p>
<div
class="mt-[10%] md:mt-[5%] w-full md:w-1/6 max-h-[80vh] md:max-h-[50vh] m-5 rounded-3xl flex flex-col items-center justify-center text-center bg-white">
<p class="text-xl font-bold mb-10 w-2/3">{{ playlistName }}</p>
<NuxtImg :src="playlistCover" class="rounded-2xl h-40 w-40 mb-6" />
<button v-if="!playlistStatus" class="bg-yellow-500 hover:bg-yellow-600 text-red-600" @click="followPlaylist">Follow Playlist</button>
<button v-else class="bg-yellow-500 hover:bg-yellow-600 text-red-600" @click="unfollowPlaylist">Unfollow Playlist</button>
<button class="bg-indigo-600 hover:bg-indigo-800 my-5 text-white" @click="emit('close-modal')">Close</button>
<div v-if="!props.userPlaylist">
<button
v-if="!playlistStatus" class="bg-yellow-500 hover:bg-yellow-600 text-red-600"
@click="followPlaylist">Follow Playlist</button>
<button
v-else class="bg-yellow-500 hover:bg-yellow-600 text-red-600" @click="unfollowPlaylist">Unfollow
Playlist</button>
</div>
<div v-else>
<button
class="bg-yellow-500 hover:bg-yellow-600 text-red-600"
@click="addPlaylist">Add Playlist</button>
</div>
<button
class="bg-indigo-600 hover:bg-indigo-800 my-5 text-white"
@click="emit('close-modal')">Close</button>
</div>
</div>
</template>
Expand Down
58 changes: 40 additions & 18 deletions components/playlists/PlaylistView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@
import type { GetPlaylistResponse } from "@/types/api/playlists";

const playlists = ref<GetPlaylistResponse[]>([]);
const userPlaylistIds = ref<{ category: "User created"; ids: number[] }[]>([]);
const userPlaylists = ref<GetPlaylistResponse[]>([]);
const categories = ref<{ category: string; ids: number[] }[]>([]);

const session = useSupabaseSession();

const showModal = ref(false);

onMounted(async () => {
if (session.value) {
await getPlaylists();
Expand All @@ -19,34 +23,52 @@ async function getPlaylists() {
playlists.value = [...data];

playlists.value.forEach(playlist => {
// Skip disabled playlists
if(!playlist.enabled) return;

playlist.categories.forEach(category => {
// Check if the category already exists in the array
const existingCategory = categories.value.find(item => item.category === category);

if (existingCategory) {
// Add the new ID if not already present
if (!existingCategory.ids.includes(playlist.id)) {
existingCategory.ids.push(playlist.id);
}
} else {
// Add a new entry if the category does not exist
categories.value.push({ category, ids: [playlist.id] });

if (!playlist.enabled) return;

/* Filter all IDs of playlists in each category. */
playlist.categories.forEach(category => {
if (category === "User created") {
const existingUserCategory = userPlaylistIds.value.find(item => item.category === "User created");

if (existingUserCategory) {
if (!existingUserCategory.ids.includes(playlist.id)) {
existingUserCategory.ids.push(playlist.id);
}
});
});
} else {
userPlaylistIds.value.push({ category: "User created", ids: [playlist.id] });
}
} else {
const existingCategory = categories.value.find(item => item.category === category);

if (existingCategory) {
if (!existingCategory.ids.includes(playlist.id)) {
existingCategory.ids.push(playlist.id);
}
} else {
categories.value.push({ category, ids: [playlist.id] });
}
}
});
const userCreatedIds = userPlaylistIds.value.flatMap(item => item.ids);
userPlaylists.value = playlists.value.filter(playlist => userCreatedIds.includes(playlist.id));
});
} catch (error) {
console.error("Failed to fetch playlists:", error);
}
}
};
</script>

<template>
<PlaylistsAddPlaylistsModal v-show="showModal" @close-modal="showModal = false" @refresh="getPlaylists"/>

<div class="flex flex-col">
<div class="overflow-y-auto" style="max-height: 85vh;">
<PlaylistsPlaylistContainer v-for="categoryEl in categories" :key="categoryEl.category" :genre="categoryEl.category" :playlist-ids="categoryEl.ids" :playlists="playlists" :categories="categories"/>
<PlaylistsPlaylistContainer
v-for="item in userPlaylistIds" :key="item.category" :genre="item.category" :playlist-ids="item.ids" :playlists="userPlaylists"
action-label="Add Playlist" :on-action="() => {showModal = true}" @refresh="getPlaylists"/>
<button v-if="userPlaylists.length === 0" class="p-2 bg-blue-500 text-white rounded-3xl ml-2" @click="showModal = true">Add own Playlist</button>
</div>
</div>
</template>
29 changes: 29 additions & 0 deletions components/playlists/UserSpotifyPlaylistsView.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<script setup lang="ts">
import useSpotify from '@/composables/useSpotify';
import type { Playlist } from "@/types/api/playlist"

const { getUserPlaylists } = useSpotify("");

const userPlaylists = ref<Playlist[] | null>([]);

onMounted(async () => {
userPlaylists.value = await getUserPlaylists();;
})

const emit = defineEmits(['refresh']);

</script>

<template>
<div class="w-full bg-gray-200 px-3 py-3 rounded-3xl mb-3 flex flex-col flex-grow-0 overflow-y-auto">
<div class="w-full bg-gray-200 rounded-3xl">
<!-- Boxes with the name and cover of the playlist in the genre -->
<div class="grid grid-cols-3 gap-1 justify-items-center items-center">
<PlaylistsPlaylistBox
v-for="item in userPlaylists" :key="item.id" :playlist-id="item.spotifyId"
:name="item.name" v-bind="item.cover ? { cover: item.cover.toString() } : {}" :user-playlist="true"
class="flex-grow-0 flex-shrink-0" @refresh="emit('refresh')" />
</div>
</div>
</div>
</template>
48 changes: 45 additions & 3 deletions composables/useSpotify.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
/* Communicate with Spotify API */
import type { Playlist } from "@/types/api/playlist"

export default function useSpotify(playlistId: string) {
const playlistStatus = ref<boolean | null>(null);
const userPlaylists = ref<Playlist[]>([]);

const session = useSupabaseSession();
const token = ref('');

if (session.value) {
token.value = session.value.provider_token ?? '';
}
Expand All @@ -24,12 +28,12 @@ export default function useSpotify(playlistId: string) {

const data = await res.json();
playlistStatus.value = data[0];
return data[0];
return playlistStatus.value;
} catch (error) {
console.error('Failed to get playlist status:', error);
return null;
}
}
};

async function followPlaylist(): Promise<void> {
try {
Expand All @@ -51,7 +55,7 @@ export default function useSpotify(playlistId: string) {
} catch (error) {
console.error('Error following playlist:', error);
}
}
};

async function unfollowPlaylist(): Promise<void> {
try {
Expand All @@ -72,12 +76,50 @@ export default function useSpotify(playlistId: string) {
} catch (error) {
console.error('Error unfollowing playlist:', error);
}
};

async function getUserPlaylists(): Promise<Playlist[] | null> {
try {
const res = await fetch(`https://api.spotify.com/v1/users/${session.value?.user.user_metadata.provider_id}/playlists`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token.value}`,
},
});

if (!res.ok) {
console.error(`Error: ${res.status} - ${res.statusText}`);
return null;
}

const data = await res.json();

console.log(data)

data.items.forEach(item => {
if (!userPlaylists.value.some(playlist => playlist.id === item.id)) {
userPlaylists.value.push({
id: item.id,
spotifyId: item.id,
cover: item.images[0].url,
enabled: true,
name: item.name
} as Playlist);
}
});

return userPlaylists.value;
} catch (error) {
console.error('Failed to get playlists:', error);
return null;
}
}

return {
playlistStatus,
getPlaylistStatus,
followPlaylist,
unfollowPlaylist,
getUserPlaylists
};
}
3 changes: 2 additions & 1 deletion server/api/v1/playlist/index.post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ export default defineEventHandler(async (event) => {
id: result.data.id,
name: result.data.name,
spotifyId: result.data.spotifyId,
cover: coverUrl
cover: coverUrl,
enabled: result.data.enabled
}

const categoriesInsert = result.data.categories.map((category) => ({
Expand Down
Loading