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): add dark mode
ptr.pet
6 months ago
b7d77d22
e8cda421
verified
This commit was signed with the committer's
known signature
.
ptr.pet
SSH Key Fingerprint:
SHA256:Abmvag+juovVufZTxyWY8KcVgrznxvBjQpJesv071Aw=
+80
-59
10 changed files
expand all
collapse all
unified
split
client
src
app.html
lib
components
BskyToggle.svelte
EventCard.svelte
FilterControls.svelte
RefreshControl.svelte
ShowControls.svelte
SortControls.svelte
StatsCard.svelte
StatusBadge.svelte
routes
+page.svelte
+9
-9
client/src/app.html
···
1
<!doctype html>
2
<html lang="en">
3
-
<head>
4
-
<meta charset="utf-8" />
5
-
<link rel="icon" href="%sveltekit.assets%/favicon.svg" />
6
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
7
-
%sveltekit.head%
8
-
</head>
9
-
<body data-sveltekit-preload-data="hover">
10
-
<div style="display: contents">%sveltekit.body%</div>
11
-
</body>
12
</html>
···
1
<!doctype html>
2
<html lang="en">
3
+
<head>
4
+
<meta charset="utf-8" />
5
+
<link rel="icon" href="%sveltekit.assets%/favicon.svg" />
6
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
7
+
%sveltekit.head%
8
+
</head>
9
+
<body class="bg-white dark:bg-gray-900" data-sveltekit-preload-data="hover">
10
+
<div style="display: contents">%sveltekit.body%</div>
11
+
</body>
12
</html>
+2
-2
client/src/lib/components/BskyToggle.svelte
···
11
<!-- svelte-ignore a11y_no_static_element_interactions -->
12
<button
13
onclick={onBskyToggle}
14
-
class="wsbadge !mt-0 !font-normal bg-blue-100 hover:bg-blue-200 border-blue-300"
15
>
16
<input checked={dontShowBsky} type="checkbox" />
17
-
<span class="ml-0.5"> hide app.bsky.* </span>
18
</button>
···
11
<!-- svelte-ignore a11y_no_static_element_interactions -->
12
<button
13
onclick={onBskyToggle}
14
+
class="wsbadge !mt-0 !font-normal bg-blue-100 dark:bg-blue-900 hover:bg-blue-200 dark:hover:bg-blue-800 border-blue-300 dark:border-blue-700"
15
>
16
<input checked={dontShowBsky} type="checkbox" />
17
+
<span class="ml-0.5 text-black dark:text-gray-200"> hide app.bsky.* </span>
18
</button>
+8
-5
client/src/lib/components/EventCard.svelte
···
104
</script>
105
106
<div
107
-
class="group flex flex-col gap-2 p-1.5 md:p-3 min-h-64 bg-white border border-gray-200 rounded-lg hover:shadow-lg md:hover:-translate-y-1 transition-all duration-200 transform"
108
class:has-activity={isAnimating}
109
style="--border-thickness: {borderThickness}px"
110
>
111
<div class="flex items-start gap-2">
112
<div
113
-
class="text-sm font-bold text-blue-600 bg-blue-100 px-3 py-1 rounded-full"
114
>
115
#{index + 1}
116
</div>
117
<div
118
title={event.nsid}
119
-
class="font-mono text-sm text-gray-700 mt-0.5 leading-relaxed rounded-full text-nowrap text-ellipsis overflow-hidden group-hover:overflow-visible group-hover:bg-gray-50 border-gray-100 group-hover:border transition-all px-1"
120
>
121
{event.nsid}
122
</div>
···
136
</div>
137
</div>
138
139
-
<style>
140
.has-activity {
141
position: relative;
142
transition: all 0.2s ease-out;
143
}
144
145
.has-activity::before {
0
0
146
content: "";
147
position: absolute;
148
top: calc(-1 * var(--border-thickness));
149
left: calc(-1 * var(--border-thickness));
150
right: calc(-1 * var(--border-thickness));
151
bottom: calc(-1 * var(--border-thickness));
152
-
border: var(--border-thickness) solid rgba(59, 130, 246, 0.8);
0
153
border-radius: calc(0.5rem + var(--border-thickness));
154
pointer-events: none;
155
transition: all 0.3s ease-out;
···
104
</script>
105
106
<div
107
+
class="group flex flex-col gap-2 p-1.5 md:p-3 min-h-64 bg-white dark:bg-gray-800/50 border border-gray-200 dark:border-gray-950 rounded-lg hover:shadow-lg md:hover:-translate-y-1 transition-all duration-200 transform"
108
class:has-activity={isAnimating}
109
style="--border-thickness: {borderThickness}px"
110
>
111
<div class="flex items-start gap-2">
112
<div
113
+
class="text-sm font-bold text-blue-600 bg-blue-100 dark:bg-indigo-950 px-3 py-1 rounded-full"
114
>
115
#{index + 1}
116
</div>
117
<div
118
title={event.nsid}
119
+
class="font-mono text-sm text-gray-700 dark:text-gray-300 mt-0.5 leading-relaxed rounded-full text-nowrap text-ellipsis overflow-hidden group-hover:overflow-visible group-hover:bg-gray-50 dark:group-hover:bg-gray-700 border-gray-100 dark:border-gray-900 group-hover:border transition-all px-1"
120
>
121
{event.nsid}
122
</div>
···
136
</div>
137
</div>
138
139
+
<style lang="postcss">
140
.has-activity {
141
position: relative;
142
transition: all 0.2s ease-out;
143
}
144
145
.has-activity::before {
146
+
@reference "../../app.css";
147
+
@apply border-blue-500 dark:border-blue-800;
148
content: "";
149
position: absolute;
150
top: calc(-1 * var(--border-thickness));
151
left: calc(-1 * var(--border-thickness));
152
right: calc(-1 * var(--border-thickness));
153
bottom: calc(-1 * var(--border-thickness));
154
+
border-width: var(--border-thickness);
155
+
border-style: solid;
156
border-radius: calc(0.5rem + var(--border-thickness));
157
pointer-events: none;
158
transition: all 0.3s ease-out;
+5
-3
client/src/lib/components/FilterControls.svelte
···
8
</script>
9
10
<div
11
-
class="wsbadge !pl-2 !px-1 !mt-0 !font-normal bg-blue-100 hover:bg-blue-200 border-blue-300"
12
>
13
-
<label for="filter-regex" class="text-blue-800 mr-1"> filter: </label>
0
0
14
<input
15
id="filter-regex"
16
value={filterRegex}
17
oninput={(e) => onFilterChange((e.target as HTMLInputElement).value)}
18
type="text"
19
placeholder="regex..."
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
/>
22
</div>
···
8
</script>
9
10
<div
11
+
class="wsbadge !pl-2 !px-1 !mt-0 !font-normal bg-blue-100 dark:bg-blue-900 hover:bg-blue-200 dark:hover:bg-blue-800 border-blue-300 dark:border-blue-700"
12
>
13
+
<label for="filter-regex" class="text-blue-800 dark:text-gray-200 mr-1">
14
+
filter:
15
+
</label>
16
<input
17
id="filter-regex"
18
value={filterRegex}
19
oninput={(e) => onFilterChange((e.target as HTMLInputElement).value)}
20
type="text"
21
placeholder="regex..."
22
+
class="bg-blue-50 dark:bg-blue-950 text-blue-900 dark:text-gray-400 placeholder-blue-400 dark:placeholder-blue-700 border border-blue-200 dark:border-blue-700 rounded-full px-1 outline-none focus:border-blue-400 min-w-0 w-24"
23
/>
24
</div>
+6
-4
client/src/lib/components/RefreshControl.svelte
···
8
</script>
9
10
<div
11
-
class="wsbadge !pl-2 !px-1 !mt-0 !font-normal bg-lime-100 hover:bg-lime-200 border-lime-300"
12
>
13
-
<label for="refresh-rate" class="text-lime-800 mr-1">refresh:</label>
0
0
14
<input
15
id="refresh-rate"
16
value={refreshRate}
···
24
pattern="[0-9]*"
25
min="0"
26
placeholder="real-time"
27
-
class="bg-green-50 text-lime-900 placeholder-lime-600 border border-lime-200 rounded-full px-1 outline-none focus:bg-white focus:border-lime-400 min-w-0 w-20"
28
/>
29
-
<span class="text-lime-700">s</span>
30
</div>
···
8
</script>
9
10
<div
11
+
class="wsbadge !pl-2 !px-1 !mt-0 !font-normal bg-lime-100 dark:bg-lime-900 dark:hover:bg-lime-800 hover:bg-lime-200 border-lime-300 dark:border-lime-700"
12
>
13
+
<label for="refresh-rate" class="text-lime-800 dark:text-lime-200 mr-1"
14
+
>refresh:</label
15
+
>
16
<input
17
id="refresh-rate"
18
value={refreshRate}
···
26
pattern="[0-9]*"
27
min="0"
28
placeholder="real-time"
29
+
class="bg-green-50 dark:bg-green-900 text-lime-900 dark:text-lime-200 placeholder-lime-600 dark:placeholder-lime-400 border border-lime-200 dark:border-lime-700 rounded-full px-1 outline-none focus:border-lime-400 min-w-0 w-20"
30
/>
31
+
<span class="text-lime-800 dark:text-lime-200">s</span>
32
</div>
+5
-3
client/src/lib/components/ShowControls.svelte
···
12
</script>
13
14
<div
15
-
class="wsbadge !pl-2 !px-1 !mt-0 !font-normal bg-pink-100 hover:bg-pink-200 border-pink-300"
16
>
17
-
<label for="show" class="text-pink-800 mr-1"> show since: </label>
0
0
18
<select
19
id="show"
20
value={show}
21
onchange={(e) =>
22
onShowChange((e.target as HTMLSelectElement).value as ShowOption)}
23
-
class="bg-pink-50 text-pink-900 border border-pink-200 rounded-full px-1 outline-none focus:bg-white focus:border-pink-400 min-w-0"
24
>
25
{#each showOptions as option}
26
<option value={option}>{option}</option>
···
12
</script>
13
14
<div
15
+
class="wsbadge !pl-2 !px-1 !mt-0 !font-normal bg-pink-100 dark:bg-pink-800 hover:bg-pink-200 dark:hover:bg-pink-700 border-pink-300 dark:border-pink-700"
16
>
17
+
<label for="show" class="text-pink-800 dark:text-pink-100 mr-1">
18
+
show since:
19
+
</label>
20
<select
21
id="show"
22
value={show}
23
onchange={(e) =>
24
onShowChange((e.target as HTMLSelectElement).value as ShowOption)}
25
+
class="bg-pink-50 dark:bg-pink-900 text-pink-900 dark:text-pink-100 border border-pink-200 dark:border-pink-700 rounded-full px-1 outline-none focus:border-pink-400 min-w-0"
26
>
27
{#each showOptions as option}
28
<option value={option}>{option}</option>
+5
-3
client/src/lib/components/SortControls.svelte
···
17
</script>
18
19
<div
20
-
class="wsbadge !pl-2 !px-1 !mt-0 !font-normal bg-purple-100 hover:bg-purple-200 border-purple-300"
21
>
22
-
<label for="sort-by" class="text-purple-800 mr-1"> sort by: </label>
0
0
23
<select
24
id="sort-by"
25
value={sortBy}
26
onchange={(e) =>
27
onSortChange((e.target as HTMLSelectElement).value as SortOption)}
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
>
30
{#each sortOptions as option}
31
<option value={option.value}>{option.label}</option>
···
17
</script>
18
19
<div
20
+
class="wsbadge !pl-2 !px-1 !mt-0 !font-normal bg-purple-100 dark:bg-purple-800 hover:bg-purple-200 dark:hover:bg-purple-700 border-purple-300 dark:border-purple-700"
21
>
22
+
<label for="sort-by" class="text-purple-800 dark:text-purple-300 mr-1">
23
+
sort by:
24
+
</label>
25
<select
26
id="sort-by"
27
value={sortBy}
28
onchange={(e) =>
29
onSortChange((e.target as HTMLSelectElement).value as SortOption)}
30
+
class="bg-purple-50 dark:bg-purple-900 text-purple-900 dark:text-purple-300 border border-purple-200 dark:border-purple-700 rounded-full px-1 outline-none focus:border-purple-400 min-w-0"
31
>
32
{#each sortOptions as option}
33
<option value={option.value}>{option.label}</option>
+16
-16
client/src/lib/components/StatsCard.svelte
···
3
4
const colorClasses = {
5
green: {
6
-
bg: "from-green-50 to-green-100",
7
-
border: "border-green-200",
8
-
titleText: "text-green-700",
9
-
valueText: "text-green-900",
10
},
11
red: {
12
-
bg: "from-red-50 to-red-100",
13
-
border: "border-red-200",
14
-
titleText: "text-red-700",
15
-
valueText: "text-red-900",
16
},
17
turqoise: {
18
-
bg: "from-teal-50 to-teal-100",
19
-
border: "border-teal-200",
20
-
titleText: "text-teal-700",
21
-
valueText: "text-teal-900",
22
},
23
orange: {
24
-
bg: "from-orange-50 to-orange-100",
25
-
border: "border-orange-200",
26
-
titleText: "text-orange-700",
27
-
valueText: "text-orange-900",
28
},
29
};
30
···
3
4
const colorClasses = {
5
green: {
6
+
bg: "from-green-50 to-green-100 dark:from-green-900 dark:to-green-800",
7
+
border: "border-green-200 dark:border-green-800",
8
+
titleText: "text-green-700 dark:text-green-400",
9
+
valueText: "text-green-900 dark:text-green-200",
10
},
11
red: {
12
+
bg: "from-red-50 to-red-100 dark:from-red-900 dark:to-red-800",
13
+
border: "border-red-200 dark:border-red-800",
14
+
titleText: "text-red-700 dark:text-red-400",
15
+
valueText: "text-red-900 dark:text-red-200",
16
},
17
turqoise: {
18
+
bg: "from-teal-50 to-teal-100 dark:from-teal-900 dark:to-teal-800",
19
+
border: "border-teal-200 dark:border-teal-800",
20
+
titleText: "text-teal-700 dark:text-teal-400",
21
+
valueText: "text-teal-900 dark:text-teal-200",
22
},
23
orange: {
24
+
bg: "from-orange-50 to-orange-100 dark:from-orange-900 dark:to-orange-800",
25
+
border: "border-orange-200 dark:border-orange-800",
26
+
titleText: "text-orange-700 dark:text-orange-400",
27
+
valueText: "text-orange-900 dark:text-orange-200",
28
},
29
};
30
+9
-5
client/src/lib/components/StatusBadge.svelte
···
8
const statusConfig = {
9
connected: {
10
text: "stream live",
11
-
classes: "bg-green-100 text-green-800 border-green-200",
0
12
},
13
connecting: {
14
text: "stream connecting",
15
-
classes: "bg-yellow-100 text-yellow-800 border-yellow-200",
0
16
},
17
error: {
18
text: "stream errored",
19
-
classes: "bg-red-100 text-red-800 border-red-200",
0
20
},
21
disconnected: {
22
text: "stream offline",
23
-
classes: "bg-gray-100 text-gray-800 border-gray-200",
0
24
},
25
};
26
···
31
<!-- connecting spinner -->
32
{#if status === "connecting"}
33
<div
34
-
class="animate-spin rounded-full h-4 w-4 border-b-2 border-yellow-800"
35
></div>
36
{/if}
37
<!-- status text -->
···
8
const statusConfig = {
9
connected: {
10
text: "stream live",
11
+
classes:
12
+
"bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 border-green-200 dark:border-green-800",
13
},
14
connecting: {
15
text: "stream connecting",
16
+
classes:
17
+
"bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200 border-yellow-200 dark:border-yellow-800",
18
},
19
error: {
20
text: "stream errored",
21
+
classes:
22
+
"bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200 border-red-200 dark:border-red-800",
23
},
24
disconnected: {
25
text: "stream offline",
26
+
classes:
27
+
"bg-gray-100 dark:bg-gray-900 text-gray-800 dark:text-gray-200 border-gray-200 dark:border-gray-800",
28
},
29
};
30
···
35
<!-- connecting spinner -->
36
{#if status === "connecting"}
37
<div
38
+
class="animate-spin rounded-full h-4 w-4 border-b-2 border-yellow-800 dark:border-yellow-200"
39
></div>
40
{/if}
41
<!-- status text -->
+15
-9
client/src/routes/+page.svelte
···
267
/>
268
</svelte:head>
269
270
-
<header class="border-gray-300 border-b mb-4 pb-2">
0
0
271
<div
272
class="px-2 md:ml-[19vw] mx-auto flex flex-wrap items-center text-center"
273
>
274
-
<h1 class="text-4xl font-bold mr-4 text-gray-900">lexicon tracker</h1>
275
-
<p class="text-lg mt-1 text-gray-600">
0
0
276
tracks lexicons seen on the jetstream {tracking_since === 0
277
? ""
278
: `(since: ${formatTimestamp(tracking_since)})`}
279
</p>
280
</div>
281
</header>
282
-
<div class="md:max-w-[61vw] mx-auto p-2">
283
<div class="min-w-fit grid grid-cols-2 xl:grid-cols-4 gap-2 2xl:gap-6 mb-8">
284
<StatsCard
285
title="total creation"
···
305
306
{#if error}
307
<div
308
-
class="bg-red-100 border border-red-300 text-red-700 px-4 py-3 rounded-lg mb-6"
309
>
310
<p>Error: {error}</p>
311
</div>
···
314
{#if eventsList.length > 0}
315
<div class="mb-8">
316
<div class="flex flex-wrap items-center gap-3 mb-3">
317
-
<h2 class="text-2xl font-bold text-gray-900">seen lexicons</h2>
0
0
318
<StatusBadge status={websocketStatus} />
319
</div>
320
<div class="flex flex-wrap items-center gap-1.5 mb-6">
···
367
{/if}
368
</div>
369
370
-
<footer class="py-2 border-t border-gray-200 text-center">
371
-
<p class="text-gray-600 text-sm">
372
source code <a
373
href="https://tangled.sh/@poor.dog/nsid-tracker"
374
target="_blank"
375
rel="noopener noreferrer"
376
-
class="text-blue-600 hover:text-blue-800 underline"
377
>@poor.dog/nsid-tracker</a
378
>
379
</p>
···
267
/>
268
</svelte:head>
269
270
+
<header
271
+
class="bg-white dark:bg-gray-900 border-gray-300 dark:border-gray-950 border-b mb-4 pb-2"
272
+
>
273
<div
274
class="px-2 md:ml-[19vw] mx-auto flex flex-wrap items-center text-center"
275
>
276
+
<h1 class="text-4xl font-bold mr-4 text-gray-900 dark:text-gray-200">
277
+
lexicon tracker
278
+
</h1>
279
+
<p class="text-lg mt-1 text-gray-600 dark:text-gray-300">
280
tracks lexicons seen on the jetstream {tracking_since === 0
281
? ""
282
: `(since: ${formatTimestamp(tracking_since)})`}
283
</p>
284
</div>
285
</header>
286
+
<div class="bg-white dark:bg-gray-900 md:max-w-[61vw] mx-auto p-2">
287
<div class="min-w-fit grid grid-cols-2 xl:grid-cols-4 gap-2 2xl:gap-6 mb-8">
288
<StatsCard
289
title="total creation"
···
309
310
{#if error}
311
<div
312
+
class="bg-red-100 dark:bg-red-900 border border-red-300 dark:border-red-700 text-red-700 dark:text-red-200 px-4 py-3 rounded-lg mb-6"
313
>
314
<p>Error: {error}</p>
315
</div>
···
318
{#if eventsList.length > 0}
319
<div class="mb-8">
320
<div class="flex flex-wrap items-center gap-3 mb-3">
321
+
<h2 class="text-2xl font-bold text-gray-900 dark:text-gray-200">
322
+
seen lexicons
323
+
</h2>
324
<StatusBadge status={websocketStatus} />
325
</div>
326
<div class="flex flex-wrap items-center gap-1.5 mb-6">
···
373
{/if}
374
</div>
375
376
+
<footer class="py-2 border-t border-gray-200 dark:border-gray-800 text-center">
377
+
<p class="text-gray-600 dark:text-gray-200 text-sm">
378
source code <a
379
href="https://tangled.sh/@poor.dog/nsid-tracker"
380
target="_blank"
381
rel="noopener noreferrer"
382
+
class="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-600 underline"
383
>@poor.dog/nsid-tracker</a
384
>
385
</p>