diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 6060120..29ea8e2 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -80,7 +80,7 @@ deploy:
cd ~/deploy
docker compose pull > deploy.log 2>&1
docker compose down >> deploy.log 2>&1
- docker compose up -d --remove-orphans >> deploy.log 2>&1
+ docker compose up -d --remove-orphans --force-recreate >> deploy.log 2>&1
docker compose ps >> deploy.log 2>&1
EOF
- ssh $SSH_ADDRESS "docker system prune -a --force"
diff --git a/infrastructure/minio/.env.template b/infrastructure/minio/.env.template
index 9d55c3a..7b7c699 100644
--- a/infrastructure/minio/.env.template
+++ b/infrastructure/minio/.env.template
@@ -1,3 +1,4 @@
MINIO_ROOT_USER=admin
MINIO_ROOT_PASSWORD=password
MINIO_VOLUMES=/data
+MINIO_BROWSER_REDIRECT_URL=https://prod-team-15-minio-2pc0i3lc.final.prodcontest.ru/minio/ui/
diff --git a/infrastructure/nginx/nginx.conf b/infrastructure/nginx/nginx.conf
index ba8fad3..2773428 100644
--- a/infrastructure/nginx/nginx.conf
+++ b/infrastructure/nginx/nginx.conf
@@ -147,11 +147,6 @@ http {
return 204;
}
- add_header 'Access-Control-Allow-Origin' "$http_origin" always;
- add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
- add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, X-Requested-With' always;
- add_header 'Access-Control-Allow-Credentials' 'true' always;
-
proxy_buffering off;
proxy_request_buffering off;
client_max_body_size 100M;
diff --git a/services/backend/Dockerfile b/services/backend/Dockerfile
index b55f660..2678475 100644
--- a/services/backend/Dockerfile
+++ b/services/backend/Dockerfile
@@ -37,6 +37,6 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --start-interval=2s --retries=3 \
- CMD wget --no-verbose --tries=1 --spider http://127.0.0.1:8080/health?format=json || exit 1
+ CMD wget --no-verbose --tries=1 --spider http://127.0.0.1:8080/api/health?format=json || exit 1
-CMD gunicorn config.wsgi --workers=8 -b 0.0.0.0:8080 --access-logfile - --error-logfile -
+CMD gunicorn config.wsgi --workers=8 -b 0.0.0.0:8080 --access-logfile - --error-logfile - --timeout=600
diff --git a/services/backend/api/v1/review/schemas.py b/services/backend/api/v1/review/schemas.py
index 70bf9b3..831550b 100644
--- a/services/backend/api/v1/review/schemas.py
+++ b/services/backend/api/v1/review/schemas.py
@@ -4,8 +4,8 @@ from uuid import UUID
from django.http import HttpRequest
from ninja import ModelSchema, Schema
-from apps.review.models import Reviewer
-from apps.task.models import CompetetionTaskSumbission
+from apps.review.models import Reviewer, Review
+from apps.task.models import CompetitionTaskSubmission
class PingOut(Schema):
@@ -25,13 +25,13 @@ class SubmissionOut(ModelSchema):
status: Literal["sent", "checking", "checked"]
class Meta:
- model = CompetetionTaskSumbission
+ model = CompetitionTaskSubmission
exclude = ("user",)
class SubmissionsOut(Schema):
- submissions: list[SubmissionOut] = []
+ submissions: list = None
@staticmethod
- def resolve_submissions(self, context: HttpRequest) -> list[SubmissionOut]:
- return list(CompetetionTaskSumbission.objects.all())
+ def resolve_submissions(self, context) -> list[SubmissionOut]:
+ return list(Review.objects.filter(reviewer=context.get("request").auth))
\ No newline at end of file
diff --git a/services/backend/api/v1/review/views.py b/services/backend/api/v1/review/views.py
index d628faf..66f608e 100644
--- a/services/backend/api/v1/review/views.py
+++ b/services/backend/api/v1/review/views.py
@@ -1,10 +1,15 @@
+import logging
from http import HTTPStatus as status
+from uuid import UUID
from django.http import HttpRequest
+from django.shortcuts import get_object_or_404
from ninja import Router
from api.v1 import schemas as global_schemas
from api.v1.review import schemas
+from api.v1.task.schemas import TaskSubmissionIn
+from apps.task.models import CompetitionTaskSubmission
router = Router(tags=["review"])
@@ -16,7 +21,7 @@ router = Router(tags=["review"])
},
description="Список отправок, на проверку которых назначен ревьюер"
)
-def get_submissions(request: HttpRequest, token) -> tuple[status, schemas.SubmissionsOut]:
+def get_submissions(request: HttpRequest, token: str) -> tuple[status, schemas.SubmissionsOut]:
return status.OK, schemas.SubmissionsOut()
@@ -30,3 +35,13 @@ def get_submissions(request: HttpRequest, token) -> tuple[status, schemas.Submis
)
def get_reviewer_profile(request: HttpRequest, token: str):
return status.OK, request.auth
+
+@router.get(
+ "{token}/submissions/{submition_id}",
+ response={
+ status.OK: schemas.SubmissionOut,
+ },
+)
+def get_submission(request: HttpRequest, token: str, submition_id: UUID) -> tuple[status, schemas.SubmissionsOut]:
+ submission = get_object_or_404(CompetitionTaskSubmission, id=submition_id)
+ return status.OK, submission
diff --git a/services/backend/api/v1/task/views.py b/services/backend/api/v1/task/views.py
index 208dd36..85d3cc1 100644
--- a/services/backend/api/v1/task/views.py
+++ b/services/backend/api/v1/task/views.py
@@ -13,7 +13,7 @@ from api.v1.task.schemas import (
)
from apps.competition.models import State
from apps.task.models import (
- CompetetionTaskSumbission,
+ CompetitionTaskSubmission,
Competition,
CompetitionTask,
)
@@ -96,23 +96,23 @@ def submit_task(
)
if task.type == CompetitionTask.CompetitionTaskType.INPUT:
- CompetetionTaskSumbission.objects.create(
+ CompetitionTaskSubmission.objects.create(
user=user,
task=task,
- status=CompetetionTaskSumbission.StatusChoices.CHECKED,
+ status=CompetitionTaskSubmission.StatusChoices.CHECKED,
result={"correct": submission.content == task.answer_file_path},
)
if task.type == CompetitionTask.CompetitionTaskType.REVIEW:
- CompetetionTaskSumbission.objects.create(
+ CompetitionTaskSubmission.objects.create(
user=user,
task=task,
- status=CompetetionTaskSumbission.StatusChoices.SENT,
+ status=CompetitionTaskSubmission.StatusChoices.SENT,
)
if task.type == CompetitionTask.CompetitionTaskType.CHECKER:
- CompetetionTaskSumbission.objects.create(
+ CompetitionTaskSubmission.objects.create(
user=user,
task=task,
- status=CompetetionTaskSumbission.StatusChoices.CHECKING,
+ status=CompetitionTaskSubmission.StatusChoices.CHECKING,
)
- return TaskSubmissionOut(id=CompetetionTaskSumbission.id)
+ return TaskSubmissionOut(id=CompetitionTaskSubmission.id)
diff --git a/services/backend/apps/competition/admin.py b/services/backend/apps/competition/admin.py
index 59e6800..2aca32c 100644
--- a/services/backend/apps/competition/admin.py
+++ b/services/backend/apps/competition/admin.py
@@ -1,6 +1,7 @@
from django.contrib import admin
from apps.competition.models import Competition
+from apps.task.admin import CompetitionTaskInline
@admin.register(Competition)
@@ -8,3 +9,4 @@ class CompetitionAdmin(admin.ModelAdmin):
list_display = ("title", "end_date", "type",)
search_fields = ("title", "description",)
list_filter = ("type", "participation_type",)
+ inlines = [CompetitionTaskInline]
diff --git a/services/backend/apps/competition/migrations/0002_competition_tasks_alter_competition_participants_and_more.py b/services/backend/apps/competition/migrations/0002_competition_tasks_alter_competition_participants_and_more.py
new file mode 100644
index 0000000..5cfcfdf
--- /dev/null
+++ b/services/backend/apps/competition/migrations/0002_competition_tasks_alter_competition_participants_and_more.py
@@ -0,0 +1,35 @@
+# Generated by Django 5.1.6 on 2025-03-01 12:21
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('competition', '0001_initial'),
+ ('task', '0001_initial'),
+ ('user', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='competition',
+ name='tasks',
+ field=models.ManyToManyField(blank=True, related_name='tasks', to='task.competitiontask'),
+ ),
+ migrations.AlterField(
+ model_name='competition',
+ name='participants',
+ field=models.ManyToManyField(blank=True, editable=False, related_name='participants', to='user.user'),
+ ),
+ 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='type',
+ field=models.CharField(choices=[('solo', 'Индивидуальный')], max_length=10, verbose_name='Тип участия'),
+ ),
+ ]
diff --git a/services/backend/apps/competition/migrations/0003_remove_competition_tasks.py b/services/backend/apps/competition/migrations/0003_remove_competition_tasks.py
new file mode 100644
index 0000000..b03500b
--- /dev/null
+++ b/services/backend/apps/competition/migrations/0003_remove_competition_tasks.py
@@ -0,0 +1,17 @@
+# Generated by Django 5.1.6 on 2025-03-01 13:49
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('competition', '0002_competition_tasks_alter_competition_participants_and_more'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='competition',
+ name='tasks',
+ ),
+ ]
diff --git a/services/backend/apps/competition/models.py b/services/backend/apps/competition/models.py
index 0c6bd19..f9d91ec 100644
--- a/services/backend/apps/competition/models.py
+++ b/services/backend/apps/competition/models.py
@@ -1,6 +1,7 @@
from datetime import datetime
from django.db import models
+from tinymce.models import HTMLField
from apps.core.models import BaseModel
from apps.user.models import User
@@ -14,26 +15,26 @@ class Competition(BaseModel):
EDU = "edu", "Образовательный"
COMPETITIVE = "competitive", "Соревновательный"
- title = models.CharField(max_length=100, verbose_name="Название")
- description = models.TextField(verbose_name="Описание")
+ title = models.CharField(max_length=100, verbose_name="аазвание")
+ description = HTMLField(verbose_name="описание")
image_url = models.FileField(
- verbose_name="Изображение соревнования", null=True, blank=True
+ verbose_name="изображение соревнования", null=True, blank=True
)
end_date = models.DateTimeField(
- verbose_name="Дедлайн участия", null=True, blank=True
+ verbose_name="дедлайн участия", null=True, blank=True
)
start_date = models.DateTimeField(
- verbose_name="Дедлайн участия", null=True, blank=True
+ verbose_name="дедлайн участия", null=True, blank=True
)
type = models.CharField(
max_length=10,
choices=CompetitionType.choices,
- verbose_name="Тип участия",
+ verbose_name="тип участия",
)
participation_type = models.CharField(
max_length=11,
choices=CompetitionParticipationType.choices,
- verbose_name="Тип соревнования",
+ verbose_name="тип соревнования",
)
participants = models.ManyToManyField(User, related_name="participants", blank=True,
editable=False)
@@ -41,6 +42,8 @@ class Competition(BaseModel):
def __str__(self):
return self.title
+
+
class Meta:
verbose_name = "соревнование"
verbose_name_plural = "соревнования"
diff --git a/services/backend/apps/core/management/commands/generate_data.py b/services/backend/apps/core/management/commands/generate_data.py
index c781811..479fd23 100644
--- a/services/backend/apps/core/management/commands/generate_data.py
+++ b/services/backend/apps/core/management/commands/generate_data.py
@@ -8,7 +8,7 @@ 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.task.models import CompetitionTaskSubmission, CompetitionTask
from apps.user.models import User, UserRole
@@ -105,7 +105,7 @@ class Command(BaseCommand):
b"Submission content",
name=f"submission_{uuid.uuid4().hex}.txt",
)
- submission = CompetetionTaskSumbission.objects.create(
+ submission = CompetitionTaskSubmission.objects.create(
user=user,
task=task,
earned_points=random.randint(
diff --git a/services/backend/apps/review/models.py b/services/backend/apps/review/models.py
index 02b74af..60eb198 100644
--- a/services/backend/apps/review/models.py
+++ b/services/backend/apps/review/models.py
@@ -1,6 +1,7 @@
from django.db import models
from apps.core.models import BaseModel
+from apps.task.models import CompetitionTaskSubmission
class Reviewer(BaseModel):
@@ -8,3 +9,14 @@ class Reviewer(BaseModel):
surname = models.CharField(max_length=100)
token = models.CharField(max_length=100)
+
+class Review(BaseModel):
+ class ReviewStatusChoices(models.TextChoices):
+ NOT_CHECKED = "not_checked"
+ CHECKING = "checking"
+ CHECKED = "checked"
+
+ reviewer = models.ForeignKey(Reviewer, on_delete=models.CASCADE)
+ submission = models.ForeignKey(CompetitionTaskSubmission, on_delete=models.CASCADE)
+
+ state = models.CharField(choices=ReviewStatusChoices.choices, max_length=11)
\ No newline at end of file
diff --git a/services/backend/apps/task/admin.py b/services/backend/apps/task/admin.py
new file mode 100644
index 0000000..a466334
--- /dev/null
+++ b/services/backend/apps/task/admin.py
@@ -0,0 +1,13 @@
+from django.contrib import admin
+
+from apps.task.models import CompetitionTask
+
+
+@admin.register(CompetitionTask)
+class CompetitionTaskAdmin(admin.ModelAdmin):
+ list_display = ("title", "type", "points")
+
+
+class CompetitionTaskInline(admin.StackedInline):
+ model = CompetitionTask
+ extra = 0
diff --git a/services/backend/apps/task/apps.py b/services/backend/apps/task/apps.py
index 46f853d..4fe9436 100644
--- a/services/backend/apps/task/apps.py
+++ b/services/backend/apps/task/apps.py
@@ -4,3 +4,4 @@ from django.apps import AppConfig
class CompetitionsConfig(AppConfig):
name = "apps.task"
label = "task"
+ verbose_name = "Задания"
diff --git a/services/backend/apps/task/migrations/0001_initial.py b/services/backend/apps/task/migrations/0001_initial.py
index c3e32f9..e65c59e 100644
--- a/services/backend/apps/task/migrations/0001_initial.py
+++ b/services/backend/apps/task/migrations/0001_initial.py
@@ -38,8 +38,8 @@ class Migration(migrations.Migration):
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.CompetetionTaskSumbission.submission_content_upload_to)),
- ('stdout', models.FileField(blank=True, null=True, upload_to=apps.task.models.CompetetionTaskSumbission.submission_stdout_upload_to)),
+ ('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)),
diff --git a/services/backend/apps/task/migrations/0002_alter_competitiontask_options_and_more.py b/services/backend/apps/task/migrations/0002_alter_competitiontask_options_and_more.py
new file mode 100644
index 0000000..4a14698
--- /dev/null
+++ b/services/backend/apps/task/migrations/0002_alter_competitiontask_options_and_more.py
@@ -0,0 +1,45 @@
+# Generated by Django 5.1.6 on 2025-03-01 12:21
+
+import apps.task.models
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('competition', '0002_competition_tasks_alter_competition_participants_and_more'),
+ ('task', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='competitiontask',
+ options={'verbose_name': 'задание', 'verbose_name_plural': 'задания'},
+ ),
+ migrations.AlterField(
+ model_name='competitiontask',
+ name='competition',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='competition.competition', verbose_name='соревнование'),
+ ),
+ migrations.AlterField(
+ model_name='competitiontask',
+ name='correct_answer_file',
+ field=models.FileField(blank=True, null=True, upload_to=apps.task.models.CompetitionTask.answer_file_upload_to, verbose_name='правильный ответ'),
+ ),
+ migrations.AlterField(
+ model_name='competitiontask',
+ name='criteries',
+ field=models.JSONField(blank=True, null=True, verbose_name='критерии проверки'),
+ ),
+ migrations.AlterField(
+ model_name='competitiontask',
+ name='points',
+ field=models.IntegerField(blank=True, null=True, verbose_name='баллы за задание'),
+ ),
+ migrations.AlterField(
+ model_name='competitiontask',
+ name='type',
+ field=models.CharField(choices=[('input', 'Input'), ('checker', 'Checker'), ('review', 'Review')], max_length=8, verbose_name='тип задания'),
+ ),
+ ]
diff --git a/services/backend/apps/task/migrations/0002_competetiontasksumbission_reviewers.py b/services/backend/apps/task/migrations/0002_competetiontasksumbission_reviewers.py
new file mode 100644
index 0000000..bda15a2
--- /dev/null
+++ b/services/backend/apps/task/migrations/0002_competetiontasksumbission_reviewers.py
@@ -0,0 +1,19 @@
+# Generated by Django 5.1.6 on 2025-03-01 12:54
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('review', '0001_initial'),
+ ('task', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='competetiontasksumbission',
+ name='reviewers',
+ field=models.ManyToManyField(blank=True, related_name='reviewers', to='review.reviewer'),
+ ),
+ ]
diff --git a/services/backend/apps/task/migrations/0003_competitiontask_max_attemps_and_more.py b/services/backend/apps/task/migrations/0003_competitiontask_max_attemps_and_more.py
new file mode 100644
index 0000000..039cbdf
--- /dev/null
+++ b/services/backend/apps/task/migrations/0003_competitiontask_max_attemps_and_more.py
@@ -0,0 +1,51 @@
+# Generated by Django 5.1.6 on 2025-03-01 13:49
+
+import apps.task.models
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('competition', '0003_remove_competition_tasks'),
+ ('task', '0002_alter_competitiontask_options_and_more'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='competitiontask',
+ name='max_attemps',
+ field=models.PositiveSmallIntegerField(default=0),
+ ),
+ migrations.AlterField(
+ model_name='competitiontask',
+ name='answer_file_path',
+ field=models.TextField(blank=True, default='stdout', null=True, verbose_name='куда сохранять решения'),
+ ),
+ migrations.AlterField(
+ model_name='competitiontask',
+ name='competition',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='competition.competition'),
+ ),
+ migrations.AlterField(
+ model_name='competitiontask',
+ name='correct_answer_file',
+ field=models.FileField(blank=True, null=True, upload_to=apps.task.models.CompetitionTask.answer_file_upload_to, verbose_name='файл с правильным ответом'),
+ ),
+ migrations.AlterField(
+ model_name='competitiontask',
+ name='criteries',
+ field=models.JSONField(blank=True, null=True, verbose_name='критерии'),
+ ),
+ migrations.AlterField(
+ model_name='competitiontask',
+ name='title',
+ field=models.CharField(max_length=50, verbose_name='заголовок'),
+ ),
+ migrations.AlterField(
+ model_name='competitiontask',
+ name='type',
+ field=models.CharField(choices=[('input', 'Ввод правильного ответа'), ('checker', 'Вывод кода'), ('review', 'Ручная')], max_length=8, verbose_name='тип проверки'),
+ ),
+ ]
diff --git a/services/backend/apps/task/models.py b/services/backend/apps/task/models.py
index 08fe998..13dd487 100644
--- a/services/backend/apps/task/models.py
+++ b/services/backend/apps/task/models.py
@@ -1,6 +1,7 @@
from uuid import uuid4
from django.db import models
+from tinymce.models import HTMLField
from apps.competition.models import Competition
from apps.core.models import BaseModel
@@ -10,35 +11,68 @@ from apps.user.models import User
class CompetitionTask(BaseModel):
class CompetitionTaskType(models.TextChoices):
- INPUT = "input"
- CHECKER = "checker"
- REVIEW = "review"
+ INPUT = "input", "Ввод правильного ответа"
+ CHECKER = "checker", "Вывод кода"
+ REVIEW = "review", "Ручная"
def answer_file_upload_to(instance, filename) -> str:
return f"/tasks/{instance.id}/answer/{uuid4()}/filename"
competition = models.ForeignKey(Competition, on_delete=models.CASCADE)
- title = models.TextField(verbose_name="заголовок", max_length=50)
- description = models.TextField(verbose_name="описание", max_length=300)
- type = models.CharField(choices=CompetitionTaskType, max_length=8)
+ title = models.CharField(verbose_name="заголовок", max_length=50)
+ description = HTMLField(verbose_name="описание", max_length=300)
+ max_attemps = models.PositiveSmallIntegerField()
+ type = models.CharField(
+ choices=CompetitionTaskType, max_length=8, verbose_name="тип проверки"
+ )
# only when "input" or "checker" type
correct_answer_file = models.FileField(
- upload_to=answer_file_upload_to, null=True, blank=True
+ upload_to=answer_file_upload_to,
+ null=True,
+ blank=True,
+ verbose_name="файл с правильным ответом",
+ )
+ points = models.IntegerField(
+ null=True, blank=True, verbose_name="баллы за задание"
)
- points = models.IntegerField(null=True, blank=True)
# only when "checker" type
- answer_file_path = models.TextField(null=True, blank=True)
+ answer_file_path = models.TextField(
+ null=True,
+ blank=True,
+ verbose_name="куда сохранять решения",
+ default="stdout",
+ )
# only when "review" type
- criteries = models.JSONField(blank=True, null=True)
+ # todo make it more humanize
+ criteries = models.JSONField(
+ blank=True,
+ null=True,
+ verbose_name="критерии",
+ default=lambda: [
+ {
+ "name": "CHANGE ME",
+ "slug": "CHANGE ME",
+ "max_value": 0,
+ "min_value": 0,
+ }
+ ],
+ )
def clean(self):
ContestTaskCriteriesValidator()(self)
+ def __str__(self):
+ return self.title
-class CompetetionTaskSumbission(BaseModel):
+ class Meta:
+ verbose_name = "задание"
+ verbose_name_plural = "задания"
+
+
+class CompetitionTaskSubmission(BaseModel):
class StatusChoices(models.TextChoices):
SENT = "sent"
CHECKING = "checking"
diff --git a/services/backend/apps/user/models.py b/services/backend/apps/user/models.py
index a5b19fc..bc7ce07 100644
--- a/services/backend/apps/user/models.py
+++ b/services/backend/apps/user/models.py
@@ -9,9 +9,9 @@ class UserRole(models.Choices):
class User(BaseModel):
- email = models.EmailField(unique=True, verbose_name="Почта")
- username = models.SlugField(unique=True, verbose_name="Юзернейм")
- password = models.TextField(verbose_name="Пароль")
+ email = models.EmailField(unique=True, verbose_name="почта")
+ username = models.SlugField(unique=True, verbose_name="юзернейм")
+ password = models.TextField(verbose_name="пароль")
status = models.CharField(
max_length=10, choices=UserRole, default="student"
diff --git a/services/backend/config/settings.py b/services/backend/config/settings.py
index e383e80..af06a21 100644
--- a/services/backend/config/settings.py
+++ b/services/backend/config/settings.py
@@ -441,6 +441,7 @@ INSTALLED_APPS = [
"django_guid",
"ninja",
"minio_storage",
+ "tinymce",
# Internal apps
"apps.core",
"apps.user",
@@ -449,6 +450,22 @@ INSTALLED_APPS = [
"apps.task",
]
+# tinymce
+TINYMCE_DEFAULT_CONFIG = {
+ "theme": "silver",
+ "height": 500,
+ "menubar": False,
+ "plugins": "advlist,autolink,lists,link,image,charmap,print,preview,anchor,"
+ "searchreplace,visualblocks,code,fullscreen,insertdatetime,media,table,paste,"
+ "code,help,wordcount",
+ "toolbar": "undo redo | formatselect | "
+ "bold italic backcolor | alignleft aligncenter "
+ "alignright alignjustify | bullist numlist outdent indent | "
+ "removeformat | help",
+ "skin": "oxide-dark",
+ "content_css": "dark"
+}
+
# GUID
DJANGO_GUID = {
diff --git a/services/backend/config/urls.py b/services/backend/config/urls.py
index 264fcc1..f69ade8 100644
--- a/services/backend/config/urls.py
+++ b/services/backend/config/urls.py
@@ -12,6 +12,8 @@ admin.site.index_title = "DataRush"
urlpatterns = [
+ # tinymce
+ path('tinymce/', include('tinymce.urls')),
# Admin urls
path("admin/", admin.site.urls),
# API urls
diff --git a/services/backend/pyproject.toml b/services/backend/pyproject.toml
index b6292e0..a3c2a81 100644
--- a/services/backend/pyproject.toml
+++ b/services/backend/pyproject.toml
@@ -13,6 +13,7 @@ dependencies = [
"django-health-check>=3.18.3",
"django-minio-storage>=0.5.7",
"django-ninja>=1.3.0",
+ "django-pagedown>=2.2.1",
"django-stubs-ext>=5.1.3",
"gunicorn>=23.0.0",
"httpx>=0.28.1",
diff --git a/services/frontend/bun.lock b/services/frontend/bun.lock
index 1e65b63..4e32df0 100644
--- a/services/frontend/bun.lock
+++ b/services/frontend/bun.lock
@@ -4,6 +4,7 @@
"": {
"name": "frontend",
"dependencies": {
+ "@monaco-editor/react": "^4.7.0",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-tabs": "^1.1.3",
@@ -11,13 +12,18 @@
"autoprefixer": "^10.4.20",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+ "katex": "^0.16.21",
"lucide-react": "^0.476.0",
+ "monaco-editor": "^0.52.2",
"ofetch": "^1.4.1",
"postcss": "^8.5.3",
"react": "^19.0.0",
"react-dom": "^19.0.0",
+ "react-markdown": "^10.0.0",
"react-router": "^7.2.0",
"react-router-dom": "^7.2.0",
+ "rehype-katex": "^7.0.1",
+ "remark-math": "^6.0.0",
"tailwind-merge": "^3.0.2",
"tailwindcss": "^4.0.9",
"tailwindcss-animate": "^1.0.7",
@@ -117,6 +123,10 @@
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.2", "", {}, "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ=="],
+ "@monaco-editor/loader": ["@monaco-editor/loader@1.5.0", "", { "dependencies": { "state-local": "^1.0.6" } }, "sha512-hKoGSM+7aAc7eRTRjpqAZucPmoNOC4UUbknb/VNoTkEIkCPhqV8LfbsgM1webRM7S/z21eHEx9Fkwx8Z/C/+Xw=="],
+
+ "@monaco-editor/react": ["@monaco-editor/react@4.7.0", "", { "dependencies": { "@monaco-editor/loader": "^1.5.0" }, "peerDependencies": { "monaco-editor": ">= 0.25.0 < 1", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA=="],
+
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
@@ -257,16 +267,30 @@
"@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
+ "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
+
"@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="],
+ "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
+
+ "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
+
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
+ "@types/katex": ["@types/katex@0.16.7", "", {}, "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ=="],
+
+ "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
+
+ "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
+
"@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="],
"@types/react": ["@types/react@19.0.10", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g=="],
"@types/react-dom": ["@types/react-dom@19.0.4", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg=="],
+ "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
+
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.25.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.25.0", "@typescript-eslint/type-utils": "8.25.0", "@typescript-eslint/utils": "8.25.0", "@typescript-eslint/visitor-keys": "8.25.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-VM7bpzAe7JO/BFf40pIT1lJqS/z1F8OaSsUB3rpFJucQA4cOSuH2RVVVkFULN+En0Djgr29/jb4EQnedUo95KA=="],
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.25.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.25.0", "@typescript-eslint/types": "8.25.0", "@typescript-eslint/typescript-estree": "8.25.0", "@typescript-eslint/visitor-keys": "8.25.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-4gbs64bnbSzu4FpgMiQ1A+D+urxkoJk/kqlDJ2W//5SygaEiAP2B4GoS7TEdxgwol2el03gckFV9lJ4QOMiiHg=="],
@@ -283,6 +307,8 @@
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.25.0", "", { "dependencies": { "@typescript-eslint/types": "8.25.0", "eslint-visitor-keys": "^4.2.0" } }, "sha512-kCYXKAum9CecGVHGij7muybDfTS2sD3t0L4bJsEZLkyrXUImiCTq1M3LG2SRtOhiHFwMR9wAFplpT6XHYjTkwQ=="],
+ "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
+
"@vitejs/plugin-react-swc": ["@vitejs/plugin-react-swc@3.8.0", "", { "dependencies": { "@swc/core": "^1.10.15" }, "peerDependencies": { "vite": "^4 || ^5 || ^6" } }, "sha512-T4sHPvS+DIqDP51ifPqa9XIRAz/kIvIi8oXcnOZZgHmMotgmmdxe/DD5tMFlt5nuIRzT0/QuiwmKlH0503Aapw=="],
"acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
@@ -299,6 +325,8 @@
"autoprefixer": ["autoprefixer@10.4.20", "", { "dependencies": { "browserslist": "^4.23.3", "caniuse-lite": "^1.0.30001646", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.0.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g=="],
+ "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
+
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
@@ -311,8 +339,18 @@
"caniuse-lite": ["caniuse-lite@1.0.30001701", "", {}, "sha512-faRs/AW3jA9nTwmJBSO1PQ6L/EOgsB5HMQQq4iCu5zhPgVVgO/pZRHlmatwijZKetFw8/Pr4q6dEN8sJuq8qTw=="],
+ "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
+
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
+ "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
+
+ "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
+
+ "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
+
+ "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="],
+
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
@@ -321,6 +359,10 @@
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
+ "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
+
+ "commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
+
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
"cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
@@ -331,18 +373,26 @@
"debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
+ "decode-named-character-reference": ["decode-named-character-reference@1.0.2", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg=="],
+
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
+ "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
+
"destr": ["destr@2.0.3", "", {}, "sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ=="],
"detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="],
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
+ "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
+
"electron-to-chromium": ["electron-to-chromium@1.5.109", "", {}, "sha512-AidaH9JETVRr9DIPGfp1kAarm/W6hRJTPuCnkF+2MqhF4KaAgRIcBc8nvjk+YMXZhwfISof/7WG29eS4iGxQLQ=="],
"enhanced-resolve": ["enhanced-resolve@5.18.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg=="],
+ "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
+
"esbuild": ["esbuild@0.25.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.0", "@esbuild/android-arm": "0.25.0", "@esbuild/android-arm64": "0.25.0", "@esbuild/android-x64": "0.25.0", "@esbuild/darwin-arm64": "0.25.0", "@esbuild/darwin-x64": "0.25.0", "@esbuild/freebsd-arm64": "0.25.0", "@esbuild/freebsd-x64": "0.25.0", "@esbuild/linux-arm": "0.25.0", "@esbuild/linux-arm64": "0.25.0", "@esbuild/linux-ia32": "0.25.0", "@esbuild/linux-loong64": "0.25.0", "@esbuild/linux-mips64el": "0.25.0", "@esbuild/linux-ppc64": "0.25.0", "@esbuild/linux-riscv64": "0.25.0", "@esbuild/linux-s390x": "0.25.0", "@esbuild/linux-x64": "0.25.0", "@esbuild/netbsd-arm64": "0.25.0", "@esbuild/netbsd-x64": "0.25.0", "@esbuild/openbsd-arm64": "0.25.0", "@esbuild/openbsd-x64": "0.25.0", "@esbuild/sunos-x64": "0.25.0", "@esbuild/win32-arm64": "0.25.0", "@esbuild/win32-ia32": "0.25.0", "@esbuild/win32-x64": "0.25.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
@@ -367,8 +417,12 @@
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
+ "estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="],
+
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
+ "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
+
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
@@ -407,18 +461,52 @@
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
+ "hast-util-from-dom": ["hast-util-from-dom@5.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hastscript": "^9.0.0", "web-namespaces": "^2.0.0" } }, "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q=="],
+
+ "hast-util-from-html": ["hast-util-from-html@2.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.1.0", "hast-util-from-parse5": "^8.0.0", "parse5": "^7.0.0", "vfile": "^6.0.0", "vfile-message": "^4.0.0" } }, "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw=="],
+
+ "hast-util-from-html-isomorphic": ["hast-util-from-html-isomorphic@2.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-from-dom": "^5.0.0", "hast-util-from-html": "^2.0.0", "unist-util-remove-position": "^5.0.0" } }, "sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw=="],
+
+ "hast-util-from-parse5": ["hast-util-from-parse5@8.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="],
+
+ "hast-util-is-element": ["hast-util-is-element@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g=="],
+
+ "hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="],
+
+ "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.5", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-object": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-gHD+HoFxOMmmXLuq9f2dZDMQHVcplCVpMfBNRpJsF03yyLZvJGzsFORe8orVuYDX9k2w0VH0uF8oryFd1whqKQ=="],
+
+ "hast-util-to-text": ["hast-util-to-text@4.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "hast-util-is-element": "^3.0.0", "unist-util-find-after": "^5.0.0" } }, "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A=="],
+
+ "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
+
+ "hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="],
+
+ "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="],
+
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
+ "inline-style-parser": ["inline-style-parser@0.2.4", "", {}, "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q=="],
+
+ "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="],
+
+ "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="],
+
+ "is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="],
+
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
+ "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="],
+
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
+ "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
+
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="],
@@ -431,6 +519,8 @@
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
+ "katex": ["katex@0.16.21", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-XvqR7FgOHtWupfMiigNzmh+MgUVmDGU2kXZm899ZkPfcuoPuFxyHmXsgATDpFZDAXCI8tvinaVcDo8PIIJSo4A=="],
+
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
@@ -461,14 +551,80 @@
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
+ "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
+
"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=="],
+ "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-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-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="],
+
+ "mdast-util-mdxjs-esm": ["mdast-util-mdxjs-esm@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-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg=="],
+
+ "mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="],
+
+ "mdast-util-to-hast": ["mdast-util-to-hast@13.2.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA=="],
+
+ "mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="],
+
+ "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="],
+
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
+ "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="],
+
+ "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-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-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="],
+
+ "micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="],
+
+ "micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "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-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="],
+
+ "micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "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-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="],
+
+ "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],
+
+ "micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="],
+
+ "micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="],
+
+ "micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="],
+
+ "micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="],
+
+ "micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="],
+
+ "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="],
+
+ "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="],
+
+ "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="],
+
+ "micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="],
+
+ "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="],
+
+ "micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="],
+
+ "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="],
+
+ "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
+
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
+ "monaco-editor": ["monaco-editor@0.52.2", "", {}, "sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ=="],
+
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="],
@@ -491,6 +647,10 @@
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
+ "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],
+
+ "parse5": ["parse5@7.2.1", "", { "dependencies": { "entities": "^4.5.0" } }, "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ=="],
+
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
@@ -509,6 +669,8 @@
"prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.6.11", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-import-sort": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-style-order": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-import-sort", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-multiline-arrays", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-style-order", "prettier-plugin-svelte"] }, "sha512-YxaYSIvZPAqhrrEpRtonnrXdghZg1irNg4qrjboCXrpybLWVs55cW2N3juhspVJiO0JBvYJT8SYsJpc8OQSnsA=="],
+ "property-information": ["property-information@7.0.0", "", {}, "sha512-7D/qOz/+Y4X/rzSB6jKxKUsQnphO046ei8qxG59mtM3RG3DHgTK81HrxrmoDVINJb8NKT5ZsRbwHvQ6B68Iyhg=="],
+
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
@@ -517,6 +679,8 @@
"react-dom": ["react-dom@19.0.0", "", { "dependencies": { "scheduler": "^0.25.0" }, "peerDependencies": { "react": "^19.0.0" } }, "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ=="],
+ "react-markdown": ["react-markdown@10.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-4mTz7Sya/YQ1jYOrkwO73VcFdkFJ8L8I9ehCxdcV0XrClHyOJGKbBk5FR4OOOG+HnyKw5u+C/Aby9TwinCteYA=="],
+
"react-remove-scroll": ["react-remove-scroll@2.6.3", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ=="],
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
@@ -527,6 +691,14 @@
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
+ "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-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-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=="],
+
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
@@ -547,8 +719,16 @@
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
+ "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
+
+ "state-local": ["state-local@1.0.7", "", {}, "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w=="],
+
+ "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
+
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
+ "style-to-object": ["style-to-object@1.0.8", "", { "dependencies": { "inline-style-parser": "0.2.4" } }, "sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g=="],
+
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"tailwind-merge": ["tailwind-merge@3.0.2", "", {}, "sha512-l7z+OYZ7mu3DTqrL88RiKrKIqO3NcpEO8V/Od04bNpvk0kiIFndGEoqfuzvj4yuhRkHKjRkII2z+KS2HfPcSxw=="],
@@ -561,6 +741,10 @@
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
+ "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
+
+ "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
+
"ts-api-utils": ["ts-api-utils@2.0.1", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w=="],
"tsconfck": ["tsconfck@3.1.5", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"], "bin": { "tsconfck": "bin/tsconfck.js" } }, "sha512-CLDfGgUp7XPswWnezWwsCRxNmgQjhYq3VXHM0/XIRxhVrKw0M1if9agzryh1QS3nxjCROvV+xWxoJO1YctzzWg=="],
@@ -579,6 +763,22 @@
"undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
+ "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
+
+ "unist-util-find-after": ["unist-util-find-after@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ=="],
+
+ "unist-util-is": ["unist-util-is@6.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw=="],
+
+ "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],
+
+ "unist-util-remove-position": ["unist-util-remove-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q=="],
+
+ "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
+
+ "unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="],
+
+ "unist-util-visit-parents": ["unist-util-visit-parents@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw=="],
+
"update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="],
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
@@ -589,16 +789,26 @@
"vaul": ["vaul@1.1.2", "", { "dependencies": { "@radix-ui/react-dialog": "^1.1.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA=="],
+ "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
+
+ "vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="],
+
+ "vfile-message": ["vfile-message@4.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw=="],
+
"vite": ["vite@6.2.0", "", { "dependencies": { "esbuild": "^0.25.0", "postcss": "^8.5.3", "rollup": "^4.30.1" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ=="],
"vite-tsconfig-paths": ["vite-tsconfig-paths@5.1.4", "", { "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", "tsconfck": "^3.0.3" }, "peerDependencies": { "vite": "*" }, "optionalPeers": ["vite"] }, "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w=="],
+ "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
+
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
+ "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
+
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
"@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
@@ -609,6 +819,8 @@
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
+ "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=="],
}
}
diff --git a/services/frontend/package.json b/services/frontend/package.json
index 680a54b..d813286 100644
--- a/services/frontend/package.json
+++ b/services/frontend/package.json
@@ -10,6 +10,7 @@
"preview": "vite preview"
},
"dependencies": {
+ "@monaco-editor/react": "^4.7.0",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-tabs": "^1.1.3",
@@ -17,13 +18,18 @@
"autoprefixer": "^10.4.20",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+ "katex": "^0.16.21",
"lucide-react": "^0.476.0",
+ "monaco-editor": "^0.52.2",
"ofetch": "^1.4.1",
"postcss": "^8.5.3",
"react": "^19.0.0",
"react-dom": "^19.0.0",
+ "react-markdown": "^10.0.0",
"react-router": "^7.2.0",
"react-router-dom": "^7.2.0",
+ "rehype-katex": "^7.0.1",
+ "remark-math": "^6.0.0",
"tailwind-merge": "^3.0.2",
"tailwindcss": "^4.0.9",
"tailwindcss-animate": "^1.0.7",
diff --git a/services/frontend/src/App.tsx b/services/frontend/src/App.tsx
index 5af70fa..3f9a92f 100644
--- a/services/frontend/src/App.tsx
+++ b/services/frontend/src/App.tsx
@@ -11,11 +11,11 @@ const App = () => {
}>
} />
} />
-
+ }
/>
-
);
};
diff --git a/services/frontend/src/components/ui/input.tsx b/services/frontend/src/components/ui/input.tsx
new file mode 100644
index 0000000..e3b009e
--- /dev/null
+++ b/services/frontend/src/components/ui/input.tsx
@@ -0,0 +1,22 @@
+import * as React from "react"
+
+import { cn } from "@/shared/lib/utils"
+
+function Input({ className, type, ...props }: React.ComponentProps<"input">) {
+ return (
+
+ )
+}
+
+export { Input }
\ No newline at end of file
diff --git a/services/frontend/src/components/ui/textarea.tsx b/services/frontend/src/components/ui/textarea.tsx
new file mode 100644
index 0000000..f01fc67
--- /dev/null
+++ b/services/frontend/src/components/ui/textarea.tsx
@@ -0,0 +1,18 @@
+import * as React from "react"
+
+import { cn } from "@/shared/lib/utils"
+
+function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
+ return (
+
+ )
+}
+
+export { Textarea }
diff --git a/services/frontend/src/pages/CompetitionPreview/index.tsx b/services/frontend/src/pages/CompetitionPreview/index.tsx
index 1f07d24..56af7d9 100644
--- a/services/frontend/src/pages/CompetitionPreview/index.tsx
+++ b/services/frontend/src/pages/CompetitionPreview/index.tsx
@@ -1,8 +1,8 @@
-
import { useState } from "react";
import { useParams, Link, useNavigate } from "react-router-dom";
import { Button } from "@/components/ui/button";
import { ArrowLeft } from "lucide-react";
+import ReactMarkdown from 'react-markdown';
import { Competition } from "@/shared/types";
import { mockCompetitions, mockTasks } from "@/shared/mocks/mocks";
@@ -48,10 +48,10 @@ const CompetitionPage = () => {
{competition.name}
-
- {competition.description
- ?.split("\n")
- .map((line, i) =>
{line}
)}
+
+
+ {competition.description || ''}
+
@@ -63,4 +63,4 @@ const CompetitionPage = () => {
);
};
-export default CompetitionPage;
+export default CompetitionPage;
\ No newline at end of file
diff --git a/services/frontend/src/pages/CompetitionSession/components/CompetitionHeader/index.tsx b/services/frontend/src/pages/CompetitionSession/components/CompetitionHeader/index.tsx
new file mode 100644
index 0000000..7357b52
--- /dev/null
+++ b/services/frontend/src/pages/CompetitionSession/components/CompetitionHeader/index.tsx
@@ -0,0 +1,45 @@
+import React from 'react';
+import { Link } from 'react-router-dom';
+import { Task } from "@/shared/types";
+import { getTaskBgColor, getTaskTextColor } from '../../utils/utils';
+
+interface CompetitionHeaderProps {
+ title: string;
+ tasks: Task[];
+ competitionId: string;
+}
+
+const CompetitionHeader: React.FC
= ({
+ title,
+ tasks,
+ competitionId
+}) => {
+ return (
+
+ );
+};
+
+export default CompetitionHeader;
\ No newline at end of file
diff --git a/services/frontend/src/pages/CompetitionSession/components/TaskContent/index.tsx b/services/frontend/src/pages/CompetitionSession/components/TaskContent/index.tsx
new file mode 100644
index 0000000..9d6b687
--- /dev/null
+++ b/services/frontend/src/pages/CompetitionSession/components/TaskContent/index.tsx
@@ -0,0 +1,55 @@
+import React from 'react';
+import ReactMarkdown from 'react-markdown';
+import remarkMath from 'remark-math';
+import rehypeKatex from 'rehype-katex';
+import 'katex/dist/katex.min.css';
+import { Task } from "@/shared/types";
+
+interface TaskContentProps {
+ task: Task;
+}
+
+const TaskContent: React.FC = ({ task }) => {
+ const markdownContent = `
+## Задача на числовую последовательность
+
+Рассмотрим последовательность чисел:
+\`2, 3, 5, 9, 17, 33, 65, 129, ...\`
+
+Каждый член этой последовательности, **начиная с третьего**, равен сумме двух предыдущих членов:
+- $a_1 = 2$
+- $a_2 = 3$
+- $a_n = a_{n-1} + a_{n-2}$ для всех $n ≥ 3$
+
+### Задание:
+Найдите сумму первых 15 членов этой последовательности.
+
+*Примечание:* Для решения задачи вам может быть полезно записать несколько первых членов последовательности:
+1. $a_1 = 2$
+2. $a_2 = 3$
+3. $a_3 = 3 + 2 = 5$
+4. $a_4 = 5 + 3 = 8$
+5. $a_5 = 8 + 5 = 13$
+
+**В ответе укажите целое число.**
+ `;
+
+ return (
+
+
+ Задача {task.number}
+
+
+
+
+ {markdownContent}
+
+
+
+ );
+};
+
+export default TaskContent;
\ No newline at end of file
diff --git a/services/frontend/src/pages/CompetitionSession/index.tsx b/services/frontend/src/pages/CompetitionSession/index.tsx
index dc9b955..7fdafa0 100644
--- a/services/frontend/src/pages/CompetitionSession/index.tsx
+++ b/services/frontend/src/pages/CompetitionSession/index.tsx
@@ -1,38 +1,24 @@
-import { useState, useEffect } from "react";
-import { useParams, useNavigate } from "react-router-dom";
+import { useState } from "react";
+import { useParams, Navigate } 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 CompetitionHeader from "./components/CompetitionHeader";
+import TaskContent from "./components/TaskContent";
+import TaskSolution from "./modules/TaskSolution";
const CompetitionSessionPage = () => {
const { id, taskId } = useParams<{ id: string; taskId?: string }>();
- const navigate = useNavigate();
- const [competitionTitle, setCompetitionTitle] = useState("Олимпиада DANO 2025. Индивидуальный этап");
const [tasks] = useState(mockTasks);
- const [selectedTaskId, setSelectedTaskId] = useState(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 currentTask = tasks.find(t => t.id === taskId) || null;
- const handleTaskClick = (taskId: string) => {
- if (selectedTaskId !== taskId) {
- setSelectedTaskId(taskId);
- navigate(`/competition/${id}/tasks/${taskId}`);
- }
- };
+ if (!taskId && tasks.length > 0) {
+ return ;
+ }
- const currentTask = tasks.find(t => t.id === selectedTaskId);
-
const handleSubmit = () => {
console.log("Submitting answer:", answer);
- // Submit logic here
};
const handleHistoryClick = () => {
@@ -40,100 +26,25 @@ const CompetitionSessionPage = () => {
};
return (
- <>
-
-
-
-
- {competitionTitle}
-
-
-
-
- {tasks.map((task) => (
-
handleTaskClick(task.id)}
- >
- {task.number}
-
- ))}
-
-
-
+
+
-
+
{currentTask ? (
- {/* Left Container - Task Description */}
-
-
- Задача {currentTask.number}
-
-
-
-
- Рассмотрим последовательность чисел 2, 3, 5, 9, 17, 33, 65, 129, ...
- Каждый член этой последовательности, начиная с третьего, равен сумме двух предыдущих членов.
-
-
- Найдите сумму первых 15 членов этой последовательности.
-
-
- В ответе укажите целое число.
-
-
-
-
- {/* Right Container - Solution Area */}
-
- {/* Solution Status Card */}
-
-
-
- Решение 12345
-
-
- Зачтено 5/10 баллов
-
-
-
- 1 марта, 08:41
-
-
-
- {/* Answer Input */}
-
-
-
- {/* Action Buttons */}
-
-
- История
-
-
- Отправить решение
-
-
-
+
+
) : (
@@ -143,9 +54,9 @@ const CompetitionSessionPage = () => {
)}
-
- >
+
+
);
};
-export default CompetitionSessionPage;
+export default CompetitionSessionPage;
\ No newline at end of file
diff --git a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/ActionButtons/index.tsx b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/ActionButtons/index.tsx
new file mode 100644
index 0000000..0a12865
--- /dev/null
+++ b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/ActionButtons/index.tsx
@@ -0,0 +1,29 @@
+import React from 'react';
+import { Button } from "@/components/ui/button";
+
+interface ActionButtonsProps {
+ onHistoryClick: () => void;
+ onSubmit: () => void;
+}
+
+const ActionButtons: React.FC = ({ onHistoryClick, onSubmit }) => {
+ return (
+
+
+ История
+
+
+ Отправить решение
+
+
+ );
+};
+
+export default ActionButtons;
\ No newline at end of file
diff --git a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/CodeSolution/index.tsx b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/CodeSolution/index.tsx
new file mode 100644
index 0000000..76800ad
--- /dev/null
+++ b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/CodeSolution/index.tsx
@@ -0,0 +1,117 @@
+import React, { useRef, useEffect, useState } from 'react';
+import * as monaco from 'monaco-editor';
+import { Copy, Check } from 'lucide-react'; // Import Lucide React icons
+
+interface CodeSolutionProps {
+ answer: string;
+ setAnswer: (value: string) => void;
+ language?: string;
+}
+
+const CodeSolution: React.FC = ({
+ answer,
+ setAnswer,
+ language = 'python'
+}) => {
+ const editorContainerRef = useRef(null);
+ const editorRef = useRef(null);
+ const [copied, setCopied] = useState(false);
+
+ const languageDisplay = language === 'python' ? 'Python 3.11' : language;
+
+ const copyToClipboard = () => {
+ if (answer) {
+ navigator.clipboard.writeText(answer)
+ .then(() => {
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ })
+ .catch(err => {
+ console.error('Failed to copy: ', err);
+ });
+ }
+ };
+
+ useEffect(() => {
+ if (editorContainerRef.current) {
+ editorRef.current = monaco.editor.create(editorContainerRef.current, {
+ value: answer,
+ language,
+ theme: 'vs-light',
+ automaticLayout: true,
+ minimap: { enabled: false },
+ scrollBeyondLastLine: false,
+ fontSize: 14,
+ fontFamily: 'hse-sans, Menlo, Monaco, "Courier New", monospace',
+ lineNumbers: 'on',
+ lineNumbersMinChars: 3,
+ glyphMargin: false,
+ folding: false,
+ roundedSelection: false,
+ renderLineHighlight: 'none',
+ overviewRulerBorder: false,
+ overviewRulerLanes: 0,
+ hideCursorInOverviewRuler: true,
+ scrollbar: {
+ useShadows: false,
+ verticalHasArrows: false,
+ horizontalHasArrows: false,
+ vertical: 'hidden',
+ horizontal: 'auto',
+ verticalScrollbarSize: 0,
+ horizontalScrollbarSize: 8,
+ alwaysConsumeMouseWheel: false
+ },
+ });
+
+ editorRef.current.onDidChangeModelContent(() => {
+ if (editorRef.current) {
+ const value = editorRef.current.getValue();
+ setAnswer(value);
+ }
+ });
+
+ return () => {
+ if (editorRef.current) {
+ editorRef.current.dispose();
+ }
+ };
+ }
+ }, [language]);
+
+ useEffect(() => {
+ if (editorRef.current) {
+ const currentValue = editorRef.current.getValue();
+ if (currentValue !== answer) {
+ editorRef.current.setValue(answer);
+ }
+ }
+ }, [answer]);
+
+ return (
+
+
+
{languageDisplay}
+
+ {copied ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ );
+};
+
+export default CodeSolution;
\ No newline at end of file
diff --git a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/FileSolution/index.tsx b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/FileSolution/index.tsx
new file mode 100644
index 0000000..5e103b6
--- /dev/null
+++ b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/FileSolution/index.tsx
@@ -0,0 +1,93 @@
+import React from 'react';
+import { FileIcon } from 'lucide-react';
+import { Button } from "@/components/ui/button";
+
+interface FileSolutionProps {
+ selectedFile: File | null;
+ setSelectedFile: (file: File | null) => void;
+ fileInputRef: React.RefObject;
+}
+
+const FileSolution: React.FC = ({
+ selectedFile,
+ setSelectedFile,
+ fileInputRef
+}) => {
+ const handleFileChange = (event: React.ChangeEvent) => {
+ if (event.target.files && event.target.files[0]) {
+ setSelectedFile(event.target.files[0]);
+ }
+ };
+
+ const handleFileUploadClick = () => {
+ fileInputRef.current?.click();
+ };
+
+ const handleDragOver = (e: React.DragEvent) => {
+ e.preventDefault();
+ e.currentTarget.classList.add('bg-gray-50');
+ };
+
+ const handleDragLeave = (e: React.DragEvent) => {
+ e.preventDefault();
+ e.currentTarget.classList.remove('bg-gray-50');
+ };
+
+ const handleDrop = (e: React.DragEvent) => {
+ e.preventDefault();
+ e.currentTarget.classList.remove('bg-gray-50');
+
+ if (e.dataTransfer.files && e.dataTransfer.files[0]) {
+ setSelectedFile(e.dataTransfer.files[0]);
+ }
+ };
+
+ return (
+ <>
+
+
+ {selectedFile ? (
+
+
+
+ {selectedFile.name}
+ {(selectedFile.size / 1024).toFixed(1)} KB
+ setSelectedFile(null)}
+ >
+ Выбрать другой файл
+
+
+
+ ) : (
+
+
+
+ Загрузить файл
+
+
+ Доступные форматы: jpg, jpeg, png
+
+
+ )}
+ >
+ );
+};
+
+export default FileSolution;
\ No newline at end of file
diff --git a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/InputSolution/index.tsx b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/InputSolution/index.tsx
new file mode 100644
index 0000000..48c5e5c
--- /dev/null
+++ b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/InputSolution/index.tsx
@@ -0,0 +1,22 @@
+import React from 'react';
+import { Input } from "@/components/ui/input";
+
+interface InputSolutionProps {
+ answer: string;
+ setAnswer: (value: string) => void;
+}
+
+const InputSolution: React.FC = ({ answer, setAnswer }) => {
+ return (
+
+ setAnswer(e.target.value)}
+ />
+
+ );
+};
+
+export default InputSolution;
\ No newline at end of file
diff --git a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/SolutionStatus/index.tsx b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/SolutionStatus/index.tsx
new file mode 100644
index 0000000..bbde29c
--- /dev/null
+++ b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/SolutionStatus/index.tsx
@@ -0,0 +1,27 @@
+import React from 'react';
+import { Task } from "@/shared/types";
+import { getTaskBgColor, getTaskTextColor } from '@/pages/CompetitionSession/utils/utils';
+
+interface SolutionStatusProps {
+ task: Task;
+}
+
+const SolutionStatus: React.FC = ({ task }) => {
+ return (
+
+
+
+ Решение 12345
+
+
+ Зачтено 5/10 баллов
+
+
+
+ 1 марта, 08:41
+
+
+ );
+};
+
+export default SolutionStatus;
\ No newline at end of file
diff --git a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx
new file mode 100644
index 0000000..c2e9fc3
--- /dev/null
+++ b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx
@@ -0,0 +1,52 @@
+import React, { useState, useRef } from 'react';
+import { Task } from "@/shared/types";
+import SolutionStatus from './components/SolutionStatus';
+import InputSolution from './components/InputSolution';
+import FileSolution from './components/FileSolution';
+import CodeSolution from './components/CodeSolution';
+import ActionButtons from './components/ActionButtons';
+
+interface TaskSolutionProps {
+ task: Task;
+ answer: string;
+ setAnswer: (value: string) => void;
+ onSubmit: () => void;
+ onHistoryClick: () => void;
+}
+
+const TaskSolution: React.FC = ({
+ task,
+ answer,
+ setAnswer,
+ onSubmit,
+ onHistoryClick,
+}) => {
+ const [selectedFile, setSelectedFile] = useState(null);
+ const fileInputRef = useRef(null);
+
+ return (
+
+
+
+ {task.solutionType === 'input' && (
+
+ )}
+
+ {task.solutionType === 'file' && (
+
+ )}
+
+ {task.solutionType === 'code' && (
+
+ )}
+
+
+
+ );
+};
+
+export default TaskSolution;
\ No newline at end of file
diff --git a/services/frontend/src/shared/mocks/mocks.ts b/services/frontend/src/shared/mocks/mocks.ts
index 2373910..58099e7 100644
--- a/services/frontend/src/shared/mocks/mocks.ts
+++ b/services/frontend/src/shared/mocks/mocks.ts
@@ -53,14 +53,55 @@ const mockCompetitions: Competition[] = [
];
const mockTasks: Task[] = [
- { id: "1", number: "1.1", status: "uncleared" },
- { id: "2", number: "1.2", status: "checking" },
- { id: "3", number: "1.3", status: "correct" },
- { id: "4", number: "2.1", status: "partial" },
- { id: "5", number: "2.2", status: "wrong" },
- { id: "6", number: "2.3", status: "uncleared" },
- { id: "7", number: "3.1", status: "checking" },
- { id: "8", number: "3.2", status: "correct" },
+ {
+ id: "1",
+ number: "1.1",
+ status: "uncleared",
+ solutionType: "input"
+ },
+ {
+ id: "2",
+ number: "1.2",
+ status: "checking",
+ solutionType: "file"
+ },
+ {
+ id: "3",
+ number: "1.3",
+ status: "correct",
+ solutionType: "code"
+ },
+ {
+ id: "4",
+ number: "2.1",
+ status: "partial",
+ solutionType: "input"
+ },
+ {
+ id: "5",
+ number: "2.2",
+ status: "wrong",
+ solutionType: "file"
+ },
+ {
+ id: "6",
+ number: "2.3",
+ status: "uncleared",
+ solutionType: "code"
+ },
+ {
+ id: "7",
+ number: "3.1",
+ status: "checking",
+ solutionType: "file"
+ },
+ {
+ id: "8",
+ number: "3.2",
+ status: "correct",
+ solutionType: "input"
+ },
];
+
export { mockCompetitions, mockTasks };
diff --git a/services/frontend/src/shared/types.ts b/services/frontend/src/shared/types.ts
index 3732350..cb4089c 100644
--- a/services/frontend/src/shared/types.ts
+++ b/services/frontend/src/shared/types.ts
@@ -14,11 +14,13 @@ interface Competition {
}
type TaskStatus = "uncleared" | "checking" | "correct" | "partial" | "wrong";
+type SolutionType = "input" | "file" | "code";
interface Task {
id: string;
number: string;
status: TaskStatus;
+ solutionType: SolutionType;
}
export { CompetitionStatus };