tracks lexicons and how many times they appeared on the jetstream
3
fork

Configure Feed

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

refactor(client): cleanup code

dusk 73978e71 8a4a46f2

+98 -97
+15
client/src/lib/api.ts
··· 1 + import { dev } from "$app/environment"; 2 + import type { EventRecord } from "./types"; 3 + import { PUBLIC_API_URL } from "$env/static/public"; 4 + 5 + export const fetchEvents = async (): Promise<EventRecord[]> => { 6 + const response = await fetch( 7 + `${dev ? "http" : "https"}://${PUBLIC_API_URL}/events`, 8 + ); 9 + if (!response.ok) { 10 + throw new Error(`(${response.status}): ${await response.json()}`); 11 + } 12 + 13 + const data = await response.json(); 14 + return data.events; 15 + };
+3 -5
client/src/lib/components/EventCard.svelte
··· 1 1 <script lang="ts"> 2 + import { formatNumber, formatTimestamp } from "$lib/format"; 2 3 import type { EventRecord } from "$lib/types"; 3 4 import { onMount, onDestroy } from "svelte"; 4 5 5 6 interface Props { 6 - nsid: string; 7 7 event: EventRecord; 8 8 index: number; 9 - formatNumber: (num: number) => string; 10 - formatTimestamp: (timestamp: number) => string; 11 9 } 12 10 13 - let { nsid, event, index, formatNumber, formatTimestamp }: Props = $props(); 11 + let { event, index }: Props = $props(); 14 12 15 13 // Border animation state 16 14 let borderThickness = $state(0); ··· 118 116 </div> 119 117 </div> 120 118 <div class="font-mono text-sm text-gray-700 mb-2 break-all leading-relaxed"> 121 - {nsid} 119 + {event.nsid} 122 120 </div> 123 121 <div class="text-lg font-bold text-green-600"> 124 122 {formatNumber(event.count)} created
+1 -1
client/src/lib/components/FilterControls.svelte
··· 32 32 class="wsbadge !mt-0 !font-normal bg-yellow-100 hover:bg-yellow-200 border-yellow-300" 33 33 > 34 34 <input checked={dontShowBsky} type="checkbox" /> 35 - <span class="ml-0.5"> don't show app.bsky.* </span> 35 + <span class="ml-0.5"> hide app.bsky.* </span> 36 36 </button> 37 37 </div> 38 38
+17 -16
client/src/lib/components/StatsCard.svelte
··· 1 1 <script lang="ts"> 2 + import { formatNumber } from "$lib/format"; 3 + 2 4 interface Props { 3 5 title: string; 4 6 value: number; 5 - colorScheme: 'green' | 'red' | 'orange'; 6 - formatNumber: (num: number) => string; 7 + colorScheme: "green" | "red" | "orange"; 7 8 } 8 9 9 - let { title, value, colorScheme, formatNumber }: Props = $props(); 10 + let { title, value, colorScheme }: Props = $props(); 10 11 11 12 const colorClasses = { 12 13 green: { 13 - bg: 'from-green-50 to-green-100', 14 - border: 'border-green-200', 15 - titleText: 'text-green-700', 16 - valueText: 'text-green-900' 14 + bg: "from-green-50 to-green-100", 15 + border: "border-green-200", 16 + titleText: "text-green-700", 17 + valueText: "text-green-900", 17 18 }, 18 19 red: { 19 - bg: 'from-red-50 to-red-100', 20 - border: 'border-red-200', 21 - titleText: 'text-red-700', 22 - valueText: 'text-red-900' 20 + bg: "from-red-50 to-red-100", 21 + border: "border-red-200", 22 + titleText: "text-red-700", 23 + valueText: "text-red-900", 23 24 }, 24 25 orange: { 25 - bg: 'from-orange-50 to-orange-100', 26 - border: 'border-orange-200', 27 - titleText: 'text-orange-700', 28 - valueText: 'text-orange-900' 29 - } 26 + bg: "from-orange-50 to-orange-100", 27 + border: "border-orange-200", 28 + titleText: "text-orange-700", 29 + valueText: "text-orange-900", 30 + }, 30 31 }; 31 32 32 33 const colors = $derived(colorClasses[colorScheme]);
+23
client/src/lib/filter.ts
··· 1 + export const createRegexFilter = (pattern: string): RegExp | null => { 2 + if (!pattern) return null; 3 + 4 + try { 5 + // Check if pattern contains regex metacharacters 6 + const hasRegexChars = /[.*+?^${}()|[\]\\]/.test(pattern); 7 + 8 + if (hasRegexChars) { 9 + // Use as regex with case-insensitive flag 10 + return new RegExp(pattern, "i"); 11 + } else { 12 + // Smart case: case-insensitive unless pattern has uppercase 13 + const hasUppercase = /[A-Z]/.test(pattern); 14 + const flags = hasUppercase ? "" : "i"; 15 + // Escape the pattern for literal matching 16 + const escapedPattern = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); 17 + return new RegExp(escapedPattern, flags); 18 + } 19 + } catch (e) { 20 + // Invalid regex, return null 21 + return null; 22 + } 23 + };
+7
client/src/lib/format.ts
··· 1 + export const formatNumber = (num: number): string => { 2 + return num.toLocaleString(); 3 + }; 4 + 5 + export const formatTimestamp = (timestamp: number): string => { 6 + return new Date(timestamp / 1000).toLocaleString(); 7 + };
+7
client/src/lib/types.ts
··· 1 1 export type EventRecord = { 2 + nsid: string; 3 + last_seen: number; 4 + count: number; 5 + deleted_count: number; 6 + }; 7 + 8 + export type NsidCounts = { 2 9 last_seen: number; 3 10 count: number; 4 11 deleted_count: number;
+25 -75
client/src/routes/+page.svelte
··· 1 1 <script lang="ts"> 2 2 import { dev } from "$app/environment"; 3 - import type { EventRecord } from "$lib/types"; 3 + import type { EventRecord, NsidCounts } from "$lib/types"; 4 4 import { onMount, onDestroy } from "svelte"; 5 - import { get, writable } from "svelte/store"; 5 + import { writable } from "svelte/store"; 6 + import { PUBLIC_API_URL } from "$env/static/public"; 7 + import { fetchEvents } from "$lib/api"; 8 + import { createRegexFilter } from "$lib/filter"; 6 9 import StatsCard from "$lib/components/StatsCard.svelte"; 7 10 import StatusBadge from "$lib/components/StatusBadge.svelte"; 8 11 import EventCard from "$lib/components/EventCard.svelte"; 9 12 import FilterControls from "$lib/components/FilterControls.svelte"; 10 - import { PUBLIC_API_URL } from "$env/static/public"; 11 13 12 - const events = writable(new Map<string, EventRecord>()); 13 - let eventsList: { nsid: string; event: EventRecord }[] = $state([]); 14 + const events = writable(new Map<string, NsidCounts>()); 15 + let eventsList: EventRecord[] = $state([]); 14 16 events.subscribe((value) => { 15 17 eventsList = value 16 18 .entries() 17 19 .map(([nsid, event]) => ({ 18 20 nsid, 19 - event, 21 + ...event, 20 22 })) 21 23 .toArray(); 22 - eventsList.sort((a, b) => b.event.count - a.event.count); 24 + eventsList.sort((a, b) => b.count - a.count); 23 25 }); 24 26 25 27 // Backpressure system 26 - let eventBuffer: { nsid: string; event: EventRecord }[] = []; 28 + let eventBuffer: EventRecord[] = []; 27 29 let updateTimer: number | null = null; 28 - let bufferedEventsCount = $state(0); 29 30 const BATCH_SIZE = 10; 30 31 const UPDATE_INTERVAL = 100; // ms 31 32 ··· 33 34 if (eventBuffer.length === 0) return; 34 35 35 36 events.update((map) => { 36 - for (const { nsid, event } of eventBuffer) { 37 - map.set(nsid, event); 37 + for (const event of eventBuffer) { 38 + map.set(event.nsid, event); 38 39 } 39 40 return map; 40 41 }); 41 42 42 43 eventBuffer = []; 43 - bufferedEventsCount = 0; 44 44 }; 45 45 46 46 const scheduleUpdate = () => { ··· 51 51 updateTimer = null; 52 52 }, UPDATE_INTERVAL); 53 53 }; 54 - let all: EventRecord = $derived( 54 + let all: NsidCounts = $derived( 55 55 eventsList.reduce( 56 - (acc, { nsid, event }) => { 56 + (acc, event) => { 57 57 return { 58 58 last_seen: 59 59 acc.last_seen > event.last_seen ··· 98 98 const jsonData = JSON.parse(jsonStr); 99 99 100 100 // Add to buffer instead of immediate update 101 - eventBuffer.push({ nsid: jsonData.nsid, event: jsonData }); 102 - bufferedEventsCount = eventBuffer.length; 101 + eventBuffer.push(jsonData); 103 102 104 103 // If buffer is full, flush immediately 105 104 if (eventBuffer.length >= BATCH_SIZE) { ··· 134 133 const loadData = async () => { 135 134 try { 136 135 error = null; 137 - 138 - const response = await fetch( 139 - `${dev ? "http" : "https"}://${PUBLIC_API_URL}/events`, 140 - ); 141 - if (!response.ok) { 142 - throw new Error(`HTTP error! status: ${response.status}`); 143 - } 144 - 145 - const data = await response.json(); 136 + const data = await fetchEvents(); 146 137 events.update((map) => { 147 - for (const event of data.events) { 138 + for (const event of data) { 148 139 map.set(event.nsid, event); 149 140 } 150 141 return map; ··· 177 168 } 178 169 }); 179 170 180 - const formatNumber = (num: number): string => { 181 - return num.toLocaleString(); 182 - }; 183 - 184 - const formatTimestamp = (timestamp: number): string => { 185 - return new Date(timestamp / 1000).toLocaleString(); 186 - }; 187 - 188 - const createRegexFilter = (pattern: string): RegExp | null => { 189 - if (!pattern) return null; 190 - 191 - try { 192 - // Check if pattern contains regex metacharacters 193 - const hasRegexChars = /[.*+?^${}()|[\]\\]/.test(pattern); 194 - 195 - if (hasRegexChars) { 196 - // Use as regex with case-insensitive flag 197 - return new RegExp(pattern, "i"); 198 - } else { 199 - // Smart case: case-insensitive unless pattern has uppercase 200 - const hasUppercase = /[A-Z]/.test(pattern); 201 - const flags = hasUppercase ? "" : "i"; 202 - // Escape the pattern for literal matching 203 - const escapedPattern = pattern.replace( 204 - /[.*+?^${}()|[\]\\]/g, 205 - "\\$&", 206 - ); 207 - return new RegExp(escapedPattern, flags); 208 - } 209 - } catch (e) { 210 - // Invalid regex, return null 211 - return null; 212 - } 213 - }; 214 - 215 - const filterEvents = (events: { nsid: string; event: EventRecord }[]) => { 171 + const filterEvents = (events: EventRecord[]) => { 216 172 let filtered = events; 217 173 218 174 // Apply regex filter ··· 233 189 </script> 234 190 235 191 <svelte:head> 236 - <title>bluesky event tracker</title> 237 - <meta name="description" content="tracks bluesky events by collection" /> 192 + <title>bluesky jetstream tracker</title> 193 + <meta 194 + name="description" 195 + content="tracks bluesky jetstream events by collection" 196 + /> 238 197 </svelte:head> 239 198 240 199 <div class="md:max-w-[60vw] mx-auto p-2"> ··· 254 213 title="total creation" 255 214 value={all.count} 256 215 colorScheme="green" 257 - {formatNumber} 258 216 /> 259 217 <StatsCard 260 218 title="total deletion" 261 219 value={all.deleted_count} 262 220 colorScheme="red" 263 - {formatNumber} 264 221 /> 265 222 <StatsCard 266 223 title="unique collections" 267 224 value={eventsList.length} 268 225 colorScheme="orange" 269 - {formatNumber} 270 226 /> 271 227 </div> 272 228 ··· 293 249 onBskyToggle={() => (dontShowBsky = !dontShowBsky)} 294 250 /> 295 251 <div class="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4"> 296 - {#each filterEvents(eventsList) as { nsid, event }, index (nsid)} 297 - <EventCard 298 - {nsid} 299 - {event} 300 - {index} 301 - {formatNumber} 302 - {formatTimestamp} 303 - /> 252 + {#each filterEvents(eventsList) as event, index (event.nsid)} 253 + <EventCard {event} {index} /> 304 254 {/each} 305 255 </div> 306 256 </div>