Merge branch 'master' of gitlab.prodcontest.ru:team-15/project

This commit is contained in:
moolcoov
2025-03-01 18:47:50 +03:00
29 changed files with 763 additions and 97 deletions
+1
View File
@@ -0,0 +1 @@
.idea
+2
View File
@@ -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
+3 -2
View File
@@ -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)
@@ -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='тип участия'),
),
]
+12 -5
View File
@@ -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
@@ -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,
},
),
]
@@ -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'),
),
]
@@ -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 = [
]
@@ -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',
),
]
-8
View File
@@ -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):
+9
View File
@@ -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")
+1
View File
@@ -5,3 +5,4 @@ class UsersConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "apps.user"
label = "user"
verbose_name = "Пользователи"
@@ -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='юзернейм'),
),
]
+7
View File
@@ -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"
)
+4
View File
@@ -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
+2
View File
@@ -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",
@@ -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 (
<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>
<div
className="flex items-center gap-1 cursor-pointer hover:opacity-80 transition-opacity px-2 py-1 rounded-md"
onClick={() => setIsProfileOpen(true)}
>
<span className="text-lg font-semibold font-hse-sans">itqdev</span>
<ChevronDown size={20} />
</div>
</div>
<Sheet open={isProfileOpen} onOpenChange={setIsProfileOpen}>
<SheetContent className="w-[300px] sm:w-[350px] p-0">
<SheetHeader className="border-b py-4 px-5">
<SheetTitle className="font-hse-sans text-lg font-medium">Профиль</SheetTitle>
</SheetHeader>
<div className="py-4 px-2">
<ProfileOption
icon={<User size={18} />}
label="Ваш профиль"
onClick={() => {
setIsProfileOpen(false);
}}
/>
<ProfileOption
icon={<Settings size={18} />}
label="Настройки"
onClick={() => {
setIsProfileOpen(false);
}}
/>
<ProfileOption
icon={<BarChart2 size={18} />}
label="Статистика"
onClick={() => {
setIsProfileOpen(false);
}}
/>
<div className="border-t mt-2 pt-2">
<ProfileOption
icon={<LogOut size={18} />}
label="Выйти"
onClick={() => {
setIsProfileOpen(false);
}}
/>
</div>
</div>
</SheetContent>
</Sheet>
</header>
);
};
export { Header };
interface ProfileOptionProps {
icon: React.ReactNode;
label: string;
onClick: () => void;
className?: string;
}
const ProfileOption: React.FC<ProfileOptionProps> = ({ icon, label, onClick, className }) => {
return (
<SheetClose asChild>
<button
className={`flex items-center gap-3 w-full px-3 py-2.5 rounded-md text-left transition-colors hover:bg-gray-100 ${className || ''}`}
onClick={onClick}
>
<span className="text-gray-600">{icon}</span>
<span className="font-hse-sans">{label}</span>
</button>
</SheetClose>
);
};
export { Header };
@@ -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<typeof DrawerPrimitive.Root>) {
return <DrawerPrimitive.Root data-slot="drawer" {...props} />
}
function DrawerTrigger({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />
}
function DrawerPortal({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />
}
function DrawerClose({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />
}
function DrawerOverlay({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
return (
<DrawerPrimitive.Overlay
data-slot="drawer-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80",
className
)}
{...props}
/>
)
}
function DrawerContent({
className,
children,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
return (
<DrawerPortal data-slot="drawer-portal">
<DrawerOverlay />
<DrawerPrimitive.Content
data-slot="drawer-content"
className={cn(
"group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg",
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg",
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:sm:max-w-sm",
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:sm:max-w-sm",
className
)}
{...props}
>
<div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
)
}
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="drawer-header"
className={cn("flex flex-col gap-1.5 p-4", className)}
{...props}
/>
)
}
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="drawer-footer"
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
)
}
function DrawerTitle({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
return (
<DrawerPrimitive.Title
data-slot="drawer-title"
className={cn("text-foreground font-semibold", className)}
{...props}
/>
)
}
function DrawerDescription({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
return (
<DrawerPrimitive.Description
data-slot="drawer-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
}
@@ -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<typeof SheetPrimitive.Root>) {
return <SheetPrimitive.Root data-slot="sheet" {...props} />
}
function SheetTrigger({
...props
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
}
function SheetClose({
...props
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
}
function SheetPortal({
...props
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
}
function SheetOverlay({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
return (
<SheetPrimitive.Overlay
data-slot="sheet-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80",
className
)}
{...props}
/>
)
}
function SheetContent({
className,
children,
side = "right",
...props
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
side?: "top" | "right" | "bottom" | "left"
}) {
return (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
data-slot="sheet-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
side === "right" &&
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
side === "left" &&
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
side === "top" &&
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
side === "bottom" &&
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
className
)}
{...props}
>
{children}
{/* Removed the default close button that was causing duplication */}
</SheetPrimitive.Content>
</SheetPortal>
)
}
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-header"
className={cn("flex flex-col gap-1.5 p-4", className)} // Kept original padding
{...props}
/>
)
}
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-footer"
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
)
}
function SheetTitle({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
return (
<SheetPrimitive.Title
data-slot="sheet-title"
className={cn("text-foreground font-semibold", className)}
{...props}
/>
)
}
function SheetDescription({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
return (
<SheetPrimitive.Description
data-slot="sheet-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Sheet,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
}
@@ -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 = () => {
<TaskContent task={currentTask} />
<TaskSolution
task={currentTask}
solutions={mockSolutions}
answer={answer}
setAnswer={setAnswer}
onSubmit={handleSubmit}
@@ -1,28 +1,51 @@
import React from 'react';
import React, { useState } from 'react';
import { Button } from "@/components/ui/button";
import SolutionHistorySheet from '../SolutionHistorySheet';
import { Solution } from "@/shared/types";
import { mockSolutions } from '@/shared/mocks/mocks';
interface ActionButtonsProps {
onHistoryClick: () => void;
onSubmit: () => void;
solutionHistory?: Solution[];
}
const ActionButtons: React.FC<ActionButtonsProps> = ({ onHistoryClick, onSubmit }) => {
const ActionButtons: React.FC<ActionButtonsProps> = ({
onHistoryClick,
onSubmit,
solutionHistory = mockSolutions
}) => {
const [isHistoryOpen, setIsHistoryOpen] = useState(false);
const handleHistoryClick = () => {
setIsHistoryOpen(true);
onHistoryClick();
};
return (
<div className="flex gap-3 justify-between">
<Button
variant="ghost"
className="font-hse-sans bg-white hover:bg-gray-100"
onClick={onHistoryClick}
>
История
</Button>
<Button
onClick={onSubmit}
className="font-hse-sans"
>
Отправить решение
</Button>
</div>
<>
<div className="flex gap-8">
<Button
variant="ghost"
className="font-hse-sans bg-white hover:bg-gray-100"
onClick={handleHistoryClick}
>
История
</Button>
<Button
onClick={onSubmit}
className="font-hse-sans flex-grow"
>
Отправить решение
</Button>
</div>
{/* чуть-чуть рак */}
<SolutionHistorySheet
isOpen={isHistoryOpen}
onOpenChange={setIsHistoryOpen}
solutions={solutionHistory}
/>
</>
);
};
@@ -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<SolutionHistorySheetProps> = ({
isOpen,
onOpenChange,
solutions
}) => {
return (
<Sheet open={isOpen} onOpenChange={onOpenChange}>
<SheetContent className="w-[350px] sm:w-[450px] p-0">
<SheetHeader className="border-b py-3 px-4">
<div className="flex justify-between items-center">
<SheetTitle className="text-lg font-medium">История решений</SheetTitle>
<SheetClose asChild>
<Button variant="ghost" size="icon" className="h-8 w-8">
<X className="h-4 w-4" />
</Button>
</SheetClose>
</div>
</SheetHeader>
<div className="flex flex-col mt-3 space-y-2.5 overflow-y-auto max-h-[calc(100vh-80px)] px-4">
{solutions.length > 0 ? (
solutions.map((solution, index) => (
<div key={index} className="w-full">
<SolutionStatus solution={solution} />
</div>
))
) : (
<div className="text-center py-8 text-gray-500">
У вас пока нет истории решений для этой задачи
</div>
)}
</div>
</SheetContent>
</Sheet>
);
};
export default SolutionHistorySheet;
@@ -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<SolutionStatusProps> = ({ task }) => {
const SolutionStatus: React.FC<SolutionStatusProps> = ({ 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 (
<div className={`${getTaskBgColor(task.status)} rounded-lg p-4 relative`}>
<div className={`${getTaskBgColor(solution.status)} rounded-lg p-4 relative`}>
<div className="flex flex-col">
<span className={`${getTaskTextColor(task.status)} font-medium`}>
Решение 12345
<span className={`${getTaskTextColor(solution.status)} font-medium`}>
Решение {solution.id}
</span>
<span className={`${getTaskTextColor(task.status)} mt-1`}>
Зачтено 5/10 баллов
<span className={`${getTaskTextColor(solution.status)} mt-1`}>
{getStatusText(solution.status, solution.score, solution.maxScore)}
</span>
</div>
<div className={`absolute bottom-2 right-3 text-xs ${getTaskTextColor(task.status)}`}>
1 марта, 08:41
<div className={`absolute bottom-2 right-3 text-xs ${getTaskTextColor(solution.status)}`}>
{solution.date}
</div>
</div>
);
@@ -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<TaskSolutionProps> = ({
task,
solutions,
answer,
setAnswer,
onSubmit,
@@ -26,7 +28,7 @@ const TaskSolution: React.FC<TaskSolutionProps> = ({
return (
<div className="md:w-[500px] flex flex-col gap-4">
<SolutionStatus task={task} />
<SolutionStatus solution={solutions[0]} />
{task.solutionType === 'input' && (
<InputSolution answer={answer} setAnswer={setAnswer} />
@@ -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)]";
}
};
@@ -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";
+39 -10
View File
@@ -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 };
+17 -3
View File
@@ -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 };
@@ -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