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

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
.idea
.venv
data

16
main.py Normal file
View File

@@ -0,0 +1,16 @@
# This is a sample Python script.
# Press Shift+F10 to execute it or replace it with your code.
# Press Double Shift to search everywhere for classes, files, tool windows, actions, and settings.
def print_hi(name):
# Use a breakpoint in the code line below to debug your script.
print(f'Hi, {name}') # Press Ctrl+F8 to toggle the breakpoint.
# Press the green button in the gutter to run the script.
if __name__ == '__main__':
print_hi('PyCharm')
# See PyCharm help at https://www.jetbrains.com/help/pycharm/

4
requirements.txt Normal file
View File

@@ -0,0 +1,4 @@
fastapi==0.104.1
uvicorn==0.24.0
python-multipart==0.0.6
jinja2==3.1.2

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"
)

108
server_simple.py Normal file
View File

@@ -0,0 +1,108 @@
# server_simple.py - упрощенная версия без Jinja2
import re
import json
from datetime import datetime
from pathlib import Path
from fastapi import FastAPI, Form
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
# ==================== КОНФИГУРАЦИЯ ====================
HOST = "0.0.0.0"
PORT = 8000
BASE_DIR = Path(__file__).parent
STATIC_DIR = BASE_DIR / "static"
DATA_DIR = BASE_DIR / "data"
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)
app = FastAPI()
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
# Читаем HTML файл
HTML_FILE = BASE_DIR / "templates" / "index.html"
def save_registration_to_file(data: dict) -> dict:
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 = []
new_record = {
"id": len(registrations) + 1,
"created_at": datetime.now().isoformat(),
**data,
"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
@app.get("/", response_class=HTMLResponse)
async def get_landing_page():
if HTML_FILE.exists():
with open(HTML_FILE, 'r', encoding='utf-8') as f:
return HTMLResponse(content=f.read())
else:
return HTMLResponse(content="<h1>Файл templates/index.html не найден</h1>")
@app.post("/api/register")
async def register(
fullname: str = Form(...),
email: str = Form(...),
phone: str = Form(...),
company: str = Form(...),
businessSize: str = Form(""),
message: str = Form(""),
agree: bool = Form(False)
):
if not agree:
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,
"message": message.strip(),
}
try:
save_registration_to_file(data)
return JSONResponse(content={"success": True, "message": "Заявка отправлена!"})
except Exception as e:
return JSONResponse(status_code=500, content={"success": False, "message": str(e)})
@app.get("/admin/registrations", response_class=HTMLResponse)
async def admin_registrations():
filepath = DATA_DIR / "registrations.json"
registrations = []
if filepath.exists():
with open(filepath, 'r', encoding='utf-8') as f:
registrations = json.load(f)
html = "<h1>Заявки</h1><table border='1'>"
for r in registrations:
html += f"<tr><td>{r.get('fullname')}</td><td>{r.get('email')}</td></tr>"
html += "</table><a href='/'>Назад</a>"
return HTMLResponse(content=html)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host=HOST, port=PORT)

444
static/css/style.css Normal file
View File

@@ -0,0 +1,444 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', sans-serif;
background-color: #fefcf5;
color: #1a1f2b;
line-height: 1.4;
scroll-behavior: smooth;
}
.container {
max-width: 1280px;
margin: 0 auto;
padding: 0 24px;
}
.section {
padding: 80px 0;
}
.section-sm {
padding: 60px 0;
}
.bg-light {
background-color: #ffffff;
}
.bg-gentle {
background-color: #f8f6f0;
}
.btn {
display: inline-flex;
align-items: center;
gap: 12px;
padding: 14px 32px;
border-radius: 60px;
font-weight: 600;
font-size: 1rem;
text-decoration: none;
transition: all 0.25s ease;
cursor: pointer;
border: none;
background: none;
font-family: inherit;
}
.btn-primary {
background: #c52a1c;
color: white;
box-shadow: 0 8px 20px rgba(197, 42, 28, 0.25);
}
.btn-primary:hover {
background: #9e2015;
transform: translateY(-3px);
box-shadow: 0 14px 28px rgba(197, 42, 28, 0.35);
}
.btn-outline {
border: 2px solid #c52a1c;
color: #c52a1c;
background: transparent;
}
.btn-outline:hover {
background: #c52a1c;
color: white;
transform: translateY(-2px);
}
h1, h2, h3 {
font-weight: 700;
letter-spacing: -0.02em;
}
h1 {
font-size: 3.2rem;
line-height: 1.2;
}
h2 {
font-size: 2.4rem;
margin-bottom: 1.5rem;
}
.badge {
background: rgba(197, 42, 28, 0.12);
color: #c52a1c;
padding: 6px 18px;
border-radius: 40px;
font-size: 0.85rem;
font-weight: 600;
display: inline-block;
margin-bottom: 20px;
}
.navbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 0;
flex-wrap: wrap;
}
.logo {
font-weight: 800;
font-size: 1.6rem;
letter-spacing: -0.02em;
}
.logo span {
color: #c52a1c;
}
.nav-links {
display: flex;
gap: 32px;
align-items: center;
}
.nav-links a {
text-decoration: none;
color: #1a1f2b;
font-weight: 500;
transition: color 0.2s;
}
.nav-links a:hover {
color: #c52a1c;
}
.hero {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 48px;
}
.hero-content {
flex: 1.2;
}
.hero-description {
font-size: 1.2rem;
margin: 24px 0 20px;
color: #2e3a48;
line-height: 1.4;
}
.hero-buttons {
display: flex;
gap: 20px;
flex-wrap: wrap;
}
.hero-info {
margin-top: 32px;
display: flex;
gap: 24px;
flex-wrap: wrap;
}
.hero-stats {
flex: 0.9;
background: white;
padding: 32px;
border-radius: 32px;
box-shadow: 0 20px 35px -12px rgba(0,0,0,0.08);
border: 1px solid #edeae2;
}
.stats-title {
font-weight: 600;
font-size: 1rem;
border-bottom: 2px solid #c52a1c;
display: inline-block;
margin-bottom: 16px;
}
.stat-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
margin-top: 20px;
}
.stat-item h4 {
font-size: 2rem;
font-weight: 800;
color: #c52a1c;
}
.stats-note {
font-size: 0.8rem;
margin-top: 20px;
color: #5e6f8d;
}
.about-grid {
display: flex;
flex-wrap: wrap;
gap: 40px;
justify-content: space-between;
}
.about-content {
flex: 1.2;
}
.about-content p {
font-size: 1.05rem;
line-height: 1.5;
margin-bottom: 24px;
}
.values-list {
display: flex;
gap: 16px;
flex-wrap: wrap;
}
.values-list span {
background: #f4f2ec;
padding: 6px 14px;
border-radius: 40px;
}
.about-quote {
flex: 0.8;
background: #fefaf2;
border-radius: 32px;
padding: 28px;
}
.about-quote p {
font-style: italic;
margin: 12px 0;
}
.quote-author {
font-weight: 600;
margin-top: 16px;
}
.section-header {
text-align: center;
max-width: 700px;
margin: 0 auto 40px;
}
.cards-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 32px;
margin-top: 32px;
}
.card {
background: white;
padding: 28px;
border-radius: 28px;
box-shadow: 0 8px 24px rgba(0,0,0,0.03);
transition: transform 0.2s, box-shadow 0.2s;
border: 1px solid #efebe3;
}
.card:hover {
transform: translateY(-6px);
box-shadow: 0 20px 30px -12px rgba(0,0,0,0.1);
}
.card i {
font-size: 2.2rem;
color: #c52a1c;
margin-bottom: 20px;
}
.agenda-list {
background: white;
border-radius: 32px;
overflow: hidden;
border: 1px solid #e9e5db;
}
.agenda-item {
display: flex;
padding: 20px 28px;
border-bottom: 1px solid #efebe3;
align-items: flex-start;
gap: 20px;
flex-wrap: wrap;
}
.agenda-time {
font-weight: 700;
min-width: 100px;
color: #c52a1c;
}
.stats-wrapper {
display: flex;
flex-wrap: wrap;
gap: 40px;
align-items: center;
}
.stats-info {
flex: 1;
}
.stats-info ul {
margin-top: 20px;
list-style: none;
}
.stats-info li {
margin-bottom: 12px;
}
.stats-cta {
flex: 1;
background: #ffffff;
border-radius: 32px;
padding: 32px;
text-align: center;
box-shadow: 0 10px 25px rgba(0,0,0,0.02);
}
.stats-cta i {
font-size: 3rem;
color: #c52a1c;
margin-bottom: 16px;
display: inline-block;
}
.form-card {
background: white;
border-radius: 36px;
padding: 40px;
box-shadow: 0 20px 40px rgba(0,0,0,0.05);
border: 1px solid #eae5db;
max-width: 700px;
margin: 0 auto;
}
.input-group {
margin-bottom: 20px;
}
input, select, textarea {
width: 100%;
padding: 14px 20px;
border-radius: 60px;
border: 1.5px solid #e2dcd0;
font-family: 'Inter', sans-serif;
font-size: 1rem;
transition: 0.2s;
background: #fff;
}
input:focus, select:focus, textarea:focus {
outline: none;
border-color: #c52a1c;
box-shadow: 0 0 0 3px rgba(197,42,28,0.1);
}
textarea {
border-radius: 28px;
resize: vertical;
}
.submit-btn {
width: 100%;
justify-content: center;
}
.form-message {
margin-top: 16px;
font-size: 0.9rem;
font-weight: 500;
}
.success-msg {
color: #1f8a4c;
background: #e0f2e9;
padding: 12px;
border-radius: 60px;
text-align: center;
}
.error-msg {
color: #c52a1c;
background: #ffe8e5;
padding: 12px;
border-radius: 60px;
text-align: center;
}
.form-note {
font-size: 0.75rem;
color: #7a7c7f;
margin-top: 20px;
text-align: center;
}
footer {
background: #0a1a2f;
color: #b9c7d9;
padding: 48px 0;
text-align: center;
}
.footer-logo {
font-weight: 800;
font-size: 1.4rem;
margin-bottom: 24px;
}
footer a {
color: #c9a87c;
text-decoration: none;
}
@media (max-width: 900px) {
h1 { font-size: 2.4rem; }
h2 { font-size: 1.9rem; }
.hero { flex-direction: column; }
.navbar { flex-direction: column; gap: 16px; }
.nav-links { gap: 20px; flex-wrap: wrap; justify-content: center; }
.section { padding: 50px 0; }
.form-card { padding: 28px; }
}
@media (max-width: 550px) {
.container { padding: 0 20px; }
.btn { padding: 10px 22px; }
}

63
static/js/main.js Normal file
View File

@@ -0,0 +1,63 @@
// Обработка формы регистрации
const form = document.getElementById('registrationForm');
const feedbackDiv = document.getElementById('formFeedback');
if (form) {
form.addEventListener('submit', async function(e) {
e.preventDefault();
if (feedbackDiv) {
feedbackDiv.innerHTML = '';
feedbackDiv.className = 'form-message';
}
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 (feedbackDiv) {
if (data.success) {
feedbackDiv.innerHTML = '<div class="success-msg">✅ ' + data.message + '</div>';
form.reset();
} else {
feedbackDiv.innerHTML = '<div class="error-msg">❌ ' + data.message + '</div>';
}
}
} catch (err) {
console.error('Error:', err);
if (feedbackDiv) {
feedbackDiv.innerHTML = '<div class="error-msg">❌ Ошибка соединения. Попробуйте позже.</div>';
}
} finally {
submitBtn.disabled = false;
submitBtn.innerHTML = originalText;
setTimeout(() => {
if (feedbackDiv) feedbackDiv.innerHTML = '';
}, 5000);
}
});
}
// Плавная прокрутка для якорных ссылок
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function(e) {
const hash = this.getAttribute('href');
if (hash === '#') return;
const target = document.querySelector(hash);
if (target) {
e.preventDefault();
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
});
});

195
templates/index.html Normal file
View File

@@ -0,0 +1,195 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>Круглый стол | БС «Патриот» | Бизнес-сообщество предпринимателей</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,300;14..32,400;14..32,500;14..32,600;14..32,700;14..32,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">
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<header>
<div class="container">
<div class="navbar">
<div class="logo">ПАТРИОТ<span>•БИЗНЕС</span></div>
<div class="nav-links">
<a href="#about">О формате</a>
<a href="#agenda">Программа</a>
<a href="#stats-block">Цифры</a>
<a href="#register">Регистрация</a>
</div>
</div>
</div>
</header>
<main>
<section class="section">
<div class="container">
<div class="hero">
<div class="hero-content">
<div class="badge"><i class="fas fa-shield-alt"></i> Закрытый клуб предпринимателей</div>
<h1>Круглый стол <br> с бизнес‑сообществом <span style="color: #c52a1c;">«Патриот»</span></h1>
<p class="hero-description">Объединяем лидеров, которые строят будущее России. Живой диалог, реальные кейсы и партнёрство без формальностей.</p>
<div class="hero-buttons">
<a href="#register" class="btn btn-primary"><i class="fas fa-ticket-alt"></i> Стать участником</a>
<a href="#agenda" class="btn btn-outline"><i class="fas fa-calendar-alt"></i> Узнать программу</a>
</div>
<div class="hero-info">
<div><i class="far fa-calendar-check"></i> 26 апреля 2026, начало в 11:00</div>
<div><i class="fas fa-lock"></i> Вход по рекомендации</div>
</div>
</div>
<div class="hero-stats">
<div class="stats-title">Сообщество в цифрах</div>
<div class="stat-grid">
<div class="stat-item"><h4>557+</h4><p>участников</p></div>
<div class="stat-item"><h4>125+</h4><p>выездов в зону СВО</p></div>
<div class="stat-item"><h4>30+</h4><p>комитетов</p></div>
<div class="stat-item"><h4>250+</h4><p>предпринимателей c 1 млрд+</p></div>
</div>
<p class="stats-note"><i class="fas fa-check-circle"></i> По данным сайта сообщества</p>
</div>
</div>
</div>
</section>
<section id="about" class="section bg-light">
<div class="container">
<div class="badge" style="background: #edeae2;">Миссия и ценности</div>
<div class="about-grid">
<div class="about-content">
<h2>Бизнес как <br> социальная миссия</h2>
<p>Сообщество «Предприниматели Патриоты» объединяет активных владельцев бизнеса, героев СВО, олимпийских чемпионов и лидеров мнений. Мы создаём среду, где бренд «Сделано в России» становится знаком качества и силы.</p>
<p>На круглом столе встретятся ЛПР, чтобы обсудить реальные инструменты масштабирования, социальные проекты и укрепление делового суверенитета. Закрытый клуб — только для единомышленников.</p>
<div class="values-list">
<span><i class="fas fa-heart"></i> Патриотизм</span>
<span><i class="fas fa-hand-sparkles"></i> Соц. ответственность</span>
<span><i class="fas fa-chart-line"></i> Лидерство</span>
<span><i class="fas fa-eye"></i> Личный пример</span>
</div>
</div>
<div class="about-quote">
<i class="fas fa-quote-left"></i>
<p>«Мы хотим, чтобы слово „патриот“ ассоциировалось с надёжным партнёром и лидером рынка. Встреча за круглым столом — место честного диалога без галстуков».</p>
<p class="quote-author">— Вячеслав Варяников, основатель сообщества</p>
</div>
</div>
</div>
</section>
<section class="bg-gentle section-sm">
<div class="container">
<div class="section-header">
<div class="badge">Почему участвовать</div>
<h2>Формат доверия и пользы</h2>
</div>
<div class="cards-grid">
<div class="card"><i class="fas fa-users"></i><h3>Топ-нетворкинг</h3><p>Встреча с первыми лицами компаний, членами комитетов и экспертами с выручкой от 1 млрд.</p></div>
<div class="card"><i class="fas fa-handshake"></i><h3>Реальные проекты</h3><p>Совместные инициативы, социальные программы и коммерческие коллаборации.</p></div>
<div class="card"><i class="fas fa-chalkboard-user"></i><h3>Живые кейсы</h3><p>Разбор практических ситуаций от участников сообщества «Патриот» без купюр.</p></div>
<div class="card"><i class="fas fa-shield-heart"></i><h3>Закрытый клуб</h3><p>Доступ по рекомендации — среда, где ценят репутацию и патриотизм.</p></div>
</div>
</div>
</section>
<section id="agenda" class="section bg-light">
<div class="container">
<div class="section-header">
<div class="badge">Программа круглого стола</div>
<h2>Стратегия, партнёрство, развитие</h2>
<p>Камерная встреча с живым обсуждением. Формат — открытый микрофон, панельные дискуссии и кофе-брейк.</p>
</div>
<div class="agenda-list">
<div class="agenda-item"><div class="agenda-time">11:00 11:30</div><div><strong>Сбор кофе, приветственный кофе-брейк</strong><br>Неформальное знакомство участников.</div></div>
<div class="agenda-item"><div class="agenda-time">11:30 12:30</div><div><strong>Панель «Бизнес и социальная ответственность»</strong><br>Как проекты помощи и поддержка СВО влияют на репутацию и прибыль.</div></div>
<div class="agenda-item"><div class="agenda-time">12:30 13:30</div><div><strong>Круглый стол «Точки роста: импортозамещение и экспорт»</strong><br>Успешные стратегии участников сообщества.</div></div>
<div class="agenda-item"><div class="agenda-time">13:30 14:30</div><div><strong>Обед и нетворкинг</strong><br>Живое общение, обмен контактами.</div></div>
<div class="agenda-item"><div class="agenda-time">14:30 15:45</div><div><strong>Открытый диалог с основателями комитетов</strong><br>Ответы на вопросы, разбор запросов участников.</div></div>
<div class="agenda-item"><div class="agenda-time">15:45 16:00</div><div><strong>Заключение, анонс совместных проектов</strong><br>Формирование рабочих групп.</div></div>
</div>
</div>
</section>
<section id="stats-block" class="bg-gentle section-sm">
<div class="container">
<div class="stats-wrapper">
<div class="stats-info">
<div class="badge">Сообщество Патриот.РФ</div>
<h2>Объединяя лидеров <br> для побед России</h2>
<p>Мы растем: от 557 участников до масштабных гуманитарных миссий. 125+ выездов, 600+ детей под опекой предпринимателей и 10+ млн подписчиков в общем охвате.</p>
<ul>
<li><i class="fas fa-check-circle"></i> 250+ предпринимателей c оборотом от 1 млрд руб.</li>
<li><i class="fas fa-check-circle"></i> 30 комитетов: спорт, обучение, соцпроекты</li>
<li><i class="fas fa-check-circle"></i> Ежемесячные стратегические сессии с героями СВО</li>
</ul>
</div>
<div class="stats-cta">
<i class="fas fa-chart-simple"></i>
<h3>Присоединиться к 557+</h3>
<p>Участники сообщества получают поддержку государства, гранты и надежные B2B-связи. Круглый стол — ваш шанс войти в закрытый пул.</p>
<a href="#register" class="btn btn-primary">Подать заявку</a>
</div>
</div>
</div>
</section>
<section id="register" class="section bg-light">
<div class="container">
<div class="section-header">
<div class="badge"><i class="fas fa-pen-alt"></i> Участие строго по отбору</div>
<h2>Стать участником круглого стола</h2>
<p>Заполните анкету — модератор сообщества свяжется с вами для подтверждения. Вход для предпринимателей, топ-менеджеров и партнёров, разделяющих ценности «Патриота».</p>
</div>
<div class="form-card">
<form id="registrationForm">
<div class="input-group">
<input type="text" name="fullname" id="fullname" placeholder="ФИО *" required>
</div>
<div class="input-group">
<input type="email" name="email" id="email" placeholder="E-mail *" required>
</div>
<div class="input-group">
<input type="tel" name="phone" id="phone" placeholder="Телефон *" required>
</div>
<div class="input-group">
<input type="text" name="company" id="company" placeholder="Компания / Бизнес-направление *" required>
</div>
<div class="input-group">
<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>
</div>
<div class="input-group">
<textarea name="message" id="message" rows="3" placeholder="Коротко о себе / почему хотите участвовать? (рекомендация или бизнес-запрос)"></textarea>
</div>
<div class="input-group">
<label>
<input type="checkbox" name="agree" id="agreeCheck" value="true" required>
Я соглашаюсь с политикой конфиденциальности и подтверждаю, что разделяю ценности сообщества «Патриот» *
</label>
</div>
<button type="submit" class="btn btn-primary submit-btn"><i class="fas fa-paper-plane"></i> Отправить заявку</button>
<div id="formFeedback" class="form-message"></div>
</form>
<p class="form-note">После одобрения вы получите персональное приглашение. Количество мест ограничено.</p>
</div>
</div>
</section>
</main>
<footer>
<div class="container">
<div class="footer-logo">ПАТРИОТ<span style="color:#c52a1c;"></span>БИЗНЕС</div>
<p>Бизнес-сообщество «Предприниматели Патриоты» — официальный сайт: <a href="https://ppatrioty.ru/" target="_blank">ppatrioty.ru</a></p>
<p>© 2026 Круглый стол с БС «Патриот». Вход по рекомендации действующих участников.</p>
</div>
</footer>
<script src="/static/js/main.js"></script>
</body>
</html>