Реалізація віджету зворотного зв'язку (Feedback Widget) на сайті

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

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

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

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

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Реалізація віджету зворотного зв'язку (Feedback Widget) на сайті
Проста
від 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

Реалізація віджету зворотного зв'язку на веб-сайті

Віджет зворотного зв'язку — це плаваюча кнопка або вбудована панель, через яку користувачі відправляють короткі повідомлення, оцінки або знімки екрану без навігації на окрему сторінку. Головна цінність — мінімальні перешкоди: користувачі не залишають контекст.

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

Віджет складається з трьох частин: кнопка-тригер, форма, відправлення 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 збільшує часові рамки до трьох-чотирьох днів.