Skip to main content

User Profile

The User Profile module provides comprehensive functionality for displaying and managing user profile information, including personal details, preferences, and workspace associations.

Overview

The User Profile page offers:

  • Complete profile information display
  • Profile editing capabilities
  • Avatar/profile picture management
  • Multi-language support
  • Workspace associations display
  • Address management

Profile Components

Profile Information

The profile system manages the following user information:

interface UserProfile {
id: string;
name: string;
email: string;
phoneNumber?: string;
profilePicture?: string;
preferredUsername?: string;
birthday?: string;
gender?: string;
language?: string;
address?: Address;
workspaceMembers?: WorkspaceMember[];
}

interface Address {
street?: string;
city?: string;
state?: string;
postalCode?: string;
country?: string;
}

Getting User Profile

Fetch Current User Profile

Use the GraphQL query to retrieve the current user's profile:

import { gql, useQuery } from '@apollo/client';

const GET_PROFILE = gql`
query GetProfile {
getProfile {
id
name
email
phoneNumber
profilePicture
preferredUsername
birthday
gender
language
address {
street
city
state
postalCode
country
}
workspaceMembers {
workspace {
id
name
type
}
}
}
}
`;

// In your component
const { data, loading, error } = useQuery(GET_PROFILE);
const userProfile = data?.getProfile;

Updating Profile Information

Edit Profile Mutation

Update user profile with the following mutation:

const UPDATE_PROFILE = gql`
mutation UpdateProfile($input: editUserInput!) {
editProfile(input: $input) {
id
name
email
phoneNumber
profilePicture
preferredUsername
birthday
gender
language
address {
street
city
state
postalCode
country
}
updatedAt
}
}
`;

// Usage
const [updateProfile] = useMutation(UPDATE_PROFILE);

await updateProfile({
variables: {
input: {
id: userId,
name: 'John Doe',
email: '[email protected]',
phoneNumber: '+1234567890',
preferredUsername: 'johndoe',
birthday: '1990-01-01',
gender: 'male',
language: 'en',
address: {
street: '123 Main St',
city: 'New York',
state: 'NY',
postalCode: '10001',
country: 'United States'
}
}
}
});

Profile Picture Management

Uploading Profile Pictures

The profile system supports image upload with validation:

// Upload image to API
const uploadImageToAPI = async (file: File): Promise<string> => {
const formData = new FormData();
formData.append('file', file);

const apiUrl = import.meta.env.VITE_WORKSPACE_API_URL || 'http://localhost:3602';
const response = await fetch(`${apiUrl}/upload/image`, {
method: 'POST',
body: formData,
});

if (!response.ok) {
throw new Error('Failed to upload image');
}

const result = await response.json();
return result.url;
};

// File validation
const handleImageUpload = (file: File) => {
// Validate file type
if (!file.type.startsWith('image/')) {
throw new Error('Please select an image file');
}

// Validate file size (max 5MB)
if (file.size > 5 * 1024 * 1024) {
throw new Error('Image must be less than 5MB');
}

// Process upload
return uploadImageToAPI(file);
};

Multi-Language Support

Supported Languages

The profile system supports 15+ languages with native names and regional flags:

const languages = [
{ code: 'en', name: 'English', nativeName: 'English', flag: '🇺🇸' },
{ code: 'es', name: 'Spanish', nativeName: 'Español', flag: '🇪🇸' },
{ code: 'fr', name: 'French', nativeName: 'Français', flag: '🇫🇷' },
{ code: 'de', name: 'German', nativeName: 'Deutsch', flag: '🇩🇪' },
{ code: 'it', name: 'Italian', nativeName: 'Italiano', flag: '🇮🇹' },
{ code: 'pt', name: 'Portuguese', nativeName: 'Português', flag: '🇵🇹' },
{ code: 'ru', name: 'Russian', nativeName: 'Русский', flag: '🇷🇺' },
{ code: 'zh', name: 'Chinese', nativeName: '中文', flag: '🇨🇳' },
{ code: 'ja', name: 'Japanese', nativeName: '日本語', flag: '🇯🇵' },
{ code: 'ko', name: 'Korean', nativeName: '한국어', flag: '🇰🇷' },
{ code: 'ar', name: 'Arabic', nativeName: 'العربية', flag: '🇸🇦', direction: 'rtl' },
{ code: 'he', name: 'Hebrew', nativeName: 'עברית', flag: '🇮🇱', direction: 'rtl' },
{ code: 'hi', name: 'Hindi', nativeName: 'हिन्दी', flag: '🇮🇳' },
{ code: 'th', name: 'Thai', nativeName: 'ไทย', flag: '🇹🇭' },
{ code: 'vi', name: 'Vietnamese', nativeName: 'Tiếng Việt', flag: '🇻🇳' }
];

Language Change Implementation

import { useTranslation } from 'react-i18next';

const { i18n } = useTranslation();

// Change language
const changeLanguage = async (languageCode: string) => {
await i18n.changeLanguage(languageCode);

// Update user profile
await updateProfile({
variables: {
input: {
id: userId,
language: languageCode
}
}
});
};

Address Management

Country Selection

The system includes a comprehensive list of 200+ countries for address management:

const countries = [
'United States', 'Canada', 'United Kingdom', 'Germany', 'France',
'Italy', 'Spain', 'Netherlands', 'Belgium', 'Switzerland',
// ... 200+ countries
].sort();

Address Validation

interface AddressInput {
street?: string;
city?: string;
state?: string;
postalCode?: string;
country?: string;
}

const validateAddress = (address: AddressInput): boolean => {
// Basic validation rules
if (address.postalCode && !/^[\w\s-]+$/.test(address.postalCode)) {
return false;
}

// Country-specific validation can be added
if (address.country === 'United States' && address.postalCode) {
return /^\d{5}(-\d{4})?$/.test(address.postalCode);
}

return true;
};

Timezone Configuration

Available Timezones

const timezones = [
{ value: 'UTC', label: 'UTC' },
{ value: 'America/New_York', label: 'Eastern Time (ET)' },
{ value: 'America/Chicago', label: 'Central Time (CT)' },
{ value: 'America/Denver', label: 'Mountain Time (MT)' },
{ value: 'America/Los_Angeles', label: 'Pacific Time (PT)' },
{ value: 'Europe/London', label: 'Greenwich Mean Time (GMT)' },
{ value: 'Europe/Paris', label: 'Central European Time (CET)' },
{ value: 'Asia/Tokyo', label: 'Japan Standard Time (JST)' },
{ value: 'Asia/Kolkata', label: 'India Standard Time (IST)' }
];

Workspace Associations

Displaying User Workspaces

Users can be members of multiple workspaces. Display them with appropriate badges:

interface WorkspaceMember {
workspace: {
id: string;
name: string;
type: string;
};
}

// Display workspaces
const renderWorkspaces = (members: WorkspaceMember[]) => {
return members.map(member => (
<Badge key={member.workspace.id} variant="secondary">
{member.workspace.name}
</Badge>
));
};

UI Components

Profile Form Components

The profile page uses these UI primitives:

import { Avatar, AvatarImage, AvatarFallback } from '@sdk/modules/ui-primitives/components/core/avatar';
import { Button } from '@sdk/modules/ui-primitives/components/core/button';
import { Card, CardContent, CardHeader, CardTitle } from '@sdk/modules/ui-primitives/components/core/card';
import { Input, Label, Select } from '@sdk/modules/ui-primitives/components/form';
import { Alert, Badge } from '@sdk/modules/ui-primitives/components/feedback';
import { useToast } from '@sdk/hooks/use-toast';

Edit Mode Toggle

Implement edit mode with state management:

const [isEditing, setIsEditing] = useState(false);
const [formData, setFormData] = useState(initialProfileData);

const handleEditToggle = () => {
if (isEditing) {
// Save changes
handleSaveProfile();
} else {
// Enter edit mode
setIsEditing(true);
}
};

const handleCancel = () => {
setIsEditing(false);
// Reset form to original data
setFormData(originalProfileData);
};

Error Handling

Profile Update Errors

Handle various error scenarios:

const [updateProfile] = useMutation(UPDATE_PROFILE, {
onCompleted: () => {
toast({
title: 'Profile Updated',
description: 'Your profile has been successfully updated.',
});
setIsEditing(false);
refetch();
},
onError: (error) => {
toast({
title: 'Update Failed',
description: error.message,
variant: 'destructive',
});
}
});

Loading States

Implement proper loading states:

if (loading) {
return (
<div className="animate-pulse space-y-4">
<div className="h-32 bg-muted rounded-lg"></div>
<div className="h-96 bg-muted rounded-lg"></div>
</div>
);
}

if (error) {
return (
<Alert variant="destructive">
<AlertTitle>Error</AlertTitle>
<AlertDescription>
Unable to load profile. Please try again later.
</AlertDescription>
</Alert>
);
}

Best Practices

Security Considerations

  1. Input Validation: Always validate user input before submission
  2. File Size Limits: Enforce maximum file sizes for profile pictures (5MB recommended)
  3. Secure Upload: Use authenticated endpoints for image uploads
  4. Sanitization: Sanitize all text inputs to prevent XSS attacks

Performance Optimization

  1. Image Optimization: Compress images before upload
  2. Lazy Loading: Load profile sections on demand
  3. Caching: Cache profile data to reduce API calls
  4. Debouncing: Debounce form inputs during editing

Accessibility

  1. Form Labels: Use proper labels for all form inputs
  2. ARIA Attributes: Include appropriate ARIA attributes
  3. Keyboard Navigation: Ensure all actions are keyboard accessible
  4. Screen Reader Support: Test with screen readers

Complete Example

Here's a minimal implementation of a user profile component:

import React, { useState, useEffect } from 'react';
import { useQuery, useMutation, gql } from '@apollo/client';
import { Card, CardContent, CardHeader, CardTitle } from '@sdk/components';
import { Button, Input, Label } from '@sdk/components';
import { useToast } from '@sdk/hooks';

const UserProfile: React.FC = () => {
const { toast } = useToast();
const [isEditing, setIsEditing] = useState(false);

const { data, loading, error, refetch } = useQuery(GET_PROFILE);
const [updateProfile] = useMutation(UPDATE_PROFILE);

const [formData, setFormData] = useState({
name: '',
email: '',
phoneNumber: '',
});

useEffect(() => {
if (data?.getProfile) {
setFormData({
name: data.getProfile.name || '',
email: data.getProfile.email || '',
phoneNumber: data.getProfile.phoneNumber || '',
});
}
}, [data]);

const handleSave = async () => {
try {
await updateProfile({
variables: {
input: {
id: data.getProfile.id,
...formData
}
}
});

toast({
title: 'Success',
description: 'Profile updated successfully',
});

setIsEditing(false);
refetch();
} catch (error) {
toast({
title: 'Error',
description: 'Failed to update profile',
variant: 'destructive',
});
}
};

if (loading) return <div>Loading...</div>;
if (error) return <div>Error loading profile</div>;

return (
<Card>
<CardHeader>
<CardTitle>User Profile</CardTitle>
<Button onClick={() => setIsEditing(!isEditing)}>
{isEditing ? 'Cancel' : 'Edit'}
</Button>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div>
<Label>Name</Label>
<Input
value={formData.name}
onChange={(e) => setFormData({...formData, name: e.target.value})}
disabled={!isEditing}
/>
</div>
<div>
<Label>Email</Label>
<Input
value={formData.email}
onChange={(e) => setFormData({...formData, email: e.target.value})}
disabled={!isEditing}
/>
</div>
<div>
<Label>Phone</Label>
<Input
value={formData.phoneNumber}
onChange={(e) => setFormData({...formData, phoneNumber: e.target.value})}
disabled={!isEditing}
/>
</div>
{isEditing && (
<Button onClick={handleSave}>Save Changes</Button>
)}
</div>
</CardContent>
</Card>
);
};

export default UserProfile;