Skip to content

Commit e76ecc9

Browse files
committed
Updated ads, and added delete function
1 parent 77c09f6 commit e76ecc9

File tree

5 files changed

+319
-11
lines changed

5 files changed

+319
-11
lines changed

app/(main)/client-page.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -392,13 +392,13 @@ function RotatingAdCopy() {
392392
description: "Build faster with our fully managed backend platform.",
393393
linkUrl: "https://appwrite.io"
394394
},
395-
{
396-
logoUrl: "https://imagine.dev/favicon.png",
397-
logoAlt: "Imagine Logo",
398-
title: "Imagine",
399-
description: "Build faster with our fullstack vibe coding platform.",
400-
linkUrl: "https://imagine.dev"
401-
}
395+
// {
396+
// logoUrl: "https://imagine.dev/favicon.png",
397+
// logoAlt: "Imagine Logo",
398+
// title: "Imagine",
399+
// description: "Build faster with the fullstack vibe coding platform.",
400+
// linkUrl: "https://imagine.dev"
401+
// }
402402
]
403403

404404
useEffect(() => {

app/(main)/threads/[id]/thread-client-page.tsx

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@ import { useEffect, useState, useMemo, useCallback, memo } from "react"
66
import type { NewsItem, Comment } from "@/lib/data"
77
import { type VoteState } from "@/lib/types"
88
import { handleVote, fetchUserVotesForResources } from "@/lib/voteHandler"
9+
import { deleteComment } from "@/lib/commentHandler"
910
import { useAuth } from "@/contexts/auth-context"
1011
import { getCachedJWT } from "@/lib/jwtCache"
1112
import { PostCard } from "@/components/post-card"
1213
import { avatars } from "@/lib/appwrite"
1314
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
1415
import { Button } from "@/components/ui/button"
15-
import { Clock, TrendingUp, Reply, ChevronDown, ChevronUp } from "lucide-react"
16+
import { Clock, TrendingUp, Reply, ChevronDown, ChevronUp, Trash2 } from "lucide-react"
1617

1718
interface ThreadClientPageProps {
1819
article: NewsItem
@@ -31,6 +32,8 @@ interface CommentItemProps {
3132
isOriginalPoster: (commentUserId: string) => boolean
3233
getCommentVoteState: (commentId: string) => VoteState
3334
getCommentChildren: (commentId: string) => Comment[]
35+
onDeleteComment?: (commentId: string) => void // Add callback for deleting comment
36+
currentUserId?: string // Add current user ID for ownership check
3437
}
3538

3639
const CommentItem = memo<CommentItemProps>(({
@@ -42,7 +45,9 @@ const CommentItem = memo<CommentItemProps>(({
4245
isAuthenticated,
4346
isOriginalPoster,
4447
getCommentVoteState,
45-
getCommentChildren
48+
getCommentChildren,
49+
onDeleteComment,
50+
currentUserId
4651
}) => {
4752
const canReply = depth < 2 // Allow replies up to depth 2 (3 levels total)
4853

@@ -86,6 +91,7 @@ const CommentItem = memo<CommentItemProps>(({
8691
<span className="text-gray-500 text-xs">original poster</span>
8792
</>
8893
)}
94+
<span className="text-gray-500">{comment.timeAgo}</span>
8995
</div>
9096
<div className="text-gray-700 text-sm mb-3 whitespace-pre-wrap">{comment.text}</div>
9197

@@ -100,6 +106,18 @@ const CommentItem = memo<CommentItemProps>(({
100106
size="sm"
101107
/>
102108
<div className="text-xs text-gray-500">{comment.timeAgo}</div>
109+
{/* Delete button - only show for comment owners, positioned before reply */}
110+
{onDeleteComment && comment.userId && currentUserId && comment.userId === currentUserId && (
111+
<Button
112+
variant="ghost"
113+
size="sm"
114+
className="h-auto p-1 text-xs text-gray-500 hover:text-red-600 flex items-center gap-1"
115+
onClick={() => onDeleteComment(comment.id)}
116+
>
117+
<Trash2 className="w-3 h-3" />
118+
Delete
119+
</Button>
120+
)}
103121
{canReply && (
104122
<Button
105123
variant="ghost"
@@ -128,6 +146,8 @@ const CommentItem = memo<CommentItemProps>(({
128146
isOriginalPoster={isOriginalPoster}
129147
getCommentVoteState={getCommentVoteState}
130148
getCommentChildren={getCommentChildren}
149+
onDeleteComment={onDeleteComment}
150+
currentUserId={currentUserId}
131151
/>
132152
))}
133153
</div>
@@ -430,6 +450,55 @@ export function ThreadClientPage({ article }: ThreadClientPageProps) {
430450
}
431451
}, [isAuthenticated, isCommentVoting, getCommentVoteState, updateCommentVoteState])
432452

453+
const handleDeleteComment = useCallback(async (commentId: string) => {
454+
if (!isAuthenticated || !user) {
455+
return
456+
}
457+
458+
if (confirm('Are you sure you want to delete this comment? This action cannot be undone.')) {
459+
try {
460+
const result = await deleteComment(commentId)
461+
if (result.success) {
462+
// Remove the comment from the comment map
463+
setCommentMap(prev => {
464+
const newMap = new Map(prev)
465+
newMap.delete(commentId)
466+
return newMap
467+
})
468+
469+
// Also remove any replies to this comment
470+
setCommentMap(prev => {
471+
const newMap = new Map(prev)
472+
const commentsToRemove: string[] = []
473+
474+
// Find all replies to this comment
475+
newMap.forEach((comment, id) => {
476+
if (comment.parentId === commentId) {
477+
commentsToRemove.push(id)
478+
}
479+
})
480+
481+
// Remove all replies
482+
commentsToRemove.forEach(id => newMap.delete(id))
483+
return newMap
484+
})
485+
486+
// Remove vote states for deleted comments
487+
setCommentVoteStates(prev => {
488+
const newStates = { ...prev }
489+
delete newStates[commentId]
490+
return newStates
491+
})
492+
} else {
493+
alert(`Failed to delete comment: ${result.error}`)
494+
}
495+
} catch (error) {
496+
console.error('Error deleting comment:', error)
497+
alert('Failed to delete comment')
498+
}
499+
}
500+
}, [isAuthenticated, user])
501+
433502
const handleSortChange = (newSortType: SortType) => {
434503
if (sortType === newSortType) {
435504
// Toggle direction if clicking the same sort type
@@ -547,6 +616,8 @@ export function ThreadClientPage({ article }: ThreadClientPageProps) {
547616
isOriginalPoster={isOriginalPoster}
548617
getCommentVoteState={getCommentVoteState}
549618
getCommentChildren={getCommentChildren}
619+
onDeleteComment={handleDeleteComment}
620+
currentUserId={user?.$id}
550621
/>
551622
))}
552623
</div>

app/api/comments/[id]/route.ts

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { NextRequest, NextResponse } from 'next/server'
2+
import { Client, Databases, Account } from 'node-appwrite'
3+
4+
// Initialize Appwrite clients for server-side operations
5+
const apiKeyClient = new Client()
6+
.setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || '')
7+
.setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '')
8+
.setKey(process.env.APPWRITE_API_KEY || '')
9+
10+
const jwtClient = new Client()
11+
.setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || '')
12+
.setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '')
13+
14+
const databases = new Databases(apiKeyClient)
15+
const account = new Account(jwtClient)
16+
17+
// Database and collection IDs
18+
const DATABASE_ID = process.env.APPWRITE_DATABASE_ID || ''
19+
const COMMENTS_COLLECTION_ID = process.env.APPWRITE_COMMENTS_COLLECTION_ID || ''
20+
const POSTS_COLLECTION_ID = process.env.APPWRITE_POSTS_COLLECTION_ID || ''
21+
22+
export async function DELETE(
23+
request: NextRequest,
24+
{ params }: { params: { id: string } }
25+
) {
26+
try {
27+
const commentId = params.id
28+
29+
if (!commentId) {
30+
return NextResponse.json(
31+
{ error: 'Comment ID is required' },
32+
{ status: 400 }
33+
)
34+
}
35+
36+
// Get and validate JWT token from Authorization header
37+
const authHeader = request.headers.get('authorization')
38+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
39+
return NextResponse.json(
40+
{ error: 'Missing or invalid authorization header' },
41+
{ status: 401 }
42+
)
43+
}
44+
45+
const jwt = authHeader.replace('Bearer ', '')
46+
47+
// Use the JWT client to validate the token and get user information
48+
let user
49+
try {
50+
jwtClient.setJWT(jwt)
51+
user = await account.get()
52+
} catch (error) {
53+
return NextResponse.json(
54+
{ error: 'Invalid or expired JWT token' },
55+
{ status: 401 }
56+
)
57+
}
58+
59+
if (!user.$id) {
60+
return NextResponse.json(
61+
{ error: 'Invalid user information' },
62+
{ status: 401 }
63+
)
64+
}
65+
66+
// Get the comment to check ownership and get postId
67+
let comment
68+
try {
69+
comment = await databases.getDocument(
70+
DATABASE_ID,
71+
COMMENTS_COLLECTION_ID,
72+
commentId
73+
)
74+
} catch (error) {
75+
return NextResponse.json(
76+
{ error: 'Comment not found' },
77+
{ status: 404 }
78+
)
79+
}
80+
81+
// Check if the user owns this comment
82+
if (comment.userId !== user.$id) {
83+
return NextResponse.json(
84+
{ error: 'You can only delete your own comments' },
85+
{ status: 403 }
86+
)
87+
}
88+
89+
// Delete the comment
90+
try {
91+
await databases.deleteDocument(
92+
DATABASE_ID,
93+
COMMENTS_COLLECTION_ID,
94+
commentId
95+
)
96+
} catch (error) {
97+
console.error('Error deleting comment:', error)
98+
return NextResponse.json(
99+
{ error: 'Failed to delete comment' },
100+
{ status: 500 }
101+
)
102+
}
103+
104+
// Decrease the comment count on the post
105+
try {
106+
await databases.incrementDocumentAttribute(
107+
DATABASE_ID,
108+
POSTS_COLLECTION_ID,
109+
comment.postId,
110+
'countComments',
111+
-1, // decrement by 1
112+
0 // minimum limit of 0
113+
)
114+
} catch (updateError) {
115+
console.error('Error updating comment count for post:', comment.postId, updateError)
116+
// Don't fail the deletion if the count update fails
117+
// The comment was deleted successfully, just the count couldn't be updated
118+
}
119+
120+
return NextResponse.json({
121+
success: true,
122+
message: 'Comment deleted successfully'
123+
})
124+
125+
} catch (error) {
126+
console.error('Error in comment deletion:', error)
127+
return NextResponse.json(
128+
{ error: 'Internal server error' },
129+
{ status: 500 }
130+
)
131+
}
132+
}

0 commit comments

Comments
 (0)