feat: add React UI for rpm.lair.cafe
- Vite + React + SWC + TypeScript SPA with react-router and react-bootstrap - Dark/light/system theme with Bootstrap 5.3 data-bs-theme - Home page with repo setup instructions and copyable code blocks - Package list and detail pages driven by packages.json - Python script to generate packages.json from repodata XML - Nginx config updated for SPA fallback, asset caching, removed autoindex - New deploy-ui workflow triggered on ui/ or nginx config changes, requires runners with nvm label - packages.json generation added to publish job after createrepo_c - Runner setup docs for nvm and sequoia-sq added to readme Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
100
ui/src/pages/PackageDetail.tsx
Normal file
100
ui/src/pages/PackageDetail.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import { Accordion, Alert, Badge, Spinner, Table } from "react-bootstrap";
|
||||
import { useParams } from "react-router";
|
||||
import { usePackages } from "../hooks/usePackages.ts";
|
||||
import { CodeBlock } from "../components/CodeBlock.tsx";
|
||||
|
||||
function formatBytes(bytes: number): string {
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
export function PackageDetail() {
|
||||
const { name } = useParams<{ name: string }>();
|
||||
const { manifest, loading, error } = usePackages();
|
||||
|
||||
if (loading) return <Spinner animation="border" />;
|
||||
if (error) return <Alert variant="danger">Failed to load packages: {error}</Alert>;
|
||||
if (!manifest) return <Alert variant="info">No package data available.</Alert>;
|
||||
|
||||
const versions = manifest.packages
|
||||
.filter((p) => p.name === name)
|
||||
.sort((a, b) => b.buildTime - a.buildTime);
|
||||
|
||||
if (versions.length === 0)
|
||||
return <Alert variant="warning">Package not found: {name}</Alert>;
|
||||
|
||||
const latest = versions[0];
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1 className="mb-1">{name}</h1>
|
||||
<p className="text-body-secondary mb-4">{latest.summary}</p>
|
||||
|
||||
<CodeBlock language="bash">{`sudo dnf install ${name}`}</CodeBlock>
|
||||
|
||||
<h2 className="mt-4 mb-3">
|
||||
Versions <Badge bg="secondary">{versions.length}</Badge>
|
||||
</h2>
|
||||
|
||||
<Table striped hover responsive>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Version</th>
|
||||
<th>Size</th>
|
||||
<th>Built</th>
|
||||
<th>Download</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{versions.map((pkg) => (
|
||||
<tr key={`${pkg.version}-${pkg.release}`}>
|
||||
<td>
|
||||
{pkg.version}-{pkg.release}
|
||||
</td>
|
||||
<td>{formatBytes(pkg.size)}</td>
|
||||
<td>{new Date(pkg.buildTime * 1000).toLocaleDateString()}</td>
|
||||
<td>
|
||||
<a href={`${manifest.baseUrl}/${pkg.rpmFilename}`}>
|
||||
{pkg.rpmFilename}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
|
||||
{versions.some((v) => v.changelog.length > 0) && (
|
||||
<>
|
||||
<h2 className="mt-4 mb-3">Changelog</h2>
|
||||
<Accordion>
|
||||
{versions
|
||||
.filter((v) => v.changelog.length > 0)
|
||||
.map((pkg) => (
|
||||
<Accordion.Item
|
||||
key={`${pkg.version}-${pkg.release}`}
|
||||
eventKey={`${pkg.version}-${pkg.release}`}
|
||||
>
|
||||
<Accordion.Header>
|
||||
{pkg.version}-{pkg.release} —{" "}
|
||||
{new Date(pkg.buildTime * 1000).toLocaleDateString()}
|
||||
</Accordion.Header>
|
||||
<Accordion.Body>
|
||||
{pkg.changelog.map((entry, i) => (
|
||||
<div key={i} className="mb-3">
|
||||
<small className="text-body-secondary">
|
||||
{new Date(entry.date * 1000).toLocaleDateString()}{" "}
|
||||
— {entry.author}
|
||||
</small>
|
||||
<pre className="mb-0 mt-1">{entry.text}</pre>
|
||||
</div>
|
||||
))}
|
||||
</Accordion.Body>
|
||||
</Accordion.Item>
|
||||
))}
|
||||
</Accordion>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user