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.

fix: wrap dynamic page with a server component to fix build

Trezy ded46efa 57ff6b88

+233 -222
+226
web/src/app/(dashboard)/lexicons/[id]/lexicon-detail.tsx
··· 1 + "use client"; 2 + 3 + import { useCallback, useEffect, useState } from "react"; 4 + import { useParams, useRouter } from "next/navigation"; 5 + 6 + import { useAuth } from "@/lib/auth-context"; 7 + import { CodePanels } from "@/components/code-panels"; 8 + import { 9 + deleteLexicon, 10 + deleteNetworkLexicon, 11 + getLexicon, 12 + uploadLexicon, 13 + type LexiconDetail, 14 + } from "@/lib/api"; 15 + import { procedureScript, queryScript } from "@/lib/lua-templates"; 16 + import { useLuaCompletions } from "@/hooks/use-lua-completions"; 17 + import { SiteHeader } from "@/components/site-header"; 18 + import { Badge } from "@/components/ui/badge"; 19 + import { Button } from "@/components/ui/button"; 20 + import { Label } from "@/components/ui/label"; 21 + 22 + export default function LexiconDetailPage() { 23 + const { id } = useParams<{ id: string }>(); 24 + const { getToken } = useAuth(); 25 + const router = useRouter(); 26 + const [lexicon, setLexicon] = useState<LexiconDetail | null>(null); 27 + const [error, setError] = useState<string | null>(null); 28 + const [deleting, setDeleting] = useState(false); 29 + const [saving, setSaving] = useState(false); 30 + 31 + // Editable text state 32 + const [jsonText, setJsonText] = useState(""); 33 + const [luaText, setLuaText] = useState(""); 34 + const [originalJson, setOriginalJson] = useState(""); 35 + const [originalLua, setOriginalLua] = useState(""); 36 + const { luaCompletions, collections } = useLuaCompletions(jsonText); 37 + 38 + const load = useCallback(() => { 39 + getLexicon(getToken, id) 40 + .then((lex) => { 41 + setLexicon(lex); 42 + const json = JSON.stringify(lex.lexicon_json, null, 2); 43 + setJsonText(json); 44 + setOriginalJson(json); 45 + 46 + // If lexicon has no script but is a query/procedure, auto-generate one 47 + if ( 48 + !lex.script && 49 + (lex.lexicon_type === "query" || lex.lexicon_type === "procedure") 50 + ) { 51 + const generated = 52 + lex.lexicon_type === "procedure" 53 + ? procedureScript(lex.target_collection ?? "") 54 + : queryScript(lex.target_collection ?? ""); 55 + setLuaText(generated); 56 + // Set originalLua to "" so isDirty becomes true, prompting user to save 57 + setOriginalLua(""); 58 + } else { 59 + setLuaText(lex.script ?? ""); 60 + setOriginalLua(lex.script ?? ""); 61 + } 62 + }) 63 + .catch((e) => setError(e instanceof Error ? e.message : String(e))); 64 + }, [getToken, id]); 65 + 66 + useEffect(() => { 67 + load(); 68 + }, [load]); 69 + 70 + const isDirty = jsonText !== originalJson || luaText !== originalLua; 71 + 72 + async function handleSave() { 73 + if (!lexicon) return; 74 + setSaving(true); 75 + setError(null); 76 + try { 77 + const lexiconJson = JSON.parse(jsonText); 78 + await uploadLexicon(getToken, { 79 + lexicon_json: lexiconJson, 80 + backfill: lexicon.backfill, 81 + script: luaText || undefined, 82 + }); 83 + load(); 84 + } catch (e: unknown) { 85 + setError(e instanceof Error ? e.message : String(e)); 86 + } finally { 87 + setSaving(false); 88 + } 89 + } 90 + 91 + async function handleDelete() { 92 + if (!lexicon) return; 93 + setDeleting(true); 94 + try { 95 + if (lexicon.source === "network") { 96 + await deleteNetworkLexicon(getToken, lexicon.id); 97 + } else { 98 + await deleteLexicon(getToken, lexicon.id); 99 + } 100 + router.push("/lexicons"); 101 + } catch (e: unknown) { 102 + setError(e instanceof Error ? e.message : String(e)); 103 + setDeleting(false); 104 + } 105 + } 106 + 107 + if (error && !lexicon) { 108 + return ( 109 + <> 110 + <SiteHeader title="Lexicon" backHref="/lexicons" /> 111 + <div className="p-4 md:p-6"> 112 + <p className="text-destructive text-sm">{error}</p> 113 + </div> 114 + </> 115 + ); 116 + } 117 + 118 + if (!lexicon) { 119 + return ( 120 + <> 121 + <SiteHeader title="Lexicon" backHref="/lexicons" /> 122 + <div className="p-4 md:p-6"> 123 + <p className="text-muted-foreground text-sm">Loading...</p> 124 + </div> 125 + </> 126 + ); 127 + } 128 + 129 + const isNetwork = lexicon.source === "network"; 130 + const showLua = 131 + lexicon.has_script || 132 + lexicon.lexicon_type === "query" || 133 + lexicon.lexicon_type === "procedure"; 134 + 135 + return ( 136 + <div className="flex flex-col h-full max-h-screen md:max-h-[calc(100vh-((var(--spacing)*2)*2))] overflow-hidden"> 137 + <SiteHeader title={lexicon.id} backHref="/lexicons" /> 138 + <div className="flex flex-col flex-1 min-h-0 gap-6 items-stretch overflow-hidden"> 139 + <div className="p-4 md:p-6"> 140 + {error && <p className="text-destructive text-sm mb-4">{error}</p>} 141 + 142 + {/* Metadata */} 143 + <div className="grid grid-cols-2 gap-x-8 gap-y-3 sm:grid-cols-3 lg:grid-cols-4"> 144 + <div> 145 + <Label className="text-muted-foreground">Type</Label> 146 + <div className="mt-1"> 147 + <Badge variant="outline">{lexicon.lexicon_type}</Badge> 148 + </div> 149 + </div> 150 + <div> 151 + <Label className="text-muted-foreground">Source</Label> 152 + <div className="mt-1"> 153 + <Badge variant={isNetwork ? "secondary" : "outline"}> 154 + {lexicon.source} 155 + </Badge> 156 + </div> 157 + </div> 158 + <div> 159 + <Label className="text-muted-foreground">Revision</Label> 160 + <p className="mt-1 tabular-nums">{lexicon.revision}</p> 161 + </div> 162 + <div> 163 + <Label className="text-muted-foreground">Backfill</Label> 164 + <p className="mt-1">{lexicon.backfill ? "Yes" : "No"}</p> 165 + </div> 166 + {lexicon.authority_did && ( 167 + <div className="col-span-2"> 168 + <Label className="text-muted-foreground">Authority DID</Label> 169 + <p className="mt-1 font-mono text-sm break-all"> 170 + {lexicon.authority_did} 171 + </p> 172 + </div> 173 + )} 174 + <div> 175 + <Label className="text-muted-foreground">Created</Label> 176 + <p className="mt-1 text-sm"> 177 + {new Date(lexicon.created_at).toLocaleString()} 178 + </p> 179 + </div> 180 + <div> 181 + <Label className="text-muted-foreground">Updated</Label> 182 + <p className="mt-1 text-sm"> 183 + {new Date(lexicon.updated_at).toLocaleString()} 184 + </p> 185 + </div> 186 + {lexicon.last_fetched_at && ( 187 + <div> 188 + <Label className="text-muted-foreground">Last Fetched</Label> 189 + <p className="mt-1 text-sm"> 190 + {new Date(lexicon.last_fetched_at).toLocaleString()} 191 + </p> 192 + </div> 193 + )} 194 + </div> 195 + </div> 196 + 197 + {/* Code Panels */} 198 + <CodePanels 199 + className="flex-1 min-h-0 px-4 md:px-6" 200 + jsonValue={jsonText} 201 + onJsonChange={isNetwork ? undefined : setJsonText} 202 + jsonReadOnly={isNetwork} 203 + luaValue={showLua ? luaText : undefined} 204 + onLuaChange={showLua ? setLuaText : undefined} 205 + luaCompletions={showLua ? luaCompletions : undefined} 206 + collections={showLua ? collections : undefined} 207 + /> 208 + 209 + {/* Actions */} 210 + <footer className="bg-sidebar-accent flex justify-between gap-2 ps-4 py-2 md:px-6 md:py-4 rounded-b-md"> 211 + <Button 212 + variant="destructive" 213 + onClick={handleDelete} 214 + disabled={deleting} 215 + > 216 + {deleting ? "Deleting..." : "Delete Lexicon"} 217 + </Button> 218 + 219 + <Button onClick={handleSave} disabled={!isDirty || saving}> 220 + {saving ? "Saving..." : "Save"} 221 + </Button> 222 + </footer> 223 + </div> 224 + </div> 225 + ); 226 + }
+7 -222
web/src/app/(dashboard)/lexicons/[id]/page.tsx
··· 1 - "use client"; 1 + import LexiconDetail from "./lexicon-detail"; 2 2 3 - import { useCallback, useEffect, useState } from "react"; 4 - import { useParams, useRouter } from "next/navigation"; 5 - 6 - import { useAuth } from "@/lib/auth-context"; 7 - import { CodePanels } from "@/components/code-panels"; 8 - import { 9 - deleteLexicon, 10 - deleteNetworkLexicon, 11 - getLexicon, 12 - uploadLexicon, 13 - type LexiconDetail, 14 - } from "@/lib/api"; 15 - import { procedureScript, queryScript } from "@/lib/lua-templates"; 16 - import { useLuaCompletions } from "@/hooks/use-lua-completions"; 17 - import { SiteHeader } from "@/components/site-header"; 18 - import { Badge } from "@/components/ui/badge"; 19 - import { Button } from "@/components/ui/button"; 20 - import { Label } from "@/components/ui/label"; 3 + // https://github.com/vercel/next.js/issues/71862 4 + // Returning [] fails with output:"export", so provide a dummy param. 5 + export async function generateStaticParams() { 6 + return [{ id: "_" }]; 7 + } 21 8 22 9 export default function LexiconDetailPage() { 23 - const { id } = useParams<{ id: string }>(); 24 - const { getToken } = useAuth(); 25 - const router = useRouter(); 26 - const [lexicon, setLexicon] = useState<LexiconDetail | null>(null); 27 - const [error, setError] = useState<string | null>(null); 28 - const [deleting, setDeleting] = useState(false); 29 - const [saving, setSaving] = useState(false); 30 - 31 - // Editable text state 32 - const [jsonText, setJsonText] = useState(""); 33 - const [luaText, setLuaText] = useState(""); 34 - const [originalJson, setOriginalJson] = useState(""); 35 - const [originalLua, setOriginalLua] = useState(""); 36 - const { luaCompletions, collections } = useLuaCompletions(jsonText); 37 - 38 - const load = useCallback(() => { 39 - getLexicon(getToken, id) 40 - .then((lex) => { 41 - setLexicon(lex); 42 - const json = JSON.stringify(lex.lexicon_json, null, 2); 43 - setJsonText(json); 44 - setOriginalJson(json); 45 - 46 - // If lexicon has no script but is a query/procedure, auto-generate one 47 - if ( 48 - !lex.script && 49 - (lex.lexicon_type === "query" || lex.lexicon_type === "procedure") 50 - ) { 51 - const generated = 52 - lex.lexicon_type === "procedure" 53 - ? procedureScript(lex.target_collection ?? "") 54 - : queryScript(lex.target_collection ?? ""); 55 - setLuaText(generated); 56 - // Set originalLua to "" so isDirty becomes true, prompting user to save 57 - setOriginalLua(""); 58 - } else { 59 - setLuaText(lex.script ?? ""); 60 - setOriginalLua(lex.script ?? ""); 61 - } 62 - }) 63 - .catch((e) => setError(e instanceof Error ? e.message : String(e))); 64 - }, [getToken, id]); 65 - 66 - useEffect(() => { 67 - load(); 68 - }, [load]); 69 - 70 - const isDirty = jsonText !== originalJson || luaText !== originalLua; 71 - 72 - async function handleSave() { 73 - if (!lexicon) return; 74 - setSaving(true); 75 - setError(null); 76 - try { 77 - const lexiconJson = JSON.parse(jsonText); 78 - await uploadLexicon(getToken, { 79 - lexicon_json: lexiconJson, 80 - backfill: lexicon.backfill, 81 - script: luaText || undefined, 82 - }); 83 - load(); 84 - } catch (e: unknown) { 85 - setError(e instanceof Error ? e.message : String(e)); 86 - } finally { 87 - setSaving(false); 88 - } 89 - } 90 - 91 - async function handleDelete() { 92 - if (!lexicon) return; 93 - setDeleting(true); 94 - try { 95 - if (lexicon.source === "network") { 96 - await deleteNetworkLexicon(getToken, lexicon.id); 97 - } else { 98 - await deleteLexicon(getToken, lexicon.id); 99 - } 100 - router.push("/lexicons"); 101 - } catch (e: unknown) { 102 - setError(e instanceof Error ? e.message : String(e)); 103 - setDeleting(false); 104 - } 105 - } 106 - 107 - if (error && !lexicon) { 108 - return ( 109 - <> 110 - <SiteHeader title="Lexicon" backHref="/lexicons" /> 111 - <div className="p-4 md:p-6"> 112 - <p className="text-destructive text-sm">{error}</p> 113 - </div> 114 - </> 115 - ); 116 - } 117 - 118 - if (!lexicon) { 119 - return ( 120 - <> 121 - <SiteHeader title="Lexicon" backHref="/lexicons" /> 122 - <div className="p-4 md:p-6"> 123 - <p className="text-muted-foreground text-sm">Loading...</p> 124 - </div> 125 - </> 126 - ); 127 - } 128 - 129 - const isNetwork = lexicon.source === "network"; 130 - const showLua = 131 - lexicon.has_script || 132 - lexicon.lexicon_type === "query" || 133 - lexicon.lexicon_type === "procedure"; 134 - 135 - return ( 136 - <div className="flex flex-col h-full max-h-screen md:max-h-[calc(100vh-((var(--spacing)*2)*2))] overflow-hidden"> 137 - <SiteHeader title={lexicon.id} backHref="/lexicons" /> 138 - <div className="flex flex-col flex-1 min-h-0 gap-6 items-stretch overflow-hidden"> 139 - <div className="p-4 md:p-6"> 140 - {error && <p className="text-destructive text-sm mb-4">{error}</p>} 141 - 142 - {/* Metadata */} 143 - <div className="grid grid-cols-2 gap-x-8 gap-y-3 sm:grid-cols-3 lg:grid-cols-4"> 144 - <div> 145 - <Label className="text-muted-foreground">Type</Label> 146 - <div className="mt-1"> 147 - <Badge variant="outline">{lexicon.lexicon_type}</Badge> 148 - </div> 149 - </div> 150 - <div> 151 - <Label className="text-muted-foreground">Source</Label> 152 - <div className="mt-1"> 153 - <Badge variant={isNetwork ? "secondary" : "outline"}> 154 - {lexicon.source} 155 - </Badge> 156 - </div> 157 - </div> 158 - <div> 159 - <Label className="text-muted-foreground">Revision</Label> 160 - <p className="mt-1 tabular-nums">{lexicon.revision}</p> 161 - </div> 162 - <div> 163 - <Label className="text-muted-foreground">Backfill</Label> 164 - <p className="mt-1">{lexicon.backfill ? "Yes" : "No"}</p> 165 - </div> 166 - {lexicon.authority_did && ( 167 - <div className="col-span-2"> 168 - <Label className="text-muted-foreground">Authority DID</Label> 169 - <p className="mt-1 font-mono text-sm break-all"> 170 - {lexicon.authority_did} 171 - </p> 172 - </div> 173 - )} 174 - <div> 175 - <Label className="text-muted-foreground">Created</Label> 176 - <p className="mt-1 text-sm"> 177 - {new Date(lexicon.created_at).toLocaleString()} 178 - </p> 179 - </div> 180 - <div> 181 - <Label className="text-muted-foreground">Updated</Label> 182 - <p className="mt-1 text-sm"> 183 - {new Date(lexicon.updated_at).toLocaleString()} 184 - </p> 185 - </div> 186 - {lexicon.last_fetched_at && ( 187 - <div> 188 - <Label className="text-muted-foreground">Last Fetched</Label> 189 - <p className="mt-1 text-sm"> 190 - {new Date(lexicon.last_fetched_at).toLocaleString()} 191 - </p> 192 - </div> 193 - )} 194 - </div> 195 - </div> 196 - 197 - {/* Code Panels */} 198 - <CodePanels 199 - className="flex-1 min-h-0 px-4 md:px-6" 200 - jsonValue={jsonText} 201 - onJsonChange={isNetwork ? undefined : setJsonText} 202 - jsonReadOnly={isNetwork} 203 - luaValue={showLua ? luaText : undefined} 204 - onLuaChange={showLua ? setLuaText : undefined} 205 - luaCompletions={showLua ? luaCompletions : undefined} 206 - collections={showLua ? collections : undefined} 207 - /> 208 - 209 - {/* Actions */} 210 - <footer className="bg-sidebar-accent flex justify-between gap-2 ps-4 py-2 md:px-6 md:py-4 rounded-b-md"> 211 - <Button 212 - variant="destructive" 213 - onClick={handleDelete} 214 - disabled={deleting} 215 - > 216 - {deleting ? "Deleting..." : "Delete Lexicon"} 217 - </Button> 218 - 219 - <Button onClick={handleSave} disabled={!isDirty || saving}> 220 - {saving ? "Saving..." : "Save"} 221 - </Button> 222 - </footer> 223 - </div> 224 - </div> 225 - ); 10 + return <LexiconDetail />; 226 11 }