Polish mobile auth and garage layout
This commit is contained in:
15
bot/main.py
15
bot/main.py
@@ -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"))
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user