feat: enhance trade feed with pagination and detailed trade information
Build Images and Deploy / Update-PROD-Stack (push) Successful in 1m29s
Build Images and Deploy / Update-PROD-Stack (push) Successful in 1m29s
This commit is contained in:
+9
-1
@@ -187,7 +187,15 @@ export default async function HomePage() {
|
||||
{/* Recently traded */}
|
||||
{recentTrades.length > 0 && (
|
||||
<section>
|
||||
<h2 className="text-lg font-semibold mb-4">Recently traded</h2>
|
||||
<h2 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||
Recently traded
|
||||
<Link
|
||||
href="/trades"
|
||||
className="ml-auto text-sm font-normal text-indigo-400 hover:text-indigo-300"
|
||||
>
|
||||
Full feed →
|
||||
</Link>
|
||||
</h2>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3">
|
||||
{recentTrades.map(({ hashtag }) => hashtag && (
|
||||
<HashtagCard
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { formatCurrency, formatNumber, formatPnl, pnlColor } from '@/lib/utils'
|
||||
import Link from 'next/link'
|
||||
import { Activity } 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 GlobalTradesPage({ searchParams }: PageProps) {
|
||||
const page = Math.max(1, parseInt(searchParams.page ?? '1', 10))
|
||||
|
||||
const [total, trades] = await Promise.all([
|
||||
prisma.trade.count({ where: { hashtagId: { not: null }, type: { not: 'LOTTERY_WIN' } } }),
|
||||
prisma.trade.findMany({
|
||||
where: { hashtagId: { not: null }, type: { not: 'LOTTERY_WIN' } },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: PAGE_SIZE,
|
||||
skip: (page - 1) * PAGE_SIZE,
|
||||
include: {
|
||||
hashtag: { select: { tag: true, displayTag: true } },
|
||||
user: { select: { username: true, displayUsername: true, isFund: true } },
|
||||
},
|
||||
}),
|
||||
])
|
||||
|
||||
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE))
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto space-y-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<Activity className="h-6 w-6 text-indigo-400" />
|
||||
<h1 className="text-2xl font-bold">Trade Feed</h1>
|
||||
<span className="text-slate-500 text-sm">({total.toLocaleString()} trades)</span>
|
||||
</div>
|
||||
|
||||
<div className="bg-surface-card border border-surface-border rounded-xl overflow-hidden">
|
||||
<div className="divide-y divide-surface-border">
|
||||
{trades.map((t) => (
|
||||
<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 ${
|
||||
t.type.startsWith('BUY')
|
||||
? 'bg-emerald-500/15 text-emerald-400'
|
||||
: 'bg-red-500/15 text-red-400'
|
||||
}`}
|
||||
>
|
||||
{t.type.replace('_', ' ')}
|
||||
</span>
|
||||
<div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{t.user.isFund ? (
|
||||
<span className="text-xs text-indigo-400">🏦</span>
|
||||
) : null}
|
||||
<Link
|
||||
href={t.user.isFund ? `/fund/${t.user.username.replace('fund:', '')}` : `/profile/${t.user.username}`}
|
||||
className="text-slate-300 hover:text-white"
|
||||
>
|
||||
{t.user.displayUsername ?? t.user.username}
|
||||
</Link>
|
||||
<span className="text-slate-600">·</span>
|
||||
<Link href={`/hashtag/${t.hashtag!.tag}`} className="text-indigo-300 hover:text-indigo-200">
|
||||
#{t.hashtag!.displayTag}
|
||||
</Link>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 mt-0.5">
|
||||
{formatDistanceToNow(t.createdAt, { addSuffix: true })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right shrink-0">
|
||||
<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>
|
||||
|
||||
{totalPages > 1 && (
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
{page > 1 && (
|
||||
<Link
|
||||
href={`/trades?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={`/trades?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>
|
||||
)
|
||||
}
|
||||
+15
-2
@@ -238,13 +238,27 @@ for (const worker of [priceWorker, maintenanceWorker, schedulerWorker]) {
|
||||
// ── Repeatable jobs ───────────────────────────────────────────────────────────
|
||||
|
||||
async function setupRepeatableJobs() {
|
||||
// Always wipe existing repeatable registrations first so that:
|
||||
// - stale entries from old PRICE_UPDATE_INTERVAL_MINUTES values don't persist
|
||||
// - jobs exhausted by BullMQ retry limits get rescheduled cleanly
|
||||
const [existingScheduler, existingMaintenance] = await Promise.all([
|
||||
schedulerQueue.getRepeatableJobs(),
|
||||
maintenanceQueue.getRepeatableJobs(),
|
||||
])
|
||||
await Promise.all([
|
||||
...existingScheduler.map((j) => schedulerQueue.removeRepeatableByKey(j.key)),
|
||||
...existingMaintenance.map((j) => maintenanceQueue.removeRepeatableByKey(j.key)),
|
||||
])
|
||||
if (existingScheduler.length || existingMaintenance.length) {
|
||||
console.log(`[worker] cleared ${existingScheduler.length} scheduler + ${existingMaintenance.length} maintenance repeatable(s)`)
|
||||
}
|
||||
|
||||
// Price update sweep — every N minutes
|
||||
await schedulerQueue.add(
|
||||
'trigger-sweep',
|
||||
{},
|
||||
{
|
||||
repeat: { every: UPDATE_INTERVAL_MIN * 60 * 1000 },
|
||||
jobId: 'price-sweep-repeatable',
|
||||
},
|
||||
)
|
||||
|
||||
@@ -254,7 +268,6 @@ async function setupRepeatableJobs() {
|
||||
{},
|
||||
{
|
||||
repeat: { pattern: '5 0 * * *' },
|
||||
jobId: 'daily-maintenance-repeatable',
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user