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