Hotjar Heatmaps und Analytics

Hotjar-Beispiele für Heatmaps, Session-Aufzeichnungen, Conversion-Trichter, Feedback-Widgets und Benutzer-Verhaltensanalyse

💻 Hotjar Hello World javascript

🟢 simple

Grundlegende Hotjar-Einrichtung mit aktivierten Heatmaps und Session-Aufzeichnungen

⏱️ 5 min 🏷️ frontend, analytics, user-experience, heatmaps
Prerequisites: JavaScript basics, HTML/CSS
// Hotjar Hello World - Basic Heatmaps and Session Recordings
// Simple setup for user behavior tracking and analysis

(function(h,o,t,j,a,r){
    h.hj=h.hj||function(){(h.hj.q=h.hj.q||[]).push(arguments)};
    h._hjSettings={hjid:1234567,hjsv:6};
    a=o.getElementsByTagName('head')[0];
    r=o.createElement('script');r.async=1;
    r.src=t+h._hjSettings.hjid+j+h._hjSettings.hjsv;
    a.appendChild(r);
})(window,document,'https://static.hotjar.com/c/hotjar-','.js?sv=');

// Initialize Hotjar with your site ID
window.hj('trigger', 'identification_changed', {
  user_id: 'user_123',
  email: '[email protected]',
  name: 'John Doe'
});

// Track custom events
window.hj('event', 'user_signed_up');

// Track page changes (for single page applications)
window.hj('stateChange', '/new-page');

console.log('Hotjar initialized and ready for tracking!');

// Track form submissions
document.addEventListener('submit', function(event) {
  const form = event.target;
  if (form.tagName === 'FORM') {
    window.hj('event', 'form_submitted', {
      form_id: form.id,
      form_name: form.name
    });
  }
});

// Track clicks on important elements
document.addEventListener('click', function(event) {
  const element = event.target;

  // Track button clicks
  if (element.matches('button, .btn, input[type="button"], input[type="submit"]')) {
    window.hj('event', 'button_clicked', {
      text: element.textContent || element.value,
      id: element.id,
      class: element.className
    });
  }

  // Track link clicks
  if (element.matches('a') && element.href) {
    window.hj('event', 'link_clicked', {
      href: element.href,
      text: element.textContent
    });
  }
});

// Track scroll depth
let maxScroll = 0;
window.addEventListener('scroll', function() {
  const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
  const winHeight = window.innerHeight;
  const docHeight = document.documentElement.scrollHeight;

  const scrollPercentage = Math.round((scrollTop + winHeight) / docHeight * 100);

  if (scrollPercentage > maxScroll) {
    maxScroll = scrollPercentage;

    // Track important scroll milestones
    if (scrollPercentage === 25) {
      window.hj('event', 'scrolled_25_percent');
    } else if (scrollPercentage === 50) {
      window.hj('event', 'scrolled_50_percent');
    } else if (scrollPercentage === 75) {
      window.hj('event', 'scrolled_75_percent');
    } else if (scrollPercentage === 90) {
      window.hj('event', 'scrolled_90_percent');
    }
  }
});

💻 Benutzerdefinierte Feedback-Widgets typescript

🟡 intermediate ⭐⭐⭐⭐

Benutzerdefinierte Feedback-Widgets mit Hotjar-Integration für Benutzer-Feedback-Sammlung implementieren

⏱️ 40 min 🏷️ frontend, feedback, user-experience, widgets
Prerequisites: TypeScript, DOM manipulation, Event handling, CSS styling
// Hotjar Custom Feedback Widgets
// Custom feedback implementation with Hotjar integration

interface FeedbackWidgetConfig {
  position: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left'
  trigger: 'click' | 'hover' | 'scroll' | 'exit-intent'
  theme: 'light' | 'dark'
  questions: FeedbackQuestion[]
  showOnPages?: string[]
  delay?: number
}

interface FeedbackQuestion {
  id: string
  type: 'rating' | 'text' | 'nps' | 'multiple-choice'
  question: string
  required: boolean
  options?: string[]
  placeholder?: string
}

interface FeedbackResponse {
  userId?: string
  sessionId?: string
  page: string
  responses: Record<string, any>
  timestamp: number
  userAgent: string
  viewport: {
    width: number
    height: number
  }
}

class HotjarFeedbackWidget {
  private config: FeedbackWidgetConfig
  private widgetElement: HTMLElement | null = null
  private isOpen: boolean = false
  private currentStep: number = 0
  private responses: Record<string, any> = {}

  constructor(config: FeedbackWidgetConfig) {
    this.config = config
    this.init()
  }

  private init() {
    this.createWidget()
    this.setupEventListeners()
    this.setupTriggerConditions()
  }

  private createWidget() {
    // Create main widget container
    this.widgetElement = document.createElement('div')
    this.widgetElement.className = `hj-feedback-widget hj-theme-${this.config.theme}`
    this.widgetElement.innerHTML = `
      <div class="hj-widget-trigger">
        <button class="hj-trigger-button">
          <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
            <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z" fill="currentColor"/>
          </svg>
          <span>Feedback</span>
        </button>
      </div>
      <div class="hj-widget-container" style="display: none;">
        <div class="hj-widget-header">
          <h3>Share your feedback</h3>
          <button class="hj-close-button">&times;</button>
        </div>
        <div class="hj-widget-content">
          <!-- Dynamic content will be inserted here -->
        </div>
        <div class="hj-widget-footer">
          <button class="hj-button hj-button-secondary" id="hj-prev-button" style="display: none;">Previous</button>
          <button class="hj-button hj-button-primary" id="hj-next-button">Next</button>
          <button class="hj-button hj-button-primary" id="hj-submit-button" style="display: none;">Submit</button>
        </div>
      </div>
    `

    // Add styles
    this.addStyles()

    // Add to page
    document.body.appendChild(this.widgetElement)
  }

  private addStyles() {
    const styles = `
      .hj-feedback-widget {
        position: fixed;
        z-index: 999999;
        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
      }

      .hj-feedback-widget.hj-position-bottom-right {
        bottom: 20px;
        right: 20px;
      }

      .hj-feedback-widget.hj-position-bottom-left {
        bottom: 20px;
        left: 20px;
      }

      .hj-feedback-widget.hj-position-top-right {
        top: 20px;
        right: 20px;
      }

      .hj-feedback-widget.hj-position-top-left {
        top: 20px;
        left: 20px;
      }

      .hj-widget-trigger {
        margin-bottom: 10px;
      }

      .hj-trigger-button {
        display: flex;
        align-items: center;
        gap: 8px;
        padding: 12px 16px;
        background: #1f77b4;
        color: white;
        border: none;
        border-radius: 8px;
        cursor: pointer;
        font-size: 14px;
        font-weight: 500;
        box-shadow: 0 2px 8px rgba(0,0,0,0.15);
        transition: all 0.2s;
      }

      .hj-trigger-button:hover {
        background: #165a8a;
        transform: translateY(-1px);
        box-shadow: 0 4px 12px rgba(0,0,0,0.2);
      }

      .hj-widget-container {
        width: 380px;
        max-width: 90vw;
        background: white;
        border-radius: 12px;
        box-shadow: 0 8px 32px rgba(0,0,0,0.1);
        overflow: hidden;
      }

      .hj-feedback-widget.hj-theme-dark .hj-widget-container {
        background: #2c3e50;
        color: white;
      }

      .hj-widget-header {
        padding: 20px;
        border-bottom: 1px solid #e1e4e8;
        display: flex;
        justify-content: space-between;
        align-items: center;
      }

      .hj-feedback-widget.hj-theme-dark .hj-widget-header {
        border-bottom-color: #34495e;
      }

      .hj-widget-header h3 {
        margin: 0;
        font-size: 18px;
        font-weight: 600;
      }

      .hj-close-button {
        background: none;
        border: none;
        font-size: 24px;
        cursor: pointer;
        color: #6c757d;
        padding: 0;
        width: 32px;
        height: 32px;
        display: flex;
        align-items: center;
        justify-content: center;
        border-radius: 4px;
      }

      .hj-close-button:hover {
        background: rgba(0,0,0,0.1);
      }

      .hj-widget-content {
        padding: 20px;
        min-height: 200px;
      }

      .hj-question-container {
        margin-bottom: 20px;
      }

      .hj-question-label {
        display: block;
        margin-bottom: 8px;
        font-weight: 500;
        font-size: 16px;
      }

      .hj-rating-stars {
        display: flex;
        gap: 8px;
        margin-top: 8px;
      }

      .hj-star {
        font-size: 24px;
        color: #ddd;
        cursor: pointer;
        transition: color 0.2s;
      }

      .hj-star:hover,
      .hj-star.active {
        color: #ffc107;
      }

      .hj-text-input,
      .hj-textarea {
        width: 100%;
        padding: 10px;
        border: 1px solid #ddd;
        border-radius: 4px;
        font-size: 14px;
        font-family: inherit;
        margin-top: 8px;
      }

      .hj-textarea {
        min-height: 100px;
        resize: vertical;
      }

      .hj-nps-scale {
        display: flex;
        gap: 4px;
        margin-top: 8px;
      }

      .hj-nps-number {
        width: 32px;
        height: 32px;
        border: 1px solid #ddd;
        border-radius: 4px;
        display: flex;
        align-items: center;
        justify-content: center;
        cursor: pointer;
        font-size: 14px;
        transition: all 0.2s;
      }

      .hj-nps-number:hover {
        background: #f8f9fa;
      }

      .hj-nps-number.selected {
        background: #1f77b4;
        color: white;
        border-color: #1f77b4;
      }

      .hj-multiple-choice {
        display: flex;
        flex-direction: column;
        gap: 8px;
        margin-top: 8px;
      }

      .hj-choice-option {
        display: flex;
        align-items: center;
        gap: 8px;
        cursor: pointer;
      }

      .hj-choice-option input[type="radio"] {
        margin: 0;
      }

      .hj-widget-footer {
        padding: 20px;
        border-top: 1px solid #e1e4e8;
        display: flex;
        justify-content: flex-end;
        gap: 10px;
      }

      .hj-feedback-widget.hj-theme-dark .hj-widget-footer {
        border-top-color: #34495e;
      }

      .hj-button {
        padding: 10px 20px;
        border: none;
        border-radius: 6px;
        font-size: 14px;
        font-weight: 500;
        cursor: pointer;
        transition: all 0.2s;
      }

      .hj-button-primary {
        background: #1f77b4;
        color: white;
      }

      .hj-button-primary:hover {
        background: #165a8a;
      }

      .hj-button-secondary {
        background: #6c757d;
        color: white;
      }

      .hj-button-secondary:hover {
        background: #545b62;
      }

      .hj-button:disabled {
        opacity: 0.6;
        cursor: not-allowed;
      }

      .hj-progress-indicator {
        display: flex;
        justify-content: center;
        gap: 6px;
        margin-bottom: 20px;
      }

      .hj-progress-dot {
        width: 8px;
        height: 8px;
        border-radius: 50%;
        background: #ddd;
        transition: background 0.2s;
      }

      .hj-progress-dot.active {
        background: #1f77b4;
      }
    `

    const styleSheet = document.createElement('style')
    styleSheet.textContent = styles
    document.head.appendChild(styleSheet)

    // Add position class
    this.widgetElement!.classList.add(`hj-position-${this.config.position}`)
  }

  private setupEventListeners() {
    if (!this.widgetElement) return

    // Trigger button click
    const triggerButton = this.widgetElement.querySelector('.hj-trigger-button')
    triggerButton?.addEventListener('click', () => {
      this.toggleWidget()
    })

    // Close button click
    const closeButton = this.widgetElement.querySelector('.hj-close-button')
    closeButton?.addEventListener('click', () => {
      this.closeWidget()
    })

    // Navigation buttons
    const prevButton = this.widgetElement.querySelector('#hj-prev-button')
    const nextButton = this.widgetElement.querySelector('#hj-next-button')
    const submitButton = this.widgetElement.querySelector('#hj-submit-button')

    prevButton?.addEventListener('click', () => {
      this.previousStep()
    })

    nextButton?.addEventListener('click', () => {
      this.nextStep()
    })

    submitButton?.addEventListener('click', () => {
      this.submitFeedback()
    })
  }

  private setupTriggerConditions() {
    // Check if current page should show widget
    if (this.config.showOnPages && this.config.showOnPages.length > 0) {
      const currentPage = window.location.pathname
      const shouldShow = this.config.showOnPages.some(page =>
        currentPage === page || currentPage.startsWith(page)
      )

      if (!shouldShow && this.widgetElement) {
        this.widgetElement.style.display = 'none'
        return
      }
    }

    // Apply delay if specified
    if (this.config.delay && this.config.delay > 0) {
      setTimeout(() => {
        this.showTrigger()
      }, this.config.delay)
    } else {
      this.showTrigger()
    }

    // Setup specific trigger types
    switch (this.config.trigger) {
      case 'scroll':
        this.setupScrollTrigger()
        break
      case 'exit-intent':
        this.setupExitIntentTrigger()
        break
    }
  }

  private showTrigger() {
    if (this.widgetElement) {
      const trigger = this.widgetElement.querySelector('.hj-widget-trigger')
      if (trigger) {
        (trigger as HTMLElement).style.display = 'block'
      }
    }
  }

  private setupScrollTrigger() {
    const scrollPercentage = 0.5 // Show after 50% scroll
    window.addEventListener('scroll', () => {
      const scrollTop = window.pageYOffset || document.documentElement.scrollTop
      const winHeight = window.innerHeight
      const docHeight = document.documentElement.scrollHeight

      const currentScroll = (scrollTop + winHeight) / docHeight

      if (currentScroll >= scrollPercentage && !this.isOpen) {
        this.openWidget()
      }
    }, { once: true })
  }

  private setupExitIntentTrigger() {
    let mouseTimer: number

    document.addEventListener('mouseout', (event) => {
      if (event.clientY <= 0) {
        mouseTimer = window.setTimeout(() => {
          if (!this.isOpen) {
            this.openWidget()
          }
        }, 500)
      }
    })

    document.addEventListener('mouseover', () => {
      clearTimeout(mouseTimer)
    })
  }

  private toggleWidget() {
    if (this.isOpen) {
      this.closeWidget()
    } else {
      this.openWidget()
    }
  }

  private openWidget() {
    if (!this.widgetElement || this.isOpen) return

    this.isOpen = true
    const container = this.widgetElement.querySelector('.hj-widget-container')
    if (container) {
      (container as HTMLElement).style.display = 'block'
    }

    this.currentStep = 0
    this.responses = {}
    this.renderCurrentStep()

    // Track widget opened in Hotjar
    window.hj('event', 'feedback_widget_opened', {
      trigger_type: this.config.trigger,
      page: window.location.pathname
    })
  }

  private closeWidget() {
    if (!this.widgetElement || !this.isOpen) return

    this.isOpen = false
    const container = this.widgetElement.querySelector('.hj-widget-container')
    if (container) {
      (container as HTMLElement).style.display = 'none'
    }

    // Track widget closed in Hotjar
    window.hj('event', 'feedback_widget_closed', {
      current_step: this.currentStep,
      completion_percentage: (this.currentStep / this.config.questions.length) * 100
    })
  }

  private renderCurrentStep() {
    if (!this.widgetElement || this.currentStep >= this.config.questions.length) return

    const content = this.widgetElement.querySelector('.hj-widget-content')
    if (!content) return

    const question = this.config.questions[this.currentStep]

    // Update progress indicator
    this.updateProgressIndicator()

    // Render question
    let questionHtml = `
      <div class="hj-question-container">
        <label class="hj-question-label">
          ${question.required ? '*' : ''}${question.question}
        </label>
    `

    switch (question.type) {
      case 'rating':
        questionHtml += this.renderRatingQuestion(question)
        break
      case 'text':
        questionHtml += this.renderTextQuestion(question)
        break
      case 'nps':
        questionHtml += this.renderNpsQuestion(question)
        break
      case 'multiple-choice':
        questionHtml += this.renderMultipleChoiceQuestion(question)
        break
    }

    questionHtml += '</div>'
    content.innerHTML = questionHtml

    // Setup question-specific event listeners
    this.setupQuestionListeners(question)

    // Update navigation buttons
    this.updateNavigationButtons()

    // Track question viewed in Hotjar
    window.hj('event', 'feedback_question_viewed', {
      question_id: question.id,
      question_type: question.type,
      step_number: this.currentStep + 1,
      total_steps: this.config.questions.length
    })
  }

  private renderRatingQuestion(question: FeedbackQuestion): string {
    return `
      <div class="hj-rating-stars" data-question-id="${question.id}">
        ${[1,2,3,4,5].map(star =>
          `<span class="hj-star" data-rating="${star}">★</span>`
        ).join('')}
      </div>
    `
  }

  private renderTextQuestion(question: FeedbackQuestion): string {
    const inputType = question.placeholder?.includes('email') ? 'email' : 'text'
    const isTextArea = question.placeholder?.toLowerCase().includes('message') ||
                      question.placeholder?.toLowerCase().includes('comment')

    if (isTextArea) {
      return `
        <textarea
          class="hj-textarea"
          data-question-id="${question.id}"
          placeholder="${question.placeholder || 'Enter your response...'}"
          ${question.required ? 'required' : ''}
        ></textarea>
      `
    } else {
      return `
        <input
          type="${inputType}"
          class="hj-text-input"
          data-question-id="${question.id}"
          placeholder="${question.placeholder || 'Enter your response...'}"
          ${question.required ? 'required' : ''}
        />
      `
    }
  }

  private renderNpsQuestion(question: FeedbackQuestion): string {
    return `
      <div class="hj-nps-scale" data-question-id="${question.id}">
        <div style="display: flex; justify-content: space-between; margin-bottom: 8px; font-size: 12px; color: #666;">
          <span>Not likely</span>
          <span>Very likely</span>
        </div>
        ${[0,1,2,3,4,5,6,7,8,9,10].map(num =>
          `<span class="hj-nps-number" data-rating="${num}">${num}</span>`
        ).join('')}
      </div>
    `
  }

  private renderMultipleChoiceQuestion(question: FeedbackQuestion): string {
    return `
      <div class="hj-multiple-choice" data-question-id="${question.id}">
        ${(question.options || []).map((option, index) =>
          `
            <label class="hj-choice-option">
              <input type="radio" name="${question.id}" value="${option}" />
              <span>${option}</span>
            </label>
          `
        ).join('')}
      </div>
    `
  }

  private setupQuestionListeners(question: FeedbackQuestion) {
    if (!this.widgetElement) return

    const questionElement = this.widgetElement.querySelector(`[data-question-id="${question.id}"]`)
    if (!questionElement) return

    switch (question.type) {
      case 'rating':
        this.setupRatingListeners(questionElement as HTMLElement, question.id)
        break
      case 'nps':
        this.setupNpsListeners(questionElement as HTMLElement, question.id)
        break
      case 'text':
        this.setupTextListeners(questionElement as HTMLElement, question.id)
        break
      case 'multiple-choice':
        this.setupMultipleChoiceListeners(questionElement as HTMLElement, question.id)
        break
    }
  }

  private setupRatingListeners(element: HTMLElement, questionId: string) {
    const stars = element.querySelectorAll('.hj-star')

    stars.forEach(star => {
      star.addEventListener('click', () => {
        const rating = parseInt((star as HTMLElement).dataset.rating || '0')
        this.responses[questionId] = rating

        // Update visual state
        stars.forEach((s, index) => {
          if (index < rating) {
            s.classList.add('active')
          } else {
            s.classList.remove('active')
          }
        })

        // Track rating in Hotjar
        window.hj('event', 'feedback_rating_given', {
          question_id: questionId,
          rating: rating
        })
      })
    })
  }

  private setupNpsListeners(element: HTMLElement, questionId: string) {
    const numbers = element.querySelectorAll('.hj-nps-number')

    numbers.forEach(number => {
      number.addEventListener('click', () => {
        const rating = parseInt((number as HTMLElement).dataset.rating || '0')
        this.responses[questionId] = rating

        // Update visual state
        numbers.forEach(n => n.classList.remove('selected'))
        number.classList.add('selected')

        // Track NPS in Hotjar
        window.hj('event', 'feedback_nps_given', {
          question_id: questionId,
          rating: rating
        })
      })
    })
  }

  private setupTextListeners(element: HTMLElement, questionId: string) {
    const input = element.querySelector('.hj-text-input, .hj-textarea') as HTMLInputElement

    if (input) {
      input.addEventListener('input', () => {
        this.responses[questionId] = input.value
      })

      input.addEventListener('blur', () => {
        // Track text response in Hotjar
        if (input.value.trim()) {
          window.hj('event', 'feedback_text_entered', {
            question_id: questionId,
            has_content: true
          })
        }
      })
    }
  }

  private setupMultipleChoiceListeners(element: HTMLElement, questionId: string) {
    const radios = element.querySelectorAll('input[type="radio"]')

    radios.forEach(radio => {
      radio.addEventListener('change', () => {
        this.responses[questionId] = (radio as HTMLInputElement).value

        // Track multiple choice in Hotjar
        window.hj('event', 'feedback_choice_selected', {
          question_id: questionId,
          choice: (radio as HTMLInputElement).value
        })
      })
    })
  }

  private updateProgressIndicator() {
    if (!this.widgetElement) return

    // Add progress indicator if not exists
    let progressIndicator = this.widgetElement.querySelector('.hj-progress-indicator')
    if (!progressIndicator) {
      const content = this.widgetElement.querySelector('.hj-widget-content')
      progressIndicator = document.createElement('div')
      progressIndicator.className = 'hj-progress-indicator'
      if (content) {
        content.insertBefore(progressIndicator, content.firstChild)
      }
    }

    // Update progress dots
    const dots = this.config.questions.map((_, index) => {
      const isActive = index === this.currentStep
      return `<span class="hj-progress-dot ${isActive ? 'active' : ''}"></span>`
    }).join('')

    progressIndicator.innerHTML = dots
  }

  private updateNavigationButtons() {
    if (!this.widgetElement) return

    const prevButton = this.widgetElement.querySelector('#hj-prev-button') as HTMLButtonElement
    const nextButton = this.widgetElement.querySelector('#hj-next-button') as HTMLButtonElement
    const submitButton = this.widgetElement.querySelector('#hj-submit-button') as HTMLButtonElement

    const isLastStep = this.currentStep === this.config.questions.length - 1
    const isFirstStep = this.currentStep === 0

    // Show/hide and disable buttons
    prevButton.style.display = isFirstStep ? 'none' : 'block'
    nextButton.style.display = isLastStep ? 'none' : 'block'
    submitButton.style.display = isLastStep ? 'block' : 'none'

    // Validate current step
    const currentQuestion = this.config.questions[this.currentStep]
    const hasResponse = this.responses[currentQuestion.id] !== undefined
    const isValidResponse = this.validateResponse(currentQuestion, this.responses[currentQuestion.id])

    nextButton.disabled = !hasResponse || !isValidResponse
    submitButton.disabled = !hasResponse || !isValidResponse
  }

  private validateResponse(question: FeedbackQuestion, response: any): boolean {
    if (question.required && !response) return false

    if (question.type === 'text' && typeof response === 'string') {
      return response.trim().length > 0
    }

    if (question.type === 'multiple-choice' && !response) {
      return false
    }

    return true
  }

  private previousStep() {
    if (this.currentStep > 0) {
      this.currentStep--
      this.renderCurrentStep()
    }
  }

  private nextStep() {
    if (this.currentStep < this.config.questions.length - 1) {
      this.currentStep++
      this.renderCurrentStep()
    }
  }

  private submitFeedback() {
    const feedbackResponse: FeedbackResponse = {
      userId: this.getCurrentUserId(),
      sessionId: this.getHotjarSessionId(),
      page: window.location.pathname,
      responses: this.responses,
      timestamp: Date.now(),
      userAgent: navigator.userAgent,
      viewport: {
        width: window.innerWidth,
        height: window.innerHeight
      }
    }

    // Send data to your backend
    this.sendFeedbackToServer(feedbackResponse)

    // Track feedback submission in Hotjar
    window.hj('event', 'feedback_submitted', {
      total_questions: this.config.questions.length,
      responses_count: Object.keys(this.responses).length,
      completion_time: Date.now() - (this.widgetElement?.dataset.openTime || Date.now())
    })

    // Close widget and show thank you message
    this.closeWidget()
    this.showThankYouMessage()

    console.log('Feedback submitted:', feedbackResponse)
  }

  private sendFeedbackToServer(response: FeedbackResponse) {
    // Send feedback to your backend API
    fetch('/api/feedback', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(response)
    }).catch(error => {
      console.error('Failed to send feedback:', error)
    })
  }

  private showThankYouMessage() {
    if (!this.widgetElement) return

    const container = this.widgetElement.querySelector('.hj-widget-container')
    if (container) {
      container.innerHTML = `
        <div style="padding: 40px 20px; text-align: center;">
          <div style="font-size: 48px; margin-bottom: 16px;">✅</div>
          <h3 style="margin: 0 0 8px 0;">Thank you for your feedback!</h3>
          <p style="margin: 0; color: #666;">Your response helps us improve our product.</p>
        </div>
      `

      (container as HTMLElement).style.display = 'block'
      this.widgetElement.dataset.openTime = Date.now().toString()

      // Auto-hide after 3 seconds
      setTimeout(() => {
        (container as HTMLElement).style.display = 'none'
      }, 3000)
    }
  }

  private getCurrentUserId(): string | undefined {
    // Get current user ID from your authentication system
    return (window as any).currentUser?.id
  }

  private getHotjarSessionId(): string | undefined {
    // Get Hotjar session ID if available
    return (window as any).hj?.session?.get?.()?.id
  }
}

// Example usage

// Basic feedback widget
const basicFeedback = new HotjarFeedbackWidget({
  position: 'bottom-right',
  trigger: 'click',
  theme: 'light',
  questions: [
    {
      id: 'satisfaction',
      type: 'rating',
      question: 'How satisfied are you with our product?',
      required: true
    },
    {
      id: 'improvement',
      type: 'text',
      question: 'What could we improve?',
      placeholder: 'Share your thoughts...',
      required: false
    }
  ]
})

// NPS survey widget
const npsSurvey = new HotjarFeedbackWidget({
  position: 'bottom-left',
  trigger: 'exit-intent',
  theme: 'dark',
  questions: [
    {
      id: 'nps',
      type: 'nps',
      question: 'How likely are you to recommend our product to a friend?',
      required: true
    },
    {
      id: 'reason',
      type: 'text',
      question: 'What is the main reason for your score?',
      placeholder: 'Please explain...',
      required: true
    }
  ]
})

export { HotjarFeedbackWidget, FeedbackWidgetConfig, FeedbackQuestion }

💻 React Hooks Integration typescript

🟡 intermediate ⭐⭐⭐

Moderne React hooks für Hotjar-Integration mit automatischem Tracking und Komponenten-Analyse

⏱️ 35 min 🏷️ react, frontend, hooks, analytics
Prerequisites: React hooks, TypeScript, Hotjar API, Event tracking
// Hotjar React Hooks Integration
// Modern React hooks for comprehensive Hotjar tracking and analytics

import React, { useEffect, useCallback, useRef, useState, useLayoutEffect } from 'react'

// Type definitions
interface HotjarEventProperties {
  [key: string]: any
}

interface HeatmapConfig {
  enabled: boolean
  clickTracking?: boolean
  scrollTracking?: boolean
  movementTracking?: boolean
}

interface RecordingConfig {
  enabled: boolean
  recordConsoleLogs?: boolean
  recordNetworkRequests?: boolean
  maskSensitiveInputs?: boolean
}

// Custom hook for Hotjar initialization
export const useHotjarInit = (siteId: number, hotjarVersion: number = 6) => {
  const [isInitialized, setIsInitialized] = useState(false)

  useEffect(() => {
    if (typeof window === 'undefined') return
    if (window.hj) {
      setIsInitialized(true)
      return
    }

    const script = document.createElement('script')
    script.innerHTML = `
      (function(h,o,t,j,a,r){
          h.hj=h.hj||function(){(h.hj.q=h.hj.q||[]).push(arguments)};
          h._hjSettings={hjid:${siteId},hjsv:${hotjarVersion}};
          a=o.getElementsByTagName('head')[0];
          r=o.createElement('script');r.async=1;
          r.src=t+h._hjSettings.hjid+j+h._hjSettings.hjsv;
          a.appendChild(r);
      })(window,document,'https://static.hotjar.com/c/hotjar-','.js?sv=');
    `

    document.head.appendChild(script)

    // Check if Hotjar is loaded
    const checkInterval = setInterval(() => {
      if (window.hj) {
        setIsInitialized(true)
        clearInterval(checkInterval)
      }
    }, 100)

    // Cleanup
    return () => {
      clearInterval(checkInterval)
    }
  }, [siteId, hotjarVersion])

  return { isInitialized }
}

// Custom hook for user identification
export const useHotjarIdentify = (userId?: string, traits?: Record<string, any>) => {
  const identifyUser = useCallback((uid: string, userTraits?: Record<string, any>) => {
    if (window.hj) {
      window.hj('identify', uid, userTraits)
    }
  }, [])

  useEffect(() => {
    if (userId) {
      identifyUser(userId, traits)
    }
  }, [userId, traits, identifyUser])

  return { identifyUser }
}

// Custom hook for event tracking
export const useHotjarEvent = () => {
  const trackEvent = useCallback((eventName: string, properties?: HotjarEventProperties) => {
    if (window.hj) {
      window.hj('event', eventName, properties)
    }
  }, [])

  return { trackEvent }
}

// Custom hook for page view tracking
export const useHotjarPageView = (pageTitle?: string) => {
  const { trackEvent } = useHotjarEvent()

  useEffect(() => {
    trackEvent('page_view', {
      page: window.location.pathname,
      title: pageTitle || document.title,
      url: window.location.href,
      referrer: document.referrer,
      timestamp: Date.now()
    })
  }, [trackEvent, pageTitle])
}

// Custom hook for form tracking
export const useHotjarFormTracking = (formName: string, options: {
  trackFieldFocus?: boolean
  trackFieldBlur?: boolean
  trackValidation?: boolean
} = {}) => {
  const { trackEvent } = useHotjarEvent()
  const formRef = useRef<HTMLFormElement>(null)

  const trackFieldEvent = useCallback((eventType: string, fieldName: string, value?: any) => {
    trackEvent('form_interaction', {
      form_name: formName,
      event_type: eventType,
      field_name: fieldName,
      value: value ? 'has_value' : 'empty',
      timestamp: Date.now()
    })
  }, [formName, trackEvent])

  const trackFormEvent = useCallback((eventType: string, data?: Record<string, any>) => {
    trackEvent('form_event', {
      form_name: formName,
      event_type: eventType,
      ...data,
      timestamp: Date.now()
    })
  }, [formName, trackEvent])

  useEffect(() => {
    if (!formRef.current || !options.trackFieldFocus) return

    const form = formRef.current

    const handleFocus = (event: FocusEvent) => {
      const target = event.target as HTMLElement
      if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.tagName === 'SELECT') {
        const fieldName = target.getAttribute('name') || target.getAttribute('id')
        if (fieldName) {
          trackFieldEvent('field_focus', fieldName)
        }
      }
    }

    form.addEventListener('focus', handleFocus, true)

    return () => {
      form.removeEventListener('focus', handleFocus, true)
    }
  }, [options.trackFieldFocus, trackFieldEvent])

  useEffect(() => {
    if (!formRef.current || !options.trackFieldBlur) return

    const form = formRef.current

    const handleBlur = (event: FocusEvent) => {
      const target = event.target as HTMLInputElement | HTMLTextAreaElement
      if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.tagName === 'SELECT') {
        const fieldName = target.getAttribute('name') || target.getAttribute('id')
        if (fieldName) {
          trackFieldEvent('field_blur', fieldName, target.value)
        }
      }
    }

    form.addEventListener('blur', handleBlur, true)

    return () => {
      form.removeEventListener('blur', handleBlur, true)
    }
  }, [options.trackFieldBlur, trackFieldEvent])

  const handleSubmit = useCallback((event: React.FormEvent) => {
    const form = event.target as HTMLFormElement
    const formData = new FormData(form)
    const fieldCount = Array.from(formData.keys()).length

    trackFormEvent('form_submit', {
      field_count: fieldCount,
      action: form.action,
      method: form.method
    })
  }, [trackFormEvent])

  const trackValidationError = useCallback((fieldName: string, errorMessage: string) => {
    if (options.trackValidation) {
      trackEvent('form_validation_error', {
        form_name: formName,
        field_name: fieldName,
        error_message: errorMessage,
        timestamp: Date.now()
      })
    }
  }, [formName, options.trackValidation, trackEvent])

  return {
    formRef,
    handleSubmit,
    trackValidationError,
    trackFormEvent
  }
}

// Custom hook for click tracking
export const useHotjarClickTracking = (options: {
  trackOutboundLinks?: boolean
  trackDownloads?: boolean
  trackButtonActions?: boolean
} = {}) => {
  const { trackEvent } = useHotjarEvent()
  const elementRef = useRef<HTMLElement>(null)

  const handleClick = useCallback((event: React.MouseEvent) => {
    const target = event.target as HTMLElement

    // Track button clicks
    if (options.trackButtonActions && (target.tagName === 'BUTTON' || target.closest('button'))) {
      const button = target.tagName === 'BUTTON' ? target : target.closest('button')
      trackEvent('button_click', {
        text: button?.textContent?.trim() || '',
        id: button?.id || '',
        class: button?.className || ''
      })
    }

    // Track link clicks
    if (target.tagName === 'A' || target.closest('a')) {
      const link = target.tagName === 'A' ? target as HTMLAnchorElement : target.closest('a')

      // Outbound links
      if (options.trackOutboundLinks && link?.hostname !== window.location.hostname) {
        trackEvent('outbound_link_click', {
          url: link?.href || '',
          text: link?.textContent?.trim() || ''
        })
      }

      // Download links
      if (options.trackDownloads && link?.href && link.href.match(/\.(pdf|doc|docx|xls|xlsx|zip|rar)$/i)) {
        trackEvent('download_link_click', {
          url: link?.href || '',
          file_type: link?.href?.split('.').pop() || '',
          file_name: link?.href?.split('/').pop() || ''
        })
      }
    }

    // Track custom clicks on tracked elements
    if (elementRef.current && (elementRef.current === target || elementRef.current.contains(target))) {
      trackEvent('element_click', {
        element_tag: target.tagName.toLowerCase(),
        element_id: target.id || '',
        element_class: target.className || '',
        element_text: target.textContent?.trim().substring(0, 50) || '',
        coordinates: {
          x: event.clientX,
          y: event.clientY
        }
      })
    }
  }, [trackEvent, options])

  useEffect(() => {
    document.addEventListener('click', handleClick, true)
    return () => {
      document.removeEventListener('click', handleClick, true)
    }
  }, [handleClick])

  return { elementRef }
}

// Custom hook for scroll tracking
export const useHotjarScrollTracking = (thresholds: number[] = [25, 50, 75, 90]) => {
  const { trackEvent } = useHotjarEvent()
  const maxScrollDepth = useRef(0)
  const scrolledThresholds = useRef<Set<number>>(new Set())

  useEffect(() => {
    const handleScroll = () => {
      const scrollTop = window.pageYOffset || document.documentElement.scrollTop
      const winHeight = window.innerHeight
      const docHeight = document.documentElement.scrollHeight

      const scrollPercentage = Math.round(((scrollTop + winHeight) / docHeight) * 100)

      // Update max scroll depth
      if (scrollPercentage > maxScrollDepth.current) {
        maxScrollDepth.current = scrollPercentage
      }

      // Check thresholds
      thresholds.forEach(threshold => {
        if (scrollPercentage >= threshold && !scrolledThresholds.current.has(threshold)) {
          scrolledThresholds.current.add(threshold)
          trackEvent('scroll_threshold_reached', {
            threshold: threshold,
            scroll_percentage: scrollPercentage,
            scroll_position: scrollTop,
            page_height: docHeight
          })
        }
      })
    }

    window.addEventListener('scroll', handleScroll, { passive: true })

    return () => {
      window.removeEventListener('scroll', handleScroll)
    }
  }, [trackEvent, thresholds])

  const getMaxScrollDepth = useCallback(() => {
    return maxScrollDepth.current
  }, [])

  return { getMaxScrollDepth }
}

// Custom hook for session engagement tracking
export const useHotjarSessionTracking = () => {
  const { trackEvent } = useHotjarEvent()
  const [sessionStartTime] = useState(Date.now())
  const [lastActivityTime, setLastActivityTime] = useState(Date.now())
  const [totalActiveTime, setTotalActiveTime] = useState(0)
  const activityTimeoutRef = useRef<number>()

  const updateActivity = useCallback(() => {
    const now = Date.now()
    const inactiveTime = now - lastActivityTime

    // Consider active if less than 30 seconds inactive
    if (inactiveTime < 30000) {
      setTotalActiveTime(prev => prev + Math.min(1000, inactiveTime))
    }

    setLastActivityTime(now)
  }, [lastActivityTime])

  const trackEngagement = useCallback((eventType: string, data?: any) => {
    updateActivity()
    trackEvent(eventType, {
      ...data,
      session_duration: Date.now() - sessionStartTime,
      active_time: totalActiveTime,
      timestamp: Date.now()
    })
  }, [updateActivity, trackEvent, sessionStartTime, totalActiveTime])

  useEffect(() => {
    const events = ['mousedown', 'keydown', 'scroll', 'touchstart', 'click']

    const handleActivity = () => {
      updateActivity()

      // Clear existing timeout
      if (activityTimeoutRef.current) {
        clearTimeout(activityTimeoutRef.current)
      }

      // Set new timeout for session end detection
      activityTimeoutRef.current = window.setTimeout(() => {
        trackEvent('session_ended', {
          total_duration: Date.now() - sessionStartTime,
          total_active_time: totalActiveTime,
          reason: 'timeout'
        })
      }, 30000) // 30 seconds of inactivity
    }

    events.forEach(event => {
      document.addEventListener(event, handleActivity, { passive: true })
    })

    // Track session start
    trackEvent('session_started', {
      timestamp: sessionStartTime,
      user_agent: navigator.userAgent,
      viewport: {
        width: window.innerWidth,
        height: window.innerHeight
      }
    })

    // Cleanup
    return () => {
      events.forEach(event => {
        document.removeEventListener(event, handleActivity)
      })

      if (activityTimeoutRef.current) {
        clearTimeout(activityTimeoutRef.current)
      }

      // Track session end
      trackEvent('session_ended', {
        total_duration: Date.now() - sessionStartTime,
        total_active_time: totalActiveTime,
        reason: 'page_unload'
      })
    }
  }, [sessionStartTime, totalActiveTime, updateActivity, trackEvent])

  return { trackEngagement, sessionStartTime }
}

// Custom hook for A/B testing with Hotjar
export const useHotjarABTest = (experimentName: string, variant?: string) => {
  const { trackEvent } = useHotjarEvent()

  useEffect(() => {
    if (variant) {
      // Set user properties for segmentation
      if (window.hj) {
        window.hj('identify', null, {
          [`ab_${experimentName}`]: variant
        })
      }

      trackEvent('ab_test_started', {
        experiment_name: experimentName,
        variant: variant
      })
    }
  }, [experimentName, variant, trackEvent])

  const trackConversion = useCallback((conversionType: string, value?: number) => {
    trackEvent('ab_test_conversion', {
      experiment_name: experimentName,
      variant: variant,
      conversion_type: conversionType,
      value: value
    })
  }, [experimentName, variant, trackEvent])

  return { trackConversion }
}

// Higher-order component for automatic tracking
export const withHotjarTracking = <P extends object>(
  WrappedComponent: React.ComponentType<P>,
  options: {
    trackPageView?: boolean
    trackClicks?: boolean
    trackScroll?: boolean
    componentName?: string
  } = {}
) => {
  const HOC = (props: P) => {
    const { elementRef } = useHotjarClickTracking({ trackButtonActions: true })
    const { trackEvent } = useHotjarEvent()

    // Track page view
    useHotjarPageView(options.componentName)

    // Track scroll depth
    if (options.trackScroll) {
      useHotjarScrollTracking()
    }

    // Track component mount
    useEffect(() => {
      if (options.componentName) {
        trackEvent('component_mounted', {
          component_name: options.componentName
        })
      }
    }, [trackEvent, options.componentName])

    return (
      <div ref={elementRef}>
        <WrappedComponent {...props} />
      </div>
    )
  }

  HOC.displayName = `withHotjarTracking(${WrappedComponent.displayName || WrappedComponent.name})`
  return HOC
}

// Example usage components

// Contact form with tracking
const TrackedContactForm: React.FC = () => {
  const { formRef, handleSubmit, trackValidationError } = useHotjarFormTracking('contact_form', {
    trackFieldFocus: true,
    trackFieldBlur: true,
    trackValidation: true
  })

  const [formData, setFormData] = useState({
    name: '',
    email: '',
    message: ''
  })

  const onSubmit = (e: React.FormEvent) => {
    e.preventDefault()
    handleSubmit(e)

    // Validate form
    if (!formData.name) {
      trackValidationError('name', 'Name is required')
      return
    }
    if (!formData.email) {
      trackValidationError('email', 'Email is required')
      return
    }

    console.log('Form submitted:', formData)
  }

  return (
    <form ref={formRef} onSubmit={onSubmit}>
      <input
        name="name"
        type="text"
        placeholder="Your Name"
        value={formData.name}
        onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
      />
      <input
        name="email"
        type="email"
        placeholder="Your Email"
        value={formData.email}
        onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
      />
      <textarea
        name="message"
        placeholder="Your Message"
        value={formData.message}
        onChange={(e) => setFormData(prev => ({ ...prev, message: e.target.value }))}
      />
      <button type="submit">Send Message</button>
    </form>
  )
}

// A/B testing example
const CheckoutButton: React.FC<{ variant: 'A' | 'B' }> = ({ variant }) => {
  const { trackConversion } = useHotjarABTest('checkout_button_test', variant)

  const handleClick = () => {
    trackConversion('checkout_started', 99.99)
    console.log('Checkout started for variant:', variant)
  }

  return (
    <button
      onClick={handleClick}
      style={{
        backgroundColor: variant === 'A' ? '#007bff' : '#28a745',
        color: 'white',
        padding: '12px 24px',
        border: 'none',
        borderRadius: '4px',
        cursor: 'pointer'
      }}
    >
      {variant === 'A' ? 'Buy Now' : 'Get Started'}
    </button>
  )
}

// Tracked component example
const TrackedProductCard = withHotjarTracking(
  ({ name, price }: { name: string; price: number }) => (
    <div className="product-card">
      <h3>{name}</h3>
      <p>${price}</p>
      <button>Add to Cart</button>
    </div>
  ),
  {
    trackPageView: true,
    trackClicks: true,
    componentName: 'ProductCard'
  }
)

export {
  TrackedContactForm,
  CheckoutButton,
  TrackedProductCard
}

💻 Analytics Dashboard Integration typescript

🔴 complex ⭐⭐⭐⭐⭐

Umfassendes Analytics Dashboard mit Hotjar-Daten und benutzerdefinierten Metriken erstellen

⏱️ 60 min 🏷️ frontend, analytics, dashboard, visualization
Prerequisites: React, TypeScript, Data visualization, Charts library, Hotjar API
// Hotjar Analytics Dashboard Integration
// Comprehensive dashboard for analyzing Hotjar data and user behavior insights

import React, { useState, useEffect, useCallback, useMemo } from 'react'
import { LineChart, Line, BarChart, Bar, PieChart, Pie, Cell, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'

// Type definitions
interface HeatmapData {
  page: string
  clicks: Array<{
    x: number
    y: number
    element: string
    count: number
  }>
  scrolls: Array<{
    percentage: number
    count: number
  }>
  date: string
}

interface SessionData {
  sessionId: string
  userId?: string
  duration: number
  pageViews: number
  clicks: number
  rageClicks: number
  errors: number
  deviceType: string
  browser: string
  country: string
  startDate: string
  endDate: string
  exitPage: string
  conversionEvents: Array<{
    type: string
    value?: number
    timestamp: number
  }>
}

interface FunnelStep {
  name: string
  stepOrder: number
  users: number
  dropOffRate: number
  avgTime: number
  conversionRate: number
}

interface DashboardMetrics {
  totalSessions: number
  totalPageViews: number
  averageSessionDuration: number
  bounceRate: number
  conversionRate: number
  topPages: Array<{ page: string; views: number; uniqueVisitors: number }>
  deviceBreakdown: Array<{ device: string; sessions: number; percentage: number }>
  countryBreakdown: Array<{ country: string; sessions: number; percentage: number }>
  hourlyActivity: Array<{ hour: number; sessions: number; activeUsers: number }>
  funnelData: FunnelStep[]
}

const HotjarAnalyticsDashboard: React.FC = () => {
  const [metrics, setMetrics] = useState<DashboardMetrics | null>(null)
  const [loading, setLoading] = useState(true)
  const [dateRange, setDateRange] = useState({
    start: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000), // 30 days ago
    end: new Date()
  })
  const [selectedMetric, setSelectedMetric] = useState<'overview' | 'funnel' | 'behavior' | 'technical'>('overview')

  // Initialize Hotjar tracking for the dashboard
  useEffect(() => {
    if (window.hj) {
      window.hj('event', 'analytics_dashboard_viewed', {
        metric_type: selectedMetric,
        date_range: {
          start: dateRange.start.toISOString(),
          end: dateRange.end.toISOString()
        }
      })
    }
  }, [selectedMetric, dateRange])

  // Fetch analytics data
  const fetchAnalyticsData = useCallback(async () => {
    setLoading(true)

    try {
      // This would typically call your backend API that queries Hotjar
      const mockData = await generateMockAnalyticsData()
      setMetrics(mockData)

      if (window.hj) {
        window.hj('event', 'analytics_data_loaded', {
          total_sessions: mockData.totalSessions,
          date_range: {
            start: dateRange.start.toISOString(),
            end: dateRange.end.toISOString()
          }
        })
      }
    } catch (error) {
      console.error('Failed to fetch analytics data:', error)
      if (window.hj) {
        window.hj('event', 'analytics_data_load_error', {
          error: error instanceof Error ? error.message : 'Unknown error'
        })
      }
    } finally {
      setLoading(false)
    }
  }, [dateRange])

  useEffect(() => {
    fetchAnalyticsData()
  }, [fetchAnalyticsData])

  // Generate mock analytics data (replace with real API call)
  const generateMockAnalyticsData = async (): Promise<DashboardMetrics> => {
    return new Promise(resolve => {
      setTimeout(() => {
        resolve({
          totalSessions: 15420,
          totalPageViews: 89450,
          averageSessionDuration: 245000, // 4.08 minutes
          bounceRate: 34.2,
          conversionRate: 12.8,
          topPages: [
            { page: '/home', views: 25400, uniqueVisitors: 18200 },
            { page: '/products', views: 19800, uniqueVisitors: 14200 },
            { page: '/about', views: 12300, uniqueVisitors: 8900 },
            { page: '/contact', views: 8700, uniqueVisitors: 6200 },
            { page: '/blog', views: 7200, uniqueVisitors: 5100 }
          ],
          deviceBreakdown: [
            { device: 'Desktop', sessions: 8920, percentage: 57.8 },
            { device: 'Mobile', sessions: 5320, percentage: 34.5 },
            { device: 'Tablet', sessions: 1180, percentage: 7.7 }
          ],
          countryBreakdown: [
            { country: 'United States', sessions: 6200, percentage: 40.2 },
            { country: 'United Kingdom', sessions: 2890, percentage: 18.7 },
            { country: 'Canada', sessions: 1870, percentage: 12.1 },
            { country: 'Australia', sessions: 1230, percentage: 8.0 },
            { country: 'Germany', sessions: 980, percentage: 6.4 }
          ],
          hourlyActivity: Array.from({ length: 24 }, (_, hour) => ({
            hour,
            sessions: Math.floor(Math.random() * 800) + 200,
            activeUsers: Math.floor(Math.random() * 300) + 50
          })),
          funnelData: [
            { name: 'Homepage Visit', stepOrder: 1, users: 15420, dropOffRate: 0, avgTime: 45000, conversionRate: 100 },
            { name: 'Product View', stepOrder: 2, users: 11200, dropOffRate: 27.3, avgTime: 78000, conversionRate: 72.7 },
            { name: 'Add to Cart', stepOrder: 3, users: 5400, dropOffRate: 51.8, avgTime: 32000, conversionRate: 35.0 },
            { name: 'Checkout Started', stepOrder: 4, users: 3800, dropOffRate: 29.6, avgTime: 120000, conversionRate: 24.6 },
            { name: 'Purchase Completed', stepOrder: 5, users: 1970, dropOffRate: 48.2, avgTime: 180000, conversionRate: 12.8 }
          ]
        })
      }, 1000)
    })
  }

  // Calculate additional metrics
  const calculatedMetrics = useMemo(() => {
    if (!metrics) return null

    return {
      engagementRate: ((metrics.averageSessionDuration / 300000) * 100).toFixed(1), // 5 min = 100%
      pagesPerSession: (metrics.totalPageViews / metrics.totalSessions).toFixed(1),
      peakActivityHour: metrics.hourlyActivity.reduce((max, hour) =>
        hour.sessions > max.sessions ? hour : max
      ).hour,
      topDevice: metrics.deviceBreakdown.reduce((max, device) =>
        device.sessions > max.sessions ? device : max
      ).device,
      conversionValue: metrics.totalSessions * metrics.conversionRate * 89.99 // Assuming avg $89.99 value
    }
  }, [metrics])

  // Export dashboard data
  const exportData = () => {
    if (!metrics) return

    const exportData = {
      dateRange,
      metrics,
      calculatedMetrics,
      exportDate: new Date().toISOString()
    }

    const dataStr = JSON.stringify(exportData, null, 2)
    const dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr)

    const exportFileDefaultName = `hotjar-analytics-${new Date().toISOString().split('T')[0]}.json`

    const linkElement = document.createElement('a')
    linkElement.setAttribute('href', dataUri)
    linkElement.setAttribute('download', exportFileDefaultName)
    linkElement.click()

    if (window.hj) {
      window.hj('event', 'analytics_data_exported', {
        date_range: dateRange,
        file_name: exportFileDefaultName
      })
    }
  }

  // Track metric selection
  const handleMetricSelection = (metric: typeof selectedMetric) => {
    setSelectedMetric(metric)
    if (window.hj) {
      window.hj('event', 'dashboard_metric_selected', {
        metric_type: metric
      })
    }
  }

  if (loading) {
    return (
      <div className="dashboard-loading">
        <div className="loading-spinner"></div>
        <p>Loading analytics data...</p>
      </div>
    )
  }

  if (!metrics || !calculatedMetrics) {
    return <div>No analytics data available</div>
  }

  const COLORS = ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd']

  return (
    <div className="hotjar-analytics-dashboard">
      <div className="dashboard-header">
        <h1>Hotjar Analytics Dashboard</h1>
        <div className="dashboard-controls">
          <div className="date-range-selector">
            <label>Date Range:</label>
            <input
              type="date"
              value={dateRange.start.toISOString().split('T')[0]}
              onChange={(e) => setDateRange(prev => ({ ...prev, start: new Date(e.target.value) }))}
            />
            <span>to</span>
            <input
              type="date"
              value={dateRange.end.toISOString().split('T')[0]}
              onChange={(e) => setDateRange(prev => ({ ...prev, end: new Date(e.target.value) }))}
            />
            <button onClick={fetchAnalyticsData}>Update</button>
          </div>
          <button onClick={exportData} className="export-button">Export Data</button>
        </div>
      </div>

      {/* Metric Navigation */}
      <div className="metric-navigation">
        {(['overview', 'funnel', 'behavior', 'technical'] as const).map(metric => (
          <button
            key={metric}
            className={`metric-tab ${selectedMetric === metric ? 'active' : ''}`}
            onClick={() => handleMetricSelection(metric)}
          >
            {metric.charAt(0).toUpperCase() + metric.slice(1)}
          </button>
        ))}
      </div>

      {/* Overview Section */}
      {selectedMetric === 'overview' && (
        <div className="overview-section">
          <div className="metrics-grid">
            <div className="metric-card">
              <h3>Total Sessions</h3>
              <p className="metric-value">{metrics.totalSessions.toLocaleString()}</p>
              <span className="metric-change positive">+12.5%</span>
            </div>

            <div className="metric-card">
              <h3>Avg Session Duration</h3>
              <p className="metric-value">{Math.round(metrics.averageSessionDuration / 1000 / 60)}m</p>
              <span className="metric-change positive">+8.3%</span>
            </div>

            <div className="metric-card">
              <h3>Bounce Rate</h3>
              <p className="metric-value">{metrics.bounceRate}%</p>
              <span className="metric-change negative">+2.1%</span>
            </div>

            <div className="metric-card">
              <h3>Conversion Rate</h3>
              <p className="metric-value">{metrics.conversionRate}%</p>
              <span className="metric-change positive">+15.7%</span>
            </div>
          </div>

          <div className="charts-grid">
            <div className="chart-container">
              <h3>Hourly Activity</h3>
              <ResponsiveContainer width="100%" height={300}>
                <LineChart data={metrics.hourlyActivity}>
                  <CartesianGrid strokeDasharray="3 3" />
                  <XAxis dataKey="hour" />
                  <YAxis />
                  <Tooltip />
                  <Legend />
                  <Line type="monotone" dataKey="sessions" stroke="#1f77b4" name="Sessions" />
                  <Line type="monotone" dataKey="activeUsers" stroke="#ff7f0e" name="Active Users" />
                </LineChart>
              </ResponsiveContainer>
            </div>

            <div className="chart-container">
              <h3>Device Breakdown</h3>
              <ResponsiveContainer width="100%" height={300}>
                <PieChart>
                  <Pie
                    data={metrics.deviceBreakdown}
                    cx="50%"
                    cy="50%"
                    labelLine={false}
                    label={({ device, percentage }) => `${device}: ${percentage}%`}
                    outerRadius={80}
                    fill="#8884d8"
                    dataKey="sessions"
                  >
                    {metrics.deviceBreakdown.map((entry, index) => (
                      <Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
                    ))}
                  </Pie>
                  <Tooltip />
                </PieChart>
              </ResponsiveContainer>
            </div>
          </div>

          <div className="top-pages-container">
            <h3>Top Pages</h3>
            <table className="data-table">
              <thead>
                <tr>
                  <th>Page</th>
                  <th>Page Views</th>
                  <th>Unique Visitors</th>
                  <th>Views per Visitor</th>
                </tr>
              </thead>
              <tbody>
                {metrics.topPages.map((page, index) => (
                  <tr key={index}>
                    <td>{page.page}</td>
                    <td>{page.views.toLocaleString()}</td>
                    <td>{page.uniqueVisitors.toLocaleString()}</td>
                    <td>{(page.views / page.uniqueVisitors).toFixed(2)}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        </div>
      )}

      {/* Funnel Section */}
      {selectedMetric === 'funnel' && (
        <div className="funnel-section">
          <h2>Conversion Funnel Analysis</h2>

          <div className="funnel-chart-container">
            <ResponsiveContainer width="100%" height={400}>
              <BarChart
                data={metrics.funnelData}
                layout="horizontal"
                margin={{ top: 20, right: 30, left: 80, bottom: 5 }}
              >
                <CartesianGrid strokeDasharray="3 3" />
                <XAxis type="number" />
                <YAxis dataKey="name" type="category" width={100} />
                <Tooltip />
                <Legend />
                <Bar dataKey="users" fill="#1f77b4" name="Users" />
              </BarChart>
            </ResponsiveContainer>
          </div>

          <div className="funnel-details">
            <h3>Funnel Step Details</h3>
            <table className="data-table">
              <thead>
                <tr>
                  <th>Step</th>
                  <th>Users</th>
                  <th>Conversion Rate</th>
                  <th>Drop-off Rate</th>
                  <th>Avg Time</th>
                </tr>
              </thead>
              <tbody>
                {metrics.funnelData.map((step, index) => (
                  <tr key={index}>
                    <td>{step.name}</td>
                    <td>{step.users.toLocaleString()}</td>
                    <td>{step.conversionRate.toFixed(1)}%</td>
                    <td className={step.dropOffRate > 30 ? 'negative' : ''}>
                      {step.dropOffRate.toFixed(1)}%
                    </td>
                    <td>{Math.round(step.avgTime / 1000)}s</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        </div>
      )}

      {/* Behavior Section */}
      {selectedMetric === 'behavior' && (
        <div className="behavior-section">
          <h2>User Behavior Analysis</h2>

          <div className="behavior-metrics">
            <div className="metric-card">
              <h3>Engagement Rate</h3>
              <p className="metric-value">{calculatedMetrics.engagementRate}%</p>
              <small>Based on session duration</small>
            </div>

            <div className="metric-card">
              <h3>Pages per Session</h3>
              <p className="metric-value">{calculatedMetrics.pagesPerSession}</p>
              <small>Average pages viewed</small>
            </div>

            <div className="metric-card">
              <h3>Peak Activity Hour</h3>
              <p className="metric-value">{calculatedMetrics.peakActivityHour}:00</p>
              <small>Most active time</small>
            </div>

            <div className="metric-card">
              <h3>Top Device</h3>
              <p className="metric-value">{calculatedMetrics.topDevice}</p>
              <small>Most used device type</small>
            </div>
          </div>

          <div className="charts-grid">
            <div className="chart-container">
              <h3>Country Distribution</h3>
              <ResponsiveContainer width="100%" height={300}>
                <PieChart>
                  <Pie
                    data={metrics.countryBreakdown}
                    cx="50%"
                    cy="50%"
                    labelLine={false}
                    label={({ country, percentage }) =>
                      percentage > 5 ? `${country}: ${percentage}%` : ''
                    }
                    outerRadius={80}
                    fill="#8884d8"
                    dataKey="sessions"
                  >
                    {metrics.countryBreakdown.map((entry, index) => (
                      <Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
                    ))}
                  </Pie>
                  <Tooltip />
                </PieChart>
              </ResponsiveContainer>
            </div>

            <div className="chart-container">
              <h3>Session Duration Distribution</h3>
              <ResponsiveContainer width="100%" height={300}>
                <BarChart data={[
                  { range: '0-30s', count: 3200 },
                  { range: '30s-2m', count: 4800 },
                  { range: '2-5m', count: 5100 },
                  { range: '5-10m', count: 1890 },
                  { range: '10m+', count: 430 }
                ]}>
                  <CartesianGrid strokeDasharray="3 3" />
                  <XAxis dataKey="range" />
                  <YAxis />
                  <Tooltip />
                  <Bar dataKey="count" fill="#2ca02c" name="Sessions" />
                </BarChart>
              </ResponsiveContainer>
            </div>
          </div>
        </div>
      )}

      {/* Technical Section */}
      {selectedMetric === 'technical' && (
        <div className="technical-section">
          <h2>Technical Performance</h2>

          <div className="technical-metrics">
            <div className="metric-card">
              <h3>Conversion Value</h3>
              <p className="metric-value">${calculatedMetrics.conversionValue.toLocaleString()}</p>
              <small>Total estimated value</small>
            </div>

            <div className="metric-card">
              <h3>Mobile Performance</h3>
              <p className="metric-value">
                {metrics.deviceBreakdown.find(d => d.device === 'Mobile')?.percentage}%
              </p>
              <small>Mobile session share</small>
            </div>
          </div>

          <div className="performance-insights">
            <h3>Performance Insights</h3>
            <div className="insights-grid">
              <div className="insight-card positive">
                <h4>✅ Strong Mobile Usage</h4>
                <p>{metrics.deviceBreakdown.find(d => d.device === 'Mobile')?.percentage}% of users access via mobile</p>
              </div>

              <div className="insight-card warning">
                <h4>⚠️ High Bounce Rate</h4>
                <p>Bounce rate of {metrics.bounceRate}% suggests landing page optimization needed</p>
              </div>

              <div className="insight-card positive">
                <h4>✅ Good Conversion Funnel</h4>
                <p>{metrics.conversionRate}% conversion rate indicates effective user journey</p>
              </div>

              <div className="insight-card warning">
                <h4>⚠️ Drop-off at Checkout</h4>
                <p>48.2% drop-off at final checkout step suggests payment friction</p>
              </div>
            </div>
          </div>
        </div>
      )}
    </div>
  )
}

// CSS for the dashboard
const dashboardStyles = `
  .hotjar-analytics-dashboard {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    padding: 20px;
    background: #f8f9fa;
    min-height: 100vh;
  }

  .dashboard-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 30px;
  }

  .dashboard-header h1 {
    margin: 0;
    color: #2c3e50;
  }

  .dashboard-controls {
    display: flex;
    gap: 20px;
    align-items: center;
  }

  .date-range-selector {
    display: flex;
    gap: 10px;
    align-items: center;
  }

  .date-range-selector input {
    padding: 8px;
    border: 1px solid #ddd;
    border-radius: 4px;
  }

  .date-range-selector button {
    padding: 8px 16px;
    background: #1f77b4;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
  }

  .export-button {
    padding: 8px 16px;
    background: #28a745;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
  }

  .metric-navigation {
    display: flex;
    gap: 10px;
    margin-bottom: 30px;
    border-bottom: 2px solid #e1e4e8;
  }

  .metric-tab {
    padding: 12px 24px;
    background: none;
    border: none;
    border-bottom: 3px solid transparent;
    cursor: pointer;
    font-size: 16px;
    color: #6c757d;
    transition: all 0.2s;
  }

  .metric-tab:hover {
    color: #1f77b4;
  }

  .metric-tab.active {
    color: #1f77b4;
    border-bottom-color: #1f77b4;
  }

  .metrics-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
    gap: 20px;
    margin-bottom: 30px;
  }

  .metric-card {
    background: white;
    padding: 24px;
    border-radius: 8px;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  }

  .metric-card h3 {
    margin: 0 0 12px 0;
    color: #6c757d;
    font-size: 14px;
    font-weight: 500;
  }

  .metric-value {
    font-size: 32px;
    font-weight: 700;
    color: #2c3e50;
    margin: 0 0 8px 0;
  }

  .metric-change {
    font-size: 14px;
    font-weight: 500;
  }

  .metric-change.positive {
    color: #28a745;
  }

  .metric-change.negative {
    color: #dc3545;
  }

  .charts-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 20px;
    margin-bottom: 30px;
  }

  .chart-container {
    background: white;
    padding: 24px;
    border-radius: 8px;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  }

  .chart-container h3 {
    margin: 0 0 20px 0;
    color: #2c3e50;
  }

  .top-pages-container {
    background: white;
    padding: 24px;
    border-radius: 8px;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  }

  .top-pages-container h3 {
    margin: 0 0 20px 0;
    color: #2c3e50;
  }

  .data-table {
    width: 100%;
    border-collapse: collapse;
  }

  .data-table th,
  .data-table td {
    padding: 12px;
    text-align: left;
    border-bottom: 1px solid #e1e4e8;
  }

  .data-table th {
    background: #f8f9fa;
    font-weight: 600;
    color: #2c3e50;
  }

  .negative {
    color: #dc3545;
    font-weight: 600;
  }

  .funnel-section,
  .behavior-section,
  .technical-section {
    h2 {
      margin: 0 0 30px 0;
      color: #2c3e50;
    }
  }

  .funnel-chart-container {
    background: white;
    padding: 24px;
    border-radius: 8px;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
    margin-bottom: 30px;
  }

  .funnel-details {
    background: white;
    padding: 24px;
    border-radius: 8px;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  }

  .funnel-details h3 {
    margin: 0 0 20px 0;
    color: #2c3e50;
  }

  .behavior-metrics {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 20px;
    margin-bottom: 30px;
  }

  .behavior-metrics .metric-card small {
    display: block;
    color: #6c757d;
    font-size: 12px;
  }

  .technical-metrics {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 20px;
    margin-bottom: 30px;
  }

  .performance-insights {
    background: white;
    padding: 24px;
    border-radius: 8px;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  }

  .performance-insights h3 {
    margin: 0 0 20px 0;
    color: #2c3e50;
  }

  .insights-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
    gap: 20px;
  }

  .insight-card {
    padding: 20px;
    border-radius: 8px;
    border-left: 4px solid;
  }

  .insight-card.positive {
    background: #d4edda;
    border-left-color: #28a745;
  }

  .insight-card.warning {
    background: #fff3cd;
    border-left-color: #ffc107;
  }

  .insight-card h4 {
    margin: 0 0 8px 0;
    color: #2c3e50;
  }

  .insight-card p {
    margin: 0;
    color: #6c757d;
  }

  .dashboard-loading {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    height: 400px;
  }

  .loading-spinner {
    width: 40px;
    height: 40px;
    border: 4px solid #f3f3f3;
    border-top: 4px solid #1f77b4;
    border-radius: 50%;
    animation: spin 1s linear infinite;
    margin-bottom: 20px;
  }

  @keyframes spin {
    0% { transform: rotate(0deg); }
    100% { transform: rotate(360deg); }
  }
`

// Inject styles
if (typeof document !== 'undefined') {
  const styleSheet = document.createElement('style')
  styleSheet.textContent = dashboardStyles
  document.head.appendChild(styleSheet)
}

export default HotjarAnalyticsDashboard