Implement reordering with optimistic updates
This commit is contained in:
@ -14,6 +14,7 @@ import { usePlaylists } from '@/composeables/api/playlist-controller/playlist-co
|
|||||||
import { ChevronsUpDown, Music4, Plus } from 'lucide-vue-next';
|
import { ChevronsUpDown, Music4, Plus } from 'lucide-vue-next';
|
||||||
import Button from '../ui/button/Button.vue';
|
import Button from '../ui/button/Button.vue';
|
||||||
import PlaylistCreateDialog from './playlists/select/PlaylistCreateDialog.vue';
|
import PlaylistCreateDialog from './playlists/select/PlaylistCreateDialog.vue';
|
||||||
|
import { useCurrentPlaylistStore } from '~/stores/use-current-playlist-store';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
open: sidebarOpen,
|
open: sidebarOpen,
|
||||||
@ -21,18 +22,18 @@ const {
|
|||||||
|
|
||||||
const { isLoading, isError, error, data } = usePlaylists();
|
const { isLoading, isError, error, data } = usePlaylists();
|
||||||
|
|
||||||
const selectedPlaylist = ref(-1);
|
const currentPlaylistStore = useCurrentPlaylistStore();
|
||||||
|
|
||||||
watch(data, (value) => {
|
watch(data, (value) => {
|
||||||
const newValue = value?.data[0]?.id || -1;
|
const newValue = value?.data[0];
|
||||||
if (selectedPlaylist.value === -1) {
|
if (currentPlaylistStore.id === -1 && newValue) {
|
||||||
selectedPlaylist.value = newValue;
|
currentPlaylistStore.load(newValue);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Select v-model="selectedPlaylist">
|
<Select v-model="currentPlaylistStore.id">
|
||||||
<div v-if="sidebarOpen" class="hover:bg-sidebar-muted cursor-pointer rounded-md select-none">
|
<div v-if="sidebarOpen" class="hover:bg-sidebar-muted cursor-pointer rounded-md select-none">
|
||||||
<SelectCustomTrigger class="w-full flex p-2 gap-2 items-center">
|
<SelectCustomTrigger class="w-full flex p-2 gap-2 items-center">
|
||||||
<Frame borderRadius="round" background="primary" padding="dense" margin="none">
|
<Frame borderRadius="round" background="primary" padding="dense" margin="none">
|
||||||
@ -42,7 +43,7 @@ watch(data, (value) => {
|
|||||||
<Skeleton v-if="isLoading" class="w-32 h-5 rounded-full" />
|
<Skeleton v-if="isLoading" class="w-32 h-5 rounded-full" />
|
||||||
<h4 v-else-if="data" class="text-xl font-semibold tracking-tight truncate">
|
<h4 v-else-if="data" class="text-xl font-semibold tracking-tight truncate">
|
||||||
<!-- TODO: i18n -->
|
<!-- TODO: i18n -->
|
||||||
{{data.data.find(playlist => playlist.id === selectedPlaylist)?.title ||
|
{{data.data.find(playlist => playlist.id === currentPlaylistStore.id)?.title ||
|
||||||
'No playlist selected'}}
|
'No playlist selected'}}
|
||||||
</h4>
|
</h4>
|
||||||
<p class="text-sm text-muted-foreground">
|
<p class="text-sm text-muted-foreground">
|
||||||
|
|||||||
@ -12,5 +12,8 @@ export * from './playlistReadDTO';
|
|||||||
export * from './playlistReadResponse';
|
export * from './playlistReadResponse';
|
||||||
export * from './playlistTrackResponse';
|
export * from './playlistTrackResponse';
|
||||||
export * from './readParams';
|
export * from './readParams';
|
||||||
|
export * from './trackBulkReorderRequest';
|
||||||
|
export * from './trackReoderAfterRequest';
|
||||||
|
export * from './trackReorderAfterRequest';
|
||||||
export * from './trackResponse';
|
export * from './trackResponse';
|
||||||
export * from './uploadBody';
|
export * from './uploadBody';
|
||||||
10
app/composeables/api/models/trackBulkReorderRequest.ts
Normal file
10
app/composeables/api/models/trackBulkReorderRequest.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
/**
|
||||||
|
* Generated by orval v7.16.0 🍺
|
||||||
|
* Do not edit manually.
|
||||||
|
* OpenAPI definition
|
||||||
|
* OpenAPI spec version: v0
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface TrackBulkReorderRequest {
|
||||||
|
trackIds?: number[];
|
||||||
|
}
|
||||||
11
app/composeables/api/models/trackReorderAfterRequest.ts
Normal file
11
app/composeables/api/models/trackReorderAfterRequest.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* Generated by orval v7.16.0 🍺
|
||||||
|
* Do not edit manually.
|
||||||
|
* OpenAPI definition
|
||||||
|
* OpenAPI spec version: v0
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface TrackReorderAfterRequest {
|
||||||
|
moveTrackId?: number;
|
||||||
|
targetTrackId?: number;
|
||||||
|
}
|
||||||
@ -31,6 +31,7 @@ import type {
|
|||||||
import type {
|
import type {
|
||||||
AddLocalTrackRequest,
|
AddLocalTrackRequest,
|
||||||
PlaylistTrackResponse,
|
PlaylistTrackResponse,
|
||||||
|
TrackBulkReorderRequest,
|
||||||
TrackResponse
|
TrackResponse
|
||||||
} from '.././models';
|
} from '.././models';
|
||||||
|
|
||||||
@ -103,6 +104,66 @@ const {mutation: mutationOptions, request: requestOptions} = options ?
|
|||||||
|
|
||||||
return useMutation(mutationOptions, queryClient);
|
return useMutation(mutationOptions, queryClient);
|
||||||
}
|
}
|
||||||
|
export const bulkReorder = (
|
||||||
|
playlistId: MaybeRef<number>,
|
||||||
|
trackBulkReorderRequest: MaybeRef<TrackBulkReorderRequest>,
|
||||||
|
options?: SecondParameter<typeof axiosInstance>,signal?: AbortSignal
|
||||||
|
) => {
|
||||||
|
playlistId = unref(playlistId);
|
||||||
|
trackBulkReorderRequest = unref(trackBulkReorderRequest);
|
||||||
|
|
||||||
|
return axiosInstance<void>(
|
||||||
|
{url: `/playlist/${playlistId}/bulk-reorder`, method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json', },
|
||||||
|
data: trackBulkReorderRequest, signal
|
||||||
|
},
|
||||||
|
options);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export const getBulkReorderMutationOptions = <TError = unknown,
|
||||||
|
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof bulkReorder>>, TError,{playlistId: number;data: TrackBulkReorderRequest}, TContext>, request?: SecondParameter<typeof axiosInstance>}
|
||||||
|
): UseMutationOptions<Awaited<ReturnType<typeof bulkReorder>>, TError,{playlistId: number;data: TrackBulkReorderRequest}, TContext> => {
|
||||||
|
|
||||||
|
const mutationKey = ['bulkReorder'];
|
||||||
|
const {mutation: mutationOptions, request: requestOptions} = options ?
|
||||||
|
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
|
||||||
|
options
|
||||||
|
: {...options, mutation: {...options.mutation, mutationKey}}
|
||||||
|
: {mutation: { mutationKey, }, request: undefined};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const mutationFn: MutationFunction<Awaited<ReturnType<typeof bulkReorder>>, {playlistId: number;data: TrackBulkReorderRequest}> = (props) => {
|
||||||
|
const {playlistId,data} = props ?? {};
|
||||||
|
|
||||||
|
return bulkReorder(playlistId,data,requestOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return { mutationFn, ...mutationOptions }}
|
||||||
|
|
||||||
|
export type BulkReorderMutationResult = NonNullable<Awaited<ReturnType<typeof bulkReorder>>>
|
||||||
|
export type BulkReorderMutationBody = TrackBulkReorderRequest
|
||||||
|
export type BulkReorderMutationError = unknown
|
||||||
|
|
||||||
|
export const useBulkReorder = <TError = unknown,
|
||||||
|
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof bulkReorder>>, TError,{playlistId: number;data: TrackBulkReorderRequest}, TContext>, request?: SecondParameter<typeof axiosInstance>}
|
||||||
|
, queryClient?: QueryClient): UseMutationReturnType<
|
||||||
|
Awaited<ReturnType<typeof bulkReorder>>,
|
||||||
|
TError,
|
||||||
|
{playlistId: number;data: TrackBulkReorderRequest},
|
||||||
|
TContext
|
||||||
|
> => {
|
||||||
|
|
||||||
|
const mutationOptions = getBulkReorderMutationOptions(options);
|
||||||
|
|
||||||
|
return useMutation(mutationOptions, queryClient);
|
||||||
|
}
|
||||||
export const getPlaylistTracks = (
|
export const getPlaylistTracks = (
|
||||||
playlistId: MaybeRef<number>,
|
playlistId: MaybeRef<number>,
|
||||||
options?: SecondParameter<typeof axiosInstance>,signal?: AbortSignal
|
options?: SecondParameter<typeof axiosInstance>,signal?: AbortSignal
|
||||||
|
|||||||
@ -7,56 +7,102 @@ import { SidebarTrigger } from '@/components/ui/sidebar'
|
|||||||
import MusicCard from '~/components/internal/musiccard/MusicCard.vue'
|
import MusicCard from '~/components/internal/musiccard/MusicCard.vue'
|
||||||
import { DnDOperations, useDroppable } from '@vue-dnd-kit/core'
|
import { DnDOperations, useDroppable } from '@vue-dnd-kit/core'
|
||||||
import Draggable from '~/components/action/Draggable.vue'
|
import Draggable from '~/components/action/Draggable.vue'
|
||||||
|
import { useCurrentPlaylistStore } from '~/stores/use-current-playlist-store'
|
||||||
|
import { bulkReorder, getGetPlaylistTracksQueryKey } from '~/composeables/api/track-controller/track-controller'
|
||||||
|
import { useGetPlaylistTracks } from '~/composeables/api/track-controller/track-controller'
|
||||||
|
import { useMutation, useQueryClient } from '@tanstack/vue-query'
|
||||||
|
|
||||||
const searchValue = ref('')
|
const searchValue = ref('')
|
||||||
|
|
||||||
// Mock data
|
|
||||||
const tracks = ref([
|
const queryClient = useQueryClient();
|
||||||
|
const currentPlaylistStore = useCurrentPlaylistStore();
|
||||||
|
|
||||||
|
const { data: playlistTracks, isLoading, error, refetch } = useGetPlaylistTracks(
|
||||||
|
computed(() => currentPlaylistStore.id),
|
||||||
{
|
{
|
||||||
id: 1,
|
query: {
|
||||||
title: "Best of Chobits OST - Beyond",
|
enabled: computed(() => currentPlaylistStore.id !== -1)
|
||||||
author: "ビヨンド",
|
|
||||||
authorLabel: "Author",
|
|
||||||
badges: ["mp3", "jpop", "anime"],
|
|
||||||
imageUrl: "https://github.com/yavuzceliker/sample-images/blob/main/docs/image-1.jpg?raw=true",
|
|
||||||
date: "about 17 years ago",
|
|
||||||
selected: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
title: "Summer Vibessssssssssssssssssssssssssssssssssssfawefjioawefjeaiofjeaoifjeaofejiaofejaiojfoaiss",
|
|
||||||
author: "Various Artists",
|
|
||||||
authorLabel: "Artists",
|
|
||||||
badges: ["mp3", "summer", "mix"],
|
|
||||||
imageUrl: "https://github.com/yavuzceliker/sample-images/blob/main/docs/image-2.jpg?raw=true",
|
|
||||||
date: "about 1 hour ago",
|
|
||||||
selected: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
title: "Unknown Track",
|
|
||||||
author: "Unknown Artist",
|
|
||||||
authorLabel: "Author",
|
|
||||||
badges: ["wav"],
|
|
||||||
imageUrl: "https://github.com/yavuzceliker/sample-images/blob/main/docs/image-3.jpg?raw=true",
|
|
||||||
selected: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
title: "Single Track",
|
|
||||||
author: "Solo Artist",
|
|
||||||
authorLabel: "Author",
|
|
||||||
badges: [],
|
|
||||||
imageUrl: "https://github.com/yavuzceliker/sample-images/blob/main/docs/image-5.jpg?raw=true",
|
|
||||||
date: "recently added",
|
|
||||||
selected: false
|
|
||||||
}
|
}
|
||||||
])
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const { mutate: reorderTracks } = useMutation({
|
||||||
|
mutationFn: ({ playlistId, trackIds }: { playlistId: number; trackIds: number[] }) =>
|
||||||
|
bulkReorder(playlistId, { trackIds }),
|
||||||
|
onMutate: async ({ playlistId, trackIds }) => {
|
||||||
|
await queryClient.cancelQueries({ queryKey: getGetPlaylistTracksQueryKey(playlistId) });
|
||||||
|
|
||||||
|
const previousTracks = queryClient.getQueryData(getGetPlaylistTracksQueryKey(playlistId));
|
||||||
|
|
||||||
|
queryClient.setQueryData(getGetPlaylistTracksQueryKey(playlistId), (old: any) => {
|
||||||
|
if (!old) return old;
|
||||||
|
return {
|
||||||
|
...old,
|
||||||
|
data: trackIds.map(id => old.data.find((t: any) => t.trackId === id || t.id === id)),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return { previousTracks };
|
||||||
|
},
|
||||||
|
onError: (err, { playlistId }, context) => {
|
||||||
|
if (context?.previousTracks) {
|
||||||
|
queryClient.setQueryData(getGetPlaylistTracksQueryKey(playlistId), context.previousTracks);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSettled: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: getGetPlaylistTracksQueryKey(currentPlaylistStore.id) });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const trackCount = computed(() => playlistTracks.value?.data.length || 0)
|
||||||
|
|
||||||
|
const mappedTracks = computed(() => {
|
||||||
|
if (!playlistTracks.value) return []
|
||||||
|
const tracks = playlistTracks.value.data
|
||||||
|
|
||||||
|
return tracks.map((track: any, index: number) => ({
|
||||||
|
id: track.trackId || track.id,
|
||||||
|
title: track.track?.title || track.title || 'Unknown Track',
|
||||||
|
author: track.track?.artist?.name || track.artist || 'Unknown Artist',
|
||||||
|
authorLabel: "Artist",
|
||||||
|
badges: track.track?.format ? [track.track.format] : ['mp3'],
|
||||||
|
imageUrl: track.track?.coverUrl || track.coverUrl || getDefaultImage(index),
|
||||||
|
date: formatDate(track.addedDate || track.createdAt),
|
||||||
|
selected: false
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
const getDefaultImage = (index: number) => {
|
||||||
|
const defaultImages = [
|
||||||
|
"https://github.com/yavuzceliker/sample-images/blob/main/docs/image-1.jpg?raw=true",
|
||||||
|
"https://github.com/yavuzceliker/sample-images/blob/main/docs/image-2.jpg?raw=true",
|
||||||
|
"https://github.com/yavuzceliker/sample-images/blob/main/docs/image-3.jpg?raw=true",
|
||||||
|
"https://github.com/yavuzceliker/sample-images/blob/main/docs/image-5.jpg?raw=true"
|
||||||
|
]
|
||||||
|
return defaultImages[index % defaultImages.length]
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDate = (dateString: string) => {
|
||||||
|
if (!dateString) return 'recently added'
|
||||||
|
|
||||||
|
const date = new Date(dateString)
|
||||||
|
const now = new Date()
|
||||||
|
const diffTime = Math.abs(now.getTime() - date.getTime())
|
||||||
|
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
|
||||||
|
|
||||||
|
if (diffDays === 1) return 'yesterday'
|
||||||
|
if (diffDays < 7) return `${diffDays} days ago`
|
||||||
|
if (diffDays < 30) return `${Math.floor(diffDays / 7)} weeks ago`
|
||||||
|
if (diffDays < 365) return `${Math.floor(diffDays / 30)} months ago`
|
||||||
|
return `${Math.floor(diffDays / 365)} years ago`
|
||||||
|
}
|
||||||
|
|
||||||
const filteredTracks = computed(() => {
|
const filteredTracks = computed(() => {
|
||||||
if (!searchValue.value) return tracks.value
|
if (!searchValue.value) return mappedTracks.value
|
||||||
|
|
||||||
return tracks.value.filter(track =>
|
return mappedTracks.value.filter(track =>
|
||||||
track.title.toLowerCase().includes(searchValue.value.toLowerCase()) ||
|
track.title.toLowerCase().includes(searchValue.value.toLowerCase()) ||
|
||||||
track.author.toLowerCase().includes(searchValue.value.toLowerCase()) ||
|
track.author.toLowerCase().includes(searchValue.value.toLowerCase()) ||
|
||||||
track.badges.some(badge => badge.toLowerCase().includes(searchValue.value.toLowerCase()))
|
track.badges.some(badge => badge.toLowerCase().includes(searchValue.value.toLowerCase()))
|
||||||
@ -64,19 +110,36 @@ const filteredTracks = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const selectTrack = (trackId: number) => {
|
const selectTrack = (trackId: number) => {
|
||||||
tracks.value.forEach(track => {
|
mappedTracks.value.forEach(track => {
|
||||||
track.selected = track.id === trackId
|
track.selected = track.id === trackId
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const { elementRef: tracksRef } = useDroppable({
|
const { elementRef: tracksRef } = useDroppable({
|
||||||
data: {
|
data: {
|
||||||
source: tracks.value,
|
source: mappedTracks.value,
|
||||||
},
|
},
|
||||||
events: {
|
events: {
|
||||||
onDrop: DnDOperations.applyTransfer,
|
onDrop: (store, payload) => {
|
||||||
|
DnDOperations.applyTransfer(store);
|
||||||
|
if (currentPlaylistStore.id === -1) return;
|
||||||
|
if (mappedTracks.value.length !== trackCount.value) return;
|
||||||
|
if (mappedTracks.value.some(t => !t)) return;
|
||||||
|
|
||||||
|
const trackIds = mappedTracks.value?.map(t => t.id);
|
||||||
|
reorderTracks({
|
||||||
|
playlistId: currentPlaylistStore.id,
|
||||||
|
trackIds,
|
||||||
|
});
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(() => currentPlaylistStore.id, (newId) => {
|
||||||
|
if (newId !== -1) {
|
||||||
|
refetch()
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -99,10 +162,24 @@ const { elementRef: tracksRef } = useDroppable({
|
|||||||
<InputWithIcon v-model="searchValue" :icon="Search" placeholder="Search..." type="search" icon-size="5"
|
<InputWithIcon v-model="searchValue" :icon="Search" placeholder="Search..." type="search" icon-size="5"
|
||||||
id="user-search" class="w-full" />
|
id="user-search" class="w-full" />
|
||||||
|
|
||||||
<div ref="tracksRef" class="space-y-2">
|
<div ref="tracksRef">
|
||||||
|
<div v-if="isLoading" class="flex justify-center items-center py-8">
|
||||||
|
<p>Loading tracks...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="error" class="flex justify-center items-center py-8">
|
||||||
|
<p class="text-red-500">Error loading tracks: {{ error }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="!filteredTracks.length" class="flex justify-center items-center py-8">
|
||||||
|
<p v-if="searchValue">No tracks found matching "{{ searchValue }}"</p>
|
||||||
|
<p v-else>No tracks in this playlist</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else ref="tracksRef" class="space-y-2">
|
||||||
<TransitionGroup name="list" ref="tracksRef">
|
<TransitionGroup name="list" ref="tracksRef">
|
||||||
<Draggable v-for="(track, index) in filteredTracks" :key="track.id" :index="index"
|
<Draggable v-for="(track, index) in filteredTracks" :key="track.id" :index="index"
|
||||||
:source="tracks">
|
:source="mappedTracks">
|
||||||
<MusicCard :key="track.id" :title="track.title" :author="track.author"
|
<MusicCard :key="track.id" :title="track.title" :author="track.author"
|
||||||
:authorLabel="track.authorLabel" :badges="track.badges" :imageUrl="track.imageUrl"
|
:authorLabel="track.authorLabel" :badges="track.badges" :imageUrl="track.imageUrl"
|
||||||
:date="track.date" :selected="track.selected" @click="selectTrack(track.id)" />
|
:date="track.date" :selected="track.selected" @click="selectTrack(track.id)" />
|
||||||
@ -110,6 +187,7 @@ const { elementRef: tracksRef } = useDroppable({
|
|||||||
</TransitionGroup>
|
</TransitionGroup>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<template #sidebar>
|
<template #sidebar>
|
||||||
<Outline padding="none" class="h-full" side="left">
|
<Outline padding="none" class="h-full" side="left">
|
||||||
<Outline padding="dense" side="bottom">
|
<Outline padding="dense" side="bottom">
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
// ~/plugins/vue-dnd-kit.client.ts
|
|
||||||
import VueDnDKitPlugin from '@vue-dnd-kit/core';
|
import VueDnDKitPlugin from '@vue-dnd-kit/core';
|
||||||
|
|
||||||
export default defineNuxtPlugin((nuxtApp) => {
|
export default defineNuxtPlugin((nuxtApp) => {
|
||||||
|
|||||||
24
app/stores/use-current-playlist-store.ts
Normal file
24
app/stores/use-current-playlist-store.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import type { PlaylistReadResponse } from '~/composeables/api/models'
|
||||||
|
|
||||||
|
export const useCurrentPlaylistStore = defineStore('current-playlist', {
|
||||||
|
state: () => ({
|
||||||
|
id: -1,
|
||||||
|
ownerId: -1,
|
||||||
|
title: '',
|
||||||
|
createdAt: '',
|
||||||
|
updatedAt: '',
|
||||||
|
}),
|
||||||
|
getters: {
|
||||||
|
getId: (state) => state.id * 2,
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
load(response: PlaylistReadResponse) {
|
||||||
|
this.id = response.id || -1;
|
||||||
|
this.ownerId = response.ownerId || -1;
|
||||||
|
this.title = response.title || '';
|
||||||
|
this.createdAt = response.createdAt || '';
|
||||||
|
this.updatedAt = response.updatedAt || '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
16
bun.lock
16
bun.lock
@ -8,6 +8,7 @@
|
|||||||
"@nuxt/fonts": "0.11.4",
|
"@nuxt/fonts": "0.11.4",
|
||||||
"@nuxt/icon": "2.1.0",
|
"@nuxt/icon": "2.1.0",
|
||||||
"@nuxt/image": "1.11.0",
|
"@nuxt/image": "1.11.0",
|
||||||
|
"@pinia/nuxt": "0.11.3",
|
||||||
"@tailwindcss/vite": "^4.1.16",
|
"@tailwindcss/vite": "^4.1.16",
|
||||||
"@tanstack/vue-query": "^5.90.7",
|
"@tanstack/vue-query": "^5.90.7",
|
||||||
"@vee-validate/zod": "^4.15.1",
|
"@vee-validate/zod": "^4.15.1",
|
||||||
@ -20,6 +21,7 @@
|
|||||||
"lucide-vue-next": "^0.548.0",
|
"lucide-vue-next": "^0.548.0",
|
||||||
"nuxt": "^4.2.0",
|
"nuxt": "^4.2.0",
|
||||||
"oidc-client-ts": "^3.3.0",
|
"oidc-client-ts": "^3.3.0",
|
||||||
|
"pinia": "^3.0.4",
|
||||||
"reka-ui": "^2.6.0",
|
"reka-ui": "^2.6.0",
|
||||||
"shadcn-nuxt": "2.3.2",
|
"shadcn-nuxt": "2.3.2",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
@ -410,6 +412,8 @@
|
|||||||
|
|
||||||
"@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.1", "", { "os": "win32", "cpu": "x64" }, "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA=="],
|
"@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.1", "", { "os": "win32", "cpu": "x64" }, "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA=="],
|
||||||
|
|
||||||
|
"@pinia/nuxt": ["@pinia/nuxt@0.11.3", "", { "dependencies": { "@nuxt/kit": "^4.2.0" }, "peerDependencies": { "pinia": "^3.0.4" } }, "sha512-7WVNHpWx4qAEzOlnyrRC88kYrwnlR/PrThWT0XI1dSNyUAXu/KBv9oR37uCgYkZroqP5jn8DfzbkNF3BtKvE9w=="],
|
||||||
|
|
||||||
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
|
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
|
||||||
|
|
||||||
"@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
|
"@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
|
||||||
@ -1532,6 +1536,8 @@
|
|||||||
|
|
||||||
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||||
|
|
||||||
|
"pinia": ["pinia@3.0.4", "", { "dependencies": { "@vue/devtools-api": "^7.7.7" }, "peerDependencies": { "typescript": ">=4.5.0", "vue": "^3.5.11" }, "optionalPeers": ["typescript"] }, "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw=="],
|
||||||
|
|
||||||
"pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="],
|
"pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="],
|
||||||
|
|
||||||
"pony-cause": ["pony-cause@1.1.1", "", {}, "sha512-PxkIc/2ZpLiEzQXu5YRDOUgBlfGYBY8156HY5ZcRAwwonMk5W/MrJP2LLkG/hF7GEQzaHo2aS7ho6ZLCOvf+6g=="],
|
"pony-cause": ["pony-cause@1.1.1", "", {}, "sha512-PxkIc/2ZpLiEzQXu5YRDOUgBlfGYBY8156HY5ZcRAwwonMk5W/MrJP2LLkG/hF7GEQzaHo2aS7ho6ZLCOvf+6g=="],
|
||||||
@ -2062,6 +2068,8 @@
|
|||||||
|
|
||||||
"@parcel/watcher-wasm/napi-wasm": ["napi-wasm@1.1.3", "", { "bundled": true }, "sha512-h/4nMGsHjZDCYmQVNODIrYACVJ+I9KItbG+0si6W/jSjdA9JbWDoU4LLeMXVcEQGHjttI2tuXqDrbGF7qkUHHg=="],
|
"@parcel/watcher-wasm/napi-wasm": ["napi-wasm@1.1.3", "", { "bundled": true }, "sha512-h/4nMGsHjZDCYmQVNODIrYACVJ+I9KItbG+0si6W/jSjdA9JbWDoU4LLeMXVcEQGHjttI2tuXqDrbGF7qkUHHg=="],
|
||||||
|
|
||||||
|
"@pinia/nuxt/@nuxt/kit": ["@nuxt/kit@4.2.0", "", { "dependencies": { "c12": "^3.3.1", "consola": "^3.4.2", "defu": "^6.1.4", "destr": "^2.0.5", "errx": "^0.1.0", "exsolve": "^1.0.7", "ignore": "^7.0.5", "jiti": "^2.6.1", "klona": "^2.0.6", "mlly": "^1.8.0", "ohash": "^2.0.11", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "rc9": "^2.1.2", "scule": "^1.3.0", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ufo": "^1.6.1", "unctx": "^2.4.1", "untyped": "^2.0.0" } }, "sha512-1yN3LL6RDN5GjkNLPUYCbNRkaYnat6hqejPyfIBBVzrWOrpiQeNMGxQM/IcVdaSuBJXAnu0sUvTKXpXkmPhljg=="],
|
||||||
|
|
||||||
"@poppinss/dumper/supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="],
|
"@poppinss/dumper/supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="],
|
||||||
|
|
||||||
"@rollup/plugin-commonjs/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
"@rollup/plugin-commonjs/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||||
@ -2154,6 +2162,8 @@
|
|||||||
|
|
||||||
"oas-validator/yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="],
|
"oas-validator/yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="],
|
||||||
|
|
||||||
|
"pinia/@vue/devtools-api": ["@vue/devtools-api@7.7.8", "", { "dependencies": { "@vue/devtools-kit": "^7.7.8" } }, "sha512-BtFcAmDbtXGwurWUFf8ogIbgZyR+rcVES1TSNEI8Em80fD8Anu+qTRN1Fc3J6vdRHlVM3fzPV1qIo+B4AiqGzw=="],
|
||||||
|
|
||||||
"postcss-svgo/svgo": ["svgo@4.0.0", "", { "dependencies": { "commander": "^11.1.0", "css-select": "^5.1.0", "css-tree": "^3.0.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.1.1", "sax": "^1.4.1" }, "bin": "./bin/svgo.js" }, "sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw=="],
|
"postcss-svgo/svgo": ["svgo@4.0.0", "", { "dependencies": { "commander": "^11.1.0", "css-select": "^5.1.0", "css-tree": "^3.0.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.1.1", "sax": "^1.4.1" }, "bin": "./bin/svgo.js" }, "sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw=="],
|
||||||
|
|
||||||
"prebuild-install/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
"prebuild-install/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||||
@ -2288,6 +2298,8 @@
|
|||||||
|
|
||||||
"nitropack/globby/slash": ["slash@5.1.0", "", {}, "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg=="],
|
"nitropack/globby/slash": ["slash@5.1.0", "", {}, "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg=="],
|
||||||
|
|
||||||
|
"pinia/@vue/devtools-api/@vue/devtools-kit": ["@vue/devtools-kit@7.7.8", "", { "dependencies": { "@vue/devtools-shared": "^7.7.8", "birpc": "^2.3.0", "hookable": "^5.5.3", "mitt": "^3.0.1", "perfect-debounce": "^1.0.0", "speakingurl": "^14.0.1", "superjson": "^2.2.2" } }, "sha512-4Y8op+AoxOJhB9fpcEF6d5vcJXWKgHxC3B0ytUB8zz15KbP9g9WgVzral05xluxi2fOeAy6t140rdQ943GcLRQ=="],
|
||||||
|
|
||||||
"postcss-svgo/svgo/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
|
"postcss-svgo/svgo/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
|
||||||
|
|
||||||
"prebuild-install/tar-fs/tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="],
|
"prebuild-install/tar-fs/tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="],
|
||||||
@ -2368,6 +2380,10 @@
|
|||||||
|
|
||||||
"listhen/clipboardy/execa/strip-final-newline": ["strip-final-newline@3.0.0", "", {}, "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw=="],
|
"listhen/clipboardy/execa/strip-final-newline": ["strip-final-newline@3.0.0", "", {}, "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw=="],
|
||||||
|
|
||||||
|
"pinia/@vue/devtools-api/@vue/devtools-kit/@vue/devtools-shared": ["@vue/devtools-shared@7.7.8", "", { "dependencies": { "rfdc": "^1.4.1" } }, "sha512-XHpO3jC5nOgYr40M9p8Z4mmKfTvUxKyRcUnpBAYg11pE78eaRFBKb0kG5yKLroMuJeeNH9LWmKp2zMU5LUc7CA=="],
|
||||||
|
|
||||||
|
"pinia/@vue/devtools-api/@vue/devtools-kit/perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="],
|
||||||
|
|
||||||
"prebuild-install/tar-fs/tar-stream/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
"prebuild-install/tar-fs/tar-stream/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
||||||
|
|
||||||
"vee-validate/@vue/devtools-api/@vue/devtools-kit/@vue/devtools-shared": ["@vue/devtools-shared@7.7.8", "", { "dependencies": { "rfdc": "^1.4.1" } }, "sha512-XHpO3jC5nOgYr40M9p8Z4mmKfTvUxKyRcUnpBAYg11pE78eaRFBKb0kG5yKLroMuJeeNH9LWmKp2zMU5LUc7CA=="],
|
"vee-validate/@vue/devtools-api/@vue/devtools-kit/@vue/devtools-shared": ["@vue/devtools-shared@7.7.8", "", { "dependencies": { "rfdc": "^1.4.1" } }, "sha512-XHpO3jC5nOgYr40M9p8Z4mmKfTvUxKyRcUnpBAYg11pE78eaRFBKb0kG5yKLroMuJeeNH9LWmKp2zMU5LUc7CA=="],
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import tailwindcss from "@tailwindcss/vite";
|
|||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
compatibilityDate: '2025-07-15',
|
compatibilityDate: '2025-07-15',
|
||||||
devtools: { enabled: true },
|
devtools: { enabled: true },
|
||||||
modules: ['@nuxt/fonts', '@nuxt/icon', '@nuxt/image', 'shadcn-nuxt'],
|
modules: ['@nuxt/fonts', '@nuxt/icon', '@nuxt/image', 'shadcn-nuxt', '@pinia/nuxt',],
|
||||||
shadcn: {
|
shadcn: {
|
||||||
prefix: '',
|
prefix: '',
|
||||||
componentDir: './components/ui'
|
componentDir: './components/ui'
|
||||||
@ -19,5 +19,8 @@ export default defineNuxtConfig({
|
|||||||
public: {
|
public: {
|
||||||
apiBaseUrl: process.env.API_BASE_URL
|
apiBaseUrl: process.env.API_BASE_URL
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
pinia: {
|
||||||
|
storesDirs: ['./app/stores/**'],
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@ -13,6 +13,7 @@
|
|||||||
"@nuxt/fonts": "0.11.4",
|
"@nuxt/fonts": "0.11.4",
|
||||||
"@nuxt/icon": "2.1.0",
|
"@nuxt/icon": "2.1.0",
|
||||||
"@nuxt/image": "1.11.0",
|
"@nuxt/image": "1.11.0",
|
||||||
|
"@pinia/nuxt": "0.11.3",
|
||||||
"@tailwindcss/vite": "^4.1.16",
|
"@tailwindcss/vite": "^4.1.16",
|
||||||
"@tanstack/vue-query": "^5.90.7",
|
"@tanstack/vue-query": "^5.90.7",
|
||||||
"@vee-validate/zod": "^4.15.1",
|
"@vee-validate/zod": "^4.15.1",
|
||||||
@ -25,6 +26,7 @@
|
|||||||
"lucide-vue-next": "^0.548.0",
|
"lucide-vue-next": "^0.548.0",
|
||||||
"nuxt": "^4.2.0",
|
"nuxt": "^4.2.0",
|
||||||
"oidc-client-ts": "^3.3.0",
|
"oidc-client-ts": "^3.3.0",
|
||||||
|
"pinia": "^3.0.4",
|
||||||
"reka-ui": "^2.6.0",
|
"reka-ui": "^2.6.0",
|
||||||
"shadcn-nuxt": "2.3.2",
|
"shadcn-nuxt": "2.3.2",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
|
|||||||
Reference in New Issue
Block a user