feat: add managed funds section to user profile and enhance trade display for fund-related transactions
Build Images and Deploy / Update-PROD-Stack (push) Failing after 34s
Build Images and Deploy / Update-PROD-Stack (push) Failing after 34s
This commit is contained in:
@@ -183,6 +183,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" />
|
||||||
Your top positions
|
Your top positions
|
||||||
|
<Link
|
||||||
|
href="/positions"
|
||||||
|
className="ml-auto text-sm font-normal text-indigo-400 hover:text-indigo-300"
|
||||||
|
>
|
||||||
|
View all →
|
||||||
|
</Link>
|
||||||
</h2>
|
</h2>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
{holdings.biggestGain && (
|
{holdings.biggestGain && (
|
||||||
|
|||||||
@@ -187,6 +187,37 @@ export default async function ProfilePage({ params }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Funds managed — only shown to the profile owner */}
|
||||||
|
{isOwn && user.managedFunds.length > 0 && (
|
||||||
|
<section>
|
||||||
|
<h2 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||||
|
<Building2 className="h-5 w-5 text-indigo-400" />
|
||||||
|
Funds you manage
|
||||||
|
</h2>
|
||||||
|
<div className="bg-surface-card border border-surface-border rounded-xl overflow-hidden">
|
||||||
|
<div className="divide-y divide-surface-border">
|
||||||
|
{user.managedFunds.map(({ fund }) => (
|
||||||
|
<div key={fund.id} className="flex items-center justify-between px-4 py-3">
|
||||||
|
<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}
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href={`/stocks?fund=${fund.slug}`}
|
||||||
|
className="text-xs text-indigo-400 hover:text-indigo-300 transition-colors"
|
||||||
|
>
|
||||||
|
Trade as this fund →
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Positions */}
|
{/* Positions */}
|
||||||
{user.positions.length > 0 && (
|
{user.positions.length > 0 && (
|
||||||
<section>
|
<section>
|
||||||
@@ -240,37 +271,6 @@ export default async function ProfilePage({ params }: Props) {
|
|||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Funds managed — only shown to the profile owner */}
|
|
||||||
{isOwn && user.managedFunds.length > 0 && (
|
|
||||||
<section>
|
|
||||||
<h2 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
|
||||||
<Building2 className="h-5 w-5 text-indigo-400" />
|
|
||||||
Funds you manage
|
|
||||||
</h2>
|
|
||||||
<div className="bg-surface-card border border-surface-border rounded-xl overflow-hidden">
|
|
||||||
<div className="divide-y divide-surface-border">
|
|
||||||
{user.managedFunds.map(({ fund }) => (
|
|
||||||
<div key={fund.id} className="flex items-center justify-between px-4 py-3">
|
|
||||||
<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}
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
href={`/stocks?fund=${fund.slug}`}
|
|
||||||
className="text-xs text-indigo-400 hover:text-indigo-300 transition-colors"
|
|
||||||
>
|
|
||||||
Trade as this fund →
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Trade history */}
|
{/* Trade history */}
|
||||||
{user.trades.length > 0 && (
|
{user.trades.length > 0 && (
|
||||||
<section>
|
<section>
|
||||||
|
|||||||
+28
-5
@@ -15,15 +15,23 @@ interface PageProps {
|
|||||||
export default async function GlobalTradesPage({ searchParams }: PageProps) {
|
export default async function GlobalTradesPage({ searchParams }: PageProps) {
|
||||||
const page = Math.max(1, parseInt(searchParams.page ?? '1', 10))
|
const page = Math.max(1, parseInt(searchParams.page ?? '1', 10))
|
||||||
|
|
||||||
|
const tradeWhere = {
|
||||||
|
OR: [
|
||||||
|
{ hashtagId: { not: null }, type: { not: 'LOTTERY_WIN' } },
|
||||||
|
{ type: { in: ['FUND_INVEST', 'FUND_REDEEM'] } },
|
||||||
|
],
|
||||||
|
} as const
|
||||||
|
|
||||||
const [total, trades] = await Promise.all([
|
const [total, trades] = await Promise.all([
|
||||||
prisma.trade.count({ where: { hashtagId: { not: null }, type: { not: 'LOTTERY_WIN' } } }),
|
prisma.trade.count({ where: tradeWhere }),
|
||||||
prisma.trade.findMany({
|
prisma.trade.findMany({
|
||||||
where: { hashtagId: { not: null }, type: { not: 'LOTTERY_WIN' } },
|
where: tradeWhere,
|
||||||
orderBy: { createdAt: 'desc' },
|
orderBy: { createdAt: 'desc' },
|
||||||
take: PAGE_SIZE,
|
take: PAGE_SIZE,
|
||||||
skip: (page - 1) * PAGE_SIZE,
|
skip: (page - 1) * PAGE_SIZE,
|
||||||
include: {
|
include: {
|
||||||
hashtag: { select: { tag: true, displayTag: true } },
|
hashtag: { select: { tag: true, displayTag: true } },
|
||||||
|
fund: { select: { name: true, slug: true } },
|
||||||
user: { select: { username: true, displayUsername: true, isFund: true } },
|
user: { select: { username: true, displayUsername: true, isFund: true } },
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -49,19 +57,34 @@ export default async function GlobalTradesPage({ searchParams }: PageProps) {
|
|||||||
className={`text-xs font-medium px-2 py-0.5 rounded shrink-0 ${
|
className={`text-xs font-medium px-2 py-0.5 rounded shrink-0 ${
|
||||||
(t.type === 'LIQUIDATE_LONG' || t.type === 'LIQUIDATE_SHORT')
|
(t.type === 'LIQUIDATE_LONG' || t.type === 'LIQUIDATE_SHORT')
|
||||||
? 'bg-orange-500/15 text-orange-400'
|
? 'bg-orange-500/15 text-orange-400'
|
||||||
|
: (t.type === 'FUND_INVEST' || t.type === 'FUND_REDEEM')
|
||||||
|
? 'bg-indigo-500/15 text-indigo-400'
|
||||||
: t.type.startsWith('BUY')
|
: t.type.startsWith('BUY')
|
||||||
? 'bg-emerald-500/15 text-emerald-400'
|
? 'bg-emerald-500/15 text-emerald-400'
|
||||||
: 'bg-red-500/15 text-red-400'
|
: 'bg-red-500/15 text-red-400'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{(t.type === 'LIQUIDATE_LONG' || t.type === 'LIQUIDATE_SHORT') ? 'LIQUIDATED' : t.type.replace('_', ' ')}
|
{(t.type === 'LIQUIDATE_LONG' || t.type === 'LIQUIDATE_SHORT') ? 'LIQUIDATED' : t.type.replace(/_/g, ' ')}
|
||||||
</span>
|
</span>
|
||||||
|
{(t.type === 'FUND_INVEST' || t.type === 'FUND_REDEEM') ? (
|
||||||
|
t.fund ? (
|
||||||
|
<Link
|
||||||
|
href={`/fund/${t.fund.slug}`}
|
||||||
|
className="text-indigo-300 hover:text-indigo-200 font-medium truncate flex-1 min-w-0"
|
||||||
|
>
|
||||||
|
{t.fund.name}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<span className="text-slate-500 font-medium flex-1 min-w-0">Deleted Fund</span>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
<Link
|
<Link
|
||||||
href={`/hashtag/${t.hashtag!.tag}`}
|
href={`/hashtag/${t.hashtag!.tag}`}
|
||||||
className="text-indigo-300 hover:text-indigo-200 font-medium truncate flex-1 min-w-0"
|
className="text-indigo-300 hover:text-indigo-200 font-medium truncate flex-1 min-w-0"
|
||||||
>
|
>
|
||||||
#{t.hashtag!.displayTag}
|
#{t.hashtag!.displayTag}
|
||||||
</Link>
|
</Link>
|
||||||
|
)}
|
||||||
<span className="shrink-0 font-medium tabular-nums">{formatCurrency(t.total)}</span>
|
<span className="shrink-0 font-medium tabular-nums">{formatCurrency(t.total)}</span>
|
||||||
</div>
|
</div>
|
||||||
{/* Secondary row: user · time (left) shares @ price (right) */}
|
{/* Secondary row: user · time (left) shares @ price (right) */}
|
||||||
@@ -79,8 +102,8 @@ export default async function GlobalTradesPage({ searchParams }: PageProps) {
|
|||||||
</div>
|
</div>
|
||||||
<span className="shrink-0 tabular-nums ml-3">{formatNumber(t.shares)} sh @ {formatCurrency(t.price)}</span>
|
<span className="shrink-0 tabular-nums ml-3">{formatNumber(t.shares)} sh @ {formatCurrency(t.price)}</span>
|
||||||
</div>
|
</div>
|
||||||
{/* PnL: sell and liquidation trades */}
|
{/* PnL: sell, liquidation, and fund redeem trades */}
|
||||||
{(t.type === 'SELL_LONG' || t.type === 'SELL_SHORT' || t.type === 'LIQUIDATE_LONG' || t.type === 'LIQUIDATE_SHORT') && (
|
{(t.type === 'SELL_LONG' || t.type === 'SELL_SHORT' || t.type === 'LIQUIDATE_LONG' || t.type === 'LIQUIDATE_SHORT' || t.type === 'FUND_REDEEM') && (
|
||||||
<div className={`text-xs text-right ${pnlColor(t.profit)}`}>{formatPnl(t.profit)}</div>
|
<div className={`text-xs text-right ${pnlColor(t.profit)}`}>{formatPnl(t.profit)}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user