This commit is contained in:
2026-04-22 18:19:33 +03:00
commit 9c18591823
8 changed files with 1222 additions and 0 deletions

389
server.py Normal file
View File

@@ -0,0 +1,389 @@
# server.py
import re
import json
from datetime import datetime
from pathlib import Path
from fastapi import FastAPI, Request, Form
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from typing import Optional
# ==================== КОНФИГУРАЦИЯ ====================
HOST = "0.0.0.0"
PORT = 8000
DEBUG = True
BASE_DIR = Path(__file__).parent
TEMPLATES_DIR = BASE_DIR / "templates"
STATIC_DIR = BASE_DIR / "static"
DATA_DIR = BASE_DIR / "data"
# Создаем необходимые папки
TEMPLATES_DIR.mkdir(exist_ok=True)
STATIC_DIR.mkdir(exist_ok=True)
DATA_DIR.mkdir(exist_ok=True)
# Создаем подпапки для статики
(STATIC_DIR / "css").mkdir(exist_ok=True)
(STATIC_DIR / "js").mkdir(exist_ok=True)
# ==================== ИНИЦИАЛИЗАЦИЯ FASTAPI ====================
app = FastAPI(title="БС Патриот - Регистрация на круглый стол", debug=DEBUG)
# Подключаем статические файлы
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
# ==================== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ====================
def save_registration_to_file(data: dict) -> dict:
"""Сохраняет заявку в JSON-файл"""
filepath = DATA_DIR / "registrations.json"
registrations = []
if filepath.exists():
try:
with open(filepath, 'r', encoding='utf-8') as f:
registrations = json.load(f)
except (json.JSONDecodeError, IOError):
registrations = []
new_record = {
"id": len(registrations) + 1,
"created_at": datetime.now().isoformat(),
"fullname": data.get("fullname"),
"email": data.get("email"),
"phone": data.get("phone"),
"company": data.get("company"),
"businessSize": data.get("businessSize", ""),
"message": data.get("message", ""),
"status": "pending"
}
registrations.append(new_record)
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(registrations, f, ensure_ascii=False, indent=2)
return new_record
def get_html_content() -> str:
"""Читает HTML файл или возвращает fallback"""
html_path = TEMPLATES_DIR / "index.html"
if html_path.exists():
with open(html_path, 'r', encoding='utf-8') as f:
return f.read()
else:
return get_fallback_html()
def get_fallback_html() -> str:
"""Fallback HTML если файл шаблона не найден"""
return """<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Круглый стол | БС «Патриот»</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
<style>
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
body {{ font-family: 'Inter', Arial, sans-serif; background: #fefcf5; color: #1a1f2b; line-height: 1.6; }}
.container {{ max-width: 1200px; margin: 0 auto; padding: 40px 20px; text-align: center; }}
h1 {{ color: #c52a1c; margin-bottom: 20px; font-size: 2.5rem; }}
.badge {{ background: rgba(197,42,28,0.12); color: #c52a1c; padding: 6px 18px; border-radius: 40px; display: inline-block; margin-bottom: 20px; }}
.btn {{ display: inline-block; background: #c52a1c; color: white; padding: 12px 30px; text-decoration: none; border-radius: 50px; margin-top: 20px; border: none; cursor: pointer; font-family: inherit; }}
.form-card {{ background: white; max-width: 500px; margin: 40px auto; padding: 30px; border-radius: 20px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); }}
input, select, textarea {{ width: 100%; padding: 12px; margin: 10px 0; border: 1px solid #ddd; border-radius: 8px; font-family: inherit; }}
button {{ background: #c52a1c; color: white; padding: 12px 30px; border: none; border-radius: 50px; cursor: pointer; font-size: 16px; font-family: inherit; width: 100%; }}
.success {{ color: #1f8a4c; background: #e0f2e9; padding: 10px; border-radius: 8px; margin-top: 10px; }}
.error {{ color: #c52a1c; background: #ffe8e5; padding: 10px; border-radius: 8px; margin-top: 10px; }}
.stats {{ display: flex; justify-content: center; gap: 30px; margin: 30px 0; flex-wrap: wrap; }}
.stat {{ text-align: center; }}
.stat h4 {{ font-size: 2rem; color: #c52a1c; }}
footer {{ background: #0a1a2f; color: #b9c7d9; padding: 30px; margin-top: 40px; }}
</style>
</head>
<body>
<div class="container">
<div class="badge"><i class="fas fa-shield-alt"></i> Закрытый клуб предпринимателей</div>
<h1>Круглый стол с бизнес-сообществом <span style="color:#c52a1c;">«Патриот»</span></h1>
<p><i class="far fa-calendar-check"></i> <strong>26 апреля 2026, начало в 11:00</strong></p>
<p><i class="fas fa-lock"></i> Вход по рекомендации</p>
<div class="stats">
<div class="stat"><h4>557+</h4><p>участников</p></div>
<div class="stat"><h4>125+</h4><p>выездов в зону СВО</p></div>
<div class="stat"><h4>30+</h4><p>комитетов</p></div>
</div>
<div class="form-card">
<h2>Регистрация на мероприятие</h2>
<form id="registrationForm">
<input type="text" name="fullname" id="fullname" placeholder="ФИО *" required>
<input type="email" name="email" id="email" placeholder="E-mail *" required>
<input type="tel" name="phone" id="phone" placeholder="Телефон *" required>
<input type="text" name="company" id="company" placeholder="Компания *" required>
<select name="businessSize" id="businessSize">
<option value="">Годовой оборот (опционально)</option>
<option value="до 50 млн руб.">до 50 млн руб.</option>
<option value="50500 млн руб.">50500 млн руб.</option>
<option value="500 млн 1 млрд руб.">500 млн 1 млрд руб.</option>
<option value="более 1 млрд руб.">более 1 млрд руб.</option>
</select>
<textarea name="message" id="message" rows="3" placeholder="Коротко о себе"></textarea>
<label>
<input type="checkbox" name="agree" id="agreeCheck" value="true" required>
Я соглашаюсь с условиями *
</label>
<button type="submit">Отправить заявку</button>
<div id="formFeedback"></div>
</form>
</div>
</div>
<footer>
<p>Бизнес-сообщество «Предприниматели Патриоты»</p>
<p>© 2026 Круглый стол с БС «Патриот»</p>
</footer>
<script>
const form = document.getElementById('registrationForm');
form.addEventListener('submit', async (e) => {{
e.preventDefault();
const feedback = document.getElementById('formFeedback');
const formData = new FormData(form);
const submitBtn = form.querySelector('button[type="submit"]');
const originalText = submitBtn.innerHTML;
submitBtn.disabled = true;
submitBtn.innerHTML = '<i class="fas fa-spinner fa-pulse"></i> Отправка...';
try {{
const response = await fetch('/api/register', {{
method: 'POST',
body: formData
}});
const data = await response.json();
if (data.success) {{
feedback.innerHTML = '<div class="success">✅ ' + data.message + '</div>';
form.reset();
}} else {{
feedback.innerHTML = '<div class="error">❌ ' + data.message + '</div>';
}}
}} catch (err) {{
console.error('Error:', err);
feedback.innerHTML = '<div class="error">❌ Ошибка соединения</div>';
}} finally {{
submitBtn.disabled = false;
submitBtn.innerHTML = originalText;
setTimeout(() => {{ feedback.innerHTML = ''; }}, 5000);
}}
}});
</script>
</body>
</html>"""
# ==================== МАРШРУТЫ ====================
@app.get("/", response_class=HTMLResponse)
async def get_landing_page():
"""Главная страница - посадочный лендинг мероприятия"""
return HTMLResponse(content=get_html_content(), status_code=200)
@app.post("/api/register")
async def register(
fullname: str = Form(...),
email: str = Form(...),
phone: str = Form(...),
company: str = Form(...),
businessSize: Optional[str] = Form(None),
message: Optional[str] = Form(None),
agree: Optional[str] = Form(None)
):
"""API-эндпоинт для регистрации участников"""
print(f"Received data: fullname={fullname}, email={email}, phone={phone}, company={company}, agree={agree}")
# Валидация
if not agree or agree != "true":
return JSONResponse(
status_code=400,
content={"success": False, "message": "Необходимо согласие с условиями"}
)
if not fullname or len(fullname.strip()) < 2:
return JSONResponse(
status_code=400,
content={"success": False, "message": "Введите корректное ФИО"}
)
email_pattern = r'^[^\s@]+@([^\s@.,]+\.)+[^\s@.,]{2,}$'
if not re.match(email_pattern, email):
return JSONResponse(
status_code=400,
content={"success": False, "message": "Введите корректный email"}
)
digits_only = re.sub(r'\D', '', phone)
if len(digits_only) < 10:
return JSONResponse(
status_code=400,
content={"success": False, "message": "Введите корректный номер телефона (минимум 10 цифр)"}
)
if not company or len(company.strip()) < 1:
return JSONResponse(
status_code=400,
content={"success": False, "message": "Укажите название компании"}
)
data = {
"fullname": fullname.strip(),
"email": email.strip().lower(),
"phone": phone.strip(),
"company": company.strip(),
"businessSize": businessSize if businessSize else "",
"message": message.strip() if message else "",
}
try:
saved = save_registration_to_file(data)
print(f"Saved registration: {saved}")
return JSONResponse(content={
"success": True,
"message": "Заявка успешно отправлена! Менеджер сообщества свяжется с вами в ближайшее время."
})
except Exception as e:
print(f"Error saving registration: {e}")
return JSONResponse(
status_code=500,
content={"success": False, "message": f"Ошибка сервера: {str(e)}"}
)
@app.get("/admin/registrations", response_class=HTMLResponse)
async def admin_registrations():
"""Админка для просмотра заявок"""
filepath = DATA_DIR / "registrations.json"
registrations = []
if filepath.exists():
try:
with open(filepath, 'r', encoding='utf-8') as f:
registrations = json.load(f)
except:
registrations = []
registrations.sort(key=lambda x: x.get("created_at", ""), reverse=True)
# Используем обычное форматирование строки без .format()
html = f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Админка | Заявки на круглый стол</title>
<style>
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
body {{ font-family: 'Inter', Arial, sans-serif; margin: 40px; background: #f5f5f5; }}
h1 {{ color: #c52a1c; margin-bottom: 20px; }}
.container {{ max-width: 1400px; margin: 0 auto; background: white; border-radius: 24px; padding: 24px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); }}
table {{ border-collapse: collapse; width: 100%; margin-top: 20px; }}
th, td {{ border: 1px solid #ddd; padding: 12px; text-align: left; vertical-align: top; }}
th {{ background: #c52a1c; color: white; position: sticky; top: 0; }}
tr:nth-child(even) {{ background: #f9f9f9; }}
.status-pending {{ color: #e67e22; font-weight: bold; }}
.count {{ margin: 20px 0; font-size: 1.1rem; }}
.back-link {{ display: inline-block; margin-top: 20px; color: #c52a1c; text-decoration: none; font-weight: 600; }}
.back-link:hover {{ text-decoration: underline; }}
@media (max-width: 768px) {{
body {{ margin: 20px; }}
.container {{ padding: 16px; overflow-x: auto; }}
th, td {{ padding: 8px; font-size: 12px; }}
}}
</style>
</head>
<body>
<div class="container">
<h1>📋 Заявки на круглый стол «Патриот»</h1>
<div class="count">📊 Всего заявок: <strong>{len(registrations)}</strong></div>
<div style="overflow-x: auto;">
<table>
<thead>
<tr>
<th>ID</th>
<th>Дата</th>
<th>ФИО</th>
<th>Email</th>
<th>Телефон</th>
<th>Компания</th>
<th>Оборот</th>
<th>Сообщение</th>
<th>Статус</th>
</tr>
</thead>
<tbody>
"""
for reg in registrations:
created = reg.get("created_at", "")[:16].replace("T", " ")
status_class = "status-pending" if reg.get('status') == 'pending' else ""
message_text = reg.get('message', '')[:100]
if len(reg.get('message', '')) > 100:
message_text += "..."
html += f"""
<tr>
<td>{reg.get('id', '')}</td>
<td>{created}</td>
<td>{reg.get('fullname', '')}</td>
<td>{reg.get('email', '')}</td>
<td>{reg.get('phone', '')}</td>
<td>{reg.get('company', '')}</td>
<td>{reg.get('businessSize', '')}</td>
<td>{message_text}</td>
<td class="{status_class}">{reg.get('status', 'pending')}</td>
</tr>
"""
html += """
</tbody>
</table>
</div>
<a href="/" class="back-link">← Вернуться на главную</a>
</div>
</body>
</html>
"""
return HTMLResponse(content=html, status_code=200)
@app.get("/health")
async def health_check():
"""Health check для хостинга"""
return {"status": "ok", "timestamp": datetime.now().isoformat()}
# ==================== ЗАПУСК ====================
if __name__ == "__main__":
import uvicorn
print("=" * 60)
print("🚀 Сервер для посадочной страницы БС «Патриот»")
print(f"📁 Шаблоны: {TEMPLATES_DIR}")
print(f"📁 Статика: {STATIC_DIR}")
print(f"📁 Данные: {DATA_DIR}")
print("=" * 60)
print(f"✨ Главная страница: http://localhost:{PORT}")
print(f"🔧 Админка: http://localhost:{PORT}/admin/registrations")
print("=" * 60)
print("💡 Для остановки сервера нажмите Ctrl+C")
print("=" * 60)
uvicorn.run(
app,
host=HOST,
port=PORT,
log_level="info"
)