Files
riotpiao.com/components/CIStatusIndicator.tsx
T

104 lines
3.3 KiB
TypeScript
Raw Normal View History

2026-08-31 22:04:57 -07:00
'use client'
import { useState, useEffect } from 'react'
import { motion } from 'framer-motion'
import { GitBranch, CheckCircle, XCircle, Loader2, AlertCircle } from 'lucide-react'
interface CIStatus {
status: string
conclusion?: string
title?: string
branch?: string
url?: string
updatedAt?: string
error?: string
}
export function CIStatusIndicator() {
const [ciStatus, setCIStatus] = useState<CIStatus | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
const fetchStatus = async () => {
try {
const res = await fetch('/api/ci-status')
const data = await res.json()
setCIStatus(data)
} catch {
setCIStatus({ status: 'error', error: 'Failed to fetch' })
} finally {
setLoading(false)
}
}
fetchStatus()
// Poll every 60 seconds
const interval = setInterval(fetchStatus, 60000)
return () => clearInterval(interval)
}, [])
const getStatusIcon = () => {
if (loading) return <Loader2 size={14} className="animate-spin" />
if (!ciStatus || ciStatus.status === 'error') return <AlertCircle size={14} />
if (ciStatus.conclusion === 'success') return <CheckCircle size={14} />
if (ciStatus.conclusion === 'failure') return <XCircle size={14} />
if (ciStatus.status === 'in_progress' || ciStatus.status === 'queued') {
return <Loader2 size={14} className="animate-spin" />
}
return <GitBranch size={14} />
}
const getStatusColor = () => {
if (loading) return 'text-gray-400'
if (!ciStatus || ciStatus.status === 'error') return 'text-gray-400'
if (ciStatus.conclusion === 'success') return 'text-green-500'
if (ciStatus.conclusion === 'failure') return 'text-red-500'
if (ciStatus.status === 'in_progress' || ciStatus.status === 'queued') {
return 'text-yellow-500'
}
return 'text-gray-400'
}
const getStatusText = () => {
if (loading) return 'Checking...'
if (!ciStatus || ciStatus.status === 'error') return 'Unknown'
if (ciStatus.conclusion === 'success') return 'Passing'
if (ciStatus.conclusion === 'failure') return 'Failed'
if (ciStatus.status === 'in_progress') return 'Running'
if (ciStatus.status === 'queued') return 'Queued'
return ciStatus.status
}
const content = (
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.5, delay: 0.8 }}
className={`flex items-center gap-2 px-3 py-1.5 rounded-full bg-white/90 dark:bg-gray-900/90 backdrop-blur-sm border border-gray-200 dark:border-gray-700 shadow-sm ${getStatusColor()}`}
>
<GitBranch size={14} className="text-gray-500 dark:text-gray-400" />
<span className="text-xs font-medium text-gray-600 dark:text-gray-300">CI</span>
<div className={`flex items-center gap-1 ${getStatusColor()}`}>
{getStatusIcon()}
<span className="text-xs font-semibold">{getStatusText()}</span>
</div>
</motion.div>
)
if (ciStatus?.url) {
return (
<a
href={ciStatus.url}
target="_blank"
rel="noopener noreferrer"
className="hover:scale-105 transition-transform"
title={ciStatus.title || 'View CI run'}
>
{content}
</a>
)
}
return content
}