From 1d7b47a0b6d01cf4de04b831d637871e213f697c Mon Sep 17 00:00:00 2001 From: = Date: Wed, 2 Jul 2025 11:41:20 +0100 Subject: [PATCH] feat: search for users by name on the leaderboard page. --- website/src/routes/api/leaderboard/+server.ts | 41 ++- website/src/routes/leaderboard/+page.svelte | 266 ++++++++++++------ 2 files changed, 215 insertions(+), 92 deletions(-) diff --git a/website/src/routes/api/leaderboard/+server.ts b/website/src/routes/api/leaderboard/+server.ts index 4401654..5b14b1e 100644 --- a/website/src/routes/api/leaderboard/+server.ts +++ b/website/src/routes/api/leaderboard/+server.ts @@ -1,9 +1,9 @@ import { json } from '@sveltejs/kit'; import { db } from '$lib/server/db'; import { user, transaction, userPortfolio, coin } from '$lib/server/db/schema'; -import { eq, desc, gte, and, sql, inArray } from 'drizzle-orm'; +import { eq, desc, gte, and, sql, inArray, ilike } from 'drizzle-orm'; -export async function GET() { +async function getLeaderboardData() { try { const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000); @@ -183,3 +183,40 @@ export async function GET() { }); } } + +async function getSearchedUsers(query: string) { + try { + return await db.select({ + userId: user.id, + username: user.username, + name: user.name, + image: user.image, + coinsValue: sql`COALESCE(SUM(CAST(${userPortfolio.quantity} AS NUMERIC) * CAST(${coin.currentPrice} AS NUMERIC)), 0)`, + balanceValue: user.baseCurrencyBalance, + totalSold: sql`COALESCE(SUM(CASE WHEN ${transaction.type} = 'SELL' THEN CAST(${transaction.totalBaseCurrencyAmount} AS NUMERIC) ELSE 0 END), 0)`, + totalBought: sql`COALESCE(SUM(CASE WHEN ${transaction.type} = 'BUY' THEN CAST(${transaction.totalBaseCurrencyAmount} AS NUMERIC) ELSE 0 END), 0)` + }).from(user) + .leftJoin(userPortfolio, eq(userPortfolio.userId, user.id)) + .leftJoin(coin, eq(coin.id, userPortfolio.coinId)) + .leftJoin(transaction, eq(transaction.userId, user.id)) + .where(ilike(user.username, `%${query}%`)) + .groupBy(user.id, user.username, user.name, user.image) + .orderBy(desc(sql`COALESCE(SUM(CAST(${userPortfolio.quantity} AS NUMERIC) * CAST(${coin.currentPrice} AS NUMERIC)), 0)`)) + .limit(2); + } catch (error) { + return []; + } +} + +export async function GET({ url }) { + const query = url.searchParams.get('search'); + + if(query?.trim() !== '' && query !== null) { + let users = await getSearchedUsers(query); + return json({ + results: users + }); + } + + return await getLeaderboardData(); +} diff --git a/website/src/routes/leaderboard/+page.svelte b/website/src/routes/leaderboard/+page.svelte index 979e434..370ab0a 100644 --- a/website/src/routes/leaderboard/+page.svelte +++ b/website/src/routes/leaderboard/+page.svelte @@ -7,20 +7,51 @@ import { onMount } from 'svelte'; import { toast } from 'svelte-sonner'; import { goto } from '$app/navigation'; - import { TrendingDown, Crown, Skull, Target, RefreshCw, Trophy } from 'lucide-svelte'; + import { TrendingDown, Crown, Skull, Target, RefreshCw, Trophy, Search, SearchX, X } from 'lucide-svelte'; import { formatValue } from '$lib/utils'; + import Input from '$lib/components/ui/input/input.svelte'; + import * as HoverCard from '$lib/components/ui/hover-card'; + import * as Avatar from '$lib/components/ui/avatar'; + import { getPublicUrl } from '$lib/utils'; + import UserProfilePreview from '$lib/components/self/UserProfilePreview.svelte'; + let searchQuery = $state(''); + let searchQueryValue = $state(''); + let searchQueryTimeout = $state(null); let leaderboardData = $state(null); let loading = $state(true); onMount(async () => { + const urlParams = new URLSearchParams(window.location.search); + const search = urlParams.get('search'); + + searchQuery = search || ''; + searchQueryValue = search || ''; + await fetchLeaderboardData(); }); + function handleSearchKeydown(event: KeyboardEvent) { + if (searchQueryTimeout) { + clearTimeout(searchQueryTimeout); + } + + searchQueryTimeout = setTimeout(() => { + searchQueryValue = searchQuery; + fetchLeaderboardData(); + }, 500); + + if (event.key === 'Enter') { + clearTimeout(searchQueryTimeout); + searchQueryValue = searchQuery; + fetchLeaderboardData(); + } + } + async function fetchLeaderboardData() { loading = true; try { - const response = await fetch('/api/leaderboard'); + const response = await fetch(`/api/leaderboard?search=${searchQueryValue}`); if (response.ok) { leaderboardData = await response.json(); } else { @@ -54,6 +85,19 @@ return { text: 'Mostly liquid', color: 'text-success' }; } + const searchColumns = [ + { + key: 'user', + label: 'User', + render: (value: any, row: any) => ({ + component: 'user', + image: row.image, + name: row.name, + username: row.username + }) + } + ] + const rugpullersColumns = [ { key: 'rank', @@ -209,10 +253,24 @@

Leaderboard

Top performers and market activity

- +
+
+ + +
+ {#if searchQueryValue} + + {/if} + +
@@ -227,93 +285,121 @@ {:else}
- - - - - - Top Rugpullers (24h) - - - Users who made the most profit from selling coins today - - - - goto(`/user/${user.userUsername || user.username}`)} - emptyMessage="No major profits recorded today" - enableUserPreview={true} - /> - - + {#if searchQueryValue} + {#if leaderboardData.results.length > 0} +
+
+ {#each leaderboardData.results as user} +
+ + + +
+ {/each} +
+
+ {:else} +
+

No users found

+

No users match your search "{searchQueryValue}"

+ +
+ {/if} + {:else} + + + + + + Top Rugpullers (24h) + + + Users who made the most profit from selling coins today + + + + goto(`/user/${user.userUsername || user.username}`)} + emptyMessage="No major profits recorded today" + enableUserPreview={true} + /> + + - - - - - - Biggest Losses (24h) - - Users who experienced the largest losses today - - - goto(`/user/${user.userUsername || user.username}`)} - emptyMessage="No major losses recorded today" - enableUserPreview={true} - /> - - + + + + + + Biggest Losses (24h) + + Users who experienced the largest losses today + + + goto(`/user/${user.userUsername || user.username}`)} + emptyMessage="No major losses recorded today" + enableUserPreview={true} + /> + + - - - - - - Top Cash Holders - - Users with the highest liquid cash balances - - - goto(`/user/${user.userUsername || user.username}`)} - emptyMessage="Everyone's invested! 💸" - enableUserPreview={true} - /> - - + + + + + + Top Cash Holders + + Users with the highest liquid cash balances + + + goto(`/user/${user.userUsername || user.username}`)} + emptyMessage="Everyone's invested! 💸" + enableUserPreview={true} + /> + + - - - - - - Highest Portfolio Values - - Users with the largest total portfolio valuations (including illiquid) - - - goto(`/user/${user.userUsername || user.username}`)} - emptyMessage="No large portfolios yet! 📉" - enableUserPreview={true} - /> - - + + + + + + Highest Portfolio Values + + Users with the largest total portfolio valuations (including illiquid) + + + goto(`/user/${user.userUsername || user.username}`)} + emptyMessage="No large portfolios yet! 📉" + enableUserPreview={true} + /> + + + {/if}
{/if}