'use client'
import Link from 'next/link'
import { useSearchParams } from 'next/navigation'
import { ArrowLeft } from 'lucide-react'
import { Suspense, useCallback, useEffect, useRef, useState } from 'react'
import { SentimentalityBar } from '@/components/widgets/SentimentalityBar'
import { getNewsFeed, refreshNewsFeed, formatPublishedDate, type NewsItem } from '@/lib/newsApi'
// ── Pull-to-refresh constants ──────────────────────────────────────────────
const PULL_THRESHOLD = 72 // px of pull before triggering refresh
const PULL_MAX = 100 // px cap so it doesn't over-stretch
function PullIndicator({ pullY, refreshing, toast }: {
pullY: number
refreshing: boolean
toast: 'refreshed' | 'error' | null
}) {
const progress = Math.min(pullY / PULL_THRESHOLD, 1)
const circumference = 2 * Math.PI * 10
return (
0 ? 'none' : 'transform 240ms ease, top 240ms ease',
height: 56,
}}
>
{refreshing ? (
// Spinning ring
) : (
// Progress arc that fills as user pulls
)}
{/* Toast message */}
{toast && (
{toast === 'refreshed' ? 'Refreshed' : 'Unable to refresh headlines'}
)}
)
}
function InterestDetailsContent() {
const searchParams = useSearchParams()
const topic = searchParams.get('topic') || 'Topic'
const [items, setItems] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [limit, setLimit] = useState(10)
// Pull-to-refresh state
const [pullY, setPullY] = useState(0)
const [refreshing, setRefreshing] = useState(false)
const [toast, setToast] = useState<'refreshed' | 'error' | null>(null)
const scrollRef = useRef(null)
const touchStartY = useRef(0)
const isPulling = useRef(false)
const isRefreshing = useRef(false) // guards against duplicate calls
const toastTimer = useRef | null>(null)
// ── Fetch headlines ──────────────────────────────────────────────────────
const fetchHeadlines = useCallback(() => {
setLoading(true)
setError(null)
getNewsFeed(topic, 'frontend', limit)
.then(setItems)
.catch((err) => setError(err.message))
.finally(() => setLoading(false))
}, [topic, limit])
useEffect(() => { fetchHeadlines() }, [fetchHeadlines])
// ── Clear toast on unmount ───────────────────────────────────────────────
useEffect(() => () => { if (toastTimer.current) clearTimeout(toastTimer.current) }, [])
// ── Show toast then hide after 2.5s ─────────────────────────────────────
const showToast = (type: 'refreshed' | 'error') => {
setToast(type)
if (toastTimer.current) clearTimeout(toastTimer.current)
toastTimer.current = setTimeout(() => setToast(null), 2500)
}
// ── Trigger refresh ──────────────────────────────────────────────────────
const triggerRefresh = useCallback(async () => {
if (isRefreshing.current) return
isRefreshing.current = true
setRefreshing(true)
setPullY(0)
try {
const ok = await refreshNewsFeed('frontend')
if (ok) {
showToast('refreshed')
fetchHeadlines()
} else {
showToast('error')
}
} catch {
showToast('error')
} finally {
setRefreshing(false)
isRefreshing.current = false
}
}, [fetchHeadlines])
// ── Touch handlers ───────────────────────────────────────────────────────
const handleTouchStart = useCallback((e: React.TouchEvent) => {
if (isRefreshing.current) return
const el = scrollRef.current
if (el && el.scrollTop === 0) {
touchStartY.current = e.touches[0].clientY
isPulling.current = true
}
}, [])
const handleTouchMove = useCallback((e: React.TouchEvent) => {
if (!isPulling.current || isRefreshing.current) return
const dy = e.touches[0].clientY - touchStartY.current
if (dy <= 0) { setPullY(0); return }
// Rubber-band decay
const capped = Math.min(dy * 0.45, PULL_MAX)
setPullY(capped)
}, [])
const handleTouchEnd = useCallback(() => {
if (!isPulling.current) return
isPulling.current = false
if (pullY >= PULL_THRESHOLD) {
triggerRefresh()
} else {
setPullY(0)
}
}, [pullY, triggerRefresh])
return (
{/* Header */}
Top headlines for {topic}
{/* Scroll area with pull-to-refresh */}
{/* Push content down while pulling or refreshing */}
{loading && (
Loading headlines...
)}
{error && (
{error}
)}
{!loading && !error && items.length === 0 && (
No headlines found for "{topic}"
)}
{!loading && !error && items.length > 0 && (
)}
)
}
export default function InterestDetails() {
return (
Loading...
}>
)
}