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,22 +321,32 @@ 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>
{/* Card */}
<div className="flex-1 min-w-0 border border-gray-700 rounded-lg overflow-hidden">
<div className="flex items-center justify-between px-4 py-2 bg-gray-800/60 border-b border-gray-700">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-sm font-semibold text-gray-200"> <span className="text-sm font-semibold text-gray-200">
{comment.author.displayName} {comment.author.displayName}
</span> </span>
<span className="text-xs text-gray-500"> <button
{format(new Date(comment.createdAt), 'MMM d, yyyy · HH:mm')} onClick={() => toggleCommentDate(comment.id)}
</span> className="text-xs text-gray-500 hover:text-gray-300 transition-colors"
>
{expandedCommentDates.has(comment.id)
? format(new Date(comment.createdAt), 'MMM d, yyyy HH:mm')
: formatDistanceToNow(new Date(comment.createdAt), { addSuffix: true })}
</button>
</div> </div>
{(comment.authorId === authUser?.id || isAdmin) && ( {(comment.authorId === authUser?.id || isAdmin) && (
<button <button
@@ -339,33 +357,30 @@ export default function TicketDetail() {
</button> </button>
)} )}
</div> </div>
<div className="prose text-sm text-gray-300"> <div className="px-4 py-3 prose prose-sm prose-invert text-gray-300 text-sm max-w-none">
<ReactMarkdown remarkPlugins={[remarkGfm]}> <ReactMarkdown remarkPlugins={[remarkGfm]}>
{comment.body} {comment.body}
</ReactMarkdown> </ReactMarkdown>
</div> </div>
</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"> <div className="flex-1 border border-gray-700 rounded-lg overflow-hidden">
<div className="flex gap-4 mb-2 border-b border-gray-800"> <div className="flex gap-4 px-4 bg-gray-800/60 border-b border-gray-700">
{(['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 pb-2 border-b-2 -mb-px transition-colors ${ className={`text-xs py-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'
@@ -375,10 +390,9 @@ export default function TicketDetail() {
</button> </button>
))} ))}
</div> </div>
<form onSubmit={submitComment} className="p-3">
<form onSubmit={submitComment}>
{preview ? ( {preview ? (
<div className="prose text-sm text-gray-300 min-h-[80px] mb-3 px-1"> <div className="prose prose-sm prose-invert text-gray-300 min-h-[80px] mb-3 px-1 max-w-none">
{commentBody.trim() {commentBody.trim()
? <ReactMarkdown remarkPlugins={[remarkGfm]}>{commentBody}</ReactMarkdown> ? <ReactMarkdown remarkPlugins={[remarkGfm]}>{commentBody}</ReactMarkdown>
: <span className="text-gray-600 italic">Nothing to preview</span> : <span className="text-gray-600 italic">Nothing to preview</span>
@@ -390,7 +404,7 @@ export default function TicketDetail() {
onChange={(e) => setCommentBody(e.target.value)} onChange={(e) => setCommentBody(e.target.value)}
placeholder="Leave a comment… Markdown supported" placeholder="Leave a comment… Markdown supported"
rows={4} 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" className="w-full bg-transparent text-gray-100 placeholder-gray-600 text-sm focus:outline-none resize-none mb-3"
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
e.preventDefault() e.preventDefault()
@@ -399,10 +413,8 @@ export default function TicketDetail() {
}} }}
/> />
)} )}
<div className="flex justify-between items-center"> <div className="flex justify-between items-center border-t border-gray-700 pt-2">
<span className="text-xs text-gray-600"> <span className="text-xs text-gray-600">Markdown · Ctrl+Enter</span>
Markdown supported · Ctrl+Enter to submit
</span>
<button <button
type="submit" type="submit"
disabled={submittingComment || !commentBody.trim()} disabled={submittingComment || !commentBody.trim()}
@@ -416,7 +428,6 @@ export default function TicketDetail() {
</div> </div>
</div> </div>
</div> </div>
</div>
)} )}
{/* ── Audit Log ── */} {/* ── Audit Log ── */}