tracks lexicons and how many times they appeared on the jetstream
1<script lang="ts">
2 import { formatNumber, formatTimestamp } from "$lib/format";
3 import type { NsidCount } from "$lib/types";
4 import { onMount, onDestroy } from "svelte";
5
6 interface Props {
7 event: NsidCount;
8 index: number;
9 }
10
11 let { event, index }: Props = $props();
12
13 // Border animation state
14 let borderThickness = $state(0);
15 let lastEventTime = $state(0);
16 let lastCount = $state(0);
17 let lastDeletedCount = $state(0);
18 let decayTimer: ReturnType<typeof setTimeout> | null = null;
19 let isAnimating = $state(false);
20
21 // Constants for border behavior
22 const MAX_BORDER_THICKNESS = 6; // Maximum border thickness in pixels
23 const INITIAL_THICKNESS_ADD = 2; // How much thickness to add for first/slow events
24 const RAPID_SUCCESSION_THRESHOLD = 50; // ms - events faster than this are considered rapid
25 const DECAY_RATE = 0.1; // How much thickness to remove per decay tick
26 const DECAY_INTERVAL = 45; // ms between decay ticks
27
28 // Track when event data changes
29 $effect(() => {
30 const currentTime = Date.now();
31 const hasChanged =
32 event.count !== lastCount ||
33 event.deleted_count !== lastDeletedCount;
34
35 if (hasChanged && (lastCount > 0 || lastDeletedCount > 0)) {
36 const timeSinceLastUpdate = currentTime - lastEventTime;
37
38 // Calculate how much thickness to add based on timing
39 let thicknessToAdd;
40 if (timeSinceLastUpdate < RAPID_SUCCESSION_THRESHOLD) {
41 // Rapid succession - add less thickness with a decay factor
42 const rapidnessFactor = Math.max(
43 0.1,
44 timeSinceLastUpdate / RAPID_SUCCESSION_THRESHOLD,
45 );
46 thicknessToAdd = INITIAL_THICKNESS_ADD * rapidnessFactor * 0.5;
47 } else {
48 // Normal/slow event - add full thickness
49 thicknessToAdd = INITIAL_THICKNESS_ADD;
50 }
51
52 // Add thickness but cap at maximum
53 borderThickness = Math.min(
54 MAX_BORDER_THICKNESS,
55 borderThickness + thicknessToAdd,
56 );
57 isAnimating = true;
58 lastEventTime = currentTime;
59
60 // Start/restart continuous decay
61 if (decayTimer) {
62 clearTimeout(decayTimer);
63 }
64 decayTimer = setTimeout(startDecay, DECAY_INTERVAL);
65 }
66
67 lastCount = event.count;
68 lastDeletedCount = event.deleted_count;
69 });
70
71 const startDecay = () => {
72 if (borderThickness <= 0) {
73 isAnimating = false;
74 decayTimer = null;
75 return;
76 }
77
78 borderThickness = Math.max(0, borderThickness - DECAY_RATE);
79
80 if (borderThickness > 0) {
81 decayTimer = setTimeout(startDecay, DECAY_INTERVAL);
82 } else {
83 isAnimating = false;
84 decayTimer = null;
85 }
86 };
87
88 onMount(() => {
89 // Initialize with current values to avoid triggering animation on mount
90 lastCount = event.count;
91 lastDeletedCount = event.deleted_count;
92 lastEventTime = Date.now();
93
94 // Start continuous decay immediately
95 decayTimer = setTimeout(startDecay, DECAY_INTERVAL);
96 });
97
98 onDestroy(() => {
99 // Clean up decay timer
100 if (decayTimer) {
101 clearTimeout(decayTimer);
102 }
103 });
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>
123 </div>
124 <div class="mt-auto flex flex-col gap-1">
125 <div class="text-3xl font-bold text-green-600">
126 {formatNumber(event.count)}
127 <div class="text-xl">created</div>
128 </div>
129 <div class="text-3xl font-bold text-red-600">
130 {formatNumber(event.deleted_count)}
131 <div class="text-xl">deleted</div>
132 </div>
133 </div>
134 <div class="text-xs text-gray-500 mt-auto">
135 last: {formatTimestamp(event.last_seen)}
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 {
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);
153 border-radius: calc(0.5rem + var(--border-thickness));
154 pointer-events: none;
155 transition: all 0.3s ease-out;
156 }
157</style>