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β
Parameter | Type | Required | Description |
---|---|---|---|
id | string | Yes | The 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:β
- Authentication Check: Verifies user session
- Comment Validation: Ensures comment exists and is active
- Like Status Check: Determines if user has already liked the comment
- Atomic Update: Uses Firestore batch operations for consistency
- Response: Returns updated like status and count
GET Request Flow:β
- Authentication Check: Verifies user session
- Comment Validation: Ensures comment exists
- Like Status Lookup: Checks user's like status for the comment
- 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')
})
})
Related Endpointsβ
GET /api/comments
- List comments with likes dataPOST /api/comments
- Create new commentGET /api/news/[id]/like
- News article likes APIGET /api/notifications
- Get like notifications