Refactor menu flows into dedicated pages
Some checks failed
ci / test (push) Has been cancelled

This commit is contained in:
VPN SaaS Dev
2026-05-16 11:59:09 +09:00
parent 01a69fc42d
commit ecfb5aa949
20 changed files with 2415 additions and 97 deletions

152
web/static/page_common.js Normal file
View File

@@ -0,0 +1,152 @@
const tg = window.Telegram?.WebApp;
tg?.ready();
tg?.expand();
const CarPassPage = (() => {
const state = { user: null, authConfig: null };
function authHeaders(extra = {}) {
const headers = { ...extra };
if (tg?.initData) headers["X-Telegram-Init-Data"] = tg.initData;
if (!tg?.initData && state.authConfig?.allow_dev_auth) {
headers["X-Dev-Telegram-Id"] = localStorage.getItem("driversDevTelegramId") || "1";
}
return headers;
}
async function api(path, options = {}) {
const { headers: optionHeaders = {}, ...fetchOptions } = options;
const headers = { "Content-Type": "application/json", ...authHeaders(optionHeaders) };
if (options.body instanceof FormData) delete headers["Content-Type"];
const response = await fetch(`/api${path}`, { ...fetchOptions, headers });
if (!response.ok) throw new Error(await response.text() || response.statusText);
if (response.status === 204) return null;
return response.json();
}
async function loadAuthConfig() {
state.authConfig = await api("/users/auth/config");
}
function showAuthOverlay() {
document.body.classList.add("auth-required");
const link = document.querySelector("#telegramLoginLink");
if (state.authConfig?.bot_username && link) {
link.href = `https://t.me/${state.authConfig.bot_username}`;
link.classList.remove("hidden");
}
}
async function ensureUser() {
if (tg?.initData) {
state.user = await api("/users/webapp-auth", {
method: "POST",
body: JSON.stringify({ init_data: tg.initData }),
});
} else if (state.authConfig?.allow_dev_auth) {
state.user = await api("/users/me");
} else {
showAuthOverlay();
throw new Error("Требуется вход через Telegram");
}
document.body.classList.remove("auth-required");
document.querySelector("#authOverlay")?.classList.add("hidden");
return state.user;
}
function toast(message, tone = "success") {
const node = document.querySelector("#toast");
if (!node) return;
node.textContent = message;
node.className = `toast ${tone}`;
window.clearTimeout(toast.timer);
toast.timer = window.setTimeout(() => node.classList.add("hidden"), 2600);
}
function setBusy(button, busy, label = "Сохраняю...") {
if (!button) return;
if (busy) {
button.dataset.label = button.textContent;
button.disabled = true;
button.classList.add("is-busy");
button.innerHTML = `<span class="spinner"></span><span>${label}</span>`;
} else {
button.disabled = false;
button.classList.remove("is-busy");
button.textContent = button.dataset.label || button.textContent;
delete button.dataset.label;
}
}
async function runAction(button, label, callback) {
setBusy(button, true, label);
try {
const result = await callback();
return result;
} catch (error) {
toast(error.message || "Ошибка", "error");
throw error;
} finally {
setBusy(button, false, label);
}
}
function escapeHtml(value) {
return String(value ?? "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
function formData(form) {
return Object.fromEntries(new FormData(form).entries());
}
function numberOrNull(value) {
return value === "" || value == null ? null : Number(value);
}
function csvList(value) {
return value ? value.split(",").map((item) => item.trim()).filter(Boolean) : null;
}
function formatDateTime(value) {
if (!value) return "-";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return String(value).slice(0, 16).replace("T", " ");
return date.toLocaleString("ru-RU", { day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit" });
}
function today() {
return new Date().toISOString().slice(0, 10);
}
async function boot(init) {
try {
await loadAuthConfig();
await ensureUser();
await init();
} catch (error) {
if (error.message === "Требуется вход через Telegram") return;
console.error(error);
toast(error.message || "Ошибка", "error");
}
}
document.querySelector("#telegramRetryBtn")?.addEventListener("click", () => window.location.reload());
return {
state,
api,
boot,
toast,
runAction,
escapeHtml,
formData,
numberOrNull,
csvList,
formatDateTime,
today,
};
})();