SavvySolve Docs

Profile Page

User profile management for viewing and editing personal information, solver status, and performance statistics

Profile Page

The profile page allows solvers to view and manage their personal information, see their solver status details, and track their performance statistics. Located at /dashboard/profile, it's accessible from the main sidebar.

Page Sections

The profile page is organized into three main cards:

  1. Account Information - Personal details and contact info
  2. Solver Status - Employment type, specialization, and revenue share
  3. Performance - All-time statistics

Account Information

This section displays basic user details with inline editing capability.

Viewing Mode

When not editing, the following fields are displayed:

FieldDescription
NameDisplay name shown to customers
EmailAccount email (managed via Clerk)
PhoneContact phone number
Member SinceAccount creation date

Editing Mode

Clicking "Edit Profile" enables inline editing for mutable fields:

app/(authenticated)/dashboard/profile/page.tsx
const handleStartEdit = () => {
  if (profile) {
    setEditName(profile.user.name ?? "");
    setEditPhone(profile.user.phone ?? "");
    setIsEditing(true);
  }
};

Editable fields:

  • Display Name
  • Phone Number

Non-editable fields:

  • Email (managed through Clerk authentication)

Saving Changes

Profile updates are persisted via the solvers.updateProfile tRPC mutation:

server/routers/solvers.ts
updateProfile: protectedProcedure
  .input(z.object({
    name: z.string().min(1).max(255).optional(),
    phone: z.string().max(50).optional().nullable(),
    callPreference: z.enum(["in_app", "bridge"]).optional(),
  }))
  .mutation(async ({ ctx, input }) => {
    const { name, phone, callPreference } = input;

    // Update user table for name and phone
    if (name !== undefined || phone !== undefined) {
      const userUpdates: Partial<typeof users.$inferInsert> = {};
      if (name !== undefined) userUpdates.name = name;
      if (phone !== undefined) userUpdates.phone = phone;

      await db.update(users)
        .set(userUpdates)
        .where(eq(users.id, ctx.auth.dbUserId));
    }

    // Update solver table for call preference
    if (callPreference !== undefined) {
      await db.update(solvers)
        .set({ callPreference })
        .where(eq(solvers.userId, ctx.auth.dbUserId));
    }

    return { success: true };
  }),

Solver Status

This read-only section shows solver-specific information that can only be modified by administrators.

Displayed Fields

FieldDescription
StatusCurrent availability (available, busy, offline, onboarding)
Employment TypeW-2 (employed) or 1099 (contract)
SpecializationSolver type (general, specialist)
Revenue SharePercentage of session earnings kept by solver
Google EmailWorkspace email for Meet integration (if configured)

Status Badges

Status is displayed with color-coded badges:

<Badge
  variant={
    solver.status === "available"
      ? "default"
      : solver.status === "busy"
        ? "secondary"
        : "outline"
  }
  className="capitalize"
>
  {solver.status}
</Badge>

Performance Statistics

The performance section shows lifetime metrics in a grid layout.

Metrics Displayed

MetricDescriptionFormat
Total SessionsCompleted support sessionsInteger
Total EarningsLifetime earningsCurrency (USD)
Average RatingMean customer ratingDecimal (1-5) with star icon
Completion RateSessions completed vs startedPercentage

Data Source

Statistics come from the getFullProfile query which aggregates session data:

server/routers/solvers.ts
getFullProfile: protectedProcedure.query(async ({ ctx }) => {
  const user = await db.query.users.findFirst({
    where: eq(users.id, ctx.auth.dbUserId),
  });

  const solver = await db.query.solvers.findFirst({
    where: eq(solvers.userId, ctx.auth.dbUserId),
  });

  // Aggregate session statistics
  const sessionStats = await db
    .select({
      totalSessions: count(),
      totalEarnings: sql<number>`COALESCE(SUM(${sessions.solverEarnings}), 0)`,
      avgRating: sql<number>`COALESCE(AVG(${sessions.rating}), 0)`,
      completedCount: sql<number>`COUNT(*) FILTER (WHERE ${sessions.status} = 'completed')`,
    })
    .from(sessions)
    .where(eq(sessions.solverId, solver.id));

  return {
    user,
    solver: {
      ...solver,
      stats: {
        totalSessions: sessionStats[0]?.totalSessions ?? 0,
        totalEarnings: sessionStats[0]?.totalEarnings ?? 0,
        averageRating: sessionStats[0]?.avgRating ?? 0,
        completionRate: calculateCompletionRate(sessionStats[0]),
      },
    },
  };
}),

Loading State

The page displays a skeleton UI while data loads:

if (isLoading) {
  return (
    <div className="space-y-6">
      <Skeleton className="h-8 w-48" />
      <Skeleton className="h-64 w-full" />
      <Skeleton className="h-48 w-full" />
    </div>
  );
}

UI Components Used

The profile page uses these shadcn/ui components:

  • Card, CardHeader, CardContent, CardTitle, CardDescription
  • Button - Edit, Save, Cancel actions
  • Input - Text fields in edit mode
  • Label - Form field labels
  • Badge - Status indicator
  • Skeleton - Loading placeholders

File Location

app/(authenticated)/dashboard/profile/
└── page.tsx    # Profile page component

On this page