SavvySolve Docs

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 settings

The 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.

app/(authenticated)/layout.tsx
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.

The desktop sidebar provides persistent navigation. It highlights the current route and groups related pages logically:

components/dashboard/Sidebar.tsx
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:

components/dashboard/MobileNav.tsx
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:

  1. Mobile menu trigger (hamburger icon, visible < 768px)
  2. Availability toggle (online/offline status)
  3. User menu (profile, settings, sign out)
components/dashboard/DashboardHeader.tsx
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:

components/dashboard/AvailabilityToggle.tsx
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:

MetricDescriptionIcon
Total SessionsCompleted support sessionsHeadphones
Total EarningsLifetime earnings in USDDollarSign
Average RatingMean rating from customersStar
Completion RatePercentage of sessions completedTrendingUp
app/(authenticated)/dashboard/page.tsx
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:

components/dashboard/UserMenu.tsx
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:

  1. getStats - Called on dashboard page load, returns aggregated metrics
  2. getProfile - Used for profile pages, includes user info
  3. updateStatus - Mutation for availability toggle

All procedures require authentication through Clerk. The protectedProcedure middleware verifies the session and injects the user ID into the context.

On this page