Compare commits

...

1 Commits

Author SHA1 Message Date
5ddb122b88 feat(manga): add import file button to manga page 2025-12-14 16:18:18 -03:00
3 changed files with 175 additions and 1 deletions

View File

@ -150,6 +150,7 @@ export interface DefaultResponseDTOMangaDTO {
export interface MangaDTO {
id: number;
malId: number;
/** @minLength 1 */
title: string;
coverImageKey?: string;

View File

@ -0,0 +1,149 @@
import { useQueryClient } from "@tanstack/react-query";
import { FileUp } from "lucide-react";
import type React from "react";
import { useState } from "react";
import { toast } from "sonner";
import { useImportMultipleFiles } from "@/api/generated/manga-import/manga-import.ts";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
interface ManualImportDialogProps {
malId: number;
mangaTitle: string;
dialogOpen: boolean;
onOpenChange: (open: boolean) => void;
queryKey: any;
}
export const ManualImportDialog = ({
dialogOpen,
onOpenChange,
malId,
mangaTitle,
queryKey,
}: ManualImportDialogProps) => {
const [dragActive, setDragActive] = useState(false);
const [files, setFiles] = useState<File[] | null>(null);
const queryClient = useQueryClient();
const { mutate, isPending } = useImportMultipleFiles({
mutation: {
onSuccess: () => {
setFiles(null);
onOpenChange(false);
toast.success("Files imported successfully!");
queryClient.invalidateQueries(queryKey);
},
onError: () => toast.error("Failed to import files."),
},
});
const handleFileImport = () => {
if (!files) {
alert("Please select one or more files to upload");
return;
}
mutate({ data: { malId: malId.toString(), files } });
};
const handleDrag = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
if (e.type === "dragenter" || e.type === "dragover") {
setDragActive(true);
} else if (e.type === "dragleave") {
setDragActive(false);
}
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setDragActive(false);
if (e.dataTransfer.files) {
setFiles(Array.from(e.dataTransfer.files));
}
};
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
setFiles(Array.from(e.target.files));
}
};
return (
<Dialog open={dialogOpen} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>Upload Files</DialogTitle>
<DialogDescription>
Upload one or more files to {mangaTitle}.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<label className="text-sm font-medium">Upload Files</label>
<div
onDragEnter={handleDrag}
onDragLeave={handleDrag}
onDragOver={handleDrag}
onDrop={handleDrop}
className={`mt-2 rounded-lg border-2 border-dashed p-6 text-center transition-colors ${
dragActive
? "border-primary bg-primary/5"
: "border-muted-foreground/25"
}`}
>
<input
type="file"
id="file-input"
onChange={handleFileSelect}
className="hidden"
multiple
accept=".cbz"
/>
<label htmlFor="file-input" className="cursor-pointer">
<div className="flex flex-col items-center gap-2">
<FileUp className="h-8 w-8 text-muted-foreground" />
<div>
<p className="text-sm font-medium">
{files
? files.map((file) => file.name).join(", ")
: "Drag and drop your files here"}
</p>
<p className="text-xs text-muted-foreground">
or click to select (.CBZ, .CBR)
</p>
</div>
</div>
</label>
</div>
</div>
<DialogFooter>
<Button
disabled={isPending}
variant="outline"
onClick={() => {
setFiles(null);
onOpenChange(false);
}}
>
Cancel
</Button>
<Button disabled={isPending} onClick={handleFileImport}>
Import
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};

View File

@ -7,6 +7,7 @@ import {
Calendar,
ChevronDown,
Database,
FileUp,
Heart,
Star,
} from "lucide-react";
@ -35,6 +36,7 @@ import {
} from "@/components/ui/collapsible.tsx";
import { useAuth } from "@/contexts/AuthContext.tsx";
import { MangaChapter } from "@/features/manga/MangaChapter.tsx";
import { ManualImportDialog } from "@/features/manga/ManualImportDialog.tsx";
import { formatToTwoDigitsDateRange } from "@/utils/dateFormatter.ts";
const Manga = () => {
@ -45,6 +47,8 @@ const Manga = () => {
const queryClient = useQueryClient();
const [isUploadFilesDialogOpen, setIsUploadFilesDialogOpen] = useState(false);
const { data: mangaData, queryKey } = useGetManga(mangaId);
const { mutate, isPending: fetchPending } = useFetchMangaChapters({
@ -165,7 +169,19 @@ const Manga = () => {
</Button>
<h1 className="text-xl font-bold text-foreground">MangaMochi</h1>
</div>
<ThemeToggle />
<div className="flex items-center gap-6">
{isAuthenticated && (
<Button
variant="outline"
onClick={() => setIsUploadFilesDialogOpen(true)}
className="gap-2"
>
<FileUp className="mr-2 h-4 w-4" />
Upload Files
</Button>
)}
<ThemeToggle />
</div>
</div>
</div>
</header>
@ -425,6 +441,14 @@ const Manga = () => {
</div>
</div>
</main>
<ManualImportDialog
malId={mangaData?.data?.malId ?? -1}
mangaTitle={mangaData?.data?.title ?? ""}
dialogOpen={isUploadFilesDialogOpen}
onOpenChange={setIsUploadFilesDialogOpen}
queryKey={queryKey}
/>
</div>
);
};