your personal website on atproto - mirror blento.app

add npmx leaderboard (hidden)

+146 -2
+3 -1
src/lib/cards/index.ts
··· 41 41 import { ProductHuntCardDefinition } from './social/ProductHuntCard'; 42 42 import { KickstarterCardDefinition } from './social/KickstarterCard'; 43 43 import { NpmxLikesCardDefinition } from './social/NpmxLikesCard'; 44 + import { NpmxLikesLeaderboardCardDefinition } from './social/NpmxLikesLeaderboardCard'; 44 45 // import { Model3DCardDefinition } from './visual/Model3DCard'; 45 46 46 47 export const AllCardDefinitions = [ ··· 86 87 GitHubContributorsCardDefinition, 87 88 ProductHuntCardDefinition, 88 89 KickstarterCardDefinition, 89 - NpmxLikesCardDefinition 90 + NpmxLikesCardDefinition, 91 + NpmxLikesLeaderboardCardDefinition 90 92 ] as const; 91 93 92 94 export const CardDefinitionsByType = AllCardDefinitions.reduce(
+116
src/lib/cards/social/NpmxLikesLeaderboardCard/NpmxLikesLeaderboardCard.svelte
··· 1 + <script lang="ts"> 2 + import type { Item } from '$lib/types'; 3 + import { onMount } from 'svelte'; 4 + import { getAdditionalUserData, getDidContext, getHandleContext } from '$lib/website/context'; 5 + import { CardDefinitionsByType } from '../..'; 6 + 7 + interface LeaderboardEntry { 8 + subjectRef: string; 9 + totalLikes: number; 10 + } 11 + 12 + interface LeaderboardData { 13 + totalLikes: number; 14 + totalUniqueLikers: number; 15 + leaderBoard: LeaderboardEntry[]; 16 + } 17 + 18 + let { item }: { item: Item } = $props(); 19 + 20 + const data = getAdditionalUserData(); 21 + // svelte-ignore state_referenced_locally 22 + let leaderboard = $state(data[item.cardType] as LeaderboardData | undefined); 23 + 24 + let did = getDidContext(); 25 + let handle = getHandleContext(); 26 + 27 + onMount(async () => { 28 + if (leaderboard) return; 29 + 30 + leaderboard = (await CardDefinitionsByType[item.cardType]?.loadData?.([], { 31 + did, 32 + handle 33 + })) as LeaderboardData | undefined; 34 + 35 + data[item.cardType] = leaderboard; 36 + }); 37 + 38 + function getPackageName(entry: LeaderboardEntry): string { 39 + return entry.subjectRef.split('/package/')[1] ?? entry.subjectRef; 40 + } 41 + </script> 42 + 43 + {#snippet leaderboardRow(entry: LeaderboardEntry, index: number)} 44 + <div 45 + class="hover:bg-base-100 dark:hover:bg-base-800 accent:hover:bg-white/10 flex w-full items-center gap-3 rounded-lg px-2 py-1.5 transition-colors" 46 + > 47 + <div 48 + class="text-base-600 dark:text-base-400 accent:text-white/60 w-6 shrink-0 text-right text-xs font-medium" 49 + > 50 + #{index + 1} 51 + </div> 52 + <div class="min-w-0 flex-1"> 53 + <div class="inline-flex w-full max-w-full items-center justify-between gap-2"> 54 + <div 55 + class="text-accent-500 accent:text-accent-50 dark:text-accent-400 min-w-0 flex-1 shrink truncate text-sm font-semibold" 56 + > 57 + {getPackageName(entry)} 58 + </div> 59 + <div 60 + class="text-base-500 dark:text-base-400 accent:text-white/60 flex shrink-0 items-center gap-1 text-xs" 61 + > 62 + <svg 63 + xmlns="http://www.w3.org/2000/svg" 64 + viewBox="0 0 24 24" 65 + fill="currentColor" 66 + class="accent:text-accent-200 text-accent-400 size-3.5" 67 + > 68 + <path 69 + d="m11.645 20.91-.007-.003-.022-.012a15.247 15.247 0 0 1-.383-.218 25.18 25.18 0 0 1-4.244-3.17C4.688 15.36 2.25 12.174 2.25 8.25 2.25 5.322 4.714 3 7.688 3A5.5 5.5 0 0 1 12 5.052 5.5 5.5 0 0 1 16.313 3c2.973 0 5.437 2.322 5.437 5.25 0 3.925-2.438 7.111-4.739 9.256a25.175 25.175 0 0 1-4.244 3.17 15.247 15.247 0 0 1-.383.219l-.022.012-.007.004-.003.001a.752.752 0 0 1-.704 0l-.003-.001Z" 70 + /> 71 + </svg> 72 + {entry.totalLikes} 73 + </div> 74 + </div> 75 + </div> 76 + </div> 77 + {/snippet} 78 + 79 + <div class="z-10 flex h-full w-full flex-col overflow-hidden"> 80 + {#if leaderboard && leaderboard.leaderBoard.length > 0} 81 + <div class="flex min-h-0 flex-1 flex-col gap-1 overflow-y-auto p-4 pb-10"> 82 + {#each leaderboard.leaderBoard as entry, index (entry.subjectRef)} 83 + <a 84 + href="https://npmx.dev/package/{getPackageName(entry)}" 85 + target="_blank" 86 + rel="noopener noreferrer" 87 + class="w-full" 88 + > 89 + {@render leaderboardRow(entry, index)} 90 + </a> 91 + {/each} 92 + </div> 93 + <div 94 + class="pointer-events-none absolute inset-x-0 bottom-0 z-10 h-12 bg-linear-to-t from-base-200 from-40% to-transparent dark:from-base-950 accent:from-accent-500" 95 + ></div> 96 + <div 97 + class="text-base-500 dark:text-base-400 accent:text-white/60 bg-base-200 dark:bg-base-950/50 accent:bg-accent-500/20 relative z-10 flex shrink-0 items-center justify-center gap-3 px-4 pb-3 text-xs" 98 + > 99 + <span>{leaderboard.totalLikes} likes</span> 100 + <span class="text-base-300 dark:text-base-600 accent:text-white/20">&middot;</span> 101 + <span>{leaderboard.totalUniqueLikers} unique likers</span> 102 + </div> 103 + {:else if leaderboard} 104 + <div 105 + class="text-base-500 dark:text-base-400 accent:text-white/60 flex h-full items-center justify-center p-4 text-center text-sm" 106 + > 107 + No leaderboard data. 108 + </div> 109 + {:else} 110 + <div 111 + class="text-base-500 dark:text-base-400 accent:text-white/60 flex h-full items-center justify-center p-4 text-center text-sm" 112 + > 113 + Loading leaderboard... 114 + </div> 115 + {/if} 116 + </div>
+26
src/lib/cards/social/NpmxLikesLeaderboardCard/index.ts
··· 1 + import type { CardDefinition } from '../../types'; 2 + import NpmxLikesLeaderboardCard from './NpmxLikesLeaderboardCard.svelte'; 3 + 4 + export const NpmxLikesLeaderboardCardDefinition = { 5 + type: 'npmxLikesLeaderboard', 6 + contentComponent: NpmxLikesLeaderboardCard, 7 + createNew: (card) => { 8 + card.w = 4; 9 + card.mobileW = 8; 10 + card.h = 4; 11 + card.mobileH = 6; 12 + }, 13 + loadData: async () => { 14 + const res = await fetch('https://blento.app/api/npmx-leaderboard'); 15 + const data = await res.json(); 16 + return data; 17 + }, 18 + minW: 3, 19 + canHaveLabel: true, 20 + 21 + keywords: ['npm', 'package', 'npmx', 'likes', 'leaderboard', 'ranking'], 22 + name: 'npmx Likes Leaderboard', 23 + 24 + //groups: ['Social'], 25 + icon: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="size-4"><path stroke-linecap="round" stroke-linejoin="round" d="M16.5 18.75h-9m9 0a3 3 0 0 1 3 3h-15a3 3 0 0 1 3-3m9 0v-4.5A3.375 3.375 0 0 0 13.125 10.875h-2.25A3.375 3.375 0 0 0 7.5 14.25v4.5m6-6V6.375a3.375 3.375 0 0 0-3-3.353A3.375 3.375 0 0 0 7.5 6.375v1.5" /></svg>` 26 + } as CardDefinition & { type: 'npmxLikesLeaderboard' };
+1 -1
src/routes/api/npmx-leaderboard/+server.ts
··· 2 2 import type { RequestHandler } from './$types'; 3 3 4 4 const LEADERBOARD_API_URL = 5 - 'https://npmx-likes-leaderboard-api-production.up.railway.app/api/leaderboard/likes'; 5 + 'https://npmx-likes-leaderboard-api-production.up.railway.app/api/leaderboard/likes?limit=20'; 6 6 7 7 export const GET: RequestHandler = async ({ platform }) => { 8 8 const cacheKey = '#npmx-leaderboard:likes';