feature(import): add import review page and card components
All checks were successful
ci/woodpecker/push/pipeline Pipeline was successful

This commit is contained in:
Rodrigo Verdiani 2025-10-30 16:16:14 -03:00
parent 7359823f4d
commit dd41f10234
3 changed files with 203 additions and 0 deletions

View File

@ -0,0 +1,128 @@
import { useQueryClient } from "@tanstack/react-query";
import { ExternalLink, Trash2 } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
import type { ImportReviewDTO } from "@/api/generated/api.schemas.ts";
import {
useDeleteImportReview,
useResolveImportReview,
} from "@/api/generated/manga-import-review/manga-import-review.ts";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
interface ImportReviewCardProps {
importReview: ImportReviewDTO;
queryKey: any;
}
export function ImportReviewCard({
importReview,
queryKey,
}: ImportReviewCardProps) {
const queryClient = useQueryClient();
const [malId, setMalId] = useState("");
const { mutate: mutateDeleteImportReview } = useDeleteImportReview({
mutation: {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey });
toast.success("Import review removed successfully");
},
},
});
const {
mutate: mutateResolveImportReview,
isPending: isPendingResolveImportReview,
} = useResolveImportReview({
mutation: {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey });
toast.success("Import review resolved successfully");
},
},
});
const handleResolve = () => {
if (!malId.trim()) {
alert("Please enter a MyAnimeList ID");
return;
}
mutateResolveImportReview({
params: { importReviewId: importReview.id, malId },
});
};
const handleRemove = () => {
mutateDeleteImportReview({ id: importReview.id });
};
const importDate = new Date(importReview.createdAt).toLocaleDateString();
return (
<Card className="p-4">
<div className="space-y-4">
<div className="flex items-start justify-between">
<div className="flex-1">
<h3 className="font-semibold text-foreground">
{importReview.title}
</h3>
<p className="text-sm text-muted-foreground">
Provider:{" "}
<span className="capitalize">{importReview.providerName}</span> {" "}
{importDate}
</p>
</div>
{importReview.externalUrl && (
<a
href={importReview.externalUrl}
target="_blank"
rel="noopener noreferrer"
className="ml-2 text-primary hover:underline"
>
<ExternalLink className="h-4 w-4" />
</a>
)}
</div>
<div className="rounded-md bg-destructive/10 p-3">
<p className="text-sm text-destructive">{importReview.reason}</p>
</div>
<div className="space-y-3">
<div>
<label className="text-sm font-medium">MyAnimeList Manga ID</label>
<Input
placeholder="Enter MAL ID to match manually"
value={malId}
onChange={(e) => setMalId(e.target.value)}
className="mt-2"
/>
</div>
<div className="flex gap-2">
<Button
onClick={handleResolve}
disabled={isPendingResolveImportReview || !malId.trim()}
className="flex-1"
>
{isPendingResolveImportReview ? "Resolving..." : "Resolve Import"}
</Button>
<Button
onClick={handleRemove}
variant="outline"
size="icon"
className="text-destructive hover:text-destructive bg-transparent"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
</div>
</Card>
);
}

View File

@ -0,0 +1,70 @@
"use client";
import { AlertCircle } from "lucide-react";
import { useEffect } from "react";
import { useNavigate } from "react-router";
import { useGetImportReviews } from "@/api/generated/manga-import-review/manga-import-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 } = useGetImportReviews();
useEffect(() => {
if (!user) {
return;
}
if (!isAuthenticated) {
navigate("/login");
}
}, [isAuthenticated, navigate, user]);
if (!user) {
return null;
}
return (
<main className="min-h-screen bg-background">
<div className="container mx-auto px-4 py-8">
<div className="mb-8">
<h1 className="text-3xl font-bold text-foreground">Import Review</h1>
<p className="mt-2 text-muted-foreground">
Review and resolve manga imports by manually matching them with
MyAnimeList entries.
</p>
</div>
{!importReviewData?.data || importReviewData.data.length === 0 ? (
<Card className="p-8 text-center">
<AlertCircle className="mx-auto h-12 w-12 text-muted-foreground" />
<h2 className="mt-4 text-lg font-semibold text-foreground">
No Imports to Review
</h2>
<p className="mt-2 text-muted-foreground">
All your imports have been processed successfully!
</p>
</Card>
) : (
<div className="space-y-4">
<div className="text-sm text-muted-foreground">
{importReviewData.data.length} import
{importReviewData.data.length !== 1 ? "s" : ""} to review
</div>
{importReviewData.data.map((importReview) => (
<ImportReviewCard
key={importReview.id}
importReview={importReview}
queryKey={queryKey}
/>
))}
</div>
)}
</div>
</main>
);
}

View File

@ -3,6 +3,7 @@ import { createBrowserRouter } from "react-router";
import { AppLayout } from "@/components/Layout/AppLayout.tsx";
const Home = lazy(() => import("./Home.tsx"));
const ImportReview = lazy(() => import("./ImportReview.tsx"));
const Manga = lazy(() => import("./Manga.tsx"));
const Chapter = lazy(() => import("./Chapter.tsx"));
const Login = lazy(() => import("./Login.tsx"));
@ -28,6 +29,10 @@ export const Router = createBrowserRouter([
path: "/register",
element: <Register />,
},
{
path: "/import-review",
element: <ImportReview />,
},
{
path: "/manga",
children: [