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
|
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) => {
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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*',
|
||||||
|
|||||||
Reference in New Issue
Block a user