diff --git a/node_modules/@tailwindcss/oxide-win32-x64-msvc/tailwindcss-oxide.win32-x64-msvc.node b/node_modules/@tailwindcss/oxide-win32-x64-msvc/tailwindcss-oxide.win32-x64-msvc.node new file mode 100644 index 0000000..76b9de0 Binary files /dev/null and b/node_modules/@tailwindcss/oxide-win32-x64-msvc/tailwindcss-oxide.win32-x64-msvc.node differ diff --git a/website/src/lib/components/self/UserProfilePreview.svelte b/website/src/lib/components/self/UserProfilePreview.svelte index 8fc1ff4..0b23b32 100644 --- a/website/src/lib/components/self/UserProfilePreview.svelte +++ b/website/src/lib/components/self/UserProfilePreview.svelte @@ -7,7 +7,7 @@ import { Calendar, Wallet } from 'lucide-svelte'; import type { UserProfileData } from '$lib/types/user-profile'; - let { userId }: { userId: number } = $props(); + let { userId, showBio = true }: { userId: number, showBio?: boolean } = $props(); let userData = $state(null); let loading = $state(true); @@ -93,8 +93,8 @@

@{profile.username}

- {#if profile.bio} -

{profile.bio}

+ {#if profile.bio && showBio} +

{profile.bio}

{/if} {#if stats} diff --git a/website/src/lib/components/self/skeletons/LeaderboardSearchSkeleton.svelte b/website/src/lib/components/self/skeletons/LeaderboardSearchSkeleton.svelte new file mode 100644 index 0000000..fb80a45 --- /dev/null +++ b/website/src/lib/components/self/skeletons/LeaderboardSearchSkeleton.svelte @@ -0,0 +1,30 @@ + + +
+
+
+ {#each Array(9) as _} +
+ +
+ + +
+
+ + +
+
+ + +
+ +
+
+
+ {/each} +
+
+
diff --git a/website/src/routes/api/leaderboard/+server.ts b/website/src/routes/api/leaderboard/+server.ts index 5b14b1e..67a2c83 100644 --- a/website/src/routes/api/leaderboard/+server.ts +++ b/website/src/routes/api/leaderboard/+server.ts @@ -1,7 +1,7 @@ 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, ilike } from 'drizzle-orm'; +import { eq, desc, gte, and, sql, inArray, ilike, count } from 'drizzle-orm'; async function getLeaderboardData() { try { @@ -184,27 +184,38 @@ async function getLeaderboardData() { } } -async function getSearchedUsers(query: string) { +async function getSearchedUsers(query: string, limit = 9, offset = 0) { try { - return await db.select({ - userId: user.id, - username: user.username, + const results = await db.select({ + id: user.id, name: user.name, + username: user.username, 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)` + bio: user.bio, + baseCurrencyBalance: user.baseCurrencyBalance, + coinValue: sql`COALESCE(SUM(CAST(${userPortfolio.quantity} AS NUMERIC) * CAST(${coin.currentPrice} AS NUMERIC)), 0)`, + totalPortfolioValue: sql`CAST(${user.baseCurrencyBalance} AS NUMERIC) + COALESCE(SUM(CAST(${userPortfolio.quantity} AS NUMERIC) * CAST(${coin.currentPrice} AS NUMERIC)), 0)`, + createdAt: user.createdAt }).from(user) .leftJoin(userPortfolio, eq(userPortfolio.userId, user.id)) .leftJoin(coin, eq(coin.id, userPortfolio.coinId)) - .leftJoin(transaction, eq(transaction.userId, user.id)) + .groupBy(user.id, user.name, user.username, user.image, user.bio, user.baseCurrencyBalance) .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); + .orderBy(desc(user.username)) + .limit(limit) + .offset(offset); + + const total = await db.select({ count: count() }).from(user).where(ilike(user.username, `%${query}%`)).limit(1); + + return { + results, + total: total[0].count + } } catch (error) { - return []; + return { + results: [], + total: 0 + } } } @@ -212,10 +223,12 @@ 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 - }); + const limit = parseInt(url.searchParams.get('limit') || '9'); + const offset = parseInt(url.searchParams.get('offset') || '0'); + + let users = await getSearchedUsers(query, limit, offset); + + return json(users); } return await getLeaderboardData(); diff --git a/website/src/routes/leaderboard/+page.svelte b/website/src/routes/leaderboard/+page.svelte index 370ab0a..f2e43fb 100644 --- a/website/src/routes/leaderboard/+page.svelte +++ b/website/src/routes/leaderboard/+page.svelte @@ -7,14 +7,15 @@ import { onMount } from 'svelte'; import { toast } from 'svelte-sonner'; import { goto } from '$app/navigation'; - import { TrendingDown, Crown, Skull, Target, RefreshCw, Trophy, Search, SearchX, X } from 'lucide-svelte'; - import { formatValue } from '$lib/utils'; + import { TrendingDown, Crown, Skull, Target, RefreshCw, Trophy, Search, SearchX, X, Wallet, Calendar } from 'lucide-svelte'; + import { formatValue, getPublicUrl } 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'; + import LeaderboardSearchSkeleton from '$lib/components/self/skeletons/LeaderboardSearchSkeleton.svelte'; + import * as Avatar from '$lib/components/ui/avatar'; + import ProfileBadges from '$lib/components/self/ProfileBadges.svelte'; + let searchOffset = $state(0); let searchQuery = $state(''); let searchQueryValue = $state(''); let searchQueryTimeout = $state(null); @@ -32,26 +33,29 @@ }); function handleSearchKeydown(event: KeyboardEvent) { + if (!/^[a-zA-Z0-9]$/.test(event.key) && event.key !== 'Enter') { + return; + } if (searchQueryTimeout) { clearTimeout(searchQueryTimeout); } + if (event.key === 'Enter') { + searchQueryValue = searchQuery; + fetchLeaderboardData(); + return; + } + searchQueryTimeout = setTimeout(() => { searchQueryValue = searchQuery; fetchLeaderboardData(); }, 500); - - if (event.key === 'Enter') { - clearTimeout(searchQueryTimeout); - searchQueryValue = searchQuery; - fetchLeaderboardData(); - } } - async function fetchLeaderboardData() { + async function fetchLeaderboardData(offset = 0) { loading = true; try { - const response = await fetch(`/api/leaderboard?search=${searchQueryValue}`); + const response = await fetch(`/api/leaderboard?search=${searchQueryValue}&offset=${offset}`); if (response.ok) { leaderboardData = await response.json(); } else { @@ -254,9 +258,9 @@

Top performers and market activity

-
+
- +
{#if searchQueryValue} {/if} -
- {#if loading} - + {#if searchQueryValue} + + {:else} + + {/if} {:else if !leaderboardData}
@@ -287,17 +294,73 @@
{#if searchQueryValue} {#if leaderboardData.results.length > 0} -
-
- {#each leaderboardData.results as user} - +
+

Showing {1 + searchOffset} - {Math.min(leaderboardData.results.length, 9) + searchOffset} of {leaderboardData.total} results

+
+ {#each Array(Math.ceil(leaderboardData.total / 9)) as _, index} + + {/each} +
-
{:else}

No users found