A comprehensive user profile card that displays user information, profile photo, bio, and interactive elements.
src/components/UserInformationCard.tsx
UserInformationCard is a reusable component that fetches and displays detailed user information including profile photo, name, bio, location, education, and interactive buttons for profile management or following.
interface Props {
userId: string;
}userId: Unique identifier of the user to display
import UserInformationCard from '@/components/UserInformationCard';
function UserProfile() {
return (
<div className="max-w-md mx-auto">
<UserInformationCard userId="user-123" />
</div>
);
}import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card';
function ArticleAuthor({ authorId, authorName }) {
return (
<HoverCard openDelay={0}>
<HoverCardTrigger>
<button>{authorName}</button>
</HoverCardTrigger>
<HoverCardContent align="start">
<UserInformationCard userId={authorId} />
</HoverCardContent>
</HoverCard>
);
}function UserDirectory({ userIds }: { userIds: string[] }) {
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{userIds.map(userId => (
<div key={userId} className="border rounded-lg p-4">
<UserInformationCard userId={userId} />
</div>
))}
</div>
);
}- Profile photo: Optimized image display with fallbacks
- Name and username: Primary identification
- Bio: User description/introduction
- Location: Geographic information
- Education: Educational background
- Profile settings: For current user (edit profile)
- Follow button: For other users (future implementation)
- Responsive layout: Adapts to container size
- Skeleton loading: Animated placeholder during data fetch
- Progressive loading: Shows structure while fetching data
- Error handling: Graceful fallback for failed requests
const query = useQuery({
queryKey: ["user", userId],
queryFn: () => userActions.getUserById(userId),
});if (query.isPending) {
return (
<div className="h-45 relative flex flex-col gap-2">
{/* Animated skeleton elements */}
<div className="size-[56px] bg-gray-200 dark:bg-gray-800 animate-pulse" />
<div className="h-4 bg-gray-200 dark:bg-gray-800 animate-pulse" />
{/* More skeleton elements */}
</div>
);
}<div className="py-3 flex items-center">
{/* Avatar */}
<div className="relative mr-4">
<Image
src={getFileUrl(query.data?.profile_photo)}
alt={query.data?.name}
width={56}
height={56}
className="w-14 h-14 rounded-full object-cover border-2 border-white/90 shadow-md"
/>
</div>
{/* Name and username */}
<div>
<h2 className="text-xl font-bold">{query.data?.name}</h2>
<p className="text-sm text-muted-foreground">{query.data?.username}</p>
</div>
</div>{session?.user?.id == userId ? (
<Button className="w-full" asChild>
<Link href="/dashboard/settings">{_t("Profile Settings")}</Link>
</Button>
) : (
<Button
onClick={() => alert("Not implemented yet")}
className="w-full"
>
{_t("Follow")}
</Button>
)}<div className="space-y-3">
{/* Bio */}
<p className="text-sm leading-relaxed text-muted-foreground">
{query.data?.bio}
</p>
{/* Location */}
{query.data?.location && (
<div className="flex flex-col">
<p className="font-semibold">{_t("Location")}</p>
<p className="text-sm text-muted-foreground">{query.data?.location}</p>
</div>
)}
{/* Education */}
{query.data?.education && (
<div className="flex flex-col">
<p className="font-semibold">{_t("Education")}</p>
<p className="text-sm text-muted-foreground">{query.data?.education}</p>
</div>
)}
</div>useTranslation: For internationalized textuseSession: For current user authentication state
useQuery: For fetching user data with caching and loading states
const session = useSession();
const isCurrentUser = session?.user?.id == userId;- Shows "Profile Settings" button for current user
- Shows "Follow" button for other users
- Different interaction patterns based on authentication
{_t("Profile Settings")} // Profile settings button
{_t("Follow")} // Follow button
{_t("Location")} // Location label
{_t("Education")} // Education label- All user-facing text is translatable
- Proper text direction support
- Cultural formatting considerations
- Matches actual content layout
- Smooth animation transitions
- Progressive disclosure pattern
- Accessible loading indicators
- React Query caching prevents redundant requests
- Optimized re-renders through proper query keys
- Image optimization through
getFileUrlutility
// Flexible layout container
<div className="space-y-4">
{/* Profile header */}
{/* Interactive buttons */}
{/* Profile details */}
</div>className="w-14 h-14 rounded-full object-cover border-2 border-white/90 shadow-md"- Adapts to container width
- Mobile-optimized touch targets
- Flexible image sizing
- Appropriate text scaling
- Graceful fallback for failed requests
- Error boundaries for component crashes
- Retry mechanisms through React Query
- Conditional rendering for optional fields
- Fallback avatars for missing profile photos
- Default values for empty fields
- Proper heading hierarchy
- Descriptive image alt text
- Accessible button labels
- Screen reader friendly structure
- Focusable interactive elements
- Logical tab order
- Proper ARIA attributes
// Optimized image loading
<Image
src={getFileUrl(query.data?.profile_photo) ?? ""}
width={56}
height={56}
className="w-14 h-14 rounded-full object-cover"
/>- Proper query key structure for caching
- Automatic background refetching
- Stale-while-revalidate pattern
function ArticleAuthorHover({ authorId }) {
return (
<HoverCard>
<HoverCardTrigger asChild>
<button>View Author</button>
</HoverCardTrigger>
<HoverCardContent>
<UserInformationCard userId={authorId} />
</HoverCardContent>
</HoverCard>
);
}function UserSearchResults({ users }) {
return (
<div className="space-y-4">
{users.map(user => (
<UserInformationCard key={user.id} userId={user.id} />
))}
</div>
);
}function TeamSection({ teamMembers }) {
return (
<section>
<h2>Our Team</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{teamMembers.map(member => (
<div key={member.id} className="bg-card rounded-lg p-6">
<UserInformationCard userId={member.id} />
</div>
))}
</div>
</section>
);
}- AppImage: For optimized profile photo display
- Button: For interactive elements
- HoverCard: For popup user previews
- Link: For navigation to user profiles
- User data from
userActions.getUserById - Session data for authentication context
- File URL generation via
getFileUrl
// Proper query key structure
queryKey: ["user", userId]
// Error handling
if (query.error) {
return <ErrorMessage />;
}
// Loading states
if (query.isPending) {
return <SkeletonLoader />;
}// Progressive loading
<div className="animate-pulse">
{/* Skeleton content that matches real layout */}
</div>
// Clear visual hierarchy
<h2 className="text-xl font-bold">{name}</h2>
<p className="text-sm text-muted-foreground">{username}</p>
// Accessible interactions
<Button aria-label={`Follow ${userName}`}>
{_t("Follow")}
</Button>// Optimized images
width={56}
height={56}
className="w-14 h-14 rounded-full object-cover"
// Efficient queries
queryFn: () => userActions.getUserById(userId),
staleTime: 5 * 60 * 1000, // 5 minutes