62 lines
2.1 KiB
TypeScript
62 lines
2.1 KiB
TypeScript
import type { CommentItem } from "@/lib/comment-model";
|
|
import { relativeTime } from "@/lib/feed-model";
|
|
|
|
export interface CommentsPanelProps {
|
|
comments: CommentItem[];
|
|
loading?: boolean;
|
|
/** Error text naming the action that failed. The list stays shown. */
|
|
error?: string | null;
|
|
value: string;
|
|
busy?: boolean;
|
|
onChange?: (v: string) => void;
|
|
onSubmit?: () => void;
|
|
onRetry?: () => void;
|
|
now?: number;
|
|
}
|
|
|
|
export function CommentsPanel(p: CommentsPanelProps) {
|
|
const empty = !p.value.trim();
|
|
return (
|
|
<div className="comments" data-testid="comments-panel">
|
|
{p.loading ? <span className="spinner" role="status" aria-label="Loading comments" /> : null}
|
|
{!p.loading && p.comments.length === 0 && !p.error ? (
|
|
<p className="comments-empty">No comments yet.</p>
|
|
) : null}
|
|
<ul className="comment-list">
|
|
{p.comments.map((c) => (
|
|
<li key={c.key} className={c.removed ? "comment removed" : "comment"} data-testid="comment">
|
|
<div className="comment-head">
|
|
<strong>{c.authorName}</strong>
|
|
<time dateTime={c.createdAt ?? undefined}>{relativeTime(c.createdAt, p.now)}</time>
|
|
</div>
|
|
{c.removed ? <p className="comment-removed">This comment was removed.</p> : <p>{c.content}</p>}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
{p.error ? (
|
|
<p className="comment-error" role="alert">
|
|
{p.error}{" "}
|
|
{p.onRetry ? <button type="button" className="link-btn" onClick={p.onRetry}>Try again</button> : null}
|
|
</p>
|
|
) : null}
|
|
<form
|
|
className="comment-form"
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
if (!empty && !p.busy) p.onSubmit?.();
|
|
}}
|
|
>
|
|
<input
|
|
type="text"
|
|
aria-label="Add a comment"
|
|
placeholder="Add a comment"
|
|
value={p.value}
|
|
maxLength={5000}
|
|
onChange={(e) => p.onChange?.(e.target.value)}
|
|
/>
|
|
<button type="submit" disabled={empty || p.busy}>{p.busy ? "Posting" : "Post"}</button>
|
|
</form>
|
|
</div>
|
|
);
|
|
}
|