Replace vue-dnd-code with vue-draggable-next
This commit is contained in:
@ -1,42 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { useDraggable } from '@vue-dnd-kit/core';
|
|
||||||
import { computed } from 'vue';
|
|
||||||
|
|
||||||
const { index, source } = defineProps<{
|
|
||||||
index: number;
|
|
||||||
source: any[];
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const { elementRef, handleDragStart, isOvered, isDragging } = useDraggable({
|
|
||||||
data: computed(() => ({
|
|
||||||
index,
|
|
||||||
source,
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div ref="elementRef" @pointerdown="handleDragStart" :class="{
|
|
||||||
'is-over': isOvered,
|
|
||||||
'is-dragging': isDragging,
|
|
||||||
}" class="draggable">
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.draggable {
|
|
||||||
cursor: move;
|
|
||||||
user-select: none;
|
|
||||||
touch-action: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.is-dragging {
|
|
||||||
opacity: 0.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.is-over {
|
|
||||||
padding-top: 0.5rem;
|
|
||||||
border-top: 2px solid var(--border);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,10 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<Frame margin="none" class="px-2 py-3 flex h-fit gap-2 hover:bg-muted" :class="selected && 'bg-muted'">
|
<Frame margin="none" class="px-2 py-3 flex h-fit gap-2 hover:bg-muted">
|
||||||
<div class="text-[2rem] h-full">
|
<div class="text-[2rem] h-full">
|
||||||
<Draggable />
|
<Draggable />
|
||||||
</div>
|
</div>
|
||||||
<Separator orientation="vertical" />
|
<Separator orientation="vertical" />
|
||||||
<div class="w-fit flex-1 flex flex-col justify-between">
|
<div class="w-fit flex-1 flex flex-col justify-between cursor-pointer" data-no-drag @click="onPlayClick">
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<div class="flex items-start justify-between w-full">
|
<div class="flex items-start justify-between w-full">
|
||||||
<h4 class="scroll-m-20 text-xl font-semibold tracking-tight truncate max-w-[32ch]">
|
<h4 class="scroll-m-20 text-xl font-semibold tracking-tight truncate max-w-[32ch]">
|
||||||
@ -26,8 +26,8 @@
|
|||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Separator orientation="vertical" />
|
<Separator orientation="vertical" class="cursor-pointer" data-no-drag @click="onPlayClick" />
|
||||||
<div class="max-w-20 max-h-20 w-20 h-20" v-if="imageUrl">
|
<div class="max-w-20 max-h-20 w-20 h-20 cursor-pointer" data-no-drag v-if="imageUrl" @click="onPlayClick">
|
||||||
<NuxtImg class="object-cover w-full h-full rounded-md" :src="imageUrl" :alt="title" />
|
<NuxtImg class="object-cover w-full h-full rounded-md" :src="imageUrl" :alt="title" />
|
||||||
</div>
|
</div>
|
||||||
</Frame>
|
</Frame>
|
||||||
@ -42,14 +42,22 @@ import Frame from '@/components/ui/frame/Frame.vue'
|
|||||||
interface Props {
|
interface Props {
|
||||||
title: string
|
title: string
|
||||||
author: string
|
author: string
|
||||||
selected?: boolean
|
|
||||||
badges?: string[]
|
badges?: string[]
|
||||||
imageUrl?: string
|
imageUrl?: string
|
||||||
date?: string
|
date?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
onPlay: []
|
||||||
|
}>();
|
||||||
|
|
||||||
withDefaults(defineProps<Props>(), {
|
withDefaults(defineProps<Props>(), {
|
||||||
authorLabel: 'Author',
|
authorLabel: 'Author',
|
||||||
badges: () => []
|
badges: () => []
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function onPlayClick(e: MouseEvent) {
|
||||||
|
emit("onPlay")
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -1,16 +1,15 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Search } from 'lucide-vue-next'
|
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { InputWithIcon } from '@/components/ui/input'
|
import { InputWithIcon } from '@/components/ui/input'
|
||||||
import { Outline } from '@/components/ui/outline'
|
import { Outline } from '@/components/ui/outline'
|
||||||
import { SidebarTrigger } from '@/components/ui/sidebar'
|
import { SidebarTrigger } from '@/components/ui/sidebar'
|
||||||
import MusicCard from '~/components/internal/musiccard/MusicCard.vue'
|
|
||||||
import { DnDOperations, useDroppable } from '@vue-dnd-kit/core'
|
|
||||||
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'
|
import { useMutation, useQueryClient } from '@tanstack/vue-query'
|
||||||
|
import { Search } from 'lucide-vue-next'
|
||||||
|
import { VueDraggableNext } from 'vue-draggable-next'
|
||||||
|
import MusicCard from '~/components/internal/musiccard/MusicCard.vue'
|
||||||
|
import type { TrackResponse } from '~/composeables/api/models'
|
||||||
|
import { bulkReorder, getGetPlaylistTracksQueryKey, useGetPlaylistTracks } from '~/composeables/api/track-controller/track-controller'
|
||||||
|
import { useCurrentPlaylistStore } from '~/stores/use-current-playlist-store'
|
||||||
|
|
||||||
const searchValue = ref('')
|
const searchValue = ref('')
|
||||||
|
|
||||||
@ -56,23 +55,33 @@ const { mutate: reorderTracks } = useMutation({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
interface MappedTrack {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
author: string;
|
||||||
|
authorLabel: string;
|
||||||
|
badges: string[];
|
||||||
|
imageUrl: string | undefined;
|
||||||
|
date: string;
|
||||||
|
}
|
||||||
|
|
||||||
const trackCount = computed(() => playlistTracks.value?.data.length || 0)
|
const mappedTracks = ref<MappedTrack[]>([])
|
||||||
|
|
||||||
const mappedTracks = computed(() => {
|
watchEffect(() => {
|
||||||
if (!playlistTracks.value) return []
|
if (!playlistTracks.value) return []
|
||||||
const tracks = playlistTracks.value.data
|
const tracks = playlistTracks.value.data
|
||||||
|
|
||||||
return tracks.map((track: any, index: number) => ({
|
const result = tracks
|
||||||
id: track.trackId || track.id,
|
.map((track: TrackResponse, index: number) => ({
|
||||||
title: track.track?.title || track.title || 'Unknown Track',
|
id: track.trackId!,
|
||||||
author: track.track?.artist?.name || track.artist || 'Unknown Artist',
|
title: track.title || 'Unknown Track',
|
||||||
authorLabel: "Artist",
|
author: track.artist || 'Unknown Artist',
|
||||||
badges: track.track?.format ? [track.track.format] : ['mp3'],
|
authorLabel: "Artist",
|
||||||
imageUrl: track.track?.coverUrl || track.coverUrl || getDefaultImage(index),
|
badges: ['mp3'], // TODO: badges
|
||||||
date: formatDate(track.addedDate || track.createdAt),
|
imageUrl: getDefaultImage(index), // TODO: imageUrl
|
||||||
selected: false
|
date: formatDate(''), // TODO: createdAt
|
||||||
}))
|
}));
|
||||||
|
mappedTracks.value = result;
|
||||||
})
|
})
|
||||||
|
|
||||||
const getDefaultImage = (index: number) => {
|
const getDefaultImage = (index: number) => {
|
||||||
@ -108,34 +117,16 @@ const filteredTracks = computed(() => {
|
|||||||
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()))
|
||||||
)
|
)
|
||||||
})
|
|
||||||
|
|
||||||
const selectTrack = (trackId: number) => {
|
|
||||||
mappedTracks.value.forEach(track => {
|
|
||||||
track.selected = track.id === trackId
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const { elementRef: tracksRef } = useDroppable({
|
|
||||||
data: {
|
|
||||||
source: mappedTracks.value,
|
|
||||||
},
|
|
||||||
events: {
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function onTrackOrderChange() {
|
||||||
|
const trackIds = mappedTracks.value?.map(t => t.id);
|
||||||
|
reorderTracks({
|
||||||
|
playlistId: currentPlaylistStore.id,
|
||||||
|
trackIds,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
watch(() => currentPlaylistStore.id, (newId) => {
|
watch(() => currentPlaylistStore.id, (newId) => {
|
||||||
if (newId !== -1) {
|
if (newId !== -1) {
|
||||||
refetch()
|
refetch()
|
||||||
@ -178,14 +169,14 @@ watch(() => currentPlaylistStore.id, (newId) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else ref="tracksRef" class="space-y-2">
|
<div v-else ref="tracksRef" class="space-y-2">
|
||||||
<TransitionGroup name="list" ref="tracksRef">
|
<VueDraggableNext v-model="mappedTracks" group="tracks" @change="onTrackOrderChange"
|
||||||
<Draggable v-for="(track, index) in filteredTracks" :key="track.id" :index="index"
|
item-key="id">
|
||||||
:source="mappedTracks">
|
<div v-for="track in mappedTracks" :key="track.id">
|
||||||
<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" />
|
||||||
</Draggable>
|
</div>
|
||||||
</TransitionGroup>
|
</VueDraggableNext>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,5 +0,0 @@
|
|||||||
import VueDnDKitPlugin from '@vue-dnd-kit/core';
|
|
||||||
|
|
||||||
export default defineNuxtPlugin((nuxtApp) => {
|
|
||||||
nuxtApp.vueApp.use(VueDnDKitPlugin);
|
|
||||||
});
|
|
||||||
8
bun.lock
8
bun.lock
@ -12,7 +12,6 @@
|
|||||||
"@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",
|
||||||
"@vue-dnd-kit/core": "^1.7.0",
|
|
||||||
"@vueuse/core": "^14.1.0",
|
"@vueuse/core": "^14.1.0",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
@ -29,6 +28,7 @@
|
|||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"vee-validate": "^4.15.1",
|
"vee-validate": "^4.15.1",
|
||||||
"vue": "^3.5.22",
|
"vue": "^3.5.22",
|
||||||
|
"vue-draggable-next": "^2.3.0",
|
||||||
"vue-router": "^4.6.3",
|
"vue-router": "^4.6.3",
|
||||||
"vue-sonner": "^2.0.9",
|
"vue-sonner": "^2.0.9",
|
||||||
"zod": "^4.1.12",
|
"zod": "^4.1.12",
|
||||||
@ -616,8 +616,6 @@
|
|||||||
|
|
||||||
"@volar/source-map": ["@volar/source-map@2.4.23", "", {}, "sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q=="],
|
"@volar/source-map": ["@volar/source-map@2.4.23", "", {}, "sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q=="],
|
||||||
|
|
||||||
"@vue-dnd-kit/core": ["@vue-dnd-kit/core@1.7.0", "", { "peerDependencies": { "@vueuse/core": "^13.1.0", "vue": "^3.5.13" } }, "sha512-6Otpo/9Fp/cX5EiUoN6XmIPtm/mOreAXi8clr9HgzVvDHD0sfkgVOFxCUiVbH46yAzr6KBU1Q1tjSWLBqua+5g=="],
|
|
||||||
|
|
||||||
"@vue-macros/common": ["@vue-macros/common@3.1.1", "", { "dependencies": { "@vue/compiler-sfc": "^3.5.22", "ast-kit": "^2.1.2", "local-pkg": "^1.1.2", "magic-string-ast": "^1.0.2", "unplugin-utils": "^0.3.0" }, "peerDependencies": { "vue": "^2.7.0 || ^3.2.25" }, "optionalPeers": ["vue"] }, "sha512-afW2DMjgCBVs33mWRlz7YsGHzoEEupnl0DK5ZTKsgziAlLh5syc5m+GM7eqeYrgiQpwMaVxa1fk73caCvPxyAw=="],
|
"@vue-macros/common": ["@vue-macros/common@3.1.1", "", { "dependencies": { "@vue/compiler-sfc": "^3.5.22", "ast-kit": "^2.1.2", "local-pkg": "^1.1.2", "magic-string-ast": "^1.0.2", "unplugin-utils": "^0.3.0" }, "peerDependencies": { "vue": "^2.7.0 || ^3.2.25" }, "optionalPeers": ["vue"] }, "sha512-afW2DMjgCBVs33mWRlz7YsGHzoEEupnl0DK5ZTKsgziAlLh5syc5m+GM7eqeYrgiQpwMaVxa1fk73caCvPxyAw=="],
|
||||||
|
|
||||||
"@vue/babel-helper-vue-transform-on": ["@vue/babel-helper-vue-transform-on@1.5.0", "", {}, "sha512-0dAYkerNhhHutHZ34JtTl2czVQHUNWv6xEbkdF5W+Yrv5pCWsqjeORdOgbtW2I9gWlt+wBmVn+ttqN9ZxR5tzA=="],
|
"@vue/babel-helper-vue-transform-on": ["@vue/babel-helper-vue-transform-on@1.5.0", "", {}, "sha512-0dAYkerNhhHutHZ34JtTl2czVQHUNWv6xEbkdF5W+Yrv5pCWsqjeORdOgbtW2I9gWlt+wBmVn+ttqN9ZxR5tzA=="],
|
||||||
@ -1766,6 +1764,8 @@
|
|||||||
|
|
||||||
"smob": ["smob@1.5.0", "", {}, "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig=="],
|
"smob": ["smob@1.5.0", "", {}, "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig=="],
|
||||||
|
|
||||||
|
"sortablejs": ["sortablejs@1.15.6", "", {}, "sha512-aNfiuwMEpfBM/CN6LY0ibyhxPfPbyFeBTYJKCvzkJ2GkUpazIt3H+QIPAMHwqQ7tMKaHz1Qj+rJJCqljnf4p3A=="],
|
||||||
|
|
||||||
"source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
|
"source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
|
||||||
|
|
||||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
@ -1972,6 +1972,8 @@
|
|||||||
|
|
||||||
"vue-devtools-stub": ["vue-devtools-stub@0.1.0", "", {}, "sha512-RutnB7X8c5hjq39NceArgXg28WZtZpGc3+J16ljMiYnFhKvd8hITxSWQSQ5bvldxMDU6gG5mkxl1MTQLXckVSQ=="],
|
"vue-devtools-stub": ["vue-devtools-stub@0.1.0", "", {}, "sha512-RutnB7X8c5hjq39NceArgXg28WZtZpGc3+J16ljMiYnFhKvd8hITxSWQSQ5bvldxMDU6gG5mkxl1MTQLXckVSQ=="],
|
||||||
|
|
||||||
|
"vue-draggable-next": ["vue-draggable-next@2.3.0", "", { "peerDependencies": { "sortablejs": "^1.14.0", "vue": "^3.5.17" } }, "sha512-ymbY0UIwfSdg0iDN/iyNNwUrTqZ/6KbPryzsvTNXBLuDCuOBdNijSK8yynNtmiSj6RapTPQfjLGQdJrZkzBd2w=="],
|
||||||
|
|
||||||
"vue-router": ["vue-router@4.6.3", "", { "dependencies": { "@vue/devtools-api": "^6.6.4" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-ARBedLm9YlbvQomnmq91Os7ck6efydTSpRP3nuOKCvgJOHNrhRoJDSKtee8kcL1Vf7nz6U+PMBL+hTvR3bTVQg=="],
|
"vue-router": ["vue-router@4.6.3", "", { "dependencies": { "@vue/devtools-api": "^6.6.4" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-ARBedLm9YlbvQomnmq91Os7ck6efydTSpRP3nuOKCvgJOHNrhRoJDSKtee8kcL1Vf7nz6U+PMBL+hTvR3bTVQg=="],
|
||||||
|
|
||||||
"vue-sonner": ["vue-sonner@2.0.9", "", { "peerDependencies": { "@nuxt/kit": "^4.0.3", "@nuxt/schema": "^4.0.3", "nuxt": "^4.0.3" }, "optionalPeers": ["@nuxt/kit", "@nuxt/schema", "nuxt"] }, "sha512-i6BokNlNDL93fpzNxN/LZSn6D6MzlO+i3qXt6iVZne3x1k7R46d5HlFB4P8tYydhgqOrRbIZEsnRd3kG7qGXyw=="],
|
"vue-sonner": ["vue-sonner@2.0.9", "", { "peerDependencies": { "@nuxt/kit": "^4.0.3", "@nuxt/schema": "^4.0.3", "nuxt": "^4.0.3" }, "optionalPeers": ["@nuxt/kit", "@nuxt/schema", "nuxt"] }, "sha512-i6BokNlNDL93fpzNxN/LZSn6D6MzlO+i3qXt6iVZne3x1k7R46d5HlFB4P8tYydhgqOrRbIZEsnRd3kG7qGXyw=="],
|
||||||
|
|||||||
@ -20,6 +20,12 @@ export default defineNuxtConfig({
|
|||||||
apiBaseUrl: process.env.API_BASE_URL
|
apiBaseUrl: process.env.API_BASE_URL
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
path: '~/components',
|
||||||
|
extensions: ['.vue'],
|
||||||
|
},
|
||||||
|
],
|
||||||
pinia: {
|
pinia: {
|
||||||
storesDirs: ['./app/stores/**'],
|
storesDirs: ['./app/stores/**'],
|
||||||
},
|
},
|
||||||
|
|||||||
86
package.json
86
package.json
@ -1,45 +1,45 @@
|
|||||||
{
|
{
|
||||||
"name": "hello-world",
|
"name": "hello-world",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nuxt build",
|
"build": "nuxt build",
|
||||||
"dev": "nuxt dev",
|
"dev": "nuxt dev",
|
||||||
"generate": "nuxt generate",
|
"generate": "nuxt generate",
|
||||||
"preview": "nuxt preview",
|
"preview": "nuxt preview",
|
||||||
"postinstall": "nuxt prepare"
|
"postinstall": "nuxt prepare"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@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",
|
"@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",
|
||||||
"@vue-dnd-kit/core": "^1.7.0",
|
"@vueuse/core": "^14.1.0",
|
||||||
"@vueuse/core": "^14.1.0",
|
"axios": "^1.13.2",
|
||||||
"axios": "^1.13.2",
|
"class-variance-authority": "^0.7.1",
|
||||||
"class-variance-authority": "^0.7.1",
|
"clsx": "^2.1.1",
|
||||||
"clsx": "^2.1.1",
|
"dotenv": "^17.2.3",
|
||||||
"dotenv": "^17.2.3",
|
"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",
|
||||||
"pinia": "^3.0.4",
|
"reka-ui": "^2.7.0",
|
||||||
"reka-ui": "^2.7.0",
|
"shadcn-nuxt": "2.3.2",
|
||||||
"shadcn-nuxt": "2.3.2",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwindcss": "^4.1.16",
|
||||||
"tailwindcss": "^4.1.16",
|
"tw-animate-css": "^1.4.0",
|
||||||
"tw-animate-css": "^1.4.0",
|
"vee-validate": "^4.15.1",
|
||||||
"vee-validate": "^4.15.1",
|
"vue": "^3.5.22",
|
||||||
"vue": "^3.5.22",
|
"vue-draggable-next": "^2.3.0",
|
||||||
"vue-router": "^4.6.3",
|
"vue-router": "^4.6.3",
|
||||||
"vue-sonner": "^2.0.9",
|
"vue-sonner": "^2.0.9",
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"orval": "^7.16.0",
|
"orval": "^7.16.0",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user