feat: search for users by name on the leaderboard page.
This commit is contained in:
parent
75abbb3d51
commit
1d7b47a0b6
2 changed files with 215 additions and 92 deletions
|
|
@ -1,9 +1,9 @@
|
||||||
import { json } from '@sveltejs/kit';
|
import { json } from '@sveltejs/kit';
|
||||||
import { db } from '$lib/server/db';
|
import { db } from '$lib/server/db';
|
||||||
import { user, transaction, userPortfolio, coin } from '$lib/server/db/schema';
|
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 {
|
try {
|
||||||
const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
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<number>`COALESCE(SUM(CAST(${userPortfolio.quantity} AS NUMERIC) * CAST(${coin.currentPrice} AS NUMERIC)), 0)`,
|
||||||
|
balanceValue: user.baseCurrencyBalance,
|
||||||
|
totalSold: sql<number>`COALESCE(SUM(CASE WHEN ${transaction.type} = 'SELL' THEN CAST(${transaction.totalBaseCurrencyAmount} AS NUMERIC) ELSE 0 END), 0)`,
|
||||||
|
totalBought: sql<number>`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();
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,20 +7,51 @@
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import { goto } from '$app/navigation';
|
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 { 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<NodeJS.Timeout | null>(null);
|
||||||
let leaderboardData = $state<any>(null);
|
let leaderboardData = $state<any>(null);
|
||||||
let loading = $state(true);
|
let loading = $state(true);
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const search = urlParams.get('search');
|
||||||
|
|
||||||
|
searchQuery = search || '';
|
||||||
|
searchQueryValue = search || '';
|
||||||
|
|
||||||
await fetchLeaderboardData();
|
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() {
|
async function fetchLeaderboardData() {
|
||||||
loading = true;
|
loading = true;
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/leaderboard');
|
const response = await fetch(`/api/leaderboard?search=${searchQueryValue}`);
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
leaderboardData = await response.json();
|
leaderboardData = await response.json();
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -54,6 +85,19 @@
|
||||||
return { text: 'Mostly liquid', color: 'text-success' };
|
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 = [
|
const rugpullersColumns = [
|
||||||
{
|
{
|
||||||
key: 'rank',
|
key: 'rank',
|
||||||
|
|
@ -209,11 +253,25 @@
|
||||||
<h1 class="text-2xl font-bold md:text-3xl">Leaderboard</h1>
|
<h1 class="text-2xl font-bold md:text-3xl">Leaderboard</h1>
|
||||||
<p class="text-muted-foreground text-sm md:text-base">Top performers and market activity</p>
|
<p class="text-muted-foreground text-sm md:text-base">Top performers and market activity</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="flex items-center relative">
|
||||||
|
<Search size={16} class="absolute left-3 pointer-events-none"></Search>
|
||||||
|
<Input type="text" placeholder="Search" class="pl-10" bind:value={searchQuery} onkeydown={handleSearchKeydown} />
|
||||||
|
</div>
|
||||||
|
{#if searchQueryValue}
|
||||||
|
<Button variant="outline" onclick={() => {
|
||||||
|
searchQuery = '';
|
||||||
|
searchQueryValue = '';
|
||||||
|
fetchLeaderboardData();
|
||||||
|
}} disabled={loading} class="w-fit">
|
||||||
|
<X class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
<Button variant="outline" onclick={fetchLeaderboardData} disabled={loading} class="w-fit">
|
<Button variant="outline" onclick={fetchLeaderboardData} disabled={loading} class="w-fit">
|
||||||
<RefreshCw class="h-4 w-4" />
|
<RefreshCw class="h-4 w-4" />
|
||||||
Refresh
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{#if loading}
|
{#if loading}
|
||||||
|
|
@ -227,6 +285,33 @@
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="grid gap-4 md:gap-6 xl:grid-cols-2">
|
<div class="grid gap-4 md:gap-6 xl:grid-cols-2">
|
||||||
|
{#if searchQueryValue}
|
||||||
|
{#if leaderboardData.results.length > 0}
|
||||||
|
<div class="flex flex-col xl:col-span-2">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{#each leaderboardData.results as user}
|
||||||
|
<div class="flex flex-col bg-muted rounded-md border border-border hover:bg-muted/50 transition ease duration-200">
|
||||||
|
<a href={`/user/${user.username}`}>
|
||||||
|
<UserProfilePreview userId={user.userId}/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="flex flex-col xl:col-span-2 items-center justify-center h-60">
|
||||||
|
<h2 class="mb-4 text-xl">No users found</h2>
|
||||||
|
<p class="text-muted-foreground text-sm md:text-base mb-4">No users match your search "{searchQueryValue}"</p>
|
||||||
|
<Button variant="outline" onclick={() => {
|
||||||
|
searchQuery = '';
|
||||||
|
searchQueryValue = '';
|
||||||
|
fetchLeaderboardData();
|
||||||
|
}}>
|
||||||
|
<h2 class="text-sm">Clear search</h2>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
<!-- Top Profit Makers -->
|
<!-- Top Profit Makers -->
|
||||||
<Card.Root class="overflow-hidden">
|
<Card.Root class="overflow-hidden">
|
||||||
<Card.Header class="pb-3 md:pb-4">
|
<Card.Header class="pb-3 md:pb-4">
|
||||||
|
|
@ -314,6 +399,7 @@
|
||||||
/>
|
/>
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Reference in a new issue