From 32c3d4ad3bf3d033b7bef6d7796fb2006f106f45 Mon Sep 17 00:00:00 2001 From: Rodrigo Verdiani Date: Sat, 28 Mar 2026 19:27:53 -0300 Subject: [PATCH 1/3] refactor: Improve upload error handling and progress display in MangaManualImportDialog --- src/components/ui/progress.tsx | 10 ++- .../components/MangaManualImportDialog.tsx | 64 ++++++++++++++----- 2 files changed, 57 insertions(+), 17 deletions(-) diff --git a/src/components/ui/progress.tsx b/src/components/ui/progress.tsx index 93349bb..6451efe 100644 --- a/src/components/ui/progress.tsx +++ b/src/components/ui/progress.tsx @@ -6,8 +6,11 @@ import { cn } from "@/lib/utils"; function Progress({ className, value, + indicatorClassName, ...props -}: React.ComponentProps) { +}: React.ComponentProps & { + indicatorClassName?: string; +}) { return ( diff --git a/src/features/home/components/MangaManualImportDialog.tsx b/src/features/home/components/MangaManualImportDialog.tsx index 65e8e7f..9660ceb 100644 --- a/src/features/home/components/MangaManualImportDialog.tsx +++ b/src/features/home/components/MangaManualImportDialog.tsx @@ -1,5 +1,5 @@ import axios from "axios"; -import { FileUp } from "lucide-react"; +import { FileUp, XCircle } from "lucide-react"; import type React from "react"; import { useState } from "react"; import { toast } from "sonner"; @@ -33,6 +33,7 @@ export const MangaManualImportDialog = ({ const [uploadProgress, setUploadProgress] = useState>( {}, ); + const [uploadErrors, setUploadErrors] = useState>({}); const { mutateAsync: requestPresignedUrl } = useRequestPresignedImport(); @@ -49,6 +50,7 @@ export const MangaManualImportDialog = ({ setIsUploading(true); setUploadProgress({}); + setUploadErrors({}); let hasError = false; for (const file of files) { @@ -85,6 +87,7 @@ export const MangaManualImportDialog = ({ } catch (error) { console.error(`Error uploading ${file.name}:`, error); hasError = true; + setUploadErrors((prev) => ({ ...prev, [file.name]: true })); toast.error(`Failed to import ${file.name}`); } } @@ -96,6 +99,7 @@ export const MangaManualImportDialog = ({ setMalId(""); setAniListId(""); setUploadProgress({}); + setUploadErrors({}); onFileImportDialogOpenChange(false); toast.success("Manga imported successfully! Backend will process it."); } @@ -223,23 +227,52 @@ export const MangaManualImportDialog = ({ 0, ) / files.length } + indicatorClassName={ + Math.round( + files.reduce( + (acc, file) => acc + (uploadProgress[file.name] || 0), + 0, + ) / files.length, + ) === 100 + ? "bg-green-500" + : "" + } /> -
- {files.map((file) => ( -
-
- - {file.name} - - {uploadProgress[file.name] || 0}% +
+ {files.map((file) => { + const isError = uploadErrors[file.name]; + const progress = uploadProgress[file.name] || 0; + return ( +
+
+ + {file.name} + + {isError ? ( + + Failed + + ) : ( + {progress}% + )} +
+
- -
- ))} + ); + })}
)} @@ -251,6 +284,7 @@ export const MangaManualImportDialog = ({ onClick={() => { setFiles(null); setUploadProgress({}); + setUploadErrors({}); onFileImportDialogOpenChange(false); }} > -- 2.49.1 From 096412cfb228a65c0efa31dbcfa848361eb0ee10 Mon Sep 17 00:00:00 2001 From: Rodrigo Verdiani Date: Sat, 28 Mar 2026 19:38:10 -0300 Subject: [PATCH 2/3] refactor: Introduce MangaImport component and update import functionality in Admin --- src/features/admin/components/MangaImport.tsx | 80 +++++++++++++++ .../components/MangaManualImportDialog.tsx | 8 +- .../components/ProviderImportDialog.tsx | 4 +- .../home/components/ImportDropdown.tsx | 69 ------------- src/pages/Admin.tsx | 99 ++++--------------- src/pages/Home.tsx | 2 - 6 files changed, 104 insertions(+), 158 deletions(-) create mode 100644 src/features/admin/components/MangaImport.tsx rename src/features/{home => admin}/components/MangaManualImportDialog.tsx (97%) rename src/features/{home => admin}/components/ProviderImportDialog.tsx (98%) delete mode 100644 src/features/home/components/ImportDropdown.tsx diff --git a/src/features/admin/components/MangaImport.tsx b/src/features/admin/components/MangaImport.tsx new file mode 100644 index 0000000..e5a905c --- /dev/null +++ b/src/features/admin/components/MangaImport.tsx @@ -0,0 +1,80 @@ +import {Card} from "@/components/ui/card.tsx"; +import {Download, FileUp} from "lucide-react"; +import {Button} from "@/components/ui/button.tsx"; +import {useState} from "react"; +import {ProviderImportDialog} from "@/features/admin/components/ProviderImportDialog.tsx"; +import {MangaManualImportDialog} from "@/features/admin/components/MangaManualImportDialog.tsx"; + +export const MangaImport = () => { + const [providerDialogOpen, setProviderDialogOpen] = useState(false); + const [fileImportDialogOpen, setFileImportDialogOpen] = useState(false); + + return (
+
+

+ Import Manga +

+

+ Import manga from external providers or upload files directly. +

+
+
+ +
+
+ +
+
+

+ Import from Provider +

+

+ Import manga from MangaDex or other supported providers. +

+
+ +
+
+ +
+
+ +
+
+

+ File Upload +

+

+ Upload CBZ or CBR files to import manga data in + bulk. +

+
+ +
+
+ + + + +
+
); +} \ No newline at end of file diff --git a/src/features/home/components/MangaManualImportDialog.tsx b/src/features/admin/components/MangaManualImportDialog.tsx similarity index 97% rename from src/features/home/components/MangaManualImportDialog.tsx rename to src/features/admin/components/MangaManualImportDialog.tsx index 9660ceb..b75ab00 100644 --- a/src/features/home/components/MangaManualImportDialog.tsx +++ b/src/features/admin/components/MangaManualImportDialog.tsx @@ -4,7 +4,7 @@ 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 { Button } from "@/components/ui/button.tsx"; import { Dialog, DialogContent, @@ -12,9 +12,9 @@ import { DialogFooter, DialogHeader, DialogTitle, -} from "@/components/ui/dialog"; -import { Input } from "@/components/ui/input"; -import { Progress } from "@/components/ui/progress"; +} from "@/components/ui/dialog.tsx"; +import { Input } from "@/components/ui/input.tsx"; +import { Progress } from "@/components/ui/progress.tsx"; interface MangaManualImportDialogProps { fileImportDialogOpen: boolean; diff --git a/src/features/home/components/ProviderImportDialog.tsx b/src/features/admin/components/ProviderImportDialog.tsx similarity index 98% rename from src/features/home/components/ProviderImportDialog.tsx rename to src/features/admin/components/ProviderImportDialog.tsx index 0db083e..7710c9f 100644 --- a/src/features/home/components/ProviderImportDialog.tsx +++ b/src/features/admin/components/ProviderImportDialog.tsx @@ -5,7 +5,7 @@ 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 { Button } from "@/components/ui/button.tsx"; import { Dialog, DialogContent, @@ -23,7 +23,7 @@ import { FormLabel, FormMessage, } from "@/components/ui/form.tsx"; -import { Input } from "@/components/ui/input"; +import { Input } from "@/components/ui/input.tsx"; import { Select, SelectContent, diff --git a/src/features/home/components/ImportDropdown.tsx b/src/features/home/components/ImportDropdown.tsx deleted file mode 100644 index b966aff..0000000 --- a/src/features/home/components/ImportDropdown.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { AlertCircle, Download, FileUp } from "lucide-react"; -import { useState } from "react"; -import { Link } from "react-router"; -import { Button } from "@/components/ui/button"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { useAuth } from "@/contexts/AuthContext.tsx"; -import { MangaManualImportDialog } from "@/features/home/components/MangaManualImportDialog.tsx"; -import { ProviderImportDialog } from "@/features/home/components/ProviderImportDialog.tsx"; - -export function ImportDropdown() { - const { isAuthenticated } = useAuth(); - - const [providerDialogOpen, setProviderDialogOpen] = useState(false); - const [fileImportDialogOpen, setFileImportDialogOpen] = useState(false); - - if (!isAuthenticated) { - return null; - } - - return ( - <> - - - - - - setProviderDialogOpen(true)}> - - Import from Provider - - setFileImportDialogOpen(true)}> - - Import from File - - - - - - - Import Review - - - - - - - - - - ); -} diff --git a/src/pages/Admin.tsx b/src/pages/Admin.tsx index ed78971..75ddf60 100644 --- a/src/pages/Admin.tsx +++ b/src/pages/Admin.tsx @@ -1,10 +1,12 @@ -import { ArrowLeft, FileStack, Server, Shield } from "lucide-react"; +import {AlertCircle, ArrowLeft, Download, 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"; +import {MangaImport} from "@/features/admin/components/MangaImport.tsx"; +import {useGetMangaImportJobs} from "@/api/generated/content/content.ts"; type Tab = | "import" @@ -29,9 +31,9 @@ const Admin = () => { // 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?.failedJobs ?? 0; const tabs: { id: Tab; label: string; icon: ReactNode; badge?: number }[] = [ { @@ -44,21 +46,21 @@ const Admin = () => { // 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", + label: "Import", + icon: , + }, + { + id: "ingest-review", + label: "Ingest Review", + icon: , + }, { id: "import-jobs", label: "Manual Import Jobs", icon: , + badge: failedImports > 0 ? failedImports : undefined, }, // { // id: "users", @@ -132,73 +134,8 @@ const Admin = () => { {/* Main Content */}
{activeTab === "providers" && } - {/*{activeTab === "manga" && }*/} - - {/*{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 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 === "import" && } {/*{activeTab === "ingest-review" && (*/} {/*
*/} diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 1a4c10f..97a586e 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -8,7 +8,6 @@ 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 { useDynamicPageSize } from "@/hooks/useDynamicPageSize.ts"; @@ -93,7 +92,6 @@ const Home = () => {
-
-- 2.49.1 From d57987845973325d41172ab8121cfc36a19bfd8b Mon Sep 17 00:00:00 2001 From: Rodrigo Verdiani Date: Sat, 28 Mar 2026 19:45:39 -0300 Subject: [PATCH 3/3] refactor: Add IngestReview component and integrate it into Admin layout --- .../components}/ImportReviewCard.tsx | 6 +- .../admin/components/IngestReview.tsx | 45 ++++++++++++ src/pages/Admin.tsx | 43 +----------- src/pages/ImportReview.tsx | 70 ------------------- 4 files changed, 50 insertions(+), 114 deletions(-) rename src/features/{import-review => admin/components}/ImportReviewCard.tsx (95%) create mode 100644 src/features/admin/components/IngestReview.tsx delete mode 100644 src/pages/ImportReview.tsx diff --git a/src/features/import-review/ImportReviewCard.tsx b/src/features/admin/components/ImportReviewCard.tsx similarity index 95% rename from src/features/import-review/ImportReviewCard.tsx rename to src/features/admin/components/ImportReviewCard.tsx index 3e95575..57b9958 100644 --- a/src/features/import-review/ImportReviewCard.tsx +++ b/src/features/admin/components/ImportReviewCard.tsx @@ -7,9 +7,9 @@ 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 { Button } from "@/components/ui/button.tsx"; +import { Card } from "@/components/ui/card.tsx"; +import { Input } from "@/components/ui/input.tsx"; interface ImportReviewCardProps { importReview: MangaIngestReviewDTO; diff --git a/src/features/admin/components/IngestReview.tsx b/src/features/admin/components/IngestReview.tsx new file mode 100644 index 0000000..ad66fd4 --- /dev/null +++ b/src/features/admin/components/IngestReview.tsx @@ -0,0 +1,45 @@ +import {Card} from "@/components/ui/card.tsx"; +import {AlertCircle} from "lucide-react"; +import {useGetMangaIngestReviews} from "@/api/generated/manga-ingest-review/manga-ingest-review.ts"; +import {ImportReviewCard} from "@/features/admin/components/ImportReviewCard.tsx"; + +export const IngestReview = () => { + const { data: importReviewData, queryKey } = useGetMangaIngestReviews(); + + return (
+
+

+ Ingest Review +

+

+ Review and resolve imports that need manual matching. +

+
+ + {!importReviewData?.data || importReviewData.data.length === 0 ? ( + + +

+ No Imports to Review +

+

+ All your imports have been processed successfully! +

+
+ ) : ( +
+
+ {importReviewData.data.length} import + {importReviewData.data.length !== 1 ? "s" : ""} to review +
+ {importReviewData.data.map((importReview) => ( + + ))} +
+ )} +
); +}; \ No newline at end of file diff --git a/src/pages/Admin.tsx b/src/pages/Admin.tsx index 75ddf60..8b8f74e 100644 --- a/src/pages/Admin.tsx +++ b/src/pages/Admin.tsx @@ -7,6 +7,7 @@ import { FailedImportJobs } from "@/features/admin/components/FailedImportJobs.t import { ProviderManager } from "@/features/admin/components/ProviderManager.tsx"; import {MangaImport} from "@/features/admin/components/MangaImport.tsx"; import {useGetMangaImportJobs} from "@/api/generated/content/content.ts"; +import {IngestReview} from "@/features/admin/components/IngestReview.tsx"; type Tab = | "import" @@ -72,10 +73,8 @@ const Admin = () => { return (
- {/* Sidebar */} - {/* Main Content */}
{activeTab === "providers" && } {/*{activeTab === "manga" && }*/} {activeTab === "import" && } - - {/*{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) => (*/} - {/* */} - {/* ))}*/} - {/*
*/} - {/* )}*/} - {/*
*/} - {/*)}*/} - + {activeTab === "ingest-review" && } {activeTab === "import-jobs" && } {/*{activeTab === "users" && }*/} diff --git a/src/pages/ImportReview.tsx b/src/pages/ImportReview.tsx deleted file mode 100644 index 8e92738..0000000 --- a/src/pages/ImportReview.tsx +++ /dev/null @@ -1,70 +0,0 @@ -"use client"; - -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"; - -export default function ImportReviewPage() { - const navigate = useNavigate(); - const { user, isAuthenticated } = useAuth(); - - const { data: importReviewData, queryKey } = useGetMangaIngestReviews(); - - useEffect(() => { - if (!user) { - return; - } - - if (!isAuthenticated) { - navigate("/login"); - } - }, [isAuthenticated, navigate, user]); - - if (!user) { - return null; - } - - return ( -
-
-
-

Import Review

-

- Review and resolve manga imports by manually matching them with - MyAnimeList entries. -

-
- - {!importReviewData?.data || importReviewData.data.length === 0 ? ( - - -

- No Imports to Review -

-

- All your imports have been processed successfully! -

-
- ) : ( -
-
- {importReviewData.data.length} import - {importReviewData.data.length !== 1 ? "s" : ""} to review -
- {importReviewData.data.map((importReview) => ( - - ))} -
- )} -
-
- ); -} -- 2.49.1