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";
|
import { AppLayout } from "@/components/Layout/AppLayout.tsx";
|
||||||
|
|
||||||
const Home = lazy(() => import("./Home.tsx"));
|
const Home = lazy(() => import("./Home.tsx"));
|
||||||
|
const ImportReview = lazy(() => import("./ImportReview.tsx"));
|
||||||
const Manga = lazy(() => import("./Manga.tsx"));
|
const Manga = lazy(() => import("./Manga.tsx"));
|
||||||
const Chapter = lazy(() => import("./Chapter.tsx"));
|
const Chapter = lazy(() => import("./Chapter.tsx"));
|
||||||
const Login = lazy(() => import("./Login.tsx"));
|
const Login = lazy(() => import("./Login.tsx"));
|
||||||
@ -28,6 +29,10 @@ export const Router = createBrowserRouter([
|
|||||||
path: "/register",
|
path: "/register",
|
||||||
element: <Register />,
|
element: <Register />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/import-review",
|
||||||
|
element: <ImportReview />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/manga",
|
path: "/manga",
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user