80 lines
2.6 KiB
TypeScript
80 lines
2.6 KiB
TypeScript
import { useEffect, useState } from 'react'
|
|
|
|
interface Release {
|
|
repo: string
|
|
version: string
|
|
name: string
|
|
publishedAt: string
|
|
url: string
|
|
}
|
|
|
|
function timeAgo(iso: string): string {
|
|
const diff = Date.now() - new Date(iso).getTime()
|
|
const days = Math.floor(diff / 86400000)
|
|
if (days === 0) return 'today'
|
|
if (days === 1) return 'yesterday'
|
|
if (days < 30) return `${days}d ago`
|
|
const months = Math.floor(days / 30)
|
|
if (months < 12) return `${months}mo ago`
|
|
return `${Math.floor(months / 12)}y ago`
|
|
}
|
|
|
|
export function GithubReleasesWidget() {
|
|
const [releases, setReleases] = useState<Release[]>([])
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [loading, setLoading] = useState(true)
|
|
|
|
useEffect(() => {
|
|
fetch('/api/updates/github')
|
|
.then(r => r.json())
|
|
.then(d => {
|
|
if (d.error) setError(d.error)
|
|
else setReleases(d.releases)
|
|
})
|
|
.catch(() => setError('Failed to fetch releases'))
|
|
.finally(() => setLoading(false))
|
|
}, [])
|
|
|
|
return (
|
|
<div className="card">
|
|
<div className="widget-header">
|
|
<div className="widget-title"><span className="dot" />GitHub Releases</div>
|
|
{!loading && !error && (
|
|
<span className="widget-badge">{releases.length} tracked</span>
|
|
)}
|
|
</div>
|
|
|
|
{loading && <div className="widget-loading">Fetching releases…</div>}
|
|
{error && <div className="widget-error">⚠ {error}</div>}
|
|
|
|
{!loading && !error && (
|
|
<div className="progress-group">
|
|
{releases.map(r => (
|
|
<div key={r.repo} className="list-item">
|
|
<div className="list-item-left" style={{ flexDirection: 'column', alignItems: 'flex-start', gap: 2 }}>
|
|
<a
|
|
href={r.url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
style={{ color: 'var(--text)', textDecoration: 'none', fontSize: 12 }}
|
|
>
|
|
{r.repo.split('/')[1]}
|
|
</a>
|
|
<span style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 10, color: 'var(--muted)' }}>
|
|
{r.repo.split('/')[0]}
|
|
</span>
|
|
</div>
|
|
<div style={{ textAlign: 'right' }}>
|
|
<div className="pill pill-green" style={{ marginBottom: 3 }}>{r.version}</div>
|
|
<div style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 10, color: 'var(--muted)' }}>
|
|
{timeAgo(r.publishedAt)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|