diff --git a/services/frontend/bun.lock b/services/frontend/bun.lock index ecd048c..cfa6012 100644 --- a/services/frontend/bun.lock +++ b/services/frontend/bun.lock @@ -15,6 +15,7 @@ "autoprefixer": "^10.4.20", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "dayjs": "^1.11.13", "js-cookie": "^3.0.5", "katex": "^0.16.21", "lucide-react": "^0.476.0", @@ -392,6 +393,8 @@ "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=="], "decode-named-character-reference": ["decode-named-character-reference@1.0.2", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg=="], diff --git a/services/frontend/package.json b/services/frontend/package.json index 9b45c78..e7ea22e 100644 --- a/services/frontend/package.json +++ b/services/frontend/package.json @@ -21,6 +21,7 @@ "autoprefixer": "^10.4.20", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "dayjs": "^1.11.13", "js-cookie": "^3.0.5", "katex": "^0.16.21", "lucide-react": "^0.476.0", diff --git a/services/frontend/src/App.tsx b/services/frontend/src/App.tsx index 9b3a5ae..7534997 100644 --- a/services/frontend/src/App.tsx +++ b/services/frontend/src/App.tsx @@ -42,9 +42,9 @@ const App = () => { /> } /> - - } /> + + } /> ); diff --git a/services/frontend/src/components/ui/button.tsx b/services/frontend/src/components/ui/button.tsx index 11b8b06..bc84fe8 100644 --- a/services/frontend/src/components/ui/button.tsx +++ b/services/frontend/src/components/ui/button.tsx @@ -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: { diff --git a/services/frontend/src/main.tsx b/services/frontend/src/main.tsx index f046499..3ba38f1 100644 --- a/services/frontend/src/main.tsx +++ b/services/frontend/src/main.tsx @@ -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( diff --git a/services/frontend/src/pages/Review/components/review-card.tsx b/services/frontend/src/pages/Review/components/review-card.tsx new file mode 100644 index 0000000..75003f8 --- /dev/null +++ b/services/frontend/src/pages/Review/components/review-card.tsx @@ -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 ( + + + + {review.competition_name} + + {review.task_title} + + + + {id} + • + + {review.review_status === ReviewStatus.NOT_CHECKED + ? `Дата посылки: ${dayjs(review.submitted_at).format("D MMMM, HH:mm")}` + : `Дата проверки: ${review.checked_at}`} + + + + {review.review_status === ReviewStatus.NOT_CHECKED + ? "Не проверено" + : ""} + + + + ); +}; diff --git a/services/frontend/src/pages/Review/index.tsx b/services/frontend/src/pages/Review/index.tsx index 4d21663..0701e14 100644 --- a/services/frontend/src/pages/Review/index.tsx +++ b/services/frontend/src/pages/Review/index.tsx @@ -1,9 +1,12 @@ -import { Loading } from "@/components/ui/Loading"; +import { Loading } from "@/components/ui/loading"; 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 ; } @@ -34,14 +53,35 @@ const ReviewPage = () => { - + - Посылки + Решения - Доступные + + Доступные + {availableReviews.length > 0 && ( + + {availableReviews.length} + + )} + Проверенные + + + + + + + + diff --git a/services/frontend/src/pages/Review/modules/no-reviews.tsx b/services/frontend/src/pages/Review/modules/no-reviews.tsx new file mode 100644 index 0000000..f82ec5b --- /dev/null +++ b/services/frontend/src/pages/Review/modules/no-reviews.tsx @@ -0,0 +1,13 @@ +import { Check } from "lucide-react"; + +export const NoReviews = () => { + return ( + + + + Посылок пока нет + Можете расслабиться + + + ); +}; diff --git a/services/frontend/src/pages/Review/modules/reviews-list.tsx b/services/frontend/src/pages/Review/modules/reviews-list.tsx new file mode 100644 index 0000000..0715a77 --- /dev/null +++ b/services/frontend/src/pages/Review/modules/reviews-list.tsx @@ -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 ; + } + + return ( + + {reviews.map((review) => ( + + ))} + + ); +}; diff --git a/services/frontend/src/shared/api/review.ts b/services/frontend/src/shared/api/review.ts index 887999b..1acc8d0 100644 --- a/services/frontend/src/shared/api/review.ts +++ b/services/frontend/src/shared/api/review.ts @@ -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(`/review/${token}`); }; export const getReviewerSubmissions = async (token: string) => { - return await apiFetch(`/review/${token}/submissions`); + return await apiFetch<{ submissions: Review[] }>( + `/review/${token}/submissions`, + ); }; diff --git a/services/frontend/src/shared/types/review.ts b/services/frontend/src/shared/types/review.ts index f3a9094..47e0a14 100644 --- a/services/frontend/src/shared/types/review.ts +++ b/services/frontend/src/shared/types/review.ts @@ -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; +}
+ {review.competition_name} +
{id}
•
+ {review.review_status === ReviewStatus.NOT_CHECKED + ? `Дата посылки: ${dayjs(review.submitted_at).format("D MMMM, HH:mm")}` + : `Дата проверки: ${review.checked_at}`} +
Можете расслабиться