2026-08-24 10:07:16 +00:00
|
|
|
|
import {Dispatch, SetStateAction, useEffect, useRef} from 'react';
|
|
|
|
|
|
import { getCartUpdates } from '../api/promoApi';
|
|
|
|
|
|
import { normalizeProductImage } from '../utils/product';
|
|
|
|
|
|
import type { Product } from '../types/cart';
|
|
|
|
|
|
|
|
|
|
|
|
interface UseCartPollingParams {
|
|
|
|
|
|
enabled: boolean;
|
|
|
|
|
|
onProductsChange: Dispatch<SetStateAction<Product[]>>;
|
|
|
|
|
|
onComplete: () => void;
|
|
|
|
|
|
onLoadingChange: (isLoading: boolean) => void;
|
|
|
|
|
|
setDisplayText: Dispatch<SetStateAction<string>>;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-28 06:41:34 +00:00
|
|
|
|
let missing_scenarios = [
|
|
|
|
|
|
[
|
|
|
|
|
|
[10_000, 'Ищем Брока...'],
|
|
|
|
|
|
[30_000, 'Брок сбежал из супермаркета...'],
|
|
|
|
|
|
[30_000, 'Мы всё ещё пытаемся его найти...'],
|
|
|
|
|
|
[10_000, 'Может быть он в колбасном отделе?'],
|
|
|
|
|
|
[10_000, 'Или в своём любимом?'],
|
|
|
|
|
|
[10_000, 'Какой у Брока любимый отдел?'],
|
|
|
|
|
|
[10_000, 'Если это прятки, то я проиграл, выходи'],
|
|
|
|
|
|
[15_000, 'Я сдаюсь, перезагрузите страницу?'],
|
|
|
|
|
|
],
|
|
|
|
|
|
[
|
|
|
|
|
|
[10_000, 'Брок у полки с соком...'],
|
|
|
|
|
|
[20_000, 'Он выбирает апельсиновый...'],
|
|
|
|
|
|
[15_000, 'Вспомнил, что любит яблочный...'],
|
|
|
|
|
|
[25_000, 'Теперь сравнивает состав...'],
|
|
|
|
|
|
[20_000, 'Сока нет, есть нектар...'],
|
|
|
|
|
|
[15_000, 'Брок возмущён...'],
|
|
|
|
|
|
[20_000, 'Он уходит с водой...'],
|
|
|
|
|
|
[10_000, 'Классика. Перезагрузите страницу?']
|
|
|
|
|
|
],
|
|
|
|
|
|
[
|
|
|
|
|
|
[10_000, 'Брок в молочном отделе...'],
|
|
|
|
|
|
[10_000, 'Ищет кефир 1%...'],
|
|
|
|
|
|
[10_000, 'Нашёл, но срок годности...'],
|
|
|
|
|
|
[10_000, 'Завтра. Брок не рискует...'],
|
|
|
|
|
|
[10_000, 'Берёт 2,5%...'],
|
|
|
|
|
|
[10_000, 'Потом ставит обратно...'],
|
|
|
|
|
|
[10_000, 'Пробует на ощупь ряженку...'],
|
|
|
|
|
|
[10_000, 'Это надолго. Перезагрузите страницу?']
|
|
|
|
|
|
],
|
|
|
|
|
|
[
|
|
|
|
|
|
[10_000, 'Брок в отделе замороженных продуктов...'],
|
|
|
|
|
|
[15_000, 'Брок застрял в отделе заморозки. Буквально...'],
|
|
|
|
|
|
[10_000, 'Брок пытается открыть морозилку изнутри...'],
|
|
|
|
|
|
[10_000, 'Кажется, два пломбира ему мешают...'],
|
|
|
|
|
|
[10_000, 'Вызвали спасателей и сотрудника с ломом...'],
|
|
|
|
|
|
[10_000, 'Брок заявил, что без пломбира не выйдет...'],
|
|
|
|
|
|
[10_000, 'Переговоры вступили в финальную стадию...'],
|
|
|
|
|
|
[10_000, 'У Брока коробка пломбира'],
|
|
|
|
|
|
[10_000, 'Перезагрузите страницу?'],
|
|
|
|
|
|
],
|
|
|
|
|
|
[
|
|
|
|
|
|
[10_000, 'Брок увидел акцию 1+1 и теперь не может выбрать...'],
|
|
|
|
|
|
[10_000, 'Брок сказал, что ему нужно ещё пять минуточек...'],
|
|
|
|
|
|
[10_000, 'Брок увлёкся бесплатными образцами...'],
|
|
|
|
|
|
[10_000, 'Кажется, Брок уже на КСО'],
|
|
|
|
|
|
[10_000, 'Брок забыл, что он собирает корзину?'],
|
|
|
|
|
|
[10_000, 'Брок двигается к выходу с арбузом...'],
|
|
|
|
|
|
[10_000, 'Брок отвлёкся на робот-пылесос...'],
|
|
|
|
|
|
[10_000, 'Перезагрузите страницу?'],
|
|
|
|
|
|
],
|
|
|
|
|
|
[
|
|
|
|
|
|
[10_000, 'Брок встал в очередь на кассу...'],
|
|
|
|
|
|
[10_000, 'У Брока в корзине один огурец и лотерейный билет...'],
|
|
|
|
|
|
[10_000, 'Брок завис на вопросе: «Пакет нужен?»'],
|
|
|
|
|
|
[10_000, 'Брок спрашивает: «А у вас есть пакет с динозаврами?»'],
|
|
|
|
|
|
[10_000, 'Брок утверждает, что видел такие пакеты в рекламе 2007 года...'],
|
|
|
|
|
|
[10_000, 'Менеджер ищет пакеты с динозаврами на складе...'],
|
|
|
|
|
|
[10_000, 'Это надолго, нужна перезагрузка'],
|
|
|
|
|
|
],
|
|
|
|
|
|
[
|
|
|
|
|
|
[10_000, 'Брок обнаружил тележку с крутящимся передним колесом...'],
|
|
|
|
|
|
[20_000, 'Теперь он использует её как скейтборд...'],
|
|
|
|
|
|
[30_000, 'Брок проехал мимо отдела напитков...'],
|
|
|
|
|
|
[15_000, 'Сотрудник делает замечание...'],
|
|
|
|
|
|
[15_000, 'Брок не слышит — он в наушниках...'],
|
|
|
|
|
|
[20_000, 'Брок врезался в пирамиду из консервов...'],
|
|
|
|
|
|
[25_000, 'Консервы разлетелись'],
|
|
|
|
|
|
[15_000, 'Брок помогает собирать, напевая оперу...'],
|
|
|
|
|
|
[10_000, 'Импровизированный концерт у полки с томатами.'],
|
|
|
|
|
|
[10_000, 'Перезагрузите страницу?'],
|
|
|
|
|
|
],
|
|
|
|
|
|
[
|
|
|
|
|
|
[10_000, 'Брок идёт к выходу, его останавливает охранник...'],
|
|
|
|
|
|
[10_000, 'Охранник просит показать чек...'],
|
|
|
|
|
|
[10_000, 'Брок показывает купон на скидку...'],
|
|
|
|
|
|
[10_000, 'Охранник терпеливо объясняет разницу...'],
|
|
|
|
|
|
[10_000, 'Брок делает вид, что не понимает по-русски...'],
|
|
|
|
|
|
[10_000, 'Охранник переходит на английский, французский...'],
|
|
|
|
|
|
[10_000, 'Брок отвечает на клингонском'],
|
|
|
|
|
|
[10_000, 'Охранник оказывается фанатом «Звёздного пути»'],
|
|
|
|
|
|
[10_000, 'Они уже пьют кофе и обсуждают спин-оффы.']
|
|
|
|
|
|
],
|
|
|
|
|
|
[
|
|
|
|
|
|
[10_000, 'Брок свернул не туда и оказался в отделе игрушек...'],
|
|
|
|
|
|
[10_000, 'Брок застыл перед полкой с резиновыми уточками...'],
|
|
|
|
|
|
[10_000, 'Брок нажимает на всех уточек по очереди...'],
|
|
|
|
|
|
[10_000, 'Магазин наполняется хором пищащих уток...'],
|
|
|
|
|
|
[10_000, 'Сотрудник пытается остановить, но Брок уже дирижирует...'],
|
|
|
|
|
|
[10_000, 'Покупатели подхватывают, достают уточек из корзин...'],
|
|
|
|
|
|
[10_000, 'Флешмоб пищащих уток выходит из-под контроля...'],
|
|
|
|
|
|
[10_000, 'Брок покидает отдел под овации, оставив уточку в кармане.'],
|
|
|
|
|
|
[10_000, 'Брока вернёт лишь перезагрузка страницы'],
|
|
|
|
|
|
]
|
|
|
|
|
|
] as const;
|
|
|
|
|
|
|
2026-08-24 10:07:16 +00:00
|
|
|
|
export function useCartPolling({
|
|
|
|
|
|
enabled,
|
|
|
|
|
|
onProductsChange,
|
|
|
|
|
|
onComplete,
|
|
|
|
|
|
onLoadingChange,
|
|
|
|
|
|
setDisplayText,
|
|
|
|
|
|
}: UseCartPollingParams) {
|
|
|
|
|
|
const timeoutRef = useRef<number | null>(null);
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!enabled) return;
|
|
|
|
|
|
|
|
|
|
|
|
let cancelled = false;
|
|
|
|
|
|
|
2026-08-28 06:41:34 +00:00
|
|
|
|
let missing_timeout: ReturnType<typeof setTimeout> | null = null;
|
|
|
|
|
|
let missing_scenario_index = Math.round(Math.random() * missing_scenarios.length);
|
|
|
|
|
|
let missing_scenario_element = -1;
|
|
|
|
|
|
|
|
|
|
|
|
const timeoutFunction = () => {
|
|
|
|
|
|
console.log(missing_scenario_index);
|
|
|
|
|
|
missing_scenario_element++;
|
|
|
|
|
|
setDisplayText(missing_scenarios[missing_scenario_index][missing_scenario_element][1])
|
|
|
|
|
|
missing_timeout = setTimeout(timeoutFunction, missing_scenarios[missing_scenario_index][missing_scenario_element][0]);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-08-24 10:07:16 +00:00
|
|
|
|
async function tick() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
onLoadingChange(true);
|
|
|
|
|
|
|
|
|
|
|
|
const update = await getCartUpdates();
|
|
|
|
|
|
|
|
|
|
|
|
if (cancelled) return;
|
|
|
|
|
|
|
2026-08-28 06:41:34 +00:00
|
|
|
|
if (update.status === 'waiting') {
|
|
|
|
|
|
if (!missing_timeout) {
|
|
|
|
|
|
missing_timeout = setTimeout(() => {
|
|
|
|
|
|
setDisplayText('Ищем Брока...')
|
|
|
|
|
|
timeoutFunction();
|
|
|
|
|
|
}, 10_000);
|
|
|
|
|
|
}
|
|
|
|
|
|
setDisplayText('Брок бегает по супермаркету...')
|
|
|
|
|
|
timeoutRef.current = window.setTimeout(tick, 2000);
|
|
|
|
|
|
return;
|
|
|
|
|
|
} else if (missing_timeout) {
|
|
|
|
|
|
clearTimeout(missing_timeout);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-24 10:07:16 +00:00
|
|
|
|
if (update.status === 'complete') {
|
|
|
|
|
|
onLoadingChange(false);
|
|
|
|
|
|
onComplete();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (update.status === 'clear') {
|
|
|
|
|
|
const nextProducts = update.payload?.chunk?.map(product => normalizeProductImage(product)) ?? [];
|
2026-08-28 06:41:34 +00:00
|
|
|
|
timeoutRef.current = window.setTimeout(tick, 3000);
|
2026-08-24 10:07:16 +00:00
|
|
|
|
onProductsChange(nextProducts);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (update.status === 'data') {
|
|
|
|
|
|
const chunk = update.payload?.chunk?.map(product => normalizeProductImage(product)) ?? [];
|
|
|
|
|
|
|
|
|
|
|
|
onProductsChange(current => {
|
|
|
|
|
|
const next = [...current];
|
|
|
|
|
|
|
|
|
|
|
|
chunk.forEach(product => {
|
|
|
|
|
|
const index = next.findIndex(item => item.name === product.name);
|
|
|
|
|
|
|
|
|
|
|
|
if (index === -1) {
|
|
|
|
|
|
next.push(product);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
next[index] = {
|
|
|
|
|
|
...next[index],
|
|
|
|
|
|
n: next[index].n + product.n,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return next;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
setDisplayText('Брок закинул пару товаров...')
|
|
|
|
|
|
}
|
2026-08-28 06:41:34 +00:00
|
|
|
|
timeoutRef.current = window.setTimeout(tick, 5000);
|
2026-08-24 10:07:16 +00:00
|
|
|
|
} catch (error) {
|
2026-08-28 06:41:34 +00:00
|
|
|
|
setDisplayText('Нет связи с супермаркетом...')
|
2026-08-24 10:07:16 +00:00
|
|
|
|
timeoutRef.current = window.setTimeout(tick, 3000);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
tick();
|
|
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
cancelled = true;
|
|
|
|
|
|
|
|
|
|
|
|
if (timeoutRef.current) {
|
|
|
|
|
|
window.clearTimeout(timeoutRef.current);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
}, [enabled, onComplete, onLoadingChange, onProductsChange]);
|
|
|
|
|
|
}
|