-
- Задание {tasks.find(t => t.id === selectedTaskId)?.number}
+
+ Задание {tasks.find((t) => t.id === selectedTaskId)?.number}
Содержание задания будет отображаться здесь.
@@ -95,4 +104,4 @@ const CompetitionRunnerPage = () => {
);
};
-export default CompetitionRunnerPage;
\ No newline at end of file
+export default CompetitionRunnerPage;
diff --git a/services/frontend/src/pages/CompetitionsPage/components/CompetitionCard/index.tsx b/services/frontend/src/pages/CompetitionsPage/components/CompetitionCard/index.tsx
index 5c06328..2e65038 100644
--- a/services/frontend/src/pages/CompetitionsPage/components/CompetitionCard/index.tsx
+++ b/services/frontend/src/pages/CompetitionsPage/components/CompetitionCard/index.tsx
@@ -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 (
-
-
-

+
-
-
-
- {isOlympics ? "Олимпиада" : "Тренировка"}
-
-
-
- {status.replace(/^\w/, c => c.toUpperCase())}
-
-
-
-
- {name}
+
+
+
+
+ {competition.isOlympics ? "Олимпиада" : "Тренировка"}
+ {competition.status != CompetitionStatus.NotParticipating && (
+ <>
+ •
+
+ {competition.status}
+
+ >
+ )}
+
+
{competition.name}
+
);
-}
\ No newline at end of file
+}
diff --git a/services/frontend/src/pages/CompetitionsPage/index.tsx b/services/frontend/src/pages/CompetitionsPage/index.tsx
index 3996b7b..f7c208b 100644
--- a/services/frontend/src/pages/CompetitionsPage/index.tsx
+++ b/services/frontend/src/pages/CompetitionsPage/index.tsx
@@ -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([]);
- const [isLoading, setIsLoading] = useState(true);
- const [error, setError] = useState(null);
+ const [competitions] = useState(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 (
- <>
-
-
- {error && (
-
-
- {error}
-
- )}
-
-
-
-
Мои события
-
-
- Текущие
- Завершенные
-
-
-
-
- {isLoading ? (
-
- ) : filteredMyCompetitions.length > 0 ? (
-
- ) : (
-
-
- {activeTab === "ongoing" ? "У вас нет текущих соревнований" : "У вас нет завершенных соревнований"}
-
-
- )}
-
-
-
-
Доступные события
-
- {isLoading ? (
-
- ) : availableCompetitions.length > 0 ? (
-
- ) : (
-
-
Нет доступных соревнований
-
- )}
-
-
- >
- );
-}
+
+
+
+ Мои события
+
+
+ В процессе
+ Завершенные
+
+
+
+
+
-export default CompetitionsPage;
\ No newline at end of file
+
+
+ );
+};
+
+const Section = ({ children }: { children: React.ReactNode }) => {
+ return {children}
;
+};
+
+const SectionHeader = ({ children }: { children: React.ReactNode }) => {
+ return {children}
;
+};
+
+const SectionTitle = ({ children }: { children: React.ReactNode }) => {
+ return {children}
;
+};
+
+export default CompetitionsPage;
diff --git a/services/frontend/src/pages/CompetitionsPage/modules/CompetitionGrid/index.tsx b/services/frontend/src/pages/CompetitionsPage/modules/CompetitionGrid/index.tsx
index 7bcc1fa..19b376c 100644
--- a/services/frontend/src/pages/CompetitionsPage/modules/CompetitionGrid/index.tsx
+++ b/services/frontend/src/pages/CompetitionsPage/modules/CompetitionGrid/index.tsx
@@ -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 (
-
- {Array.from({ length: numberOfSkeletons }).map((_, index) => (
-
- ))}
-
- );
- }
-
+export function CompetitionGrid({ competitions }: CompetitionGridProps) {
return (
-
+
{competitions.map((competition) => (
-
+
))}
);
-}
\ No newline at end of file
+}
diff --git a/services/frontend/src/shared/mocks/mocks.ts b/services/frontend/src/shared/mocks/mocks.ts
index ac54fe8..b05ad67 100644
--- a/services/frontend/src/shared/mocks/mocks.ts
+++ b/services/frontend/src/shared/mocks/mocks.ts
@@ -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 }
\ No newline at end of file
+export { mockCompetitions, mockTasks };
diff --git a/services/frontend/src/shared/types.ts b/services/frontend/src/shared/types.ts
new file mode 100644
index 0000000..3732350
--- /dev/null
+++ b/services/frontend/src/shared/types.ts
@@ -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 };
diff --git a/services/frontend/src/shared/types/types.ts b/services/frontend/src/shared/types/types.ts
deleted file mode 100644
index d670376..0000000
--- a/services/frontend/src/shared/types/types.ts
+++ /dev/null
@@ -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}
\ No newline at end of file
diff --git a/services/frontend/src/styles/globals.css b/services/frontend/src/styles/globals.css
index bcfe1c4..f7807bf 100644
--- a/services/frontend/src/styles/globals.css
+++ b/services/frontend/src/styles/globals.css
@@ -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;
diff --git a/services/frontend/src/widgets/navbar-layout.tsx b/services/frontend/src/widgets/navbar-layout.tsx
new file mode 100644
index 0000000..3b40070
--- /dev/null
+++ b/services/frontend/src/widgets/navbar-layout.tsx
@@ -0,0 +1,15 @@
+import { Header } from "@/components/layout/header";
+import { Outlet } from "react-router";
+
+const NavbarLayout = () => {
+ return (
+ <>
+
+
+
+
+ >
+ );
+};
+
+export { NavbarLayout };
diff --git a/services/frontend/tailwind.config.js b/services/frontend/tailwind.config.js
deleted file mode 100644
index 50cd5f6..0000000
--- a/services/frontend/tailwind.config.js
+++ /dev/null
@@ -1,11 +0,0 @@
-/** @type {import('tailwindcss').Config} */
-module.exports = {
- theme: {
- extend: {
- fontFamily: {
- 'hse-sans': ['"HSE Sans"', 'system-ui', 'sans-serif'],
- },
- },
- },
- plugins: [],
-}