feat: add trade history page and update profile links
Build Images and Deploy / Update-PROD-Stack (push) Successful in 1m18s
Build Images and Deploy / Update-PROD-Stack (push) Successful in 1m18s
This commit is contained in:
@@ -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
@@ -91,18 +91,6 @@ export default async function HomePage() {
|
||||
>
|
||||
Lucky Dip
|
||||
</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">
|
||||
<TrendingUp className="h-5 w-5 text-indigo-400" />
|
||||
Trending now
|
||||
<Link
|
||||
href="/stocks"
|
||||
className="ml-auto text-sm font-normal text-indigo-400 hover:text-indigo-300"
|
||||
>
|
||||
Full market →
|
||||
</Link>
|
||||
</h2>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3">
|
||||
{topHashtags.map((h) => {
|
||||
|
||||
@@ -164,6 +164,14 @@ export default async function ProfilePage({ params }: Props) {
|
||||
<TrendingDown className="h-5 w-5 text-indigo-400" />
|
||||
)}
|
||||
Trade history
|
||||
{isOwn && (
|
||||
<Link
|
||||
href="/history"
|
||||
className="ml-auto text-sm font-normal text-indigo-400 hover:text-indigo-300"
|
||||
>
|
||||
View all →
|
||||
</Link>
|
||||
)}
|
||||
</h2>
|
||||
<div className="bg-surface-card border border-surface-border rounded-xl overflow-hidden">
|
||||
<div className="divide-y divide-surface-border">
|
||||
|
||||
@@ -4,6 +4,7 @@ export const config = {
|
||||
matcher: [
|
||||
'/profile/:path*',
|
||||
'/positions',
|
||||
'/history',
|
||||
'/admin/:path*',
|
||||
'/api/trade/:path*',
|
||||
'/api/research/:path*',
|
||||
|
||||
Reference in New Issue
Block a user