mirror of
https://github.com/wasrusgen/zov-tech.git
synced 2026-06-03 19:04:49 +00:00
miniapp: 4 UX fixes from user feedback
1. PHONE NORMALIZATION - On blur (or before submit): '9001234567' -> '+7 900 123-45-67' - Handles 8XXX, 7XXX, +7XXX, 10-digit mobile prefixes - Leaves untouched if not Russian-looking number 2. BRAND LIST FOR RF 2026 - PODBOR_SINGLE_BRAND_OPTIONS updated with realistic 2026 brands - Promoted: Haier, Korting, Midea, Hisense, Бирюса, Атлант, Pozis, DEXP - Bosch/Siemens marked with ⚠ (parallel-import) - Miele/Liebherr/Smeg also marked ⚠ - PODBOR_BRANDS per-category fully refreshed 3. BUDGET ADAPTIVE HINTS - Hints now scale by selected categories share of full kitchen - Just fridge picked → 'Средний' shows ~88-175 тыс instead of 350-700к - Full 8 categories → original 350-700к - PODBOR_BUDGET_SHARES + PODBOR_BUDGET_RANGES constants 4. INFRA STEP CONDITIONAL - Stove power question only shown if hob category picked - Vent question only shown if hood category picked - If neither → step auto-skips to summary (with brief notice) - Summary 'Назад' button respects skip — goes to strategy if needed
This commit is contained in:
parent
44281b1e07
commit
80580db446
@ -50,30 +50,56 @@ const PODBOR_BRAND_STRATEGY = [
|
||||
{ key: "different", label: "Разные марки по категориям", hint: "соберём оптимальный микс" },
|
||||
];
|
||||
|
||||
/* Бренды, у которых есть полная линейка кухонной техники (для single-mode) */
|
||||
/* Бренды, у которых есть полная линейка кухонной техники, реально доступные в РФ (2026).
|
||||
tier: premium / middle / budget · note: "available" | "parallel" (параллельный импорт). */
|
||||
const PODBOR_SINGLE_BRAND_OPTIONS = [
|
||||
{ key: "miele", label: "Miele", tier: "premium" },
|
||||
{ key: "gaggenau", label: "Gaggenau", tier: "premium" },
|
||||
{ key: "asko", label: "Asko", tier: "premium" },
|
||||
{ key: "v_zug", label: "V-ZUG", tier: "premium" },
|
||||
{ key: "neff", label: "Neff", tier: "middle" },
|
||||
{ key: "bosch", label: "Bosch", tier: "middle" },
|
||||
{ key: "siemens", label: "Siemens", tier: "middle" },
|
||||
{ key: "electrolux", label: "Electrolux", tier: "middle" },
|
||||
{ key: "aeg", label: "AEG", tier: "middle" },
|
||||
{ key: "samsung", label: "Samsung", tier: "middle" },
|
||||
{ key: "lg", label: "LG", tier: "middle" },
|
||||
{ key: "hansa", label: "Hansa", tier: "budget" },
|
||||
{ key: "beko", label: "Beko", tier: "budget" },
|
||||
// Премиум — официально или через параллельный импорт
|
||||
{ key: "miele", label: "Miele", tier: "premium", note: "parallel" },
|
||||
{ key: "asko", label: "Asko", tier: "premium", note: "available" },
|
||||
{ key: "smeg", label: "Smeg", tier: "premium", note: "parallel" },
|
||||
{ key: "gorenje", label: "Gorenje", tier: "premium", note: "available" },
|
||||
|
||||
// Средний — реально работающие бренды
|
||||
{ key: "haier", label: "Haier", tier: "middle", note: "available" },
|
||||
{ key: "samsung", label: "Samsung", tier: "middle", note: "available" },
|
||||
{ key: "lg", label: "LG", tier: "middle", note: "available" },
|
||||
{ key: "korting", label: "Körting", tier: "middle", note: "available" },
|
||||
{ key: "midea", label: "Midea", tier: "middle", note: "available" },
|
||||
{ key: "bosch", label: "Bosch ⚠", tier: "middle", note: "parallel" },
|
||||
{ key: "siemens", label: "Siemens ⚠", tier: "middle", note: "parallel" },
|
||||
|
||||
// Бюджет — российские/китайские
|
||||
{ key: "biryusa", label: "Бирюса", tier: "budget", note: "available" },
|
||||
{ key: "atlant", label: "Атлант", tier: "budget", note: "available" },
|
||||
{ key: "pozis", label: "Pozis", tier: "budget", note: "available" },
|
||||
{ key: "hisense", label: "Hisense", tier: "budget", note: "available" },
|
||||
{ key: "hansa", label: "Hansa", tier: "budget", note: "available" },
|
||||
{ key: "dexp", label: "DEXP", tier: "budget", note: "available" },
|
||||
|
||||
{ key: "ai_pick", label: "Пусть AI выберет под бюджет", recommended: true },
|
||||
];
|
||||
|
||||
/* Доля бюджета каждой категории от полного комплекта (для адаптивных вилок). */
|
||||
const PODBOR_BUDGET_SHARES = {
|
||||
fridge: 25, hob: 12, oven: 15, dw: 10,
|
||||
hood: 8, microwave: 5, coffee: 15, washer: 10,
|
||||
};
|
||||
|
||||
/* Базовые вилки для ПОЛНОГО комплекта 8 категорий (в тыс. ₽).
|
||||
Адаптируются по выбранным категориям через PODBOR_BUDGET_SHARES. */
|
||||
const PODBOR_BUDGET_RANGES = {
|
||||
luxe: { from: 1500, to: 3000 }, // от 1.5М
|
||||
premium: { from: 700, to: 1500 },
|
||||
middle: { from: 350, to: 700 },
|
||||
budget: { from: 100, to: 350 },
|
||||
};
|
||||
|
||||
const PODBOR_BUDGET_PRESETS = [
|
||||
{ key: "luxe", label: "Люкс", hint: "от 1.5М ₽ за весь комплект" },
|
||||
{ key: "premium", label: "Премиум", hint: "700к – 1.5М ₽" },
|
||||
{ key: "middle", label: "Средний", hint: "350к – 700к ₽", recommended: true },
|
||||
{ key: "budget", label: "Бюджет", hint: "до 350к ₽" },
|
||||
{ key: "exact", label: "Точные цифры", hint: "ввести от-до по категориям" },
|
||||
{ key: "luxe", label: "Люкс", desc: "лучшее без оглядки на цену" },
|
||||
{ key: "premium", label: "Премиум", desc: "топовые модели · все опции" },
|
||||
{ key: "middle", label: "Средний", desc: "оптимальный баланс · цена/функции", recommended: true },
|
||||
{ key: "budget", label: "Бюджет", desc: "только нужное" },
|
||||
{ key: "exact", label: "Точные цифры", desc: "вилки от-до по каждой категории" },
|
||||
];
|
||||
|
||||
const PODBOR_PICK_STRATEGIES = [
|
||||
@ -635,47 +661,47 @@ const PODBOR_PARAMS = {
|
||||
},
|
||||
};
|
||||
|
||||
/* Бренды для каждой категории — для чипов с тирами.
|
||||
Сокращённый набор; полный список можно расширить из исходного HTML. */
|
||||
/* Бренды по категориям (актуально на 2026, РФ).
|
||||
⚠ — параллельный импорт, остальные — официально доступны. */
|
||||
const PODBOR_BRANDS = {
|
||||
fridge: {
|
||||
premium: ["Liebherr", "Miele", "Sub-Zero", "V-ZUG"],
|
||||
middle: ["Bosch", "Siemens", "Samsung", "LG"],
|
||||
budget: ["Indesit", "Beko", "Hotpoint"],
|
||||
premium: ["Miele ⚠", "Liebherr ⚠", "Asko", "Gorenje"],
|
||||
middle: ["Haier", "Samsung", "LG", "Korting", "Bosch ⚠", "Siemens ⚠"],
|
||||
budget: ["Бирюса", "Атлант", "Pozis", "Hisense", "Indesit", "Hansa"],
|
||||
},
|
||||
hob: {
|
||||
premium: ["Miele", "Gaggenau", "AEG"],
|
||||
middle: ["Bosch", "Siemens", "Electrolux", "Hansa"],
|
||||
budget: ["Hotpoint", "Beko", "Indesit"],
|
||||
premium: ["Miele ⚠", "Asko", "Gorenje", "Smeg ⚠"],
|
||||
middle: ["Korting", "Haier", "Midea", "Bosch ⚠", "Siemens ⚠"],
|
||||
budget: ["Hansa", "Hisense", "DEXP", "Дарина"],
|
||||
},
|
||||
oven: {
|
||||
premium: ["Miele", "Gaggenau", "Neff"],
|
||||
middle: ["Bosch", "Siemens", "Electrolux", "AEG"],
|
||||
budget: ["Hansa", "Beko", "Hotpoint"],
|
||||
premium: ["Miele ⚠", "Asko", "Gorenje", "Smeg ⚠"],
|
||||
middle: ["Korting", "Haier", "Midea", "Samsung", "Bosch ⚠"],
|
||||
budget: ["Hansa", "Hisense", "DEXP", "Дарина"],
|
||||
},
|
||||
dw: {
|
||||
premium: ["Miele", "Asko", "V-ZUG"],
|
||||
middle: ["Bosch", "Siemens", "Electrolux"],
|
||||
budget: ["Hansa", "Beko", "Indesit"],
|
||||
premium: ["Miele ⚠", "Asko", "Gorenje"],
|
||||
middle: ["Haier", "Midea", "Korting", "Bosch ⚠"],
|
||||
budget: ["Hansa", "Hisense", "Indesit"],
|
||||
},
|
||||
hood: {
|
||||
premium: ["Miele", "Falmec", "Faber"],
|
||||
middle: ["Bosch", "Siemens", "Elica"],
|
||||
budget: ["Hansa", "Hotpoint", "Maunfeld"],
|
||||
premium: ["Miele ⚠", "Falmec ⚠", "Faber ⚠", "Gorenje"],
|
||||
middle: ["Korting", "Maunfeld", "Elikor", "Haier"],
|
||||
budget: ["Hansa", "Hisense", "DEXP", "Krona"],
|
||||
},
|
||||
microwave: {
|
||||
premium: ["Miele", "Neff"],
|
||||
middle: ["Bosch", "Siemens", "Samsung", "LG"],
|
||||
budget: ["Whirlpool", "Hansa", "Beko"],
|
||||
premium: ["Miele ⚠", "Asko"],
|
||||
middle: ["Samsung", "LG", "Haier", "Midea", "Bosch ⚠"],
|
||||
budget: ["Hansa", "Hisense", "DEXP", "Polaris"],
|
||||
},
|
||||
coffee: {
|
||||
premium: ["Miele", "Jura", "De'Longhi PrimaDonna"],
|
||||
middle: ["De'Longhi", "Saeco", "Bosch"],
|
||||
budget: ["Krups", "Philips"],
|
||||
premium: ["Miele ⚠", "Jura ⚠", "Saeco ⚠"],
|
||||
middle: ["De'Longhi ⚠", "Philips ⚠", "Polaris", "Bork ⚠"],
|
||||
budget: ["Polaris", "Redmond", "Kitfort"],
|
||||
},
|
||||
washer: {
|
||||
premium: ["Miele", "Asko", "V-ZUG"],
|
||||
middle: ["Bosch", "Siemens", "Samsung", "LG"],
|
||||
budget: ["Indesit", "Hotpoint", "Beko"],
|
||||
premium: ["Miele ⚠", "Asko", "Gorenje"],
|
||||
middle: ["Haier", "Samsung", "LG", "Korting", "Bosch ⚠"],
|
||||
budget: ["Атлант", "Indesit", "Hansa", "Hisense"],
|
||||
},
|
||||
};
|
||||
|
||||
@ -892,8 +892,26 @@ const Podbor = (function () {
|
||||
|
||||
function renderBudget() {
|
||||
const bp = state.budget_preset || "";
|
||||
|
||||
// Считаем суммарную долю выбранных категорий от полного комплекта
|
||||
const share = (state.categories || []).reduce(
|
||||
(s, c) => s + (PODBOR_BUDGET_SHARES[c] || 0), 0
|
||||
) / 100;
|
||||
const shareSafe = share > 0 ? share : 1;
|
||||
|
||||
// Подмешиваем hint с реальной вилкой для выбранных категорий
|
||||
const presetsWithRange = PODBOR_BUDGET_PRESETS.map(o => {
|
||||
if (o.key === "exact") return { ...o, hint: o.desc };
|
||||
const r = PODBOR_BUDGET_RANGES[o.key];
|
||||
if (!r) return { ...o, hint: o.desc };
|
||||
const from = Math.round(r.from * shareSafe);
|
||||
const to = Math.round(r.to * shareSafe);
|
||||
const label = from >= 1000 ? `${(from/1000).toFixed(1)}–${(to/1000).toFixed(1)}М ₽` : `${from}–${to} тыс ₽`;
|
||||
return { ...o, hint: `${label} · ${o.desc}` };
|
||||
});
|
||||
|
||||
const presetGrid = renderPinCards(
|
||||
PODBOR_BUDGET_PRESETS,
|
||||
presetsWithRange,
|
||||
o => (bp === o.key ? "on" : ""),
|
||||
key => { update({ budget_preset: key }); render(); }
|
||||
);
|
||||
@ -1010,10 +1028,24 @@ const Podbor = (function () {
|
||||
/* ===================== Step: infra ===================== */
|
||||
|
||||
function renderInfra() {
|
||||
const node = el(`
|
||||
const cats = state.categories || [];
|
||||
const askStove = cats.includes("hob");
|
||||
const askVent = cats.includes("hood");
|
||||
|
||||
// Если ни одна из релевантных категорий не выбрана — пропускаем шаг
|
||||
if (!askStove && !askVent) {
|
||||
// Автопереход на summary через микропаузу (чтобы пользователь увидел)
|
||||
setTimeout(() => { go("summary"); }, 50);
|
||||
return el(`
|
||||
<section class="podbor-step">
|
||||
<h2 class="display-title">Инфраструктура<br><span class="accent">кухни</span></h2>
|
||||
<p class="lede">Газ или электрика — определит тип варочной. Подключение вытяжки — нужны ли выводы или угольный фильтр.</p>
|
||||
<p class="lede" style="text-align:center;padding:30px;">
|
||||
Инфраструктурные вопросы для выбранных категорий не требуются — переходим к итогу...
|
||||
</p>
|
||||
</section>
|
||||
`);
|
||||
}
|
||||
|
||||
const stoveBlock = askStove ? `
|
||||
<div class="block">
|
||||
<div class="block-head">Подключение варочной</div>
|
||||
<div class="opt-list">
|
||||
@ -1022,6 +1054,9 @@ const Podbor = (function () {
|
||||
`).join("")}
|
||||
</div>
|
||||
</div>
|
||||
` : "";
|
||||
|
||||
const ventBlock = askVent ? `
|
||||
<div class="block">
|
||||
<div class="block-head">Вытяжка → внутридомовая вентиляция?</div>
|
||||
<div class="opt-list">
|
||||
@ -1031,6 +1066,20 @@ const Podbor = (function () {
|
||||
</div>
|
||||
<div class="hint">Если «Нет» — менеджер закладывает угольный фильтр. Если «Да» — заранее планируем выводы.</div>
|
||||
</div>
|
||||
` : "";
|
||||
|
||||
const lede = (askStove && askVent)
|
||||
? "Газ или электрика — определит тип варочной. Подключение вытяжки — нужны ли выводы или угольный фильтр."
|
||||
: askStove
|
||||
? "Газ или электрика — определит тип варочной (индукция / стеклокерамика / газ)."
|
||||
: "Подключение вытяжки — нужны ли выводы в вентшахту или угольный фильтр.";
|
||||
|
||||
const node = el(`
|
||||
<section class="podbor-step">
|
||||
<h2 class="display-title">Инфраструктура<br><span class="accent">кухни</span></h2>
|
||||
<p class="lede">${lede}</p>
|
||||
${stoveBlock}
|
||||
${ventBlock}
|
||||
<div class="podbor-cta-row">
|
||||
<button class="btn-secondary" data-go="strategy">Назад</button>
|
||||
<button class="btn-primary" data-go="summary">Дальше</button>
|
||||
@ -1103,7 +1152,7 @@ const Podbor = (function () {
|
||||
</label>
|
||||
|
||||
<div class="podbor-cta-row">
|
||||
<button class="btn-secondary" data-go="infra">Назад</button>
|
||||
<button class="btn-secondary" id="summaryBack">Назад</button>
|
||||
<button class="btn-primary" id="submitBtn">Отправить · AI подберёт</button>
|
||||
</div>
|
||||
|
||||
@ -1112,6 +1161,12 @@ const Podbor = (function () {
|
||||
`);
|
||||
bindInputs(node);
|
||||
bindNav(node);
|
||||
// Кнопка "Назад" — обходим infra если она авто-пропускается
|
||||
node.querySelector("#summaryBack").addEventListener("click", () => {
|
||||
const cats = state.categories || [];
|
||||
const goTo = (cats.includes("hob") || cats.includes("hood")) ? "infra" : "strategy";
|
||||
go(goTo);
|
||||
});
|
||||
node.querySelector("#submitBtn").addEventListener("click", () => onSubmit(node));
|
||||
return node;
|
||||
}
|
||||
@ -1131,6 +1186,11 @@ const Podbor = (function () {
|
||||
}
|
||||
|
||||
try {
|
||||
// Финальная нормализация телефона перед отправкой
|
||||
const normPhone = normalizePhone(state.client_phone || "");
|
||||
if (normPhone && normPhone !== state.client_phone) {
|
||||
update({ client_phone: normPhone });
|
||||
}
|
||||
const res = await fetch(`${BACKEND_URL}/api/podbor`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
@ -1335,8 +1395,30 @@ const Podbor = (function () {
|
||||
inp.addEventListener("input", e => {
|
||||
update({ [e.target.dataset.bind]: e.target.value });
|
||||
});
|
||||
// Нормализация телефона на blur
|
||||
if (inp.dataset.bind === "client_phone") {
|
||||
inp.addEventListener("blur", e => {
|
||||
const normalized = normalizePhone(e.target.value);
|
||||
if (normalized && normalized !== e.target.value) {
|
||||
e.target.value = normalized;
|
||||
update({ client_phone: normalized });
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* Приводим телефон к единому формату +7 XXX XXX-XX-XX.
|
||||
Принимает: 8XXXXXXXXXX, 7XXXXXXXXXX, +7XXXXXXXXXX, 9XXXXXXXXX (без префикса). */
|
||||
function normalizePhone(raw) {
|
||||
if (!raw) return "";
|
||||
const digits = raw.replace(/\D/g, "");
|
||||
let d = digits;
|
||||
if (d.length === 11 && d.startsWith("8")) d = "7" + d.slice(1);
|
||||
if (d.length === 10 && d.startsWith("9")) d = "7" + d; // мобильный без префикса
|
||||
if (d.length !== 11 || !d.startsWith("7")) return raw.trim(); // не похоже на РФ-номер — не трогаем
|
||||
return `+7 ${d.slice(1, 4)} ${d.slice(4, 7)}-${d.slice(7, 9)}-${d.slice(9, 11)}`;
|
||||
}
|
||||
|
||||
function bindNav(node) {
|
||||
node.querySelectorAll("[data-go]").forEach(b => {
|
||||
|
||||
@ -12,8 +12,8 @@
|
||||
<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">
|
||||
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
||||
<link rel="stylesheet" href="assets/styles.css?v=20260511e">
|
||||
<link rel="stylesheet" href="assets/podbor.css?v=20260511e">
|
||||
<link rel="stylesheet" href="assets/styles.css?v=20260511f">
|
||||
<link rel="stylesheet" href="assets/podbor.css?v=20260511f">
|
||||
</head>
|
||||
<body>
|
||||
<main id="app">
|
||||
@ -21,10 +21,10 @@
|
||||
<div class="spinner"></div>
|
||||
</div>
|
||||
</main>
|
||||
<script src="assets/icons.js?v=20260511e"></script>
|
||||
<script src="assets/podbor.config.js?v=20260511e"></script>
|
||||
<script src="assets/podbor.picts.js?v=20260511e"></script>
|
||||
<script src="assets/podbor.js?v=20260511e"></script>
|
||||
<script src="assets/app.js?v=20260511e"></script>
|
||||
<script src="assets/icons.js?v=20260511f"></script>
|
||||
<script src="assets/podbor.config.js?v=20260511f"></script>
|
||||
<script src="assets/podbor.picts.js?v=20260511f"></script>
|
||||
<script src="assets/podbor.js?v=20260511f"></script>
|
||||
<script src="assets/app.js?v=20260511f"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user