Restructure views and use Telegram native action button

This commit is contained in:
Codex 2026-09-07 20:42:43 +00:00
parent 90c739d5ee
commit 41dbaa66f4
3 changed files with 113 additions and 48 deletions

View file

@ -12,6 +12,8 @@
const state = {
kind: "income",
board: "all",
currentView: "dashboardView",
previousView: "dashboardView",
categoryFilter: "",
summary: null,
config: null,
@ -24,17 +26,19 @@
const $ = (selector) => document.querySelector(selector);
const money = new Intl.NumberFormat("ru-RU", { style: "currency", currency: "RUB", maximumFractionDigits: 2 });
function safeTelegram(action) {
try { action(); } catch (error) { console.warn("Telegram WebApp method failed", error); }
}
function initTelegram() {
if (!tg) return;
const safely = (action) => {
try { action(); } catch (error) { console.warn("Telegram WebApp method failed", error); }
};
safely(() => tg.ready());
safely(() => tg.expand());
safely(() => tg.enableClosingConfirmation?.());
if (tg.isVersionAtLeast?.("6.1")) safely(() => tg.setBackgroundColor("#111111"));
if (tg.isVersionAtLeast?.("6.9")) safely(() => tg.setHeaderColor("#111111"));
if (tg.isVersionAtLeast?.("7.10")) safely(() => tg.setBottomBarColor("#111111"));
safeTelegram(() => tg.ready());
safeTelegram(() => tg.expand());
safeTelegram(() => tg.enableClosingConfirmation?.());
if (tg.isVersionAtLeast?.("6.1")) safeTelegram(() => tg.setBackgroundColor("#111111"));
if (tg.isVersionAtLeast?.("6.9")) safeTelegram(() => tg.setHeaderColor("#111111"));
if (tg.isVersionAtLeast?.("7.7")) safeTelegram(() => tg.disableVerticalSwipes());
if (tg.isVersionAtLeast?.("7.10")) safeTelegram(() => tg.setBottomBarColor("#111111"));
}
function showToast(message) {
@ -54,15 +58,59 @@
function bindNavigation() {
document.querySelectorAll(".bottom-nav button").forEach((button) => {
button.addEventListener("click", () => {
document.querySelectorAll(".bottom-nav button, .view").forEach((item) => item.classList.remove("active"));
button.classList.add("active");
$(`#${button.dataset.view}`).classList.add("active");
if (button.dataset.view === "leaderboardView") loadLeaderboard();
});
button.addEventListener("click", () => showView(button.dataset.view));
});
}
function showView(viewId) {
document.querySelectorAll(".bottom-nav button, .view").forEach((item) => item.classList.remove("active"));
$(`#${viewId}`).classList.add("active");
document.querySelector(`.bottom-nav button[data-view="${viewId}"]`)?.classList.add("active");
state.currentView = viewId;
document.body.classList.toggle("form-open", viewId === "addView");
updateNativeControls();
window.scrollTo({ top: 0, behavior: "auto" });
if (viewId === "leaderboardView") loadLeaderboard();
}
function openAddView() {
if (state.currentView !== "addView") state.previousView = state.currentView;
showView("addView");
window.setTimeout(() => $("#amount").focus(), 0);
}
function closeAddView() {
showView(state.previousView || "dashboardView");
}
function setupNativeControls() {
const isTelegramClient = Boolean(tg?.platform && tg.platform !== "unknown");
if (!isTelegramClient || !tg?.MainButton) return;
safeTelegram(() => tg.MainButton.onClick(() => {
if (state.currentView === "addView") {
$("#transactionForm").requestSubmit();
} else {
openAddView();
}
}));
safeTelegram(() => tg.BackButton?.onClick(closeAddView));
updateNativeControls();
}
function updateNativeControls() {
const isTelegramClient = Boolean(tg?.platform && tg.platform !== "unknown");
if (!isTelegramClient || !tg?.MainButton) return;
const isForm = state.currentView === "addView";
safeTelegram(() => tg.MainButton.setParams({
text: isForm ? "Сохранить операцию" : "Добавить запись",
color: "#f0d28a",
text_color: "#17130b",
is_active: true,
is_visible: true,
}));
safeTelegram(() => isForm ? tg.BackButton?.show() : tg.BackButton?.hide());
}
function fillCategories() {
const select = $("#category");
const items = [...new Set([...categories[state.kind], ...state.customCategories[state.kind]])];
@ -143,10 +191,12 @@
}
if (!canSendViaTelegram()) return false;
try {
safeTelegram(() => tg.MainButton?.showProgress());
tg.HapticFeedback?.notificationOccurred("success");
tg.sendData(encoded);
return true;
} catch (error) {
safeTelegram(() => tg.MainButton?.hideProgress());
console.error("Telegram sendData failed", error);
showToast("Telegram не принял запись. Обновите приложение и попробуйте снова");
return false;
@ -190,9 +240,6 @@
$("#balanceValue").textContent = money.format(summary.balance_cents / 100);
$("#incomeValue").textContent = money.format(summary.income_cents / 100);
$("#expenseValue").textContent = money.format(summary.expense_cents / 100);
const percent = Number(summary.saved_percent);
$("#savedPercent").textContent = `${percent.toLocaleString("ru-RU", { maximumFractionDigits: 1 })}%`;
$("#savingProgress").style.width = `${Math.max(0, Math.min(100, percent))}%`;
renderCategories(summary.categories || []);
}
@ -369,6 +416,7 @@
initTelegram();
setIdentity();
bindNavigation();
setupNativeControls();
bindForm();
bindLeaderboard();
fillCategories();

View file

@ -6,9 +6,9 @@
<meta name="color-scheme" content="dark" />
<meta name="theme-color" content="#111111" />
<title>Пожитки</title>
<link rel="stylesheet" href="./styles.css?v=6" />
<link rel="stylesheet" href="./styles.css?v=7" />
<script src="./telegram-web-app.js"></script>
<script src="./app.js?v=4" defer></script>
<script src="./app.js?v=7" defer></script>
</head>
<body>
<main class="shell">
@ -24,25 +24,28 @@
</header>
<section id="dashboardView" class="view active" aria-labelledby="dashboardTab">
<article class="hero-card">
<p>Накоплено</p>
<strong id="balanceValue">0 ₽</strong>
<div class="saving-row">
<span id="savedPercent">0%</span>
<div class="progress"><i id="savingProgress"></i></div>
</div>
<small>от всех поступлений</small>
</article>
<div class="dashboard-summary">
<article class="hero-card">
<p>Текущий баланс</p>
<strong id="balanceValue">0 ₽</strong>
</article>
<div class="metric-grid">
<article class="metric income">
<span class="metric-icon"></span>
<div><small>Поступления</small><strong id="incomeValue">0 ₽</strong></div>
</article>
<article class="metric expense">
<span class="metric-icon"></span>
<div><small>Расходы</small><strong id="expenseValue">0 ₽</strong></div>
</article>
<div class="metric-grid">
<article class="metric income">
<span class="metric-icon"></span>
<div><small>Поступления</small><strong id="incomeValue">0 ₽</strong></div>
</article>
<article class="metric expense">
<span class="metric-icon"></span>
<div><small>Расходы</small><strong id="expenseValue">0 ₽</strong></div>
</article>
</div>
</div>
</section>
<section id="historyView" class="view" aria-labelledby="historyTab">
<div class="section-title history-page-heading">
<div><p class="eyebrow">Движение денег</p><h2>История</h2></div>
</div>
<div class="section-title">
@ -62,8 +65,8 @@
</div>
</section>
<section id="addView" class="view" aria-labelledby="addTab">
<div class="section-title add-heading">
<section id="addView" class="view" aria-labelledby="addHeading">
<div id="addHeading" class="section-title add-heading">
<div><p class="eyebrow">Новая запись</p><h2>Движение денег</h2></div>
</div>
<form id="transactionForm" class="transaction-form">
@ -93,7 +96,6 @@
<input id="note" name="note" maxlength="160" placeholder="За что или откуда" />
</label>
<button class="primary-button" type="submit">Сохранить через Telegram</button>
<p class="form-hint">Telegram передаст запись боту вместе с вашим подтверждённым ID.</p>
</form>
</section>
@ -120,7 +122,7 @@
<nav class="bottom-nav" aria-label="Основная навигация">
<button id="dashboardTab" class="active" data-view="dashboardView"><span></span>Обзор</button>
<button id="addTab" data-view="addView"><span class="add-mark"></span>Добавить</button>
<button id="historyTab" data-view="historyView"><span></span>История</button>
<button id="leaderboardTab" data-view="leaderboardView"><span></span>Рейтинг</button>
</nav>

View file

@ -20,7 +20,13 @@
-webkit-tap-highlight-color: transparent;
}
html, body { margin: 0; min-height: 100%; background: var(--bg); }
html, body {
margin: 0;
min-height: 100%;
background: var(--bg);
overscroll-behavior: none;
overscroll-behavior-y: none;
}
body {
position: relative;
@ -28,6 +34,7 @@ body {
min-height: 100vh;
min-height: 100dvh;
padding: max(14px, env(safe-area-inset-top)) 18px calc(96px + env(safe-area-inset-bottom));
overflow-x: hidden;
background: transparent;
}
@ -46,7 +53,7 @@ body::before {
body::after {
z-index: -1;
background: rgba(0, 0, 0, .16);
background: rgba(0, 0, 0, .2);
}
button, input, select { font: inherit; color: inherit; }
@ -63,7 +70,7 @@ button:focus { outline: none; }
button:focus-visible { outline: 2px solid rgba(255, 255, 255, .72); outline-offset: 2px; }
button:active { filter: brightness(.88); }
.shell { width: min(100%, 560px); margin: 0 auto; }
.shell { display: flex; flex-direction: column; width: min(100%, 560px); min-height: calc(var(--tg-viewport-stable-height, 100dvh) - 110px); margin: 0 auto; }
.topbar { display: flex; align-items: center; justify-content: space-between; margin: 6px 2px 26px; text-shadow: 0 2px 14px rgba(0, 0, 0, .55); }
.eyebrow { margin: 0 0 3px; text-transform: uppercase; letter-spacing: .12em; font-size: 10px; font-weight: 750; color: var(--muted); }
h1, h2, p { margin-top: 0; }
@ -72,11 +79,16 @@ h2 { margin: 0; font-size: 23px; letter-spacing: -.025em; }
.user-chip { display: flex; align-items: center; gap: 8px; max-width: 48%; padding: 5px 11px 5px 5px; border: 1px solid var(--line); border-radius: 999px; background: var(--surface); font-size: 12px; font-weight: 650; white-space: nowrap; overflow: hidden; }
.mini-avatar { display: grid; flex: 0 0 auto; place-items: center; width: 29px; height: 29px; border-radius: 50%; background: rgba(0, 0, 0, .28); color: var(--text); }
.view { display: none; padding-top: clamp(120px, 17vh, 175px); }
.view { display: none; padding-top: 0; }
.view.active { display: block; }
#dashboardView.active { display: flex; flex: 1; flex-direction: column; justify-content: flex-end; }
.dashboard-summary { width: 100%; margin-top: auto; }
.hero-card {
min-height: 210px;
display: flex;
flex-direction: column;
justify-content: flex-end;
min-height: 190px;
padding: 27px;
border-radius: 29px;
border: 1px solid var(--line);
@ -91,6 +103,8 @@ h2 { margin: 0; font-size: 23px; letter-spacing: -.025em; }
.progress { flex: 1; height: 7px; overflow: hidden; border-radius: 99px; background: rgba(255,255,255,.2); }
.progress i { display: block; width: 0; height: 100%; border-radius: inherit; background: #e8d487; transition: width .4s ease; }
.hero-card small { display: block; margin-top: 7px; opacity: .55; font-size: 11px; }
.dashboard-summary .hero-card strong { margin-bottom: 0; }
.dashboard-summary .metric-grid { margin-bottom: 0; }
.metric-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 11px; margin: 12px 0 28px; }
.metric { display: flex; align-items: center; gap: 11px; min-width: 0; padding: 17px 15px; border: 1px solid var(--line); border-radius: 20px; background: var(--surface); }
@ -113,6 +127,7 @@ h2 { margin: 0; font-size: 23px; letter-spacing: -.025em; }
.category-bar i { display: block; height: 100%; background: var(--accent); }
.category-item.expense-item .category-bar i { background: var(--danger); }
.history-heading { margin-top: 28px; }
.history-page-heading { margin: 5px 3px 24px; }
.transaction-list { overflow: hidden; border: 1px solid var(--line); border-radius: 21px; background: var(--surface); }
.transaction-row { display: grid; grid-template-columns: 40px 1fr auto; align-items: center; gap: 11px; padding: 13px 14px; border-bottom: 1px solid var(--line); }
.transaction-row:last-child { border-bottom: 0; }
@ -168,7 +183,7 @@ h2 { margin: 0; font-size: 23px; letter-spacing: -.025em; }
.bottom-nav button { display: grid; place-items: center; gap: 2px; padding: 3px; border: 0; background: transparent; color: var(--muted); font-size: 9px; font-weight: 650; }
.bottom-nav button > span { font-size: 22px; line-height: 30px; }
.bottom-nav button.active { color: var(--accent); }
.bottom-nav .add-mark { display: grid; place-items: center; width: 42px; height: 32px; border-radius: 12px; background: var(--accent); color: var(--accent-text); }
.form-open .bottom-nav { display: none; }
.toast { position: fixed; z-index: 20; left: 50%; bottom: calc(92px + env(safe-area-inset-bottom)); max-width: calc(100% - 36px); padding: 11px 16px; border: 1px solid var(--line); border-radius: 12px; transform: translate(-50%, 20px); background: rgba(0, 0, 0, .62); color: var(--text); opacity: 0; pointer-events: none; transition: .2s ease; font-size: 12px; box-shadow: var(--shadow); backdrop-filter: blur(18px); -webkit-backdrop-filter: blur(18px); }
.toast.show { opacity: 1; transform: translate(-50%, 0); }