learn and share notes on atproto (wip) 馃
malfestio.stormlightlabs.org/
readability
solid
axum
atproto
srs
1import { api } from "$lib/api";
2import type { Comment } from "$lib/model";
3import { authStore } from "$lib/store";
4import { Button } from "$ui/Button";
5import { createResource, createSignal, For, Show } from "solid-js";
6
7type CommentNode = { comment: Comment; children: CommentNode[] };
8
9type CommentSectionProps = { deckId: string };
10
11function buildTree(comments: Comment[]): CommentNode[] {
12 const map = new Map<string, CommentNode>();
13 const roots: CommentNode[] = [];
14 for (const c of comments) {
15 map.set(c.id, { comment: c, children: [] });
16 }
17
18 for (const c of comments) {
19 if (c.parent_id && map.has(c.parent_id)) {
20 map.get(c.parent_id)!.children.push(map.get(c.id)!);
21 } else {
22 roots.push(map.get(c.id)!);
23 }
24 }
25 return roots;
26}
27
28export function CommentSection(props: CommentSectionProps) {
29 const [comments, { refetch }] = createResource(async () => {
30 const res = await api.getComments(props.deckId);
31 if (res.ok) {
32 return (await res.json()) as Comment[];
33 }
34 return [];
35 });
36
37 const [mainComment, setMainComment] = createSignal("");
38 const [replyComment, setReplyComment] = createSignal("");
39 const [replyTo, setReplyTo] = createSignal<string | null>(null);
40
41 const submitComment = async (parentId?: string) => {
42 const content = parentId ? replyComment() : mainComment();
43 if (!content.trim()) return;
44
45 await api.addComment(props.deckId, content, parentId);
46
47 if (parentId) {
48 setReplyComment("");
49 setReplyTo(null);
50 } else {
51 setMainComment("");
52 }
53 refetch();
54 };
55
56 const CommentItem = (node: { node: CommentNode }) => (
57 <div class="border-l-2 border-gray-200 pl-4 my-2">
58 <div class="text-sm font-bold text-gray-600">{node.node.comment.author_did}</div>
59 <div class="my-1">{node.node.comment.content}</div>
60 <div class="text-xs text-gray-500 flex gap-2">
61 <span>{new Date(node.node.comment.created_at).toLocaleString()}</span>
62 <button
63 class="text-blue-500 hover:underline"
64 onClick={() => {
65 setReplyTo(node.node.comment.id);
66 setReplyComment("");
67 }}>
68 Reply
69 </button>
70 </div>
71
72 <Show when={replyTo() === node.node.comment.id}>
73 <div class="mt-2 flex gap-2">
74 <input
75 type="text"
76 class="border rounded p-1 flex-1 text-sm"
77 value={replyComment()}
78 onInput={(e) => setReplyComment(e.currentTarget.value)}
79 placeholder="Write a reply..." />
80 <Button size="sm" onClick={() => submitComment(node.node.comment.id)}>Post</Button>
81 <Button size="sm" variant="ghost" onClick={() => setReplyTo(null)}>Cancel</Button>
82 </div>
83 </Show>
84
85 <For each={node.node.children}>{(child) => <CommentItem node={child} />}</For>
86 </div>
87 );
88
89 return (
90 <div class="mt-8">
91 <h3 class="text-xl font-bold mb-4">Comments</h3>
92
93 <Show when={authStore.user}>
94 <div class="flex gap-2 mb-6">
95 <textarea
96 class="border rounded p-2 flex-1 w-full"
97 rows={2}
98 placeholder="Add a comment..."
99 value={mainComment()}
100 onInput={(e) => setMainComment(e.currentTarget.value)} />
101 <div class="flex flex-col justify-end">
102 <Button onClick={() => submitComment()} disabled={false}>Post</Button>
103 </div>
104 </div>
105 </Show>
106
107 <Show when={comments()} fallback={<div class="animate-pulse">Loading comments...</div>}>
108 {(data) => {
109 const list = (Array.isArray(data) ? data : []) as Comment[];
110 return (
111 <div class="space-y-4">
112 <For each={buildTree(list)}>{(node) => <CommentItem node={node} />}</For>
113 {list.length === 0 && <div class="text-gray-500 italic">No comments yet.</div>}
114 </div>
115 );
116 }}
117 </Show>
118 </div>
119 );
120}