Restructure views and use Telegram native action button
This commit is contained in:
parent
90c739d5ee
commit
41dbaa66f4
3 changed files with 113 additions and 48 deletions
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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); }
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue