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:
- Account Information - Personal details and contact info
- Solver Status - Employment type, specialization, and revenue share
- 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:
| Field | Description |
|---|---|
| Name | Display name shown to customers |
| Account email (managed via Clerk) | |
| Phone | Contact phone number |
| Member Since | Account creation date |
Editing Mode
Clicking "Edit Profile" enables inline editing for mutable fields:
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:
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
| Field | Description |
|---|---|
| Status | Current availability (available, busy, offline, onboarding) |
| Employment Type | W-2 (employed) or 1099 (contract) |
| Specialization | Solver type (general, specialist) |
| Revenue Share | Percentage of session earnings kept by solver |
| Google Email | Workspace 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
| Metric | Description | Format |
|---|---|---|
| Total Sessions | Completed support sessions | Integer |
| Total Earnings | Lifetime earnings | Currency (USD) |
| Average Rating | Mean customer rating | Decimal (1-5) with star icon |
| Completion Rate | Sessions completed vs started | Percentage |
Data Source
Statistics come from the getFullProfile query which aggregates session data:
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,CardDescriptionButton- Edit, Save, Cancel actionsInput- Text fields in edit modeLabel- Form field labelsBadge- Status indicatorSkeleton- Loading placeholders
File Location
app/(authenticated)/dashboard/profile/
└── page.tsx # Profile page componentRelated Documentation
- Settings Page - Configure preferences
- Dashboard - Solver home page
- User Role System - Role-based access