102 lines
4.5 KiB
TypeScript
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>
|
|
)
|
|
}
|