fix: main page

This commit is contained in:
moolcoov
2025-03-01 12:39:09 +03:00
parent 433c1e8b46
commit 65f73fb4a0
17 changed files with 383 additions and 361 deletions
+9 -5
View File
@@ -3,17 +3,21 @@ import "./styles/globals.css";
import CompetitionsPage from "./pages/CompetitionsPage";
import CompetitionPreviewPage from "./pages/CompetitionPreviewPage";
import CompetitionRunnerPage from "./pages/CompetitionRunnerPage";
import { NavbarLayout } from "./widgets/navbar-layout";
const App = () => {
return (
<Routes>
<Route path="/" element={<CompetitionsPage/>} />
<Route element={<NavbarLayout />}>
<Route path="/" element={<CompetitionsPage />} />
</Route>
<Route path="/competition/:id" element={<CompetitionPreviewPage />} />
<Route path="/competition/:id/tasks/:taskId" element={<CompetitionRunnerPage />} />
<Route
path="/competition/:id/tasks/:taskId"
element={<CompetitionRunnerPage />}
/>
</Routes>
);
};
export default App;
export default App;
@@ -0,0 +1,21 @@
import { DataRush } from "@/components/ui/icons/datarush";
import { ChevronDown } from "lucide-react";
import { Link } from "react-router";
const Header = () => {
return (
<header className="bg-card sticky top-0 z-30 flex h-[72px] w-full items-center justify-center">
<div className="flex w-full max-w-5xl items-center justify-between">
<Link to="/">
<DataRush />
</Link>
<div className="flex items-center gap-1">
<span className="text-lg font-semibold">itqdev</span>
<ChevronDown size={20} />
</div>
</div>
</header>
);
};
export { Header };
+18 -18
View File
@@ -1,18 +1,15 @@
import * as React from "react"
import * as React from "react";
import { cn } from "@/shared/lib/utils"
import { cn } from "@/shared/lib/utils";
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col rounded-xl border shadow-sm",
className
)}
className={cn("bg-card flex flex-col rounded-xl", className)}
{...props}
/>
)
);
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
@@ -22,7 +19,7 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
className={cn("flex flex-col gap-1.5 px-6", className)}
{...props}
/>
)
);
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
@@ -32,7 +29,7 @@ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
className={cn("leading-none font-semibold", className)}
{...props}
/>
)
);
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
@@ -42,17 +39,13 @@ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
);
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
)
<div data-slot="card-content" className={cn("p-5", className)} {...props} />
);
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
@@ -62,7 +55,14 @@ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
className={cn("flex items-center px-6", className)}
{...props}
/>
)
);
}
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardDescription,
CardContent,
};
@@ -0,0 +1,22 @@
const DataRush = ({ size = 52 }: { size?: number }) => {
return (
<svg
height={size}
viewBox="0 0 149 52"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect width="149" height="52" fill="#333333" />
<path
d="M28.296 26.6C28.296 31.352 25.032 35 19.872 35H13.56V18.2H19.872C25.032 18.2 28.296 21.848 28.296 26.6ZM24.264 26.624C24.264 24.056 22.512 22.208 19.8 22.208H17.592V30.992H19.8C22.512 30.992 24.264 29.168 24.264 26.624ZM28.0838 35L34.2758 18.2H38.3318L44.4998 35H40.3958L38.9798 31.136H33.6038L32.1878 35H28.0838ZM34.7078 27.872H37.8518L36.2918 23.6L34.7078 27.872ZM56.4529 18.2V21.848H51.8209V35H47.8129V21.848H43.1809V18.2H56.4529ZM55.1072 35L61.2992 18.2H65.3552L71.5232 35H67.4192L66.0032 31.136H60.6272L59.2112 35H55.1072ZM61.7312 27.872H64.8752L63.3152 23.6L61.7312 27.872Z"
fill="#FFDD2D"
/>
<path
d="M73.3256 18.2H79.5656C82.8296 18.2 85.4216 20.792 85.4216 24.032C85.4216 26.336 84.0056 28.304 81.9656 29.144C83.0456 29.456 83.8376 31.52 85.3256 31.52C85.5896 31.52 85.8536 31.472 86.1656 31.352V35C85.4456 35.192 84.7976 35.288 84.2216 35.288C80.1656 35.288 79.5176 30.32 78.1976 29.744H77.3576V35H73.3256V18.2ZM77.3576 21.848V26.096H79.1576C80.3576 26.096 81.3896 25.232 81.3896 23.984C81.3896 22.76 80.3576 21.848 79.1576 21.848H77.3576ZM103.378 18.2V28.04C103.378 32.216 100.282 35.36 95.7216 35.36C91.1616 35.36 88.0416 32.216 88.0416 28.04V18.2H92.0736V28.232C92.0736 30.248 93.7296 31.616 95.7216 31.616C97.7136 31.616 99.3696 30.248 99.3696 28.232V18.2H103.378ZM105.556 32.576L107.668 30.128C108.964 31.472 110.452 32 111.628 32C113.044 32 114.004 31.184 114.004 30.056C114.004 27.368 106.18 29.312 106.18 22.736C106.18 20.048 108.268 17.84 111.964 17.84C114.412 17.84 115.828 18.68 117.724 20.336L115.612 22.808C114.292 21.608 113.404 21.032 111.94 21.032C110.74 21.032 109.996 21.536 109.996 22.616C109.996 25.376 117.844 23.576 117.844 30.056C117.844 32.816 115.9 35.36 111.628 35.36C109.372 35.36 107.476 34.544 105.556 32.576ZM120.13 18.2H124.138V24.488H131.386V18.2H135.394V35H131.386V28.136H124.138V35H120.13V18.2Z"
fill="white"
/>
</svg>
);
};
export { DataRush };
+15 -21
View File
@@ -1,7 +1,7 @@
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import * as React from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs";
import { cn } from "@/shared/lib/utils"
import { cn } from "@/shared/lib/utils";
function Tabs({
className,
@@ -10,10 +10,10 @@ function Tabs({
return (
<TabsPrimitive.Root
data-slot="tabs"
className={cn("flex flex-col", className)}
className={cn("flex flex-col gap-2", className)}
{...props}
/>
)
);
}
function TabsList({
@@ -24,34 +24,28 @@ function TabsList({
<TabsPrimitive.List
data-slot="tabs-list"
className={cn(
"inline-flex items-center justify-center gap-6",
className
"inline-flex w-fit items-center justify-center gap-6 rounded-lg",
className,
)}
{...props}
/>
)
);
}
function TabsTrigger({
className,
value,
...props
}: React.ComponentProps<typeof TabsPrimitive.Trigger> & { value: string }) {
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
return (
<TabsPrimitive.Trigger
data-slot="tabs-trigger"
value={value}
className={cn(
"relative px-1 py-2 text-sm font-medium outline-none",
"text-gray-500",
"data-[state=active]:font-semibold after:absolute after:bottom-0 after:left-0 after:right-0 after:h-0.5",
value === "ongoing" && "data-[state=active]:text-yellow-500 data-[state=active]:after:bg-yellow-500",
value === "completed" && "data-[state=active]:text-green-500 data-[state=active]:after:bg-green-500",
className
"data-[state=active]:text-foreground text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring data-[state=active]:border-primary-foreground inline-flex cursor-pointer items-center justify-center border-b-2 border-transparent pt-3 pb-[18px] text-base font-semibold whitespace-nowrap focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50",
className,
)}
{...props}
/>
)
);
}
function TabsContent({
@@ -61,10 +55,10 @@ function TabsContent({
return (
<TabsPrimitive.Content
data-slot="tabs-content"
className={cn("mt-2 outline-none", className)}
className={cn("flex-1 outline-none", className)}
{...props}
/>
)
);
}
export { Tabs, TabsList, TabsTrigger, TabsContent }
export { Tabs, TabsList, TabsTrigger, TabsContent };
@@ -1,24 +0,0 @@
import { ChevronDown } from "lucide-react";
const Navbar = () => {
return (
<nav className="bg-white border-b border-gray-200 py-3 px-4 fixed top-0 left-0 right-0 z-10">
<div className="container mx-auto flex justify-between items-center">
<div className="flex items-center">
<div className="bg-black px-3 py-2 rounded font-hse-sans">
<span className="font-bold text-yellow-400">DATA</span>
<span className="font-bold text-white">RUSH</span>
</div>
</div>
<div className="flex items-center cursor-pointer">
<span className="mr-2 font-semibold font-hse-sans">itqdev</span>
<ChevronDown size={16} />
</div>
</div>
</nav>
);
};
export default Navbar
@@ -1,12 +1,11 @@
import { useEffect, useState } from "react";
import { useParams, useNavigate } from "react-router-dom";
import Navbar from "@/modules/Navbar";
import Navbar from "@/widgets/Navbar";
import { Button } from "@/components/ui/button";
import { ArrowLeft } from "lucide-react";
import { Competition } from "@/shared/types/types";
import { Competition } from "@/shared/types";
import { mockCompetitions, mockTasks } from "@/shared/mocks/mocks";
const CompetitionPreview = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
@@ -17,7 +16,7 @@ const CompetitionPreview = () => {
const fetchCompetition = async () => {
try {
setTimeout(() => {
const found = mockCompetitions.find(comp => comp.id === id);
const found = mockCompetitions.find((comp) => comp.id === id);
setCompetition(found || null);
setIsLoading(false);
}, 500);
@@ -37,7 +36,7 @@ const CompetitionPreview = () => {
const handleContinue = () => {
if (competition?.id) {
const competitionTasks = mockTasks[competition.id];
if (competitionTasks && competitionTasks.length > 0) {
const firstTaskId = competitionTasks[0].id;
navigate(`/competition/${competition.id}/tasks/${firstTaskId}`);
@@ -50,49 +49,55 @@ const CompetitionPreview = () => {
return (
<>
<Navbar />
<div className="container mx-auto px-4 py-8 mt-16">
<button
<div className="container mx-auto mt-16 px-4 py-8">
<button
onClick={handleBack}
className="flex items-center text-gray-600 mb-8 font-hse-sans"
className="font-hse-sans mb-8 flex items-center text-gray-600"
>
<ArrowLeft size={16} className="mr-2" />
Назад к соревнованиям
</button>
{isLoading ? (
<div className="flex justify-center items-center h-64">
<div className="flex h-64 items-center justify-center">
<p className="font-hse-sans text-gray-500">Загрузка...</p>
</div>
) : competition ? (
<div className="max-w-5xl mx-auto bg-white rounded-lg overflow-hidden shadow-lg">
<div className="w-full h-80 overflow-hidden">
<img
src={competition.imageUrl}
<div className="mx-auto max-w-5xl overflow-hidden rounded-lg bg-white shadow-lg">
<div className="h-80 w-full overflow-hidden">
<img
src={competition.imageUrl}
alt={competition.name}
className="w-full h-full object-cover"
className="h-full w-full object-cover"
/>
</div>
<div className="p-8">
<div className="flex justify-between items-center mb-8">
<h1 className="text-3xl font-semibold font-hse-sans mr-6 flex-1">{competition.name}</h1>
<Button
className="bg-yellow-400 hover:bg-yellow-500 text-black font-hse-sans text-base px-12 min-w-[180px]"
<div className="mb-8 flex items-center justify-between">
<h1 className="font-hse-sans mr-6 flex-1 text-3xl font-semibold">
{competition.name}
</h1>
<Button
className="font-hse-sans min-w-[180px] bg-yellow-400 px-12 text-base text-black hover:bg-yellow-500"
onClick={handleContinue}
>
Продолжить
</Button>
</div>
<div className="text-gray-700 font-hse-sans text-lg leading-relaxed">
<div className="font-hse-sans text-lg leading-relaxed text-gray-700">
<p>{competition.description}</p>
</div>
</div>
</div>
) : (
<div className="text-center py-12">
<h2 className="text-2xl font-bold mb-2 font-hse-sans">Соревнование не найдено</h2>
<p className="text-gray-600 font-hse-sans">Запрошенное соревнование не существует или было удалено.</p>
<div className="py-12 text-center">
<h2 className="font-hse-sans mb-2 text-2xl font-bold">
Соревнование не найдено
</h2>
<p className="font-hse-sans text-gray-600">
Запрошенное соревнование не существует или было удалено.
</p>
</div>
)}
</div>
@@ -100,4 +105,4 @@ const CompetitionPreview = () => {
);
};
export default CompetitionPreview;
export default CompetitionPreview;
@@ -1,9 +1,7 @@
import { useState } from "react";
import { useParams } from "react-router-dom";
import Navbar from "@/modules/Navbar";
import { Task, TaskStatus } from "@/shared/types/types";
import Navbar from "@/widgets/Navbar";
import { Task, TaskStatus } from "@/shared/types";
const sampleTasks: Task[] = [
{ id: "1", number: "1.1", status: "uncleared" },
@@ -18,27 +16,39 @@ const sampleTasks: Task[] = [
const CompetitionRunnerPage = () => {
const { id } = useParams<{ id: string }>();
const [competitionTitle, setCompetitionTitle] = useState("Олимпиада DANO 2025. Индивидуальный этап");
const [competitionTitle, setCompetitionTitle] = useState(
"Олимпиада DANO 2025. Индивидуальный этап",
);
const [tasks, setTasks] = useState<Task[]>(sampleTasks);
const [selectedTaskId, setSelectedTaskId] = useState<string | null>(null);
const getTaskBgColor = (status: TaskStatus): string => {
switch (status) {
case "uncleared": return "bg-[var(--color-task-uncleared)]";
case "checking": return "bg-[var(--color-task-checking)]";
case "correct": return "bg-[var(--color-task-correct)]";
case "partial": return "bg-[var(--color-task-partial)]";
case "wrong": return "bg-[var(--color-task-wrong)]";
case "uncleared":
return "bg-[var(--color-task-uncleared)]";
case "checking":
return "bg-[var(--color-task-checking)]";
case "correct":
return "bg-[var(--color-task-correct)]";
case "partial":
return "bg-[var(--color-task-partial)]";
case "wrong":
return "bg-[var(--color-task-wrong)]";
}
};
const getTaskTextColor = (status: TaskStatus): string => {
switch (status) {
case "uncleared": return "text-gray-600";
case "checking": return "text-gray-800";
case "correct": return "text-green-800";
case "partial": return "text-green-700";
case "wrong": return "text-red-800";
case "uncleared":
return "text-gray-600";
case "checking":
return "text-gray-800";
case "correct":
return "text-green-800";
case "partial":
return "text-green-700";
case "wrong":
return "text-red-800";
}
};
@@ -49,21 +59,20 @@ const CompetitionRunnerPage = () => {
return (
<>
<Navbar />
<div className="sticky top-16 z-10 bg-white border-b border-gray-200 shadow-sm">
<div className="sticky top-16 z-10 border-b border-gray-200 bg-white shadow-sm">
<div className="container mx-auto px-4">
<div className="py-4">
<h1 className="text-xl font-semibold font-hse-sans">{competitionTitle}</h1>
<h1 className="font-hse-sans text-xl font-semibold">
{competitionTitle}
</h1>
</div>
<div className="flex items-center gap-3 pb-4 overflow-x-auto scrollbar-thin scrollbar-thumb-gray-300">
<div className="scrollbar-thin scrollbar-thumb-gray-300 flex items-center gap-3 overflow-x-auto pb-4">
{tasks.map((task) => (
<div
<div
key={task.id}
className={`${getTaskBgColor(task.status)} ${getTaskTextColor(task.status)}
rounded-lg px-4 py-2 font-medium text-sm font-hse-sans cursor-pointer
transition-transform hover:scale-105 flex-shrink-0
${selectedTaskId === task.id ? 'ring-2 ring-black' : ''}`}
className={`${getTaskBgColor(task.status)} ${getTaskTextColor(task.status)} font-hse-sans flex-shrink-0 cursor-pointer rounded-lg px-4 py-2 text-sm font-medium transition-transform hover:scale-105 ${selectedTaskId === task.id ? "ring-2 ring-black" : ""}`}
onClick={() => handleTaskClick(task.id)}
>
{task.number}
@@ -72,13 +81,13 @@ const CompetitionRunnerPage = () => {
</div>
</div>
</div>
<div className="container mx-auto px-4 py-8">
<div className="bg-white rounded-lg p-6 shadow-sm">
<div className="rounded-lg bg-white p-6 shadow-sm">
{selectedTaskId ? (
<div className="font-hse-sans">
<h2 className="text-lg font-medium mb-4">
Задание {tasks.find(t => t.id === selectedTaskId)?.number}
<h2 className="mb-4 text-lg font-medium">
Задание {tasks.find((t) => t.id === selectedTaskId)?.number}
</h2>
<p className="text-gray-700">
Содержание задания будет отображаться здесь.
@@ -95,4 +104,4 @@ const CompetitionRunnerPage = () => {
);
};
export default CompetitionRunnerPage;
export default CompetitionRunnerPage;
@@ -1,55 +1,47 @@
import { Competition } from "@/shared/types/types";
import { Competition, CompetitionStatus } from "@/shared/types";
import { cn } from "@/shared/lib/utils";
import {
Card,
CardContent,
CardFooter,
} from "@/components/ui/card";
import { useNavigate } from "react-router";
import { Card, CardContent } from "@/components/ui/card";
interface CompetitionCardProps {
competition: Competition;
className?: string;
}
export function CompetitionCard({ competition, className }: CompetitionCardProps) {
const { id, name, imageUrl, isOlympics, status } = competition;
const navigate = useNavigate();
const handleClick = () => {
navigate(`/competition/${id}`);
};
export function CompetitionCard({
competition,
className,
}: CompetitionCardProps) {
return (
<Card
className={cn("overflow-hidden h-full", className)}
onClick={handleClick}
<Card
className={cn(
"aspect-square h-full max-h-80 w-auto overflow-hidden",
className,
)}
>
<div className="relative h-48 overflow-hidden">
<img
src={imageUrl}
alt={name}
className="w-full h-full object-cover transition-transform duration-300 hover:scale-105"
<div className="relative h-full overflow-hidden">
<img
src={competition.imageUrl}
alt={competition.name}
className="h-full w-full object-cover object-center"
/>
</div>
<CardFooter className="p-4 pb-0 flex items-center text-xs font-medium font-hse-sans">
<span className="text-gray-500">
{isOlympics ? "Олимпиада" : "Тренировка"}
</span>
<span className="mx-2 w-1.5 h-1.5 rounded-full bg-gray-300"></span>
<span className={cn(
status === 'В процессе' && "text-yellow-500",
status === 'Завершено' && "text-green-500",
status === 'Не участвую' && "text-gray-500"
)}>
{status.replace(/^\w/, c => c.toUpperCase())}
</span>
</CardFooter>
<CardContent className="p-4 pt-2">
<h3 className="font-semibold text-lg line-clamp-2 font-hse-sans">{name}</h3>
<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></span>
<span className="text-primary-foreground">
{competition.status}
</span>
</>
)}
</div>
<h3 className="text-xl font-semibold">{competition.name}</h3>
</div>
</CardContent>
</Card>
);
}
}
@@ -1,98 +1,122 @@
import { useState, useEffect } from 'react';
import { Competition, Status } from '@/shared/types/types';
import { CompetitionGrid } from './modules/CompetitionGrid';
import { Alert, AlertDescription } from "@/components/ui/alert";
import { AlertCircle } from "lucide-react";
import { useState, useEffect } from "react";
import { Competition, CompetitionStatus } from "@/shared/types";
import { CompetitionGrid } from "./modules/CompetitionGrid";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import Navbar from '@/modules/Navbar';
import { mockCompetitions } from '@/shared/mocks/mocks';
const mockCompetitions: Competition[] = [
{
id: "1",
name: "Олимпиада DANO 2025. Индивидуальный этап",
imageUrl: "/DANO.png",
isOlympics: true,
status: CompetitionStatus.InProgress,
},
{
id: "2",
name: "Олимпиада DANO 2025. Индивидуальный этап",
imageUrl: "/DANO.png",
isOlympics: false,
status: CompetitionStatus.NotParticipating,
},
{
id: "3",
name: "Олимпиада DANO 2025. Индивидуальный этап",
imageUrl: "/DANO.png",
isOlympics: false,
status: CompetitionStatus.InProgress,
},
{
id: "4",
name: "Олимпиада DANO 2025. Индивидуальный этап",
imageUrl: "/DANO.png",
isOlympics: true,
status: CompetitionStatus.Completed,
},
{
id: "5",
name: "Олимпиада DANO 2025. Индивидуальный этап",
imageUrl: "/DANO.png",
isOlympics: false,
status: CompetitionStatus.Completed,
},
{
id: "6",
name: "Олимпиада DANO 2025. Индивидуальный этап",
imageUrl: "/DANO.png",
isOlympics: true,
status: CompetitionStatus.NotParticipating,
},
{
id: "6",
name: "Олимпиада DANO 2025. Индивидуальный этап",
imageUrl: "/DANO.png",
isOlympics: true,
status: CompetitionStatus.NotParticipating,
},
{
id: "6",
name: "Олимпиада DANO 2025. Индивидуальный этап",
imageUrl: "/DANO.png",
isOlympics: true,
status: CompetitionStatus.NotParticipating,
},
];
const CompetitionsPage = () => {
const [competitions, setCompetitions] = useState<Competition[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [competitions] = useState<Competition[]>(mockCompetitions);
const [activeTab, setActiveTab] = useState("ongoing");
useEffect(() => {
// ! симуляция фетча
const fetchCompetitions = async () => {
try {
setTimeout(() => {
setCompetitions(mockCompetitions);
setIsLoading(false);
}, 800);
} catch (error) {
setError('Соревнования не найдены, пожалуйста, попробуйте позже');
setIsLoading(false);
}
};
fetchCompetitions();
}, []);
const myCompetitions = competitions.filter(comp =>
comp.status === Status.InProgress || comp.status === Status.Completed
const myCompetitions = competitions.filter(
(comp) =>
comp.status === CompetitionStatus.InProgress ||
comp.status === CompetitionStatus.Completed,
);
const filteredMyCompetitions = myCompetitions.filter(comp =>
activeTab === "ongoing" ? comp.status === Status.InProgress : comp.status === Status.Completed
const filteredMyCompetitions = myCompetitions.filter((comp) =>
activeTab === "ongoing"
? comp.status === CompetitionStatus.InProgress
: comp.status === CompetitionStatus.Completed,
);
const availableCompetitions = competitions.filter(comp =>
comp.status === 'Не участвую'
const availableCompetitions = competitions.filter(
(comp) => comp.status === "Не участвую",
);
return (
<>
<Navbar />
<div className="container mx-auto px-4 py-8 mt-16">
{error && (
<Alert variant="destructive" className="mb-6">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<div className="mb-12">
<div className="flex justify-between items-center mb-6">
<h2 className="text-2xl font-semibold font-hse-sans">Мои события</h2>
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-auto">
<TabsList>
<TabsTrigger value="ongoing" className="font-hse-sans">Текущие</TabsTrigger>
<TabsTrigger value="completed" className="font-hse-sans">Завершенные</TabsTrigger>
</TabsList>
</Tabs>
</div>
{isLoading ? (
<CompetitionGrid competitions={[]} isLoading={true} />
) : filteredMyCompetitions.length > 0 ? (
<CompetitionGrid competitions={filteredMyCompetitions} isLoading={false} />
) : (
<div className="flex justify-center items-center h-40 bg-gray-50 rounded-lg">
<p className="text-gray-500 font-hse-sans">
{activeTab === "ongoing" ? "У вас нет текущих соревнований" : "У вас нет завершенных соревнований"}
</p>
</div>
)}
</div>
<div>
<h2 className="text-2xl font-semibold mb-6 font-hse-sans">Доступные события</h2>
{isLoading ? (
<CompetitionGrid competitions={[]} isLoading={true} />
) : availableCompetitions.length > 0 ? (
<CompetitionGrid competitions={availableCompetitions} isLoading={false} />
) : (
<div className="flex justify-center items-center h-40 bg-gray-50 rounded-lg">
<p className="text-gray-500 font-hse-sans">Нет доступных соревнований</p>
</div>
)}
</div>
</div>
</>
);
}
<div className="flex flex-col gap-8">
<Section>
<SectionHeader>
<SectionTitle>Мои события</SectionTitle>
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList>
<TabsTrigger value="ongoing">В процессе</TabsTrigger>
<TabsTrigger value="completed">Завершенные</TabsTrigger>
</TabsList>
</Tabs>
</SectionHeader>
<CompetitionGrid competitions={filteredMyCompetitions} />
</Section>
export default CompetitionsPage;
<Section>
<SectionHeader>
<SectionTitle>События</SectionTitle>
</SectionHeader>
<CompetitionGrid competitions={availableCompetitions} />
</Section>
</div>
);
};
const Section = ({ children }: { children: React.ReactNode }) => {
return <div className="flex flex-col gap-8">{children}</div>;
};
const SectionHeader = ({ children }: { children: React.ReactNode }) => {
return <div className="flex h-[58px] items-center gap-2">{children}</div>;
};
const SectionTitle = ({ children }: { children: React.ReactNode }) => {
return <h1 className="w-full text-3xl font-semibold">{children}</h1>;
};
export default CompetitionsPage;
@@ -1,46 +1,16 @@
import { Competition } from "@/shared/types/types";
import { Competition } from "@/shared/types";
import { CompetitionCard } from "../../components/CompetitionCard";
import CompetitionSkeleton from "../../components/CompetitionSkeleton";
import { cn } from "@/shared/lib/utils";
interface CompetitionGridProps {
competitions: Competition[];
isLoading?: boolean;
className?: string;
skeletonCount?: number;
}
export function CompetitionGrid({
competitions,
isLoading = false,
className,
skeletonCount
}: CompetitionGridProps) {
const gridClasses = cn(
"grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6",
className
);
const numberOfSkeletons = skeletonCount ?? (competitions.length > 0 ? competitions.length : 4);
if (isLoading) {
return (
<div className={gridClasses}>
{Array.from({ length: numberOfSkeletons }).map((_, index) => (
<CompetitionSkeleton key={index} />
))}
</div>
);
}
export function CompetitionGrid({ competitions }: CompetitionGridProps) {
return (
<div className={gridClasses}>
<div className="grid grid-cols-3 gap-9">
{competitions.map((competition) => (
<CompetitionCard
key={competition.id}
competition={competition}
/>
<CompetitionCard key={competition.id} competition={competition} />
))}
</div>
);
}
}
+34 -32
View File
@@ -1,62 +1,64 @@
import { Competition, Status } from "../types/types";
import { Competition, CompetitionStatus } from "../types";
const mockCompetitions: Competition[] = [
{
id: '1',
name: 'Олимпиада DANO 2025. Индивидуальный этап',
imageUrl: '/DANO.png',
id: "1",
name: "Олимпиада DANO 2025. Индивидуальный этап",
imageUrl: "/DANO.png",
isOlympics: true,
status: Status.InProgress,
description: 'Проверка глубоких знаний и навыков в анализе данных. Будет несколько творческих заданий со свободным ответом. Задания выполняются индивидуально, вес тура в итоговом результате – 0,5. Этап пройдет онлайн в заданное время, с применением системы прокторинга. На работу дается 240 минут.'
status: CompetitionStatus.InProgress,
description:
"Проверка глубоких знаний и навыков в анализе данных. Будет несколько творческих заданий со свободным ответом. Задания выполняются индивидуально, вес тура в итоговом результате – 0,5. Этап пройдет онлайн в заданное время, с применением системы прокторинга. На работу дается 240 минут.",
},
{
id: '2',
name: 'Олимпиада DANO 2025. Индивидуальный этап',
imageUrl: '/DANO.png',
id: "2",
name: "Олимпиада DANO 2025. Индивидуальный этап",
imageUrl: "/DANO.png",
isOlympics: false,
status: Status.NotParticipating,
description: 'Индивидуальный этап олимпиады DANO 2025 – это уникальная возможность для студентов продемонстрировать свои навыки анализа данных и решения сложных задач. Участники будут работать с реальными наборами данных и применять современные методы машинного обучения и статистического анализа.'
status: CompetitionStatus.NotParticipating,
description:
"Индивидуальный этап олимпиады DANO 2025 – это уникальная возможность для студентов продемонстрировать свои навыки анализа данных и решения сложных задач. Участники будут работать с реальными наборами данных и применять современные методы машинного обучения и статистического анализа.",
},
{
id: '3',
name: 'Олимпиада DANO 2025. Индивидуальный этап',
imageUrl: '/DANO.png',
id: "3",
name: "Олимпиада DANO 2025. Индивидуальный этап",
imageUrl: "/DANO.png",
isOlympics: false,
status: Status.InProgress
status: CompetitionStatus.InProgress,
},
{
id: '4',
name: 'Олимпиада DANO 2025. Индивидуальный этап',
imageUrl: '/DANO.png',
id: "4",
name: "Олимпиада DANO 2025. Индивидуальный этап",
imageUrl: "/DANO.png",
isOlympics: true,
status: Status.Completed
status: CompetitionStatus.Completed,
},
{
id: '5',
name: 'Олимпиада DANO 2025. Индивидуальный этап',
imageUrl: '/DANO.png',
id: "5",
name: "Олимпиада DANO 2025. Индивидуальный этап",
imageUrl: "/DANO.png",
isOlympics: false,
status: Status.Completed
status: CompetitionStatus.Completed,
},
{
id: '6',
name: 'Олимпиада DANO 2025. Индивидуальный этап',
imageUrl: '/DANO.png',
id: "6",
name: "Олимпиада DANO 2025. Индивидуальный этап",
imageUrl: "/DANO.png",
isOlympics: true,
status: Status.NotParticipating
}
status: CompetitionStatus.NotParticipating,
},
];
const mockTasks = {
'1': [
"1": [
{ id: "1.1", number: "1.1", status: "uncleared" },
{ id: "1.2", number: "1.2", status: "checking" },
{ id: "1.3", number: "1.3", status: "correct" },
],
'2': [
"2": [
{ id: "2.1", number: "1.1", status: "uncleared" },
{ id: "2.2", number: "1.2", status: "uncleared" },
]
],
};
export { mockCompetitions, mockTasks }
export { mockCompetitions, mockTasks };
+25
View File
@@ -0,0 +1,25 @@
enum CompetitionStatus {
InProgress = "В процессе",
NotParticipating = "Не участвую",
Completed = "Завершено",
}
interface Competition {
id: string;
name: string;
imageUrl: string;
isOlympics: boolean;
status: CompetitionStatus;
description?: string;
}
type TaskStatus = "uncleared" | "checking" | "correct" | "partial" | "wrong";
interface Task {
id: string;
number: string;
status: TaskStatus;
}
export { CompetitionStatus };
export type { Competition, TaskStatus, Task };
@@ -1,25 +0,0 @@
enum Status {
InProgress = 'В процессе',
NotParticipating = 'Не участвую',
Completed = 'Завершено'
}
interface Competition {
id: string;
name: string;
imageUrl: string;
isOlympics: boolean;
status: Status;
description?: string;
}
type TaskStatus = "uncleared" | "checking" | "correct" | "partial" | "wrong";
interface Task {
id: string;
number: string;
status: TaskStatus;
}
export {Status}
export type {Competition, TaskStatus, Task}
+6 -7
View File
@@ -5,14 +5,14 @@
@custom-variant dark (&:is(.dark *));
:root {
--background: oklch(1 0 0);
--background: oklch(0.97 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--primary: oklch(89.97% 0.1763 97.07);
--primary-foreground: oklch(82.87% 0.1701 94.8);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
@@ -41,7 +41,7 @@
}
@theme inline {
--font-hse-sans: "HSE Sans", system-ui, sans-serif
--font-hse-sans: "HSE Sans", system-ui, sans-serif;
}
.dark {
--background: oklch(0.145 0 0);
@@ -76,7 +76,7 @@
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(0.269 0 0);
--sidebar-ring: oklch(0.439 0 0);
--task-uncleared: oklch(0.955 0 0);
--task-checking: oklch(0.899 0.1763 97.07);
--task-correct: oklch(0.962 0.0561 158.62);
@@ -127,12 +127,11 @@
--color-task-correct: var(--task-correct);
--color-task-partial: var(--task-partial);
--color-task-wrong: var(--task-wrong);
}
@layer base {
* {
@apply border-border outline-ring/50;
@apply border-border outline-ring/50 font-hse-sans;
}
body {
@apply bg-background text-foreground;
@@ -0,0 +1,15 @@
import { Header } from "@/components/layout/header";
import { Outlet } from "react-router";
const NavbarLayout = () => {
return (
<>
<Header />
<div className="m-auto mt-6 w-full max-w-5xl">
<Outlet />
</div>
</>
);
};
export { NavbarLayout };
-11
View File
@@ -1,11 +0,0 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
theme: {
extend: {
fontFamily: {
'hse-sans': ['"HSE Sans"', 'system-ui', 'sans-serif'],
},
},
},
plugins: [],
}