SoftTraderFrontend/components/pages/InterestsPage.tsx
2026-03-20 11:10:13 -04:00

183 lines
6.5 KiB
TypeScript

'use client'
import Link from 'next/link'
import { Plus } from 'lucide-react'
import { useEffect, useState } from 'react'
import { getWatchlistQuote, type MappedQuote } from '@/lib/stockApi'
import { getUserSymbols, getUserTopics, deleteUserInterest } from '@/lib/userInterestsApi'
import { getNewsFeed } from '@/lib/newsApi'
import { StockWatchlistRow } from '@/components/widgets/StockWatchlistRow'
import { InterestHeadlineRow } from '@/components/widgets/InterestHeadlineRow'
interface TopicItem {
id: string
topic: string
headline: string
}
export function InterestsPage() {
// Symbols state
const [watchlist, setWatchlist] = useState<(MappedQuote & { id: string })[]>([])
const [loadingWatchlist, setLoadingWatchlist] = useState(true)
const [watchlistError, setWatchlistError] = useState<string | null>(null)
const [watchlistEmpty, setWatchlistEmpty] = useState(false)
// Topics state
const [topics, setTopics] = useState<TopicItem[]>([])
const [loadingTopics, setLoadingTopics] = useState(true)
const [topicsError, setTopicsError] = useState<string | null>(null)
const [topicsEmpty, setTopicsEmpty] = useState(false)
// Track which row is currently open (only one at a time)
const [openRowId, setOpenRowId] = useState<string | null>(null)
// Fetch symbols from backend, then fetch quotes for each
useEffect(() => {
setLoadingWatchlist(true)
setWatchlistError(null)
setWatchlistEmpty(false)
getUserSymbols()
.then((symbols) => {
if (symbols.length === 0) {
setWatchlistEmpty(true)
setWatchlist([])
setLoadingWatchlist(false)
return
}
return Promise.all(symbols.map((sym) => getWatchlistQuote(sym)))
.then((quotes) =>
setWatchlist(quotes.map((q, i) => ({ ...q, id: String(i + 1) })))
)
})
.catch((err) => setWatchlistError(err.message))
.finally(() => setLoadingWatchlist(false))
}, [])
// Fetch topics from backend
useEffect(() => {
setLoadingTopics(true)
setTopicsError(null)
setTopicsEmpty(false)
getUserTopics()
.then(async (topicNames) => {
if (topicNames.length === 0) {
setTopicsEmpty(true)
setTopics([])
return
}
// Fetch top headline for each topic concurrently (request-amount=1)
const topicItems = await Promise.all(
topicNames.map(async (t, i) => {
let headline = ''
try {
const news = await getNewsFeed(t, 'frontend', 1)
headline = news[0]?.title ?? ''
} catch {
// Non-fatal: card still renders without a headline preview
}
return { id: String(i + 1), topic: t, headline }
})
)
setTopics(topicItems)
})
.catch((err) => setTopicsError(err.message))
.finally(() => setLoadingTopics(false))
}, [])
// Delete handlers — return Promise so SwipeableRow can manage async state
const handleDeleteSymbol = async (symbol: string): Promise<void> => {
await deleteUserInterest('symbol', symbol)
setWatchlist((prev) => {
const next = prev.filter((s) => s.symbol !== symbol)
if (next.length === 0) setWatchlistEmpty(true)
return next
})
setOpenRowId(null)
}
const handleDeleteTopic = async (topicName: string): Promise<void> => {
await deleteUserInterest('topic', topicName)
setTopics((prev) => {
const next = prev.filter((t) => t.topic !== topicName)
if (next.length === 0) setTopicsEmpty(true)
return next
})
setOpenRowId(null)
}
return (
<div className="h-full flex flex-col">
{/* Header */}
<div className="bg-background border-b border-border px-6 py-4 flex items-center justify-between">
<h1 className="text-2xl font-bold">Interests</h1>
<Link
href="/add-item"
className="w-10 h-10 flex items-center justify-center bg-primary text-primary-foreground hover:bg-blue-700 transition-colors"
title="Add new item"
>
<Plus size={20} />
</Link>
</div>
{/* Content - Split into two halves */}
<div className="flex-1 flex flex-col">
{/* Top Half: Watchlist */}
<div className="flex-1 border-b border-border overflow-y-auto">
<div className="p-6 space-y-4">
<h2 className="text-lg font-semibold">Symbol Watchlist</h2>
{loadingWatchlist && (
<div className="text-sm text-muted-foreground">Loading symbols...</div>
)}
{watchlistError && (
<div className="text-sm text-negative">{watchlistError}</div>
)}
{!loadingWatchlist && !watchlistError && watchlistEmpty && (
<div className="text-sm text-muted-foreground">
You are not currently following any symbols. Please add a symbol first.
</div>
)}
{!loadingWatchlist && !watchlistError && !watchlistEmpty && watchlist.map((stock) => (
<StockWatchlistRow
key={stock.id}
stock={stock}
onDelete={() => handleDeleteSymbol(stock.symbol)}
isOpen={openRowId === `symbol-${stock.symbol}`}
onOpenChange={(open) => setOpenRowId(open ? `symbol-${stock.symbol}` : null)}
/>
))}
</div>
</div>
{/* Bottom Half: Topic Watchlist */}
<div className="flex-1 overflow-y-auto">
<div className="p-6 space-y-4">
<h2 className="text-lg font-semibold">Topic Watchlist</h2>
{loadingTopics && (
<div className="text-sm text-muted-foreground">Loading topics...</div>
)}
{topicsError && (
<div className="text-sm text-negative">{topicsError}</div>
)}
{!loadingTopics && !topicsError && topicsEmpty && (
<div className="text-sm text-muted-foreground">
You are not currently following any topics. Please add a topic first.
</div>
)}
{!loadingTopics && !topicsError && !topicsEmpty && topics.map((interest) => (
<InterestHeadlineRow
key={interest.id}
interest={interest}
onDelete={() => handleDeleteTopic(interest.topic)}
isOpen={openRowId === `topic-${interest.topic}`}
onOpenChange={(open) => setOpenRowId(open ? `topic-${interest.topic}` : null)}
/>
))}
</div>
</div>
</div>
</div>
)
}