bake canonical build harness (deterministic /ship for forks) [portfolio]

This commit is contained in:
vibeasite-bot
2026-08-22 06:22:27 +01:00
parent 0bde039e65
commit 7fe1381099
77 changed files with 6566 additions and 196 deletions
+214
View File
@@ -0,0 +1,214 @@
// ---------------------------------------------------------------------------
// DataTable — pre-built sortable, paginated table wired to AnyDB
// ---------------------------------------------------------------------------
// PRE-INSTALLED. CodeWriterAgent just uses:
// import { DataTable } from "@/components/DataTable"
// <DataTable
// collection="orders"
// columns={[
// { key: 'customer', label: 'Customer' },
// { key: 'total_cents', label: 'Amount', render: (v) => `$${(v/100).toFixed(2)}` },
// { key: 'status', label: 'Status', render: (v) => <Badge>{v}</Badge> },
// { key: 'created_at', label: 'Date', render: (v) => new Date(v).toLocaleDateString() },
// ]}
// filter={{ status: 'active' }}
// pageSize={10}
// searchField="customer"
// />
// ---------------------------------------------------------------------------
import { useState, useMemo } from "react";
import { useCollection } from "@/hooks/use-anydb";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { ChevronUp, ChevronDown, Search, ChevronLeft, ChevronRight } from "lucide-react";
interface Column<T = any> {
key: string;
label: string;
sortable?: boolean;
render?: (value: any, row: T) => React.ReactNode;
className?: string;
}
interface DataTableProps<T = any> {
collection: string;
columns: Column<T>[];
filter?: Record<string, any>;
pageSize?: number;
searchField?: string;
searchPlaceholder?: string;
onRowClick?: (row: T) => void;
emptyMessage?: string;
className?: string;
}
export function DataTable<T extends Record<string, any> = any>({
collection,
columns,
filter = {},
pageSize = 10,
searchField,
searchPlaceholder = "Search...",
onRowClick,
emptyMessage = "No data found",
className = "",
}: DataTableProps<T>) {
const [page, setPage] = useState(0);
const [sortKey, setSortKey] = useState<string | null>(null);
const [sortDir, setSortDir] = useState<1 | -1>(-1);
const [search, setSearch] = useState("");
const queryFilter = useMemo(() => {
const f = { ...filter };
if (searchField && search.trim()) {
f[searchField] = { $regex: search.trim(), $options: "i" };
}
return f;
}, [filter, searchField, search]);
const sort = sortKey ? { [sortKey]: sortDir } : { created_at: -1 as const };
const { data, total, loading, error } = useCollection<T>(collection, {
filter: queryFilter,
sort,
limit: pageSize,
skip: page * pageSize,
});
const totalPages = Math.ceil(total / pageSize);
const handleSort = (key: string) => {
if (sortKey === key) {
setSortDir(d => (d === 1 ? -1 : 1));
} else {
setSortKey(key);
setSortDir(-1);
}
setPage(0);
};
if (error) {
return (
<div className="text-center py-8 text-destructive">
<p>Failed to load data: {error}</p>
<Button variant="outline" size="sm" className="mt-2" onClick={() => window.location.reload()}>
Retry
</Button>
</div>
);
}
return (
<div className={`rounded-lg border bg-card ${className}`}>
{/* Search bar */}
{searchField && (
<div className="p-4 border-b">
<div className="relative max-w-sm">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder={searchPlaceholder}
value={search}
onChange={(e) => { setSearch(e.target.value); setPage(0); }}
className="pl-9 h-10"
/>
</div>
</div>
)}
{/* Table */}
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b bg-muted/50">
{columns.map((col) => (
<th
key={col.key}
className={`px-4 py-3 text-left font-medium text-muted-foreground ${
col.sortable !== false ? "cursor-pointer select-none hover:text-foreground transition-colors" : ""
} ${col.className || ""}`}
onClick={() => col.sortable !== false && handleSort(col.key)}
>
<div className="flex items-center gap-1">
{col.label}
{col.sortable !== false && sortKey === col.key && (
sortDir === 1 ? <ChevronUp className="h-3 w-3" /> : <ChevronDown className="h-3 w-3" />
)}
</div>
</th>
))}
</tr>
</thead>
<tbody>
{loading ? (
Array.from({ length: pageSize }).map((_, i) => (
<tr key={i} className="border-b">
{columns.map((col) => (
<td key={col.key} className="px-4 py-3">
<Skeleton className="h-5 w-full" />
</td>
))}
</tr>
))
) : data.length === 0 ? (
<tr>
<td colSpan={columns.length} className="px-4 py-12 text-center text-muted-foreground">
{emptyMessage}
</td>
</tr>
) : (
data.map((row, i) => (
<tr
key={row._id || i}
className={`border-b transition-colors hover:bg-muted/30 ${
onRowClick ? "cursor-pointer" : ""
}`}
onClick={() => onRowClick?.(row)}
>
{columns.map((col) => (
<td key={col.key} className={`px-4 py-3 ${col.className || ""}`}>
{col.render ? col.render(row[col.key], row) : String(row[col.key] ?? "")}
</td>
))}
</tr>
))
)}
</tbody>
</table>
</div>
{/* Pagination */}
{totalPages > 1 && (
<div className="flex items-center justify-between px-4 py-3 border-t">
<p className="text-sm text-muted-foreground">
Showing {page * pageSize + 1}{Math.min((page + 1) * pageSize, total)} of {total}
</p>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
disabled={page === 0}
onClick={() => setPage(p => p - 1)}
className="h-8"
>
<ChevronLeft className="h-4 w-4" />
</Button>
<span className="text-sm tabular-nums">
{page + 1} / {totalPages}
</span>
<Button
variant="outline"
size="sm"
disabled={page >= totalPages - 1}
onClick={() => setPage(p => p + 1)}
className="h-8"
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</div>
)}
</div>
);
}