mirror of
https://gitlab.com/megazordpobeda/DataRush.git
synced 2026-05-23 19:07:10 +00:00
feat: review
This commit is contained in:
@@ -15,6 +15,7 @@
|
|||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"dayjs": "^1.11.13",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"katex": "^0.16.21",
|
"katex": "^0.16.21",
|
||||||
"lucide-react": "^0.476.0",
|
"lucide-react": "^0.476.0",
|
||||||
@@ -392,6 +393,8 @@
|
|||||||
|
|
||||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||||
|
|
||||||
|
"dayjs": ["dayjs@1.11.13", "", {}, "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg=="],
|
||||||
|
|
||||||
"debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
|
"debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
|
||||||
|
|
||||||
"decode-named-character-reference": ["decode-named-character-reference@1.0.2", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg=="],
|
"decode-named-character-reference": ["decode-named-character-reference@1.0.2", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg=="],
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"dayjs": "^1.11.13",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"katex": "^0.16.21",
|
"katex": "^0.16.21",
|
||||||
"lucide-react": "^0.476.0",
|
"lucide-react": "^0.476.0",
|
||||||
|
|||||||
@@ -42,9 +42,9 @@ const App = () => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Route path="/profile" element={<UserProfile />} />
|
<Route path="/profile" element={<UserProfile />} />
|
||||||
|
|
||||||
<Route path="/review/:token" element={<ReviewPage />} />
|
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
|
<Route path="/review/:token" element={<ReviewPage />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority";
|
|||||||
import { cn } from "@/shared/lib/utils";
|
import { cn } from "@/shared/lib/utils";
|
||||||
|
|
||||||
const buttonVariants = cva(
|
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: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ import { StrictMode } from "react";
|
|||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
import { BrowserRouter } from "react-router";
|
import { BrowserRouter } from "react-router";
|
||||||
import App from "./App.tsx";
|
import App from "./App.tsx";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
|
import "dayjs/locale/ru";
|
||||||
|
dayjs.locale("ru");
|
||||||
|
|
||||||
createRoot(document.getElementById("root")!).render(
|
createRoot(document.getElementById("root")!).render(
|
||||||
<StrictMode>
|
<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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
import { Loading } from "@/components/ui/Loading";
|
import { Loading } from "@/components/ui/loading";
|
||||||
import { getReviewer, getReviewerSubmissions } from "@/shared/api/review";
|
import { getReviewer, getReviewerSubmissions } from "@/shared/api/review";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useNavigate, useParams } from "react-router";
|
import { useNavigate, useParams } from "react-router";
|
||||||
import { ReviewHeader } from "./modules/review-header";
|
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 ReviewPage = () => {
|
||||||
const { token } = useParams<{ token: string }>();
|
const { token } = useParams<{ token: string }>();
|
||||||
@@ -20,6 +23,22 @@ const ReviewPage = () => {
|
|||||||
retry: 0,
|
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) {
|
if (reviewerQuery.isLoading || submissionsQuery.isLoading) {
|
||||||
return <Loading />;
|
return <Loading />;
|
||||||
}
|
}
|
||||||
@@ -34,14 +53,35 @@ const ReviewPage = () => {
|
|||||||
<div className="mx-auto max-w-5xl">
|
<div className="mx-auto max-w-5xl">
|
||||||
<ReviewHeader reviewer={reviewerQuery.data} />
|
<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">
|
<div className="flex items-center justify-between">
|
||||||
<h1 className="text-3xl font-semibold">Посылки</h1>
|
<h1 className="text-3xl font-semibold">Решения</h1>
|
||||||
<TabsList>
|
<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>
|
<TabsTrigger value="checked">Проверенные</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<TabsContent value="available" asChild>
|
||||||
|
<ReviewsList reviews={availableReviews} />
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="checked" asChild>
|
||||||
|
<ReviewsList reviews={checkedReviews} />
|
||||||
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
import { apiFetch } from ".";
|
import { apiFetch } from ".";
|
||||||
import { Reviewer } from "../types/review";
|
import { Review, Reviewer } from "../types/review";
|
||||||
|
|
||||||
export const getReviewer = async (token: string) => {
|
export const getReviewer = async (token: string) => {
|
||||||
return await apiFetch<Reviewer>(`/review/${token}`);
|
return await apiFetch<Reviewer>(`/review/${token}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getReviewerSubmissions = async (token: string) => {
|
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;
|
name: string;
|
||||||
surname: 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;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user