Offer: Senior Backend Engineer
Candidate Daniel Costa · Engineering · 3 comments
Yuki Tanaka
Offer drafted at €78,000 base with a 10% bonus. That is mid-band for Senior Engineer in Madrid.Mon 14:05
Ahmed Raza
Looks right. Can we add the €1,500 home-office budget we gave the last two hires?Mon 15:20
Chloe Martin
Finance approved the headcount. Start date 2 November keeps it inside Q4 budget.Tue 09:12
Enter sends the message. Shift+Enter adds a new line.
comment-thread.tsx
"use client";
import { useState } from "react";
import { LuEye, LuThumbsUp } from "react-icons/lu";
import { Avatar, Badge, Button, Composer, MessageBubble, Reaction } from "@nexera-ui/react";
interface Comment {
id: string;
name: string;
initials: string;
time: string;
text: string;
likes: number;
replies: Comment[];
}
const THREAD: Comment[] = [
{
id: "c1",
name: "Yuki Tanaka",
initials: "YT",
time: "Mon 14:05",
likes: 2,
text: "Offer drafted at €78,000 base with a 10% bonus. That is mid-band for Senior Engineer in Madrid.",
replies: [
{
id: "c1r1",
name: "Ahmed Raza",
initials: "AR",
time: "Mon 15:20",
likes: 1,
replies: [],
text: "Looks right. Can we add the €1,500 home-office budget we gave the last two hires?",
},
],
},
{
id: "c2",
name: "Chloe Martin",
initials: "CM",
time: "Tue 09:12",
likes: 0,
replies: [],
text: "Finance approved the headcount. Start date 2 November keeps it inside Q4 budget.",
},
];
function Thread({
comment,
onReply,
isReply = false,
}: {
comment: Comment;
onReply: (c: Comment) => void;
isReply?: boolean;
}) {
const [liked, setLiked] = useState(false);
const [seen, setSeen] = useState(false);
return (
<li className={`grid gap-1.5 ${isReply ? "ms-10 border-s-2 border-subtle ps-3" : ""}`}>
<MessageBubble
name={comment.name}
time={comment.time}
avatar={<Avatar size="md" initials={comment.initials} isDecorative />}
>
{comment.text}
</MessageBubble>
<div className="flex flex-wrap items-center gap-1 ps-10">
<div
role="group"
aria-label={`Reactions to ${comment.name}'s comment`}
className="flex gap-1"
>
<Reaction
icon={<LuThumbsUp />}
label="Thumbs up"
count={comment.likes + (liked ? 1 : 0)}
isSelected={liked}
onChange={setLiked}
/>
<Reaction
icon={<LuEye />}
label="Seen"
count={seen ? 1 : 0}
isSelected={seen}
onChange={setSeen}
/>
</div>
<Button
variant="ghost"
size="sm"
onPress={() => {
onReply(comment);
}}
>
Reply
</Button>
</div>
{comment.replies.length > 0 ? (
<ul className="m-0 grid list-none gap-3 p-0">
{comment.replies.map((r) => (
<Thread key={r.id} comment={r} onReply={onReply} isReply />
))}
</ul>
) : null}
</li>
);
}
/** Comment thread: a team discusses a job offer, with nested replies, reactions and a composer that can reply. */
export default function CommentThread() {
const [thread, setThread] = useState(THREAD);
const [replyTo, setReplyTo] = useState<Comment | null>(null);
const send = (text: string) => {
const mine: Comment = {
id: `n${total}`,
name: "Sara Malik",
initials: "SM",
time: "Now",
text,
likes: 0,
replies: [],
};
const parentId = replyTo
? (thread.find((c) => c.id === replyTo.id || c.replies.some((r) => r.id === replyTo.id))
?.id ?? null)
: null;
setThread((t) =>
parentId
? t.map((c) => (c.id === parentId ? { ...c, replies: [...c.replies, mine] } : c))
: [...t, mine],
);
setReplyTo(null);
};
const total = thread.reduce((n, c) => n + 1 + c.replies.length, 0);
return (
<div className="@container bg-page px-4 py-8 @3xl:px-8">
<section
aria-labelledby="thread-title"
className="mx-auto grid w-full max-w-2xl gap-4 rounded-xl border border-default bg-surface p-4 shadow-sm @3xl:p-6"
>
<header className="flex flex-wrap items-start justify-between gap-3">
<div className="grid gap-1">
<h2 id="thread-title" className="text-heading-h4 text-primary">
Offer: Senior Backend Engineer
</h2>
<p className="text-body-small text-secondary">
Candidate Daniel Costa · Engineering · {total} comments
</p>
</div>
<Badge tone="warning" label="Awaiting approval" />
</header>
<ul
aria-label="Comments"
className="m-0 grid list-none gap-5 border-t border-default p-0 pt-4"
>
{thread.map((c) => (
<Thread key={c.id} comment={c} onReply={setReplyTo} />
))}
</ul>
<div className="overflow-hidden rounded-lg border border-default">
<Composer
aria-label="Add a comment"
placeholder="Add a comment"
onSend={send}
{...(replyTo
? {
replyTo: replyTo.text,
replyAuthor: replyTo.name,
onCancelReply: () => {
setReplyTo(null);
},
}
: {})}
/>
</div>
</section>
</div>
);
}
More messaging and collaboration blocks
- ChatOne conversation with bubbles, reactions and a composer.
- InboxConversation list beside the open chat.
- Helpdesk assistantAsk HR or support a question and get answers.
- Notification centreUnread notifications in a popover.
- AnnouncementA banner for news that affects everyone.
- Document editorWrite and format text with links and lists.
- Day separators and system notesShow who joined and where each day starts.