Redesign comments to GitHub/Gitea style
All checks were successful
Build & Push / TypeScript Check (client) (push) Successful in 14s
Build & Push / Build Server (push) Successful in 36s
Build & Push / Build Client (push) Successful in 33s

- Each comment is a bordered card with a distinct header bar (author name,
  clickable relative timestamp, hover-to-reveal delete) and a body section
- Subtle spine line connects comments in the avatar column
- Composer matches card style: same header bar for Write/Preview tabs,
  transparent textarea inside, submit row with border-top
- Comment timestamps default to relative, click to toggle absolute
  (mirrors sidebar date toggle pattern)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Josh Wright
2026-03-31 12:38:36 -04:00
parent 8bea999b93
commit 2a6090e473

View File

@@ -78,6 +78,7 @@ export default function TicketDetail() {
const [editingSeverity, setEditingSeverity] = useState(false) const [editingSeverity, setEditingSeverity] = useState(false)
const [editingAssignee, setEditingAssignee] = useState(false) const [editingAssignee, setEditingAssignee] = useState(false)
const [expandedDates, setExpandedDates] = useState<Set<string>>(new Set()) const [expandedDates, setExpandedDates] = useState<Set<string>>(new Set())
const [expandedCommentDates, setExpandedCommentDates] = useState<Set<string>>(new Set())
const toggleDate = (key: string) => const toggleDate = (key: string) =>
setExpandedDates((prev) => { setExpandedDates((prev) => {
@@ -86,6 +87,13 @@ export default function TicketDetail() {
return next return next
}) })
const toggleCommentDate = (id: string) =>
setExpandedCommentDates((prev) => {
const next = new Set(prev)
next.has(id) ? next.delete(id) : next.add(id)
return next
})
const isAdmin = authUser?.role === 'ADMIN' const isAdmin = authUser?.role === 'ADMIN'
useEffect(() => { useEffect(() => {
@@ -313,107 +321,110 @@ export default function TicketDetail() {
{/* ── Comments ── */} {/* ── Comments ── */}
{tab === 'comments' && ( {tab === 'comments' && (
<div> <div className="p-6 space-y-4">
{ticket.comments && ticket.comments.length > 0 ? ( {ticket.comments && ticket.comments.length > 0 ? (
<div className="divide-y divide-gray-800"> ticket.comments.map((comment, i) => (
{ticket.comments.map((comment) => ( <div key={comment.id} className="flex gap-3 group">
<div key={comment.id} className="p-6 group"> {/* Avatar + spine */}
<div className="flex items-start gap-3"> <div className="flex flex-col items-center">
<Avatar name={comment.author.displayName} size="md" /> <Avatar name={comment.author.displayName} size="md" />
<div className="flex-1 min-w-0"> {i < (ticket.comments!.length - 1) && (
<div className="flex items-center justify-between mb-2"> <div className="flex-1 w-px bg-gray-800 mt-2" />
<div className="flex items-center gap-2"> )}
<span className="text-sm font-semibold text-gray-200"> </div>
{comment.author.displayName} {/* Card */}
</span> <div className="flex-1 min-w-0 border border-gray-700 rounded-lg overflow-hidden">
<span className="text-xs text-gray-500"> <div className="flex items-center justify-between px-4 py-2 bg-gray-800/60 border-b border-gray-700">
{format(new Date(comment.createdAt), 'MMM d, yyyy · HH:mm')} <div className="flex items-center gap-2">
</span> <span className="text-sm font-semibold text-gray-200">
</div> {comment.author.displayName}
{(comment.authorId === authUser?.id || isAdmin) && ( </span>
<button <button
onClick={() => deleteComment(comment.id)} onClick={() => toggleCommentDate(comment.id)}
className="opacity-0 group-hover:opacity-100 text-gray-600 hover:text-red-400 transition-all" className="text-xs text-gray-500 hover:text-gray-300 transition-colors"
> >
<Trash2 size={13} /> {expandedCommentDates.has(comment.id)
</button> ? format(new Date(comment.createdAt), 'MMM d, yyyy HH:mm')
)} : formatDistanceToNow(new Date(comment.createdAt), { addSuffix: true })}
</div> </button>
<div className="prose text-sm text-gray-300">
<ReactMarkdown remarkPlugins={[remarkGfm]}>
{comment.body}
</ReactMarkdown>
</div>
</div> </div>
{(comment.authorId === authUser?.id || isAdmin) && (
<button
onClick={() => deleteComment(comment.id)}
className="opacity-0 group-hover:opacity-100 text-gray-600 hover:text-red-400 transition-all"
>
<Trash2 size={13} />
</button>
)}
</div>
<div className="px-4 py-3 prose prose-sm prose-invert text-gray-300 text-sm max-w-none">
<ReactMarkdown remarkPlugins={[remarkGfm]}>
{comment.body}
</ReactMarkdown>
</div> </div>
</div> </div>
))} </div>
</div> ))
) : ( ) : (
<div className="py-16 text-center text-sm text-gray-600"> <div className="py-12 text-center text-sm text-gray-600">
No comments yet No comments yet
</div> </div>
)} )}
{/* Comment composer */} {/* Composer */}
<div className="border-t border-gray-800 p-6"> <div className="flex gap-3">
<div className="flex gap-3"> <Avatar name={authUser?.displayName ?? '?'} size="md" />
<Avatar name={authUser?.displayName ?? '?'} size="md" /> <div className="flex-1 border border-gray-700 rounded-lg overflow-hidden">
<div className="flex-1"> <div className="flex gap-4 px-4 bg-gray-800/60 border-b border-gray-700">
<div className="flex gap-4 mb-2 border-b border-gray-800"> {(['Write', 'Preview'] as const).map((label) => (
{(['Write', 'Preview'] as const).map((label) => ( <button
<button key={label}
key={label} onClick={() => setPreview(label === 'Preview')}
onClick={() => setPreview(label === 'Preview')} className={`text-xs py-2 border-b-2 -mb-px transition-colors ${
className={`text-xs pb-2 border-b-2 -mb-px transition-colors ${ (label === 'Preview') === preview
(label === 'Preview') === preview ? 'border-blue-500 text-blue-400'
? 'border-blue-500 text-blue-400' : 'border-transparent text-gray-500 hover:text-gray-300'
: 'border-transparent text-gray-500 hover:text-gray-300' }`}
}`} >
> {label}
{label} </button>
</button> ))}
))}
</div>
<form onSubmit={submitComment}>
{preview ? (
<div className="prose text-sm text-gray-300 min-h-[80px] mb-3 px-1">
{commentBody.trim()
? <ReactMarkdown remarkPlugins={[remarkGfm]}>{commentBody}</ReactMarkdown>
: <span className="text-gray-600 italic">Nothing to preview</span>
}
</div>
) : (
<textarea
value={commentBody}
onChange={(e) => setCommentBody(e.target.value)}
placeholder="Leave a comment… Markdown supported"
rows={4}
className="w-full bg-gray-800 border border-gray-700 text-gray-100 placeholder-gray-600 rounded-lg px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none mb-3"
onKeyDown={(e) => {
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
e.preventDefault()
submitComment(e as unknown as React.FormEvent)
}
}}
/>
)}
<div className="flex justify-between items-center">
<span className="text-xs text-gray-600">
Markdown supported · Ctrl+Enter to submit
</span>
<button
type="submit"
disabled={submittingComment || !commentBody.trim()}
className="flex items-center gap-2 px-4 py-1.5 bg-blue-600 text-white text-sm rounded-lg hover:bg-blue-700 disabled:opacity-50 transition-colors"
>
<Send size={13} />
Comment
</button>
</div>
</form>
</div> </div>
<form onSubmit={submitComment} className="p-3">
{preview ? (
<div className="prose prose-sm prose-invert text-gray-300 min-h-[80px] mb-3 px-1 max-w-none">
{commentBody.trim()
? <ReactMarkdown remarkPlugins={[remarkGfm]}>{commentBody}</ReactMarkdown>
: <span className="text-gray-600 italic">Nothing to preview</span>
}
</div>
) : (
<textarea
value={commentBody}
onChange={(e) => setCommentBody(e.target.value)}
placeholder="Leave a comment… Markdown supported"
rows={4}
className="w-full bg-transparent text-gray-100 placeholder-gray-600 text-sm focus:outline-none resize-none mb-3"
onKeyDown={(e) => {
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
e.preventDefault()
submitComment(e as unknown as React.FormEvent)
}
}}
/>
)}
<div className="flex justify-between items-center border-t border-gray-700 pt-2">
<span className="text-xs text-gray-600">Markdown · Ctrl+Enter</span>
<button
type="submit"
disabled={submittingComment || !commentBody.trim()}
className="flex items-center gap-2 px-4 py-1.5 bg-blue-600 text-white text-sm rounded-lg hover:bg-blue-700 disabled:opacity-50 transition-colors"
>
<Send size={13} />
Comment
</button>
</div>
</form>
</div> </div>
</div> </div>
</div> </div>