Skip to main content

Comment Likes API

Overview​

The Comment Likes API allows authenticated users to like and unlike comments throughout the Ring Platform. This endpoint supports optimistic UI updates and tracks user engagement with comment content, including nested comment interactions.

Endpoint Details​

  • URL: /api/comments/[id]/like
  • Methods: POST, GET
  • Authentication: Required (NextAuth.js session)
  • Content-Type: application/json

URL Parameters​

ParameterTypeRequiredDescription
idstringYesThe unique identifier of the comment to like/unlike

Authentication​

This endpoint requires user authentication via NextAuth.js session:

import { getSession } from 'next-auth/react'

const session = await getSession()
if (!session) {
// Handle unauthenticated state
}

POST - Like/Unlike Comment​

Request Body​

No request body required. The action (like/unlike) is determined automatically based on current like status.

Response Format​

Success Response (200)​

{
"success": true,
"data": {
"commentId": "comment123",
"action": "like",
"liked": true,
"likes": 15,
"userId": "user456"
},
"message": "Comment liked successfully"
}

Unlike Response (200)​

{
"success": true,
"data": {
"commentId": "comment123",
"action": "unlike",
"liked": false,
"likes": 14,
"userId": "user456"
},
"message": "Comment unliked successfully"
}

GET - Check Like Status​

Response Format​

Success Response (200)​

{
"success": true,
"data": {
"commentId": "comment123",
"liked": true,
"likes": 15,
"userId": "user456"
}
}

Error Responses​

Unauthorized (401)​

{
"error": "Authentication required"
}

Bad Request (400)​

{
"error": "Comment ID is required"
}
{
"error": "Cannot like inactive comment"
}

Not Found (404)​

{
"error": "Comment not found"
}

Internal Server Error (500)​

{
"error": "Failed to process like action"
}
{
"error": "Failed to get like status"
}

Implementation Examples​

React Component with Optimistic Updates​

import { useState, useEffect } from 'react'
import { useSession } from 'next-auth/react'

interface CommentLikeData {
commentId: string
liked: boolean
likes: number
userId: string
}

interface CommentLikeButtonProps {
commentId: string
initialLikes: number
initialLiked?: boolean
onLikeUpdate?: (commentId: string, liked: boolean, likes: number) => void
}

function CommentLikeButton({
commentId,
initialLikes,
initialLiked = false,
onLikeUpdate
}: CommentLikeButtonProps) {
const { data: session } = useSession()
const [liked, setLiked] = useState(initialLiked)
const [likes, setLikes] = useState(initialLikes)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)

// Fetch initial like status
useEffect(() => {
if (!session) return

const fetchLikeStatus = async () => {
try {
const response = await fetch(`/api/comments/${commentId}/like`)

if (response.ok) {
const data = await response.json()
setLiked(data.data.liked)
setLikes(data.data.likes)
}
} catch (err) {
console.error('Error fetching like status:', err)
}
}

fetchLikeStatus()
}, [commentId, session])

const handleLikeToggle = async () => {
if (!session || loading) return

// Optimistic update
const wasLiked = liked
const newLiked = !liked
const newLikes = newLiked ? likes + 1 : likes - 1

setLiked(newLiked)
setLikes(newLikes)
setLoading(true)
setError(null)

try {
const response = await fetch(`/api/comments/${commentId}/like`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
})

if (!response.ok) {
throw new Error('Failed to toggle like')
}

const result = await response.json()

// Update with server response
setLiked(result.data.liked)
setLikes(result.data.likes)

// Notify parent component
onLikeUpdate?.(commentId, result.data.liked, result.data.likes)

} catch (err) {
// Revert optimistic update on error
setLiked(wasLiked)
setLikes(wasLiked ? likes + 1 : likes - 1)
setError(err instanceof Error ? err.message : 'Unknown error')
} finally {
setLoading(false)
}
}

if (!session) {
return (
<div className="flex items-center space-x-1 text-gray-500">
<span className="text-sm">❀️ {likes}</span>
</div>
)
}

return (
<div className="flex items-center space-x-1">
<button
onClick={handleLikeToggle}
disabled={loading}
className={`flex items-center space-x-1 px-2 py-1 rounded transition-colors ${
liked
? 'text-red-600 hover:text-red-700 bg-red-50 hover:bg-red-100'
: 'text-gray-600 hover:text-red-600 hover:bg-red-50'
} disabled:opacity-50`}
>
<span className={`text-sm ${loading ? 'animate-pulse' : ''}`}>
{liked ? '❀️' : '🀍'}
</span>
<span className="text-sm font-medium">{likes}</span>
</button>

{error && (
<div className="text-xs text-red-500 ml-2">{error}</div>
)}
</div>
)
}

React Hook for Comment Likes​

import { useState, useCallback } from 'react'
import { useSession } from 'next-auth/react'

export function useCommentLikes() {
const { data: session } = useSession()
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)

const toggleLike = useCallback(async (commentId: string) => {
if (!session) return { success: false, error: 'Not authenticated' }

setLoading(true)
setError(null)

try {
const response = await fetch(`/api/comments/${commentId}/like`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
})

if (!response.ok) {
const errorData = await response.json()
throw new Error(errorData.error || 'Failed to toggle like')
}

const result = await response.json()

return {
success: true,
data: result.data,
message: result.message
}
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred'
setError(errorMessage)
return { success: false, error: errorMessage }
} finally {
setLoading(false)
}
}, [session])

const getLikeStatus = useCallback(async (commentId: string) => {
if (!session) return { success: false, error: 'Not authenticated' }

try {
const response = await fetch(`/api/comments/${commentId}/like`)

if (!response.ok) {
throw new Error('Failed to get like status')
}

const result = await response.json()
return {
success: true,
data: result.data
}
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred'
return { success: false, error: errorMessage }
}
}, [session])

const batchGetLikeStatus = useCallback(async (commentIds: string[]) => {
if (!session || commentIds.length === 0) return { success: false, error: 'Invalid request' }

try {
const results = await Promise.all(
commentIds.map(id =>
fetch(`/api/comments/${id}/like`)
.then(res => res.json())
.then(data => ({ commentId: id, ...data.data }))
)
)

return {
success: true,
data: results
}
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred'
return { success: false, error: errorMessage }
}
}, [session])

return {
toggleLike,
getLikeStatus,
batchGetLikeStatus,
loading,
error,
clearError: () => setError(null)
}
}

Comments List with Likes​

import { useState, useEffect } from 'react'
import { useCommentLikes } from '@/hooks/useCommentLikes'

interface Comment {
id: string
content: string
authorName: string
authorAvatar: string | null
likes: number
replies: number
createdAt: string
level: number
}

interface CommentsListProps {
targetId: string
targetType: 'news' | 'entity' | 'opportunity'
comments: Comment[]
onCommentsUpdate: (comments: Comment[]) => void
}

function CommentsList({ targetId, targetType, comments, onCommentsUpdate }: CommentsListProps) {
const { batchGetLikeStatus } = useCommentLikes()
const [likeStates, setLikeStates] = useState<Record<string, boolean>>({})

// Fetch like states for all comments
useEffect(() => {
const fetchLikeStates = async () => {
const commentIds = comments.map(c => c.id)
const result = await batchGetLikeStatus(commentIds)

if (result.success) {
const states: Record<string, boolean> = {}
result.data.forEach((item: any) => {
states[item.commentId] = item.liked
})
setLikeStates(states)
}
}

if (comments.length > 0) {
fetchLikeStates()
}
}, [comments, batchGetLikeStatus])

const handleLikeUpdate = (commentId: string, liked: boolean, likes: number) => {
// Update like state
setLikeStates(prev => ({ ...prev, [commentId]: liked }))

// Update comment likes count
const updatedComments = comments.map(comment =>
comment.id === commentId
? { ...comment, likes }
: comment
)
onCommentsUpdate(updatedComments)
}

return (
<div className="space-y-4">
{comments.map((comment) => (
<div key={comment.id} className="bg-white border rounded-lg p-4">
<div className="flex items-start space-x-3">
{comment.authorAvatar && (
<img
src={comment.authorAvatar}
alt={comment.authorName}
className="w-8 h-8 rounded-full"
/>
)}
<div className="flex-1 min-w-0">
<div className="flex items-center space-x-2">
<span className="font-medium text-gray-900">{comment.authorName}</span>
<span className="text-sm text-gray-500">
{new Date(comment.createdAt).toLocaleDateString()}
</span>
</div>
<p className="mt-1 text-gray-700">{comment.content}</p>

<div className="flex items-center space-x-4 mt-3">
<CommentLikeButton
commentId={comment.id}
initialLikes={comment.likes}
initialLiked={likeStates[comment.id] || false}
onLikeUpdate={handleLikeUpdate}
/>

<button className="text-sm text-gray-500 hover:text-gray-700">
Reply ({comment.replies})
</button>
</div>
</div>
</div>
</div>
))}
</div>
)
}

Server Action (Next.js 13+)​

'use server'

import { auth } from '@/auth'
import { redirect } from 'next/navigation'
import { revalidatePath } from 'next/cache'

export async function toggleCommentLikeAction(commentId: string, currentPath: string) {
const session = await auth()

if (!session) {
redirect('/auth/signin')
}

try {
const response = await fetch(`${process.env.NEXTAUTH_URL}/api/comments/${commentId}/like`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Cookie': request.headers.get('cookie') || '',
},
})

if (!response.ok) {
const error = await response.json()
throw new Error(error.error || 'Failed to toggle like')
}

const result = await response.json()

// Revalidate the current page to show updated like count
revalidatePath(currentPath)

return {
success: true,
data: result.data,
message: result.message
}

} catch (error) {
console.error('Server action error:', error)
throw error
}
}

cURL Examples​

Like a Comment​

curl -X POST https://ring.ck.ua/api/comments/comment123/like \
-H "Content-Type: application/json" \
-H "Cookie: next-auth.session-token=your-session-token"

Check Like Status​

curl -X GET https://ring.ck.ua/api/comments/comment123/like \
-H "Cookie: next-auth.session-token=your-session-token"

Business Logic​

POST Request Flow:​

  1. Authentication Check: Verifies user session
  2. Comment Validation: Ensures comment exists and is active
  3. Like Status Check: Determines if user has already liked the comment
  4. Atomic Update: Uses Firestore batch operations for consistency
  5. Response: Returns updated like status and count

GET Request Flow:​

  1. Authentication Check: Verifies user session
  2. Comment Validation: Ensures comment exists
  3. Like Status Lookup: Checks user's like status for the comment
  4. Response: Returns current like status and total count

Database Schema​

Comment Document​

{
"id": "comment123",
"content": "Great article! Very informative.",
"authorId": "user456",
"authorName": "John Doe",
"targetId": "news789",
"targetType": "news",
"likes": 15,
"replies": 3,
"status": "active",
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-01-15T14:22:00Z"
}

Comment Like Document​

{
"id": "comment123_user456",
"commentId": "comment123",
"userId": "user456",
"userName": "John Doe",
"userAvatar": "https://example.com/avatar.jpg",
"createdAt": "2024-01-15T14:22:00Z"
}

Security Considerations​

  • Authentication Required: Only authenticated users can like comments
  • User Tracking: Individual like records prevent duplicate likes
  • Comment Status Check: Only active comments can be liked
  • Atomic Operations: Batch operations ensure data consistency

Performance Notes​

  • Batch Operations: Uses Firestore batch writes for atomic updates
  • Optimistic Updates: Frontend can update UI before server confirmation
  • Indexed Queries: Like lookups use composite document IDs for performance
  • Minimal Data Transfer: Only necessary fields are returned

Rate Limiting​

  • User-based: 60 requests per minute per authenticated user
  • Global: 1000 requests per minute across all users

Analytics Integration​

// Track like events for analytics
const trackCommentLike = (commentId: string, action: 'like' | 'unlike') => {
// Google Analytics
gtag('event', 'comment_engagement', {
action: action,
comment_id: commentId,
engagement_type: 'like'
})

// Custom analytics
analytics.track('Comment Like', {
commentId,
action,
timestamp: new Date().toISOString()
})
}

Testing​

Unit Test Example​

import { POST, GET } from '@/app/api/comments/[id]/like/route'
import { NextRequest } from 'next/server'

jest.mock('@/auth')
jest.mock('firebase-admin/firestore')

describe('/api/comments/[id]/like', () => {
it('should like a comment successfully', async () => {
(auth as jest.Mock).mockResolvedValue({
user: { id: 'user123', name: 'John Doe' }
})

const request = new NextRequest('http://localhost:3000/api/comments/comment123/like', {
method: 'POST'
})

const response = await POST(request, { params: { id: 'comment123' } })
const data = await response.json()

expect(response.status).toBe(200)
expect(data.success).toBe(true)
expect(data.data.action).toBe('like')
expect(data.data.liked).toBe(true)
})

it('should get like status successfully', async () => {
const request = new NextRequest('http://localhost:3000/api/comments/comment123/like')

const response = await GET(request, { params: { id: 'comment123' } })
const data = await response.json()

expect(response.status).toBe(200)
expect(data.success).toBe(true)
expect(data.data.commentId).toBe('comment123')
})
})