From 8b3afc17376937adb226df7e7256a1adda836baa Mon Sep 17 00:00:00 2001 From: lenhanphung <44486647+lenhanphung@users.noreply.github.com> Date: Tue, 13 Jan 2026 20:44:42 +0700 Subject: [PATCH] - Replace avatar image from API with gradient avatar from twake-mui (#457) - Display 2-letter initials (e.g., "JD" for "John Doe") instead of single letter - Add getInitials helper function in avatarUtils for reusability - Update Avatar in PeopleSearch, CalendarSearch, and Menubar components - Remove avatarUrl prop usage across all Avatar components --- __test__/components/Menubar.test.tsx | 8 +- .../features/Events/EventDisplay.test.tsx | 19 +++ __test__/utils/avatarUtils.test.ts | 113 ++++++++++++++++++ package-lock.json | 44 ++++++- src/components/Attendees/PeopleSearch.tsx | 3 +- src/components/Calendar/CalendarSearch.tsx | 4 +- src/components/Event/utils/eventUtils.tsx | 4 +- src/components/Menubar/Menubar.tsx | 26 +--- src/utils/avatarUtils.ts | 18 +++ 9 files changed, 206 insertions(+), 33 deletions(-) create mode 100644 __test__/utils/avatarUtils.test.ts diff --git a/__test__/components/Menubar.test.tsx b/__test__/components/Menubar.test.tsx index 99efe32..e9e37ff 100644 --- a/__test__/components/Menubar.test.tsx +++ b/__test__/components/Menubar.test.tsx @@ -100,7 +100,7 @@ describe("Calendar App Component Display Tests", () => { />, preloadedState ); - expect(screen.getByText("t")).toBeInTheDocument(); + expect(screen.getByText("T")).toBeInTheDocument(); }); // Edge cases for avatar display logic @@ -131,7 +131,7 @@ describe("Calendar App Component Display Tests", () => { />, preloadedState ); - expect(screen.getByText("t")).toBeInTheDocument(); + expect(screen.getByText("T")).toBeInTheDocument(); }); it("handles user with only name", () => { @@ -161,7 +161,7 @@ describe("Calendar App Component Display Tests", () => { />, preloadedState ); - expect(screen.getByText("t")).toBeInTheDocument(); + expect(screen.getByText("T")).toBeInTheDocument(); }); it("handles user with both name and family_name", () => { @@ -221,7 +221,7 @@ describe("Calendar App Component Display Tests", () => { />, preloadedState ); - expect(screen.getByText("t")).toBeInTheDocument(); + expect(screen.getByText("T")).toBeInTheDocument(); }); it("handles user with empty email", () => { diff --git a/__test__/features/Events/EventDisplay.test.tsx b/__test__/features/Events/EventDisplay.test.tsx index f794acc..738d7a2 100644 --- a/__test__/features/Events/EventDisplay.test.tsx +++ b/__test__/features/Events/EventDisplay.test.tsx @@ -2032,6 +2032,25 @@ describe("Helper functions", () => { expect(typeof result.color).toBe("string"); }); + it("stringAvatar returns 2-letter initials for full name", () => { + const result = stringAvatar("John Doe"); + expect(result.children).toBe("JD"); + expect(result.color).toBeDefined(); + expect(typeof result.color).toBe("string"); + }); + + it("stringAvatar handles single word names", () => { + const result = stringAvatar("Alice"); + expect(result.children).toBe("A"); + expect(result.color).toBeDefined(); + }); + + it("stringAvatar handles email addresses", () => { + const result = stringAvatar("test@example.com"); + expect(result.children).toBe("T"); + expect(result.color).toBeDefined(); + }); + it("InfoRow renders text and link if url is valid", () => { renderWithProviders( ({ + nameToColor: jest.fn((name: string) => { + if (!name) return undefined; + const colors = [ + "sunrise", + "downy", + "sugarCoral", + "pinkBonnet", + "blueMana", + "nightBlue", + "snowPea", + "pluviophile", + "cornflower", + "paleGreen", + "moonBlue", + ]; + const hash = Array.from(name.toUpperCase()) + .map((letter) => letter.charCodeAt(0)) + .reduce((sum, number) => sum + number, 0); + return colors[hash % colors.length]; + }), +})); + +describe("avatarUtils", () => { + describe("getInitials", () => { + it("returns 2-letter initials for full name", () => { + expect(getInitials("John Doe")).toBe("JD"); + expect(getInitials("Alice Smith")).toBe("AS"); + expect(getInitials("Nguyễn Văn")).toBe("NV"); + }); + + it("returns single letter for single word", () => { + expect(getInitials("Alice")).toBe("A"); + expect(getInitials("John")).toBe("J"); + }); + + it("returns uppercase initials", () => { + expect(getInitials("john doe")).toBe("JD"); + expect(getInitials("ALICE SMITH")).toBe("AS"); + expect(getInitials("a")).toBe("A"); + }); + + it("handles multiple spaces between words", () => { + expect(getInitials("John Doe")).toBe("JD"); + expect(getInitials("Alice Smith Brown")).toBe("AS"); + }); + + it("handles email addresses", () => { + expect(getInitials("john.doe@email.com")).toBe("J"); + expect(getInitials("test@example.com")).toBe("T"); + }); + + it("handles empty string", () => { + expect(getInitials("")).toBe(""); + }); + + it("handles whitespace-only string", () => { + expect(getInitials(" ")).toBe(""); + }); + + it("handles single character", () => { + expect(getInitials("A")).toBe("A"); + expect(getInitials("a")).toBe("A"); + }); + + it("handles names with special characters", () => { + expect(getInitials("Jean-Pierre")).toBe("J"); + expect(getInitials("Mary Jane Watson")).toBe("MJ"); + }); + + it("handles unicode characters", () => { + expect(getInitials("Nguyễn Văn")).toBe("NV"); + expect(getInitials("José María")).toBe("JM"); + }); + + it("takes first two words for names with more than two words", () => { + expect(getInitials("John Michael Smith")).toBe("JM"); + expect(getInitials("Alice Bob Charlie")).toBe("AB"); + }); + }); + + describe("stringToGradient", () => { + it("returns color for valid string", () => { + const result = stringToGradient("John Doe"); + expect(result).toBeDefined(); + expect(typeof result).toBe("string"); + }); + + it("returns same color for same input", () => { + const result1 = stringToGradient("John Doe"); + const result2 = stringToGradient("John Doe"); + expect(result1).toBe(result2); + }); + + it("returns undefined for empty string", () => { + expect(stringToGradient("")).toBeUndefined(); + }); + + it("returns color for single character", () => { + const result = stringToGradient("A"); + expect(result).toBeDefined(); + expect(typeof result).toBe("string"); + }); + + it("returns color for email address", () => { + const result = stringToGradient("test@example.com"); + expect(result).toBeDefined(); + expect(typeof result).toBe("string"); + }); + }); +}); diff --git a/package-lock.json b/package-lock.json index 7c6a50b..8b2aaeb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -138,6 +138,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -2070,6 +2071,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -2093,6 +2095,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -2195,6 +2198,7 @@ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -2238,6 +2242,7 @@ "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -2510,6 +2515,7 @@ "resolved": "https://registry.npmjs.org/@fullcalendar/core/-/core-6.1.20.tgz", "integrity": "sha512-1cukXLlePFiJ8YKXn/4tMKsy0etxYLCkXk8nUCFi11nRONF2Ba2CD5b21/ovtOO2tL6afTJfwmc1ed3HG7eB1g==", "license": "MIT", + "peer": true, "dependencies": { "preact": "~10.12.1" } @@ -3136,6 +3142,7 @@ "version": "1.1.3", "resolved": "https://npm.pkg.github.com/download/@linagora/twake-mui/1.1.3/c8ed9f2e04beaf55c1cfd1e014d2f43081c975ae", "integrity": "sha512-lLylOmnMOpXRlkFCsKq2Jaf1bfaobQsHz1fvxRIR0MIs8EAy2gyKc/5D22Q/Mqo3aN2zEum9qrd1D030lm8NJg==", + "license": "MIT", "dependencies": { "react": "^18.2.0", "react-dom": "^18.2.0" @@ -3251,6 +3258,7 @@ "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.6.tgz", "integrity": "sha512-R4DaYF3dgCQCUAkr4wW1w26GHXcf5rCmBRHVBuuvJvaGLmZdD8EjatP80Nz5JCw0KxORAzwftnHzXVnjR8HnFw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "@mui/core-downloads-tracker": "^7.3.6", @@ -3361,6 +3369,7 @@ "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.6.tgz", "integrity": "sha512-8fehAazkHNP1imMrdD2m2hbA9sl7Ur6jfuNweh5o4l9YPty4iaZzRXqYvBCWQNwFaSHmMEj2KPbyXGp7Bt73Rg==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "@mui/private-theming": "^7.3.6", @@ -3719,6 +3728,7 @@ "integrity": "sha512-LvoOF53PL6zXgdzEhgnnP51S4FseDFH1bHrobK4EK6zZX/tN8qgf5tdlmN7h4OkMv/Qs1oUfvj0QcLWSstnnvA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rspack/core": "1.6.8", "@rspack/lite-tapable": "~1.1.0", @@ -3946,6 +3956,7 @@ "integrity": "sha512-FolcIAH5FW4J2FET+qwjd1kNeFbCkd0VLuIHO0thyolEjaPSxw5qxG67DA7BZGm6PVcoiSgPLks1DL6eZ8c+fA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@module-federation/runtime-tools": "0.21.6", "@rspack/binding": "1.6.8", @@ -4205,6 +4216,7 @@ "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -4289,6 +4301,7 @@ "integrity": "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.8.0" } @@ -4299,6 +4312,7 @@ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -4594,6 +4608,7 @@ "integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -4615,6 +4630,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -4626,6 +4642,7 @@ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "dev": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -4682,6 +4699,7 @@ "integrity": "sha512-XtssGWJvypyM2ytBnSnKtHYOGT+4ZwTnBVl36TA4nRO2f4PRNGz5/1OszHzcZCvcBMh+qb7I06uoCmLTRdR9og==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.51.0", @@ -4711,6 +4729,7 @@ "integrity": "sha512-3xP4XzzDNQOIqBMWogftkwxhg5oMKApqY0BAflmLZiFYHqyhSOxv/cd/zPQLTcCXr4AkaKb25joocY0BD1WC6A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.51.0", "@typescript-eslint/types": "8.51.0", @@ -5217,6 +5236,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5976,6 +5996,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -6687,6 +6708,7 @@ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.21.0" }, @@ -6702,7 +6724,8 @@ "version": "1.11.19", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/debug": { "version": "4.4.3", @@ -7301,6 +7324,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -8655,6 +8679,7 @@ "resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz", "integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.7.6" } @@ -9629,6 +9654,7 @@ "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "30.2.0", "@jest/types": "30.2.0", @@ -10392,6 +10418,7 @@ "integrity": "sha512-/xdLSWxK5QkqG524ONSjvg3V/FkNyCv538OIBdQqPNaAta3AsXj/Bd2FbvR87yMbXO2hFSWiAe/Q6IkVPDw+mw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/core": "^7.19.6", "@svgr/babel-preset": "^6.5.1", @@ -10850,6 +10877,7 @@ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", @@ -11286,6 +11314,7 @@ "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", "license": "MIT", + "peer": true, "engines": { "node": "*" } @@ -11295,6 +11324,7 @@ "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz", "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==", "license": "MIT", + "peer": true, "dependencies": { "moment": "^2.29.4" }, @@ -11330,7 +11360,6 @@ } ], "license": "MIT", - "peer": true, "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -12279,6 +12308,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -12301,6 +12331,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -12320,6 +12351,7 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -12344,6 +12376,7 @@ "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -12444,7 +12477,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redux-first-history": { "version": "5.2.0", @@ -13671,6 +13705,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -13851,6 +13886,7 @@ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -14067,6 +14103,7 @@ "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -14875,6 +14912,7 @@ "integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src/components/Attendees/PeopleSearch.tsx b/src/components/Attendees/PeopleSearch.tsx index ebeedd3..f94df95 100644 --- a/src/components/Attendees/PeopleSearch.tsx +++ b/src/components/Attendees/PeopleSearch.tsx @@ -8,6 +8,7 @@ import { ListItemText, TextField, } from "@linagora/twake-mui"; +import { stringAvatar } from "../Event/utils/eventUtils"; import { type ReactNode, useCallback, @@ -287,7 +288,7 @@ export function PeopleSearch({ return ( - + - {user?.name && user?.family_name - ? `${user.name[0]}${user.family_name[0]}` - : (user?.email?.[0] ?? "")} + {getInitials(getUserDisplayName(user))} @@ -338,19 +330,11 @@ export function Menubar({ }} > - {user?.name && user?.family_name - ? `${user.name[0]}${user.family_name[0]}` - : (user?.email?.[0] ?? "")} + {getInitials(getUserDisplayName(user))} {getUserDisplayName(user)} diff --git a/src/utils/avatarUtils.ts b/src/utils/avatarUtils.ts index f5c1101..e554a93 100644 --- a/src/utils/avatarUtils.ts +++ b/src/utils/avatarUtils.ts @@ -9,3 +9,21 @@ export function stringToGradient(str: string): string | undefined { if (!str) return undefined; return nameToColor(str); } + +/** + * Get initials from a name string (max 2 characters) + * @param name - Name string like "John Doe", "Alice", or "john.doe@email.com" + * @returns Initials like "JD", "A", or "J" + */ +export function getInitials(name: string): string { + if (!name) return ""; + + const trimmed = name.trim(); + const words = trimmed.split(/\s+/).filter(Boolean); + + if (words.length >= 2) { + return `${words[0][0]}${words[1][0]}`.toUpperCase(); + } + + return trimmed[0]?.toUpperCase() ?? ""; +}