improve stocks page for funds
Build Images and Deploy / Update-PROD-Stack (push) Successful in 1m22s
Build Images and Deploy / Update-PROD-Stack (push) Successful in 1m22s
This commit is contained in:
@@ -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
@@ -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
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user