Voting
Learn how to implement upvoting and downvoting on feedback using the Feedhog SDK.
Voting
The SDK provides methods to toggle votes on feedback and check vote status. Voting helps prioritize features based on user interest.
vote(feedbackId)
Toggles a vote on a feedback item. If the user has already voted, their vote is removed. If they haven't voted, a vote is added.
async vote(feedbackId: string): Promise<VoteResult>Returns
interface VoteResult {
voted: boolean; // true if vote was added, false if removed
voteCount: number; // Total votes after the operation
}Example
import Feedhog from '@feedhog/js';
const feedhog = new Feedhog({ apiKey: 'fhpk_your_public_key' });
// Identify user first (recommended for persistent voting)
await feedhog.identify({
externalId: 'user-123',
email: 'john@example.com'
});
// Toggle vote
const result = await feedhog.vote('fb_abc123');
if (result.voted) {
console.log('Vote added! Total votes:', result.voteCount);
} else {
console.log('Vote removed. Total votes:', result.voteCount);
}hasVoted(feedbackId)
Checks if the current user has voted on a feedback item.
async hasVoted(feedbackId: string): Promise<VoteResult>Example
const { voted, voteCount } = await feedhog.hasVoted('fb_abc123');
console.log('Has voted:', voted);
console.log('Total votes:', voteCount);Vote Button Component
A complete voting component for React:
'use client';
import { useState, useEffect } from 'react';
import Feedhog from '@feedhog/js';
const feedhog = new Feedhog({
apiKey: process.env.NEXT_PUBLIC_FEEDHOG_API_KEY!
});
interface VoteButtonProps {
feedbackId: string;
initialVoteCount?: number;
userId?: string;
userEmail?: string;
}
export function VoteButton({
feedbackId,
initialVoteCount = 0,
userId,
userEmail
}: VoteButtonProps) {
const [voteCount, setVoteCount] = useState(initialVoteCount);
const [hasVoted, setHasVoted] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [isChecking, setIsChecking] = useState(true);
// Check initial vote status
useEffect(() => {
async function checkVote() {
if (userId) {
await feedhog.identify({ externalId: userId, email: userEmail });
}
try {
const result = await feedhog.hasVoted(feedbackId);
setHasVoted(result.voted);
setVoteCount(result.voteCount);
} catch (error) {
console.error('Failed to check vote:', error);
} finally {
setIsChecking(false);
}
}
checkVote();
}, [feedbackId, userId, userEmail]);
async function handleVote() {
if (isLoading) return;
setIsLoading(true);
try {
const result = await feedhog.vote(feedbackId);
setHasVoted(result.voted);
setVoteCount(result.voteCount);
} catch (error) {
console.error('Failed to vote:', error);
} finally {
setIsLoading(false);
}
}
if (isChecking) {
return (
<button disabled className="vote-button loading">
<span className="vote-icon">△</span>
<span className="vote-count">...</span>
</button>
);
}
return (
<button
onClick={handleVote}
disabled={isLoading}
className={`vote-button ${hasVoted ? 'voted' : ''}`}
aria-pressed={hasVoted}
aria-label={hasVoted ? 'Remove vote' : 'Add vote'}
>
<span className="vote-icon">{hasVoted ? '▲' : '△'}</span>
<span className="vote-count">{voteCount}</span>
</button>
);
}CSS Styles
.vote-button {
display: flex;
flex-direction: column;
align-items: center;
padding: 8px 12px;
border: 1px solid #e5e7eb;
border-radius: 8px;
background: white;
cursor: pointer;
transition: all 0.2s;
}
.vote-button:hover:not(:disabled) {
border-color: #3b82f6;
}
.vote-button.voted {
background: #eff6ff;
border-color: #3b82f6;
color: #3b82f6;
}
.vote-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.vote-icon {
font-size: 16px;
}
.vote-count {
font-size: 14px;
font-weight: 600;
}Voting in a List
Display a list of feedback with voting:
'use client';
import { useEffect, useState } from 'react';
import Feedhog from '@feedhog/js';
import type { FeedbackListItem } from '@feedhog/js';
import { VoteButton } from './vote-button';
const feedhog = new Feedhog({
apiKey: process.env.NEXT_PUBLIC_FEEDHOG_API_KEY!
});
interface FeedbackListProps {
userId?: string;
userEmail?: string;
}
export function FeedbackList({ userId, userEmail }: FeedbackListProps) {
const [items, setItems] = useState<FeedbackListItem[]>([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
async function load() {
if (userId) {
await feedhog.identify({ externalId: userId, email: userEmail });
}
const result = await feedhog.list({
sortBy: 'votes',
limit: 20
});
setItems(result.items);
setIsLoading(false);
}
load();
}, [userId, userEmail]);
if (isLoading) return <div>Loading...</div>;
return (
<ul className="feedback-list">
{items.map(item => (
<li key={item.id} className="feedback-item">
<VoteButton
feedbackId={item.id}
initialVoteCount={item.voteCount}
userId={userId}
userEmail={userEmail}
/>
<div className="feedback-content">
<h3>{item.title}</h3>
{item.description && <p>{item.description}</p>}
<span className="badge">{item.type}</span>
<span className="badge">{item.status}</span>
</div>
</li>
))}
</ul>
);
}Anonymous vs Identified Voting
Anonymous Voting
When no user is identified, votes are tracked by IP address:
const feedhog = new Feedhog({ apiKey: 'fhpk_your_public_key' });
// No identify() call - votes are anonymous
const result = await feedhog.vote('fb_abc123');Anonymous voting limitations:
- Votes are tied to IP address
- Users cannot vote from different devices
- Votes may be lost if IP changes
Identified Voting
When a user is identified, votes are tied to their account:
const feedhog = new Feedhog({ apiKey: 'fhpk_your_public_key' });
await feedhog.identify({
externalId: 'user-123',
email: 'john@example.com'
});
// Votes are tied to user-123
const result = await feedhog.vote('fb_abc123');Benefits of identified voting:
- Votes persist across devices
- Users can see their voting history
- More accurate vote counts
Optimistic Updates
For better UX, update the UI immediately before the server responds:
'use client';
import { useState, useCallback } from 'react';
import Feedhog from '@feedhog/js';
const feedhog = new Feedhog({
apiKey: process.env.NEXT_PUBLIC_FEEDHOG_API_KEY!
});
export function OptimisticVoteButton({
feedbackId,
initialVoteCount,
initialHasVoted
}: {
feedbackId: string;
initialVoteCount: number;
initialHasVoted: boolean;
}) {
const [voteCount, setVoteCount] = useState(initialVoteCount);
const [hasVoted, setHasVoted] = useState(initialHasVoted);
const [isPending, setIsPending] = useState(false);
const handleVote = useCallback(async () => {
if (isPending) return;
// Optimistic update
const newVoted = !hasVoted;
const newCount = hasVoted ? voteCount - 1 : voteCount + 1;
setHasVoted(newVoted);
setVoteCount(newCount);
setIsPending(true);
try {
const result = await feedhog.vote(feedbackId);
// Sync with server response
setHasVoted(result.voted);
setVoteCount(result.voteCount);
} catch (error) {
// Revert on error
setHasVoted(hasVoted);
setVoteCount(voteCount);
console.error('Vote failed:', error);
} finally {
setIsPending(false);
}
}, [feedbackId, hasVoted, voteCount, isPending]);
return (
<button
onClick={handleVote}
className={hasVoted ? 'voted' : ''}
disabled={isPending}
>
{hasVoted ? '▲' : '△'} {voteCount}
</button>
);
}Error Handling
Handle common voting errors:
import Feedhog, { FeedhogApiError } from '@feedhog/js';
const feedhog = new Feedhog({ apiKey: 'fhpk_your_public_key' });
try {
const result = await feedhog.vote('fb_abc123');
} catch (error) {
if (error instanceof FeedhogApiError) {
switch (error.status) {
case 401:
console.error('Invalid API key');
break;
case 404:
console.error('Feedback not found');
break;
case 429:
console.error('Too many requests, please slow down');
break;
default:
console.error('Vote failed:', error.message);
}
}
}