feat: add trade history page and update profile links
Build Images and Deploy / Update-PROD-Stack (push) Successful in 1m18s

This commit is contained in:
2026-03-18 19:05:09 -04:00
parent 6b32b28af1
commit d11e2e7b9a
4 changed files with 156 additions and 12 deletions
+141
View File
@@ -0,0 +1,141 @@
import { prisma } from '@/lib/prisma'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
import { redirect } from 'next/navigation'
import { formatCurrency, formatNumber, formatPnl, pnlColor } from '@/lib/utils'
import Link from 'next/link'
import { TrendingUp } from 'lucide-react'
import { formatDistanceToNow } from 'date-fns'
export const dynamic = 'force-dynamic'
const PAGE_SIZE = 50
interface PageProps {
searchParams: { page?: string }
}
export default async function TradeHistoryPage({ searchParams }: PageProps) {
const session = await getServerSession(authOptions)
if (!session) redirect('/auth/signin')
const page = Math.max(1, parseInt(searchParams.page ?? '1', 10))
const [total, trades] = await Promise.all([
prisma.trade.count({ where: { userId: session.user.id } }),
prisma.trade.findMany({
where: { userId: session.user.id },
orderBy: { createdAt: 'desc' },
take: PAGE_SIZE,
skip: (page - 1) * PAGE_SIZE,
include: {
hashtag: { select: { tag: true, displayTag: true } },
},
}),
])
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE))
return (
<div className="max-w-3xl mx-auto space-y-6">
{/* Header */}
<div className="flex items-center gap-3">
<TrendingUp className="h-6 w-6 text-indigo-400" />
<h1 className="text-2xl font-bold">Trade History</h1>
<span className="text-slate-500 text-sm">({total} total)</span>
</div>
{trades.length === 0 ? (
<div className="text-center py-16 text-slate-500">
<TrendingUp className="h-12 w-12 mx-auto mb-3 opacity-30" />
<p>No trades yet.</p>
<Link href="/" className="text-indigo-400 hover:text-indigo-300 text-sm mt-2 inline-block">
Browse trending hashtags
</Link>
</div>
) : (
<>
<div className="bg-surface-card border border-surface-border rounded-xl overflow-hidden">
<div className="divide-y divide-surface-border">
{trades.map((t) => {
const isLottery = t.type === 'LOTTERY_WIN'
return (
<div key={t.id} className="flex items-center justify-between px-4 py-3 text-sm">
<div className="flex items-center gap-3">
<span
className={`text-xs font-medium px-2 py-0.5 rounded shrink-0 ${
isLottery
? 'bg-amber-500/15 text-amber-400'
: t.type.startsWith('BUY')
? 'bg-emerald-500/15 text-emerald-400'
: 'bg-red-500/15 text-red-400'
}`}
>
{t.type.replace(/_/g, ' ')}
</span>
<div>
{isLottery ? (
<span className="text-amber-300">Lucky Dip</span>
) : (
<Link
href={`/hashtag/${t.hashtag!.tag}`}
className="hover:text-indigo-300"
>
#{t.hashtag!.displayTag}
</Link>
)}
<p className="text-xs text-slate-500 mt-0.5">
{formatDistanceToNow(t.createdAt, { addSuffix: true })}
</p>
</div>
</div>
<div className="text-right">
{isLottery ? (
<p className="text-emerald-400 font-medium">{formatCurrency(t.profit)}</p>
) : (
<>
<p>{formatNumber(t.shares)} sh @ {formatCurrency(t.price)}</p>
<p className="text-xs text-slate-500">{formatCurrency(t.total)}</p>
{(t.type === 'SELL_LONG' || t.type === 'SELL_SHORT') && (
<p className={`text-xs ${pnlColor(t.profit)}`}>
{formatPnl(t.profit)}
</p>
)}
</>
)}
</div>
</div>
)
})}
</div>
</div>
{/* Pagination */}
{totalPages > 1 && (
<div className="flex items-center justify-center gap-2">
{page > 1 && (
<Link
href={`/history?page=${page - 1}`}
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
</Link>
)}
<span className="text-slate-500 text-sm">
Page {page} of {totalPages}
</span>
{page < totalPages && (
<Link
href={`/history?page=${page + 1}`}
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
</Link>
)}
</div>
)}
</>
)}
</div>
)
}
+6 -12
View File
@@ -91,18 +91,6 @@ export default async function HomePage() {
> >
Lucky Dip Lucky Dip
</Link> </Link>
<Link
href="/stocks"
className="bg-surface-card border border-surface-border hover:border-indigo-500/50 px-6 py-2.5 rounded-lg font-medium transition-colors"
>
Markets
</Link>
<Link
href="/positions"
className="bg-surface-card border border-surface-border hover:border-indigo-500/50 px-6 py-2.5 rounded-lg font-medium transition-colors"
>
My Positions
</Link>
</> </>
) : ( ) : (
<> <>
@@ -172,6 +160,12 @@ export default async function HomePage() {
<h2 className="text-lg font-semibold mb-4 flex items-center gap-2"> <h2 className="text-lg font-semibold mb-4 flex items-center gap-2">
<TrendingUp className="h-5 w-5 text-indigo-400" /> <TrendingUp className="h-5 w-5 text-indigo-400" />
Trending now Trending now
<Link
href="/stocks"
className="ml-auto text-sm font-normal text-indigo-400 hover:text-indigo-300"
>
Full market
</Link>
</h2> </h2>
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3"> <div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3">
{topHashtags.map((h) => { {topHashtags.map((h) => {
+8
View File
@@ -164,6 +164,14 @@ export default async function ProfilePage({ params }: Props) {
<TrendingDown className="h-5 w-5 text-indigo-400" /> <TrendingDown className="h-5 w-5 text-indigo-400" />
)} )}
Trade history Trade history
{isOwn && (
<Link
href="/history"
className="ml-auto text-sm font-normal text-indigo-400 hover:text-indigo-300"
>
View all
</Link>
)}
</h2> </h2>
<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">
<div className="divide-y divide-surface-border"> <div className="divide-y divide-surface-border">
+1
View File
@@ -4,6 +4,7 @@ export const config = {
matcher: [ matcher: [
'/profile/:path*', '/profile/:path*',
'/positions', '/positions',
'/history',
'/admin/:path*', '/admin/:path*',
'/api/trade/:path*', '/api/trade/:path*',
'/api/research/:path*', '/api/research/:path*',