152 lines
5.7 KiB
TypeScript
152 lines
5.7 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import useSWR, { mutate } from 'swr'
|
|
import Link from 'next/link'
|
|
import { useSession } from 'next-auth/react'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Textarea } from '@/components/ui/textarea'
|
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
|
import { formatDistanceToNow } from 'date-fns'
|
|
import { UserBadges } from './ui/user-badges'
|
|
import { toast } from 'sonner'
|
|
import { Trash2 } from 'lucide-react'
|
|
|
|
interface Comment {
|
|
_id: string
|
|
userId: string
|
|
userName: string
|
|
userImage: string
|
|
userVerified?: boolean
|
|
userIsAdmin?: boolean
|
|
userIsBetaTester?: boolean
|
|
userIsBugHunter?: boolean
|
|
text: string
|
|
isHolder: boolean
|
|
createdAt: string
|
|
}
|
|
|
|
const fetcher = (url: string) => fetch(url).then((res) => res.json())
|
|
|
|
export function Comments({ coinId, creatorId }: { coinId: string, creatorId?: string }) {
|
|
const { data: session } = useSession()
|
|
const { data: comments } = useSWR<Comment[]>(`/api/comments?coinId=${coinId}`, fetcher)
|
|
const [text, setText] = useState('')
|
|
const [loading, setLoading] = useState(false)
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
if (!text.trim() || !session) return
|
|
|
|
setLoading(true)
|
|
try {
|
|
await fetch('/api/comments', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ coinId, text }),
|
|
})
|
|
setText('')
|
|
mutate(`/api/comments?coinId=${coinId}`)
|
|
} catch (error) {
|
|
console.error('Failed to post comment', error)
|
|
toast.error('Failed to post comment')
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const handleDelete = async (commentId: string) => {
|
|
try {
|
|
const res = await fetch(`/api/comments?id=${commentId}`, { method: 'DELETE' });
|
|
if (res.ok) {
|
|
mutate(`/api/comments?coinId=${coinId}`)
|
|
toast.success('Comment deleted')
|
|
} else {
|
|
const data = await res.json()
|
|
toast.error(data.error || 'Failed to delete')
|
|
}
|
|
} catch (e) {
|
|
toast.error('Failed to delete comment')
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="flex flex-col gap-6">
|
|
<div className="rounded-xl border border-border bg-card/30 p-4">
|
|
<h3 className="mb-4 font-semibold">Discussion</h3>
|
|
|
|
{session ? (
|
|
<form onSubmit={handleSubmit} className="mb-6 space-y-3">
|
|
<Textarea
|
|
value={text}
|
|
onChange={(e) => setText(e.target.value)}
|
|
placeholder="What do you think about this coin?"
|
|
className="min-h-[80px] bg-muted/30"
|
|
/>
|
|
<div className="flex justify-end">
|
|
<Button type="submit" size="sm" disabled={loading || !text.trim()}>
|
|
{loading ? 'Posting...' : 'Post Comment'}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
) : (
|
|
<div className="mb-6 rounded-lg bg-muted/20 p-4 text-center text-sm text-muted-foreground">
|
|
Please sign in to join the discussion
|
|
</div>
|
|
)}
|
|
|
|
<div className="space-y-4">
|
|
{comments?.map((comment, i) => (
|
|
<div key={`${comment._id}-${i}`} className="flex gap-3 group">
|
|
<Avatar className="h-8 w-8">
|
|
<AvatarImage src={comment.userImage} />
|
|
<AvatarFallback>{comment.userName?.substring(0, 2)}</AvatarFallback>
|
|
</Avatar>
|
|
<div className="flex-1 space-y-1">
|
|
<div className="flex items-center gap-2">
|
|
<Link href={`/u/${comment.userName}`} className="text-sm font-semibold hover:underline">
|
|
{comment.userName}
|
|
</Link>
|
|
<UserBadges
|
|
isAdmin={comment.userIsAdmin}
|
|
isVerified={comment.userVerified}
|
|
isBugHunter={comment.userIsBugHunter}
|
|
isBetaTester={comment.userIsBetaTester}
|
|
onlyShowHighest
|
|
/>
|
|
{creatorId === comment.userId && (
|
|
<span className="rounded bg-blue-500/20 px-1.5 py-0.5 text-[10px] font-bold text-blue-500 border border-blue-500/30">
|
|
CREATOR
|
|
</span>
|
|
)}
|
|
{comment.isHolder && creatorId !== comment.userId && (
|
|
<span className="rounded bg-primary/20 px-1.5 py-0.5 text-[10px] font-medium text-primary">
|
|
HOLDER
|
|
</span>
|
|
)}
|
|
<span className="text-xs text-muted-foreground">
|
|
{formatDistanceToNow(new Date(comment.createdAt), { addSuffix: true })}
|
|
</span>
|
|
|
|
{session && (session.user.id === comment.userId || session.user.id === creatorId || (session.user as any).isAdmin) && (
|
|
<button
|
|
onClick={() => handleDelete(comment._id)}
|
|
className="opacity-0 group-hover:opacity-100 transition-opacity ml-auto text-muted-foreground hover:text-destructive p-1"
|
|
title="Delete comment"
|
|
>
|
|
<Trash2 className="w-3 h-3" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
<p className="text-sm text-foreground/90 whitespace-pre-wrap">{comment.text}</p>
|
|
</div>
|
|
</div>
|
|
))}
|
|
{comments?.length === 0 && (
|
|
<p className="text-center text-sm text-muted-foreground">No comments yet. Be the first!</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|