Загрузить файлы в «static»
This commit is contained in:
32
static/index.html
Normal file
32
static/index.html
Normal file
@@ -0,0 +1,32 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Robo Arena — РСУ</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="auth-wrap">
|
||||
<div class="auth-panel">
|
||||
<div class="robot-icon">🤖</div>
|
||||
<div class="sys-label">Robo Arena / РСУ</div>
|
||||
<h1>Удалённая система управления боевыми роботами</h1>
|
||||
<p>Прототип личного кабинета для мониторинга арены, дистанционного управления роботами и ведения боевого журнала в реальном времени.</p>
|
||||
<div class="arena-stats">
|
||||
<div class="arena-stat">⚔ Управление роботами <span>ONLINE</span></div>
|
||||
<div class="arena-stat">📡 Телеметрия арены <span>LIVE</span></div>
|
||||
<div class="arena-stat">🛡 Защищённый доступ <span>TLS</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="auth-form-area">
|
||||
<div class="auth-box">
|
||||
<h2>Добро пожаловать, пилот</h2>
|
||||
<p class="sub">Войдите или зарегистрируйтесь, чтобы управлять своими роботами на арене</p>
|
||||
<a class="btn btn-primary" href="/static/login.html">Войти в систему</a>
|
||||
<a class="btn btn-ghost" style="margin-top:12px;width:100%;display:flex;align-items:center;justify-content:center;" href="/static/register.html">Стать пилотом</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
79
static/login.html
Normal file
79
static/login.html
Normal file
@@ -0,0 +1,79 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Вход — Robo Arena</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="auth-wrap">
|
||||
<div class="auth-panel">
|
||||
<div class="robot-icon">⚔</div>
|
||||
<div class="sys-label">Robo Arena / РСУ</div>
|
||||
<h1>Вход пилота</h1>
|
||||
<p>Управляйте боевыми роботами, следите за телеметрией и ведите журнал сражений из любой точки мира.</p>
|
||||
<div class="arena-stats">
|
||||
<div class="arena-stat">🤖 Роботы в арене <span>READY</span></div>
|
||||
<div class="arena-stat">📊 Журнал боёв <span>LOG</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="auth-form-area">
|
||||
<div class="auth-box">
|
||||
<h2>Авторизация</h2>
|
||||
<p class="sub">Введите данные своего аккаунта пилота</p>
|
||||
|
||||
<div class="field">
|
||||
<label for="email">Электронная почта</label>
|
||||
<input type="email" id="email" placeholder="pilot@arena.ru" autocomplete="email">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="password">Пароль</label>
|
||||
<input type="password" id="password" placeholder="••••••••" autocomplete="current-password">
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary" id="submit-btn">Войти на арену</button>
|
||||
<p class="err-msg" id="error-text"></p>
|
||||
|
||||
<div class="link-row">Нет аккаунта? <a href="/static/register.html">Стать пилотом</a></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const errEl = document.getElementById("error-text");
|
||||
const submitBtn = document.getElementById("submit-btn");
|
||||
|
||||
submitBtn.addEventListener("click", async () => {
|
||||
errEl.textContent = "";
|
||||
const email = document.getElementById("email").value.trim();
|
||||
const password = document.getElementById("password").value;
|
||||
|
||||
if (!email || !password) { errEl.textContent = "Заполните все поля"; return; }
|
||||
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = "Проверяем…";
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/login", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
credentials: "include",
|
||||
body: JSON.stringify({ email, password })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) { errEl.textContent = data.detail || "Неверная почта или пароль"; return; }
|
||||
window.location.href = "/static/dashboard.html";
|
||||
} catch {
|
||||
errEl.textContent = "Не удалось связаться с сервером";
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = "Войти на арену";
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", e => { if (e.key === "Enter") submitBtn.click(); });
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
234
static/profile.html
Normal file
234
static/profile.html
Normal file
@@ -0,0 +1,234 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Robo Arena — Профиль пилота</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body class="page-app page-profile">
|
||||
|
||||
<nav class="topbar">
|
||||
<div class="topbar-brand">
|
||||
<span class="logo-icon">🤖</span>
|
||||
<span>Robo Arena</span> / РСУ
|
||||
</div>
|
||||
<span class="topbar-user" id="topbar-email"></span>
|
||||
<a class="btn btn-sm active-nav" href="/static/profile.html">Профиль</a>
|
||||
<a class="btn btn-sm" href="/static/dashboard.html">Арена</a>
|
||||
<button class="btn btn-sm" id="logout-btn">Выйти</button>
|
||||
</nav>
|
||||
|
||||
<div class="main main--profile">
|
||||
|
||||
<div class="profile-layout">
|
||||
|
||||
<!-- Боковая панель -->
|
||||
<aside class="profile-sidebar">
|
||||
<div class="profile-sidebar-glow"></div>
|
||||
<div class="profile-avatar" id="profile-avatar">👤</div>
|
||||
<h2 class="profile-sidebar-name" id="sidebar-name">Пилот</h2>
|
||||
<p class="profile-sidebar-email" id="sidebar-email"></p>
|
||||
|
||||
<div class="profile-status" id="profile-status">
|
||||
<div class="profile-status-label">Заполненность профиля</div>
|
||||
<div class="profile-progress-wrap">
|
||||
<div class="profile-progress-bar" id="progress-bar" style="width:0%"></div>
|
||||
</div>
|
||||
<div class="profile-status-text" id="status-text">0 из 4 полей</div>
|
||||
</div>
|
||||
|
||||
<div class="profile-sidebar-hints">
|
||||
<div class="hint-item">
|
||||
<span class="hint-icon">🛡</span>
|
||||
<span>Данные нужны для идентификации пилота на арене</span>
|
||||
</div>
|
||||
<div class="hint-item">
|
||||
<span class="hint-icon">🔒</span>
|
||||
<span>Информация хранится только в вашем аккаунте</span>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Форма -->
|
||||
<div class="profile-form-card">
|
||||
<div class="profile-form-header">
|
||||
<div class="profile-badge">⚔ Профиль пилота</div>
|
||||
<h2>Данные оператора</h2>
|
||||
<p class="sub">Заполните личную информацию для участия в системе управления боевыми роботами</p>
|
||||
</div>
|
||||
|
||||
<form id="profile-form" autocomplete="on">
|
||||
|
||||
<div class="form-section">
|
||||
<div class="form-section-title">
|
||||
<span class="form-section-icon">📋</span>
|
||||
Личные данные
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label for="full_name">ФИО</label>
|
||||
<div class="input-wrap">
|
||||
<span class="input-icon">👤</span>
|
||||
<input type="text" id="full_name" placeholder="Иванов Иван Иванович" autocomplete="name">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-field">
|
||||
<label for="birth_date">Дата рождения</label>
|
||||
<div class="input-wrap">
|
||||
<span class="input-icon">🎂</span>
|
||||
<input type="date" id="birth_date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label for="gender">Пол</label>
|
||||
<div class="input-wrap">
|
||||
<span class="input-icon">⚧</span>
|
||||
<select id="gender">
|
||||
<option value="">— выберите —</option>
|
||||
<option value="male">Мужской</option>
|
||||
<option value="female">Женский</option>
|
||||
<option value="other">Не указан</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<div class="form-section-title">
|
||||
<span class="form-section-icon">📞</span>
|
||||
Контакты
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label for="phone">Телефон</label>
|
||||
<div class="input-wrap">
|
||||
<span class="input-icon">📱</span>
|
||||
<input type="tel" id="phone" placeholder="+7 (999) 123-45-67" autocomplete="tel">
|
||||
</div>
|
||||
<span class="field-hint">Формат: +7 или 8, минимум 7 цифр</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary" id="save-btn">
|
||||
<span>💾 Сохранить профиль</span>
|
||||
</button>
|
||||
<a class="btn btn-ghost btn-back" href="/static/dashboard.html">← На арену</a>
|
||||
</div>
|
||||
|
||||
<p class="err-msg" id="error-text"></p>
|
||||
<p class="ok-msg" id="ok-text"></p>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const errEl = document.getElementById("error-text");
|
||||
const okEl = document.getElementById("ok-text");
|
||||
const saveBtn = document.getElementById("save-btn");
|
||||
|
||||
function updateSidebar(u) {
|
||||
document.getElementById("topbar-email").textContent = u.email;
|
||||
document.getElementById("sidebar-email").textContent = u.email;
|
||||
|
||||
if (u.full_name) {
|
||||
document.getElementById("sidebar-name").textContent = u.full_name;
|
||||
document.getElementById("profile-avatar").textContent = u.full_name.trim()[0].toUpperCase();
|
||||
}
|
||||
|
||||
const fields = [u.full_name, u.birth_date, u.phone, u.gender];
|
||||
const filled = fields.filter(Boolean).length;
|
||||
const pct = Math.round((filled / 4) * 100);
|
||||
|
||||
document.getElementById("progress-bar").style.width = pct + "%";
|
||||
document.getElementById("status-text").textContent = `${filled} из 4 полей`;
|
||||
|
||||
const statusEl = document.getElementById("profile-status");
|
||||
if (filled === 4) {
|
||||
statusEl.classList.add("profile-status--complete");
|
||||
document.getElementById("status-text").textContent = "Профиль заполнен ✓";
|
||||
} else {
|
||||
statusEl.classList.remove("profile-status--complete");
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMe() {
|
||||
const res = await fetch("/api/me", { credentials: "include" });
|
||||
if (!res.ok) { window.location.href = "/static/login.html"; return; }
|
||||
const u = await res.json();
|
||||
document.getElementById("full_name").value = u.full_name || "";
|
||||
document.getElementById("birth_date").value = u.birth_date || "";
|
||||
document.getElementById("phone").value = u.phone || "";
|
||||
document.getElementById("gender").value = u.gender || "";
|
||||
updateSidebar(u);
|
||||
}
|
||||
|
||||
["full_name", "birth_date", "phone", "gender"].forEach(id => {
|
||||
document.getElementById(id).addEventListener("input", () => {
|
||||
updateSidebar({
|
||||
full_name: document.getElementById("full_name").value.trim(),
|
||||
birth_date: document.getElementById("birth_date").value,
|
||||
phone: document.getElementById("phone").value.trim(),
|
||||
gender: document.getElementById("gender").value,
|
||||
email: document.getElementById("sidebar-email").textContent,
|
||||
});
|
||||
});
|
||||
document.getElementById(id).addEventListener("change", () => {
|
||||
document.getElementById(id).dispatchEvent(new Event("input"));
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById("profile-form").addEventListener("submit", async e => {
|
||||
e.preventDefault();
|
||||
errEl.textContent = "";
|
||||
okEl.textContent = "";
|
||||
|
||||
const payload = {
|
||||
full_name: document.getElementById("full_name").value.trim(),
|
||||
birth_date: document.getElementById("birth_date").value,
|
||||
phone: document.getElementById("phone").value.trim(),
|
||||
gender: document.getElementById("gender").value,
|
||||
};
|
||||
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.querySelector("span").textContent = "Сохраняем…";
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/me/profile", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
credentials: "include",
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
errEl.textContent = data.detail || "Не удалось сохранить профиль";
|
||||
return;
|
||||
}
|
||||
okEl.textContent = "✓ Профиль сохранён! Переход на арену…";
|
||||
updateSidebar(data);
|
||||
setTimeout(() => { window.location.href = "/static/dashboard.html"; }, 1400);
|
||||
} catch {
|
||||
errEl.textContent = "Не удалось связаться с сервером";
|
||||
} finally {
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.querySelector("span").textContent = "💾 Сохранить профиль";
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("logout-btn").addEventListener("click", async () => {
|
||||
await fetch("/api/logout", { method: "POST", credentials: "include" });
|
||||
window.location.href = "/static/index.html";
|
||||
});
|
||||
|
||||
loadMe();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
79
static/register.html
Normal file
79
static/register.html
Normal file
@@ -0,0 +1,79 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Регистрация — Robo Arena</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="auth-wrap">
|
||||
<div class="auth-panel">
|
||||
<div class="robot-icon">🛡</div>
|
||||
<div class="sys-label">Robo Arena / РСУ</div>
|
||||
<h1>Регистрация пилота</h1>
|
||||
<p>Один аккаунт — неограниченное количество боевых роботов под вашим управлением на арене.</p>
|
||||
<div class="arena-stats">
|
||||
<div class="arena-stat">⚡ Развёртывание роботов <span>INSTANT</span></div>
|
||||
<div class="arena-stat">🔧 Управление мощностью <span>0–100%</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="auth-form-area">
|
||||
<div class="auth-box">
|
||||
<h2>Новый пилот</h2>
|
||||
<p class="sub">Создайте аккаунт — после регистрации заполните профиль</p>
|
||||
|
||||
<div class="field">
|
||||
<label for="email">Электронная почта</label>
|
||||
<input type="email" id="email" placeholder="pilot@arena.ru" autocomplete="email">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="password">Пароль <span style="color:var(--muted);font-weight:400;text-transform:none;letter-spacing:0">(не менее 6 символов)</span></label>
|
||||
<input type="password" id="password" placeholder="••••••••" autocomplete="new-password">
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary" id="submit-btn">Зарегистрироваться</button>
|
||||
<p class="err-msg" id="error-text"></p>
|
||||
|
||||
<div class="link-row">Уже есть аккаунт? <a href="/static/login.html">Войти</a></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const errEl = document.getElementById("error-text");
|
||||
const submitBtn = document.getElementById("submit-btn");
|
||||
|
||||
submitBtn.addEventListener("click", async () => {
|
||||
errEl.textContent = "";
|
||||
const email = document.getElementById("email").value.trim();
|
||||
const password = document.getElementById("password").value;
|
||||
|
||||
if (!email || !password) { errEl.textContent = "Заполните все поля"; return; }
|
||||
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = "Подождите…";
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/register", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
credentials: "include",
|
||||
body: JSON.stringify({ email, password })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) { errEl.textContent = data.detail || "Ошибка регистрации"; return; }
|
||||
window.location.href = "/static/profile.html";
|
||||
} catch {
|
||||
errEl.textContent = "Не удалось связаться с сервером";
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = "Зарегистрироваться";
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", e => { if (e.key === "Enter") submitBtn.click(); });
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
1335
static/style.css
Normal file
1335
static/style.css
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user