diff --git a/website/src/lib/components/self/CommentSection.svelte b/website/src/lib/components/self/CommentSection.svelte index 8cef634..d2c8327 100644 --- a/website/src/lib/components/self/CommentSection.svelte +++ b/website/src/lib/components/self/CommentSection.svelte @@ -5,12 +5,13 @@ import * as Avatar from '$lib/components/ui/avatar'; import * as HoverCard from '$lib/components/ui/hover-card'; import { Badge } from '$lib/components/ui/badge'; - import { MessageCircle, Send, Loader2, Heart, CalendarDays } from 'lucide-svelte'; + import { MessageCircle, Send, Loader2, Heart } from 'lucide-svelte'; import { USER_DATA } from '$lib/stores/user-data'; import { toast } from 'svelte-sonner'; import { goto } from '$app/navigation'; import { formatTimeAgo, getPublicUrl } from '$lib/utils'; import SignInConfirmDialog from '$lib/components/self/SignInConfirmDialog.svelte'; + import UserProfilePreview from '$lib/components/self/UserProfilePreview.svelte'; import WebSocket, { type WebSocketHandle } from '$lib/components/self/WebSocket.svelte'; const { coinSymbol } = $props<{ coinSymbol: string }>(); @@ -221,7 +222,7 @@ {#each comments as comment (comment.id)}
- + +
+
+ + + diff --git a/website/src/routes/+page.svelte b/website/src/routes/+page.svelte index 2ba6a62..817e994 100644 --- a/website/src/routes/+page.svelte +++ b/website/src/routes/+page.svelte @@ -1,13 +1,14 @@ @@ -102,44 +148,11 @@

Market Overview

- - - - Name - Price - 24h Change - - - - - - {#each coins as coin} - - - - - {coin.name} (*{coin.symbol}) - - - ${formatPrice(coin.price)} - - = 0 ? 'success' : 'destructive'}> - {coin.change24h >= 0 ? '+' : ''}{coin.change24h.toFixed(2)}% - - - - - - {/each} - - + goto(`/coin/${coin.symbol}`)} + /> diff --git a/website/src/routes/api/coin/[coinSymbol]/comments/+server.ts b/website/src/routes/api/coin/[coinSymbol]/comments/+server.ts index 0db7ff0..ce5fc95 100644 --- a/website/src/routes/api/coin/[coinSymbol]/comments/+server.ts +++ b/website/src/routes/api/coin/[coinSymbol]/comments/+server.ts @@ -35,8 +35,6 @@ export async function GET({ params, request }) { userName: user.name, userUsername: user.username, userImage: user.image, - userBio: user.bio, - userCreatedAt: user.createdAt, isLikedByUser: session?.user ? sql`EXISTS(SELECT 1 FROM ${commentLike} WHERE ${commentLike.userId} = ${session.user.id} AND ${commentLike.commentId} = ${comment.id})` : sql`FALSE` @@ -109,8 +107,6 @@ export async function POST({ request, params }) { userName: user.name, userUsername: user.username, userImage: user.image, - userBio: user.bio, - userCreatedAt: user.createdAt, isLikedByUser: sql`FALSE` }) .from(comment) diff --git a/website/src/routes/api/user/[userId]/+server.ts b/website/src/routes/api/user/[userId]/+server.ts new file mode 100644 index 0000000..5260b81 --- /dev/null +++ b/website/src/routes/api/user/[userId]/+server.ts @@ -0,0 +1,139 @@ +import { json, error } from '@sveltejs/kit'; +import { db } from '$lib/server/db'; +import { user, coin, transaction, userPortfolio } from '$lib/server/db/schema'; +import { eq, desc, sql, count, and, gte } from 'drizzle-orm'; + +export async function GET({ params }) { + const { userId } = params; + + if (!userId) { + throw error(400, 'User ID or username is required'); + } + + try { + const isNumeric = /^\d+$/.test(userId); + + const userProfile = await db.query.user.findFirst({ + where: isNumeric ? eq(user.id, parseInt(userId)) : eq(user.username, userId), + columns: { + id: true, + name: true, + username: true, + bio: true, + image: true, + createdAt: true, + baseCurrencyBalance: true, + isAdmin: true, + } + }); + + if (!userProfile) { + throw error(404, 'User not found'); + } + + const actualUserId = userProfile.id; + + // get created coins + const createdCoins = await db + .select({ + id: coin.id, + name: coin.name, + symbol: coin.symbol, + icon: coin.icon, + currentPrice: coin.currentPrice, + marketCap: coin.marketCap, + volume24h: coin.volume24h, + change24h: coin.change24h, + createdAt: coin.createdAt, + }) + .from(coin) + .where(eq(coin.creatorId, actualUserId)) + .orderBy(desc(coin.createdAt)) + .limit(10); + + // get portfolio value and holdings count + const portfolioStats = await db + .select({ + holdingsCount: count(), + totalValue: sql`COALESCE(SUM(CAST(${userPortfolio.quantity} AS NUMERIC) * CAST(${coin.currentPrice} AS NUMERIC)), 0)` + }) + .from(userPortfolio) + .innerJoin(coin, eq(userPortfolio.coinId, coin.id)) + .where(eq(userPortfolio.userId, actualUserId)); + + // get recent transactions + const recentTransactions = await db + .select({ + id: transaction.id, + type: transaction.type, + coinSymbol: coin.symbol, + coinName: coin.name, + coinIcon: coin.icon, + quantity: transaction.quantity, + pricePerCoin: transaction.pricePerCoin, + totalBaseCurrencyAmount: transaction.totalBaseCurrencyAmount, + timestamp: transaction.timestamp, + }) + .from(transaction) + .innerJoin(coin, eq(transaction.coinId, coin.id)) + .where(eq(transaction.userId, actualUserId)) + .orderBy(desc(transaction.timestamp)) + .limit(10); + + // calc total portfolio value + const baseCurrencyBalance = parseFloat(userProfile.baseCurrencyBalance); + const holdingsValue = portfolioStats[0]?.totalValue || 0; + const totalPortfolioValue = baseCurrencyBalance + holdingsValue; + + // get all transaction statistics + const transactionStats = await db + .select({ + totalTransactions: count(), + totalBuyVolume: sql`COALESCE(SUM(CASE WHEN ${transaction.type} = 'BUY' THEN CAST(${transaction.totalBaseCurrencyAmount} AS NUMERIC) ELSE 0 END), 0)`, + totalSellVolume: sql`COALESCE(SUM(CASE WHEN ${transaction.type} = 'SELL' THEN CAST(${transaction.totalBaseCurrencyAmount} AS NUMERIC) ELSE 0 END), 0)` + }) + .from(transaction) + .where(eq(transaction.userId, actualUserId)); + + const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000); + const transactionStats24h = await db + .select({ + transactions24h: count(), + buyVolume24h: sql`COALESCE(SUM(CASE WHEN ${transaction.type} = 'BUY' THEN CAST(${transaction.totalBaseCurrencyAmount} AS NUMERIC) ELSE 0 END), 0)`, + sellVolume24h: sql`COALESCE(SUM(CASE WHEN ${transaction.type} = 'SELL' THEN CAST(${transaction.totalBaseCurrencyAmount} AS NUMERIC) ELSE 0 END), 0)` + }) + .from(transaction) + .where( + and( + eq(transaction.userId, actualUserId), + gte(transaction.timestamp, twentyFourHoursAgo) + ) + ); + + return json({ + profile: { + ...userProfile, + baseCurrencyBalance, + totalPortfolioValue, + }, + stats: { + totalPortfolioValue, + baseCurrencyBalance, + holdingsValue, + holdingsCount: portfolioStats[0]?.holdingsCount || 0, + coinsCreated: createdCoins.length, + totalTransactions: transactionStats[0]?.totalTransactions || 0, + totalBuyVolume: transactionStats[0]?.totalBuyVolume || 0, + totalSellVolume: transactionStats[0]?.totalSellVolume || 0, + transactions24h: transactionStats24h[0]?.transactions24h || 0, + buyVolume24h: transactionStats24h[0]?.buyVolume24h || 0, + sellVolume24h: transactionStats24h[0]?.sellVolume24h || 0, + }, + createdCoins, + recentTransactions + }); + } catch (e) { + console.error('Failed to fetch user profile:', e); + throw error(500, 'Failed to fetch user profile'); + } +} diff --git a/website/src/routes/coin/[coinSymbol]/+page.svelte b/website/src/routes/coin/[coinSymbol]/+page.svelte index 0d4a1cf..6b1feea 100644 --- a/website/src/routes/coin/[coinSymbol]/+page.svelte +++ b/website/src/routes/coin/[coinSymbol]/+page.svelte @@ -6,6 +6,7 @@ import * as HoverCard from '$lib/components/ui/hover-card'; import TradeModal from '$lib/components/self/TradeModal.svelte'; import CommentSection from '$lib/components/self/CommentSection.svelte'; + import UserProfilePreview from '$lib/components/self/UserProfilePreview.svelte'; import { TrendingUp, TrendingDown, @@ -28,13 +29,13 @@ import CoinIcon from '$lib/components/self/CoinIcon.svelte'; import { USER_DATA } from '$lib/stores/user-data'; import { fetchPortfolioData } from '$lib/stores/portfolio-data'; + import { getPublicUrl } from '$lib/utils.js'; const { data } = $props(); const coinSymbol = data.coinSymbol; let coin = $state(null); let loading = $state(true); - let creatorImageUrl = $state(null); let chartData = $state([]); let volumeData = $state([]); let userHolding = $state(0); @@ -61,15 +62,6 @@ chartData = result.candlestickData || []; volumeData = result.volumeData || []; - if (coin.creatorId) { - try { - const imageResponse = await fetch(`/api/user/${coin.creatorId}/image`); - const imageResult = await imageResponse.json(); - creatorImageUrl = imageResult.url; - } catch (e) { - console.error('Failed to load creator image:', e); - } - } } catch (e) { console.error('Failed to fetch coin data:', e); toast.error('Failed to load coin data'); @@ -339,37 +331,16 @@ goto(`/user/${coin.creatorId}`)} + onclick={() => goto(`/user/${coin.creatorUsername}`)} > - + {coin.creatorName.charAt(0)} {coin.creatorName} (@{coin.creatorUsername}) -
- - - {coin.creatorName.charAt(0)} - -
-

{coin.creatorName}

-

@{coin.creatorUsername}

- {#if coin.creatorBio} -

{coin.creatorBio}

- {/if} -
- - - Joined {new Date(coin.creatorCreatedAt).toLocaleDateString('en-US', { - year: 'numeric', - month: 'long' - })} - -
-
-
+
diff --git a/website/src/routes/leaderboard/+page.svelte b/website/src/routes/leaderboard/+page.svelte index 696f89d..921a6b6 100644 --- a/website/src/routes/leaderboard/+page.svelte +++ b/website/src/routes/leaderboard/+page.svelte @@ -1,9 +1,9 @@ @@ -100,54 +241,13 @@ - {#if leaderboardData.topRugpullers.length === 0} -
-

No major profits recorded today

-
- {:else} - - - - Rank - User - Profit - - - - {#each leaderboardData.topRugpullers as user, index} - {@const rankInfo = getRankIcon(index)} - goto(`/user/${user.userId}`)} - > - -
- - #{index + 1} -
-
- -
- - - {user.name?.charAt(0) || '?'} - -
-

{user.name}

-

@{user.username}

-
-
-
- - {formatValue(user.totalExtracted)} - -
- {/each} -
-
- {/if} + goto(`/user/${user.userUsername || user.username}`)} + emptyMessage="No major profits recorded today" + enableUserPreview={true} + />
@@ -161,56 +261,13 @@ Users who experienced the largest losses today - {#if leaderboardData.biggestLosers.length === 0} -
-

- No major losses recorded today -

-
- {:else} - - - - Rank - User - Loss - - - - {#each leaderboardData.biggestLosers as user, index} - {@const rankInfo = getRankIcon(index)} - goto(`/user/${user.userId}`)} - > - -
- - #{index + 1} -
-
- -
- - - {user.name?.charAt(0) || '?'} - -
-

{user.name}

-

@{user.username}

-
-
-
- - -{formatValue(user.totalLoss)} - -
- {/each} -
-
- {/if} + goto(`/user/${user.userUsername || user.username}`)} + emptyMessage="No major losses recorded today" + enableUserPreview={true} + />
@@ -224,54 +281,13 @@ Users with the highest liquid cash balances - {#if leaderboardData.cashKings.length === 0} -
-

Everyone's invested! 💸

-
- {:else} - - - - Rank - User - Cash - - - - {#each leaderboardData.cashKings as user, index} - {@const rankInfo = getRankIcon(index)} - goto(`/user/${user.userId}`)} - > - -
- - #{index + 1} -
-
- -
- - - {user.name?.charAt(0) || '?'} - -
-

{user.name}

-

@{user.username}

-
-
-
- - {formatValue(user.baseCurrencyBalance)} - -
- {/each} -
-
- {/if} + goto(`/user/${user.userUsername || user.username}`)} + emptyMessage="Everyone's invested! 💸" + enableUserPreview={true} + />
@@ -287,61 +303,13 @@ > - {#if leaderboardData.paperMillionaires.length === 0} -
-

No large portfolios yet! 📉

-
- {:else} - - - - Rank - User - Portfolio - Liquidity - - - - {#each leaderboardData.paperMillionaires as user, index} - {@const rankInfo = getRankIcon(index)} - {@const liquidityInfo = getLiquidityWarning(user.liquidityRatio)} - goto(`/user/${user.userId}`)} - > - -
- - #{index + 1} -
-
- -
- - - {user.name?.charAt(0) || '?'} - -
-

{user.name}

-

@{user.username}

-
-
-
- - {formatValue(user.totalPortfolioValue)} - - - - {liquidityInfo.text} - - -
- {/each} -
-
- {/if} + goto(`/user/${user.userUsername || user.username}`)} + emptyMessage="No large portfolios yet! 📉" + enableUserPreview={true} + />
diff --git a/website/src/routes/user/[username]/+page.svelte b/website/src/routes/user/[username]/+page.svelte new file mode 100644 index 0000000..fc8a793 --- /dev/null +++ b/website/src/routes/user/[username]/+page.svelte @@ -0,0 +1,450 @@ + + + + {profileData?.profile?.name + ? `${profileData.profile.name} (@${profileData.profile.username})` + : 'Loading...'} - Rugplay + + + +
+ {#if loading} +
+
+ +
Loading profile...
+
+
+ {:else if !profileData} +
+
+
Failed to load profile
+ +
+
+ {:else} + + + +
+ +
+ + + {profileData.profile.name.charAt(0).toUpperCase()} + +
+ + +
+
+
+

{profileData.profile.name}

+ + + +
+

@{profileData.profile.username}

+
+ + {#if profileData.profile.bio} +

+ {profileData.profile.bio} +

+ {/if} + +
+ + Joined {memberSince} +
+
+
+
+
+ + +
+ + + +
+
Total Portfolio
+ +
+
+ {formatValue(totalPortfolioValue)} +
+

{profileData.stats.holdingsCount} holdings

+
+
+ + + + +
+
Liquid Value
+
+
+ {formatValue(baseCurrencyBalance)} +
+

Available cash

+
+
+ + + + +
+
Illiquid Value
+
+
+ {formatValue(holdingsValue)} +
+

Coin holdings

+
+
+ + + + +
+
Buy/Sell Ratio
+
+
+
+
+
+
+ {buyPercentage.toFixed(1)}% + buy + {sellPercentage.toFixed(1)}% + sell +
+
+
+
+ + +
+ + + +
+
Buy Activity
+ +
+
+
+ {formatValue(totalBuyVolume)} +
+
Total amount spent
+
+
+
+ {formatValue(buyVolume24h)} +
+
24h buy volume
+
+
+
+ + + + +
+
Sell Activity
+ +
+
+
+ {formatValue(totalSellVolume)} +
+
Total amount received
+
+
+
+ {formatValue(sellVolume24h)} +
+
24h sell volume
+
+
+
+ + + + +
+
Total Trading Volume
+ All Time +
+
+ {formatValue(totalTradingVolumeAllTime)} +
+
+ {profileData.stats.totalTransactions} total trades +
+
+
+ + + + +
+
24h Trading Volume
+ 24h +
+
+ {formatValue(totalTradingVolume24h)} +
+
+ {profileData.stats.transactions24h || 0} trades today +
+
+
+
+ + + {#if hasCreatedCoins} + + + + + Created Coins ({profileData.createdCoins.length}) + + Coins launched by {profileData.profile.name} + + + goto(`/coin/${coin.symbol}`)} + /> + + + {/if} + + + + + + + Recent Trading Activity + + Latest transactions by {profileData.profile.name} + + + + + + {/if} +
diff --git a/website/src/routes/user/[username]/+page.ts b/website/src/routes/user/[username]/+page.ts new file mode 100644 index 0000000..253a906 --- /dev/null +++ b/website/src/routes/user/[username]/+page.ts @@ -0,0 +1,5 @@ +export async function load({ params }) { + return { + username: params.username + }; +} diff --git a/website/static/404.gif b/website/static/404.gif new file mode 100644 index 0000000..970097b Binary files /dev/null and b/website/static/404.gif differ