import {
AlertTriangle,
BookOpen,
Bug,
Calendar,
ChevronDown,
ChevronUp,
Coffee,
ExternalLink,
Heart,
History,
Lightbulb,
Loader2,
Tag,
} from 'lucide-react';
import { type ComponentType, type CSSProperties, type ReactNode } from 'react';
import { useTranslation } from '../../i18n';
import { escapeReleaseHtml as escapeHtml, parseReleaseNotes } from '../../utils/releaseNotes';
import { fs } from '../shared/DialogShell';
import { SETTINGS_BUTTON, SettingsCard, StatusPill } from '../Settings/settingsKit';
import { useGithubReleases } from './useGithubReleases';
const REPO = 'liketrek/TREK';
/** The Discord mark, sized and coloured like the lucide glyphs beside it. */
function DiscordIcon({ size = 18 }: { size?: number }) {
return (
);
}
interface SupportLink {
href: string;
/** The brand's own colour: the icon and the hover edge take it, nothing else. */
brand: string;
icon: ComponentType<{ size?: number }>;
title: ReactNode;
hint: ReactNode;
}
/** One link to the project's other places: a card like the bookings', its brand only on the icon. */
function SupportCard({ link }: { link: SupportLink }) {
const Icon = link.icon;
return (
{link.title}
{link.hint}
);
}
export default function GitHubPanel({ isPrerelease = false }: { isPrerelease?: boolean }) {
const { t } = useTranslation();
const {
releases, shownReleases: shown, loading, error, expanded, toggleExpand, hasMore, loadingMore, handleLoadMore, formatDate,
} = useGithubReleases({ isPrerelease });
// Simple markdown-to-html for release notes (handles headers, bold, lists, links)
const renderBody = (body: string) => {
if (!body) return null;
const inlineFormat = (text: string) => {
return escapeHtml(text)
.replace(/\*\*(.+?)\*\*/g, '$1')
.replace(
/`(.+?)`/g,
'$1'
)
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, url) => {
const safeUrl = url.startsWith('http://') || url.startsWith('https://') ? url : '#';
return `${label}`;
});
};
return parseReleaseNotes(body).map((block, idx) => {
if (block.kind === 'ul') {
return (
{block.items.map((item, i) => (
-
))}
);
}
if (block.kind === 'h4') {
return (
{block.text}
);
}
if (block.kind === 'h3') {
return (
{block.text}
);
}
return (
);
});
};
const supportLinks: SupportLink[] = [
{
href: 'https://ko-fi.com/mauriceboe',
brand: '#ff5e5b', // theme-lint-disable: Ko-fi brand colour
icon: Coffee,
title: 'Ko-fi',
hint: t('admin.github.support'),
},
{
href: 'https://buymeacoffee.com/mauriceboe',
brand: '#e0b400', // theme-lint-disable: Buy Me a Coffee brand yellow, deepened so it reads on white
icon: Heart,
title: 'Buy Me a Coffee',
hint: t('admin.github.support'),
},
{
href: 'https://discord.gg/NhZBDSd4qW',
brand: '#5865F2', // theme-lint-disable: Discord brand colour
icon: DiscordIcon,
title: 'Discord',
hint: 'Join the community',
},
{
href: 'https://github.com/liketrek/TREK/issues/new?template=bug_report.yml',
brand: 'var(--danger)',
icon: Bug,
title: t('settings.about.reportBug'),
hint: t('settings.about.reportBugHint'),
},
{
href: 'https://github.com/liketrek/TREK/discussions/new?category=feature-requests',
brand: 'var(--warning)',
icon: Lightbulb,
title: t('settings.about.featureRequest'),
hint: t('settings.about.featureRequestHint'),
},
{
href: 'https://github.com/liketrek/TREK/wiki',
brand: 'var(--text-secondary)',
icon: BookOpen,
title: 'Wiki',
hint: t('settings.about.wikiHint'),
},
];
const subtitle = t('admin.github.subtitle').replace('{repo}', REPO);
let releasesCard: ReactNode;
if (loading) {
releasesCard = (
);
} else if (error && releases.length === 0) {
releasesCard = ;
} else {
releasesCard = (
GitHub
}
>
{/* Timeline */}
{/* Timeline line, from the first dot's middle down to the last one's */}
{shown.length > 1 &&
}
{shown.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)}
)}
)}
);
})}
{/* A failed "Load more" keeps the timeline and stays retryable */}
{error && (
{t('admin.github.error')}: {error}
)}
{/* Load more */}
{hasMore && (
)}
);
}
return (
{/* Support links: one card, the six places as tiles inside it */}
{supportLinks.map((link) => )}
{/* Loading / Error / Releases */}
{releasesCard}
);
}