146 lines
4.6 KiB
TypeScript
146 lines
4.6 KiB
TypeScript
|
|
'use client'
|
||
|
|
|
||
|
|
import { useState } from 'react'
|
||
|
|
import { Button } from '@/components/ui/button'
|
||
|
|
import { Textarea } from '@/components/ui/textarea'
|
||
|
|
import { Card, CardContent } from '@/components/ui/card'
|
||
|
|
import { Badge } from '@/components/ui/badge'
|
||
|
|
import { MessageSquare, Lock, Send, User } from 'lucide-react'
|
||
|
|
import { cn } from '@/lib/utils'
|
||
|
|
|
||
|
|
interface Comment {
|
||
|
|
id: string
|
||
|
|
author: string
|
||
|
|
content: string
|
||
|
|
createdAt: string
|
||
|
|
}
|
||
|
|
|
||
|
|
interface DiscussionThreadProps {
|
||
|
|
comments: Comment[]
|
||
|
|
onAddComment?: (content: string) => void
|
||
|
|
isLocked?: boolean
|
||
|
|
maxLength?: number
|
||
|
|
isSubmitting?: boolean
|
||
|
|
}
|
||
|
|
|
||
|
|
function formatRelativeTime(dateStr: string): string {
|
||
|
|
const date = new Date(dateStr)
|
||
|
|
const now = new Date()
|
||
|
|
const diffMs = now.getTime() - date.getTime()
|
||
|
|
const diffMinutes = Math.floor(diffMs / 60000)
|
||
|
|
const diffHours = Math.floor(diffMinutes / 60)
|
||
|
|
const diffDays = Math.floor(diffHours / 24)
|
||
|
|
|
||
|
|
if (diffMinutes < 1) return 'just now'
|
||
|
|
if (diffMinutes < 60) return `${diffMinutes}m ago`
|
||
|
|
if (diffHours < 24) return `${diffHours}h ago`
|
||
|
|
if (diffDays < 7) return `${diffDays}d ago`
|
||
|
|
return date.toLocaleDateString()
|
||
|
|
}
|
||
|
|
|
||
|
|
export function DiscussionThread({
|
||
|
|
comments,
|
||
|
|
onAddComment,
|
||
|
|
isLocked = false,
|
||
|
|
maxLength = 2000,
|
||
|
|
isSubmitting = false,
|
||
|
|
}: DiscussionThreadProps) {
|
||
|
|
const [newComment, setNewComment] = useState('')
|
||
|
|
|
||
|
|
const handleSubmit = () => {
|
||
|
|
const trimmed = newComment.trim()
|
||
|
|
if (!trimmed || !onAddComment) return
|
||
|
|
onAddComment(trimmed)
|
||
|
|
setNewComment('')
|
||
|
|
}
|
||
|
|
|
||
|
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||
|
|
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
||
|
|
e.preventDefault()
|
||
|
|
handleSubmit()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="space-y-4">
|
||
|
|
{/* Locked banner */}
|
||
|
|
{isLocked && (
|
||
|
|
<div className="flex items-center gap-2 rounded-lg bg-muted p-3 text-sm text-muted-foreground">
|
||
|
|
<Lock className="h-4 w-4 shrink-0" />
|
||
|
|
Discussion is closed. No new comments can be added.
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* Comments list */}
|
||
|
|
{comments.length === 0 ? (
|
||
|
|
<div className="flex flex-col items-center justify-center py-8 text-center">
|
||
|
|
<MessageSquare className="h-10 w-10 text-muted-foreground/50" />
|
||
|
|
<p className="mt-2 text-sm font-medium text-muted-foreground">No comments yet</p>
|
||
|
|
<p className="text-xs text-muted-foreground">
|
||
|
|
Be the first to share your thoughts on this project.
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
) : (
|
||
|
|
<div className="space-y-3">
|
||
|
|
{comments.map((comment) => (
|
||
|
|
<Card key={comment.id}>
|
||
|
|
<CardContent className="p-4">
|
||
|
|
<div className="flex items-start gap-3">
|
||
|
|
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-muted">
|
||
|
|
<User className="h-4 w-4 text-muted-foreground" />
|
||
|
|
</div>
|
||
|
|
<div className="flex-1 min-w-0">
|
||
|
|
<div className="flex items-center gap-2 flex-wrap">
|
||
|
|
<span className="text-sm font-medium">{comment.author}</span>
|
||
|
|
<span className="text-xs text-muted-foreground">
|
||
|
|
{formatRelativeTime(comment.createdAt)}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
<p className="mt-1 text-sm whitespace-pre-wrap break-words">
|
||
|
|
{comment.content}
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* Add comment form */}
|
||
|
|
{!isLocked && onAddComment && (
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Textarea
|
||
|
|
placeholder="Add a comment... (Ctrl+Enter to send)"
|
||
|
|
value={newComment}
|
||
|
|
onChange={(e) => setNewComment(e.target.value.slice(0, maxLength))}
|
||
|
|
onKeyDown={handleKeyDown}
|
||
|
|
rows={3}
|
||
|
|
disabled={isSubmitting}
|
||
|
|
/>
|
||
|
|
<div className="flex items-center justify-between">
|
||
|
|
<span
|
||
|
|
className={cn(
|
||
|
|
'text-xs',
|
||
|
|
newComment.length > maxLength * 0.9
|
||
|
|
? 'text-destructive'
|
||
|
|
: 'text-muted-foreground'
|
||
|
|
)}
|
||
|
|
>
|
||
|
|
{newComment.length}/{maxLength}
|
||
|
|
</span>
|
||
|
|
<Button
|
||
|
|
size="sm"
|
||
|
|
onClick={handleSubmit}
|
||
|
|
disabled={!newComment.trim() || isSubmitting}
|
||
|
|
>
|
||
|
|
<Send className="mr-2 h-4 w-4" />
|
||
|
|
{isSubmitting ? 'Sending...' : 'Comment'}
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
}
|