Feedback Methods

Learn how to submit, list, and retrieve feedback using the Feedhog SDK.

Feedback Methods

The SDK provides methods to submit new feedback, list existing feedback with filters, and retrieve detailed feedback information.

submit(input)

Submits new feedback. If a user has been identified, the feedback is associated with them.

async submit(input: SubmitFeedbackInput): Promise<FeedbackListItem>

Parameters

interface SubmitFeedbackInput {
  /** Feedback title (required) */
  title: string;
  /** Detailed description */
  description?: string;
  /** Type of feedback (defaults to "idea") */
  type?: FeedbackType;
  /** Additional metadata */
  metadata?: Record<string, unknown>;
}

type FeedbackType = "bug" | "idea" | "question" | "other";

Returns

interface FeedbackListItem {
  id: string;
  title: string;
  description: string | null;
  type: FeedbackType;
  status: FeedbackStatus;
  voteCount: number;
  commentCount: number;
  createdAt: string;
  endUser: {
    name: string | null;
    avatarUrl: string | null;
  } | null;
}

Example

import Feedhog from '@feedhog/js';

const feedhog = new Feedhog({ apiKey: 'fhpk_your_public_key' });

// First identify the user (optional)
await feedhog.identify({
  externalId: 'user-123',
  email: 'john@example.com',
  name: 'John Doe'
});

// Submit feedback
const feedback = await feedhog.submit({
  title: 'Add dark mode',
  description: 'Would love a dark theme option for better night-time usage. Maybe an automatic switch based on system preferences?',
  type: 'idea',
  metadata: {
    page: '/settings',
    browser: navigator.userAgent,
    screenSize: `${window.innerWidth}x${window.innerHeight}`
  }
});

console.log('Feedback ID:', feedback.id);
console.log('Status:', feedback.status); // "new"
console.log('Vote count:', feedback.voteCount); // 1 (auto-upvoted)

Form Example

'use client';

import { useState } from 'react';
import Feedhog from '@feedhog/js';
import type { FeedbackType } from '@feedhog/js';

const feedhog = new Feedhog({
  apiKey: process.env.NEXT_PUBLIC_FEEDHOG_API_KEY!
});

export function FeedbackForm() {
  const [title, setTitle] = useState('');
  const [description, setDescription] = useState('');
  const [type, setType] = useState<FeedbackType>('idea');
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [error, setError] = useState<string | null>(null);

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    setIsSubmitting(true);
    setError(null);

    try {
      await feedhog.submit({ title, description, type });
      setTitle('');
      setDescription('');
      alert('Thank you for your feedback!');
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Failed to submit');
    } finally {
      setIsSubmitting(false);
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <select value={type} onChange={(e) => setType(e.target.value as FeedbackType)}>
        <option value="idea">Feature Request</option>
        <option value="bug">Bug Report</option>
        <option value="question">Question</option>
        <option value="other">Other</option>
      </select>

      <input
        type="text"
        value={title}
        onChange={(e) => setTitle(e.target.value)}
        placeholder="What's on your mind?"
        required
        maxLength={200}
      />

      <textarea
        value={description}
        onChange={(e) => setDescription(e.target.value)}
        placeholder="Tell us more (optional)"
        rows={4}
      />

      {error && <p className="text-red-500">{error}</p>}

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Submitting...' : 'Submit Feedback'}
      </button>
    </form>
  );
}

list(options)

Returns a paginated list of feedback for the project.

async list(options?: ListFeedbackOptions): Promise<PaginatedResponse<FeedbackListItem>>

Parameters

interface ListFeedbackOptions {
  /** Filter by status(es) */
  status?: FeedbackStatus | FeedbackStatus[];
  /** Filter by type(s) */
  type?: FeedbackType | FeedbackType[];
  /** Search query */
  search?: string;
  /** Sort order */
  sortBy?: SortBy;
  /** Page number (1-indexed) */
  page?: number;
  /** Items per page (max 100) */
  limit?: number;
}

type FeedbackStatus =
  | "new"
  | "under-review"
  | "planned"
  | "in-progress"
  | "completed"
  | "closed";

type SortBy = "newest" | "oldest" | "votes" | "comments";

Returns

interface PaginatedResponse<T> {
  items: T[];
  total: number;
  page: number;
  limit: number;
  totalPages: number;
}

Examples

import Feedhog from '@feedhog/js';

const feedhog = new Feedhog({ apiKey: 'fhpk_your_public_key' });

// Get all feedback (default: newest first, 20 per page)
const all = await feedhog.list();
console.log(`Total: ${all.total} items`);

// Get feature requests sorted by votes
const topIdeas = await feedhog.list({
  type: 'idea',
  sortBy: 'votes',
  limit: 10
});

// Get planned and in-progress items
const roadmap = await feedhog.list({
  status: ['planned', 'in-progress'],
  sortBy: 'votes'
});

// Search feedback
const searchResults = await feedhog.list({
  search: 'dark mode',
  limit: 5
});

// Paginate through results
let page = 1;
let hasMore = true;

while (hasMore) {
  const result = await feedhog.list({
    page,
    limit: 50
  });

  console.log(`Page ${page}: ${result.items.length} items`);

  hasMore = page < result.totalPages;
  page++;
}

Roadmap Component

'use client';

import { useEffect, useState } from 'react';
import Feedhog from '@feedhog/js';
import type { FeedbackListItem } from '@feedhog/js';

const feedhog = new Feedhog({
  apiKey: process.env.NEXT_PUBLIC_FEEDHOG_API_KEY!
});

export function PublicRoadmap() {
  const [items, setItems] = useState<FeedbackListItem[]>([]);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    feedhog.list({
      status: ['planned', 'in-progress', 'completed'],
      type: 'idea',
      sortBy: 'votes',
      limit: 50
    })
    .then(result => setItems(result.items))
    .finally(() => setIsLoading(false));
  }, []);

  if (isLoading) return <div>Loading roadmap...</div>;

  const planned = items.filter(i => i.status === 'planned');
  const inProgress = items.filter(i => i.status === 'in-progress');
  const completed = items.filter(i => i.status === 'completed');

  return (
    <div className="grid grid-cols-3 gap-6">
      <Column title="Planned" items={planned} />
      <Column title="In Progress" items={inProgress} />
      <Column title="Completed" items={completed} />
    </div>
  );
}

function Column({ title, items }: { title: string; items: FeedbackListItem[] }) {
  return (
    <div>
      <h2 className="font-bold text-lg mb-4">{title}</h2>
      <div className="space-y-3">
        {items.map(item => (
          <div key={item.id} className="p-4 border rounded-lg">
            <h3 className="font-medium">{item.title}</h3>
            <p className="text-sm text-gray-500 mt-1">
              {item.voteCount} votes · {item.commentCount} comments
            </p>
          </div>
        ))}
      </div>
    </div>
  );
}

get(feedbackId)

Gets a single feedback item with full details including comments.

async get(feedbackId: string): Promise<FeedbackDetail>

Returns

interface FeedbackDetail extends FeedbackListItem {
  userHasVoted: boolean;
  comments: FeedbackComment[];
}

interface FeedbackComment {
  id: string;
  content: string;
  createdAt: string;
  author: {
    name: string | null;
    avatarUrl?: string | null;
    isTeam?: boolean;
  } | null;
}

Example

import Feedhog from '@feedhog/js';

const feedhog = new Feedhog({ apiKey: 'fhpk_your_public_key' });

// Identify user first to get accurate vote status
await feedhog.identify({
  externalId: 'user-123',
  email: 'john@example.com'
});

// Get feedback details
const feedback = await feedhog.get('fb_abc123');

console.log('Title:', feedback.title);
console.log('Description:', feedback.description);
console.log('Status:', feedback.status);
console.log('Has voted:', feedback.userHasVoted);
console.log('Vote count:', feedback.voteCount);
console.log('Comments:', feedback.comments.length);

// Display comments
feedback.comments.forEach(comment => {
  const authorName = comment.author?.name || 'Anonymous';
  const isTeam = comment.author?.isTeam ? ' (Team)' : '';
  console.log(`${authorName}${isTeam}: ${comment.content}`);
});

Feedback Detail Page

'use client';

import { useEffect, useState } from 'react';
import Feedhog from '@feedhog/js';
import type { FeedbackDetail } from '@feedhog/js';

const feedhog = new Feedhog({
  apiKey: process.env.NEXT_PUBLIC_FEEDHOG_API_KEY!
});

export function FeedbackPage({ feedbackId }: { feedbackId: string }) {
  const [feedback, setFeedback] = useState<FeedbackDetail | null>(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    feedhog.get(feedbackId)
      .then(setFeedback)
      .catch(console.error)
      .finally(() => setIsLoading(false));
  }, [feedbackId]);

  if (isLoading) return <div>Loading...</div>;
  if (!feedback) return <div>Feedback not found</div>;

  return (
    <article>
      <header>
        <span className="badge">{feedback.type}</span>
        <span className="badge">{feedback.status}</span>
        <h1>{feedback.title}</h1>
        <p className="meta">
          {feedback.voteCount} votes · {feedback.commentCount} comments
        </p>
      </header>

      {feedback.description && (
        <p className="description">{feedback.description}</p>
      )}

      <section>
        <h2>Comments</h2>
        {feedback.comments.length === 0 ? (
          <p>No comments yet</p>
        ) : (
          <ul>
            {feedback.comments.map(comment => (
              <li key={comment.id}>
                <strong>
                  {comment.author?.name || 'Anonymous'}
                  {comment.author?.isTeam && ' (Team)'}
                </strong>
                <p>{comment.content}</p>
                <time>{new Date(comment.createdAt).toLocaleDateString()}</time>
              </li>
            ))}
          </ul>
        )}
      </section>
    </article>
  );
}