first commit

This commit is contained in:
greyjoy
2026-08-02 22:54:13 +04:00
commit f55735a59e
17 changed files with 2883 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
SECRET_KEY=replace-with-a-long-random-string
ADMIN_PASSWORD=replace-with-a-private-admin-password
+5
View File
@@ -0,0 +1,5 @@
.venv/
__pycache__/
*.pyc
.env
consultations.db
+3
View File
@@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.venv" />
</content>
<orderEntry type="jdk" jdkName="Python 3.10 (anastasia_viktorovna)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
+6
View File
@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/anastasia_viktorovna.iml" filepath="$PROJECT_DIR$/.idea/anastasia_viktorovna.iml" />
</modules>
</component>
</project>
Generated
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
+162
View File
@@ -0,0 +1,162 @@
# Анастасия Панкратенкова
> Персональный сайт преподавателя математики и физики с записью на консультацию по подготовке к ОГЭ и ЕГЭ.
![Статус проекта](https://img.shields.io/badge/status-MVP-e77862?style=flat-square)
![Python](https://img.shields.io/badge/Python-3.10%2B-10211d?style=flat-square&logo=python&logoColor=white)
![Flask](https://img.shields.io/badge/Flask-3.x-10211d?style=flat-square&logo=flask&logoColor=white)
![SQLite](https://img.shields.io/badge/SQLite-local%20database-bdd3ba?style=flat-square&logo=sqlite&logoColor=10211d)
## О проекте
Сайт-визитка для Анастасии Викторовны Панкратенковой — преподавателя математики и физики.
На сайте можно:
- познакомиться с преподавателем, образованием и опытом;
- узнать о подготовке по математике и физике;
- понять, как проходят занятия;
- оставить заявку на консультацию;
- выбрать предмет, экзамен и формат занятий.
Заявки сохраняются в локальную базу данных SQLite. Подключение уведомлений в Telegram или по электронной почте можно добавить следующим этапом.
## Технологии
| Технология | Назначение |
| --- | --- |
| Python 3.10+ | Основной язык приложения |
| Flask 3.x | Веб-сервер и маршрутизация |
| Jinja2 | Рендеринг HTML-шаблонов |
| SQLite | Хранение заявок на консультацию |
| HTML5 | Структура страниц |
| CSS3 | Адаптивный визуальный стиль |
| JavaScript | Небольшие интерактивные эффекты |
## Структура проекта
```text
anastasia_viktorovna/
├── app.py # Flask-приложение и обработка заявок
├── requirements.txt # Зависимости проекта
├── .env.example # Пример переменных окружения
├── templates/
│ ├── index.html # Главная страница
│ ├── success.html # Страница успешной отправки
│ ├── admin_login.html # Вход в админ-панель
│ └── admin.html # Список заявок
├── static/
│ ├── css/styles.css # Стили и адаптивная вёрстка
│ ├── js/main.js # Клиентские эффекты
│ └── images/teacher.jpg # Фотография преподавателя
└── consultations.db # Создаётся автоматически при запуске
```
## Запуск в PyCharm
### 1. Открыть проект
Откройте в PyCharm папку проекта:
```text
C:\Users\greyjoy\PycharmProjects\anastasia_viktorovna
```
### 2. Выбрать интерпретатор
В PyCharm выберите интерпретатор виртуального окружения:
```text
.venv\Scripts\python.exe
```
Если виртуальное окружение ещё не создано, выполните в терминале PyCharm:
```powershell
py -3.10 -m venv .venv
.venv\Scripts\Activate.ps1
```
### 3. Установить зависимости
```powershell
python -m pip install -r requirements.txt
```
### 4. Запустить сайт
```powershell
python app.py
```
После запуска откройте в браузере:
<http://127.0.0.1:5000>
Остановить сервер можно сочетанием `Ctrl+C` в терминале.
## Как работает форма
1. Посетитель заполняет имя, контакт, предмет, экзамен и формат занятий.
2. Flask проверяет обязательные поля.
3. Заявка записывается в таблицу `consultation_requests` файла `consultations.db`.
4. Пользователь видит страницу успешной отправки.
При обновлении существующего проекта недостающие поля базы добавляются автоматически. Старые заявки получают статус «Новая» и значение времени «Гибко».
В текущей версии заявки не отправляются автоматически преподавателю. Для рабочего сайта нужно подключить канал уведомлений и указать контактные данные Анастасии Викторовны.
## Переменные окружения
Для запуска в интернете рекомендуется заменить тестовый секретный ключ Flask:
```powershell
$env:SECRET_KEY = "ваш-длинный-случайный-ключ"
python app.py
```
Файл `.env.example` содержит пример необходимой переменной.
## Админ-панель
Панель доступна по адресу:
<http://127.0.0.1:5000/admin>
По умолчанию для локальной разработки используется пароль `admin123`. Перед публикацией сайта обязательно задайте собственный пароль:
```powershell
$env:ADMIN_PASSWORD = "ваш-приватный-пароль"
python app.py
```
После входа в панели отображаются:
- имя ученика или родителя;
- контакт для обратной связи;
- выбранный предмет;
- экзамен — ОГЭ или ЕГЭ;
- формат занятий;
- желаемое время занятий;
- комментарий из формы;
- дата и номер заявки.
Также в панели доступны:
- сводная статистика по всем заявкам;
- поиск по имени, контакту, комментарию и внутренней заметке;
- фильтрация по статусу и предмету;
- четыре статуса: «Новая», «Связались», «Запланирована», «Завершена»;
- внутренние заметки преподавателя по каждой заявке;
- удаление заявки с подтверждением;
- экспорт текущего списка в CSV для Excel или Google Таблиц.
Ссылка на админ-панель не показывается посетителям сайта. Доступ защищён сессионной авторизацией Flask.
## Дальнейшее развитие
- уведомления о новых заявках в Telegram;
- закрытая страница для просмотра заявок;
- реальные контакты и расписание;
- отзывы учеников и результаты подготовки;
- публикация сайта на хостинге с Python.
+352
View File
@@ -0,0 +1,352 @@
import csv
import io
import os
import sqlite3
from datetime import datetime
from functools import wraps
from hmac import compare_digest
from pathlib import Path
from flask import Flask, flash, make_response, redirect, render_template, request, session, url_for
BASE_DIR = Path(__file__).resolve().parent
DATABASE_PATH = BASE_DIR / "consultations.db"
SUBJECTS = {"math": "Математика", "physics": "Физика"}
EXAMS = {"oge": "ОГЭ", "ege": "ЕГЭ", "other": "Пока не определились"}
FORMATS = {"online": "Онлайн", "offline": "Очно", "other": "Пока не определились"}
PREFERRED_TIMES = {
"flexible": "Гибко",
"morning": "Утро",
"day": "День",
"evening": "Вечер",
}
REQUEST_STATUSES = {
"new": "Новая",
"contacted": "Связались",
"scheduled": "Запланирована",
"closed": "Завершена",
}
app = Flask(__name__)
app.config["SECRET_KEY"] = os.getenv("SECRET_KEY", "change-this-secret-key")
app.config["SESSION_COOKIE_HTTPONLY"] = True
app.config["SESSION_COOKIE_SAMESITE"] = "Lax"
def get_connection():
connection = sqlite3.connect(DATABASE_PATH)
connection.row_factory = sqlite3.Row
return connection
def init_database():
with get_connection() as connection:
connection.execute(
"""
CREATE TABLE IF NOT EXISTS consultation_requests (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
contact TEXT NOT NULL,
subject TEXT NOT NULL,
exam TEXT NOT NULL,
format TEXT NOT NULL,
preferred_time TEXT NOT NULL DEFAULT 'flexible',
message TEXT,
status TEXT NOT NULL DEFAULT 'new',
admin_note TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL,
updated_at TEXT
)
"""
)
existing_columns = {
row["name"]
for row in connection.execute("PRAGMA table_info(consultation_requests)").fetchall()
}
migrations = {
"preferred_time": "TEXT NOT NULL DEFAULT 'flexible'",
"status": "TEXT NOT NULL DEFAULT 'new'",
"admin_note": "TEXT NOT NULL DEFAULT ''",
"updated_at": "TEXT",
}
for column, definition in migrations.items():
if column not in existing_columns:
connection.execute(
f"ALTER TABLE consultation_requests ADD COLUMN {column} {definition}"
)
@app.context_processor
def inject_site_data():
return {"current_year": datetime.now().year}
@app.get("/")
def index():
return render_template("index.html")
@app.post("/submit")
def submit_request():
name = request.form.get("name", "").strip()
contact = request.form.get("contact", "").strip()
subject_key = request.form.get("subject", "").strip()
exam_key = request.form.get("exam", "").strip()
format_key = request.form.get("format", "").strip()
preferred_time_key = request.form.get("preferred_time", "").strip()
message = request.form.get("message", "").strip()[:2000]
if (
not name
or not contact
or subject_key not in SUBJECTS
or exam_key not in EXAMS
or format_key not in FORMATS
or preferred_time_key not in PREFERRED_TIMES
):
flash("Пожалуйста, заполните обязательные поля формы.", "error")
return redirect(url_for("index") + "#contact")
created_at = datetime.now().isoformat(timespec="minutes")
with get_connection() as connection:
connection.execute(
"""
INSERT INTO consultation_requests
(name, contact, subject, exam, format, preferred_time, message, status, admin_note, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
name[:120],
contact[:160],
SUBJECTS[subject_key],
EXAMS[exam_key],
FORMATS[format_key],
PREFERRED_TIMES[preferred_time_key],
message,
"new",
"",
created_at,
created_at,
),
)
return redirect(url_for("success", name=name))
@app.get("/success")
def success():
return render_template("success.html", name=request.args.get("name", ""))
def admin_required(view):
@wraps(view)
def wrapped_view(*args, **kwargs):
if not session.get("is_admin"):
return redirect(url_for("admin_login"))
return view(*args, **kwargs)
return wrapped_view
def get_request_filters():
status = request.args.get("status", "").strip()
subject = request.args.get("subject", "").strip()
search = request.args.get("q", "").strip()
return {
"status": status if status in REQUEST_STATUSES else "",
"subject": subject if subject in SUBJECTS.values() else "",
"q": search[:100],
}
def build_request_query(filters):
conditions = []
parameters = []
if filters["status"]:
conditions.append("status = ?")
parameters.append(filters["status"])
if filters["subject"]:
conditions.append("subject = ?")
parameters.append(filters["subject"])
if filters["q"]:
conditions.append("(name LIKE ? OR contact LIKE ? OR message LIKE ? OR admin_note LIKE ?)")
search_pattern = f"%{filters['q']}%"
parameters.extend([search_pattern] * 4)
where_clause = f"WHERE {' AND '.join(conditions)}" if conditions else ""
return where_clause, parameters
def fetch_requests(connection, filters):
where_clause, parameters = build_request_query(filters)
return connection.execute(
f"""
SELECT id, name, contact, subject, exam, format, preferred_time,
message, status, admin_note, created_at, updated_at
FROM consultation_requests
{where_clause}
ORDER BY id DESC
""",
parameters,
).fetchall()
def fetch_stats(connection):
stats = connection.execute(
"""
SELECT
COUNT(*) AS total,
COALESCE(SUM(CASE WHEN status = 'new' THEN 1 ELSE 0 END), 0) AS new,
COALESCE(SUM(CASE WHEN status = 'contacted' THEN 1 ELSE 0 END), 0) AS contacted,
COALESCE(SUM(CASE WHEN status = 'scheduled' THEN 1 ELSE 0 END), 0) AS scheduled,
COALESCE(SUM(CASE WHEN status = 'closed' THEN 1 ELSE 0 END), 0) AS closed
FROM consultation_requests
"""
).fetchone()
return dict(stats)
@app.route("/admin/login", methods=["GET", "POST"])
def admin_login():
if session.get("is_admin"):
return redirect(url_for("admin_dashboard"))
if request.method == "POST":
password = request.form.get("password", "")
admin_password = os.getenv("ADMIN_PASSWORD", "admin123")
if compare_digest(password, admin_password):
session.clear()
session["is_admin"] = True
return redirect(url_for("admin_dashboard"))
flash("Неверный пароль.", "error")
return render_template("admin_login.html")
@app.get("/admin")
@admin_required
def admin_dashboard():
filters = get_request_filters()
with get_connection() as connection:
consultation_requests = fetch_requests(connection, filters)
stats = fetch_stats(connection)
return render_template(
"admin.html",
consultation_requests=consultation_requests,
filters=filters,
stats=stats,
statuses=REQUEST_STATUSES,
subjects=SUBJECTS.values(),
)
@app.post("/admin/request/<int:request_id>/status")
@admin_required
def admin_update_status(request_id):
status = request.form.get("status", "").strip()
if status not in REQUEST_STATUSES:
flash("Неизвестный статус заявки.", "error")
return redirect(url_for("admin_dashboard"))
with get_connection() as connection:
connection.execute(
"UPDATE consultation_requests SET status = ?, updated_at = ? WHERE id = ?",
(status, datetime.now().isoformat(timespec="minutes"), request_id),
)
flash("Статус заявки обновлён.", "success")
return redirect(url_for("admin_dashboard"))
@app.post("/admin/request/<int:request_id>/note")
@admin_required
def admin_update_note(request_id):
admin_note = request.form.get("admin_note", "").strip()[:2000]
with get_connection() as connection:
connection.execute(
"UPDATE consultation_requests SET admin_note = ?, updated_at = ? WHERE id = ?",
(admin_note, datetime.now().isoformat(timespec="minutes"), request_id),
)
flash("Заметка сохранена.", "success")
return redirect(url_for("admin_dashboard"))
@app.post("/admin/request/<int:request_id>/delete")
@admin_required
def admin_delete_request(request_id):
with get_connection() as connection:
connection.execute("DELETE FROM consultation_requests WHERE id = ?", (request_id,))
flash("Заявка удалена.", "success")
return redirect(url_for("admin_dashboard"))
@app.get("/admin/export.csv")
@admin_required
def admin_export():
filters = get_request_filters()
with get_connection() as connection:
consultation_requests = fetch_requests(connection, filters)
output = io.StringIO()
writer = csv.writer(output)
writer.writerow(
[
"Номер",
"Имя",
"Контакт",
"Предмет",
"Экзамен",
"Формат",
"Желаемое время",
"Статус",
"Комментарий",
"Заметка преподавателя",
"Создана",
"Обновлена",
]
)
for consultation in consultation_requests:
writer.writerow(
[
consultation["id"],
consultation["name"],
consultation["contact"],
consultation["subject"],
consultation["exam"],
consultation["format"],
consultation["preferred_time"],
REQUEST_STATUSES.get(consultation["status"], consultation["status"]),
consultation["message"] or "",
consultation["admin_note"] or "",
consultation["created_at"],
consultation["updated_at"] or "",
]
)
response = make_response("\ufeff" + output.getvalue())
response.headers["Content-Type"] = "text/csv; charset=utf-8"
response.headers["Content-Disposition"] = (
f"attachment; filename=consultations-{datetime.now().strftime('%Y-%m-%d')}.csv"
)
return response
@app.get("/admin/logout")
def admin_logout():
session.clear()
return redirect(url_for("admin_login"))
init_database()
if __name__ == "__main__":
app.run(debug=True)
+1
View File
@@ -0,0 +1 @@
Flask>=3.0,<4.0
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

+20
View File
@@ -0,0 +1,20 @@
document.addEventListener("DOMContentLoaded", () => {
const header = document.querySelector(".site-header");
if (header) {
const updateHeader = () => {
header.classList.toggle("is-scrolled", window.scrollY > 24);
};
updateHeader();
window.addEventListener("scroll", updateHeader, { passive: true });
}
document.querySelectorAll("form[data-confirm]").forEach((form) => {
form.addEventListener("submit", (event) => {
if (!window.confirm(form.dataset.confirm)) {
event.preventDefault();
}
});
});
});
+114
View File
@@ -0,0 +1,114 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Административная панель заявок Анастасии Панкратенковой.">
<title>Заявки — Анастасия Панкратенкова</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
</head>
<body class="admin-page admin-dashboard-page">
<div class="admin-shell">
<header class="admin-header">
<a class="brand" href="{{ url_for('index') }}">
<span class="brand-mark">АП</span>
<span class="brand-name">Анастасия<br>Панкратенкова</span>
</a>
<div class="admin-header-actions">
<span class="admin-status"><i></i> Вы вошли как администратор</span>
<a class="admin-logout" href="{{ url_for('admin_logout') }}">Выйти <span></span></a>
</div>
</header>
<main class="admin-main">
<div class="admin-title-row">
<div>
<p class="eyebrow"><span class="eyebrow-dot"></span> Панель преподавателя</p>
<h1>Ваши <em>заявки.</em></h1>
</div>
<div class="requests-count"><strong>{{ stats["total"] }}</strong><span>обращений<br>всего</span></div>
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="admin-flashes">
{% for category, message in messages %}
<div class="flash-message {{ category }}">{{ message }}</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
<section class="admin-stats" aria-label="Статистика заявок">
<div class="stat-card stat-card-total"><span>Всего</span><strong>{{ stats["total"] }}</strong><small>за всё время</small></div>
<div class="stat-card"><span>Новые</span><strong>{{ stats["new"] }}</strong><small>ждут ответа</small></div>
<div class="stat-card"><span>В работе</span><strong>{{ stats["contacted"] }}</strong><small>связались</small></div>
<div class="stat-card"><span>Запланированы</span><strong>{{ stats["scheduled"] }}</strong><small>занятия</small></div>
<div class="stat-card"><span>Завершены</span><strong>{{ stats["closed"] }}</strong><small>архив</small></div>
</section>
<section class="admin-toolbar" aria-label="Фильтры заявок">
<form class="admin-filters" action="{{ url_for('admin_dashboard') }}" method="get">
<label class="filter-search"><span>Поиск</span><input type="search" name="q" value="{{ filters["q"] }}" placeholder="Имя, контакт или комментарий"></label>
<label><span>Статус</span><select name="status"><option value="">Все статусы</option>{% for status_key, status_label in statuses.items() %}<option value="{{ status_key }}" {% if filters["status"] == status_key %}selected{% endif %}>{{ status_label }}</option>{% endfor %}</select></label>
<label><span>Предмет</span><select name="subject"><option value="">Все предметы</option>{% for subject in subjects %}<option value="{{ subject }}" {% if filters["subject"] == subject %}selected{% endif %}>{{ subject }}</option>{% endfor %}</select></label>
<button class="button button-primary filter-button" type="submit">Применить <span></span></button>
</form>
<div class="toolbar-links">
{% if filters["q"] or filters["status"] or filters["subject"] %}<a class="reset-filters" href="{{ url_for('admin_dashboard') }}">Сбросить фильтры</a>{% endif %}
<a class="export-link" href="{{ url_for('admin_export', q=filters["q"], status=filters["status"], subject=filters["subject"]) }}">Скачать CSV <span></span></a>
</div>
</section>
{% if consultation_requests %}
<div class="results-heading"><span>Показано заявок: <strong>{{ consultation_requests|length }}</strong></span><span>Новые сверху</span></div>
<section class="requests-list" aria-label="Список заявок">
{% for consultation in consultation_requests %}
<article class="request-card status-border-{{ consultation["status"] }}">
<div class="request-card-head">
<div class="request-card-head-left"><span class="status-badge status-{{ consultation["status"] }}">{{ statuses.get(consultation["status"], consultation["status"]) }}</span><span class="request-id">Заявка №{{ consultation["id"] }}</span></div>
<time datetime="{{ consultation["created_at"] }}">{{ consultation["created_at"].replace("T", " · ") }}</time>
</div>
<div class="request-card-main">
<div class="request-person"><h2>{{ consultation["name"] }}</h2><span>{{ consultation["contact"] }}</span></div>
<dl class="request-meta">
<div><dt>Предмет</dt><dd>{{ consultation["subject"] }}</dd></div>
<div><dt>Экзамен</dt><dd>{{ consultation["exam"] }}</dd></div>
<div><dt>Формат</dt><dd>{{ consultation["format"] }}</dd></div>
<div><dt>Время</dt><dd>{{ consultation["preferred_time"] }}</dd></div>
</dl>
</div>
{% if consultation["message"] %}<p class="request-message"><span>Комментарий</span>{{ consultation["message"] }}</p>{% endif %}
<div class="request-actions">
<form class="request-status-form" action="{{ url_for('admin_update_status', request_id=consultation["id"]) }}" method="post">
<label for="status-{{ consultation["id"] }}">Статус</label>
<select id="status-{{ consultation["id"] }}" name="status">{% for status_key, status_label in statuses.items() %}<option value="{{ status_key }}" {% if consultation["status"] == status_key %}selected{% endif %}>{{ status_label }}</option>{% endfor %}</select>
<button class="mini-button" type="submit">Сохранить</button>
</form>
<details class="request-note" {% if consultation["admin_note"] %}open{% endif %}>
<summary>{% if consultation["admin_note"] %}Изменить заметку{% else %}Добавить заметку{% endif %} <span></span></summary>
<form action="{{ url_for('admin_update_note', request_id=consultation["id"]) }}" method="post">
<textarea name="admin_note" rows="2" maxlength="2000" placeholder="Например: перезвонить вечером...">{{ consultation["admin_note"] }}</textarea>
<button class="mini-button" type="submit">Сохранить заметку</button>
</form>
</details>
<form class="request-delete-form" action="{{ url_for('admin_delete_request', request_id=consultation["id"]) }}" method="post" data-confirm="Удалить эту заявку? Восстановить её будет нельзя.">
<button class="delete-button" type="submit">Удалить</button>
</form>
</div>
</article>
{% endfor %}
</section>
{% else %}
<section class="empty-requests">
<span></span>
<h2>{% if filters["q"] or filters["status"] or filters["subject"] %}Ничего не найдено.{% else %}Пока заявок нет.{% endif %}</h2>
<p>{% if filters["q"] or filters["status"] or filters["subject"] %}Попробуйте изменить условия поиска или сбросить фильтры.{% else %}Новые обращения появятся здесь после заполнения формы на сайте.{% endif %}</p>
{% if filters["q"] or filters["status"] or filters["subject"] %}<a class="button button-primary" href="{{ url_for('admin_dashboard') }}">Сбросить фильтры <span></span></a>{% else %}<a class="button button-primary" href="{{ url_for('index') }}#contact">Посмотреть форму <span></span></a>{% endif %}
</section>
{% endif %}
</main>
</div>
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
</body>
</html>
+35
View File
@@ -0,0 +1,35 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Вход для преподавателя — Анастасия Панкратенкова</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
</head>
<body class="admin-page">
<main class="admin-login-card">
<a class="brand admin-brand" href="{{ url_for('index') }}">
<span class="brand-mark">АП</span>
<span class="brand-name">Анастасия<br>Панкратенкова</span>
</a>
<div class="admin-login-heading">
<p class="eyebrow"><span class="eyebrow-dot"></span> Приватный раздел</p>
<h1>Заявки<br><em>на консультацию.</em></h1>
<p>Введите пароль, чтобы посмотреть обращения учеников.</p>
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="flash-message {{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<form action="{{ url_for('admin_login') }}" method="post" class="admin-login-form">
<label for="password">Пароль администратора</label>
<input id="password" type="password" name="password" placeholder="Введите пароль" autocomplete="current-password" required autofocus>
<button class="button button-primary form-button" type="submit">Войти в панель <span></span></button>
</form>
<a class="admin-back-link" href="{{ url_for('index') }}">← Вернуться на сайт</a>
</main>
</body>
</html>
+163
View File
@@ -0,0 +1,163 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Подготовка к ОГЭ и ЕГЭ по математике и физике с Анастасией Панкратенковой.">
<title>Анастасия Панкратенкова — математика и физика</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
</head>
<body>
<div class="page-shell">
<header class="site-header">
<a class="brand" href="#top" aria-label="На главную">
<span class="brand-mark">АП</span>
<span class="brand-name">Анастасия<br>Панкратенкова</span>
</a>
<nav class="main-nav" aria-label="Основная навигация">
<a href="#about">О преподавателе</a>
<a href="#subjects">Направления</a>
<a href="#contact">Записаться</a>
</nav>
<a class="header-link" href="#contact">Консультация <span></span></a>
</header>
<main id="top">
<section class="hero section-wrap">
<div class="hero-copy">
<p class="eyebrow"><span class="eyebrow-dot"></span> Математика · физика · подготовка к экзаменам</p>
<h1>Понимать, решать, <em>быть уверенным.</em></h1>
<p class="hero-lead">Помогаю школьникам увидеть логику точных наук и спокойно прийти к нужному результату на ОГЭ и ЕГЭ.</p>
<div class="hero-actions">
<a class="button button-primary" href="#contact">Записаться на консультацию <span></span></a>
<a class="text-link" href="#about">Познакомиться <span></span></a>
</div>
<div class="hero-proof">
<div class="proof-avatars" aria-hidden="true">
<span>О</span><span>Е</span><span>+</span>
</div>
<p>Индивидуальный подход<br><strong>с 2017 года</strong></p>
</div>
</div>
<div class="hero-visual">
<div class="hero-orbit orbit-one"></div>
<div class="hero-orbit orbit-two"></div>
<div class="hero-photo-frame">
<img src="{{ url_for('static', filename='images/teacher.jpg') }}" alt="Анастасия Панкратенкова">
</div>
<div class="floating-note note-top"><span></span><strong>Разбираем<br>до ясности</strong></div>
<div class="floating-note note-bottom"><strong>2</strong><span>красных<br>диплома</span></div>
<div class="math-scribble scribble-one">y = kx + b</div>
<div class="math-scribble scribble-two">F = ma</div>
</div>
</section>
<section class="ticker" aria-label="Ключевые направления">
<div class="ticker-track">
<span>Математика</span><i></i><span>Физика</span><i></i><span>ОГЭ</span><i></i><span>ЕГЭ</span><i></i><span>Математика</span><i></i><span>Физика</span>
</div>
</section>
<section class="about section-wrap section-grid" id="about">
<div class="section-label"><span>01</span><span>О преподавателе</span></div>
<div class="about-content">
<div class="section-heading-row">
<h2>Точные науки<br><em>могут быть понятными.</em></h2>
<span class="round-arrow"></span>
</div>
<p class="section-intro">Я — Анастасия Викторовна, преподаватель математики и физики. На занятиях мы не заучиваем решения, а разбираемся, как всё устроено — шаг за шагом и в удобном для ученика темпе.</p>
<div class="about-details">
<div class="detail-item"><span class="detail-number">01</span><p><strong>Образование</strong>АГУ, бакалавриат «Математика» и магистратура «Математическое образование».</p></div>
<div class="detail-item"><span class="detail-number">02</span><p><strong>Результат</strong>Обе ступени образования окончены с красным дипломом.</p></div>
<div class="detail-item"><span class="detail-number">03</span><p><strong>Опыт</strong>Официально веду педагогическую деятельность с 2017 года.</p></div>
</div>
</div>
</section>
<section class="subjects section-wrap" id="subjects">
<div class="section-label"><span>02</span><span>Направления</span></div>
<div class="subjects-heading">
<h2>Выберите свою<br><em>точку старта.</em></h2>
<p>Первая консультация помогает определить текущий уровень, цели и комфортный план подготовки.</p>
</div>
<div class="subject-cards">
<article class="subject-card subject-card-dark">
<div class="card-top"><span class="card-number">01</span><span class="card-icon"></span></div>
<div><p class="card-kicker">Подготовка</p><h3>Математика</h3><p class="card-description">От уверенной базы до сложных задач второй части. Разбираем теорию, практику и стратегию экзамена.</p></div>
<a href="#contact" class="card-link">Обсудить подготовку <span></span></a>
</article>
<article class="subject-card subject-card-accent">
<div class="card-top"><span class="card-number">02</span><span class="card-icon">F</span></div>
<div><p class="card-kicker">Подготовка</p><h3>Физика</h3><p class="card-description">Учимся видеть физику за формулами: решаем задачи, выстраиваем связи и готовимся к нужному баллу.</p></div>
<a href="#contact" class="card-link">Обсудить подготовку <span></span></a>
</article>
</div>
</section>
<section class="process section-wrap">
<div class="section-label"><span>03</span><span>Как проходят занятия</span></div>
<div class="process-content">
<h2>Спокойная подготовка<br><em>с понятным маршрутом.</em></h2>
<div class="process-list">
<div class="process-item"><span>01</span><div><h3>Знакомимся с задачей</h3><p>Определяем цель, уровень знаний и точки, которые требуют внимания.</p></div><b></b></div>
<div class="process-item"><span>02</span><div><h3>Собираем систему</h3><p>Выстраиваем понятную теорию и закрепляем её практикой на реальных заданиях.</p></div><b></b></div>
<div class="process-item"><span>03</span><div><h3>Двигаемся к результату</h3><p>Отслеживаем прогресс, разбираем ошибки и учимся уверенно работать на экзамене.</p></div><b></b></div>
</div>
</div>
</section>
<section class="education section-wrap">
<div class="education-card">
<div class="education-mark"></div>
<div><p class="card-kicker">Образование</p><h2>Два диплома.<br><em>Одна цель.</em></h2></div>
<div class="education-years"><span>2013</span><i></i><span>2019</span></div>
<div class="education-copy"><p>АГУ · бакалавриат<br>«Математика»</p><p>АГУ · магистратура<br>«Математическое образование»</p></div>
<div class="red-diploma">Красный диплом <span>× 2</span></div>
</div>
</section>
<section class="contact section-wrap" id="contact">
<div class="contact-intro">
<div class="section-label"><span>04</span><span>Первый шаг</span></div>
<h2>Давайте<br><em>познакомимся.</em></h2>
<p>Оставьте заявку — я свяжусь с вами, отвечу на вопросы и помогу выбрать формат подготовки.</p>
<div class="contact-sign">Анастасия <span>Панкратенкова</span></div>
</div>
<div class="form-card">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="flash-message {{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<form action="{{ url_for('submit_request') }}" method="post">
<div class="form-row">
<label>Ваше имя <span>*</span><input type="text" name="name" placeholder="Например, Мария" required></label>
<label>Как связаться <span>*</span><input type="text" name="contact" placeholder="Телефон или Telegram" required></label>
</div>
<div class="form-row">
<label>Предмет <span>*</span><select name="subject" required><option value="" disabled selected>Выберите предмет</option><option value="math">Математика</option><option value="physics">Физика</option></select></label>
<label>Экзамен <span>*</span><select name="exam" required><option value="" disabled selected>Выберите экзамен</option><option value="oge">ОГЭ</option><option value="ege">ЕГЭ</option><option value="other">Пока не определились</option></select></label>
</div>
<div class="form-row form-row-wide">
<label>Желаемое время занятий <span>*</span><select name="preferred_time" required><option value="" disabled selected>Когда вам удобнее?</option><option value="morning">Утро</option><option value="day">День</option><option value="evening">Вечер</option><option value="flexible">Готовы обсудить</option></select></label>
</div>
<fieldset class="format-field"><legend>Формат занятий <span>*</span></legend><div class="radio-options"><label><input type="radio" name="format" value="online" required><span>Онлайн</span></label><label><input type="radio" name="format" value="offline"><span>Очно</span></label><label><input type="radio" name="format" value="other"><span>Не знаю</span></label></div></fieldset>
<label>Комментарий <span class="optional">необязательно</span><textarea name="message" rows="3" placeholder="Расскажите, какая цель у занятий..."></textarea></label>
<button class="button button-primary form-button" type="submit">Отправить заявку <span></span></button>
<p class="form-note">Нажимая кнопку, вы соглашаетесь на обработку персональных данных.</p>
</form>
</div>
</section>
</main>
<footer class="site-footer section-wrap">
<a class="brand" href="#top"><span class="brand-mark">АП</span><span class="brand-name">Анастасия<br>Панкратенкова</span></a>
<p>Математика и физика<br>с пониманием.</p>
<span>© {{ current_year }}</span>
</footer>
</div>
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
</body>
</html>
+18
View File
@@ -0,0 +1,18 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Заявка отправлена — Анастасия Панкратенкова</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
</head>
<body class="success-page">
<main class="success-card">
<span class="success-symbol"></span>
<p class="eyebrow"><span class="eyebrow-dot"></span> Заявка отправлена</p>
<h1>Спасибо, {{ name or "что написали" }}!</h1>
<p>Анастасия свяжется с вами в ближайшее время, чтобы обсудить цель, формат и удобное время занятий.</p>
<a class="button button-primary" href="{{ url_for('index') }}">Вернуться на сайт <span></span></a>
</main>
</body>
</html>