import { useState, useEffect } from 'react' import { Tag, Calendar, ExternalLink, ChevronDown, ChevronUp, Loader2, Heart, Coffee } from 'lucide-react' import { useTranslation } from '../../i18n' import apiClient from '../../api/client' const REPO = 'mauriceboe/NOMAD' const PER_PAGE = 10 export default function GitHubPanel() { const { t, language } = useTranslation() const [releases, setReleases] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [expanded, setExpanded] = useState({}) const [page, setPage] = useState(1) const [hasMore, setHasMore] = useState(true) const [loadingMore, setLoadingMore] = useState(false) const fetchReleases = async (pageNum = 1, append = false) => { try { const res = await apiClient.get(`/auth/github-releases`, { params: { per_page: PER_PAGE, page: pageNum } }) const data = res.data setReleases(prev => append ? [...prev, ...data] : data) setHasMore(data.length === PER_PAGE) } catch (err: unknown) { setError(err instanceof Error ? err.message : 'Unknown error') } } useEffect(() => { setLoading(true) fetchReleases(1).finally(() => setLoading(false)) }, []) const handleLoadMore = async () => { const next = page + 1 setLoadingMore(true) await fetchReleases(next, true) setPage(next) setLoadingMore(false) } const toggleExpand = (id) => { setExpanded(prev => ({ ...prev, [id]: !prev[id] })) } const formatDate = (dateStr) => { const d = new Date(dateStr) return d.toLocaleDateString(language === 'de' ? 'de-DE' : 'en-US', { day: 'numeric', month: 'short', year: 'numeric' }) } // Simple markdown-to-html for release notes (handles headers, bold, lists, links) const renderBody = (body) => { if (!body) return null const lines = body.split('\n') const elements = [] let listItems = [] const flushList = () => { if (listItems.length > 0) { elements.push( ) listItems = [] } } const inlineFormat = (text) => { return text .replace(/\*\*(.+?)\*\*/g, '$1') .replace(/`(.+?)`/g, '$1') .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1') } for (const line of lines) { const trimmed = line.trim() if (!trimmed) { flushList(); continue } if (trimmed.startsWith('### ')) { flushList() elements.push(

{trimmed.slice(4)}

) } else if (trimmed.startsWith('## ')) { flushList() elements.push(

{trimmed.slice(3)}

) } else if (/^[-*] /.test(trimmed)) { listItems.push(trimmed.slice(2)) } else { flushList() elements.push(

) } } flushList() return elements } return (

{/* Support cards */}
{ e.currentTarget.style.borderColor = '#ff5e5b'; e.currentTarget.style.boxShadow = '0 0 0 1px #ff5e5b22' }} onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border-primary)'; e.currentTarget.style.boxShadow = 'none' }} >
Ko-fi
{language === 'de' ? 'Hilft mir, TREK weiterzuentwickeln' : 'Helps me keep building TREK'}
{ e.currentTarget.style.borderColor = '#ffdd00'; e.currentTarget.style.boxShadow = '0 0 0 1px #ffdd0022' }} onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border-primary)'; e.currentTarget.style.boxShadow = 'none' }} >
Buy Me a Coffee
{language === 'de' ? 'Hilft mir, TREK weiterzuentwickeln' : 'Helps me keep building TREK'}
{/* Loading / Error / Releases */} {loading ? (
) : error ? (

{t('admin.github.error')}

{error}

) : (

{t('admin.github.title')}

{t('admin.github.subtitle').replace('{repo}', REPO)}

GitHub
{/* Timeline */}
{/* Timeline line */}
{releases.map((release, idx) => { const isLatest = idx === 0 const isExpanded = expanded[release.id] return (
{/* Timeline dot */}
{/* Release content */}
{release.tag_name} {isLatest && ( {t('admin.github.latest')} )} {release.prerelease && ( {t('admin.github.prerelease')} )}
{release.name && release.name !== release.tag_name && (

{release.name}

)}
{formatDate(release.published_at || release.created_at)} {release.author && ( {t('admin.github.by')} {release.author.login} )}
{/* Expandable body */} {release.body && (
{isExpanded && (
{renderBody(release.body)}
)}
)}
) })}
{/* Load more */} {hasMore && (
)}
)}
) }