feature(import): add import review page and card components
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
7359823f4d
commit
dd41f10234
128
src/features/import-review/ImportReviewCard.tsx
Normal file
128
src/features/import-review/ImportReviewCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
70
src/pages/ImportReview.tsx
Normal file
70
src/pages/ImportReview.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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: [
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user