import { useState, useEffect } from 'react' import { Tag, Calendar, ExternalLink, ChevronDown, ChevronUp, Loader2, Heart, Coffee, Bug, Lightbulb, BookOpen } from 'lucide-react' import { getLocaleForLanguage, useTranslation } from '../../i18n' import apiClient from '../../api/client' const REPO = 'mauriceboe/TREK' const PER_PAGE = 10 interface GithubRelease { id: number prerelease: boolean tag_name: string name: string | null body: string | null published_at: string | null created_at: string author: { login: string } | null [key: string]: unknown } export default function GitHubPanel({ isPrerelease = false }: { isPrerelease?: boolean }) { 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(`/admin/github-releases`, { params: { per_page: PER_PAGE, page: pageNum } }) const data = Array.isArray(res.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(getLocaleForLanguage(language), { 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.map((item, i) => (
  • ))}
) listItems = [] } } const escapeHtml = (str) => str.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"') const inlineFormat = (text) => { return escapeHtml(text) .replace(/\*\*(.+?)\*\*/g, '$1') .replace(/`(.+?)`/g, '$1') .replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, url) => { const safeUrl = url.startsWith('http://') || url.startsWith('https://') ? url : '#' return `${label}` }) } 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
{t('admin.github.support')}
{ 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
{t('admin.github.support')}
{ e.currentTarget.style.borderColor = '#5865F2'; e.currentTarget.style.boxShadow = '0 0 0 1px #5865F222' }} onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border-primary)'; e.currentTarget.style.boxShadow = 'none' }} >
Discord
Join the community
{ e.currentTarget.style.borderColor = '#ef4444'; e.currentTarget.style.boxShadow = '0 0 0 1px #ef444422' }} onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border-primary)'; e.currentTarget.style.boxShadow = 'none' }} >
{t('settings.about.reportBug')}
{t('settings.about.reportBugHint')}
{ e.currentTarget.style.borderColor = '#f59e0b'; e.currentTarget.style.boxShadow = '0 0 0 1px #f59e0b22' }} onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border-primary)'; e.currentTarget.style.boxShadow = 'none' }} >
{t('settings.about.featureRequest')}
{t('settings.about.featureRequestHint')}
{ e.currentTarget.style.borderColor = '#6366f1'; e.currentTarget.style.boxShadow = '0 0 0 1px #6366f122' }} onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border-primary)'; e.currentTarget.style.boxShadow = 'none' }} >
Wiki
{t('settings.about.wikiHint')}
{/* 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 */}
{(isPrerelease ? releases : releases.filter(r => !r.prerelease)).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 && (
)}
)}
) }