183 lines
6.5 KiB
TypeScript
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>
|
|
)
|
|
}
|