Polish mobile auth and garage layout

This commit is contained in:
VPN SaaS Dev
2026-05-13 05:03:49 +09:00
parent 7fd4ab768f
commit 0eb503083a
4 changed files with 187 additions and 15 deletions

View File

@@ -32,15 +32,24 @@ def main_keyboard() -> ReplyKeyboardMarkup:
)
def webapp_inline_keyboard() -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(
inline_keyboard=[
[InlineKeyboardButton(text="Открыть CarPass", web_app=WebAppInfo(url=settings.effective_webapp_url))],
]
)
@dp.message(Command("start"))
async def start(message: Message) -> None:
user = await api.upsert_user(message.from_user)
text = (
f"Готово, {user.get('first_name') or 'водитель'}.\n\n"
"Здесь можно вести заправки, обслуживание, ремонты и смотреть стоимость владения. "
"Основная работа идет в mini app, а бот остается быстрым входом."
"CarPass — цифровой паспорт автомобиля: заправки, обслуживание, напоминания, подтвержденная история и стоимость владения.\n\n"
"Нажми «Открыть CarPass», чтобы перейти в приложение."
)
await message.answer(text, reply_markup=main_keyboard())
await message.answer(text, reply_markup=webapp_inline_keyboard())
await message.answer("Быстрый вход также закреплен на клавиатуре ниже.", reply_markup=main_keyboard())
@dp.message(Command("add_car"))

View File

@@ -14,11 +14,15 @@
<body class="auth-required">
<div class="auth-overlay" id="authOverlay">
<div class="auth-panel">
<p class="eyebrow">Drivers</p>
<h1>Гараж</h1>
<p id="authMessage">Это приложение открывается через Telegram-бота. Откройте Mini App из Telegram.</p>
<p class="eyebrow">CarPass</p>
<h1>Цифровой паспорт авто</h1>
<p id="authMessage">Откройте приложение через Telegram-бота, чтобы безопасно привязать гараж к вашему аккаунту.</p>
<div class="auth-actions">
<a id="telegramLoginLink" class="telegram-login-link hidden" href="#" rel="noreferrer">Открыть в Telegram</a>
<button id="telegramRetryBtn" class="telegram-secondary-btn" type="button">Проверить вход</button>
</div>
<div id="telegramLoginSlot" class="telegram-login-slot"></div>
<a id="telegramLoginLink" class="telegram-login-link hidden" href="#" target="_blank" rel="noreferrer">Открыть бота</a>
<p class="auth-note" id="authNote">Если Telegram уже открыт, нажмите «Открыть гараж» в боте.</p>
</div>
</div>
<main class="shell">

View File

@@ -513,7 +513,7 @@ async function ensureUser() {
hideAuthOverlay();
return;
}
showTelegramOpenHint();
await showTelegramLogin();
throw new Error("Требуется вход через Telegram");
}
@@ -527,18 +527,27 @@ function showTelegramOpenHint() {
const slot = document.querySelector("#telegramLoginSlot");
const link = document.querySelector("#telegramLoginLink");
const message = document.querySelector("#authMessage");
const note = document.querySelector("#authNote");
overlay?.classList.remove("hidden");
document.body.classList.add("auth-required");
const botUsername = state.authConfig?.bot_username;
if (message) {
message.textContent = "Это приложение открывается через Telegram-бота. Откройте Mini App из Telegram.";
message.textContent = botUsername
? "Откройте CarPass через Telegram. Бот привяжет гараж к вашему аккаунту и покажет кнопку Mini App."
: "Это приложение открывается через Telegram-бота. Настройте BOT_USERNAME на сервере.";
}
if (slot && !slot.dataset.ready) slot.textContent = "";
if (note) {
note.textContent = isMobileBrowser()
? "После перехода в Telegram нажмите в боте кнопку «Открыть гараж»."
: "На компьютере можно войти кнопкой Telegram ниже или открыть бота.";
}
if (slot) slot.textContent = "";
if (!botUsername) {
return;
}
if (link) {
link.href = `https://t.me/${botUsername}`;
link.href = telegramBotUrl(botUsername);
link.target = isMobileBrowser() ? "_self" : "_blank";
link.classList.remove("hidden");
}
}
@@ -549,6 +558,11 @@ async function showTelegramLogin() {
if (!slot || slot.dataset.ready) return;
const botUsername = state.authConfig?.bot_username;
if (!botUsername) return;
if (isMobileBrowser()) {
slot.innerHTML = `<span class="telegram-login-help">В мобильном браузере авторизация проходит через Telegram-бота.</span>`;
slot.dataset.ready = "true";
return;
}
window.onTelegramAuth = async (user) => {
state.user = await api("/users/telegram-login", {
method: "POST",
@@ -573,6 +587,14 @@ async function showTelegramLogin() {
slot.appendChild(script);
}
function isMobileBrowser() {
return /Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent);
}
function telegramBotUrl(botUsername) {
return `https://t.me/${botUsername}?start=garage`;
}
async function savePushSubscription(subscription) {
if (!state.user || !subscription) return;
await api(`/users/${state.user.id}/push-subscriptions`, {
@@ -750,7 +772,7 @@ function renderCars() {
(car) => `
<button class="car-item ${car.id === state.selectedCarId ? "active" : ""}" data-car="${car.id}">
<span class="car-badge">${(car.make || car.name).slice(0, 2).toUpperCase()}</span>
<span>
<span class="car-copy">
<strong>${car.name}</strong>
<small>${[car.make, car.model, car.trim, car.year, car.fuel_type].filter(Boolean).join(" ") || t("Без деталей")}</small>
</span>
@@ -1333,6 +1355,15 @@ document.querySelector("#refreshBtn").addEventListener("click", (event) => {
});
});
document.querySelector("#telegramRetryBtn")?.addEventListener("click", () => {
runAction(document.querySelector("#telegramRetryBtn"), "Обновляю данные...", async () => {
await ensureUser();
await loadCatalog();
initCarCatalog();
await loadCars();
});
});
document.querySelector("#periodPreset").addEventListener("change", async (event) => {
await runAction(event.currentTarget, "Обновляю данные...", async () => {
applyPeriodPreset(event.currentTarget.value);

View File

@@ -1292,8 +1292,8 @@ select {
.auth-panel,
.scan-panel {
width: min(420px, 100%);
padding: 20px;
width: min(460px, 100%);
padding: 22px;
border: 1px solid var(--line);
border-radius: 8px;
background: #fff;
@@ -1305,8 +1305,16 @@ select {
color: var(--muted);
}
.auth-actions {
display: grid;
gap: 10px;
}
.telegram-login-slot {
min-height: 46px;
display: grid;
min-height: 42px;
place-items: center;
margin-top: 12px;
}
.telegram-login-link {
@@ -1323,6 +1331,26 @@ select {
text-decoration: none;
}
.telegram-secondary-btn {
width: 100%;
min-height: 44px;
background: #fff;
color: var(--text);
border: 1px solid var(--line);
box-shadow: none;
}
.telegram-login-help,
.auth-note {
color: var(--muted);
font-size: 13px;
text-align: center;
}
.auth-note {
margin: 12px 0 0;
}
.telegram-login-link.hidden {
display: none;
}
@@ -1451,3 +1479,103 @@ select {
max-height: 92vh;
}
}
@media (max-width: 640px) {
body {
overflow-x: hidden;
}
.shell {
width: 100%;
max-width: 100vw;
padding-inline: 10px;
}
.hero-grid {
display: grid;
grid-template-columns: 1fr;
overflow: visible;
}
.summary-card {
min-width: 0;
}
.layout,
.panel,
.workspace {
min-width: 0;
width: 100%;
}
.panel {
padding: 12px;
}
.cars {
display: grid;
grid-template-columns: 1fr;
overflow: visible;
max-width: 100%;
}
.car-item {
grid-template-columns: 38px minmax(0, 1fr);
min-width: 0;
max-width: 100%;
width: 100%;
padding-inline: 10px;
}
.car-badge {
width: 38px;
height: 38px;
}
.car-copy {
min-width: 0;
}
.car-copy strong,
.car-copy small {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.passport-head {
display: grid;
grid-template-columns: 1fr auto;
}
.score-ring {
width: 72px;
height: 72px;
flex-basis: 72px;
}
.quick-actions {
grid-template-columns: repeat(3, minmax(96px, 1fr));
overflow-x: auto;
padding-bottom: 8px;
}
.report-records,
.history,
.record {
max-width: 100%;
overflow-wrap: anywhere;
}
.auth-overlay {
align-items: stretch;
padding: 14px;
}
.auth-panel {
align-self: center;
width: 100%;
padding: 20px;
}
}