[READ-ONLY] Mirror of https://github.com/flo-bit/bluesky-visualizers. visualizing bluesky live data in different ways flo-bit.dev/bluesky-visualizers/
bluesky svelte visualizations
0

Configure Feed

Select the types of activity you want to include in your feed.

add redirect for trending hashtags

Florian (Nov 23, 2024, 3:39 PM +0100) 46b53f4d 6a2f556d

+3 -255
+3 -255
src/routes/trending/+page.svelte
··· 1 1 <script lang="ts"> 2 - import { onMount } from 'svelte'; 3 - import { relativeTime } from 'svelte-relative-time'; 2 + import { onMount } from "svelte"; 4 3 5 - import { gsap } from 'gsap'; 6 - 7 - import { Flip } from 'gsap/Flip'; 8 - import { tick } from 'svelte'; 9 - import Credit from '$lib/Credit.svelte'; 10 - 11 - gsap.registerPlugin(Flip); 12 - 13 - let wordCounts = new Map<string, number>(); 14 - let posts = 0; 15 - 16 - let lastUpdatePostTime = 0; 17 - let lastUpdateTime = 0; 18 - 19 - let mostPopularHashtags: [string, number][] = []; 20 - 21 - let startTime = 0; 22 - 4 + // redirect to trending 23 5 onMount(() => { 24 - function secondsToMicroseconds(seconds: number) { 25 - return Math.floor(seconds * 1000000); 26 - } 27 - 28 - startTime = Date.now() * 1000 - secondsToMicroseconds(60 * 60); 29 - console.log(startTime); 30 - 31 - // WebSocket URL from BlueSky 32 - const url = 33 - 'wss://jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.bsky.feed.post&cursor=' + 34 - startTime; 35 - 36 - // WebSocket logic 37 - const ws = new WebSocket(url); 38 - ws.onopen = () => { 39 - console.log('Connected to BlueSky WebSocket'); 40 - }; 41 - 42 - ws.onmessage = (event) => { 43 - const json = JSON.parse(event.data); 44 - 45 - // console.log(json); 46 - if ( 47 - json.kind === 'commit' && 48 - json.commit.collection === 'app.bsky.feed.post' && 49 - json.commit.operation === 'create' && 50 - json.commit.record.text && 51 - (json.commit.record.langs?.includes(languageCode) || 52 - (!json.commit.record.langs && languageCode === 'en')) 53 - ) { 54 - // get text of post 55 - const text: string = json.commit.record.text; 56 - 57 - // check if text contains # 58 - if (!text.includes('#')) { 59 - // console.log(json); 60 - return; 61 - } 62 - 63 - // find all hashtags 64 - const hashtags = text.match(/#(\w+)/g); 65 - 66 - hashtags?.forEach((hashtag) => { 67 - hashtag = hashtag.toLowerCase(); 68 - wordCounts.set(hashtag, (wordCounts.get(hashtag) || 0) + 1); 69 - }); 70 - 71 - // check if we're up to date 72 - 73 - const postTime = json.time_us; 74 - if (Date.now() > lastUpdateTime + 5000 && posts > 100) { 75 - lastUpdateTime = Date.now(); 76 - 77 - // update last update time 78 - lastUpdatePostTime = postTime; 79 - 80 - // get most popular hashtags 81 - update(); 82 - } 83 - 84 - posts++; 85 - } 86 - }; 87 - 88 - return () => { 89 - ws.close(); 90 - }; 6 + window.location.href = "https://flo-bit.dev/bluesky-trending/"; 91 7 }); 92 - 93 - async function update() { 94 - const state = Flip.getState('.item', { 95 - props: 'opacity,y' 96 - }); 97 - 98 - mostPopularHashtags = getMostPopularHashtags(15); 99 - 100 - await tick(); 101 - 102 - Flip.from(state, { 103 - duration: 0.5, 104 - ease: 'power3.inOut', 105 - onEnter: (el) => { 106 - gsap.from(el, { opacity: 0, y: 20, duration: 0.5 }); 107 - }, 108 - onExit: (el: HTMLElement) => { 109 - gsap.to(el, { opacity: 0, y: -20, duration: 0.5 }); 110 - } 111 - }); 112 - } 113 - 114 - function getMostPopularHashtags(num: number) { 115 - // get num most popular hashtags with counts 116 - const mostPopular = Array.from(wordCounts.entries()) 117 - .sort((a, b) => b[1] - a[1]) 118 - .slice(0, num); 119 - return mostPopular; 120 - } 121 - 122 - const languages = [ 123 - { value: 'en', label: 'English' }, 124 - { value: 'zh', label: 'Mandarin Chinese' }, 125 - { value: 'hi', label: 'Hindi' }, 126 - { value: 'es', label: 'Spanish' }, 127 - { value: 'ar', label: 'Arabic' }, 128 - { value: 'bn', label: 'Bengali' }, 129 - { value: 'fr', label: 'French' }, 130 - { value: 'ru', label: 'Russian' }, 131 - { value: 'pt', label: 'Portuguese' }, 132 - { value: 'id', label: 'Indonesian' }, 133 - { value: 'ur', label: 'Urdu' }, 134 - { value: 'ja', label: 'Japanese' }, 135 - { value: 'de', label: 'German' }, 136 - { value: 'ko', label: 'Korean' }, 137 - { value: 'vi', label: 'Vietnamese' }, 138 - { value: 'tr', label: 'Turkish' }, 139 - { value: 'it', label: 'Italian' }, 140 - { value: 'pl', label: 'Polish' }, 141 - { value: 'uk', label: 'Ukrainian' }, 142 - { value: 'nl', label: 'Dutch' } 143 - ]; 144 - 145 - let showLanguageSelector = false; 146 - 147 - let languageCode = 'en'; 148 - let language = 'English'; 149 8 </script> 150 - 151 - <div class="absolute left-0 top-0 flex w-full items-center justify-between px-4 py-2"> 152 - <Credit name="trending hashtags" /> 153 - </div> 154 - 155 - <div class="mx-auto max-w-xl px-4 py-24 text-white"> 156 - <div class="flex items-center gap-2 text-2xl font-bold"> 157 - Most Popular Hashtags on <svg 158 - fill="currentColor" 159 - xmlns="http://www.w3.org/2000/svg" 160 - viewBox="-40 -40 680 620" 161 - version="1.1" 162 - class="size-7 fill-cyan-400" 163 - > 164 - <path 165 - d="m135.72 44.03c66.496 49.921 138.02 151.14 164.28 205.46 26.262-54.316 97.782-155.54 164.28-205.46 47.98-36.021 125.72-63.892 125.72 24.795 0 17.712-10.155 148.79-16.111 170.07-20.703 73.984-96.144 92.854-163.25 81.433 117.3 19.964 147.14 86.092 82.697 152.22-122.39 125.59-175.91-31.511-189.63-71.766-2.514-7.3797-3.6904-10.832-3.7077-7.8964-0.0174-2.9357-1.1937 0.51669-3.7077 7.8964-13.714 40.255-67.233 197.36-189.63 71.766-64.444-66.128-34.605-132.26 82.697-152.22-67.108 11.421-142.55-7.4491-163.25-81.433-5.9562-21.282-16.111-152.36-16.111-170.07 0-88.687 77.742-60.816 125.72-24.795z" 166 - ></path> 167 - </svg> 168 - </div> 169 - <div class="mt-2 text-sm text-gray-400"> 170 - {#if lastUpdatePostTime > 0} 171 - of all posts between <span use:relativeTime={{ date: startTime / 1000 }}></span> and 172 - <span use:relativeTime={{ date: lastUpdatePostTime / 1000 }}></span>. analysing more... 173 - {:else} 174 - analysing posts... 175 - {/if} 176 - </div> 177 - 178 - {#if !showLanguageSelector} 179 - <button 180 - class="mt-2 text-sm font-semibold text-gray-100 hover:text-cyan-400" 181 - on:click={() => (showLanguageSelector = true)} 182 - > 183 - {language} (change language) 184 - </button> 185 - {:else} 186 - <div class="mt-2"> 187 - <label for="location" class="block text-sm/6 font-medium text-gray-50">language</label> 188 - <select 189 - on:change={(e) => { 190 - showLanguageSelector = false; 191 - languageCode = (e.target as HTMLSelectElement).value; 192 - language = languages.find((l) => l.value === languageCode)?.label || 'English'; 193 - 194 - wordCounts.clear(); 195 - update(); 196 - }} 197 - id="location" 198 - name="location" 199 - class="mt-2 block w-full rounded-md border-0 bg-gray-900 py-1.5 pl-3 pr-10 text-gray-50 ring-1 ring-inset ring-gray-800 focus:ring-2 focus:ring-cyan-600 sm:text-sm/6" 200 - > 201 - {#each languages as language} 202 - <option selected={languageCode === language.value} value={language.value} 203 - >{language.label}</option 204 - > 205 - {/each} 206 - </select> 207 - </div> 208 - {/if} 209 - 210 - <div class="mt-8"> 211 - {#if mostPopularHashtags.length === 0} 212 - <svg 213 - width="24" 214 - height="24" 215 - viewBox="0 0 24 24" 216 - xmlns="http://www.w3.org/2000/svg" 217 - class="mx-auto mt-16 size-16 text-cyan-400" 218 - fill="currentColor" 219 - ><style> 220 - .spinner_b2T7 { 221 - animation: spinner_xe7Q 0.8s linear infinite; 222 - } 223 - .spinner_YRVV { 224 - animation-delay: -0.65s; 225 - } 226 - .spinner_c9oY { 227 - animation-delay: -0.5s; 228 - } 229 - @keyframes spinner_xe7Q { 230 - 93.75%, 231 - 100% { 232 - r: 3px; 233 - } 234 - 46.875% { 235 - r: 0.2px; 236 - } 237 - } 238 - </style><circle class="spinner_b2T7" cx="4" cy="12" r="3" /><circle 239 - class="spinner_b2T7 spinner_YRVV" 240 - cx="12" 241 - cy="12" 242 - r="3" 243 - /><circle class="spinner_b2T7 spinner_c9oY" cx="20" cy="12" r="3" /></svg 244 - > 245 - {/if} 246 - {#each mostPopularHashtags as hashtag} 247 - <a 248 - href={'https://bsky.app/hashtag/' + hashtag[0].split('#')[1]} 249 - target="_blank" 250 - class="item group mb-2 flex w-fit items-baseline rounded-xl border border-white/10 bg-white/5 px-2 py-1 text-lg font-semibold" 251 - data-flip-id={hashtag[0]} 252 - > 253 - <span class="text-cyan-100 transition-colors duration-150 group-hover:text-cyan-400" 254 - >{hashtag[0]}</span 255 - > 256 - <span class="ml-2 text-xs text-gray-200">({hashtag[1]}x)</span> 257 - </a> 258 - {/each} 259 - </div> 260 - </div>