tangled
alpha
login
or
join now
ptr.pet
/
nsid-tracker
3
fork
atom
tracks lexicons and how many times they appeared on the jetstream
3
fork
atom
overview
issues
pulls
pipelines
feat(client): implement sorting and setting refresh rate
ptr.pet
8 months ago
e8dfb4a1
b4c008ad
verified
This commit was signed with the committer's
known signature
.
ptr.pet
SSH Key Fingerprint:
SHA256:Abmvag+juovVufZTxyWY8KcVgrznxvBjQpJesv071Aw=
+226
-41
6 changed files
expand all
collapse all
unified
split
client
src
lib
components
BskyToggle.svelte
FilterControls.svelte
RefreshControl.svelte
SortControls.svelte
types.ts
routes
+page.svelte
+25
client/src/lib/components/BskyToggle.svelte
reviewed
···
1
1
+
<script lang="ts">
2
2
+
interface Props {
3
3
+
dontShowBsky: boolean;
4
4
+
onBskyToggle: () => void;
5
5
+
}
6
6
+
7
7
+
let { dontShowBsky, onBskyToggle }: Props = $props();
8
8
+
</script>
9
9
+
10
10
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
11
11
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
12
12
+
<button
13
13
+
onclick={onBskyToggle}
14
14
+
class="wsbadge !mt-0 !font-normal bg-yellow-100 hover:bg-yellow-200 border-yellow-300"
15
15
+
>
16
16
+
<input checked={dontShowBsky} type="checkbox" />
17
17
+
<span class="ml-0.5"> hide app.bsky.* </span>
18
18
+
</button>
19
19
+
20
20
+
<style lang="postcss">
21
21
+
@reference "../../app.css";
22
22
+
.wsbadge {
23
23
+
@apply text-sm font-semibold mt-1.5 px-2.5 py-0.5 rounded-full border;
24
24
+
}
25
25
+
</style>
+13
-28
client/src/lib/components/FilterControls.svelte
reviewed
···
1
1
<script lang="ts">
2
2
interface Props {
3
3
filterRegex: string;
4
4
-
dontShowBsky: boolean;
5
4
onFilterChange: (value: string) => void;
6
6
-
onBskyToggle: () => void;
7
5
}
8
6
9
9
-
let { filterRegex, dontShowBsky, onFilterChange, onBskyToggle }: Props =
10
10
-
$props();
7
7
+
let { filterRegex, onFilterChange }: Props = $props();
11
8
</script>
12
9
13
13
-
<div class="flex flex-wrap items-center gap-3 mb-6">
14
14
-
<div
15
15
-
class="wsbadge !pl-2 !px-1 !mt-0 !font-normal bg-blue-100 hover:bg-blue-200 border-blue-300"
16
16
-
>
17
17
-
<label for="filter-regex" class="text-blue-800 mr-1"> filter: </label>
18
18
-
<input
19
19
-
id="filter-regex"
20
20
-
value={filterRegex}
21
21
-
oninput={(e) =>
22
22
-
onFilterChange((e.target as HTMLInputElement).value)}
23
23
-
type="text"
24
24
-
placeholder="regex..."
25
25
-
class="bg-blue-50 text-blue-900 placeholder-blue-400 border border-blue-200 rounded-full px-1 outline-none focus:bg-white focus:border-blue-400 min-w-0 w-24"
26
26
-
/>
27
27
-
</div>
28
28
-
<!-- svelte-ignore a11y_click_events_have_key_events -->
29
29
-
<!-- svelte-ignore a11y_no_static_element_interactions -->
30
30
-
<button
31
31
-
onclick={onBskyToggle}
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
35
-
<span class="ml-0.5"> hide app.bsky.* </span>
36
36
-
</button>
10
10
+
<div
11
11
+
class="wsbadge !pl-2 !px-1 !mt-0 !font-normal bg-blue-100 hover:bg-blue-200 border-blue-300"
12
12
+
>
13
13
+
<label for="filter-regex" class="text-blue-800 mr-1"> filter: </label>
14
14
+
<input
15
15
+
id="filter-regex"
16
16
+
value={filterRegex}
17
17
+
oninput={(e) => onFilterChange((e.target as HTMLInputElement).value)}
18
18
+
type="text"
19
19
+
placeholder="regex..."
20
20
+
class="bg-blue-50 text-blue-900 placeholder-blue-400 border border-blue-200 rounded-full px-1 outline-none focus:bg-white focus:border-blue-400 min-w-0 w-24"
21
21
+
/>
37
22
</div>
38
23
39
24
<style lang="postcss">
+37
client/src/lib/components/RefreshControl.svelte
reviewed
···
1
1
+
<script lang="ts">
2
2
+
interface Props {
3
3
+
refreshRate: string;
4
4
+
onRefreshChange: (value: string) => void;
5
5
+
}
6
6
+
7
7
+
let { refreshRate, onRefreshChange }: Props = $props();
8
8
+
</script>
9
9
+
10
10
+
<div
11
11
+
class="wsbadge !pl-2 !px-1 !mt-0 !font-normal bg-green-100 hover:bg-green-200 border-green-300"
12
12
+
>
13
13
+
<label for="refresh-rate" class="text-green-800 mr-1">refresh:</label>
14
14
+
<input
15
15
+
id="refresh-rate"
16
16
+
value={refreshRate}
17
17
+
oninput={(e) => {
18
18
+
const el = e.target as HTMLInputElement;
19
19
+
if (!el.validity.valid) el.value = el.value.replace(/\D+/g, "");
20
20
+
onRefreshChange(el.value);
21
21
+
}}
22
22
+
type="text"
23
23
+
inputmode="numeric"
24
24
+
pattern="[0-9]*"
25
25
+
min="0"
26
26
+
placeholder="real-time"
27
27
+
class="bg-green-50 text-green-900 placeholder-green-400 border border-green-200 rounded-full px-1 outline-none focus:bg-white focus:border-green-400 min-w-0 w-20"
28
28
+
/>
29
29
+
<span class="text-green-700">s</span>
30
30
+
</div>
31
31
+
32
32
+
<style lang="postcss">
33
33
+
@reference "../../app.css";
34
34
+
.wsbadge {
35
35
+
@apply text-sm font-semibold mt-1.5 px-2.5 py-0.5 rounded-full border;
36
36
+
}
37
37
+
</style>
+41
client/src/lib/components/SortControls.svelte
reviewed
···
1
1
+
<script lang="ts">
2
2
+
import type { SortOption } from "$lib/types";
3
3
+
4
4
+
interface Props {
5
5
+
sortBy: SortOption;
6
6
+
onSortChange: (value: SortOption) => void;
7
7
+
}
8
8
+
9
9
+
let { sortBy, onSortChange }: Props = $props();
10
10
+
11
11
+
const sortOptions = [
12
12
+
{ value: "total" as const, label: "total count" },
13
13
+
{ value: "created" as const, label: "created count" },
14
14
+
{ value: "deleted" as const, label: "deleted count" },
15
15
+
{ value: "date" as const, label: "newest first" },
16
16
+
];
17
17
+
</script>
18
18
+
19
19
+
<div
20
20
+
class="wsbadge !pl-2 !px-1 !mt-0 !font-normal bg-purple-100 hover:bg-purple-200 border-purple-300"
21
21
+
>
22
22
+
<label for="sort-by" class="text-purple-800 mr-1"> sort by: </label>
23
23
+
<select
24
24
+
id="sort-by"
25
25
+
value={sortBy}
26
26
+
onchange={(e) =>
27
27
+
onSortChange((e.target as HTMLSelectElement).value as SortOption)}
28
28
+
class="bg-purple-50 text-purple-900 border border-purple-200 rounded-full px-1 outline-none focus:bg-white focus:border-purple-400 min-w-0"
29
29
+
>
30
30
+
{#each sortOptions as option}
31
31
+
<option value={option.value}>{option.label}</option>
32
32
+
{/each}
33
33
+
</select>
34
34
+
</div>
35
35
+
36
36
+
<style lang="postcss">
37
37
+
@reference "../../app.css";
38
38
+
.wsbadge {
39
39
+
@apply text-sm font-semibold mt-1.5 px-2.5 py-0.5 rounded-full border;
40
40
+
}
41
41
+
</style>
+2
client/src/lib/types.ts
reviewed
···
13
13
count: number;
14
14
deleted_count: number;
15
15
};
16
16
+
17
17
+
export type SortOption = "total" | "created" | "deleted" | "date";
+108
-13
client/src/routes/+page.svelte
reviewed
···
1
1
<script lang="ts">
2
2
import { dev } from "$app/environment";
3
3
-
import type { EventRecord, NsidCount } from "$lib/types";
3
3
+
import type { EventRecord, NsidCount, SortOption } from "$lib/types";
4
4
import { onMount, onDestroy } from "svelte";
5
5
import { writable } from "svelte/store";
6
6
import { PUBLIC_API_URL } from "$env/static/public";
···
10
10
import StatusBadge from "$lib/components/StatusBadge.svelte";
11
11
import EventCard from "$lib/components/EventCard.svelte";
12
12
import FilterControls from "$lib/components/FilterControls.svelte";
13
13
+
import SortControls from "$lib/components/SortControls.svelte";
14
14
+
import BskyToggle from "$lib/components/BskyToggle.svelte";
15
15
+
import RefreshControl from "$lib/components/RefreshControl.svelte";
13
16
14
17
const events = writable(new Map<string, EventRecord>());
18
18
+
const pendingUpdates = new Map<string, EventRecord>();
15
19
let eventsList: NsidCount[] = $state([]);
20
20
+
let updateTimer: NodeJS.Timeout | null = null;
16
21
events.subscribe((value) => {
17
22
eventsList = value
18
23
.entries()
···
21
26
...event,
22
27
}))
23
28
.toArray();
24
24
-
eventsList.sort((a, b) => b.count - a.count);
25
29
});
26
30
let per_second = $state(0);
27
31
···
47
51
let error: string | null = $state(null);
48
52
let filterRegex = $state("");
49
53
let dontShowBsky = $state(false);
54
54
+
let sortBy: SortOption = $state("total");
55
55
+
let refreshRate = $state("");
56
56
+
let previousRefreshRate = "";
50
57
51
58
let websocket: WebSocket | null = null;
52
59
let isStreamOpen = $state(false);
···
71
78
if (jsonData.per_second > 0) {
72
79
per_second = jsonData.per_second;
73
80
}
74
74
-
events.update((map) => {
81
81
+
82
82
+
// Store updates in pending map if refresh rate is set
83
83
+
if (refreshRate) {
75
84
for (const [nsid, event] of Object.entries(jsonData.events)) {
76
76
-
map.set(nsid, event as EventRecord);
85
85
+
pendingUpdates.set(nsid, event as EventRecord);
77
86
}
78
78
-
return map;
79
79
-
});
87
87
+
} else {
88
88
+
// Apply updates immediately if no refresh rate
89
89
+
events.update((map) => {
90
90
+
for (const [nsid, event] of Object.entries(
91
91
+
jsonData.events,
92
92
+
)) {
93
93
+
map.set(nsid, event as EventRecord);
94
94
+
}
95
95
+
return map;
96
96
+
});
97
97
+
}
80
98
};
81
99
websocket.onerror = (error) => {
82
100
console.error("ws error:", error);
···
109
127
}
110
128
};
111
129
130
130
+
// Set refresh rate when sort mode changes
131
131
+
$effect(() => {
132
132
+
if (sortBy === "date" && !refreshRate) {
133
133
+
// Only set to 2 if currently empty (real-time)
134
134
+
previousRefreshRate = "";
135
135
+
refreshRate = "2";
136
136
+
} else if (refreshRate === "2" && sortBy !== "date") {
137
137
+
// Only restore to empty if we auto-set it and switching away from date
138
138
+
refreshRate = previousRefreshRate;
139
139
+
previousRefreshRate = "";
140
140
+
}
141
141
+
});
142
142
+
143
143
+
// Update the refresh timer when refresh rate changes
144
144
+
$effect(() => {
145
145
+
if (updateTimer) {
146
146
+
clearInterval(updateTimer);
147
147
+
updateTimer = null;
148
148
+
}
149
149
+
150
150
+
if (refreshRate) {
151
151
+
const rate = parseInt(refreshRate, 10) * 1000; // Convert to milliseconds
152
152
+
if (!isNaN(rate) && rate > 0) {
153
153
+
updateTimer = setInterval(() => {
154
154
+
if (pendingUpdates.size > 0) {
155
155
+
events.update((map) => {
156
156
+
for (const [nsid, event] of pendingUpdates) {
157
157
+
map.set(nsid, event);
158
158
+
}
159
159
+
pendingUpdates.clear();
160
160
+
return map;
161
161
+
});
162
162
+
}
163
163
+
}, rate);
164
164
+
}
165
165
+
}
166
166
+
});
167
167
+
112
168
onMount(() => {
113
169
loadData();
114
170
connectToStream();
115
171
});
116
172
117
173
onDestroy(() => {
174
174
+
// Clear refresh timer
175
175
+
if (updateTimer) {
176
176
+
clearInterval(updateTimer);
177
177
+
updateTimer = null;
178
178
+
}
118
179
// Close WebSocket connection
119
180
if (websocket) {
120
181
websocket.close();
121
182
}
122
183
});
184
184
+
185
185
+
const sortEvents = (events: NsidCount[], sortBy: SortOption) => {
186
186
+
const sorted = [...events];
187
187
+
switch (sortBy) {
188
188
+
case "total":
189
189
+
sorted.sort(
190
190
+
(a, b) =>
191
191
+
b.count + b.deleted_count - (a.count + a.deleted_count),
192
192
+
);
193
193
+
break;
194
194
+
case "created":
195
195
+
sorted.sort((a, b) => b.count - a.count);
196
196
+
break;
197
197
+
case "deleted":
198
198
+
sorted.sort((a, b) => b.deleted_count - a.deleted_count);
199
199
+
break;
200
200
+
case "date":
201
201
+
sorted.sort((a, b) => b.last_seen - a.last_seen);
202
202
+
break;
203
203
+
}
204
204
+
return sorted;
205
205
+
};
123
206
124
207
const filterEvents = (events: NsidCount[]) => {
125
208
let filtered = events;
···
199
282
<h2 class="text-2xl font-bold text-gray-900">seen lexicons</h2>
200
283
<StatusBadge status={websocketStatus} />
201
284
</div>
202
202
-
<FilterControls
203
203
-
{filterRegex}
204
204
-
{dontShowBsky}
205
205
-
onFilterChange={(value) => (filterRegex = value)}
206
206
-
onBskyToggle={() => (dontShowBsky = !dontShowBsky)}
207
207
-
/>
285
285
+
<div class="flex flex-wrap items-center gap-1.5 mb-6">
286
286
+
<FilterControls
287
287
+
{filterRegex}
288
288
+
onFilterChange={(value) => (filterRegex = value)}
289
289
+
/>
290
290
+
<SortControls
291
291
+
{sortBy}
292
292
+
onSortChange={(value: SortOption) => (sortBy = value)}
293
293
+
/>
294
294
+
<BskyToggle
295
295
+
{dontShowBsky}
296
296
+
onBskyToggle={() => (dontShowBsky = !dontShowBsky)}
297
297
+
/>
298
298
+
<RefreshControl
299
299
+
{refreshRate}
300
300
+
onRefreshChange={(value) => (refreshRate = value)}
301
301
+
/>
302
302
+
</div>
208
303
<div
209
304
class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-4"
210
305
>
211
211
-
{#each filterEvents(eventsList) as event, index (event.nsid)}
306
306
+
{#each sortEvents(filterEvents(eventsList), sortBy) as event, index (event.nsid)}
212
307
<EventCard {event} {index} />
213
308
{/each}
214
309
</div>