mirror of
https://gitlab.com/megazordpobeda/DataRush.git
synced 2026-05-23 01:37:11 +00:00
Merge branch 'master' of gitlab.prodcontest.ru:team-15/project
This commit is contained in:
@@ -62,3 +62,8 @@ class TaskAttachmentSchema(ModelSchema):
|
|||||||
"file",
|
"file",
|
||||||
"public",
|
"public",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TaskStatusSchema(Schema):
|
||||||
|
task_name: str
|
||||||
|
result: int
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from http import HTTPStatus as status
|
from http import HTTPStatus as status
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404, get_list_or_404
|
||||||
from ninja import File, Router, UploadedFile
|
from ninja import File, Router, UploadedFile
|
||||||
|
|
||||||
from api.v1.ping.schemas import PingOut
|
from api.v1.ping.schemas import PingOut
|
||||||
@@ -11,6 +11,7 @@ from api.v1.task.schemas import (
|
|||||||
TaskAttachmentSchema,
|
TaskAttachmentSchema,
|
||||||
TaskOutSchema,
|
TaskOutSchema,
|
||||||
TaskSubmissionOut,
|
TaskSubmissionOut,
|
||||||
|
TaskStatusSchema,
|
||||||
)
|
)
|
||||||
from apps.achievement.models import Achievement, UserAchievement
|
from apps.achievement.models import Achievement, UserAchievement
|
||||||
from apps.competition.models import State
|
from apps.competition.models import State
|
||||||
@@ -116,14 +117,16 @@ def submit_task(
|
|||||||
return status.FORBIDDEN, ForbiddenError()
|
return status.FORBIDDEN, ForbiddenError()
|
||||||
|
|
||||||
if task.type == CompetitionTask.CompetitionTaskType.INPUT:
|
if task.type == CompetitionTask.CompetitionTaskType.INPUT:
|
||||||
|
verdict = content.read() == task.correct_answer_file.read()
|
||||||
submission = CompetitionTaskSubmission.objects.create(
|
submission = CompetitionTaskSubmission.objects.create(
|
||||||
user=user,
|
user=user,
|
||||||
task=task,
|
task=task,
|
||||||
status=CompetitionTaskSubmission.StatusChoices.CHECKED,
|
status=CompetitionTaskSubmission.StatusChoices.CHECKED,
|
||||||
content=content,
|
content=content,
|
||||||
result={
|
result={
|
||||||
"correct": content.read() == task.correct_answer_file.read()
|
"correct": verdict
|
||||||
},
|
},
|
||||||
|
earned_points=task.points
|
||||||
)
|
)
|
||||||
if task.type == CompetitionTask.CompetitionTaskType.REVIEW:
|
if task.type == CompetitionTask.CompetitionTaskType.REVIEW:
|
||||||
submission = CompetitionTaskSubmission.objects.create(
|
submission = CompetitionTaskSubmission.objects.create(
|
||||||
@@ -173,3 +176,31 @@ def get_submissions_history(request, competition_id: UUID, task_id: UUID):
|
|||||||
def get_task_attachments(request, competition_id: UUID, task_id: UUID):
|
def get_task_attachments(request, competition_id: UUID, task_id: UUID):
|
||||||
task = get_object_or_404(CompetitionTask, id=task_id)
|
task = get_object_or_404(CompetitionTask, id=task_id)
|
||||||
return status.OK, CompetitionTaskAttachment.objects.filter(task=task).all()
|
return status.OK, CompetitionTaskAttachment.objects.filter(task=task).all()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"competitions/{competition_id}/results",
|
||||||
|
response={
|
||||||
|
status.OK: list[TaskStatusSchema],
|
||||||
|
status.UNAUTHORIZED: UnauthorizedError
|
||||||
|
},
|
||||||
|
)
|
||||||
|
def get_competition_results(request, competition_id: UUID):
|
||||||
|
tasks = get_list_or_404(CompetitionTask, competition_id=competition_id)
|
||||||
|
|
||||||
|
data = []
|
||||||
|
|
||||||
|
for task in tasks:
|
||||||
|
submissions = CompetitionTaskSubmission.objects.filter(
|
||||||
|
user=request.auth, task=task
|
||||||
|
).filter(status="checked").all()
|
||||||
|
if not submissions:
|
||||||
|
result = 0
|
||||||
|
else:
|
||||||
|
result = submissions[0].earned_points
|
||||||
|
data.append(TaskStatusSchema(
|
||||||
|
task_name=task.title,
|
||||||
|
result=result
|
||||||
|
))
|
||||||
|
|
||||||
|
return status.OK, data
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class CompetitionTaskSubmissionAdmin(admin.ModelAdmin):
|
|||||||
"user__username",
|
"user__username",
|
||||||
"user__email",
|
"user__email",
|
||||||
)
|
)
|
||||||
list_filter = ("plagiarism_checked", "status")
|
list_filter = ("plagiarism_detected", "status")
|
||||||
ordering = ["-timestamp"]
|
ordering = ["-timestamp"]
|
||||||
|
|
||||||
def has_add_permission(self, request, obj=None):
|
def has_add_permission(self, request, obj=None):
|
||||||
|
|||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
# Generated by Django 5.1.6 on 2025-03-03 13:12
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('task', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='competitiontasksubmission',
|
||||||
|
name='plagiarism_checked',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='competitiontasksubmission',
|
||||||
|
name='plagiarism_detected',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='обнаружен плагиат'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -178,8 +178,8 @@ class CompetitionTaskSubmission(BaseModel):
|
|||||||
checked_at = models.DateTimeField(
|
checked_at = models.DateTimeField(
|
||||||
null=True, blank=True, verbose_name="дата проверки"
|
null=True, blank=True, verbose_name="дата проверки"
|
||||||
)
|
)
|
||||||
plagiarism_checked = models.BooleanField(
|
plagiarism_detected = models.BooleanField(
|
||||||
default=False, verbose_name="проверено на плагиат"
|
default=False, verbose_name="обнаружен плагиат"
|
||||||
)
|
)
|
||||||
timestamp = models.DateTimeField(
|
timestamp = models.DateTimeField(
|
||||||
auto_now_add=True, verbose_name="дата отправки"
|
auto_now_add=True, verbose_name="дата отправки"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import httpx
|
|||||||
from celery import shared_task
|
from celery import shared_task
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from apps.task.models import CompetitionTaskSubmission
|
from apps.task.models import CompetitionTaskSubmission
|
||||||
|
|
||||||
@@ -12,10 +13,16 @@ from apps.task.models import CompetitionTaskSubmission
|
|||||||
def analyze_data_task(self, submission_id):
|
def analyze_data_task(self, submission_id):
|
||||||
submission = CompetitionTaskSubmission.objects.get(id=submission_id)
|
submission = CompetitionTaskSubmission.objects.get(id=submission_id)
|
||||||
try:
|
try:
|
||||||
code_url = f"{settings.MINIO_DEFAULT_CUSTOM_ENDPOINT_URL}{submission.content.path}"
|
code_url = (
|
||||||
|
f"{settings.MINIO_DEFAULT_CUSTOM_ENDPOINT_URL}/"
|
||||||
|
f"{urlparse(submission.content.url).path}"
|
||||||
|
)
|
||||||
files = [
|
files = [
|
||||||
{
|
{
|
||||||
"url": f"{settings.MINIO_DEFAULT_CUSTOM_ENDPOINT_URL}{attachment.path}",
|
"url": (
|
||||||
|
f"{settings.MINIO_DEFAULT_CUSTOM_ENDPOINT_URL}/"
|
||||||
|
f"{urlparse(submission.content.url).path}"
|
||||||
|
),
|
||||||
"bind_path": attachment.bind_at,
|
"bind_path": attachment.bind_at,
|
||||||
}
|
}
|
||||||
for attachment in submission.task.attachments.filter(
|
for attachment in submission.task.attachments.filter(
|
||||||
@@ -40,12 +47,12 @@ def analyze_data_task(self, submission_id):
|
|||||||
|
|
||||||
submission.stdout.save("output.txt", ContentFile(result["output"]))
|
submission.stdout.save("output.txt", ContentFile(result["output"]))
|
||||||
submission.result = {
|
submission.result = {
|
||||||
"correct": result["hash_match"],
|
"correct": result["correct"],
|
||||||
"result_hash": result["result_hash"],
|
"hash_match": result["hash_match"],
|
||||||
"error": result.get("error"),
|
"error": result.get("error"),
|
||||||
}
|
}
|
||||||
submission.earned_points = (
|
submission.earned_points = (
|
||||||
submission.task.points if result["hash_match"] else 0
|
submission.task.points if result["correct"] else 0
|
||||||
)
|
)
|
||||||
submission.status = CompetitionTaskSubmission.StatusChoices.CHECKED
|
submission.status = CompetitionTaskSubmission.StatusChoices.CHECKED
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
"react-router": "^7.2.0",
|
"react-router": "^7.2.0",
|
||||||
"react-router-dom": "^7.2.0",
|
"react-router-dom": "^7.2.0",
|
||||||
"rehype-katex": "^7.0.1",
|
"rehype-katex": "^7.0.1",
|
||||||
|
"remark-gfm": "^4.0.1",
|
||||||
"remark-math": "^6.0.0",
|
"remark-math": "^6.0.0",
|
||||||
"tailwind-merge": "^3.0.2",
|
"tailwind-merge": "^3.0.2",
|
||||||
"tailwindcss": "^4.0.9",
|
"tailwindcss": "^4.0.9",
|
||||||
@@ -605,8 +606,24 @@
|
|||||||
|
|
||||||
"lucide-react": ["lucide-react@0.476.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-x6cLTk8gahdUPje0hSgLN1/MgiJH+Xl90Xoxy9bkPAsMPOUiyRSKR4JCDPGVCEpyqnZXH3exFWNItcvra9WzUQ=="],
|
"lucide-react": ["lucide-react@0.476.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-x6cLTk8gahdUPje0hSgLN1/MgiJH+Xl90Xoxy9bkPAsMPOUiyRSKR4JCDPGVCEpyqnZXH3exFWNItcvra9WzUQ=="],
|
||||||
|
|
||||||
|
"markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
|
||||||
|
|
||||||
|
"mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="],
|
||||||
|
|
||||||
"mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="],
|
"mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="],
|
||||||
|
|
||||||
|
"mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="],
|
||||||
|
|
||||||
|
"mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="],
|
||||||
|
|
||||||
|
"mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="],
|
||||||
|
|
||||||
|
"mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="],
|
||||||
|
|
||||||
|
"mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="],
|
||||||
|
|
||||||
|
"mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="],
|
||||||
|
|
||||||
"mdast-util-math": ["mdast-util-math@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "longest-streak": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.1.0", "unist-util-remove-position": "^5.0.0" } }, "sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w=="],
|
"mdast-util-math": ["mdast-util-math@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "longest-streak": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.1.0", "unist-util-remove-position": "^5.0.0" } }, "sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w=="],
|
||||||
|
|
||||||
"mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="],
|
"mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="],
|
||||||
@@ -629,6 +646,20 @@
|
|||||||
|
|
||||||
"micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="],
|
"micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="],
|
||||||
|
|
||||||
|
"micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="],
|
||||||
|
|
||||||
|
"micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="],
|
||||||
|
|
||||||
|
"micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="],
|
||||||
|
|
||||||
|
"micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="],
|
||||||
|
|
||||||
|
"micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="],
|
||||||
|
|
||||||
|
"micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="],
|
||||||
|
|
||||||
|
"micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="],
|
||||||
|
|
||||||
"micromark-extension-math": ["micromark-extension-math@3.1.0", "", { "dependencies": { "@types/katex": "^0.16.0", "devlop": "^1.0.0", "katex": "^0.16.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg=="],
|
"micromark-extension-math": ["micromark-extension-math@3.1.0", "", { "dependencies": { "@types/katex": "^0.16.0", "devlop": "^1.0.0", "katex": "^0.16.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg=="],
|
||||||
|
|
||||||
"micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="],
|
"micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="],
|
||||||
@@ -743,12 +774,16 @@
|
|||||||
|
|
||||||
"rehype-katex": ["rehype-katex@7.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/katex": "^0.16.0", "hast-util-from-html-isomorphic": "^2.0.0", "hast-util-to-text": "^4.0.0", "katex": "^0.16.0", "unist-util-visit-parents": "^6.0.0", "vfile": "^6.0.0" } }, "sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA=="],
|
"rehype-katex": ["rehype-katex@7.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/katex": "^0.16.0", "hast-util-from-html-isomorphic": "^2.0.0", "hast-util-to-text": "^4.0.0", "katex": "^0.16.0", "unist-util-visit-parents": "^6.0.0", "vfile": "^6.0.0" } }, "sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA=="],
|
||||||
|
|
||||||
|
"remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="],
|
||||||
|
|
||||||
"remark-math": ["remark-math@6.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-math": "^3.0.0", "micromark-extension-math": "^3.0.0", "unified": "^11.0.0" } }, "sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA=="],
|
"remark-math": ["remark-math@6.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-math": "^3.0.0", "micromark-extension-math": "^3.0.0", "unified": "^11.0.0" } }, "sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA=="],
|
||||||
|
|
||||||
"remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="],
|
"remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="],
|
||||||
|
|
||||||
"remark-rehype": ["remark-rehype@11.1.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-g/osARvjkBXb6Wo0XvAeXQohVta8i84ACbenPpoSsxTOQH/Ae0/RGP4WZgnMH5pMLpsj4FG7OHmcIcXxpza8eQ=="],
|
"remark-rehype": ["remark-rehype@11.1.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-g/osARvjkBXb6Wo0XvAeXQohVta8i84ACbenPpoSsxTOQH/Ae0/RGP4WZgnMH5pMLpsj4FG7OHmcIcXxpza8eQ=="],
|
||||||
|
|
||||||
|
"remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="],
|
||||||
|
|
||||||
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
||||||
|
|
||||||
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
|
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
|
||||||
@@ -873,6 +908,8 @@
|
|||||||
|
|
||||||
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||||
|
|
||||||
|
"mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
|
||||||
|
|
||||||
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
|
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
"react-router": "^7.2.0",
|
"react-router": "^7.2.0",
|
||||||
"react-router-dom": "^7.2.0",
|
"react-router-dom": "^7.2.0",
|
||||||
"rehype-katex": "^7.0.1",
|
"rehype-katex": "^7.0.1",
|
||||||
|
"remark-gfm": "^4.0.1",
|
||||||
"remark-math": "^6.0.0",
|
"remark-math": "^6.0.0",
|
||||||
"tailwind-merge": "^3.0.2",
|
"tailwind-merge": "^3.0.2",
|
||||||
"tailwindcss": "^4.0.9",
|
"tailwindcss": "^4.0.9",
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import { useParams, Link, useNavigate } from "react-router-dom";
|
import { useParams, Link, useNavigate } from "react-router-dom";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ArrowLeft, Clock, Trophy, BookOpen } from "lucide-react";
|
import { ArrowLeft, Clock, Trophy, BookOpen, BarChart2, AlertCircle } from "lucide-react";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||||
import { getCompetition, startCompetition } from "@/shared/api/competitions";
|
import { getCompetition, startCompetition } from "@/shared/api/competitions";
|
||||||
import { getCompetitionTasks } from "@/shared/api/session";
|
import { getCompetitionTasks } from "@/shared/api/session";
|
||||||
import { Loading } from "@/components/ui/loading";
|
import { Loading } from "@/components/ui/loading";
|
||||||
import { CompetitionType } from "@/shared/types/competition";
|
import { CompetitionType } from "@/shared/types/competition";
|
||||||
|
import remarkMath from "remark-math";
|
||||||
|
import remarkGfm from "remark-gfm";
|
||||||
|
import rehypeKatex from "rehype-katex";
|
||||||
|
|
||||||
const CompetitionPage = () => {
|
const CompetitionPage = () => {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
@@ -39,6 +42,7 @@ const CompetitionPage = () => {
|
|||||||
console.error("Failed to start competition:", error);
|
console.error("Failed to start competition:", error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const formatDate = (date?: Date | string) => {
|
const formatDate = (date?: Date | string) => {
|
||||||
if (!date) return "";
|
if (!date) return "";
|
||||||
|
|
||||||
@@ -56,6 +60,10 @@ const CompetitionPage = () => {
|
|||||||
const handleStart = () => {
|
const handleStart = () => {
|
||||||
startMutation.mutate();
|
startMutation.mutate();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleViewResults = () => {
|
||||||
|
console.log("sorryan");
|
||||||
|
};
|
||||||
|
|
||||||
if (competitionQuery.isLoading) {
|
if (competitionQuery.isLoading) {
|
||||||
return <Loading />;
|
return <Loading />;
|
||||||
@@ -66,6 +74,27 @@ const CompetitionPage = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const competition = competitionQuery.data;
|
const competition = competitionQuery.data;
|
||||||
|
|
||||||
|
const isCompetitionEnded = () => {
|
||||||
|
if (!competition?.end_date) return false;
|
||||||
|
|
||||||
|
const endDate = new Date(competition.end_date);
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
return now > endDate;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isCompetitionNotStarted = () => {
|
||||||
|
if (!competition?.start_date) return false;
|
||||||
|
|
||||||
|
const startDate = new Date(competition.start_date);
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
return now < startDate;
|
||||||
|
};
|
||||||
|
|
||||||
|
const competitionEnded = isCompetitionEnded();
|
||||||
|
const competitionNotStarted = isCompetitionNotStarted();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
@@ -103,6 +132,18 @@ const CompetitionPage = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{competitionEnded && competition.type === CompetitionType.COMPETITIVE && (
|
||||||
|
<div className="bg-red-100 text-red-700 px-3 py-1 rounded-full text-sm font-medium">
|
||||||
|
Завершено
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{competitionNotStarted && competition.type === CompetitionType.COMPETITIVE && (
|
||||||
|
<div className="bg-yellow-100 text-yellow-700 px-3 py-1 rounded-full text-sm font-medium">
|
||||||
|
Скоро начнется
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 className="text-[34px] leading-11 font-semibold text-balance">
|
<h1 className="text-[34px] leading-11 font-semibold text-balance">
|
||||||
@@ -128,18 +169,43 @@ const CompetitionPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="prose prose-lg max-w-none text-xl leading-10 font-normal">
|
<div className="prose prose-lg max-w-none text-xl leading-10 font-normal">
|
||||||
<ReactMarkdown>{competition.description || ""}</ReactMarkdown>
|
<ReactMarkdown
|
||||||
|
remarkPlugins={[remarkMath, remarkGfm]}
|
||||||
|
rehypePlugins={[rehypeKatex]}
|
||||||
|
>
|
||||||
|
{competition.description || ""}
|
||||||
|
</ReactMarkdown>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-full *:w-full md:w-96">
|
<div className="w-full *:w-full md:w-96">
|
||||||
<Button
|
{competitionEnded && competition.type === CompetitionType.COMPETITIVE ? (
|
||||||
size={"lg"}
|
<Button
|
||||||
onClick={handleStart}
|
size={"lg"}
|
||||||
disabled={startMutation.isPending}
|
onClick={handleViewResults}
|
||||||
>
|
className="bg-indigo-600 hover:bg-indigo-700"
|
||||||
{startMutation.isPending ? "Загрузка..." : "Приступить к выполнению"}
|
>
|
||||||
</Button>
|
<BarChart2 size={18} className="mr-2" />
|
||||||
|
Смотреть результаты
|
||||||
|
</Button>
|
||||||
|
) : competitionNotStarted && competition.type === CompetitionType.COMPETITIVE ? (
|
||||||
|
<Button
|
||||||
|
size={"lg"}
|
||||||
|
disabled={true}
|
||||||
|
className="bg-gray-200 text-gray-500 cursor-not-allowed"
|
||||||
|
>
|
||||||
|
<AlertCircle size={18} className="mr-2" />
|
||||||
|
Скоро начнется
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
size={"lg"}
|
||||||
|
onClick={handleStart}
|
||||||
|
disabled={startMutation.isPending}
|
||||||
|
>
|
||||||
|
{startMutation.isPending ? "Загрузка..." : "Приступить к выполнению"}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+92
-19
@@ -1,8 +1,8 @@
|
|||||||
import React from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import { Task } from '@/shared/types/task';
|
import { Task } from '@/shared/types/task';
|
||||||
import { ArrowLeft } from 'lucide-react';
|
import { ArrowLeft, Clock } from 'lucide-react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { CompetitionType } from '@/shared/types/competition';
|
||||||
|
|
||||||
interface CompetitionHeaderProps {
|
interface CompetitionHeaderProps {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -10,6 +10,9 @@ interface CompetitionHeaderProps {
|
|||||||
competitionId: string;
|
competitionId: string;
|
||||||
setAnswer: (value: string) => void;
|
setAnswer: (value: string) => void;
|
||||||
setSelectedFile: (file: File | null) => void;
|
setSelectedFile: (file: File | null) => void;
|
||||||
|
competitionType?: CompetitionType;
|
||||||
|
startDate?: Date;
|
||||||
|
endDate?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CompetitionHeader: React.FC<CompetitionHeaderProps> = ({
|
const CompetitionHeader: React.FC<CompetitionHeaderProps> = ({
|
||||||
@@ -17,33 +20,103 @@ const CompetitionHeader: React.FC<CompetitionHeaderProps> = ({
|
|||||||
tasks,
|
tasks,
|
||||||
competitionId,
|
competitionId,
|
||||||
setAnswer,
|
setAnswer,
|
||||||
setSelectedFile
|
setSelectedFile,
|
||||||
|
competitionType,
|
||||||
|
startDate,
|
||||||
|
endDate
|
||||||
}) => {
|
}) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [timeLeft, setTimeLeft] = useState<string>('');
|
||||||
|
|
||||||
const handleTaskSelect = (taskId: string) => {
|
const handleTaskSelect = (taskId: string) => {
|
||||||
setAnswer("");
|
setAnswer("");
|
||||||
setSelectedFile(null);
|
setSelectedFile(null);
|
||||||
console.log("SETTER ERROR")
|
|
||||||
navigate(`/competition/${competitionId}/tasks/${taskId}`);
|
navigate(`/competition/${competitionId}/tasks/${taskId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const formatDate = (date?: Date) => {
|
||||||
|
if (!date) return '';
|
||||||
|
|
||||||
|
const dateObj = typeof date === 'string' ? new Date(date) : date;
|
||||||
|
return dateObj.toLocaleString('ru-RU', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!endDate || competitionType !== CompetitionType.COMPETITIVE) return;
|
||||||
|
|
||||||
|
const endDateObj = typeof endDate === 'string' ? new Date(endDate) : endDate;
|
||||||
|
|
||||||
|
const updateTimer = () => {
|
||||||
|
const now = new Date();
|
||||||
|
const diff = endDateObj.getTime() - now.getTime();
|
||||||
|
|
||||||
|
if (diff <= 0) {
|
||||||
|
navigate(`/competition/${competitionId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hours = Math.floor(diff / (1000 * 60 * 60));
|
||||||
|
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
|
||||||
|
const seconds = Math.floor((diff % (1000 * 60)) / 1000);
|
||||||
|
|
||||||
|
setTimeLeft(`${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
updateTimer();
|
||||||
|
const timerInterval = setInterval(updateTimer, 1000);
|
||||||
|
|
||||||
|
return () => clearInterval(timerInterval);
|
||||||
|
}, [endDate, competitionId, navigate, competitionType]);
|
||||||
|
|
||||||
|
const showTimeSection = competitionType === CompetitionType.COMPETITIVE && (startDate || endDate);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="bg-white shadow-sm sticky top-0 z-30 w-full">
|
<header className="bg-white shadow-sm sticky top-0 z-30 w-full">
|
||||||
<div className="mx-auto max-w-6xl px-4">
|
<div className="mx-auto max-w-6xl px-4">
|
||||||
<div className="flex items-center justify-between py-4">
|
<div className="flex items-center justify-between py-4">
|
||||||
<Link
|
<div>
|
||||||
to={`/competition/${competitionId}`}
|
<Link
|
||||||
className="flex items-center text-gray-600 hover:text-gray-900 transition-colors font-hse-sans text-sm"
|
to={`/competition/${competitionId}`}
|
||||||
>
|
className="flex items-center text-gray-600 hover:text-gray-900 transition-colors font-hse-sans text-sm"
|
||||||
<ArrowLeft className="h-4 w-4 mr-1" />
|
>
|
||||||
</Link>
|
<ArrowLeft className="h-4 w-4 mr-1" />
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<h1 className="font-hse-sans text-xl font-semibold text-center flex-1">
|
||||||
|
{title}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<h1 className="font-hse-sans text-xl font-semibold text-center flex-1">
|
{showTimeSection ? (
|
||||||
{title}
|
<div className="flex items-center text-gray-600 font-hse-sans text-sm">
|
||||||
</h1>
|
<div className="flex flex-col items-end">
|
||||||
|
{startDate && (
|
||||||
<div className="w-[70px]"></div>
|
<span className="text-xs text-gray-500">
|
||||||
|
Начало: {formatDate(startDate)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{endDate && (
|
||||||
|
<span className="text-xs text-gray-500">
|
||||||
|
Конец: {formatDate(endDate)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{timeLeft && (
|
||||||
|
<span className="font-medium text-red-600">
|
||||||
|
Осталось: {timeLeft}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="w-[70px]"></div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-center gap-4 pb-4 overflow-x-auto no-scrollbar">
|
<div className="flex items-center justify-center gap-4 pb-4 overflow-x-auto no-scrollbar">
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React from 'react';
|
|||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
import remarkMath from 'remark-math';
|
import remarkMath from 'remark-math';
|
||||||
import rehypeKatex from 'rehype-katex';
|
import rehypeKatex from 'rehype-katex';
|
||||||
|
import remarkGfm from 'remark-gfm';
|
||||||
import 'katex/dist/katex.min.css';
|
import 'katex/dist/katex.min.css';
|
||||||
import { Task } from '@/shared/types/task';
|
import { Task } from '@/shared/types/task';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
@@ -24,18 +25,27 @@ const TaskContent: React.FC<TaskContentProps> = ({ task }) => {
|
|||||||
|
|
||||||
const attachments = attachmentsQuery.data || [];
|
const attachments = attachmentsQuery.data || [];
|
||||||
|
|
||||||
|
const convertToMarkdown = (text: string): string => {
|
||||||
|
if (!text) return '';
|
||||||
|
|
||||||
|
let markdown = text.replace(/\n/g, '\n\n');
|
||||||
|
return markdown;
|
||||||
|
};
|
||||||
|
|
||||||
|
const markdownText = convertToMarkdown(task.description);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 bg-white rounded-lg p-6">
|
<div className="flex-1 bg-white rounded-lg p-6">
|
||||||
<h2 className="text-3xl font-semibold mb-6 font-hse-sans">
|
<h2 className="text-3xl font-semibold mb-6 font-hse-sans">
|
||||||
Задача {task.in_competition_position}
|
{task.title}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div className="prose prose-lg max-w-none text-gray-700 font-hse-sans mb-6">
|
<div className="prose prose-lg max-w-none text-gray-700 font-hse-sans mb-6">
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
remarkPlugins={[remarkMath]}
|
remarkPlugins={[remarkMath, remarkGfm]}
|
||||||
rehypePlugins={[rehypeKatex]}
|
rehypePlugins={[rehypeKatex]}
|
||||||
>
|
>
|
||||||
{task.description}
|
{markdownText}
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -97,6 +97,9 @@ const CompetitionSession = () => {
|
|||||||
competitionId={competitionId}
|
competitionId={competitionId}
|
||||||
setAnswer={setAnswer}
|
setAnswer={setAnswer}
|
||||||
setSelectedFile={setSelectedFile}
|
setSelectedFile={setSelectedFile}
|
||||||
|
competitionType={competition?.type}
|
||||||
|
startDate={competition?.start_date}
|
||||||
|
endDate={competition?.end_date}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<main className="flex-1 bg-[#F8F8F8] pb-8">
|
<main className="flex-1 bg-[#F8F8F8] pb-8">
|
||||||
@@ -120,6 +123,7 @@ const CompetitionSession = () => {
|
|||||||
selectedFile={selectedFile}
|
selectedFile={selectedFile}
|
||||||
setSelectedFile={setSelectedFile}
|
setSelectedFile={setSelectedFile}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
|
isSubmitting={submitMutation.isPending}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
+1
-3
@@ -8,7 +8,6 @@ interface FileSolutionProps {
|
|||||||
fileInputRef: React.RefObject<HTMLInputElement>;
|
fileInputRef: React.RefObject<HTMLInputElement>;
|
||||||
existingFileUrl?: string | null;
|
existingFileUrl?: string | null;
|
||||||
onClearExistingFile?: () => void; // New prop to clear existing file URL
|
onClearExistingFile?: () => void; // New prop to clear existing file URL
|
||||||
firstSolution: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const FileSolution: React.FC<FileSolutionProps> = ({
|
const FileSolution: React.FC<FileSolutionProps> = ({
|
||||||
@@ -17,7 +16,6 @@ const FileSolution: React.FC<FileSolutionProps> = ({
|
|||||||
fileInputRef,
|
fileInputRef,
|
||||||
existingFileUrl = null,
|
existingFileUrl = null,
|
||||||
onClearExistingFile,
|
onClearExistingFile,
|
||||||
firstSolution
|
|
||||||
}) => {
|
}) => {
|
||||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
if (event.target.files && event.target.files[0]) {
|
if (event.target.files && event.target.files[0]) {
|
||||||
@@ -68,7 +66,7 @@ const FileSolution: React.FC<FileSolutionProps> = ({
|
|||||||
? existingFileUrl.split('/').pop() || 'file'
|
? existingFileUrl.split('/').pop() || 'file'
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
const hasFile = !!selectedFile || (!!existingFileUrl && !firstSolution);
|
const hasFile = !!selectedFile || !!existingFileUrl;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ const TaskSolution: React.FC<TaskSolutionProps> = ({
|
|||||||
const latestSolution = solutionHistory[solutionHistory.length - 1];
|
const latestSolution = solutionHistory[solutionHistory.length - 1];
|
||||||
setDisplayedSolution(latestSolution);
|
setDisplayedSolution(latestSolution);
|
||||||
}
|
}
|
||||||
}, [solutionHistory, displayedSolution]);
|
}, [solutionHistory]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (prevTaskIdRef.current !== task.id) {
|
if (prevTaskIdRef.current !== task.id) {
|
||||||
@@ -70,38 +70,41 @@ const TaskSolution: React.FC<TaskSolutionProps> = ({
|
|||||||
}
|
}
|
||||||
}, [task.id, solutionHistory]);
|
}, [task.id, solutionHistory]);
|
||||||
|
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
if (solutionHistory.length > 0 &&
|
// if (solutionHistory.length > 0 &&
|
||||||
(!displayedSolution ||
|
// (!displayedSolution ||
|
||||||
(solutionHistory[solutionHistory.length - 1].id !== displayedSolution.id &&
|
// (solutionHistory[solutionHistory.length - 1].id !== displayedSolution.id))) {
|
||||||
displayedSolution.id === solutionHistory[solutionHistory.length - 2]?.id))) {
|
// setDisplayedSolution(solutionHistory[solutionHistory.length - 1]);
|
||||||
setDisplayedSolution(solutionHistory[solutionHistory.length - 1]);
|
// }
|
||||||
}
|
// }, [solutionHistory, displayedSolution]);
|
||||||
}, [solutionHistory, displayedSolution]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadSolutionContent = async () => {
|
const loadSolutionContent = async () => {
|
||||||
if (!displayedSolution || !displayedSolution.content) return;
|
if (!displayedSolution || !displayedSolution.content) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (task.type === TaskType.FILE) {
|
if (task.type === TaskType.FILE) {
|
||||||
|
setAnswer("");
|
||||||
setSelectedFile(null);
|
setSelectedFile(null);
|
||||||
setSelectedSolutionUrl(displayedSolution.content);
|
setSelectedSolutionUrl(displayedSolution.content);
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
|
setSelectedFile(null);
|
||||||
|
setSelectedSolutionUrl(null);
|
||||||
const response = await fetch(displayedSolution.content);
|
const response = await fetch(displayedSolution.content);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to fetch solution content: ${response.status}`);
|
throw new Error(`Failed to fetch solution content: ${response.status}`);
|
||||||
}
|
}
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
|
|
||||||
setAnswer(text);
|
setAnswer(text);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading solution content:', error);
|
console.error('Error loading solution content:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loadSolutionContent();
|
loadSolutionContent();
|
||||||
}, [displayedSolution, task.type, setAnswer, setSelectedFile]);
|
}, [displayedSolution, setAnswer, setSelectedFile]);
|
||||||
|
|
||||||
const handleOpenHistory = () => {
|
const handleOpenHistory = () => {
|
||||||
setIsHistoryOpen(true);
|
setIsHistoryOpen(true);
|
||||||
@@ -144,7 +147,6 @@ const TaskSolution: React.FC<TaskSolutionProps> = ({
|
|||||||
fileInputRef={fileInputRef}
|
fileInputRef={fileInputRef}
|
||||||
existingFileUrl={selectedSolutionUrl}
|
existingFileUrl={selectedSolutionUrl}
|
||||||
onClearExistingFile={handleClearExistingFile}
|
onClearExistingFile={handleClearExistingFile}
|
||||||
firstSolution={solutionHistory.length > 0}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { User } from "../types/user";
|
import { User } from "../types/user";
|
||||||
import { getCurrentUser } from "../api/user";
|
import { getCurrentUser } from "../api/user";
|
||||||
|
import Cookies from "js-cookie";
|
||||||
|
|
||||||
interface UserState {
|
interface UserState {
|
||||||
user: User | null;
|
user: User | null;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
|
||||||
fetchUser: () => Promise<void>;
|
fetchUser: () => Promise<void>;
|
||||||
|
clearUser: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useUserStore = create<UserState>((set) => ({
|
const useUserStore = create<UserState>((set) => ({
|
||||||
@@ -18,6 +20,16 @@ const useUserStore = create<UserState>((set) => ({
|
|||||||
const user = await getCurrentUser();
|
const user = await getCurrentUser();
|
||||||
set({ user, loading: false });
|
set({ user, loading: false });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
clearUser: () => {
|
||||||
|
set({ user: null });
|
||||||
|
|
||||||
|
const cookies = Cookies.get();
|
||||||
|
Object.keys(cookies).forEach(cookieName => {
|
||||||
|
Cookies.remove(cookieName, { path: '/' });
|
||||||
|
Cookies.remove(cookieName);
|
||||||
|
});
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export { useUserStore };
|
export { useUserStore };
|
||||||
Reference in New Issue
Block a user