fix: merge conflicts

This commit is contained in:
rngsurrounded
2025-03-01 19:34:35 +09:00
44 changed files with 908 additions and 404 deletions
+3 -2
View File
@@ -18,6 +18,7 @@ router = Router(tags=["competition"])
status.OK: schemas.CompetitionOut,
status.BAD_REQUEST: global_schemas.BadRequestError,
status.UNAUTHORIZED: global_schemas.UnauthorizedError,
status.NOT_FOUND: global_schemas.NotFoundError,
},
)
def get_competition(
@@ -30,14 +31,14 @@ def get_competition(
@router.get(
"competitions",
response={
status.OK: list[schemas.CompetitionListInstanceOut],
status.OK: list[schemas.CompetitionOut],
status.BAD_REQUEST: global_schemas.BadRequestError,
status.UNAUTHORIZED: global_schemas.UnauthorizedError,
},
)
def list_competitions(
request: HttpRequest, is_participating: bool
) -> tuple[status, list[schemas.CompetitionListInstanceOut]]:
) -> tuple[status, list[schemas.CompetitionOut]]:
user = request.auth
if is_participating:
competitions = Competition.objects.filter(participants=user)
+26
View File
@@ -0,0 +1,26 @@
from abc import ABC
from typing import Optional
from django.http import HttpRequest
from django.shortcuts import get_object_or_404
from django.urls import resolve
from ninja.errors import AuthenticationError
from ninja.security import APIKeyQuery
from ninja.security.apikey import APIKeyBase
from apps.review.models import Reviewer
class APIKeyPath(APIKeyBase, ABC):
openapi_in: str = "path"
def _get_key(self, request: HttpRequest) -> Optional[str]:
func, args, kwargs = resolve(request.path)
return kwargs.get(self.param_name)
class ReviewerAuth(APIKeyPath):
param_name = "token"
def authenticate(self, request, token):
if not (reviewer := Reviewer.objects.filter(token=token).first()):
raise AuthenticationError
return reviewer
+37
View File
@@ -0,0 +1,37 @@
from typing import List, Literal
from uuid import UUID
from django.http import HttpRequest
from ninja import Schema, ModelSchema
from apps.review.models import Reviewer
from apps.task.models import CompetetionTaskSumbission
class PingOut(Schema):
status: str = "ok"
class ReviewerOut(ModelSchema):
id: UUID
class Meta:
model = Reviewer
exclude = ("token",)
class SubmissionOut(ModelSchema):
id: UUID
status: Literal["sent", "checking", "checked"]
class Meta:
model = CompetetionTaskSumbission
exclude = (
"user",
)
class SubmissionsOut(Schema):
submissions: list[SubmissionOut] = []
@staticmethod
def resolve_submissions(self, context: HttpRequest) -> List[SubmissionOut]:
print(CompetetionTaskSumbission.objects.all())
return list(CompetetionTaskSumbission.objects.all())
+34
View File
@@ -0,0 +1,34 @@
from http import HTTPStatus as status
from django.http import HttpRequest
from ninja import Router
from api.v1.review import schemas
from api.v1 import schemas as global_schemas
router = Router(tags=["review"])
@router.get(
"{token}/tasks",
response={
status.OK: schemas.SubmissionsOut,
},
)
def ping(request: HttpRequest, token) -> tuple[status, schemas.SubmissionsOut]:
return status.OK, schemas.SubmissionsOut()
@router.get(
"{token}",
response={
status.OK: schemas.ReviewerOut,
status.UNAUTHORIZED: global_schemas.UnauthorizedError
},
description="token есть и в сваггер авторизации, но оно не работает, не верьте. подставляйте токен вручную в query"
)
def get_reviewer(
request: HttpRequest,
token: str
):
return status.OK, request.auth
+8
View File
@@ -6,7 +6,9 @@ from api.v1 import handlers
from api.v1.auth import BearerAuth
from api.v1.competition.views import router as competition_router
from api.v1.ping.views import router as ping_router
from api.v1.review.auth import ReviewerAuth
from api.v1.user.views import router as user_router
from api.v1.review.views import router as review_router
router = NinjaAPI(
title="DataRush API",
@@ -30,6 +32,12 @@ router.add_router(
competition_router,
auth=BearerAuth(),
)
router.add_router(
"review",
review_router,
auth=ReviewerAuth(),
)
for exception, handler in handlers.exception_handlers:
+21
View File
@@ -0,0 +1,21 @@
from typing import Literal
from uuid import UUID
from ninja import ModelSchema, Schema
from apps.competition.models import State
from apps.task.models import CompetitionTask
class TaskOutSchema(ModelSchema):
class Meta:
model = CompetitionTask
fields = ["id", "competition", "title", "description", "type"]
class TaskSubmissionIn(Schema):
type: Literal["input", "file", "code"]
content: str
class TaskSubmissionOut(Schema):
submission_id: UUID
+115
View File
@@ -0,0 +1,115 @@
from http import HTTPStatus as status
from uuid import UUID
from django.shortcuts import get_object_or_404
from ninja import Router
from django.shortcuts import get_object_or_404
from api.v1.schemas import NotFoundError, UnauthorizedError, ForbiddenError
from api.v1.ping.schemas import PingOut
from api.v1.task.schemas import (
TaskOutSchema,
TaskSubmissionOut,
TaskSubmissionIn,
)
from apps.task.models import (
Competition,
CompetitionTask,
CompetetionTaskSumbission,
)
from apps.competition.models import State
router = Router(tags=["competition"])
@router.post(
"competitions/{competition_id}/start",
description="Start a competition completing (open access to tasks)",
response={
status.OK: PingOut,
status.UNAUTHORIZED: UnauthorizedError,
status.NOT_FOUND: NotFoundError,
},
)
def start_competition(request, competition_id: UUID) -> PingOut:
competition = get_object_or_404(Competition, pk=competition_id)
state_obj, _ = State.objects.update_or_create(
user=request.auth, competition=competition, state="started"
)
return status.OK, PingOut()
@router.get(
"competitions/{competition_id}/tasks",
description="Get all tasks of competition (works only if user started competition)",
response={
status.OK: list[TaskOutSchema],
status.UNAUTHORIZED: UnauthorizedError,
status.FORBIDDEN: ForbiddenError,
status.NOT_FOUND: NotFoundError,
},
)
def get_competition_tasks(request, competition_id: UUID) -> list[TaskOutSchema]:
competition = get_object_or_404(Competition, pk=competition_id)
state = State.objects.filter(
user=request.auth, competition=competition, state="started"
).first()
if not state:
return 403, ForbiddenError()
return status.OK, CompetitionTask.objects.filter(competition=competition).all()
@router.get(
"competitions/{competition_id}/tasks/{task_id}",
description="Get a task of competition task",
response={
status.OK: TaskOutSchema,
status.UNAUTHORIZED: UnauthorizedError,
status.FORBIDDEN: ForbiddenError,
status.NOT_FOUND: NotFoundError,
},
)
def get_task(request, competition_id: str, task_id: str) -> TaskOutSchema: ...
@router.post(
"competitions/{competition_id}/tasks/{task_id}/submit",
description="Submit task solution",
response={
status.OK: TaskSubmissionOut,
status.UNAUTHORIZED: UnauthorizedError,
status.FORBIDDEN: ForbiddenError,
status.NOT_FOUND: NotFoundError,
},
)
def submit_task(
request, competition_id: str, task_id: str, submission: TaskSubmissionIn
) -> PingOut:
user = request.auth
competetion = get_object_or_404(Competition, id=competition_id)
task = get_object_or_404(
CompetitionTask, competetion=competetion, id=task_id
)
if task.type == CompetitionTask.CompetitionTaskType.INPUT:
CompetetionTaskSumbission.objects.create(
user=user,
task=task,
status=CompetetionTaskSumbission.StatusChoices.CHECKED,
result={"correct": submission.content == task.answer_file_path},
)
if task.type == CompetitionTask.CompetitionTaskType.REVIEW:
CompetetionTaskSumbission.objects.create(
user=user,
task=task,
status=CompetetionTaskSumbission.StatusChoices.SENT,
)
if task.type == CompetitionTask.CompetitionTaskType.CHECKER:
CompetetionTaskSumbission.objects.create(
user=user,
task=task,
status=CompetetionTaskSumbission.StatusChoices.CHECKING,
)
return TaskSubmissionOut(id=CompetetionTaskSumbission.id)
@@ -1,5 +1,6 @@
# Generated by Django 5.1.6 on 2025-02-28 21:27
# Generated by Django 5.1.6 on 2025-03-01 08:47
import django.db.models.deletion
import uuid
from django.db import migrations, models
@@ -9,6 +10,7 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('user', '0001_initial'),
]
operations = [
@@ -23,10 +25,23 @@ class Migration(migrations.Migration):
('start_date', models.DateTimeField(blank=True, null=True, verbose_name='Дедлайн участия')),
('type', models.CharField(choices=[('solo', 'Solo')], max_length=10, verbose_name='Тип участия')),
('participation_type', models.CharField(choices=[('edu', 'Edu'), ('competitive', 'Competitive')], max_length=11, verbose_name='Тип соревнования')),
('participants', models.ManyToManyField(related_name='participants', to='user.user')),
],
options={
'verbose_name': 'соревнование',
'verbose_name_plural': 'соревнования',
},
),
migrations.CreateModel(
name='State',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('state', models.CharField(choices=[('not_started', 'Not Started'), ('started', 'Started'), ('finished', 'Finished')], max_length=11)),
('competition', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='competition.competition')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='user.user')),
],
options={
'abstract': False,
},
),
]
@@ -1,19 +0,0 @@
# Generated by Django 5.1.6 on 2025-02-28 22:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('competition', '0001_initial'),
('user', '0002_user_status'),
]
operations = [
migrations.AddField(
model_name='competition',
name='participants',
field=models.ManyToManyField(related_name='participants', to='user.user'),
),
]
@@ -1,28 +0,0 @@
# Generated by Django 5.1.6 on 2025-02-28 23:26
import django.db.models.deletion
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('competition', '0002_competition_participants'),
('user', '0003_alter_user_status'),
]
operations = [
migrations.CreateModel(
name='State',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('state', models.CharField(choices=[('not_started', 'Not Started'), ('started', 'Started'), ('finished', 'Finished')], max_length=11)),
('competition', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='competition.competition')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='user.user')),
],
options={
'abstract': False,
},
),
]
@@ -0,0 +1,19 @@
# Generated by Django 5.1.6 on 2025-03-01 08:10
import datetime
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('competition', '0003_state'),
]
operations = [
migrations.AddField(
model_name='state',
name='changed_at',
field=models.DateTimeField(default=datetime.datetime.now),
),
]
@@ -1,3 +1,5 @@
from datetime import datetime
from django.db import models
from apps.core.models import BaseModel
@@ -49,3 +51,4 @@ class State(BaseModel):
user = models.ForeignKey(User, on_delete=models.CASCADE)
competition = models.ForeignKey(Competition, on_delete=models.CASCADE)
state = models.CharField(choices=StateChoices.choices, max_length=11)
changed_at = models.DateTimeField(default=datetime.now)
+6
View File
@@ -0,0 +1,6 @@
from django.apps import AppConfig
class CoreConfig(AppConfig):
name = "apps.review"
label = "review"
@@ -0,0 +1,27 @@
# Generated by Django 5.1.6 on 2025-03-01 08:47
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Reviewer',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.CharField(max_length=100)),
('surname', models.CharField(max_length=100)),
('token', models.CharField(max_length=100)),
],
options={
'abstract': False,
},
),
]
+10
View File
@@ -0,0 +1,10 @@
from django.db import models
from apps.core.models import BaseModel
class Reviewer(BaseModel):
name = models.CharField(max_length=100)
surname = models.CharField(max_length=100)
token = models.CharField(max_length=100)
@@ -0,0 +1,51 @@
# Generated by Django 5.1.6 on 2025-03-01 09:42
import apps.task.models
import django.db.models.deletion
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('competition', '0001_initial'),
('user', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='CompetitionTask',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('title', models.TextField(max_length=50, verbose_name='заголовок')),
('description', models.TextField(max_length=300, verbose_name='описание')),
('type', models.CharField(choices=[('input', 'Input'), ('checker', 'Checker'), ('review', 'Review')], max_length=8)),
('correct_answer_file', models.FileField(upload_to=apps.task.models.CompetitionTask.answer_file_upload_to)),
('answer_file_path', models.TextField()),
('criteries', models.JSONField(blank=True, null=True)),
('competition', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='competition.competition')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='CompetetionTaskSumbission',
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(upload_to=apps.task.models.CompetetionTaskSumbission.submission_stdout_upload_to)),
('result', models.JSONField(default={})),
('timestamp', models.DateTimeField(auto_now_add=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='user.user')),
('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='task.competitiontask')),
],
options={
'abstract': False,
},
),
]
+51 -7
View File
@@ -1,11 +1,12 @@
from random import choice
from uuid import uuid4
from competition.models import Competition
from core.models import BaseModel
from django.db import models
from apps.task.validators import ContestTaskCriteriesValidator
from apps.competition.models import Competition
from apps.core.models import BaseModel
from apps.user.models import User
class CompetitionTask(BaseModel):
class CompetitionTaskType(models.TextChoices):
@@ -14,21 +15,64 @@ class CompetitionTask(BaseModel):
REVIEW = "review"
def answer_file_upload_to(instance, filename) -> str:
return f"/tasks/{instance.id}/answer/{uuid4}"
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)
type = models.CharField(choices=CompetitionTaskType, max_length=8)
# only when "input" or "checker" type
correct_answer_file = models.FileField(upload_to=answer_file_upload_to)
correct_answer_file = models.FileField(
upload_to=answer_file_upload_to, null=True, blank=True
)
points = models.IntegerField(null=True, blank=True)
# only when "checker" type
answer_file_path = models.TextField()
answer_file_path = models.TextField(null=True, blank=True)
# only when "review" type
criteries = models.JSONField(blank=True, null=True)
def clean(self):
ContestTaskCriteriesValidator()(self)
class CompetetionTaskSumbission(BaseModel):
class StatusChoices(models.TextChoices):
SENT = "sent"
CHECKING = "checking"
CHECKED = "checked"
def submission_content_upload_to(instance, filename) -> str:
return f"/submissions/{instance.id}/content"
def submission_stdout_upload_to(instance, filename) -> str:
return f"/submissions/{instance.id}/stdout"
user = models.ForeignKey(User, on_delete=models.CASCADE)
task = models.ForeignKey(CompetitionTask, on_delete=models.CASCADE)
status = models.CharField(
choices=StatusChoices.choices,
default=StatusChoices.SENT,
max_length=8,
)
# code or text or file
content = models.FileField(upload_to=submission_content_upload_to)
# only if task type is checker
stdout = models.FileField(
upload_to=submission_stdout_upload_to, null=True, blank=True
)
# depends on task type:
# - input: {"correct": boolean}
# - file: {"total_points": integer, "by_criteria": ["criteria_name": integer]}
# - code: {"correct": boolean}
result = models.JSONField(default=None, null=True, blank=True)
# just more readable result representation, maybe will be calcuated somehow more complex depends on criteria
earned_points = models.IntegerField()
timestamp = models.DateTimeField(auto_now_add=True)
+128
View File
@@ -0,0 +1,128 @@
import tempfile
import os
import sys
import ast
from io import StringIO
import hashlib
from config.celery import app
ALLOWED_MODULES = {
"pandas",
"numpy",
"matplotlib",
"seaborn",
"scipy",
"sklearn",
"datetime",
"json",
"csv",
"math",
"statistics",
}
class SecurityException(Exception):
pass
def validate_code(code_str):
try:
tree = ast.parse(code_str)
except SyntaxError as e:
raise SecurityException(f"Syntax error: {str(e)}")
class ImportVisitor(ast.NodeVisitor):
def visit_Import(self, node):
for alias in node.names:
module = alias.name.split(".")[0]
if module not in ALLOWED_MODULES:
raise SecurityException(f"Disallowed import: {module}")
def visit_ImportFrom(self, node):
if node.module:
module = node.module.split(".")[0]
if module not in ALLOWED_MODULES:
raise SecurityException(
f"Disallowed import from: {module}"
)
class SecurityVisitor(ast.NodeVisitor):
def generic_visit(self, node):
if isinstance(node, (ast.Call, ast.Attribute)):
if "system" in getattr(node, "attr", ""):
raise SecurityException("Dangerous system call detected")
super().generic_visit(node)
try:
ImportVisitor().visit(tree)
SecurityVisitor().visit(tree)
except SecurityException as e:
raise
except Exception as e:
raise SecurityException(f"Security check failed: {str(e)}")
def secure_exec(code_str, result_path):
original_dir = os.getcwd()
original_stdout = sys.stdout
sys.stdout = captured_stdout = StringIO()
result_content = None
with tempfile.TemporaryDirectory() as temp_dir:
try:
os.chdir(temp_dir)
restricted_globals = {
"__builtins__": {
"open": lambda f, *a, **kw: open(f, *a, **kw),
"print": print,
"str": str,
"int": int,
"float": float,
"bool": bool,
"list": list,
"dict": dict,
"tuple": tuple,
"set": set,
}
}
exec(code_str, restricted_globals)
if result_path == "stdout":
result_content = captured_stdout.getvalue().encode("utf-8")
else:
with open(result_path, "rb") as f:
result_content = f.read()
except Exception as e:
raise RuntimeError(f"Execution error: {str(e)}")
finally:
os.chdir(original_dir)
sys.stdout = original_stdout
return result_content
@app.task(bind=True)
def analyze_data_task(self, code_str, result_path, expected_bytes):
try:
validate_code(code_str)
result_content = secure_exec(code_str, result_path)
result_hash = hashlib.sha256(result_content).hexdigest()
expected_hash = hashlib.sha256(expected_bytes).hexdigest()
return {
"success": True,
"match": result_hash == expected_hash,
"result_hash": result_hash,
"expected_hash": expected_hash,
}
except SecurityException as e:
return {"success": False, "error": f"Security violation: {str(e)}"}
except RuntimeError as e:
return {"success": False, "error": f"Execution error: {str(e)}"}
except Exception as e:
return {"success": False, "error": f"Unexpected error: {str(e)}"}
@@ -1,4 +1,4 @@
# Generated by Django 5.1.6 on 2025-02-28 20:46
# Generated by Django 5.1.6 on 2025-03-01 08:47
import uuid
from django.db import migrations, models
@@ -19,6 +19,7 @@ class Migration(migrations.Migration):
('email', models.EmailField(max_length=254, unique=True, verbose_name='Почта')),
('username', models.SlugField(unique=True, verbose_name='Юзернейм')),
('password', models.TextField(verbose_name='Пароль')),
('status', models.CharField(choices=[('student', 'Student'), ('metodist', 'Metodist')], default='student', max_length=10)),
],
options={
'verbose_name': 'пользователь',
@@ -1,18 +0,0 @@
# Generated by Django 5.1.6 on 2025-02-28 22:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('user', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='user',
name='status',
field=models.CharField(choices=[('student', 'Student'), ('metodist', 'Metodist')], default='student', max_length=10),
),
]
@@ -1,18 +0,0 @@
# Generated by Django 5.1.6 on 2025-02-28 22:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('user', '0002_user_status'),
]
operations = [
migrations.AlterField(
model_name='user',
name='status',
field=models.CharField(choices=[('student', 'Student'), ('metodist', 'Metodist')], default='student', max_length=10),
),
]
+2
View File
@@ -445,6 +445,8 @@ INSTALLED_APPS = [
"apps.core",
"apps.user",
"apps.competition",
"apps.review",
"apps.task"
]
# GUID
+7 -3
View File
@@ -3,16 +3,20 @@ import "./styles/globals.css";
import CompetitionsPage from "./pages/CompetitionsPage";
import CompetitionPreviewPage from "./pages/CompetitionPreviewPage";
import CompetitionRunnerPage from "./pages/CompetitionRunnerPage";
import { NavbarLayout } from "./widgets/navbar-layout";
const App = () => {
return (
<Routes>
<Route element={<NavbarLayout />}>
<Route path="/" element={<CompetitionsPage />} />
</Route>
<Route path="/competition/:id" element={<CompetitionPreviewPage />} />
<Route path="/competition/:id/tasks/:taskId" element={<CompetitionRunnerPage />} />
<Route
path="/competition/:id/tasks/:taskId"
element={<CompetitionRunnerPage />}
/>
</Routes>
);
};
@@ -0,0 +1,21 @@
import { DataRush } from "@/components/ui/icons/datarush";
import { ChevronDown } from "lucide-react";
import { Link } from "react-router";
const Header = () => {
return (
<header className="bg-card sticky top-0 z-30 flex h-[72px] w-full items-center justify-center">
<div className="flex w-full max-w-5xl items-center justify-between">
<Link to="/">
<DataRush />
</Link>
<div className="flex items-center gap-1">
<span className="text-lg font-semibold">itqdev</span>
<ChevronDown size={20} />
</div>
</div>
</header>
);
};
export { Header };
+18 -18
View File
@@ -1,18 +1,15 @@
import * as React from "react"
import * as React from "react";
import { cn } from "@/shared/lib/utils"
import { cn } from "@/shared/lib/utils";
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col rounded-xl border shadow-sm",
className
)}
className={cn("bg-card flex flex-col rounded-xl", className)}
{...props}
/>
)
);
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
@@ -22,7 +19,7 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
className={cn("flex flex-col gap-1.5 px-6", className)}
{...props}
/>
)
);
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
@@ -32,7 +29,7 @@ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
className={cn("leading-none font-semibold", className)}
{...props}
/>
)
);
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
@@ -42,17 +39,13 @@ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
);
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
)
<div data-slot="card-content" className={cn("p-5", className)} {...props} />
);
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
@@ -62,7 +55,14 @@ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
className={cn("flex items-center px-6", className)}
{...props}
/>
)
);
}
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardDescription,
CardContent,
};
@@ -0,0 +1,22 @@
const DataRush = ({ size = 52 }: { size?: number }) => {
return (
<svg
height={size}
viewBox="0 0 149 52"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect width="149" height="52" fill="#333333" />
<path
d="M28.296 26.6C28.296 31.352 25.032 35 19.872 35H13.56V18.2H19.872C25.032 18.2 28.296 21.848 28.296 26.6ZM24.264 26.624C24.264 24.056 22.512 22.208 19.8 22.208H17.592V30.992H19.8C22.512 30.992 24.264 29.168 24.264 26.624ZM28.0838 35L34.2758 18.2H38.3318L44.4998 35H40.3958L38.9798 31.136H33.6038L32.1878 35H28.0838ZM34.7078 27.872H37.8518L36.2918 23.6L34.7078 27.872ZM56.4529 18.2V21.848H51.8209V35H47.8129V21.848H43.1809V18.2H56.4529ZM55.1072 35L61.2992 18.2H65.3552L71.5232 35H67.4192L66.0032 31.136H60.6272L59.2112 35H55.1072ZM61.7312 27.872H64.8752L63.3152 23.6L61.7312 27.872Z"
fill="#FFDD2D"
/>
<path
d="M73.3256 18.2H79.5656C82.8296 18.2 85.4216 20.792 85.4216 24.032C85.4216 26.336 84.0056 28.304 81.9656 29.144C83.0456 29.456 83.8376 31.52 85.3256 31.52C85.5896 31.52 85.8536 31.472 86.1656 31.352V35C85.4456 35.192 84.7976 35.288 84.2216 35.288C80.1656 35.288 79.5176 30.32 78.1976 29.744H77.3576V35H73.3256V18.2ZM77.3576 21.848V26.096H79.1576C80.3576 26.096 81.3896 25.232 81.3896 23.984C81.3896 22.76 80.3576 21.848 79.1576 21.848H77.3576ZM103.378 18.2V28.04C103.378 32.216 100.282 35.36 95.7216 35.36C91.1616 35.36 88.0416 32.216 88.0416 28.04V18.2H92.0736V28.232C92.0736 30.248 93.7296 31.616 95.7216 31.616C97.7136 31.616 99.3696 30.248 99.3696 28.232V18.2H103.378ZM105.556 32.576L107.668 30.128C108.964 31.472 110.452 32 111.628 32C113.044 32 114.004 31.184 114.004 30.056C114.004 27.368 106.18 29.312 106.18 22.736C106.18 20.048 108.268 17.84 111.964 17.84C114.412 17.84 115.828 18.68 117.724 20.336L115.612 22.808C114.292 21.608 113.404 21.032 111.94 21.032C110.74 21.032 109.996 21.536 109.996 22.616C109.996 25.376 117.844 23.576 117.844 30.056C117.844 32.816 115.9 35.36 111.628 35.36C109.372 35.36 107.476 34.544 105.556 32.576ZM120.13 18.2H124.138V24.488H131.386V18.2H135.394V35H131.386V28.136H124.138V35H120.13V18.2Z"
fill="white"
/>
</svg>
);
};
export { DataRush };
+15 -21
View File
@@ -1,7 +1,7 @@
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import * as React from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs";
import { cn } from "@/shared/lib/utils"
import { cn } from "@/shared/lib/utils";
function Tabs({
className,
@@ -10,10 +10,10 @@ function Tabs({
return (
<TabsPrimitive.Root
data-slot="tabs"
className={cn("flex flex-col", className)}
className={cn("flex flex-col gap-2", className)}
{...props}
/>
)
);
}
function TabsList({
@@ -24,34 +24,28 @@ function TabsList({
<TabsPrimitive.List
data-slot="tabs-list"
className={cn(
"inline-flex items-center justify-center gap-6",
className
"inline-flex w-fit items-center justify-center gap-6 rounded-lg",
className,
)}
{...props}
/>
)
);
}
function TabsTrigger({
className,
value,
...props
}: React.ComponentProps<typeof TabsPrimitive.Trigger> & { value: string }) {
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
return (
<TabsPrimitive.Trigger
data-slot="tabs-trigger"
value={value}
className={cn(
"relative px-1 py-2 text-sm font-medium outline-none",
"text-gray-500",
"data-[state=active]:font-semibold after:absolute after:bottom-0 after:left-0 after:right-0 after:h-0.5",
value === "ongoing" && "data-[state=active]:text-yellow-500 data-[state=active]:after:bg-yellow-500",
value === "completed" && "data-[state=active]:text-green-500 data-[state=active]:after:bg-green-500",
className
"data-[state=active]:text-foreground text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring data-[state=active]:border-primary-foreground inline-flex cursor-pointer items-center justify-center border-b-2 border-transparent pt-3 pb-[18px] text-base font-semibold whitespace-nowrap focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50",
className,
)}
{...props}
/>
)
);
}
function TabsContent({
@@ -61,10 +55,10 @@ function TabsContent({
return (
<TabsPrimitive.Content
data-slot="tabs-content"
className={cn("mt-2 outline-none", className)}
className={cn("flex-1 outline-none", className)}
{...props}
/>
)
);
}
export { Tabs, TabsList, TabsTrigger, TabsContent }
export { Tabs, TabsList, TabsTrigger, TabsContent };
@@ -1,12 +1,11 @@
import { useEffect, useState } from "react";
import { useParams, useNavigate } from "react-router-dom";
import Navbar from "@/modules/Navbar";
import Navbar from "@/widgets/Navbar";
import { Button } from "@/components/ui/button";
import { ArrowLeft } from "lucide-react";
import { Competition } from "@/shared/types/types";
import { Competition } from "@/shared/types";
import { mockCompetitions, mockTasks } from "@/shared/mocks/mocks";
const CompetitionPreview = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
@@ -17,7 +16,7 @@ const CompetitionPreview = () => {
const fetchCompetition = async () => {
try {
setTimeout(() => {
const found = mockCompetitions.find(comp => comp.id === id);
const found = mockCompetitions.find((comp) => comp.id === id);
setCompetition(found || null);
setIsLoading(false);
}, 500);
@@ -48,49 +47,55 @@ const CompetitionPreview = () => {
return (
<>
<Navbar />
<div className="container mx-auto px-4 py-8 mt-16">
<div className="container mx-auto mt-16 px-4 py-8">
<button
onClick={handleBack}
className="flex items-center text-gray-600 mb-8 font-hse-sans"
className="font-hse-sans mb-8 flex items-center text-gray-600"
>
<ArrowLeft size={16} className="mr-2" />
Назад к соревнованиям
</button>
{isLoading ? (
<div className="flex justify-center items-center h-64">
<div className="flex h-64 items-center justify-center">
<p className="font-hse-sans text-gray-500">Загрузка...</p>
</div>
) : competition ? (
<div className="max-w-5xl mx-auto bg-white rounded-lg overflow-hidden shadow-lg">
<div className="w-full h-80 overflow-hidden">
<div className="mx-auto max-w-5xl overflow-hidden rounded-lg bg-white shadow-lg">
<div className="h-80 w-full overflow-hidden">
<img
src={competition.imageUrl}
alt={competition.name}
className="w-full h-full object-cover"
className="h-full w-full object-cover"
/>
</div>
<div className="p-8">
<div className="flex justify-between items-center mb-8">
<h1 className="text-3xl font-semibold font-hse-sans mr-6 flex-1">{competition.name}</h1>
<div className="mb-8 flex items-center justify-between">
<h1 className="font-hse-sans mr-6 flex-1 text-3xl font-semibold">
{competition.name}
</h1>
<Button
className="bg-yellow-400 hover:bg-yellow-500 text-black font-hse-sans text-base px-12 min-w-[180px]"
className="font-hse-sans min-w-[180px] bg-yellow-400 px-12 text-base text-black hover:bg-yellow-500"
onClick={handleContinue}
>
Продолжить
</Button>
</div>
<div className="text-gray-700 font-hse-sans text-lg leading-relaxed">
<div className="font-hse-sans text-lg leading-relaxed text-gray-700">
<p>{competition.description}</p>
</div>
</div>
</div>
) : (
<div className="text-center py-12">
<h2 className="text-2xl font-bold mb-2 font-hse-sans">Соревнование не найдено</h2>
<p className="text-gray-600 font-hse-sans">Запрошенное соревнование не существует или было удалено.</p>
<div className="py-12 text-center">
<h2 className="font-hse-sans mb-2 text-2xl font-bold">
Соревнование не найдено
</h2>
<p className="font-hse-sans text-gray-600">
Запрошенное соревнование не существует или было удалено.
</p>
</div>
)}
</div>
@@ -1,6 +1,6 @@
import { useState, useEffect } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { Task } from "@/shared/types/types";
import { Task } from "@/shared/types";
import { getTaskBgColor, getTaskTextColor } from "./utils/utils";
import { mockTasks } from "@/shared/mocks/mocks";
import { Button } from "@/components/ui/button";
@@ -42,10 +42,11 @@ const CompetitionRunnerPage = () => {
return (
<>
<div className="sticky top-0 z-10 bg-white">
<div className="max-w-6xl mx-auto px-4">
<div className="py-3 text-center">
<h1 className="text-lg font-semibold font-hse-sans">{competitionTitle}</h1>
<div className="sticky top-16 z-10 bg-white border-b border-gray-200 shadow-sm">
<div className="container mx-auto px-4">
<div className="py-4">
<h1 className="text-xl font-semibold font-hse-sans">{competitionTitle}</h1>
</div>
<div className="flex items-center justify-center gap-2 pb-3 overflow-x-auto no-scrollbar">
@@ -1,54 +1,46 @@
import { Competition } from "@/shared/types/types";
import { Competition, CompetitionStatus } from "@/shared/types";
import { cn } from "@/shared/lib/utils";
import {
Card,
CardContent,
CardFooter,
} from "@/components/ui/card";
import { useNavigate } from "react-router";
import { Card, CardContent } from "@/components/ui/card";
interface CompetitionCardProps {
competition: Competition;
className?: string;
}
export function CompetitionCard({ competition, className }: CompetitionCardProps) {
const { id, name, imageUrl, isOlympics, status } = competition;
const navigate = useNavigate();
const handleClick = () => {
navigate(`/competition/${id}`);
};
export function CompetitionCard({
competition,
className,
}: CompetitionCardProps) {
return (
<Card
className={cn("overflow-hidden h-full", className)}
onClick={handleClick}
className={cn(
"aspect-square h-full max-h-80 w-auto overflow-hidden",
className,
)}
>
<div className="relative h-48 overflow-hidden">
<div className="relative h-full overflow-hidden">
<img
src={imageUrl}
alt={name}
className="w-full h-full object-cover transition-transform duration-300 hover:scale-105"
src={competition.imageUrl}
alt={competition.name}
className="h-full w-full object-cover object-center"
/>
</div>
<CardFooter className="p-4 pb-0 flex items-center text-xs font-medium font-hse-sans">
<span className="text-gray-500">
{isOlympics ? "Олимпиада" : "Тренировка"}
<CardContent>
<div className="flex flex-col gap-2.5">
<div className="text-muted-foreground flex items-center gap-2 *:text-sm *:font-semibold">
<span>{competition.isOlympics ? "Олимпиада" : "Тренировка"}</span>
{competition.status != CompetitionStatus.NotParticipating && (
<>
<span></span>
<span className="text-primary-foreground">
{competition.status}
</span>
<span className="mx-2 w-1.5 h-1.5 rounded-full bg-gray-300"></span>
<span className={cn(
status === 'В процессе' && "text-yellow-500",
status === 'Завершено' && "text-green-500",
status === 'Не участвую' && "text-gray-500"
)}>
{status.replace(/^\w/, c => c.toUpperCase())}
</span>
</CardFooter>
<CardContent className="p-4 pt-2">
<h3 className="font-semibold text-lg line-clamp-2 font-hse-sans">{name}</h3>
</>
)}
</div>
<h3 className="text-xl font-semibold">{competition.name}</h3>
</div>
</CardContent>
</Card>
);
@@ -1,98 +1,122 @@
import { useState, useEffect } from 'react';
import { Competition, Status } from '@/shared/types/types';
import { CompetitionGrid } from './modules/CompetitionGrid';
import { Alert, AlertDescription } from "@/components/ui/alert";
import { AlertCircle } from "lucide-react";
import { useState, useEffect } from "react";
import { Competition, CompetitionStatus } from "@/shared/types";
import { CompetitionGrid } from "./modules/CompetitionGrid";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import Navbar from '@/modules/Navbar';
import { mockCompetitions } from '@/shared/mocks/mocks';
const mockCompetitions: Competition[] = [
{
id: "1",
name: "Олимпиада DANO 2025. Индивидуальный этап",
imageUrl: "/DANO.png",
isOlympics: true,
status: CompetitionStatus.InProgress,
},
{
id: "2",
name: "Олимпиада DANO 2025. Индивидуальный этап",
imageUrl: "/DANO.png",
isOlympics: false,
status: CompetitionStatus.NotParticipating,
},
{
id: "3",
name: "Олимпиада DANO 2025. Индивидуальный этап",
imageUrl: "/DANO.png",
isOlympics: false,
status: CompetitionStatus.InProgress,
},
{
id: "4",
name: "Олимпиада DANO 2025. Индивидуальный этап",
imageUrl: "/DANO.png",
isOlympics: true,
status: CompetitionStatus.Completed,
},
{
id: "5",
name: "Олимпиада DANO 2025. Индивидуальный этап",
imageUrl: "/DANO.png",
isOlympics: false,
status: CompetitionStatus.Completed,
},
{
id: "6",
name: "Олимпиада DANO 2025. Индивидуальный этап",
imageUrl: "/DANO.png",
isOlympics: true,
status: CompetitionStatus.NotParticipating,
},
{
id: "6",
name: "Олимпиада DANO 2025. Индивидуальный этап",
imageUrl: "/DANO.png",
isOlympics: true,
status: CompetitionStatus.NotParticipating,
},
{
id: "6",
name: "Олимпиада DANO 2025. Индивидуальный этап",
imageUrl: "/DANO.png",
isOlympics: true,
status: CompetitionStatus.NotParticipating,
},
];
const CompetitionsPage = () => {
const [competitions, setCompetitions] = useState<Competition[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [competitions] = useState<Competition[]>(mockCompetitions);
const [activeTab, setActiveTab] = useState("ongoing");
useEffect(() => {
// ! симуляция фетча
const fetchCompetitions = async () => {
try {
setTimeout(() => {
setCompetitions(mockCompetitions);
setIsLoading(false);
}, 800);
} catch (error) {
setError('Соревнования не найдены, пожалуйста, попробуйте позже');
setIsLoading(false);
}
};
fetchCompetitions();
}, []);
const myCompetitions = competitions.filter(comp =>
comp.status === Status.InProgress || comp.status === Status.Completed
const myCompetitions = competitions.filter(
(comp) =>
comp.status === CompetitionStatus.InProgress ||
comp.status === CompetitionStatus.Completed,
);
const filteredMyCompetitions = myCompetitions.filter(comp =>
activeTab === "ongoing" ? comp.status === Status.InProgress : comp.status === Status.Completed
const filteredMyCompetitions = myCompetitions.filter((comp) =>
activeTab === "ongoing"
? comp.status === CompetitionStatus.InProgress
: comp.status === CompetitionStatus.Completed,
);
const availableCompetitions = competitions.filter(comp =>
comp.status === 'Не участвую'
const availableCompetitions = competitions.filter(
(comp) => comp.status === "Не участвую",
);
return (
<>
<Navbar />
<div className="container mx-auto px-4 py-8 mt-16">
{error && (
<Alert variant="destructive" className="mb-6">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<div className="mb-12">
<div className="flex justify-between items-center mb-6">
<h2 className="text-2xl font-semibold font-hse-sans">Мои события</h2>
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-auto">
<div className="flex flex-col gap-8">
<Section>
<SectionHeader>
<SectionTitle>Мои события</SectionTitle>
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList>
<TabsTrigger value="ongoing" className="font-hse-sans">Текущие</TabsTrigger>
<TabsTrigger value="completed" className="font-hse-sans">Завершенные</TabsTrigger>
<TabsTrigger value="ongoing">В процессе</TabsTrigger>
<TabsTrigger value="completed">Завершенные</TabsTrigger>
</TabsList>
</Tabs>
</div>
</SectionHeader>
<CompetitionGrid competitions={filteredMyCompetitions} />
</Section>
{isLoading ? (
<CompetitionGrid competitions={[]} isLoading={true} />
) : filteredMyCompetitions.length > 0 ? (
<CompetitionGrid competitions={filteredMyCompetitions} isLoading={false} />
) : (
<div className="flex justify-center items-center h-40 bg-gray-50 rounded-lg">
<p className="text-gray-500 font-hse-sans">
{activeTab === "ongoing" ? "У вас нет текущих соревнований" : "У вас нет завершенных соревнований"}
</p>
<Section>
<SectionHeader>
<SectionTitle>События</SectionTitle>
</SectionHeader>
<CompetitionGrid competitions={availableCompetitions} />
</Section>
</div>
)}
</div>
<div>
<h2 className="text-2xl font-semibold mb-6 font-hse-sans">Доступные события</h2>
{isLoading ? (
<CompetitionGrid competitions={[]} isLoading={true} />
) : availableCompetitions.length > 0 ? (
<CompetitionGrid competitions={availableCompetitions} isLoading={false} />
) : (
<div className="flex justify-center items-center h-40 bg-gray-50 rounded-lg">
<p className="text-gray-500 font-hse-sans">Нет доступных соревнований</p>
</div>
)}
</div>
</div>
</>
);
}
};
const Section = ({ children }: { children: React.ReactNode }) => {
return <div className="flex flex-col gap-8">{children}</div>;
};
const SectionHeader = ({ children }: { children: React.ReactNode }) => {
return <div className="flex h-[58px] items-center gap-2">{children}</div>;
};
const SectionTitle = ({ children }: { children: React.ReactNode }) => {
return <h1 className="w-full text-3xl font-semibold">{children}</h1>;
};
export default CompetitionsPage;
@@ -1,45 +1,15 @@
import { Competition } from "@/shared/types/types";
import { Competition } from "@/shared/types";
import { CompetitionCard } from "../../components/CompetitionCard";
import CompetitionSkeleton from "../../components/CompetitionSkeleton";
import { cn } from "@/shared/lib/utils";
interface CompetitionGridProps {
competitions: Competition[];
isLoading?: boolean;
className?: string;
skeletonCount?: number;
}
export function CompetitionGrid({
competitions,
isLoading = false,
className,
skeletonCount
}: CompetitionGridProps) {
const gridClasses = cn(
"grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6",
className
);
const numberOfSkeletons = skeletonCount ?? (competitions.length > 0 ? competitions.length : 4);
if (isLoading) {
export function CompetitionGrid({ competitions }: CompetitionGridProps) {
return (
<div className={gridClasses}>
{Array.from({ length: numberOfSkeletons }).map((_, index) => (
<CompetitionSkeleton key={index} />
))}
</div>
);
}
return (
<div className={gridClasses}>
<div className="grid grid-cols-3 gap-9">
{competitions.map((competition) => (
<CompetitionCard
key={competition.id}
competition={competition}
/>
<CompetitionCard key={competition.id} competition={competition} />
))}
</div>
);
+31 -29
View File
@@ -1,50 +1,52 @@
import { Competition, Status, Task } from "../types/types";
import { Competition, CompetitionStatus, Task } from "../types";
const mockCompetitions: Competition[] = [
{
id: '1',
name: 'Олимпиада DANO 2025. Индивидуальный этап',
imageUrl: '/DANO.png',
id: "1",
name: "Олимпиада DANO 2025. Индивидуальный этап",
imageUrl: "/DANO.png",
isOlympics: true,
status: Status.InProgress,
description: 'Проверка глубоких знаний и навыков в анализе данных. Будет несколько творческих заданий со свободным ответом. Задания выполняются индивидуально, вес тура в итоговом результате – 0,5. Этап пройдет онлайн в заданное время, с применением системы прокторинга. На работу дается 240 минут.'
status: CompetitionStatus.InProgress,
description:
"Проверка глубоких знаний и навыков в анализе данных. Будет несколько творческих заданий со свободным ответом. Задания выполняются индивидуально, вес тура в итоговом результате – 0,5. Этап пройдет онлайн в заданное время, с применением системы прокторинга. На работу дается 240 минут.",
},
{
id: '2',
name: 'Олимпиада DANO 2025. Индивидуальный этап',
imageUrl: '/DANO.png',
id: "2",
name: "Олимпиада DANO 2025. Индивидуальный этап",
imageUrl: "/DANO.png",
isOlympics: false,
status: Status.NotParticipating,
description: 'Индивидуальный этап олимпиады DANO 2025 – это уникальная возможность для студентов продемонстрировать свои навыки анализа данных и решения сложных задач. Участники будут работать с реальными наборами данных и применять современные методы машинного обучения и статистического анализа.'
status: CompetitionStatus.NotParticipating,
description:
"Индивидуальный этап олимпиады DANO 2025 – это уникальная возможность для студентов продемонстрировать свои навыки анализа данных и решения сложных задач. Участники будут работать с реальными наборами данных и применять современные методы машинного обучения и статистического анализа.",
},
{
id: '3',
name: 'Олимпиада DANO 2025. Индивидуальный этап',
imageUrl: '/DANO.png',
id: "3",
name: "Олимпиада DANO 2025. Индивидуальный этап",
imageUrl: "/DANO.png",
isOlympics: false,
status: Status.InProgress
status: CompetitionStatus.InProgress,
},
{
id: '4',
name: 'Олимпиада DANO 2025. Индивидуальный этап',
imageUrl: '/DANO.png',
id: "4",
name: "Олимпиада DANO 2025. Индивидуальный этап",
imageUrl: "/DANO.png",
isOlympics: true,
status: Status.Completed
status: CompetitionStatus.Completed,
},
{
id: '5',
name: 'Олимпиада DANO 2025. Индивидуальный этап',
imageUrl: '/DANO.png',
id: "5",
name: "Олимпиада DANO 2025. Индивидуальный этап",
imageUrl: "/DANO.png",
isOlympics: false,
status: Status.Completed
status: CompetitionStatus.Completed,
},
{
id: '6',
name: 'Олимпиада DANO 2025. Индивидуальный этап',
imageUrl: '/DANO.png',
id: "6",
name: "Олимпиада DANO 2025. Индивидуальный этап",
imageUrl: "/DANO.png",
isOlympics: true,
status: Status.NotParticipating
}
status: CompetitionStatus.NotParticipating,
},
];
const mockTasks: Task[] = [
@@ -58,4 +60,4 @@ const mockTasks: Task[] = [
{ id: "8", number: "3.2", status: "correct" },
];
export { mockCompetitions, mockTasks }
export { mockCompetitions, mockTasks };
+25
View File
@@ -0,0 +1,25 @@
enum CompetitionStatus {
InProgress = "В процессе",
NotParticipating = "Не участвую",
Completed = "Завершено",
}
interface Competition {
id: string;
name: string;
imageUrl: string;
isOlympics: boolean;
status: CompetitionStatus;
description?: string;
}
type TaskStatus = "uncleared" | "checking" | "correct" | "partial" | "wrong";
interface Task {
id: string;
number: string;
status: TaskStatus;
}
export { CompetitionStatus };
export type { Competition, TaskStatus, Task };
@@ -1,25 +0,0 @@
enum Status {
InProgress = 'В процессе',
NotParticipating = 'Не участвую',
Completed = 'Завершено'
}
interface Competition {
id: string;
name: string;
imageUrl: string;
isOlympics: boolean;
status: Status;
description?: string;
}
type TaskStatus = "uncleared" | "checking" | "correct" | "partial" | "wrong";
interface Task {
id: string;
number: string;
status: TaskStatus;
}
export {Status}
export type {Competition, TaskStatus, Task}
+5 -5
View File
@@ -5,14 +5,14 @@
@custom-variant dark (&:is(.dark *));
:root {
--background: oklch(1 0 0);
--background: oklch(0.97 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--primary: oklch(89.97% 0.1763 97.07);
--primary-foreground: oklch(82.87% 0.1701 94.8);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
@@ -53,7 +53,7 @@
}
@theme inline {
--font-hse-sans: "HSE Sans", system-ui, sans-serif
--font-hse-sans: "HSE Sans", system-ui, sans-serif;
}
.dark {
--background: oklch(0.145 0 0);
@@ -144,7 +144,7 @@
@layer base {
* {
@apply border-border outline-ring/50;
@apply border-border outline-ring/50 font-hse-sans;
}
body {
@apply bg-background text-foreground;
@@ -0,0 +1,15 @@
import { Header } from "@/components/layout/header";
import { Outlet } from "react-router";
const NavbarLayout = () => {
return (
<>
<Header />
<div className="m-auto mt-6 w-full max-w-5xl">
<Outlet />
</div>
</>
);
};
export { NavbarLayout };
-11
View File
@@ -1,11 +0,0 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
theme: {
extend: {
fontFamily: {
'hse-sans': ['"HSE Sans"', 'system-ui', 'sans-serif'],
},
},
},
plugins: [],
}