diff --git a/package-lock.json b/package-lock.json index 3782cec..466ad1e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,6 +40,7 @@ "zod": "^4.1.12" }, "devDependencies": { + "@biomejs/biome": "2.4.9", "@eslint/js": "^9.36.0", "@types/node": "^24.9.2", "@types/react": "^19.1.16", @@ -432,6 +433,169 @@ "node": ">=6.9.0" } }, + "node_modules/@biomejs/biome": { + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.4.9.tgz", + "integrity": "sha512-wvZW92FrwitTcacvCBT8xdAbfbxWfDLwjYMmU3djjqQTh7Ni4ZdiWIT/x5VcZ+RQuxiKzIOzi5D+dcyJDFZMsA==", + "dev": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.4.9", + "@biomejs/cli-darwin-x64": "2.4.9", + "@biomejs/cli-linux-arm64": "2.4.9", + "@biomejs/cli-linux-arm64-musl": "2.4.9", + "@biomejs/cli-linux-x64": "2.4.9", + "@biomejs/cli-linux-x64-musl": "2.4.9", + "@biomejs/cli-win32-arm64": "2.4.9", + "@biomejs/cli-win32-x64": "2.4.9" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.4.9.tgz", + "integrity": "sha512-d5G8Gf2RpH5pYwiHLPA+UpG3G9TLQu4WM+VK6sfL7K68AmhcEQ9r+nkj/DvR/GYhYox6twsHUtmWWWIKfcfQQA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.4.9.tgz", + "integrity": "sha512-LNCLNgqDMG7BLdc3a8aY/dwKPK7+R8/JXJoXjCvZh2gx8KseqBdFDKbhrr7HCWF8SzNhbTaALhTBoh/I6rf9lA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.4.9.tgz", + "integrity": "sha512-4adnkAUi6K4C/emPRgYznMOcLlUqZdXWM6aIui4VP4LraE764g6Q4YguygnAUoxKjKIXIWPteKMgRbN0wsgwcg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.4.9.tgz", + "integrity": "sha512-8RCww5xnPn2wpK4L/QDGDOW0dq80uVWfppPxHIUg6mOs9B6gRmqPp32h1Ls3T8GnW8Wo5A8u7vpTwz4fExN+sw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.4.9.tgz", + "integrity": "sha512-L10na7POF0Ks/cgLFNF1ZvIe+X4onLkTi5oP9hY+Rh60Q+7fWzKDDCeGyiHUFf1nGIa9dQOOUPGe2MyYg8nMSQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.4.9.tgz", + "integrity": "sha512-5TD+WS9v5vzXKzjetF0hgoaNFHMcpQeBUwKKVi3JbG1e9UCrFuUK3Gt185fyTzvRdwYkJJEMqglRPjmesmVv4A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.4.9.tgz", + "integrity": "sha512-aDZr0RBC3sMGJOU10BvG7eZIlWLK/i51HRIfScE2lVhfts2dQTreowLiJJd+UYg/tHKxS470IbzpuKmd0MiD6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.4.9.tgz", + "integrity": "sha512-NS4g/2G9SoQ4ktKtz31pvyc/rmgzlcIDCGU/zWbmHJAqx6gcRj2gj5Q/guXhoWTzCUaQZDIqiCQXHS7BcGYc0w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, "node_modules/@commander-js/extra-typings": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/@commander-js/extra-typings/-/extra-typings-14.0.0.tgz", diff --git a/package.json b/package.json index 659567b..03583a6 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "zod": "^4.1.12" }, "devDependencies": { + "@biomejs/biome": "2.4.9", "@eslint/js": "^9.36.0", "@types/node": "^24.9.2", "@types/react": "^19.1.16", diff --git a/src/api/generated/api.schemas.ts b/src/api/generated/api.schemas.ts index 2a8d7cb..bccf058 100644 --- a/src/api/generated/api.schemas.ts +++ b/src/api/generated/api.schemas.ts @@ -145,9 +145,9 @@ export interface MangaContentImagesDTO { contentImageKeys: string[]; } -export interface DefaultResponseDTOListMangaImportJobDTO { +export interface DefaultResponseDTOMangaImportJobPageResponseDTO { timestamp?: string; - data?: MangaImportJobDTO[]; + data?: MangaImportJobPageResponseDTO; message?: string; } @@ -175,6 +175,44 @@ export interface MangaImportJobDTO { errorStackTrace?: string; } +export interface MangaImportJobPageResponseDTO { + page?: PageMangaImportJobDTO; + totalJobs?: number; + pendingJobs?: number; + processingJobs?: number; + completedJobs?: number; + failedJobs?: number; +} + +export interface PageMangaImportJobDTO { + totalPages?: number; + totalElements?: number; + size?: number; + content?: MangaImportJobDTO[]; + number?: number; + pageable?: PageableObject; + numberOfElements?: number; + sort?: SortObject; + first?: boolean; + last?: boolean; + empty?: boolean; +} + +export interface PageableObject { + offset?: number; + pageNumber?: number; + pageSize?: number; + paged?: boolean; + unpaged?: boolean; + sort?: SortObject; +} + +export interface SortObject { + empty?: boolean; + sorted?: boolean; + unsorted?: boolean; +} + export interface DefaultResponseDTOPageMangaListDTO { timestamp?: string; data?: PageMangaListDTO; @@ -222,21 +260,6 @@ export interface PageMangaListDTO { empty?: boolean; } -export interface PageableObject { - offset?: number; - pageNumber?: number; - pageSize?: number; - paged?: boolean; - unpaged?: boolean; - sort?: SortObject; -} - -export interface SortObject { - empty?: boolean; - sorted?: boolean; - unsorted?: boolean; -} - export interface DefaultResponseDTOMangaDTO { timestamp?: string; data?: MangaDTO; @@ -337,6 +360,36 @@ export type GetContentProvidersParams = { manualImport?: boolean; }; +export type GetMangaImportJobsParams = { +searchQuery?: string; +status?: GetMangaImportJobsStatus; +/** + * Zero-based page index (0..N) + * @minimum 0 + */ +page?: number; +/** + * The size of the page to be returned + * @minimum 1 + */ +size?: number; +/** + * Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. + */ +sort?: string[]; +}; + +export type GetMangaImportJobsStatus = typeof GetMangaImportJobsStatus[keyof typeof GetMangaImportJobsStatus]; + + +// eslint-disable-next-line @typescript-eslint/no-redeclare +export const GetMangaImportJobsStatus = { + PENDING: 'PENDING', + PROCESSING: 'PROCESSING', + SUCCESS: 'SUCCESS', + FAILED: 'FAILED', +} as const; + export type GetMangasParams = { searchQuery?: string; genreIds?: number[]; diff --git a/src/api/generated/content/content.ts b/src/api/generated/content/content.ts index 284dfa8..7683bb6 100644 --- a/src/api/generated/content/content.ts +++ b/src/api/generated/content/content.ts @@ -25,12 +25,13 @@ import type { import type { DefaultResponseDTOListMangaContentDTO, - DefaultResponseDTOListMangaImportJobDTO, DefaultResponseDTOMangaContentImagesDTO, + DefaultResponseDTOMangaImportJobPageResponseDTO, DefaultResponseDTOPresignedImportResponseDTO, DefaultResponseDTOVoid, DownloadContentArchiveParams, FileImportRequestDTO, + GetMangaImportJobsParams, PresignedImportRequestDTO } from '../api.schemas'; @@ -434,17 +435,18 @@ export function useGetMangaContentImages,signal?: AbortSignal ) => { - return customInstance( - {url: `/content/import/jobs`, method: 'GET', signal + return customInstance( + {url: `/content/import/jobs`, method: 'GET', + params, signal }, options); } @@ -452,23 +454,23 @@ export const getMangaImportJobs = ( -export const getGetMangaImportJobsQueryKey = () => { +export const getGetMangaImportJobsQueryKey = (params?: GetMangaImportJobsParams,) => { return [ - `/content/import/jobs` + `/content/import/jobs`, ...(params ? [params]: []) ] as const; } -export const getGetMangaImportJobsQueryOptions = >, TError = unknown>( options?: { query?:Partial>, TError, TData>>, request?: SecondParameter} +export const getGetMangaImportJobsQueryOptions = >, TError = unknown>(params?: GetMangaImportJobsParams, options?: { query?:Partial>, TError, TData>>, request?: SecondParameter} ) => { const {query: queryOptions, request: requestOptions} = options ?? {}; - const queryKey = queryOptions?.queryKey ?? getGetMangaImportJobsQueryKey(); + const queryKey = queryOptions?.queryKey ?? getGetMangaImportJobsQueryKey(params); - const queryFn: QueryFunction>> = ({ signal }) => getMangaImportJobs(requestOptions, signal); + const queryFn: QueryFunction>> = ({ signal }) => getMangaImportJobs(params, requestOptions, signal); @@ -482,7 +484,7 @@ export type GetMangaImportJobsQueryError = unknown export function useGetMangaImportJobs>, TError = unknown>( - options: { query:Partial>, TError, TData>> & Pick< + params: undefined | GetMangaImportJobsParams, options: { query:Partial>, TError, TData>> & Pick< DefinedInitialDataOptions< Awaited>, TError, @@ -492,7 +494,7 @@ export function useGetMangaImportJobs & { queryKey: DataTag } export function useGetMangaImportJobs>, TError = unknown>( - options?: { query?:Partial>, TError, TData>> & Pick< + params?: GetMangaImportJobsParams, options?: { query?:Partial>, TError, TData>> & Pick< UndefinedInitialDataOptions< Awaited>, TError, @@ -502,19 +504,19 @@ export function useGetMangaImportJobs & { queryKey: DataTag } export function useGetMangaImportJobs>, TError = unknown>( - options?: { query?:Partial>, TError, TData>>, request?: SecondParameter} + params?: GetMangaImportJobsParams, options?: { query?:Partial>, TError, TData>>, request?: SecondParameter} , queryClient?: QueryClient ): UseQueryResult & { queryKey: DataTag } /** - * @summary Get a list of manga import jobs + * @summary Get a paginated list of manga import jobs */ export function useGetMangaImportJobs>, TError = unknown>( - options?: { query?:Partial>, TError, TData>>, request?: SecondParameter} + params?: GetMangaImportJobsParams, options?: { query?:Partial>, TError, TData>>, request?: SecondParameter} , queryClient?: QueryClient ): UseQueryResult & { queryKey: DataTag } { - const queryOptions = getGetMangaImportJobsQueryOptions(options) + const queryOptions = getGetMangaImportJobsQueryOptions(params,options) const query = useQuery(queryOptions, queryClient) as UseQueryResult & { queryKey: DataTag }; diff --git a/src/components/AuthHeader.tsx b/src/components/AuthHeader.tsx index 81a8b77..361cae7 100644 --- a/src/components/AuthHeader.tsx +++ b/src/components/AuthHeader.tsx @@ -1,4 +1,4 @@ -import {LogIn, LogOut, Settings, Shield, User} from "lucide-react"; +import { LogIn, LogOut, Settings, Shield, User } from "lucide-react"; import { Link, useNavigate } from "react-router"; import { Avatar, AvatarFallback } from "@/components/ui/avatar.tsx"; import { Button } from "@/components/ui/button"; diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert.tsx index ecc8029..382ce73 100644 --- a/src/components/ui/alert.tsx +++ b/src/components/ui/alert.tsx @@ -63,4 +63,4 @@ function AlertDescription({ ); } -export { Alert, AlertTitle, AlertDescription }; +export { Alert, AlertDescription, AlertTitle }; diff --git a/src/components/ui/avatar.tsx b/src/components/ui/avatar.tsx index 6812135..e2bf3ad 100644 --- a/src/components/ui/avatar.tsx +++ b/src/components/ui/avatar.tsx @@ -48,4 +48,4 @@ function AvatarFallback({ ); } -export { Avatar, AvatarImage, AvatarFallback }; +export { Avatar, AvatarFallback, AvatarImage }; diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx index fe7ed64..2cb8c5d 100644 --- a/src/components/ui/card.tsx +++ b/src/components/ui/card.tsx @@ -83,10 +83,10 @@ function CardFooter({ className, ...props }: React.ComponentProps<"div">) { export { Card, - CardHeader, - CardFooter, - CardTitle, CardAction, - CardDescription, CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, }; diff --git a/src/components/ui/collapsible.tsx b/src/components/ui/collapsible.tsx index 584cc60..bdd3cdb 100644 --- a/src/components/ui/collapsible.tsx +++ b/src/components/ui/collapsible.tsx @@ -28,4 +28,4 @@ function CollapsibleContent({ ); } -export { Collapsible, CollapsibleTrigger, CollapsibleContent }; +export { Collapsible, CollapsibleContent, CollapsibleTrigger }; diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx index fe60a69..a7a298f 100644 --- a/src/components/ui/dropdown-menu.tsx +++ b/src/components/ui/dropdown-menu.tsx @@ -238,18 +238,18 @@ function DropdownMenuSubContent({ export { DropdownMenu, - DropdownMenuPortal, - DropdownMenuTrigger, + DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, - DropdownMenuLabel, DropdownMenuItem, - DropdownMenuCheckboxItem, + DropdownMenuLabel, + DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, - DropdownMenuSubTrigger, DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuTrigger, }; diff --git a/src/components/ui/form.tsx b/src/components/ui/form.tsx index 22c13ee..197bb1b 100644 --- a/src/components/ui/form.tsx +++ b/src/components/ui/form.tsx @@ -156,12 +156,12 @@ function FormMessage({ className, ...props }: React.ComponentProps<"p">) { } export { - useFormField, Form, - FormItem, - FormLabel, FormControl, FormDescription, - FormMessage, FormField, + FormItem, + FormLabel, + FormMessage, + useFormField, }; diff --git a/src/components/ui/progress.tsx b/src/components/ui/progress.tsx index af2ccf5..93349bb 100644 --- a/src/components/ui/progress.tsx +++ b/src/components/ui/progress.tsx @@ -1,29 +1,29 @@ -import * as React from "react" -import { Progress as ProgressPrimitive } from "radix-ui" +import { Progress as ProgressPrimitive } from "radix-ui"; +import type * as React from "react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; function Progress({ - className, - value, - ...props + className, + value, + ...props }: React.ComponentProps) { - return ( - - - - ) + return ( + + + + ); } -export { Progress } +export { Progress }; diff --git a/src/components/ui/table.tsx b/src/components/ui/table.tsx index a47749b..fdf06b4 100644 --- a/src/components/ui/table.tsx +++ b/src/components/ui/table.tsx @@ -1,114 +1,114 @@ -import * as React from "react" +import type * as React from "react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; function Table({ className, ...props }: React.ComponentProps<"table">) { - return ( -
- - - ) + return ( +
+
+ + ); } function TableHeader({ className, ...props }: React.ComponentProps<"thead">) { - return ( - - ) + return ( + + ); } function TableBody({ className, ...props }: React.ComponentProps<"tbody">) { - return ( - - ) + return ( + + ); } function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) { - return ( - tr]:last:border-b-0", - className - )} - {...props} - /> - ) + return ( + tr]:last:border-b-0", + className, + )} + {...props} + /> + ); } function TableRow({ className, ...props }: React.ComponentProps<"tr">) { - return ( - - ) + return ( + + ); } function TableHead({ className, ...props }: React.ComponentProps<"th">) { - return ( -
[role=checkbox]]:translate-y-[2px]", - className - )} - {...props} - /> - ) + return ( + [role=checkbox]]:translate-y-[2px]", + className, + )} + {...props} + /> + ); } function TableCell({ className, ...props }: React.ComponentProps<"td">) { - return ( - [role=checkbox]]:translate-y-[2px]", - className - )} - {...props} - /> - ) + return ( + [role=checkbox]]:translate-y-[2px]", + className, + )} + {...props} + /> + ); } function TableCaption({ - className, - ...props + className, + ...props }: React.ComponentProps<"caption">) { - return ( -
- ) + return ( + + ); } export { - Table, - TableHeader, - TableBody, - TableFooter, - TableHead, - TableRow, - TableCell, - TableCaption, -} + Table, + TableBody, + TableCaption, + TableCell, + TableFooter, + TableHead, + TableHeader, + TableRow, +}; diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx index be81ad8..17fc4e3 100644 --- a/src/components/ui/tabs.tsx +++ b/src/components/ui/tabs.tsx @@ -61,4 +61,4 @@ function TabsContent({ ); } -export { Tabs, TabsList, TabsTrigger, TabsContent }; +export { Tabs, TabsContent, TabsList, TabsTrigger }; diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx index b0c62e2..7595b8d 100644 --- a/src/contexts/AuthContext.tsx +++ b/src/contexts/AuthContext.tsx @@ -6,8 +6,8 @@ import { useState, } from "react"; import { toast } from "sonner"; -import {useRegisterUser} from "@/api/generated/user/user.ts"; -import {useLogin} from "@/api/generated/authentication/authentication.ts"; +import { useLogin } from "@/api/generated/authentication/authentication.ts"; +import { useRegisterUser } from "@/api/generated/user/user.ts"; export interface UserPreferences { theme: "light" | "dark"; diff --git a/src/contexts/UIStateContext.tsx b/src/contexts/UIStateContext.tsx index 8642a57..6aeca64 100644 --- a/src/contexts/UIStateContext.tsx +++ b/src/contexts/UIStateContext.tsx @@ -1,143 +1,141 @@ import { - type ReactNode, - createContext, - useCallback, - useContext, - useMemo, - useState, + createContext, + type ReactNode, + useCallback, + useContext, + useMemo, + useState, } from "react"; import type { SortOption } from "@/features/home/components/SortDropdown.tsx"; interface UIStateContextType { - /* Home Filter State */ - currentPage: number; - setCurrentPage: (page: number) => void; - selectedGenres: number[]; - setSelectedGenres: (genres: number[]) => void; - selectedStatus: string[]; - setSelectedStatus: (status: string[]) => void; - minRating: number; - setMinRating: (rating: number) => void; - userFavorites: boolean; - setUserFavorites: (favorites: boolean) => void; - showAdultContent: boolean; - setShowAdultContent: (show: boolean) => void; - sortOption: SortOption; - setSortOption: (sort: SortOption) => void; - searchText: string; - setSearchText: (text: string) => void; - resetFilters: () => void; + /* Home Filter State */ + currentPage: number; + setCurrentPage: (page: number) => void; + selectedGenres: number[]; + setSelectedGenres: (genres: number[]) => void; + selectedStatus: string[]; + setSelectedStatus: (status: string[]) => void; + minRating: number; + setMinRating: (rating: number) => void; + userFavorites: boolean; + setUserFavorites: (favorites: boolean) => void; + showAdultContent: boolean; + setShowAdultContent: (show: boolean) => void; + sortOption: SortOption; + setSortOption: (sort: SortOption) => void; + searchText: string; + setSearchText: (text: string) => void; + resetFilters: () => void; - /* Manga Provider Card State */ - expandedProviderIds: number[]; - toggleProviderId: (id: number) => void; + /* Manga Provider Card State */ + expandedProviderIds: number[]; + toggleProviderId: (id: number) => void; - /* Scroll Persistence */ - scrollPositions: Record; - setScrollPosition: (key: string, position: number) => void; + /* Scroll Persistence */ + scrollPositions: Record; + setScrollPosition: (key: string, position: number) => void; } const UIStateContext = createContext(undefined); export const UIStateProvider = ({ children }: { children: ReactNode }) => { - /* Home Filter State */ - const [currentPage, setCurrentPage] = useState(1); - const [selectedGenres, setSelectedGenres] = useState([]); - const [selectedStatus, setSelectedStatus] = useState([]); - const [minRating, setMinRating] = useState(0); - const [userFavorites, setUserFavorites] = useState(false); - const [showAdultContent, setShowAdultContent] = useState(false); - const [sortOption, setSortOption] = useState("title-asc"); - const [searchText, setSearchText] = useState(""); + /* Home Filter State */ + const [currentPage, setCurrentPage] = useState(1); + const [selectedGenres, setSelectedGenres] = useState([]); + const [selectedStatus, setSelectedStatus] = useState([]); + const [minRating, setMinRating] = useState(0); + const [userFavorites, setUserFavorites] = useState(false); + const [showAdultContent, setShowAdultContent] = useState(false); + const [sortOption, setSortOption] = useState("title-asc"); + const [searchText, setSearchText] = useState(""); - /* Manga Provider Card State */ - const [expandedProviderIds, setExpandedProviderIds] = useState([]); + /* Manga Provider Card State */ + const [expandedProviderIds, setExpandedProviderIds] = useState([]); - /* Scroll Persistence */ - const [scrollPositions, setScrollPositions] = useState< - Record - >({}); + /* Scroll Persistence */ + const [scrollPositions, setScrollPositions] = useState< + Record + >({}); + const resetFilters = useCallback(() => { + setCurrentPage(1); + setSelectedGenres([]); + setSelectedStatus([]); + setMinRating(0); + setUserFavorites(false); + setShowAdultContent(false); + setSortOption("title-asc"); + setSearchText(""); + }, []); + const toggleProviderId = useCallback((id: number) => { + setExpandedProviderIds((prev) => { + if (prev.includes(id)) { + return prev.filter((pId) => pId !== id); + } else { + return [...prev, id]; + } + }); + }, []); - const resetFilters = useCallback(() => { - setCurrentPage(1); - setSelectedGenres([]); - setSelectedStatus([]); - setMinRating(0); - setUserFavorites(false); - setShowAdultContent(false); - setSortOption("title-asc"); - setSearchText(""); - }, []); + const setScrollPosition = useCallback((key: string, position: number) => { + setScrollPositions((prev) => ({ + ...prev, + [key]: position, + })); + }, []); - const toggleProviderId = useCallback((id: number) => { - setExpandedProviderIds((prev) => { - if (prev.includes(id)) { - return prev.filter((pId) => pId !== id); - } else { - return [...prev, id]; - } - }); - }, []); + const value = useMemo( + () => ({ + currentPage, + setCurrentPage, + selectedGenres, + setSelectedGenres, + selectedStatus, + setSelectedStatus, + minRating, + setMinRating, + userFavorites, + setUserFavorites, + showAdultContent, + setShowAdultContent, + sortOption, + setSortOption, + searchText, + setSearchText, + resetFilters, + expandedProviderIds, + toggleProviderId, + scrollPositions, + setScrollPosition, + }), + [ + currentPage, + selectedGenres, + selectedStatus, + minRating, + userFavorites, + showAdultContent, + sortOption, + searchText, + resetFilters, + expandedProviderIds, + toggleProviderId, + scrollPositions, + setScrollPosition, + ], + ); - const setScrollPosition = useCallback((key: string, position: number) => { - setScrollPositions((prev) => ({ - ...prev, - [key]: position, - })); - }, []); - - const value = useMemo( - () => ({ - currentPage, - setCurrentPage, - selectedGenres, - setSelectedGenres, - selectedStatus, - setSelectedStatus, - minRating, - setMinRating, - userFavorites, - setUserFavorites, - showAdultContent, - setShowAdultContent, - sortOption, - setSortOption, - searchText, - setSearchText, - resetFilters, - expandedProviderIds, - toggleProviderId, - scrollPositions, - setScrollPosition, - }), - [ - currentPage, - selectedGenres, - selectedStatus, - minRating, - userFavorites, - showAdultContent, - sortOption, - searchText, - resetFilters, - expandedProviderIds, - toggleProviderId, - scrollPositions, - setScrollPosition, - ], - ); - - return ( - {children} - ); + return ( + {children} + ); }; export const useUIState = () => { - const context = useContext(UIStateContext); - if (context === undefined) { - throw new Error("useUIState must be used within a UIStateProvider"); - } - return context; + const context = useContext(UIStateContext); + if (context === undefined) { + throw new Error("useUIState must be used within a UIStateProvider"); + } + return context; }; diff --git a/src/features/admin/components/FailedImportJobs.tsx b/src/features/admin/components/FailedImportJobs.tsx index 56fe305..d7714a3 100644 --- a/src/features/admin/components/FailedImportJobs.tsx +++ b/src/features/admin/components/FailedImportJobs.tsx @@ -1,304 +1,355 @@ -import {useGetMangaImportJobs} from "@/api/generated/content/content.ts"; -import type {MangaImportJobDTO, MangaImportJobDTOStatus} from "@/api/generated/api.schemas.ts"; +import { AlertTriangle, ExternalLink, FileText, Search } from "lucide-react"; +import { useState } from "react"; +import { useDebounce } from "use-debounce"; +import type { + MangaImportJobDTO, + MangaImportJobDTOStatus, +} from "@/api/generated/api.schemas.ts"; +import { useGetMangaImportJobs } from "@/api/generated/content/content.ts"; +import { Pagination } from "@/components/Pagination.tsx"; import { Badge } from "@/components/ui/badge"; -import {AlertTriangle, ExternalLink, FileText, RefreshCw} from "lucide-react"; -import {Card} from "@/components/ui/card.tsx"; -import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@/components/ui/table.tsx"; -import {Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle} from "@/components/ui/dialog.tsx"; -import {useState} from "react"; -import {Button} from "@/components/ui/button.tsx"; +import { Button } from "@/components/ui/button.tsx"; +import { Card } from "@/components/ui/card.tsx"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog.tsx"; +import { Input } from "@/components/ui/input.tsx"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select.tsx"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table.tsx"; export const FailedImportJobs = () => { - const {data} = useGetMangaImportJobs(); - const importJobs = data?.data; + const [searchQueryText, setSearchQueryText] = useState(""); + const [searchQuery] = useDebounce(searchQueryText, 500); + const [statusFilter, setStatusFilter] = useState< + MangaImportJobDTOStatus | undefined + >(undefined); + const [selectedJob, setSelectedJob] = useState( + null, + ); + const [errorDialogOpen, setErrorDialogOpen] = useState(false); + const [currentPage, setCurrentPage] = useState(1); - // const [searchQuery, setSearchQuery] = useState("") - // const [statusFilter, setStatusFilter] = useState("ALL") - const [selectedJob, setSelectedJob] = useState(null) - const [errorDialogOpen, setErrorDialogOpen] = useState(false) + const { data } = useGetMangaImportJobs({ + status: statusFilter, + searchQuery: searchQuery, + page: currentPage - 1, + size: 12, + }); + const importJobsData = data?.data; - // const filteredJobs = jobs.filter((job) => { - // const matchesSearch = - // job.filename.toLowerCase().includes(searchQuery.toLowerCase()) || - // job.id.toLowerCase().includes(searchQuery.toLowerCase()) || - // job.malId?.includes(searchQuery) || - // job.anilistId?.includes(searchQuery) - // const matchesStatus = statusFilter === "ALL" || job.status === statusFilter - // return matchesSearch && matchesStatus - // }) + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }); + }; - const formatDate = (dateString: string) => { - return new Date(dateString).toLocaleString("en-US", { - year: "numeric", - month: "short", - day: "numeric", - hour: "2-digit", - minute: "2-digit", - }) - } + const getStatusBadge = (job: MangaImportJobDTO) => { + const variants: Record< + MangaImportJobDTOStatus, + { + variant: "default" | "secondary" | "destructive" | "outline"; + className: string; + } + > = { + PENDING: { + variant: "secondary", + className: "bg-muted text-muted-foreground", + }, + PROCESSING: { variant: "default", className: "bg-blue-500 text-white" }, + SUCCESS: { variant: "default", className: "bg-green-500 text-white" }, + FAILED: { + variant: "destructive", + className: "cursor-pointer hover:bg-destructive/80", + }, + }; - const getStatusBadge = (job: MangaImportJobDTO) => { - const variants: Record = { - PENDING: { variant: "secondary", className: "bg-muted text-muted-foreground" }, - PROCESSING: { variant: "default", className: "bg-blue-500 text-white" }, - SUCCESS: { variant: "default", className: "bg-green-500 text-white" }, - FAILED: { variant: "destructive", className: "cursor-pointer hover:bg-destructive/80" }, - } + if (!job.status) { + return null; + } - if (!job.status) { - return null; - } + const config = variants[job.status]; - const config = variants[job.status] + if (job.status === "FAILED") { + return ( + { + setSelectedJob(job); + setErrorDialogOpen(true); + }} + > + + {job.status} + + ); + } - if (job.status === "FAILED") { - return ( - { - setSelectedJob(job) - setErrorDialogOpen(true) - }} - > - - {job.status} - - ) - } + return ( + + {job.status} + + ); + }; - return ( - - {job.status === "PROCESSING" && ( - - )} - {job.status} - - ) - } + return ( +
+
+

+ Manual Import Jobs +

+

+ View and manage file import jobs from S3 storage. +

+
- const stats = { - total: importJobs?.length, - pending: importJobs?.filter((j) => j.status === "PENDING").length, - processing: importJobs?.filter((j) => j.status === "PROCESSING").length, - completed: importJobs?.filter((j) => j.status === "SUCCESS").length, - failed: importJobs?.filter((j) => j.status === "FAILED").length, - } + {importJobsData && ( +
+ +

Total Jobs

+

+ {importJobsData.totalJobs} +

+
+ +

Pending

+

+ {importJobsData.pendingJobs} +

+
+ +

Processing

+

+ {importJobsData.processingJobs} +

+
+ +

Completed

+

+ {importJobsData.completedJobs} +

+
+ +

Failed

+

+ {importJobsData.failedJobs} +

+
+
+ )} - return ( -
-
-

- Manual Import Jobs -

-

- View and manage file import jobs from S3 storage. -

-
+
+
+ + setSearchQueryText(e.target.value)} + className="pl-10" + /> +
+ +
- {/* Stats */} -
- -

Total Jobs

-

{stats.total}

-
- -

Pending

-

- {stats.pending} -

-
- -

Processing

-

{stats.processing}

-
- -

Completed

-

{stats.completed}

-
- -

Failed

-

{stats.failed}

-
-
+ + + + + ID + Status + MAL ID + AniList ID + Filename + S3 Key + Created At + Updated At + + + + {!importJobsData?.page?.content || + importJobsData.page?.content.length === 0 ? ( + + +
+ +

No import jobs found

+
+
+
+ ) : ( + importJobsData.page.content.map((job) => ( + + {job.id} + {getStatusBadge(job)} + + {job.malId ? ( + + {job.malId} + + + ) : ( + - + )} + + + {job.aniListId ? ( + + {job.aniListId} + + + ) : ( + - + )} + + + {job.filename} + + + + {job.s3Key} + + + + + {formatDate(job.createdAt ?? "")} + + + {formatDate(job.updatedAt ?? "")} + + + )) + )} +
+
- {/* Filters */} - {/*
*/} - {/*
*/} - {/* */} - {/* setSearchQuery(e.target.value)}*/} - {/* className="pl-10"*/} - {/* />*/} - {/*
*/} - {/* setStatusFilter(value as ImportStatus | "ALL")}*/} - {/* >*/} - {/* */} - {/* */} - {/* */} - {/* */} - {/* All Statuses*/} - {/* Pending*/} - {/* Processing*/} - {/* Completed*/} - {/* Failed*/} - {/* */} - {/* */} - {/*
*/} + {importJobsData?.page?.totalPages && + importJobsData.page.totalPages > 1 && ( + + )} +
- {/* Table */} - - - - - ID - Status - MAL ID - AniList ID - Filename - S3 Key - Created At - Updated At - - - - {!importJobs || importJobs.length === 0 ? ( - - -
- -

No import jobs found

-
-
-
- ) : ( - importJobs.map((job) => ( - - {job.id} - {getStatusBadge(job)} - - {job.malId ? ( - - {job.malId} - - - ) : ( - - - )} - - - {job.aniListId ? ( - - {job.aniListId} - - - ) : ( - - - )} - - - {job.filename} - - - - {job.s3Key} - - - - - {formatDate(job.createdAt ?? "")} - - - {formatDate(job.updatedAt ?? "")} - - - )) - )} -
-
-
- - {/* Error Details Dialog */} - - - - - - Import Error Details - - - Job ID: {selectedJob?.id} | File: {selectedJob?.filename} - - - {selectedJob?.errorMessage && ( -
-
- -

- {selectedJob.errorMessage} -

-
- {selectedJob.errorStackTrace && ( -
- -
-                                        {selectedJob.errorStackTrace}
-                                    
-
- )} -
- - {/* {*/} - {/* // Mock retry action*/} - {/* setJobs((prev) =>*/} - {/* prev.map((j) =>*/} - {/* j.id === selectedJob.id*/} - {/* ? { ...j, status: "PENDING" as ImportStatus, updatedAt: new Date().toISOString() }*/} - {/* : j*/} - {/* )*/} - {/* )*/} - {/* setErrorDialogOpen(false)*/} - {/* }}*/} - {/*>*/} - {/* */} - {/* Retry Import*/} - {/**/} -
-
- )} -
-
-
- ) -}; \ No newline at end of file + + + + + + Import Error Details + + + Job ID: {selectedJob?.id} | File: {selectedJob?.filename} + + + {selectedJob?.errorMessage && ( +
+
+ +

+ {selectedJob.errorMessage} +

+
+ {selectedJob.errorStackTrace && ( +
+ +
+										{selectedJob.errorStackTrace}
+									
+
+ )} +
+ + {/* {*/} + {/* // Mock retry action*/} + {/* setJobs((prev) =>*/} + {/* prev.map((j) =>*/} + {/* j.id === selectedJob.id*/} + {/* ? { ...j, status: "PENDING" as ImportStatus, updatedAt: new Date().toISOString() }*/} + {/* : j*/} + {/* )*/} + {/* )*/} + {/* setErrorDialogOpen(false)*/} + {/* }}*/} + {/*>*/} + {/* */} + {/* Retry Import*/} + {/**/} +
+
+ )} +
+
+
+ ); +}; diff --git a/src/features/admin/components/ProviderManager.tsx b/src/features/admin/components/ProviderManager.tsx index 2ca5dcd..19fed53 100644 --- a/src/features/admin/components/ProviderManager.tsx +++ b/src/features/admin/components/ProviderManager.tsx @@ -1,123 +1,134 @@ +import { ExternalLink, Loader2, RefreshCw } from "lucide-react"; import { - useFetchAllContentProviderMangas, - useFetchContentProviderMangas, - useGetContentProviders + useFetchAllContentProviderMangas, + useFetchContentProviderMangas, + useGetContentProviders, } from "@/api/generated/ingestion/ingestion.ts"; -import {Card} from "@/components/ui/card.tsx"; -import {Badge} from "@/components/ui/badge.tsx"; -import {ExternalLink, Loader2, RefreshCw} from "lucide-react"; -import {Button} from "@/components/ui/button.tsx"; +import { Badge } from "@/components/ui/badge.tsx"; +import { Button } from "@/components/ui/button.tsx"; +import { Card } from "@/components/ui/card.tsx"; export const ProviderManager = () => { - const { data } = useGetContentProviders(); - const providers = data?.data?.providers; + const { data } = useGetContentProviders(); + const providers = data?.data?.providers; - const { mutate: mutateFetchContentProviderMangas, isPending: isPendingFetchContentProviderMangas } = useFetchContentProviderMangas(); + const { + mutate: mutateFetchContentProviderMangas, + isPending: isPendingFetchContentProviderMangas, + } = useFetchContentProviderMangas(); - const { mutate: mutateFetchAllContentProviderMangas, isPending: isPendingFetchAllContentProviderMangas } = useFetchAllContentProviderMangas(); + const { + mutate: mutateFetchAllContentProviderMangas, + isPending: isPendingFetchAllContentProviderMangas, + } = useFetchAllContentProviderMangas(); - const activeCount = providers?.filter((p) => p.active).length + const activeCount = providers?.filter((p) => p.active).length; - return ( -
-
-
-

- Manga Providers -

-

- {activeCount} of {providers?.length} providers active -

-
- -
+ return ( +
+
+
+

+ Manga Providers +

+

+ {activeCount} of {providers?.length} providers active +

+
+ +
-
- {providers?.map((provider) => { - return ( - -
- {/* Active Toggle */} - {/* handleToggleActive(provider.id)}*/} - {/* disabled={isUpdating}*/} - {/*/>*/} +
+ {providers?.map((provider) => { + return ( + +
+ {/* Active Toggle */} + {/* handleToggleActive(provider.id)}*/} + {/* disabled={isUpdating}*/} + {/*/>*/} -
-
-

- {provider.name} -

- - {provider?.active ? "active" : "inactive"} - -
+
+
+

+ {provider.name} +

+ + {provider?.active ? "active" : "inactive"} + +
- {provider?.url && - (
-
- - {provider.url} - - - - -
-
)} + {provider?.url && ( +
+
+ + {provider.url} + + + + +
+
+ )} -

- {/*{provider.mangaCount} manga indexed | Last updated:{" "}*/} - {/*{new Date(provider.lastUpdated).toLocaleString()}*/} -

-
+

+ {/*{provider.mangaCount} manga indexed | Last updated:{" "}*/} + {/*{new Date(provider.lastUpdated).toLocaleString()}*/} +

+
- -
-
- ) - })} -
-
- ) -} \ No newline at end of file + +
+ + ); + })} +
+
+ ); +}; diff --git a/src/features/home/components/FilterSidebar.tsx b/src/features/home/components/FilterSidebar.tsx index 0387dd9..0fb0aad 100644 --- a/src/features/home/components/FilterSidebar.tsx +++ b/src/features/home/components/FilterSidebar.tsx @@ -1,11 +1,11 @@ import { Star, X } from "lucide-react"; +import { useGetGenres } from "@/api/generated/catalog/catalog.ts"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Separator } from "@/components/ui/separator.tsx"; import { Skeleton } from "@/components/ui/skeleton.tsx"; import { Switch } from "@/components/ui/switch.tsx"; import { useAuth } from "@/contexts/AuthContext.tsx"; -import {useGetGenres} from "@/api/generated/catalog/catalog.ts"; interface FilterSidebarProps { selectedGenres: number[]; diff --git a/src/features/home/components/MangaCard.tsx b/src/features/home/components/MangaCard.tsx index bb4ecd8..11747c1 100644 --- a/src/features/home/components/MangaCard.tsx +++ b/src/features/home/components/MangaCard.tsx @@ -6,12 +6,15 @@ import type { MangaListDTO, PageMangaListDTO, } from "@/api/generated/api.schemas.ts"; +import { + useSetFavorite, + useSetUnfavorite, +} from "@/api/generated/user-interaction/user-interaction.ts"; import { Badge } from "@/components/ui/badge.tsx"; import { Button } from "@/components/ui/button.tsx"; import { Card, CardContent } from "@/components/ui/card.tsx"; import { useAuth } from "@/contexts/AuthContext.tsx"; import { formatToTwoDigitsDateRange } from "@/utils/dateFormatter.ts"; -import {useSetFavorite, useSetUnfavorite} from "@/api/generated/user-interaction/user-interaction.ts"; interface MangaCardProps { manga: MangaListDTO; @@ -63,8 +66,7 @@ export const MangaCard = ({ manga, queryKey }: MangaCardProps) => { (isFavorite: boolean) => isFavorite ? mutateUnfavorite({ mangaId: manga.id }) - : mutateFavorite({ mangaId: manga.id }) - , + : mutateFavorite({ mangaId: manga.id }), [mutateUnfavorite, manga.id, mutateFavorite], ); diff --git a/src/features/home/components/MangaManualImportDialog.tsx b/src/features/home/components/MangaManualImportDialog.tsx index 765baf7..65e8e7f 100644 --- a/src/features/home/components/MangaManualImportDialog.tsx +++ b/src/features/home/components/MangaManualImportDialog.tsx @@ -1,7 +1,9 @@ +import axios from "axios"; import { FileUp } from "lucide-react"; import type React from "react"; import { useState } from "react"; import { toast } from "sonner"; +import { useRequestPresignedImport } from "@/api/generated/content/content.ts"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -13,8 +15,6 @@ import { } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Progress } from "@/components/ui/progress"; -import axios from "axios"; -import { useRequestPresignedImport } from "@/api/generated/content/content.ts"; interface MangaManualImportDialogProps { fileImportDialogOpen: boolean; @@ -30,7 +30,9 @@ export const MangaManualImportDialog = ({ const [dragActive, setDragActive] = useState(false); const [files, setFiles] = useState(null); const [isUploading, setIsUploading] = useState(false); - const [uploadProgress, setUploadProgress] = useState>({}); + const [uploadProgress, setUploadProgress] = useState>( + {}, + ); const { mutateAsync: requestPresignedUrl } = useRequestPresignedImport(); @@ -140,9 +142,7 @@ export const MangaManualImportDialog = ({
- +
- + (
- + {file.name} {uploadProgress[file.name] || 0}% diff --git a/src/features/home/components/ProviderImportDialog.tsx b/src/features/home/components/ProviderImportDialog.tsx index b2b4190..81749ca 100644 --- a/src/features/home/components/ProviderImportDialog.tsx +++ b/src/features/home/components/ProviderImportDialog.tsx @@ -3,6 +3,7 @@ import { useCallback } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; +import { useGetContentProviders } from "@/api/generated/ingestion/ingestion.ts"; import { useImportFromProvider } from "@/api/generated/manga-import/manga-import.ts"; import { Button } from "@/components/ui/button"; import { @@ -30,7 +31,6 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select.tsx"; -import {useGetContentProviders} from "@/api/generated/ingestion/ingestion.ts"; interface ProviderImportDialogProps { dialogOpen: boolean; diff --git a/src/features/import-review/ImportReviewCard.tsx b/src/features/import-review/ImportReviewCard.tsx index f6911c9..3e95575 100644 --- a/src/features/import-review/ImportReviewCard.tsx +++ b/src/features/import-review/ImportReviewCard.tsx @@ -2,14 +2,14 @@ import { useQueryClient } from "@tanstack/react-query"; import { ExternalLink, Trash2 } from "lucide-react"; import { useState } from "react"; import { toast } from "sonner"; +import type { MangaIngestReviewDTO } from "@/api/generated/api.schemas.ts"; +import { + useDeleteMangaIngestReview, + useResolveMangaIngestReview, +} from "@/api/generated/manga-ingest-review/manga-ingest-review.ts"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; -import type {MangaIngestReviewDTO} from "@/api/generated/api.schemas.ts"; -import { - useDeleteMangaIngestReview, - useResolveMangaIngestReview -} from "@/api/generated/manga-ingest-review/manga-ingest-review.ts"; interface ImportReviewCardProps { importReview: MangaIngestReviewDTO; @@ -73,8 +73,10 @@ export function ImportReviewCard({

Provider:{" "} - {importReview.contentProviderName} •{" "} - {importDate} + + {importReview.contentProviderName} + {" "} + • {importDate}

{importReview.externalUrl && ( diff --git a/src/features/manga/MangaChapter.tsx b/src/features/manga/MangaChapter.tsx index 11893d1..780aae0 100644 --- a/src/features/manga/MangaChapter.tsx +++ b/src/features/manga/MangaChapter.tsx @@ -1,11 +1,14 @@ import { useQueryClient } from "@tanstack/react-query"; import { Check, Database, Download, Eye, Loader2 } from "lucide-react"; import { useCallback, useState } from "react"; -import { useNavigate } from "react-router"; -import { Button } from "@/components/ui/button"; import ReactCountryFlag from "react-country-flag"; -import {useDownloadContentArchive, useGetMangaProviderContent} from "@/api/generated/content/content.ts"; -import {useFetchContentProviderContent} from "@/api/generated/ingestion/ingestion.ts"; +import { useNavigate } from "react-router"; +import { + useDownloadContentArchive, + useGetMangaProviderContent, +} from "@/api/generated/content/content.ts"; +import { useFetchContentProviderContent } from "@/api/generated/ingestion/ingestion.ts"; +import { Button } from "@/components/ui/button"; interface MangaChapterProps { mangaId: number; @@ -18,7 +21,8 @@ export const MangaChapter = ({ }: MangaChapterProps) => { const navigate = useNavigate(); - const { isPending, data, queryKey } = useGetMangaProviderContent(mangaProviderId); + const { isPending, data, queryKey } = + useGetMangaProviderContent(mangaProviderId); const queryClient = useQueryClient(); @@ -28,7 +32,7 @@ export const MangaChapter = ({ const url = window.URL.createObjectURL(data); const link = document.createElement("a"); link.href = url; - link.setAttribute("download", mangaContentId + ".cbz"); + link.setAttribute("download", `${mangaContentId}.cbz`); document.body.appendChild(link); link.click(); link.remove(); @@ -37,12 +41,13 @@ export const MangaChapter = ({ }, }); - const { mutate, isPending: isPendingFetchChapter } = useFetchContentProviderContent({ - mutation: { - onSuccess: () => queryClient.invalidateQueries({ queryKey }), - onSettled: () => setFetchingId(null), - }, - }); + const { mutate, isPending: isPendingFetchChapter } = + useFetchContentProviderContent({ + mutation: { + onSuccess: () => queryClient.invalidateQueries({ queryKey }), + onSettled: () => setFetchingId(null), + }, + }); const [fetchingId, setFetchingId] = useState(null); @@ -77,8 +82,9 @@ export const MangaChapter = ({ >
{chapter.isRead ? ( @@ -145,7 +151,7 @@ export const MangaChapter = ({ className="gap-2 cursor-pointer" > - {isPendingFetchChapter && fetchingId == chapter.id + {isPendingFetchChapter && fetchingId === chapter.id ? "Fetching..." : "Fetch from Provider"} diff --git a/src/hooks/useDynamicPageSize.ts b/src/hooks/useDynamicPageSize.ts index a698faf..49f5690 100644 --- a/src/hooks/useDynamicPageSize.ts +++ b/src/hooks/useDynamicPageSize.ts @@ -1,20 +1,20 @@ import useWindowDimensions from "@/hooks/useWindowDimensions.ts"; export const useDynamicPageSize = (rows = 4) => { - const { width } = useWindowDimensions(); + const { width } = useWindowDimensions(); - if (width >= 1280) { - // xl: 5 columns - return 5 * rows; - } - if (width >= 1024) { - // lg: 4 columns - return 4 * rows; - } - if (width >= 768) { - // md: 3 columns - return 3 * rows; - } - // default: 2 columns - return 2 * rows; + if (width >= 1280) { + // xl: 5 columns + return 5 * rows; + } + if (width >= 1024) { + // lg: 4 columns + return 4 * rows; + } + if (width >= 768) { + // md: 3 columns + return 3 * rows; + } + // default: 2 columns + return 2 * rows; }; diff --git a/src/hooks/useScrollPersistence.ts b/src/hooks/useScrollPersistence.ts index fbde66f..9033cac 100644 --- a/src/hooks/useScrollPersistence.ts +++ b/src/hooks/useScrollPersistence.ts @@ -2,18 +2,18 @@ import { useLayoutEffect } from "react"; import { useUIState } from "@/contexts/UIStateContext.tsx"; export const useScrollPersistence = (key: string) => { - const { scrollPositions, setScrollPosition } = useUIState(); + const { scrollPositions, setScrollPosition } = useUIState(); - useLayoutEffect(() => { - // Restore scroll position - const savedPosition = scrollPositions[key]; - if (savedPosition !== undefined) { - window.scrollTo(0, savedPosition); - } + useLayoutEffect(() => { + // Restore scroll position + const savedPosition = scrollPositions[key]; + if (savedPosition !== undefined) { + window.scrollTo(0, savedPosition); + } - // Save scroll position on unmount or before key changes - return () => { - setScrollPosition(key, window.scrollY); - }; - }, [key, setScrollPosition]); // eslint-disable-next-line react-hooks/exhaustive-deps + // Save scroll position on unmount or before key changes + return () => { + setScrollPosition(key, window.scrollY); + }; + }, [key, setScrollPosition, scrollPositions]); // eslint-disable-next-line react-hooks/exhaustive-deps }; diff --git a/src/hooks/useWindowDimensions.ts b/src/hooks/useWindowDimensions.ts index e80b418..507b70e 100644 --- a/src/hooks/useWindowDimensions.ts +++ b/src/hooks/useWindowDimensions.ts @@ -1,26 +1,26 @@ import { useEffect, useState } from "react"; function getWindowDimensions() { - const { innerWidth: width, innerHeight: height } = window; - return { - width, - height, - }; + const { innerWidth: width, innerHeight: height } = window; + return { + width, + height, + }; } export default function useWindowDimensions() { - const [windowDimensions, setWindowDimensions] = useState( - getWindowDimensions(), - ); + const [windowDimensions, setWindowDimensions] = useState( + getWindowDimensions(), + ); - useEffect(() => { - function handleResize() { - setWindowDimensions(getWindowDimensions()); - } + useEffect(() => { + function handleResize() { + setWindowDimensions(getWindowDimensions()); + } - window.addEventListener("resize", handleResize); - return () => window.removeEventListener("resize", handleResize); - }, []); + window.addEventListener("resize", handleResize); + return () => window.removeEventListener("resize", handleResize); + }, []); - return windowDimensions; + return windowDimensions; } diff --git a/src/pages/Admin.tsx b/src/pages/Admin.tsx index a36e98d..ed78971 100644 --- a/src/pages/Admin.tsx +++ b/src/pages/Admin.tsx @@ -1,241 +1,247 @@ -import {useAuth} from "@/contexts/AuthContext.tsx"; -import {type ReactNode, useEffect, useState} from "react"; -import {useNavigate} from "react-router"; -import {ArrowLeft, FileStack, Server, Shield} from "lucide-react"; -import {Button} from "@/components/ui/button.tsx"; -import {ProviderManager} from "@/features/admin/components/ProviderManager.tsx"; -import {FailedImportJobs} from "@/features/admin/components/FailedImportJobs.tsx"; +import { ArrowLeft, FileStack, Server, Shield } from "lucide-react"; +import { type ReactNode, useEffect, useState } from "react"; +import { useNavigate } from "react-router"; +import { Button } from "@/components/ui/button.tsx"; +import { useAuth } from "@/contexts/AuthContext.tsx"; +import { FailedImportJobs } from "@/features/admin/components/FailedImportJobs.tsx"; +import { ProviderManager } from "@/features/admin/components/ProviderManager.tsx"; -type Tab = "import" | "providers" | "manga" | "ingest-review" | "import-jobs" | "users" +type Tab = + | "import" + | "providers" + | "manga" + | "ingest-review" + | "import-jobs" + | "users"; const Admin = () => { - const { isLoading, isAuthenticated } = useAuth(); - const navigate = useNavigate(); - const [activeTab, setActiveTab] = useState("providers") + const { isLoading, isAuthenticated } = useAuth(); + const navigate = useNavigate(); + const [activeTab, setActiveTab] = useState("providers"); - useEffect(() => { - // TODO: fix this - if (!isLoading && !isAuthenticated) { - // navigate("/login"); - // return; - } + useEffect(() => { + // TODO: fix this + if (!isLoading && !isAuthenticated) { + // navigate("/login"); + // return; + } - // TODO: add user role verification - }, [isAuthenticated, isLoading, navigate]); + // TODO: add user role verification + }, [isAuthenticated, isLoading]); - // const { data } = useGetMangaImportJobs(); - // - // const failedImports = data?.data?.filter(dto => dto.status === MangaImportJobDTOStatus.FAILED)?.length; + // const { data } = useGetMangaImportJobs(); + // + // const failedImports = data?.data?.filter(dto => dto.status === MangaImportJobDTOStatus.FAILED)?.length; - const tabs: { id: Tab; label: string; icon: ReactNode; badge?: number }[] = [ - { - id: "providers", - label: "Providers", - icon: , - }, - // { - // id: "manga", - // label: "Manga Library", - // icon: , - // }, - // { - // id: "import", - // label: "Import", - // icon: , - // }, - // { - // id: "ingest-review", - // label: "Ingest Review", - // icon: , - // badge: failedImports.length > 0 ? failedImports.length : undefined, - // }, - { - id: "import-jobs", - label: "Manual Import Jobs", - icon: , - }, - // { - // id: "users", - // label: "User Management", - // icon: , - // }, - ] + const tabs: { id: Tab; label: string; icon: ReactNode; badge?: number }[] = [ + { + id: "providers", + label: "Providers", + icon: , + }, + // { + // id: "manga", + // label: "Manga Library", + // icon: , + // }, + // { + // id: "import", + // label: "Import", + // icon: , + // }, + // { + // id: "ingest-review", + // label: "Ingest Review", + // icon: , + // badge: failedImports.length > 0 ? failedImports.length : undefined, + // }, + { + id: "import-jobs", + label: "Manual Import Jobs", + icon: , + }, + // { + // id: "users", + // label: "User Management", + // icon: , + // }, + ]; - return ( -
-
- {/* Sidebar */} - - {/* Main Content */} -
- {activeTab === "providers" && } + {/* Main Content */} +
+ {activeTab === "providers" && } - {/*{activeTab === "manga" && }*/} + {/*{activeTab === "manga" && }*/} - {/*{activeTab === "import" && (*/} - {/*
*/} - {/*
*/} - {/*

*/} - {/* Import Manga*/} - {/*

*/} - {/*

*/} - {/* Import manga from external providers or upload files directly.*/} - {/*

*/} - {/*
*/} + {/*{activeTab === "import" && (*/} + {/*
*/} + {/*
*/} + {/*

*/} + {/* Import Manga*/} + {/*

*/} + {/*

*/} + {/* Import manga from external providers or upload files directly.*/} + {/*

*/} + {/*
*/} - {/*
*/} - {/* /!* Import from Provider *!/*/} - {/* */} - {/*
*/} - {/*
*/} - {/* */} - {/*
*/} - {/*
*/} - {/*

*/} - {/* Import from Provider*/} - {/*

*/} - {/*

*/} - {/* Import manga from MangaDex, MangaPlus, Bato.to, or*/} - {/* other supported providers.*/} - {/*

*/} - {/*
*/} - {/* */} - {/*
*/} - {/*
*/} + {/*
*/} + {/* /!* Import from Provider *!/*/} + {/* */} + {/*
*/} + {/*
*/} + {/* */} + {/*
*/} + {/*
*/} + {/*

*/} + {/* Import from Provider*/} + {/*

*/} + {/*

*/} + {/* Import manga from MangaDex, MangaPlus, Bato.to, or*/} + {/* other supported providers.*/} + {/*

*/} + {/*
*/} + {/* */} + {/*
*/} + {/*
*/} - {/* /!* Import from File *!/*/} - {/* */} - {/*
*/} - {/*
*/} - {/* */} - {/*
*/} - {/*
*/} - {/*

*/} - {/* Bulk File Upload*/} - {/*

*/} - {/*

*/} - {/* Upload JSON, CSV, or text files to import manga data in*/} - {/* bulk.*/} - {/*

*/} - {/*
*/} - {/* {*/} - {/* const importBtn = document.querySelector(*/} - {/* '[data-import-dropdown]'*/} - {/* ) as HTMLButtonElement*/} - {/* if (importBtn) importBtn.click()*/} - {/* }}*/} - {/* >*/} - {/* */} - {/* Upload Files*/} - {/* */} - {/*
*/} - {/*
*/} - {/*
*/} - {/*
*/} - {/*)}*/} + {/* /!* Import from File *!/*/} + {/* */} + {/*
*/} + {/*
*/} + {/* */} + {/*
*/} + {/*
*/} + {/*

*/} + {/* Bulk File Upload*/} + {/*

*/} + {/*

*/} + {/* Upload JSON, CSV, or text files to import manga data in*/} + {/* bulk.*/} + {/*

*/} + {/*
*/} + {/* {*/} + {/* const importBtn = document.querySelector(*/} + {/* '[data-import-dropdown]'*/} + {/* ) as HTMLButtonElement*/} + {/* if (importBtn) importBtn.click()*/} + {/* }}*/} + {/* >*/} + {/* */} + {/* Upload Files*/} + {/* */} + {/*
*/} + {/*
*/} + {/*
*/} + {/*
*/} + {/*)}*/} - {/*{activeTab === "ingest-review" && (*/} - {/*
*/} - {/*
*/} - {/*

*/} - {/* Ingest Review*/} - {/*

*/} - {/*

*/} - {/* Review and resolve imports that need manual matching.*/} - {/*

*/} - {/*
*/} + {/*{activeTab === "ingest-review" && (*/} + {/*
*/} + {/*
*/} + {/*

*/} + {/* Ingest Review*/} + {/*

*/} + {/*

*/} + {/* Review and resolve imports that need manual matching.*/} + {/*

*/} + {/*
*/} - {/* {failedImports.length === 0 ? (*/} - {/* */} - {/* */} - {/*

*/} - {/* No Pending Reviews*/} - {/*

*/} - {/*

*/} - {/* All imports have been processed successfully.*/} - {/*

*/} - {/*
*/} - {/* ) : (*/} - {/*
*/} - {/*

*/} - {/* {failedImports.length} import*/} - {/* {failedImports.length !== 1 ? "s" : ""} to review*/} - {/*

*/} - {/* {failedImports.map((fi) => (*/} - {/* */} - {/* ))}*/} - {/*
*/} - {/* )}*/} - {/*
*/} - {/*)}*/} + {/* {failedImports.length === 0 ? (*/} + {/* */} + {/* */} + {/*

*/} + {/* No Pending Reviews*/} + {/*

*/} + {/*

*/} + {/* All imports have been processed successfully.*/} + {/*

*/} + {/*
*/} + {/* ) : (*/} + {/*
*/} + {/*

*/} + {/* {failedImports.length} import*/} + {/* {failedImports.length !== 1 ? "s" : ""} to review*/} + {/*

*/} + {/* {failedImports.map((fi) => (*/} + {/* */} + {/* ))}*/} + {/*
*/} + {/* )}*/} + {/*
*/} + {/*)}*/} - {activeTab === "import-jobs" && } + {activeTab === "import-jobs" && } - {/*{activeTab === "users" && }*/} -
-
-
- ) -} + {/*{activeTab === "users" && }*/} +
+
+ + ); +}; -export default Admin; \ No newline at end of file +export default Admin; diff --git a/src/pages/Chapter.tsx b/src/pages/Chapter.tsx index ec9e285..4f4bd2b 100644 --- a/src/pages/Chapter.tsx +++ b/src/pages/Chapter.tsx @@ -1,11 +1,11 @@ import { ArrowLeft, ChevronLeft, ChevronRight, Home } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import { useNavigate, useParams } from "react-router"; +import { useGetMangaContentImages } from "@/api/generated/content/content.ts"; +import { useMarkContentAsRead } from "@/api/generated/user-interaction/user-interaction.ts"; import { ThemeToggle } from "@/components/ThemeToggle.tsx"; import { Button } from "@/components/ui/button"; import { useReadingTracker } from "@/features/chapter/hooks/useReadingTracker.ts"; -import {useGetMangaContentImages} from "@/api/generated/content/content.ts"; -import {useMarkContentAsRead} from "@/api/generated/user-interaction/user-interaction.ts"; const Chapter = () => { const { setCurrentChapterPage, getCurrentChapterPage } = useReadingTracker(); @@ -35,12 +35,12 @@ const Chapter = () => { if (currentPage === data.data?.contentImageKeys.length) { mutate({ mangaContentId: chapterNumber }); } - }, [data, mutate, currentPage]); + }, [data, mutate, currentPage, chapterNumber, isLoading]); /** Persist reading progress */ useEffect(() => { setCurrentChapterPage(chapterNumber, currentPage); - }, [chapterNumber, currentPage]); + }, [chapterNumber, currentPage, setCurrentChapterPage]); /** Restore stored page */ useEffect(() => { @@ -51,7 +51,7 @@ const Chapter = () => { setVisibleCount(stored); // for infinite scroll } } - }, [isLoading, data?.data]); + }, [isLoading, data?.data, chapterNumber, getCurrentChapterPage]); /** Infinite scroll observer */ useEffect(() => { @@ -90,7 +90,7 @@ const Chapter = () => { imgs.forEach((img) => observer.observe(img)); return () => observer.disconnect(); - }, [infiniteScroll, visibleCount]); + }, [infiniteScroll]); useEffect(() => { if (!data?.data) return; @@ -107,7 +107,7 @@ const Chapter = () => { // Single page mode → scroll to top window.scrollTo({ top: 0 }); } - }, [infiniteScroll]); + }, [infiniteScroll, currentPage, data?.data]); if (!data?.data) { return ( diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 0433ddf..1a4c10f 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -1,23 +1,20 @@ import { BookOpen, Search } from "lucide-react"; import { useEffect, useRef } from "react"; import { useDebounce } from "use-debounce"; +import { useGetMangas } from "@/api/generated/catalog/catalog.ts"; import { AuthHeader } from "@/components/AuthHeader.tsx"; import { Pagination } from "@/components/Pagination.tsx"; import { ThemeToggle } from "@/components/ThemeToggle.tsx"; import { Input } from "@/components/ui/input.tsx"; +import { useUIState } from "@/contexts/UIStateContext.tsx"; import { FilterSidebar } from "@/features/home/components/FilterSidebar.tsx"; import { ImportDropdown } from "@/features/home/components/ImportDropdown.tsx"; import { MangaGrid } from "@/features/home/components/MangaGrid.tsx"; -import { - SortDropdown, -} from "@/features/home/components/SortDropdown.tsx"; -import { useUIState } from "@/contexts/UIStateContext.tsx"; +import { SortDropdown } from "@/features/home/components/SortDropdown.tsx"; import { useDynamicPageSize } from "@/hooks/useDynamicPageSize.ts"; -import {useGetMangas} from "@/api/generated/catalog/catalog.ts"; const ROWS_PER_PAGE = 4; - const Home = () => { const itemsPerPage = useDynamicPageSize(ROWS_PER_PAGE); const { @@ -60,7 +57,7 @@ const Home = () => { setCurrentPage(1); startSearchRef.current = debouncedSearchText; } - }, [debouncedSearchText]); + }, [debouncedSearchText, setCurrentPage]); const totalPages = mangasData?.data?.totalPages; diff --git a/src/pages/ImportReview.tsx b/src/pages/ImportReview.tsx index 6e97dc0..8e92738 100644 --- a/src/pages/ImportReview.tsx +++ b/src/pages/ImportReview.tsx @@ -3,10 +3,10 @@ import { AlertCircle } from "lucide-react"; import { useEffect } from "react"; import { useNavigate } from "react-router"; +import { useGetMangaIngestReviews } from "@/api/generated/manga-ingest-review/manga-ingest-review.ts"; import { Card } from "@/components/ui/card"; import { useAuth } from "@/contexts/AuthContext.tsx"; import { ImportReviewCard } from "@/features/import-review/ImportReviewCard.tsx"; -import {useGetMangaIngestReviews} from "@/api/generated/manga-ingest-review/manga-ingest-review.ts"; export default function ImportReviewPage() { const navigate = useNavigate(); diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index 8aa6ba0..1fb60dc 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -44,7 +44,7 @@ const Login = () => { async (values: z.infer) => { await login(values.email, values.password); }, - [formSchema, login], + [login], ); return ( diff --git a/src/pages/Manga.tsx b/src/pages/Manga.tsx index 4a56077..d94a315 100644 --- a/src/pages/Manga.tsx +++ b/src/pages/Manga.tsx @@ -13,6 +13,14 @@ import { import { useCallback } from "react"; import { useNavigate, useParams } from "react-router"; import { toast } from "sonner"; +import { useGetManga } from "@/api/generated/catalog/catalog.ts"; +import { useFetchContentProviderContentList } from "@/api/generated/ingestion/ingestion.ts"; +import { + useFollowManga, + useSetFavorite, + useSetUnfavorite, + useUnfollowManga, +} from "@/api/generated/user-interaction/user-interaction.ts"; import { ThemeToggle } from "@/components/ThemeToggle.tsx"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -24,17 +32,9 @@ import { } from "@/components/ui/collapsible.tsx"; import { useAuth } from "@/contexts/AuthContext.tsx"; import { useUIState } from "@/contexts/UIStateContext.tsx"; -import { useScrollPersistence } from "@/hooks/useScrollPersistence.ts"; import { MangaChapter } from "@/features/manga/MangaChapter.tsx"; +import { useScrollPersistence } from "@/hooks/useScrollPersistence.ts"; import { formatToTwoDigitsDateRange } from "@/utils/dateFormatter.ts"; -import {useGetManga} from "@/api/generated/catalog/catalog.ts"; -import {useFetchContentProviderContentList} from "@/api/generated/ingestion/ingestion.ts"; -import { - useFollowManga, - useSetFavorite, - useSetUnfavorite, - useUnfollowManga -} from "@/api/generated/user-interaction/user-interaction.ts"; const Manga = () => { const { isAuthenticated } = useAuth(); @@ -46,11 +46,12 @@ const Manga = () => { const { data: mangaData, queryKey } = useGetManga(mangaId); - const { mutate, isPending: fetchPending } = useFetchContentProviderContentList({ - mutation: { - onSuccess: () => queryClient.invalidateQueries({ queryKey }), - }, - }); + const { mutate, isPending: fetchPending } = + useFetchContentProviderContentList({ + mutation: { + onSuccess: () => queryClient.invalidateQueries({ queryKey }), + }, + }); // const { mutate: fetchAllMutate, isPending: fetchAllPending } = // useFetchAllChapters({ @@ -173,8 +174,8 @@ const Manga = () => { src={ (mangaData.data?.coverImageKey && import.meta.env.VITE_OMV_BASE_URL + - "/" + - mangaData.data?.coverImageKey) || + "/" + + mangaData.data?.coverImageKey) || "/placeholder.svg" } alt={mangaData.data?.title ?? ""} @@ -226,10 +227,11 @@ const Manga = () => { disabled={isPendingFavoriteChange} > @@ -283,7 +285,7 @@ const Manga = () => {

Chapters

{mangaData.data?.chapterCount && - mangaData.data?.chapterCount > 0 + mangaData.data?.chapterCount > 0 ? mangaData.data?.chapterCount : "-"}

@@ -364,41 +366,45 @@ const Manga = () => {

- {provider.supportsChapterFetch && - provider.active && ( -
- {/**/} - {/* fetchAllMutate({*/} - {/* mangaProviderId: provider.id?? -1,*/} - {/* })*/} - {/* }*/} - {/* className="gap-2"*/} - {/*>*/} - {/* */} - {/* Fetch all from Provider*/} - {/**/} - -
- )} + {provider.supportsChapterFetch && provider.active && ( +
+ {/**/} + {/* fetchAllMutate({*/} + {/* mangaProviderId: provider.id?? -1,*/} + {/* })*/} + {/* }*/} + {/* className="gap-2"*/} + {/*>*/} + {/* */} + {/* Fetch all from Provider*/} + {/**/} + +
+ )}
diff --git a/src/pages/Profile.tsx b/src/pages/Profile.tsx index d1fa75a..e4fafaa 100644 --- a/src/pages/Profile.tsx +++ b/src/pages/Profile.tsx @@ -118,7 +118,7 @@ const Profile = () => { step="6" value={itemsPerPage} onChange={(e) => - setItemsPerPage(Number.parseInt(e.target.value)) + setItemsPerPage(Number.parseInt(e.target.value, 10)) } />

diff --git a/src/pages/Register.tsx b/src/pages/Register.tsx index 93b082e..4bea743 100644 --- a/src/pages/Register.tsx +++ b/src/pages/Register.tsx @@ -55,7 +55,7 @@ const Register = () => { async (data: z.infer) => { await register(data.email, data.password, data.name); }, - [register, formSchema], + [register], ); return ( diff --git a/src/pages/Router.tsx b/src/pages/Router.tsx index 12fe9df..369a078 100644 --- a/src/pages/Router.tsx +++ b/src/pages/Router.tsx @@ -24,7 +24,7 @@ export const Router = createBrowserRouter([ }, { path: "/admin", - element: + element: , }, { path: "/login",