| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188 |
- {{--
- Компонент: <x-web-form slug="..." />
- Создан: 2026-05-07
- Переменные: $slug — slug формы из таблицы web_forms
- Логика: загружает форму по slug, рендерит поля согласно конфигурации,
- отправляет через fetch POST /forms/{slug}/submit (JSON),
- показывает сообщение об успехе или ошибки валидации.
- Если форма не найдена или неактивна — ничего не рендерит.
- --}}
- @php
- $webForm = \App\Models\WebForm::where('slug', $slug)->where('is_active', true)->first();
- @endphp
- @if($webForm && count($webForm->fields ?? []) > 0)
- @php $fid = 'wf_' . $webForm->slug; @endphp
- <form id="{{ $fid }}" class="wf-form" novalidate>
- @csrf
- <div class="wf-fields">
- @php
- // Группируем поля попарно для half-width
- $fields = $webForm->fields;
- $i = 0;
- $total = count($fields);
- @endphp
- @while($i < $total)
- @php $f = $fields[$i]; $next = $fields[$i + 1] ?? null; @endphp
- @if($f['width'] === 'half' && $next && $next['width'] === 'half')
- {{-- Пара half-width полей --}}
- <div class="form-row">
- <div class="form-group col-md-6">
- @include('components._wf_field', ['field' => $f])
- </div>
- <div class="form-group col-md-6">
- @include('components._wf_field', ['field' => $next])
- </div>
- </div>
- @php $i += 2; @endphp
- @else
- <div class="form-group">
- @include('components._wf_field', ['field' => $f])
- </div>
- @php $i++; @endphp
- @endif
- @endwhile
- </div>
- <div class="wf-errors alert alert-danger" style="display:none"></div>
- <button type="submit" class="btn btn-red" style="width:100%;justify-content:center;font-size:15px;padding:15px">
- Отправить заявку →
- </button>
- <p style="font-size:12px;color:var(--light);margin-top:12px;text-align:center">
- Нажимая кнопку, вы соглашаетесь с <a href="{{ route('privacy') }}" style="color:var(--red)">политикой конфиденциальности</a>
- </p>
- </form>
- <div id="{{ $fid }}_ok" style="display:none;background:var(--w);border:1px solid var(--line);border-radius:var(--r);padding:40px;text-align:center">
- <div style="font-size:52px;margin-bottom:16px">✅</div>
- <h3 style="font-family:var(--fh);font-size:24px;font-weight:800;margin-bottom:8px">Заявка отправлена!</h3>
- <p style="color:var(--muted);font-size:14px">Свяжемся в ближайшее время.</p>
- </div>
- <script>
- (function () {
- // Форматирует 10 пользовательских цифр в +7 (XXX) XXX-XX-XX.
- // Разделители добавляются только при наличии цифр после них — без "висячих" дефисов.
- function phoneFmt(d) {
- var out = '+7';
- if (d.length > 0) out += ' (' + d.slice(0, 3);
- if (d.length >= 3) out += ')';
- if (d.length > 3) out += ' ' + d.slice(3, 6);
- if (d.length > 6) out += '-' + d.slice(6, 8);
- if (d.length > 8) out += '-' + d.slice(8, 10);
- return out;
- }
- // Маска телефона: полностью keydown-driven, input-событие не используется
- function initPhoneMask(el) {
- el.setAttribute('placeholder', '+7 (___) ___-__-__');
- el.setAttribute('maxlength', '18');
- // Возвращает пользовательские цифры (без ведущей 7/8)
- function digits() {
- return el.value.replace(/\D/g, '').replace(/^[78]/, '');
- }
- el.addEventListener('focus', function () {
- if (!this.value) this.value = '+7';
- });
- el.addEventListener('blur', function () {
- if (digits().length === 0) this.value = '';
- });
- el.addEventListener('keydown', function (e) {
- if (e.ctrlKey || e.metaKey) return;
- if (['Tab','Enter','ArrowLeft','ArrowRight','Home','End'].includes(e.key)) return;
- e.preventDefault();
- var d = digits();
- if (e.key === 'Backspace') {
- d = d.slice(0, -1);
- } else if (/^\d$/.test(e.key) && d.length < 10) {
- d += e.key;
- } else {
- return;
- }
- this.value = phoneFmt(d);
- });
- // Вставка из буфера — нормализуем цифры
- el.addEventListener('paste', function (e) {
- e.preventDefault();
- var text = (e.clipboardData || window.clipboardData).getData('text');
- var d = (digits() + text.replace(/\D/g, '').replace(/^[78]/, '')).slice(0, 10);
- this.value = phoneFmt(d);
- });
- }
- // Валидация email: красная рамка при некорректном адресе
- function initEmailValidation(el) {
- el.addEventListener('blur', function () {
- if (!this.value) { this.style.borderColor = ''; return; }
- var ok = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(this.value);
- this.style.borderColor = ok ? '' : 'var(--red, #d12626)';
- });
- el.addEventListener('input', function () { this.style.borderColor = ''; });
- }
- var form = document.getElementById('{{ $fid }}');
- var ok = document.getElementById('{{ $fid }}_ok');
- var errs = form.querySelector('.wf-errors');
- // Применяем маски ко всем полям формы
- form.querySelectorAll('input[type=tel]').forEach(initPhoneMask);
- form.querySelectorAll('input[type=email]').forEach(initEmailValidation);
- form.addEventListener('submit', function (e) {
- e.preventDefault();
- errs.style.display = 'none';
- var data = {};
- var inputs = form.querySelectorAll('[data-wf-name]');
- inputs.forEach(function (el) {
- var name = el.dataset.wfName;
- var type = el.type || el.tagName.toLowerCase();
- if (type === 'checkbox') {
- data[name] = el.checked ? '1' : '';
- } else {
- data[name] = el.value;
- }
- });
- var btn = form.querySelector('[type=submit]');
- btn.disabled = true;
- btn.textContent = 'Отправляем...';
- fetch('{{ route('forms.submit', $webForm->slug) }}', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]')?.content
- || '{{ csrf_token() }}'
- },
- body: JSON.stringify(data),
- })
- .then(function (r) { return r.json().then(function (j) { return { ok: r.ok, data: j }; }); })
- .then(function (res) {
- if (res.ok && res.data.ok) {
- form.style.display = 'none';
- ok.style.display = 'block';
- } else {
- var msgs = Object.values(res.data.errors || {}).flat();
- errs.innerHTML = msgs.map(function (m) { return '<p class="mb-1">'+m+'</p>'; }).join('');
- errs.style.display = 'block';
- btn.disabled = false;
- btn.textContent = 'Отправить заявку →';
- }
- })
- .catch(function () {
- errs.innerHTML = '<p>Ошибка соединения. Попробуйте ещё раз.</p>';
- errs.style.display = 'block';
- btn.disabled = false;
- btn.textContent = 'Отправить заявку →';
- });
- });
- })();
- </script>
- @endif
|