diff --git a/src/api/generated/api.schemas.ts b/src/api/generated/api.schemas.ts index 1b18d87..b169a68 100644 --- a/src/api/generated/api.schemas.ts +++ b/src/api/generated/api.schemas.ts @@ -4,10 +4,6 @@ * OpenAPI definition * OpenAPI spec version: v0 */ -export interface UpdateMangaDataCommand { - mangaId?: number; -} - export interface DefaultResponseDTOVoid { timestamp?: string; data?: unknown; @@ -103,9 +99,9 @@ export interface PageMangaListDTO { export interface PageableObject { offset?: number; + paged?: boolean; pageNumber?: number; pageSize?: number; - paged?: boolean; sort?: SortObject; unpaged?: boolean; } @@ -152,6 +148,8 @@ export interface MangaDTO { score: number; providers: MangaProviderDTO[]; chapterCount: number; + favorite: boolean; + following: boolean; } export interface MangaProviderDTO { @@ -173,6 +171,8 @@ export interface MangaChapterImagesDTO { id: number; /** @minLength 1 */ mangaTitle: string; + previousChapterId?: number; + nextChapterId?: number; chapterImageKeys: string[]; } diff --git a/src/api/generated/dev-controller/dev-controller.ts b/src/api/generated/dev-controller/dev-controller.ts deleted file mode 100644 index 54d2356..0000000 --- a/src/api/generated/dev-controller/dev-controller.ts +++ /dev/null @@ -1,86 +0,0 @@ -/** - * Generated by orval v7.15.0 🍺 - * Do not edit manually. - * OpenAPI definition - * OpenAPI spec version: v0 - */ -import { - useMutation -} from '@tanstack/react-query'; -import type { - MutationFunction, - QueryClient, - UseMutationOptions, - UseMutationResult -} from '@tanstack/react-query'; - -import type { - UpdateMangaDataCommand -} from '../api.schemas'; - -import { customInstance } from '../../api'; - - -type SecondParameter unknown> = Parameters[1]; - - - -export const sendRecord = ( - updateMangaDataCommand: UpdateMangaDataCommand, - options?: SecondParameter,signal?: AbortSignal -) => { - - - return customInstance( - {url: `/records`, method: 'POST', - headers: {'Content-Type': 'application/json', }, - data: updateMangaDataCommand, signal - }, - options); - } - - - -export const getSendRecordMutationOptions = (options?: { mutation?:UseMutationOptions>, TError,{data: UpdateMangaDataCommand}, TContext>, request?: SecondParameter} -): UseMutationOptions>, TError,{data: UpdateMangaDataCommand}, TContext> => { - -const mutationKey = ['sendRecord']; -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>, {data: UpdateMangaDataCommand}> = (props) => { - const {data} = props ?? {}; - - return sendRecord(data,requestOptions) - } - - - - - return { mutationFn, ...mutationOptions }} - - export type SendRecordMutationResult = NonNullable>> - export type SendRecordMutationBody = UpdateMangaDataCommand - export type SendRecordMutationError = unknown - - export const useSendRecord = (options?: { mutation?:UseMutationOptions>, TError,{data: UpdateMangaDataCommand}, TContext>, request?: SecondParameter} - , queryClient?: QueryClient): UseMutationResult< - Awaited>, - TError, - {data: UpdateMangaDataCommand}, - TContext - > => { - - const mutationOptions = getSendRecordMutationOptions(options); - - return useMutation(mutationOptions, queryClient); - } - \ No newline at end of file diff --git a/src/api/generated/management/management.ts b/src/api/generated/management/management.ts new file mode 100644 index 0000000..b89a469 --- /dev/null +++ b/src/api/generated/management/management.ts @@ -0,0 +1,154 @@ +/** + * Generated by orval v7.15.0 🍺 + * Do not edit manually. + * OpenAPI definition + * OpenAPI spec version: v0 + */ +import { + useMutation +} from '@tanstack/react-query'; +import type { + MutationFunction, + QueryClient, + UseMutationOptions, + UseMutationResult +} from '@tanstack/react-query'; + +import type { + DefaultResponseDTOVoid +} from '../api.schemas'; + +import { customInstance } from '../../api'; + + +type SecondParameter unknown> = Parameters[1]; + + + +/** + * Queue the retrieval of the manga lists from the content providers + * @summary Queue update manga list + */ +export const updateMangaList = ( + + options?: SecondParameter,signal?: AbortSignal +) => { + + + return customInstance( + {url: `/management/update-manga-list`, method: 'POST', signal + }, + options); + } + + + +export const getUpdateMangaListMutationOptions = (options?: { mutation?:UseMutationOptions>, TError,void, TContext>, request?: SecondParameter} +): UseMutationOptions>, TError,void, TContext> => { + +const mutationKey = ['updateMangaList']; +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>, void> = () => { + + + return updateMangaList(requestOptions) + } + + + + + return { mutationFn, ...mutationOptions }} + + export type UpdateMangaListMutationResult = NonNullable>> + + export type UpdateMangaListMutationError = unknown + + /** + * @summary Queue update manga list + */ +export const useUpdateMangaList = (options?: { mutation?:UseMutationOptions>, TError,void, TContext>, request?: SecondParameter} + , queryClient?: QueryClient): UseMutationResult< + Awaited>, + TError, + void, + TContext + > => { + + const mutationOptions = getUpdateMangaListMutationOptions(options); + + return useMutation(mutationOptions, queryClient); + } + /** + * Triggers the cleanup of untracked S3 images + * @summary Cleanup unused S3 images + */ +export const imageCleanup = ( + + options?: SecondParameter,signal?: AbortSignal +) => { + + + return customInstance( + {url: `/management/image-cleanup`, method: 'POST', signal + }, + options); + } + + + +export const getImageCleanupMutationOptions = (options?: { mutation?:UseMutationOptions>, TError,void, TContext>, request?: SecondParameter} +): UseMutationOptions>, TError,void, TContext> => { + +const mutationKey = ['imageCleanup']; +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>, void> = () => { + + + return imageCleanup(requestOptions) + } + + + + + return { mutationFn, ...mutationOptions }} + + export type ImageCleanupMutationResult = NonNullable>> + + export type ImageCleanupMutationError = unknown + + /** + * @summary Cleanup unused S3 images + */ +export const useImageCleanup = (options?: { mutation?:UseMutationOptions>, TError,void, TContext>, request?: SecondParameter} + , queryClient?: QueryClient): UseMutationResult< + Awaited>, + TError, + void, + TContext + > => { + + const mutationOptions = getImageCleanupMutationOptions(options); + + return useMutation(mutationOptions, queryClient); + } + \ No newline at end of file diff --git a/src/api/generated/manga/manga.ts b/src/api/generated/manga/manga.ts index 58a9e7f..7405d4d 100644 --- a/src/api/generated/manga/manga.ts +++ b/src/api/generated/manga/manga.ts @@ -102,6 +102,132 @@ export const useFetchMangaChapters = ,signal?: AbortSignal +) => { + + + return customInstance( + {url: `/mangas/${encodeURIComponent(String(mangaId))}/unfollowManga`, method: 'POST', signal + }, + options); + } + + + +export const getUnfollowMangaMutationOptions = (options?: { mutation?:UseMutationOptions>, TError,{mangaId: number}, TContext>, request?: SecondParameter} +): UseMutationOptions>, TError,{mangaId: number}, TContext> => { + +const mutationKey = ['unfollowManga']; +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>, {mangaId: number}> = (props) => { + const {mangaId} = props ?? {}; + + return unfollowManga(mangaId,requestOptions) + } + + + + + return { mutationFn, ...mutationOptions }} + + export type UnfollowMangaMutationResult = NonNullable>> + + export type UnfollowMangaMutationError = unknown + + /** + * @summary Unfollow the manga specified by its ID + */ +export const useUnfollowManga = (options?: { mutation?:UseMutationOptions>, TError,{mangaId: number}, TContext>, request?: SecondParameter} + , queryClient?: QueryClient): UseMutationResult< + Awaited>, + TError, + {mangaId: number}, + TContext + > => { + + const mutationOptions = getUnfollowMangaMutationOptions(options); + + return useMutation(mutationOptions, queryClient); + } + /** + * Follow the manga specified by its ID. + * @summary Follow the manga specified by its ID + */ +export const followManga = ( + mangaId: number, + options?: SecondParameter,signal?: AbortSignal +) => { + + + return customInstance( + {url: `/mangas/${encodeURIComponent(String(mangaId))}/followManga`, method: 'POST', signal + }, + options); + } + + + +export const getFollowMangaMutationOptions = (options?: { mutation?:UseMutationOptions>, TError,{mangaId: number}, TContext>, request?: SecondParameter} +): UseMutationOptions>, TError,{mangaId: number}, TContext> => { + +const mutationKey = ['followManga']; +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>, {mangaId: number}> = (props) => { + const {mangaId} = props ?? {}; + + return followManga(mangaId,requestOptions) + } + + + + + return { mutationFn, ...mutationOptions }} + + export type FollowMangaMutationResult = NonNullable>> + + export type FollowMangaMutationError = unknown + + /** + * @summary Follow the manga specified by its ID + */ +export const useFollowManga = (options?: { mutation?:UseMutationOptions>, TError,{mangaId: number}, TContext>, request?: SecondParameter} + , queryClient?: QueryClient): UseMutationResult< + Awaited>, + TError, + {mangaId: number}, + TContext + > => { + + const mutationOptions = getFollowMangaMutationOptions(options); + + return useMutation(mutationOptions, queryClient); + } + /** * Retrieve a list of mangas with their details. * @summary Get a list of mangas */ diff --git a/src/features/home/components/MangaDexImportDialog.tsx b/src/features/home/components/MangaDexImportDialog.tsx index cbec502..2b78c64 100644 --- a/src/features/home/components/MangaDexImportDialog.tsx +++ b/src/features/home/components/MangaDexImportDialog.tsx @@ -32,10 +32,9 @@ export const MangaDexImportDialog = ({ mangaDexDialogOpen, onMangaDexDialogOpenChange, }: MangaDexImportDialogProps) => { - const formSchema = z - .object({ - value: z.string().min(1, "Please enter a MangaDex ID or URL."), - }); + const formSchema = z.object({ + value: z.string().min(1, "Please enter a MangaDex ID or URL."), + }); const form = useForm>({ resolver: zodResolver(formSchema), diff --git a/src/pages/Manga.tsx b/src/pages/Manga.tsx index 186ad44..2de9ba3 100644 --- a/src/pages/Manga.tsx +++ b/src/pages/Manga.tsx @@ -1,18 +1,27 @@ import { useQueryClient } from "@tanstack/react-query"; import { ArrowLeft, + Bell, + BellOff, BookOpen, Calendar, ChevronDown, Database, + Heart, Star, } from "lucide-react"; -import { useState } from "react"; +import { useCallback, useState } from "react"; import { useNavigate, useParams } from "react-router"; import { toast } from "sonner"; +import { + useSetFavorite, + useSetUnfavorite, +} from "@/api/generated/favorite-mangas/favorite-mangas.ts"; import { useFetchMangaChapters, + useFollowManga, useGetManga, + useUnfollowManga, } from "@/api/generated/manga/manga.ts"; import { useFetchAllChapters } from "@/api/generated/manga-chapter/manga-chapter.ts"; import { ThemeToggle } from "@/components/ThemeToggle.tsx"; @@ -24,10 +33,12 @@ import { CollapsibleContent, CollapsibleTrigger, } from "@/components/ui/collapsible.tsx"; +import { useAuth } from "@/contexts/AuthContext.tsx"; import { MangaChapter } from "@/features/manga/MangaChapter.tsx"; import { formatToTwoDigitsDateRange } from "@/utils/dateFormatter.ts"; const Manga = () => { + const { isAuthenticated } = useAuth(); const navigate = useNavigate(); const params = useParams(); const mangaId = Number(params.mangaId); @@ -53,6 +64,65 @@ const Manga = () => { const [openProviders, setOpenProviders] = useState>(new Set()); + const { mutate: mutateFavorite, isPending: isPendingFavorite } = + useSetFavorite({ + mutation: { + onSuccess: () => queryClient.invalidateQueries({ queryKey }), + }, + }); + + const { mutate: mutateUnfavorite, isPending: isPendingUnfavorite } = + useSetUnfavorite({ + mutation: { + onSuccess: () => queryClient.invalidateQueries({ queryKey }), + }, + }); + + const isPendingFavoriteChange = isPendingFavorite || isPendingUnfavorite; + + const handleFavoriteClick = useCallback( + (isFavorite: boolean) => { + isFavorite + ? mutateUnfavorite({ id: mangaData?.data?.id ?? -1 }) + : mutateFavorite({ id: mangaData?.data?.id ?? -1 }); + }, + [mutateUnfavorite, mutateFavorite, mangaData?.data?.id], + ); + + const { mutate: mutateFollow, isPending: isPendingFollow } = useFollowManga({ + mutation: { + onSuccess: () => { + queryClient.invalidateQueries({ queryKey }); + toast.success( + "We will notify you when new content if available for this manga.", + ); + }, + }, + }); + + const { mutate: mutateUnfollow, isPending: isPendingUnfollow } = + useUnfollowManga({ + mutation: { + onSuccess: () => { + queryClient.invalidateQueries({ queryKey }); + toast.success( + "You will no longer received notifications for this manga.", + ); + }, + }, + }); + + const isPendingFollowChange = isPendingFollow || isPendingUnfollow; + + const handleFollowClick = useCallback( + (isFollowing: boolean) => { + isFollowing + ? mutateUnfollow({ mangaId: mangaData?.data?.id ?? -1 }) + : mutateFollow({ mangaId: mangaData?.data?.id ?? -1 }); + }, + [mangaData?.data?.id, mutateUnfollow, mutateFollow], + ); + if (!mangaData) { return (
@@ -125,16 +195,57 @@ const Manga = () => { {/* Details */}
-
+

{mangaData.data?.title}

- - {mangaData.data?.status} - +
+ + {mangaData.data?.status} + + + {isAuthenticated && ( + <> + + + + )} +

{mangaData.data?.authors.join(", ")}