learn and share notes on atproto (wip) 馃 malfestio.stormlightlabs.org/
readability solid axum atproto srs
at main 120 lines 3.9 kB view raw
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}