Реалізація Urgency/Scarcity елементів (таймер, обмежена кількість) на сайті

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Реалізація Urgency/Scarcity елементів (таймер, обмежена кількість) на сайті
Середня
від 1 робочого дня до 3 робочих днів
Часті питання

Наші компетенції:

Етапи розробки
Останні роботи
  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Реалізація елементів Urgency/Scarcity (таймер, обмежена кількість) на сайті

Urgency (терміновість) та scarcity (дефіцит) — класичні принципи Чалдіні, що працюють в e-commerce уже 20 років. Проблема: більшість реалізацій або технічно ненадійні (таймер скидається при оновленні сторінки), або виглядають як очевидна маніпуляція (лічильник «залишилось 3 штуки», який ніколи не змінюється). Нижче — реалізація, яка працює чесно та технічно правильно.

Таймер зворотного відліку

Вимога до надійності: таймер не повинен скидатися при оновленні сторінки. Неможливо робити це через new Date() + N хвилин при кожному монтуванні компонента.

Правильна схема:

  1. При першому посещенні сторінки акції створюємо запис у Redis з TTL
  2. При подальших посещеннях беремо залишковий час з Redis
  3. Для неавторизованих — ключ за sessionId з cookie
// Отримання або створення таймера сеансу
public function getCountdown(Request $request, string $promoCode): array
{
    $sessionId = $request->cookie('session_id') ?? Str::uuid()->toString();
    $key = "countdown:{$promoCode}:{$sessionId}";

    $ttl = Redis::ttl($key);

    if ($ttl <= 0) {
        $duration = 1800; // 30 хвилин
        Redis::setex($key, $duration, now()->addSeconds($duration)->timestamp);
        $ttl = $duration;
    }

    return [
        'ends_at'     => now()->addSeconds($ttl)->toIso8601String(),
        'session_id'  => $sessionId,
    ];
}

Компонент таймера (React):

const CountdownTimer: React.FC<{ endsAt: string }> = ({ endsAt }) => {
  const [timeLeft, setTimeLeft] = useState(0);

  useEffect(() => {
    const target = new Date(endsAt).getTime();

    const tick = () => {
      const diff = Math.max(0, target - Date.now());
      setTimeLeft(diff);
    };

    tick();
    const interval = setInterval(tick, 1000);
    return () => clearInterval(interval);
  }, [endsAt]);

  const hours   = Math.floor(timeLeft / 3_600_000);
  const minutes = Math.floor((timeLeft % 3_600_000) / 60_000);
  const seconds = Math.floor((timeLeft % 60_000) / 1000);

  if (timeLeft === 0) return <ExpiredBanner />;

  return (
    <div className="countdown" role="timer" aria-live="polite">
      <Digit value={hours}   label="г" />
      <Digit value={minutes} label="х" />
      <Digit value={seconds} label="с" />
    </div>
  );
};

Компонент Digit додає flip-анімацію при змінені значення через CSS @keyframes.

Індикатор запасів

Показувати реальні запаси зі складської системи — найкраща практика. Якщо кількість ≤ N, показуємо попередження:

const StockIndicator: React.FC<{ stock: number }> = ({ stock }) => {
  if (stock > 10) return null;

  return (
    <div className={`stock-indicator stock-indicator--${stock <= 3 ? 'critical' : 'low'}`}>
      {stock <= 3
        ? `Останні ${stock} шт.`
        : `Залишилось ${stock} шт. — закінчується`}
    </div>
  );
};

Синхронізація з реальним складом — якщо використовуємо 1С або WMS, витягаємо через API та кешуємо у Redis на 5 хвилин:

public function getStockLevel(int $productId): int
{
    return Cache::remember("stock:{$productId}", 300, function () use ($productId) {
        return $this->warehouseApi->getAvailableQuantity($productId);
    });
}

Важливо: не показувати точний залишок для дуже популярних товарів — це створює ефект стадного поведінки та дещо посилює конверсію.

«X людей переглядають прямо зараз»

Лічильник активних користувачів на сторінці товару — реальний або приблизний.

Реальна реалізація через Redis:

// При завантаженні сторінки товару
public function trackView(int $productId, string $sessionId): int
{
    $key = "viewers:{$productId}";
    Redis::zadd($key, time(), $sessionId);
    Redis::zremrangebyscore($key, 0, time() - 300); // видаляємо старше за 5 хв
    Redis::expire($key, 600);

    return Redis::zcard($key);
}

Для оновлення в реальному часі — або polling кожні 30 секунд, або Server-Sent Events:

// SSE endpoint
public function viewersStream(int $productId): StreamedResponse
{
    return response()->stream(function () use ($productId) {
        while (true) {
            $count = $this->viewerService->getCount($productId);
            echo "data: {\"viewers\": {$count}}\n\n";
            ob_flush();
            flush();
            sleep(30);
        }
    }, 200, [
        'Content-Type'  => 'text/event-stream',
        'Cache-Control' => 'no-cache',
    ]);
}
// На клієнті
useEffect(() => {
  const es = new EventSource(`/api/products/${productId}/viewers`);
  es.onmessage = (e) => setViewers(JSON.parse(e.data).viewers);
  return () => es.close();
}, [productId]);

Flash sale

Flash sale вимагає атомарної роботи з запасами — без конкурентних записів:

// Lua-скрипт у Redis для атомарного зменшення
$script = <<<'LUA'
local key = KEYS[1]
local current = tonumber(redis.call('GET', key) or '0')
if current <= 0 then
    return -1
end
return redis.call('DECR', key)
LUA;

$remaining = Redis::eval($script, 1, "flash_sale:{$saleId}:stock");

if ($remaining < 0) {
    return response()->json(['error' => 'Товар закінчився'], 422);
}

«Останній в кошику»

Якщо товар додан в кошики інших користувачів і реальний запас совпадає або менший за кількість зарезервованих одиниць:

{isLastInCart && (
  <Alert variant="warning">
    Цей товар є в кошиках інших покупців. Оформіть замовлення, щоб зарезервувати його.
  </Alert>
)}

Технічна реалізація: таблиця cart_reservations(product_id, quantity, session_id, expires_at). Резервування знімається через 30 хвилин або при оформленні замовлення.

Етика та антипаттерни

Елементи urgency працюють, коли вони чесні. Паттерни, які шкодять репутації:

  • Таймер, який скидається при кожному заході на сторінку
  • «Залишилось 2 штуки» на товарі з постійним наявністю
  • Лічильник переглядачів, який випадково генерується на клієнті

Ці прийоми короткотермінно підвищують конверсію, але довготермінно руйнують довіру — користувачі помічають розбіжності.

Терміни

Завдання Час
Countdown timer (Redis + компонент) 1 день
Індикатор запасів (реальні дані) 0,5 дня
Лічильник переглядачів (SSE) 1 день
Flash sale з Redis-резервуванням 1–2 дні

Базовий набір (таймер + запаси): 1,5–2 дні.