borsa / nextjs-app /src /lib /news.ts
veteroner's picture
fix: read eligibility from scan work snapshot
2cecfa4
import { API_BASE } from '@/lib/runtime-config'
import { fetchJson } from '@/lib/http'
/**
* News and Announcements API
*
* IMPORTANT: This module must not return fake/mock data.
* If a data source is not available, functions return empty arrays.
*/
export interface NewsArticle {
title: string
description: string
url: string
source: string
publishedAt: string
image?: string
symbol?: string
}
export interface KAPAnnouncement {
id: string
title: string
company: string
symbol: string
category: string
publishedAt: string
url: string
isImportant: boolean
}
type BackendNewsItem = {
id?: number | string
symbol?: string
title?: string
content?: string
source?: string
published_at?: string
url?: string
}
function normalizeBackendNews(item: BackendNewsItem): NewsArticle | null {
if (!item) return null
const title = String(item.title ?? '').trim()
const description = String(item.content ?? '').trim()
const url = String(item.url ?? '').trim()
const source = String(item.source ?? '').trim()
const publishedAt = String(item.published_at ?? '').trim()
if (!title || !source || !publishedAt) return null
return {
title,
description,
url: url || '',
source,
publishedAt,
symbol: item.symbol ? String(item.symbol).toUpperCase() : undefined,
}
}
/**
* Fetch Turkish finance news via backend.
* Backend endpoint (HF): GET /api/news?limit=N
*/
export async function getTurkishFinanceNews(limit: number = 20): Promise<NewsArticle[]> {
try {
const data = await fetchJson<Record<string, unknown>>(`${API_BASE}/api/news?limit=${encodeURIComponent(String(limit))}`, { method: 'GET' }, { timeoutMs: 15000, retries: 1 })
const items: BackendNewsItem[] = Array.isArray(data?.data) ? data.data : Array.isArray(data) ? data : []
return items.map(normalizeBackendNews).filter(Boolean) as NewsArticle[]
} catch (error) {
console.error('Error fetching news:', error)
return []
}
}
/**
* Fetch stock-specific news by filtering backend news response.
*/
export async function getStockNews(symbol: string, limit: number = 10): Promise<NewsArticle[]> {
const sym = String(symbol || '').toUpperCase().trim()
if (!sym) return []
// Prefer backend-side filtering when supported.
try {
const data = await fetchJson<Record<string, unknown>>(
`${API_BASE}/api/news?symbol=${encodeURIComponent(sym)}&limit=${encodeURIComponent(String(limit))}`,
{ method: 'GET' },
{ timeoutMs: 15000, retries: 1 }
)
const items: BackendNewsItem[] = Array.isArray(data?.data) ? data.data : Array.isArray(data) ? data : []
const normalized = items.map(normalizeBackendNews).filter(Boolean) as NewsArticle[]
if (normalized.length) return normalized.slice(0, limit)
} catch (error) {
// Fall back to client-side filtering.
console.error('Error fetching stock news:', error)
}
const all = await getTurkishFinanceNews(Math.max(limit * 5, 50))
const filtered = all.filter((n) => (n.symbol ? n.symbol === sym : false) || n.title.toUpperCase().includes(sym) || n.description.toUpperCase().includes(sym))
return filtered.slice(0, limit)
}
/**
* KAP announcements are not implemented on the backend yet.
* Returning empty is safer than returning fake announcements.
*/
export async function getKAPAnnouncements(_symbol?: string, _limit: number = 20): Promise<KAPAnnouncement[]> {
return []
}
/**
* Economic calendar is not implemented on the backend yet.
* Returning empty is safer than returning fake events.
*/
export async function getEconomicCalendar(_days: number = 7): Promise<Record<string, unknown>[]> {
return []
}
/**
* Format time ago (Turkish)
*/
export function formatTimeAgo(date: string | Date): string {
const now = new Date()
const past = new Date(date)
const diffMs = now.getTime() - past.getTime()
const diffMins = Math.floor(diffMs / 60000)
const diffHours = Math.floor(diffMs / 3600000)
const diffDays = Math.floor(diffMs / 86400000)
if (diffMins < 1) return 'Az önce'
if (diffMins < 60) return `${diffMins} dakika önce`
if (diffHours < 24) return `${diffHours} saat önce`
if (diffDays < 30) return `${diffDays} gün önce`
return past.toLocaleDateString('tr-TR', {
day: 'numeric',
month: 'short',
year: 'numeric'
})
}
/**
* Get combined news feed (news + KAP announcements)
*/
export async function getCombinedFeed(
symbol?: string,
limit: number = 30
): Promise<Array<NewsArticle | KAPAnnouncement>> {
try {
const news = symbol ? await getStockNews(symbol, limit) : await getTurkishFinanceNews(limit)
return news.slice(0, limit)
} catch (error) {
console.error('Error fetching combined feed:', error)
return []
}
}
/**
* Search news by keyword
*/
export async function searchNews(
query: string,
limit: number = 20
): Promise<NewsArticle[]> {
try {
const allNews = await getTurkishFinanceNews(100)
const filtered = allNews.filter(news => {
const searchText = `${news.title} ${news.description}`.toLowerCase()
return searchText.includes(query.toLowerCase())
})
return filtered.slice(0, limit)
} catch (error) {
console.error('Error searching news:', error)
return []
}
}
/**
* Get trending stocks from news mentions
*/
export async function getTrendingStocksFromNews(): Promise<Array<{
symbol: string
mentions: number
sentiment: 'positive' | 'negative' | 'neutral'
}>> {
try {
const news = await getTurkishFinanceNews(200)
const counts = new Map<string, number>()
for (const n of news) {
if (!n.symbol) continue
counts.set(n.symbol, (counts.get(n.symbol) || 0) + 1)
}
return Array.from(counts.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 10)
.map(([symbol, mentions]) => ({ symbol, mentions, sentiment: 'neutral' }))
} catch (error) {
console.error('Error getting trending stocks:', error)
return []
}
}