miniapp: phone validation on intro — blocks transition with bad number

- New isValidPhone(raw): checks 11-digit Russian after normalization (8/7/+7/9-prefix)
- Intro 'Начать' button now custom click handler instead of data-go
- Validates name (non-empty) and phone (Russian format)
- Inline .field-error red message under invalid field
- .field-hint shows format help under phone input
- Haptic 'warning' feedback on invalid submit
- Phone is auto-normalized to '+7 900 123-45-67' before transition
This commit is contained in:
wasrusgen 2026-05-11 16:48:52 +03:00
parent 0f2635d5f8
commit 5ceffa4f69
4 changed files with 92 additions and 10 deletions

View File

@ -148,6 +148,27 @@
margin: 0; margin: 0;
} }
/* ----- Form errors / hints ----- */
.field-error {
font-family: var(--font-mono);
font-size: 11px;
letter-spacing: 0.02em;
color: #8A3E2A;
min-height: 14px;
margin-top: 4px;
line-height: 1.3;
}
.field-error:empty { display: none; }
.field-hint {
font-family: var(--font-mono);
font-size: 10px;
letter-spacing: 0.04em;
color: var(--muted);
margin-top: 4px;
line-height: 1.3;
}
/* ----- Form basics ----- */ /* ----- Form basics ----- */
.field { .field {
display: flex; display: flex;

View File

@ -178,22 +178,74 @@ const Podbor = (function () {
<label class="field"> <label class="field">
<span class="field-label">Клиент</span> <span class="field-label">Клиент</span>
<input type="text" data-bind="client_name" value="${state.client_name || ""}" placeholder="Например: А. Пестова"> <input type="text" data-bind="client_name" value="${state.client_name || ""}" placeholder="Например: А. Пестова">
<span class="field-error" id="nameError"></span>
</label> </label>
</div> </div>
<div class="form-row"> <div class="form-row">
<label class="field"> <label class="field">
<span class="field-label">Телефон</span> <span class="field-label">Телефон</span>
<input type="tel" data-bind="client_phone" value="${state.client_phone || ""}" placeholder="+7 ..."> <input type="tel" data-bind="client_phone" value="${state.client_phone || ""}" placeholder="+7 900 123-45-67">
<span class="field-hint">Формат: +7, 8, или 10 цифр начиная с 9</span>
<span class="field-error" id="phoneError"></span>
</label> </label>
</div> </div>
<div class="podbor-cta-row"> <div class="podbor-cta-row">
<button class="btn-primary" data-go="categories">Начать</button> <button class="btn-primary" id="introNext">Начать</button>
</div> </div>
</section> </section>
`); `);
bindInputs(node); bindInputs(node);
bindNav(node);
const phoneInput = node.querySelector("input[data-bind='client_phone']");
const phoneError = node.querySelector("#phoneError");
const nameInput = node.querySelector("input[data-bind='client_name']");
const nameError = node.querySelector("#nameError");
// Валидация на blur — мягкие подсказки
phoneInput.addEventListener("blur", () => {
const v = phoneInput.value.trim();
if (v && !isValidPhone(v)) {
phoneError.textContent = "Похоже на неполный номер. Нужно 11 цифр (или 10 с цифры 9)";
} else {
phoneError.textContent = "";
}
});
node.querySelector("#introNext").addEventListener("click", () => {
// Имя
const name = (state.client_name || "").trim();
if (!name) {
nameError.textContent = "Укажите имя клиента";
nameInput.focus();
haptic && haptic("warning");
return;
} else {
nameError.textContent = "";
}
// Телефон
const phone = (state.client_phone || "").trim();
if (!phone) {
phoneError.textContent = "Укажите телефон клиента";
phoneInput.focus();
haptic && haptic("warning");
return;
}
if (!isValidPhone(phone)) {
phoneError.textContent = "Неверный формат. Пример: +7 900 123-45-67 или 89001234567";
phoneInput.focus();
haptic && haptic("warning");
return;
}
// Нормализуем перед переходом
const normalized = normalizePhone(phone);
if (normalized !== phone) {
update({ client_phone: normalized });
}
go("categories");
});
return node; return node;
} }
@ -1539,6 +1591,15 @@ const Podbor = (function () {
return `+7 ${d.slice(1, 4)} ${d.slice(4, 7)}-${d.slice(7, 9)}-${d.slice(9, 11)}`; return `+7 ${d.slice(1, 4)} ${d.slice(4, 7)}-${d.slice(7, 9)}-${d.slice(9, 11)}`;
} }
/* Проверка: получится ли валидный РФ-номер из введённого. */
function isValidPhone(raw) {
if (!raw) return false;
let d = raw.replace(/\D/g, "");
if (d.length === 11 && d.startsWith("8")) d = "7" + d.slice(1);
if (d.length === 10 && d.startsWith("9")) d = "7" + d;
return d.length === 11 && d.startsWith("7");
}
function bindNav(node) { function bindNav(node) {
node.querySelectorAll("[data-go]").forEach(b => { node.querySelectorAll("[data-go]").forEach(b => {
b.addEventListener("click", () => go(b.dataset.go)); b.addEventListener("click", () => go(b.dataset.go));

View File

@ -12,8 +12,8 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Geist:wght@400;500;600&family=Newsreader:ital,wght@0,400..600;1,400..600&family=Instrument+Serif:ital@0;1&family=JetBrains+Mono:wght@400;500&display=swap"> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Geist:wght@400;500;600&family=Newsreader:ital,wght@0,400..600;1,400..600&family=Instrument+Serif:ital@0;1&family=JetBrains+Mono:wght@400;500&display=swap">
<script src="https://telegram.org/js/telegram-web-app.js"></script> <script src="https://telegram.org/js/telegram-web-app.js"></script>
<link rel="stylesheet" href="assets/styles.css?v=20260511i"> <link rel="stylesheet" href="assets/styles.css?v=20260511j">
<link rel="stylesheet" href="assets/podbor.css?v=20260511i"> <link rel="stylesheet" href="assets/podbor.css?v=20260511j">
</head> </head>
<body> <body>
<main id="app"> <main id="app">
@ -21,10 +21,10 @@
<div class="spinner"></div> <div class="spinner"></div>
</div> </div>
</main> </main>
<script src="assets/icons.js?v=20260511i"></script> <script src="assets/icons.js?v=20260511j"></script>
<script src="assets/podbor.config.js?v=20260511i"></script> <script src="assets/podbor.config.js?v=20260511j"></script>
<script src="assets/podbor.picts.js?v=20260511i"></script> <script src="assets/podbor.picts.js?v=20260511j"></script>
<script src="assets/podbor.js?v=20260511i"></script> <script src="assets/podbor.js?v=20260511j"></script>
<script src="assets/app.js?v=20260511i"></script> <script src="assets/app.js?v=20260511j"></script>
</body> </body>
</html> </html>

0
wb.json Normal file
View File