Реализация Social Proof элементов (уведомления о покупках, счётчики) на сайте

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.
Разработка и обслуживание любых видов сайтов:
Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Реализация Social Proof элементов (уведомления о покупках, счётчики) на сайте
Средняя
~2-3 рабочих дня
Часто задаваемые вопросы
Наши компетенции:
Этапы разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1214
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    852
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    823
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    815

Реализация Social Proof элементов (уведомления о покупках, счётчики) на сайте

Social proof работает на простом психологическом механизме: люди ориентируются на действия других при принятии решений. Технически это набор компонентов — всплывающие уведомления о покупках, счётчики онлайн-пользователей, бейджи «X купили сегодня». Главное — не переборщить: агрессивный social proof превращается в тёмный паттерн и роняет доверие.

Всплывающие уведомления о покупках (Purchase Notifications)

// purchase-notifications.ts
interface PurchaseEvent {
  customerName: string;   // "Алексей из Москвы"
  productName: string;
  productUrl?: string;
  timeAgo: string;        // "2 минуты назад"
  avatarUrl?: string;
}

interface NotificationConfig {
  position?: 'bottom-left' | 'bottom-right';
  displayMs?: number;     // сколько висит уведомление
  intervalMs?: number;    // пауза между уведомлениями
  maxQueue?: number;      // макс. в очереди
}

export class PurchaseNotifier {
  private queue: PurchaseEvent[] = [];
  private isShowing = false;
  private container: HTMLElement;
  private config: Required<NotificationConfig>;

  constructor(config: NotificationConfig = {}) {
    this.config = {
      position: 'bottom-left',
      displayMs: 5000,
      intervalMs: 8000,
      maxQueue: 5,
      ...config,
    };

    this.container = this.createContainer();
    document.body.appendChild(this.container);
  }

  private createContainer(): HTMLElement {
    const el = document.createElement('div');
    const pos = this.config.position;
    el.style.cssText = `
      position: fixed;
      ${pos === 'bottom-left' ? 'left: 20px' : 'right: 20px'};
      bottom: 20px;
      z-index: 9998;
      pointer-events: none;
    `;
    return el;
  }

  push(events: PurchaseEvent[]) {
    const toAdd = events.slice(0, this.config.maxQueue - this.queue.length);
    this.queue.push(...toAdd);
    if (!this.isShowing) this.showNext();
  }

  private async showNext() {
    if (this.queue.length === 0) { this.isShowing = false; return; }
    this.isShowing = true;

    const event = this.queue.shift()!;
    const toast = this.createToast(event);
    this.container.appendChild(toast);

    // анимация появления
    requestAnimationFrame(() => {
      toast.style.opacity = '1';
      toast.style.transform = 'translateY(0)';
    });

    await new Promise(r => setTimeout(r, this.config.displayMs));

    // анимация исчезновения
    toast.style.opacity = '0';
    toast.style.transform = 'translateY(10px)';
    await new Promise(r => setTimeout(r, 300));
    toast.remove();

    await new Promise(r => setTimeout(r, this.config.intervalMs));
    this.showNext();
  }

  private createToast(event: PurchaseEvent): HTMLElement {
    const toast = document.createElement('div');
    toast.style.cssText = `
      display: flex;
      align-items: center;
      gap: 12px;
      background: #fff;
      border: 1px solid #e5e7eb;
      border-radius: 12px;
      padding: 12px 16px;
      box-shadow: 0 4px 16px rgba(0,0,0,.1);
      max-width: 300px;
      pointer-events: auto;
      cursor: default;
      opacity: 0;
      transform: translateY(16px);
      transition: opacity .3s, transform .3s;
      font-family: system-ui, sans-serif;
      font-size: 13px;
    `;

    const avatar = event.avatarUrl
      ? `<img src="${event.avatarUrl}" alt="" width="36" height="36" style="border-radius:50%;flex-shrink:0">`
      : `<div style="width:36px;height:36px;border-radius:50%;background:#dbeafe;display:flex;align-items:center;justify-content:center;font-size:16px;flex-shrink:0">🛒</div>`;

    const productLink = event.productUrl
      ? `<a href="${event.productUrl}" style="color:#1d4ed8;text-decoration:none;font-weight:500">${event.productName}</a>`
      : `<strong>${event.productName}</strong>`;

    toast.innerHTML = `
      ${avatar}
      <div>
        <div style="color:#111827">
          <strong>${event.customerName}</strong> купил(а) ${productLink}
        </div>
        <div style="color:#9ca3af;margin-top:2px">${event.timeAgo}</div>
      </div>
      <button style="margin-left:auto;background:none;border:none;cursor:pointer;color:#9ca3af;font-size:16px;padding:0;line-height:1" aria-label="Закрыть">×</button>
    `;

    toast.querySelector('button')?.addEventListener('click', () => {
      toast.remove();
    });

    return toast;
  }

  destroy() {
    this.container.remove();
  }
}

Загрузка реальных данных из API

// Используем реальные данные о покупках
async function loadRecentPurchases(productId?: string): Promise<PurchaseEvent[]> {
  const url = new URL('/api/social-proof/purchases', location.origin);
  if (productId) url.searchParams.set('product_id', productId);
  url.searchParams.set('limit', '10');

  const res = await fetch(url.toString());
  const data: Array<{
    buyer_city: string;
    product_name: string;
    product_url: string;
    purchased_at: string;
  }> = await res.json();

  const rtf = new Intl.RelativeTimeFormat('ru', { numeric: 'auto' });

  return data.map(item => {
    const secondsAgo = (Date.now() - new Date(item.purchased_at).getTime()) / 1000;
    let timeAgo: string;

    if (secondsAgo < 3600) {
      timeAgo = rtf.format(-Math.floor(secondsAgo / 60), 'minute');
    } else if (secondsAgo < 86400) {
      timeAgo = rtf.format(-Math.floor(secondsAgo / 3600), 'hour');
    } else {
      timeAgo = rtf.format(-Math.floor(secondsAgo / 86400), 'day');
    }

    return {
      customerName: item.buyer_city,
      productName: item.product_name,
      productUrl: item.product_url,
      timeAgo,
    };
  });
}

// Инициализация
const notifier = new PurchaseNotifier({ position: 'bottom-left', displayMs: 6000 });
loadRecentPurchases().then(events => notifier.push(events));

Счётчик онлайн-пользователей

Реальный счётчик — через WebSocket или Server-Sent Events. Для простых случаев достаточно SSE:

// online-counter.ts
export function initOnlineCounter(selector: string) {
  const el = document.querySelector(selector);
  if (!el) return;

  const sse = new EventSource('/api/online-count');

  sse.addEventListener('count', (e: MessageEvent) => {
    const count = parseInt(e.data, 10);
    el.textContent = formatCount(count);

    // пульсация при изменении
    el.classList.remove('pulse');
    void (el as HTMLElement).offsetWidth; // reflow trick
    el.classList.add('pulse');
  });

  sse.addEventListener('error', () => {
    setTimeout(() => initOnlineCounter(selector), 5000);
  });

  return () => sse.close();
}

function formatCount(n: number): string {
  if (n >= 1000) return `${(n / 1000).toFixed(1)}k`;
  return String(n);
}
// Серверная часть (Node.js/Express)
import { Router } from 'express';

const onlineCountRouter = Router();
const clients = new Set<NodeJS.WritableStream>();

// Обновляем счётчик из Redis или in-memory
setInterval(async () => {
  const count = await redis.get('online_users_count') ?? '0';
  const message = `event: count\ndata: ${count}\n\n`;
  clients.forEach(client => {
    try { client.write(message); }
    catch { clients.delete(client); }
  });
}, 5000);

onlineCountRouter.get('/', (req, res) => {
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');
  res.flushHeaders();

  clients.add(res);
  req.on('close', () => clients.delete(res));
});

Бейджи «X купили за 24 часа»

Статичные или полустатичные элементы, которые можно кешировать:

// SalesBadge.tsx
interface SalesBadgeProps {
  count: number;
  period?: '24h' | '7d' | '30d';
  threshold?: number;   // не показывать, если меньше
}

const PERIOD_LABELS = {
  '24h': 'за сутки',
  '7d':  'за неделю',
  '30d': 'за месяц',
};

export function SalesBadge({ count, period = '24h', threshold = 5 }: SalesBadgeProps) {
  if (count < threshold) return null;

  return (
    <div className="inline-flex items-center gap-1.5 bg-orange-50 border border-orange-200 text-orange-700 text-xs font-medium px-2.5 py-1 rounded-full">
      <span className="w-1.5 h-1.5 bg-orange-400 rounded-full animate-pulse" />
      {count} {pluralize(count, ['продажа', 'продажи', 'продаж'])} {PERIOD_LABELS[period]}
    </div>
  );
}

function pluralize(n: number, forms: [string, string, string]): string {
  const abs = Math.abs(n) % 100;
  const mod = abs % 10;
  if (abs > 10 && abs < 20) return forms[2];
  if (mod === 1) return forms[0];
  if (mod >= 2 && mod <= 4) return forms[1];
  return forms[2];
}

Что важно с точки зрения UX

Все данные должны быть реальными или хотя бы основанными на реальных агрегатах. Полностью фиктивные уведомления — нарушение доверия и в ряде юрисдикций требований к рекламе. Если реальных покупок нет — не показывайте уведомления вообще.

Частота не должна раздражать: одно уведомление каждые 8–15 секунд — верхняя граница. Пользователь пришёл читать контент, а не смотреть на мигающие баннеры.

Обязательна кнопка закрытия и возможность отключить через cookie/localStorage.

Сроки

Компонент уведомлений о покупках с реальным API — два дня. SSE-счётчик онлайн с Redis — ещё день. Бейджи продаж — полдня. Итого три-четыре дня для всего набора.