feat: add CI status indicator and favicon
Build & Push Portfolio Image / build-push (push) Successful in 2m40s
Build & Push Portfolio Image / build-push (push) Successful in 2m40s
- Add CI status badge in header (auto-syncs from Forgejo) - Add rock-svg-icon.svg as favicon for Chrome - Add SOPS encrypted secrets for Forgejo token - Add decrypt script for local dev
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
'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
|
||||
}
|
||||
Reference in New Issue
Block a user