Merge remote-tracking branch 'origin/master'

This commit is contained in:
Timur
2025-03-01 14:33:11 +03:00
27 changed files with 352 additions and 402 deletions
+4 -4
View File
@@ -1,11 +1,11 @@
DJANGO_SECRET_KEY=secretees
DJANGO_DEBUG=False
DJANGO_ALLOWED_HOSTS=*
DJANGO_CSRF_TRUSTED_ORIGINS=http://localhost,http://127.0.0.1
DJANGO_CSRF_TRUSTED_ORIGINS=http://localhost,http://127.0.0.1,https://*.final.prodcontest.ru
DJANGO_CORS_ALLOWED_ORIGINS=*
DJANGO_INTERNAL_IPS=127.0.0.1
DJANGO_LANGUAGE_CODE=en-us
DJANGO_STATIC_URL=http://localhost:13241/
DJANGO_LANGUAGE_CODE=ru
DJANGO_STATIC_URL=static/
REDIS_URI=redis://redis:6379
DJANGO_DB_URI=postgresql://postgres:postgres@postgres/postgres
@@ -15,6 +15,6 @@ DJANGO_SUPERUSER_EMAIL=admin@mail.com
DJANGO_SUPERUSER_PASSWORD=admin
MINIO_ENDPOINT=minio:9000
MINIO_CUSTOM_ENDPOINT_URL=http://127.0.0.1:13244
MINIO_CUSTOM_ENDPOINT_URL=https://prod-team-15-minio-2pc0i3lc.final.prodcontest.ru
MINIO_ACCESS_KEY=admin
MINIO_SECRET_KEY=password
+2 -2
View File
@@ -48,10 +48,10 @@ domain = localhost
enforce_domain = false
# The full public facing url
root_url = %(protocol)s://%(domain)s:%(http_port)s/
root_url = %(protocol)s://%(domain)s:%(http_port)s/admin/grafana/
# Serve Grafana from subpath specified in `root_url` setting. By default it is set to `false` for compatibility reasons.
serve_from_sub_path = false
serve_from_sub_path = true
# Log web requests
router_logging = false
+80
View File
@@ -109,6 +109,24 @@ http {
proxy_read_timeout 600s;
}
location /static {
proxy_pass http://backend-staticfiles:80;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
proxy_hide_header X-Powered-By;
proxy_connect_timeout 75s;
proxy_send_timeout 600s;
proxy_read_timeout 600s;
}
location /api {
proxy_pass http://backend:8080;
proxy_http_version 1.1;
@@ -137,9 +155,71 @@ http {
client_max_body_size 100M;
}
location /admin/grafana/ {
rewrite ^/admin/grafana/(.*) /$1 break;
proxy_pass http://grafana:3000/;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
error_page 404 = @notfound;
location @notfound {
return 444;
}
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name prod-team-15-minio-2pc0i3lc.final.prodcontest.ru;
ssl_certificate /etc/nginx/certs/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/privkey.pem;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
ignore_invalid_headers off;
client_max_body_size 0;
proxy_buffering off;
proxy_request_buffering off;
location / {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 300;
proxy_http_version 1.1;
proxy_set_header Connection "";
chunked_transfer_encoding off;
proxy_pass http://minio:9000;
}
location /minio/ui/ {
rewrite ^/minio/ui/(.*) /$1 break;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-NginX-Proxy true;
real_ip_header X-Real-IP;
proxy_connect_timeout 300;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
chunked_transfer_encoding off;
proxy_pass http://minio:9001;
}
}
}
+4 -5
View File
@@ -1,26 +1,25 @@
from abc import ABC
from typing import Optional
from django.http import HttpRequest
from django.shortcuts import get_object_or_404
from django.urls import resolve
from ninja.errors import AuthenticationError
from ninja.security import APIKeyQuery
from ninja.security.apikey import APIKeyBase
from apps.review.models import Reviewer
class APIKeyPath(APIKeyBase, ABC):
openapi_in: str = "path"
def _get_key(self, request: HttpRequest) -> Optional[str]:
def _get_key(self, request: HttpRequest) -> str | None:
func, args, kwargs = resolve(request.path)
return kwargs.get(self.param_name)
class ReviewerAuth(APIKeyPath):
param_name = "token"
def authenticate(self, request, token):
if not (reviewer := Reviewer.objects.filter(token=token).first()):
raise AuthenticationError
return reviewer
return reviewer
+8 -8
View File
@@ -1,8 +1,8 @@
from typing import List, Literal
from typing import Literal
from uuid import UUID
from django.http import HttpRequest
from ninja import Schema, ModelSchema
from ninja import ModelSchema, Schema
from apps.review.models import Reviewer
from apps.task.models import CompetetionTaskSumbission
@@ -11,6 +11,7 @@ from apps.task.models import CompetetionTaskSumbission
class PingOut(Schema):
status: str = "ok"
class ReviewerOut(ModelSchema):
id: UUID
@@ -18,20 +19,19 @@ class ReviewerOut(ModelSchema):
model = Reviewer
exclude = ("token",)
class SubmissionOut(ModelSchema):
id: UUID
status: Literal["sent", "checking", "checked"]
class Meta:
model = CompetetionTaskSumbission
exclude = (
"user",
)
exclude = ("user",)
class SubmissionsOut(Schema):
submissions: list[SubmissionOut] = []
@staticmethod
def resolve_submissions(self, context: HttpRequest) -> List[SubmissionOut]:
print(CompetetionTaskSumbission.objects.all())
return list(CompetetionTaskSumbission.objects.all())
def resolve_submissions(self, context: HttpRequest) -> list[SubmissionOut]:
return list(CompetetionTaskSumbission.objects.all())
+8 -10
View File
@@ -3,19 +3,20 @@ from http import HTTPStatus as status
from django.http import HttpRequest
from ninja import Router
from api.v1.review import schemas
from api.v1 import schemas as global_schemas
from api.v1.review import schemas
router = Router(tags=["review"])
@router.get(
"{token}/tasks",
"{token}/submissions",
response={
status.OK: schemas.SubmissionsOut,
},
description="Список отправок, на проверку которых назначен ревьюер"
)
def ping(request: HttpRequest, token) -> tuple[status, schemas.SubmissionsOut]:
def get_submissions(request: HttpRequest, token) -> tuple[status, schemas.SubmissionsOut]:
return status.OK, schemas.SubmissionsOut()
@@ -23,12 +24,9 @@ def ping(request: HttpRequest, token) -> tuple[status, schemas.SubmissionsOut]:
"{token}",
response={
status.OK: schemas.ReviewerOut,
status.UNAUTHORIZED: global_schemas.UnauthorizedError
status.UNAUTHORIZED: global_schemas.UnauthorizedError,
},
description="token есть и в сваггер авторизации, но оно не работает, не верьте. подставляйте токен вручную в query"
description="token есть и в сваггер авторизации, но оно не работает, не верьте. подставляйте токен вручную в query",
)
def get_reviewer(
request: HttpRequest,
token: str
):
return status.OK, request.auth
def get_reviewer_profile(request: HttpRequest, token: str):
return status.OK, request.auth
+1 -2
View File
@@ -7,8 +7,8 @@ from api.v1.auth import BearerAuth
from api.v1.competition.views import router as competition_router
from api.v1.ping.views import router as ping_router
from api.v1.review.auth import ReviewerAuth
from api.v1.user.views import router as user_router
from api.v1.review.views import router as review_router
from api.v1.user.views import router as user_router
router = NinjaAPI(
title="DataRush API",
@@ -39,6 +39,5 @@ router.add_router(
)
for exception, handler in handlers.exception_handlers:
router.add_exception_handler(exception, partial(handler, router=router))
+1 -1
View File
@@ -1,8 +1,8 @@
from typing import Literal
from uuid import UUID
from ninja import ModelSchema, Schema
from apps.competition.models import State
from apps.task.models import CompetitionTask
+13 -10
View File
@@ -3,21 +3,20 @@ from uuid import UUID
from django.shortcuts import get_object_or_404
from ninja import Router
from django.shortcuts import get_object_or_404
from api.v1.schemas import NotFoundError, UnauthorizedError, ForbiddenError
from api.v1.ping.schemas import PingOut
from api.v1.schemas import ForbiddenError, NotFoundError, UnauthorizedError
from api.v1.task.schemas import (
TaskOutSchema,
TaskSubmissionOut,
TaskSubmissionIn,
)
from apps.task.models import (
Competition,
CompetitionTask,
CompetetionTaskSumbission,
TaskSubmissionOut,
)
from apps.competition.models import State
from apps.task.models import (
CompetetionTaskSumbission,
Competition,
CompetitionTask,
)
router = Router(tags=["competition"])
@@ -49,7 +48,9 @@ def start_competition(request, competition_id: UUID) -> PingOut:
status.NOT_FOUND: NotFoundError,
},
)
def get_competition_tasks(request, competition_id: UUID) -> list[TaskOutSchema]:
def get_competition_tasks(
request, competition_id: UUID
) -> list[TaskOutSchema]:
competition = get_object_or_404(Competition, pk=competition_id)
state = State.objects.filter(
user=request.auth, competition=competition, state="started"
@@ -57,7 +58,9 @@ def get_competition_tasks(request, competition_id: UUID) -> list[TaskOutSchema]:
if not state:
return 403, ForbiddenError()
return status.OK, CompetitionTask.objects.filter(competition=competition).all()
return status.OK, CompetitionTask.objects.filter(
competition=competition
).all()
@router.get(
+1
View File
@@ -64,6 +64,7 @@ def sign_in(request, data: LoginSchema):
def get_me(request):
return 200, request.auth
@router.get(
path="/user/{user_id}",
response={
@@ -0,0 +1,136 @@
import random
import uuid
from datetime import timedelta
from django.contrib.auth.hashers import make_password
from django.core.files.base import ContentFile
from django.core.management.base import BaseCommand
from django.utils import timezone
from apps.competition.models import Competition, State
from apps.task.models import CompetetionTaskSumbission, CompetitionTask
from apps.user.models import User, UserRole
class Command(BaseCommand):
help = "Generate sample data for Users, Competitions, Tasks, Submissions, and States."
def handle(self, *args, **options):
self.stdout.write("Starting data generation...")
users = self.create_users(5)
competitions = self.create_competitions(2, users)
tasks = self.create_tasks(competitions)
self.create_submissions(tasks, users)
self.create_states(competitions, users)
self.stdout.write("Data generation completed.")
def create_users(self, count):
users = []
for i in range(1, count + 1):
email = f"user{i}@example.com"
username = f"user{i}"
password = (
"password123" # In production, use proper password handling.
)
role = random.choice(
[UserRole.STUDENT.value, UserRole.METODIST.value]
)
user, created = User.objects.get_or_create(
email=email,
defaults={
"username": username,
"password": make_password(password),
"status": role,
},
)
users.append(user)
self.stdout.write(f"Created user: {username}")
return users
def create_competitions(self, count, users):
competitions = []
now = timezone.now()
for i in range(1, count + 1):
title = f"Competition {i}"
description = f"Description for competition {i}"
start_date = now - timedelta(days=random.randint(1, 10))
end_date = now + timedelta(days=random.randint(1, 10))
competition = Competition.objects.create(
title=title,
description=description,
start_date=start_date,
end_date=end_date,
type="solo", # assuming only one type for now
participation_type=random.choice(["edu", "competitive"]),
)
# Add random participants
selected_users = random.sample(
users, k=min(len(users), random.randint(1, len(users)))
)
competition.participants.add(*selected_users)
competitions.append(competition)
self.stdout.write(f"Created competition: {title}")
return competitions
def create_tasks(self, competitions):
tasks = []
task_types = [
CompetitionTask.CompetitionTaskType.INPUT.value,
]
for comp in competitions:
# Create 3 tasks per competition
for i in range(1, 4):
task_type = random.choice(task_types)
title = f"Task {i} for {comp.title}"
description = f"Task description for task {i} in {comp.title}"
task = CompetitionTask.objects.create(
competition=comp,
title=title,
description=description,
type=task_type,
points=random.randint(1, 10),
)
tasks.append(task)
self.stdout.write(f"Created task: {title} (type: {task_type})")
return tasks
def create_submissions(self, tasks, users):
for task in tasks:
# Each task will get between 1 and 3 submissions
num_submissions = random.randint(1, 3)
for _ in range(num_submissions):
user = random.choice(users)
# Create a dummy content file
dummy_content = ContentFile(
b"Submission content",
name=f"submission_{uuid.uuid4().hex}.txt",
)
submission = CompetetionTaskSumbission.objects.create(
user=user,
task=task,
earned_points=random.randint(
0, task.points if task.points else 10
),
content=dummy_content,
)
submission.save()
self.stdout.write(
f"Created submission for task '{task.title}' by user '{user.username}'"
)
def create_states(self, competitions, users):
# For each competition, create a State for some of its participants
state_choices = [choice for choice in State.StateChoices.values]
for comp in competitions:
for user in comp.participants.all():
state_obj, created = State.objects.get_or_create(
user=user,
competition=comp,
defaults={
"state": random.choice(state_choices),
"changed_at": timezone.now(),
},
)
self.stdout.write(
f"Created state '{state_obj.state}' for user '{user.username}' in competition '{comp.title}'"
)
+1 -1
View File
@@ -7,4 +7,4 @@ class Reviewer(BaseModel):
name = models.CharField(max_length=100)
surname = models.CharField(max_length=100)
token = models.CharField(max_length=100)
token = models.CharField(max_length=100)
+3 -3
View File
@@ -1,13 +1,13 @@
from random import choice
from uuid import uuid4
from django.db import models
from apps.task.validators import ContestTaskCriteriesValidator
from apps.competition.models import Competition
from apps.core.models import BaseModel
from apps.task.validators import ContestTaskCriteriesValidator
from apps.user.models import User
class CompetitionTask(BaseModel):
class CompetitionTaskType(models.TextChoices):
INPUT = "input"
@@ -45,7 +45,7 @@ class CompetetionTaskSumbission(BaseModel):
CHECKED = "checked"
def submission_content_upload_to(instance, filename) -> str:
return f"/submissions/{instance.id}/content"
return f"submissions/{instance.id}/content"
def submission_stdout_upload_to(instance, filename) -> str:
return f"/submissions/{instance.id}/stdout"
+11 -10
View File
@@ -1,9 +1,10 @@
import tempfile
import ast
import hashlib
import os
import sys
import ast
import tempfile
from io import StringIO
import hashlib
from config.celery import app
ALLOWED_MODULES = {
@@ -29,7 +30,7 @@ def validate_code(code_str):
try:
tree = ast.parse(code_str)
except SyntaxError as e:
raise SecurityException(f"Syntax error: {str(e)}")
raise SecurityException(f"Syntax error: {e!s}")
class ImportVisitor(ast.NodeVisitor):
def visit_Import(self, node):
@@ -56,10 +57,10 @@ def validate_code(code_str):
try:
ImportVisitor().visit(tree)
SecurityVisitor().visit(tree)
except SecurityException as e:
except SecurityException:
raise
except Exception as e:
raise SecurityException(f"Security check failed: {str(e)}")
raise SecurityException(f"Security check failed: {e!s}")
def secure_exec(code_str, result_path):
@@ -95,7 +96,7 @@ def secure_exec(code_str, result_path):
result_content = f.read()
except Exception as e:
raise RuntimeError(f"Execution error: {str(e)}")
raise RuntimeError(f"Execution error: {e!s}")
finally:
os.chdir(original_dir)
sys.stdout = original_stdout
@@ -121,8 +122,8 @@ def analyze_data_task(self, code_str, result_path, expected_bytes):
}
except SecurityException as e:
return {"success": False, "error": f"Security violation: {str(e)}"}
return {"success": False, "error": f"Security violation: {e!s}"}
except RuntimeError as e:
return {"success": False, "error": f"Execution error: {str(e)}"}
return {"success": False, "error": f"Execution error: {e!s}"}
except Exception as e:
return {"success": False, "error": f"Unexpected error: {str(e)}"}
return {"success": False, "error": f"Unexpected error: {e!s}"}
+2 -2
View File
@@ -12,12 +12,12 @@ class Criteria(BaseModel):
class ContestTaskCriteriesValidator:
def __call__(self, instance):
if instance.criterties and not isinstance(instance.criterties, list):
if instance.criteries and not isinstance(instance.criteries, list):
err = "criteries must be a valid dictionary"
raise ValidationError(err)
try:
for criteria in instance.criterties:
for criteria in instance.criteries if instance.criteries else []:
Criteria(**criteria)
except PydanticValidationError:
err = "invalid criteries data"
+1 -4
View File
@@ -24,9 +24,6 @@ class TestSignUp(TestCase):
user.full_clean()
def test_missing_params(self):
user = User(
password="123123",
username="132131232131"
)
user = User(password="123123", username="132131232131")
with self.assertRaises(ValidationError):
user.full_clean()
+1 -1
View File
@@ -446,7 +446,7 @@ INSTALLED_APPS = [
"apps.user",
"apps.competition",
"apps.review",
"apps.task"
"apps.task",
]
# GUID
+7 -7
View File
@@ -1,19 +1,19 @@
import { Routes, Route } from "react-router";
import "./styles/globals.css";
import CompetitionsPage from "./pages/Competitions";
import CompetitionPage from "./pages/Competition";
import CompetitionRunnerPage from "./pages/CompetitionSession";
import Competitions from "./pages/Competitions";
import CompetitionPreview from './pages/CompetitionPreview'
import CompetitionSession from "./pages/CompetitionSession";
import { NavbarLayout } from "./widgets/navbar-layout";
const App = () => {
return (
<Routes>
<Route element={<NavbarLayout />}>
<Route path="/" element={<CompetitionsPage />} />
<Route path="/competitions/:id" element={<CompetitionPage />} />
<Route path="/" element={<Competitions />} />
<Route path="/competition/:id" element={<CompetitionPreview />} />
<Route
path="/competitions/:id/tasks/:taskId"
element={<CompetitionRunnerPage />}
path="/competition/:id/tasks/:taskId"
element={<CompetitionSession />}
/>
</Route>
</Routes>
@@ -1,16 +1,29 @@
import { useState } from "react";
import { useParams, Link } from "react-router-dom";
import { useParams, Link, useNavigate } from "react-router-dom";
import { Button } from "@/components/ui/button";
import { ArrowLeft } from "lucide-react";
import { Competition } from "@/shared/types";
import { mockCompetitions } from "@/shared/mocks/mocks";
import { mockCompetitions, mockTasks } from "@/shared/mocks/mocks";
const CompetitionPage = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const [competition] = useState<Competition>(
mockCompetitions.find((comp) => comp.id === id)!,
);
const handleContinue = () => {
if (competition?.id) {
if (mockTasks && mockTasks.length > 0) {
const firstTaskId = mockTasks[0].id;
navigate(`/competition/${competition.id}/tasks/${firstTaskId}`);
} else {
navigate(`/competition/${competition.id}/tasks`);
}
}
};
return (
<div className="flex flex-col gap-4">
<Link
@@ -42,7 +55,7 @@ const CompetitionPage = () => {
</div>
</div>
<div className="w-96 *:w-full">
<Button>Продолжить</Button>
<Button onClick={handleContinue}>Продолжить</Button>
</div>
</div>
</div>
@@ -1,106 +0,0 @@
import { useEffect, useState } from "react";
import { useParams, useNavigate } from "react-router-dom";
import Navbar from "@/widgets/Navbar";
import { Button } from "@/components/ui/button";
import { ArrowLeft } from "lucide-react";
import { Competition } from "@/shared/types";
import { mockCompetitions, mockTasks } from "@/shared/mocks/mocks";
const CompetitionPreview = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const [competition, setCompetition] = useState<Competition | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const fetchCompetition = async () => {
try {
setTimeout(() => {
const found = mockCompetitions.find((comp) => comp.id === id);
setCompetition(found || null);
setIsLoading(false);
}, 500);
} catch (error) {
console.error("Error fetching competition:", error);
setIsLoading(false);
}
};
fetchCompetition();
}, [id]);
const handleBack = () => {
navigate(-1);
};
const handleContinue = () => {
if (competition?.id) {
if (mockTasks && mockTasks.length > 0) {
const firstTaskId = mockTasks[0].id;
navigate(`/competition/${competition.id}/tasks/${firstTaskId}`);
} else {
navigate(`/competition/${competition.id}/tasks`);
}
}
};
return (
<>
<Navbar />
<div className="container mx-auto mt-16 px-4 py-8">
<button
onClick={handleBack}
className="font-hse-sans mb-8 flex items-center text-gray-600"
>
<ArrowLeft size={16} className="mr-2" />
Назад к соревнованиям
</button>
{isLoading ? (
<div className="flex h-64 items-center justify-center">
<p className="font-hse-sans text-gray-500">Загрузка...</p>
</div>
) : competition ? (
<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="h-full w-full object-cover"
/>
</div>
<div className="p-8">
<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="font-hse-sans text-lg leading-relaxed text-gray-700">
<p>{competition.description}</p>
</div>
</div>
</div>
) : (
<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>
</>
);
};
export default CompetitionPreview;
@@ -1,150 +0,0 @@
import { useState, useEffect } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { Task } from "@/shared/types";
import { getTaskBgColor, getTaskTextColor } from "./utils/utils";
import { mockTasks } from "@/shared/mocks/mocks";
import { Button } from "@/components/ui/button";
import { Calendar } from "lucide-react";
const CompetitionRunnerPage = () => {
const { id, taskId } = useParams<{ id: string; taskId?: string }>();
const navigate = useNavigate();
const [competitionTitle, setCompetitionTitle] = useState("Олимпиада DANO 2025. Индивидуальный этап");
const [tasks] = useState<Task[]>(mockTasks);
const [selectedTaskId, setSelectedTaskId] = useState<string | null>(taskId || null);
const [answer, setAnswer] = useState("");
useEffect(() => {
if (taskId) {
setSelectedTaskId(taskId);
} else if (tasks.length > 0) {
navigate(`/competition/${id}/tasks/${tasks[0].id}`, { replace: true });
}
}, [taskId, tasks, id, navigate]);
const handleTaskClick = (taskId: string) => {
if (selectedTaskId !== taskId) {
setSelectedTaskId(taskId);
navigate(`/competition/${id}/tasks/${taskId}`);
}
};
const currentTask = tasks.find(t => t.id === selectedTaskId);
const handleSubmit = () => {
console.log("Submitting answer:", answer);
// Submit logic here
};
const handleHistoryClick = () => {
console.log("View history");
};
return (
<>
<div className="sticky top-0 z-10 bg-white">
<div className="max-w-6xl mx-auto px-4">
<div className="py-3 text-center">
<h1 className="text-lg font-semibold font-hse-sans">{competitionTitle}</h1>
</div>
<div className="flex items-center justify-center gap-2 pb-3 overflow-x-auto no-scrollbar">
{tasks.map((task) => (
<div
key={task.id}
className={`${getTaskBgColor(task.status)} ${getTaskTextColor(task.status)}
rounded-lg px-3 py-1.5 font-medium text-sm font-hse-sans cursor-pointer
transition-all hover:brightness-95 flex-shrink-0
${selectedTaskId === task.id ? 'shadow-md transform scale-105' : ''}`}
onClick={() => handleTaskClick(task.id)}
>
{task.number}
</div>
))}
</div>
</div>
</div>
<div className="bg-[#F8F8F8] min-h-screen pb-8">
<div className="max-w-6xl mx-auto px-4 py-6">
{currentTask ? (
<div className="flex flex-col md:flex-row gap-6 font-hse-sans">
{/* Left Container - Task Description */}
<div className="flex-1 bg-white rounded-lg p-6">
<h2 className="text-xl font-medium mb-4">
Задача {currentTask.number}
</h2>
<div className="prose max-w-none text-gray-700">
<p>
Рассмотрим последовательность чисел 2, 3, 5, 9, 17, 33, 65, 129, ...
Каждый член этой последовательности, начиная с третьего, равен сумме двух предыдущих членов.
</p>
<p className="mt-4">
Найдите сумму первых 15 членов этой последовательности.
</p>
<p className="mt-4">
В ответе укажите целое число.
</p>
</div>
</div>
{/* Right Container - Solution Area */}
<div className="md:w-[350px] flex flex-col gap-4">
{/* Solution Status Card */}
<div className={`${getTaskBgColor(currentTask.status)} rounded-lg p-4 relative`}>
<div className="flex flex-col">
<span className={`${getTaskTextColor(currentTask.status)} font-medium`}>
Решение 12345
</span>
<span className={`${getTaskTextColor(currentTask.status)} mt-1`}>
Зачтено 5/10 баллов
</span>
</div>
<div className="absolute bottom-2 right-3 text-xs text-gray-600">
1 марта, 08:41
</div>
</div>
{/* Answer Input */}
<div className="bg-white rounded-lg p-4">
<textarea
className="w-full h-32 border border-gray-300 rounded-md p-3 font-hse-sans text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Введите ответ"
value={answer}
onChange={(e) => setAnswer(e.target.value)}
/>
</div>
{/* Action Buttons */}
<div className="flex gap-3 justify-between">
<Button
variant="outline"
className="font-hse-sans"
onClick={handleHistoryClick}
>
История
</Button>
<Button
className="bg-yellow-400 hover:bg-yellow-500 text-black font-hse-sans"
onClick={handleSubmit}
>
Отправить решение
</Button>
</div>
</div>
</div>
) : (
<div className="flex justify-center items-center h-40 bg-white rounded-lg">
<p className="font-hse-sans text-gray-500">
Загрузка задания...
</p>
</div>
)}
</div>
</div>
</>
);
};
export default CompetitionRunnerPage;
@@ -4,18 +4,13 @@ import { Task } from "@/shared/types";
import { getTaskBgColor, getTaskTextColor } from "./utils/utils";
import { mockTasks } from "@/shared/mocks/mocks";
import { Button } from "@/components/ui/button";
import { Calendar } from "lucide-react";
const CompetitionRunnerPage = () => {
const CompetitionSessionPage = () => {
const { id, taskId } = useParams<{ id: string; taskId?: string }>();
const navigate = useNavigate();
const [competitionTitle, setCompetitionTitle] = useState(
"Олимпиада DANO 2025. Индивидуальный этап",
);
const [competitionTitle, setCompetitionTitle] = useState("Олимпиада DANO 2025. Индивидуальный этап");
const [tasks] = useState<Task[]>(mockTasks);
const [selectedTaskId, setSelectedTaskId] = useState<string | null>(
taskId || null,
);
const [selectedTaskId, setSelectedTaskId] = useState<string | null>(taskId || null);
const [answer, setAnswer] = useState("");
useEffect(() => {
@@ -33,40 +28,35 @@ const CompetitionRunnerPage = () => {
}
};
const currentTask = tasks.find((t) => t.id === selectedTaskId);
const currentTask = tasks.find(t => t.id === selectedTaskId);
const handleSubmit = () => {
console.log("Submitting answer:", answer);
// Submit logic here
};
const handleHistoryClick = () => {
console.log("View history");
};
return (
<<<<<<< HEAD:services/frontend/src/pages/CompetitionRunnerPage/index.tsx
<>
<div className="sticky top-0 z-10 bg-white">
<div className="max-w-6xl mx-auto px-4">
<div className="py-3 text-center">
<h1 className="text-lg font-semibold font-hse-sans">{competitionTitle}</h1>
=======
<>
<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="font-hse-sans text-xl font-semibold">
<div className="sticky top-0 z-10 bg-white">
<div className="mx-auto max-w-6xl px-4">
<div className="py-3 text-center">
<h1 className="font-hse-sans text-lg font-semibold">
{competitionTitle}
</h1>
>>>>>>> 58f493250150ba62ac4f325a0708b96eb88661e9:services/frontend/src/pages/CompetitionSession/index.tsx
</div>
<div className="no-scrollbar flex items-center justify-center gap-2 overflow-x-auto pb-3">
<div className="flex items-center justify-center gap-2 pb-3 overflow-x-auto no-scrollbar">
{tasks.map((task) => (
<div
<div
key={task.id}
className={`${getTaskBgColor(task.status)} ${getTaskTextColor(task.status)} font-hse-sans flex-shrink-0 cursor-pointer rounded-lg px-3 py-1.5 text-sm font-medium transition-all hover:brightness-95 ${selectedTaskId === task.id ? "scale-105 transform shadow-md" : ""}`}
className={`${getTaskBgColor(task.status)} ${getTaskTextColor(task.status)}
rounded-lg px-3 py-1.5 font-medium text-sm font-hse-sans cursor-pointer
transition-all hover:brightness-95 flex-shrink-0
${selectedTaskId === task.id ? 'shadow-md transform scale-105' : ''}`}
onClick={() => handleTaskClick(task.id)}
>
{task.number}
@@ -75,74 +65,69 @@ const CompetitionRunnerPage = () => {
</div>
</div>
</div>
<div className="min-h-screen bg-[#F8F8F8] pb-8">
<div className="mx-auto max-w-6xl px-4 py-6">
<div className="bg-[#F8F8F8] min-h-screen pb-8">
<div className="max-w-6xl mx-auto px-4 py-6">
{currentTask ? (
<div className="font-hse-sans flex flex-col gap-6 md:flex-row">
<div className="flex flex-col md:flex-row gap-6 font-hse-sans">
{/* Left Container - Task Description */}
<div className="flex-1 rounded-lg bg-white p-6">
<h2 className="mb-4 text-xl font-medium">
<div className="flex-1 bg-white rounded-lg p-6">
<h2 className="text-xl font-medium mb-4">
Задача {currentTask.number}
</h2>
<div className="prose max-w-none text-gray-700">
<p>
Рассмотрим последовательность чисел 2, 3, 5, 9, 17, 33, 65,
129, ... Каждый член этой последовательности, начиная с
третьего, равен сумме двух предыдущих членов.
Рассмотрим последовательность чисел 2, 3, 5, 9, 17, 33, 65, 129, ...
Каждый член этой последовательности, начиная с третьего, равен сумме двух предыдущих членов.
</p>
<p className="mt-4">
Найдите сумму первых 15 членов этой последовательности.
</p>
<p className="mt-4">В ответе укажите целое число.</p>
<p className="mt-4">
В ответе укажите целое число.
</p>
</div>
</div>
{/* Right Container - Solution Area */}
<div className="flex flex-col gap-4 md:w-[350px]">
<div className="md:w-[350px] flex flex-col gap-4">
{/* Solution Status Card */}
<div
className={`${getTaskBgColor(currentTask.status)} relative rounded-lg p-4`}
>
<div className={`${getTaskBgColor(currentTask.status)} rounded-lg p-4 relative`}>
<div className="flex flex-col">
<span
className={`${getTaskTextColor(currentTask.status)} font-medium`}
>
<span className={`${getTaskTextColor(currentTask.status)} font-medium`}>
Решение 12345
</span>
<span
className={`${getTaskTextColor(currentTask.status)} mt-1`}
>
<span className={`${getTaskTextColor(currentTask.status)} mt-1`}>
Зачтено 5/10 баллов
</span>
</div>
<div className="absolute right-3 bottom-2 text-xs text-gray-600">
<div className="absolute bottom-2 right-3 text-xs text-gray-600">
1 марта, 08:41
</div>
</div>
{/* Answer Input */}
<div className="rounded-lg bg-white p-4">
<textarea
className="font-hse-sans h-32 w-full rounded-md border border-gray-300 p-3 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none"
<div className="bg-white rounded-lg p-4">
<textarea
className="w-full h-32 border border-gray-300 rounded-md p-3 font-hse-sans text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Введите ответ"
value={answer}
onChange={(e) => setAnswer(e.target.value)}
/>
</div>
{/* Action Buttons */}
<div className="flex justify-between gap-3">
<Button
variant="outline"
<div className="flex gap-3 justify-between">
<Button
variant="outline"
className="font-hse-sans"
onClick={handleHistoryClick}
>
История
</Button>
<Button
className="font-hse-sans bg-yellow-400 text-black hover:bg-yellow-500"
<Button
className="bg-yellow-400 hover:bg-yellow-500 text-black font-hse-sans"
onClick={handleSubmit}
>
Отправить решение
@@ -151,8 +136,10 @@ const CompetitionRunnerPage = () => {
</div>
</div>
) : (
<div className="flex h-40 items-center justify-center rounded-lg bg-white">
<p className="font-hse-sans text-gray-500">Загрузка задания...</p>
<div className="flex justify-center items-center h-40 bg-white rounded-lg">
<p className="font-hse-sans text-gray-500">
Загрузка задания...
</p>
</div>
)}
</div>
@@ -161,4 +148,4 @@ const CompetitionRunnerPage = () => {
);
};
export default CompetitionRunnerPage;
export default CompetitionSessionPage;
@@ -1,5 +1,4 @@
import { TaskStatus } from "@/shared/types/types";
import { TaskStatus } from "@/shared/types";
const getTaskBgColor = (status: TaskStatus): string => {
switch (status) {
case "uncleared": return "bg-[var(--color-task-uncleared)]";
@@ -1,7 +1,6 @@
import { Competition, CompetitionStatus } from "@/shared/types";
import { cn } from "@/shared/lib/utils";
import { Card, CardContent } from "@/components/ui/card";
import { useNavigate } from "react-router";
interface CompetitionCardProps {
competition: Competition;
@@ -12,11 +11,6 @@ export function CompetitionCard({
competition,
className,
}: CompetitionCardProps) {
const navigate = useNavigate();
const handleClick = () => {
navigate(`/competition/${competition.id}`);
};
return (
<Card
@@ -24,7 +18,6 @@ export function CompetitionCard({
"aspect-square h-full max-h-80 w-auto overflow-hidden",
className,
)}
onClick={handleClick}
>
<div className="relative h-full overflow-hidden">
<img
@@ -10,7 +10,7 @@ export function CompetitionGrid({ competitions }: CompetitionGridProps) {
return (
<div className="grid grid-cols-3 gap-9">
{competitions.map((competition) => (
<Link key={competition.id} to={`/competitions/${competition.id}`}>
<Link key={competition.id} to={`/competition/${competition.id}`}>
<CompetitionCard competition={competition} />
</Link>
))}