import { useEffect, useMemo, useState } from 'react'; import { fetchChangelog, type ChangelogEntry } from '../lib/changelog'; import { SparklesIcon, SpinnerIcon } from '../components/icons'; const PAGE_SIZE = 10; // Installed app version comes from the preload bridge (process.env.npm_- // package_version at preload build time). Falls back to '0.0.0' outside // Electron so the page still renders in a browser preview. const installedVersion = window.electronAPI?.appVersion ?? '0.0.0'; export function ChangelogPage() { const [entries, setEntries] = useState(null); const [error, setError] = useState(null); const [visible, setVisible] = useState(PAGE_SIZE); // Compare the installed version against the top changelog entry. The // server-side changelog is sorted newest-first by the release script, so // entries[0] is always the published latest. const latestVersion = entries?.[0]?.version ?? null; const versionStatus = useMemo<'loading' | 'current' | 'outdated' | 'ahead'>(() => { if (entries === null) return 'loading'; if (!latestVersion) return 'current'; const cmp = compareSemver(installedVersion, latestVersion); if (cmp === 0) return 'current'; if (cmp < 0) return 'outdated'; return 'ahead'; }, [entries, latestVersion]); useEffect(() => { let cancelled = false; void (async () => { try { const list = await fetchChangelog(); if (!cancelled) setEntries(list); } catch (err: unknown) { if (!cancelled) { setError(err instanceof Error ? err.message : 'Konnte Changelog nicht laden.'); } } })(); return () => { cancelled = true; }; }, []); return (

Was ist neu

Alle Änderungen in dieser App, neueste zuerst.

{entries === null && !error && (
Lade Changelog…
)} {error && (

Fehler beim Laden

{error}

)} {entries !== null && entries.length === 0 && !error && (
Noch keine Einträge vorhanden.
)} {entries && entries.length > 0 && (
    {entries.slice(0, visible).map((entry) => (
  1. v{entry.version}

    {entry.notes}

  2. ))} {visible < entries.length && (
  3. )}
)}
); } function formatDate(iso: string): string { try { const d = new Date(iso); if (Number.isNaN(d.getTime())) return iso; return d.toLocaleDateString('de-DE', { year: 'numeric', month: 'short', day: 'numeric', }); } catch { return iso; } } // Compact status chip in the header that tells the user whether their // installed build matches the latest published version. Three visual // tones: emerald (current), amber (outdated → update available), neutral // (loading / unknown). The "ahead" case (dev build > released) shares the // neutral tone since users running it always know what they're doing. function VersionBadge({ status, installed, latest, }: { status: 'loading' | 'current' | 'outdated' | 'ahead'; installed: string; latest: string | null; }) { if (status === 'loading') { return ( v{installed} ); } if (status === 'current') { return ( v{installed} · aktuell ); } if (status === 'outdated' && latest) { return ( v{installed} · Update verfügbar neueste: v{latest} ); } return ( v{installed} ); } // Lightweight semver comparator: parses major.minor.patch as ints and // compares numerically. Returns negative if a < b, zero if equal, positive // if a > b. Handles malformed inputs by treating non-numeric segments as // 0 so a typo doesn't flag a perfectly current install as outdated. function compareSemver(a: string, b: string): number { const parse = (s: string): [number, number, number] => { const parts = s.split('.').map((p) => { const n = parseInt(p, 10); return Number.isFinite(n) ? n : 0; }); return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0]; }; const [aMaj, aMin, aPat] = parse(a); const [bMaj, bMin, bPat] = parse(b); if (aMaj !== bMaj) return aMaj - bMaj; if (aMin !== bMin) return aMin - bMin; return aPat - bPat; }