fix some stuff

This commit is contained in:
q 2026-05-19 09:40:26 +03:00
parent 1fa0ea2fd5
commit ab642bc8b0
24 changed files with 846 additions and 15 deletions

BIN
!doc.tar.gz Normal file

Binary file not shown.

View file

@ -0,0 +1 @@
,q,think,23.03.2026 13:02,file:///home/q/.config/libreoffice/4;

BIN
!doc/dashboard.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

204
!doc/dashboard_auth.html Normal file
View file

@ -0,0 +1,204 @@
<!DOCTYPE html>
<html lang="ru" data-bs-theme="dark">
<head><base href="http://localhost:1337/">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Личный кабинет — FitnessCenter</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;600;700&display=swap" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<link rel="stylesheet" href="/static/css/main.css">
</head>
<body>
<header class="navbar navbar-expand-lg border-bottom bg-gradient position-sticky top-0 z-3 backdrop-blur">
<div class="container-fluid px-4">
<a class="navbar-brand d-flex align-items-center gap-2" href="/">
<img src="/static/img/logo.jpg" alt="logo" class="brand-logo">
<span class="fw-bold">FitnessCenter</span>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mainNav" aria-controls="mainNav" aria-expanded="false" aria-label="Переключить меню">
<span class="navbar-toggler-icon"></span>
</button>
<nav class="collapse navbar-collapse" id="mainNav">
<ul class="navbar-nav ms-auto align-items-lg-center gap-lg-3">
<li class="nav-item"><a class="nav-link" href="/">Главная</a></li>
<li class="nav-item"><a class="nav-link" href="#trainers">Тренеры</a></li>
<li class="nav-item"><a class="nav-link" href="#booking">Записаться</a></li>
<li class="nav-item"><a class="nav-link" href="#about">О клубе</a></li>
<li class="nav-item"><a class="nav-link" href="/dashboard/">Кабинет</a></li>
<li class="nav-item"><a class="btn btn-outline-light" href="/logout/">Выйти</a></li>
</ul>
</nav>
</div>
</header>
<main class="py-4">
<div class="container">
</div>
<div class="container py-4 py-lg-5">
<div class="row g-4">
<div class="col-lg-4">
<div class="card glass-card shadow-sm h-100">
<div class="card-body">
<div class="d-flex align-items-center mb-3">
<div class="avatar bg-accent-subtle text-accent fw-bold me-3">Е</div>
<div>
<div class="fw-bold">Егор Тестов</div>
<div class="text-secondary small">report@example.com</div>
</div>
</div>
<div class="alert alert-success">Активен до 15.04.2026</div>
<a class="btn btn-outline-light w-100" href="/logout/">Выйти</a>
</div>
</div>
</div>
<div class="col-lg-8">
<div class="card glass-card shadow-sm mb-4">
<div class="card-body">
<h2 class="h5 fw-bold mb-3">Оформить абонемент</h2>
<div class="row g-3">
<div class="col-md-6">
<div class="border rounded-3 p-3 h-100">
<div class="d-flex justify-content-between">
<div>
<div class="fw-bold">Старт 1 месяц</div>
<div class="text-secondary small">30 дней</div>
</div>
<div class="fw-bold">2490,00 ₽</div>
</div>
<p class="text-secondary small mb-3">Базовый доступ ко всем зонам клуба.</p>
<form method="post" action="/subscribe/1/">
<input type="hidden" name="csrfmiddlewaretoken" value="QshsQpHZSSJVoE2GWNaG1SFOKp0SDqod8ZMtE9jWebMvq5xBssM98IxQM3bqZ2MK">
<button class="btn btn-accent w-100" type="submit">Купить</button>
</form>
</div>
</div>
<div class="col-md-6">
<div class="border rounded-3 p-3 h-100">
<div class="d-flex justify-content-between">
<div>
<div class="fw-bold">Сила 3 месяца</div>
<div class="text-secondary small">90 дней</div>
</div>
<div class="fw-bold">6490,00 ₽</div>
</div>
<p class="text-secondary small mb-3">Три месяца + гостевой визит раз в месяц.</p>
<form method="post" action="/subscribe/2/">
<input type="hidden" name="csrfmiddlewaretoken" value="QshsQpHZSSJVoE2GWNaG1SFOKp0SDqod8ZMtE9jWebMvq5xBssM98IxQM3bqZ2MK">
<button class="btn btn-accent w-100" type="submit">Купить</button>
</form>
</div>
</div>
<div class="col-md-6">
<div class="border rounded-3 p-3 h-100">
<div class="d-flex justify-content-between">
<div>
<div class="fw-bold">Премиум 12 месяцев</div>
<div class="text-secondary small">365 дней</div>
</div>
<div class="fw-bold">19900,00 ₽</div>
</div>
<p class="text-secondary small mb-3">Год без ограничений, приоритетная запись на тренировки.</p>
<form method="post" action="/subscribe/3/">
<input type="hidden" name="csrfmiddlewaretoken" value="QshsQpHZSSJVoE2GWNaG1SFOKp0SDqod8ZMtE9jWebMvq5xBssM98IxQM3bqZ2MK">
<button class="btn btn-accent w-100" type="submit">Купить</button>
</form>
</div>
</div>
</div>
</div>
</div>
<div class="card glass-card shadow-sm">
<div class="card-body">
<h2 class="h5 fw-bold mb-3">История</h2>
<div class="table-responsive">
<table class="table align-middle mb-0">
<thead>
<tr>
<th>План</th>
<th>Период</th>
<th>Статус</th>
</tr>
</thead>
<tbody>
<tr>
<td>Старт 1 месяц</td>
<td>16.03.2026 — 15.04.2026</td>
<td>
<span class="badge text-bg-success">Активен</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<footer class="footer py-5" id="booking">
<div class="container">
<div class="row align-items-center g-3">
<div class="col-md-4">
<h4 class="fw-bold mb-3">Приобрести абонемент</h4>
<p class="text-secondary">Оставьте контакты — менеджер подберёт план за пару минут.</p>
</div>
<div class="col-md-8">
<form class="row g-3" aria-label="Заявка на абонемент">
<div class="col-md-4">
<input type="text" class="form-control form-control-lg" placeholder="Ф.И.О" required>
</div>
<div class="col-md-4">
<input type="tel" class="form-control form-control-lg" placeholder="+7 (___) ___-__-__" required>
</div>
<div class="col-md-4">
<select class="form-select form-select-lg" required>
<option value="" selected disabled>Выберите клуб</option>
<option>Центр на Большой Садовой — ул. Большая Садовая, 105</option>
<option>Западный Арена — пр-т Стачки, 190</option>
<option>Северный Loft — пр-т Космонавтов, 14/1</option>
<option>Левенцовка Energy — ул. Малиновского, 25Г</option>
<option>Береговой RiverFit — Береговая ул., 15к3</option>
</select>
</div>
<div class="col-12 text-md-end">
<button class="btn btn-accent btn-lg" type="button">Позвоните мне</button>
</div>
</form>
</div>
</div>
<div class="mt-4 small text-secondary">© 2026 FitnessCenter — здоровое тело как стиль жизни.</div>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
</body>
</html>

143
!doc/embed_docx_images.py Normal file
View file

@ -0,0 +1,143 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import shutil
import tempfile
import zipfile
from pathlib import Path
from urllib.parse import unquote, urlparse
from xml.etree import ElementTree as ET
PKG_NS = "http://schemas.openxmlformats.org/package/2006/relationships"
R_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
A_NS = "http://schemas.openxmlformats.org/drawingml/2006/main"
CT_NS = "http://schemas.openxmlformats.org/package/2006/content-types"
ET.register_namespace("", PKG_NS)
ET.register_namespace("a", A_NS)
ET.register_namespace("r", R_NS)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Embed externally linked local images into a DOCX file."
)
parser.add_argument("input_docx", type=Path)
parser.add_argument("output_docx", type=Path)
return parser.parse_args()
def local_path_from_target(target: str) -> Path | None:
parsed = urlparse(target)
if parsed.scheme != "file":
return None
return Path(unquote(parsed.path))
def ensure_content_type(defaults_root: ET.Element, extension: str) -> None:
ext = extension.lower().lstrip(".")
existing = defaults_root.find(f"{{{CT_NS}}}Default[@Extension='{ext}']")
if existing is not None:
return
content_types = {
"png": "image/png",
"jpg": "image/jpeg",
"jpeg": "image/jpeg",
"gif": "image/gif",
"bmp": "image/bmp",
"svg": "image/svg+xml",
}
content_type = content_types.get(ext)
if not content_type:
return
ET.SubElement(
defaults_root,
f"{{{CT_NS}}}Default",
{"Extension": ext, "ContentType": content_type},
)
def main() -> int:
args = parse_args()
input_docx = args.input_docx.resolve()
output_docx = args.output_docx.resolve()
with tempfile.TemporaryDirectory() as tmpdir:
workdir = Path(tmpdir)
with zipfile.ZipFile(input_docx) as zin:
zin.extractall(workdir)
rels_path = workdir / "word" / "_rels" / "document.xml.rels"
doc_path = workdir / "word" / "document.xml"
types_path = workdir / "[Content_Types].xml"
media_dir = workdir / "word" / "media"
media_dir.mkdir(parents=True, exist_ok=True)
rels_tree = ET.parse(rels_path)
rels_root = rels_tree.getroot()
doc_tree = ET.parse(doc_path)
doc_root = doc_tree.getroot()
types_tree = ET.parse(types_path)
types_root = types_tree.getroot()
rel_to_target: dict[str, str] = {}
next_image_num = 1
existing_media = sorted(media_dir.glob("image*.*"))
if existing_media:
next_image_num = len(existing_media) + 1
for rel in rels_root.findall(f"{{{PKG_NS}}}Relationship"):
rel_type = rel.attrib.get("Type", "")
target_mode = rel.attrib.get("TargetMode")
if not rel_type.endswith("/image") or target_mode != "External":
continue
target = rel.attrib.get("Target", "")
local_path = local_path_from_target(target)
if not local_path or not local_path.exists():
continue
image_name = f"image{next_image_num}{local_path.suffix.lower()}"
next_image_num += 1
embedded_target = f"media/{image_name}"
shutil.copy2(local_path, media_dir / image_name)
rel.attrib["Target"] = embedded_target
rel.attrib.pop("TargetMode", None)
rel_to_target[rel.attrib["Id"]] = embedded_target
ensure_content_type(types_root, local_path.suffix)
if not rel_to_target:
raise SystemExit("No external local images found in DOCX relationships.")
for blip in doc_root.findall(f".//{{{A_NS}}}blip"):
link_key = f"{{{R_NS}}}link"
embed_key = f"{{{R_NS}}}embed"
rel_id = blip.attrib.get(link_key)
if rel_id in rel_to_target:
blip.attrib[embed_key] = rel_id
blip.attrib.pop(link_key, None)
rels_tree.write(rels_path, encoding="UTF-8", xml_declaration=True)
doc_tree.write(doc_path, encoding="UTF-8", xml_declaration=True)
types_tree.write(types_path, encoding="UTF-8", xml_declaration=True)
if output_docx.exists():
output_docx.unlink()
with zipfile.ZipFile(output_docx, "w", zipfile.ZIP_DEFLATED) as zout:
for path in sorted(workdir.rglob("*")):
if path.is_dir():
continue
zout.write(path, path.relative_to(workdir).as_posix())
return 0
if __name__ == "__main__":
raise SystemExit(main())

BIN
!doc/example.docx Normal file

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,143 @@
# Отчёт по проекту FitnessCenter
Подготовлено на основе технической документации проекта, работающего локального экземпляра сайта и шаблона оформления `example.docx`.
Город: Ростов-на-Дону
Дата подготовки отчёта: 16.03.2026
## Введение
Проект **FitnessCenter** представляет собой веб-приложение фитнес-клуба с лендингом, регистрацией пользователей, авторизацией, личным кабинетом и оформлением абонементов. Система ориентирована на быстрый запуск в учебной или демонстрационной среде и построена по модели серверного рендеринга HTML без отдельного JSON API.
Целью работы команды было создание понятного и визуально цельного сайта, который одновременно решает маркетинговую задачу лендинга и прикладную задачу оформления абонементов через личный кабинет.
## Краткое описание системы
В ходе анализа технической документации и интерфейса были выделены следующие ключевые возможности проекта:
- главная страница с презентацией клуба, тарифов, тренеров и филиалов;
- регистрация нового пользователя с проверкой телефона в формате `+7XXXXXXXXXX`;
- вход в систему через форму авторизации;
- личный кабинет с отображением активного абонемента и истории покупок;
- оформление нового абонемента из личного кабинета;
- административное управление данными через стандартную Django-админку;
- запуск в Docker-контуре с публикацией сервиса на порту `1337`.
## Технологический стек и архитектура
По технической документации проект реализован на следующем стеке:
- backend: `Python 3.13`, `Django 5.1.4`;
- frontend: `Bootstrap 5.3`, кастомные стили в `static/css/main.css`, шрифт `Manrope`;
- база данных по умолчанию: `SQLite`, с возможностью переноса на `PostgreSQL`;
- инфраструктура: `Dockerfile`, `docker-compose.yml`, `entrypoint.sh`;
- архитектурный подход: серверный рендеринг HTML-страниц без выделенного REST API.
Ключевые структурные элементы проекта:
- `gymclub/` содержит настройки Django и верхнеуровневую маршрутизацию;
- `club/` реализует модели, формы, views и бизнес-логику абонементов;
- `templates/` содержит шаблоны страниц `home`, `register`, `login`, `dashboard`;
- `static/` и `staticfiles/` отвечают за визуальную часть проекта;
- маршруты регистрации, входа, выхода и покупки абонементов сосредоточены в `club/urls.py`.
## Реализованный функционал
В итоговой версии системы реализованы следующие пользовательские сценарии:
### 1. Лендинг фитнес-клуба
Главная страница знакомит пользователя с клубом, доступными тарифами, тренерами и адресами филиалов. Интерфейс выполнен в тёмной цветовой схеме с акцентным зелёным цветом, что делает сайт визуально единым и современным.
### 2. Регистрация и авторизация
Пользователь может создать аккаунт, указав логин, имя, фамилию, телефон, email и пароль. После регистрации доступны вход в систему и переход в личный кабинет. Для телефона реализована нормализация и валидация, что снижает количество некорректных записей.
### 3. Личный кабинет и покупка абонементов
После авторизации пользователь видит активный абонемент, историю покупок и перечень доступных тарифов. Покупка выполняется через серверный сценарий, который рассчитывает дату окончания абонемента автоматически.
### 4. Эксплуатационная готовность
Проект запускается в контейнере Docker, а настройки окружения вынесены в переменные. Это упрощает перенос между стендами и делает демонстрацию предсказуемой.
## Команда проекта и распределение работ
Оценка трудозатрат ниже является проектной и отражает предполагаемый вклад участников на протяжении основного этапа разработки с **10.02.2026 по 14.03.2026**.
- **Комин Архип**
Роль: тимлид, инфраструктура, DevOps и backend.
Оценка занятости: **64 часа**.
Основные задачи: постановка архитектуры проекта, подготовка Docker-контура, настройка переменных окружения, организация структуры Django-приложения, реализация моделей `MembershipPlan`, `Profile`, `Subscription`, настройка маршрутов и контроль интеграции backend-части с шаблонами.
- **Токмачев Егор**
Роль: full-stack разработчик.
Оценка занятости: **58 часов**.
Основные задачи: реализация форм регистрации и входа, связка пользовательских сценариев с бизнес-логикой, доработка личного кабинета, интеграция покупки абонементов, отладка переходов между страницами и проверка корректности пользовательских сообщений.
- **Стрельников Вадим**
Роль: frontend разработчик, дизайнер.
Оценка занятости: **49 часов**.
Основные задачи: разработка визуальной концепции сайта, вёрстка главной страницы и экранов авторизации, настройка тёмной темы, адаптация карточек тарифов и тренеров, подбор визуальных акцентов, доработка читаемости и согласованности интерфейса на разных экранах.
- **Акуев Магомед**
Роль: тестировщик, дизайнер.
Оценка занятости: **37 часов**.
Основные задачи: ручное тестирование регистрации, входа и кабинета, проверка сценариев покупки абонемента, поиск визуальных и функциональных дефектов, проверка текстов форм и состояний ошибок, участие в финальной полировке элементов интерфейса.
**Суммарная оценка трудозатрат команды: 208 часов.**
## Итоговые экраны системы
Ниже приведены скриншоты реально работающего локального экземпляра проекта.
### Главная страница
![Главная страница проекта](home.png)
На главной странице представлены ключевое позиционирование клуба, тарифы, карточки тренеров, адреса филиалов и информационные блоки.
### Страница входа
![Страница входа](login.png)
Экран входа минималистичен и сфокусирован на быстрой авторизации пользователя.
### Страница регистрации
![Страница регистрации](register.png)
Форма регистрации включает обязательные пользовательские поля и согласие на обработку персональных данных.
### Личный кабинет пользователя
![Личный кабинет пользователя](dashboard.png)
В кабинете отображаются профиль пользователя, срок действия активного абонемента, доступные тарифы и история покупок.
## Вывод
По результатам анализа документации и проверки интерфейса можно сделать вывод, что проект **FitnessCenter** доведён до работоспособного состояния и решает основные задачи учебного веб-продукта: демонстрацию услуг фитнес-клуба, регистрацию пользователей, авторизацию и оформление абонементов. Архитектура проекта остаётся достаточно простой для сопровождения, а Docker-окружение делает запуск и демонстрацию удобными.
Отдельным преимуществом проекта является согласованная работа команды: backend, frontend, дизайн и тестирование разделены по ролям, но при этом объединены в единый пользовательский сценарий. Это делает результат целостным как с технической, так и с визуальной точки зрения.
## Использованные материалы и литература
### Локальные материалы проекта
- техническая документация проекта: `../DOCS_RU.md`;
- локально запущенный сайт: `http://localhost:1337`;
- шаблон оформления и структуры отчёта: `example.docx`.
### Подобранная литература
1. Django Software Foundation. **Django documentation** [Электронный ресурс]. URL: `https://docs.djangoproject.com/en/5.1/` (дата обращения: 16.03.2026).
2. Bootstrap Team. **Bootstrap v5.3 Documentation** [Электронный ресурс]. URL: `https://getbootstrap.com/docs/5.3/` (дата обращения: 16.03.2026).
3. Fowler M. **Patterns of Enterprise Application Architecture**. Boston: Addison-Wesley, 2002.
4. Krug S. **Don't Make Me Think, Revisited: A Common Sense Approach to Web Usability**. 3rd ed. Berkeley: New Riders, 2014.
5. Pressman R. S., Maxim B. R. **Software Engineering: A Practitioner's Approach**. 9th ed. New York: McGraw-Hill, 2019.
6. Gamma E., Helm R., Johnson R., Vlissides J. **Design Patterns: Elements of Reusable Object-Oriented Software**. Boston: Addison-Wesley, 1994.
7. Sommerville I. **Software Engineering**. 10th ed. Boston: Pearson, 2015.
8. Нильсен Я. **Веб-дизайн: удобство использования веб-сайтов**. М.: Вильямс, 2010.
9. Купер А., Рейман Р., Кронин Д., Носсел К. **Интерфейс. Основы проектирования взаимодействия**. 4-е изд. СПб.: Питер, 2017.
10. Макконнелл С. **Совершенный код**. 2-е изд. СПб.: Питер, 2019.

View file

@ -0,0 +1,94 @@
<?xml version="1.0" encoding="UTF-8"?>
<indexing>
<object alt="" name="Image1" object_type="graphic"/>
<object alt="" name="Image2" object_type="graphic"/>
<object alt="" name="Image3" object_type="graphic"/>
<object alt="" name="Image4" object_type="graphic"/>
<paragraph index="21" node_type="writer">Отчёт по проекту FitnessCenter</paragraph>
<paragraph index="22" node_type="writer">Подготовлено на основе технической документации проекта, работающего локального экземпляра сайта и шаблона оформления example.docx.</paragraph>
<paragraph index="23" node_type="writer">Город: Ростов-на-Дону
Дата подготовки отчёта: 16.03.2026</paragraph>
<paragraph index="24" node_type="writer">Введение</paragraph>
<paragraph index="25" node_type="writer">Проект FitnessCenter представляет собой веб-приложение фитнес-клуба с лендингом, регистрацией пользователей, авторизацией, личным кабинетом и оформлением абонементов. Система ориентирована на быстрый запуск в учебной или демонстрационной среде и построена по модели серверного рендеринга HTML без отдельного JSON API.</paragraph>
<paragraph index="26" node_type="writer">Целью работы команды было создание понятного и визуально цельного сайта, который одновременно решает маркетинговую задачу лендинга и прикладную задачу оформления абонементов через личный кабинет.</paragraph>
<paragraph index="27" node_type="writer">Краткое описание системы</paragraph>
<paragraph index="28" node_type="writer">В ходе анализа технической документации и интерфейса были выделены следующие ключевые возможности проекта:</paragraph>
<paragraph index="29" node_type="writer">главная страница с презентацией клуба, тарифов, тренеров и филиалов;</paragraph>
<paragraph index="30" node_type="writer">регистрация нового пользователя с проверкой телефона в формате +7XXXXXXXXXX;</paragraph>
<paragraph index="31" node_type="writer">вход в систему через форму авторизации;</paragraph>
<paragraph index="32" node_type="writer">личный кабинет с отображением активного абонемента и истории покупок;</paragraph>
<paragraph index="33" node_type="writer">оформление нового абонемента из личного кабинета;</paragraph>
<paragraph index="34" node_type="writer">административное управление данными через стандартную Django-админку;</paragraph>
<paragraph index="35" node_type="writer">запуск в Docker-контуре с публикацией сервиса на порту 1337.</paragraph>
<paragraph index="36" node_type="writer">Технологический стек и архитектура</paragraph>
<paragraph index="37" node_type="writer">По технической документации проект реализован на следующем стеке:</paragraph>
<paragraph index="38" node_type="writer">backend: Python 3.13, Django 5.1.4;</paragraph>
<paragraph index="39" node_type="writer">frontend: Bootstrap 5.3, кастомные стили в static/css/main.css, шрифт Manrope;</paragraph>
<paragraph index="40" node_type="writer">база данных по умолчанию: SQLite, с возможностью переноса на PostgreSQL;</paragraph>
<paragraph index="41" node_type="writer">инфраструктура: Dockerfile, docker-compose.yml, entrypoint.sh;</paragraph>
<paragraph index="42" node_type="writer">архитектурный подход: серверный рендеринг HTML-страниц без выделенного REST API.</paragraph>
<paragraph index="43" node_type="writer">Ключевые структурные элементы проекта:</paragraph>
<paragraph index="44" node_type="writer">gymclub/ содержит настройки Django и верхнеуровневую маршрутизацию;</paragraph>
<paragraph index="45" node_type="writer">club/ реализует модели, формы, views и бизнес-логику абонементов;</paragraph>
<paragraph index="46" node_type="writer">templates/ содержит шаблоны страниц home, register, login, dashboard;</paragraph>
<paragraph index="47" node_type="writer">static/ и staticfiles/ отвечают за визуальную часть проекта;</paragraph>
<paragraph index="48" node_type="writer">маршруты регистрации, входа, выхода и покупки абонементов сосредоточены в club/urls.py.</paragraph>
<paragraph index="49" node_type="writer">Реализованный функционал</paragraph>
<paragraph index="50" node_type="writer">В итоговой версии системы реализованы следующие пользовательские сценарии:</paragraph>
<paragraph index="51" node_type="writer">1. Лендинг фитнес-клуба</paragraph>
<paragraph index="52" node_type="writer">Главная страница знакомит пользователя с клубом, доступными тарифами, тренерами и адресами филиалов. Интерфейс выполнен в тёмной цветовой схеме с акцентным зелёным цветом, что делает сайт визуально единым и современным.</paragraph>
<paragraph index="53" node_type="writer">2. Регистрация и авторизация</paragraph>
<paragraph index="54" node_type="writer">Пользователь может создать аккаунт, указав логин, имя, фамилию, телефон, email и пароль. После регистрации доступны вход в систему и переход в личный кабинет. Для телефона реализована нормализация и валидация, что снижает количество некорректных записей.</paragraph>
<paragraph index="55" node_type="writer">3. Личный кабинет и покупка абонементов</paragraph>
<paragraph index="56" node_type="writer">После авторизации пользователь видит активный абонемент, историю покупок и перечень доступных тарифов. Покупка выполняется через серверный сценарий, который рассчитывает дату окончания абонемента автоматически.</paragraph>
<paragraph index="57" node_type="writer">4. Эксплуатационная готовность</paragraph>
<paragraph index="58" node_type="writer">Проект запускается в контейнере Docker, а настройки окружения вынесены в переменные. Это упрощает перенос между стендами и делает демонстрацию предсказуемой.</paragraph>
<paragraph index="59" node_type="writer">Команда проекта и распределение работ</paragraph>
<paragraph index="60" node_type="writer">Оценка трудозатрат ниже является проектной и отражает предполагаемый вклад участников на протяжении основного этапа разработки с 10.02.2026 по 14.03.2026.</paragraph>
<paragraph index="61" node_type="writer">Комин Архип
Роль: тимлид, инфраструктура, DevOps и backend.
Оценка занятости: 64 часа.
Основные задачи: постановка архитектуры проекта, подготовка Docker-контура, настройка переменных окружения, организация структуры Django-приложения, реализация моделей MembershipPlan, Profile, Subscription, настройка маршрутов и контроль интеграции backend-части с шаблонами.</paragraph>
<paragraph index="62" node_type="writer">Токмачев Егор
Роль: full-stack разработчик.
Оценка занятости: 58 часов.
Основные задачи: реализация форм регистрации и входа, связка пользовательских сценариев с бизнес-логикой, доработка личного кабинета, интеграция покупки абонементов, отладка переходов между страницами и проверка корректности пользовательских сообщений.</paragraph>
<paragraph index="63" node_type="writer">Стрельников Вадим
Роль: frontend разработчик, дизайнер.
Оценка занятости: 49 часов.
Основные задачи: разработка визуальной концепции сайта, вёрстка главной страницы и экранов авторизации, настройка тёмной темы, адаптация карточек тарифов и тренеров, подбор визуальных акцентов, доработка читаемости и согласованности интерфейса на разных экранах.</paragraph>
<paragraph index="64" node_type="writer">Акуев Магомед
Роль: тестировщик, дизайнер.
Оценка занятости: 37 часов.
Основные задачи: ручное тестирование регистрации, входа и кабинета, проверка сценариев покупки абонемента, поиск визуальных и функциональных дефектов, проверка текстов форм и состояний ошибок, участие в финальной полировке элементов интерфейса.</paragraph>
<paragraph index="65" node_type="writer">Суммарная оценка трудозатрат команды: 208 часов.</paragraph>
<paragraph index="66" node_type="writer">Итоговые экраны системы</paragraph>
<paragraph index="67" node_type="writer">Ниже приведены скриншоты реально работающего локального экземпляра проекта.</paragraph>
<paragraph index="68" node_type="writer">Главная страница</paragraph>
<paragraph index="70" node_type="writer">На главной странице представлены ключевое позиционирование клуба, тарифы, карточки тренеров, адреса филиалов и информационные блоки.</paragraph>
<paragraph index="71" node_type="writer">Страница входа</paragraph>
<paragraph index="73" node_type="writer">Экран входа минималистичен и сфокусирован на быстрой авторизации пользователя.</paragraph>
<paragraph index="74" node_type="writer">Страница регистрации</paragraph>
<paragraph index="76" node_type="writer">Форма регистрации включает обязательные пользовательские поля и согласие на обработку персональных данных.</paragraph>
<paragraph index="77" node_type="writer">Личный кабинет пользователя</paragraph>
<paragraph index="79" node_type="writer">В кабинете отображаются профиль пользователя, срок действия активного абонемента, доступные тарифы и история покупок.</paragraph>
<paragraph index="80" node_type="writer">Вывод</paragraph>
<paragraph index="81" node_type="writer">По результатам анализа документации и проверки интерфейса можно сделать вывод, что проект FitnessCenter доведён до работоспособного состояния и решает основные задачи учебного веб-продукта: демонстрацию услуг фитнес-клуба, регистрацию пользователей, авторизацию и оформление абонементов. Архитектура проекта остаётся достаточно простой для сопровождения, а Docker-окружение делает запуск и демонстрацию удобными.</paragraph>
<paragraph index="82" node_type="writer">Отдельным преимуществом проекта является согласованная работа команды: backend, frontend, дизайн и тестирование разделены по ролям, но при этом объединены в единый пользовательский сценарий. Это делает результат целостным как с технической, так и с визуальной точки зрения.</paragraph>
<paragraph index="83" node_type="writer">Использованные материалы и литература</paragraph>
<paragraph index="84" node_type="writer">Локальные материалы проекта</paragraph>
<paragraph index="85" node_type="writer">техническая документация проекта: ../DOCS_RU.md;</paragraph>
<paragraph index="86" node_type="writer">локально запущенный сайт: http://localhost:1337;</paragraph>
<paragraph index="87" node_type="writer">шаблон оформления и структуры отчёта: example.docx.</paragraph>
<paragraph index="88" node_type="writer">Подобранная литература</paragraph>
<paragraph index="89" node_type="writer">Django Software Foundation. Django documentation [Электронный ресурс]. URL: https://docs.djangoproject.com/en/5.1/ (дата обращения: 16.03.2026).</paragraph>
<paragraph index="90" node_type="writer">Bootstrap Team. Bootstrap v5.3 Documentation [Электронный ресурс]. URL: https://getbootstrap.com/docs/5.3/ (дата обращения: 16.03.2026).</paragraph>
<paragraph index="91" node_type="writer">Fowler M. Patterns of Enterprise Application Architecture. Boston: Addison-Wesley, 2002.</paragraph>
<paragraph index="92" node_type="writer">Krug S. Don't Make Me Think, Revisited: A Common Sense Approach to Web Usability. 3rd ed. Berkeley: New Riders, 2014.</paragraph>
<paragraph index="93" node_type="writer">Pressman R. S., Maxim B. R. Software Engineering: A Practitioner's Approach. 9th ed. New York: McGraw-Hill, 2019.</paragraph>
<paragraph index="94" node_type="writer">Gamma E., Helm R., Johnson R., Vlissides J. Design Patterns: Elements of Reusable Object-Oriented Software. Boston: Addison-Wesley, 1994.</paragraph>
<paragraph index="95" node_type="writer">Sommerville I. Software Engineering. 10th ed. Boston: Pearson, 2015.</paragraph>
<paragraph index="96" node_type="writer">Нильсен Я. Веб-дизайн: удобство использования веб-сайтов. М.: Вильямс, 2010.</paragraph>
<paragraph index="97" node_type="writer">Купер А., Рейман Р., Кронин Д., Носсел К. Интерфейс. Основы проектирования взаимодействия. 4-е изд. СПб.: Питер, 2017.</paragraph>
<paragraph index="98" node_type="writer">Макконнелл С. Совершенный код. 2-е изд. СПб.: Питер, 2019.</paragraph>
</indexing>

BIN
!doc/home.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 584 KiB

BIN
!doc/login.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 KiB

BIN
!doc/register.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 KiB

91
DOCS_RU.md Normal file
View file

@ -0,0 +1,91 @@
# FitnessCenter — эксплуатационная документация
## Назначение
Одностраничный лендинг и личный кабинет фитнес‑клуба с регистрацией, входом и покупкой абонементов. Рендеринг на сервере (Django), без JSON API.
## Стек
- Python 3.13, Django 5.1.4
- Bootstrap 5.3 (CDN), шрифт Manrope, кастомный тёмный UI `static/css/main.css`
- БД по умолчанию: SQLite (`db.sqlite3`); совместимо с Postgres
- Контейнеризация: Dockerfile (python:3.13-slim) + `docker-compose.yml` (порт 1337→8000)
## Архитектура и файлы
- `gymclub/` — настройки Django, URLмаршрутизация верхнего уровня.
- `club/` — доменная логика: модели, формы, views, urls, context processor с адресами клубов.
- `templates/` — серверные шаблоны: `base.html`, `home.html`, `register.html`, `login.html`, `dashboard.html`.
- `static/` — стили и логотип, подключаются через `{% static %}`; собранные файлы кладутся в `staticfiles/`.
- `entrypoint.sh` — миграции + `collectstatic` (ошибки игнорируются) и запуск `runserver` на 0.0.0.0:8000.
- `www/` — архивная статическая версия лендинга (не используется приложением).
## Данные и бизнес‑логика
- `MembershipPlan`: имя, описание, длительность в днях, цена. Начальные тарифы создаются миграцией `0002_default_plans`.
- `Profile`: OneToOne к пользователю, хранит телефон.
- `Subscription`: ссылка на пользователя и план, даты начала/окончания; `is_active` проверяет `end_date` ≥ сегодня.
- Хелпер `Subscription.create_for_plan(user, plan)` рассчитывает дату окончания от текущей даты и создаёт запись — используйте его вместо ручного сохранения.
- Проверка телефона (RegistrationForm): нормализует «8» в «+7», требует 11 цифр формата +7XXXXXXXXXX.
- Парольные валидаторы стандартные, но проверка «простых паролей» отключена (CommonPasswordValidator не подключён).
## Маршруты (HTML API)
Все пути объявлены в `club/urls.py`, подключены в `gymclub/urls.py`.
- `GET /` — витрина с планами, адресами клубов.
- `GET /register/`, `POST /register/` — регистрация пользователя, автологин, создание Profile.
- `GET /login/`, `POST /login/` — аутентификация через Django AuthenticationForm.
- `GET /logout/` — logout + redirect домой (view использует `logout`, форма не нужна).
- `GET /dashboard/` (требует авторизации) — активный абонемент, история, покупка новых.
- `POST /subscribe/<int:plan_id>/` — покупка плана через `Subscription.create_for_plan`.
- `admin/` — стандартная админка для планов, подписок и профилей.
Все формы защищены CSRF; сообщения пользователю выводятся через Django messages.
## UI и контент
- Тёмная тема с акцентным цветом `--accent` в `static/css/main.css`; поменяйте переменные в корне файла для ребрендинга.
- Список клубов задаётся константой `CLUBS` в `club/context_processors.py` и доступен в шаблонах как `clubs`.
- Главная страница содержит секции «Абонементы», «Тренеры», «Клубы», «О нас» и форму заявки в футере (пока без обработки).
## Переменные окружения
Файл примера `.env.example`:
- `DJANGO_DEBUG` (True/False)
- `DJANGO_ALLOWED_HOSTS` (список через запятую, `*` по умолчанию)
- `DJANGO_SECRET_KEY` (смените в проде)
Дополнительно в `settings.py`: язык `ru-ru`, часовой пояс `Europe/Moscow`.
## Запуск
### В Docker
```bash
cp .env.example .env
docker compose build
docker compose up
# Откройте http://localhost:1337
```
Контейнер монтирует текущую директорию, так что правки видны без пересборки.
### Локально (без Docker)
```bash
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env # при необходимости отредактируйте
python manage.py migrate
python manage.py runserver
```
### Админка и данные
- Создайте суперпользователя: `python manage.py createsuperuser`.
- Начальные планы создаются миграцией; новые можно добавить через /admin/ или фикстуры.
### Переключение на Postgres (кратко)
1. Добавьте сервис в `docker-compose.yml` или поднимите внешнюю БД.
2. В `gymclub/settings.py` задайте `ENGINE = 'django.db.backends.postgresql'` и параметры `NAME/USER/PASSWORD/HOST/PORT` (лучше через переменные окружения).
3. Выполните миграции заново (`python manage.py migrate`).
## Хитрости и рекомендации
- Используйте `Subscription.create_for_plan` — он правильно считает `end_date` от текущего дня.
- Телефон хранится в нормализованном формате `+7XXXXXXXXXX`; это можно использовать для SMSинтеграций.
- Планы отсортированы по цене (`ordering = ['price']`), поэтому вывод на главной будет от дешёвого к дорогому.
- `entrypoint.sh` не падает, если `collectstatic` без настроек S3; удобнее для локалки, но в проде задайте `STATIC_ROOT` и уберите `|| true` при необходимости.
- Блок футера «Позвоните мне» пока статический — если нужна отправка заявки, повесьте action на форму и добавьте view.
## Полезные пути
- Основной код: `club/views.py`, `club/forms.py`, `club/models.py`
- Стили: `static/css/main.css`
- Шаблоны: `templates/`
- Контейнеризация: `Dockerfile`, `docker-compose.yml`, `entrypoint.sh`

View file

@ -7,6 +7,7 @@
- Frontend: Bootstrap 5 (CDN), шрифт Manrope (Google Fonts), кастомный CSS `static/css/main.css`.
- БД по умолчанию: SQLite (`db.sqlite3`). Настроена смена на Postgres при необходимости.
- Docker: `python:3.13-slim`, entrypoint выполняет миграции и стартует `runserver`.
- Встроенный список клубов (Ростов-на-Дону) доступен во всех шаблонах через context processor.
## Маршруты и HTTPзапросы
Все маршруты определены в `club/urls.py` и подключены через `gymclub/urls.py`.
@ -24,12 +25,15 @@
- Все формы используют CSRFтокен.
- Сообщения пользователю выводятся через Django messages (успех/ошибка).
- API JSON не реализован; взаимодействие сервер‑рендерингом HTML.
- Проверка паролей упрощена: отключена `CommonPasswordValidator` (часто используемые пароли не блокируются).
- Телефон проверяется на реальный российский номер: формат +7XXXXXXXXXX, сохраняется в профиль.
## Модели данных (`club/models.py`)
- `MembershipPlan`: `name`, `description`, `duration_days`, `price`, `created_at`.
- `Profile`: `user` (OneToOne), `phone`, `created_at`.
- `Subscription`: `user`, `plan`, `start_date`, `end_date`, `created_at`, вычисляемый `is_active`.
- Миграция `0002_default_plans` создаёт 3 дефолтных тарифа (1, 3, 12 месяцев).
- Контекстный список клубов в Ростове-на-Дону (5 адресов).
## Формы
- `RegistrationForm` (расширенный `UserCreationForm`): username, имя, фамилия, телефон, email, пароль ×2; автосоздаёт `Profile`.

View file

@ -0,0 +1,14 @@
CLUBS = [
{"name": "Центр на Большой Садовой", "address": "ул. Большая Садовая, 105"},
{"name": "Западный Арена", "address": "пр-т Стачки, 190"},
{"name": "Северный Loft", "address": "пр-т Космонавтов, 14/1"},
{"name": "Левенцовка Energy", "address": "ул. Малиновского, 25Г"},
{"name": "Береговой RiverFit", "address": "Береговая ул., 15к3"},
]
def clubs_info(request):
"""
Делает список клубов доступным во всех шаблонах (шапка/футер/страницы).
"""
return {"clubs": CLUBS}

View file

@ -1,3 +1,5 @@
import re
from django import forms
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
from django.contrib.auth.models import User
@ -35,10 +37,21 @@ class RegistrationForm(BootstrapMixin, UserCreationForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._init_bootstrap()
self.fields["phone"].widget.attrs.setdefault("placeholder", "+7XXXXXXXXXX")
self.fields["phone"].widget.attrs.setdefault("inputmode", "tel")
self.fields["username"].label = "Логин"
for name in ["password1", "password2"]:
self.fields[name].widget.attrs["autocomplete"] = "new-password"
def clean_phone(self):
raw = self.cleaned_data.get("phone", "")
digits = "".join(ch for ch in raw if ch.isdigit())
if digits.startswith("8"):
digits = "7" + digits[1:]
if not digits.startswith("7") or len(digits) != 11:
raise forms.ValidationError("Введите реальный номер в формате +7XXXXXXXXXX")
return f"+{digits}"
def save(self, commit=True):
user = super().save(commit=False)
user.first_name = self.cleaned_data.get("first_name", "")

View file

@ -4,13 +4,14 @@ from django.contrib.auth.decorators import login_required
from django.shortcuts import get_object_or_404, redirect, render
from django.utils import timezone
from .context_processors import CLUBS
from .forms import LoginForm, RegistrationForm
from .models import MembershipPlan, Subscription
def home(request):
plans = MembershipPlan.objects.all()
return render(request, "home.html", {"plans": plans})
return render(request, "home.html", {"plans": plans, "clubs": CLUBS})
def register_view(request):

Binary file not shown.

View file

@ -4,7 +4,7 @@ services:
build: .
command: /app/entrypoint.sh
ports:
- "8000:8000"
- "1337:8000"
environment:
DJANGO_DEBUG: "True"
DJANGO_ALLOWED_HOSTS: "*"

View file

@ -40,6 +40,7 @@ TEMPLATES = [
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
"club.context_processors.clubs_info",
],
},
},
@ -58,7 +59,6 @@ DATABASES = {
AUTH_PASSWORD_VALIDATORS = [
{"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
]

View file

@ -44,8 +44,27 @@ a:hover { color: var(--accent-strong); }
.table > :not(caption) > * > * { background: transparent; color: var(--text); }
.table { --bs-table-border-color: rgba(255,255,255,0.06); }
.form-control, .form-select { background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); color: var(--text); }
.form-control:focus, .form-select:focus { border-color: var(--accent); box-shadow: 0 0 0 0.2rem rgba(124,240,200,0.2); }
.form-control,
.form-select {
background: rgba(255,255,255,0.04);
border: 1px solid rgba(255,255,255,0.08);
color: var(--text);
}
.form-control:focus,
.form-select:focus {
border-color: var(--accent);
box-shadow: 0 0 0 0.2rem rgba(124,240,200,0.2);
}
/* Читаемость выпадашек на всех платформах */
.form-select,
.form-select option {
color: #0c1014;
background-color: #f8fafc;
}
.footer .form-select {
background-color: #ffffff;
color: #0c1014;
}
.alert { border: 1px solid rgba(255,255,255,0.08); }

View file

@ -44,8 +44,27 @@ a:hover { color: var(--accent-strong); }
.table > :not(caption) > * > * { background: transparent; color: var(--text); }
.table { --bs-table-border-color: rgba(255,255,255,0.06); }
.form-control, .form-select { background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); color: var(--text); }
.form-control:focus, .form-select:focus { border-color: var(--accent); box-shadow: 0 0 0 0.2rem rgba(124,240,200,0.2); }
.form-control,
.form-select {
background: rgba(255,255,255,0.04);
border: 1px solid rgba(255,255,255,0.08);
color: var(--text);
}
.form-control:focus,
.form-select:focus {
border-color: var(--accent);
box-shadow: 0 0 0 0.2rem rgba(124,240,200,0.2);
}
/* Читаемость выпадашек на всех платформах */
.form-select,
.form-select option {
color: #0c1014;
background-color: #f8fafc;
}
.footer .form-select {
background-color: #ffffff;
color: #0c1014;
}
.alert { border: 1px solid rgba(255,255,255,0.08); }

View file

@ -73,9 +73,9 @@
<div class="col-md-4">
<select class="form-select form-select-lg" required>
<option value="" selected disabled>Выберите клуб</option>
<option>Центр</option>
<option>Юг</option>
<option>Север</option>
{% for club in clubs %}
<option>{{ club.name }} — {{ club.address }}</option>
{% endfor %}
</select>
</div>
<div class="col-12 text-md-end">

View file

@ -10,6 +10,7 @@
<div class="d-flex flex-wrap gap-3">
<a class="btn btn-accent btn-lg" href="{% url 'register' %}">Зарегистрироваться</a>
<a class="btn btn-outline-light btn-lg" href="#pricing">Выбрать абонемент</a>
<a class="btn btn-light btn-lg" href="#booking">Записаться</a>
</div>
</div>
<div class="col-lg-6">
@ -64,15 +65,99 @@
</div>
</section>
<section class="container py-4 py-lg-5" id="trainers">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold">Тренеры</h2>
<span class="text-secondary small">Подберём специалиста под вашу цель</span>
</div>
<div class="row g-4">
<div class="col-md-4">
<div class="card h-100 glass-card">
<div class="card-body">
<div class="fw-bold h5 mb-1">Алексей Кравцов</div>
<div class="text-secondary small mb-2">Силовой тренинг · 8 лет стажа</div>
<p class="text-secondary small mb-3">Составит программу на набор силы и массы, следит за техникой, специализируется на жиме и приседе.</p>
<a class="btn btn-accent w-100" href="#booking">Записаться</a>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card h-100 glass-card">
<div class="card-body">
<div class="fw-bold h5 mb-1">Мария Иванова</div>
<div class="text-secondary small mb-2">Функционал & кардио · 6 лет стажа</div>
<p class="text-secondary small mb-3">Работает с похудением и общей выносливостью, включает TRX, HIIT и работу с собственным весом.</p>
<a class="btn btn-accent w-100" href="#booking">Записаться</a>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card h-100 glass-card">
<div class="card-body">
<div class="fw-bold h5 mb-1">Игорь Петров</div>
<div class="text-secondary small mb-2">Реабилитация · 10 лет стажа</div>
<p class="text-secondary small mb-3">Помогает восстановиться после травм, аккуратно возвращает к нагрузкам, сотрудничает с врачами.</p>
<a class="btn btn-accent w-100" href="#booking">Записаться</a>
</div>
</div>
</div>
</div>
</section>
<section class="container py-4 py-lg-5" id="locations">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2 class="fw-bold">Клубы в Ростове-на-Дону</h2>
<span class="text-secondary small">Все локации работают 24/7</span>
</div>
<div class="row g-3">
{% for club in clubs %}
<div class="col-md-6 col-lg-4">
<div class="card h-100 glass-card">
<div class="card-body">
<div class="fw-bold mb-1">{{ club.name }}</div>
<div class="text-secondary">{{ club.address }}</div>
<div class="text-secondary small mt-2">Бесплатный первый визит по онлайн-заявке</div>
</div>
</div>
</div>
{% endfor %}
</div>
</section>
<section class="container py-4 py-lg-5" id="booking">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold">Записаться на тренировку</h2>
<span class="text-secondary small">Первое занятие — бесплатно</span>
</div>
<div class="row justify-content-center">
<div class="col-md-7">
<div class="card glass-card">
<div class="card-body p-4">
<p class="text-secondary mb-4">Зарегистрируйтесь или войдите в личный кабинет, чтобы оформить абонемент и начать тренировки.</p>
<div class="d-flex flex-wrap gap-3">
{% if user.is_authenticated %}
<a class="btn btn-accent btn-lg" href="{% url 'dashboard' %}">Перейти в кабинет</a>
{% else %}
<a class="btn btn-accent btn-lg" href="{% url 'register' %}">Зарегистрироваться</a>
<a class="btn btn-outline-light btn-lg" href="{% url 'login' %}">Уже есть аккаунт</a>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</section>
<section class="container py-5" id="about">
<div class="row g-4 align-items-center">
<div class="col-md-6">
<h2 class="fw-bold">Команда тренеров</h2>
<p class="text-secondary">Опытные специалисты по силовым, функциональному тренингу и реабилитации. Подберём программу под вашу цель — похудение, набор массы или поддержка здоровья.</p>
<h2 class="fw-bold">О нас</h2>
<p class="text-secondary">FitnessCenter — сеть клубов в Ростове-на-Дону с круглосуточным доступом, современной силовой зоной, кардио, залом функционального тренинга и комфортными раздевалками.</p>
<ul class="list-unstyled text-secondary">
<li class="mb-2">• Персональные и групповые тренировки</li>
<li class="mb-2">• ДНК-тест и нутрициолог</li>
<li class="mb-2">• Приложение с планом занятий</li>
<li class="mb-2">• 24/7, электронный доступ по QR</li>
<li class="mb-2">• Персоналки, групповые занятия, детская зона</li>
<li class="mb-2">• Душ, полотенца, фитнес-бар с кофе и протеином</li>
<li class="mb-2">• Онлайн-кабинет с абонементами и историей посещений</li>
</ul>
</div>
<div class="col-md-6">