first commit
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
SECRET_KEY=replace-with-a-long-random-string
|
||||||
|
ADMIN_PASSWORD=replace-with-a-private-admin-password
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
.venv/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
.env
|
||||||
|
consultations.db
|
||||||
Generated
+3
@@ -0,0 +1,3 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
Generated
+10
@@ -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
@@ -0,0 +1,6 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<settings>
|
||||||
|
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||||
|
<version value="1.0" />
|
||||||
|
</settings>
|
||||||
|
</component>
|
||||||
Generated
+8
@@ -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
@@ -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>
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
# Анастасия Панкратенкова
|
||||||
|
|
||||||
|
> Персональный сайт преподавателя математики и физики с записью на консультацию по подготовке к ОГЭ и ЕГЭ.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
## О проекте
|
||||||
|
|
||||||
|
Сайт-визитка для Анастасии Викторовны Панкратенковой — преподавателя математики и физики.
|
||||||
|
|
||||||
|
На сайте можно:
|
||||||
|
|
||||||
|
- познакомиться с преподавателем, образованием и опытом;
|
||||||
|
- узнать о подготовке по математике и физике;
|
||||||
|
- понять, как проходят занятия;
|
||||||
|
- оставить заявку на консультацию;
|
||||||
|
- выбрать предмет, экзамен и формат занятий.
|
||||||
|
|
||||||
|
Заявки сохраняются в локальную базу данных 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.
|
||||||
@@ -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)
|
||||||
@@ -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 |
@@ -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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user