feature(home): add import functionality with MangaDex and manual file import dialogs
All checks were successful
ci/woodpecker/push/pipeline Pipeline was successful
All checks were successful
ci/woodpecker/push/pipeline Pipeline was successful
This commit is contained in:
parent
a2f1b48b9a
commit
8e92389f53
37
package-lock.json
generated
37
package-lock.json
generated
@ -10,6 +10,7 @@
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@radix-ui/react-avatar": "^1.1.10",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
@ -1560,6 +1561,42 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog": {
|
||||
"version": "1.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz",
|
||||
"integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.11",
|
||||
"@radix-ui/react-focus-guards": "1.1.3",
|
||||
"@radix-ui/react-focus-scope": "1.1.7",
|
||||
"@radix-ui/react-id": "1.1.1",
|
||||
"@radix-ui/react-portal": "1.1.9",
|
||||
"@radix-ui/react-presence": "1.1.5",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-slot": "1.2.3",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||
"aria-hidden": "^1.2.4",
|
||||
"react-remove-scroll": "^2.6.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-direction": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@radix-ui/react-avatar": "^1.1.10",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
|
||||
141
src/components/ui/dialog.tsx
Normal file
141
src/components/ui/dialog.tsx
Normal file
@ -0,0 +1,141 @@
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { XIcon } from "lucide-react";
|
||||
import type * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Dialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
|
||||
}
|
||||
|
||||
function DialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function DialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
|
||||
}
|
||||
|
||||
function DialogClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
|
||||
}
|
||||
|
||||
function DialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<DialogPrimitive.Overlay
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogContent({
|
||||
className,
|
||||
children,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||
showCloseButton?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DialogPortal data-slot="dialog-portal">
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close
|
||||
data-slot="dialog-close"
|
||||
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||
>
|
||||
<XIcon />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-header"
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
data-slot="dialog-title"
|
||||
className={cn("text-lg leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
data-slot="dialog-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
};
|
||||
69
src/features/home/components/ImportDropdown.tsx
Normal file
69
src/features/home/components/ImportDropdown.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
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 { MangaDexImportDialog } from "@/features/home/components/MangaDexImportDialog.tsx";
|
||||
import { MangaManualImportDialog } from "@/features/home/components/MangaManualImportDialog.tsx";
|
||||
|
||||
export function ImportDropdown() {
|
||||
const { isAuthenticated } = useAuth();
|
||||
|
||||
const [mangaDexDialogOpen, setMangaDexDialogOpen] = useState(false);
|
||||
const [fileImportDialogOpen, setFileImportDialogOpen] = useState(false);
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="gap-2 bg-transparent">
|
||||
<Download className="h-4 w-4" />
|
||||
Import
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="">
|
||||
<DropdownMenuItem onClick={() => setMangaDexDialogOpen(true)}>
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
Import from MangaDex
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setFileImportDialogOpen(true)}>
|
||||
<FileUp className="mr-2 h-4 w-4" />
|
||||
Import from File
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem asChild>
|
||||
<Link
|
||||
to="/import-review"
|
||||
className="cursor-pointer gap-2 text-amber-600 dark:text-amber-500"
|
||||
>
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<span>Import Review</span>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<MangaDexImportDialog
|
||||
mangaDexDialogOpen={mangaDexDialogOpen}
|
||||
onMangaDexDialogOpenChange={setMangaDexDialogOpen}
|
||||
/>
|
||||
|
||||
<MangaManualImportDialog
|
||||
fileImportDialogOpen={fileImportDialogOpen}
|
||||
onFileImportDialogOpenChange={setFileImportDialogOpen}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
149
src/features/home/components/MangaDexImportDialog.tsx
Normal file
149
src/features/home/components/MangaDexImportDialog.tsx
Normal file
@ -0,0 +1,149 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useCallback } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { useImportFromMangaDex } 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.tsx";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form.tsx";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
interface MangaDexImportDialogProps {
|
||||
mangaDexDialogOpen: boolean;
|
||||
onMangaDexDialogOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const MangaDexImportDialog = ({
|
||||
mangaDexDialogOpen,
|
||||
onMangaDexDialogOpenChange,
|
||||
}: MangaDexImportDialogProps) => {
|
||||
const formSchema = z
|
||||
.object({
|
||||
value: z.string().min(1, "Please enter a MangaDex ID or URL."),
|
||||
})
|
||||
.refine(
|
||||
(data: { value: string }) =>
|
||||
data.value.length > 36 &&
|
||||
!data.value.match(/title\/([0-9a-fA-F-]{36})/),
|
||||
{
|
||||
message: "Invalid MangaDex ID or URL",
|
||||
path: ["value"],
|
||||
abort: true,
|
||||
},
|
||||
)
|
||||
.refine((data: { value: string }) => data.value.length !== 36, {
|
||||
message: "Invalid MangaDex ID",
|
||||
path: ["value"],
|
||||
abort: true,
|
||||
});
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
value: "",
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: importMangaDex, isPending: isPendingImportMangaDex } =
|
||||
useImportFromMangaDex({
|
||||
mutation: {
|
||||
onSuccess: () => {
|
||||
form.reset();
|
||||
onMangaDexDialogOpenChange(false);
|
||||
toast.success("Manga imported successfully!");
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(data: z.infer<typeof formSchema>) => {
|
||||
let id = data.value;
|
||||
if (data.value.length > 36) {
|
||||
const match = data.value.match(/title\/([0-9a-fA-F-]{36})/);
|
||||
if (match) {
|
||||
id = match[1];
|
||||
} else {
|
||||
alert("Invalid MangaDex URL or ID");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (id.length !== 36) {
|
||||
alert("Invalid MangaDex ID");
|
||||
return;
|
||||
}
|
||||
|
||||
importMangaDex({ data: { id } });
|
||||
},
|
||||
[formSchema, importMangaDex],
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog open={mangaDexDialogOpen} onOpenChange={onMangaDexDialogOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Import from MangaDex</DialogTitle>
|
||||
<DialogDescription>
|
||||
Enter a MangaDex manga URL or ID to import it to your library.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="importForm"
|
||||
onSubmit={form.handleSubmit(handleSubmit)}
|
||||
className="space-y-4"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="value"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>MangaDex URL or ID</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="e.g., https://mangadex.org/title/..."
|
||||
disabled={isPendingImportMangaDex}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => onMangaDexDialogOpenChange(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isPendingImportMangaDex}
|
||||
form="importForm"
|
||||
>
|
||||
Import
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
179
src/features/home/components/MangaManualImportDialog.tsx
Normal file
179
src/features/home/components/MangaManualImportDialog.tsx
Normal file
@ -0,0 +1,179 @@
|
||||
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";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
interface MangaManualImportDialogProps {
|
||||
fileImportDialogOpen: boolean;
|
||||
onFileImportDialogOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const MangaManualImportDialog = ({
|
||||
fileImportDialogOpen,
|
||||
onFileImportDialogOpenChange,
|
||||
}: MangaManualImportDialogProps) => {
|
||||
const [malId, setMalId] = useState("");
|
||||
const [dragActive, setDragActive] = useState(false);
|
||||
const [files, setFiles] = useState<File[] | null>(null);
|
||||
|
||||
const { mutate, isPending } = useImportMultipleFiles({
|
||||
mutation: {
|
||||
onSuccess: () => {
|
||||
setFiles(null);
|
||||
setMalId("");
|
||||
onFileImportDialogOpenChange(false);
|
||||
toast.success("Manga imported successfully!");
|
||||
},
|
||||
onError: () => toast.error("Failed to import manga."),
|
||||
},
|
||||
});
|
||||
|
||||
const handleFileImport = () => {
|
||||
if (!files) {
|
||||
alert("Please select one or more files to upload");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!malId.trim()) {
|
||||
alert("Please enter a MyAnimeList manga ID");
|
||||
return;
|
||||
}
|
||||
|
||||
let id = malId;
|
||||
|
||||
if (!/^\d+$/.test(malId)) {
|
||||
const regex =
|
||||
/https?:\/\/(?:www\.)?myanimelist\.net\/(manga)\/(\d+)(?:\/|$)/i;
|
||||
const match = malId.match(regex);
|
||||
|
||||
if (match) {
|
||||
id = match[2];
|
||||
} else {
|
||||
alert("Invalid MyAnimeList URL or ID");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
mutate({ data: { malId: id, 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={fileImportDialogOpen}
|
||||
onOpenChange={onFileImportDialogOpenChange}
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Import from File</DialogTitle>
|
||||
<DialogDescription>
|
||||
Upload one or more files and provide the MyAnimeList manga URL (or
|
||||
ID) to import manga data.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="text-sm font-medium">
|
||||
MyAnimeList Manga URL (or ID)
|
||||
</label>
|
||||
<Input
|
||||
placeholder="e.g., https://myanimelist.net/manga/..."
|
||||
value={malId}
|
||||
onChange={(e) => setMalId(e.target.value)}
|
||||
className="mt-2"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium">Upload File</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>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
disabled={isPending}
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setFiles(null);
|
||||
onFileImportDialogOpenChange(false);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button disabled={isPending} onClick={handleFileImport}>
|
||||
Import
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@ -6,6 +6,7 @@ import { AuthHeader } from "@/components/AuthHeader.tsx";
|
||||
import { Pagination } from "@/components/Pagination.tsx";
|
||||
import { Input } from "@/components/ui/input.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,
|
||||
@ -75,7 +76,7 @@ const Home = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
{/*<ImportDropdown />*/}
|
||||
<ImportDropdown />
|
||||
{/*<ThemeToggle />*/}
|
||||
<AuthHeader />
|
||||
</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user