Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/components/sidebar/SidebarAccountDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export default function SidebarAccountDropdown({isActive, onClick}: AccountDropd
<div
className={clsx("flex items-center gap-3 rounded-lg p-2 hover:bg-gray-50 transition-colors", isActive ? "bg-indigo-100 bg-opacity-75 rounded-lg p-2" : '')}>
<div className="h-8 w-8 rounded-full flex items-center justify-center">
<UserAvatar avatar={user?.avatar} avatarSize={32}/>
<UserAvatar user={user} size={32}/>
</div>
<div className={clsx("flex-1 text-left", isActive ? "text-gray-700" : '')}>
<p className="text-sm font-medium text-gray-900">{user?.firstName} {user?.lastName}</p>
Expand Down
6 changes: 3 additions & 3 deletions src/modules/calendar/components/CalendarLeaveItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ export default function CalendarLeaveItem({leave}: CalendarLeaveItemProps) {
>
<div className="flex items-center gap-2">
<UserAvatar
avatar={leave.user?.avatar}
avatarSize={28}
className="ring-1 ring-white bg-white"
user={leave.user}
size={28}

/>
<span className="text-xs font-medium">
{leave.user.firstName}
Expand Down
4 changes: 2 additions & 2 deletions src/modules/leave/components/LeaveConflicts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ export default function LeaveConflicts({conflicts}: LeaveConflictProps) {
>
<div className="flex items-center gap-2">
<UserAvatar
avatar={leave.user?.avatar}
avatarSize={40}
user={leave.user}
size={40}
/>
<span className="text-sm font-medium">{leave.user.firstName} {leave.user.lastName}</span>
</div>
Expand Down
4 changes: 2 additions & 2 deletions src/modules/leave/components/LeaveStatusUpdateDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ export default function LeaveStatusUpdateDialog({
>
<div className="flex flex-col items-center gap-3">
<UserAvatar
avatar={user.avatar}
avatarSize={64}
user={user}
size={64}
className="border-4 border-primary/10 group-hover:border-primary/20 transition-colors"
/>
<div className="text-center">
Expand Down
2 changes: 1 addition & 1 deletion src/modules/leave/pages/LeavesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ function LeaveRow({leave, handleRowClick}: LeaveRowProps) {
return (
<TableRow>
<TableCell className="flex items-center gap-2 font-medium">
<UserAvatar avatar={leave.user?.avatar} avatarSize={32}/>
<UserAvatar user={leave.user} size={32}/>
<span
className="cursor-pointer text-sm font-semibold text-blue-600 hover:text-blue-800"
onClick={() => viewEmployeeProfile(leave.user.id)}
Expand Down
42 changes: 22 additions & 20 deletions src/modules/user/components/UserAvatar.tsx
Original file line number Diff line number Diff line change
@@ -1,51 +1,53 @@
import React, {useContext} from "react";
import {AssetResponse} from "@/core/types/user.ts";
import {UserResponse} from "@/core/types/user.ts";
import {UserContext} from "@/contexts/UserContext.tsx";
import {User} from "lucide-react";
import UserDefaultAvatar from "@/modules/user/components/UserDefaultAvatar.tsx";

type AvatarProps = {
avatar: AssetResponse | null;
avatarSize: number;
size: number;
className?: string;
user: UserResponse;
};

export default function UserAvatar({avatar, avatarSize, className}: AvatarProps) {
const { accessToken } = useContext(UserContext);
const avatarSrc = avatar ? `${avatar.url}?token=${accessToken}` : null;
export default function UserAvatar({size, className, user}: AvatarProps) {
const {accessToken} = useContext(UserContext);

const commonStyles = {
height: `${avatarSize}px`,
width: `${avatarSize}px`,
height: `${size}px`,
width: `${size}px`,
objectFit: 'cover' as const,
transition: 'all 0.2s ease-in-out'
};
if(!user) {
return (
<div
className={`relative inline-flex items-center justify-center rounded-full ${className ?? ''}`}
style={commonStyles}
>
<UserDefaultAvatar name={"O"} size={size}/>
</div>
);
}

if (!avatarSrc) {
if (!user.avatar) {
return (
<div
className={`relative inline-flex items-center justify-center border-2 border-gray-300 rounded-full ${className ?? ''}`}
className={`relative inline-flex items-center justify-center rounded-full ${className ?? ''}`}
style={commonStyles}
>
<User
className="text-gray-300"
size={Math.max(avatarSize)}
/>
<UserDefaultAvatar name={user.firstName + " " + user.lastName} size={size}/>
</div>
);
}

return (
<div className="relative inline-block">
<img
src={avatarSrc}
src={`${user.avatar.url}?token=${accessToken}`}
alt="User Avatar"
className={`rounded-full hover:opacity-90 ${className ?? ''}`}
style={commonStyles}
loading="lazy"
onError={(e) => {
e.currentTarget.onerror = null;
e.currentTarget.src = 'fallback-avatar-url.jpg'; // Add your fallback image URL
}}
/>
</div>
);
Expand Down
143 changes: 143 additions & 0 deletions src/modules/user/components/UserDefaultAvatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import React, { useMemo } from 'react';

interface ColorPalette {
backgrounds: string[];
texts: string[];
}

interface UserDefaultAvatarProps {
/** Full name to generate initials from */
name: string;
/** Size of the avatar in pixels (both width and height) */
size?: number;
/** Optional CSS class name for additional styling */
className?: string;
/** Font family for the initials */
fontFamily?: string;
/** Custom color palette for backgrounds and text */
colorPalette?: ColorPalette;
}

/**
* UserDefaultAvatar - A React component that generates avatar circles with initials
* similar to the example in the image.
*/
export default function UserDefaultAvatar({
name,
size = 48,
className = '',
fontFamily = 'ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"',
colorPalette = {
backgrounds: [
'#E8F2FF', // Light Blue
'#FFF1E8', // Light Orange/Peach
'#E8FFE9', // Light Green
'#F5E8FF', // Light Purple
'#FFE8E8', // Light Pink/Red
'#F2F4F7', // Light Grey
'#E8FBFF', // Light Cyan
'#FFF8E8', // Light Yellow
'#FFE8F7', // Light Pink
'#E8FFFD', // Light Mint
'#F7E8FF', // Light Lavender
'#FFE8EC', // Light Coral
'#E8FFF1', // Light Seafoam
'#FFF3E8', // Light Apricot
'#E8EEFF', // Light Periwinkle
'#FFFFE8' // Light Cream
],
texts: [
'#2E5AAC', // Dark Blue
'#AC6B2E', // Dark Orange
'#2EAC4A', // Dark Green
'#8A2EAC', // Dark Purple
'#AC2E2E', // Dark Red
'#4A5468', // Dark Grey
'#2E9DAC', // Dark Cyan
'#AC962E', // Dark Yellow
'#AC2E8A', // Dark Pink
'#2EACA3', // Dark Mint
'#892EAC', // Dark Lavender
'#AC2E4A', // Dark Coral
'#2EAC77', // Dark Seafoam
'#AC5F2E', // Dark Apricot
'#2E41AC', // Dark Periwinkle
'#ACAC2E' // Dark Olive
]
}
}: UserDefaultAvatarProps) {
// Generate initials from the name
const initials = useMemo((): string => {
if (!name || typeof name !== 'string') return '??';

const parts = name.trim().split(/\s+/);

if (parts.length <= 1) {
// If there's only one name, use the first two characters if possible
return name.length > 1
? name.substring(0, 2).toUpperCase()
: name.substring(0, 1).toUpperCase();
}

// Get first char of first name and first char of last name
const firstInitial = parts[0].charAt(0);
const lastInitial = parts[parts.length - 1].charAt(0);

return (firstInitial + lastInitial).toUpperCase();
}, [name]);

// Get color index based on name
const colorIndex = useMemo((): number => {
if (!name) return 0;

// Simple hash function to ensure consistent colors for the same name
let hash = 0;
for (let i = 0; i < name.length; i++) {
hash = ((hash << 5) - hash) + name.charCodeAt(i);
hash |= 0; // Convert to 32bit integer
}

return Math.abs(hash) % colorPalette.backgrounds.length;
}, [name, colorPalette.backgrounds.length]);

// Get background and text colors
const backgroundColor = colorPalette.backgrounds[colorIndex];
const textColor = colorPalette.texts[colorIndex];

// Calculate dimensions
const strokeWidth = 1;
const radius = (size / 2) - (strokeWidth / 2);
const fontSize = size * 0.35; // 35% of circle size

return (
<svg
width={size}
height={size}
viewBox={`0 0 ${size} ${size}`}
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<circle
cx={size / 2}
cy={size / 2}
r={radius}
fill={backgroundColor}
stroke={textColor}
strokeWidth={strokeWidth}
strokeOpacity="0.2"
/>
<text
x={size / 2}
y={(size / 2 + fontSize / 3) - 4}
fontSize={fontSize}
fontFamily={fontFamily}
fontWeight="bold"
fill={textColor}
textAnchor="middle"
dominantBaseline="middle"
>
{initials}
</text>
</svg>
);
};
2 changes: 1 addition & 1 deletion src/modules/user/components/UserDetailsCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export default function UserDetailsCard({employeeDetails, leavePolicy}: Employee
{employeeDetails ? (
<Card className="p-4 space-y-6">
<div className="flex items-center gap-6">
<UserAvatar avatar={employeeDetails.avatar} avatarSize={64}/>
<UserAvatar user={employeeDetails} size={64}/>
<div className="space-y-2">
<div className="flex items-center gap-3">
<h2 className="text-xl font-semibold">
Expand Down
6 changes: 3 additions & 3 deletions src/modules/user/components/UserList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ export function UserList({
className="flex items-center space-x-2 cursor-pointer hover:text-primary transition-colors"
>
<UserAvatar
avatar={employee?.avatar}
avatarSize={40}
className="border-2 border-muted"
user={employee}
size={46}
className=""
/>
<div className="flex flex-col">
<span
Expand Down
2 changes: 1 addition & 1 deletion src/modules/user/pages/ProfilePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export default function ProfilePage() {
{/* Avatar Section */}
<div className="flex flex-col items-center">
<div className="relative">
<UserAvatar avatar={user?.avatar} avatarSize={160}/>
<UserAvatar user={user} size={160}/>
<div
className="absolute bottom-0 right-0 bg-indigo-600 hover:bg-indigo-700 transition-colors p-3 rounded-full shadow-lg">
<label className="cursor-pointer" htmlFor="upload-photo">
Expand Down