Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 507ab62d0a | |||
| faac35302b | |||
| e25e18af45 | |||
| 0a634b6e20 | |||
| 03facb5db8 | |||
| 9f25c4c049 | |||
| e398469365 | |||
| 4ee840fe8e | |||
| 0b63d3cf04 | |||
| 693d1b50df | |||
| ce6720309f | |||
| 3079624e77 | |||
| 9a57f52bdc | |||
| 76b6634399 |
@@ -0,0 +1,151 @@
|
|||||||
|
# Sunset Pass — Проект для Сирпы
|
||||||
|
|
||||||
|
## Суть проекта
|
||||||
|
Клиент: **Sirpa Lehtinen** (sirpulik@gmail.com)
|
||||||
|
Огороженный парк для собак в Финляндии (Saarijärven koirapuisto).
|
||||||
|
Нужно разработать **веб/мобильное приложение** системы бронирования.
|
||||||
|
|
||||||
|
### Как работает система
|
||||||
|
Клиент открывает сайт/приложение → выбирает слот в календаре → платит онлайн → получает PIN → приходит к воротам → вводит PIN → ворота открываются → гуляет → уведомление за X минут → время вышло → выходит.
|
||||||
|
|
||||||
|
Одна группа в парке за раз. Без поводка (в Финляндии запрещено, парк — единственное место где можно).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Что уже знаем (из опросника 1)
|
||||||
|
|
||||||
|
| Параметр | Ответ |
|
||||||
|
|---|---|
|
||||||
|
| Размер парка | 2 гектара |
|
||||||
|
| Люди одновременно | Одна группа, без посторонних |
|
||||||
|
| Слот | Мин 1 час, макс 1 день |
|
||||||
|
| Часы работы | 7:00–23:00 каждый день |
|
||||||
|
| Оплата | Онлайн |
|
||||||
|
| Отмена | За 24 часа |
|
||||||
|
| Регистрация | Не нужна (имя + email) |
|
||||||
|
| Оборудование ворот | Ничего нет — всё с нуля |
|
||||||
|
| PIN | Система генерирует автоматически после оплаты |
|
||||||
|
| Брендинг | Название: Saarijärven koirapuisto, логотипа нет |
|
||||||
|
| Язык | Финский |
|
||||||
|
| Дедлайн | Середина июня 2026 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Что ещё не знаем (ждём ответа на опросник 2)
|
||||||
|
|
||||||
|
Опросник 2 отправлен Сирпе: **https://antonsemenovser.github.io/Sunset-Pass/questionnaire2.html**
|
||||||
|
|
||||||
|
Ждём ответа на 24 вопроса:
|
||||||
|
- Тип слотов (фиксированные или гибкие)
|
||||||
|
- Мультислот (несколько часов подряд)
|
||||||
|
- Как далеко вперёд бронировать
|
||||||
|
- Самостоятельная отмена
|
||||||
|
- Цена за час и тарифы
|
||||||
|
- Возврат денег при отмене
|
||||||
|
- Счёт-фактура
|
||||||
|
- Система замка (критично для интеграции!)
|
||||||
|
- Длина PIN, доставка PIN
|
||||||
|
- Уведомления (подтверждение, напоминание, предупреждение о конце)
|
||||||
|
- Панель управления Сирпы
|
||||||
|
- Формат (сайт / PWA / нативное приложение)
|
||||||
|
- Язык (только финский или + английский)
|
||||||
|
- Логотип и дизайн
|
||||||
|
- GDPR и правила пользования
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Процесс работы над проектом
|
||||||
|
|
||||||
|
### Этапы
|
||||||
|
1. ✅ Опросник 1 — базовые требования (получен)
|
||||||
|
2. 🔄 Опросник 2 — уточняющие вопросы (отправлен, ждём ответа)
|
||||||
|
3. ⏳ Опросник 3 — если после 2 останутся пробелы
|
||||||
|
4. ⏳ Написать полное ТЗ
|
||||||
|
5. ⏳ Согласовать бюджет и план
|
||||||
|
6. ⏳ Разработка
|
||||||
|
|
||||||
|
### Как читать ответы клиента
|
||||||
|
Ответы приходят как GitHub Issues в репозитории:
|
||||||
|
**https://github.com/antonsemenovser/Sunset-Pass/issues**
|
||||||
|
|
||||||
|
Команда для чтения нового ответа:
|
||||||
|
```
|
||||||
|
gh issue list --repo antonsemenovser/Sunset-Pass
|
||||||
|
gh issue view [НОМЕР] --repo antonsemenovser/Sunset-Pass
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Как отправлять опросники клиенту
|
||||||
|
|
||||||
|
### Шаблон письма
|
||||||
|
```
|
||||||
|
Здравствуйте, Сирпа!
|
||||||
|
|
||||||
|
[Контекст — почему новый опросник, что уже сделано]
|
||||||
|
|
||||||
|
Пожалуйста, заполните [второй/третий] опросник:
|
||||||
|
|
||||||
|
👉 [ССЫЛКА]
|
||||||
|
|
||||||
|
С уважением,
|
||||||
|
Anton / Kot Kanagrilli Oy
|
||||||
|
kotkanagrilli@gmail.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### Как создать новый опросник
|
||||||
|
1. Скопировать `questionnaire2.html` → `questionnaire3.html`
|
||||||
|
2. Скопировать `script2.js` → `script3.js`
|
||||||
|
3. В HTML изменить: заголовок, вопросы, `src="script3.js"`
|
||||||
|
4. В JS изменить: `TOTAL`, список полей в `TRACKED_FIELDS`/`updateProgress()`
|
||||||
|
5. `git add . && git commit -m "feat: add questionnaire3" && git push origin gh-pages`
|
||||||
|
6. Ссылка: `https://antonsemenovser.github.io/Sunset-Pass/questionnaire3.html`
|
||||||
|
|
||||||
|
### Правила хорошего опросника
|
||||||
|
- Радиокнопки с вариантами ответа — не textarea
|
||||||
|
- Под каждым вопросом поле "Свой комментарий"
|
||||||
|
- Где нужен "свой вариант" — поле появляется при выборе
|
||||||
|
- Ответ клиента видно в GitHub Issues
|
||||||
|
- Тулбар: Заново / Сохранить / Поделиться
|
||||||
|
- Кнопка внизу: "Форма по другому вопросу"
|
||||||
|
- Предпросмотр справа
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Структура файлов проекта
|
||||||
|
|
||||||
|
```
|
||||||
|
C:\pro\Sunset Pass\
|
||||||
|
├── CLAUDE.md ← этот файл
|
||||||
|
├── index.html ← Опросник 1 (заполнен Сирпой)
|
||||||
|
├── questionnaire2.html ← Опросник 2 (отправлен, ждём)
|
||||||
|
├── script.js ← JS для опросника 1
|
||||||
|
├── script2.js ← JS для опросника 2
|
||||||
|
├── style.css ← Общие стили (для всех опросников)
|
||||||
|
├── success.html ← Страница после отправки
|
||||||
|
├── logo.jpg ← Лого Kot Kanagrilli Oy
|
||||||
|
└── docs/
|
||||||
|
├── analysis/
|
||||||
|
│ └── 2026-05-12-what-we-know.md ← Анализ что знаем/не знаем
|
||||||
|
└── superpowers/
|
||||||
|
├── plans/ ← Планы реализации
|
||||||
|
└── specs/ ← Дизайн-доки
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Когда придёт ответ на опросник 2
|
||||||
|
|
||||||
|
1. Прочитать issue: `gh issue list --repo antonsemenovser/Sunset-Pass`
|
||||||
|
2. Решить — достаточно ли для ТЗ или нужен опросник 3
|
||||||
|
3. Если достаточно → писать ТЗ
|
||||||
|
4. ТЗ сохранить в `docs/tz/tz-v1.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Контакты
|
||||||
|
|
||||||
|
- **Клиент:** Sirpa Lehtinen — sirpulik@gmail.com
|
||||||
|
- **Наш email:** kotkanagrilli@gmail.com (anton.semenov.ser@gmail.com)
|
||||||
|
- **GitHub репо:** https://github.com/antonsemenovser/Sunset-Pass
|
||||||
|
- **Живой сайт:** https://antonsemenovser.github.io/Sunset-Pass/
|
||||||
+263
-129
@@ -7,137 +7,271 @@
|
|||||||
<link rel="stylesheet" href="style.css">
|
<link rel="stylesheet" href="style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
|
||||||
<header>
|
|
||||||
<h1>Sunset Pass</h1>
|
|
||||||
<p class="subtitle">Опросник для разработки системы бронирования парка</p>
|
|
||||||
<div class="progress-bar">
|
|
||||||
<div class="progress-fill" id="progressFill"></div>
|
|
||||||
</div>
|
|
||||||
<p class="progress-text" id="progressText">Вопрос 1 из 12</p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<form id="questionnaireForm" action="https://formspree.io/f/xrejjoav" method="POST">
|
<!-- Шапка компании -->
|
||||||
|
<div class="company-header">
|
||||||
<!-- О парке -->
|
<img src="logo.jpg" alt="Kot Kanagrilli Oy" class="company-logo">
|
||||||
<section class="section">
|
<div class="company-info">
|
||||||
<h2>О парке</h2>
|
<div class="company-name">Kot Kanagrilli Oy</div>
|
||||||
|
<a href="mailto:kotkanagrilli@gmail.com" class="company-email">kotkanagrilli@gmail.com</a>
|
||||||
<div class="question" data-index="1">
|
</div>
|
||||||
<label for="park_size">1. Какой размер парка? (площадь, примерно)</label>
|
|
||||||
<textarea id="park_size" name="park_size" placeholder="Например: 2000 кв.м, примерно 50x40 метров"></textarea>
|
|
||||||
<textarea id="park_size_comment" name="park_size_comment" class="comment" placeholder="Комментарий (необязательно)"></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="question" data-index="2">
|
|
||||||
<label for="max_people">2. Сколько человек максимум могут быть в парке одновременно?</label>
|
|
||||||
<textarea id="max_people" name="max_people" placeholder="Например: 5 человек с собаками"></textarea>
|
|
||||||
<textarea id="max_people_comment" name="max_people_comment" class="comment" placeholder="Комментарий (необязательно)"></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="question" data-index="3">
|
|
||||||
<label for="slot_duration">3. Какова минимальная и максимальная длительность одного слота?</label>
|
|
||||||
<textarea id="slot_duration" name="slot_duration" placeholder="Например: минимум 30 минут, максимум 2 часа"></textarea>
|
|
||||||
<textarea id="slot_duration_comment" name="slot_duration_comment" class="comment" placeholder="Комментарий (необязательно)"></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="question" data-index="4">
|
|
||||||
<label for="working_hours">4. Часы работы парка (по дням недели)?</label>
|
|
||||||
<textarea id="working_hours" name="working_hours" placeholder="Например: пн-пт 8:00-20:00, сб-вс 9:00-21:00"></textarea>
|
|
||||||
<textarea id="working_hours_comment" name="working_hours_comment" class="comment" placeholder="Комментарий (необязательно)"></textarea>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- О бронировании -->
|
|
||||||
<section class="section">
|
|
||||||
<h2>О бронировании</h2>
|
|
||||||
|
|
||||||
<div class="question" data-index="5">
|
|
||||||
<label for="payment">5. Как вы хотите принимать оплату?</label>
|
|
||||||
<textarea id="payment" name="payment" placeholder="Например: онлайн картой, наличными на месте, выставлять счёт"></textarea>
|
|
||||||
<textarea id="payment_comment" name="payment_comment" class="comment" placeholder="Комментарий (необязательно)"></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="question" data-index="6">
|
|
||||||
<label for="cancellation">6. Нужна ли возможность отмены бронирования? За сколько часов до начала?</label>
|
|
||||||
<textarea id="cancellation" name="cancellation" placeholder="Например: да, за 24 часа до начала / нет, отмена невозможна"></textarea>
|
|
||||||
<textarea id="cancellation_comment" name="cancellation_comment" class="comment" placeholder="Комментарий (необязательно)"></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="question" data-index="7">
|
|
||||||
<label for="registration">7. Нужна ли регистрация пользователя или достаточно имя + email?</label>
|
|
||||||
<textarea id="registration" name="registration" placeholder="Например: достаточно имени и email / нужна полная регистрация с паролем"></textarea>
|
|
||||||
<textarea id="registration_comment" name="registration_comment" class="comment" placeholder="Комментарий (необязательно)"></textarea>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- О воротах -->
|
|
||||||
<section class="section">
|
|
||||||
<h2>О воротах</h2>
|
|
||||||
|
|
||||||
<div class="question" data-index="8">
|
|
||||||
<label for="gate_equipment">8. Какое оборудование уже есть на воротах или нужно закупить?</label>
|
|
||||||
<textarea id="gate_equipment" name="gate_equipment" placeholder="Например: уже есть электронный замок марки X / нужно закупить всё с нуля"></textarea>
|
|
||||||
<textarea id="gate_equipment_comment" name="gate_equipment_comment" class="comment" placeholder="Комментарий (необязательно)"></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="question" data-index="9">
|
|
||||||
<label for="pin_generation">9. PIN-код — кто его генерирует: вы вручную или система автоматически?</label>
|
|
||||||
<textarea id="pin_generation" name="pin_generation" placeholder="Например: система должна генерировать автоматически после оплаты"></textarea>
|
|
||||||
<textarea id="pin_generation_comment" name="pin_generation_comment" class="comment" placeholder="Комментарий (необязательно)"></textarea>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- О дизайне -->
|
|
||||||
<section class="section">
|
|
||||||
<h2>О дизайне и языке</h2>
|
|
||||||
|
|
||||||
<div class="question" data-index="10">
|
|
||||||
<label for="branding">10. Есть ли логотип, фирменные цвета, название парка?</label>
|
|
||||||
<textarea id="branding" name="branding" placeholder="Например: название 'Koira Park', цвета зелёный и белый, логотип есть (могу прислать)"></textarea>
|
|
||||||
<textarea id="branding_comment" name="branding_comment" class="comment" placeholder="Комментарий (необязательно)"></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="question" data-index="11">
|
|
||||||
<label for="language">11. На каком языке должен быть сайт/приложение?</label>
|
|
||||||
<textarea id="language" name="language" placeholder="Например: только финский / финский + английский / финский + русский"></textarea>
|
|
||||||
<textarea id="language_comment" name="language_comment" class="comment" placeholder="Комментарий (необязательно)"></textarea>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- О сроках -->
|
|
||||||
<section class="section">
|
|
||||||
<h2>О сроках</h2>
|
|
||||||
|
|
||||||
<div class="question" data-index="12">
|
|
||||||
<label for="deadline">12. Когда нужен готовый продукт?</label>
|
|
||||||
<textarea id="deadline" name="deadline" placeholder="Например: через 2 месяца / к лету 2026 / как можно скорее"></textarea>
|
|
||||||
<textarea id="deadline_comment" name="deadline_comment" class="comment" placeholder="Комментарий (необязательно)"></textarea>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Контактные данные -->
|
|
||||||
<section class="section">
|
|
||||||
<h2>Ваши контактные данные</h2>
|
|
||||||
<div class="question">
|
|
||||||
<label for="client_name">Имя</label>
|
|
||||||
<input type="text" id="client_name" name="client_name" required placeholder="Ваше имя">
|
|
||||||
</div>
|
|
||||||
<div class="question">
|
|
||||||
<label for="client_email">Email</label>
|
|
||||||
<input type="email" id="client_email" name="client_email" required placeholder="ваш@email.com">
|
|
||||||
</div>
|
|
||||||
<div class="question">
|
|
||||||
<label for="client_phone">Телефон (необязательно)</label>
|
|
||||||
<input type="tel" id="client_phone" name="client_phone" placeholder="+358 XX XXX XXXX">
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<button type="submit" id="submitBtn">Отправить ответы</button>
|
|
||||||
<p class="submit-note">После отправки мы свяжемся с вами в течение 1-2 рабочих дней.</p>
|
|
||||||
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Тулбар -->
|
||||||
|
<div class="toolbar">
|
||||||
|
<div class="toolbar-left">
|
||||||
|
<span class="toolbar-title">Sunset Pass — Опросник о проекте</span>
|
||||||
|
<span class="save-status" id="saveStatus"></span>
|
||||||
|
</div>
|
||||||
|
<div class="toolbar-right">
|
||||||
|
<button class="toolbar-btn" id="btnReset" title="Очистить форму"><span class="btn-icon">↺</span><span class="btn-text"> Заново</span></button>
|
||||||
|
<button class="toolbar-btn" id="btnSave" title="Сохранить"><span class="btn-icon">💾</span><span class="btn-text"> Сохранить</span></button>
|
||||||
|
<button class="toolbar-btn primary" id="btnShare" title="Поделиться ссылкой"><span class="btn-icon">🔗</span><span class="btn-text"> Поделиться</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Flash сообщение -->
|
||||||
|
<div class="flash" id="flash"></div>
|
||||||
|
|
||||||
|
<!-- Основной макет -->
|
||||||
|
<div class="app-layout">
|
||||||
|
|
||||||
|
<!-- Колонка с формой -->
|
||||||
|
<div class="form-column">
|
||||||
|
<header>
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div class="progress-fill" id="progressFill"></div>
|
||||||
|
</div>
|
||||||
|
<p class="progress-text" id="progressText">Заполнено: 0 из 12</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<form id="questionnaireForm" action="https://formspree.io/f/xrejjoav" method="POST">
|
||||||
|
<input type="hidden" name="_replyto" id="_replyto">
|
||||||
|
<input type="hidden" name="sender_time" id="sender_time">
|
||||||
|
<input type="hidden" name="sender_timezone" id="sender_timezone">
|
||||||
|
<input type="hidden" name="anton_time" id="anton_time">
|
||||||
|
|
||||||
|
<!-- О парке -->
|
||||||
|
<section class="section" data-section-id="park">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2 contenteditable="false">О парке</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="question" data-index="1">
|
||||||
|
<label for="park_size">1. Какой размер парка? (площадь, примерно)</label>
|
||||||
|
<textarea id="park_size" name="park_size" placeholder="Например: 2000 кв.м, примерно 50x40 метров"></textarea>
|
||||||
|
<textarea id="park_size_comment" name="park_size_comment" class="comment" placeholder="Комментарий (необязательно)"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="question" data-index="2">
|
||||||
|
<label for="max_people">2. Сколько человек максимум могут быть в парке одновременно?</label>
|
||||||
|
<textarea id="max_people" name="max_people" placeholder="Например: 5 человек с собаками"></textarea>
|
||||||
|
<textarea id="max_people_comment" name="max_people_comment" class="comment" placeholder="Комментарий (необязательно)"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="question" data-index="3">
|
||||||
|
<label for="slot_duration">3. Какова минимальная и максимальная длительность одного слота?</label>
|
||||||
|
<textarea id="slot_duration" name="slot_duration" placeholder="Например: минимум 30 минут, максимум 2 часа"></textarea>
|
||||||
|
<textarea id="slot_duration_comment" name="slot_duration_comment" class="comment" placeholder="Комментарий (необязательно)"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="question" data-index="4">
|
||||||
|
<label for="working_hours">4. Часы работы парка (по дням недели)?</label>
|
||||||
|
<textarea id="working_hours" name="working_hours" placeholder="Например: пн-пт 8:00-20:00, сб-вс 9:00-21:00"></textarea>
|
||||||
|
<textarea id="working_hours_comment" name="working_hours_comment" class="comment" placeholder="Комментарий (необязательно)"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" class="add-field-btn" onclick="addField(this)">+ Добавить поле</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<button type="button" class="add-section-btn" onclick="addSection(this)">+ Добавить раздел</button>
|
||||||
|
|
||||||
|
<!-- О бронировании -->
|
||||||
|
<section class="section" data-section-id="booking">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2 contenteditable="false">О бронировании</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="question" data-index="5">
|
||||||
|
<label for="payment">5. Как вы хотите принимать оплату?</label>
|
||||||
|
<textarea id="payment" name="payment" placeholder="Например: онлайн картой, наличными на месте, выставлять счёт"></textarea>
|
||||||
|
<textarea id="payment_comment" name="payment_comment" class="comment" placeholder="Комментарий (необязательно)"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="question" data-index="6">
|
||||||
|
<label for="cancellation">6. Нужна ли возможность отмены бронирования? За сколько часов до начала?</label>
|
||||||
|
<textarea id="cancellation" name="cancellation" placeholder="Например: да, за 24 часа до начала / нет, отмена невозможна"></textarea>
|
||||||
|
<textarea id="cancellation_comment" name="cancellation_comment" class="comment" placeholder="Комментарий (необязательно)"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="question" data-index="7">
|
||||||
|
<label for="registration">7. Нужна ли регистрация пользователя или достаточно имя + email?</label>
|
||||||
|
<textarea id="registration" name="registration" placeholder="Например: достаточно имени и email / нужна полная регистрация с паролем"></textarea>
|
||||||
|
<textarea id="registration_comment" name="registration_comment" class="comment" placeholder="Комментарий (необязательно)"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" class="add-field-btn" onclick="addField(this)">+ Добавить поле</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<button type="button" class="add-section-btn" onclick="addSection(this)">+ Добавить раздел</button>
|
||||||
|
|
||||||
|
<!-- О воротах -->
|
||||||
|
<section class="section" data-section-id="gates">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2 contenteditable="false">О воротах</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="question" data-index="8">
|
||||||
|
<label for="gate_equipment">8. Какое оборудование уже есть на воротах или нужно закупить?</label>
|
||||||
|
<textarea id="gate_equipment" name="gate_equipment" placeholder="Например: уже есть электронный замок марки X / нужно закупить всё с нуля"></textarea>
|
||||||
|
<textarea id="gate_equipment_comment" name="gate_equipment_comment" class="comment" placeholder="Комментарий (необязательно)"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="question" data-index="9">
|
||||||
|
<label for="pin_generation">9. PIN-код — кто его генерирует: вы вручную или система автоматически?</label>
|
||||||
|
<textarea id="pin_generation" name="pin_generation" placeholder="Например: система должна генерировать автоматически после оплаты"></textarea>
|
||||||
|
<textarea id="pin_generation_comment" name="pin_generation_comment" class="comment" placeholder="Комментарий (необязательно)"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" class="add-field-btn" onclick="addField(this)">+ Добавить поле</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<button type="button" class="add-section-btn" onclick="addSection(this)">+ Добавить раздел</button>
|
||||||
|
|
||||||
|
<!-- О дизайне -->
|
||||||
|
<section class="section" data-section-id="design">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2 contenteditable="false">О дизайне и языке</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="question" data-index="10">
|
||||||
|
<label for="branding">10. Есть ли логотип, фирменные цвета, название парка?</label>
|
||||||
|
<textarea id="branding" name="branding" placeholder="Например: название 'Koira Park', цвета зелёный и белый, логотип есть (могу прислать)"></textarea>
|
||||||
|
<textarea id="branding_comment" name="branding_comment" class="comment" placeholder="Комментарий (необязательно)"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="question" data-index="11">
|
||||||
|
<label for="language">11. На каком языке должен быть сайт/приложение?</label>
|
||||||
|
<textarea id="language" name="language" placeholder="Например: только финский / финский + английский / финский + русский"></textarea>
|
||||||
|
<textarea id="language_comment" name="language_comment" class="comment" placeholder="Комментарий (необязательно)"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" class="add-field-btn" onclick="addField(this)">+ Добавить поле</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<button type="button" class="add-section-btn" onclick="addSection(this)">+ Добавить раздел</button>
|
||||||
|
|
||||||
|
<!-- О сроках -->
|
||||||
|
<section class="section" data-section-id="deadlines">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2 contenteditable="false">О сроках</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="question" data-index="12">
|
||||||
|
<label for="deadline">12. Когда нужен готовый продукт?</label>
|
||||||
|
<textarea id="deadline" name="deadline" placeholder="Например: через 2 месяца / к лету 2026 / как можно скорее"></textarea>
|
||||||
|
<textarea id="deadline_comment" name="deadline_comment" class="comment" placeholder="Комментарий (необязательно)"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" class="add-field-btn" onclick="addField(this)">+ Добавить поле</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<button type="button" class="add-section-btn" onclick="addSection(this)">+ Добавить раздел</button>
|
||||||
|
|
||||||
|
<!-- Контактные данные -->
|
||||||
|
<section class="section" data-section-id="contacts">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2 contenteditable="false">Ваши контактные данные</h2>
|
||||||
|
</div>
|
||||||
|
<div class="question">
|
||||||
|
<label for="client_name">Имя</label>
|
||||||
|
<input type="text" id="client_name" name="client_name" required placeholder="Ваше имя">
|
||||||
|
</div>
|
||||||
|
<div class="question">
|
||||||
|
<label for="client_email">Email</label>
|
||||||
|
<input type="email" id="client_email" name="client_email" required placeholder="ваш@email.com">
|
||||||
|
</div>
|
||||||
|
<div class="question">
|
||||||
|
<label for="client_phone">Телефон (необязательно)</label>
|
||||||
|
<input type="tel" id="client_phone" name="client_phone" placeholder="+358 XX XXX XXXX">
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<button type="submit" id="submitBtn">Отправить ответы</button>
|
||||||
|
<p class="submit-note">После отправки мы свяжемся с вами в течение 1-2 рабочих дней.</p>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Панель предпросмотра -->
|
||||||
|
<div class="preview-column">
|
||||||
|
<div class="preview-panel">
|
||||||
|
<div class="preview-header">Предпросмотр</div>
|
||||||
|
<div class="preview-a4" id="previewContent">
|
||||||
|
<p class="preview-empty">Начните заполнять форму — здесь появится предпросмотр</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Модальное окно -->
|
||||||
|
<div class="modal-overlay" id="modalOverlay" onclick="closeModalOutside(event)">
|
||||||
|
<div class="modal">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3>Обращение по другому вопросу</h3>
|
||||||
|
<button class="modal-close" onclick="closeModal()">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p class="modal-subtitle">Заполните форму — мы свяжемся с вами в ближайшее время.</p>
|
||||||
|
<form id="generalForm" action="https://formspree.io/f/xrejjoav" method="POST">
|
||||||
|
<input type="hidden" name="form_type" value="general_inquiry">
|
||||||
|
<input type="hidden" name="_replyto" id="general_replyto">
|
||||||
|
|
||||||
|
<div class="modal-field">
|
||||||
|
<label for="g_subject">Тема обращения <span class="required">*</span></label>
|
||||||
|
<input type="text" id="g_subject" name="subject" required placeholder="Например: вопрос о сотрудничестве">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-field">
|
||||||
|
<label for="g_message">Сообщение <span class="required">*</span></label>
|
||||||
|
<textarea id="g_message" name="message" required placeholder="Опишите ваш вопрос или предложение..."></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-field">
|
||||||
|
<label for="g_name">Ваше имя <span class="required">*</span></label>
|
||||||
|
<input type="text" id="g_name" name="name" required placeholder="Имя и фамилия">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-field">
|
||||||
|
<label for="g_email">Электронная почта <span class="required">*</span></label>
|
||||||
|
<input type="email" id="g_email" name="email" required placeholder="ваш@email.com">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-field">
|
||||||
|
<label for="g_phone">Телефон <span class="optional">(необязательно)</span></label>
|
||||||
|
<input type="tel" id="g_phone" name="phone" placeholder="+358 XX XXX XXXX">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="modal-submit" id="generalSubmitBtn">Отправить обращение</button>
|
||||||
|
<p class="modal-note">Нажимая кнопку, вы соглашаетесь на обработку персональных данных</p>
|
||||||
|
</form>
|
||||||
|
<div class="modal-success" id="modalSuccess" style="display:none;">
|
||||||
|
<div class="modal-success-icon">✅</div>
|
||||||
|
<h4>Обращение отправлено!</h4>
|
||||||
|
<p>Мы получили ваше сообщение и свяжемся с вами в ближайшее время.</p>
|
||||||
|
<button class="modal-submit" onclick="closeModal()">Закрыть</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Кнопка внизу страницы -->
|
||||||
|
<div class="bottom-contact">
|
||||||
|
<button class="bottom-contact-btn" onclick="openModal()">✉ Форма по другому вопросу</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="script.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -0,0 +1,563 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Saarijärven koirapuisto — Уточняющий опросник</title>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
<style>
|
||||||
|
.radio-group, .checkbox-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.radio-group label, .checkbox-group label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-weight: 400;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border: 1.5px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: border-color 0.15s, background 0.15s;
|
||||||
|
}
|
||||||
|
.radio-group label:hover, .checkbox-group label:hover {
|
||||||
|
border-color: #2d6a4f;
|
||||||
|
background: #f0faf4;
|
||||||
|
}
|
||||||
|
.radio-group input[type="radio"],
|
||||||
|
.checkbox-group input[type="checkbox"] {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
accent-color: #2d6a4f;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.radio-group input[type="radio"]:checked + span,
|
||||||
|
.checkbox-group input[type="checkbox"]:checked + span {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2d6a4f;
|
||||||
|
}
|
||||||
|
.radio-group label:has(input:checked),
|
||||||
|
.checkbox-group label:has(input:checked) {
|
||||||
|
border-color: #2d6a4f;
|
||||||
|
background: #f0faf4;
|
||||||
|
}
|
||||||
|
.question-comment {
|
||||||
|
margin-top: 10px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1.5px dashed #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-family: inherit;
|
||||||
|
color: #555;
|
||||||
|
background: #f9f9f9;
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
.question-comment::placeholder { color: #bbb; font-style: italic; }
|
||||||
|
.question-comment:focus { outline: none; border-color: #2d6a4f; background: #fff; }
|
||||||
|
.other-input {
|
||||||
|
margin-top: 6px;
|
||||||
|
margin-left: 28px;
|
||||||
|
width: calc(100% - 28px);
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1.5px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-family: inherit;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.other-input.visible { display: block; }
|
||||||
|
.question-label {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.context-note {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #888;
|
||||||
|
font-style: italic;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
background: #f8f8f8;
|
||||||
|
border-radius: 8px;
|
||||||
|
border-left: 3px solid #c8e6c9;
|
||||||
|
}
|
||||||
|
.variant-card {
|
||||||
|
border: 1.5px solid #e0e0e0;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 14px 18px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.15s, background 0.15s;
|
||||||
|
}
|
||||||
|
.variant-card:has(input:checked) {
|
||||||
|
border-color: #2d6a4f;
|
||||||
|
background: #f0faf4;
|
||||||
|
}
|
||||||
|
.variant-card.recommended {
|
||||||
|
border-color: #2d6a4f;
|
||||||
|
}
|
||||||
|
.variant-card label {
|
||||||
|
font-weight: 400;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
.variant-card label:hover { background: none; border: none; }
|
||||||
|
.variant-title { font-weight: 700; font-size: 0.95rem; }
|
||||||
|
.variant-desc { font-size: 0.85rem; color: #555; margin-top: 4px; }
|
||||||
|
.variant-badge {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 20px;
|
||||||
|
margin-left: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.badge-green { background: #e8f5e9; color: #2d6a4f; }
|
||||||
|
.badge-orange { background: #fff3e0; color: #e65100; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="company-header">
|
||||||
|
<img src="logo.jpg" alt="Kot Kanagrilli Oy" class="company-logo">
|
||||||
|
<div class="company-info">
|
||||||
|
<div class="company-name">Kot Kanagrilli Oy</div>
|
||||||
|
<a href="mailto:kotkanagrilli@gmail.com" class="company-email">kotkanagrilli@gmail.com</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="toolbar">
|
||||||
|
<div class="toolbar-left">
|
||||||
|
<span class="toolbar-title">Saarijärven koirapuisto — Уточняющий опросник</span>
|
||||||
|
<span class="save-status" id="saveStatus"></span>
|
||||||
|
</div>
|
||||||
|
<div class="toolbar-right">
|
||||||
|
<button class="toolbar-btn" id="btnReset" title="Очистить форму"><span class="btn-icon">↺</span><span class="btn-text"> Заново</span></button>
|
||||||
|
<button class="toolbar-btn" id="btnSave" title="Сохранить"><span class="btn-icon">💾</span><span class="btn-text"> Сохранить</span></button>
|
||||||
|
<button class="toolbar-btn primary" id="btnShare" title="Поделиться ссылкой"><span class="btn-icon">🔗</span><span class="btn-text"> Поделиться</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flash" id="flash"></div>
|
||||||
|
|
||||||
|
<div class="app-layout">
|
||||||
|
<div class="form-column">
|
||||||
|
<header>
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div class="progress-fill" id="progressFill"></div>
|
||||||
|
</div>
|
||||||
|
<p class="progress-text" id="progressText">Заполнено: 0 из 24</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<form id="questionnaireForm" action="https://formspree.io/f/xrejjoav" method="POST">
|
||||||
|
<input type="hidden" name="_replyto" id="_replyto">
|
||||||
|
<input type="hidden" name="sender_time" id="sender_time">
|
||||||
|
<input type="hidden" name="sender_timezone" id="sender_timezone">
|
||||||
|
<input type="hidden" name="anton_time" id="anton_time">
|
||||||
|
<input type="hidden" name="questionnaire_number" value="2">
|
||||||
|
|
||||||
|
<!-- 1. Бронирование -->
|
||||||
|
<section class="section" data-section-id="booking">
|
||||||
|
<div class="section-header"><h2>Как работает бронирование</h2></div>
|
||||||
|
|
||||||
|
<div class="question" data-index="1">
|
||||||
|
<span class="question-label">1. Как устроены слоты времени?</span>
|
||||||
|
<div class="radio-group">
|
||||||
|
<label><input type="radio" name="slot_type" value="fixed"><span>Фиксированные каждый час (9:00, 10:00, 11:00...)</span></label>
|
||||||
|
<label><input type="radio" name="slot_type" value="flexible"><span>Клиент сам выбирает любое время начала</span></label>
|
||||||
|
<label><input type="radio" name="slot_type" value="advice"><span>Не знаю, посоветуйте</span></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="question" data-index="2">
|
||||||
|
<span class="question-label">2. Можно ли взять несколько часов подряд одним бронированием?</span>
|
||||||
|
<div class="radio-group">
|
||||||
|
<label><input type="radio" name="multi_hour" value="yes"><span>Да, без ограничений</span></label>
|
||||||
|
<label><input type="radio" name="multi_hour" value="limited"><span>Да, но не больше X часов</span></label>
|
||||||
|
<label><input type="radio" name="multi_hour" value="no"><span>Нет, только по одному часу</span></label>
|
||||||
|
</div>
|
||||||
|
<input type="text" class="other-input" id="multi_hour_limit" name="multi_hour_limit" placeholder="Максимум сколько часов?">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="question" data-index="3">
|
||||||
|
<span class="question-label">3. Что видит клиент в календаре?</span>
|
||||||
|
<div class="radio-group">
|
||||||
|
<label><input type="radio" name="calendar_view" value="free_only"><span>Только свободные слоты</span></label>
|
||||||
|
<label><input type="radio" name="calendar_view" value="both"><span>И свободные и занятые (занятые отмечены серым)</span></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="question" data-index="4">
|
||||||
|
<span class="question-label">4. Как далеко вперёд можно бронировать?</span>
|
||||||
|
<div class="radio-group">
|
||||||
|
<label><input type="radio" name="advance_booking" value="7"><span>7 дней</span></label>
|
||||||
|
<label><input type="radio" name="advance_booking" value="14"><span>14 дней</span></label>
|
||||||
|
<label><input type="radio" name="advance_booking" value="30"><span>30 дней</span></label>
|
||||||
|
<label><input type="radio" name="advance_booking" value="60"><span>60 дней</span></label>
|
||||||
|
<label><input type="radio" name="advance_booking" value="unlimited"><span>Без ограничений</span></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="question" data-index="5">
|
||||||
|
<span class="question-label">5. Клиент может сам отменить бронирование через приложение?</span>
|
||||||
|
<div class="radio-group">
|
||||||
|
<label><input type="radio" name="self_cancel" value="yes"><span>Да, самостоятельно через приложение</span></label>
|
||||||
|
<label><input type="radio" name="self_cancel" value="contact"><span>Нет, только написав мне</span></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 2. Оплата -->
|
||||||
|
<section class="section" data-section-id="payment">
|
||||||
|
<div class="section-header"><h2>Оплата</h2></div>
|
||||||
|
|
||||||
|
<div class="question" data-index="6">
|
||||||
|
<span class="question-label">6. Цена за один час аренды парка (€)</span>
|
||||||
|
<div class="radio-group">
|
||||||
|
<label><input type="radio" name="price_per_hour" value="under10"><span>До 10€</span></label>
|
||||||
|
<label><input type="radio" name="price_per_hour" value="10-15"><span>10–15€</span></label>
|
||||||
|
<label><input type="radio" name="price_per_hour" value="15-20"><span>15–20€</span></label>
|
||||||
|
<label><input type="radio" name="price_per_hour" value="over20"><span>Больше 20€</span></label>
|
||||||
|
<label><input type="radio" name="price_per_hour" value="other"><span>Свой вариант</span></label>
|
||||||
|
</div>
|
||||||
|
<input type="text" class="other-input" id="price_other" name="price_other" placeholder="Напишите сумму...">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="question" data-index="7">
|
||||||
|
<span class="question-label">7. Будут ли разные тарифы?</span>
|
||||||
|
<div class="radio-group">
|
||||||
|
<label><input type="radio" name="price_tiers" value="single"><span>Один тариф для всех</span></label>
|
||||||
|
<label><input type="radio" name="price_tiers" value="weekday_weekend"><span>Будни дешевле, выходные дороже</span></label>
|
||||||
|
<label><input type="radio" name="price_tiers" value="evening"><span>Вечерний/ночной тариф отдельно</span></label>
|
||||||
|
<label><input type="radio" name="price_tiers" value="complex"><span>Несколько разных тарифов</span></label>
|
||||||
|
<label><input type="radio" name="price_tiers" value="later"><span>Решу позже</span></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="question" data-index="8">
|
||||||
|
<span class="question-label">8. При отмене за 24+ часа — что происходит с деньгами?</span>
|
||||||
|
<div class="radio-group">
|
||||||
|
<label><input type="radio" name="refund_policy" value="full"><span>Полный возврат автоматически</span></label>
|
||||||
|
<label><input type="radio" name="refund_policy" value="partial"><span>Возврат за вычетом комиссии</span></label>
|
||||||
|
<label><input type="radio" name="refund_policy" value="reschedule"><span>Нет возврата, только перенос на другую дату</span></label>
|
||||||
|
<label><input type="radio" name="refund_policy" value="no_refund"><span>Возврат не предусмотрен</span></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="question" data-index="9">
|
||||||
|
<span class="question-label">9. Нужен ли счёт-фактура (lasku) для корпоративных клиентов?</span>
|
||||||
|
<div class="radio-group">
|
||||||
|
<label><input type="radio" name="invoice" value="yes"><span>Да</span></label>
|
||||||
|
<label><input type="radio" name="invoice" value="no"><span>Нет</span></label>
|
||||||
|
<label><input type="radio" name="invoice" value="later"><span>Решу позже</span></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 3. PIN и ворота -->
|
||||||
|
<section class="section" data-section-id="pin">
|
||||||
|
<div class="section-header"><h2>PIN-код и ворота</h2></div>
|
||||||
|
<p class="context-note">Наше приложение будет автоматически генерировать PIN и отправлять его в систему замка. Нам важно знать какую систему вы планируете установить — чтобы подключить к ней наше приложение.</p>
|
||||||
|
|
||||||
|
<div class="question" data-index="10">
|
||||||
|
<span class="question-label">10. Какую систему замка/ворот планируете установить?</span>
|
||||||
|
<div class="radio-group">
|
||||||
|
<label><input type="radio" name="lock_system" value="nuki"><span>Nuki</span></label>
|
||||||
|
<label><input type="radio" name="lock_system" value="yale"><span>Yale</span></label>
|
||||||
|
<label><input type="radio" name="lock_system" value="abloy"><span>Abloy (финский стандарт)</span></label>
|
||||||
|
<label><input type="radio" name="lock_system" value="unknown"><span>Ещё не выбрала, нужна рекомендация</span></label>
|
||||||
|
<label><input type="radio" name="lock_system" value="other"><span>Другая система</span></label>
|
||||||
|
</div>
|
||||||
|
<input type="text" class="other-input" id="lock_other" name="lock_other" placeholder="Напишите марку/модель...">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="question" data-index="11">
|
||||||
|
<span class="question-label">11. PIN-код — сколько цифр?</span>
|
||||||
|
<div class="radio-group">
|
||||||
|
<label><input type="radio" name="pin_length" value="4"><span>4 цифры</span></label>
|
||||||
|
<label><input type="radio" name="pin_length" value="6"><span>6 цифр</span></label>
|
||||||
|
<label><input type="radio" name="pin_length" value="advice"><span>Посоветуйте</span></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="question" data-index="12">
|
||||||
|
<span class="question-label">12. Как клиент получает PIN после оплаты?</span>
|
||||||
|
<div class="radio-group">
|
||||||
|
<label><input type="radio" name="pin_delivery" value="email"><span>По email</span></label>
|
||||||
|
<label><input type="radio" name="pin_delivery" value="app"><span>В приложении (пуш-уведомление)</span></label>
|
||||||
|
<label><input type="radio" name="pin_delivery" value="both"><span>И email и в приложении</span></label>
|
||||||
|
<label><input type="radio" name="pin_delivery" value="sms"><span>По SMS</span></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 4. Уведомления -->
|
||||||
|
<section class="section" data-section-id="notifications">
|
||||||
|
<div class="section-header"><h2>Уведомления</h2></div>
|
||||||
|
|
||||||
|
<div class="question" data-index="13">
|
||||||
|
<span class="question-label">13. Подтверждение бронирования клиенту сразу после оплаты?</span>
|
||||||
|
<div class="radio-group">
|
||||||
|
<label><input type="radio" name="booking_confirm" value="email"><span>Да, по email</span></label>
|
||||||
|
<label><input type="radio" name="booking_confirm" value="sms"><span>Да, по SMS</span></label>
|
||||||
|
<label><input type="radio" name="booking_confirm" value="both"><span>И email и SMS</span></label>
|
||||||
|
<label><input type="radio" name="booking_confirm" value="no"><span>Не нужно</span></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="question" data-index="14">
|
||||||
|
<span class="question-label">14. Напоминание клиенту перед началом слота — за сколько?</span>
|
||||||
|
<div class="radio-group">
|
||||||
|
<label><input type="radio" name="reminder_before" value="24h"><span>За 24 часа</span></label>
|
||||||
|
<label><input type="radio" name="reminder_before" value="1h"><span>За 1 час</span></label>
|
||||||
|
<label><input type="radio" name="reminder_before" value="30min"><span>За 30 минут</span></label>
|
||||||
|
<label><input type="radio" name="reminder_before" value="no"><span>Не нужно</span></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="question" data-index="15">
|
||||||
|
<span class="question-label">15. Предупреждение когда время заканчивается — за сколько минут?</span>
|
||||||
|
<div class="radio-group">
|
||||||
|
<label><input type="radio" name="time_warning" value="30min"><span>За 30 минут</span></label>
|
||||||
|
<label><input type="radio" name="time_warning" value="15min"><span>За 15 минут</span></label>
|
||||||
|
<label><input type="radio" name="time_warning" value="5min"><span>За 5 минут</span></label>
|
||||||
|
<label><input type="radio" name="time_warning" value="no"><span>Не нужно</span></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="question" data-index="16">
|
||||||
|
<span class="question-label">16. Вы (Сирпа) получаете уведомление о каждом новом бронировании?</span>
|
||||||
|
<div class="radio-group">
|
||||||
|
<label><input type="radio" name="admin_notify" value="email"><span>Да, по email</span></label>
|
||||||
|
<label><input type="radio" name="admin_notify" value="sms"><span>Да, по SMS</span></label>
|
||||||
|
<label><input type="radio" name="admin_notify" value="both"><span>И email и SMS</span></label>
|
||||||
|
<label><input type="radio" name="admin_notify" value="no"><span>Нет, буду сама проверять</span></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 5. Панель управления -->
|
||||||
|
<section class="section" data-section-id="admin">
|
||||||
|
<div class="section-header"><h2>Ваша панель управления</h2></div>
|
||||||
|
|
||||||
|
<div class="question" data-index="17">
|
||||||
|
<span class="question-label">17. Что хотите видеть в своей панели? (можно несколько)</span>
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<label><input type="checkbox" name="admin_features" value="bookings_list"><span>Список всех бронирований</span></label>
|
||||||
|
<label><input type="checkbox" name="admin_features" value="revenue"><span>Статистику доходов</span></label>
|
||||||
|
<label><input type="checkbox" name="admin_features" value="manual_booking"><span>Создать/отменить бронирование вручную</span></label>
|
||||||
|
<label><input type="checkbox" name="admin_features" value="block_dates"><span>Блокировка дат (техработы, отпуск)</span></label>
|
||||||
|
<label><input type="checkbox" name="admin_features" value="change_price"><span>Изменение цен</span></label>
|
||||||
|
<label><input type="checkbox" name="admin_features" value="clients"><span>История клиентов</span></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="question" data-index="18">
|
||||||
|
<span class="question-label">18. Нужна ли история клиентов (кто бронировал раньше)?</span>
|
||||||
|
<div class="radio-group">
|
||||||
|
<label><input type="radio" name="client_history" value="yes"><span>Да</span></label>
|
||||||
|
<label><input type="radio" name="client_history" value="no"><span>Нет</span></label>
|
||||||
|
<label><input type="radio" name="client_history" value="later"><span>Решу позже</span></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 6. Формат приложения -->
|
||||||
|
<section class="section" data-section-id="format">
|
||||||
|
<div class="section-header"><h2>Формат приложения</h2></div>
|
||||||
|
|
||||||
|
<div class="question" data-index="19">
|
||||||
|
<span class="question-label">19. Какой формат приложения?</span>
|
||||||
|
|
||||||
|
<div class="variant-card">
|
||||||
|
<label>
|
||||||
|
<input type="radio" name="product_format" value="web">
|
||||||
|
<div>
|
||||||
|
<div class="variant-title">Только сайт <span class="variant-badge badge-green">Быстро, дёшево</span></div>
|
||||||
|
<div class="variant-desc">Работает в браузере телефона, компьютера, планшета. Не нужно ничего устанавливать.</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="variant-card recommended">
|
||||||
|
<label>
|
||||||
|
<input type="radio" name="product_format" value="web_pwa">
|
||||||
|
<div>
|
||||||
|
<div class="variant-title">Сайт + установка на телефон <span class="variant-badge badge-green">Рекомендуем</span></div>
|
||||||
|
<div class="variant-desc">Работает как приложение, но не требует App Store. Клиент добавляет на экран — и всё.</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="variant-card">
|
||||||
|
<label>
|
||||||
|
<input type="radio" name="product_format" value="ios_android">
|
||||||
|
<div>
|
||||||
|
<div class="variant-title">Приложение для iPhone и Android <span class="variant-badge badge-orange">Дороже, дольше</span></div>
|
||||||
|
<div class="variant-desc">Полноценное приложение в App Store и Google Play. Максимальный комфорт для клиентов.</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="question" data-index="20">
|
||||||
|
<span class="question-label">20. Язык приложения?</span>
|
||||||
|
<div class="radio-group">
|
||||||
|
<label><input type="radio" name="language" value="fi"><span>Только финский</span></label>
|
||||||
|
<label><input type="radio" name="language" value="fi_en"><span>Финский + английский</span></label>
|
||||||
|
<label><input type="radio" name="language" value="fi_en_ru"><span>Финский + английский + русский</span></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 7. Дизайн -->
|
||||||
|
<section class="section" data-section-id="design">
|
||||||
|
<div class="section-header"><h2>Дизайн</h2></div>
|
||||||
|
|
||||||
|
<div class="question" data-index="21">
|
||||||
|
<span class="question-label">21. Логотип?</span>
|
||||||
|
<div class="radio-group">
|
||||||
|
<label><input type="radio" name="logo" value="make_simple"><span>Сделайте простой логотип</span></label>
|
||||||
|
<label><input type="radio" name="logo" value="own_designer"><span>Найду дизайнера сама</span></label>
|
||||||
|
<label><input type="radio" name="logo" value="not_needed"><span>Логотип не нужен</span></label>
|
||||||
|
<label><input type="radio" name="logo" value="later"><span>Решу позже</span></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="question" data-index="22">
|
||||||
|
<span class="question-label">22. Цвета и стиль сайта?</span>
|
||||||
|
<div class="radio-group">
|
||||||
|
<label><input type="radio" name="design_style" value="nature"><span>Природные (зелёный, коричневый, тёплые тона)</span></label>
|
||||||
|
<label><input type="radio" name="design_style" value="minimal"><span>Современный минимализм (белый/чёрный)</span></label>
|
||||||
|
<label><input type="radio" name="design_style" value="bright"><span>Яркий и дружелюбный</span></label>
|
||||||
|
<label><input type="radio" name="design_style" value="no_pref"><span>Нет предпочтений, доверяю вам</span></label>
|
||||||
|
<label><input type="radio" name="design_style" value="other"><span>Свой вариант</span></label>
|
||||||
|
</div>
|
||||||
|
<input type="text" class="other-input" id="design_other" name="design_other" placeholder="Опишите пожелания...">
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 8. Юридическое -->
|
||||||
|
<section class="section" data-section-id="legal">
|
||||||
|
<div class="section-header"><h2>Юридическое</h2></div>
|
||||||
|
<p class="context-note">По закону ЕС (GDPR) любой финский сайт обязан иметь политику конфиденциальности и правила использования.</p>
|
||||||
|
|
||||||
|
<div class="question" data-index="23">
|
||||||
|
<span class="question-label">23. Правила пользования парком — кто составляет?</span>
|
||||||
|
<div class="radio-group">
|
||||||
|
<label><input type="radio" name="terms" value="you_template"><span>Подготовьте шаблон, я дополню своими правилами</span></label>
|
||||||
|
<label><input type="radio" name="terms" value="me"><span>Составлю сама</span></label>
|
||||||
|
<label><input type="radio" name="terms" value="you_full"><span>Подготовьте полностью</span></label>
|
||||||
|
<label><input type="radio" name="terms" value="what_is_it"><span>Не знаю что это — объясните</span></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="question" data-index="24">
|
||||||
|
<span class="question-label">24. Политика конфиденциальности (GDPR) — кто составляет?</span>
|
||||||
|
<div class="radio-group">
|
||||||
|
<label><input type="radio" name="privacy" value="you_template"><span>Подготовьте шаблон, я дополню</span></label>
|
||||||
|
<label><input type="radio" name="privacy" value="me"><span>Составлю сама</span></label>
|
||||||
|
<label><input type="radio" name="privacy" value="you_full"><span>Подготовьте полностью</span></label>
|
||||||
|
<label><input type="radio" name="privacy" value="what_is_it"><span>Не знаю что это — объясните</span></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Контакты -->
|
||||||
|
<section class="section" data-section-id="contacts">
|
||||||
|
<div class="section-header"><h2>Ваши контактные данные</h2></div>
|
||||||
|
<div class="question">
|
||||||
|
<label for="client_name">Имя</label>
|
||||||
|
<input type="text" id="client_name" name="client_name" required placeholder="Ваше имя" value="Sirpa Lehtinen">
|
||||||
|
</div>
|
||||||
|
<div class="question">
|
||||||
|
<label for="client_email">Email</label>
|
||||||
|
<input type="email" id="client_email" name="client_email" required placeholder="ваш@email.com" value="sirpulik@gmail.com">
|
||||||
|
</div>
|
||||||
|
<div class="question">
|
||||||
|
<label for="client_phone">Телефон (необязательно)</label>
|
||||||
|
<input type="tel" id="client_phone" name="client_phone" placeholder="+358 XX XXX XXXX">
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<button type="submit" id="submitBtn">Отправить ответы</button>
|
||||||
|
<p class="submit-note">После отправки мы свяжемся с вами в течение 1-2 рабочих дней.</p>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Предпросмотр -->
|
||||||
|
<div class="preview-column">
|
||||||
|
<div class="preview-panel">
|
||||||
|
<div class="preview-header">Предпросмотр</div>
|
||||||
|
<div class="preview-a4" id="previewContent">
|
||||||
|
<p class="preview-empty">Начните заполнять форму — здесь появится предпросмотр</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Кнопка внизу страницы -->
|
||||||
|
<div class="bottom-contact">
|
||||||
|
<button class="bottom-contact-btn" onclick="openModal()">✉ Форма по другому вопросу</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Модальное окно -->
|
||||||
|
<div class="modal-overlay" id="modalOverlay" onclick="closeModalOutside(event)">
|
||||||
|
<div class="modal">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3>Обращение по другому вопросу</h3>
|
||||||
|
<button class="modal-close" onclick="closeModal()">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p class="modal-subtitle">Заполните форму — мы свяжемся с вами в ближайшее время.</p>
|
||||||
|
<form id="generalForm" action="https://formspree.io/f/xrejjoav" method="POST">
|
||||||
|
<input type="hidden" name="form_type" value="general_inquiry">
|
||||||
|
<input type="hidden" name="_replyto" id="general_replyto">
|
||||||
|
<div class="modal-field">
|
||||||
|
<label for="g_subject">Тема обращения <span class="required">*</span></label>
|
||||||
|
<input type="text" id="g_subject" name="subject" required placeholder="Например: вопрос о сотрудничестве">
|
||||||
|
</div>
|
||||||
|
<div class="modal-field">
|
||||||
|
<label for="g_message">Сообщение <span class="required">*</span></label>
|
||||||
|
<textarea id="g_message" name="message" required placeholder="Опишите ваш вопрос или предложение..."></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="modal-field">
|
||||||
|
<label for="g_name">Ваше имя <span class="required">*</span></label>
|
||||||
|
<input type="text" id="g_name" name="name" required placeholder="Имя и фамилия">
|
||||||
|
</div>
|
||||||
|
<div class="modal-field">
|
||||||
|
<label for="g_email">Электронная почта <span class="required">*</span></label>
|
||||||
|
<input type="email" id="g_email" name="email" required placeholder="ваш@email.com">
|
||||||
|
</div>
|
||||||
|
<div class="modal-field">
|
||||||
|
<label for="g_phone">Телефон <span class="optional">(необязательно)</span></label>
|
||||||
|
<input type="tel" id="g_phone" name="phone" placeholder="+358 XX XXX XXXX">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="modal-submit" id="generalSubmitBtn">Отправить обращение</button>
|
||||||
|
<p class="modal-note">Нажимая кнопку, вы соглашаетесь на обработку персональных данных</p>
|
||||||
|
</form>
|
||||||
|
<div class="modal-success" id="modalSuccess" style="display:none;">
|
||||||
|
<div class="modal-success-icon">✅</div>
|
||||||
|
<h4>Обращение отправлено!</h4>
|
||||||
|
<p>Мы получили ваше сообщение и свяжемся с вами в ближайшее время.</p>
|
||||||
|
<button class="modal-submit" onclick="closeModal()">Закрыть</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="script2.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,439 @@
|
|||||||
|
// ─── Состояние ───────────────────────────────────────────────────────────────
|
||||||
|
let saveTimer = null;
|
||||||
|
let dynamicSectionCount = 0;
|
||||||
|
let dynamicFieldCount = 0;
|
||||||
|
|
||||||
|
// ─── Инициализация ────────────────────────────────────────────────────────────
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
loadFromURL();
|
||||||
|
setupAutosave();
|
||||||
|
updatePreview();
|
||||||
|
updateProgress();
|
||||||
|
|
||||||
|
document.getElementById('client_email').addEventListener('input', function () {
|
||||||
|
document.getElementById('_replyto').value = this.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('questionnaireForm').addEventListener('submit', function () {
|
||||||
|
const now = new Date();
|
||||||
|
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||||
|
const offsetMin = -now.getTimezoneOffset();
|
||||||
|
const offsetSign = offsetMin >= 0 ? '+' : '-';
|
||||||
|
const offsetHours = String(Math.floor(Math.abs(offsetMin) / 60)).padStart(2, '0');
|
||||||
|
const offsetMins = String(Math.abs(offsetMin) % 60).padStart(2, '0');
|
||||||
|
const senderTime = now.toLocaleString('ru-RU', { hour12: false }) + ` (UTC${offsetSign}${offsetHours}:${offsetMins}, ${tz})`;
|
||||||
|
|
||||||
|
const antonTime = now.toLocaleString('ru-RU', {
|
||||||
|
timeZone: 'Europe/Helsinki',
|
||||||
|
hour12: false,
|
||||||
|
year: 'numeric', month: '2-digit', day: '2-digit',
|
||||||
|
hour: '2-digit', minute: '2-digit', second: '2-digit'
|
||||||
|
}) + ' (UTC+3, Финляндия)';
|
||||||
|
|
||||||
|
document.getElementById('sender_time').value = senderTime;
|
||||||
|
document.getElementById('sender_timezone').value = tz;
|
||||||
|
document.getElementById('anton_time').value = antonTime;
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
document.getElementById('btnReset').addEventListener('click', resetForm);
|
||||||
|
document.getElementById('btnSave').addEventListener('click', saveManual);
|
||||||
|
document.getElementById('btnShare').addEventListener('click', shareForm);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Прогресс ─────────────────────────────────────────────────────────────────
|
||||||
|
function updateProgress() {
|
||||||
|
const staticFields = ['park_size','max_people','slot_duration','working_hours',
|
||||||
|
'payment','cancellation','registration','gate_equipment','pin_generation',
|
||||||
|
'branding','language','deadline'];
|
||||||
|
let filled = 0;
|
||||||
|
staticFields.forEach(name => {
|
||||||
|
const el = document.getElementById(name);
|
||||||
|
if (el && el.value.trim()) filled++;
|
||||||
|
});
|
||||||
|
const total = staticFields.length;
|
||||||
|
const pct = Math.round((filled / total) * 100);
|
||||||
|
document.getElementById('progressFill').style.width = pct + '%';
|
||||||
|
document.getElementById('progressText').textContent = `Заполнено: ${filled} из ${total}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Превью ───────────────────────────────────────────────────────────────────
|
||||||
|
function updatePreview() {
|
||||||
|
const container = document.getElementById('previewContent');
|
||||||
|
const sections = document.querySelectorAll('.section');
|
||||||
|
let html = '';
|
||||||
|
|
||||||
|
sections.forEach(section => {
|
||||||
|
const title = section.querySelector('h2');
|
||||||
|
const questions = section.querySelectorAll('.question');
|
||||||
|
let sectionHtml = '';
|
||||||
|
|
||||||
|
questions.forEach(q => {
|
||||||
|
const inputs = q.querySelectorAll('textarea, input[type="text"], input[type="email"], input[type="tel"]');
|
||||||
|
const label = q.querySelector('label, .dynamic-question-header input.field-title');
|
||||||
|
let labelText = '';
|
||||||
|
let mainVal = '';
|
||||||
|
let commentVal = '';
|
||||||
|
|
||||||
|
if (label) {
|
||||||
|
labelText = label.tagName === 'LABEL' ? label.textContent.trim() : label.value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
inputs.forEach((inp, i) => {
|
||||||
|
const val = inp.value.trim();
|
||||||
|
if (i === 0) mainVal = val;
|
||||||
|
if (inp.classList.contains('comment')) commentVal = val;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (mainVal) {
|
||||||
|
sectionHtml += `<div class="preview-item">
|
||||||
|
<div class="preview-item-label">${labelText}</div>
|
||||||
|
<div class="preview-item-value">${escapeHtml(mainVal)}</div>
|
||||||
|
${commentVal ? `<div class="preview-item-comment">↳ ${escapeHtml(commentVal)}</div>` : ''}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (sectionHtml && title) {
|
||||||
|
html += `<div class="preview-section">
|
||||||
|
<div class="preview-section-title">${title.textContent.trim()}</div>
|
||||||
|
${sectionHtml}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
container.innerHTML = html || '<p class="preview-empty">Начните заполнять форму — здесь появится предпросмотр</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(str) {
|
||||||
|
return str.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Автосохранение ───────────────────────────────────────────────────────────
|
||||||
|
function setupAutosave() {
|
||||||
|
document.getElementById('questionnaireForm').addEventListener('input', function () {
|
||||||
|
updatePreview();
|
||||||
|
updateProgress();
|
||||||
|
clearTimeout(saveTimer);
|
||||||
|
saveTimer = setTimeout(() => {
|
||||||
|
saveToStorage();
|
||||||
|
updateURL();
|
||||||
|
}, 10000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveToStorage() {
|
||||||
|
const data = collectFormData();
|
||||||
|
localStorage.setItem('sunsetpass_form', JSON.stringify(data));
|
||||||
|
document.getElementById('saveStatus').textContent = 'Сохранено ' + formatTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveManual() {
|
||||||
|
saveToStorage();
|
||||||
|
updateURL();
|
||||||
|
showFlash('Сохранено!');
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime() {
|
||||||
|
const d = new Date();
|
||||||
|
return d.getHours().toString().padStart(2,'0') + ':' + d.getMinutes().toString().padStart(2,'0');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Сбор данных формы ────────────────────────────────────────────────────────
|
||||||
|
function collectFormData() {
|
||||||
|
const form = document.getElementById('questionnaireForm');
|
||||||
|
const data = { fields: {}, dynamicSections: [] };
|
||||||
|
|
||||||
|
// Статичные поля
|
||||||
|
form.querySelectorAll('textarea[name], input[name]').forEach(el => {
|
||||||
|
if (el.name && el.name !== '_replyto') {
|
||||||
|
data.fields[el.name] = el.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Динамические секции
|
||||||
|
form.querySelectorAll('.section.dynamic-section').forEach(sec => {
|
||||||
|
const title = sec.querySelector('h2') ? sec.querySelector('h2').textContent.trim() : '';
|
||||||
|
const fields = [];
|
||||||
|
sec.querySelectorAll('.dynamic-question').forEach(q => {
|
||||||
|
const titleInput = q.querySelector('input.field-title');
|
||||||
|
const textarea = q.querySelector('textarea');
|
||||||
|
fields.push({
|
||||||
|
title: titleInput ? titleInput.value : '',
|
||||||
|
value: textarea ? textarea.value : ''
|
||||||
|
});
|
||||||
|
});
|
||||||
|
data.dynamicSections.push({ title, fields });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Дополнительные поля в статичных секциях
|
||||||
|
data.extraFields = [];
|
||||||
|
form.querySelectorAll('.section:not(.dynamic-section) .dynamic-question').forEach(q => {
|
||||||
|
const sectionId = q.closest('.section').dataset.sectionId;
|
||||||
|
const titleInput = q.querySelector('input.field-title');
|
||||||
|
const textarea = q.querySelector('textarea');
|
||||||
|
data.extraFields.push({
|
||||||
|
sectionId,
|
||||||
|
title: titleInput ? titleInput.value : '',
|
||||||
|
value: textarea ? textarea.value : ''
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Загрузка / URL ───────────────────────────────────────────────────────────
|
||||||
|
function updateURL() {
|
||||||
|
const data = collectFormData();
|
||||||
|
const json = JSON.stringify(data);
|
||||||
|
const b64 = btoa(unescape(encodeURIComponent(json)));
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
url.searchParams.set('d', b64);
|
||||||
|
window.history.replaceState(null, '', url.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadFromURL() {
|
||||||
|
const params = new URL(window.location.href).searchParams;
|
||||||
|
const b64 = params.get('d');
|
||||||
|
if (b64) {
|
||||||
|
try {
|
||||||
|
const json = decodeURIComponent(escape(atob(b64)));
|
||||||
|
const data = JSON.parse(json);
|
||||||
|
restoreFormData(data);
|
||||||
|
document.getElementById('saveStatus').textContent = 'Загружено из ссылки';
|
||||||
|
return;
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
// Попробовать localStorage
|
||||||
|
const saved = localStorage.getItem('sunsetpass_form');
|
||||||
|
if (saved) {
|
||||||
|
try {
|
||||||
|
restoreFormData(JSON.parse(saved));
|
||||||
|
document.getElementById('saveStatus').textContent = 'Восстановлено из сохранения';
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreFormData(data) {
|
||||||
|
if (!data) return;
|
||||||
|
const form = document.getElementById('questionnaireForm');
|
||||||
|
|
||||||
|
// Статичные поля
|
||||||
|
if (data.fields) {
|
||||||
|
Object.entries(data.fields).forEach(([name, value]) => {
|
||||||
|
const el = form.querySelector(`[name="${name}"]`);
|
||||||
|
if (el) el.value = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Дополнительные поля в статичных секциях
|
||||||
|
if (data.extraFields) {
|
||||||
|
data.extraFields.forEach(item => {
|
||||||
|
const section = form.querySelector(`.section[data-section-id="${item.sectionId}"]`);
|
||||||
|
if (section) {
|
||||||
|
const addBtn = section.querySelector('.add-field-btn');
|
||||||
|
addField(addBtn, item.title, item.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Динамические секции
|
||||||
|
if (data.dynamicSections) {
|
||||||
|
data.dynamicSections.forEach(sec => {
|
||||||
|
const lastAddSectionBtn = [...form.querySelectorAll('.add-section-btn')].pop();
|
||||||
|
if (lastAddSectionBtn) {
|
||||||
|
const newSection = addSection(lastAddSectionBtn, sec.title);
|
||||||
|
if (newSection && sec.fields) {
|
||||||
|
sec.fields.forEach(field => {
|
||||||
|
const addBtn = newSection.querySelector('.add-field-btn');
|
||||||
|
addField(addBtn, field.title, field.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePreview();
|
||||||
|
updateProgress();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Поделиться ───────────────────────────────────────────────────────────────
|
||||||
|
function shareForm() {
|
||||||
|
saveToStorage();
|
||||||
|
updateURL();
|
||||||
|
const url = window.location.href;
|
||||||
|
|
||||||
|
if (navigator.share) {
|
||||||
|
navigator.share({ title: 'Sunset Pass — Опросник', url }).catch(() => copyToClipboard(url));
|
||||||
|
} else {
|
||||||
|
copyToClipboard(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyToClipboard(text) {
|
||||||
|
if (navigator.clipboard) {
|
||||||
|
navigator.clipboard.writeText(text).then(() => showFlash('Ссылка скопирована!')).catch(() => fallbackCopy(text));
|
||||||
|
} else {
|
||||||
|
fallbackCopy(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fallbackCopy(text) {
|
||||||
|
const ta = document.createElement('textarea');
|
||||||
|
ta.value = text;
|
||||||
|
ta.style.position = 'fixed';
|
||||||
|
ta.style.opacity = '0';
|
||||||
|
document.body.appendChild(ta);
|
||||||
|
ta.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
document.body.removeChild(ta);
|
||||||
|
showFlash('Ссылка скопирована!');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Заново ───────────────────────────────────────────────────────────────────
|
||||||
|
function resetForm() {
|
||||||
|
if (!confirm('Очистить всю форму? Это действие нельзя отменить.')) return;
|
||||||
|
|
||||||
|
const form = document.getElementById('questionnaireForm');
|
||||||
|
|
||||||
|
// Очистить статичные поля
|
||||||
|
form.querySelectorAll('textarea, input[type="text"], input[type="email"], input[type="tel"]').forEach(el => {
|
||||||
|
el.value = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Удалить динамические поля
|
||||||
|
form.querySelectorAll('.dynamic-question').forEach(el => el.remove());
|
||||||
|
|
||||||
|
// Удалить динамические секции
|
||||||
|
form.querySelectorAll('.section.dynamic-section').forEach(el => el.remove());
|
||||||
|
// Удалить add-section-btn дублей (оставить только оригинальные)
|
||||||
|
const allAddBtns = form.querySelectorAll('.add-section-btn');
|
||||||
|
allAddBtns.forEach((btn, i) => { if (i >= 6) btn.remove(); });
|
||||||
|
|
||||||
|
localStorage.removeItem('sunsetpass_form');
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
url.searchParams.delete('d');
|
||||||
|
window.history.replaceState(null, '', url.toString());
|
||||||
|
|
||||||
|
document.getElementById('saveStatus').textContent = '';
|
||||||
|
updatePreview();
|
||||||
|
updateProgress();
|
||||||
|
showFlash('Форма очищена');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Flash ────────────────────────────────────────────────────────────────────
|
||||||
|
function showFlash(msg) {
|
||||||
|
const el = document.getElementById('flash');
|
||||||
|
el.textContent = msg;
|
||||||
|
el.classList.add('show');
|
||||||
|
setTimeout(() => el.classList.remove('show'), 2500);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Добавить поле ────────────────────────────────────────────────────────────
|
||||||
|
function addField(btn, titleVal, textVal) {
|
||||||
|
dynamicFieldCount++;
|
||||||
|
const id = 'dyn_field_' + dynamicFieldCount;
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'dynamic-question';
|
||||||
|
div.innerHTML = `
|
||||||
|
<div class="dynamic-question-header">
|
||||||
|
<input class="field-title" type="text" placeholder="Название поля..." value="${escapeAttr(titleVal || '')}">
|
||||||
|
<button type="button" class="remove-btn" onclick="this.closest('.dynamic-question').remove(); updatePreview(); updateProgress();" title="Удалить поле">×</button>
|
||||||
|
</div>
|
||||||
|
<textarea id="${id}" name="${id}" placeholder="Ответ...">${escapeAttr(textVal || '')}</textarea>
|
||||||
|
`;
|
||||||
|
btn.parentNode.insertBefore(div, btn);
|
||||||
|
|
||||||
|
div.querySelector('input.field-title').addEventListener('input', updatePreview);
|
||||||
|
div.querySelector('textarea').addEventListener('input', updatePreview);
|
||||||
|
|
||||||
|
return div;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Добавить раздел ──────────────────────────────────────────────────────────
|
||||||
|
function addSection(btn, titleVal) {
|
||||||
|
dynamicSectionCount++;
|
||||||
|
const section = document.createElement('section');
|
||||||
|
section.className = 'section dynamic-section';
|
||||||
|
section.dataset.sectionId = 'dyn_' + dynamicSectionCount;
|
||||||
|
section.innerHTML = `
|
||||||
|
<div class="section-header" style="display:flex;align-items:center;gap:8px;">
|
||||||
|
<h2 contenteditable="true" style="flex:1;outline:none;border-bottom:2px solid #2d6a4f;padding-bottom:4px;" placeholder="Название раздела...">${escapeAttr(titleVal || '')}</h2>
|
||||||
|
<button type="button" class="remove-btn" onclick="this.closest('.section').nextElementSibling?.classList.contains('add-section-btn') && this.closest('.section').nextElementSibling.remove(); this.closest('.section').remove(); updatePreview();" title="Удалить раздел">×</button>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="add-field-btn" onclick="addField(this)">+ Добавить поле</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
section.querySelector('h2').addEventListener('input', updatePreview);
|
||||||
|
|
||||||
|
// Вставить перед кнопкой добавления раздела
|
||||||
|
btn.parentNode.insertBefore(section, btn);
|
||||||
|
|
||||||
|
// Добавить новую кнопку добавления раздела после секции
|
||||||
|
const newBtn = document.createElement('button');
|
||||||
|
newBtn.type = 'button';
|
||||||
|
newBtn.className = 'add-section-btn';
|
||||||
|
newBtn.textContent = '+ Добавить раздел';
|
||||||
|
newBtn.onclick = function() { addSection(this); };
|
||||||
|
section.parentNode.insertBefore(newBtn, btn);
|
||||||
|
|
||||||
|
return section;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Модальное окно ──────────────────────────────────────────────────────────
|
||||||
|
function openModal() {
|
||||||
|
document.getElementById('modalOverlay').classList.add('open');
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
document.getElementById('modalOverlay').classList.remove('open');
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModalOutside(e) {
|
||||||
|
if (e.target === document.getElementById('modalOverlay')) closeModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
const generalForm = document.getElementById('generalForm');
|
||||||
|
if (generalForm) {
|
||||||
|
generalForm.addEventListener('submit', async function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const btn = document.getElementById('generalSubmitBtn');
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = 'Отправляем...';
|
||||||
|
|
||||||
|
// Синхронизировать _replyto
|
||||||
|
const emailVal = document.getElementById('g_email').value;
|
||||||
|
document.getElementById('general_replyto').value = emailVal;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = new FormData(generalForm);
|
||||||
|
const res = await fetch(generalForm.action, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
headers: { 'Accept': 'application/json' }
|
||||||
|
});
|
||||||
|
if (res.ok) {
|
||||||
|
generalForm.style.display = 'none';
|
||||||
|
document.getElementById('modalSuccess').style.display = 'block';
|
||||||
|
} else {
|
||||||
|
throw new Error('Ошибка сервера');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = 'Отправить обращение';
|
||||||
|
showFlash('Ошибка отправки. Попробуйте ещё раз.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Закрыть по Escape
|
||||||
|
document.addEventListener('keydown', function (e) {
|
||||||
|
if (e.key === 'Escape') closeModal();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function escapeAttr(str) {
|
||||||
|
return str.replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
||||||
|
}
|
||||||
+252
@@ -0,0 +1,252 @@
|
|||||||
|
const TOTAL = 24;
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
// Добавить поле комментария под каждый вопрос
|
||||||
|
document.querySelectorAll('.question[data-index]').forEach(q => {
|
||||||
|
const name = (q.querySelector('[name]') || {}).name || '';
|
||||||
|
const ta = document.createElement('textarea');
|
||||||
|
ta.className = 'question-comment';
|
||||||
|
ta.name = name + '_comment';
|
||||||
|
ta.placeholder = 'Свой комментарий или уточнение (необязательно)...';
|
||||||
|
q.appendChild(ta);
|
||||||
|
});
|
||||||
|
|
||||||
|
updateProgress();
|
||||||
|
|
||||||
|
document.getElementById('client_email').addEventListener('input', function () {
|
||||||
|
document.getElementById('_replyto').value = this.value;
|
||||||
|
});
|
||||||
|
document.getElementById('_replyto').value = document.getElementById('client_email').value;
|
||||||
|
|
||||||
|
document.getElementById('btnReset').addEventListener('click', function () {
|
||||||
|
if (!confirm('Очистить всю форму? Это действие нельзя отменить.')) return;
|
||||||
|
document.getElementById('questionnaireForm').querySelectorAll('input[type="radio"], input[type="checkbox"]').forEach(el => el.checked = false);
|
||||||
|
document.getElementById('questionnaireForm').querySelectorAll('textarea, input[type="text"], input[type="tel"], input[type="email"]').forEach(el => { if (!['client_name','client_email'].includes(el.id)) el.value = ''; });
|
||||||
|
document.querySelectorAll('.other-input').forEach(el => el.classList.remove('visible'));
|
||||||
|
updateProgress(); updatePreview();
|
||||||
|
showFlash('Форма очищена');
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('btnSave').addEventListener('click', function () {
|
||||||
|
updateURL(); showFlash('Сохранено!');
|
||||||
|
document.getElementById('saveStatus').textContent = 'Сохранено ' + formatTime();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('btnShare').addEventListener('click', function () {
|
||||||
|
updateURL();
|
||||||
|
const url = window.location.href;
|
||||||
|
if (navigator.clipboard) {
|
||||||
|
navigator.clipboard.writeText(url).then(() => showFlash('Ссылка скопирована!'));
|
||||||
|
} else {
|
||||||
|
const ta = document.createElement('textarea');
|
||||||
|
ta.value = url; ta.style.position = 'fixed'; ta.style.opacity = '0';
|
||||||
|
document.body.appendChild(ta); ta.select(); document.execCommand('copy');
|
||||||
|
document.body.removeChild(ta); showFlash('Ссылка скопирована!');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Модальное окно
|
||||||
|
const generalForm = document.getElementById('generalForm');
|
||||||
|
if (generalForm) {
|
||||||
|
generalForm.addEventListener('submit', async function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const btn = document.getElementById('generalSubmitBtn');
|
||||||
|
btn.disabled = true; btn.textContent = 'Отправляем...';
|
||||||
|
document.getElementById('general_replyto').value = document.getElementById('g_email').value;
|
||||||
|
try {
|
||||||
|
const res = await fetch(generalForm.action, { method: 'POST', body: new FormData(generalForm), headers: { 'Accept': 'application/json' } });
|
||||||
|
if (res.ok) { generalForm.style.display = 'none'; document.getElementById('modalSuccess').style.display = 'block'; }
|
||||||
|
else throw new Error();
|
||||||
|
} catch { btn.disabled = false; btn.textContent = 'Отправить обращение'; showFlash('Ошибка. Попробуйте ещё раз.'); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeModal(); });
|
||||||
|
|
||||||
|
document.getElementById('questionnaireForm').addEventListener('change', function () {
|
||||||
|
updateProgress();
|
||||||
|
updatePreview();
|
||||||
|
handleConditionals();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('questionnaireForm').addEventListener('input', function () {
|
||||||
|
updateProgress();
|
||||||
|
updatePreview();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('questionnaireForm').addEventListener('submit', function (e) {
|
||||||
|
// Собрать чекбоксы в одно скрытое поле
|
||||||
|
const checked = [...document.querySelectorAll('input[name="admin_features"]:checked')].map(el => el.value);
|
||||||
|
let hidden = document.getElementById('admin_features_combined');
|
||||||
|
if (!hidden) {
|
||||||
|
hidden = document.createElement('input');
|
||||||
|
hidden.type = 'hidden';
|
||||||
|
hidden.id = 'admin_features_combined';
|
||||||
|
hidden.name = 'admin_features_combined';
|
||||||
|
document.getElementById('questionnaireForm').appendChild(hidden);
|
||||||
|
}
|
||||||
|
hidden.value = checked.join(', ');
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||||
|
const offsetMin = -now.getTimezoneOffset();
|
||||||
|
const offsetSign = offsetMin >= 0 ? '+' : '-';
|
||||||
|
const offsetHours = String(Math.floor(Math.abs(offsetMin) / 60)).padStart(2, '0');
|
||||||
|
const offsetMins = String(Math.abs(offsetMin) % 60).padStart(2, '0');
|
||||||
|
document.getElementById('sender_time').value =
|
||||||
|
now.toLocaleString('ru-RU', { hour12: false }) +
|
||||||
|
` (UTC${offsetSign}${offsetHours}:${offsetMins}, ${tz})`;
|
||||||
|
document.getElementById('sender_timezone').value = tz;
|
||||||
|
document.getElementById('anton_time').value =
|
||||||
|
now.toLocaleString('ru-RU', {
|
||||||
|
timeZone: 'Europe/Helsinki', hour12: false,
|
||||||
|
year: 'numeric', month: '2-digit', day: '2-digit',
|
||||||
|
hour: '2-digit', minute: '2-digit', second: '2-digit'
|
||||||
|
}) + ' (UTC+3, Финляндия)';
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
handleConditionals();
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleConditionals() {
|
||||||
|
// Показать поле "свой вариант" для радио с value="other"/"limited"
|
||||||
|
const conditionals = [
|
||||||
|
{ radio: 'price_per_hour', value: 'other', inputId: 'price_other' },
|
||||||
|
{ radio: 'lock_system', value: 'other', inputId: 'lock_other' },
|
||||||
|
{ radio: 'design_style', value: 'other', inputId: 'design_other' },
|
||||||
|
{ radio: 'multi_hour', value: 'limited', inputId: 'multi_hour_limit' },
|
||||||
|
];
|
||||||
|
|
||||||
|
conditionals.forEach(({ radio, value, inputId }) => {
|
||||||
|
const selected = document.querySelector(`input[name="${radio}"]:checked`);
|
||||||
|
const input = document.getElementById(inputId);
|
||||||
|
if (!input) return;
|
||||||
|
if (selected && selected.value === value) {
|
||||||
|
input.classList.add('visible');
|
||||||
|
} else {
|
||||||
|
input.classList.remove('visible');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateProgress() {
|
||||||
|
const radioGroups = [
|
||||||
|
'slot_type','multi_hour','calendar_view','advance_booking','self_cancel',
|
||||||
|
'price_per_hour','price_tiers','refund_policy','invoice',
|
||||||
|
'lock_system','pin_length','pin_delivery',
|
||||||
|
'booking_confirm','reminder_before','time_warning','admin_notify',
|
||||||
|
'client_history',
|
||||||
|
'product_format','language',
|
||||||
|
'logo','design_style',
|
||||||
|
'terms','privacy'
|
||||||
|
];
|
||||||
|
|
||||||
|
let filled = 0;
|
||||||
|
|
||||||
|
// Радио (23 группы)
|
||||||
|
radioGroups.forEach(name => {
|
||||||
|
if (document.querySelector(`input[name="${name}"]:checked`)) filled++;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Чекбокс (1 группа)
|
||||||
|
if (document.querySelector('input[name="admin_features"]:checked')) filled++;
|
||||||
|
|
||||||
|
const pct = Math.round((filled / TOTAL) * 100);
|
||||||
|
document.getElementById('progressFill').style.width = pct + '%';
|
||||||
|
document.getElementById('progressText').textContent = `Заполнено: ${filled} из ${TOTAL}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePreview() {
|
||||||
|
const container = document.getElementById('previewContent');
|
||||||
|
const labels = {
|
||||||
|
slot_type: 'Тип слотов',
|
||||||
|
multi_hour: 'Несколько часов подряд',
|
||||||
|
calendar_view: 'Вид календаря',
|
||||||
|
advance_booking: 'Бронирование вперёд',
|
||||||
|
self_cancel: 'Самостоятельная отмена',
|
||||||
|
price_per_hour: 'Цена за час',
|
||||||
|
price_tiers: 'Тарифы',
|
||||||
|
refund_policy: 'Возврат при отмене',
|
||||||
|
invoice: 'Счёт-фактура',
|
||||||
|
lock_system: 'Система замка',
|
||||||
|
pin_length: 'Длина PIN',
|
||||||
|
pin_delivery: 'Доставка PIN',
|
||||||
|
booking_confirm: 'Подтверждение бронирования',
|
||||||
|
reminder_before: 'Напоминание перед слотом',
|
||||||
|
time_warning: 'Предупреждение о конце',
|
||||||
|
admin_notify: 'Уведомления для вас',
|
||||||
|
client_history: 'История клиентов',
|
||||||
|
product_format: 'Формат приложения',
|
||||||
|
language: 'Язык',
|
||||||
|
logo: 'Логотип',
|
||||||
|
design_style: 'Стиль дизайна',
|
||||||
|
terms: 'Правила пользования',
|
||||||
|
privacy: 'Политика конфиденциальности',
|
||||||
|
};
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
|
||||||
|
Object.entries(labels).forEach(([name, label]) => {
|
||||||
|
const checked = document.querySelector(`input[name="${name}"]:checked`);
|
||||||
|
if (checked) {
|
||||||
|
const spanText = checked.closest('label').querySelector('span')?.textContent || checked.value;
|
||||||
|
html += `<div class="preview-item">
|
||||||
|
<div class="preview-item-label">${label}</div>
|
||||||
|
<div class="preview-item-value">${escapeHtml(spanText)}</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Чекбоксы
|
||||||
|
const checkedBoxes = [...document.querySelectorAll('input[name="admin_features"]:checked')];
|
||||||
|
if (checkedBoxes.length > 0) {
|
||||||
|
const vals = checkedBoxes.map(el => el.closest('label').querySelector('span')?.textContent || el.value).join(', ');
|
||||||
|
html += `<div class="preview-item">
|
||||||
|
<div class="preview-item-label">Панель управления</div>
|
||||||
|
<div class="preview-item-value">${escapeHtml(vals)}</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = html || '<p class="preview-empty">Начните заполнять форму — здесь появится предпросмотр</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(str) {
|
||||||
|
return str.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateURL() {
|
||||||
|
const data = {};
|
||||||
|
document.querySelectorAll('input[type="radio"]:checked, input[type="checkbox"]:checked').forEach(el => {
|
||||||
|
if (!data[el.name]) data[el.name] = [];
|
||||||
|
data[el.name].push(el.value);
|
||||||
|
});
|
||||||
|
const b64 = btoa(unescape(encodeURIComponent(JSON.stringify(data))));
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
url.searchParams.set('d', b64);
|
||||||
|
window.history.replaceState(null, '', url.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime() {
|
||||||
|
const d = new Date();
|
||||||
|
return d.getHours().toString().padStart(2,'0') + ':' + d.getMinutes().toString().padStart(2,'0');
|
||||||
|
}
|
||||||
|
|
||||||
|
function openModal() {
|
||||||
|
document.getElementById('modalOverlay').classList.add('open');
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
document.getElementById('modalOverlay').classList.remove('open');
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModalOutside(e) {
|
||||||
|
if (e.target === document.getElementById('modalOverlay')) closeModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showFlash(msg) {
|
||||||
|
const el = document.getElementById('flash');
|
||||||
|
el.textContent = msg;
|
||||||
|
el.classList.add('show');
|
||||||
|
setTimeout(() => el.classList.remove('show'), 2500);
|
||||||
|
}
|
||||||
@@ -11,27 +11,160 @@ body {
|
|||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
/* ─── Шапка компании ─── */
|
||||||
max-width: 720px;
|
.company-header {
|
||||||
margin: 0 auto;
|
display: flex;
|
||||||
padding: 40px 20px 80px;
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 16px 24px;
|
||||||
|
background: #1a1a1a;
|
||||||
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
header {
|
.company-logo {
|
||||||
text-align: center;
|
width: 52px;
|
||||||
margin-bottom: 48px;
|
height: 52px;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
.company-name {
|
||||||
font-size: 2rem;
|
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
margin-bottom: 8px;
|
font-size: 1rem;
|
||||||
color: #1a1a1a;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subtitle {
|
.company-email {
|
||||||
color: #666;
|
font-size: 0.85rem;
|
||||||
font-size: 1rem;
|
color: #e53935;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.company-email:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Тулбар ─── */
|
||||||
|
.toolbar {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
background: #fff;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
padding: 10px 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-status {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #888;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-right {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-btn {
|
||||||
|
padding: 7px 14px;
|
||||||
|
border: 1.5px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-family: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s, border-color 0.15s;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-btn:hover {
|
||||||
|
background: #f0f0f0;
|
||||||
|
border-color: #bbb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-btn.primary {
|
||||||
|
background: #2d6a4f;
|
||||||
|
color: #fff;
|
||||||
|
border-color: #2d6a4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-btn.primary:hover {
|
||||||
|
background: #1b4332;
|
||||||
|
border-color: #1b4332;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Flash ─── */
|
||||||
|
.flash {
|
||||||
|
position: fixed;
|
||||||
|
top: 60px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%) translateY(-20px);
|
||||||
|
background: #1a1a1a;
|
||||||
|
color: #fff;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 0.2s, transform 0.2s;
|
||||||
|
z-index: 200;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flash.show {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(-50%) translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Макет ─── */
|
||||||
|
.app-layout {
|
||||||
|
display: flex;
|
||||||
|
gap: 24px;
|
||||||
|
max-width: 1300px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 32px 24px 80px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-column {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 720px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-column {
|
||||||
|
width: 360px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
position: sticky;
|
||||||
|
top: 72px;
|
||||||
|
max-height: calc(100vh - 80px);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Прогресс ─── */
|
||||||
|
header {
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +174,7 @@ h1 {
|
|||||||
background: #e0e0e0;
|
background: #e0e0e0;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-fill {
|
.progress-fill {
|
||||||
@@ -53,48 +186,53 @@ h1 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.progress-text {
|
.progress-text {
|
||||||
font-size: 0.85rem;
|
font-size: 0.82rem;
|
||||||
color: #888;
|
color: #888;
|
||||||
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ─── Секции ─── */
|
||||||
.section {
|
.section {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 32px;
|
padding: 28px 32px 20px;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 8px;
|
||||||
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
|
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 2px solid #e8f5e9;
|
||||||
|
}
|
||||||
|
|
||||||
.section h2 {
|
.section h2 {
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #2d6a4f;
|
color: #2d6a4f;
|
||||||
margin-bottom: 24px;
|
|
||||||
padding-bottom: 12px;
|
|
||||||
border-bottom: 2px solid #e8f5e9;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.question {
|
.question {
|
||||||
margin-bottom: 28px;
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.question:last-child {
|
.question:last-of-type {
|
||||||
margin-bottom: 0;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
display: block;
|
display: block;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 6px;
|
||||||
font-size: 0.95rem;
|
font-size: 0.92rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
textarea, input[type="text"], input[type="email"], input[type="tel"] {
|
textarea, input[type="text"], input[type="email"], input[type="tel"] {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 12px 14px;
|
padding: 10px 12px;
|
||||||
border: 1.5px solid #ddd;
|
border: 1.5px solid #ddd;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
font-size: 0.95rem;
|
font-size: 0.92rem;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
color: #1a1a1a;
|
color: #1a1a1a;
|
||||||
background: #fafafa;
|
background: #fafafa;
|
||||||
@@ -103,7 +241,7 @@ textarea, input[type="text"], input[type="email"], input[type="tel"] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
min-height: 80px;
|
min-height: 72px;
|
||||||
}
|
}
|
||||||
|
|
||||||
textarea:focus, input:focus {
|
textarea:focus, input:focus {
|
||||||
@@ -113,9 +251,9 @@ textarea:focus, input:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
textarea.comment {
|
textarea.comment {
|
||||||
min-height: 50px;
|
min-height: 44px;
|
||||||
margin-top: 8px;
|
margin-top: 6px;
|
||||||
font-size: 0.88rem;
|
font-size: 0.85rem;
|
||||||
color: #555;
|
color: #555;
|
||||||
background: #f5f5f5;
|
background: #f5f5f5;
|
||||||
}
|
}
|
||||||
@@ -125,62 +263,446 @@ textarea.comment::placeholder {
|
|||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ─── Кнопки добавления ─── */
|
||||||
|
.add-field-btn {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
margin-top: 12px;
|
||||||
|
background: none;
|
||||||
|
border: 1.5px dashed #c8e6c9;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #2d6a4f;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-family: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s, border-color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-field-btn:hover {
|
||||||
|
background: #f0faf4;
|
||||||
|
border-color: #2d6a4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-section-btn {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
background: none;
|
||||||
|
border: 1.5px dashed #ddd;
|
||||||
|
border-radius: 10px;
|
||||||
|
color: #888;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-family: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s, border-color 0.15s, color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-section-btn:hover {
|
||||||
|
background: #f8f8f8;
|
||||||
|
border-color: #aaa;
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Динамические поля */
|
||||||
|
.dynamic-question {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dynamic-question-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dynamic-question input.field-title {
|
||||||
|
flex: 1;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.92rem;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1.5px solid #ddd;
|
||||||
|
border-radius: 0;
|
||||||
|
background: transparent;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dynamic-question input.field-title:focus {
|
||||||
|
border-bottom-color: #2d6a4f;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #ccc;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
line-height: 1;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-btn:hover {
|
||||||
|
color: #e53935;
|
||||||
|
background: #fff0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Динамические секции */
|
||||||
|
.dynamic-section .section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dynamic-section h2[contenteditable="true"] {
|
||||||
|
flex: 1;
|
||||||
|
outline: none;
|
||||||
|
border-bottom: 2px solid #2d6a4f;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Submit ─── */
|
||||||
button[type="submit"] {
|
button[type="submit"] {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 16px;
|
padding: 15px;
|
||||||
background: #2d6a4f;
|
background: #2d6a4f;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
font-size: 1.1rem;
|
font-size: 1.05rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
font-family: inherit;
|
||||||
transition: background 0.2s, transform 0.1s;
|
transition: background 0.2s, transform 0.1s;
|
||||||
margin-top: 32px;
|
margin-top: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
button[type="submit"]:hover {
|
button[type="submit"]:hover { background: #1b4332; }
|
||||||
background: #1b4332;
|
button[type="submit"]:active { transform: scale(0.99); }
|
||||||
}
|
button[type="submit"]:disabled { background: #aaa; cursor: not-allowed; }
|
||||||
|
|
||||||
button[type="submit"]:active {
|
|
||||||
transform: scale(0.99);
|
|
||||||
}
|
|
||||||
|
|
||||||
button[type="submit"]:disabled {
|
|
||||||
background: #aaa;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit-note {
|
.submit-note {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #888;
|
color: #888;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Превью панель ─── */
|
||||||
|
.preview-panel {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-header {
|
||||||
|
padding: 12px 20px;
|
||||||
|
background: #f0faf4;
|
||||||
|
border-bottom: 1px solid #e8f5e9;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
margin-top: 12px;
|
font-weight: 600;
|
||||||
|
color: #2d6a4f;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-message {
|
.preview-a4 {
|
||||||
background: #fff3f3;
|
padding: 24px 20px;
|
||||||
border: 1px solid #f5c6cb;
|
min-height: 400px;
|
||||||
color: #721c24;
|
font-size: 0.88rem;
|
||||||
padding: 12px 16px;
|
line-height: 1.6;
|
||||||
border-radius: 8px;
|
}
|
||||||
margin-top: 16px;
|
|
||||||
|
.preview-empty {
|
||||||
|
color: #bbb;
|
||||||
|
font-style: italic;
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-section-title {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: #2d6a4f;
|
||||||
|
border-bottom: 1.5px solid #e8f5e9;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-item {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-item-label {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
color: #555;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-item-value {
|
||||||
|
color: #1a1a1a;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-item-comment {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #888;
|
||||||
|
font-style: italic;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Кнопка внизу ─── */
|
||||||
|
.bottom-contact {
|
||||||
|
max-width: 1300px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 24px 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-contact-btn {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 16px;
|
||||||
|
background: #1a1a1a;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-family: inherit;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-contact-btn:hover {
|
||||||
|
background: #e53935;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Модальное окно ─── */
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0,0,0,0.5);
|
||||||
|
z-index: 300;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 16px;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-overlay.open {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 520px;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
box-shadow: 0 20px 60px rgba(0,0,0,0.2);
|
||||||
|
transform: translateY(20px);
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-overlay.open .modal {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 20px 24px 16px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header h3 {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: #999;
|
||||||
|
cursor: pointer;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close:hover {
|
||||||
|
color: #333;
|
||||||
|
background: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 20px 24px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-subtitle {
|
||||||
|
color: #666;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
display: none;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-field {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-field label {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-field textarea {
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.required {
|
||||||
|
color: #e53935;
|
||||||
|
}
|
||||||
|
|
||||||
|
.optional {
|
||||||
|
color: #aaa;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-submit {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 14px;
|
||||||
|
background: #1a1a1a;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-top: 20px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-submit:hover {
|
||||||
|
background: #e53935;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-submit:disabled {
|
||||||
|
background: #aaa;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-note {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #bbb;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-success {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-success-icon {
|
||||||
|
font-size: 3rem;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-success h4 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-success p {
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Планшет ─── */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.app-layout {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-column {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-column {
|
||||||
|
width: 100%;
|
||||||
|
position: static;
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Телефон ─── */
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
.container {
|
.toolbar {
|
||||||
padding: 24px 16px 60px;
|
padding: 8px 12px;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-title {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-status {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
max-width: 100px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-right {
|
||||||
|
gap: 6px;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-btn {
|
||||||
|
padding: 7px 10px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-text {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-layout {
|
||||||
|
padding: 16px 12px 80px;
|
||||||
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section {
|
.section {
|
||||||
padding: 20px;
|
padding: 20px 14px 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
.bottom-contact {
|
||||||
font-size: 1.5rem;
|
padding: 0 12px 40px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user