Реализация виджета обратной связи (Feedback Widget) на сайте

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

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

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Реализация виджета обратной связи (Feedback Widget) на сайте
Простая
от 1 рабочего дня до 3 рабочих дней
Часто задаваемые вопросы
Наши компетенции:
Этапы разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1227
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1163
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    859
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1074
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    829
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    833

Реализация виджета обратной связи (Feedback Widget) на сайте

Feedback Widget — плавающая кнопка или встроенная панель, через которую пользователь отправляет короткое сообщение, оценку или скриншот без перехода на отдельную страницу. Основная ценность — минимальное трение: пользователь не уходит из контекста.

Минимальная реализация

Виджет состоит из трёх частей: кнопка-триггер, форма, отправка на API.

// feedback-widget.ts
interface FeedbackPayload {
  type: 'bug' | 'idea' | 'other';
  message: string;
  url: string;
  userAgent: string;
  timestamp: string;
}

class FeedbackWidget {
  private container: HTMLElement;
  private isOpen = false;
  private endpoint: string;

  constructor(endpoint: string) {
    this.endpoint = endpoint;
    this.container = this.createWidget();
    document.body.appendChild(this.container);
  }

  private createWidget(): HTMLElement {
    const wrapper = document.createElement('div');
    wrapper.innerHTML = `
      <style>
        .fw-btn {
          position: fixed;
          bottom: 24px;
          right: 24px;
          background: #3b82f6;
          color: #fff;
          border: none;
          border-radius: 24px;
          padding: 10px 18px;
          font-size: 14px;
          cursor: pointer;
          box-shadow: 0 4px 12px rgba(59,130,246,.4);
          z-index: 9999;
          transition: transform .15s;
        }
        .fw-btn:hover { transform: translateY(-2px); }
        .fw-panel {
          position: fixed;
          bottom: 72px;
          right: 24px;
          width: 320px;
          background: #fff;
          border: 1px solid #e5e7eb;
          border-radius: 12px;
          padding: 20px;
          box-shadow: 0 8px 30px rgba(0,0,0,.12);
          z-index: 9999;
          display: none;
          font-family: system-ui, sans-serif;
        }
        .fw-panel.open { display: block; }
        .fw-title { font-weight: 600; margin: 0 0 12px; font-size: 15px; }
        .fw-types { display: flex; gap: 8px; margin-bottom: 12px; }
        .fw-type {
          flex: 1; padding: 6px; border: 1px solid #e5e7eb;
          border-radius: 6px; background: none; cursor: pointer;
          font-size: 12px; transition: border-color .1s, background .1s;
        }
        .fw-type.active {
          border-color: #3b82f6; background: #eff6ff; color: #1d4ed8;
        }
        .fw-textarea {
          width: 100%; box-sizing: border-box;
          border: 1px solid #e5e7eb; border-radius: 6px;
          padding: 8px; font-size: 13px; resize: vertical;
          min-height: 80px; font-family: inherit;
        }
        .fw-textarea:focus { outline: 2px solid #3b82f6; border-color: transparent; }
        .fw-submit {
          margin-top: 10px; width: 100%; padding: 9px;
          background: #3b82f6; color: #fff; border: none;
          border-radius: 6px; cursor: pointer; font-size: 14px;
        }
        .fw-submit:disabled { opacity: .6; cursor: not-allowed; }
        .fw-success { text-align: center; padding: 20px; color: #16a34a; font-size: 14px; }
      </style>
      <button class="fw-btn" aria-label="Оставить отзыв">💬 Отзыв</button>
      <div class="fw-panel" role="dialog" aria-label="Форма обратной связи">
        <p class="fw-title">Что хотите сообщить?</p>
        <div class="fw-types">
          <button class="fw-type active" data-type="bug">🐛 Баг</button>
          <button class="fw-type" data-type="idea">💡 Идея</button>
          <button class="fw-type" data-type="other">💬 Другое</button>
        </div>
        <textarea class="fw-textarea" placeholder="Опишите, что произошло..." rows="3"></textarea>
        <button class="fw-submit">Отправить</button>
      </div>
    `;

    const btn = wrapper.querySelector<HTMLButtonElement>('.fw-btn')!;
    const panel = wrapper.querySelector<HTMLElement>('.fw-panel')!;
    const textarea = wrapper.querySelector<HTMLTextAreaElement>('.fw-textarea')!;
    const submit = wrapper.querySelector<HTMLButtonElement>('.fw-submit')!;
    let selectedType: FeedbackPayload['type'] = 'bug';

    btn.addEventListener('click', () => {
      this.isOpen = !this.isOpen;
      panel.classList.toggle('open', this.isOpen);
      if (this.isOpen) textarea.focus();
    });

    wrapper.querySelectorAll<HTMLButtonElement>('.fw-type').forEach(typeBtn => {
      typeBtn.addEventListener('click', () => {
        wrapper.querySelectorAll('.fw-type').forEach(b => b.classList.remove('active'));
        typeBtn.classList.add('active');
        selectedType = typeBtn.dataset.type as FeedbackPayload['type'];
      });
    });

    submit.addEventListener('click', async () => {
      const message = textarea.value.trim();
      if (!message) { textarea.focus(); return; }

      submit.disabled = true;
      submit.textContent = 'Отправляю...';

      try {
        await this.send({ type: selectedType, message });
        panel.innerHTML = '<div class="fw-success">✅ Спасибо за отзыв!</div>';
        setTimeout(() => {
          panel.classList.remove('open');
          this.isOpen = false;
        }, 2000);
      } catch {
        submit.disabled = false;
        submit.textContent = 'Ошибка — попробовать снова';
      }
    });

    // закрытие по Escape
    document.addEventListener('keydown', e => {
      if (e.key === 'Escape' && this.isOpen) {
        this.isOpen = false;
        panel.classList.remove('open');
      }
    });

    return wrapper;
  }

  private async send(data: Pick<FeedbackPayload, 'type' | 'message'>) {
    const payload: FeedbackPayload = {
      ...data,
      url: window.location.href,
      userAgent: navigator.userAgent,
      timestamp: new Date().toISOString(),
    };

    const res = await fetch(this.endpoint, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload),
    });

    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return res.json();
  }
}

// инициализация
new FeedbackWidget('/api/feedback');

Серверная часть (Node.js/Express)

// routes/feedback.ts
import { Router } from 'express';

export const feedbackRouter = Router();

feedbackRouter.post('/', async (req, res) => {
  const { type, message, url, userAgent, timestamp } = req.body;

  if (!message || message.length > 2000) {
    return res.status(400).json({ error: 'Invalid message' });
  }

  // сохраняем в БД
  await db.feedback.create({
    data: { type, message, url, userAgent, timestamp },
  });

  // уведомление в Slack (опционально)
  if (process.env.SLACK_WEBHOOK) {
    await fetch(process.env.SLACK_WEBHOOK, {
      method: 'POST',
      body: JSON.stringify({
        text: `*Новый отзыв [${type}]*\n${message}\nСтраница: ${url}`,
      }),
    });
  }

  res.json({ ok: true });
});

Сроки

Базовый виджет без скриншотов — один-два дня разработки, включая серверный endpoint и Slack-уведомления. Добавление скриншота через html2canvas или нативный Screen Capture API увеличивает срок до трёх-четырёх дней.