Skip to content

Admin Dashboard

A complete admin dashboard with sidebar navigation, live KPI stats, a CRUD data table backed by API calls, and create/edit/delete flows with loading and error handling.

import { createSnapshot } from "@lastshotlabs/snapshot";
import {
LayoutBase, NavBase, NavUserMenuBase, ContainerBase,
GridBase, RowBase, ColumnBase, SpacerBase,
StatCardBase, DataTableBase, ButtonBase,
ModalBase, DrawerBase, ConfirmDialogBase,
InputField, SelectField, AlertBase, SkeletonBase,
resolveTokens,
} from "@lastshotlabs/snapshot/ui";
import { useState, useEffect } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
const snap = createSnapshot({
apiUrl: "/api",
loginPath: "/login",
homePath: "/",
});
const snapshotCss = resolveTokens({
flavor: "neutral",
overrides: {
colors: { primary: "#3b82f6" },
font: { sans: "Inter" },
},
});
// ── Types ─────────────────────────────────────────────────────────────────
interface User {
id: string;
name: string;
email: string;
role: "admin" | "member";
status: "active" | "inactive";
}
interface Stats {
revenue: number;
revenueTrend: number;
users: number;
usersTrend: number;
orders: number;
ordersTrend: number;
conversion: number;
conversionTrend: number;
}
// ── App Shell ─────────────────────────────────────────────────────────────
export function AdminApp() {
const { user, isLoading } = snap.useUser();
const { mutate: logout } = snap.useLogout();
if (isLoading) return null;
if (!user) return null;
return (
<snap.QueryProvider>
<LayoutBase
variant="sidebar"
nav={
<NavBase
variant="sidebar"
logo={{ text: "Admin", path: "/" }}
items={[
{ label: "Dashboard", path: "/", icon: "home" },
{ label: "Users", path: "/users", icon: "users" },
{ label: "Orders", path: "/orders", icon: "shopping-cart" },
{ label: "Settings", path: "/settings", icon: "settings" },
]}
collapsible
/>
}
>
<ContainerBase maxWidth="xl" padding="lg">
<RowBase justify="between" align="center">
<h1>Dashboard</h1>
<NavUserMenuBase
userName={user.name}
userAvatar={user.avatarUrl}
items={[
{ label: "Profile", icon: "user", onClick: () => (window.location.href = "/profile") },
{ label: "Sign out", icon: "log-out", onClick: () => logout() },
]}
/>
</RowBase>
<SpacerBase size="lg" />
<StatsRow />
<SpacerBase size="xl" />
<UsersTable />
</ContainerBase>
</LayoutBase>
</snap.QueryProvider>
);
}
// ── Stats Row ─────────────────────────────────────────────────────────────
function StatsRow() {
const { data: stats, isLoading } = useQuery<Stats>({
queryKey: ["/stats/dashboard"],
queryFn: () => snap.api.get("/stats/dashboard"),
});
if (isLoading) {
return (
<GridBase columns={4} gap="md">
{[1, 2, 3, 4].map((i) => <SkeletonBase key={i} height="120px" />)}
</GridBase>
);
}
return (
<GridBase columns={4} gap="md">
<StatCardBase
label="Total Revenue"
value={`$${(stats?.revenue ?? 0).toLocaleString()}`}
icon="dollar-sign"
trend={{
direction: (stats?.revenueTrend ?? 0) >= 0 ? "up" : "down",
value: `${(stats?.revenueTrend ?? 0) >= 0 ? "+" : ""}${stats?.revenueTrend ?? 0}%`,
percentage: Math.abs(stats?.revenueTrend ?? 0),
sentiment: (stats?.revenueTrend ?? 0) >= 0 ? "positive" : "negative",
}}
/>
<StatCardBase
label="Active Users"
value={(stats?.users ?? 0).toLocaleString()}
icon="users"
trend={{
direction: (stats?.usersTrend ?? 0) >= 0 ? "up" : "down",
value: `${(stats?.usersTrend ?? 0) >= 0 ? "+" : ""}${stats?.usersTrend ?? 0}%`,
percentage: Math.abs(stats?.usersTrend ?? 0),
sentiment: (stats?.usersTrend ?? 0) >= 0 ? "positive" : "negative",
}}
/>
<StatCardBase
label="Orders"
value={(stats?.orders ?? 0).toLocaleString()}
icon="shopping-cart"
trend={{
direction: (stats?.ordersTrend ?? 0) >= 0 ? "up" : "down",
value: `${(stats?.ordersTrend ?? 0) >= 0 ? "+" : ""}${stats?.ordersTrend ?? 0}%`,
percentage: Math.abs(stats?.ordersTrend ?? 0),
sentiment: (stats?.ordersTrend ?? 0) >= 0 ? "positive" : "negative",
}}
/>
<StatCardBase
label="Conversion"
value={`${stats?.conversion ?? 0}%`}
icon="percent"
trend={{
direction: (stats?.conversionTrend ?? 0) >= 0 ? "up" : "down",
value: `${(stats?.conversionTrend ?? 0) >= 0 ? "+" : ""}${stats?.conversionTrend ?? 0}%`,
percentage: Math.abs(stats?.conversionTrend ?? 0),
sentiment: (stats?.conversionTrend ?? 0) >= 0 ? "positive" : "neutral",
}}
/>
</GridBase>
);
}
// ── Users CRUD Table ──────────────────────────────────────────────────────
function UsersTable() {
const queryClient = useQueryClient();
const [page, setPage] = useState(1);
const [search, setSearch] = useState("");
const [sort, setSort] = useState({ column: "name", direction: "asc" as "asc" | "desc" });
const pageSize = 10;
// Fetch users with server-side pagination, search, and sorting
const url = `/users?page=${page}&pageSize=${pageSize}&search=${search}&sort=${sort.column}&order=${sort.direction}`;
const { data, isLoading, error } = useQuery<{ items: User[]; total: number }>({
queryKey: ["/users", page, pageSize, search, sort.column, sort.direction],
queryFn: () => snap.api.get(url),
});
// Mutations
const createMutation = useMutation({
mutationFn: (user: Omit<User, "id">) => snap.api.post("/users", user),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["/users"] }),
});
const updateMutation = useMutation({
mutationFn: (user: User) => snap.api.patch(`/users/${user.id}`, user),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["/users"] }),
});
const deleteMutation = useMutation({
mutationFn: (id: string) => snap.api.delete(`/users/${id}`),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["/users"] }),
});
// Overlay state
const [createOpen, setCreateOpen] = useState(false);
const [editUser, setEditUser] = useState<User | null>(null);
const [deleteUser, setDeleteUser] = useState<User | null>(null);
const [successMsg, setSuccessMsg] = useState<string | null>(null);
const showSuccess = (msg: string) => {
setSuccessMsg(msg);
setTimeout(() => setSuccessMsg(null), 3000);
};
const columns = [
{ field: "name" as const, label: "Name", sortable: true },
{ field: "email" as const, label: "Email", sortable: true },
{ field: "role" as const, label: "Role", format: "badge" as const, badgeColors: { admin: "blue", member: "gray" } },
{ field: "status" as const, label: "Status", format: "badge" as const, badgeColors: { active: "green", inactive: "red" } },
];
return (
<>
{successMsg && <AlertBase severity="success" onClose={() => setSuccessMsg(null)}>{successMsg}</AlertBase>}
<RowBase justify="between" align="center">
<h2>Users</h2>
<ButtonBase label="Add User" icon="plus" onClick={() => setCreateOpen(true)} />
</RowBase>
<SpacerBase size="md" />
<DataTableBase
columns={columns}
rows={data?.items ?? []}
isLoading={isLoading}
error={error?.message}
emptyMessage="No users found"
searchable
search={search}
onSearchChange={setSearch}
sort={sort}
onSortChange={(col) => setSort((prev) =>
prev.column === col
? { column: col, direction: prev.direction === "asc" ? "desc" : "asc" }
: { column: col, direction: "asc" }
)}
pagination={{
currentPage: page,
totalPages: Math.ceil((data?.total ?? 0) / pageSize),
pageSize,
totalRows: data?.total ?? 0,
}}
onPageChange={setPage}
hoverable
striped
rowActions={[
{ label: "Edit", icon: "edit", onAction: (row) => setEditUser(row as User) },
{ label: "Delete", icon: "trash", variant: "destructive", onAction: (row) => setDeleteUser(row as User) },
]}
/>
{/* Create Modal */}
<UserFormModal
title="Create User"
open={createOpen}
onClose={() => { setCreateOpen(false); createMutation.reset(); }}
onSave={(user) => {
createMutation.mutate(user, {
onSuccess: () => { setCreateOpen(false); showSuccess(`${user.name} created`); },
});
}}
isPending={createMutation.isPending}
error={createMutation.error?.message}
/>
{/* Edit Drawer */}
<EditUserDrawer
user={editUser}
onClose={() => { setEditUser(null); updateMutation.reset(); }}
onSave={(updated) => {
updateMutation.mutate(updated, {
onSuccess: () => { setEditUser(null); showSuccess(`${updated.name} updated`); },
});
}}
isPending={updateMutation.isPending}
error={updateMutation.error?.message}
/>
{/* Delete Confirmation */}
<ConfirmDialogBase
title="Delete User"
description={`Are you sure you want to delete ${deleteUser?.name}? This action cannot be undone.`}
open={deleteUser !== null}
onClose={() => { setDeleteUser(null); deleteMutation.reset(); }}
onConfirm={() => {
if (deleteUser) {
const name = deleteUser.name;
deleteMutation.mutate(deleteUser.id, {
onSuccess: () => showSuccess(`${name} deleted`),
});
setDeleteUser(null);
}
}}
confirmLabel="Delete"
confirmVariant="destructive"
/>
</>
);
}
// ── Create User Modal ─────────────────────────────────────────────────────
function UserFormModal({ title, open, onClose, onSave, isPending, error }: {
title: string;
open: boolean;
onClose: () => void;
onSave: (user: Omit<User, "id">) => void;
isPending: boolean;
error?: string;
}) {
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [role, setRole] = useState<"admin" | "member">("member");
const handleClose = () => {
setName(""); setEmail(""); setRole("member");
onClose();
};
return (
<ModalBase
title={title}
open={open}
onClose={handleClose}
footer={[
{ label: "Cancel", variant: "outline", onClick: handleClose },
{
label: isPending ? "Creating..." : "Create",
onClick: () => onSave({ name, email, role, status: "active" }),
disabled: isPending || !name || !email,
},
]}
>
<ColumnBase gap="md">
{error && <AlertBase severity="error">{error}</AlertBase>}
<InputField label="Name" value={name} onChange={setName} required />
<InputField label="Email" type="email" value={email} onChange={setEmail} required />
<SelectField
label="Role"
value={role}
onChange={(v) => setRole(v as "admin" | "member")}
options={[{ label: "Admin", value: "admin" }, { label: "Member", value: "member" }]}
/>
</ColumnBase>
</ModalBase>
);
}
// ── Edit User Drawer ──────────────────────────────────────────────────────
function EditUserDrawer({ user, onClose, onSave, isPending, error }: {
user: User | null;
onClose: () => void;
onSave: (user: User) => void;
isPending: boolean;
error?: string;
}) {
const [draft, setDraft] = useState<User | null>(null);
// Sync draft when a new user is opened for editing
useEffect(() => {
if (user) setDraft({ ...user });
}, [user?.id]);
const activeUser = draft ?? user;
return (
<DrawerBase
title="Edit User"
side="right"
size="md"
open={user !== null}
onClose={() => { setDraft(null); onClose(); }}
footer={[
{ label: "Cancel", variant: "outline", onClick: () => { setDraft(null); onClose(); } },
{
label: isPending ? "Saving..." : "Save",
onClick: () => { if (activeUser) onSave(activeUser); },
disabled: isPending,
},
]}
>
{activeUser && (
<ColumnBase gap="md">
{error && <AlertBase severity="error">{error}</AlertBase>}
<InputField label="Name" value={activeUser.name} onChange={(v) => setDraft({ ...activeUser, name: v })} />
<InputField label="Email" type="email" value={activeUser.email} onChange={(v) => setDraft({ ...activeUser, email: v })} />
<SelectField
label="Role"
value={activeUser.role}
onChange={(v) => setDraft({ ...activeUser, role: v as "admin" | "member" })}
options={[{ label: "Admin", value: "admin" }, { label: "Member", value: "member" }]}
/>
<SelectField
label="Status"
value={activeUser.status}
onChange={(v) => setDraft({ ...activeUser, status: v as "active" | "inactive" })}
options={[{ label: "Active", value: "active" }, { label: "Inactive", value: "inactive" }]}
/>
</ColumnBase>
)}
</DrawerBase>
);
}
  • Sidebar navigation with collapsible menu
  • User dropdown with sign-out
  • Live KPI stats fetched from /stats/dashboard with skeleton loading
  • Server-paginated data table with search, sorting, and row actions
  • Create user modal with loading state and error display
  • Edit user drawer with draft state so edits don’t mutate the original
  • Delete confirmation with loading state
  • Success alerts that auto-dismiss after 3 seconds
  • Query invalidation so the table refreshes after every mutation

This recipe expects your Slingshot backend to expose:

MethodEndpointPurpose
GET/stats/dashboardReturns Stats object
GET/users?page=&pageSize=&search=&sort=&order=Returns { items: User[], total: number }
POST/usersCreates a user, returns User
PATCH/users/:idUpdates a user, returns User
DELETE/users/:idDeletes a user