Admin panel
A CRUD members table with search, sortable columns, pagination, and validated create/edit forms.
An admin layout with a sidebar and a members resource: searchable, sortable, paginated table; create and edit dialogs with field-level validation and ARIA error wiring; activate/deactivate toggles and delete with confirmation. The data layer in src/api.ts mirrors the HTTP calls you would make (list, create, update, delete) so moving to a real API is a drop-in change.
- react
- admin
- crud
- table
- forms
- tailwind
This preview is the template's actual file tree and source code — exactly what is copied into a new project. Codexa does not publish screenshots or mock-ups of templates; run the project to see it rendered.
Files12 files
import { useMemo, useState } from "react";
import { useMembers, type Member } from "./api";
import { MemberForm } from "./MemberForm";
type SortKey = "name" | "email" | "role" | "createdAt";
const PAGE_SIZE = 4;
export default function App() {
const api = useMembers();
const [query, setQuery] = useState("");
const [sort, setSort] = useState<{ key: SortKey; dir: 1 | -1 }>({ key: "name", dir: 1 });
const [page, setPage] = useState(1);
const [editing, setEditing] = useState<Member | "new" | null>(null);
const rows = useMemo(() => {
const q = query.toLowerCase();
const filtered = api.members.filter((m) => m.name.toLowerCase().includes(q) || m.email.toLowerCase().includes(q));
return filtered.sort((a, b) => a[sort.key].localeCompare(b[sort.key]) * sort.dir);
}, [api.members, query, sort]);
const pages = Math.max(1, Math.ceil(rows.length / PAGE_SIZE));
const current = Math.min(page, pages);
const visible = rows.slice((current - 1) * PAGE_SIZE, current * PAGE_SIZE);
const toggleSort = (key: SortKey) => setSort((s) => (s.key === key ? { key, dir: s.dir === 1 ? -1 : 1 } : { key, dir: 1 }));
return (
<div className="min-h-screen bg-slate-100 text-slate-900 md:grid md:grid-cols-[220px_1fr]">
<aside className="border-b border-slate-200 bg-white p-4 md:border-b-0 md:border-r">
<p className="text-sm font-semibold tracking-wide">ADMIN</p>
<nav className="mt-4 space-y-1 text-sm" aria-label="Sections">
<span aria-current="page" className="block rounded-md bg-slate-100 px-3 py-2 font-medium">
Members
</span>
<span className="block px-3 py-2 text-slate-400">Settings (add a page)</span>
</nav>
</aside>
<main className="p-4 sm:p-6">
<div className="flex flex-wrap items-center justify-between gap-3">
<h1 className="text-xl font-semibold">Members</h1>
<button type="button" onClick={() => setEditing("new")} className="rounded-md bg-blue-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-700">
Add member
</button>
</div>
<div className="mt-4 rounded-xl border border-slate-200 bg-white">
<div className="border-b border-slate-200 p-3">
<input
value={query}
onChange={(e) => {
setQuery(e.target.value);
setPage(1);
}}
placeholder="Search by name or email"
aria-label="Search members"
className="w-full rounded-md border border-slate-300 px-3 py-1.5 text-sm sm:w-72"
/>
</div>
<div className="overflow-x-auto">
<table className="w-full min-w-[640px] text-sm">
<thead className="text-left text-slate-500">
<tr>
{(["name", "email", "role", "createdAt"] as SortKey[]).map((k) => (
<th key={k} className="p-3">
<button type="button" onClick={() => toggleSort(k)} className="font-medium hover:text-slate-900" aria-sort={sort.key === k ? (sort.dir === 1 ? "ascending" : "descending") : undefined}>
{k === "createdAt" ? "Created" : k[0].toUpperCase() + k.slice(1)}
{sort.key === k ? (sort.dir === 1 ? " ↑" : " ↓") : ""}
</button>
</th>
))}
<th className="p-3">Status</th>
<th className="p-3">
<span className="sr-only">Actions</span>
</th>
</tr>
</thead>
<tbody>
{visible.map((m) => (
<tr key={m.id} className="border-t border-slate-100">
<td className="p-3 font-medium">{m.name}</td>
<td className="p-3 text-slate-600">{m.email}</td>
<td className="p-3 capitalize">{m.role}</td>
<td className="p-3 text-slate-600">{m.createdAt}</td>
<td className="p-3">
<button type="button" onClick={() => api.update(m.id, { active: !m.active })} className={"rounded-full px-2 py-0.5 text-xs " + (m.active ? "bg-green-100 text-green-800" : "bg-slate-200 text-slate-700")} aria-label={(m.active ? "Deactivate " : "Activate ") + m.name}>
{m.active ? "Active" : "Inactive"}
</button>
</td>
<td className="p-3 text-right">
<button type="button" onClick={() => setEditing(m)} className="text-blue-600 hover:underline">
Edit
</button>
<button
type="button"
onClick={() => {
if (window.confirm("Delete " + m.name + "?")) api.remove(m.id);
}}
className="ml-3 text-red-600 hover:underline"
>
Delete
</button>
</td>
</tr>
))}
{visible.length === 0 ? (
<tr>
<td colSpan={6} className="p-8 text-center text-slate-500">
No members match.
</td>
</tr>
) : null}
</tbody>
</table>
</div>
<nav className="flex items-center justify-between border-t border-slate-200 p-3 text-sm" aria-label="Pagination">
<span className="text-slate-500">
{rows.length} member{rows.length === 1 ? "" : "s"}
</span>
<div className="flex items-center gap-2">
<button type="button" disabled={current <= 1} onClick={() => setPage(current - 1)} className="rounded border border-slate-300 px-2 py-1 disabled:opacity-40">
Previous
</button>
<span>
Page {current} of {pages}
</span>
<button type="button" disabled={current >= pages} onClick={() => setPage(current + 1)} className="rounded border border-slate-300 px-2 py-1 disabled:opacity-40">
Next
</button>
</div>
</nav>
</div>
{editing ? (
<MemberForm
initial={editing === "new" ? null : editing}
onCancel={() => setEditing(null)}
onSave={(values) => {
if (editing === "new") api.create(values);
else api.update(editing.id, values);
setEditing(null);
}}
/>
) : null}
</main>
</div>
);
}
Dependencies
Documented from the template's package.json. The runtime has a prebuilt dependency set for Vite + React + TypeScript, so installs are fast.
| Package | Version |
|---|---|
| react | ^19.1.0 |
| react-dom | ^19.1.0 |
| vite | ^6.3.0 |
| @vitejs/plugin-react | ^4.4.0 |
| tailwindcss | ^4.1.0 |
| @tailwindcss/vite | ^4.1.0 |
| typescript | ^5.8.0 |
How it runs
- Framework
- Vite + React + TypeScript
- Dev command
- npm run dev
- Build command
- npm run build
- Output directory
- dist
- Port
- 5173
- Opens at
- /
- Difficulty
- intermediate
Projects run in an isolated sandbox with a live preview. Read the projects documentation for details on settings, snapshots and deployment.
Need the raw data? GET /api/v1/templates/vite-react-admin returns this template with its files.
Build on Admin panel
Create a free account, start from this template, and ask the AI agent to turn it into your app.