This commit is contained in:
rngsurrounded
2025-03-02 23:58:01 +09:00
11 changed files with 165 additions and 9 deletions
+2 -2
View File
@@ -32,9 +32,9 @@ const App = () => {
/>
<Route path="/profile" element={<UserProfile />} />
<Route path="/review/:token" element={<ReviewPage />} />
</Route>
<Route path="/review/:token" element={<ReviewPage />} />
</Routes>
</QueryClientProvider>
);
@@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/shared/lib/utils";
const buttonVariants = cva(
"inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,box-shadow] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
"inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,box-shadow,scale] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive not-disabled:active:scale-[0.95] ",
{
variants: {
variant: {
+4
View File
@@ -2,6 +2,10 @@ import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router";
import App from "./App.tsx";
import dayjs from "dayjs";
import "dayjs/locale/ru";
dayjs.locale("ru");
createRoot(document.getElementById("root")!).render(
<StrictMode>
@@ -0,0 +1,37 @@
import { Review, ReviewStatus } from "@/shared/types/review";
import dayjs from "dayjs";
interface ReviewCardProps {
review: Review;
}
export const ReviewCard = ({ review }: ReviewCardProps) => {
const id = review.id.split("-").at(-1)?.slice(0, 6);
return (
<div className="bg-card flex items-center justify-between gap-8 rounded-lg px-8 py-5">
<div className="flex flex-col gap-1">
<p className="text-muted-foreground font-semibold">
{review.competition_name}
</p>
<h1 className="text-2xl font-semibold">{review.task_title}</h1>
</div>
<div className="flex flex-col items-end gap-1 text-right">
<div className="text-muted-foreground flex gap-1.5 font-semibold">
<p>{id}</p>
<p></p>
<p>
{review.review_status === ReviewStatus.NOT_CHECKED
? `Дата посылки: ${dayjs(review.submitted_at).format("D MMMM, HH:mm")}`
: `Дата проверки: ${review.checked_at}`}
</p>
</div>
<h1 className="text-2xl font-semibold">
{review.review_status === ReviewStatus.NOT_CHECKED
? "Не проверено"
: ""}
</h1>
</div>
</div>
);
};
+44 -4
View File
@@ -3,7 +3,10 @@ import { getReviewer, getReviewerSubmissions } from "@/shared/api/review";
import { useQuery } from "@tanstack/react-query";
import { useNavigate, useParams } from "react-router";
import { ReviewHeader } from "./modules/review-header";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { ReviewsList } from "./modules/reviews-list";
import React from "react";
import { ReviewStatus } from "@/shared/types/review";
const ReviewPage = () => {
const { token } = useParams<{ token: string }>();
@@ -20,6 +23,22 @@ const ReviewPage = () => {
retry: 0,
});
const availableReviews = React.useMemo(
() =>
(submissionsQuery.data?.submissions || []).filter(
(s) => s.review_status === ReviewStatus.NOT_CHECKED,
),
[submissionsQuery.data],
);
const checkedReviews = React.useMemo(
() =>
(submissionsQuery.data?.submissions || []).filter(
(s) => s.review_status === ReviewStatus.CHECKED,
),
[submissionsQuery.data],
);
if (reviewerQuery.isLoading || submissionsQuery.isLoading) {
return <Loading />;
}
@@ -34,14 +53,35 @@ const ReviewPage = () => {
<div className="mx-auto max-w-5xl">
<ReviewHeader reviewer={reviewerQuery.data} />
<Tabs defaultValue="available" className="my-3">
<Tabs
defaultValue="available"
className="my-3 flex flex-col items-stretch gap-6"
>
<div className="flex items-center justify-between">
<h1 className="text-3xl font-semibold">Посылки</h1>
<h1 className="text-3xl font-semibold">Решения</h1>
<TabsList>
<TabsTrigger value="available">Доступные</TabsTrigger>
<TabsTrigger
value="available"
className="flex items-center gap-2"
>
<span>Доступные</span>
{availableReviews.length > 0 && (
<div className="bg-primary min-w-5 rounded-full px-1.5 py-0.5 text-xs">
{availableReviews.length}
</div>
)}
</TabsTrigger>
<TabsTrigger value="checked">Проверенные</TabsTrigger>
</TabsList>
</div>
<TabsContent value="available" asChild>
<ReviewsList reviews={availableReviews} />
</TabsContent>
<TabsContent value="checked" asChild>
<ReviewsList reviews={checkedReviews} />
</TabsContent>
</Tabs>
</div>
</div>
@@ -0,0 +1,13 @@
import { Check } from "lucide-react";
export const NoReviews = () => {
return (
<div className="flex flex-col items-center gap-4">
<Check size={32} />
<div className="flex flex-col items-center gap-2">
<h2 className="text-2xl font-semibold">Посылок пока нет</h2>
<p className="text-muted-foreground text-lg">Можете расслабиться</p>
</div>
</div>
);
};
@@ -0,0 +1,21 @@
import { Review } from "@/shared/types/review";
import { ReviewCard } from "../components/review-card";
import { NoReviews } from "./no-reviews";
interface ReviewsListProp {
reviews: Review[];
}
export const ReviewsList = ({ reviews }: ReviewsListProp) => {
if (reviews.length === 0) {
return <NoReviews />;
}
return (
<div className="flex flex-col items-stretch gap-5">
{reviews.map((review) => (
<ReviewCard key={review.id} review={review} />
))}
</div>
);
};
+4 -2
View File
@@ -1,10 +1,12 @@
import { apiFetch } from ".";
import { Reviewer } from "../types/review";
import { Review, Reviewer } from "../types/review";
export const getReviewer = async (token: string) => {
return await apiFetch<Reviewer>(`/review/${token}`);
};
export const getReviewerSubmissions = async (token: string) => {
return await apiFetch(`/review/${token}/submissions`);
return await apiFetch<{ submissions: Review[] }>(
`/review/${token}/submissions`,
);
};
@@ -3,3 +3,38 @@ export interface Reviewer {
name: string;
surname: string;
}
export interface Review {
id: string;
review_status: ReviewStatus;
evaluation?: ReviewEvaluation[];
criteries?: ReviewCriteria[];
submitted_at: Date;
competition: string;
competition_name: string;
task: string;
content: string;
stdout?: string;
result?: {};
earned_points?: number;
checked_at?: Date;
task_title: string;
}
export enum ReviewStatus {
NOT_CHECKED = "not_checked",
CHECKED = "checked",
CHECKING = "checking",
}
export interface ReviewEvaluation {
slug: string;
mark: number;
}
export interface ReviewCriteria {
name: string;
slug: string;
max_value: number;
min_value: number;
}