(function cleanIndexUrl() {
if (location.pathname !== '/' && /\/index\.html$/.test(location.pathname)) {
const cleanPath = location.pathname.replace(/index\.html$/, '');
history.replaceState({}, '', cleanPath + location.search + location.hash);
}
})();
(function initTransclickSite() {
const counterId = 107738676;
const cookieKey = 'transclick-cookie-ok';
const calculatorLoginUrl = '/lk-test/login.html';
const returnUrlKey = 'transclick-return-url';
const defaultCalculatorBackendUrl = 'https://functions.yandexcloud.net/d4egpn97k07letkb0j0l';
const pendingAuthorizedRequestKey = 'lkPendingAuthorizedRequest';
const calculatorYandexLoginUrl = '/lk-test/login.html?start_yandex=1&flow=site_calc';
const externalCalculatorBackendUrl = 'https://functions.yandexcloud.net/d4egpn97k07letkb0j0l';
const calculatorCreateRequestResultKey = 'lkCreateRequestResult';
const menuBtn = document.querySelector('.menu-toggle');
const mobileNav = document.querySelector('.mobile-nav');
const cookieBanner = document.getElementById('cookie-banner');
const cookieAccept = document.getElementById('cookie-accept');
const searchForm = document.getElementById('site-search-form');
const searchInput = document.getElementById('site-search-input');
const searchResults = document.getElementById('search-results');
const searchIndex = Array.isArray(window.__TRANCLICK_SEARCH_INDEX) ? window.__TRANCLICK_SEARCH_INDEX : [];
const searchClusterMap = new Map(searchIndex.filter((item) => item?.cluster).map((item) => [item.cluster, item]));
const searchIndexByKey = new Map(searchIndex.filter((item) => item?.key).map((item) => [item.key, item]));
const searchNoiseWords = new Set(['и', 'или', 'в', 'во', 'на', 'по', 'из', 'с', 'со', 'к', 'ко', 'от', 'до', 'для', 'под', 'над', 'при', 'у', 'о', 'об']);
let calculatorModalState = null;
let mobileNavOpenedAt = 0;
const smallWords = new Set(['и', 'в', 'во', 'на', 'по', 'из', 'к', 'ко', 'с', 'со', 'до', 'для', 'от', 'за', 'без', 'под', 'или']);
const forceLowerWords = new Set(['переезд', 'тонник', 'тонна', 'тонны', 'тонн', 'машина', 'фура', 'трал', 'контейнер', 'автовоз', 'авто', 'газель']);
const tonnagePatterns = [
{ id: '2-5', patterns: [/(^|\s)(?:до\s*)?2(?:\s|[.,])*5\s*(?:т|тонн(?:а|ы)?|тонник)?(?=\s|$)/, /газел/i, /малотоннаж/i] },
{ id: '5', patterns: [/(^|\s)5\s*(?:т|тонн(?:а|ы)?|тонник)(?=\s|$)/, /пятитон/i] },
{ id: '10', patterns: [/(^|\s)10\s*(?:т|тонн(?:а|ы)?|тонник)(?=\s|$)/, /десятитон/i] },
{ id: '20', patterns: [/(^|\s)20\s*(?:т|тонн(?:а|ы)?|тонник)(?=\s|$)/, /(^|\s)фура(?=\s|$)/, /еврофур/i, /двадцатитон/i] }
];
const calculatorCargoTypes = [
'Домашние вещи',
'Бытовая техника',
'Оплачиваемый переезд',
'Пенсионный переезд (Оплачиваемый)',
'Военный переезд (Оплачиваемый)',
'Мебель',
'Транспортное средство',
'Кузов автомобиля',
'Контейнер',
'Контейнер морской',
'Коммерческий груз',
'Продукты',
'ТНП',
'Соль',
'Сахар',
'Мука',
'Рыба (неживая)',
'Кондитерские изделия',
'Негабаритный груз',
'Оборудование',
'Торговое оборудование',
'Экскаватор-погрузчик',
'Комбайн',
'Трактор',
'Спец. техника',
'Строительные материалы',
'Кирпич',
'Газобетонные блоки',
'Вагончик-бытовка',
'Лодка',
'Аэро-лодка',
'Катер',
'Яхта',
'Изделия из металла',
'Металлопрокат',
'Изделия из резины',
'Изделия из пластика',
'Бытовая химия',
'Станок',
'Текстиль',
'Труба',
'Пиломатериалы',
'ЛДСП',
'Запчасти',
'ЖД запчасти',
'Химия (не опасная)',
'Опасный груз (ADR 1-9)',
'Хрупкий груз',
'Фудтрак',
'Другое'
];
const calculatorServiceOptions = [
'Грузчики на погрузке',
'Грузчики на выгрузке',
'Страхование груза',
'Кран на погрузке',
'Кран на выгрузке',
'Перевозка домашних животных',
'Услуги не нужны'
];
const calculatorDocumentOptions = [
'Справка о расчёте стоимости',
'Коммерческое предложение по грузоперевозке',
'Счёт для юридических лиц',
'Образцы документов для военнослужащих и государственных служащих',
'Ничего не нужно'
];
const calculatorServiceTypes = [
'Междугородний переезд',
'Перевозка домашних вещей',
'Частные грузоперевозки',
'Грузоперевозки по России',
'Военный переезд',
'Машина до 2,5 тонн',
'Машина 5 тонн',
'Машина 10 тонн',
'Фура 20 тонн',
'Контейнерные перевозки',
'Перевозка тралом',
'Перевозка авто автовозом',
'Логистика для завода',
'Заказать доставку груза'
];
const helperEntries = [
{
labels: ['Надёжный перевозчик с понятными документами'],
title: 'Проверить перевозчика до заявки',
text:
'До старта перевозки можно посмотреть открытые документы компании, реквизиты и оферту. Это помогает спокойно принять решение и понимать, с кем вы работаете.',
href: '/documents/'
},
{
labels: ['Сопровождение заявки без потери деталей', 'статус заявки онлайн'],
title: 'Что значит сопровождение заявки',
text:
'После расчёта по перевозке сохраняются важные детали: документы, статусы, согласования и сообщения по вашей заявке. Не нужно собирать информацию по разным чатам и звонкам.',
href: '/lk-test/login.html'
},
{
labels: ['Расчёт под маршрут, объём и сроки', 'рассчитать грузоперевозку онлайн'],
title: 'Как считается перевозка',
text:
'Мы считаем перевозку не по шаблону, а по вашему маршруту, объёму, весу, срокам и условиям погрузки. Поэтому итоговая стоимость привязана к реальной задаче.',
href: '/tarify/'
},
{
labels: ['Для частных клиентов, бизнеса и производства'],
title: 'Кому подходит Трансклик',
text:
'Мы работаем с частными клиентами, семьями, бизнесом, снабжением, заводами и производственными площадками. Для каждой задачи подбирается свой формат перевозки.',
href: '/#services'
},
{
labels: ['Вещи, грузы, авто, контейнеры и негабарит'],
title: 'Какие перевозки можно заказать',
text:
'Через Трансклик можно организовать переезд, отправку вещей, доставку коммерческого груза, перевозку автомобиля, контейнерную отправку и сложные проектные перевозки.',
href: '/#services'
},
{
labels: ['От 200 кг по России'],
title: 'С какого веса можно заказать перевозку',
text:
'Трансклик работает с перевозками от 200 кг и выше. Если груз больше обычной посылки и нужен отдельный расчёт по маршруту, эта услуга подходит.',
href: '/gruzoperevozki-po-rossii/'
},
{
labels: ['Междугородние перевозки по России', 'грузоперевозки по россии', 'междугородние грузоперевозки', 'доставка груза по россии', 'заказать перевозку груза'],
href: '/gruzoperevozki-po-rossii/'
},
{
labels: ['междугородний переезд', 'переезд на пмж'],
href: '/mezhdugorodniy-pereezd/'
},
{
labels: ['перевозка вещей в другой город', 'отправить вещи в другой город', 'перевозка мебели между городами'],
href: '/perevozka-domashnih-veshchei/'
},
{
labels: ['военный переезд'],
href: '/gruzoperevozki-dlya-voennosluzhashchih/'
},
{
labels: ['машина до 2,5 тонн', 'газель межгород'],
href: '/gruzoperevozki-do-2-5-tonn/'
},
{
labels: ['машина 5 тонн'],
href: '/gruzoperevozki-5-tonn/'
},
{
labels: ['машина 10 тонн'],
href: '/gruzoperevozki-10-tonn/'
},
{
labels: ['фура 20 тонн'],
href: '/fury-20-tonn/'
},
{
labels: ['трал', 'перевозка тралом'],
href: '/perevozka-tralom/'
},
{
labels: ['автовоз'],
href: '/avtovoz-perevozka-avto/'
},
{
labels: ['контейнерные перевозки'],
href: '/konteynernye-gruzoperevozki/'
},
{
labels: ['логистика для бизнеса', 'логистика для завода', 'перевозка оборудования'],
href: '/zavodu-trebuetsya-perevozchik/'
},
{
labels: ['документы по перевозке в кабинете'],
title: 'Где смотреть документы по перевозке',
text:
'Открытые документы компании размещены на сайте. Документы по вашей заявке, история согласований и рабочая переписка доступны после входа в личный кабинет.',
href: '/documents/'
},
{
labels: ['личный кабинет перевозки', 'сайт перевозчика с личным кабинетом'],
title: 'Что даёт личный кабинет',
text:
'Личный кабинет нужен не вместо услуги, а для удобного ведения перевозки после расчёта: здесь видны документы, статусы, сообщения и история работы по заявке.',
href: '/lk-test/login.html'
}
];
const helperLookup = new Map();
const relatedSearchGroups = {
home: ['gruzoperevozki-po-rossii', 'mezhdugorodniy-pereezd', 'tarify', 'documents'],
'general-cargo': ['zakazat-dostavku-gruza', 'tarify', 'documents', 'zavodu-trebuetsya-perevozchik'],
move: ['perevozka-domashnih-veshchei', 'chastnye-gruzoperevozki-po-rossii', 'gruzoperevozki-dlya-voennosluzhashchih', 'tarify'],
'military-move': ['documents', 'mezhdugorodniy-pereezd', 'chastnye-gruzoperevozki-po-rossii', 'tarify'],
household: ['mezhdugorodniy-pereezd', 'chastnye-gruzoperevozki-po-rossii', 'tarify', 'documents'],
business: ['fury-20-tonn', 'konteynernye-gruzoperevozki', 'gruzoperevozki-po-rossii', 'documents'],
container: ['gruzoperevozki-po-rossii', 'zavodu-trebuetsya-perevozchik', 'tarify', 'documents'],
oversized: ['perevozka-tralom', 'fury-20-tonn', 'documents', 'tarify'],
trawl: ['perevozka-negabaritnyh-gruzov', 'fury-20-tonn', 'documents', 'tarify'],
'car-carrier': ['tarify', 'documents', 'otzyvy', 'gruzoperevozki-po-rossii'],
poput: ['gruzoperevozki-po-rossii', 'tarify', 'chastnye-gruzoperevozki-po-rossii', 'documents'],
tarifs: ['gruzoperevozki-po-rossii', 'mezhdugorodniy-pereezd', 'fury-20-tonn', 'documents'],
tariffs: ['gruzoperevozki-po-rossii', 'mezhdugorodniy-pereezd', 'fury-20-tonn', 'documents'],
documents: ['tarify', 'gruzoperevozki-po-rossii', 'otzyvy', 'mezhdugorodniy-pereezd'],
reviews: ['gruzoperevozki-po-rossii', 'mezhdugorodniy-pereezd', 'documents', 'tarify'],
'truck-2-5': ['gruzoperevozki-5-tonn', 'gruzoperevozki-10-tonn', 'tarify', 'zakazat-dostavku-gruza'],
'truck-5': ['gruzoperevozki-do-2-5-tonn', 'gruzoperevozki-10-tonn', 'tarify', 'zakazat-dostavku-gruza'],
'truck-10': ['gruzoperevozki-5-tonn', 'fury-20-tonn', 'tarify', 'zavodu-trebuetsya-perevozchik'],
'truck-20': ['gruzoperevozki-10-tonn', 'zavodu-trebuetsya-perevozchik', 'tarify', 'konteynernye-gruzoperevozki']
};
function trackGoal(name) {
if (typeof window.ym === 'function') {
window.ym(counterId, 'reachGoal', name);
}
}
function escapeHtml(value) {
return String(value ?? '')
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
function normalize(text) {
return String(text || '')
.toLowerCase()
.replace(/ё/g, 'е')
.replace(/[«»"']/g, ' ')
.replace(/[^a-zа-я0-9\s-]/gi, ' ')
.replace(/\s+/g, ' ')
.trim();
}
function formatQueryTitle(query) {
const words = normalize(query)
.split(' ')
.filter(Boolean);
return words
.map((word, index) => {
const previous = index > 0 ? words[index - 1] : '';
if (word === 'пмж') return 'ПМЖ';
if (smallWords.has(word) && index > 0) return word;
if (/^\d/.test(word)) return word;
if (forceLowerWords.has(word) && index > 0 && !smallWords.has(previous)) return word;
return word.charAt(0).toUpperCase() + word.slice(1);
})
.join(' ');
}
function escapeRegExp(value) {
return String(value || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function tokenizeSearch(text) {
return normalize(text)
.split(' ')
.filter(Boolean);
}
function meaningfulSearchTokens(text) {
return tokenizeSearch(text).filter((token) => /^\d+$/.test(token) || (token.length > 1 && !searchNoiseWords.has(token)));
}
function highlightSearchText(text, query) {
const source = String(text || '');
const tokens = meaningfulSearchTokens(query)
.filter((token, index, values) => values.indexOf(token) === index)
.sort((a, b) => b.length - a.length)
.slice(0, 6);
if (!source || !tokens.length) return escapeHtml(source);
const matcher = new RegExp(`(${tokens.map(escapeRegExp).join('|')})`, 'ig');
return source
.split(matcher)
.map((part, index) => (index % 2 ? `${escapeHtml(part)}` : escapeHtml(part)))
.join('');
}
function syncSearchQueryState(query) {
const value = String(query || '').trim();
const url = value ? `${location.pathname}?q=${encodeURIComponent(value)}` : location.pathname;
history.replaceState({}, '', url);
}
function normalizePhone(value) {
return String(value || '').replace(/[^\d+]/g, '').trim();
}
function normalizeEmail(value) {
return String(value || '').trim().toLowerCase();
}
function setReturnUrl(url) {
const value = String(url || '').trim();
if (!value) return;
sessionStorage.setItem(returnUrlKey, value);
}
function getCalculatorBackendUrls() {
const seen = new Set();
return [
window.CALCULATOR_BACKEND_URL,
window.LK_BACKEND_URL,
defaultCalculatorBackendUrl,
externalCalculatorBackendUrl
].map((value) => String(value || '').trim()).filter((value) => {
if (!value || seen.has(value)) return false;
seen.add(value);
return true;
});
}
async function requestCalculatorJson(url, payload) {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
let data = {};
try {
data = await response.json();
} catch (error) {}
if (!response.ok || !data.ok) {
const requestError = new Error(data.error || 'Не удалось отправить заявку.');
requestError.status = response.status;
throw requestError;
}
return data;
}
async function postCalculatorJson(payload) {
const urls = getCalculatorBackendUrls();
if (!urls.length) {
throw new Error('Не указан адрес backend-функции для калькулятора.');
}
let lastError = null;
for (let index = 0; index < urls.length; index += 1) {
try {
return await requestCalculatorJson(urls[index], payload);
} catch (error) {
lastError = error;
const status = Number(error?.status || 0);
const shouldFallback = index < urls.length - 1 && (!status || status === 404 || status >= 500);
if (!shouldFallback) throw error;
}
}
throw lastError || new Error('Не удалось отправить заявку.');
}
async function siteAtiCitySearch(query, siteAtiGuard) {
return postCalculatorJson({
action: 'site_ati_city_search',
query: String(query || '').trim(),
site_ati_guard: String(siteAtiGuard || '').trim()
});
}
function renderCheckboxGroup(options, groupName) {
return options
.map(
(item, index) => ``
)
.join('');
}
function showInlineMessage(node, text) {
if (!node) return;
node.textContent = text || '';
node.classList.toggle('is-visible', Boolean(text));
}
function renderSiteFromCitySuggestions(items) {
const box = document.getElementById('calc-route-from-suggestions');
if (!box) return;
if (!items.length) {
box.innerHTML = '';
box.classList.add('hidden');
return;
}
box.innerHTML = items.map((item) => `
${escapeHtml(String(item.normalized_name || item.display_name || ''))}
${escapeHtml(String(item.display_name || ''))}
`).join('');
box.classList.remove('hidden');
}
function renderSiteToCitySuggestions(items) {
const box = document.getElementById('calc-route-to-suggestions');
if (!box) return;
if (!items.length) {
box.innerHTML = '';
box.classList.add('hidden');
return;
}
box.innerHTML = items.map((item) => `
${escapeHtml(String(item.normalized_name || item.display_name || ''))}
${escapeHtml(String(item.display_name || ''))}
`).join('');
box.classList.remove('hidden');
}
function clearSiteFromCityAtiFields(form) {
if (!form) return;
const fields = [
'ati_from_city_id',
'ati_from_city_name',
'ati_from_city_lat',
'ati_from_city_lon'
];
fields.forEach((name) => {
const input = form.querySelector(`[name="${name}"]`);
if (input) input.value = '';
});
}
function clearSiteToCityAtiFields(form) {
if (!form) return;
const fields = [
'ati_to_city_id',
'ati_to_city_name',
'ati_to_city_lat',
'ati_to_city_lon'
];
fields.forEach((name) => {
const input = form.querySelector(`[name="${name}"]`);
if (input) input.value = '';
});
}
function applySiteFromCitySelection(form, item) {
if (!form || !item) return;
const routeInput = document.getElementById('calc-route-from');
const cityId = String(item.getAttribute('data-city-id') || '').trim();
const cityName = String(item.getAttribute('data-city-name') || '').trim();
const displayName = String(item.getAttribute('data-display-name') || '').trim();
const lat = String(item.getAttribute('data-lat') || '').trim();
const lon = String(item.getAttribute('data-lon') || '').trim();
if (routeInput) {
routeInput.value = displayName || cityName;
}
const fieldMap = {
ati_from_city_id: cityId,
ati_from_city_name: cityName || displayName,
ati_from_city_lat: lat,
ati_from_city_lon: lon
};
Object.entries(fieldMap).forEach(([name, value]) => {
const input = form.querySelector(`[name="${name}"]`);
if (input) input.value = value;
});
renderSiteFromCitySuggestions([]);
}
function applySiteToCitySelection(form, item) {
if (!form || !item) return;
const routeInput = document.getElementById('calc-route-to');
const cityId = String(item.getAttribute('data-city-id') || '').trim();
const cityName = String(item.getAttribute('data-city-name') || '').trim();
const displayName = String(item.getAttribute('data-display-name') || '').trim();
const lat = String(item.getAttribute('data-lat') || '').trim();
const lon = String(item.getAttribute('data-lon') || '').trim();
if (routeInput) {
routeInput.value = displayName || cityName;
}
const fieldMap = {
ati_to_city_id: cityId,
ati_to_city_name: cityName || displayName,
ati_to_city_lat: lat,
ati_to_city_lon: lon
};
Object.entries(fieldMap).forEach(([name, value]) => {
const input = form.querySelector(`[name="${name}"]`);
if (input) input.value = value;
});
renderSiteToCitySuggestions([]);
}
function getActiveCalcChannel(root) {
return root?.querySelector('.calc-switcher__btn.is-active')?.dataset.target || 'phone';
}
function getSiteAtiGuard(form) {
return String(new FormData(form).get('site_ati_guard') || '').trim();
}
function collectCalculatorDraft(form) {
const formData = new FormData(form);
const routeFrom = String(formData.get('route_from') || '').trim();
const routeTo = String(formData.get('route_to') || '').trim();
const cargoType = String(formData.get('cargo_type') || '').trim();
const cargoWeight = String(formData.get('cargo_weight') || '').trim();
const cargoDimensions = String(formData.get('cargo_dimensions') || '').trim();
const weightUnit = String(formData.get('weight_unit') || '').trim();
const dimensionsUnit = String(formData.get('dimensions_unit') || '').trim();
const atiFromCityId = String(formData.get('ati_from_city_id') || '').trim();
const atiFromCityName = String(formData.get('ati_from_city_name') || '').trim();
const atiFromCityLat = String(formData.get('ati_from_city_lat') || '').trim();
const atiFromCityLon = String(formData.get('ati_from_city_lon') || '').trim();
const atiToCityId = String(formData.get('ati_to_city_id') || '').trim();
const atiToCityName = String(formData.get('ati_to_city_name') || '').trim();
const atiToCityLat = String(formData.get('ati_to_city_lat') || '').trim();
const atiToCityLon = String(formData.get('ati_to_city_lon') || '').trim();
const servicePage = String(formData.get('service_page') || '').trim();
const siteAtiGuard = String(formData.get('site_ati_guard') || '').trim();
const services = formData.getAll('services');
const documents = formData.getAll('required_documents');
return {
route_from: routeFrom,
route_to: routeTo,
ati_from_city_id: atiFromCityId,
ati_from_city_name: atiFromCityName,
ati_from_city_lat: atiFromCityLat,
ati_from_city_lon: atiFromCityLon,
ati_to_city_id: atiToCityId,
ati_to_city_name: atiToCityName,
ati_to_city_lat: atiToCityLat,
ati_to_city_lon: atiToCityLon,
cargo_type: cargoType,
cargo_weight: cargoWeight,
weight_unit: weightUnit,
cargo_dimensions: cargoDimensions,
dimensions_unit: dimensionsUnit,
weight: cargoWeight,
dimensions: cargoDimensions,
service_page: servicePage,
site_ati_guard: siteAtiGuard,
services,
required_documents: documents,
documents,
comment: servicePage ? `Страница обращения: ${servicePage}.` : '',
privacy_consent: Boolean(formData.get('privacy_consent'))
};
}
function buildCalculatorSummaryText(payload) {
const route = [payload.route_from, payload.route_to].filter(Boolean).join(' — ');
const details = [payload.cargo_type, payload.cargo_weight, payload.cargo_dimensions].filter(Boolean).join(', ');
if (route && details) return `${route}. ${details}.`;
if (route) return `${route}.`;
if (details) return details;
return 'Перевозка по России';
}
function getDefaultCalculatorContext(pageKey) {
const pageMap = {
'mezhdugorodniy-pereezd': {
cargo: 'Оплачиваемый переезд',
services: ['Грузчики на погрузке', 'Грузчики на выгрузке']
},
'perevozka-domashnih-veshchei': {
cargo: 'Домашние вещи',
services: ['Грузчики на погрузке', 'Грузчики на выгрузке']
},
'gruzoperevozki-dlya-voennosluzhashchih': {
cargo: 'Военный переезд (Оплачиваемый)',
documents: ['Образцы документов для военнослужащих и государственных служащих']
},
'avtovoz-perevozka-avto': {
cargo: 'Транспортное средство',
services: ['Страхование груза']
},
'konteynernye-gruzoperevozki': {
cargo: 'Контейнер'
},
'perevozka-tralom': {
cargo: 'Негабаритный груз'
},
'zavodu-trebuetsya-perevozchik': {
cargo: 'Коммерческий груз',
documents: ['Коммерческое предложение по грузоперевозке', 'Счёт для юридических лиц']
}
};
return pageMap[pageKey] || null;
}
function getCurrentServiceLabel(button) {
if (button?.closest('.contact-shell')) {
return document.querySelector('h1')?.textContent?.trim() || 'Грузоперевозки по России';
}
const heading = button?.closest('section, article, .service-copy, .hero')?.querySelector('h1, h2, h3');
return heading?.textContent?.trim() || document.querySelector('h1')?.textContent?.trim() || 'Грузоперевозки по России';
}
function getCalculatorServiceByPage(pageKey) {
const pageMap = {
'mezhdugorodniy-pereezd': 'Междугородний переезд',
'perevozka-domashnih-veshchei': 'Перевозка домашних вещей',
'chastnye-gruzoperevozki-po-rossii': 'Частные грузоперевозки',
'gruzoperevozki-po-rossii': 'Грузоперевозки по России',
'gruzoperevozki-dlya-voennosluzhashchih': 'Военный переезд',
'gruzoperevozki-do-2-5-tonn': 'Машина до 2,5 тонн',
'gruzoperevozki-5-tonn': 'Машина 5 тонн',
'gruzoperevozki-10-tonn': 'Машина 10 тонн',
'fury-20-tonn': 'Фура 20 тонн',
'konteynernye-gruzoperevozki': 'Контейнерные перевозки',
'perevozka-tralom': 'Перевозка тралом',
'avtovoz-perevozka-avto': 'Перевозка авто автовозом',
'zavodu-trebuetsya-perevozchik': 'Логистика для завода',
'zakazat-dostavku-gruza': 'Заказать доставку груза'
};
return pageMap[pageKey] || '';
}
function closeMobileNav() {
if (!menuBtn || !mobileNav) return;
mobileNav.classList.remove('open');
document.body.classList.remove('mobile-nav-open');
menuBtn.setAttribute('aria-expanded', 'false');
}
function isMobileNavOpen() {
return Boolean(menuBtn && mobileNav && mobileNav.classList.contains('open'));
}
function toggleMobileNav() {
if (!menuBtn || !mobileNav) return;
const isOpen = mobileNav.classList.toggle('open');
document.body.classList.toggle('mobile-nav-open', isOpen);
menuBtn.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
if (isOpen) mobileNavOpenedAt = Date.now();
}
function shouldKeepMobileNavOpen(target) {
return target instanceof Element && Boolean(target.closest('.mobile-nav, .menu-toggle'));
}
function canAutoCloseMobileNav(target) {
return isMobileNavOpen() && window.innerWidth <= 980 && Date.now() - mobileNavOpenedAt > 260 && !shouldKeepMobileNavOpen(target);
}
if (menuBtn && mobileNav) {
menuBtn.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
toggleMobileNav();
});
mobileNav.addEventListener('click', (event) => {
event.stopPropagation();
const link = event.target.closest('a');
if (link) closeMobileNav();
});
window.addEventListener('resize', () => {
if (window.innerWidth > 980) closeMobileNav();
});
document.addEventListener('click', (event) => {
if (!isMobileNavOpen() || shouldKeepMobileNavOpen(event.target)) return;
closeMobileNav();
});
window.addEventListener(
'scroll',
() => {
if (!isMobileNavOpen() || window.innerWidth > 980 || Date.now() - mobileNavOpenedAt <= 260) return;
closeMobileNav();
},
{ passive: true }
);
['wheel', 'touchmove'].forEach((eventName) => {
document.addEventListener(
eventName,
(event) => {
if (!canAutoCloseMobileNav(event.target)) return;
closeMobileNav();
},
{ passive: true }
);
});
}
function getQuickSectionLabel(section, index) {
const heading =
section.querySelector('.section-head h2, .service-copy h1, .home-hero-copy h1, .docs-main .section-head h2, .policy-card h2, .service-aside h2')?.textContent?.trim() || '';
if (section.id === 'contacts') return 'Расчёт';
if (section.id === 'services') return 'Услуги';
if (section.querySelector('.search-form')) return 'Поиск';
if (section.querySelector('.home-hero-copy h1')) return 'Главное';
if (!heading && index === 0) return 'Главное';
if (/частые вопросы|вопросы/i.test(heading)) return 'FAQ';
if (/смежные услуги|близкие направления/i.test(heading)) return 'Ещё услуги';
if (/почему выбирают|что получает/i.test(heading)) return 'Плюсы';
if (/когда подходит/i.test(heading)) return 'Кому подходит';
if (/что важно|как /i.test(heading)) return 'Подробно';
if (/тариф/i.test(heading)) return 'Тарифы';
if (/документ/i.test(heading)) return 'Документы';
if (/отзыв/i.test(heading)) return 'Отзывы';
if (/поиск/i.test(heading)) return 'Поиск';
if (/о компании/i.test(heading)) return 'О компании';
if (index === 0) return 'Главное';
if (heading.length <= 22) return heading;
return heading.slice(0, 22).trim() + '…';
}
function initQuickNavigation() {
const header = document.querySelector('.site-header');
const main = document.querySelector('main');
if (!header || !main || document.body.dataset.page === 'lk-test') return;
if (document.body.dataset.page === 'poisk') return;
const sections = Array.from(main.querySelectorAll(':scope > section')).filter((section) => {
if (section.classList.contains('cookie-banner')) return false;
if (section.classList.contains('home-breadcrumbs-bar')) return false;
return section.querySelector('h1, h2, .search-form, .tariff-table, .reviews-grid, .policy-card');
});
if (sections.length < 2) return;
const navShell = document.createElement('div');
navShell.className = 'section-nav-shell';
navShell.innerHTML = `
`;
header.insertAdjacentElement('afterend', navShell);
const track = navShell.querySelector('.section-nav-track');
const items = [];
let activeId = '';
sections.forEach((section, index) => {
const label = getQuickSectionLabel(section, index);
const id = section.id || `section-${index + 1}`;
section.id = id;
section.dataset.quickSection = label;
const link = document.createElement('a');
link.className = 'section-tab';
link.href = `#${id}`;
link.textContent = label;
link.addEventListener('click', (event) => {
event.preventDefault();
section.scrollIntoView({ behavior: 'smooth', block: 'start' });
});
track.appendChild(link);
items.push({ id, section, link });
});
function updateOffsets() {
const navHeight = navShell.offsetHeight || 0;
navShell.style.top = `${header.offsetHeight}px`;
document.documentElement.style.setProperty('--quick-nav-offset', `${header.offsetHeight + navHeight + 22}px`);
}
function setActive(id) {
if (!id || activeId === id) return;
activeId = id;
items.forEach((item) => {
item.link.classList.toggle('is-active', item.id === id);
});
const current = items.find((item) => item.id === id);
current?.link.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
}
function syncActive() {
const threshold = (parseInt(getComputedStyle(document.documentElement).getPropertyValue('--quick-nav-offset'), 10) || 150) + 24;
let current = items[0]?.id || '';
items.forEach((item) => {
if (item.section.getBoundingClientRect().top <= threshold) {
current = item.id;
}
});
setActive(current);
}
updateOffsets();
syncActive();
let ticking = false;
window.addEventListener('scroll', () => {
if (ticking) return;
ticking = true;
window.requestAnimationFrame(() => {
syncActive();
ticking = false;
});
}, { passive: true });
window.addEventListener('resize', () => {
updateOffsets();
syncActive();
});
}
function initStickyBreadcrumbs() {
const header = document.querySelector('.site-header');
const source = document.querySelector('.breadcrumbs');
if (!header || !source) return;
const shell = document.createElement('div');
shell.className = 'mobile-breadcrumb-shell';
shell.innerHTML = ``;
const quickNav = document.querySelector('.section-nav-shell');
if (quickNav) {
quickNav.insertAdjacentElement('afterend', shell);
} else {
header.insertAdjacentElement('afterend', shell);
}
document.body.classList.add('has-sticky-breadcrumbs');
function syncOffsets() {
const quickNavHeight = quickNav?.offsetHeight || 0;
const crumbHeight = shell.offsetHeight || 0;
shell.style.top = `${header.offsetHeight + quickNavHeight}px`;
document.documentElement.style.setProperty('--quick-nav-offset', `${header.offsetHeight + quickNavHeight + crumbHeight + 22}px`);
}
syncOffsets();
window.addEventListener('resize', syncOffsets);
}
function ensureCalculatorModal() {
if (calculatorModalState) return calculatorModalState;
const root = document.createElement('div');
root.id = 'calc-modal';
root.className = 'calc-modal';
root.setAttribute('aria-hidden', 'true');
root.innerHTML = `
Трансклик
Сервис логистики
Вход в личный кабинет
Войдите в личный кабинет
Чтобы узнать расчёт стоимости и задать вопросы специалисту, укажите телефон или email и получите код доступа.
`;
document.body.appendChild(root);
const state = {
root,
formStep: root.querySelector('.calc-step--form'),
authStep: root.querySelector('.calc-step--auth'),
requestForm: root.querySelector('#calcRequestForm'),
authForm: root.querySelector('#calcAuthForm'),
requestMessage: root.querySelector('#calcRequestMessage'),
authMessage: root.querySelector('#calcAuthMessage'),
summary: root.querySelector('#calcSummaryText')
};
root.querySelectorAll('[data-close-calc]').forEach((button) => {
button.addEventListener('click', () => closeCalculatorModal());
});
root.querySelector('[data-calc-back]')?.addEventListener('click', () => setCalculatorStep('form'));
root.querySelectorAll('.calc-switcher__btn').forEach((button) => {
button.addEventListener('click', () => {
root.querySelectorAll('.calc-switcher__btn').forEach((item) => item.classList.remove('is-active'));
button.classList.add('is-active');
const target = button.dataset.target;
root.querySelectorAll('[data-auth-group]').forEach((group) => {
group.classList.toggle('hidden', group.dataset.authGroup !== target);
});
});
});
console.log('FORM FOUND:', state.requestForm);
state.requestForm?.addEventListener('submit', (event) => {
event.preventDefault();
showInlineMessage(state.requestMessage, '');
const payload = collectCalculatorDraft(state.requestForm);
console.log('PAYLOAD BEFORE SEND:', payload);
if (!payload.ati_from_city_id || !payload.ati_to_city_id) {
showInlineMessage(state.requestMessage, 'Выберите город из списка.');
return;
}
if (!payload.privacy_consent) {
showInlineMessage(state.requestMessage, 'Подтвердите согласие на обработку данных, чтобы продолжить.');
return;
}
state.summary.textContent = `Расчёт: ${buildCalculatorSummaryText(payload)}`;
setCalculatorStep('auth');
});
state.authForm?.addEventListener('submit', async (event) => {
event.preventDefault();
showInlineMessage(state.authMessage, '');
const channel = getActiveCalcChannel(root);
const phone = normalizePhone(root.querySelector('#calc-auth-phone')?.value || '');
const email = normalizeEmail(root.querySelector('#calc-auth-email')?.value || '');
if (channel === 'phone' && !phone) {
showInlineMessage(state.authMessage, 'Введите телефон, чтобы перейти к входу в кабинет.');
return;
}
if (channel === 'email' && !email) {
showInlineMessage(state.authMessage, 'Введите email, чтобы перейти к входу в кабинет.');
return;
}
const requestPayload = collectCalculatorDraft(state.requestForm);
const authPayload = {
channel,
phone: channel === 'phone' ? phone : '',
email: channel === 'email' ? email : ''
};
const submitBtn = state.authForm.querySelector('button[type="submit"]');
const initialText = submitBtn?.textContent || '';
if (submitBtn) {
submitBtn.disabled = true;
submitBtn.textContent = 'Создаем заявку...';
}
try {
const result = await postCalculatorJson({
action: 'create_request',
...requestPayload,
...authPayload
});
sessionStorage.setItem('lkDraftRequest', JSON.stringify(requestPayload));
sessionStorage.setItem('lkDraftAuth', JSON.stringify(authPayload));
sessionStorage.setItem(calculatorCreateRequestResultKey, JSON.stringify({
ok: true,
action: 'create_request',
contact_id: result.contact_id || '',
deal_id: result.deal_id || '',
channel,
phone: authPayload.phone,
email: authPayload.email,
message: result.message || (channel === 'phone'
? 'Заявка создана. Код должен прийти на указанный телефон.'
: 'Заявка создана. Код должен прийти на указанный email.')
}));
setReturnUrl(location.pathname + location.search + location.hash);
window.location.href = calculatorLoginUrl;
} catch (error) {
showInlineMessage(state.authMessage, error?.message || 'Не удалось создать заявку.');
} finally {
if (submitBtn) {
submitBtn.disabled = false;
submitBtn.textContent = initialText || 'Войти и продолжить';
}
}
});
const calcYandexLoginButton = root.querySelector('#calcYandexLoginButton');
calcYandexLoginButton?.addEventListener('click', () => {
showInlineMessage(state.authMessage, '');
startCalculatorYandexFlow(state);
});
calculatorModalState = state;
return state;
}
function startCalculatorYandexFlow(modal) {
const requestPayload = collectCalculatorDraft(modal.requestForm);
if (!requestPayload.ati_from_city_id || !requestPayload.ati_to_city_id) {
showInlineMessage(modal.authMessage, 'Сначала выберите города из списка.');
return;
}
if (!requestPayload.privacy_consent) {
showInlineMessage(modal.authMessage, 'Подтвердите согласие на обработку данных, чтобы продолжить.');
return;
}
try {
sessionStorage.setItem('lkDraftRequest', JSON.stringify(requestPayload));
sessionStorage.setItem(pendingAuthorizedRequestKey, JSON.stringify({
action: 'create_request_authorized',
source: 'site_calc',
requestPayload
}));
} catch (error) {}
setReturnUrl(location.pathname + location.search + location.hash);
window.location.href = calculatorYandexLoginUrl;
}
function setCalculatorStep(step) {
const modal = ensureCalculatorModal();
const isAuth = step === 'auth';
modal.formStep.classList.toggle('hidden', isAuth);
modal.authStep.classList.toggle('hidden', !isAuth);
showInlineMessage(modal.requestMessage, '');
showInlineMessage(modal.authMessage, '');
}
function closeCalculatorModal() {
const modal = document.getElementById('calc-modal');
if (!modal) return;
modal.classList.remove('is-visible');
modal.setAttribute('aria-hidden', 'true');
document.body.classList.remove('calc-modal-open');
}
function bindSiteAtiAutocomplete(modal) {
const form = modal?.requestForm;
const fromInput = document.getElementById('calc-route-from');
const toInput = document.getElementById('calc-route-to');
if (!form || !fromInput || !toInput) return;
if (form.dataset.atiAutocompleteBound === '1') return;
form.dataset.atiAutocompleteBound = '1';
let fromTimer = null;
let toTimer = null;
fromInput.addEventListener('input', () => {
clearSiteFromCityAtiFields(form);
const query = String(fromInput.value || '').trim();
const siteAtiGuard = getSiteAtiGuard(form);
if (fromTimer) clearTimeout(fromTimer);
if (query.length < 2) {
renderSiteFromCitySuggestions([]);
return;
}
fromTimer = setTimeout(async () => {
try {
const result = await siteAtiCitySearch(query, siteAtiGuard);
renderSiteFromCitySuggestions(Array.isArray(result?.items) ? result.items : []);
} catch (error) {
renderSiteFromCitySuggestions([]);
}
}, 300);
});
toInput.addEventListener('input', () => {
clearSiteToCityAtiFields(form);
const query = String(toInput.value || '').trim();
const siteAtiGuard = getSiteAtiGuard(form);
if (toTimer) clearTimeout(toTimer);
if (query.length < 2) {
renderSiteToCitySuggestions([]);
return;
}
toTimer = setTimeout(async () => {
try {
const result = await siteAtiCitySearch(query, siteAtiGuard);
renderSiteToCitySuggestions(Array.isArray(result?.items) ? result.items : []);
} catch (error) {
renderSiteToCitySuggestions([]);
}
}, 300);
});
const fromBox = document.getElementById('calc-route-from-suggestions');
const toBox = document.getElementById('calc-route-to-suggestions');
fromBox?.addEventListener('click', (event) => {
const item = event.target.closest('.ati-suggestion-item');
if (!item) return;
applySiteFromCitySelection(form, item);
});
toBox?.addEventListener('click', (event) => {
const item = event.target.closest('.ati-suggestion-item');
if (!item) return;
applySiteToCitySelection(form, item);
});
document.addEventListener('click', (event) => {
const target = event.target;
if (target === fromInput || fromBox?.contains(target)) {
return;
}
if (target === toInput || toBox?.contains(target)) {
return;
}
renderSiteFromCitySuggestions([]);
renderSiteToCitySuggestions([]);
});
}
function openCalculator(button) {
const modal = ensureCalculatorModal();
const requestForm = modal.requestForm;
const context = getDefaultCalculatorContext(document.body.dataset.page || '');
const serviceLabel = getCurrentServiceLabel(button);
const pageService = getCalculatorServiceByPage(document.body.dataset.page || '');
const selectedService = pageService || serviceLabel;
requestForm?.reset();
showInlineMessage(modal.requestMessage, '');
showInlineMessage(modal.authMessage, '');
modal.summary.textContent = '';
modal.root.querySelectorAll('.calc-switcher__btn').forEach((item) => item.classList.toggle('is-active', item.dataset.target === 'phone'));
modal.root.querySelectorAll('[data-auth-group]').forEach((group) => {
group.classList.toggle('hidden', group.dataset.authGroup !== 'phone');
});
if (requestForm) {
requestForm.querySelector('[name="service_page"]').value = selectedService || '';
if (context?.cargo) {
requestForm.querySelector('[name="cargo_type"]').value = context.cargo;
}
const serviceSet = new Set(context?.services || []);
const documentSet = new Set(context?.documents || []);
requestForm.querySelectorAll('input[name="services"]').forEach((input) => {
input.checked = serviceSet.has(input.value);
});
requestForm.querySelectorAll('input[name="required_documents"]').forEach((input) => {
input.checked = documentSet.has(input.value);
});
}
bindSiteAtiAutocomplete(modal);
setReturnUrl(location.pathname + location.search + location.hash);
setCalculatorStep('form');
modal.root.classList.add('is-visible');
modal.root.setAttribute('aria-hidden', 'false');
document.body.classList.add('calc-modal-open');
}
document.querySelectorAll('[data-open-calc]').forEach((button) => {
button.addEventListener('click', (event) => {
event.preventDefault();
trackGoal('new_calc_click');
openCalculator(button);
});
});
function hasCookieConsent() {
try {
return localStorage.getItem(cookieKey) === '1' || document.cookie.includes(cookieKey + '=1');
} catch {
return document.cookie.includes(cookieKey + '=1');
}
}
function acceptCookieBanner() {
try {
localStorage.setItem(cookieKey, '1');
} catch {}
document.cookie = cookieKey + '=1; path=/; max-age=' + 60 * 60 * 24 * 365;
cookieBanner?.classList.remove('is-visible');
}
window.acceptTransclickCookies = acceptCookieBanner;
if (cookieBanner && !hasCookieConsent()) {
cookieBanner.classList.add('is-visible');
}
cookieAccept?.addEventListener('click', (event) => {
event.preventDefault();
acceptCookieBanner();
});
function ensureHelperModal() {
let modal = document.getElementById('helper-modal');
if (!modal) {
modal = document.createElement('div');
modal.id = 'helper-modal';
modal.className = 'helper-modal';
modal.setAttribute('aria-hidden', 'true');
modal.innerHTML = `
`;
document.body.appendChild(modal);
}
return {
root: modal,
title: modal.querySelector('#helper-modal-title'),
text: modal.querySelector('#helper-modal-text'),
link: modal.querySelector('#helper-modal-link')
};
}
function openHelper(entry) {
const modal = ensureHelperModal();
modal.title.textContent = entry.title || '';
modal.text.textContent = entry.text || '';
if (entry.href) {
modal.link.href = entry.href;
modal.link.classList.remove('hidden');
} else {
modal.link.classList.add('hidden');
modal.link.removeAttribute('href');
}
modal.root.classList.add('is-visible');
modal.root.setAttribute('aria-hidden', 'false');
document.body.classList.add('helper-modal-open');
}
function closeHelper() {
const modal = document.getElementById('helper-modal');
if (!modal) return;
modal.classList.remove('is-visible');
modal.setAttribute('aria-hidden', 'true');
document.body.classList.remove('helper-modal-open');
}
function putHelper(label, config) {
const key = normalize(label);
if (!key || helperLookup.has(key)) return;
helperLookup.set(key, {
title: config.title || label,
text: config.text || '',
href: config.href || ''
});
}
helperEntries.forEach((entry) => {
entry.labels.forEach((label) => putHelper(label, entry));
});
searchIndex.forEach((item) => {
const labels = [item.title, item.serviceTitle, ...(item.aliases || []), ...(item.flags || [])].filter(Boolean);
labels.forEach((label) =>
putHelper(label, {
title: item.serviceTitle || item.title,
text: item.description,
href: item.url
})
);
});
function shouldOpenHelper(helper) {
return Boolean(helper && ((helper.title && helper.title.trim()) || (helper.text && helper.text.trim())));
}
function enhanceInfoNodes() {
document.querySelectorAll('.badge, .keyword-chip').forEach((node) => {
if (node.closest('.search-result__meta')) return;
if (node.matches('a, button')) return;
const label = node.textContent.trim();
const helper = helperLookup.get(normalize(label));
if (!helper) return;
const useModal = shouldOpenHelper(helper);
const replacement = document.createElement(useModal || !helper.href ? 'button' : 'a');
replacement.className = node.className + ' ' + (node.classList.contains('badge') ? 'badge--action' : 'keyword-chip--action');
replacement.textContent = label;
if (useModal) {
replacement.type = 'button';
replacement.addEventListener('click', () => openHelper(helper));
} else if (helper.href) {
replacement.href = helper.href;
}
node.replaceWith(replacement);
});
}
document.addEventListener('click', (event) => {
const helperTrigger = event.target.closest('[data-helper-title]');
if (helperTrigger) {
event.preventDefault();
openHelper({
title: helperTrigger.getAttribute('data-helper-title') || '',
text: helperTrigger.getAttribute('data-helper-text') || '',
href: helperTrigger.getAttribute('data-helper-link') || ''
});
return;
}
if (event.target.closest('[data-close-helper]')) {
event.preventDefault();
closeHelper();
}
});
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
closeMobileNav();
closeHelper();
closeCalculatorModal();
}
});
function detectTonnage(text) {
for (const item of tonnagePatterns) {
if (item.patterns.some((pattern) => pattern.test(text))) return item.id;
}
return null;
}
function includesAny(text, values) {
return values.some((value) => text.includes(value));
}
function classifyQuery(text) {
if (!text) return null;
const tonnage = detectTonnage(text);
if (tonnage) return `truck-${tonnage}`;
if (includesAny(text, ['трал', 'низкорам', 'низкорамник'])) return 'trawl';
if (includesAny(text, ['документ', 'реквизит', 'оферта', 'егрип', 'карточка'])) return 'documents';
if (includesAny(text, ['отзыв', 'отзывы', 'рейтинг', 'мнение'])) return 'reviews';
if (includesAny(text, ['тариф', 'тарифы', 'цена'])) return 'tariffs';
const military = includesAny(text, ['военн', 'военнослуж', 'службе']);
const move = includesAny(text, ['переезд', 'переехать', 'квартирн', 'пмж']);
const household = includesAny(text, ['вещ', 'мебел', 'короб', 'домашн', 'личн']);
const carDelivery =
includesAny(text, ['автовоз']) ||
((includesAny(text, ['авто', 'автомобил', 'машин']) || /авто$/.test(text)) && includesAny(text, ['достав', 'перевоз', 'отправ', 'перегон']));
const cargoFallback = includesAny(text, ['трактор', 'спецтехник', 'оборудован', 'станок', 'товар', 'паллет', 'запчаст', 'материал']);
if (military && (move || household)) return 'military-move';
if (carDelivery || /(^|\s)(?:перевозк|доставк|отправить)\S*.*(?:авто|автомоб)/.test(text)) return 'car-carrier';
if (includesAny(text, ['контейнер'])) return 'container';
if (includesAny(text, ['негабар', 'крупногабар', 'тяжеловес'])) return 'oversized';
if (cargoFallback) return 'general-cargo';
if (includesAny(text, ['завод', 'производств', 'снабжен', 'склад', 'b2b', 'отгруз'])) return 'business';
if (includesAny(text, ['попут'])) return 'poput';
if (military) return 'military-move';
if (move) return 'move';
if (household) return 'household';
if (includesAny(text, ['грузоперевоз', 'доставка груза', 'перевозка груза', 'отправить груз'])) return 'general-cargo';
return null;
}
function getClusterItem(cluster) {
return searchClusterMap.get(cluster) || null;
}
function intentDescription(cluster, item) {
const serviceTitle = String(item.serviceTitle || item.title || '').toLowerCase();
if (cluster === 'move') return 'Страница о переезде в другой город: что входит, как считается цена и с чего начать.';
if (cluster === 'military-move') return 'Страница о военном переезде: документы, порядок расчёта и перевозка имущества по России.';
if (cluster === 'household') return 'Страница о перевозке вещей: мебель, коробки, техника и домашнее имущество между городами.';
if (cluster === 'general-cargo') return 'Страница о грузоперевозках по России: виды перевозок, стоимость и подбор транспорта.';
if (cluster === 'business') return 'Страница для бизнеса, заводов и снабжения: регулярные рейсы, поставки и рабочий порядок по заявкам.';
if (cluster === 'container') return 'Страница о контейнерной перевозке: когда она подходит, что влияет на цену и как начать.';
if (cluster === 'oversized') return 'Страница о перевозке негабаритного груза: маршрут, подготовка и сопровождение сложной отправки.';
if (cluster === 'trawl') return 'Страница о перевозке тралом: спецтехника, тяжёлое оборудование и нестандартные грузы.';
if (cluster === 'car-carrier') return 'Страница о перевозке автомобиля автовозом: стоимость, сроки и порядок передачи машины.';
if (cluster === 'poput') return 'Страница о попутной перевозке: когда можно сэкономить и как понять, подходит ли этот вариант.';
if (cluster === 'tariffs') return 'Страница с тарифами и ценовыми ориентирами по основным видам перевозок.';
if (cluster === 'documents') return 'Страница с реквизитами, офертой и открытыми документами компании.';
if (cluster === 'reviews') return 'Страница с отзывами клиентов о реальных перевозках, расчёте и сопровождении заявки.';
if (cluster.startsWith('truck-')) return `Страница о перевозке ${serviceTitle}: для каких задач подходит машина и как считают стоимость.`;
return item.description;
}
function buildDirectResult(cluster) {
const item = getClusterItem(cluster);
if (!item) return [];
return [
{
...item,
description: intentDescription(cluster, item),
directMatch: true,
score: Number(item.priority || 0) + 260
}
];
}
function buildCompanionResults(cluster, currentUrl) {
const keys = Array.isArray(relatedSearchGroups[cluster]) ? relatedSearchGroups[cluster] : [];
return keys
.map((key) => searchIndexByKey.get(key))
.filter((item) => item && item.url !== currentUrl);
}
function uniqueSearchValues(values) {
return [...new Set(values.filter(Boolean))];
}
function scoreSearchItem(item, query, preferredCluster) {
const normalized = normalize(query);
if (!normalized) return null;
const title = normalize(item.title);
const serviceTitle = normalize(item.serviceTitle || item.title);
const description = normalize(item.description);
const keywords = normalize(item.keywords);
const aliases = uniqueSearchValues((Array.isArray(item.aliases) ? item.aliases : []).map((value) => normalize(value)));
const flags = uniqueSearchValues((Array.isArray(item.flags) ? item.flags : []).map((value) => normalize(value)));
const badges = uniqueSearchValues((Array.isArray(item.badges) ? item.badges : []).map((value) => normalize(value)));
const titleFields = uniqueSearchValues([title, serviceTitle]);
const intentFields = uniqueSearchValues([...titleFields, ...flags, ...aliases]);
const bodyFields = uniqueSearchValues([description, keywords, ...badges]);
const priority = Number(item.priority || 0);
let score = priority;
if (preferredCluster && item.cluster === preferredCluster) score += 160;
if (titleFields.some((field) => field === normalized)) score += 320;
else if (intentFields.some((field) => field === normalized)) score += 260;
if (titleFields.some((field) => field.startsWith(normalized))) score += 150;
else if (intentFields.some((field) => field.startsWith(normalized))) score += 120;
if (titleFields.some((field) => field.includes(normalized))) score += 180;
else if (intentFields.some((field) => field.includes(normalized))) score += 135;
else if (bodyFields.some((field) => field.includes(normalized))) score += 60;
const tokens = meaningfulSearchTokens(normalized);
if (!tokens.length) return score > priority ? { ...item, score } : null;
let matchedTokens = 0;
let titleMatches = 0;
let intentMatches = 0;
let bodyMatches = 0;
tokens.forEach((token) => {
if (titleFields.some((field) => field.includes(token))) {
matchedTokens += 1;
titleMatches += 1;
intentMatches += 1;
score += 38;
return;
}
if (intentFields.some((field) => field.includes(token))) {
matchedTokens += 1;
intentMatches += 1;
score += 26;
return;
}
if (bodyFields.some((field) => field.includes(token))) {
matchedTokens += 1;
bodyMatches += 1;
score += 10;
return;
}
score -= tokens.length > 1 ? 18 : 6;
});
if (!matchedTokens) return null;
const coverage = matchedTokens / tokens.length;
score += Math.round(coverage * 90);
if (titleMatches === tokens.length) score += 85;
if (intentMatches === tokens.length) score += 45;
if (bodyMatches && !intentMatches) score -= 18;
if (coverage < 0.55 && tokens.length > 1) score -= 40;
const phraseMatches = intentFields.filter((field) => field.includes(normalized) || normalized.includes(field));
if (phraseMatches.length) {
const closestDelta = phraseMatches.reduce((best, field) => Math.min(best, Math.abs(field.length - normalized.length)), 999);
score += Math.max(0, 48 - closestDelta);
}
return { ...item, score };
}
function lexicalSearch(query, preferredCluster) {
const normalized = normalize(query);
if (!normalized) return [];
return searchIndex
.map((item) => scoreSearchItem(item, normalized, preferredCluster))
.filter(Boolean)
.filter((item) => item.score > Number(item.priority || 0))
.sort((a, b) => b.score - a.score || Number(b.priority || 0) - Number(a.priority || 0))
.slice(0, 8);
}
function searchPages(query) {
const normalized = normalize(query);
if (!normalized) return [];
const cluster = classifyQuery(normalized);
const direct = cluster ? buildDirectResult(cluster) : [];
const companions = direct.length ? buildCompanionResults(cluster, direct[0].url) : [];
const lexical = direct.length ? [] : lexicalSearch(query, cluster);
const seen = new Set();
return [...direct, ...companions, ...lexical].filter((item) => {
if (!item?.url || seen.has(item.url)) return false;
seen.add(item.url);
return true;
});
}
function getSearchSourceLabel(item) {
const section =
item.cluster === 'home'
? 'Главная'
: item.cluster === 'documents'
? 'Документы'
: item.cluster === 'reviews'
? 'Отзывы'
: item.cluster === 'tariffs'
? 'Тарифы'
: item.cluster === 'business'
? 'Для бизнеса'
: 'Услуги';
const pageTitle = item.serviceTitle || item.title || 'Страница';
if (section === 'Главная' || section === pageTitle) return `Трансклик › ${pageTitle}`;
return `Трансклик › ${section} › ${pageTitle}`;
}
function getRelatedSearchLinks(item) {
const keys = Array.isArray(relatedSearchGroups[item.cluster]) ? relatedSearchGroups[item.cluster] : [];
return keys
.map((key) => searchIndexByKey.get(key))
.filter((related) => related && related.url !== item.url)
.slice(0, 4);
}
function getSearchResultTitle(item, currentQuery) {
return item.serviceTitle || item.title || formatQueryTitle(currentQuery);
}
function refineSearchDescription(text) {
return String(text || '')
.replace(/ведение перевозки/gi, 'понятный порядок перевозки')
.replace(/кабинетом по заявке/gi, 'удобным личным кабинетом')
.replace(/кабинет по заявке/gi, 'личный кабинет клиента')
.replace(/сопровождение перевозки/gi, 'поддержка по перевозке')
.replace(/сопровождение заявки/gi, 'помощь по перевозке')
.replace(/документы и статус перевозки/gi, 'документы и ход перевозки')
.trim();
}
function updateSearchKeywordsVisibility(query) {
const keywords = searchForm?.closest('.section')?.querySelector('.search-keywords');
if (!keywords) return;
keywords.hidden = Boolean(String(query || '').trim());
}
function renderSearchResults(query) {
if (!searchResults) return;
const trimmed = String(query || '').trim();
updateSearchKeywordsVisibility(trimmed);
if (!trimmed) {
searchResults.innerHTML = '';
return;
}
const found = searchPages(trimmed);
if (!found.length) {
searchResults.innerHTML = `Ничего не нашли по запросу «${escapeHtml(trimmed)}». Попробуйте написать короче: переезд, 5 тонн, фура 20 тонн, трал, документы.
`;
return;
}
searchResults.innerHTML = found
.map((item) => {
const relatedLinks = getRelatedSearchLinks(item);
const pageTitle = getSearchResultTitle(item, trimmed);
const description = refineSearchDescription(item.description || '');
const badges = Array.isArray(item.badges) ? item.badges.slice(0, 2) : [];
return `
${escapeHtml(getSearchSourceLabel(item))}
${highlightSearchText(pageTitle, trimmed)}
${
badges.length
? `${badges.map((badge) => `${escapeHtml(badge)}`).join('')}
`
: ''
}
${highlightSearchText(description, trimmed)}
${
relatedLinks.length
? ``
: ''
}
`;
})
.join('');
}
function initHomeSearchFirst() {
if (document.body.dataset.page !== 'home') return;
const main = document.querySelector('main');
const heroSection = main?.querySelector('.hero');
const searchSection = searchForm?.closest('.section');
const searchCard = searchSection?.querySelector('.search-card, .search-panel');
const searchResultsNode = searchSection?.querySelector('#search-results');
if (main && heroSection && searchSection && searchSection !== main.firstElementChild) {
main.insertBefore(searchSection, heroSection);
}
searchCard?.classList.add('search-card--hero');
const searchEyebrow = searchCard?.querySelector('.eyebrow');
const searchHeading = searchCard?.querySelector('.section-head h2');
const searchLead = searchCard?.querySelector('.section-head p');
if (searchEyebrow) searchEyebrow.textContent = 'Быстрый поиск перевозки';
if (searchHeading) searchHeading.textContent = 'Найдите услугу и сразу получите расчёт';
if (searchLead) {
searchLead.textContent =
'Введите маршрут, услугу, тип груза или машину. Поиск покажет нужную страницу, а кнопка расчёта откроет вашу форму калькулятора.';
}
if (searchInput) {
searchInput.placeholder = 'Например: переезд из Сургута в Москву, машина 5 тонн, контейнер, трал';
}
if (searchSection && searchResultsNode && !searchSection.querySelector('.search-keywords')) {
const keywords = document.createElement('div');
keywords.className = 'keyword-cloud search-keywords';
keywords.innerHTML = [
'переезд в другой город',
'перевозка домашних вещей',
'машина до 2,5 тонн',
'машина 5 тонн',
'машина 10 тонн',
'фура 20 тонн',
'контейнерные перевозки',
'перевозка тралом'
]
.map((label) => ``)
.join('');
searchResultsNode.insertAdjacentElement('afterend', keywords);
}
const heroTitle = heroSection?.querySelector('h1');
const heroLead = heroSection?.querySelector('p');
const heroBadges = heroSection?.querySelectorAll('.badge');
const heroCard = heroSection?.querySelector('.hero-card');
const heroCardTitle = heroCard?.querySelector('.hero-card__head h2');
const fakeFields = heroCard?.querySelectorAll('.fake-field');
const fieldLabels = heroCard?.querySelectorAll('.field-label');
const priceBox = heroCard?.querySelector('.price-box');
const priceStrong = priceBox?.querySelector('strong');
const priceLabelNodes = priceBox?.querySelectorAll('.field-label');
if (heroTitle) heroTitle.textContent = 'Транспортная компания для перевозок по России от 200 кг';
if (heroLead) {
heroLead.textContent =
'Трансклик помогает перевезти домашние вещи, мебель, технику, коммерческий груз, автомобиль, контейнер и негабарит. Для частных клиентов, бизнеса, заводов, фабрик и снабжения.';
}
const badgeTexts = [
'От 200 кг по России',
'Переезды и перевозка вещей',
'Для частных клиентов и бизнеса',
'Машины 2,5 / 5 / 10 / 20 тонн',
'Автовоз, контейнер и трал',
'Понятные документы и расчёт'
];
heroBadges?.forEach((badge, index) => {
if (badgeTexts[index]) badge.textContent = badgeTexts[index];
});
heroCard?.classList.add('hero-card--route');
if (heroCardTitle) heroCardTitle.textContent = 'Белый маршрут для вашей перевозки: от запроса до понятного расчёта.';
if (heroCard && !heroCard.querySelector('.hero-road')) {
const road = document.createElement('div');
road.className = 'hero-road';
road.innerHTML = `
1Описываете задачу
2Получаете расчёт
3Запускаете перевозку
`;
heroCard.querySelector('.hero-card__body')?.insertAdjacentElement('afterbegin', road);
}
if (fieldLabels?.[0]) fieldLabels[0].textContent = 'Что можно перевезти';
if (fakeFields?.[0]) {
fakeFields[0].textContent = 'Вещи, мебель, технику, паллеты, оборудование, автомобиль, контейнерный и тяжёлый груз';
}
if (fieldLabels?.[1]) fieldLabels[1].textContent = 'Для кого';
if (fakeFields?.[1]) fakeFields[1].textContent = 'Частные клиенты, семьи, бизнес, снабжение, производство';
if (fieldLabels?.[2]) fieldLabels[2].textContent = 'Как удобно начать';
if (fakeFields?.[2]) fakeFields[2].textContent = 'Через поиск по сайту или через форму расчёта с выбором услуги и типа груза';
if (priceLabelNodes?.[0]) priceLabelNodes[0].textContent = 'Слоган Трансклик';
if (priceStrong) priceStrong.textContent = 'Перевозка с открытым маршрутом';
if (priceLabelNodes?.[1]) {
priceLabelNodes[1].textContent = 'Сначала становится понятна цена и порядок действий, затем спокойно запускается сама перевозка.';
}
}
initHomeSearchFirst();
if (searchForm && searchInput && searchResults) {
searchForm.closest('.search-card, .policy-card')?.classList.add('search-card');
searchForm.closest('.container')?.classList.add('search-container-wide');
searchForm.closest('.section')?.addEventListener('click', (event) => {
const chip = event.target.closest('[data-search-chip]');
if (!chip) return;
event.preventDefault();
const query = chip.getAttribute('data-search-chip') || chip.textContent.trim();
searchInput.value = query;
syncSearchQueryState(query);
renderSearchResults(query);
searchInput.focus();
trackGoal('site_search');
});
const params = new URLSearchParams(location.search);
const initialQuery = params.get('q') || '';
if (initialQuery) {
searchInput.value = initialQuery;
}
renderSearchResults(initialQuery);
searchForm.addEventListener('submit', (event) => {
event.preventDefault();
const query = searchInput.value.trim();
syncSearchQueryState(query);
renderSearchResults(query);
if (query) trackGoal('site_search');
});
searchInput.addEventListener('input', () => {
const query = searchInput.value.trim();
syncSearchQueryState(query);
renderSearchResults(query);
});
searchInput.addEventListener('search', () => {
const query = searchInput.value.trim();
syncSearchQueryState(query);
renderSearchResults(query);
});
}
enhanceInfoNodes();
initQuickNavigation();
initStickyBreadcrumbs();
})();