local-frontier / src /components /hardware /HardwareDatabase.tsx
Onur Solmaz
feat: render hardware database with shadcn
9695414
Raw
History Blame Contribute Delete
5.65 kB
import * as React from "react";
import { Card, CardContent } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import type { HardwareDatabaseRow, SelectOption } from "@/types/local-frontier";
export type HardwareDatabaseProps = {
rows: readonly HardwareDatabaseRow[];
producers: readonly SelectOption[];
priceBands: readonly SelectOption[];
};
function matchesRow(
row: HardwareDatabaseRow,
search: string,
producer: string,
priceBand: string,
): boolean {
const query = search.trim().toLowerCase();
return (
(!query || row.searchText.includes(query)) &&
(producer === "all" || row.acceleratorVendorKey === producer) &&
(priceBand === "all" || row.priceBand === priceBand)
);
}
function FilterSelect({
label,
value,
options,
onValueChange,
}: {
label: string;
value: string;
options: readonly SelectOption[];
onValueChange: (value: string) => void;
}): React.JSX.Element {
return (
<label className="grid gap-1.5">
<span className="text-xs font-semibold text-muted-foreground">
{label}
</span>
<Select value={value} onValueChange={onValueChange}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{options.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</label>
);
}
function HardwareRow({ row }: { row: HardwareDatabaseRow }): React.JSX.Element {
return (
<TableRow>
<TableCell>
<a
className="font-semibold text-foreground underline-offset-2 hover:underline"
href={row.deviceHref}
>
{row.name}
</a>
<span className="mt-1 inline-flex border border-border px-2 py-0.5 text-xs">
{row.kind}
</span>
</TableCell>
<TableCell>
<a
className="text-foreground underline-offset-2 hover:underline"
href={row.systemVendorHref}
>
{row.systemVendor}
</a>
</TableCell>
<TableCell>
<a
className="text-foreground underline-offset-2 hover:underline"
href={row.acceleratorVendorHref}
>
{row.acceleratorVendor}
</a>
</TableCell>
<TableCell>
<span>{row.memory}</span>
<span className="ml-1 inline-flex border border-border px-1.5 py-0.5 text-xs">
{row.memorySymbol}
</span>
<span className="block">{row.memoryType}</span>
</TableCell>
<TableCell>{row.bandwidth}</TableCell>
<TableCell>
{row.latestPrice}
{row.estimated ? (
<span title="Estimated or reconstructed price"></span>
) : null}
</TableCell>
<TableCell>{row.priceBandLabel}</TableCell>
<TableCell>
<span>{row.evidence}</span>
<span className="mt-1 block text-xs">{row.confidence}</span>
</TableCell>
</TableRow>
);
}
function HardwareTable({
rows,
}: {
rows: readonly HardwareDatabaseRow[];
}): React.JSX.Element {
if (!rows.length) {
return (
<div className="p-8 text-muted-foreground">
No devices match the current filters.
</div>
);
}
return (
<Table className="min-w-[1120px]">
<TableHeader>
<TableRow>
<TableHead>Device</TableHead>
<TableHead>System vendor</TableHead>
<TableHead>Accelerator vendor</TableHead>
<TableHead>Memory</TableHead>
<TableHead>Bandwidth</TableHead>
<TableHead>Latest price</TableHead>
<TableHead>Price band</TableHead>
<TableHead>Evidence</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{rows.map((row) => (
<HardwareRow key={row.id} row={row} />
))}
</TableBody>
</Table>
);
}
export function HardwareDatabase({
rows,
producers,
priceBands,
}: HardwareDatabaseProps): React.JSX.Element {
const [search, setSearch] = React.useState("");
const [producer, setProducer] = React.useState("all");
const [priceBand, setPriceBand] = React.useState("all");
const filteredRows = rows.filter((row) =>
matchesRow(row, search, producer, priceBand),
);
return (
<Card>
<CardContent className="grid gap-4 p-0">
<div className="grid grid-cols-[minmax(0,1fr)_minmax(12rem,16rem)_minmax(12rem,16rem)] gap-3 p-4 max-[980px]:grid-cols-1">
<label className="grid gap-1.5">
<span className="text-xs font-semibold text-muted-foreground">
Search
</span>
<Input
placeholder="Mac Studio, DGX, W7900, 128GB"
type="search"
value={search}
onChange={(event) => {
setSearch(event.currentTarget.value);
}}
/>
</label>
<FilterSelect
label="Accelerator vendor"
options={producers}
value={producer}
onValueChange={setProducer}
/>
<FilterSelect
label="Price band"
options={priceBands}
value={priceBand}
onValueChange={setPriceBand}
/>
</div>
<HardwareTable rows={filteredRows} />
</CardContent>
</Card>
);
}