A lexicon-driven AppView for ATProto. happyview.dev
backfill firehose jetstream atproto appview oauth lexicon
8
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add network lexicons page

Trezy 373f9d8b e7d9a653

+199
+199
web/src/app/(dashboard)/network-lexicons/page.tsx
··· 1 + "use client" 2 + 3 + import { useCallback, useEffect, useState } from "react" 4 + 5 + import { useAuth } from "@/lib/auth-context" 6 + import { 7 + addNetworkLexicon, 8 + deleteNetworkLexicon, 9 + getNetworkLexicons, 10 + type NetworkLexiconSummary, 11 + } from "@/lib/api" 12 + import { SiteHeader } from "@/components/site-header" 13 + import { Button } from "@/components/ui/button" 14 + import { 15 + Dialog, 16 + DialogClose, 17 + DialogContent, 18 + DialogDescription, 19 + DialogFooter, 20 + DialogHeader, 21 + DialogTitle, 22 + DialogTrigger, 23 + } from "@/components/ui/dialog" 24 + import { Input } from "@/components/ui/input" 25 + import { Label } from "@/components/ui/label" 26 + import { 27 + Table, 28 + TableBody, 29 + TableCell, 30 + TableHead, 31 + TableHeader, 32 + TableRow, 33 + } from "@/components/ui/table" 34 + 35 + export default function NetworkLexiconsPage() { 36 + const { token } = useAuth() 37 + const [items, setItems] = useState<NetworkLexiconSummary[]>([]) 38 + const [error, setError] = useState<string | null>(null) 39 + 40 + const load = useCallback(() => { 41 + if (!token) return 42 + getNetworkLexicons(token).then(setItems).catch((e) => setError(e.message)) 43 + }, [token]) 44 + 45 + useEffect(() => { 46 + load() 47 + }, [load]) 48 + 49 + async function handleDelete(nsid: string) { 50 + if (!token) return 51 + try { 52 + await deleteNetworkLexicon(token, nsid) 53 + load() 54 + } catch (e: unknown) { 55 + setError(e instanceof Error ? e.message : String(e)) 56 + } 57 + } 58 + 59 + return ( 60 + <> 61 + <SiteHeader title="Network Lexicons" /> 62 + <div className="flex flex-1 flex-col gap-4 p-4 md:p-6"> 63 + {error && <p className="text-destructive text-sm">{error}</p>} 64 + 65 + <div className="flex items-center justify-between"> 66 + <h2 className="text-lg font-semibold">Tracked Network Lexicons</h2> 67 + <AddDialog token={token!} onSuccess={load} /> 68 + </div> 69 + 70 + <div className="rounded-lg border"> 71 + <Table> 72 + <TableHeader> 73 + <TableRow> 74 + <TableHead>NSID</TableHead> 75 + <TableHead>Authority DID</TableHead> 76 + <TableHead>Target Collection</TableHead> 77 + <TableHead>Last Fetched</TableHead> 78 + <TableHead className="text-right">Actions</TableHead> 79 + </TableRow> 80 + </TableHeader> 81 + <TableBody> 82 + {items.length === 0 && ( 83 + <TableRow> 84 + <TableCell 85 + colSpan={5} 86 + className="text-muted-foreground text-center" 87 + > 88 + No network lexicons tracked yet. 89 + </TableCell> 90 + </TableRow> 91 + )} 92 + {items.map((item) => ( 93 + <TableRow key={item.nsid}> 94 + <TableCell className="font-mono text-sm"> 95 + {item.nsid} 96 + </TableCell> 97 + <TableCell className="font-mono text-sm"> 98 + {item.authority_did} 99 + </TableCell> 100 + <TableCell className="font-mono text-sm"> 101 + {item.target_collection ?? "--"} 102 + </TableCell> 103 + <TableCell> 104 + {item.last_fetched_at 105 + ? new Date(item.last_fetched_at).toLocaleString() 106 + : "Never"} 107 + </TableCell> 108 + <TableCell className="text-right"> 109 + <Button 110 + variant="destructive" 111 + size="sm" 112 + onClick={() => handleDelete(item.nsid)} 113 + > 114 + Delete 115 + </Button> 116 + </TableCell> 117 + </TableRow> 118 + ))} 119 + </TableBody> 120 + </Table> 121 + </div> 122 + </div> 123 + </> 124 + ) 125 + } 126 + 127 + function AddDialog({ 128 + token, 129 + onSuccess, 130 + }: { 131 + token: string 132 + onSuccess: () => void 133 + }) { 134 + const [nsid, setNsid] = useState("") 135 + const [targetCollection, setTargetCollection] = useState("") 136 + const [error, setError] = useState<string | null>(null) 137 + const [open, setOpen] = useState(false) 138 + 139 + async function handleAdd() { 140 + setError(null) 141 + try { 142 + await addNetworkLexicon(token, { 143 + nsid, 144 + target_collection: targetCollection || undefined, 145 + }) 146 + setNsid("") 147 + setTargetCollection("") 148 + setOpen(false) 149 + onSuccess() 150 + } catch (e: unknown) { 151 + setError(e instanceof Error ? e.message : String(e)) 152 + } 153 + } 154 + 155 + return ( 156 + <Dialog open={open} onOpenChange={setOpen}> 157 + <DialogTrigger asChild> 158 + <Button>Add Network Lexicon</Button> 159 + </DialogTrigger> 160 + <DialogContent> 161 + <DialogHeader> 162 + <DialogTitle>Add Network Lexicon</DialogTitle> 163 + <DialogDescription> 164 + Track a lexicon from the ATProto network by its NSID. 165 + </DialogDescription> 166 + </DialogHeader> 167 + <div className="flex flex-col gap-4"> 168 + {error && <p className="text-destructive text-sm">{error}</p>} 169 + <div className="flex flex-col gap-2"> 170 + <Label htmlFor="nsid">NSID</Label> 171 + <Input 172 + id="nsid" 173 + value={nsid} 174 + onChange={(e) => setNsid(e.target.value)} 175 + placeholder="com.example.record" 176 + /> 177 + </div> 178 + <div className="flex flex-col gap-2"> 179 + <Label htmlFor="nl-target-collection"> 180 + Target Collection (optional) 181 + </Label> 182 + <Input 183 + id="nl-target-collection" 184 + value={targetCollection} 185 + onChange={(e) => setTargetCollection(e.target.value)} 186 + placeholder="com.example.record" 187 + /> 188 + </div> 189 + </div> 190 + <DialogFooter> 191 + <DialogClose asChild> 192 + <Button variant="outline">Cancel</Button> 193 + </DialogClose> 194 + <Button onClick={handleAdd}>Add</Button> 195 + </DialogFooter> 196 + </DialogContent> 197 + </Dialog> 198 + ) 199 + }