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 admins management page

Trezy 6b17eabe c8f86a63

+174
+174
web/src/app/(dashboard)/admins/page.tsx
··· 1 + "use client" 2 + 3 + import { useCallback, useEffect, useState } from "react" 4 + 5 + import { useAuth } from "@/lib/auth-context" 6 + import { addAdmin, deleteAdmin, getAdmins, type AdminSummary } from "@/lib/api" 7 + import { SiteHeader } from "@/components/site-header" 8 + import { Button } from "@/components/ui/button" 9 + import { 10 + Dialog, 11 + DialogClose, 12 + DialogContent, 13 + DialogDescription, 14 + DialogFooter, 15 + DialogHeader, 16 + DialogTitle, 17 + DialogTrigger, 18 + } from "@/components/ui/dialog" 19 + import { Input } from "@/components/ui/input" 20 + import { Label } from "@/components/ui/label" 21 + import { 22 + Table, 23 + TableBody, 24 + TableCell, 25 + TableHead, 26 + TableHeader, 27 + TableRow, 28 + } from "@/components/ui/table" 29 + 30 + export default function AdminsPage() { 31 + const { token } = useAuth() 32 + const [admins, setAdmins] = useState<AdminSummary[]>([]) 33 + const [error, setError] = useState<string | null>(null) 34 + 35 + const load = useCallback(() => { 36 + if (!token) return 37 + getAdmins(token).then(setAdmins).catch((e) => setError(e.message)) 38 + }, [token]) 39 + 40 + useEffect(() => { 41 + load() 42 + }, [load]) 43 + 44 + async function handleDelete(id: string) { 45 + if (!token) return 46 + try { 47 + await deleteAdmin(token, id) 48 + load() 49 + } catch (e: unknown) { 50 + setError(e instanceof Error ? e.message : String(e)) 51 + } 52 + } 53 + 54 + return ( 55 + <> 56 + <SiteHeader title="Admins" /> 57 + <div className="flex flex-1 flex-col gap-4 p-4 md:p-6"> 58 + {error && <p className="text-destructive text-sm">{error}</p>} 59 + 60 + <div className="flex items-center justify-between"> 61 + <h2 className="text-lg font-semibold">Admin Users</h2> 62 + <AddAdminDialog token={token!} onSuccess={load} /> 63 + </div> 64 + 65 + <div className="rounded-lg border"> 66 + <Table> 67 + <TableHeader> 68 + <TableRow> 69 + <TableHead>DID</TableHead> 70 + <TableHead>Created</TableHead> 71 + <TableHead>Last Used</TableHead> 72 + <TableHead className="text-right">Actions</TableHead> 73 + </TableRow> 74 + </TableHeader> 75 + <TableBody> 76 + {admins.length === 0 && ( 77 + <TableRow> 78 + <TableCell 79 + colSpan={4} 80 + className="text-muted-foreground text-center" 81 + > 82 + No admins yet. 83 + </TableCell> 84 + </TableRow> 85 + )} 86 + {admins.map((admin) => ( 87 + <TableRow key={admin.id}> 88 + <TableCell className="font-mono text-sm"> 89 + {admin.did} 90 + </TableCell> 91 + <TableCell> 92 + {new Date(admin.created_at).toLocaleString()} 93 + </TableCell> 94 + <TableCell> 95 + {admin.last_used_at 96 + ? new Date(admin.last_used_at).toLocaleString() 97 + : "Never"} 98 + </TableCell> 99 + <TableCell className="text-right"> 100 + <Button 101 + variant="destructive" 102 + size="sm" 103 + onClick={() => handleDelete(admin.id)} 104 + > 105 + Delete 106 + </Button> 107 + </TableCell> 108 + </TableRow> 109 + ))} 110 + </TableBody> 111 + </Table> 112 + </div> 113 + </div> 114 + </> 115 + ) 116 + } 117 + 118 + function AddAdminDialog({ 119 + token, 120 + onSuccess, 121 + }: { 122 + token: string 123 + onSuccess: () => void 124 + }) { 125 + const [did, setDid] = useState("") 126 + const [error, setError] = useState<string | null>(null) 127 + const [open, setOpen] = useState(false) 128 + 129 + async function handleAdd() { 130 + setError(null) 131 + try { 132 + await addAdmin(token, { did }) 133 + setDid("") 134 + setOpen(false) 135 + onSuccess() 136 + } catch (e: unknown) { 137 + setError(e instanceof Error ? e.message : String(e)) 138 + } 139 + } 140 + 141 + return ( 142 + <Dialog open={open} onOpenChange={setOpen}> 143 + <DialogTrigger asChild> 144 + <Button>Add Admin</Button> 145 + </DialogTrigger> 146 + <DialogContent> 147 + <DialogHeader> 148 + <DialogTitle>Add Admin</DialogTitle> 149 + <DialogDescription> 150 + Add a new admin by their DID. 151 + </DialogDescription> 152 + </DialogHeader> 153 + <div className="flex flex-col gap-4"> 154 + {error && <p className="text-destructive text-sm">{error}</p>} 155 + <div className="flex flex-col gap-2"> 156 + <Label htmlFor="admin-did">DID</Label> 157 + <Input 158 + id="admin-did" 159 + value={did} 160 + onChange={(e) => setDid(e.target.value)} 161 + placeholder="did:plc:..." 162 + /> 163 + </div> 164 + </div> 165 + <DialogFooter> 166 + <DialogClose asChild> 167 + <Button variant="outline">Cancel</Button> 168 + </DialogClose> 169 + <Button onClick={handleAdd}>Add</Button> 170 + </DialogFooter> 171 + </DialogContent> 172 + </Dialog> 173 + ) 174 + }