Add Album and Song management pages; implement AlbumSlider component and update Card component for genre handling
This commit is contained in:
1 parent
a9cb8c45d5
commit
4a54f8b2a2
6 files changed
+359
-15
No files matched your search
@@ -1,8 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Album } from "@/utils/types"
|
import type { Album } from "@/utils/types"
|
||||||
|
|
||||||
type AlbumProps = Omit<Album, "artists"> & {
|
type AlbumProps = Omit<Omit<Album, "genre">, "artists"> & {
|
||||||
artists: Array<Omit<Artist, "file">>
|
artists: Array<Omit<Artist, "file">>
|
||||||
|
genre?: Genre
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<AlbumProps>()
|
const props = defineProps<AlbumProps>()
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const { data, pending, refresh } = useAsyncData("random-albums", async () => {
|
||||||
|
const { data: genres } = await cfetch("/genres/random")
|
||||||
|
|
||||||
|
const results = await Promise.allSettled(
|
||||||
|
genres.map(async (genre: Genre) => {
|
||||||
|
const res = await cfetch(`/genres/albums/${genre._id}`)
|
||||||
|
|
||||||
|
return {
|
||||||
|
genre,
|
||||||
|
albums: res.data,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
return results
|
||||||
|
.filter(
|
||||||
|
(
|
||||||
|
r,
|
||||||
|
): r is PromiseFulfilledResult<{
|
||||||
|
genre: any
|
||||||
|
albums: any[]
|
||||||
|
}> => r.status === "fulfilled",
|
||||||
|
)
|
||||||
|
.map((r) => r.value)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<template v-for="item in data" :key="item.genre._id">
|
||||||
|
<Slider v-if="item.albums.length" :title="item.genre.name">
|
||||||
|
<AlbumCard
|
||||||
|
v-for="album in item.albums"
|
||||||
|
:key="album._id"
|
||||||
|
:name="album.name"
|
||||||
|
:file="`${BACKEND_SERVE}${album.file}`"
|
||||||
|
:artists="album.artists"
|
||||||
|
:_id="album._id"
|
||||||
|
/>
|
||||||
|
</Slider>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { Field } from "~/components/Admin/ModalForm.vue"
|
||||||
|
import type { Row } from "~/ui/Table.vue"
|
||||||
|
|
||||||
|
const { data, refresh } = useApiFetch<Track[]>("/all/artists")
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
const tableRows: Row[] = [
|
||||||
|
{
|
||||||
|
name: "_id",
|
||||||
|
displayName: "ID",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "file",
|
||||||
|
displayName: "Cover",
|
||||||
|
type: "image",
|
||||||
|
prefix: `${BACKEND_SERVE}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "name",
|
||||||
|
displayName: "Name",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "action",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const modalFields: Field[] = [
|
||||||
|
{ key: "name", label: "Name" },
|
||||||
|
{ key: "file", label: "Cover file", type: "file" },
|
||||||
|
]
|
||||||
|
|
||||||
|
const showModal = ref<boolean>(false)
|
||||||
|
const item = ref<Track | undefined>(undefined)
|
||||||
|
|
||||||
|
const save = async (value: any) => {
|
||||||
|
if (value._id) {
|
||||||
|
try {
|
||||||
|
await cfetch("/admin/artist", {
|
||||||
|
method: "PATCH",
|
||||||
|
data: value,
|
||||||
|
params: { id: value._id },
|
||||||
|
forceFormData: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
toast.show("Artist updated successfully", "success")
|
||||||
|
} catch {
|
||||||
|
toast.show("Failed to update artist", "error")
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
await cfetch("/admin/artist", {
|
||||||
|
method: "POST",
|
||||||
|
data: value,
|
||||||
|
})
|
||||||
|
|
||||||
|
toast.show("Artist added successfully", "success")
|
||||||
|
} catch {
|
||||||
|
toast.show("Failed to add artist", "error")
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh()
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param id Set it to a value to make it edit instead of creating a new one
|
||||||
|
*/
|
||||||
|
const show = (id?: string) => {
|
||||||
|
if (!id) {
|
||||||
|
item.value = undefined
|
||||||
|
showModal.value = true
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const curItem: any = data.value.filter((i) => i._id == id)[0]
|
||||||
|
|
||||||
|
if (!curItem) return
|
||||||
|
|
||||||
|
item.value = curItem
|
||||||
|
|
||||||
|
showModal.value = true
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<h1 class="title">Manage artists</h1>
|
||||||
|
|
||||||
|
<Table :rows="tableRows" :data="data" class="table">
|
||||||
|
<template #action="{ item, index }">
|
||||||
|
<div class="action">
|
||||||
|
<Button @click="show(item._id)">Edit</Button>
|
||||||
|
<Button @click="">Delete</Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
|
<Button @click="show()">Add a new artist</Button>
|
||||||
|
|
||||||
|
<AdminModalForm
|
||||||
|
v-model:open="showModal"
|
||||||
|
@save="save"
|
||||||
|
:fields="modalFields"
|
||||||
|
:item="item"
|
||||||
|
:title="item ? 'Edit artist' : 'Add new artist'"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@use "@/styles/util" as *;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
@include fontSize(24px);
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { Field } from "~/components/Admin/ModalForm.vue"
|
||||||
|
import type { Row } from "~/ui/Table.vue"
|
||||||
|
|
||||||
|
const { data, refresh } = useApiFetch<Album[]>("/all/tracks")
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
const tableRows: Row[] = [
|
||||||
|
{
|
||||||
|
name: "_id",
|
||||||
|
displayName: "ID",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fileDir",
|
||||||
|
displayName: "Audio File",
|
||||||
|
type: "audio",
|
||||||
|
prefix: `${BACKEND_SERVE}`,
|
||||||
|
suffix: "/original.m4a",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "name",
|
||||||
|
displayName: "Name",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "album.name",
|
||||||
|
displayName: "Album",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "action",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
import { type Option } from "~/ui/SearchSelect.vue"
|
||||||
|
|
||||||
|
const { data: artistData } = useApiFetch<Genre[]>("/all/artists")
|
||||||
|
const artistFetchOptions = async (q?: string): Promise<Option[]> => {
|
||||||
|
return artistData.value.map<Option>((g) => ({
|
||||||
|
label: g.name,
|
||||||
|
value: g._id,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: albumData } = useApiFetch<Genre[]>("/all/albums")
|
||||||
|
const albumFetchOptions = async (q?: string): Promise<Option[]> => {
|
||||||
|
return albumData.value.map<Option>((g) => ({
|
||||||
|
label: g.name,
|
||||||
|
value: g._id,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const modalFields: Field[] = [
|
||||||
|
{ key: "name", label: "Name" },
|
||||||
|
{ key: "file", label: "Audio file", type: "file" },
|
||||||
|
{
|
||||||
|
key: "artists",
|
||||||
|
label: "Artists",
|
||||||
|
type: "search-select-array",
|
||||||
|
fetchOptions: artistFetchOptions,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "album",
|
||||||
|
label: "Album",
|
||||||
|
type: "search-select",
|
||||||
|
fetchOptions: albumFetchOptions,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const showModal = ref<boolean>(false)
|
||||||
|
const item = ref<Track | undefined>(undefined)
|
||||||
|
|
||||||
|
const save = async (value: any) => {
|
||||||
|
if (value._id) {
|
||||||
|
try {
|
||||||
|
await cfetch("/admin/track", {
|
||||||
|
method: "PATCH",
|
||||||
|
data: value,
|
||||||
|
params: { id: value._id },
|
||||||
|
forceFormData: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
toast.show("Song updated successfully", "success")
|
||||||
|
} catch {
|
||||||
|
toast.show("Failed to update song", "error")
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
await cfetch("/admin/track", {
|
||||||
|
method: "POST",
|
||||||
|
data: value,
|
||||||
|
})
|
||||||
|
|
||||||
|
toast.show("Song added successfully", "success")
|
||||||
|
} catch {
|
||||||
|
toast.show("Failed to add song", "error")
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh()
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param id Set it to a value to make it edit instead of creating a new one
|
||||||
|
*/
|
||||||
|
const show = (id?: string) => {
|
||||||
|
if (!id) {
|
||||||
|
item.value = undefined
|
||||||
|
showModal.value = true
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const curItem: any = data.value.filter((i) => i._id == id)[0]
|
||||||
|
|
||||||
|
if (!curItem) return
|
||||||
|
|
||||||
|
const transformed = { ...curItem }
|
||||||
|
|
||||||
|
if (transformed.album)
|
||||||
|
transformed.album = {
|
||||||
|
value: curItem.album._id,
|
||||||
|
label: curItem.album.name,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (transformed.artists)
|
||||||
|
transformed.artists = transformed.artists.map((a: Artist) => ({
|
||||||
|
value: a._id,
|
||||||
|
label: a.name,
|
||||||
|
}))
|
||||||
|
|
||||||
|
item.value = transformed
|
||||||
|
|
||||||
|
showModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(data, () => console.log(data))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<h1 class="title">Manage songs</h1>
|
||||||
|
|
||||||
|
<Table :rows="tableRows" :data="data" class="table">
|
||||||
|
<template #action="{ item, index }">
|
||||||
|
<div class="action">
|
||||||
|
<Button @click="show(item._id)">Edit</Button>
|
||||||
|
<Button @click="">Delete</Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
|
<Button @click="show()">Add a new song</Button>
|
||||||
|
|
||||||
|
<AdminModalForm
|
||||||
|
v-model:open="showModal"
|
||||||
|
@save="save"
|
||||||
|
:fields="modalFields"
|
||||||
|
:item="item"
|
||||||
|
:title="item ? 'Edit song' : 'Add new song'"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@use "@/styles/util" as *;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
@include fontSize(24px);
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+1
-14
@@ -1,18 +1,5 @@
|
|||||||
<script setup lang="ts"></script>
|
<script setup lang="ts"></script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Slider title="Folk">
|
<AlbumSlider />
|
||||||
<AlbumCard
|
|
||||||
v-for="index in 10"
|
|
||||||
:key="index"
|
|
||||||
name="I Know You'll Forget Me"
|
|
||||||
file="https://t2.genius.com/unsafe/344x344/https%3A%2F%2Fimages.genius.com%2F837160aed59b6cb52acc489d89e99fa6.1000x998x1.jpg"
|
|
||||||
:artists="[
|
|
||||||
{ name: 'Kado', _id: '1' },
|
|
||||||
{ name: 'Kado', _id: '1' },
|
|
||||||
]"
|
|
||||||
:genre="{ name: 'Folk', _id: '1' }"
|
|
||||||
_id="1"
|
|
||||||
/>
|
|
||||||
</Slider>
|
|
||||||
</template>
|
</template>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export const BACKEND_SERVE = `${import.meta.env.VITE_PUBLIC_BACKEND}/static/`
|
||||||
Reference in new issue
Block a user