102 lines
3.2 KiB
TypeScript
102 lines
3.2 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import TimeLine from './TimeLine';
|
|
import TimeLineItem from './TimeLineItem';
|
|
|
|
type ChangeItem = { html: string; sub?: string[] };
|
|
type ChangeEntry = { version: string; dateIso: string; dateLabel: string; items: ChangeItem[] };
|
|
|
|
function fmtDateDE(iso: string) {
|
|
try {
|
|
return new Intl.DateTimeFormat('de-DE', { dateStyle: 'short' }).format(new Date(iso));
|
|
} catch {
|
|
return iso;
|
|
}
|
|
}
|
|
|
|
function parseXml(xml: string): ChangeEntry[] {
|
|
const doc = new DOMParser().parseFromString(xml, 'text/xml');
|
|
const entries = Array.from(doc.getElementsByTagName('entry')).map((e) => {
|
|
const version = e.getAttribute('version') ?? '';
|
|
const dateIso = e.getAttribute('date') ?? '';
|
|
const dateLabel = fmtDateDE(dateIso);
|
|
|
|
const items = Array.from(e.children)
|
|
.filter((c) => c.tagName === 'item')
|
|
.map((itemEl) => {
|
|
// Haupttext: nur Text- und CDATA-Knoten zusammensetzen (Subitems ausblenden)
|
|
const mainText = Array.from(itemEl.childNodes)
|
|
.filter((n) => n.nodeType === Node.TEXT_NODE || n.nodeType === Node.CDATA_SECTION_NODE)
|
|
.map((n) => n.textContent ?? '')
|
|
.join('')
|
|
.trim();
|
|
|
|
const subs = Array.from(itemEl.getElementsByTagName('subitem')).map(
|
|
(s) => (s.textContent ?? '').trim()
|
|
);
|
|
|
|
return { html: mainText, sub: subs.length ? subs : undefined };
|
|
});
|
|
|
|
return { version, dateIso, dateLabel, items };
|
|
});
|
|
|
|
// Neueste zuerst
|
|
entries.sort((a, b) => new Date(b.dateIso).getTime() - new Date(a.dateIso).getTime());
|
|
return entries;
|
|
}
|
|
|
|
export default function Changelog() {
|
|
const [data, setData] = useState<ChangeEntry[] | null>(null);
|
|
const [err, setErr] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
let alive = true;
|
|
fetch('/changelog.xml', { cache: 'no-cache' })
|
|
.then((r) => (r.ok ? r.text() : Promise.reject(r.statusText)))
|
|
.then((txt) => {
|
|
if (!alive) return;
|
|
setData(parseXml(txt));
|
|
})
|
|
.catch((e) => {
|
|
if (!alive) return;
|
|
setErr(String(e));
|
|
});
|
|
return () => {
|
|
alive = false;
|
|
};
|
|
}, []);
|
|
|
|
if (err) {
|
|
return <div className="text-sm text-red-600">Changelog konnte nicht geladen werden: {err}</div>;
|
|
}
|
|
if (!data) {
|
|
return <div className="text-sm text-gray-500">Changelog wird geladen…</div>;
|
|
}
|
|
|
|
return (
|
|
<TimeLine>
|
|
{data.map((entry) => (
|
|
<TimeLineItem key={`${entry.version}-${entry.dateIso}`} title={entry.version} date={entry.dateLabel}>
|
|
<ul className="list-disc list-inside">
|
|
{entry.items.map((it, idx) => (
|
|
<li key={idx}>
|
|
{/* HTML aus XML (z. B. <u>…</u>) ist erlaubt, da Datei aus eigenem Repo kommt */}
|
|
<span dangerouslySetInnerHTML={{ __html: it.html }} />
|
|
{it.sub && it.sub.length > 0 && (
|
|
<ul className="list-disc list-inside pl-6">
|
|
{it.sub.map((s, i) => (
|
|
<li key={i}>{s}</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</TimeLineItem>
|
|
))}
|
|
</TimeLine>
|
|
);
|
|
}
|