feat: added web
Some checks failed
Deploy / deploy (push) Has been cancelled

This commit is contained in:
Ignat Karelov
2026-06-02 17:45:51 +03:00
parent 709df7eddc
commit 0719b11093
39 changed files with 12450 additions and 0 deletions

561
assets/js/main.js Normal file
View File

@@ -0,0 +1,561 @@
(function ($) {
'use strict';
$(function () {
const $expertise = $('.expertise');
if (!$expertise.length || typeof Swiper !== 'function') {
return;
}
const $slider = $expertise.find('.expertise__cases');
const $wrapper = $expertise.find('.expertise__track');
const $tabs = $expertise.find('.expertise__tab');
const sourceSlides = $wrapper.find('.expertise-card').map(function () {
const $slide = $(this);
return {
group: $slide.data('expertise-group'),
html: this.outerHTML,
};
}).get();
let expertiseSwiper = null;
let activeFilter = null;
const getSlidesByFilter = function (filter) {
return sourceSlides.filter(function (slide) {
return slide.group === filter;
});
};
const updateTabs = function (filter) {
$tabs.each(function () {
const $tab = $(this);
const isActive = $tab.data('expertise-filter') === filter;
$tab
.toggleClass('is-active', isActive)
.attr({
'aria-selected': String(isActive),
'aria-pressed': String(isActive),
});
});
};
const destroySlider = function () {
if (!expertiseSwiper) {
return;
}
expertiseSwiper.destroy(true, true);
expertiseSwiper = null;
};
const getLoopSlidesHtml = function (slides) {
// Для loop в Swiper важно, чтобы реальных слайдов было заметно больше,
// чем помещается в видимой области. Иначе Swiper отключает loop на широких экранах.
const minSlidesForLoop = 24;
const repeatCount = Math.max(1, Math.ceil(minSlidesForLoop / slides.length));
let html = '';
for (let i = 0; i < repeatCount; i += 1) {
slides.forEach(function (slide) {
html += slide.html;
});
}
return html;
};
const initSlider = function () {
const slidesCount = $wrapper.children('.swiper-slide').length;
const loopEnabled = slidesCount > 1;
expertiseSwiper = new Swiper($slider[0], {
slidesPerView: 'auto',
spaceBetween: 16,
speed: 650,
grabCursor: true,
allowTouchMove: true,
watchOverflow: false,
loop: loopEnabled,
loopedSlides: Math.min(12, slidesCount),
loopAdditionalSlides: Math.min(12, slidesCount),
loopPreventsSliding: false,
normalizeSlideIndex: true,
observer: true,
observeParents: true,
breakpoints: {
768: {
spaceBetween: 24,
},
1200: {
spaceBetween: 32,
},
},
});
};
const renderSlides = function (filter) {
const slides = getSlidesByFilter(filter);
if (!slides.length) {
return;
}
destroySlider();
$wrapper.html(getLoopSlidesHtml(slides));
initSlider();
};
const setActiveFilter = function (filter) {
if (activeFilter === filter || !getSlidesByFilter(filter).length) {
return;
}
activeFilter = filter;
updateTabs(filter);
renderSlides(filter);
};
const initialFilter = $tabs.filter('.is-active').first().data('expertise-filter')
|| $tabs.first().data('expertise-filter');
setActiveFilter(initialFilter);
$tabs.on('click', function () {
setActiveFilter($(this).data('expertise-filter'));
});
});
})(jQuery);
(function ($) {
'use strict';
$(function () {
const $results = $('.results');
if (!$results.length) {
return;
}
$results.addClass('is-animation-ready');
const showResults = function (target) {
$(target).addClass('is-in-view');
};
if (!('IntersectionObserver' in window)) {
$results.each(function () {
showResults(this);
});
return;
}
const observer = new IntersectionObserver(function (entries, currentObserver) {
entries.forEach(function (entry) {
if (!entry.isIntersecting) {
return;
}
showResults(entry.target);
currentObserver.unobserve(entry.target);
});
}, {
threshold: 0.24,
rootMargin: '0px 0px -12% 0px',
});
$results.each(function () {
observer.observe(this);
});
});
})(jQuery);
(function ($) {
'use strict';
$(function () {
const $faq = $('.faq');
if (!$faq.length) {
return;
}
$faq.on('click', '.faq-item__button', function () {
const $button = $(this);
const $item = $button.closest('.faq-item');
const $panel = $item.find('.faq-item__panel').first();
const isOpen = $button.attr('aria-expanded') === 'true';
$button.attr('aria-expanded', String(!isOpen));
if (isOpen) {
$panel.stop(true, true).slideUp(220, function () {
$panel.attr('hidden', true).removeAttr('style');
});
} else {
$panel.removeAttr('hidden').hide().stop(true, true).slideDown(220, function () {
$panel.removeAttr('style');
});
}
});
});
})(jQuery);
(function ($) {
'use strict';
$(function () {
const $forms = $('.contact-sentence');
if (!$forms.length) {
return;
}
const options = {
task: ['лендинг', 'интернет-магазин', 'интеграцию', 'дизайн', 'доработку сайта'],
method: ['Telegram', 'WhatsApp', 'звонок', 'email'],
time: ['09:0012:00', '12:0015:00', '15:0018:00', 'любое время'],
};
let $dateInput = $('.contact-date-input');
if (!$dateInput.length) {
$dateInput = $('<input class="contact-date-input" type="date" aria-hidden="true">').appendTo('body');
}
const cleanValue = function ($field) {
const text = $.trim($field.text()).replace(/\s+/g, ' ');
const placeholder = $.trim($field.data('placeholder') || '');
const normalized = text.replace(/[()]/g, '').replace(/▾/g, '').trim();
if ($field.hasClass('is-placeholder') || normalized === placeholder) {
return '';
}
return normalized;
};
const getPlaceholderText = function ($field) {
const placeholder = $.trim($field.data('placeholder') || '');
const withArrow = $field.hasClass('contact-field--selector');
return '(' + placeholder + (withArrow ? ' ▾' : '') + ')';
};
const setFieldValue = function ($field, value, isPlaceholder) {
const text = isPlaceholder ? getPlaceholderText($field) : value;
$field
.text(text)
.toggleClass('is-placeholder', !!isPlaceholder)
.toggleClass('is-filled', !isPlaceholder && !!value)
.removeClass('is-error');
updateHidden($field.closest('form'));
};
const updateHidden = function ($form) {
$form.find('.contact-field').each(function () {
const $field = $(this);
const name = $field.data('field');
if (!name) {
return;
}
$form.find('input[name="' + name + '"]').val(cleanValue($field));
});
};
const selectAll = function (el) {
const range = document.createRange();
const selection = window.getSelection();
range.selectNodeContents(el);
selection.removeAllRanges();
selection.addRange(range);
};
const setCaretEnd = function (el) {
const range = document.createRange();
const selection = window.getSelection();
range.selectNodeContents(el);
range.collapse(false);
selection.removeAllRanges();
selection.addRange(range);
};
const formatPhone = function (value) {
const raw = String(value || '').replace(/\s+/g, '');
const hasPlus = raw.indexOf('+') === 0;
const digits = raw.replace(/\D/g, '').slice(0, 11);
if (!digits) {
return hasPlus ? '+' : '';
}
let formatted = hasPlus || digits.charAt(0) === '7' ? '+' : '';
formatted += digits.slice(0, 1);
if (digits.length > 1) {
formatted += ' ' + digits.slice(1, 4);
}
if (digits.length > 4) {
formatted += ' ' + digits.slice(4, 7);
}
if (digits.length > 7) {
formatted += ' ' + digits.slice(7, 9);
}
if (digits.length > 9) {
formatted += ' ' + digits.slice(9, 11);
}
return formatted;
};
const openDropdown = function ($field) {
const type = $field.data('type');
const values = options[type] || [];
if (!values.length) {
return;
}
$('.contact-dropdown').remove();
const rect = $field[0].getBoundingClientRect();
const $dropdown = $('<ul class="contact-dropdown" role="listbox"></ul>');
values.forEach(function (value) {
$('<li role="option"></li>').text(value).appendTo($dropdown);
});
$('body').append($dropdown);
const left = Math.min(rect.left + window.pageXOffset - 10, window.pageXOffset + window.innerWidth - $dropdown.outerWidth() - 12);
$dropdown.css({
top: rect.bottom + window.pageYOffset + 8,
left: Math.max(12, left),
});
$dropdown.on('click', 'li', function () {
setFieldValue($field, $(this).text(), false);
$dropdown.remove();
});
};
const openDatePicker = function ($field) {
const rect = $field[0].getBoundingClientRect();
const picker = $dateInput[0];
$dateInput
.val('')
.css({
left: rect.left + 'px',
top: rect.top + 'px',
width: Math.max(rect.width, 24) + 'px',
height: Math.max(rect.height, 24) + 'px',
})
.off('change.contact')
.one('change.contact', function () {
if (this.value) {
setFieldValue($field, this.value, false);
}
});
requestAnimationFrame(function () {
try {
if (typeof picker.showPicker === 'function') {
picker.showPicker();
} else {
picker.focus();
picker.click();
}
} catch (error) {
picker.focus();
picker.click();
}
});
};
const isEmail = function (value) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
};
const validateForm = function ($form) {
let valid = true;
let $firstError = null;
const markError = function ($field) {
valid = false;
$field.addClass('is-error');
if (!$firstError) {
$firstError = $field;
}
};
$form.find('.contact-field').removeClass('is-error');
const required = ['name', 'task', 'phone', 'email', 'method', 'date', 'time'];
required.forEach(function (name) {
const $field = $form.find('.contact-field[data-field="' + name + '"]');
const value = cleanValue($field);
if (!value) {
markError($field);
}
});
const $name = $form.find('.contact-field[data-field="name"]');
const $phone = $form.find('.contact-field[data-field="phone"]');
const $email = $form.find('.contact-field[data-field="email"]');
const name = cleanValue($name);
const phone = cleanValue($phone);
const email = cleanValue($email);
if (name && name.length < 2) {
markError($name);
}
if (phone && phone.replace(/\D/g, '').length < 10) {
markError($phone);
}
if (email && !isEmail(email)) {
markError($email);
}
if (!valid && $firstError) {
$form.find('.contact-sentence__status')
.removeClass('is-success')
.text('Заполните подсвеченные поля.');
$('html, body').animate({ scrollTop: $firstError.offset().top - 120 }, 260);
}
return valid;
};
$forms.each(function () {
updateHidden($(this));
});
$(document).on('click keydown', '.contact-field--editable', function (event) {
const $field = $(this);
if ($field.attr('contenteditable')) {
return;
}
if (event.type === 'keydown' && event.key !== 'Enter' && event.key !== ' ') {
return;
}
event.preventDefault();
const value = cleanValue($field);
$field.attr('contenteditable', 'true');
$field.text(value);
$field.removeClass('is-placeholder is-error');
$field.focus();
selectAll($field[0]);
});
$(document).on('input', '.contact-field--editable', function () {
const $field = $(this);
if ($field.data('type') === 'phone') {
$field.text(formatPhone($field.text()));
setCaretEnd(this);
}
updateHidden($field.closest('form'));
});
$(document).on('paste', '.contact-field--editable[data-type="phone"]', function (event) {
event.preventDefault();
const clipboard = event.originalEvent.clipboardData || window.clipboardData;
const text = clipboard ? clipboard.getData('text') : '';
$(this).text(formatPhone(text));
setCaretEnd(this);
updateHidden($(this).closest('form'));
});
$(document).on('blur keydown', '.contact-field--editable[contenteditable="true"]', function (event) {
if (event.type === 'keydown' && event.key !== 'Enter') {
return;
}
event.preventDefault();
const $field = $(this);
const value = $.trim($field.text()).replace(/\s+/g, ' ');
$field.removeAttr('contenteditable');
if (!value) {
setFieldValue($field, '', true);
} else {
setFieldValue($field, value, false);
}
});
$(document).on('click keydown', '.contact-field--selector', function (event) {
if (event.type === 'keydown' && event.key !== 'Enter' && event.key !== ' ') {
return;
}
event.preventDefault();
event.stopPropagation();
const $field = $(this);
if ($field.data('type') === 'date') {
openDatePicker($field);
} else {
openDropdown($field);
}
});
$(document).on('click', function () {
$('.contact-dropdown').remove();
});
$forms.on('submit', function (event) {
event.preventDefault();
const $form = $(this);
const $status = $form.find('.contact-sentence__status');
updateHidden($form);
if (!validateForm($form)) {
return;
}
const payload = Object.fromEntries(new FormData($form[0]).entries());
console.log('Contact form payload:', payload);
$status
.addClass('is-success')
.text('Заявка подготовлена. Подключите обработчик отправки формы.');
});
});
})(jQuery);