improve stocks page for funds
Build Images and Deploy / Update-PROD-Stack (push) Successful in 1m22s

This commit is contained in:
2026-03-18 22:16:25 -04:00
parent 39dd864245
commit 63e1821e98
4 changed files with 111 additions and 9 deletions
+3
View File
@@ -137,6 +137,9 @@ export default async function LeaderboardPage({
> >
<Users className="h-4 w-4" /> <Users className="h-4 w-4" />
Players Players
{players.length > 0 && (
<span className="text-xs bg-white/10 px-1.5 py-0.5 rounded-full">{players.length}</span>
)}
</Link> </Link>
<Link <Link
href="/leaderboard?tab=funds" href="/leaderboard?tab=funds"
+1 -1
View File
@@ -11,7 +11,7 @@ export const revalidate = 0
async function getStats() { async function getStats() {
const [userCount, hashtagCount, tradeCount, topHashtags, recentTrades] = await Promise.all([ const [userCount, hashtagCount, tradeCount, topHashtags, recentTrades] = await Promise.all([
prisma.user.count(), prisma.user.count({ where: { isFund: false } }),
prisma.hashtag.count({ where: { isActive: true } }), prisma.hashtag.count({ where: { isActive: true } }),
prisma.trade.count(), prisma.trade.count(),
// Top by current price (most active) // Top by current price (most active)
+106 -7
View File
@@ -1,8 +1,9 @@
import { prisma } from '@/lib/prisma' import { prisma } from '@/lib/prisma'
import { formatCurrency } from '@/lib/utils' import { formatCurrency } from '@/lib/utils'
import Link from 'next/link' import Link from 'next/link'
import { ArrowUp, ArrowDown, ArrowUpDown, BarChart2 } from 'lucide-react' import { ArrowUp, ArrowDown, ArrowUpDown, BarChart2, Building2 } from 'lucide-react'
import { formatDistanceToNow } from 'date-fns' import { formatDistanceToNow } from 'date-fns'
import { calcFundNav } from '@/lib/pricing'
export const dynamic = 'force-dynamic' export const dynamic = 'force-dynamic'
@@ -12,7 +13,7 @@ type SortField = 'price' | 'tag' | 'change' | 'updated'
type SortDir = 'asc' | 'desc' type SortDir = 'asc' | 'desc'
interface PageProps { interface PageProps {
searchParams: { page?: string; sort?: string; dir?: string } searchParams: { page?: string; sort?: string; dir?: string; tab?: string }
} }
function SortLink({ function SortLink({
@@ -33,7 +34,7 @@ function SortLink({
const Icon = isActive ? (currentDir === 'desc' ? ArrowDown : ArrowUp) : ArrowUpDown const Icon = isActive ? (currentDir === 'desc' ? ArrowDown : ArrowUp) : ArrowUpDown
return ( return (
<Link <Link
href={`/stocks?page=1&sort=${field}&dir=${nextDir}`} href={`/stocks?page=1&sort=${field}&dir=${nextDir}&tab=stocks`}
className={`flex items-center gap-1 hover:text-slate-200 transition-colors select-none ${isActive ? 'text-indigo-400' : 'text-slate-400'}`} className={`flex items-center gap-1 hover:text-slate-200 transition-colors select-none ${isActive ? 'text-indigo-400' : 'text-slate-400'}`}
> >
{label} {label}
@@ -43,6 +44,7 @@ function SortLink({
} }
export default async function StocksPage({ searchParams }: PageProps) { export default async function StocksPage({ searchParams }: PageProps) {
const tab = searchParams.tab === 'funds' ? 'funds' : 'stocks'
const page = Math.max(1, parseInt(searchParams.page ?? '1', 10)) const page = Math.max(1, parseInt(searchParams.page ?? '1', 10))
const sort = (['price', 'tag', 'change', 'updated'].includes(searchParams.sort ?? '') const sort = (['price', 'tag', 'change', 'updated'].includes(searchParams.sort ?? '')
? searchParams.sort ? searchParams.sort
@@ -150,6 +152,44 @@ export default async function StocksPage({ searchParams }: PageProps) {
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE)) const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE))
// Funds (always fetched for both tabs — cheap query)
const rawFunds = await prisma.hedgeFund.findMany({
include: {
user: {
select: {
balance: true,
positions: {
where: { shares: { gt: 0 } },
select: {
positionType: true, shares: true, avgBuyPrice: true,
hashtag: { select: { currentPrice: true } },
},
},
},
},
managers: { select: { userId: true } },
_count: { select: { investments: true } },
},
orderBy: { createdAt: 'asc' },
})
const funds = rawFunds.map((f) => {
const portfolioValue = f.user.positions.reduce((sum, p) => {
const val = p.positionType === 'LONG'
? p.shares * p.hashtag.currentPrice
: p.avgBuyPrice * p.shares - (p.hashtag.currentPrice - p.avgBuyPrice) * p.shares
return sum + val
}, 0)
const totalValue = f.user.balance + portfolioValue
return {
id: f.id, name: f.name, slug: f.slug,
cash: f.user.balance, portfolioValue, totalValue,
nav: calcFundNav(totalValue, f.sharesOutstanding),
managerCount: f.managers.length,
investorCount: f._count.investments,
}
}).sort((a, b) => b.totalValue - a.totalValue)
return ( return (
<div className="max-w-5xl mx-auto space-y-6"> <div className="max-w-5xl mx-auto space-y-6">
{/* Header */} {/* Header */}
@@ -157,11 +197,34 @@ export default async function StocksPage({ searchParams }: PageProps) {
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<BarChart2 className="h-6 w-6 text-indigo-400" /> <BarChart2 className="h-6 w-6 text-indigo-400" />
<h1 className="text-2xl font-bold">Markets</h1> <h1 className="text-2xl font-bold">Markets</h1>
<span className="text-slate-500 text-sm">({total} active)</span>
</div> </div>
</div> </div>
{/* Table */} {/* Tabs */}
<div className="flex gap-1 bg-surface-card border border-surface-border rounded-xl p-1 w-fit">
<Link
href="/stocks?tab=stocks"
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
tab === 'stocks' ? 'bg-indigo-600 text-white' : 'text-slate-400 hover:text-slate-200'
}`}
>
<BarChart2 className="h-4 w-4" />
Hashtags
<span className="text-xs bg-white/10 px-1.5 py-0.5 rounded-full">{total}</span>
</Link>
<Link
href="/stocks?tab=funds"
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
tab === 'funds' ? 'bg-indigo-600 text-white' : 'text-slate-400 hover:text-slate-200'
}`}
>
<Building2 className="h-4 w-4" />
Hedge Funds
<span className="text-xs bg-white/10 px-1.5 py-0.5 rounded-full">{funds.length}</span>
</Link>
</div>
{tab === 'stocks' && (<>
<div className="bg-surface-card border border-surface-border rounded-xl overflow-hidden"> <div className="bg-surface-card border border-surface-border rounded-xl overflow-hidden">
{/* Column headers */} {/* Column headers */}
<div className="grid grid-cols-[2fr_1fr_1fr_1fr_1fr] gap-4 px-4 py-2.5 border-b border-surface-border text-xs font-medium"> <div className="grid grid-cols-[2fr_1fr_1fr_1fr_1fr] gap-4 px-4 py-2.5 border-b border-surface-border text-xs font-medium">
@@ -248,7 +311,7 @@ export default async function StocksPage({ searchParams }: PageProps) {
<div className="flex items-center justify-center gap-2"> <div className="flex items-center justify-center gap-2">
{page > 1 && ( {page > 1 && (
<Link <Link
href={`/stocks?page=${page - 1}&sort=${sort}&dir=${dir}`} href={`/stocks?page=${page - 1}&sort=${sort}&dir=${dir}&tab=stocks`}
className="px-3 py-1.5 text-sm bg-surface-card border border-surface-border rounded-lg hover:border-indigo-500/50 transition-colors" className="px-3 py-1.5 text-sm bg-surface-card border border-surface-border rounded-lg hover:border-indigo-500/50 transition-colors"
> >
Prev Prev
@@ -259,7 +322,7 @@ export default async function StocksPage({ searchParams }: PageProps) {
</span> </span>
{page < totalPages && ( {page < totalPages && (
<Link <Link
href={`/stocks?page=${page + 1}&sort=${sort}&dir=${dir}`} href={`/stocks?page=${page + 1}&sort=${sort}&dir=${dir}&tab=stocks`}
className="px-3 py-1.5 text-sm bg-surface-card border border-surface-border rounded-lg hover:border-indigo-500/50 transition-colors" className="px-3 py-1.5 text-sm bg-surface-card border border-surface-border rounded-lg hover:border-indigo-500/50 transition-colors"
> >
Next Next
@@ -267,6 +330,42 @@ export default async function StocksPage({ searchParams }: PageProps) {
)} )}
</div> </div>
)} )}
</>)}
{tab === 'funds' && (
<div className="bg-surface-card border border-surface-border rounded-xl overflow-hidden">
<div className="grid grid-cols-[1fr_repeat(3,_8rem)] gap-4 px-4 py-2.5 border-b border-surface-border text-xs text-slate-500">
<span>Fund</span>
<span className="text-right">Total value</span>
<span className="text-right hidden sm:block">NAV / share</span>
<span className="text-right hidden sm:block">Investors</span>
</div>
{funds.length === 0 ? (
<p className="text-center py-16 text-slate-500">No hedge funds yet.</p>
) : (
<div className="divide-y divide-surface-border">
{funds.map((fund) => (
<div
key={fund.id}
className="grid grid-cols-[1fr_repeat(3,_8rem)] gap-4 items-center px-4 py-3 hover:bg-surface-border/30 transition-colors"
>
<Link
href={`/fund/${fund.slug}`}
className="font-medium hover:text-indigo-300 transition-colors flex items-center gap-2"
>
<Building2 className="h-3.5 w-3.5 text-indigo-400 shrink-0" />
{fund.name}
<span className="text-xs text-slate-500">{fund.managerCount} manager{fund.managerCount !== 1 ? 's' : ''}</span>
</Link>
<span className="text-right font-medium tabular-nums">{formatCurrency(fund.totalValue)}</span>
<span className="text-right text-slate-400 text-sm hidden sm:block tabular-nums">{formatCurrency(fund.nav)}</span>
<span className="text-right text-slate-400 text-sm hidden sm:block">{fund.investorCount}</span>
</div>
))}
</div>
)}
</div>
)}
</div> </div>
) )
} }
+1 -1
View File
File diff suppressed because one or more lines are too long