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.

fix: sort events server side, fix styling on client

dusk 24f00247 ff9bc917

+53 -28
+1 -1
client/src/lib/types.ts
··· 1 1 export type EventRecord = { 2 2 nsid: string; 3 - timestamp: number; 3 + last_seen: number; 4 4 count: number; 5 5 deleted_count: number; 6 6 };
+48 -26
client/src/routes/+page.svelte
··· 1 1 <script lang="ts"> 2 + import { dev } from "$app/environment"; 2 3 import type { EventRecord } from "$lib/types"; 4 + import { onMount } from "svelte"; 3 5 4 - interface Props { 5 - // eslint-disable-next-line @typescript-eslint/no-explicit-any 6 - data: any; 7 - } 8 - let { data }: Props = $props(); 9 - 10 - let events: EventRecord[] = $state(data.events); 11 - let usableEvents = $derived(events.filter((e) => e.nsid !== "*")); 12 - let allNsidRecord = $derived( 13 - events.find((e) => { 14 - return e.nsid === "*"; 15 - }), 6 + let events: EventRecord[] = $state([]); 7 + let all: EventRecord = $derived( 8 + events.reduce( 9 + (acc, event) => { 10 + return { 11 + nsid: "*", 12 + last_seen: 13 + acc.last_seen > event.last_seen 14 + ? acc.last_seen 15 + : event.last_seen, 16 + count: acc.count + event.count, 17 + deleted_count: acc.deleted_count + event.deleted_count, 18 + }; 19 + }, 20 + { 21 + nsid: "*", 22 + last_seen: 0, 23 + count: 0, 24 + deleted_count: 0, 25 + }, 26 + ), 16 27 ); 17 28 let error: string | null = $state(null); 18 29 let dontShowBsky = $state(false); ··· 21 32 try { 22 33 error = null; 23 34 24 - const response = await fetch("/api/events"); 35 + const response = dev 36 + ? await fetch("http://localhost:3000/events") 37 + : await fetch("/api/events"); 25 38 if (!response.ok) { 26 39 throw new Error(`HTTP error! status: ${response.status}`); 27 40 } ··· 37 50 } 38 51 }; 39 52 53 + onMount(() => { 54 + loadData(); 55 + }); 56 + 40 57 const formatNumber = (num: number): string => { 41 58 return num.toLocaleString(); 42 59 }; ··· 61 78 </p> 62 79 </header> 63 80 64 - <div class="mx-auto w-fit grid grid-cols-2 md:grid-cols-3 mb-8"> 81 + <div class="mx-auto w-fit grid grid-cols-2 md:grid-cols-3 gap-5 mb-8"> 65 82 <div 66 83 class="bg-gradient-to-r from-green-50 to-green-100 p-3 md:p-6 rounded-lg border border-green-200" 67 84 > 68 85 <h3 class="text-base font-medium text-green-700 mb-2"> 69 - total events created 86 + total creation 70 87 </h3> 71 88 <p class="text-3xl font-bold text-green-900"> 72 89 {// eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain 73 - formatNumber(allNsidRecord?.count!)} 90 + formatNumber(all.count)} 74 91 </p> 75 92 </div> 76 93 <div 77 94 class="bg-gradient-to-r from-red-50 to-red-100 p-3 md:p-6 rounded-lg border border-red-200" 78 95 > 79 96 <h3 class="text-base font-medium text-red-700 mb-2"> 80 - total events deleted 97 + total deletion 81 98 </h3> 82 99 <p class="text-3xl font-bold text-red-900"> 83 100 {// eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain 84 - formatNumber(allNsidRecord?.deleted_count!)} 101 + formatNumber(all.deleted_count)} 85 102 </p> 86 103 </div> 87 104 <div ··· 91 108 unique collections 92 109 </h3> 93 110 <p class="text-3xl font-bold text-orange-900"> 94 - {formatNumber(usableEvents.length)} 111 + {formatNumber(events.length)} 95 112 </p> 96 113 </div> 97 114 </div> ··· 103 120 > 104 121 refresh 105 122 </button> 106 - <div class="mt-2"> 123 + <!-- svelte-ignore a11y_click_events_have_key_events --> 124 + <!-- svelte-ignore a11y_no_static_element_interactions --> 125 + <button 126 + onclick={() => (dontShowBsky = !dontShowBsky)} 127 + class="mt-2 bg-yellow-100 hover:bg-yellow-200 px-2 py-1 rounded-full" 128 + > 107 129 <input bind:checked={dontShowBsky} type="checkbox" /> 108 - <span> don't show app.bsky.* </span> 109 - </div> 130 + <span class="ml-1"> don't show app.bsky.* </span> 131 + </button> 110 132 </div> 111 133 112 134 {#if error} ··· 117 139 </div> 118 140 {/if} 119 141 120 - {#if usableEvents.length > 0} 142 + {#if events.length > 0} 121 143 <div class="mb-8"> 122 144 <h2 class="text-2xl font-bold mb-6 text-gray-900"> 123 145 events by collection 124 146 </h2> 125 147 <div class="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4"> 126 - {#each usableEvents.filter((e) => { 148 + {#each events.filter((e) => { 127 149 return dontShowBsky ? !e.nsid.startsWith("app.bsky.") : true; 128 150 }) as event, index (event.nsid)} 129 151 <div 130 - class="mx-auto md:mx-0 w-fit bg-white border border-gray-200 rounded-lg md:p-6 hover:shadow-lg transition-shadow duration-200 hover:-translate-y-1 transform" 152 + class="mx-auto md:mx-0 bg-white border border-gray-200 rounded-lg md:p-6 hover:shadow-lg transition-shadow duration-200 hover:-translate-y-1 transform" 131 153 > 132 154 <div class="flex justify-between items-start mb-3"> 133 155 <div ··· 148 170 {formatNumber(event.deleted_count)} deleted 149 171 </div> 150 172 <div class="text-xs text-gray-500"> 151 - last: {formatTimestamp(event.timestamp)} 173 + last: {formatTimestamp(event.last_seen)} 152 174 </div> 153 175 </div> 154 176 {/each}
+2 -1
server/src/api.rs
··· 9 9 pub async fn serve(db: Arc<Db>) { 10 10 let app = Router::new().route("/events", get(events)).with_state(db); 11 11 12 - let addr = "0.0.0.0:3123"; 12 + let addr = "0.0.0.0:3000"; 13 13 let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); 14 14 tracing::info!("starting serve on {addr}"); 15 15 axum::serve(listener, app).await.unwrap(); ··· 37 37 last_seen: counts.last_seen, 38 38 }) 39 39 } 40 + events.sort_unstable_by(|a, b| b.count.cmp(&a.count)); 40 41 Ok(Json(Events { events })) 41 42 }
+2
server/src/db.rs
··· 18 18 pub deleted: bool, 19 19 } 20 20 21 + // counts is nsid -> NsidCounts 22 + // hits is tree per nsid: timestamp -> NsidHit 21 23 pub struct Db { 22 24 inner: Keyspace, 23 25 hits: papaya::HashMap<SmolStr, Partition>,