Files
kandev/apps/web/components/settings/changelog-list.tsx
T
2026-02-26 23:08:15 +00:00

135 lines
4.4 KiB
TypeScript

"use client";
import { useMemo, useState } from "react";
import ReactMarkdown from "react-markdown";
import { Card, CardContent, CardHeader, CardTitle } from "@kandev/ui/card";
import { Badge } from "@kandev/ui/badge";
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
PaginationEllipsis,
} from "@kandev/ui/pagination";
import { IconExternalLink } from "@tabler/icons-react";
import { remarkPlugins, markdownComponents } from "@/components/shared/markdown-components";
import { getChangelog, type ChangelogEntry } from "@/lib/changelog";
import { getReleaseUrl } from "@/lib/release-notes";
const PAGE_SIZE = 10;
function ChangelogEntryCard({ entry }: { entry: ChangelogEntry }) {
const releaseUrl = getReleaseUrl(entry.version);
return (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="text-base flex items-center gap-2">
<Badge variant="secondary">v{entry.version}</Badge>
{entry.date && (
<span className="text-sm font-normal text-muted-foreground">{entry.date}</span>
)}
</CardTitle>
<a
href={releaseUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors cursor-pointer"
>
View on GitHub
<IconExternalLink className="h-3 w-3" />
</a>
</div>
</CardHeader>
<CardContent>
<div className="text-sm">
<ReactMarkdown remarkPlugins={remarkPlugins} components={markdownComponents}>
{entry.notes}
</ReactMarkdown>
</div>
</CardContent>
</Card>
);
}
function buildPageNumbers(currentPage: number, totalPages: number): (number | "ellipsis")[] {
if (totalPages <= 5) return Array.from({ length: totalPages }, (_, i) => i + 1);
const pages: (number | "ellipsis")[] = [1];
if (currentPage > 3) pages.push("ellipsis");
const start = Math.max(2, currentPage - 1);
const end = Math.min(totalPages - 1, currentPage + 1);
for (let i = start; i <= end; i++) pages.push(i);
if (currentPage < totalPages - 2) pages.push("ellipsis");
pages.push(totalPages);
return pages;
}
export function ChangelogList() {
const changelog = getChangelog();
const [currentPage, setCurrentPage] = useState(1);
const totalPages = Math.ceil(changelog.length / PAGE_SIZE);
const pageEntries = useMemo(() => {
const start = (currentPage - 1) * PAGE_SIZE;
return changelog.slice(start, start + PAGE_SIZE);
}, [changelog, currentPage]);
if (changelog.length === 0) {
return <p className="text-sm text-muted-foreground">No changelog entries available.</p>;
}
const pageNumbers = buildPageNumbers(currentPage, totalPages);
return (
<div className="space-y-4">
{pageEntries.map((entry) => (
<ChangelogEntryCard key={entry.version} entry={entry} />
))}
{totalPages > 1 && (
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
className={currentPage === 1 ? "pointer-events-none opacity-50" : "cursor-pointer"}
/>
</PaginationItem>
{pageNumbers.map((page, i) =>
page === "ellipsis" ? (
<PaginationItem key={`ellipsis-${i}`}>
<PaginationEllipsis />
</PaginationItem>
) : (
<PaginationItem key={page}>
<PaginationLink
isActive={currentPage === page}
onClick={() => setCurrentPage(page)}
className="cursor-pointer"
>
{page}
</PaginationLink>
</PaginationItem>
),
)}
<PaginationItem>
<PaginationNext
onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))}
className={
currentPage === totalPages ? "pointer-events-none opacity-50" : "cursor-pointer"
}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
)}
</div>
);
}