Integrate api with hey-api

This commit is contained in:
2026-03-06 16:38:04 +05:00
parent 3de38c650b
commit 5a69c6f39f
22 changed files with 2217 additions and 174 deletions

View File

@@ -2,7 +2,7 @@
import tinycolor from "tinycolor2";
import type { VNodeRef } from "vue";
import Badge from "../badge/Badge.vue";
import { Dot } from "lucide-vue-next";
import { Dot, Smile } from "lucide-vue-next";
const imageElement = shallowRef<VNodeRef | null>(null);
@@ -17,23 +17,21 @@ watch(palette, () => {
});
interface Props {
id: number;
id: string;
image_url: string;
image_alt?: string;
title: string;
type: string;
subtitle?: string;
description?: string;
tooltipEnabled?: boolean;
tooltipMetadata: {
studio: string;
type: string;
episodes: number;
rating: number;
tags?: string[];
rating?: number;
episodeCount?: number;
type?: string;
studio?: string;
genres?: string[];
},
tooltipCollisionBoundary?: Element;
@@ -64,8 +62,14 @@ const props = defineProps<Props>();
<TooltipContent :sideOffset="16" :collisionBoundary="props.tooltipCollisionBoundary"
:avoidCollisions="true" side="right" tooltipColor="muted" tooltipArrowColor="muted">
<div class="flex flex-col gap-1 m-4">
<div class="flex">
<div class="flex gap-4">
<p class="text-xl font-bold truncate max-w-[24ch]">{{ props.title }}</p>
<div class="flex items-center gap-2">
<Smile />
<p class="text-xl font-bold">
{{ props.tooltipMetadata.rating }}
</p>
</div>
</div>
<p class="text-lg font-semibold tracking-wider" :style="{ color: paletteVibrantHsl }">{{
props.tooltipMetadata?.studio }}
@@ -73,10 +77,10 @@ const props = defineProps<Props>();
<div class="flex items-center text-base font-semibold">
<p>{{ props.tooltipMetadata?.type }}</p>
<Dot />
<p>{{ props.tooltipMetadata?.rating }} episodes</p>
<p>{{ props.tooltipMetadata?.episodeCount }} эпизод(ов)</p>
</div>
<div class="flex gap-2">
<Badge v-for="tag in props.tooltipMetadata?.tags">
<div class="flex flex-wrap gap-2 max-w-64">
<Badge v-for="tag in props.tooltipMetadata?.genres">
{{ tag }}
</Badge>
</div>

View File

@@ -0,0 +1,16 @@
// This file is auto-generated by @hey-api/openapi-ts
import { type ClientOptions, type Config, createClient, createConfig } from './client';
import type { ClientOptions as ClientOptions2 } from './types.gen';
/**
* The `createClientConfig()` function will be called on client initialization
* and the returned object will become the client's initial configuration.
*
* You may want to initialize your client this way instead of calling
* `setConfig()`. This is useful for example if you're using Next.js
* to ensure your client always has the correct values.
*/
export type CreateClientConfig<T extends ClientOptions = ClientOptions2> = (override?: Config<ClientOptions & T>) => Config<Required<ClientOptions> & T>;
export const client = createClient(createConfig<ClientOptions2>({ baseURL: 'http://localhost:8080' }));

View File

@@ -0,0 +1,218 @@
// This file is auto-generated by @hey-api/openapi-ts
import { useAsyncData, useFetch, useLazyAsyncData, useLazyFetch } from 'nuxt/app';
import { reactive, ref, toValue, watch } from 'vue';
import { createSseClient } from '../core/serverSentEvents.gen';
import type { HttpMethod } from '../core/types.gen';
import { getValidRequestBody } from '../core/utils.gen';
import type { Client, Config, RequestOptions } from './types.gen';
import {
buildUrl,
createConfig,
executeFetchFn,
mergeConfigs,
mergeHeaders,
mergeInterceptors,
serializeBody,
setAuthParams,
unwrapRefs,
} from './utils.gen';
export const createClient = (config: Config = {}): Client => {
let _config = mergeConfigs(createConfig(), config);
const getConfig = (): Config => ({ ..._config });
const setConfig = (config: Config): Config => {
_config = mergeConfigs(_config, config);
return getConfig();
};
const beforeRequest = async (options: RequestOptions) => {
const opts = {
..._config,
...options,
$fetch: options.$fetch ?? _config.$fetch ?? $fetch,
headers: mergeHeaders(_config.headers, options.headers),
onRequest: mergeInterceptors(_config.onRequest, options.onRequest),
onResponse: mergeInterceptors(_config.onResponse, options.onResponse),
};
if (opts.security) {
await setAuthParams({
...opts,
security: opts.security,
});
}
if (opts.requestValidator) {
await opts.requestValidator(opts);
}
const url = buildUrl(opts);
return { opts, url };
};
const request: Client['request'] = ({ asyncDataOptions, composable = '$fetch', ...options }) => {
const key = options.key;
const opts = {
..._config,
...options,
$fetch: options.$fetch ?? _config.$fetch ?? $fetch,
headers: mergeHeaders(_config.headers, options.headers),
onRequest: mergeInterceptors(_config.onRequest, options.onRequest),
onResponse: mergeInterceptors(_config.onResponse, options.onResponse),
};
const { requestValidator, responseTransformer, responseValidator, security } = opts;
if (requestValidator || security) {
// auth must happen in interceptors otherwise we'd need to require
// asyncContext enabled
// https://nuxt.com/docs/guide/going-further/experimental-features#asynccontext
opts.onRequest = [
async ({ options }) => {
if (security) {
await setAuthParams({
auth: opts.auth,
headers: options.headers,
query: options.query,
security,
});
}
if (requestValidator) {
await requestValidator({
...options,
// @ts-expect-error
body: options.rawBody,
});
}
},
...opts.onRequest,
];
}
if (responseTransformer || responseValidator) {
opts.onResponse = [
...opts.onResponse,
async ({ options, response }) => {
if (options.responseType && options.responseType !== 'json') {
return;
}
if (!response.ok) {
return;
}
if (responseValidator) {
await responseValidator(response._data);
}
if (responseTransformer) {
response._data = await responseTransformer(response._data);
}
},
];
}
// remove Content-Type header if body is empty to avoid sending invalid requests
if (opts.body === undefined || opts.body === '') {
opts.headers.delete('Content-Type');
}
const fetchFn = opts.$fetch;
if (composable === '$fetch') {
return executeFetchFn(
// @ts-expect-error
opts,
fetchFn,
);
}
if (composable === 'useFetch' || composable === 'useLazyFetch') {
opts.rawBody = opts.body;
const bodyParams = reactive({
body: opts.body,
bodySerializer: opts.bodySerializer,
});
const body = ref(serializeBody({ ...opts, body: toValue(opts.body) }));
opts.body = body;
watch(bodyParams, (changed) => {
body.value = serializeBody(changed);
});
return composable === 'useLazyFetch'
? useLazyFetch(() => buildUrl(opts), { ...opts, ...asyncDataOptions })
: useFetch(() => buildUrl(opts), { ...opts, ...asyncDataOptions });
}
const handler: any = () =>
executeFetchFn(
// @ts-expect-error
opts,
fetchFn,
);
if (composable === 'useAsyncData') {
return key
? useAsyncData(key, handler, asyncDataOptions)
: useAsyncData(handler, asyncDataOptions);
}
if (composable === 'useLazyAsyncData') {
return key
? useLazyAsyncData(key, handler, asyncDataOptions)
: useLazyAsyncData(handler, asyncDataOptions);
}
return undefined as any;
};
const makeMethodFn = (method: Uppercase<HttpMethod>) => (options: RequestOptions) =>
request({ ...options, method });
const makeSseFn = (method: Uppercase<HttpMethod>) => async (options: RequestOptions) => {
const { opts, url } = await beforeRequest(options);
return createSseClient({
...unwrapRefs(opts),
body: opts.body as BodyInit | null | undefined,
method,
onRequest: undefined,
serializedBody: getValidRequestBody(opts) as BodyInit | null | undefined,
signal: unwrapRefs(opts.signal) as AbortSignal,
url,
});
};
const _buildUrl: Client['buildUrl'] = (options) =>
buildUrl({ ..._config, ...options } as typeof options);
return {
buildUrl: _buildUrl,
connect: makeMethodFn('CONNECT'),
delete: makeMethodFn('DELETE'),
get: makeMethodFn('GET'),
getConfig,
head: makeMethodFn('HEAD'),
options: makeMethodFn('OPTIONS'),
patch: makeMethodFn('PATCH'),
post: makeMethodFn('POST'),
put: makeMethodFn('PUT'),
request,
setConfig,
sse: {
connect: makeSseFn('CONNECT'),
delete: makeSseFn('DELETE'),
get: makeSseFn('GET'),
head: makeSseFn('HEAD'),
options: makeSseFn('OPTIONS'),
patch: makeSseFn('PATCH'),
post: makeSseFn('POST'),
put: makeSseFn('PUT'),
trace: makeSseFn('TRACE'),
},
trace: makeMethodFn('TRACE'),
} as Client;
};

View File

@@ -0,0 +1,24 @@
// This file is auto-generated by @hey-api/openapi-ts
export type { Auth } from '../core/auth.gen';
export type { QuerySerializerOptions } from '../core/bodySerializer.gen';
export {
formDataBodySerializer,
jsonBodySerializer,
urlSearchParamsBodySerializer,
} from '../core/bodySerializer.gen';
export { buildClientParams } from '../core/params.gen';
export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen';
export { createClient } from './client.gen';
export type {
Client,
ClientOptions,
Composable,
Config,
CreateClientConfig,
Options,
RequestOptions,
RequestResult,
TDataShape,
} from './types.gen';
export { createConfig } from './utils.gen';

View File

@@ -0,0 +1,191 @@
// This file is auto-generated by @hey-api/openapi-ts
import type {
AsyncDataOptions,
useAsyncData,
useFetch,
UseFetchOptions,
useLazyAsyncData,
useLazyFetch,
} from 'nuxt/app';
import type { Ref } from 'vue';
import type { Auth } from '../core/auth.gen';
import type { QuerySerializerOptions } from '../core/bodySerializer.gen';
import type {
ServerSentEventsOptions,
ServerSentEventsResult,
} from '../core/serverSentEvents.gen';
import type { Client as CoreClient, Config as CoreConfig } from '../core/types.gen';
export type ArraySeparatorStyle = ArrayStyle | MatrixStyle;
type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited';
type MatrixStyle = 'label' | 'matrix' | 'simple';
export type ObjectSeparatorStyle = ObjectStyle | MatrixStyle;
type ObjectStyle = 'form' | 'deepObject';
export type QuerySerializer = (query: Parameters<Client['buildUrl']>[0]['query']) => string;
type WithRefs<TData> = {
[K in keyof TData]: NonNullable<TData[K]> extends object
? WithRefs<NonNullable<TData[K]>> | Ref<NonNullable<TData[K]>> | Extract<TData[K], null>
: NonNullable<TData[K]> | Ref<NonNullable<TData[K]>> | Extract<TData[K], null>;
};
// copied from Nuxt
export type KeysOf<T> = Array<T extends T ? (keyof T extends string ? keyof T : never) : never>;
export interface Config<T extends ClientOptions = ClientOptions>
extends
Omit<FetchOptions<unknown>, 'baseURL' | 'body' | 'headers' | 'method' | 'query'>,
WithRefs<Pick<FetchOptions<unknown>, 'query'>>,
Omit<CoreConfig, 'querySerializer'> {
/**
* Base URL for all requests made by this client.
*/
baseURL?: T['baseURL'];
/**
* A function for serializing request query parameters. By default, arrays
* will be exploded in form style, objects will be exploded in deepObject
* style, and reserved characters are percent-encoded.
*
* {@link https://swagger.io/docs/specification/serialization/#query View examples}
*/
querySerializer?: QuerySerializer | QuerySerializerOptions;
}
export interface RequestOptions<
TComposable extends Composable = '$fetch',
ResT = unknown,
DefaultT = undefined,
Url extends string = string,
>
extends
Config,
WithRefs<{
path?: FetchOptions<unknown>['query'];
query?: FetchOptions<unknown>['query'];
}>,
Pick<
ServerSentEventsOptions<ResT>,
| 'onSseError'
| 'onSseEvent'
| 'sseDefaultRetryDelay'
| 'sseMaxRetryAttempts'
| 'sseMaxRetryDelay'
> {
asyncDataOptions?: AsyncDataOptions<ResT, ResT, KeysOf<ResT>, DefaultT>;
/**
* Any body that you want to add to your request.
*
* {@link https://developer.mozilla.org/docs/Web/API/fetch#body}
*/
body?: NonNullable<unknown> | Ref<NonNullable<unknown>> | null;
composable?: TComposable;
key?: string;
rawBody?: NonNullable<unknown> | Ref<NonNullable<unknown>> | null;
/**
* Security mechanism(s) to use for the request.
*/
security?: ReadonlyArray<Auth>;
url: Url;
}
export type RequestResult<
TComposable extends Composable,
ResT,
TError,
> = TComposable extends '$fetch'
? ReturnType<typeof $fetch<ResT>>
: TComposable extends 'useAsyncData'
? ReturnType<typeof useAsyncData<ResT | null, TError>>
: TComposable extends 'useFetch'
? ReturnType<typeof useFetch<ResT | null, TError>>
: TComposable extends 'useLazyAsyncData'
? ReturnType<typeof useLazyAsyncData<ResT | null, TError>>
: TComposable extends 'useLazyFetch'
? ReturnType<typeof useLazyFetch<ResT | null, TError>>
: never;
export interface ClientOptions {
baseURL?: string;
}
type MethodFn = <
TComposable extends Composable = '$fetch',
ResT = unknown,
TError = unknown,
DefaultT = undefined,
>(
options: Omit<RequestOptions<TComposable, ResT, DefaultT>, 'method'>,
) => RequestResult<TComposable, ResT, TError>;
type SseFn = <
TComposable extends Composable = '$fetch',
ResT = unknown,
TError = unknown,
DefaultT = undefined,
>(
options: Omit<RequestOptions<TComposable, ResT, DefaultT>, 'method'>,
) => Promise<ServerSentEventsResult<RequestResult<TComposable, ResT, TError>>>;
type RequestFn = <
TComposable extends Composable = '$fetch',
ResT = unknown,
TError = unknown,
DefaultT = undefined,
>(
options: Omit<RequestOptions<TComposable, ResT, DefaultT>, 'method'> &
Pick<Required<RequestOptions<TComposable, ResT, DefaultT>>, 'method'>,
) => RequestResult<TComposable, ResT, TError>;
/**
* The `createClientConfig()` function will be called on client initialization
* and the returned object will become the client's initial configuration.
*
* You may want to initialize your client this way instead of calling
* `setConfig()`. This is useful for example if you're using Next.js
* to ensure your client always has the correct values.
*/
export type CreateClientConfig<T extends ClientOptions = ClientOptions> = (
override?: Config<ClientOptions & T>,
) => Config<Required<ClientOptions> & T>;
export interface TDataShape {
body?: unknown;
headers?: unknown;
path?: FetchOptions<unknown>['query'];
query?: FetchOptions<unknown>['query'];
url: string;
}
export type BuildUrlOptions<
TData extends Omit<TDataShape, 'headers'> = Omit<TDataShape, 'headers'>,
> = Pick<WithRefs<TData>, 'path' | 'query'> &
Pick<TData, 'url'> &
Pick<Options<'$fetch', TData>, 'baseURL' | 'querySerializer'>;
type BuildUrlFn = <TData extends Omit<TDataShape, 'headers'>>(
options: BuildUrlOptions<TData>,
) => string;
export type Client = CoreClient<RequestFn, Config, MethodFn, BuildUrlFn, SseFn>;
type OmitKeys<T, K> = Pick<T, Exclude<keyof T, K>>;
export type Options<
TComposable extends Composable = '$fetch',
TData extends TDataShape = TDataShape,
ResT = unknown,
DefaultT = undefined,
> = OmitKeys<RequestOptions<TComposable, ResT, DefaultT>, 'body' | 'path' | 'query' | 'url'> &
([TData] extends [never] ? unknown : WithRefs<Omit<TData, 'url'>>);
type FetchOptions<TData> = Omit<UseFetchOptions<TData, TData>, keyof AsyncDataOptions<TData>>;
export type Composable =
| '$fetch'
| 'useAsyncData'
| 'useFetch'
| 'useLazyAsyncData'
| 'useLazyFetch';

View File

@@ -0,0 +1,390 @@
// This file is auto-generated by @hey-api/openapi-ts
import type { ComputedRef, Ref } from 'vue';
import { isRef, toValue, unref } from 'vue';
import { getAuthToken } from '../core/auth.gen';
import type { QuerySerializerOptions } from '../core/bodySerializer.gen';
import { jsonBodySerializer } from '../core/bodySerializer.gen';
import {
serializeArrayParam,
serializeObjectParam,
serializePrimitiveParam,
} from '../core/pathSerializer.gen';
import type {
ArraySeparatorStyle,
BuildUrlOptions,
Client,
ClientOptions,
Config,
QuerySerializer,
RequestOptions,
} from './types.gen';
type PathSerializer = Pick<Required<BuildUrlOptions>, 'path' | 'url'>;
const PATH_PARAM_RE = /\{[^{}]+\}/g;
type MaybeArray<T> = T | T[];
const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => {
let url = _url;
const matches = _url.match(PATH_PARAM_RE);
if (matches) {
for (const match of matches) {
let explode = false;
let name = match.substring(1, match.length - 1);
let style: ArraySeparatorStyle = 'simple';
if (name.endsWith('*')) {
explode = true;
name = name.substring(0, name.length - 1);
}
if (name.startsWith('.')) {
name = name.substring(1);
style = 'label';
} else if (name.startsWith(';')) {
name = name.substring(1);
style = 'matrix';
}
const value = toValue((toValue(path) as Record<string, unknown> | undefined)?.[name]);
if (value === undefined || value === null) {
continue;
}
if (Array.isArray(value)) {
url = url.replace(match, serializeArrayParam({ explode, name, style, value }));
continue;
}
if (typeof value === 'object') {
url = url.replace(
match,
serializeObjectParam({
explode,
name,
style,
value: value as Record<string, unknown>,
valueOnly: true,
}),
);
continue;
}
if (style === 'matrix') {
url = url.replace(
match,
`;${serializePrimitiveParam({
name,
value: value as string,
})}`,
);
continue;
}
const replaceValue = encodeURIComponent(
style === 'label' ? `.${value as string}` : (value as string),
);
url = url.replace(match, replaceValue);
}
}
return url;
};
export const createQuerySerializer = <T = unknown>({
parameters = {},
...args
}: QuerySerializerOptions = {}) => {
const querySerializer = (queryParams: T) => {
const search: string[] = [];
const qParams = toValue(queryParams);
if (qParams && typeof qParams === 'object') {
for (const name in qParams) {
const value = toValue(qParams[name]);
if (value === undefined || value === null) {
continue;
}
const options = parameters[name] || args;
if (Array.isArray(value)) {
const serializedArray = serializeArrayParam({
allowReserved: options.allowReserved,
explode: true,
name,
style: 'form',
value,
...options.array,
});
if (serializedArray) search.push(serializedArray);
} else if (typeof value === 'object') {
const serializedObject = serializeObjectParam({
allowReserved: options.allowReserved,
explode: true,
name,
style: 'deepObject',
value: value as Record<string, unknown>,
...options.object,
});
if (serializedObject) search.push(serializedObject);
} else {
const serializedPrimitive = serializePrimitiveParam({
allowReserved: options.allowReserved,
name,
value: value as string,
});
if (serializedPrimitive) search.push(serializedPrimitive);
}
}
}
return search.join('&');
};
return querySerializer;
};
const checkForExistence = (
options: Pick<RequestOptions, 'auth' | 'query'> & {
headers: Headers;
},
name?: string,
): boolean => {
if (!name) {
return false;
}
if (
options.headers.has(name) ||
(toValue(options.query) as Record<string, unknown> | undefined)?.[name] ||
options.headers.get('Cookie')?.includes(`${name}=`)
) {
return true;
}
return false;
};
export const setAuthParams = async ({
security,
...options
}: Pick<Required<RequestOptions>, 'security'> &
Pick<RequestOptions, 'auth' | 'query'> & {
headers: Headers;
}) => {
for (const auth of security) {
if (checkForExistence(options, auth.name)) {
continue;
}
const token = await getAuthToken(auth, options.auth);
if (!token) {
continue;
}
const name = auth.name ?? 'Authorization';
switch (auth.in) {
case 'query': {
if (!options.query) {
options.query = {};
}
const queryValue = toValue(options.query) as Record<string, unknown> | undefined;
if (queryValue) {
queryValue[name] = token;
}
break;
}
case 'cookie':
options.headers.append('Cookie', `${name}=${token}`);
break;
case 'header':
default:
options.headers.set(name, token);
break;
}
}
};
export const buildUrl: Client['buildUrl'] = (options) => {
const url = getUrl({
baseUrl: options.baseURL as string,
path: options.path,
query: options.query,
querySerializer:
typeof options.querySerializer === 'function'
? options.querySerializer
: createQuerySerializer(options.querySerializer),
url: options.url,
});
return url;
};
export const getUrl = ({
baseUrl,
path,
query,
querySerializer,
url: _url,
}: Pick<BuildUrlOptions, 'path' | 'query' | 'url'> & {
baseUrl?: string;
querySerializer: QuerySerializer;
}) => {
const pathUrl = _url.startsWith('/') ? _url : `/${_url}`;
let url = (baseUrl ?? '') + pathUrl;
if (path) {
url = defaultPathSerializer({ path, url });
}
let search = query ? querySerializer(query) : '';
if (search.startsWith('?')) {
search = search.substring(1);
}
if (search) {
url += `?${search}`;
}
return url;
};
export const mergeConfigs = (a: Config, b: Config): Config => {
const config = { ...a, ...b };
if (config.baseURL?.endsWith('/')) {
config.baseURL = config.baseURL.substring(0, config.baseURL.length - 1);
}
config.headers = mergeHeaders(a.headers, b.headers);
return config;
};
const headersEntries = (headers: Headers): Array<[string, string]> => {
const entries: Array<[string, string]> = [];
headers.forEach((value, key) => {
entries.push([key, value]);
});
return entries;
};
export const mergeHeaders = (
...headers: Array<Required<Config>['headers'] | undefined>
): Headers => {
const mergedHeaders = new Headers();
for (const header of headers) {
if (!header || typeof header !== 'object') {
continue;
}
let h: unknown = header;
if (isRef(h)) {
h = unref(h);
}
const iterator =
h instanceof Headers ? headersEntries(h) : Object.entries(h as Record<string, unknown>);
for (const [key, value] of iterator) {
if (value === null) {
mergedHeaders.delete(key);
} else if (Array.isArray(value)) {
for (const v of value) {
mergedHeaders.append(key, unwrapRefs(v) as string);
}
} else if (value !== undefined) {
const v = unwrapRefs(value);
// assume object headers are meant to be JSON stringified, i.e. their
// content value in OpenAPI specification is 'application/json'
mergedHeaders.set(key, typeof v === 'object' ? JSON.stringify(v) : (v as string));
}
}
}
return mergedHeaders;
};
export const mergeInterceptors = <T>(...args: Array<MaybeArray<T>>): Array<T> =>
args.reduce<Array<T>>((acc, item) => {
if (typeof item === 'function') {
acc.push(item);
} else if (Array.isArray(item)) {
return acc.concat(item);
}
return acc;
}, []);
const defaultQuerySerializer = createQuerySerializer({
allowReserved: false,
array: {
explode: true,
style: 'form',
},
object: {
explode: true,
style: 'deepObject',
},
});
const defaultHeaders = {
'Content-Type': 'application/json',
};
export const createConfig = <T extends ClientOptions = ClientOptions>(
override: Config<Omit<ClientOptions, keyof T> & T> = {},
): Config<Omit<ClientOptions, keyof T> & T> => ({
...jsonBodySerializer,
headers: defaultHeaders,
querySerializer: defaultQuerySerializer,
...override,
});
type UnwrapRefs<T> =
T extends Ref<infer V>
? V
: T extends ComputedRef<infer V>
? V
: T extends Record<string, unknown> // this doesn't handle functions well
? { [K in keyof T]: UnwrapRefs<T[K]> }
: T;
export const unwrapRefs = <T>(value: T): UnwrapRefs<T> => {
if (value === null || typeof value !== 'object' || value instanceof Headers) {
return (isRef(value) ? unref(value) : value) as UnwrapRefs<T>;
}
if (value instanceof Blob) {
return value as UnwrapRefs<T>;
}
if (Array.isArray(value)) {
return value.map((item) => unwrapRefs(item)) as UnwrapRefs<T>;
}
if (isRef(value)) {
return unwrapRefs(unref(value) as T);
}
// unwrap into new object to avoid modifying the source
const result: Record<string, unknown> = {};
for (const key in value) {
result[key] = unwrapRefs(value[key] as T);
}
return result as UnwrapRefs<T>;
};
export const serializeBody = (
opts: Pick<Parameters<Client['request']>[0], 'body' | 'bodySerializer'>,
) => {
if (opts.body && opts.bodySerializer) {
return opts.bodySerializer(opts.body);
}
return opts.body;
};
export const executeFetchFn = (
opts: Omit<Parameters<Client['request']>[0], 'composable'>,
fetchFn: Required<Config>['$fetch'],
) => {
const unwrappedOpts = unwrapRefs(opts);
unwrappedOpts.rawBody = unwrappedOpts.body;
unwrappedOpts.body = serializeBody(unwrappedOpts);
return fetchFn(
buildUrl(opts),
// @ts-expect-error
unwrappedOpts,
);
};

View File

@@ -0,0 +1,41 @@
// This file is auto-generated by @hey-api/openapi-ts
export type AuthToken = string | undefined;
export interface Auth {
/**
* Which part of the request do we use to send the auth?
*
* @default 'header'
*/
in?: 'header' | 'query' | 'cookie';
/**
* Header or query parameter name.
*
* @default 'Authorization'
*/
name?: string;
scheme?: 'basic' | 'bearer';
type: 'apiKey' | 'http';
}
export const getAuthToken = async (
auth: Auth,
callback: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken,
): Promise<string | undefined> => {
const token = typeof callback === 'function' ? await callback(auth) : callback;
if (!token) {
return;
}
if (auth.scheme === 'bearer') {
return `Bearer ${token}`;
}
if (auth.scheme === 'basic') {
return `Basic ${btoa(token)}`;
}
return token;
};

View File

@@ -0,0 +1,82 @@
// This file is auto-generated by @hey-api/openapi-ts
import type { ArrayStyle, ObjectStyle, SerializerOptions } from './pathSerializer.gen';
export type QuerySerializer = (query: Record<string, unknown>) => string;
export type BodySerializer = (body: unknown) => unknown;
type QuerySerializerOptionsObject = {
allowReserved?: boolean;
array?: Partial<SerializerOptions<ArrayStyle>>;
object?: Partial<SerializerOptions<ObjectStyle>>;
};
export type QuerySerializerOptions = QuerySerializerOptionsObject & {
/**
* Per-parameter serialization overrides. When provided, these settings
* override the global array/object settings for specific parameter names.
*/
parameters?: Record<string, QuerySerializerOptionsObject>;
};
const serializeFormDataPair = (data: FormData, key: string, value: unknown): void => {
if (typeof value === 'string' || value instanceof Blob) {
data.append(key, value);
} else if (value instanceof Date) {
data.append(key, value.toISOString());
} else {
data.append(key, JSON.stringify(value));
}
};
const serializeUrlSearchParamsPair = (data: URLSearchParams, key: string, value: unknown): void => {
if (typeof value === 'string') {
data.append(key, value);
} else {
data.append(key, JSON.stringify(value));
}
};
export const formDataBodySerializer = {
bodySerializer: (body: unknown): FormData => {
const data = new FormData();
Object.entries(body as Record<string, unknown>).forEach(([key, value]) => {
if (value === undefined || value === null) {
return;
}
if (Array.isArray(value)) {
value.forEach((v) => serializeFormDataPair(data, key, v));
} else {
serializeFormDataPair(data, key, value);
}
});
return data;
},
};
export const jsonBodySerializer = {
bodySerializer: (body: unknown): string =>
JSON.stringify(body, (_key, value) => (typeof value === 'bigint' ? value.toString() : value)),
};
export const urlSearchParamsBodySerializer = {
bodySerializer: (body: unknown): string => {
const data = new URLSearchParams();
Object.entries(body as Record<string, unknown>).forEach(([key, value]) => {
if (value === undefined || value === null) {
return;
}
if (Array.isArray(value)) {
value.forEach((v) => serializeUrlSearchParamsPair(data, key, v));
} else {
serializeUrlSearchParamsPair(data, key, value);
}
});
return data.toString();
},
};

View File

@@ -0,0 +1,169 @@
// This file is auto-generated by @hey-api/openapi-ts
type Slot = 'body' | 'headers' | 'path' | 'query';
export type Field =
| {
in: Exclude<Slot, 'body'>;
/**
* Field name. This is the name we want the user to see and use.
*/
key: string;
/**
* Field mapped name. This is the name we want to use in the request.
* If omitted, we use the same value as `key`.
*/
map?: string;
}
| {
in: Extract<Slot, 'body'>;
/**
* Key isn't required for bodies.
*/
key?: string;
map?: string;
}
| {
/**
* Field name. This is the name we want the user to see and use.
*/
key: string;
/**
* Field mapped name. This is the name we want to use in the request.
* If `in` is omitted, `map` aliases `key` to the transport layer.
*/
map: Slot;
};
export interface Fields {
allowExtra?: Partial<Record<Slot, boolean>>;
args?: ReadonlyArray<Field>;
}
export type FieldsConfig = ReadonlyArray<Field | Fields>;
const extraPrefixesMap: Record<string, Slot> = {
$body_: 'body',
$headers_: 'headers',
$path_: 'path',
$query_: 'query',
};
const extraPrefixes = Object.entries(extraPrefixesMap);
type KeyMap = Map<
string,
| {
in: Slot;
map?: string;
}
| {
in?: never;
map: Slot;
}
>;
const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => {
if (!map) {
map = new Map();
}
for (const config of fields) {
if ('in' in config) {
if (config.key) {
map.set(config.key, {
in: config.in,
map: config.map,
});
}
} else if ('key' in config) {
map.set(config.key, {
map: config.map,
});
} else if (config.args) {
buildKeyMap(config.args, map);
}
}
return map;
};
interface Params {
body: unknown;
headers: Record<string, unknown>;
path: Record<string, unknown>;
query: Record<string, unknown>;
}
const stripEmptySlots = (params: Params) => {
for (const [slot, value] of Object.entries(params)) {
if (value && typeof value === 'object' && !Array.isArray(value) && !Object.keys(value).length) {
delete params[slot as Slot];
}
}
};
export const buildClientParams = (args: ReadonlyArray<unknown>, fields: FieldsConfig) => {
const params: Params = {
body: {},
headers: {},
path: {},
query: {},
};
const map = buildKeyMap(fields);
let config: FieldsConfig[number] | undefined;
for (const [index, arg] of args.entries()) {
if (fields[index]) {
config = fields[index];
}
if (!config) {
continue;
}
if ('in' in config) {
if (config.key) {
const field = map.get(config.key)!;
const name = field.map || config.key;
if (field.in) {
(params[field.in] as Record<string, unknown>)[name] = arg;
}
} else {
params.body = arg;
}
} else {
for (const [key, value] of Object.entries(arg ?? {})) {
const field = map.get(key);
if (field) {
if (field.in) {
const name = field.map || key;
(params[field.in] as Record<string, unknown>)[name] = value;
} else {
params[field.map] = value;
}
} else {
const extra = extraPrefixes.find(([prefix]) => key.startsWith(prefix));
if (extra) {
const [prefix, slot] = extra;
(params[slot] as Record<string, unknown>)[key.slice(prefix.length)] = value;
} else if ('allowExtra' in config && config.allowExtra) {
for (const [slot, allowed] of Object.entries(config.allowExtra)) {
if (allowed) {
(params[slot as Slot] as Record<string, unknown>)[key] = value;
break;
}
}
}
}
}
}
}
stripEmptySlots(params);
return params;
};

View File

@@ -0,0 +1,171 @@
// This file is auto-generated by @hey-api/openapi-ts
interface SerializeOptions<T> extends SerializePrimitiveOptions, SerializerOptions<T> {}
interface SerializePrimitiveOptions {
allowReserved?: boolean;
name: string;
}
export interface SerializerOptions<T> {
/**
* @default true
*/
explode: boolean;
style: T;
}
export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited';
export type ArraySeparatorStyle = ArrayStyle | MatrixStyle;
type MatrixStyle = 'label' | 'matrix' | 'simple';
export type ObjectStyle = 'form' | 'deepObject';
type ObjectSeparatorStyle = ObjectStyle | MatrixStyle;
interface SerializePrimitiveParam extends SerializePrimitiveOptions {
value: string;
}
export const separatorArrayExplode = (style: ArraySeparatorStyle) => {
switch (style) {
case 'label':
return '.';
case 'matrix':
return ';';
case 'simple':
return ',';
default:
return '&';
}
};
export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => {
switch (style) {
case 'form':
return ',';
case 'pipeDelimited':
return '|';
case 'spaceDelimited':
return '%20';
default:
return ',';
}
};
export const separatorObjectExplode = (style: ObjectSeparatorStyle) => {
switch (style) {
case 'label':
return '.';
case 'matrix':
return ';';
case 'simple':
return ',';
default:
return '&';
}
};
export const serializeArrayParam = ({
allowReserved,
explode,
name,
style,
value,
}: SerializeOptions<ArraySeparatorStyle> & {
value: unknown[];
}) => {
if (!explode) {
const joinedValues = (
allowReserved ? value : value.map((v) => encodeURIComponent(v as string))
).join(separatorArrayNoExplode(style));
switch (style) {
case 'label':
return `.${joinedValues}`;
case 'matrix':
return `;${name}=${joinedValues}`;
case 'simple':
return joinedValues;
default:
return `${name}=${joinedValues}`;
}
}
const separator = separatorArrayExplode(style);
const joinedValues = value
.map((v) => {
if (style === 'label' || style === 'simple') {
return allowReserved ? v : encodeURIComponent(v as string);
}
return serializePrimitiveParam({
allowReserved,
name,
value: v as string,
});
})
.join(separator);
return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues;
};
export const serializePrimitiveParam = ({
allowReserved,
name,
value,
}: SerializePrimitiveParam) => {
if (value === undefined || value === null) {
return '';
}
if (typeof value === 'object') {
throw new Error(
'Deeply-nested arrays/objects arent supported. Provide your own `querySerializer()` to handle these.',
);
}
return `${name}=${allowReserved ? value : encodeURIComponent(value)}`;
};
export const serializeObjectParam = ({
allowReserved,
explode,
name,
style,
value,
valueOnly,
}: SerializeOptions<ObjectSeparatorStyle> & {
value: Record<string, unknown> | Date;
valueOnly?: boolean;
}) => {
if (value instanceof Date) {
return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`;
}
if (style !== 'deepObject' && !explode) {
let values: string[] = [];
Object.entries(value).forEach(([key, v]) => {
values = [...values, key, allowReserved ? (v as string) : encodeURIComponent(v as string)];
});
const joinedValues = values.join(',');
switch (style) {
case 'form':
return `${name}=${joinedValues}`;
case 'label':
return `.${joinedValues}`;
case 'matrix':
return `;${name}=${joinedValues}`;
default:
return joinedValues;
}
}
const separator = separatorObjectExplode(style);
const joinedValues = Object.entries(value)
.map(([key, v]) =>
serializePrimitiveParam({
allowReserved,
name: style === 'deepObject' ? `${name}[${key}]` : key,
value: v as string,
}),
)
.join(separator);
return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues;
};

View File

@@ -0,0 +1,117 @@
// This file is auto-generated by @hey-api/openapi-ts
/**
* JSON-friendly union that mirrors what Pinia Colada can hash.
*/
export type JsonValue =
| null
| string
| number
| boolean
| JsonValue[]
| { [key: string]: JsonValue };
/**
* Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes.
*/
export const queryKeyJsonReplacer = (_key: string, value: unknown) => {
if (value === undefined || typeof value === 'function' || typeof value === 'symbol') {
return undefined;
}
if (typeof value === 'bigint') {
return value.toString();
}
if (value instanceof Date) {
return value.toISOString();
}
return value;
};
/**
* Safely stringifies a value and parses it back into a JsonValue.
*/
export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => {
try {
const json = JSON.stringify(input, queryKeyJsonReplacer);
if (json === undefined) {
return undefined;
}
return JSON.parse(json) as JsonValue;
} catch {
return undefined;
}
};
/**
* Detects plain objects (including objects with a null prototype).
*/
const isPlainObject = (value: unknown): value is Record<string, unknown> => {
if (value === null || typeof value !== 'object') {
return false;
}
const prototype = Object.getPrototypeOf(value as object);
return prototype === Object.prototype || prototype === null;
};
/**
* Turns URLSearchParams into a sorted JSON object for deterministic keys.
*/
const serializeSearchParams = (params: URLSearchParams): JsonValue => {
const entries = Array.from(params.entries()).sort(([a], [b]) => a.localeCompare(b));
const result: Record<string, JsonValue> = {};
for (const [key, value] of entries) {
const existing = result[key];
if (existing === undefined) {
result[key] = value;
continue;
}
if (Array.isArray(existing)) {
(existing as string[]).push(value);
} else {
result[key] = [existing, value];
}
}
return result;
};
/**
* Normalizes any accepted value into a JSON-friendly shape for query keys.
*/
export const serializeQueryKeyValue = (value: unknown): JsonValue | undefined => {
if (value === null) {
return null;
}
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
return value;
}
if (value === undefined || typeof value === 'function' || typeof value === 'symbol') {
return undefined;
}
if (typeof value === 'bigint') {
return value.toString();
}
if (value instanceof Date) {
return value.toISOString();
}
if (Array.isArray(value)) {
return stringifyToJsonValue(value);
}
if (typeof URLSearchParams !== 'undefined' && value instanceof URLSearchParams) {
return serializeSearchParams(value);
}
if (isPlainObject(value)) {
return stringifyToJsonValue(value);
}
return undefined;
};

View File

@@ -0,0 +1,243 @@
// This file is auto-generated by @hey-api/openapi-ts
import type { Config } from './types.gen';
export type ServerSentEventsOptions<TData = unknown> = Omit<RequestInit, 'method'> &
Pick<Config, 'method' | 'responseTransformer' | 'responseValidator'> & {
/**
* Fetch API implementation. You can use this option to provide a custom
* fetch instance.
*
* @default globalThis.fetch
*/
fetch?: typeof fetch;
/**
* Implementing clients can call request interceptors inside this hook.
*/
onRequest?: (url: string, init: RequestInit) => Promise<Request>;
/**
* Callback invoked when a network or parsing error occurs during streaming.
*
* This option applies only if the endpoint returns a stream of events.
*
* @param error The error that occurred.
*/
onSseError?: (error: unknown) => void;
/**
* Callback invoked when an event is streamed from the server.
*
* This option applies only if the endpoint returns a stream of events.
*
* @param event Event streamed from the server.
* @returns Nothing (void).
*/
onSseEvent?: (event: StreamEvent<TData>) => void;
serializedBody?: RequestInit['body'];
/**
* Default retry delay in milliseconds.
*
* This option applies only if the endpoint returns a stream of events.
*
* @default 3000
*/
sseDefaultRetryDelay?: number;
/**
* Maximum number of retry attempts before giving up.
*/
sseMaxRetryAttempts?: number;
/**
* Maximum retry delay in milliseconds.
*
* Applies only when exponential backoff is used.
*
* This option applies only if the endpoint returns a stream of events.
*
* @default 30000
*/
sseMaxRetryDelay?: number;
/**
* Optional sleep function for retry backoff.
*
* Defaults to using `setTimeout`.
*/
sseSleepFn?: (ms: number) => Promise<void>;
url: string;
};
export interface StreamEvent<TData = unknown> {
data: TData;
event?: string;
id?: string;
retry?: number;
}
export type ServerSentEventsResult<TData = unknown, TReturn = void, TNext = unknown> = {
stream: AsyncGenerator<
TData extends Record<string, unknown> ? TData[keyof TData] : TData,
TReturn,
TNext
>;
};
export const createSseClient = <TData = unknown>({
onRequest,
onSseError,
onSseEvent,
responseTransformer,
responseValidator,
sseDefaultRetryDelay,
sseMaxRetryAttempts,
sseMaxRetryDelay,
sseSleepFn,
url,
...options
}: ServerSentEventsOptions): ServerSentEventsResult<TData> => {
let lastEventId: string | undefined;
const sleep = sseSleepFn ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms)));
const createStream = async function* () {
let retryDelay: number = sseDefaultRetryDelay ?? 3000;
let attempt = 0;
const signal = options.signal ?? new AbortController().signal;
while (true) {
if (signal.aborted) break;
attempt++;
const headers =
options.headers instanceof Headers
? options.headers
: new Headers(options.headers as Record<string, string> | undefined);
if (lastEventId !== undefined) {
headers.set('Last-Event-ID', lastEventId);
}
try {
const requestInit: RequestInit = {
redirect: 'follow',
...options,
body: options.serializedBody,
headers,
signal,
};
let request = new Request(url, requestInit);
if (onRequest) {
request = await onRequest(url, requestInit);
}
// fetch must be assigned here, otherwise it would throw the error:
// TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation
const _fetch = options.fetch ?? globalThis.fetch;
const response = await _fetch(request);
if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`);
if (!response.body) throw new Error('No body in SSE response');
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
let buffer = '';
const abortHandler = () => {
try {
reader.cancel();
} catch {
// noop
}
};
signal.addEventListener('abort', abortHandler);
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += value;
// Normalize line endings: CRLF -> LF, then CR -> LF
buffer = buffer.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
const chunks = buffer.split('\n\n');
buffer = chunks.pop() ?? '';
for (const chunk of chunks) {
const lines = chunk.split('\n');
const dataLines: Array<string> = [];
let eventName: string | undefined;
for (const line of lines) {
if (line.startsWith('data:')) {
dataLines.push(line.replace(/^data:\s*/, ''));
} else if (line.startsWith('event:')) {
eventName = line.replace(/^event:\s*/, '');
} else if (line.startsWith('id:')) {
lastEventId = line.replace(/^id:\s*/, '');
} else if (line.startsWith('retry:')) {
const parsed = Number.parseInt(line.replace(/^retry:\s*/, ''), 10);
if (!Number.isNaN(parsed)) {
retryDelay = parsed;
}
}
}
let data: unknown;
let parsedJson = false;
if (dataLines.length) {
const rawData = dataLines.join('\n');
try {
data = JSON.parse(rawData);
parsedJson = true;
} catch {
data = rawData;
}
}
if (parsedJson) {
if (responseValidator) {
await responseValidator(data);
}
if (responseTransformer) {
data = await responseTransformer(data);
}
}
onSseEvent?.({
data,
event: eventName,
id: lastEventId,
retry: retryDelay,
});
if (dataLines.length) {
yield data as any;
}
}
}
} finally {
signal.removeEventListener('abort', abortHandler);
reader.releaseLock();
}
break; // exit loop on normal completion
} catch (error) {
// connection failed or aborted; retry after delay
onSseError?.(error);
if (sseMaxRetryAttempts !== undefined && attempt >= sseMaxRetryAttempts) {
break; // stop after firing error
}
// exponential backoff: double retry each attempt, cap at 30s
const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 30000);
await sleep(backoff);
}
}
};
const stream = createStream();
return { stream };
};

View File

@@ -0,0 +1,104 @@
// This file is auto-generated by @hey-api/openapi-ts
import type { Auth, AuthToken } from './auth.gen';
import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from './bodySerializer.gen';
export type HttpMethod =
| 'connect'
| 'delete'
| 'get'
| 'head'
| 'options'
| 'patch'
| 'post'
| 'put'
| 'trace';
export type Client<
RequestFn = never,
Config = unknown,
MethodFn = never,
BuildUrlFn = never,
SseFn = never,
> = {
/**
* Returns the final request URL.
*/
buildUrl: BuildUrlFn;
getConfig: () => Config;
request: RequestFn;
setConfig: (config: Config) => Config;
} & {
[K in HttpMethod]: MethodFn;
} & ([SseFn] extends [never] ? { sse?: never } : { sse: { [K in HttpMethod]: SseFn } });
export interface Config {
/**
* Auth token or a function returning auth token. The resolved value will be
* added to the request payload as defined by its `security` array.
*/
auth?: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken;
/**
* A function for serializing request body parameter. By default,
* {@link JSON.stringify()} will be used.
*/
bodySerializer?: BodySerializer | null;
/**
* An object containing any HTTP headers that you want to pre-populate your
* `Headers` object with.
*
* {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more}
*/
headers?:
| RequestInit['headers']
| Record<
string,
string | number | boolean | (string | number | boolean)[] | null | undefined | unknown
>;
/**
* The request method.
*
* {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more}
*/
method?: Uppercase<HttpMethod>;
/**
* A function for serializing request query parameters. By default, arrays
* will be exploded in form style, objects will be exploded in deepObject
* style, and reserved characters are percent-encoded.
*
* This method will have no effect if the native `paramsSerializer()` Axios
* API function is used.
*
* {@link https://swagger.io/docs/specification/serialization/#query View examples}
*/
querySerializer?: QuerySerializer | QuerySerializerOptions;
/**
* A function validating request data. This is useful if you want to ensure
* the request conforms to the desired shape, so it can be safely sent to
* the server.
*/
requestValidator?: (data: unknown) => Promise<unknown>;
/**
* A function transforming response data before it's returned. This is useful
* for post-processing data, e.g. converting ISO strings into Date objects.
*/
responseTransformer?: (data: unknown) => Promise<unknown>;
/**
* A function validating response data. This is useful if you want to ensure
* the response conforms to the desired shape, so it can be safely passed to
* the transformers and returned to the user.
*/
responseValidator?: (data: unknown) => Promise<unknown>;
}
type IsExactlyNeverOrNeverUndefined<T> = [T] extends [never]
? true
: [T] extends [never | undefined]
? [undefined] extends [T]
? false
: true
: false;
export type OmitNever<T extends Record<string, unknown>> = {
[K in keyof T as IsExactlyNeverOrNeverUndefined<T[K]> extends true ? never : K]: T[K];
};

View File

@@ -0,0 +1,140 @@
// This file is auto-generated by @hey-api/openapi-ts
import type { BodySerializer, QuerySerializer } from './bodySerializer.gen';
import {
type ArraySeparatorStyle,
serializeArrayParam,
serializeObjectParam,
serializePrimitiveParam,
} from './pathSerializer.gen';
export interface PathSerializer {
path: Record<string, unknown>;
url: string;
}
export const PATH_PARAM_RE = /\{[^{}]+\}/g;
export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => {
let url = _url;
const matches = _url.match(PATH_PARAM_RE);
if (matches) {
for (const match of matches) {
let explode = false;
let name = match.substring(1, match.length - 1);
let style: ArraySeparatorStyle = 'simple';
if (name.endsWith('*')) {
explode = true;
name = name.substring(0, name.length - 1);
}
if (name.startsWith('.')) {
name = name.substring(1);
style = 'label';
} else if (name.startsWith(';')) {
name = name.substring(1);
style = 'matrix';
}
const value = path[name];
if (value === undefined || value === null) {
continue;
}
if (Array.isArray(value)) {
url = url.replace(match, serializeArrayParam({ explode, name, style, value }));
continue;
}
if (typeof value === 'object') {
url = url.replace(
match,
serializeObjectParam({
explode,
name,
style,
value: value as Record<string, unknown>,
valueOnly: true,
}),
);
continue;
}
if (style === 'matrix') {
url = url.replace(
match,
`;${serializePrimitiveParam({
name,
value: value as string,
})}`,
);
continue;
}
const replaceValue = encodeURIComponent(
style === 'label' ? `.${value as string}` : (value as string),
);
url = url.replace(match, replaceValue);
}
}
return url;
};
export const getUrl = ({
baseUrl,
path,
query,
querySerializer,
url: _url,
}: {
baseUrl?: string;
path?: Record<string, unknown>;
query?: Record<string, unknown>;
querySerializer: QuerySerializer;
url: string;
}) => {
const pathUrl = _url.startsWith('/') ? _url : `/${_url}`;
let url = (baseUrl ?? '') + pathUrl;
if (path) {
url = defaultPathSerializer({ path, url });
}
let search = query ? querySerializer(query) : '';
if (search.startsWith('?')) {
search = search.substring(1);
}
if (search) {
url += `?${search}`;
}
return url;
};
export function getValidRequestBody(options: {
body?: unknown;
bodySerializer?: BodySerializer | null;
serializedBody?: unknown;
}) {
const hasBody = options.body !== undefined;
const isSerializedBody = hasBody && options.bodySerializer;
if (isSerializedBody) {
if ('serializedBody' in options) {
const hasSerializedBody =
options.serializedBody !== undefined && options.serializedBody !== '';
return hasSerializedBody ? options.serializedBody : null;
}
// not all clients implement a serializedBody property (i.e. client-axios)
return options.body !== '' ? options.body : null;
}
// plain/text body
if (hasBody) {
return options.body;
}
// no body was provided
return undefined;
}

View File

@@ -0,0 +1,4 @@
// This file is auto-generated by @hey-api/openapi-ts
export { getSearch, type Options } from './sdk.gen';
export type { ClientOptions, GetSearchData, GetSearchResponse, GetSearchResponses, SearchEntryDto, SearchResponseDto } from './types.gen';

View File

@@ -0,0 +1,24 @@
// This file is auto-generated by @hey-api/openapi-ts
import type { Client, Composable, Options as Options2, TDataShape } from './client';
import { client } from './client.gen';
import type { GetSearchData, GetSearchResponse } from './types.gen';
export type Options<TComposable extends Composable = '$fetch', TData extends TDataShape = TDataShape, ResT = unknown, DefaultT = undefined> = Options2<TComposable, TData, ResT, DefaultT> & {
/**
* You can provide a client instance returned by `createClient()` instead of
* individual options. This might be also useful if you want to implement a
* custom client.
*/
client?: Client;
/**
* You can pass arbitrary values through the `meta` object. This can be
* used to access values that aren't defined as part of the SDK function.
*/
meta?: Record<string, unknown>;
};
/**
* Search
*/
export const getSearch = <TComposable extends Composable = '$fetch', DefaultT extends GetSearchResponse = GetSearchResponse>(options: Options<TComposable, GetSearchData, GetSearchResponse, DefaultT>) => (options.client ?? client).get<TComposable, GetSearchResponse | DefaultT, unknown, DefaultT>({ url: '/search', ...options });

View File

@@ -0,0 +1,40 @@
// This file is auto-generated by @hey-api/openapi-ts
export type ClientOptions = {
baseURL: 'http://localhost:8080' | (string & {});
};
export type SearchEntryDto = {
id?: string;
posterURLs?: Array<string>;
title?: string;
episodeCount?: number;
rating?: number;
ratingSource?: string;
type?: string;
studio?: string;
genres?: Array<string>;
durationMin?: number;
};
export type SearchResponseDto = {
result?: Array<SearchEntryDto>;
};
export type GetSearchData = {
body?: never;
path?: never;
query?: {
title?: string;
};
url: '/search';
};
export type GetSearchResponses = {
/**
* OK
*/
200: SearchResponseDto;
};
export type GetSearchResponse = GetSearchResponses[keyof GetSearchResponses];

View File

@@ -70,7 +70,7 @@ He is seen seeking out a PK (Player Killer) known as Tri-Edge, whose victims sup
}
</script>
<template>
<div class="flex flex-col lg:flex-row gap-4 lg:gap-8 xl:gap-16 bg-muted p-4 sm:p-6 md:p-8 lg:p-12 xl:max-h-3/4">
<div class="flex flex-col lg:flex-row gap-4 lg:gap-8 xl:gap-16 bg-muted p-4 sm:p-6 md:p-8 lg:p-12 xl:max-h-5/6">
<div class="flex flex-col items-center lg:items-start gap-4 lg:gap-6 lg:min-w-72 xl:min-w-96 ">
<div class="max-w-xs lg:max-w-sm xl:max-w-md">
<NuxtImg :src="data.poster" class="w-full object-cover aspect-3/4 rounded-lg shadow-lg"

View File

@@ -3,153 +3,20 @@ import HeroSection from '@/components/ui/internal/HeroSection.vue';
import { Search, SlidersHorizontal } from 'lucide-vue-next';
import MediaImageCard from '~/components/ui/internal/MediaImageCard.vue';
import SkeletonMediaImageCard from '~/components/ui/internal/SkeletonMediaImageCard.vue';
import { refDebounced } from '@vueuse/core'
import { getSearch } from '~/openapi/generated';
const tooltipCollisionBoundary = ref<Element | undefined>(undefined);
const trendingAnime = [
{
id: 1,
image_url: "example/jigokuraku.jpg",
title: "Jigokuraku 2nd Season",
studio: "MAPPA",
type: "TV Show",
episodes: 12,
rating: 78,
tags: ["action", "adventure", "dark fantasy"],
status: "Ongoing",
duration: "23 min"
const searchTitle = ref('')
const debouncedSearchTitle = refDebounced(searchTitle, 1000);
},
{
id: 2,
image_url: "example/jujutsukaisen.jpg",
title: "Jujutsu Kaisen: Shimetsu Kaiyuu - Zenpen",
studio: "MAPPA",
type: "TV Show",
episodes: 24,
rating: 85,
tags: ["action", "supernatural", "dark fantasy"],
status: "Finished",
duration: "23 min"
},
{
id: 3,
image_url: "example/okiraku.jpg",
title: "Okiraku Ryoushu no Tanoshii Ryouchi Bouei",
studio: "CloverWorks",
type: "TV Show",
episodes: 13,
rating: 72,
tags: ["comedy", "fantasy", "slice of life"],
status: "Ongoing",
duration: "24 min"
},
{
id: 4,
image_url: "example/oshi no ko.jpg",
title: "[Oshi no Ko] 3rd Season",
studio: "Doga Kobo",
type: "TV Show",
episodes: 11,
rating: 88,
tags: ["drama", "psychological", "showbiz"],
status: "Airing",
duration: "24 min"
},
{
id: 5,
image_url: "example/shibou.jpg",
title: "Shibou Yuugi de Meshi wo Kuu.",
studio: "Studio Bind",
type: "TV Show",
episodes: 12,
rating: 75,
tags: ["cooking", "fantasy", "isekai"],
status: "Finished",
duration: "23 min"
},
{
id: 6,
image_url: "example/sousounofrieren.jpg",
title: "Sousou no Frieren 2nd Season",
studio: "Madhouse",
type: "TV Show",
episodes: 28,
rating: 92,
tags: ["adventure", "drama", "fantasy"],
status: "Airing",
duration: "24 min"
},
{
id: 7,
image_url: "example/tamonkun.jpg",
title: "Tamon-kun Ima Docchi!?",
studio: "Kyoto Animation",
type: "TV Show",
episodes: 12,
rating: 68,
tags: ["comedy", "romance", "school"],
status: "Ongoing",
duration: "24 min"
},
{
id: 8,
image_url: "example/yuusha party.jpg",
title: "Yuusha Party wo Oidasareta Kiyou Binbou",
studio: "Wit Studio",
type: "TV Show",
episodes: 13,
rating: 70,
tags: ["action", "adventure", "fantasy"],
status: "Finished",
duration: "23 min"
},
{
id: 9,
image_url: "example/jujutsukaisen.jpg",
title: "Jujutsu Kaisen: Shimetsu Kaiyuu - Zenpen soetjsot giwjiagjwiag wgi wajig wig gwi",
studio: "MAPPA",
type: "TV Show",
episodes: 24,
rating: 85,
tags: ["action", "supernatural", "dark fantasy"],
status: "Finished",
duration: "23 min"
},
{
id: 10,
image_url: "example/okiraku.jpg",
title: "Okiraku Ryoushu no Tanoshii Ryouchi Bouei teisotsoeitj wgjiwag wgj wigwji",
studio: "CloverWorks",
type: "TV Show",
episodes: 13,
rating: 72,
tags: ["comedy", "fantasy", "slice of life"],
status: "Ongoing",
duration: "24 min"
},
{
id: 11,
image_url: "example/oshi no ko.jpg",
title: "[Oshi no Ko] 3rd Season iaowjegiogjoiawgio waiogjawiog wag",
studio: "Doga Kobo",
type: "TV Show",
episodes: 11,
rating: 88,
tags: ["drama", "psychological", "showbiz"],
status: "Airing",
duration: "24 min"
const { data: trendingAnimeData, status, error } = await getSearch({
composable: 'useFetch',
query: {
title: debouncedSearchTitle
}
];
})
</script>
<template>
@@ -158,7 +25,7 @@ const trendingAnime = [
<div class="flex flex-col items-center m-8 gap-8">
<div class="flex gap-4 max-w-1/4 md:min-w-3xl sm:min-w-64">
<InputGroup>
<InputGroupInput placeholder="Search" />
<InputGroupInput placeholder="Search" v-model="searchTitle" />
<InputGroupAddon>
<Search />
</InputGroupAddon>
@@ -178,10 +45,15 @@ const trendingAnime = [
</div>
<div ref="tooltipCollisionBoundary"
class="grid grid-cols-3 md:grid-cols-4 lg:grid-cols-5 2xl:grid-cols-6 gap-4 gap-y-12 justify-items-center w-full">
<MediaImageCard v-for="anime in trendingAnime" :key="anime.id" :id="anime.id"
:image_url="anime.image_url" :title="anime.title" :type="anime.type" :tooltipEnabled="true"
:tooltipMetadata="anime" :tooltipCollisionBoundary="tooltipCollisionBoundary" />
<SkeletonMediaImageCard v-for="index in 10" />
<template v-if="status === 'pending'">
<SkeletonMediaImageCard v-for="index in 10" :key="index" />
</template>
<template v-else-if="status === 'success'">
<MediaImageCard v-for="anime in trendingAnimeData?.result || []" :key="anime.title"
:id="anime.id!" :image_url="anime?.posterURLs?.[0] || 'TODO'" :title="anime.title || 'TODO'"
:tooltipEnabled="true" :tooltipMetadata="anime"
:tooltipCollisionBoundary="tooltipCollisionBoundary" />
</template>
</div>
</div>
</div>