mirror of
https://gitlab.com/megazordpobeda/DataRush.git
synced 2026-05-22 22:07:10 +00:00
Merge branch 'master' of gitlab.prodcontest.ru:team-15/project
This commit is contained in:
@@ -1 +1,33 @@
|
||||
# DataRush
|
||||
|
||||
Инновационный сервис для проведения соревнований по анализу данных
|
||||
|
||||
|
||||
## Запуск
|
||||
|
||||
Склонируйте репозиторий и пропишите
|
||||
|
||||
```bash
|
||||
docker compose up
|
||||
```
|
||||
|
||||
## Основные ручки
|
||||
|
||||
* `/` - основное приложение
|
||||
* `/api/v1/docs` - swagger-ui документация
|
||||
* `/admin` - админка
|
||||
* `/admin/grafana` - графана
|
||||
* `/docs` - гайд по анализу данных
|
||||
|
||||
После запуска по методу выше создается пользователь в админке (`/admin`) с данными ниже:`admin`
|
||||
- `admin` - логин
|
||||
- `proooooood` - пароль
|
||||
|
||||
|
||||
## Тесты
|
||||
|
||||
Написаны unit-тесты (на базе Django TestCase) и E2E (Postman коллекция)
|
||||
|
||||

|
||||
|
||||
![django test]()
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 261 KiB |
@@ -67,3 +67,4 @@ class TaskAttachmentSchema(ModelSchema):
|
||||
class TaskStatusSchema(Schema):
|
||||
task_name: str
|
||||
result: int
|
||||
max_points: int
|
||||
|
||||
@@ -195,14 +195,21 @@ def get_competition_results(request, competition_id: UUID):
|
||||
for task in tasks:
|
||||
submissions = CompetitionTaskSubmission.objects.filter(
|
||||
user=request.auth, task=task
|
||||
).filter(status="checked").all()
|
||||
).filter(status="checked").order_by("-earned_points").all()
|
||||
if not submissions:
|
||||
result = 0
|
||||
all_submissions_count = CompetitionTaskSubmission.objects.filter(
|
||||
user=request.auth, task=task
|
||||
).count()
|
||||
if all_submissions_count == 0:
|
||||
result = -2
|
||||
else:
|
||||
result = -1
|
||||
else:
|
||||
result = submissions[0].earned_points
|
||||
data.append(TaskStatusSchema(
|
||||
task_name=task.title,
|
||||
result=result
|
||||
result=result,
|
||||
max_points=task.points,
|
||||
))
|
||||
|
||||
return status.OK, data
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 116 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.7 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.0 KiB |
@@ -2,6 +2,7 @@ import random
|
||||
import uuid
|
||||
from datetime import timedelta, datetime
|
||||
|
||||
from PIL.Image import Image
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from django.core.files.base import ContentFile, File
|
||||
@@ -13,10 +14,11 @@ from apps.review.models import Reviewer
|
||||
from apps.task.models import (
|
||||
CompetitionTask,
|
||||
CompetitionTaskCriteria,
|
||||
CompetitionTaskSubmission,
|
||||
CompetitionTaskSubmission, CompetitionTaskAttachment,
|
||||
)
|
||||
from apps.user.models import User, UserRole
|
||||
|
||||
# Примеры файлов с правильными ответами
|
||||
ans1 = ContentFile(
|
||||
b"1984",
|
||||
name=f"submission_{uuid.uuid4().hex}.txt",
|
||||
@@ -25,46 +27,86 @@ ans2 = ContentFile(
|
||||
b"3",
|
||||
name=f"submission_{uuid.uuid4().hex}.txt",
|
||||
)
|
||||
ans3 = ContentFile(
|
||||
b"42",
|
||||
name=f"submission_{uuid.uuid4().hex}.txt",
|
||||
)
|
||||
ans4 = ContentFile(
|
||||
b"11",
|
||||
name=f"submission_{uuid.uuid4().hex}.txt",
|
||||
)
|
||||
dataset = ContentFile(
|
||||
b"it is a dataset",
|
||||
name=f"dataset-{uuid.uuid4().hex}.txt",
|
||||
)
|
||||
dataset2 = ContentFile(
|
||||
b"it is a dataset",
|
||||
name=f"dataset-{uuid.uuid4().hex}.csv",
|
||||
)
|
||||
|
||||
now = timezone.now()
|
||||
|
||||
image_dir = f"{settings.BASE_DIR}/apps/core/contents/images"
|
||||
f = open(f"{image_dir}/dano.jpg", "rb")
|
||||
dano_image = File(f, name="dano.jpg")
|
||||
|
||||
# Расширенный список соревнований, включая 3 новых
|
||||
competitions = [
|
||||
{
|
||||
"obj": None, # докидывает в процессе
|
||||
"obj": None, # будет заполнено позже
|
||||
"title": "DANO. Финал",
|
||||
"description": "Олимпиада по анализу данных от Т-Банка и ВШЭ",
|
||||
"start_date": now - timedelta(days=2),
|
||||
"end_date": now + timedelta(days=5),
|
||||
"type": "competitive",
|
||||
"participation_type": "solo",
|
||||
"image": dano_image,
|
||||
"tasks": [
|
||||
{
|
||||
"obj": None,
|
||||
"title": "Задача 1",
|
||||
"description": """На маркетплейсе «Е-шопинг» продаются различные товары. Одна из задач аналитика —
|
||||
прогнозировать, сколько товаров будет продаваться при определенной цене. В ходе
|
||||
исследований и экспериментов был выявлен вид зависимости:
|
||||
$Q(P) = Q_0 \times e^{E \times \frac{P_0 - P}{P_0}}$
|
||||
"description": (
|
||||
"""
|
||||
На маркетплейсе «Е-шопинг» продаются различные товары. Одна из задач аналитика —
|
||||
прогнозировать, сколько товаров будет продаваться при определенной цене. В ходе исследований
|
||||
был выявлен вид зависимости:\n
|
||||
$Q(P) = Q_0 \\times e^{E \\times \\frac{P_0 - P}{P_0}}$\n
|
||||
где Q — это количество проданных единиц товара при цене P,
|
||||
Q 0 — количество проданных единиц товара при цене P0 ,
|
||||
E — коэффициент чувствительности количества проданных единиц товара к изменению
|
||||
цены.
|
||||
1. Кофемашину «Кофе каждый день» купили 200 000 раз (Q 0 ) при цене 20 000 ₽ (P 0
|
||||
).
|
||||
Позже продавец поднял цену на 5 000 ₽, при этом продажи сократились на 24 000
|
||||
штук. Какой коэффициент чувствительности Е имеет этот товар? Ответ округлите
|
||||
до двух знаков после запятой.
|
||||
2. Потом продавец решил поставить новую цену на эту же модель: 22 000 ₽. Сколько
|
||||
продаж согласно нашей зависимости будет у этого товара? Используйте результаты
|
||||
предыдущего пункта. Ответ округлите до целых.
|
||||
3. Другой продавец предлагает на нашем маркетплейсе кухонные ножи и сковородки.
|
||||
Благодаря исследованиям были получены следующие формулы зависимостей
|
||||
количества проданных товаров:
|
||||
|
||||
Найдите, сколько заработает продавец при цене по 3 000 ₽ за нож и сковороду
|
||||
при условии, что себестоимость ножа — 1 000 ₽, а сковородки — 2 000 ₽.Ответ
|
||||
округлите до целых.""".strip(),
|
||||
округлите до целых.
|
||||
Найдите, сколько заработает продавец при цене по 3 000 ₽ за нож и сковороду при условии,
|
||||
что себестоимость ножа — 1 000 ₽, а сковородки — 2 000 ₽. Ответ округлите до целых.
|
||||
""".strip()
|
||||
),
|
||||
"type": CompetitionTask.CompetitionTaskType.INPUT.value,
|
||||
"points": 3,
|
||||
"submission_reviewers_count": 2,
|
||||
"max_attempts": 20,
|
||||
"correct_answer_file": ans1
|
||||
"correct_answer_file": ans1,
|
||||
},
|
||||
{
|
||||
"obj": None,
|
||||
"title": "Задача 2",
|
||||
"description": """
|
||||
Напишите "hello_dano" на питоне
|
||||
""".strip(),
|
||||
"description": "Найдите максимальную зарплату программиста из датасета на питоне",
|
||||
"type": CompetitionTask.CompetitionTaskType.CHECKER.value,
|
||||
"attachment": dataset,
|
||||
"attachment_path": "dataset",
|
||||
"points": 25,
|
||||
"submission_reviewers_count": 2,
|
||||
"max_attempts": 50,
|
||||
@@ -72,60 +114,66 @@ E — коэффициент чувствительности количеств
|
||||
{
|
||||
"obj": None,
|
||||
"title": "Задача 3",
|
||||
"description": """Небольшой интернет-магазин собрал данные о действиях пользователей на своем сайте
|
||||
"attachment": dataset2,
|
||||
"attachment_path": "dataset2",
|
||||
"description": """
|
||||
Небольшой интернет-магазин собрал данные о действиях пользователей на своем сайте
|
||||
за последние несколько месяцев.
|
||||
ecommerce_logs.csv — журнал действий пользователей:
|
||||
• user_id — идентификатор пользователя.
|
||||
• action — тип действия пользователя:
|
||||
— visit — посещение сайта;
|
||||
— click — клик на карточку товара;
|
||||
— cart — добавление товара в корзину;
|
||||
— delete — удаление товара из корзины;
|
||||
— purchase — покупка товаров.
|
||||
• date_time — время совершения действия.
|
||||
• product_id — идентификатор товара.
|
||||
• quantity — количество добавленного в корзину товара.
|
||||
• delivery_price — стоимость доставки.
|
||||
• sex — пол пользователя.
|
||||
• region — регион пользователя.
|
||||
• price — цена товара.
|
||||
|
||||
• user_id — идентификатор пользователя.
|
||||
• action — тип действия пользователя:
|
||||
— visit — посещение сайта;
|
||||
— click — клик на карточку товара;
|
||||
— cart — добавление товара в корзину;
|
||||
— delete — удаление товара из корзины;
|
||||
— purchase — покупка товаров.
|
||||
• date_time — время совершения действия.
|
||||
• product_id — идентификатор товара.
|
||||
• quantity — количество добавленного в корзину товара.
|
||||
• delivery_price — стоимость доставки.
|
||||
• sex — пол пользователя.
|
||||
• region — регион пользователя.
|
||||
• price — цена товара.
|
||||
Ваша задача — проанализировать поведение пользователей, выявить возможные
|
||||
проблемы при покупке и предложить решения. Ваш анализ поможет понять, на каком
|
||||
этапе воронки магазин теряет покупателей и какие изменения можно внести, чтобы
|
||||
улучшить процесс покупок в интернет-магазине.
|
||||
Как правило, количество пользователей на каждом последующем шаге уменьшается,
|
||||
и такая ситуация называется “воронкой”. Конверсия — это отношение количества
|
||||
пользователей на каком-то одном шаге к количеству пользователей на одном
|
||||
из предыдущих шагов. Например, конверсия из визита сайта в добавление товара
|
||||
в корзину рассчитывается так: количество пользователей, добавивших товар в корзину,
|
||||
делится на количество пользователей, посетивших сайт.
|
||||
Вам нужно изучить воронку конверсии, которая показывает, как пользователи переходят
|
||||
от одного шага к другому на сайте. В нашем случае воронка состоит из следующих шагов:
|
||||
1. Посещение сайта.
|
||||
2. Просмотр карточки товара.
|
||||
3. Добавление товара в корзину.
|
||||
4. Покупка.
|
||||
|
||||
1. Посещение сайта.
|
||||
2. Просмотр карточки товара.
|
||||
3. Добавление товара в корзину.
|
||||
4. Покупка.
|
||||
3 / 11
|
||||
1.) Посчитайте конверсию (округлите ответ до 3 знаков после запятой):
|
||||
• Из визита на сайт в клик на карточку товара.
|
||||
• Из клика в добавление в корзину.
|
||||
• Из добавления в корзину в покупку.
|
||||
• Из визита на сайт в добавление в корзину.
|
||||
• Из визита на сайт в покупку.
|
||||
• Из визита на сайт в клик на карточку товара.
|
||||
• Из клика в добавление в корзину.
|
||||
• Из добавления в корзину в покупку.
|
||||
• Из визита на сайт в добавление в корзину.
|
||||
• Из визита на сайт в покупку.
|
||||
2. Постройте воронку конверсии с помощью столбчатой диаграммы:
|
||||
• По оси X — шаги воронки.
|
||||
• По оси Y — количество уникальных пользователей на каждом шаге.
|
||||
• По оси X — шаги воронки.
|
||||
• По оси Y — количество уникальных пользователей на каждом шаге.
|
||||
3. Определите, на каком этапе конверсия из предыдущего шага ниже всего.
|
||||
Сформулируйте одну гипотезу, связанную с поведением пользователей, которая
|
||||
может объяснить падение конверсии именно на этом этапе. Обоснуйте механизм
|
||||
работы приведенной гипотезы.
|
||||
4. Постройте график динамики (по оси X — дни) для каждой из конверсий:
|
||||
• Конверсия из визита в клик.
|
||||
• Конверсия из визита в добавление в корзину.
|
||||
• Конверсия из визита в покупку.
|
||||
• Конверсия из визита в клик.
|
||||
• Конверсия из визита в добавление в корзину.
|
||||
• Конверсия из визита в покупку.
|
||||
5. На графике найдите просадку конверсии: укажите, какая конверсия просела
|
||||
и в какой примерно период это произошло (допустимая погрешность — 1–3
|
||||
дня).
|
||||
6. Чем вызвано снижение конверсии в этот период? Какие изменения в бизнесе
|
||||
или поведении пользователей могли бы объяснить это? Ответьте на оба
|
||||
вопроса, опираясь на данные.
|
||||
""".strip(),
|
||||
""".strip(),
|
||||
"type": CompetitionTask.CompetitionTaskType.REVIEW.value,
|
||||
"points": 10,
|
||||
"submission_reviewers_count": 2,
|
||||
@@ -136,18 +184,18 @@ ecommerce_logs.csv — журнал действий пользователей:
|
||||
"name": "Обоснованность решения",
|
||||
"slug": "validity",
|
||||
"description": "Аргументация",
|
||||
"max_value": 5
|
||||
"max_value": 5,
|
||||
},
|
||||
{
|
||||
"obj": None,
|
||||
"name": "Правильность",
|
||||
"slug": "correctness",
|
||||
"description": "Насколько точные и верные ответы были представлены.",
|
||||
"max_value": 5
|
||||
}
|
||||
]
|
||||
"description": "Точность вычислений",
|
||||
"max_value": 5,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
],
|
||||
},
|
||||
{
|
||||
"obj": None,
|
||||
@@ -161,53 +209,225 @@ ecommerce_logs.csv — журнал действий пользователей:
|
||||
{
|
||||
"obj": None,
|
||||
"title": "Задача 1",
|
||||
"description": """Сколько этапов в DANO?""".strip(),
|
||||
"description": """
|
||||
Конверсия — это доля клиентов, перешедших с одного этапа на другой. Например,
|
||||
на сайт с заявками на кредитные карты зашли 50 человек, после ознакомления
|
||||
с условиями заявку на оформление карты (далее — заявку) оставили только 45
|
||||
из них. В данном случае конверсия составляет 90% = 45/50.
|
||||
Рассмотрим следующую ситуацию. В ноябре сайт посетили 100 мужчин и 100
|
||||
женщин, при этом из них заявки оставили 10 мужчин и всего 5 женщин.
|
||||
1. Посчитайте конверсии для мужчин и для женщин из захода на сайт
|
||||
в оформление заявки.
|
||||
2. Посчитайте общую конверсию для всех пользователей.
|
||||
3. В декабре была проведена дополнительная рекламная компания, и общее
|
||||
число зашедших на сайт стало больше. При этом конверсия для мужчин стала
|
||||
равна 12%, а для женщин — 7%. Может ли быть такое, что общая конверсия
|
||||
в декабре упала? Если да, то приведите численный пример. Если нет —
|
||||
докажите.
|
||||
4. При условии увеличения конверсий у мужчин и у женщин до 12% и 7%
|
||||
соответственно в каком интервале будет лежать общая конверсия? Обоснуйте
|
||||
свой ответ.
|
||||
""".strip(),
|
||||
"type": CompetitionTask.CompetitionTaskType.INPUT.value,
|
||||
"points": 3,
|
||||
"submission_reviewers_count": 2,
|
||||
"max_attempts": 20,
|
||||
"correct_answer_file": ans2
|
||||
"correct_answer_file": ans2,
|
||||
},
|
||||
{
|
||||
"obj": None,
|
||||
"title": "Задача 2",
|
||||
"description": """
|
||||
Напишите отзыв про DANO(Хороший)
|
||||
""".strip(),
|
||||
Каждый день Дима звонит в пекарню, чтобы узнать, есть ли сегодня в продаже его
|
||||
любимые булочки с повидлом. За последние 3 дня булочки были в наличии 2 раза,
|
||||
а 1 раз их не было.
|
||||
Пусть переменная Х = 0, если булочек нет, и Х = 1, если булочки есть. Наличие
|
||||
булочек в конкретный день не зависит от наличия булочек в любой другой день.
|
||||
1. Сколько наблюдений собрал Дима? Выпишите все значения из его выборки
|
||||
через запятую. Посчитайте для этой выборки среднее значение Х, дисперсию Х.
|
||||
В какой доле случаев булочки были в наличии?
|
||||
2. Пусть p — вероятность того, что булочки в наличии (p не может быть меньше 0
|
||||
и больше 1. Например, p может быть равно 0,2 = 1/5 — то есть в одном из пяти
|
||||
случаев булочки в наличии). Чему равна вероятность, что сегодня булочки
|
||||
есть, а завтра их не будет? Чему равна вероятность, что за два дня в один день
|
||||
булочки будут, а в другой — не будут? (Напишите два выражения, зависящие
|
||||
от p.)
|
||||
3. Чему равна вероятность получения наблюдений как у Димы? (Напишите одно
|
||||
выражение, зависящее от p.)
|
||||
4. При каком значении p вероятность получить выборку как у Димы максимальна?
|
||||
Вычислите его. Как это значение соотносится с наблюдениями Димы?
|
||||
5. Дима нашел значение p из предыдущего пункта и сделал вывод, что
|
||||
на следующий день булочки испекут с вероятностью p. Верный ли вывод сделал
|
||||
Дима? Поясните свой ответ.
|
||||
6. Рядом с домом Димы открыли новую пекарню, где тоже делают булочки
|
||||
с повидлом. Дима решил сравнить две пекарни. Для этого он собрал выборку
|
||||
за 100 дней: в новой пекарне булочки были в наличии 70 дней, в старой — 60
|
||||
дней. Какую гипотезу может проверить Дима? Какой механизм, может лежать
|
||||
в основе этой гипотезы? При описании механизма вы можете сами дополнить
|
||||
историю Димы (например, предположить расположение старой пекарни,
|
||||
себестоимость повидла и другое, что могло бы помочь объяснить гипотезу,
|
||||
необязательно рассматривать приведенные примеры). Помогите Диме
|
||||
проверить описанную вами гипотезу. Есть ли разница в производительности
|
||||
между новой пекарней и старой?
|
||||
""".strip(),
|
||||
"type": CompetitionTask.CompetitionTaskType.REVIEW.value,
|
||||
"points": 15,
|
||||
"submission_reviewers_count": 2,
|
||||
"max_attempts": 1,
|
||||
"criteries": [
|
||||
{
|
||||
"obj": None,
|
||||
"name": "Хорошесть отзыва",
|
||||
"slug": "validity",
|
||||
"description": "Хорошесть",
|
||||
"max_value": 10
|
||||
"description": "Критерий качества отзыва",
|
||||
"max_value": 10,
|
||||
},
|
||||
{
|
||||
"obj": None,
|
||||
"name": "Подробность",
|
||||
"slug": "detail",
|
||||
"description": "Насколько подробно расписан ответ.",
|
||||
"max_value": 5
|
||||
}
|
||||
]
|
||||
"description": "Насколько подробно расписан ответ",
|
||||
"max_value": 5,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"obj": None,
|
||||
"title": "Задача 3",
|
||||
"description": """
|
||||
Напишите выведите 1+3 на питоне
|
||||
""".strip(),
|
||||
Вы аналитик ведущей игровой компании GameMasters Inc., которая
|
||||
специализируется на разработке мобильных игр. Ваши коллеги разработали
|
||||
обновленный игровой магазин, в котором игроки могут приобретать внутриигровые
|
||||
предметы и суперспособности. Ваша задача — провести сравнение, чтобы
|
||||
определить, как внедрение нового магазина повлияло на поведение пользователей
|
||||
в игре.
|
||||
Для этого пользователи были разделены на две равные группы случайным образом:
|
||||
А — пользователи, которым доступен только старый магазин;
|
||||
B — пользователи, которым доступен только обновленный магазин.
|
||||
Спустя месяц после запуска по каждому пользователю из каждой группы были
|
||||
посчитаны следующие метрики:
|
||||
‘revenue_per_user’ — доход, который был получен от пользователя за период;
|
||||
‘orders_cnt_per_user’ — количество заказов, которое совершено пользователем
|
||||
за период;
|
||||
‘converted_from_main_screen_to_item_card_screen’ — флаг захода на экран
|
||||
с товарами (0 — если пользователь не заходил на карточку товара, 1 — если
|
||||
заходил).
|
||||
В таблице приведены значения этих метрик. Также в ней находится столбец ‘group’,
|
||||
в котором указано, к какой группе (A или B) относится пользователь и столбец
|
||||
‘period’ — характеризующий значение метрик до начала теста и во время проведения
|
||||
теста. На карточке товара содержится дополнительная информация, фотография
|
||||
и его характеристики. Однако оплатить товар можно и без захода на карточку
|
||||
товара.
|
||||
Задача: сравните группы по каждой метрике и сделайте вывод о том, стоит ли
|
||||
продолжить внедрение обновленного магазина или нужно вернуть старый
|
||||
""".strip(),
|
||||
"type": CompetitionTask.CompetitionTaskType.CHECKER.value,
|
||||
"points": 30,
|
||||
"submission_reviewers_count": 2,
|
||||
"max_attempts": 100,
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"obj": None,
|
||||
"title": "Data Challenge 2025(FAKE DANO)",
|
||||
"description": """
|
||||
Ну типо дано
|
||||
""".strip(),
|
||||
"start_date": now - timedelta(days=1),
|
||||
"end_date": now + timedelta(days=10),
|
||||
"type": "competitive",
|
||||
"participation_type": "solo",
|
||||
"tasks": [
|
||||
{
|
||||
"obj": None,
|
||||
"title": "Анализ трендов", # TODO сюда добавить бд
|
||||
"description": """
|
||||
Скачайте базу данных со специальной страницы (https://dano.hse.ru/data), изучите ее более
|
||||
внимательно: посмотрите на переменные, посчитайте описательные статистики, постройте
|
||||
предварительные графики и таблицы, обратите внимание на выбросы.
|
||||
Продумайте все детали: исследовательский вопрос, гипотезу, механизм и пр.
|
||||
Разработайте дизайн исследования:
|
||||
• что вам нужно для того, чтобы ответить на исследовательский вопрос
|
||||
• что нужно чтобы проверить ту или иную гипотезу
|
||||
• какие таблицы и графики вам понадобятся
|
||||
• какую информацию из них можно извлечь
|
||||
• как интерпретировать получаемые результаты
|
||||
• помогает ли это в вашем исследовании
|
||||
• несет ли полезную информацию
|
||||
• действительно ли эти построенные таблицы и графики необходимы и продвигают ваш
|
||||
проект или может быть необходимы другие
|
||||
• какие методы и модели вам нужны
|
||||
• в какой последовательности выполнять все расчеты и построения
|
||||
Обращайте особое внимание на то, что все эти процедуры должны быть оправданы и
|
||||
продвигать вас в направлении поиска ответа на исследовательский вопрос.
|
||||
Распределите задачи между членами команды. Установите сроки. Придерживайтесь взятых
|
||||
на себя обязательств и данных друг другу обещаний – делайте все в срок. Обсуждайте между
|
||||
собой полученные результаты, ищите наиболее удачный способ проверить ваши гипотезы,
|
||||
наиболее удачные графики и таблицы. Советуйтесь с ментором, обращайтесь к нему за
|
||||
помощью – его задача помочь вам отобрать правильные идеи и подсказать как их технически
|
||||
реализовать.
|
||||
Заведите общее облачное пространство, где будут хранится все ваши результаты.
|
||||
Структурируйте, создавайте необходимые папки, называйте папки и документы говорящими
|
||||
именами, оставляйте комментарии
|
||||
""".strip(),
|
||||
"type": CompetitionTask.CompetitionTaskType.REVIEW.value,
|
||||
"points": 20,
|
||||
"submission_reviewers_count": 3,
|
||||
"max_attempts": 2,
|
||||
"attachment": dataset,
|
||||
"attachment_path": "dataset",
|
||||
"criteries": [
|
||||
{
|
||||
"obj": None,
|
||||
"name": "Качество анализа",
|
||||
"slug": "analysis_quality",
|
||||
"description": "Глубина анализа данных",
|
||||
"max_value": 10,
|
||||
},
|
||||
{
|
||||
"obj": None,
|
||||
"name": "Обоснованность выводов",
|
||||
"slug": "insight",
|
||||
"description": "Логичность выводов",
|
||||
"max_value": 10,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"obj": None,
|
||||
"title": "Ещё задачка",
|
||||
"description": """
|
||||
Как известно, Израиль является одной из лидирующих стран по темпам вакцинации. По
|
||||
данным на июнь 2021 г. в стране вакцинировано 60% граждан (85% взрослого населения).
|
||||
Однако среди заразившихся в этом же месяце (июне 2021 года), как признали власти
|
||||
Израиля, примерно половина была уже вакцинирована. Что можно сказать об
|
||||
эффективности вакцины на основании этих данных?
|
||||
1) Данные не свидетельствуют об эффективности вакцины, т. к. вероятность
|
||||
заразиться составляет 50%, независимо от того, вакцинировался человек или нет
|
||||
2) Данные не свидетельствуют об эффективности вакцины, т. к. среди
|
||||
вакцинированных есть заразившиеся
|
||||
3) Данные свидетельствуют об эффективности вакцины, т. к. если бы она не работала,
|
||||
доля вакцинированных среди заболевших была бы равна доле вакцинированных
|
||||
среди всего населения страны
|
||||
4) Данные свидетельствуют об эффективности вакцины, т. к. вакцинированные
|
||||
переносят болезнь в более легкой форме
|
||||
""".strip(),
|
||||
"type": CompetitionTask.CompetitionTaskType.INPUT.value,
|
||||
"points": 15,
|
||||
"submission_reviewers_count": 2,
|
||||
"max_attempts": 50,
|
||||
"correct_answer_file": ans3
|
||||
},
|
||||
{
|
||||
"obj": None,
|
||||
"title": "Быстрый ответ",
|
||||
"description": "Сколько будет 6 * 7?",
|
||||
"type": CompetitionTask.CompetitionTaskType.INPUT.value,
|
||||
"points": 5,
|
||||
"submission_reviewers_count": 2,
|
||||
"max_attempts": 10,
|
||||
"correct_answer_file": ans3,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
users = [
|
||||
@@ -222,20 +442,163 @@ users = [
|
||||
"username": "dreamonovich",
|
||||
"password": "password123!",
|
||||
"role": UserRole.STUDENT.value,
|
||||
}
|
||||
},
|
||||
{
|
||||
"email": "alisa.kuznetsova@gmail.com",
|
||||
"username": "alisa_kuz",
|
||||
"password": "password123!",
|
||||
"role": UserRole.STUDENT.value,
|
||||
},
|
||||
{
|
||||
"email": "ivan.petrov@gmail.com",
|
||||
"username": "ivan_petrov",
|
||||
"password": "password123!",
|
||||
"role": UserRole.STUDENT.value,
|
||||
},
|
||||
{
|
||||
"email": "olga.sidorova@gmail.com",
|
||||
"username": "olga_sid",
|
||||
"password": "password123!",
|
||||
"role": UserRole.STUDENT.value,
|
||||
},
|
||||
{
|
||||
"email": "karim@gmail.com",
|
||||
"username": "karim",
|
||||
"password": "password123!",
|
||||
"role": UserRole.STUDENT.value,
|
||||
},
|
||||
{
|
||||
"email": "noble@gmail.com",
|
||||
"username": "noble",
|
||||
"password": "password123!",
|
||||
"role": UserRole.STUDENT.value,
|
||||
},
|
||||
{
|
||||
"email": "koller@gmail.com",
|
||||
"username": "koller",
|
||||
"password": "password123!",
|
||||
"role": UserRole.STUDENT.value,
|
||||
},
|
||||
{
|
||||
"email": "gold_checker@gmail.com",
|
||||
"username": "gold_checker",
|
||||
"password": "password123!",
|
||||
"role": UserRole.STUDENT.value,
|
||||
},
|
||||
{
|
||||
"email": "looka@gmail.com",
|
||||
"username": "looka",
|
||||
"password": "password123!",
|
||||
"role": UserRole.STUDENT.value,
|
||||
},
|
||||
{
|
||||
"email": "danil_malikov@gmail.com",
|
||||
"username": "danil_malikov",
|
||||
"password": "password123!",
|
||||
"role": UserRole.STUDENT.value,
|
||||
},
|
||||
{
|
||||
"email": "marina-looks@gmail.com",
|
||||
"username": "marina-looks",
|
||||
"password": "password123!",
|
||||
"role": UserRole.STUDENT.value,
|
||||
},
|
||||
{
|
||||
"email": "pasha@gmail.com",
|
||||
"username": "pasha",
|
||||
"password": "password123!",
|
||||
"role": UserRole.STUDENT.value,
|
||||
},
|
||||
{
|
||||
"email": "oleg-tinkov@gmail.com",
|
||||
"username": "oleg-tinkov",
|
||||
"password": "password123!",
|
||||
"role": UserRole.STUDENT.value,
|
||||
},
|
||||
{
|
||||
"email": "baron_ivanych@gmail.com",
|
||||
"username": "baron_ivanych",
|
||||
"password": "password123!",
|
||||
"role": UserRole.STUDENT.value,
|
||||
},
|
||||
]
|
||||
|
||||
reviewers = [
|
||||
{
|
||||
"name": "Владислав",
|
||||
"surname": "Пикиневич",
|
||||
"token": "aa443163-9861-4b8d-b8f7-81ecd25f6088"
|
||||
"token": "aa443163-9861-4b8d-b8f7-81ecd25f6088",
|
||||
},
|
||||
{
|
||||
"name": "Александр",
|
||||
"surname": "Шахов",
|
||||
"token": "d2e8904a-01dd-4f84-a8b0-8a60f1a3b6c0"
|
||||
}
|
||||
"token": "d2e8904a-01dd-4f84-a8b0-8a60f1a3b6c0",
|
||||
},
|
||||
{
|
||||
"name": "Мария",
|
||||
"surname": "Иванова",
|
||||
"token": "e3f8904a-23cd-4f84-a8b0-9b70f1a4b7d1",
|
||||
},
|
||||
{
|
||||
"name": "Сергей",
|
||||
"surname": "Смирнов",
|
||||
"token": "f4g9015b-45de-5g95-b9c1-0c81g2b5c8e2",
|
||||
},
|
||||
{
|
||||
"name": "Паша",
|
||||
"surname": "Проверкин",
|
||||
"token": "f4g9015b-45de-5g95-b9c1-0c81g2b3c8e2",
|
||||
},
|
||||
{
|
||||
"name": "Илья",
|
||||
"surname": "Продкин",
|
||||
"token": "f4g9015b-45de-5g95-b8c1-0c81g2b5c8e2",
|
||||
},
|
||||
{
|
||||
"name": "Влад",
|
||||
"surname": "Проверкин",
|
||||
"token": "f4g9015b-45de-5g95-b9c1-0c81g2b5c8e1",
|
||||
},
|
||||
{
|
||||
"name": "Сашка",
|
||||
"surname": "Пашкин",
|
||||
"token": "f4g9015b-45de-5g95-b9c1-1c81g2b5c8e2",
|
||||
},
|
||||
{
|
||||
"name": "Чарльз",
|
||||
"surname": "Проверкин",
|
||||
"token": "b4g9015b-45de-5g95-b9g1-0c81g2b5c8e2",
|
||||
},
|
||||
{
|
||||
"name": "Тимурка",
|
||||
"surname": "Проверкин",
|
||||
"token": "f4g9015b-25de-5g95-b9c1-0c81g2b5c8e2",
|
||||
},
|
||||
{
|
||||
"name": "Александр",
|
||||
"surname": "Даношкин",
|
||||
"token": "f4g9015t-45de-5g95-b9c1-0c81g2b5c8e2",
|
||||
},
|
||||
{
|
||||
"name": "Паша",
|
||||
"surname": "Проверкин",
|
||||
"token": "f4g9015r-45de-5g95-b9c1-0c81g2b5c8e2",
|
||||
},
|
||||
{
|
||||
"name": "Лука",
|
||||
"surname": "Проверкин",
|
||||
"token": "f4g9015e-45de-5g95-b9c1-0c81g2b5c8e2",
|
||||
},
|
||||
{
|
||||
"name": "Кирилл",
|
||||
"surname": "Проверкин",
|
||||
"token": "f4g9015w-45de-5g95-b9c1-0c81g2b5c8e2",
|
||||
},
|
||||
{
|
||||
"name": "Олег",
|
||||
"surname": "Проверкин",
|
||||
"token": "f4g9015q-45de-5g95-b9c1-0c81g2b5c8e2",
|
||||
},
|
||||
]
|
||||
|
||||
class Command(BaseCommand):
|
||||
@@ -251,6 +614,8 @@ class Command(BaseCommand):
|
||||
self.create_states(competitions, users)
|
||||
self.stdout.write("Data generation completed.")
|
||||
|
||||
f.close()
|
||||
|
||||
def create_reviewers(self, count):
|
||||
reviewers_objs = []
|
||||
for reviewer in reviewers:
|
||||
@@ -290,6 +655,10 @@ class Command(BaseCommand):
|
||||
participation_type=competition['participation_type'],
|
||||
)
|
||||
|
||||
if competition.get("image"):
|
||||
competition_obj.image_url = competition['image']
|
||||
competition_obj.save()
|
||||
|
||||
competitions[i]['obj'] = competition_obj
|
||||
competition_obj.participants.add(*users)
|
||||
competitions_objs.append(competition_obj)
|
||||
@@ -306,19 +675,26 @@ class Command(BaseCommand):
|
||||
for i, competition in enumerate(competitions):
|
||||
for j, task in enumerate(competition['tasks']):
|
||||
task_obj = CompetitionTask.objects.create(
|
||||
in_competition_position=j,
|
||||
in_competition_position=j+1,
|
||||
competition=competition['obj'],
|
||||
title=task['title'],
|
||||
description=task['description'],
|
||||
type=task['type'],
|
||||
points=task['points'],
|
||||
submission_reviewers_count=task['submission_reviewers_count'],
|
||||
max_attempts=task['max_attempts'],
|
||||
max_attempts=task.get('max_attempts'),
|
||||
)
|
||||
competitions[i]['tasks'][j]['obj'] = task_obj
|
||||
|
||||
if task['type'] == CompetitionTask.CompetitionTaskType.INPUT.value:
|
||||
task_obj.correct_answer_file = task['correct_answer_file']
|
||||
if task.get("attachment"):
|
||||
CompetitionTaskAttachment.objects.create(
|
||||
task=task_obj,
|
||||
file=task['attachment'],
|
||||
bind_at=task['attachment_path'],
|
||||
public=True
|
||||
)
|
||||
|
||||
if (
|
||||
task['type']
|
||||
@@ -383,4 +759,4 @@ class Command(BaseCommand):
|
||||
)
|
||||
self.stdout.write(
|
||||
f"Created state '{state_obj.state}' for user '{user.username}' in competition '{comp.title}'"
|
||||
)
|
||||
)
|
||||
@@ -5,6 +5,7 @@
|
||||
"name": "frontend",
|
||||
"dependencies": {
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@radix-ui/react-accordion": "^1.2.3",
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
||||
"@radix-ui/react-label": "^2.1.2",
|
||||
@@ -154,8 +155,12 @@
|
||||
|
||||
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.1", "", {}, "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA=="],
|
||||
|
||||
"@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-collapsible": "1.1.3", "@radix-ui/react-collection": "1.1.2", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-RIQ15mrcvqIkDARJeERSuXSry2N8uYnxkdDetpfmalT/+0ntOXLkFOsh9iwlAsCv+qcmhZjbdJogIm6WBa6c4A=="],
|
||||
|
||||
"@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.2", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg=="],
|
||||
|
||||
"@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-jFSerheto1X03MUC0g6R7LedNW9EEGWdg9W1+MlpkMLwGkgkbUXLPBH/KIuWKXUoeYRVY11llqbTBDzuLg7qrw=="],
|
||||
|
||||
"@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-slot": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw=="],
|
||||
|
||||
"@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw=="],
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@radix-ui/react-accordion": "^1.2.3",
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
||||
"@radix-ui/react-label": "^2.1.2",
|
||||
|
||||
@@ -30,12 +30,12 @@ export const Header = () => {
|
||||
</Link>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<Link
|
||||
to="/docs/"
|
||||
className="text-sm font-medium text-gray-700 hover:text-gray-900 transition-colors flex items-center gap-1"
|
||||
>
|
||||
Обучающие материалы
|
||||
</Link>
|
||||
<a
|
||||
href="/docs/"
|
||||
className="text-sm font-medium text-gray-700 hover:text-gray-900 transition-colors flex items-center gap-1"
|
||||
>
|
||||
<FileText className="h-4 w-4" />
|
||||
</a>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import * as React from "react"
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
||||
import { ChevronDownIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/shared/lib/utils"
|
||||
|
||||
function Accordion({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
|
||||
return <AccordionPrimitive.Root data-slot="accordion" {...props} />
|
||||
}
|
||||
|
||||
function AccordionItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
|
||||
return (
|
||||
<AccordionPrimitive.Item
|
||||
data-slot="accordion-item"
|
||||
className={cn("border-b last:border-b-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AccordionTrigger({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
|
||||
return (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
data-slot="accordion-trigger"
|
||||
className={cn(
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
)
|
||||
}
|
||||
|
||||
function AccordionContent({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
|
||||
return (
|
||||
<AccordionPrimitive.Content
|
||||
data-slot="accordion-content"
|
||||
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn("pt-0 pb-4", className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
)
|
||||
}
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
||||
+13
-3
@@ -1,19 +1,21 @@
|
||||
import React from 'react';
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { Loader2, CheckCircle } from "lucide-react";
|
||||
|
||||
interface ActionButtonsProps {
|
||||
onSubmit: () => void;
|
||||
onHistoryClick: () => void;
|
||||
isSubmitting?: boolean;
|
||||
hasSubmissionsLeft?: boolean;
|
||||
isCleared: boolean;
|
||||
}
|
||||
|
||||
const ActionButtons: React.FC<ActionButtonsProps> = ({
|
||||
onSubmit,
|
||||
onHistoryClick,
|
||||
isSubmitting = false,
|
||||
hasSubmissionsLeft = true
|
||||
hasSubmissionsLeft = true,
|
||||
isCleared
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex gap-8">
|
||||
@@ -26,7 +28,15 @@ const ActionButtons: React.FC<ActionButtonsProps> = ({
|
||||
История
|
||||
</Button>
|
||||
|
||||
{hasSubmissionsLeft ? (
|
||||
{isCleared ? (
|
||||
<Button
|
||||
className="font-hse-sans flex-grow bg-green-600 hover:bg-green-700"
|
||||
disabled={true}
|
||||
>
|
||||
<CheckCircle className="mr-2 h-4 w-4" />
|
||||
Задача сдана!
|
||||
</Button>
|
||||
) : hasSubmissionsLeft ? (
|
||||
<Button
|
||||
onClick={onSubmit}
|
||||
className="font-hse-sans flex-grow"
|
||||
|
||||
@@ -44,6 +44,10 @@ const TaskSolution: React.FC<TaskSolutionProps> = ({
|
||||
|
||||
const solutionHistory = solutionsQuery.data || [];
|
||||
|
||||
let lastSolutionPoints = 0;
|
||||
if (solutionHistory.length > 0) {
|
||||
lastSolutionPoints = solutionHistory[solutionHistory.length - 1].earned_points
|
||||
}
|
||||
const maxAttempts = task.max_attempts || -1;
|
||||
const submissionsUsed = solutionHistory.length;
|
||||
const submissionsLeft = Math.max(0, maxAttempts - submissionsUsed);
|
||||
@@ -174,6 +178,7 @@ const TaskSolution: React.FC<TaskSolutionProps> = ({
|
||||
onHistoryClick={handleOpenHistory}
|
||||
isSubmitting={isSubmitting}
|
||||
hasSubmissionsLeft={hasSubmissionsLeft}
|
||||
isCleared={task.points === lastSolutionPoints}
|
||||
/>
|
||||
|
||||
<SolutionHistorySheet
|
||||
|
||||
@@ -3,7 +3,11 @@ import { CompetitionGrid } from "./modules/CompetitionsGrid";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getCompetitions } from "@/shared/api/competitions";
|
||||
import { NoCompetitions } from "./modules/NoCompetitions";
|
||||
import {
|
||||
NoActiveCompetitions,
|
||||
NoCompetitions,
|
||||
NoCompletedCompetitions,
|
||||
} from "./modules/NoCompetitions";
|
||||
import { TabsContent } from "@radix-ui/react-tabs";
|
||||
import { Loading } from "@/components/ui/loading";
|
||||
import { CompetitionState } from "@/shared/types/competition";
|
||||
@@ -54,8 +58,8 @@ const CompetitionsPage = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-6 sm:gap-8">
|
||||
{(activeCompetitionsQuery.data ?? []).length > 0 && (
|
||||
<Section>
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} asChild>
|
||||
<Section>
|
||||
<SectionHeader>
|
||||
<SectionTitle>Мои события</SectionTitle>
|
||||
|
||||
@@ -70,14 +74,22 @@ const CompetitionsPage = () => {
|
||||
</SectionHeader>
|
||||
|
||||
<TabsContent value={CompetitionTab.ONGOING} asChild>
|
||||
<CompetitionGrid competitions={startedCompetitions} />
|
||||
{startedCompetitions.length > 0 ? (
|
||||
<CompetitionGrid competitions={startedCompetitions} />
|
||||
) : (
|
||||
<NoActiveCompetitions />
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value={CompetitionTab.COMPLETED} asChild>
|
||||
<CompetitionGrid competitions={finishedCompetitions} />
|
||||
{finishedCompetitions.length > 0 ? (
|
||||
<CompetitionGrid competitions={finishedCompetitions} />
|
||||
) : (
|
||||
<NoCompletedCompetitions />
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</Section>
|
||||
</Section>
|
||||
</Tabs>
|
||||
)}
|
||||
|
||||
<Section>
|
||||
|
||||
@@ -13,3 +13,27 @@ export const NoCompetitions = () => {
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const NoActiveCompetitions = () => {
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<Ban size={32} />
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<h2 className="text-2xl font-semibold">Нет активных событий</h2>
|
||||
<p className="text-muted-foreground text-lg">Начните новое</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const NoCompletedCompetitions = () => {
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<Ban size={32} />
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<h2 className="text-2xl font-semibold">Завершенных событий нет</h2>
|
||||
<p className="text-muted-foreground text-lg">Завершите начатое</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { User } from "@/shared/types/user";
|
||||
import { UserInfo } from "./widgets/user-info";
|
||||
import { UserAchievements } from "./widgets/user-achievements";
|
||||
import { UserStats } from "./widgets/user-stats";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { userFetch } from ".";
|
||||
import { Competition } from "../types/competition";
|
||||
import { Competition, CompetitionResult } from "../types/competition";
|
||||
|
||||
export const getCompetitions = async (participating?: boolean) => {
|
||||
return await userFetch<Competition[]>("/competitions", {
|
||||
@@ -13,6 +13,10 @@ export const getCompetition = async (id: string) => {
|
||||
return await userFetch<Competition>(`/competitions/${id}`);
|
||||
};
|
||||
|
||||
export const getCompetitionResults = async (id: string) => {
|
||||
return await userFetch<CompetitionResult>(`/competitions/${id}/results`);
|
||||
}
|
||||
|
||||
export const startCompetition = async (competitionId: string) => {
|
||||
return await userFetch(`/competitions/${competitionId}/start`, {
|
||||
method: "POST",
|
||||
|
||||
@@ -24,3 +24,8 @@ export enum CompetitionType {
|
||||
export enum CompetitionParticipationType {
|
||||
SOLO = "solo",
|
||||
}
|
||||
|
||||
export interface CompetitionResult {
|
||||
task_name: string;
|
||||
result: number;
|
||||
}
|
||||
Reference in New Issue
Block a user