поиск+доставка
This commit is contained in:
+197
-5
@@ -11,6 +11,10 @@ declare(strict_types=1);
|
||||
$megaCategories = [];
|
||||
$currentPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/';
|
||||
$isCartPage = $currentPath === '/cart';
|
||||
$yandexMapsApiKey = (string) app_env('YANDEX_MAPS_API_KEY', '9ab8ad56-c350-4a2c-8f23-35acb102ee7a');
|
||||
$queryParameters = [];
|
||||
parse_str((string) (parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_QUERY) ?? ''), $queryParameters);
|
||||
$currentSearchQuery = (string) ($_GET['q'] ?? $queryParameters['q'] ?? '');
|
||||
$brandLogoSvg = <<<'SVG'
|
||||
<svg class="brand-logo-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 650 220" role="img" aria-labelledby="brand-logo-title brand-logo-desc">
|
||||
<title id="brand-logo-title">Рыбсток</title>
|
||||
@@ -65,7 +69,15 @@ $faviconSvg = <<<'SVG'
|
||||
</svg>
|
||||
SVG;
|
||||
$faviconHref = 'data:image/svg+xml,' . rawurlencode($faviconSvg);
|
||||
|
||||
$homeEmbeddedImages = [
|
||||
'home-hero-picnic' => '/assets/images/home/hero-picnic-sunset.png',
|
||||
'home-warehouse-fish' => '/assets/images/home/warehouse-fish.jpg',
|
||||
'home-banner-meat' => '/assets/images/home/banner-meat-grill.png',
|
||||
'home-banner-seafood' => '/assets/images/home/banner-seafood-fresh.png',
|
||||
'home-warehouse-main' => '/assets/images/home/warehouse-main.jpg',
|
||||
'home-warehouse-arrival' => '/assets/images/home/warehouse-arrival.jpg',
|
||||
'home-warehouse-cold' => '/assets/images/home/warehouse-cold.jpg',
|
||||
];
|
||||
try {
|
||||
$megaCategories = (new \App\Repositories\CategoryRepository())->tree();
|
||||
} catch (\Throwable) {
|
||||
@@ -85,6 +97,19 @@ try {
|
||||
<meta name="keywords" content="<?= e($metaKeywords) ?>">
|
||||
<?php endif; ?>
|
||||
<link rel="icon" type="image/svg+xml" href="<?= e($faviconHref) ?>">
|
||||
<?php if ($yandexMapsApiKey !== ''): ?>
|
||||
<meta name="yandex-maps-api-key" content="<?= e($yandexMapsApiKey) ?>">
|
||||
<script>
|
||||
window.RYBSTOCK_YANDEX_MAPS_API_KEY = <?= json_encode($yandexMapsApiKey, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
<style id="embedded-home-images">
|
||||
:root {
|
||||
<?php foreach ($homeEmbeddedImages as $homeImageKey => $homeImagePath): ?>
|
||||
--<?= e($homeImageKey) ?>: url("<?= e($homeImagePath) ?>");
|
||||
<?php endforeach; ?>
|
||||
}
|
||||
</style>
|
||||
<meta name="theme-color" content="#173d2c">
|
||||
<link rel="stylesheet" href="/assets/css/app.css">
|
||||
</head>
|
||||
@@ -110,14 +135,14 @@ try {
|
||||
|
||||
<section class="smart-search-bar" aria-label="Поиск по каталогу">
|
||||
<div class="smart-search-inner">
|
||||
<form class="smart-search-form" action="/catalog" method="get">
|
||||
<form class="smart-search-form" action="/search" method="get" onsubmit="return window.rybstockSearchSubmit ? window.rybstockSearchSubmit(this) : true">
|
||||
<label class="smart-search-label" for="site-search">Умный поиск</label>
|
||||
<div class="smart-search-control">
|
||||
<input id="site-search" name="q" type="search" placeholder="Например: форель стейки, креветки 1 кг, красная рыба, что взять на ужин">
|
||||
<input id="site-search" name="q" type="search" value="<?= e($currentSearchQuery) ?>" placeholder="Например: форель стейки, креветки 1 кг, красная рыба, что взять на ужин">
|
||||
<button class="voice-search-button" type="button" aria-label="Голосовой ввод" title="Голосовой ввод">Голос</button>
|
||||
<button class="smart-search-submit" type="submit">Найти</button>
|
||||
</div>
|
||||
<p>Понимает склонения, похожие запросы и бытовые формулировки. Обучаемый поиск подключим к товарам после загрузки каталога.</p>
|
||||
<p>Понимает склонения, фасовки, похожие запросы и бытовые формулировки по текущему ассортименту.</p>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
@@ -204,6 +229,41 @@ try {
|
||||
</div>
|
||||
</footer>
|
||||
<script>
|
||||
(() => {
|
||||
const form = document.querySelector('.smart-search-form');
|
||||
const input = document.querySelector('#site-search');
|
||||
|
||||
if (!form || !input) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.rybstockSearchSubmit = (searchForm) => {
|
||||
const searchInput = searchForm.querySelector('input[name="q"]');
|
||||
const query = searchInput ? searchInput.value.trim() : '';
|
||||
|
||||
if (query === '') {
|
||||
window.location.href = '/catalog';
|
||||
return false;
|
||||
}
|
||||
|
||||
window.location.href = '/search/' + encodeURIComponent(query);
|
||||
return false;
|
||||
};
|
||||
|
||||
form.addEventListener('submit', (event) => {
|
||||
const query = input.value.trim();
|
||||
|
||||
if (query === '') {
|
||||
event.preventDefault();
|
||||
window.location.href = '/catalog';
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
window.location.href = '/search/' + encodeURIComponent(query);
|
||||
});
|
||||
})();
|
||||
|
||||
(() => {
|
||||
const button = document.querySelector('.voice-search-button');
|
||||
const input = document.querySelector('#site-search');
|
||||
@@ -234,6 +294,12 @@ try {
|
||||
const phrase = event.results[0]?.[0]?.transcript || '';
|
||||
input.value = phrase;
|
||||
input.focus();
|
||||
|
||||
if (phrase.trim() !== '') {
|
||||
window.setTimeout(() => {
|
||||
input.form?.requestSubmit();
|
||||
}, 120);
|
||||
}
|
||||
});
|
||||
|
||||
recognition.addEventListener('end', () => {
|
||||
@@ -271,6 +337,8 @@ try {
|
||||
cartItems: 'rybstock_cart_items',
|
||||
deliveryType: 'rybstock_delivery_type',
|
||||
deliveryDistance: 'rybstock_delivery_distance',
|
||||
deliveryZone: 'rybstock_delivery_zone',
|
||||
deliveryGeoStatus: 'rybstock_delivery_geo_status',
|
||||
deliveryLift: 'rybstock_delivery_lift',
|
||||
deliveryAddress: 'rybstock_delivery_address'
|
||||
};
|
||||
@@ -278,6 +346,8 @@ try {
|
||||
const getPaymentMode = () => localStorage.getItem(storageKeys.paymentMode) || 'cash';
|
||||
const getDeliveryType = () => localStorage.getItem(storageKeys.deliveryType) || '';
|
||||
const getDeliveryDistance = () => Math.max(0, Number(localStorage.getItem(storageKeys.deliveryDistance) || 0));
|
||||
const getDeliveryZone = () => localStorage.getItem(storageKeys.deliveryZone) || '';
|
||||
const getDeliveryGeoStatus = () => localStorage.getItem(storageKeys.deliveryGeoStatus) || '';
|
||||
const getDeliveryLift = () => localStorage.getItem(storageKeys.deliveryLift) === '1';
|
||||
const getCartTotal = () => Number(localStorage.getItem(storageKeys.cartTotal) || 0);
|
||||
const getCartItems = () => {
|
||||
@@ -360,11 +430,107 @@ try {
|
||||
return `Уважаемый клиент, Вы выбрали ${deliveryLabels[type] || 'получение'}, минимальная сумма - ${formatRub(minimum)}, добавьте пожалуйста товары на сумму ${formatRub(missing)}.`;
|
||||
};
|
||||
|
||||
const moscowCenter = [55.755864, 37.617698];
|
||||
const mkadRadiusKm = 17.7;
|
||||
let yandexMapsPromise = null;
|
||||
let deliveryAddressTimer = null;
|
||||
|
||||
const setDeliveryGeo = (distance, zone, status) => {
|
||||
localStorage.setItem(storageKeys.deliveryDistance, String(Math.max(0, Number(distance) || 0)));
|
||||
localStorage.setItem(storageKeys.deliveryZone, zone || '');
|
||||
localStorage.setItem(storageKeys.deliveryGeoStatus, status || '');
|
||||
};
|
||||
|
||||
const loadYandexMapsApi = () => {
|
||||
if (window.ymaps) {
|
||||
return new Promise((resolve) => window.ymaps.ready(resolve));
|
||||
}
|
||||
|
||||
if (yandexMapsPromise) {
|
||||
return yandexMapsPromise;
|
||||
}
|
||||
|
||||
const apiKey = window.RYBSTOCK_YANDEX_MAPS_API_KEY || document.querySelector('meta[name="yandex-maps-api-key"]')?.content || '';
|
||||
|
||||
yandexMapsPromise = new Promise((resolve, reject) => {
|
||||
if (!apiKey) {
|
||||
reject(new Error('Yandex Maps API key is empty'));
|
||||
return;
|
||||
}
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.src = `https://api-maps.yandex.ru/2.1/?apikey=${encodeURIComponent(apiKey)}&lang=ru_RU`;
|
||||
script.async = true;
|
||||
script.onload = () => window.ymaps ? window.ymaps.ready(resolve) : reject(new Error('Yandex Maps API is unavailable'));
|
||||
script.onerror = () => reject(new Error('Yandex Maps API loading failed'));
|
||||
document.head.append(script);
|
||||
});
|
||||
|
||||
return yandexMapsPromise;
|
||||
};
|
||||
|
||||
const haversineKm = (from, to) => {
|
||||
const toRad = (value) => value * Math.PI / 180;
|
||||
const earthRadiusKm = 6371;
|
||||
const dLat = toRad(to[0] - from[0]);
|
||||
const dLon = toRad(to[1] - from[1]);
|
||||
const lat1 = toRad(from[0]);
|
||||
const lat2 = toRad(to[0]);
|
||||
const a = Math.sin(dLat / 2) ** 2 + Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLon / 2) ** 2;
|
||||
|
||||
return 2 * earthRadiusKm * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
};
|
||||
|
||||
const resolveDeliveryAddress = async (address) => {
|
||||
const normalizedAddress = String(address || '').trim();
|
||||
|
||||
if (normalizedAddress.length < 6) {
|
||||
setDeliveryGeo(0, '', '');
|
||||
updateAllPrices();
|
||||
return;
|
||||
}
|
||||
|
||||
setDeliveryGeo(0, '', 'Рассчитываем расстояние по адресу...');
|
||||
updateAllPrices();
|
||||
|
||||
try {
|
||||
await loadYandexMapsApi();
|
||||
const result = await window.ymaps.geocode(normalizedAddress, { results: 1 });
|
||||
const first = result.geoObjects.get(0);
|
||||
|
||||
if (!first) {
|
||||
setDeliveryGeo(0, '', 'Адрес не найден. Проверьте написание, менеджер уточнит доставку по телефону.');
|
||||
updateAllPrices();
|
||||
return;
|
||||
}
|
||||
|
||||
const coords = first.geometry.getCoordinates();
|
||||
const distanceFromCenter = haversineKm(moscowCenter, coords);
|
||||
const outsideKm = Math.max(0, Math.ceil(distanceFromCenter - mkadRadiusKm));
|
||||
|
||||
if (outsideKm <= 0) {
|
||||
setDeliveryGeo(0, 'inside', 'Адрес найден: внутри МКАД.');
|
||||
} else {
|
||||
setDeliveryGeo(outsideKm, 'outside', `Адрес найден: примерно ${outsideKm} км от МКАД.`);
|
||||
}
|
||||
} catch (error) {
|
||||
setDeliveryGeo(0, '', 'Не удалось автоматически рассчитать адрес. Менеджер уточнит доставку по телефону.');
|
||||
}
|
||||
|
||||
updateAllPrices();
|
||||
};
|
||||
|
||||
const scheduleDeliveryAddressResolve = (address) => {
|
||||
window.clearTimeout(deliveryAddressTimer);
|
||||
deliveryAddressTimer = window.setTimeout(() => resolveDeliveryAddress(address), 700);
|
||||
};
|
||||
|
||||
const deliveryCalculation = (items = getCartItems()) => {
|
||||
const type = getDeliveryType();
|
||||
const amount = cartCurrentTotal(items);
|
||||
const lift = getDeliveryLift() && type !== 'pickup' ? 390 : 0;
|
||||
const distance = getDeliveryDistance();
|
||||
const zone = getDeliveryZone();
|
||||
const minimum = deliveryMinimums[type] || 0;
|
||||
const minimumOk = type ? amount >= minimum : false;
|
||||
const minimumMessage = type ? deliveryMinimumMessage(type, amount) : '';
|
||||
@@ -414,6 +580,10 @@ try {
|
||||
}
|
||||
|
||||
if (type === 'outside_mkad' && distance <= 0) {
|
||||
const zoneNote = zone === 'inside'
|
||||
? 'Адрес похож на адрес внутри МКАД. Выберите доставку внутри МКАД, если это верно.'
|
||||
: 'Введите адрес доставки, и сайт рассчитает примерное расстояние за МКАД автоматически.';
|
||||
|
||||
return {
|
||||
selected: true,
|
||||
minimumOk,
|
||||
@@ -422,7 +592,7 @@ try {
|
||||
lift,
|
||||
total: lift,
|
||||
label: 'Доставка за МКАД',
|
||||
note: 'Доставка за МКАД доступна от 5000 руб. До 10 км от МКАД бесплатная доставка от 10000 руб.; дальше минимальная сумма для бесплатной доставки считается как 1000 руб. за каждый км от МКАД. Если заказ не проходит на бесплатную доставку, расчет: 199 руб. + 60 руб. за км.'
|
||||
note: `${zoneNote} Доставка за МКАД доступна от 5000 руб. До 10 км от МКАД бесплатная доставка от 10000 руб.; дальше минимум для бесплатной доставки считается как 1000 руб. за каждый км от МКАД. Если заказ не проходит на бесплатную доставку, расчет: 199 руб. + 60 руб. за км.`
|
||||
};
|
||||
}
|
||||
|
||||
@@ -542,6 +712,7 @@ try {
|
||||
const outsideDistanceField = cartPage.querySelector('[data-outside-distance-field]');
|
||||
const liftField = cartPage.querySelector('[data-lift-field]');
|
||||
const addressInput = cartPage.querySelector('[data-cart-address]');
|
||||
const addressStatusNode = cartPage.querySelector('[data-cart-address-status]');
|
||||
const distanceInput = cartPage.querySelector('[data-cart-distance]');
|
||||
const liftInput = cartPage.querySelector('[data-cart-lift]');
|
||||
const items = getCartItems();
|
||||
@@ -619,6 +790,12 @@ try {
|
||||
distanceInput.value = getDeliveryDistance() > 0 ? String(getDeliveryDistance()) : '';
|
||||
}
|
||||
|
||||
if (addressStatusNode) {
|
||||
const status = getDeliveryGeoStatus();
|
||||
addressStatusNode.hidden = status === '';
|
||||
addressStatusNode.textContent = status;
|
||||
}
|
||||
|
||||
if (liftInput) {
|
||||
liftInput.checked = getDeliveryLift();
|
||||
}
|
||||
@@ -757,9 +934,16 @@ try {
|
||||
if (deliveryType) {
|
||||
localStorage.setItem(storageKeys.deliveryType, deliveryType.value);
|
||||
localStorage.setItem(storageKeys.deliveryDistance, '0');
|
||||
localStorage.setItem(storageKeys.deliveryZone, '');
|
||||
|
||||
if (deliveryType.value === 'pickup') {
|
||||
localStorage.setItem(storageKeys.deliveryLift, '0');
|
||||
localStorage.setItem(storageKeys.deliveryGeoStatus, '');
|
||||
} else {
|
||||
const address = localStorage.getItem(storageKeys.deliveryAddress) || '';
|
||||
if (address.trim() !== '') {
|
||||
scheduleDeliveryAddressResolve(address);
|
||||
}
|
||||
}
|
||||
|
||||
updateAllPrices();
|
||||
@@ -788,6 +972,11 @@ try {
|
||||
if (addressInput) {
|
||||
localStorage.setItem(storageKeys.deliveryAddress, addressInput.value);
|
||||
localStorage.setItem(storageKeys.deliveryDistance, '0');
|
||||
localStorage.setItem(storageKeys.deliveryZone, '');
|
||||
localStorage.setItem(storageKeys.deliveryGeoStatus, addressInput.value.trim() === '' ? '' : 'Рассчитываем расстояние по адресу...');
|
||||
if (getDeliveryType() !== 'pickup') {
|
||||
scheduleDeliveryAddressResolve(addressInput.value);
|
||||
}
|
||||
updateAllPrices();
|
||||
}
|
||||
});
|
||||
@@ -850,6 +1039,7 @@ try {
|
||||
deliveryType: getDeliveryType(),
|
||||
address: localStorage.getItem(storageKeys.deliveryAddress) || '',
|
||||
distance: getDeliveryDistance(),
|
||||
deliveryZone: getDeliveryZone(),
|
||||
lift: getDeliveryLift(),
|
||||
items,
|
||||
productsTotal,
|
||||
@@ -1094,3 +1284,5 @@ try {
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user