140 lines
3.7 KiB
TypeScript
140 lines
3.7 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { getAnonymousUserId } from '@/lib/client/anonymousUser';
|
|
|
|
interface CommentVoteButtonsProps {
|
|
commentId: string;
|
|
initialUpvotes: number;
|
|
initialDownvotes: number;
|
|
className?: string;
|
|
}
|
|
|
|
export default function CommentVoteButtons({
|
|
commentId,
|
|
initialUpvotes,
|
|
initialDownvotes,
|
|
className = '',
|
|
}: CommentVoteButtonsProps) {
|
|
const [upvotes, setUpvotes] = useState(initialUpvotes);
|
|
const [downvotes, setDownvotes] = useState(initialDownvotes);
|
|
const [userVote, setUserVote] = useState<1 | -1 | null>(null);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
|
|
// Load user's previous vote from localStorage on mount
|
|
useEffect(() => {
|
|
const storedVote = localStorage.getItem(`comment-vote-${commentId}`);
|
|
if (storedVote === '1') {
|
|
setUserVote(1);
|
|
} else if (storedVote === '-1') {
|
|
setUserVote(-1);
|
|
}
|
|
}, [commentId]);
|
|
|
|
const handleVote = async (direction: 1 | -1) => {
|
|
if (isLoading) return;
|
|
|
|
const anonymousId = getAnonymousUserId();
|
|
const previousVote = userVote;
|
|
const previousUpvotes = upvotes;
|
|
const previousDownvotes = downvotes;
|
|
|
|
// If user clicked same vote, no change
|
|
if (userVote === direction) {
|
|
return;
|
|
}
|
|
|
|
setIsLoading(true);
|
|
|
|
// Calculate new vote counts
|
|
let newUpvotes = upvotes;
|
|
let newDownvotes = downvotes;
|
|
|
|
if (previousVote === 1) {
|
|
newUpvotes -= 1;
|
|
} else if (previousVote === -1) {
|
|
newDownvotes -= 1;
|
|
}
|
|
|
|
if (direction === 1) {
|
|
newUpvotes += 1;
|
|
} else {
|
|
newDownvotes += 1;
|
|
}
|
|
|
|
setUserVote(direction);
|
|
setUpvotes(newUpvotes);
|
|
setDownvotes(newDownvotes);
|
|
|
|
try {
|
|
const response = await fetch(`/api/comments/${commentId}/vote-web`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ vote: direction, anonymousId }),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Vote failed');
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
// Update with server response
|
|
setUpvotes(data.votes.upvotes);
|
|
setDownvotes(data.votes.downvotes);
|
|
setUserVote(data.userVote);
|
|
|
|
// Store vote in localStorage
|
|
localStorage.setItem(`comment-vote-${commentId}`, String(direction));
|
|
} catch (error) {
|
|
// Revert on error
|
|
setUserVote(previousVote);
|
|
setUpvotes(previousUpvotes);
|
|
setDownvotes(previousDownvotes);
|
|
alert('Failed to vote. Please try again.');
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className={`flex items-center gap-3 ${className}`}>
|
|
<button
|
|
onClick={() => handleVote(1)}
|
|
disabled={isLoading}
|
|
className={`
|
|
flex items-center gap-1 text-sm px-2 py-1 rounded border transition-all duration-200
|
|
${
|
|
userVote === 1
|
|
? 'border-blue-500 bg-blue-500/10 text-blue-400'
|
|
: 'border-slate-700 text-slate-400 hover:bg-slate-800'
|
|
}
|
|
disabled:opacity-50 disabled:cursor-not-allowed
|
|
`}
|
|
>
|
|
<span>▲</span>
|
|
<span className="font-medium">{upvotes}</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => handleVote(-1)}
|
|
disabled={isLoading}
|
|
className={`
|
|
flex items-center gap-1 text-sm px-2 py-1 rounded border transition-all duration-200
|
|
${
|
|
userVote === -1
|
|
? 'border-red-500 bg-red-500/10 text-red-400'
|
|
: 'border-slate-700 text-slate-400 hover:bg-slate-800'
|
|
}
|
|
disabled:opacity-50 disabled:cursor-not-allowed
|
|
`}
|
|
>
|
|
<span>▼</span>
|
|
<span className="font-medium">{downvotes}</span>
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|