diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..485dee6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 29ea8e2..c9a3f05 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -25,6 +25,7 @@ variables: --destination "${IMAGE_NAME}:latest" --cache=true --registry-mirror=dockerhub.timeweb.cloud + retry: 2 build_frontend: <<: *build-template @@ -84,3 +85,4 @@ deploy: docker compose ps >> deploy.log 2>&1 EOF - ssh $SSH_ADDRESS "docker system prune -a --force" + retry: 2 diff --git a/services/backend/api/v1/user/views.py b/services/backend/api/v1/user/views.py index ff49988..6eb821b 100644 --- a/services/backend/api/v1/user/views.py +++ b/services/backend/api/v1/user/views.py @@ -1,5 +1,6 @@ from http import HTTPStatus as status +from django.contrib.auth.hashers import check_password from django.shortcuts import get_object_or_404 from ninja import Router from ninja.errors import AuthenticationError @@ -27,7 +28,6 @@ router = Router(tags=["user"]) ) def sign_up(request, data: RegisterSchema): user = User(**data.dict()) - user.full_clean() user.save() token = BearerAuth.generate_jwt(user) @@ -45,9 +45,10 @@ def sign_up(request, data: RegisterSchema): ) def sign_in(request, data: LoginSchema): user = User.objects.filter(email=data.email).first() + print(check_password(data.password, user.password)) if not user: raise AuthenticationError - if user.password != data.password: + if not check_password(data.password, user.password): raise AuthenticationError token = BearerAuth.generate_jwt(user) diff --git a/services/backend/apps/competition/migrations/0004_alter_competition_description_and_more.py b/services/backend/apps/competition/migrations/0004_alter_competition_description_and_more.py new file mode 100644 index 0000000..d5f462e --- /dev/null +++ b/services/backend/apps/competition/migrations/0004_alter_competition_description_and_more.py @@ -0,0 +1,49 @@ +# Generated by Django 5.1.6 on 2025-03-01 14:46 + +import tinymce.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('competition', '0003_remove_competition_tasks'), + ] + + operations = [ + migrations.AlterField( + model_name='competition', + name='description', + field=tinymce.models.HTMLField(verbose_name='описание'), + ), + migrations.AlterField( + model_name='competition', + name='end_date', + field=models.DateTimeField(blank=True, null=True, verbose_name='дедлайн участия'), + ), + migrations.AlterField( + model_name='competition', + name='image_url', + field=models.FileField(blank=True, null=True, upload_to='', verbose_name='изображение соревнования'), + ), + migrations.AlterField( + model_name='competition', + name='participation_type', + field=models.CharField(choices=[('edu', 'Образовательный'), ('competitive', 'Соревновательный')], max_length=11, verbose_name='тип соревнования'), + ), + migrations.AlterField( + model_name='competition', + name='start_date', + field=models.DateTimeField(blank=True, null=True, verbose_name='дедлайн участия'), + ), + migrations.AlterField( + model_name='competition', + name='title', + field=models.CharField(max_length=100, verbose_name='аазвание'), + ), + migrations.AlterField( + model_name='competition', + name='type', + field=models.CharField(choices=[('solo', 'Индивидуальный')], max_length=10, verbose_name='тип участия'), + ), + ] diff --git a/services/backend/apps/competition/models.py b/services/backend/apps/competition/models.py index f9d91ec..92bf05f 100644 --- a/services/backend/apps/competition/models.py +++ b/services/backend/apps/competition/models.py @@ -15,10 +15,16 @@ class Competition(BaseModel): EDU = "edu", "Образовательный" COMPETITIVE = "competitive", "Соревновательный" - title = models.CharField(max_length=100, verbose_name="аазвание") - description = HTMLField(verbose_name="описание") + def image_url_upload_to(instance, filename): + return f"/competitions/{instance.id}/image" + + title = models.CharField(max_length=100, verbose_name="название") + description = models.TextField(verbose_name="описание") image_url = models.FileField( - verbose_name="изображение соревнования", null=True, blank=True + verbose_name="изображение соревнования", + null=True, + blank=True, + upload_to=image_url_upload_to, ) end_date = models.DateTimeField( verbose_name="дедлайн участия", null=True, blank=True @@ -36,8 +42,9 @@ class Competition(BaseModel): choices=CompetitionParticipationType.choices, verbose_name="тип соревнования", ) - participants = models.ManyToManyField(User, related_name="participants", blank=True, - editable=False) + participants = models.ManyToManyField( + User, related_name="participants", blank=True, editable=False + ) def __str__(self): return self.title diff --git a/services/backend/apps/review/migrations/0002_review.py b/services/backend/apps/review/migrations/0002_review.py new file mode 100644 index 0000000..c9ded38 --- /dev/null +++ b/services/backend/apps/review/migrations/0002_review.py @@ -0,0 +1,26 @@ +# Generated by Django 5.1.6 on 2025-03-01 14:47 + +import django.db.models.deletion +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('review', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Review', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('state', models.CharField(choices=[('not_checked', 'Not Checked'), ('checking', 'Checking'), ('checked', 'Checked')], max_length=11)), + ('reviewer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='review.reviewer')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/services/backend/apps/review/migrations/0003_review_submission.py b/services/backend/apps/review/migrations/0003_review_submission.py new file mode 100644 index 0000000..fd976b0 --- /dev/null +++ b/services/backend/apps/review/migrations/0003_review_submission.py @@ -0,0 +1,20 @@ +# Generated by Django 5.1.6 on 2025-03-01 14:47 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('review', '0002_review'), + ('task', '0005_alter_competitiontask_description_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='review', + name='submission', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='task.competitiontasksubmission'), + ), + ] diff --git a/services/backend/apps/task/migrations/0004_merge_20250301_1739.py b/services/backend/apps/task/migrations/0004_merge_20250301_1739.py new file mode 100644 index 0000000..8d06cf8 --- /dev/null +++ b/services/backend/apps/task/migrations/0004_merge_20250301_1739.py @@ -0,0 +1,14 @@ +# Generated by Django 5.1.6 on 2025-03-01 14:39 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('task', '0002_competetiontasksumbission_reviewers'), + ('task', '0003_competitiontask_max_attemps_and_more'), + ] + + operations = [ + ] diff --git a/services/backend/apps/task/migrations/0005_alter_competitiontask_description_and_more.py b/services/backend/apps/task/migrations/0005_alter_competitiontask_description_and_more.py new file mode 100644 index 0000000..fb0d89d --- /dev/null +++ b/services/backend/apps/task/migrations/0005_alter_competitiontask_description_and_more.py @@ -0,0 +1,48 @@ +# Generated by Django 5.1.6 on 2025-03-01 14:47 + +import apps.task.models +import django.db.models.deletion +import tinymce.models +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('task', '0004_merge_20250301_1739'), + ('user', '0002_alter_user_email_alter_user_password_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='competitiontask', + name='description', + field=tinymce.models.HTMLField(max_length=300, verbose_name='описание'), + ), + migrations.AlterField( + model_name='competitiontask', + name='max_attemps', + field=models.PositiveSmallIntegerField(), + ), + migrations.CreateModel( + name='CompetitionTaskSubmission', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('status', models.CharField(choices=[('sent', 'Sent'), ('checking', 'Checking'), ('checked', 'Checked')], default='sent', max_length=8)), + ('content', models.FileField(upload_to=apps.task.models.CompetitionTaskSubmission.submission_content_upload_to)), + ('stdout', models.FileField(blank=True, null=True, upload_to=apps.task.models.CompetitionTaskSubmission.submission_stdout_upload_to)), + ('result', models.JSONField(blank=True, default=None, null=True)), + ('earned_points', models.IntegerField()), + ('timestamp', models.DateTimeField(auto_now_add=True)), + ('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='task.competitiontask')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='user.user')), + ], + options={ + 'abstract': False, + }, + ), + migrations.DeleteModel( + name='CompetetionTaskSumbission', + ), + ] diff --git a/services/backend/apps/task/models.py b/services/backend/apps/task/models.py index 13dd487..3373809 100644 --- a/services/backend/apps/task/models.py +++ b/services/backend/apps/task/models.py @@ -51,14 +51,6 @@ class CompetitionTask(BaseModel): blank=True, null=True, verbose_name="критерии", - default=lambda: [ - { - "name": "CHANGE ME", - "slug": "CHANGE ME", - "max_value": 0, - "min_value": 0, - } - ], ) def clean(self): diff --git a/services/backend/apps/user/admin.py b/services/backend/apps/user/admin.py new file mode 100644 index 0000000..89dca07 --- /dev/null +++ b/services/backend/apps/user/admin.py @@ -0,0 +1,9 @@ +from django.contrib import admin + +from apps.user.models import User + + +@admin.register(User) +class UserAdmin(admin.ModelAdmin): + list_display = ("email", "username") + search_fields = ("id", "email", "username") diff --git a/services/backend/apps/user/apps.py b/services/backend/apps/user/apps.py index 2f3daa6..dd71f2d 100644 --- a/services/backend/apps/user/apps.py +++ b/services/backend/apps/user/apps.py @@ -5,3 +5,4 @@ class UsersConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" name = "apps.user" label = "user" + verbose_name = "Пользователи" diff --git a/services/backend/apps/user/migrations/0002_alter_user_email_alter_user_password_and_more.py b/services/backend/apps/user/migrations/0002_alter_user_email_alter_user_password_and_more.py new file mode 100644 index 0000000..a733466 --- /dev/null +++ b/services/backend/apps/user/migrations/0002_alter_user_email_alter_user_password_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 5.1.6 on 2025-03-01 14:46 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('user', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='email', + field=models.EmailField(max_length=254, unique=True, verbose_name='почта'), + ), + migrations.AlterField( + model_name='user', + name='password', + field=models.TextField(verbose_name='пароль'), + ), + migrations.AlterField( + model_name='user', + name='username', + field=models.SlugField(unique=True, verbose_name='юзернейм'), + ), + ] diff --git a/services/backend/apps/user/models.py b/services/backend/apps/user/models.py index bc7ce07..f702b6f 100644 --- a/services/backend/apps/user/models.py +++ b/services/backend/apps/user/models.py @@ -1,4 +1,5 @@ from django.db import models +from django.contrib.auth.hashers import check_password, make_password from apps.core.models import BaseModel @@ -13,6 +14,12 @@ class User(BaseModel): username = models.SlugField(unique=True, verbose_name="юзернейм") password = models.TextField(verbose_name="пароль") + def make_password(self): + return make_password(self.password) + + def check_password(self, password): + return check_password(self.password, password) + status = models.CharField( max_length=10, choices=UserRole, default="student" ) diff --git a/services/backend/config/settings.py b/services/backend/config/settings.py index af06a21..dbb0717 100644 --- a/services/backend/config/settings.py +++ b/services/backend/config/settings.py @@ -483,6 +483,10 @@ DJANGO_GUID = { LANGUAGE_COOKIE_AGE = 31449600 +PASSWORD_HASHERS = [ + "django.contrib.auth.hashers.Argon2PasswordHasher", +] + LANGUAGE_COOKIE_DOMAIN = None LANGUAGE_COOKIE_HTTPONLY = False diff --git a/services/backend/pyproject.toml b/services/backend/pyproject.toml index a3c2a81..def2053 100644 --- a/services/backend/pyproject.toml +++ b/services/backend/pyproject.toml @@ -4,6 +4,7 @@ version = "0.1.0" readme = "README.md" requires-python = ">=3.10,<3.12" dependencies = [ + "argon2-cffi>=23.1.0", "celery>=5.4.0", "colorlog>=6.9.0", "django-cors-headers>=4.6.0", @@ -15,6 +16,7 @@ dependencies = [ "django-ninja>=1.3.0", "django-pagedown>=2.2.1", "django-stubs-ext>=5.1.3", + "django-tinymce>=4.1.0", "gunicorn>=23.0.0", "httpx>=0.28.1", "pillow>=11.1.0", diff --git a/services/frontend/src/components/layout/header.tsx b/services/frontend/src/components/layout/header.tsx index f9a7264..5bc2502 100644 --- a/services/frontend/src/components/layout/header.tsx +++ b/services/frontend/src/components/layout/header.tsx @@ -1,21 +1,99 @@ +import React, { useState } from 'react'; import { DataRush } from "@/components/ui/icons/datarush"; -import { ChevronDown } from "lucide-react"; +import { ChevronDown, User, Settings, BarChart2, LogOut } from "lucide-react"; import { Link } from "react-router"; +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, + SheetClose +} from "@/components/ui/sheet"; const Header = () => { + const [isProfileOpen, setIsProfileOpen] = useState(false); + return (
-
- itqdev +
setIsProfileOpen(true)} + > + itqdev
+ + + + + Профиль + + +
+ } + label="Ваш профиль" + onClick={() => { + setIsProfileOpen(false); + }} + /> + + } + label="Настройки" + onClick={() => { + setIsProfileOpen(false); + }} + /> + + } + label="Статистика" + onClick={() => { + setIsProfileOpen(false); + }} + /> + +
+ } + label="Выйти" + onClick={() => { + setIsProfileOpen(false); + }} + /> +
+
+
+
); }; -export { Header }; +interface ProfileOptionProps { + icon: React.ReactNode; + label: string; + onClick: () => void; + className?: string; +} + +const ProfileOption: React.FC = ({ icon, label, onClick, className }) => { + return ( + + + + ); +}; + +export { Header }; \ No newline at end of file diff --git a/services/frontend/src/components/ui/drawer.tsx b/services/frontend/src/components/ui/drawer.tsx new file mode 100644 index 0000000..bec5a19 --- /dev/null +++ b/services/frontend/src/components/ui/drawer.tsx @@ -0,0 +1,130 @@ +import * as React from "react" +import { Drawer as DrawerPrimitive } from "vaul" + +import { cn } from "@/shared/lib/utils" + +function Drawer({ + ...props +}: React.ComponentProps) { + return +} + +function DrawerTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function DrawerPortal({ + ...props +}: React.ComponentProps) { + return +} + +function DrawerClose({ + ...props +}: React.ComponentProps) { + return +} + +function DrawerOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DrawerContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + +
+ {children} + + + ) +} + +function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DrawerTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DrawerDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Drawer, + DrawerPortal, + DrawerOverlay, + DrawerTrigger, + DrawerClose, + DrawerContent, + DrawerHeader, + DrawerFooter, + DrawerTitle, + DrawerDescription, +} diff --git a/services/frontend/src/components/ui/sheet.tsx b/services/frontend/src/components/ui/sheet.tsx new file mode 100644 index 0000000..9adc293 --- /dev/null +++ b/services/frontend/src/components/ui/sheet.tsx @@ -0,0 +1,134 @@ +import * as React from "react" +import * as SheetPrimitive from "@radix-ui/react-dialog" +import { XIcon } from "lucide-react" + +import { cn } from "@/shared/lib/utils" + +function Sheet({ ...props }: React.ComponentProps) { + return +} + +function SheetTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function SheetClose({ + ...props +}: React.ComponentProps) { + return +} + +function SheetPortal({ + ...props +}: React.ComponentProps) { + return +} + +function SheetOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SheetContent({ + className, + children, + side = "right", + ...props +}: React.ComponentProps & { + side?: "top" | "right" | "bottom" | "left" +}) { + return ( + + + + {children} + {/* Removed the default close button that was causing duplication */} + + + ) +} + +function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function SheetFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function SheetTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SheetDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Sheet, + SheetTrigger, + SheetClose, + SheetContent, + SheetHeader, + SheetFooter, + SheetTitle, + SheetDescription, +} \ No newline at end of file diff --git a/services/frontend/src/pages/CompetitionSession/index.tsx b/services/frontend/src/pages/CompetitionSession/index.tsx index 7fdafa0..f068751 100644 --- a/services/frontend/src/pages/CompetitionSession/index.tsx +++ b/services/frontend/src/pages/CompetitionSession/index.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import { useParams, Navigate } from "react-router-dom"; import { Task } from "@/shared/types"; -import { mockTasks } from "@/shared/mocks/mocks"; +import { mockSolutions, mockTasks } from "@/shared/mocks/mocks"; import CompetitionHeader from "./components/CompetitionHeader"; import TaskContent from "./components/TaskContent"; import TaskSolution from "./modules/TaskSolution"; @@ -40,6 +40,7 @@ const CompetitionSessionPage = () => { void; onSubmit: () => void; + solutionHistory?: Solution[]; } -const ActionButtons: React.FC = ({ onHistoryClick, onSubmit }) => { +const ActionButtons: React.FC = ({ + onHistoryClick, + onSubmit, + solutionHistory = mockSolutions +}) => { + const [isHistoryOpen, setIsHistoryOpen] = useState(false); + + const handleHistoryClick = () => { + setIsHistoryOpen(true); + onHistoryClick(); + }; + return ( -
- - -
+ <> +
+ + +
+ {/* чуть-чуть рак */} + + ); }; diff --git a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/SolutionHistorySheet/index.tsx b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/SolutionHistorySheet/index.tsx new file mode 100644 index 0000000..dcaaa82 --- /dev/null +++ b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/SolutionHistorySheet/index.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { Sheet, SheetClose, SheetContent, SheetHeader, SheetTitle } from "@/components/ui/sheet"; +import { Button } from "@/components/ui/button"; +import { X } from "lucide-react"; +import SolutionStatus from '../SolutionStatus'; +import { Solution } from "@/shared/types"; + +interface SolutionHistorySheetProps { + isOpen: boolean; + onOpenChange: (open: boolean) => void; + solutions: Solution[]; +} + +const SolutionHistorySheet: React.FC = ({ + isOpen, + onOpenChange, + solutions +}) => { + return ( + + + +
+ История решений + + + +
+
+ +
+ {solutions.length > 0 ? ( + solutions.map((solution, index) => ( +
+ +
+ )) + ) : ( +
+ У вас пока нет истории решений для этой задачи +
+ )} +
+
+
+ ); +}; + +export default SolutionHistorySheet; \ No newline at end of file diff --git a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/SolutionStatus/index.tsx b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/SolutionStatus/index.tsx index bbde29c..008ff8e 100644 --- a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/SolutionStatus/index.tsx +++ b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/SolutionStatus/index.tsx @@ -1,24 +1,41 @@ import React from 'react'; -import { Task } from "@/shared/types"; +import { Solution, TaskStatus } from "@/shared/types"; import { getTaskBgColor, getTaskTextColor } from '@/pages/CompetitionSession/utils/utils'; interface SolutionStatusProps { - task: Task; + solution: Solution; } -const SolutionStatus: React.FC = ({ task }) => { +const SolutionStatus: React.FC = ({ solution }) => { + const getStatusText = (status: TaskStatus, score?: number, maxScore?: number) => { + switch (status) { + case TaskStatus.Checking: + return 'На проверке'; + case TaskStatus.Wrong: + return 'Неверный ответ'; + case TaskStatus.Correct: + return `Зачтено ${maxScore}/${maxScore} баллов`; + case TaskStatus.Partial: + return `Зачтено ${score}/${maxScore} баллов`; + case TaskStatus.Uncleared: + return 'Не решено'; + default: + return ''; + } + }; + return ( -
+
- - Решение 12345 + + Решение {solution.id} - - Зачтено 5/10 баллов + + {getStatusText(solution.status, solution.score, solution.maxScore)}
-
- 1 марта, 08:41 +
+ {solution.date}
); diff --git a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx index c2e9fc3..58db705 100644 --- a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx +++ b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx @@ -1,5 +1,5 @@ import React, { useState, useRef } from 'react'; -import { Task } from "@/shared/types"; +import { Solution, Task } from "@/shared/types"; import SolutionStatus from './components/SolutionStatus'; import InputSolution from './components/InputSolution'; import FileSolution from './components/FileSolution'; @@ -8,6 +8,7 @@ import ActionButtons from './components/ActionButtons'; interface TaskSolutionProps { task: Task; + solutions: Solution[]; answer: string; setAnswer: (value: string) => void; onSubmit: () => void; @@ -16,6 +17,7 @@ interface TaskSolutionProps { const TaskSolution: React.FC = ({ task, + solutions, answer, setAnswer, onSubmit, @@ -26,7 +28,7 @@ const TaskSolution: React.FC = ({ return (
- + {task.solutionType === 'input' && ( diff --git a/services/frontend/src/pages/CompetitionSession/utils/utils.ts b/services/frontend/src/pages/CompetitionSession/utils/utils.ts index 22e5420..9ba336e 100644 --- a/services/frontend/src/pages/CompetitionSession/utils/utils.ts +++ b/services/frontend/src/pages/CompetitionSession/utils/utils.ts @@ -1,21 +1,21 @@ import { TaskStatus } from "@/shared/types"; 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 TaskStatus.Uncleared: return "bg-[var(--color-task-uncleared)]"; + case TaskStatus.Checking: return "bg-[var(--color-task-checking)]"; + case TaskStatus.Correct: return "bg-[var(--color-task-correct)]"; + case TaskStatus.Partial: return "bg-[var(--color-task-partial)]"; + case TaskStatus.Wrong: return "bg-[var(--color-task-wrong)]"; } }; const getTaskTextColor = (status: TaskStatus): string => { switch (status) { - case "uncleared": return "text-[var(--color-task-text-uncleared)]"; - case "checking": return "text-[var(--color-task-text-checking)]"; - case "correct": return "text-[var(--color-task-text-correct)]"; - case "partial": return "text-[var(--color-task-text-partial)]"; - case "wrong": return "text-[var(--color-task-text-wrong)]"; + case TaskStatus.Uncleared: return "text-[var(--color-task-text-uncleared)]"; + case TaskStatus.Checking: return "text-[var(--color-task-text-checking)]"; + case TaskStatus.Correct: return "text-[var(--color-task-text-correct)]"; + case TaskStatus.Partial: return "text-[var(--color-task-text-partial)]"; + case TaskStatus.Wrong: return "text-[var(--color-task-text-wrong)]"; } }; diff --git a/services/frontend/src/pages/Competitions/index.tsx b/services/frontend/src/pages/Competitions/index.tsx index 08c213d..0af28c2 100644 --- a/services/frontend/src/pages/Competitions/index.tsx +++ b/services/frontend/src/pages/Competitions/index.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from "react"; +import { useState } from "react"; import { Competition, CompetitionStatus } from "@/shared/types"; import { CompetitionGrid } from "./modules/CompetitionGrid"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; diff --git a/services/frontend/src/shared/mocks/mocks.ts b/services/frontend/src/shared/mocks/mocks.ts index 58099e7..b7e2525 100644 --- a/services/frontend/src/shared/mocks/mocks.ts +++ b/services/frontend/src/shared/mocks/mocks.ts @@ -1,4 +1,4 @@ -import { Competition, CompetitionStatus, Task } from "../types"; +import { Competition, CompetitionStatus, Solution, Task, TaskStatus } from "../types"; const mockCompetitions: Competition[] = [ { @@ -56,52 +56,81 @@ const mockTasks: Task[] = [ { id: "1", number: "1.1", - status: "uncleared", + status: TaskStatus.Uncleared, solutionType: "input" }, { id: "2", number: "1.2", - status: "checking", + status: TaskStatus.Checking, solutionType: "file" }, { id: "3", number: "1.3", - status: "correct", + status: TaskStatus.Correct, solutionType: "code" }, { id: "4", number: "2.1", - status: "partial", + status: TaskStatus.Partial, solutionType: "input" }, { id: "5", number: "2.2", - status: "wrong", + status: TaskStatus.Wrong, solutionType: "file" }, { id: "6", number: "2.3", - status: "uncleared", + status: TaskStatus.Uncleared, solutionType: "code" }, { id: "7", number: "3.1", - status: "checking", + status: TaskStatus.Checking, solutionType: "file" }, { id: "8", number: "3.2", - status: "correct", + status: TaskStatus.Correct, solutionType: "input" }, ]; -export { mockCompetitions, mockTasks }; +const mockSolutions: Solution[] = [ + { + id: '1', + status: TaskStatus.Wrong, + date: '1 марта, 08:41', + }, + { + id: '2', + status: TaskStatus.Partial, + score: 5, + maxScore: 10, + date: '28 февраля, 15:22', + }, + { + id: '3', + status: TaskStatus.Correct, + score: 0, + maxScore: 10, + date: '27 февраля, 12:10', + }, + { + id: '4', + status: TaskStatus.Checking, + date: '1 марта, 08:41', + }, + +]; + + +export { mockCompetitions, mockTasks, mockSolutions }; diff --git a/services/frontend/src/shared/types.ts b/services/frontend/src/shared/types.ts index cb4089c..6645a59 100644 --- a/services/frontend/src/shared/types.ts +++ b/services/frontend/src/shared/types.ts @@ -4,6 +4,14 @@ enum CompetitionStatus { Completed = "Завершено", } +enum TaskStatus { + Uncleared = "uncleared", + Checking = "checking", + Correct = "correct", + Partial = "partial", + Wrong = "wrong" +} + interface Competition { id: string; name: string; @@ -13,9 +21,15 @@ interface Competition { description?: string; } -type TaskStatus = "uncleared" | "checking" | "correct" | "partial" | "wrong"; type SolutionType = "input" | "file" | "code"; +interface Solution { + id: string, + status: TaskStatus, + date: string, + score?: number, + maxScore?: number, +} interface Task { id: string; number: string; @@ -23,5 +37,5 @@ interface Task { solutionType: SolutionType; } -export { CompetitionStatus }; -export type { Competition, TaskStatus, Task }; +export { CompetitionStatus, TaskStatus }; +export type { Solution, Competition, Task }; diff --git a/services/frontend/src/widgets/Navbar/index.tsx b/services/frontend/src/widgets/Navbar/index.tsx deleted file mode 100644 index a1b681a..0000000 --- a/services/frontend/src/widgets/Navbar/index.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { ChevronDown } from "lucide-react"; - -const Navbar = () => { - return ( - - ); -}; - - -export default Navbar