The profiles router handles user profile management, including bio, skills, work availability, and reputation/rating systems.
Queries
getProfile
Get a user’s public profile by user ID or handle.
Access: Public (respects privacy settings)
User handle (3-20 characters)
Either userId or handle must be provided.
Response:
{
success: boolean;
data: {
user: {
id: string;
name: string | null;
email: string; // Empty if profile is private
image: string | null;
handle: string | null;
isProfilePrivate: boolean;
createdAt: Date;
};
profile: UserProfile | null; // null if private
reputation: UserReputation | null; // null if private
};
isPrivate: boolean;
}
Example:
// Get by handle
const { data } = await trpc.profiles.getProfile.query({
handle: "johndoe"
});
// Get by user ID
const { data } = await trpc.profiles.getProfile.query({
userId: "uuid-here"
});
if (data?.isPrivate) {
console.log("This profile is private");
} else {
console.log(`Bio: ${data?.data.profile?.bio}`);
}
getMyProfile
Get the current user’s own profile.
Access: Protected
Response:
{
success: boolean;
data: {
user: {
id: string;
name: string | null;
email: string;
image: string | null;
createdAt: Date;
};
profile: UserProfile | null;
reputation: UserReputation | null;
};
}
getTopContributors
Get top contributors ranked by earnings, bounties completed, or rating.
Access: Public
Number of contributors to return (1-50)
sortBy
string
default:"totalEarned"
Sort criteria: totalEarned, bountiesCompleted, or averageRating
Example:
const { data } = await trpc.profiles.getTopContributors.query({
limit: 10,
sortBy: 'bountiesCompleted'
});
data?.data.forEach(contributor => {
console.log(`${contributor.user.name}: ${contributor.reputation.bountiesCompleted} bounties`);
});
getUserRatings
Get ratings/reviews for a specific user.
Access: Public
Response:
{
success: boolean;
data: Array<{
rating: {
id: string;
rating: number; // 1-5
comment: string | null;
createdAt: Date;
};
rater: {
id: string;
name: string | null;
image: string | null;
};
}>;
pagination: {
page: number;
limit: number;
total: number;
totalPages: number;
};
}
searchProfiles
Search user profiles by name, bio, or skills.
Access: Public
Search term (minimum 1 character)
Filter by specific skills
Filter by work availability
Example:
const { data } = await trpc.profiles.searchProfiles.query({
query: "react developer",
skills: ["React", "TypeScript"],
availableForWork: true,
limit: 20
});
Mutations
updateProfile
Update the current user’s profile.
Access: Protected
User bio (max 500 characters)
Location (max 100 characters)
GitHub username (max 50 characters)
Twitter/X username (max 50 characters)
Preferred programming languages
Hourly rate (must match pattern: /^\d+(\.\d{1,2})?$/)
Whether user is available for work
Example:
const result = await trpc.profiles.updateProfile.mutate({
bio: "Full-stack developer specializing in React and Node.js",
location: "San Francisco, CA",
website: "https://johndoe.dev",
githubUsername: "johndoe",
skills: ["React", "TypeScript", "Node.js", "PostgreSQL"],
preferredLanguages: ["JavaScript", "TypeScript", "Python"],
hourlyRate: "75.00",
currency: "USD",
availableForWork: true
});
console.log(result.message); // "Profile updated successfully"
rateUser
Rate/review a user after completing a bounty together.
Access: Protected
UUID of the bounty context
Optional review comment (max 500 characters)
Restrictions:
- Cannot rate yourself
- Can only rate once per user per bounty
Example:
try {
const result = await trpc.profiles.rateUser.mutate({
ratedUserId: "user-uuid",
bountyId: "bounty-uuid",
rating: 5,
comment: "Great work! Delivered on time and exceeded expectations."
});
console.log(result.message); // "Rating submitted successfully"
} catch (error) {
if (error.message.includes('already rated')) {
console.error('You already rated this user for this bounty');
}
}
Code Examples
Display User Profile
import { trpc } from '@/lib/trpc';
const UserProfile = ({ handle }: { handle: string }) => {
const { data, isLoading } = trpc.profiles.getProfile.useQuery({ handle });
if (isLoading) return <div>Loading...</div>;
if (data?.isPrivate) return <div>This profile is private</div>;
const { user, profile, reputation } = data?.data || {};
return (
<div className="profile">
<img src={user?.image || '/default-avatar.png'} alt={user?.name || 'User'} />
<h1>{user?.name}</h1>
<p>@{user?.handle}</p>
{profile && (
<div>
<p>{profile.bio}</p>
<p>Location: {profile.location}</p>
<p>Skills: {profile.skills?.join(', ')}</p>
{profile.availableForWork && (
<span className="badge">Available for work</span>
)}
</div>
)}
{reputation && (
<div className="stats">
<div>
<strong>{reputation.bountiesCompleted}</strong>
<span>Bounties Completed</span>
</div>
<div>
<strong>${reputation.totalEarned}</strong>
<span>Total Earned</span>
</div>
<div>
<strong>{reputation.averageRating}/5</strong>
<span>Average Rating</span>
</div>
</div>
)}
</div>
);
};
import { trpc } from '@/lib/trpc';
import { useState } from 'react';
const ProfileEditor = () => {
const { data: profile } = trpc.profiles.getMyProfile.useQuery();
const updateMutation = trpc.profiles.updateProfile.useMutation();
const [formData, setFormData] = useState({
bio: profile?.data.profile?.bio || '',
location: profile?.data.profile?.location || '',
skills: profile?.data.profile?.skills || [],
availableForWork: profile?.data.profile?.availableForWork || false
});
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
await updateMutation.mutateAsync(formData);
alert('Profile updated!');
} catch (error) {
alert(`Error: ${error.message}`);
}
};
return (
<form onSubmit={handleSubmit}>
<textarea
value={formData.bio}
onChange={(e) => setFormData({ ...formData, bio: e.target.value })}
placeholder="Tell us about yourself..."
maxLength={500}
/>
<input
type="text"
value={formData.location}
onChange={(e) => setFormData({ ...formData, location: e.target.value })}
placeholder="Location"
maxLength={100}
/>
<label>
<input
type="checkbox"
checked={formData.availableForWork}
onChange={(e) => setFormData({ ...formData, availableForWork: e.target.checked })}
/>
Available for work
</label>
<button type="submit" disabled={updateMutation.isLoading}>
{updateMutation.isLoading ? 'Saving...' : 'Save Profile'}
</button>
</form>
);
};
Top Contributors Leaderboard
import { trpc } from '@/lib/trpc';
const Leaderboard = () => {
const { data } = trpc.profiles.getTopContributors.useQuery({
limit: 10,
sortBy: 'totalEarned'
});
return (
<div className="leaderboard">
<h2>Top Contributors</h2>
<ol>
{data?.data.map((contributor, index) => (
<li key={contributor.user.id}>
<span className="rank">#{index + 1}</span>
<img src={contributor.user.image || '/default-avatar.png'} />
<div>
<strong>{contributor.user.name}</strong>
<div className="stats">
<span>${contributor.reputation?.totalEarned} earned</span>
<span>{contributor.reputation?.bountiesCompleted} bounties</span>
<span>{contributor.reputation?.averageRating}/5 rating</span>
</div>
</div>
</li>
))}
</ol>
</div>
);
};
Rate a User
import { trpc } from '@/lib/trpc';
import { useState } from 'react';
const RateUserForm = ({ userId, bountyId }: { userId: string; bountyId: string }) => {
const [rating, setRating] = useState(5);
const [comment, setComment] = useState('');
const rateMutation = trpc.profiles.rateUser.useMutation();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
await rateMutation.mutateAsync({
ratedUserId: userId,
bountyId,
rating,
comment
});
alert('Rating submitted!');
} catch (error) {
alert(error.message);
}
};
return (
<form onSubmit={handleSubmit}>
<div className="star-rating">
{[1, 2, 3, 4, 5].map(star => (
<button
key={star}
type="button"
onClick={() => setRating(star)}
className={star <= rating ? 'active' : ''}
>
★
</button>
))}
</div>
<textarea
value={comment}
onChange={(e) => setComment(e.target.value)}
placeholder="Write a review (optional)"
maxLength={500}
/>
<button type="submit" disabled={rateMutation.isLoading}>
Submit Rating
</button>
</form>
);
};