Solver Dashboard
Authenticated dashboard for solvers to manage availability, view stats, and access platform features
Solver Dashboard
The solver dashboard is the central hub for tech support providers on SavvySolve. It provides real-time statistics, availability management, and navigation to all solver-facing features. The dashboard is protected by Clerk authentication and only accessible to users with solver profiles.
Architecture Overview
The dashboard uses Next.js App Router's route groups to create an authenticated layout that wraps all solver pages:
app/
└── (authenticated)/
├── layout.tsx # Auth check + sidebar + header
└── dashboard/
├── page.tsx # Stats overview
├── queue/ # Available tickets
├── sessions/ # Session history
├── earnings/ # Payment history
├── profile/ # Solver profile
└── settings/ # Account settingsThe route group (authenticated) doesn't affect the URL structure—/dashboard is the actual path—but allows sharing a layout that enforces authentication across all child routes.
Authenticated Layout
The layout in app/(authenticated)/layout.tsx performs two critical functions: verifying authentication and establishing the visual structure.
import { auth } from "@clerk/nextjs/server";
import { redirect } from "next/navigation";
import { Sidebar } from "@/components/dashboard/Sidebar";
import { DashboardHeader } from "@/components/dashboard/DashboardHeader";
export default async function AuthenticatedLayout({
children,
}: {
children: React.ReactNode;
}) {
const { userId } = await auth();
if (!userId) {
redirect("/sign-in");
}
return (
<div className="flex h-screen overflow-hidden bg-background">
<Sidebar className="hidden md:flex" />
<div className="flex flex-1 flex-col overflow-hidden">
<DashboardHeader />
<main className="flex-1 overflow-auto p-4 md:p-6">{children}</main>
</div>
</div>
);
}Clerk's auth() function runs on the server and returns the current user's ID if authenticated. Unauthenticated users are redirected to the sign-in page before the dashboard renders.
Navigation Components
Sidebar
The desktop sidebar provides persistent navigation. It highlights the current route and groups related pages logically:
const navItems = [
{ label: "Dashboard", href: "/dashboard", icon: LayoutDashboard },
{ label: "Queue", href: "/dashboard/queue", icon: ListTodo },
{ label: "My Sessions", href: "/dashboard/sessions", icon: History },
{ label: "Earnings", href: "/dashboard/earnings", icon: DollarSign },
{ label: "Profile", href: "/dashboard/profile", icon: User },
{ label: "Settings", href: "/dashboard/settings", icon: Settings },
];Active state detection handles both exact matches (/dashboard) and prefix matches (/dashboard/sessions/123), ensuring nested routes correctly highlight their parent nav item.
Mobile Navigation
On screens narrower than 768px, the sidebar is replaced with a hamburger menu that opens a slide-out sheet:
export function MobileNav() {
const [open, setOpen] = useState(false);
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<Button variant="ghost" size="icon" className="md:hidden">
<Menu className="h-6 w-6" />
</Button>
</SheetTrigger>
<SheetContent side="left">
{/* Navigation items with larger touch targets */}
</SheetContent>
</Sheet>
);
}The mobile navigation uses larger touch targets (py-3 instead of py-2) to accommodate the PRD's requirement for accessibility by users 65+.
Dashboard Header
The header sits above the main content area and contains:
- Mobile menu trigger (hamburger icon, visible < 768px)
- Availability toggle (online/offline status)
- User menu (profile, settings, sign out)
export function DashboardHeader() {
return (
<header className="sticky top-0 z-40 flex h-16 items-center justify-between border-b px-4">
<MobileNav />
<div className="flex items-center gap-4">
<AvailabilityToggle />
<UserMenu />
</div>
</header>
);
}Availability Toggle
Solvers control their availability status through a toggle button in the header. This determines whether they appear in the queue for new tickets:
export function AvailabilityToggle() {
const { data: stats, refetch } = trpc.solvers.getStats.useQuery();
const updateStatus = trpc.solvers.updateStatus.useMutation({
onSettled: () => refetch(),
});
const isAvailable = stats?.currentStatus === "available";
const handleToggle = () => {
updateStatus.mutate({
status: isAvailable ? "offline" : "available",
});
};
return (
<Button onClick={handleToggle} variant="outline">
<Circle className={isAvailable ? "fill-green-500" : "fill-muted"} />
{isAvailable ? "Available" : "Offline"}
</Button>
);
}The toggle is disabled for solvers still in onboarding or currently busy with a session. These states are managed server-side to prevent invalid state transitions.
Statistics Dashboard
The main dashboard page displays four key metrics in a responsive grid:
| Metric | Description | Icon |
|---|---|---|
| Total Sessions | Completed support sessions | Headphones |
| Total Earnings | Lifetime earnings in USD | DollarSign |
| Average Rating | Mean rating from customers | Star |
| Completion Rate | Percentage of sessions completed | TrendingUp |
const statCards = [
{
title: "Total Sessions",
value: stats?.totalSessions ?? 0,
icon: Headphones,
color: "text-blue-600",
bgColor: "bg-blue-100",
},
// ... other stats
];
return (
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
{statCards.map((stat) => (
<Card key={stat.title}>
<CardHeader>
<CardTitle>{stat.title}</CardTitle>
<stat.icon className={stat.color} />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stat.value}</div>
</CardContent>
</Card>
))}
</div>
);The grid is responsive: 1 column on mobile, 2 on tablet, 4 on desktop. This ensures stats remain readable across devices.
User Menu
The user menu dropdown provides quick access to account-related actions:
export function UserMenu() {
const { user } = useUser();
const { signOut } = useClerk();
return (
<DropdownMenu>
<DropdownMenuTrigger>
<Avatar>
<AvatarImage src={user.imageUrl} />
<AvatarFallback>{initials}</AvatarFallback>
</Avatar>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onClick={() => router.push("/dashboard/profile")}>
Profile
</DropdownMenuItem>
<DropdownMenuItem onClick={() => router.push("/dashboard/settings")}>
Settings
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={signOut}>Sign out</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}Data Flow
The dashboard fetches data through tRPC procedures defined in server/routers/solvers.ts:
getStats- Called on dashboard page load, returns aggregated metricsgetProfile- Used for profile pages, includes user infoupdateStatus- Mutation for availability toggle
All procedures require authentication through Clerk. The protectedProcedure middleware verifies the session and injects the user ID into the context.
Related Documentation
- API Layer - tRPC router implementation details
- Clerk Integration - Authentication setup
- Database Architecture - Solver and user schemas