website-monitor/frontend/components/seo-ranking-card.tsx

102 lines
4.5 KiB
TypeScript

'use client'
import { useQuery } from '@tanstack/react-query'
import { monitorAPI } from '@/lib/api'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Sparkline } from '@/components/sparkline'
interface Props {
monitorId: string
keywords: string[]
}
export function SEORankingCard({ monitorId, keywords }: Props) {
const { data: rankings, isLoading } = useQuery({
queryKey: ['rankings', monitorId],
queryFn: async () => {
const response = await monitorAPI.rankings(monitorId)
return response // { history: [], latest: [] }
}
})
if (isLoading) {
return (
<Card>
<CardContent className="py-6 flex justify-center">
<div className="h-6 w-6 animate-spin rounded-full border-2 border-primary border-t-transparent" />
</CardContent>
</Card>
)
}
const { latest = [], history = [] } = rankings || {}
// Group history by keyword for sparklines
const historyByKeyword = (history as any[]).reduce((acc, item) => {
if (!acc[item.keyword]) acc[item.keyword] = []
acc[item.keyword].push(item)
return acc
}, {} as Record<string, any[]>)
return (
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{keywords.map(keyword => {
const latestRank = latest.find((r: any) => r.keyword === keyword)
const keywordHistory = historyByKeyword[keyword] || []
// Sort history by date asc for sparkline
const rankHistory = keywordHistory
.sort((a: any, b: any) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())
.map((item: any) => item.rank || 101) // Use 101 for unranked
return (
<Card key={keyword} className="overflow-hidden">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium flex justify-between items-start">
<span className="truncate pr-2" title={keyword}>{keyword}</span>
{latestRank?.rank ? (
<Badge variant={latestRank.rank <= 3 ? 'success' : latestRank.rank <= 10 ? 'default' : 'secondary'}>
#{latestRank.rank}
</Badge>
) : (
<Badge variant="outline" className="text-muted-foreground">
Not found
</Badge>
)}
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-xs text-muted-foreground mb-3">
{latestRank?.urlFound ? (
<a href={latestRank.urlFound} target="_blank" rel="noopener noreferrer" className="hover:underline truncate block">
{new URL(latestRank.urlFound).pathname}
</a>
) : (
<span>Not in top 100</span>
)}
</div>
{rankHistory.length > 1 && (
<div className="h-10 w-full mt-2">
{/* Simple visualization if Sparkline component accepts array */}
<Sparkline
data={rankHistory}
color={latestRank?.rank ? "#8b5cf6" : "#cbd5e1"}
height={40}
width={100}
/>
</div>
)}
<div className="mt-2 text-[10px] text-muted-foreground text-right">
Last checked: {latestRank ? new Date(latestRank.createdAt).toLocaleDateString() : 'Never'}
</div>
</CardContent>
</Card>
)
})}
</div>
)
}