diff --git a/app/components/Admin/ModalForm.vue b/app/components/Admin/ModalForm.vue index b8af52e..ba19536 100644 --- a/app/components/Admin/ModalForm.vue +++ b/app/components/Admin/ModalForm.vue @@ -47,6 +47,8 @@ interface ModalFormProps { fields: Field[] title?: string onSave: (value: any) => Promise + loading?: boolean + loadingText?: string } const props = defineProps>() @@ -150,7 +152,10 @@ watch( /> - + @@ -169,4 +174,19 @@ watch( flex-direction: column; gap: 0.8rem; } + +.spinner { + width: 1rem; + height: 1rem; + border: 2px solid currentColor; + border-right-color: transparent; + border-radius: 50%; + animation: spin 0.7s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} diff --git a/app/pages/admin/songs.vue b/app/pages/admin/songs.vue index f8cb620..db6ce07 100644 --- a/app/pages/admin/songs.vue +++ b/app/pages/admin/songs.vue @@ -106,6 +106,7 @@ const getOptionId = (option: string | Option) => const showModal = ref(false) const item = ref(undefined) +const addingSong = ref(false) const save = async (value: any) => { if (value._id) { @@ -127,24 +128,31 @@ const save = async (value: any) => { return false } } else { + if (!value.name || !value.file || !value.artists?.length) { + toast.show( + "Name, audio file, and at least one artist are required", + "error", + ) + return false + } + + if (value.createSingle && (!value.cover || !value.genre)) { + toast.show( + "A cover image and genre are required for a single", + "error", + ) + return false + } + + if (!value.createSingle && !value.album) { + toast.show("Choose an album or create a single album", "error") + return false + } + + addingSong.value = true + await nextTick() try { - if (!value.name || !value.file || !value.artists?.length) { - toast.show( - "Name, audio file, and at least one artist are required", - "error", - ) - return false - } - if (value.createSingle) { - if (!value.cover || !value.genre) { - toast.show( - "A cover image and genre are required for a single", - "error", - ) - return false - } - const albumResponse = await cfetch<{ _id: string }>("/admin/album", { method: "POST", data: { @@ -155,12 +163,6 @@ const save = async (value: any) => { }, }) value.album = albumResponse._id - } else if (!value.album) { - toast.show( - "Choose an album or create a single album", - "error", - ) - return false } await cfetch("/admin/track", { @@ -178,6 +180,8 @@ const save = async (value: any) => { toast.show("Failed to add song", "error") return false + } finally { + addingSong.value = false } } @@ -241,6 +245,8 @@ useHead({ title: "Admin - Songs" }) :fields="modalFields" :item="item" :title="item ? 'Edit song' : 'Add new song'" + :loading="addingSong" + :loading-text="'Adding song…'" /> diff --git a/app/ui/Button.vue b/app/ui/Button.vue index 783309f..3c57dd3 100644 --- a/app/ui/Button.vue +++ b/app/ui/Button.vue @@ -8,6 +8,7 @@ interface ButtonProps { full?: boolean iconName?: string centerText?: boolean + disabled?: boolean } const props = withDefaults(defineProps(), { @@ -22,6 +23,7 @@ const props = withDefaults(defineProps(), { props.type == 'link' && props.to != undefined ? NuxtLink : 'button' " :to="props.to" + :disabled="props.disabled" :class="[ `button type-${props.variant}`, {