tracks lexicons and how many times they appeared on the jetstream
at main 5.6 kB view raw
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 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> 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 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; 159 } 160</style>