mirror of
https://gitlab.com/megazordpobeda/DataRush.git
synced 2026-05-23 07:27:10 +00:00
feat: something with something
This commit is contained in:
@@ -1,17 +1,28 @@
|
||||
import { useState } from "react";
|
||||
import { useParams, Link, useNavigate } from "react-router-dom";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import { Competition } from "@/shared/types";
|
||||
import { mockCompetitions, mockTasks } from "@/shared/mocks/mocks";
|
||||
import { mockTasks } from "@/shared/mocks/mocks";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getCompetition } from "@/shared/api/competitions";
|
||||
import { Loading } from "@/components/ui/Loading";
|
||||
|
||||
const CompetitionPage = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const [competition] = useState<Competition>(
|
||||
mockCompetitions.find((comp) => comp.id === id)!,
|
||||
);
|
||||
|
||||
const { data: competition, isLoading } = useQuery({
|
||||
queryKey: ["competition", id],
|
||||
queryFn: async () => getCompetition(id || ""),
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (!id || !competition) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const handleContinue = () => {
|
||||
if (competition?.id) {
|
||||
@@ -35,18 +46,20 @@ const CompetitionPage = () => {
|
||||
</Link>
|
||||
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="aspect-2 h-auto w-full overflow-hidden rounded-xl">
|
||||
<img
|
||||
src={competition.imageUrl}
|
||||
alt={competition.name}
|
||||
className="h-full w-full object-cover object-center"
|
||||
/>
|
||||
</div>
|
||||
{competition.image_url && (
|
||||
<div className="aspect-2 h-auto w-full overflow-hidden rounded-xl">
|
||||
<img
|
||||
src={competition.image_url}
|
||||
alt={competition.title}
|
||||
className="h-full w-full object-cover object-center"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col-reverse gap-8 md:flex-row">
|
||||
<div className="flex flex-1 flex-col gap-5">
|
||||
<h1 className="text-[34px] leading-11 font-semibold text-balance">
|
||||
{competition.name}
|
||||
{competition.title}
|
||||
</h1>
|
||||
<div className="prose prose-lg max-w-none text-xl leading-10 font-normal">
|
||||
<ReactMarkdown>{competition.description || ""}</ReactMarkdown>
|
||||
|
||||
@@ -11,7 +11,7 @@ const CompetitionSession = () => {
|
||||
const [tasks] = useState<Task[]>(mockTasks);
|
||||
const [answer, setAnswer] = useState("");
|
||||
|
||||
const currentTask = tasks.find(t => t.id === taskId) || null;
|
||||
const currentTask = tasks.find((t) => t.id === taskId) || tasks.at(0);
|
||||
|
||||
if (!taskId && tasks.length > 0) {
|
||||
return <Navigate to={`/competition/${id}/tasks/${tasks[0].id}`} replace />;
|
||||
@@ -20,22 +20,21 @@ const CompetitionSession = () => {
|
||||
const handleSubmit = () => {
|
||||
console.log("Submitting answer:", answer);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div className="flex flex-col min-h-screen">
|
||||
<CompetitionHeader
|
||||
<div className="flex min-h-screen flex-col">
|
||||
<CompetitionHeader
|
||||
title="Олимпиада DANO 2025. Индивидуальный этап"
|
||||
tasks={tasks}
|
||||
competitionId={id || ""}
|
||||
tasks={tasks}
|
||||
competitionId={id || ""}
|
||||
/>
|
||||
|
||||
|
||||
<main className="flex-1 bg-[#F8F8F8] pb-8">
|
||||
<div className="max-w-6xl mx-auto px-4 py-6">
|
||||
<div className="mx-auto max-w-6xl px-4 py-6">
|
||||
{currentTask ? (
|
||||
<div className="flex flex-col md:flex-row gap-6 font-hse-sans">
|
||||
<div className="font-hse-sans flex flex-col gap-6 md:flex-row">
|
||||
<TaskContent task={currentTask} />
|
||||
<TaskSolution
|
||||
<TaskSolution
|
||||
task={currentTask}
|
||||
solutions={mockSolutions}
|
||||
answer={answer}
|
||||
@@ -44,10 +43,8 @@ const CompetitionSession = () => {
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex justify-center items-center h-40 bg-white rounded-lg">
|
||||
<p className="font-hse-sans text-gray-500">
|
||||
Загрузка задания...
|
||||
</p>
|
||||
<div className="flex h-40 items-center justify-center rounded-lg bg-white">
|
||||
<p className="font-hse-sans text-gray-500">Загрузка задания...</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -56,4 +53,4 @@ const CompetitionSession = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default CompetitionSession;
|
||||
export default CompetitionSession;
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { Competition, CompetitionStatus } from "@/shared/types";
|
||||
import { cn } from "@/shared/lib/utils";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import {
|
||||
Competition,
|
||||
CompetitionState,
|
||||
CompetitionType,
|
||||
} from "@/shared/types/competition";
|
||||
|
||||
interface CompetitionCardProps {
|
||||
competition: Competition;
|
||||
@@ -16,28 +20,36 @@ export function CompetitionCard({
|
||||
className={cn("aspect-square h-full w-auto overflow-hidden", className)}
|
||||
>
|
||||
<div className="relative h-full overflow-hidden">
|
||||
<img
|
||||
src={competition.imageUrl}
|
||||
alt={competition.name}
|
||||
className="h-full w-full object-cover object-center"
|
||||
/>
|
||||
{competition.image_url && (
|
||||
<img
|
||||
src={competition.image_url}
|
||||
alt={competition.title}
|
||||
className="h-full w-full object-cover object-center"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<CardContent>
|
||||
<div className="flex flex-col gap-2.5">
|
||||
<div className="text-muted-foreground flex items-center gap-2 *:text-sm *:font-semibold">
|
||||
<span>{competition.isOlympics ? "Олимпиада" : "Тренировка"}</span>
|
||||
{competition.status != CompetitionStatus.NotParticipating && (
|
||||
<span>
|
||||
{competition.type === CompetitionType.COMPETITIVE
|
||||
? "Соревнование"
|
||||
: "Тренировка"}
|
||||
</span>
|
||||
{competition.state != CompetitionState.NOT_STARTED && (
|
||||
<>
|
||||
<span>•</span>
|
||||
<span className="text-primary-foreground">
|
||||
{competition.status}
|
||||
{competition.state === CompetitionState.STARTED
|
||||
? "В прогрессе"
|
||||
: "Завершено"}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<h3 className="line-clamp-2 text-xl font-semibold">
|
||||
{competition.name}
|
||||
{competition.title}
|
||||
</h3>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import { cn } from "@/shared/lib/utils";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
||||
interface CompetitionTagProps {
|
||||
label: string;
|
||||
variant: 'olympics' | 'status';
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const CompetitionTag = ({ label, variant, className }: CompetitionTagProps) => {
|
||||
return (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className={cn(
|
||||
"text-xs font-medium",
|
||||
variant === 'olympics' && "bg-yellow-400 text-yellow-800 hover:bg-yellow-500 font-hse-sans",
|
||||
variant === 'status' && "bg-black text-white hover:bg-gray-800 font-hse-sans",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
export default CompetitionTag
|
||||
@@ -1,49 +1,96 @@
|
||||
import { useState } from "react";
|
||||
import { Competition, CompetitionStatus } from "@/shared/types";
|
||||
import { CompetitionGrid } from "./modules/CompetitionGrid";
|
||||
import React, { useState } from "react";
|
||||
import { CompetitionGrid } from "./modules/CompetitionsGrid";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { mockCompetitions } from "@/shared/mocks/mocks";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getCompetitions } from "@/shared/api/competitions";
|
||||
import { NoCompetitions } from "./modules/NoCompetitions";
|
||||
import { TabsContent } from "@radix-ui/react-tabs";
|
||||
import { Loading } from "@/components/ui/Loading";
|
||||
import { CompetitionState } from "@/shared/types/competition";
|
||||
|
||||
enum CompetitionTab {
|
||||
ONGOING = "ongoing",
|
||||
COMPLETED = "completed",
|
||||
}
|
||||
|
||||
const CompetitionsPage = () => {
|
||||
const [competitions] = useState<Competition[]>(mockCompetitions);
|
||||
const [activeTab, setActiveTab] = useState("ongoing");
|
||||
const [activeTab, setActiveTab] = useState<string>(CompetitionTab.ONGOING);
|
||||
|
||||
const myCompetitions = competitions.filter(
|
||||
(comp) =>
|
||||
comp.status === CompetitionStatus.InProgress ||
|
||||
comp.status === CompetitionStatus.Completed,
|
||||
const activeCompetitionsQuery = useQuery({
|
||||
queryKey: ["active-competitions"],
|
||||
queryFn: async () => getCompetitions(true),
|
||||
retry: 1,
|
||||
});
|
||||
|
||||
const inactiveCompetitionsQuery = useQuery({
|
||||
queryKey: ["inactive-competitions"],
|
||||
queryFn: async () => getCompetitions(false),
|
||||
retry: 1,
|
||||
});
|
||||
|
||||
const startedCompetitions = React.useMemo(
|
||||
() =>
|
||||
(activeCompetitionsQuery.data ?? []).filter(
|
||||
(comp) => comp.state === CompetitionState.STARTED,
|
||||
),
|
||||
[activeCompetitionsQuery.data],
|
||||
);
|
||||
|
||||
const filteredMyCompetitions = myCompetitions.filter((comp) =>
|
||||
activeTab === "ongoing"
|
||||
? comp.status === CompetitionStatus.InProgress
|
||||
: comp.status === CompetitionStatus.Completed,
|
||||
const finishedCompetitions = React.useMemo(
|
||||
() =>
|
||||
(activeCompetitionsQuery.data ?? []).filter(
|
||||
(comp) => comp.state === CompetitionState.FINISHED,
|
||||
),
|
||||
[activeCompetitionsQuery.data],
|
||||
);
|
||||
|
||||
const availableCompetitions = competitions.filter(
|
||||
(comp) => comp.status === "Не участвую",
|
||||
);
|
||||
if (
|
||||
activeCompetitionsQuery.isLoading ||
|
||||
inactiveCompetitionsQuery.isLoading
|
||||
) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 sm:gap-8">
|
||||
<Section>
|
||||
<SectionHeader>
|
||||
<SectionTitle>Мои события</SectionTitle>
|
||||
{(activeCompetitionsQuery.data ?? []).length > 0 && (
|
||||
<Section>
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="ongoing">В процессе</TabsTrigger>
|
||||
<TabsTrigger value="completed">Завершенные</TabsTrigger>
|
||||
</TabsList>
|
||||
<SectionHeader>
|
||||
<SectionTitle>Мои события</SectionTitle>
|
||||
|
||||
<TabsList>
|
||||
<TabsTrigger value={CompetitionTab.ONGOING}>
|
||||
В процессе
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value={CompetitionTab.COMPLETED}>
|
||||
Завершенные
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</SectionHeader>
|
||||
|
||||
<TabsContent value={CompetitionTab.ONGOING} asChild>
|
||||
<CompetitionGrid competitions={startedCompetitions} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value={CompetitionTab.COMPLETED} asChild>
|
||||
<CompetitionGrid competitions={finishedCompetitions} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</SectionHeader>
|
||||
<CompetitionGrid competitions={filteredMyCompetitions} />
|
||||
</Section>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
<Section>
|
||||
<SectionHeader>
|
||||
<SectionTitle>События</SectionTitle>
|
||||
</SectionHeader>
|
||||
<CompetitionGrid competitions={availableCompetitions} />
|
||||
{(inactiveCompetitionsQuery.data ?? []).length > 0 ? (
|
||||
<CompetitionGrid
|
||||
competitions={inactiveCompetitionsQuery.data ?? []}
|
||||
/>
|
||||
) : (
|
||||
<NoCompetitions />
|
||||
)}
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { Competition } from "@/shared/types";
|
||||
import { CompetitionCard } from "../../components/CompetitionCard";
|
||||
import { Competition } from "@/shared/types/competition";
|
||||
import { CompetitionCard } from "../components/CompetitionCard";
|
||||
import { Link } from "react-router";
|
||||
|
||||
interface CompetitionGridProps {
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Ban } from "lucide-react";
|
||||
|
||||
export const NoCompetitions = () => {
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<Ban 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>
|
||||
);
|
||||
};
|
||||
@@ -18,7 +18,7 @@ const LoginPage = () => {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-10 px-4 py-10 sm:gap-18 sm:py-18">
|
||||
<DataRush size={52} className="min-h-[52px]" />
|
||||
<DataRush size={50} className="min-h-[52px]" />
|
||||
<div className="flex w-full max-w-96 flex-col items-center gap-7">
|
||||
<h1 className="text-center text-4xl font-semibold">
|
||||
Добро пожаловать!
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
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";
|
||||
|
||||
const ReviewPage = () => {
|
||||
const { token } = useParams<{ token: string }>();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const reviewerQuery = useQuery({
|
||||
queryKey: ["reviewer", token],
|
||||
queryFn: async () => getReviewer(token || ""),
|
||||
retry: 0,
|
||||
});
|
||||
const submissionsQuery = useQuery({
|
||||
queryKey: ["submissions", token],
|
||||
queryFn: async () => getReviewerSubmissions(token || ""),
|
||||
retry: 0,
|
||||
});
|
||||
|
||||
if (reviewerQuery.isLoading || submissionsQuery.isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (!token || !reviewerQuery.data || !submissionsQuery.data) {
|
||||
navigate("/");
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="px-4">
|
||||
<div className="mx-auto max-w-5xl">
|
||||
<ReviewHeader reviewer={reviewerQuery.data} />
|
||||
|
||||
<Tabs defaultValue="available" className="my-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-3xl font-semibold">Посылки</h1>
|
||||
<TabsList>
|
||||
<TabsTrigger value="available">Доступные</TabsTrigger>
|
||||
<TabsTrigger value="checked">Проверенные</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReviewPage;
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
import { DataRushReview } from "@/components/ui/icons/datarush-review";
|
||||
import { Reviewer } from "@/shared/types/review";
|
||||
import { Link } from "react-router";
|
||||
|
||||
interface ReviewHeaderProps {
|
||||
reviewer: Reviewer;
|
||||
}
|
||||
|
||||
export const ReviewHeader = ({ reviewer }: ReviewHeaderProps) => {
|
||||
return (
|
||||
<header className="flex h-[90px] items-center justify-between gap-4">
|
||||
<DataRushReview />
|
||||
<div className="flex items-center gap-4">
|
||||
<p className="text-right font-semibold">
|
||||
{reviewer.name} {reviewer.surname}
|
||||
</p>
|
||||
<Link
|
||||
to="/"
|
||||
className={buttonVariants({ size: "sm", variant: "secondary" })}
|
||||
>
|
||||
Выйти
|
||||
</Link>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user