ATProto forum built with ESAV
at main 167 lines 4.4 kB view raw
1import { useAtomValue } from "jotai"; 2import { useState } from "react"; 3import { websocketStatusAtom, logEntriesAtom } from "./atoms"; 4import type { LogEntry } from "./types"; 5 6 7export function ReconnectingHeader() { 8 const status = useAtomValue(websocketStatusAtom); 9 10 if (status === "open") { 11 return null; 12 } 13 14 const message = 15 status === "connecting" 16 ? "Connecting to ESAV Live..." 17 : "Connection lost. Attempting to reconnect..."; 18 19 return ( 20 <div 21 style={{ 22 position: "sticky", 23 top: 0, 24 left: 0, 25 width: "100%", 26 padding: "8px", 27 backgroundColor: "#ffc107", 28 color: "#333", 29 textAlign: "center", 30 fontWeight: "bold", 31 zIndex: 1000, 32 boxShadow: "0 2px 4px rgba(0,0,0,0.1)", 33 }} 34 > 35 {message} 36 </div> 37 ); 38} 39 40const LogEntryItem = ({ entry }: { entry: LogEntry }) => { 41 const { type, timestamp, payload } = entry; 42 43 const typeStyles = { 44 incoming: { icon: "⬇️", color: "#4caf50", name: "Incoming" }, 45 outgoing: { icon: "⬆️", color: "#ffeb3b", name: "Outgoing" }, 46 status: { icon: "ℹ️", color: "#2196f3", name: "Status" }, 47 error: { icon: "❌", color: "#f44336", name: "Error" }, 48 }; 49 50 const { icon, color, name } = typeStyles[type]; 51 52 return ( 53 <div 54 style={{ 55 borderBottom: "1px solid #444", 56 padding: "8px", 57 fontFamily: "monospace", 58 fontSize: "12px", 59 borderLeft: `4px solid ${color}`, 60 }} 61 > 62 <div style={{ fontWeight: "bold", marginBottom: "4px" }}> 63 <span style={{ marginRight: "8px" }}>{icon}</span> 64 {name} 65 <span style={{ float: "right", color: "#888" }}> 66 {timestamp.toLocaleTimeString()} 67 </span> 68 </div> 69 {typeof payload === "object" ? ( 70 <pre 71 style={{ 72 margin: 0, 73 padding: "8px", 74 backgroundColor: "rgba(0,0,0,0.2)", 75 borderRadius: "4px", 76 whiteSpace: "pre-wrap", 77 wordBreak: "break-all", 78 fontSize: "11px", 79 maxHeight: "200px", 80 overflowY: "auto", 81 }} 82 > 83 {JSON.stringify(payload, null, 2)} 84 </pre> 85 ) : ( 86 <code style={{ color: "#ccc" }}>{String(payload)}</code> 87 )} 88 </div> 89 ); 90}; 91 92export function DeltaLogViewer() { 93 const [open, setOpen] = useState(false); 94 const log = useAtomValue(logEntriesAtom); 95 96 return ( 97 <div 98 style={{ 99 position: "fixed", 100 bottom: "10px", 101 right: "10px", 102 width: open ? "min(850px,90dvw)" : "280px", 103 backgroundColor: "#2d2d2d", 104 color: "#f1f1f1", 105 border: "1px solid #555", 106 borderRadius: "8px", 107 boxShadow: "0 4px 12px rgba(0,0,0,0.3)", 108 zIndex: 2000, 109 overflow: "hidden", 110 display: "flex", 111 flexDirection: "column", 112 maxHeight: "600px", 113 transition: "width 0.1s ease", 114 }} 115 > 116 <div 117 style={{ 118 display: "flex", 119 justifyContent: "space-between", 120 alignItems: "center", 121 padding: "8px 12px", 122 backgroundColor: "#3c3c3c", 123 borderBottom: "1px solid #555", 124 fontWeight: 700, 125 }} 126 > 127 <span>ESAV Live Log</span> 128 <button 129 onClick={() => setOpen(!open)} 130 style={{ 131 background: "transparent", 132 border: "none", 133 color: "#ccc", 134 fontSize: "16px", 135 cursor: "pointer", 136 padding: "4px 8px", 137 borderRadius: "4px", 138 transition: "background 0.01s", 139 }} 140 onMouseEnter={(e) => (e.currentTarget.style.background = "#444")} 141 onMouseLeave={(e) => 142 (e.currentTarget.style.background = "transparent") 143 } 144 title={open ? "Collapse log" : "Expand log"} 145 > 146 {open ? "close" : "open"} 147 </button> 148 </div> 149 <div 150 style={{ 151 flex: 1, 152 overflowY: "auto", 153 display: open ? "flex" : "none", 154 flexDirection: "column", 155 }} 156 > 157 {log.length === 0 ? ( 158 <div style={{ padding: "10px", color: "#888" }}> 159 Waiting for events... 160 </div> 161 ) : ( 162 log.map((entry) => <LogEntryItem key={entry.id} entry={entry} />) 163 )} 164 </div> 165 </div> 166 ); 167}