|
|
import { Button } from '@/components/ui/button' |
|
|
import { createReactionDraftEvent } from '@/lib/draft-event' |
|
|
import { useNostr } from '@/providers/NostrProvider' |
|
|
import noteStatsService from '@/services/note-stats.service' |
|
|
import { Event } from 'nostr-tools' |
|
|
import { ChevronDown, ChevronUp } from 'lucide-react' |
|
|
import { useMemo, useState } from 'react' |
|
|
import { useNoteStatsById } from '@/hooks/useNoteStatsById' |
|
|
import logger from '@/lib/logger' |
|
|
|
|
|
export default function VoteButtons({ event }: { event: Event }) { |
|
|
const { pubkey, publish, checkLogin } = useNostr() |
|
|
const [voting, setVoting] = useState<string | null>(null) |
|
|
const noteStats = useNoteStatsById(event.id) |
|
|
|
|
|
// Calculate vote counts and user's current vote |
|
|
const { userVote, score } = useMemo(() => { |
|
|
const stats = noteStats || {} |
|
|
const reactions = stats.likes || [] |
|
|
|
|
|
const upvoteReactions = reactions.filter(r => r.emoji === '⬆️') |
|
|
const downvoteReactions = reactions.filter(r => r.emoji === '⬇️') |
|
|
|
|
|
const score = upvoteReactions.length - downvoteReactions.length |
|
|
|
|
|
// Check if current user has voted |
|
|
let userVote: 'up' | 'down' | null = null |
|
|
if (pubkey) { |
|
|
if (upvoteReactions.some(r => r.pubkey === pubkey)) { |
|
|
userVote = 'up' |
|
|
} else if (downvoteReactions.some(r => r.pubkey === pubkey)) { |
|
|
userVote = 'down' |
|
|
} |
|
|
} |
|
|
|
|
|
return { userVote, score } |
|
|
}, [noteStats, pubkey]) |
|
|
|
|
|
const vote = async (type: 'up' | 'down') => { |
|
|
checkLogin(async () => { |
|
|
if (voting || !pubkey) return |
|
|
|
|
|
// Prevent voting if user already voted (no toggling allowed) |
|
|
if (userVote) { |
|
|
return // User already voted, don't allow multiple votes |
|
|
} |
|
|
|
|
|
setVoting(type) |
|
|
const timer = setTimeout(() => setVoting(null), 10_000) |
|
|
|
|
|
try { |
|
|
if (!noteStats?.updatedAt) { |
|
|
await noteStatsService.fetchNoteStats(event, pubkey) |
|
|
} |
|
|
|
|
|
// Create the vote reaction |
|
|
const emoji = type === 'up' ? '⬆️' : '⬇️' |
|
|
|
|
|
// Check if user already voted this way |
|
|
const existingVote = userVote === type |
|
|
if (existingVote) { |
|
|
// Remove vote by creating a reaction with the same emoji (this will toggle it off) |
|
|
const reaction = createReactionDraftEvent(event, emoji) |
|
|
const evt = await publish(reaction) |
|
|
noteStatsService.updateNoteStatsByEvents([evt]) |
|
|
} else { |
|
|
// If user voted the opposite way, first remove the old vote |
|
|
if (userVote) { |
|
|
const oldEmoji = userVote === 'up' ? '⬆️' : '⬇️' |
|
|
const removeReaction = createReactionDraftEvent(event, oldEmoji) |
|
|
await publish(removeReaction) |
|
|
} |
|
|
|
|
|
// Then add the new vote |
|
|
const reaction = createReactionDraftEvent(event, emoji) |
|
|
const evt = await publish(reaction) |
|
|
noteStatsService.updateNoteStatsByEvents([evt]) |
|
|
} |
|
|
} catch (error) { |
|
|
logger.error('Vote failed', { error, eventId: event.id }) |
|
|
} finally { |
|
|
setVoting(null) |
|
|
clearTimeout(timer) |
|
|
} |
|
|
}) |
|
|
} |
|
|
|
|
|
return ( |
|
|
<div className="flex flex-col items-center gap-1"> |
|
|
<Button |
|
|
variant="ghost" |
|
|
size="sm" |
|
|
className={`h-6 w-6 p-0 hover:bg-muted hover:text-foreground ${ |
|
|
userVote === 'up' ? 'bg-muted text-foreground' : 'text-muted-foreground' |
|
|
}`} |
|
|
onClick={() => vote('up')} |
|
|
disabled={voting !== null || userVote !== null} |
|
|
> |
|
|
<ChevronUp className={`h-4 w-4 ${userVote === 'up' ? 'font-bold stroke-2 text-foreground' : ''}`} /> |
|
|
</Button> |
|
|
|
|
|
<span className={`text-xs font-medium min-w-[20px] text-center ${ |
|
|
score > 0 ? 'text-green-600' : score < 0 ? 'text-red-600' : 'text-muted-foreground' |
|
|
}`}> |
|
|
{score} |
|
|
</span> |
|
|
|
|
|
<Button |
|
|
variant="ghost" |
|
|
size="sm" |
|
|
className={`h-6 w-6 p-0 hover:bg-muted hover:text-foreground ${ |
|
|
userVote === 'down' ? 'bg-muted text-foreground' : 'text-muted-foreground' |
|
|
}`} |
|
|
onClick={() => vote('down')} |
|
|
disabled={voting !== null || userVote !== null} |
|
|
> |
|
|
<ChevronDown className={`h-4 w-4 ${userVote === 'down' ? 'font-bold stroke-2 text-foreground' : ''}`} /> |
|
|
</Button> |
|
|
</div> |
|
|
) |
|
|
}
|
|
|
|