feat: add import review functionality and improve error handling
This commit is contained in:
parent
112dfc7325
commit
73485fadda
21
api/api.ts
21
api/api.ts
@ -1,11 +1,17 @@
|
||||
import axios, { AxiosRequestConfig } from "axios";
|
||||
import axios, { AxiosRequestConfig, isAxiosError } from "axios";
|
||||
import { User } from "@/contexts/auth-context";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export const Api = axios.create({
|
||||
baseURL: "http://localhost:8080",
|
||||
responseType: "json",
|
||||
});
|
||||
|
||||
interface ErrorResponseDTO {
|
||||
timestamp: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
Api.interceptors.request.use(async (config) => {
|
||||
const jsonString = localStorage.getItem("userData");
|
||||
if (jsonString) {
|
||||
@ -16,6 +22,19 @@ Api.interceptors.request.use(async (config) => {
|
||||
return config;
|
||||
});
|
||||
|
||||
Api.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
if (!isAxiosError(error)) {
|
||||
console.log(error);
|
||||
return;
|
||||
}
|
||||
|
||||
const errorDTO = error.response?.data as ErrorResponseDTO;
|
||||
toast.error(errorDTO.message);
|
||||
},
|
||||
);
|
||||
|
||||
export const customInstance = <T>(
|
||||
config: AxiosRequestConfig,
|
||||
options?: AxiosRequestConfig,
|
||||
|
||||
@ -21,10 +21,22 @@ import type {
|
||||
} from "@tanstack/react-query";
|
||||
|
||||
import { customInstance } from "./api";
|
||||
export interface DefaultResponseDTOVoid {
|
||||
timestamp?: string;
|
||||
data?: unknown;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface ImportMangaDexRequestDTO {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface DefaultResponseDTOImportMangaDexResponseDTO {
|
||||
timestamp?: string;
|
||||
data?: ImportMangaDexResponseDTO;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface ImportMangaDexResponseDTO {
|
||||
id: number;
|
||||
}
|
||||
@ -56,6 +68,18 @@ export interface AuthenticationResponseDTO {
|
||||
role: AuthenticationResponseDTORole;
|
||||
}
|
||||
|
||||
export interface DefaultResponseDTOAuthenticationResponseDTO {
|
||||
timestamp?: string;
|
||||
data?: AuthenticationResponseDTO;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface DefaultResponseDTOPageMangaListDTO {
|
||||
timestamp?: string;
|
||||
data?: PageMangaListDTO;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface MangaListDTO {
|
||||
id: number;
|
||||
/** @minLength 1 */
|
||||
@ -87,9 +111,9 @@ export interface PageMangaListDTO {
|
||||
|
||||
export interface PageableObject {
|
||||
offset?: number;
|
||||
paged?: boolean;
|
||||
pageNumber?: number;
|
||||
pageSize?: number;
|
||||
paged?: boolean;
|
||||
sort?: SortObject;
|
||||
unpaged?: boolean;
|
||||
}
|
||||
@ -100,6 +124,12 @@ export interface SortObject {
|
||||
unsorted?: boolean;
|
||||
}
|
||||
|
||||
export interface DefaultResponseDTOListMangaChapterDTO {
|
||||
timestamp?: string;
|
||||
data?: MangaChapterDTO[];
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface MangaChapterDTO {
|
||||
id: number;
|
||||
/** @minLength 1 */
|
||||
@ -108,6 +138,12 @@ export interface MangaChapterDTO {
|
||||
isRead: boolean;
|
||||
}
|
||||
|
||||
export interface DefaultResponseDTOMangaDTO {
|
||||
timestamp?: string;
|
||||
data?: MangaDTO;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface MangaDTO {
|
||||
id: number;
|
||||
/** @minLength 1 */
|
||||
@ -134,6 +170,12 @@ export interface MangaProviderDTO {
|
||||
chaptersDownloaded: number;
|
||||
}
|
||||
|
||||
export interface DefaultResponseDTOMangaChapterImagesDTO {
|
||||
timestamp?: string;
|
||||
data?: MangaChapterImagesDTO;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface MangaChapterImagesDTO {
|
||||
id: number;
|
||||
/** @minLength 1 */
|
||||
@ -141,6 +183,30 @@ export interface MangaChapterImagesDTO {
|
||||
chapterImageKeys: string[];
|
||||
}
|
||||
|
||||
export interface DefaultResponseDTOListImportReviewDTO {
|
||||
timestamp?: string;
|
||||
data?: ImportReviewDTO[];
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface ImportReviewDTO {
|
||||
id: number;
|
||||
/** @minLength 1 */
|
||||
title: string;
|
||||
/** @minLength 1 */
|
||||
providerName: string;
|
||||
externalUrl?: string;
|
||||
/** @minLength 1 */
|
||||
reason: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface DefaultResponseDTOListGenreDTO {
|
||||
timestamp?: string;
|
||||
data?: GenreDTO[];
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface GenreDTO {
|
||||
id: number;
|
||||
/** @minLength 1 */
|
||||
@ -167,6 +233,11 @@ export type ImportMultipleFilesBody = {
|
||||
files: Blob[];
|
||||
};
|
||||
|
||||
export type ResolveImportReviewParams = {
|
||||
importReviewId: number;
|
||||
malId: string;
|
||||
};
|
||||
|
||||
export type GetMangasParams = {
|
||||
searchQuery?: string;
|
||||
genreIds?: number[];
|
||||
@ -200,7 +271,7 @@ export const fetchMangaChapters = (
|
||||
options?: SecondParameter<typeof customInstance>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return customInstance<void>(
|
||||
return customInstance<DefaultResponseDTOVoid>(
|
||||
{
|
||||
url: `http://rov-lenovo.badger-pirarucu.ts.net:8080/mangas/${encodeURIComponent(String(mangaProviderId))}/fetch-chapters`,
|
||||
method: "POST",
|
||||
@ -288,7 +359,7 @@ export const setUnfavorite = (
|
||||
options?: SecondParameter<typeof customInstance>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return customInstance<void>(
|
||||
return customInstance<DefaultResponseDTOVoid>(
|
||||
{
|
||||
url: `http://rov-lenovo.badger-pirarucu.ts.net:8080/mangas/${encodeURIComponent(String(id))}/unfavorite`,
|
||||
method: "POST",
|
||||
@ -376,7 +447,7 @@ export const setFavorite = (
|
||||
options?: SecondParameter<typeof customInstance>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return customInstance<void>(
|
||||
return customInstance<DefaultResponseDTOVoid>(
|
||||
{
|
||||
url: `http://rov-lenovo.badger-pirarucu.ts.net:8080/mangas/${encodeURIComponent(String(id))}/favorite`,
|
||||
method: "POST",
|
||||
@ -464,9 +535,9 @@ export const markAsRead = (
|
||||
options?: SecondParameter<typeof customInstance>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return customInstance<void>(
|
||||
return customInstance<DefaultResponseDTOVoid>(
|
||||
{
|
||||
url: `http://rov-lenovo.badger-pirarucu.ts.net:8080/mangas/${encodeURIComponent(String(chapterId))}/mark-as-read`,
|
||||
url: `http://rov-lenovo.badger-pirarucu.ts.net:8080/mangas/chapters/${encodeURIComponent(String(chapterId))}/mark-as-read`,
|
||||
method: "POST",
|
||||
signal,
|
||||
},
|
||||
@ -544,180 +615,7 @@ export const useMarkAsRead = <TError = unknown, TContext = unknown>(
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Update manga info
|
||||
*/
|
||||
export const updateMangaInfo = (
|
||||
mangaId: number,
|
||||
options?: SecondParameter<typeof customInstance>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return customInstance<void>(
|
||||
{
|
||||
url: `http://rov-lenovo.badger-pirarucu.ts.net:8080/mangas/manga/${encodeURIComponent(String(mangaId))}/info`,
|
||||
method: "POST",
|
||||
signal,
|
||||
},
|
||||
options,
|
||||
);
|
||||
};
|
||||
|
||||
export const getUpdateMangaInfoMutationOptions = <
|
||||
TError = unknown,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateMangaInfo>>,
|
||||
TError,
|
||||
{ mangaId: number },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof customInstance>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateMangaInfo>>,
|
||||
TError,
|
||||
{ mangaId: number },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ["updateMangaInfo"];
|
||||
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 updateMangaInfo>>,
|
||||
{ mangaId: number }
|
||||
> = (props) => {
|
||||
const { mangaId } = props ?? {};
|
||||
|
||||
return updateMangaInfo(mangaId, requestOptions);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type UpdateMangaInfoMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof updateMangaInfo>>
|
||||
>;
|
||||
|
||||
export type UpdateMangaInfoMutationError = unknown;
|
||||
|
||||
/**
|
||||
* @summary Update manga info
|
||||
*/
|
||||
export const useUpdateMangaInfo = <TError = unknown, TContext = unknown>(
|
||||
options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateMangaInfo>>,
|
||||
TError,
|
||||
{ mangaId: number },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof customInstance>;
|
||||
},
|
||||
queryClient?: QueryClient,
|
||||
): UseMutationResult<
|
||||
Awaited<ReturnType<typeof updateMangaInfo>>,
|
||||
TError,
|
||||
{ mangaId: number },
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getUpdateMangaInfoMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions, queryClient);
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Download all chapters for a manga provider
|
||||
*/
|
||||
export const downloadAllChapters = (
|
||||
mangaProviderId: number,
|
||||
options?: SecondParameter<typeof customInstance>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return customInstance<void>(
|
||||
{
|
||||
url: `http://rov-lenovo.badger-pirarucu.ts.net:8080/mangas/chapter/${encodeURIComponent(String(mangaProviderId))}/download-all`,
|
||||
method: "POST",
|
||||
signal,
|
||||
},
|
||||
options,
|
||||
);
|
||||
};
|
||||
|
||||
export const getDownloadAllChaptersMutationOptions = <
|
||||
TError = unknown,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof downloadAllChapters>>,
|
||||
TError,
|
||||
{ mangaProviderId: number },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof customInstance>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof downloadAllChapters>>,
|
||||
TError,
|
||||
{ mangaProviderId: number },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ["downloadAllChapters"];
|
||||
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 downloadAllChapters>>,
|
||||
{ mangaProviderId: number }
|
||||
> = (props) => {
|
||||
const { mangaProviderId } = props ?? {};
|
||||
|
||||
return downloadAllChapters(mangaProviderId, requestOptions);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type DownloadAllChaptersMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof downloadAllChapters>>
|
||||
>;
|
||||
|
||||
export type DownloadAllChaptersMutationError = unknown;
|
||||
|
||||
/**
|
||||
* @summary Download all chapters for a manga provider
|
||||
*/
|
||||
export const useDownloadAllChapters = <TError = unknown, TContext = unknown>(
|
||||
options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof downloadAllChapters>>,
|
||||
TError,
|
||||
{ mangaProviderId: number },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof customInstance>;
|
||||
},
|
||||
queryClient?: QueryClient,
|
||||
): UseMutationResult<
|
||||
Awaited<ReturnType<typeof downloadAllChapters>>,
|
||||
TError,
|
||||
{ mangaProviderId: number },
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getDownloadAllChaptersMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions, queryClient);
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch the chapter from the provider
|
||||
* @summary Fetch chapter
|
||||
*/
|
||||
export const fetchChapter = (
|
||||
@ -725,9 +623,9 @@ export const fetchChapter = (
|
||||
options?: SecondParameter<typeof customInstance>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return customInstance<void>(
|
||||
return customInstance<DefaultResponseDTOVoid>(
|
||||
{
|
||||
url: `http://rov-lenovo.badger-pirarucu.ts.net:8080/mangas/chapter/${encodeURIComponent(String(chapterId))}/fetch`,
|
||||
url: `http://rov-lenovo.badger-pirarucu.ts.net:8080/mangas/chapters/${encodeURIComponent(String(chapterId))}/fetch`,
|
||||
method: "POST",
|
||||
signal,
|
||||
},
|
||||
@ -805,6 +703,7 @@ export const useFetchChapter = <TError = unknown, TContext = unknown>(
|
||||
};
|
||||
|
||||
/**
|
||||
* Download a chapter as a compressed file by its ID.
|
||||
* @summary Download chapter archive
|
||||
*/
|
||||
export const downloadChapterArchive = (
|
||||
@ -815,7 +714,7 @@ export const downloadChapterArchive = (
|
||||
) => {
|
||||
return customInstance<Blob>(
|
||||
{
|
||||
url: `http://rov-lenovo.badger-pirarucu.ts.net:8080/mangas/chapter/${encodeURIComponent(String(chapterId))}/download-archive`,
|
||||
url: `http://rov-lenovo.badger-pirarucu.ts.net:8080/mangas/chapters/${encodeURIComponent(String(chapterId))}/download`,
|
||||
method: "POST",
|
||||
params,
|
||||
responseType: "blob",
|
||||
@ -909,7 +808,7 @@ export const importMultipleFiles = (
|
||||
formData.append(`files`, value),
|
||||
);
|
||||
|
||||
return customInstance<string>(
|
||||
return customInstance<DefaultResponseDTOVoid>(
|
||||
{
|
||||
url: `http://rov-lenovo.badger-pirarucu.ts.net:8080/manga/import/upload`,
|
||||
method: "POST",
|
||||
@ -990,6 +889,252 @@ export const useImportMultipleFiles = <TError = unknown, TContext = unknown>(
|
||||
return useMutation(mutationOptions, queryClient);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get list of pending import reviews.
|
||||
* @summary Get list of pending import reviews
|
||||
*/
|
||||
export const getImportReviews = (
|
||||
options?: SecondParameter<typeof customInstance>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return customInstance<DefaultResponseDTOListImportReviewDTO>(
|
||||
{
|
||||
url: `http://rov-lenovo.badger-pirarucu.ts.net:8080/manga/import/review`,
|
||||
method: "GET",
|
||||
signal,
|
||||
},
|
||||
options,
|
||||
);
|
||||
};
|
||||
|
||||
export const getGetImportReviewsQueryKey = () => {
|
||||
return [
|
||||
`http://rov-lenovo.badger-pirarucu.ts.net:8080/manga/import/review`,
|
||||
] as const;
|
||||
};
|
||||
|
||||
export const getGetImportReviewsQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getImportReviews>>,
|
||||
TError = unknown,
|
||||
>(options?: {
|
||||
query?: Partial<
|
||||
UseQueryOptions<Awaited<ReturnType<typeof getImportReviews>>, TError, TData>
|
||||
>;
|
||||
request?: SecondParameter<typeof customInstance>;
|
||||
}) => {
|
||||
const { query: queryOptions, request: requestOptions } = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getGetImportReviewsQueryKey();
|
||||
|
||||
const queryFn: QueryFunction<
|
||||
Awaited<ReturnType<typeof getImportReviews>>
|
||||
> = ({ signal }) => getImportReviews(requestOptions, signal);
|
||||
|
||||
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getImportReviews>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: DataTag<QueryKey, TData, TError> };
|
||||
};
|
||||
|
||||
export type GetImportReviewsQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof getImportReviews>>
|
||||
>;
|
||||
export type GetImportReviewsQueryError = unknown;
|
||||
|
||||
export function useGetImportReviews<
|
||||
TData = Awaited<ReturnType<typeof getImportReviews>>,
|
||||
TError = unknown,
|
||||
>(
|
||||
options: {
|
||||
query: Partial<
|
||||
UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getImportReviews>>,
|
||||
TError,
|
||||
TData
|
||||
>
|
||||
> &
|
||||
Pick<
|
||||
DefinedInitialDataOptions<
|
||||
Awaited<ReturnType<typeof getImportReviews>>,
|
||||
TError,
|
||||
Awaited<ReturnType<typeof getImportReviews>>
|
||||
>,
|
||||
"initialData"
|
||||
>;
|
||||
request?: SecondParameter<typeof customInstance>;
|
||||
},
|
||||
queryClient?: QueryClient,
|
||||
): DefinedUseQueryResult<TData, TError> & {
|
||||
queryKey: DataTag<QueryKey, TData, TError>;
|
||||
};
|
||||
export function useGetImportReviews<
|
||||
TData = Awaited<ReturnType<typeof getImportReviews>>,
|
||||
TError = unknown,
|
||||
>(
|
||||
options?: {
|
||||
query?: Partial<
|
||||
UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getImportReviews>>,
|
||||
TError,
|
||||
TData
|
||||
>
|
||||
> &
|
||||
Pick<
|
||||
UndefinedInitialDataOptions<
|
||||
Awaited<ReturnType<typeof getImportReviews>>,
|
||||
TError,
|
||||
Awaited<ReturnType<typeof getImportReviews>>
|
||||
>,
|
||||
"initialData"
|
||||
>;
|
||||
request?: SecondParameter<typeof customInstance>;
|
||||
},
|
||||
queryClient?: QueryClient,
|
||||
): UseQueryResult<TData, TError> & {
|
||||
queryKey: DataTag<QueryKey, TData, TError>;
|
||||
};
|
||||
export function useGetImportReviews<
|
||||
TData = Awaited<ReturnType<typeof getImportReviews>>,
|
||||
TError = unknown,
|
||||
>(
|
||||
options?: {
|
||||
query?: Partial<
|
||||
UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getImportReviews>>,
|
||||
TError,
|
||||
TData
|
||||
>
|
||||
>;
|
||||
request?: SecondParameter<typeof customInstance>;
|
||||
},
|
||||
queryClient?: QueryClient,
|
||||
): UseQueryResult<TData, TError> & {
|
||||
queryKey: DataTag<QueryKey, TData, TError>;
|
||||
};
|
||||
/**
|
||||
* @summary Get list of pending import reviews
|
||||
*/
|
||||
|
||||
export function useGetImportReviews<
|
||||
TData = Awaited<ReturnType<typeof getImportReviews>>,
|
||||
TError = unknown,
|
||||
>(
|
||||
options?: {
|
||||
query?: Partial<
|
||||
UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getImportReviews>>,
|
||||
TError,
|
||||
TData
|
||||
>
|
||||
>;
|
||||
request?: SecondParameter<typeof customInstance>;
|
||||
},
|
||||
queryClient?: QueryClient,
|
||||
): UseQueryResult<TData, TError> & {
|
||||
queryKey: DataTag<QueryKey, TData, TError>;
|
||||
} {
|
||||
const queryOptions = getGetImportReviewsQueryOptions(options);
|
||||
|
||||
const query = useQuery(queryOptions, queryClient) as UseQueryResult<
|
||||
TData,
|
||||
TError
|
||||
> & { queryKey: DataTag<QueryKey, TData, TError> };
|
||||
|
||||
query.queryKey = queryOptions.queryKey;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve import review by ID.
|
||||
* @summary Resolve import review
|
||||
*/
|
||||
export const resolveImportReview = (
|
||||
params: ResolveImportReviewParams,
|
||||
options?: SecondParameter<typeof customInstance>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return customInstance<DefaultResponseDTOVoid>(
|
||||
{
|
||||
url: `http://rov-lenovo.badger-pirarucu.ts.net:8080/manga/import/review`,
|
||||
method: "POST",
|
||||
params,
|
||||
signal,
|
||||
},
|
||||
options,
|
||||
);
|
||||
};
|
||||
|
||||
export const getResolveImportReviewMutationOptions = <
|
||||
TError = unknown,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof resolveImportReview>>,
|
||||
TError,
|
||||
{ params: ResolveImportReviewParams },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof customInstance>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof resolveImportReview>>,
|
||||
TError,
|
||||
{ params: ResolveImportReviewParams },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ["resolveImportReview"];
|
||||
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 resolveImportReview>>,
|
||||
{ params: ResolveImportReviewParams }
|
||||
> = (props) => {
|
||||
const { params } = props ?? {};
|
||||
|
||||
return resolveImportReview(params, requestOptions);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type ResolveImportReviewMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof resolveImportReview>>
|
||||
>;
|
||||
|
||||
export type ResolveImportReviewMutationError = unknown;
|
||||
|
||||
/**
|
||||
* @summary Resolve import review
|
||||
*/
|
||||
export const useResolveImportReview = <TError = unknown, TContext = unknown>(
|
||||
options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof resolveImportReview>>,
|
||||
TError,
|
||||
{ params: ResolveImportReviewParams },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof customInstance>;
|
||||
},
|
||||
queryClient?: QueryClient,
|
||||
): UseMutationResult<
|
||||
Awaited<ReturnType<typeof resolveImportReview>>,
|
||||
TError,
|
||||
{ params: ResolveImportReviewParams },
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getResolveImportReviewMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions, queryClient);
|
||||
};
|
||||
|
||||
/**
|
||||
* Imports manga data from MangaDex into the local database.
|
||||
* @summary Import manga from MangaDex
|
||||
@ -999,7 +1144,7 @@ export const importFromMangaDex = (
|
||||
options?: SecondParameter<typeof customInstance>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return customInstance<ImportMangaDexResponseDTO>(
|
||||
return customInstance<DefaultResponseDTOImportMangaDexResponseDTO>(
|
||||
{
|
||||
url: `http://rov-lenovo.badger-pirarucu.ts.net:8080/manga/import/manga-dex`,
|
||||
method: "POST",
|
||||
@ -1089,7 +1234,7 @@ export const registerUser = (
|
||||
options?: SecondParameter<typeof customInstance>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return customInstance<void>(
|
||||
return customInstance<DefaultResponseDTOVoid>(
|
||||
{
|
||||
url: `http://rov-lenovo.badger-pirarucu.ts.net:8080/auth/register`,
|
||||
method: "POST",
|
||||
@ -1179,7 +1324,7 @@ export const authenticateUser = (
|
||||
options?: SecondParameter<typeof customInstance>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return customInstance<AuthenticationResponseDTO>(
|
||||
return customInstance<DefaultResponseDTOAuthenticationResponseDTO>(
|
||||
{
|
||||
url: `http://rov-lenovo.badger-pirarucu.ts.net:8080/auth/login`,
|
||||
method: "POST",
|
||||
@ -1269,7 +1414,7 @@ export const getMangas = (
|
||||
options?: SecondParameter<typeof customInstance>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return customInstance<PageMangaListDTO>(
|
||||
return customInstance<DefaultResponseDTOPageMangaListDTO>(
|
||||
{
|
||||
url: `http://rov-lenovo.badger-pirarucu.ts.net:8080/mangas`,
|
||||
method: "GET",
|
||||
@ -1420,7 +1565,7 @@ export const getMangaChapters = (
|
||||
options?: SecondParameter<typeof customInstance>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return customInstance<MangaChapterDTO[]>(
|
||||
return customInstance<DefaultResponseDTOListMangaChapterDTO>(
|
||||
{
|
||||
url: `http://rov-lenovo.badger-pirarucu.ts.net:8080/mangas/${encodeURIComponent(String(mangaProviderId))}/chapters`,
|
||||
method: "GET",
|
||||
@ -1597,7 +1742,7 @@ export const getManga = (
|
||||
options?: SecondParameter<typeof customInstance>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return customInstance<MangaDTO>(
|
||||
return customInstance<DefaultResponseDTOMangaDTO>(
|
||||
{
|
||||
url: `http://rov-lenovo.badger-pirarucu.ts.net:8080/mangas/${encodeURIComponent(String(mangaId))}`,
|
||||
method: "GET",
|
||||
@ -1741,17 +1886,17 @@ export function useGetManga<
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a list of manga chapters for a specific manga/provider combination.
|
||||
* @summary Get the available chapters for a specific manga/provider combination
|
||||
* Retrieve a list of manga chapter images for a specific manga/provider combination.
|
||||
* @summary Get the images for a specific manga/provider combination
|
||||
*/
|
||||
export const getMangaChapterImages = (
|
||||
chapterId: number,
|
||||
options?: SecondParameter<typeof customInstance>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return customInstance<MangaChapterImagesDTO>(
|
||||
return customInstance<DefaultResponseDTOMangaChapterImagesDTO>(
|
||||
{
|
||||
url: `http://rov-lenovo.badger-pirarucu.ts.net:8080/mangas/${encodeURIComponent(String(chapterId))}/images`,
|
||||
url: `http://rov-lenovo.badger-pirarucu.ts.net:8080/mangas/chapters/${encodeURIComponent(String(chapterId))}/images`,
|
||||
method: "GET",
|
||||
signal,
|
||||
},
|
||||
@ -1761,7 +1906,7 @@ export const getMangaChapterImages = (
|
||||
|
||||
export const getGetMangaChapterImagesQueryKey = (chapterId?: number) => {
|
||||
return [
|
||||
`http://rov-lenovo.badger-pirarucu.ts.net:8080/mangas/${chapterId}/images`,
|
||||
`http://rov-lenovo.badger-pirarucu.ts.net:8080/mangas/chapters/${chapterId}/images`,
|
||||
] as const;
|
||||
};
|
||||
|
||||
@ -1881,7 +2026,7 @@ export function useGetMangaChapterImages<
|
||||
queryKey: DataTag<QueryKey, TData, TError>;
|
||||
};
|
||||
/**
|
||||
* @summary Get the available chapters for a specific manga/provider combination
|
||||
* @summary Get the images for a specific manga/provider combination
|
||||
*/
|
||||
|
||||
export function useGetMangaChapterImages<
|
||||
@ -1923,7 +2068,7 @@ export const getGenres = (
|
||||
options?: SecondParameter<typeof customInstance>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return customInstance<GenreDTO[]>(
|
||||
return customInstance<DefaultResponseDTOListGenreDTO>(
|
||||
{
|
||||
url: `http://rov-lenovo.badger-pirarucu.ts.net:8080/genres`,
|
||||
method: "GET",
|
||||
@ -2053,3 +2198,89 @@ export function useGetGenres<
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete pending import review by ID.
|
||||
* @summary Delete pending import review
|
||||
*/
|
||||
export const deleteImportReview = (
|
||||
id: number,
|
||||
options?: SecondParameter<typeof customInstance>,
|
||||
) => {
|
||||
return customInstance<DefaultResponseDTOVoid>(
|
||||
{
|
||||
url: `http://rov-lenovo.badger-pirarucu.ts.net:8080/manga/import/review/${encodeURIComponent(String(id))}`,
|
||||
method: "DELETE",
|
||||
},
|
||||
options,
|
||||
);
|
||||
};
|
||||
|
||||
export const getDeleteImportReviewMutationOptions = <
|
||||
TError = unknown,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteImportReview>>,
|
||||
TError,
|
||||
{ id: number },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof customInstance>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteImportReview>>,
|
||||
TError,
|
||||
{ id: number },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ["deleteImportReview"];
|
||||
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 deleteImportReview>>,
|
||||
{ id: number }
|
||||
> = (props) => {
|
||||
const { id } = props ?? {};
|
||||
|
||||
return deleteImportReview(id, requestOptions);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type DeleteImportReviewMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof deleteImportReview>>
|
||||
>;
|
||||
|
||||
export type DeleteImportReviewMutationError = unknown;
|
||||
|
||||
/**
|
||||
* @summary Delete pending import review
|
||||
*/
|
||||
export const useDeleteImportReview = <TError = unknown, TContext = unknown>(
|
||||
options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteImportReview>>,
|
||||
TError,
|
||||
{ id: number },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof customInstance>;
|
||||
},
|
||||
queryClient?: QueryClient,
|
||||
): UseMutationResult<
|
||||
Awaited<ReturnType<typeof deleteImportReview>>,
|
||||
TError,
|
||||
{ id: number },
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getDeleteImportReviewMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions, queryClient);
|
||||
};
|
||||
|
||||
70
app/import-review/page.tsx
Normal file
70
app/import-review/page.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
"use client";
|
||||
|
||||
import { useAuth } from "@/contexts/auth-context";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
import { FailedImportCard } from "@/components/failed-import-card";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
import { useGetImportReviews } from "@/api/mangamochi";
|
||||
|
||||
export default function ImportReviewPage() {
|
||||
const { user, isAuthenticated } = useAuth();
|
||||
const router = useRouter();
|
||||
|
||||
const { data: failedImportsData, queryKey } = useGetImportReviews();
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
router.push("/login");
|
||||
}
|
||||
}, [isAuthenticated, router, user]);
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-background">
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-foreground">Import Review</h1>
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
Review and resolve manga imports by manually matching them with
|
||||
MyAnimeList entries.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{!failedImportsData?.data || failedImportsData.data.length === 0 ? (
|
||||
<Card className="p-8 text-center">
|
||||
<AlertCircle className="mx-auto h-12 w-12 text-muted-foreground" />
|
||||
<h2 className="mt-4 text-lg font-semibold text-foreground">
|
||||
No Imports to Review
|
||||
</h2>
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
All your imports have been processed successfully!
|
||||
</p>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{failedImportsData.data.length} import
|
||||
{failedImportsData.data.length !== 1 ? "s" : ""} to review
|
||||
</div>
|
||||
{failedImportsData.data.map((failedImport) => (
|
||||
<FailedImportCard
|
||||
key={failedImport.id}
|
||||
failedImport={failedImport}
|
||||
queryKey={queryKey}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@ -25,12 +25,12 @@ export default function ChapterReaderPage() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentPage === data.chapterImageKeys.length) {
|
||||
if (currentPage === data.data?.chapterImageKeys.length) {
|
||||
mutate({ chapterId: chapterNumber });
|
||||
}
|
||||
}, [data, mutate, currentPage]);
|
||||
|
||||
if (!data) {
|
||||
if (!data?.data) {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-background">
|
||||
<div className="text-center">
|
||||
@ -46,7 +46,11 @@ export default function ChapterReaderPage() {
|
||||
}
|
||||
|
||||
const goToNextPage = () => {
|
||||
if (currentPage < data?.chapterImageKeys.length) {
|
||||
if (!data?.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentPage < data.data.chapterImageKeys.length) {
|
||||
setCurrentPage(currentPage + 1);
|
||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||
}
|
||||
@ -86,7 +90,7 @@ export default function ChapterReaderPage() {
|
||||
</Button>
|
||||
<div className="hidden sm:block">
|
||||
<h1 className="text-sm font-semibold text-foreground">
|
||||
{data.mangaTitle}
|
||||
{data.data.mangaTitle}
|
||||
</h1>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Chapter {chapterNumber}
|
||||
@ -95,7 +99,7 @@ export default function ChapterReaderPage() {
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Page {currentPage} / {data.chapterImageKeys.length}
|
||||
Page {currentPage} / {data.data.chapterImageKeys.length}
|
||||
</span>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
@ -108,7 +112,7 @@ export default function ChapterReaderPage() {
|
||||
{/* Mobile title */}
|
||||
<div className="mb-4 sm:hidden">
|
||||
<h1 className="text-lg font-semibold text-foreground">
|
||||
{data.mangaTitle}
|
||||
{data.data.mangaTitle}
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Chapter {chapterNumber}
|
||||
@ -120,7 +124,8 @@ export default function ChapterReaderPage() {
|
||||
<Image
|
||||
src={
|
||||
"http://omv.badger-pirarucu.ts.net:9000/mangamochi/" +
|
||||
data.chapterImageKeys[currentPage - 1] || "/placeholder.svg"
|
||||
data.data.chapterImageKeys[currentPage - 1] ||
|
||||
"/placeholder.svg"
|
||||
}
|
||||
alt={`Page ${currentPage}`}
|
||||
width={1000}
|
||||
@ -144,11 +149,11 @@ export default function ChapterReaderPage() {
|
||||
Previous Page
|
||||
</Button>
|
||||
<span className="text-sm font-medium text-foreground">
|
||||
{currentPage} / {data.chapterImageKeys.length}
|
||||
{currentPage} / {data.data.chapterImageKeys.length}
|
||||
</span>
|
||||
<Button
|
||||
onClick={goToNextPage}
|
||||
disabled={currentPage === data.chapterImageKeys.length}
|
||||
disabled={currentPage === data.data.chapterImageKeys.length}
|
||||
variant="outline"
|
||||
className="gap-2 bg-transparent"
|
||||
>
|
||||
|
||||
@ -68,7 +68,7 @@ export default function MangaDetailPage() {
|
||||
});
|
||||
};
|
||||
|
||||
const author = mangaData.authors.join(", ");
|
||||
const author = mangaData.data?.authors.join(", ");
|
||||
|
||||
const formatter = new Intl.DateTimeFormat("en-US", {
|
||||
month: "2-digit",
|
||||
@ -76,11 +76,11 @@ export default function MangaDetailPage() {
|
||||
year: "numeric",
|
||||
});
|
||||
|
||||
const publishedTo = mangaData.publishedTo
|
||||
? formatter.format(new Date(mangaData.publishedTo))
|
||||
const publishedTo = mangaData.data?.publishedTo
|
||||
? formatter.format(new Date(mangaData.data.publishedTo))
|
||||
: null;
|
||||
const publishedFrom = mangaData.publishedFrom
|
||||
? formatter.format(new Date(mangaData.publishedFrom))
|
||||
const publishedFrom = mangaData.data?.publishedFrom
|
||||
? formatter.format(new Date(mangaData.data.publishedFrom))
|
||||
: null;
|
||||
|
||||
const dateRange = publishedTo
|
||||
@ -118,12 +118,12 @@ export default function MangaDetailPage() {
|
||||
<div className="relative aspect-[2/3] overflow-hidden rounded-lg border border-border bg-muted lg:sticky lg:top-8 lg:h-fit">
|
||||
<Image
|
||||
src={
|
||||
(mangaData.coverImageKey &&
|
||||
(mangaData.data?.coverImageKey &&
|
||||
"http://omv.badger-pirarucu.ts.net:9000/mangamochi/" +
|
||||
mangaData.coverImageKey) ||
|
||||
mangaData.data?.coverImageKey) ||
|
||||
"/placeholder.svg"
|
||||
}
|
||||
alt={mangaData.title}
|
||||
alt={mangaData.data?.title ?? ""}
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
@ -134,26 +134,26 @@ export default function MangaDetailPage() {
|
||||
<div>
|
||||
<div className="mb-3 flex items-start justify-between gap-4">
|
||||
<h1 className="text-balance text-4xl font-bold tracking-tight text-foreground">
|
||||
{mangaData.title}
|
||||
{mangaData.data?.title}
|
||||
</h1>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="border border-border bg-card text-foreground"
|
||||
>
|
||||
{mangaData.status}
|
||||
{mangaData.data?.status}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-lg text-muted-foreground">{author}</p>
|
||||
</div>
|
||||
|
||||
{mangaData.alternativeTitles &&
|
||||
mangaData.alternativeTitles.length > 0 && (
|
||||
{mangaData.data?.alternativeTitles &&
|
||||
mangaData.data?.alternativeTitles.length > 0 && (
|
||||
<div>
|
||||
<h3 className="mb-2 text-sm font-medium text-muted-foreground">
|
||||
Alternative Titles
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{mangaData.alternativeTitles.map((title, index) => (
|
||||
{mangaData.data?.alternativeTitles.map((title, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="rounded-md bg-muted px-3 py-1 text-sm text-foreground"
|
||||
@ -166,7 +166,7 @@ export default function MangaDetailPage() {
|
||||
)}
|
||||
|
||||
<p className="text-pretty text-justify leading-relaxed text-foreground">
|
||||
{mangaData.synopsis}
|
||||
{mangaData.data?.synopsis}
|
||||
</p>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
@ -175,8 +175,8 @@ export default function MangaDetailPage() {
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Rating</p>
|
||||
<p className="text-lg font-semibold text-foreground">
|
||||
{mangaData.score && mangaData.score > 0
|
||||
? mangaData.score
|
||||
{mangaData.data?.score && mangaData.data?.score > 0
|
||||
? mangaData.data?.score
|
||||
: "-"}
|
||||
</p>
|
||||
</div>
|
||||
@ -187,8 +187,9 @@ export default function MangaDetailPage() {
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Chapters</p>
|
||||
<p className="text-lg font-semibold text-foreground">
|
||||
{mangaData.chapterCount && mangaData.chapterCount > 0
|
||||
? mangaData.chapterCount
|
||||
{mangaData.data?.chapterCount &&
|
||||
mangaData.data?.chapterCount > 0
|
||||
? mangaData.data?.chapterCount
|
||||
: "-"}
|
||||
</p>
|
||||
</div>
|
||||
@ -209,14 +210,14 @@ export default function MangaDetailPage() {
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Providers</p>
|
||||
<p className="text-lg font-semibold text-foreground">
|
||||
{mangaData.providerCount}
|
||||
{mangaData.data?.providerCount}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{mangaData.genres.map((genre) => (
|
||||
{mangaData.data?.genres.map((genre) => (
|
||||
<Badge
|
||||
key={genre}
|
||||
variant="outline"
|
||||
@ -235,7 +236,7 @@ export default function MangaDetailPage() {
|
||||
</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
{mangaData?.providers.map((provider) => (
|
||||
{mangaData.data?.providers.map((provider) => (
|
||||
<Card key={provider.id} className="border-border bg-card">
|
||||
<Collapsible
|
||||
open={openProviders.has(provider.id)}
|
||||
|
||||
14
app/page.tsx
14
app/page.tsx
@ -36,7 +36,7 @@ export default function HomePage() {
|
||||
score: minRating,
|
||||
});
|
||||
|
||||
const totalPages = mangas?.totalPages;
|
||||
const totalPages = mangas?.data?.totalPages;
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen bg-background">
|
||||
@ -67,7 +67,7 @@ export default function HomePage() {
|
||||
MangaMochi
|
||||
</h1>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
{mangas?.totalElements} titles available
|
||||
{mangas?.data?.totalElements} titles available
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -97,17 +97,17 @@ export default function HomePage() {
|
||||
</header>
|
||||
|
||||
<main className="px-8 py-8">
|
||||
{mangas?.content && mangas.content.length > 0 ? (
|
||||
{mangas?.data?.content && mangas.data.content.length > 0 ? (
|
||||
<>
|
||||
{mangas?.totalElements && (
|
||||
{mangas?.data?.totalElements && (
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Showing {(currentPage - 1) * ITEMS_PER_PAGE + 1} to{" "}
|
||||
{Math.min(
|
||||
currentPage * ITEMS_PER_PAGE,
|
||||
mangas.totalElements,
|
||||
mangas.data.totalElements,
|
||||
)}{" "}
|
||||
of {mangas.totalElements}
|
||||
of {mangas.data.totalElements}
|
||||
</p>
|
||||
<SortOptions
|
||||
currentSort={sortOption}
|
||||
@ -115,7 +115,7 @@ export default function HomePage() {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<MangaGrid manga={mangas?.content} queryKey={queryKey} />
|
||||
<MangaGrid manga={mangas?.data?.content} queryKey={queryKey} />
|
||||
|
||||
{totalPages && totalPages > 1 && (
|
||||
<div className="mt-12">
|
||||
|
||||
134
components/failed-import-card.tsx
Normal file
134
components/failed-import-card.tsx
Normal file
@ -0,0 +1,134 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { useState } from "react";
|
||||
import { ExternalLink, Trash2 } from "lucide-react";
|
||||
import {
|
||||
ImportReviewDTO,
|
||||
useDeleteImportReview,
|
||||
useResolveImportReview,
|
||||
} from "@/api/mangamochi";
|
||||
import { toast } from "sonner";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
interface FailedImportCardProps {
|
||||
failedImport: ImportReviewDTO;
|
||||
queryKey: any;
|
||||
}
|
||||
|
||||
export function FailedImportCard({
|
||||
failedImport,
|
||||
queryKey,
|
||||
}: FailedImportCardProps) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [malId, setMalId] = useState("");
|
||||
|
||||
const {
|
||||
mutate: mutateDeleteImportReview,
|
||||
isPending: isPendingDeleteImportReview,
|
||||
} = useDeleteImportReview({
|
||||
mutation: {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey });
|
||||
|
||||
toast.success("Import review removed successfully");
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
mutate: mutateResolveImportReview,
|
||||
isPending: isPendingResolveImportReview,
|
||||
} = useResolveImportReview({
|
||||
mutation: {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey });
|
||||
toast.success("Import review resolved successfully");
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const handleResolve = () => {
|
||||
if (!malId.trim()) {
|
||||
alert("Please enter a MyAnimeList ID");
|
||||
return;
|
||||
}
|
||||
|
||||
mutateResolveImportReview({
|
||||
params: { importReviewId: failedImport.id, malId },
|
||||
});
|
||||
};
|
||||
|
||||
const handleRemove = () => {
|
||||
mutateDeleteImportReview({ id: failedImport.id });
|
||||
};
|
||||
|
||||
const importDate = new Date(failedImport.createdAt).toLocaleDateString();
|
||||
|
||||
return (
|
||||
<Card className="p-4">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<h3 className="font-semibold text-foreground">
|
||||
{failedImport.title}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Provider:{" "}
|
||||
<span className="capitalize">{failedImport.providerName}</span> •{" "}
|
||||
{importDate}
|
||||
</p>
|
||||
</div>
|
||||
{failedImport.externalUrl && (
|
||||
<a
|
||||
href={failedImport.externalUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="ml-2 text-primary hover:underline"
|
||||
>
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="rounded-md bg-destructive/10 p-3">
|
||||
<p className="text-sm text-destructive">{failedImport.reason}</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="text-sm font-medium">MyAnimeList Manga ID</label>
|
||||
<Input
|
||||
placeholder="Enter MAL ID to match manually"
|
||||
value={malId}
|
||||
onChange={(e) => setMalId(e.target.value)}
|
||||
className="mt-2"
|
||||
type="number"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={handleResolve}
|
||||
disabled={isPendingResolveImportReview || !malId.trim()}
|
||||
className="flex-1"
|
||||
>
|
||||
{isPendingResolveImportReview ? "Resolving..." : "Resolve Import"}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleRemove}
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="text-destructive hover:text-destructive bg-transparent"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@ -137,7 +137,7 @@ export function FilterSidebar({
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{isPendingGenres && <Skeleton className="h-[240px] w-full" />}
|
||||
{!isPendingGenres &&
|
||||
genresData?.map((genre) => {
|
||||
genresData?.data?.map((genre) => {
|
||||
const isSelected = selectedGenres.includes(genre.id);
|
||||
return (
|
||||
<Badge
|
||||
|
||||
@ -8,12 +8,14 @@ import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Download, FileUp } from "lucide-react";
|
||||
import { AlertCircle, Download, FileUp } from "lucide-react";
|
||||
import { useAuth } from "@/contexts/auth-context";
|
||||
import { MangaDexImportDialog } from "@/components/manga-dex-import-dialog";
|
||||
import { MangaManualImportDialog } from "@/components/manga-manual-import-dialog";
|
||||
import Link from "next/link";
|
||||
|
||||
export function ImportDropdown() {
|
||||
const { isAuthenticated } = useAuth();
|
||||
@ -43,6 +45,17 @@ export function ImportDropdown() {
|
||||
<FileUp className="mr-2 h-4 w-4" />
|
||||
Import from File
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem asChild>
|
||||
<Link
|
||||
href="/import-review"
|
||||
className="cursor-pointer gap-2 text-amber-600 dark:text-amber-500"
|
||||
>
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<span>Import Review</span>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
|
||||
@ -74,7 +74,7 @@ export const MangaChapter: FC<MangaChapterProps> = ({
|
||||
</div>
|
||||
) : data ? (
|
||||
<div className="mt-4 space-y-2">
|
||||
{data.map((chapter) => {
|
||||
{data.data?.map((chapter) => {
|
||||
return (
|
||||
<div
|
||||
key={chapter.id}
|
||||
|
||||
@ -48,7 +48,6 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
router.push("/login");
|
||||
toast.success("Registration successful! You can now login.");
|
||||
},
|
||||
onError: () => toast.error("Registration failed. Please try again."),
|
||||
},
|
||||
});
|
||||
|
||||
@ -56,12 +55,16 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
useAuthenticateUser({
|
||||
mutation: {
|
||||
onSuccess: (response) => {
|
||||
if (!response.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
const userData: User = {
|
||||
id: response.id,
|
||||
email: response.email,
|
||||
name: response.name,
|
||||
token: response.token,
|
||||
role: response.role,
|
||||
id: response.data.id,
|
||||
email: response.data.email,
|
||||
name: response.data.name,
|
||||
token: response.data.token,
|
||||
role: response.data.role,
|
||||
};
|
||||
|
||||
setUser(userData);
|
||||
@ -70,7 +73,6 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
|
||||
router.push("/");
|
||||
},
|
||||
onError: () => toast.error("Login failed. Please try again."),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user