Add orval and backend integration

This commit is contained in:
2025-11-11 19:53:09 +05:00
parent f56a235e86
commit 3186fa16e5
17 changed files with 1174 additions and 86 deletions

View File

@ -0,0 +1,75 @@
<script setup lang="ts">
import { ChevronsUpDown, Music4 } from 'lucide-vue-next';
import Frame from '@/components/ui/frame/Frame.vue';
import Select from '@/components/ui/select/Select.vue';
import SelectCustomTrigger from '@/components/ui/select/SelectCustomTrigger.vue';
import { useSidebar } from '@/components/ui/sidebar';
import SelectContent from '@/components/ui/select/SelectContent.vue';
import SelectLabel from '@/components/ui/select/SelectLabel.vue';
import SelectSeparator from '@/components/ui/select/SelectSeparator.vue';
import SelectGroup from '@/components/ui/select/SelectGroup.vue';
import SelectItem from '@/components/ui/select/SelectItem.vue';
import { usePlaylists } from '@/composeables/api/playlist-controller/playlist-controller';
const {
open: sidebarOpen,
} = useSidebar()
const { isLoading, isError, error, data } = usePlaylists();
</script>
<template>
<Select>
<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">
<Frame borderRadius="round" background="primary" padding="dense" margin="none">
<Music4 class="text-primary-foreground" :size="24" />
</Frame>
<div class="overflow-hidden text-start">
<h4 class="text-xl font-semibold tracking-tight truncate">
My playlist
</h4>
<p class="text-sm text-muted-foreground">
11 track(s)
</p>
</div>
<div class="ml-auto">
<ChevronsUpDown />
</div>
</SelectCustomTrigger>
</div>
<div v-if="!sidebarOpen">
<div class="flex items-center">
<SelectCustomTrigger as-child>
<Frame borderRadius="round" background="primary" padding="dense" margin="none">
<Music4 class="text-primary-foreground" :size="24" />
</Frame>
</SelectCustomTrigger>
</div>
</div>
<SelectContent class="w-full" v-if="isLoading">
<UiSpinner />
</SelectContent>
<SelectContent class="w-full" v-else-if="isError">
<SelectLabel>{{ error?.message }}</SelectLabel>
</SelectContent>
<SelectContent class="w-full" v-else-if="data">
<SelectLabel>Playlists</SelectLabel>
<SelectSeparator />
<SelectGroup>
<SelectItem value="test">
<span>Test</span>
</SelectItem>
<SelectItem value="second">
<span>Second</span>
</SelectItem>
<SelectItem value="third">
<span>Third</span>
</SelectItem>
<SelectItem value="fourth">
<span>Fourth</span>
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</template>

View File

@ -11,17 +11,8 @@ import {
SidebarHeader, SidebarHeader,
SidebarRail, SidebarRail,
} from "@/components/ui/sidebar" } from "@/components/ui/sidebar"
import { useSidebar } from "@/components/ui/sidebar";
import EditAudio from "@/components/icon/EditAudio.vue"; import EditAudio from "@/components/icon/EditAudio.vue";
import Frame from "@/components/ui/frame/Frame.vue"; import PlaylistSelect from "@/components/internal/PlaylistSelect.vue";
import Select from "@/components/ui/select/Select.vue";
import SelectTrigger from "@/components/ui/select/SelectTrigger.vue";
import SelectContent from "@/components/ui/select/SelectContent.vue";
import SelectLabel from "@/components/ui/select/SelectLabel.vue";
import SelectSeparator from "@/components/ui/select/SelectSeparator.vue";
import SelectGroup from "@/components/ui/select/SelectGroup.vue";
import SelectItem from "@/components/ui/select/SelectItem.vue";
import SelectCustomTrigger from "@/components/ui/select/SelectCustomTrigger.vue";
const items = [ const items = [
{ {
@ -50,62 +41,13 @@ const items = [
}, },
]; ];
const {
open,
} = useSidebar()
const route = useRoute() const route = useRoute()
</script> </script>
<template> <template>
<Sidebar collapsible="icon"> <Sidebar collapsible="icon">
<SidebarHeader> <SidebarHeader>
<Select> <PlaylistSelect />
<div v-if="open" class="hover:bg-sidebar-muted cursor-pointer rounded-md select-none">
<SelectCustomTrigger class="w-full flex p-2 gap-2 items-center">
<Frame borderRadius="round" background="primary" padding="dense" margin="none">
<Music4 class="text-primary-foreground" :size="24" />
</Frame>
<div class="overflow-hidden text-start">
<h4 class="text-xl font-semibold tracking-tight truncate">
My playlist
</h4>
<p class="text-sm text-muted-foreground">
11 track(s)
</p>
</div>
<div class="ml-auto">
<ChevronsUpDown />
</div>
</SelectCustomTrigger>
</div>
<div v-if="!open">
<div class="flex items-center">
<SelectCustomTrigger as-child>
<Frame borderRadius="round" background="primary" padding="dense" margin="none">
<Music4 class="text-primary-foreground" :size="24" />
</Frame>
</SelectCustomTrigger>
</div>
</div>
<SelectContent class="w-full">
<SelectLabel>Playlists</SelectLabel>
<SelectSeparator />
<SelectGroup>
<SelectItem value="test">
<span>Test</span>
</SelectItem>
<SelectItem value="second">
<span>Second</span>
</SelectItem>
<SelectItem value="third">
<span>Third</span>
</SelectItem>
<SelectItem value="fourth">
<span>Fourth</span>
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</SidebarHeader> </SidebarHeader>
<SidebarContent> <SidebarContent>
<SidebarGroup> <SidebarGroup>

View File

@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { Loader2Icon } from "lucide-vue-next"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<Loader2Icon
role="status"
aria-label="Loading"
:class="cn('size-4 animate-spin', props.class)"
/>
</template>

View File

@ -0,0 +1 @@
export { default as Spinner } from "./Spinner.vue"

View File

@ -0,0 +1,22 @@
import Axios, { type AxiosRequestConfig, type AxiosResponse } from 'axios';
export const AXIOS_INSTANCE = Axios.create();
export const axiosInstance = <T>(
config: AxiosRequestConfig,
options?: AxiosRequestConfig,
): Promise<AxiosResponse<T, any>> => {
const baseURL = useRuntimeConfig().public.apiBaseUrl;
console.log(baseURL)
const source = Axios.CancelToken.source();
const promise = AXIOS_INSTANCE({
...config,
...{
...options,
baseURL: baseURL
},
cancelToken: source.token,
});
return promise;
};

View File

@ -0,0 +1,11 @@
/**
* Generated by orval v7.16.0 🍺
* Do not edit manually.
* OpenAPI definition
* OpenAPI spec version: v0
*/
export * from './playlistCreateDTO';
export * from './playlistReadDTO';
export * from './readParams';
export * from './uploadBody';

View File

@ -0,0 +1,10 @@
/**
* Generated by orval v7.16.0 🍺
* Do not edit manually.
* OpenAPI definition
* OpenAPI spec version: v0
*/
export interface PlaylistCreateDTO {
title?: string;
}

View File

@ -0,0 +1,14 @@
/**
* Generated by orval v7.16.0 🍺
* Do not edit manually.
* OpenAPI definition
* OpenAPI spec version: v0
*/
export interface PlaylistReadDTO {
id?: number;
ownerId?: number;
title?: string;
createdAt?: string;
updatedAt?: string;
}

View File

@ -0,0 +1,10 @@
/**
* Generated by orval v7.16.0 🍺
* Do not edit manually.
* OpenAPI definition
* OpenAPI spec version: v0
*/
export type ReadParams = {
document: string;
};

View File

@ -0,0 +1,10 @@
/**
* Generated by orval v7.16.0 🍺
* Do not edit manually.
* OpenAPI definition
* OpenAPI spec version: v0
*/
export type UploadBody = {
document: Blob;
};

View File

@ -0,0 +1,161 @@
/**
* Generated by orval v7.16.0 🍺
* Do not edit manually.
* OpenAPI definition
* OpenAPI spec version: v0
*/
import {
useMutation,
useQuery
} from '@tanstack/vue-query';
import type {
DataTag,
MutationFunction,
QueryClient,
QueryFunction,
QueryKey,
UseMutationOptions,
UseMutationReturnType,
UseQueryOptions,
UseQueryReturnType
} from '@tanstack/vue-query';
import {
unref
} from 'vue';
import type {
MaybeRef
} from 'vue';
import type {
PlaylistCreateDTO,
PlaylistReadDTO
} from '.././models';
import { axiosInstance } from '.././axios-instance';
type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
export const createPlaylist = (
playlistCreateDTO: MaybeRef<PlaylistCreateDTO>,
options?: SecondParameter<typeof axiosInstance>,signal?: AbortSignal
) => {
playlistCreateDTO = unref(playlistCreateDTO);
return axiosInstance<PlaylistReadDTO>(
{url: `/playlist`, method: 'POST',
headers: {'Content-Type': 'application/json', },
data: playlistCreateDTO, signal
},
options);
}
export const getCreatePlaylistMutationOptions = <TError = unknown,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof createPlaylist>>, TError,{data: PlaylistCreateDTO}, TContext>, request?: SecondParameter<typeof axiosInstance>}
): UseMutationOptions<Awaited<ReturnType<typeof createPlaylist>>, TError,{data: PlaylistCreateDTO}, TContext> => {
const mutationKey = ['createPlaylist'];
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 createPlaylist>>, {data: PlaylistCreateDTO}> = (props) => {
const {data} = props ?? {};
return createPlaylist(data,requestOptions)
}
return { mutationFn, ...mutationOptions }}
export type CreatePlaylistMutationResult = NonNullable<Awaited<ReturnType<typeof createPlaylist>>>
export type CreatePlaylistMutationBody = PlaylistCreateDTO
export type CreatePlaylistMutationError = unknown
export const useCreatePlaylist = <TError = unknown,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof createPlaylist>>, TError,{data: PlaylistCreateDTO}, TContext>, request?: SecondParameter<typeof axiosInstance>}
, queryClient?: QueryClient): UseMutationReturnType<
Awaited<ReturnType<typeof createPlaylist>>,
TError,
{data: PlaylistCreateDTO},
TContext
> => {
const mutationOptions = getCreatePlaylistMutationOptions(options);
return useMutation(mutationOptions, queryClient);
}
export const playlists = (
options?: SecondParameter<typeof axiosInstance>,signal?: AbortSignal
) => {
return axiosInstance<PlaylistReadDTO[]>(
{url: `/playlists`, method: 'GET', signal
},
options);
}
export const getPlaylistsQueryKey = () => {
return [
'playlists'
] as const;
}
export const getPlaylistsQueryOptions = <TData = Awaited<ReturnType<typeof playlists>>, TError = unknown>( options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof playlists>>, TError, TData>>, request?: SecondParameter<typeof axiosInstance>}
) => {
const {query: queryOptions, request: requestOptions} = options ?? {};
const queryKey = getPlaylistsQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof playlists>>> = ({ signal }) => playlists(requestOptions, signal);
return { queryKey, queryFn, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof playlists>>, TError, TData>
}
export type PlaylistsQueryResult = NonNullable<Awaited<ReturnType<typeof playlists>>>
export type PlaylistsQueryError = unknown
export function usePlaylists<TData = Awaited<ReturnType<typeof playlists>>, TError = unknown>(
options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof playlists>>, TError, TData>>, request?: SecondParameter<typeof axiosInstance>}
, queryClient?: QueryClient
): UseQueryReturnType<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {
const queryOptions = getPlaylistsQueryOptions(options)
const query = useQuery(queryOptions, queryClient) as UseQueryReturnType<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
query.queryKey = unref(queryOptions).queryKey as DataTag<QueryKey, TData, TError>;
return query;
}

View File

@ -0,0 +1,164 @@
/**
* Generated by orval v7.16.0 🍺
* Do not edit manually.
* OpenAPI definition
* OpenAPI spec version: v0
*/
import {
useMutation,
useQuery
} from '@tanstack/vue-query';
import type {
DataTag,
MutationFunction,
QueryClient,
QueryFunction,
QueryKey,
UseMutationOptions,
UseMutationReturnType,
UseQueryOptions,
UseQueryReturnType
} from '@tanstack/vue-query';
import {
unref
} from 'vue';
import type {
MaybeRef
} from 'vue';
import type {
ReadParams,
UploadBody
} from '.././models';
import { axiosInstance } from '.././axios-instance';
type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
export const upload = (
uploadBody: MaybeRef<UploadBody>,
options?: SecondParameter<typeof axiosInstance>,signal?: AbortSignal
) => {
uploadBody = unref(uploadBody);
const formData = new FormData();
formData.append(`document`, uploadBody.document)
return axiosInstance<string>(
{url: `/upload`, method: 'POST',
headers: {'Content-Type': 'multipart/form-data', },
data: formData, signal
},
options);
}
export const getUploadMutationOptions = <TError = unknown,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof upload>>, TError,{data: UploadBody}, TContext>, request?: SecondParameter<typeof axiosInstance>}
): UseMutationOptions<Awaited<ReturnType<typeof upload>>, TError,{data: UploadBody}, TContext> => {
const mutationKey = ['upload'];
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 upload>>, {data: UploadBody}> = (props) => {
const {data} = props ?? {};
return upload(data,requestOptions)
}
return { mutationFn, ...mutationOptions }}
export type UploadMutationResult = NonNullable<Awaited<ReturnType<typeof upload>>>
export type UploadMutationBody = UploadBody
export type UploadMutationError = unknown
export const useUpload = <TError = unknown,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof upload>>, TError,{data: UploadBody}, TContext>, request?: SecondParameter<typeof axiosInstance>}
, queryClient?: QueryClient): UseMutationReturnType<
Awaited<ReturnType<typeof upload>>,
TError,
{data: UploadBody},
TContext
> => {
const mutationOptions = getUploadMutationOptions(options);
return useMutation(mutationOptions, queryClient);
}
export const read = (
params: MaybeRef<ReadParams>,
options?: SecondParameter<typeof axiosInstance>,signal?: AbortSignal
) => {
params = unref(params);
return axiosInstance<string>(
{url: `/read`, method: 'GET',
params: unref(params), signal
},
options);
}
export const getReadQueryKey = (params?: MaybeRef<ReadParams>,) => {
return [
'read', ...(params ? [params]: [])
] as const;
}
export const getReadQueryOptions = <TData = Awaited<ReturnType<typeof read>>, TError = unknown>(params: MaybeRef<ReadParams>, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof read>>, TError, TData>>, request?: SecondParameter<typeof axiosInstance>}
) => {
const {query: queryOptions, request: requestOptions} = options ?? {};
const queryKey = getReadQueryKey(params);
const queryFn: QueryFunction<Awaited<ReturnType<typeof read>>> = ({ signal }) => read(params, requestOptions, signal);
return { queryKey, queryFn, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof read>>, TError, TData>
}
export type ReadQueryResult = NonNullable<Awaited<ReturnType<typeof read>>>
export type ReadQueryError = unknown
export function useRead<TData = Awaited<ReturnType<typeof read>>, TError = unknown>(
params: MaybeRef<ReadParams>, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof read>>, TError, TData>>, request?: SecondParameter<typeof axiosInstance>}
, queryClient?: QueryClient
): UseQueryReturnType<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {
const queryOptions = getReadQueryOptions(params,options)
const query = useQuery(queryOptions, queryClient) as UseQueryReturnType<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
query.queryKey = unref(queryOptions).queryKey as DataTag<QueryKey, TData, TError>;
return query;
}

19
app/plugins/vue-query.ts Normal file
View File

@ -0,0 +1,19 @@
import { VueQueryPlugin, QueryClient } from '@tanstack/vue-query'
import { defineNuxtPlugin } from 'nuxt/app'
export const DEFAULT_QUERIES_OPTIONS = {
staleTime: 1000 * 60 * 5,
retry: 1,
refetchOnWindowFocus: false,
refetchOnMount: false,
}
export default defineNuxtPlugin((nuxtApp) => {
const queryClient = new QueryClient({
defaultOptions: {
queries: DEFAULT_QUERIES_OPTIONS,
},
})
nuxtApp.vueApp.use(VueQueryPlugin, { queryClient })
})

641
bun.lock

File diff suppressed because it is too large Load Diff

View File

@ -14,5 +14,10 @@ export default defineNuxtConfig({
plugins: [ plugins: [
tailwindcss() tailwindcss()
] ]
},
runtimeConfig: {
public: {
apiBaseUrl: process.env.API_BASE_URL
}
} }
}) })

34
orval.config.js Normal file
View File

@ -0,0 +1,34 @@
import { defineConfig } from 'orval';
import * as dotenv from 'dotenv';
dotenv.config();
export default defineConfig({
'mp3-composer': {
input: {
target: 'http://localhost:8080/v3/api-docs',
parserOptions: {
resolve: {
http: {
headers: {
Authorization:
'Basic ' + Buffer.from(`${process.env.API_USERNAME}:${process.env.API_PASSWORD}`).toString('base64'),
},
},
},
},
},
output: {
target: './app/composeables/api/composer.ts',
schemas: './app/composeables/api/models',
mode: 'tags-split',
client: 'vue-query',
override: {
mutator: {
path: './app/composeables/api/axios-instance.ts',
name: 'axiosInstance',
},
},
}
},
});

View File

@ -14,9 +14,12 @@
"@nuxt/icon": "2.1.0", "@nuxt/icon": "2.1.0",
"@nuxt/image": "1.11.0", "@nuxt/image": "1.11.0",
"@tailwindcss/vite": "^4.1.16", "@tailwindcss/vite": "^4.1.16",
"@tanstack/vue-query": "^5.90.7",
"@vueuse/core": "^14.0.0", "@vueuse/core": "^14.0.0",
"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",
"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",
@ -29,6 +32,7 @@
"vue-router": "^4.6.3" "vue-router": "^4.6.3"
}, },
"devDependencies": { "devDependencies": {
"orval": "^7.16.0",
"typescript": "^5.9.3" "typescript": "^5.9.3"
} }
} }