Hotjar Heatmaps and Analytics

Hotjar examples for heatmaps, session recordings, conversion funnels, feedback widgets, and user behavior analysis

Key Facts

Category
Developer Tools
Items
4
Format Families
sample

Sample Overview

Hotjar examples for heatmaps, session recordings, conversion funnels, feedback widgets, and user behavior analysis This sample set belongs to Developer Tools and can be used to test related workflows inside Elysia Tools.

💻 Hotjar Hello World javascript

🟢 simple

Basic Hotjar setup with heatmaps and session recordings enabled

⏱️ 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('identify', 'user_123', {
  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');
    }
  }
});

💻 Custom Feedback Widgets typescript

🟡 intermediate ⭐⭐⭐⭐

Implement custom feedback widgets with Hotjar integration for user feedback collection

⏱️ 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 ⭐⭐⭐

Modern React hooks for Hotjar integration with automatic tracking and component-level analytics

⏱️ 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 ⭐⭐⭐⭐⭐

Build a comprehensive analytics dashboard using Hotjar data and custom metrics

⏱️ 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