BillTracker/client/pages/ReleaseNotesPage.jsx

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>
);
}