From 17b112f061decb95a8eaa9f050b7008f2fdeb158 Mon Sep 17 00:00:00 2001 From: wasrusgen Date: Sun, 10 May 2026 23:57:03 +0300 Subject: [PATCH] miniapp: hierarchical wizard for fridge category (style D pictograms) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New PODBOR_PARAMS schema with steps[] supporting single/multi + optionsBy branches - 11 fridge SVG pictograms in podbor.picts.js (style D — 3D perspective with shadow) - renderCategoryWizard with step-by-step flow, chips for prior answers, review screen - Legacy renderCategoryDetail still used for other 7 categories until migrated - Auto-advance on single-select, Дальше button for multi-select - Backend-compatible: per_cat[catKey].answers replaces .params/.features --- .tmp_ssh.py | 71 ++ .../A _ Editorial Calm _ _ _ _ _.html | 608 ++++++++++++++++++ ...Clean _ _ _ blueprint_ italic display.html | 608 ++++++++++++++++++ design-drafts/README.md | 27 + design-drafts/_css_A.css | 0 design-drafts/_css_C.css | 0 ...cted_A___Editorial_Calm__________.html.txt | 76 +++ ...n_______blueprint__italic_display.html.txt | 76 +++ miniapp/assets/podbor.config.js | 112 +++- miniapp/assets/podbor.css | 214 ++++++ miniapp/assets/podbor.js | 289 ++++++++- miniapp/assets/podbor.picts.js | 286 ++++++++ miniapp/index.html | 13 +- 13 files changed, 2332 insertions(+), 48 deletions(-) create mode 100644 .tmp_ssh.py create mode 100644 design-drafts/A _ Editorial Calm _ _ _ _ _.html create mode 100644 design-drafts/C _ Architectural Clean _ _ _ blueprint_ italic display.html create mode 100644 design-drafts/README.md create mode 100644 design-drafts/_css_A.css create mode 100644 design-drafts/_css_C.css create mode 100644 design-drafts/_extracted_A___Editorial_Calm__________.html.txt create mode 100644 design-drafts/_extracted_C___Architectural_Clean_______blueprint__italic_display.html.txt create mode 100644 miniapp/assets/podbor.picts.js diff --git a/.tmp_ssh.py b/.tmp_ssh.py new file mode 100644 index 0000000..8d0c787 --- /dev/null +++ b/.tmp_ssh.py @@ -0,0 +1,71 @@ +"""Tiny SSH helper for VPS setup. Used only in this session. +Usage: + python .tmp_ssh.py exec "" + python .tmp_ssh.py upload +""" +import sys, os, io, paramiko +# Force UTF-8 stdout so Cyrillic / arrows survive on Windows cp1251 console +try: + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace") + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace") +except Exception: + pass + +HOST = "94.241.170.144" +USER = "root" +PASS = "qywz*aCXwL2Sr7" +KEY_PATH = os.path.expanduser("~/.ssh/zov_vps_ed25519") + + +def make_client(): + client = paramiko.SSHClient() + client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + # Try key first, fall back to password + try: + client.connect(HOST, username=USER, key_filename=KEY_PATH, timeout=15) + except (paramiko.AuthenticationException, paramiko.SSHException, OSError): + client.connect(HOST, username=USER, password=PASS, timeout=15, allow_agent=False, look_for_keys=False) + return client + + +def run(cmd, timeout=120): + c = make_client() + try: + stdin, stdout, stderr = c.exec_command(cmd, timeout=timeout) + rc = stdout.channel.recv_exit_status() + out = stdout.read().decode("utf-8", errors="replace") + err = stderr.read().decode("utf-8", errors="replace") + return rc, out, err + finally: + c.close() + + +def upload(local, remote): + c = make_client() + try: + sftp = c.open_sftp() + sftp.put(local, remote) + sftp.close() + finally: + c.close() + + +if __name__ == "__main__": + args = sys.argv[1:] + if not args: + print("commands: exec | upload ") + sys.exit(1) + op = args[0] + if op == "exec": + rc, out, err = run(" ".join(args[1:])) + if out: + sys.stdout.write(out) + if err: + sys.stderr.write(err) + sys.exit(rc) + elif op == "upload": + upload(args[1], args[2]) + print("uploaded") + else: + print("unknown op:", op) + sys.exit(1) diff --git a/design-drafts/A _ Editorial Calm _ _ _ _ _.html b/design-drafts/A _ Editorial Calm _ _ _ _ _.html new file mode 100644 index 0000000..7135152 --- /dev/null +++ b/design-drafts/A _ Editorial Calm _ _ _ _ _.html @@ -0,0 +1,608 @@ +A _ Editorial Calm _ _ _ _ _
9:41
Коммуникации
Развёртка стен · нажмите, чтобы добавить
столешница 850
120
600
600
850
1100
Список точек · 5
Розетка ×2
высота 120 мм · стена B
Холодная
высота 600 мм · стена B
Горячая
высота 600 мм · стена B
Газ ø15
высота 850 мм · стена B
Над столешн.
высота 1100 мм · стена B
\ No newline at end of file diff --git a/design-drafts/C _ Architectural Clean _ _ _ blueprint_ italic display.html b/design-drafts/C _ Architectural Clean _ _ _ blueprint_ italic display.html new file mode 100644 index 0000000..3218d1f --- /dev/null +++ b/design-drafts/C _ Architectural Clean _ _ _ blueprint_ italic display.html @@ -0,0 +1,608 @@ +C _ Architectural Clean _ _ _ blueprint_ italic display
9:41
Проект
№ K-2025-0184

И. Тимирясов

пос. Барвиха, дом 8
Текущий этап
95% · до 11 мая

Монтаж

Замер
Проект
Согласование
Производство
Доставка
Монтаж
Этапы
ЗамерГотово
Замерил Артём О. · 02 мая
ПроектГотово
Проект v3 · Е. Маркина
СогласованиеГотово
Ожидает подписи клиента
ПроизводствоГотово
Цех №2 · Партия 18
ДоставкаГотово
Курьер выезжает
06
МонтажСейчас
Бригада 4
Действия
\ No newline at end of file diff --git a/design-drafts/README.md b/design-drafts/README.md new file mode 100644 index 0000000..8d9375d --- /dev/null +++ b/design-drafts/README.md @@ -0,0 +1,27 @@ +# Design drafts + +Сюда складываются HTML/изображения с дизайн-концепциями MiniApp, сгенерированные через Claude Skills (Frontend design, Interactive prototype, и т.п.). + +## Workflow + +1. В Claude.ai прикрепить скил **Frontend design** или **Interactive prototype**. +2. Сгенерировать концепцию. +3. **Save as standalone HTML** → положить файл сюда (имя свободное, например `option-1-ios.html`). +4. Сказать «вот вариант» — я перенесу стили / лейаут / типографику в реальный `miniapp/`. + +## Что внутри концепции должно быть видно + +Минимум: +- Шапка профиля (имя, салон, статус) +- Список разделов меню +- Хотя бы один из подэкранов (например, «Подбор техники» — чек-лист) + +Желательно: +- Светлая и тёмная тема +- Состояние active / lapsed для статуса менеджера +- Пустые состояния («Заявок пока нет») + +## Не складывать сюда + +- Реальный код продакшена — он в `miniapp/` +- Скриншоты можно, но HTML предпочтительнее (легче перенести стили) diff --git a/design-drafts/_css_A.css b/design-drafts/_css_A.css new file mode 100644 index 0000000..e69de29 diff --git a/design-drafts/_css_C.css b/design-drafts/_css_C.css new file mode 100644 index 0000000..e69de29 diff --git a/design-drafts/_extracted_A___Editorial_Calm__________.html.txt b/design-drafts/_extracted_A___Editorial_Calm__________.html.txt new file mode 100644 index 0000000..1227935 --- /dev/null +++ b/design-drafts/_extracted_A___Editorial_Calm__________.html.txt @@ -0,0 +1,76 @@ +A _ Editorial Calm _ _ _ _ _
9:41
Коммуникации
Развёртка стен · нажмите, чтобы добавить
столешница 850
120
600
600
850
1100
Список точек · 5
Розетка ×2
высота 120 мм · стена B
Холодная
высота 600 мм · стена B
Горячая
высота 600 мм · стена B
Газ ø15
высота 850 мм · стена B
Над столешн.
высота 1100 мм · стена B
\ No newline at end of file diff --git a/design-drafts/_extracted_C___Architectural_Clean_______blueprint__italic_display.html.txt b/design-drafts/_extracted_C___Architectural_Clean_______blueprint__italic_display.html.txt new file mode 100644 index 0000000..c875b7d --- /dev/null +++ b/design-drafts/_extracted_C___Architectural_Clean_______blueprint__italic_display.html.txt @@ -0,0 +1,76 @@ +C _ Architectural Clean _ _ _ blueprint_ italic display
9:41
Проект
№ K-2025-0184

И. Тимирясов

пос. Барвиха, дом 8
Текущий этап
95% · до 11 мая

Монтаж

Замер
Проект
Согласование
Производство
Доставка
Монтаж
Этапы
ЗамерГотово
Замерил Артём О. · 02 мая
ПроектГотово
Проект v3 · Е. Маркина
СогласованиеГотово
Ожидает подписи клиента
ПроизводствоГотово
Цех №2 · Партия 18
ДоставкаГотово
Курьер выезжает
06
МонтажСейчас
Бригада 4
Действия
\ No newline at end of file diff --git a/miniapp/assets/podbor.config.js b/miniapp/assets/podbor.config.js index 92c815f..4ad2fb4 100644 --- a/miniapp/assets/podbor.config.js +++ b/miniapp/assets/podbor.config.js @@ -42,42 +42,86 @@ const PODBOR_PRIORITIES = [ { key: "service", label: "Сервис и гарантия" }, ]; -/* Параметры по категориям: Главное (всегда видно) + Подробнее (свёрнуто) */ +/* Параметры по категориям. + ---------------------------------------------------------- + Новая схема (иерархический wizard): + steps: [ + { + key: "install", + title: "Тип установки", + type: "single" | "multi", + options: [ { key, label, hint, star?, pict? } ] + // ИЛИ если опции зависят от предыдущего шага: + optionsBy: { dependsOn: "", map: { : [options] } } + }, + ... + ] + + Старая схема (legacy, без wizard): + primary: [...], features: [...] + ---------------------------------------------------------- */ const PODBOR_PARAMS = { fridge: { - primary: [ - { key: "type", label: "Тип", options: [ - { key: "two_chamber", label: "Двухкамерный" }, - { key: "sbs", label: "Side-by-side" }, - { key: "french", label: "French Door" }, - { key: "column", label: "Колонна (встр.)" }, - { key: "combi", label: "Комбинированный" }, - ]}, - { key: "width", label: "Ширина, см", options: [ - { key: "54", label: "54" }, { key: "60", label: "60" }, - { key: "70", label: "70" }, { key: "75", label: "75" }, { key: "91", label: "91" }, - ]}, - { key: "volume", label: "Объём, л", options: [ - { key: "to300", label: "до 300" }, - { key: "300-450", label: "300–450" }, - { key: "450-600", label: "450–600" }, - { key: "600+", label: "600+" }, - ]}, - { key: "color", label: "Цвет", options: [ - { key: "white", label: "Белый" }, - { key: "inox", label: "Нерж. сталь" }, - { key: "black", label: "Чёрный" }, - { key: "anthracite", label: "Антрацит" }, - { key: "builtin", label: "Под фасад" }, - ]}, - ], - features: [ - { key: "nofrost", label: "NoFrost", hint: "не нужно размораживать вручную" }, - { key: "inverter", label: "Инвертор", hint: "тише и экономичнее на ~30%" }, - { key: "freshzone", label: "Зона свежести", hint: "овощи и зелень дольше хрустящие" }, - { key: "silent", label: "≤40 дБ", hint: "почти не слышно ночью" }, - { key: "smart", label: "Smart / Wi-Fi", hint: "управление с телефона" }, - { key: "ice", label: "Лёдогенератор", hint: "автоматически делает кубики" }, + steps: [ + { + key: "install", + title: "Тип установки", + type: "single", + options: [ + { key: "built_in", label: "Встроенный", hint: "под фасад", pict: "fridge_install_builtin" }, + { key: "freestanding", label: "Отдельностоящий", hint: "соло на полу", pict: "fridge_install_freestanding" }, + ], + }, + { + key: "chamber", + title: "Тип камеры", + type: "single", + optionsBy: { + dependsOn: "install", + map: { + built_in: [ + { key: "single", label: "Однокамерный", hint: "только холод", pict: "fridge_bi_single" }, + { key: "two_chamber", label: "Двухкамерный", hint: "холод + мороз", pict: "fridge_bi_two" }, + { key: "col_cold", label: "Холодильная колонна", hint: "только холод · высокая", pict: "fridge_bi_colcold" }, + { key: "col_freeze", label: "Морозильная колонна", hint: "только мороз · высокая", pict: "fridge_bi_colfreeze" }, + { key: "col_pair", label: "Пара колонн", hint: "холод + мороз · рядом", pict: "fridge_bi_colpair" }, + ], + freestanding: [ + { key: "single", label: "Однокамерный", hint: "мини · бар", pict: "fridge_fs_single" }, + { key: "two_chamber", label: "Двухкамерный", hint: "морозилка снизу", pict: "fridge_fs_two" }, + { key: "sbs", label: "Side-by-Side", hint: "распашной · 2 двери", pict: "fridge_fs_sbs" }, + { key: "french", label: "French Door", hint: "2 двери · ящик мороза", pict: "fridge_fs_french" }, + { key: "freezer", label: "Морозильная камера", hint: "отдельный морозильник", pict: "fridge_fs_freezer" }, + ], + }, + }, + }, + { + key: "size", + title: "Размер", + type: "single", + options: [ + { key: "narrow", label: "Узкий", hint: "W 45–55 см" }, + { key: "standard", label: "Стандарт", hint: "W 55–60 см", star: true }, + { key: "wide", label: "Широкий", hint: "W 60–75 см" }, + { key: "xl", label: "XL", hint: "W 80–100 см · SbS / French Door" }, + ], + }, + { + key: "features", + title: "Особенности", + type: "multi", + options: [ + { key: "nofrost", label: "No Frost", hint: "не нужно размораживать" }, + { key: "inverter", label: "Inverter", hint: "тише и экономичнее" }, + { key: "freshzone", label: "Зона свежести", hint: "BioFresh / овощи дольше" }, + { key: "silent", label: "≤40 дБ", hint: "почти не слышно ночью" }, + { key: "smart", label: "Wi-Fi", hint: "управление с телефона" }, + { key: "ice", label: "Лёдогенератор", hint: "кубики автоматически" }, + { key: "wine", label: "Винная зона", hint: "" }, + { key: "dispenser", label: "Диспенсер воды", hint: "холодная вода / лёд через дверь" }, + ], + }, ], }, hob: { diff --git a/miniapp/assets/podbor.css b/miniapp/assets/podbor.css index fa39b8d..7fb5f43 100644 --- a/miniapp/assets/podbor.css +++ b/miniapp/assets/podbor.css @@ -790,3 +790,217 @@ } .feature.on .feature-tick svg { width: 12px; height: 12px; stroke-width: 2.5; } + +/* ============================================================ + Иерархический wizard (новая схема steps[]) + ============================================================ */ + +.podbor-wizard { gap: var(--s4); } + +.wiz-header { + display: grid; + grid-template-columns: 28px 1fr 28px; + align-items: center; + gap: 12px; + margin-bottom: 4px; +} + +.wiz-header-meta { + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; +} + +.wiz-cat { + font-family: var(--font-display); + font-style: italic; + font-size: 20px; + line-height: 1; + letter-spacing: -0.01em; + color: var(--ink); +} + +.wiz-progress { + font-family: var(--font-mono); + font-size: 10px; + font-weight: 500; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--muted); +} + +.wiz-cat-icon { + width: 28px; + height: 28px; + color: var(--accent-2); + display: grid; + place-items: center; +} +.wiz-cat-icon svg { width: 22px; height: 22px; } + +/* Чипы прошлых ответов */ +.wiz-chips { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin: -4px 0 4px; +} +.wiz-chip { + font-family: var(--font-mono); + font-size: 10px; + font-weight: 500; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--accent-2); + background: var(--warm); + border: 1px solid var(--accent-2); + padding: 4px 10px; + border-radius: var(--r-pill); + cursor: pointer; + transition: background 0.12s; +} +.wiz-chip:active { background: #EAD9B6; } + +/* Заголовок шага */ +.wiz-title { + font-family: var(--font-display); + font-style: italic; + font-size: 24px; + font-weight: 400; + line-height: 1.1; + letter-spacing: -0.02em; + color: var(--ink); + margin: 6px 0 0; +} +.wiz-multi { + font-style: normal; + font-family: var(--font-mono); + font-size: 11px; + font-weight: 500; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--muted); +} + +/* Сетка карточек */ +.wiz-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; +} +@media (min-width: 540px) { + .wiz-grid { grid-template-columns: repeat(3, 1fr); } +} + +.wiz-card { + position: relative; + background: #fff; + border: 1px solid var(--line); + border-radius: var(--r-card, 14px); + padding: 12px 10px 14px; + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + cursor: pointer; + transition: all 0.12s; + text-align: center; + font: inherit; + color: var(--ink); +} +.wiz-card:active { background: var(--warm); transform: scale(0.98); } +.wiz-card.on { + border-color: var(--accent-2); + background: var(--warm); + box-shadow: 0 0 0 1px var(--accent-2) inset; +} +.wiz-card.star::before { + content: "⭐"; + position: absolute; + top: 6px; + left: 8px; + font-size: 11px; + opacity: 0.85; +} + +.wiz-pict { + width: 72px; + height: 96px; + display: grid; + place-items: center; +} +.wiz-pict svg { width: 100%; height: 100%; display: block; } +.wiz-pict-placeholder { + width: 72px; + height: 96px; + background: repeating-linear-gradient(45deg, transparent, transparent 6px, rgba(107, 74, 43, 0.06) 6px, rgba(107, 74, 43, 0.06) 7px); + border: 1px dashed var(--line); + border-radius: 6px; +} + +.wiz-label { + font-family: var(--font-sans); + font-size: 13.5px; + font-weight: 600; + letter-spacing: -0.01em; + color: var(--ink); + line-height: 1.2; +} +.wiz-hint { + font-family: var(--font-mono); + font-size: 9.5px; + font-weight: 500; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--muted); + line-height: 1.25; +} +.wiz-tick { + position: absolute; + top: 6px; + right: 6px; + width: 20px; + height: 20px; + background: var(--accent-2); + color: var(--paper); + border-radius: var(--r-pill); + display: grid; + place-items: center; +} +.wiz-tick svg { width: 11px; height: 11px; stroke-width: 2.5; } + +/* Review-экран */ +.rev-list { + background: #fff; + border: 1px solid var(--line); + border-radius: var(--r-card, 14px); + overflow: hidden; +} +.rev-row { + display: flex; + justify-content: space-between; + align-items: baseline; + padding: 12px 14px; + border-bottom: 1px solid var(--line); + gap: 12px; +} +.rev-row:last-child { border-bottom: 0; } +.rev-label { + font-family: var(--font-mono); + font-size: 10px; + font-weight: 500; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--muted); + flex-shrink: 0; +} +.rev-val { + font-family: var(--font-sans); + font-size: 14px; + font-weight: 500; + color: var(--ink); + text-align: right; + line-height: 1.3; +} +.rev-val .muted { color: var(--muted); font-weight: 400; } diff --git a/miniapp/assets/podbor.js b/miniapp/assets/podbor.js index 4124162..d4f6259 100644 --- a/miniapp/assets/podbor.js +++ b/miniapp/assets/podbor.js @@ -192,10 +192,22 @@ const Podbor = (function () { /* ===================== Step: detail — menu + per-category sub-screen ===================== */ function isCategoryFilled(catKey) { - const cat = state.per_cat[catKey]; - if (!cat || !cat.params) return false; - const params = PODBOR_PARAMS[catKey]?.primary || []; - return params.every(p => cat.params[p.key]); + const cs = state.per_cat[catKey]; + if (!cs) return false; + const config = PODBOR_PARAMS[catKey]; + if (!config) return false; + // Новая схема: все single-шаги должны иметь ответ. Multi (features) — необязательно. + if (config.steps) { + const ans = cs.answers || {}; + return config.steps.every(step => { + if (step.type === "multi") return true; // multi необязателен + return !!ans[step.key]; + }); + } + // Старая схема + if (!cs.params) return false; + const params = config.primary || []; + return params.every(p => cs.params[p.key]); } function renderDetail() { @@ -211,6 +223,9 @@ const Podbor = (function () { } if (detailView !== "menu" && detailView.startsWith("cat:")) { const catKey = detailView.slice(4); + const config = PODBOR_PARAMS[catKey]; + // Новая иерархическая схема → wizard. Старая → legacy-форма. + if (config?.steps) return renderCategoryWizard(catKey); return renderCategoryDetail(catKey); } return renderDetailMenu(); @@ -255,18 +270,276 @@ const Podbor = (function () { } function buildPerCatSummary(catKey) { - const cat = state.per_cat[catKey]; - if (!cat || !cat.params) return "—"; - const params = PODBOR_PARAMS[catKey]?.primary || []; + const cs = state.per_cat[catKey]; + if (!cs) return "—"; + const config = PODBOR_PARAMS[catKey]; + // Новая схема + if (config?.steps) { + const ans = cs.answers || {}; + const labels = []; + for (const step of config.steps) { + if (step.type === "multi") continue; + const val = ans[step.key]; + if (!val) continue; + const opts = resolveStepOptions(step, ans); + const opt = opts.find(o => o.key === val); + if (opt) labels.push(opt.label); + } + return labels.join(" · ") || "—"; + } + // Старая схема + if (!cs.params) return "—"; + const params = config?.primary || []; const labels = params .map(p => { - const opt = p.options.find(o => o.key === cat.params[p.key]); + const opt = p.options.find(o => o.key === cs.params[p.key]); return opt ? opt.label : null; }) .filter(Boolean); return labels.join(" · ") || "—"; } + /* Возвращает реальный options[] для шага с учётом optionsBy */ + function resolveStepOptions(step, answers) { + if (step.options) return step.options; + if (step.optionsBy) { + const depVal = answers[step.optionsBy.dependsOn]; + return (step.optionsBy.map && step.optionsBy.map[depVal]) || []; + } + return []; + } + + /* ===================== Иерархический wizard внутри категории ===================== */ + + function getCatState(catKey) { + const cs = state.per_cat[catKey]; + if (cs && cs.answers) return cs; // уже в новой форме + // Миграция / инициализация + return { answers: {}, notes: cs?.notes || "", _step: 0 }; + } + + function setCatState(catKey, patch) { + const prev = getCatState(catKey); + const next = { ...prev, ...patch }; + update({ per_cat: { ...state.per_cat, [catKey]: next } }); + } + + function renderCategoryWizard(catKey) { + const cat = PODBOR_CATEGORIES.find(x => x.key === catKey); + const config = PODBOR_PARAMS[catKey]; + const cs = getCatState(catKey); + const stepIdx = Math.max(0, Math.min(cs._step || 0, config.steps.length)); + + // Финальный экран категории — обзор + заметки + кнопка "Готово" + if (stepIdx >= config.steps.length) { + return renderCategoryReview(catKey); + } + + const step = config.steps[stepIdx]; + const options = resolveStepOptions(step, cs.answers); + const isMulti = step.type === "multi"; + const currentVal = cs.answers[step.key]; + const currentArr = isMulti ? (Array.isArray(currentVal) ? currentVal : []) : null; + + // Чипы прошлых ответов (single-шаги) + const prevChips = config.steps.slice(0, stepIdx) + .filter(s => s.type !== "multi") + .map(s => { + const v = cs.answers[s.key]; + if (!v) return ""; + const opts = resolveStepOptions(s, cs.answers); + const o = opts.find(x => x.key === v); + return o ? `${o.label}` : ""; + }).join(""); + + const cardsHtml = options.map(o => { + const isOn = isMulti ? currentArr.includes(o.key) : currentVal === o.key; + const pict = o.pict && PODBOR_PICTS[o.pict]; + return ` + + `; + }).join(""); + + const stepNum = stepIdx + 1; + const stepTotal = config.steps.length; + + const node = el(` +
+
+ +
+
${cat.label}
+
Шаг ${stepNum} из ${stepTotal}
+
+
${ICONS[cat.icon] || ""}
+
+ + ${prevChips ? `
${prevChips}
` : ""} + +

${step.title}${isMulti ? ' · можно несколько' : ""}

+ +
${cardsHtml}
+ +
+ ${stepIdx > 0 + ? `` + : `` + } + ${isMulti + ? `` + : (currentVal ? `` : "") + } +
+
+ `); + + // Клик по карточке + node.querySelectorAll(".wiz-card").forEach(card => { + card.addEventListener("click", () => { + const val = card.dataset.val; + const cs2 = getCatState(catKey); + const newAns = { ...cs2.answers }; + if (isMulti) { + const arr = Array.isArray(newAns[step.key]) ? newAns[step.key] : []; + newAns[step.key] = arr.includes(val) ? arr.filter(x => x !== val) : [...arr, val]; + } else { + newAns[step.key] = val; + // Если меняем answer для шага, от которого зависит следующий — сбросим все последующие answers + for (let i = stepIdx + 1; i < config.steps.length; i++) { + const s = config.steps[i]; + if (s.optionsBy && s.optionsBy.dependsOn === step.key) { + delete newAns[s.key]; + } + } + } + setCatState(catKey, { answers: newAns }); + // Single-select: автопереход на следующий шаг + if (!isMulti) { + setCatState(catKey, { _step: stepIdx + 1 }); + haptic && haptic("impact"); + } + render(); + }); + }); + + // Чипы — клик возвращает к шагу + node.querySelectorAll(".wiz-chip[data-jump]").forEach(chip => { + chip.addEventListener("click", () => { + const targetKey = chip.dataset.jump; + const targetIdx = config.steps.findIndex(s => s.key === targetKey); + if (targetIdx >= 0) { + setCatState(catKey, { _step: targetIdx }); + render(); + } + }); + }); + + // Кнопки + const wizPrev = node.querySelector("#wizPrev"); + if (wizPrev) wizPrev.addEventListener("click", () => { + setCatState(catKey, { _step: Math.max(0, stepIdx - 1) }); + render(); + }); + const wizMenu = node.querySelector("#wizMenu"); + if (wizMenu) wizMenu.addEventListener("click", () => { detailView = "menu"; render(); }); + const wizNext = node.querySelector("#wizNext"); + if (wizNext) wizNext.addEventListener("click", () => { + setCatState(catKey, { _step: stepIdx + 1 }); + haptic && haptic("impact"); + render(); + }); + // Header back — на предыдущий шаг или к меню + node.querySelector(".podbor-back").addEventListener("click", () => { + if (stepIdx > 0) { + setCatState(catKey, { _step: stepIdx - 1 }); + render(); + } else { + detailView = "menu"; + render(); + } + }); + + return node; + } + + function renderCategoryReview(catKey) { + const cat = PODBOR_CATEGORIES.find(x => x.key === catKey); + const config = PODBOR_PARAMS[catKey]; + const cs = getCatState(catKey); + + const rows = config.steps.map(step => { + const v = cs.answers[step.key]; + const opts = resolveStepOptions(step, cs.answers); + if (step.type === "multi") { + const arr = Array.isArray(v) ? v : []; + const labels = arr.map(k => opts.find(o => o.key === k)?.label).filter(Boolean); + return ` +
+
${step.title}
+
${labels.length ? labels.join(" · ") : 'не выбрано'}
+
+ `; + } + const opt = opts.find(o => o.key === v); + return ` +
+
${step.title}
+
${opt ? opt.label : ''}
+
+ `; + }).join(""); + + const node = el(` +
+
+ +
+
${cat.label}
+
Готово
+
+
${ICONS[cat.icon] || ""}
+
+ +

Проверьте ответы

+ +
${rows}
+ + + +
+ + +
+
+ `); + node.querySelector("#wizEdit").addEventListener("click", () => { + setCatState(catKey, { _step: 0 }); + render(); + }); + node.querySelector("#wizDone").addEventListener("click", () => { + detailView = "menu"; + haptic && haptic("success"); + render(); + }); + node.querySelector(".podbor-back").addEventListener("click", () => { + setCatState(catKey, { _step: config.steps.length - 1 }); + render(); + }); + const ta = node.querySelector("textarea[data-bind='cat_notes']"); + if (ta) ta.addEventListener("input", e => { + setCatState(catKey, { notes: e.target.value }); + }); + return node; + } + function renderCategoryDetail(catKey) { const cat = PODBOR_CATEGORIES.find(x => x.key === catKey); const config = PODBOR_PARAMS[catKey]; diff --git a/miniapp/assets/podbor.picts.js b/miniapp/assets/podbor.picts.js new file mode 100644 index 0000000..3a7787f --- /dev/null +++ b/miniapp/assets/podbor.picts.js @@ -0,0 +1,286 @@ +/* ============================================================ + Подбор техники — SVG-пиктограммы (стиль D · 3D перспектива) + ------------------------------------------------------------ + PODBOR_PICTS_DEFS — injected once into on load, + содержит линейные градиенты, на которые ссылаются пиктограммы. + PODBOR_PICTS — словарь key → SVG-строка. + ============================================================ */ + +const PODBOR_PICTS_DEFS = ` + +`; + +/* Инжектим defs один раз */ +(function injectPodborDefs() { + if (typeof document === "undefined") return; + function inject() { + if (document.getElementById("podbor-picts-defs")) return; + const wrap = document.createElement("div"); + wrap.id = "podbor-picts-defs"; + wrap.innerHTML = PODBOR_PICTS_DEFS; + document.body.appendChild(wrap); + } + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", inject, { once: true }); + } else { + inject(); + } +})(); + +const PODBOR_PICTS = { + /* ===== Холодильник · тип установки ===== */ + + fridge_install_builtin: ` + + + + + + + + + + `, + + fridge_install_freestanding: ` + + + + + + + + + + + + `, + + /* ===== Холодильник · встроенный · тип камеры ===== */ + + fridge_bi_single: ` + + + + + + + + + + + + + `, + + fridge_bi_two: ` + + + + + + + + + + + + + + + + + `, + + fridge_bi_colcold: ` + + + + + + + + + + + + + + `, + + fridge_bi_colfreeze: ` + + + + + + + + + + + + + + `, + + fridge_bi_colpair: ` + + + + + + + + + + + + + + + + + + + + + + + + + `, + + /* ===== Холодильник · отдельностоящий · тип камеры ===== */ + + fridge_fs_single: ` + + + + + + + + + + + + + + `, + + fridge_fs_two: ` + + + + + + + + + + + + + + + + + + + `, + + fridge_fs_sbs: ` + + + + + + + + + + + + + + + + + + + + + + + + + `, + + fridge_fs_french: ` + + + + + + + + + + + + + + + + + + + + + `, + + fridge_fs_freezer: ` + + + + + + + + + + + + + + + `, +}; diff --git a/miniapp/index.html b/miniapp/index.html index d2f029e..e333907 100644 --- a/miniapp/index.html +++ b/miniapp/index.html @@ -12,8 +12,8 @@ - - + +
@@ -21,9 +21,10 @@
- - - - + + + + +