@@ -25,6 +25,9 @@ export default function PostDetailClient() {
2525 const [ replyToId , setReplyToId ] = useState < number | null > ( null ) ;
2626 const [ editingCommentId , setEditingCommentId ] = useState < number | null > ( null ) ;
2727 const [ editContent , setEditContent ] = useState ( '' ) ;
28+ const [ upVotedPostIds , setUpVotedPostIds ] = useState < number [ ] > ( [ ] ) ;
29+ const [ downVotedPostIds , setDownVotedPostIds ] = useState < number [ ] > ( [ ] ) ;
30+ const [ localPostPoints , setLocalPostPoints ] = useState < number | null > ( null ) ;
2831 const [ upVotedCommentIds , setUpVotedCommentIds ] = useState < number [ ] > ( [ ] ) ;
2932 const [ downVotedCommentIds , setDownVotedCommentIds ] = useState < number [ ] > ( [ ] ) ;
3033 const [ localCommentPoints , setLocalCommentPoints ] = useState < Record < number , number > > ( { } ) ;
@@ -49,14 +52,19 @@ export default function PostDetailClient() {
4952 setPost ( response . post ) ;
5053 setComments ( response . comments || [ ] ) ;
5154
52- // Load user's comment votes if authenticated
55+ // Load user's votes if authenticated
5356 if ( isAuthenticated ) {
5457 try {
55- const votes = await gateway . getUserPostCommentVotes ( postId ) ;
58+ const [ votes , activity ] = await Promise . all ( [
59+ gateway . getUserPostCommentVotes ( postId ) ,
60+ gateway . getUserPostActivity ( ) ,
61+ ] ) ;
5662 setUpVotedCommentIds ( votes . upVotedCommentIds || [ ] ) ;
5763 setDownVotedCommentIds ( votes . downVotedCommentIds || [ ] ) ;
64+ setUpVotedPostIds ( activity . upVotedPostIds || [ ] ) ;
65+ setDownVotedPostIds ( activity . downVotedPostIds || [ ] ) ;
5866 } catch ( err ) {
59- console . error ( 'Failed to load comment votes:' , err ) ;
67+ console . error ( 'Failed to load votes:' , err ) ;
6068 }
6169 }
6270 } catch ( err ) {
@@ -70,6 +78,47 @@ export default function PostDetailClient() {
7078 loadPost ( ) ;
7179 } , [ postId , isAuthenticated ] ) ;
7280
81+ const handlePostVote = async ( weight : number , e : React . MouseEvent ) => {
82+ e . stopPropagation ( ) ;
83+ if ( ! isAuthenticated ) return ;
84+
85+ try {
86+ const currentUpVoted = upVotedPostIds . includes ( postId ) ;
87+ const currentDownVoted = downVotedPostIds . includes ( postId ) ;
88+ const currentPoints = localPostPoints ?? post ?. points ?? 0 ;
89+
90+ let newWeight = weight ;
91+ let pointsDelta = 0 ;
92+
93+ if ( weight === 1 ) {
94+ if ( currentUpVoted ) {
95+ newWeight = 0 ;
96+ pointsDelta = - 1 ;
97+ setUpVotedPostIds ( prev => prev . filter ( id => id !== postId ) ) ;
98+ } else {
99+ pointsDelta = currentDownVoted ? 2 : 1 ;
100+ setUpVotedPostIds ( prev => [ ...prev , postId ] ) ;
101+ setDownVotedPostIds ( prev => prev . filter ( id => id !== postId ) ) ;
102+ }
103+ } else if ( weight === - 1 ) {
104+ if ( currentDownVoted ) {
105+ newWeight = 0 ;
106+ pointsDelta = 1 ;
107+ setDownVotedPostIds ( prev => prev . filter ( id => id !== postId ) ) ;
108+ } else {
109+ pointsDelta = currentUpVoted ? - 2 : - 1 ;
110+ setDownVotedPostIds ( prev => [ ...prev , postId ] ) ;
111+ setUpVotedPostIds ( prev => prev . filter ( id => id !== postId ) ) ;
112+ }
113+ }
114+
115+ setLocalPostPoints ( currentPoints + pointsDelta ) ;
116+ await gateway . votePost ( postId , newWeight ) ;
117+ } catch ( err ) {
118+ console . error ( 'Failed to vote on post:' , err ) ;
119+ }
120+ } ;
121+
73122 const handleCommentVote = async ( commentId : number , weight : number , e : React . MouseEvent ) => {
74123 e . stopPropagation ( ) ;
75124
@@ -391,27 +440,60 @@ export default function PostDetailClient() {
391440 < div className = "container mx-auto max-w-5xl px-4 py-8 space-y-6" >
392441 { /* Post Card */ }
393442 < div className = "bg-white rounded-lg shadow-md p-6" >
394- < div className = "mb-4" >
395- < div className = "flex items-start justify-between mb-4" >
396- < h1 className = "text-3xl font-normal flex-1" >
397- { post . url ? (
398- < a href = { post . url } className = "text-blue-600 hover:underline" >
399- { post . title }
400- </ a >
401- ) : (
402- post . title
403- ) }
404- </ h1 >
405- { canEditPost ( post ) && (
406- < Link href = { routes . postEdit ( postId , slug ) } >
407- < PrimaryButton className = "ml-4" >
408- Edit
409- </ PrimaryButton >
410- </ Link >
411- ) }
443+ < div className = "flex gap-4 mb-4" >
444+ { /* Post Voting */ }
445+ < div className = "flex flex-col items-center text-gray-500 -mt-1" >
446+ < button type = "button"
447+ onClick = { ( e ) => handlePostVote ( 1 , e ) }
448+ className = { `text-2xl leading-none p-1 transition-colors ${
449+ upVotedPostIds . includes ( postId )
450+ ? 'text-green-600'
451+ : 'hover:text-green-600'
452+ } `}
453+ title = { upVotedPostIds . includes ( postId ) ? 'Remove upvote' : 'Upvote' }
454+ disabled = { ! isAuthenticated }
455+ >
456+ ▲
457+ </ button >
458+ < span className = "font-semibold text-base" >
459+ { localPostPoints ?? post . points ?? 0 }
460+ </ span >
461+ < button type = "button"
462+ onClick = { ( e ) => handlePostVote ( - 1 , e ) }
463+ className = { `text-2xl leading-none p-1 transition-colors ${
464+ downVotedPostIds . includes ( postId )
465+ ? 'text-red-600'
466+ : 'hover:text-red-600'
467+ } `}
468+ title = { downVotedPostIds . includes ( postId ) ? 'Remove downvote' : 'Downvote' }
469+ disabled = { ! isAuthenticated }
470+ >
471+ ▼
472+ </ button >
412473 </ div >
413474
414- < div className = "flex items-center gap-3 text-sm text-gray-600 mb-4" >
475+ { /* Post Header */ }
476+ < div className = "flex-1" >
477+ < div className = "flex items-start justify-between mb-4" >
478+ < h1 className = "text-3xl font-normal flex-1" >
479+ { post . url ? (
480+ < a href = { post . url } className = "text-blue-600 hover:underline" >
481+ { post . title }
482+ </ a >
483+ ) : (
484+ post . title
485+ ) }
486+ </ h1 >
487+ { canEditPost ( post ) && (
488+ < Link href = { routes . postEdit ( postId , slug ) } >
489+ < PrimaryButton className = "ml-4" >
490+ Edit
491+ </ PrimaryButton >
492+ </ Link >
493+ ) }
494+ </ div >
495+
496+ < div className = "flex items-center gap-3 text-sm text-gray-600 mb-4" >
415497 { post . userProfileUrl && (
416498 < img
417499 src = { post . userProfileUrl }
@@ -448,6 +530,7 @@ export default function PostDetailClient() {
448530 ) }
449531 </ div >
450532 ) }
533+ </ div >
451534 </ div >
452535 </ div >
453536
0 commit comments