135 lines
4.3 KiB
JavaScript
135 lines
4.3 KiB
JavaScript
import { useCallback, useEffect, useState } from 'react';
|
|
import { Link } from 'react-router-dom';
|
|
import { ArrowLeft, RefreshCw } from 'lucide-react';
|
|
import { toast } from 'sonner';
|
|
import { api } from '@/api';
|
|
import { cn } from '@/lib/utils';
|
|
import { Button } from '@/components/ui/button';
|
|
import { MarkdownText } from '@/components/MarkdownText';
|
|
|
|
function formatDateTime(value) {
|
|
if (!value) return null;
|
|
const date = new Date(value);
|
|
if (Number.isNaN(date.getTime())) return value;
|
|
return date.toLocaleString();
|
|
}
|
|
|
|
function HistoryLine({ line, index }) {
|
|
const trimmed = line.trim();
|
|
|
|
if (!trimmed) return <div key={index} className="h-3" />;
|
|
if (trimmed === '---') return <div key={index} className="my-5 border-t border-border" />;
|
|
|
|
if (trimmed.startsWith('# ')) {
|
|
return (
|
|
<h1 key={index} className="text-2xl font-bold tracking-tight mb-5">
|
|
<MarkdownText text={trimmed.slice(2)} />
|
|
</h1>
|
|
);
|
|
}
|
|
|
|
if (trimmed.startsWith('## ')) {
|
|
return (
|
|
<h2 key={index} className="text-lg font-semibold tracking-tight mt-6 mb-3">
|
|
<MarkdownText text={trimmed.slice(3)} />
|
|
</h2>
|
|
);
|
|
}
|
|
|
|
if (trimmed.startsWith('### ')) {
|
|
return (
|
|
<h3 key={index} className="text-xs font-bold uppercase tracking-widest text-muted-foreground mt-5 mb-2">
|
|
<MarkdownText text={trimmed.slice(4)} />
|
|
</h3>
|
|
);
|
|
}
|
|
|
|
if (trimmed.startsWith('- ')) {
|
|
return (
|
|
<div key={index} className="flex items-start gap-2 py-1.5 text-sm leading-6">
|
|
<span className="mt-2 h-1.5 w-1.5 shrink-0 rounded-full bg-primary/70" />
|
|
<p><MarkdownText text={trimmed.slice(2)} /></p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<p key={index} className="text-sm leading-6 text-muted-foreground">
|
|
<MarkdownText text={line} />
|
|
</p>
|
|
);
|
|
}
|
|
|
|
export default function ReleaseNotesPage() {
|
|
const [data, setData] = useState(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState(null);
|
|
|
|
const load = useCallback(async () => {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
setData(await api.releaseHistory());
|
|
} catch (err) {
|
|
setError(err.message || 'Failed to load release notes.');
|
|
toast.error(err.message || 'Failed to load release notes.');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => { load(); }, [load]);
|
|
|
|
const history = data?.history || '';
|
|
|
|
return (
|
|
<div className="space-y-5">
|
|
<div className="flex items-center justify-between gap-3">
|
|
<div>
|
|
<Button asChild variant="ghost" size="sm" className="mb-2 -ml-2">
|
|
<Link to="/status">
|
|
<ArrowLeft className="h-3.5 w-3.5" />
|
|
Status
|
|
</Link>
|
|
</Button>
|
|
<h1 className="text-2xl font-bold tracking-tight">Release Notes</h1>
|
|
<p className="text-sm text-muted-foreground mt-0.5">
|
|
{data?.version ? `Current version v${data.version}` : 'Full project changelog'}
|
|
{data?.updated_at ? ` · Updated ${formatDateTime(data.updated_at)}` : ''}
|
|
</p>
|
|
</div>
|
|
<Button variant="outline" size="sm" onClick={load} disabled={loading}>
|
|
<RefreshCw className={cn('h-3.5 w-3.5 mr-1.5', loading && 'animate-spin')} />
|
|
Refresh
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="table-surface">
|
|
{loading ? (
|
|
<div className="p-6 space-y-3 animate-pulse">
|
|
<div className="h-4 w-40 rounded bg-muted" />
|
|
<div className="h-3 w-full rounded bg-muted" />
|
|
<div className="h-3 w-5/6 rounded bg-muted" />
|
|
<div className="h-3 w-3/4 rounded bg-muted" />
|
|
</div>
|
|
) : error ? (
|
|
<div className="p-6">
|
|
<p className="text-sm font-medium text-destructive">Unable to load release notes.</p>
|
|
<p className="text-sm text-muted-foreground mt-1">{error}</p>
|
|
</div>
|
|
) : !history.trim() ? (
|
|
<div className="p-6">
|
|
<p className="text-sm text-muted-foreground">No release notes are available.</p>
|
|
</div>
|
|
) : (
|
|
<div className="p-6">
|
|
{history.split('\n').map((line, index) => (
|
|
<HistoryLine key={`${index}-${line}`} line={line} index={index} />
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|