Skip to content

Commit 4d3ec51

Browse files
authored
Merge pull request #20 from code4rena-dev/samus/avatar-component-12
Add avatar component
2 parents 1dd78d9 + 43a666b commit 4d3ec51

File tree

11 files changed

+233
-59
lines changed

11 files changed

+233
-59
lines changed

package-lock.json

Lines changed: 1 addition & 52 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@code4rena/components-library",
3-
"version": "1.2.0",
3+
"version": "2.0.0",
44
"description": "Code4rena's official components library ",
55
"types": "./dist/lib.d.ts",
66
"exports": {
@@ -71,7 +71,6 @@
7171
"container-query-polyfill": "^1.0.2",
7272
"date-fns": "^2.30.0",
7373
"luxon": "^3.3.0",
74-
"react-avatar": "^5.0.3",
7574
"react-select": "^5.7.4"
7675
},
7776
"peerDependencies": {
@@ -85,4 +84,4 @@
8584
"@babel/preset-react"
8685
]
8786
}
88-
}
87+
}

src/lib/Avatar/Avatar.scss

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
.avatar {
2+
position: relative;
3+
display: inline-block;
4+
overflow: hidden;
5+
vertical-align: middle;
6+
7+
.avatar__image {
8+
width: 100%;
9+
height: 100%;
10+
object-fit: cover;
11+
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1);
12+
}
13+
14+
.avatar__initials {
15+
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1);
16+
17+
.avatar__initials-text {
18+
display: flex;
19+
align-items: center;
20+
justify-content: center;
21+
width: 100%;
22+
height: 100%;
23+
color: #fff;
24+
font-weight: 600;
25+
}
26+
}
27+
}
28+
29+
.widget__avatar-container {
30+
display: flex;
31+
flex-direction: row;
32+
align-items: center;
33+
gap: 10px;
34+
flex-wrap: wrap;
35+
}
36+

src/lib/Avatar/Avatar.stories.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import React from "react";
2+
import { Meta, StoryObj } from "@storybook/react";
3+
import { Avatar } from "./Avatar";
4+
5+
const meta: Meta<typeof Avatar> = {
6+
component: Avatar,
7+
title: "Avatar",
8+
tags: ["autodocs"],
9+
argTypes: {
10+
size: { control: "number" },
11+
round: { control: "number" },
12+
},
13+
};
14+
export default meta;
15+
16+
type Story = StoryObj<typeof Avatar>;
17+
18+
export const ImageAvatar: Story = (args) => <Avatar {...args} />;
19+
ImageAvatar.args = {
20+
imgElement: <img src="/images/default-avatar.png" alt="Placeholder" />,
21+
name: "0xJohnWithALongName",
22+
size: 50,
23+
round: 25,
24+
};
25+
26+
export const InitialsAvatar: Story = (args) => <Avatar {...args} />;
27+
InitialsAvatar.args = {
28+
name: "John-With-A-Long-Name",
29+
size: 50,
30+
round: 25,
31+
};

src/lib/Avatar/Avatar.test.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import React from "react";
2+
import { Avatar } from "./Avatar";
3+
import { render, screen } from "@testing-library/react";
4+
import "@testing-library/jest-dom";
5+
6+
const defaultArgs = {
7+
name: "John Doe",
8+
size: 50,
9+
round: 25,
10+
};
11+
12+
describe("========== Avatar Component - RUNNING TESTS ==========", () => {
13+
test("Renders with image avatar", () => {
14+
const imgElement = (
15+
<img src="/images/default-avatar.png" alt="Placeholder" />
16+
);
17+
render(<Avatar imgElement={imgElement} {...defaultArgs} />);
18+
const avatar = screen.getByRole("img");
19+
expect(avatar).toHaveAttribute("src", "/images/default-avatar.png");
20+
});
21+
22+
test("Renders with initials avatar", () => {
23+
render(<Avatar {...defaultArgs} />);
24+
const avatar = screen.getByText("JD");
25+
expect(avatar).toBeInTheDocument();
26+
});
27+
28+
test("Sets alt text for image avatar", () => {
29+
const imgElement = (
30+
<img src="/images/default-avatar.png" alt="User avatar" />
31+
);
32+
render(<Avatar imgElement={imgElement} {...defaultArgs} />);
33+
const avatar = screen.getByAltText("User avatar");
34+
expect(avatar).toBeInTheDocument();
35+
});
36+
});

src/lib/Avatar/Avatar.tsx

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import React, { cloneElement } from "react";
2+
import { AvatarProps } from "./Avatar.types";
3+
import "./Avatar.scss";
4+
5+
// Parse the initials of the user from their name
6+
const parseInitials = (name: string) => {
7+
if (!name) return "✷"; // If no name then return "✷"
8+
if (name.length <= 2) return name; // If name is 2 characters or less then return it whole
9+
if (name.substring(0, 2).toLowerCase() === "0x") name = name.substring(2); // If starts with "0x" then remove it
10+
let nameArray = name.split(" "); // Initials by 'space' separator
11+
if (nameArray.length <= 1) nameArray = name.split("."); // Initials by 'dot' separator
12+
if (nameArray.length <= 1) nameArray = name.split("_"); // Initials by 'underscore' separator
13+
if (nameArray.length <= 1) nameArray = name.split("-"); // Initials by 'dash' separator
14+
if (nameArray.length <= 1) {
15+
const nameString = name.replace(/([A-Z])/g, " $1").trim(); // Initials by case-change
16+
nameArray = nameString.split(" ");
17+
}
18+
if (nameArray.length <= 1) return name.substring(0, 1); // Fallback to first letter of name
19+
return `${nameArray[0].substring(0, 1)}${nameArray[1].substring(0, 1)}`;
20+
};
21+
22+
// Array of dark pastel colors suitable as bg for the white initials
23+
const pastelColors = [
24+
"#9B4DCA",
25+
"#8E44AD",
26+
"#2980B9",
27+
"#3498DB",
28+
"#1ABC9C",
29+
"#16A085",
30+
"#27AE60",
31+
"#2ECC71",
32+
"#F1C40F",
33+
"#F39C12",
34+
"#E67E22",
35+
"#D35400",
36+
"#E74C3C",
37+
"#C0392B",
38+
"#EC407A",
39+
"#D81B60",
40+
"#8E24AA",
41+
"#6A1B9A",
42+
"#4A148C",
43+
"#4527A0",
44+
];
45+
46+
// Generate a random color from the name string
47+
const generateColor = (str: string) => {
48+
const index = str.length % pastelColors.length;
49+
return pastelColors[index];
50+
};
51+
52+
/**
53+
* A stylized Avatar component for displaying user avatars.
54+
* This component supports displaying an image avatar or a fallback avatar with initials.
55+
* The fallback avatar is a colored circle with the user's initials.
56+
*
57+
* @param imgElement - An optional image element to use as the avatar.
58+
* @param name - The name of the user. Used to generate initials for the fallback avatar.
59+
* @param size - The size of the avatar in pixels.
60+
* @param round - The border-radius of the avatar in pixels. Use this to make the avatar round.
61+
*/
62+
export const Avatar: React.FC<AvatarProps> = ({
63+
imgElement,
64+
name,
65+
size,
66+
round,
67+
}) => {
68+
const clonedImgElement = imgElement
69+
? cloneElement(imgElement, {
70+
className: "avatar__image",
71+
width: size,
72+
height: size,
73+
})
74+
: null;
75+
76+
return (
77+
<div
78+
className={"avatar"}
79+
style={{
80+
borderRadius: round ? `${round}px` : "none",
81+
width: `${size}px`,
82+
height: `${size}px`,
83+
}}
84+
>
85+
{clonedImgElement ? (
86+
clonedImgElement
87+
) : (
88+
<div
89+
className={"avatar__initials"}
90+
style={{
91+
width: `${size}px`,
92+
height: `${size}px`,
93+
backgroundColor: generateColor(name),
94+
}}
95+
>
96+
<span
97+
className={"avatar__initials-text"}
98+
style={{
99+
lineHeight: `${size}px`,
100+
fontSize: `${size * 0.45}px`,
101+
}}
102+
>
103+
{parseInitials(name)}
104+
</span>
105+
</div>
106+
)}
107+
</div>
108+
);
109+
};

src/lib/Avatar/Avatar.types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export interface AvatarProps {
2+
imgElement?: JSX.Element;
3+
name: string;
4+
size: number;
5+
round?: number;
6+
}

src/lib/Avatar/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./Avatar";

src/lib/NavBar/NavBar.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import walletConnectIcon from "../../../public/logos/wallet-connect-logo.svg";
66
import c4LogoIcon from "../../../public/logos/c4-logo.svg";
77
import registerIcon from "../../../public/icons/register.svg";
88
import { Dropdown } from "../Dropdown/Dropdown";
9-
import Avatar from "react-avatar";
109
import { ModalProps, NavBarProps, Web3ProviderType } from "./NavBar.types";
1110
import "./NavBar.scss";
11+
import { Avatar } from "../Avatar";
1212

1313
const UserDropdown = ({
1414
userImage = "/",
@@ -20,7 +20,12 @@ const UserDropdown = ({
2020
logoutHandler: () => void;
2121
}) => {
2222
const avatar = () => (
23-
<Avatar src={userImage} name={username} size="30px" round={true} />
23+
<Avatar
24+
imgElement={userImage ? <img src={userImage} alt={username} /> : undefined}
25+
name={username}
26+
size={30}
27+
round={30}
28+
/>
2429
);
2530

2631
const children = (

src/lib/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
export * from "./Alert";
2+
export * from "./Avatar";
23
export * from "./Button";
4+
export * from "./Card";
35
export * from "./ContestStatus";
46
export * from "./ContestTile";
57
export * from "./Dropdown";
68
export * from "./EyebrowBar";
79
export * from "./Input";
810
export * from "./NavBar";
9-
export * from "./Tag";
10-
export * from "./Card";
11+
export * from "./Tag";

0 commit comments

Comments
 (0)