Skip to content

Commit

Permalink
style: new avatar and avatar group components (#2584)
Browse files Browse the repository at this point in the history
* style: new avatar components

* chore: bug fixes

* chore: add pixel to size

* chore: add comments to helper functions

* fix: build errors
  • Loading branch information
aaryan610 authored Nov 1, 2023
1 parent 1a24f9e commit 490e032
Showing 52 changed files with 555 additions and 1,825 deletions.
6 changes: 3 additions & 3 deletions packages/ui/package.json
Original file line number Diff line number Diff line change
@@ -10,13 +10,13 @@
"dist/**"
],
"scripts": {
"build": "tsup src/index.tsx --format esm,cjs --dts --external react",
"dev": "tsup src/index.tsx --format esm,cjs --watch --dts --external react",
"build": "tsup src/index.ts --format esm,cjs --dts --external react",
"dev": "tsup src/index.ts --format esm,cjs --watch --dts --external react",
"lint": "eslint src/",
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist"
},
"devDependencies": {
"@types/react-color" : "^3.0.9",
"@types/react-color": "^3.0.9",
"@types/node": "^20.5.2",
"@types/react": "18.2.0",
"@types/react-dom": "18.2.0",
85 changes: 85 additions & 0 deletions packages/ui/src/avatar/avatar-group.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import React from "react";
// ui
import { Tooltip } from "../tooltip";
// types
import { TAvatarSize, getSizeInfo, isAValidNumber } from "./avatar";

type Props = {
/**
* The children of the avatar group.
* These should ideally should be `Avatar` components
*/
children: React.ReactNode;
/**
* The maximum number of avatars to display.
* If the number of children exceeds this value, the additional avatars will be replaced by a count of the remaining avatars.
* @default 2
*/
max?: number;
/**
* Whether to show the tooltip or not
* @default true
*/
showTooltip?: boolean;
/**
* The size of the avatars
* Possible values: "sm", "md", "base", "lg"
* @default "md"
*/
size?: TAvatarSize;
};

export const AvatarGroup: React.FC<Props> = (props) => {
const { children, max = 2, showTooltip = true, size = "md" } = props;

// calculate total length of avatars inside the group
const totalAvatars = React.Children.toArray(children).length;

// slice the children to the maximum number of avatars
const avatars = React.Children.toArray(children).slice(0, max);

// assign the necessary props from the AvatarGroup component to the Avatar components
const avatarsWithUpdatedProps = avatars.map((avatar) => {
const updatedProps: Partial<Props> = {
showTooltip,
size,
};

return React.cloneElement(avatar as React.ReactElement, updatedProps);
});

// get size details based on the size prop
const sizeInfo = getSizeInfo(size);

return (
<div className={`flex ${sizeInfo.spacing}`}>
{avatarsWithUpdatedProps.map((avatar, index) => (
<div key={index} className="ring-1 ring-custom-border-200 rounded-full">
{avatar}
</div>
))}
{max < totalAvatars && (
<Tooltip
tooltipContent={`${totalAvatars} total`}
disabled={!showTooltip}
>
<div
className={`${
!isAValidNumber(size) ? sizeInfo.avatarSize : ""
} ring-1 ring-custom-border-200 bg-custom-primary-500 text-white rounded-full grid place-items-center text-[9px]`}
style={
isAValidNumber(size)
? {
width: `${size}px`,
height: `${size}px`,
}
: {}
}
>
+{totalAvatars - max}
</div>
</Tooltip>
)}
</div>
);
};
168 changes: 168 additions & 0 deletions packages/ui/src/avatar/avatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import React from "react";
// ui
import { Tooltip } from "../tooltip";

export type TAvatarSize = "sm" | "md" | "base" | "lg" | number;

type Props = {
/**
* The name of the avatar which will be displayed on the tooltip
*/
name?: string;
/**
* The background color if the avatar image fails to load
*/
fallbackBackgroundColor?: string;
/**
* The text to display if the avatar image fails to load
*/
fallbackText?: string;
/**
* The text color if the avatar image fails to load
*/
fallbackTextColor?: string;
/**
* Whether to show the tooltip or not
* @default true
*/
showTooltip?: boolean;
/**
* The size of the avatars
* Possible values: "sm", "md", "base", "lg"
* @default "md"
*/
size?: TAvatarSize;
/**
* The shape of the avatar
* Possible values: "circle", "square"
* @default "circle"
*/
shape?: "circle" | "square";
/**
* The source of the avatar image
*/
src?: string;
};

/**
* Get the size details based on the size prop
* @param size The size of the avatar
* @returns The size details
*/
export const getSizeInfo = (size: TAvatarSize) => {
switch (size) {
case "sm":
return {
avatarSize: "h-4 w-4",
fontSize: "text-xs",
spacing: "-space-x-1",
};
case "md":
return {
avatarSize: "h-5 w-5",
fontSize: "text-xs",
spacing: "-space-x-1",
};
case "base":
return {
avatarSize: "h-6 w-6",
fontSize: "text-sm",
spacing: "-space-x-1.5",
};
case "lg":
return {
avatarSize: "h-7 w-7",
fontSize: "text-sm",
spacing: "-space-x-1.5",
};
default:
return {
avatarSize: "h-5 w-5",
fontSize: "text-xs",
spacing: "-space-x-1",
};
}
};

/**
* Get the border radius based on the shape prop
* @param shape The shape of the avatar
* @returns The border radius
*/
export const getBorderRadius = (shape: "circle" | "square") => {
switch (shape) {
case "circle":
return "rounded-full";
case "square":
return "rounded-md";
default:
return "rounded-full";
}
};

/**
* Check if the value is a valid number
* @param value The value to check
* @returns Whether the value is a valid number or not
*/
export const isAValidNumber = (value: any) => {
return typeof value === "number" && !isNaN(value);
};

export const Avatar: React.FC<Props> = (props) => {
const {
name,
fallbackBackgroundColor,
fallbackText,
fallbackTextColor,
showTooltip = true,
size = "md",
shape = "circle",
src,
} = props;

// get size details based on the size prop
const sizeInfo = getSizeInfo(size);

return (
<Tooltip
tooltipContent={fallbackText ?? name ?? "?"}
disabled={!showTooltip}
>
<div
className={`${
!isAValidNumber(size) ? sizeInfo.avatarSize : ""
} overflow-hidden grid place-items-center ${getBorderRadius(shape)}`}
style={
isAValidNumber(size)
? {
height: `${size}px`,
width: `${size}px`,
}
: {}
}
>
{src ? (
<img
src={src}
className={`h-full w-full ${getBorderRadius(shape)}`}
alt={name}
/>
) : (
<div
className={`${
sizeInfo.fontSize
} grid place-items-center h-full w-full ${getBorderRadius(shape)}`}
style={{
backgroundColor:
fallbackBackgroundColor ?? "rgba(var(--color-primary-500))",
color: fallbackTextColor ?? "#ffffff",
}}
>
{name ? name[0].toUpperCase() : fallbackText ?? "?"}
</div>
)}
</div>
</Tooltip>
);
};
2 changes: 2 additions & 0 deletions packages/ui/src/avatar/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./avatar-group";
export * from "./avatar";
File renamed without changes.
9 changes: 5 additions & 4 deletions packages/ui/src/index.tsx → packages/ui/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
export * from "./avatar";
export * from "./breadcrumbs";
export * from "./button";
export * from "./dropdowns";
export * from "./form-fields";
export * from "./icons";
export * from "./progress";
export * from "./spinners";
export * from "./loader";
export * from "./tooltip";
export * from "./icons";
export * from "./breadcrumbs";
export * from "./dropdowns";
export * from "./loader";
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@ import useProjectMembers from "hooks/use-project-members";
// constants
import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
// ui
import { Avatar } from "components/ui";
import { Avatar } from "@plane/ui";
// icons
import { Check } from "lucide-react";
// types
@@ -31,13 +31,13 @@ export const ChangeIssueAssignee: FC<Props> = ({ setIsPaletteOpen, issue, user }
const { members } = useProjectMembers(workspaceSlug as string, projectId as string);

const options =
members?.map(({ member }: any) => ({
members?.map(({ member }) => ({
value: member.id,
query: member.display_name,
content: (
<>
<div className="flex items-center gap-2">
<Avatar user={member} />
<Avatar name={member.display_name} src={member.avatar} showTooltip={false} />
{member.display_name}
</div>
{issue.assignees.includes(member.id) && (
12 changes: 4 additions & 8 deletions web/components/core/filters/filters-list.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
import React from "react";

// icons
import { PriorityIcon, StateGroupIcon } from "@plane/ui";
import { X } from "lucide-react";
// ui
import { Avatar } from "components/ui";
import { Avatar, PriorityIcon, StateGroupIcon } from "@plane/ui";
// helpers
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
// helpers
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
// types
import { IIssueFilterOptions, IIssueLabels, IState, IUserLite, TStateGroups } from "types";
// constants
import { STATE_GROUP_COLORS } from "constants/state";
import { X } from "lucide-react";

type Props = {
filters: Partial<IIssueFilterOptions>;
@@ -149,7 +145,7 @@ export const FiltersList: React.FC<Props> = ({ filters, setFilters, clearAllFilt
key={memberId}
className="inline-flex items-center gap-x-1 rounded-full bg-custom-background-90 px-1"
>
<Avatar user={member} />
<Avatar name={member?.display_name} src={member?.avatar} showTooltip={false} />
<span>{member?.display_name}</span>
<span
className="cursor-pointer"
@@ -173,7 +169,7 @@ export const FiltersList: React.FC<Props> = ({ filters, setFilters, clearAllFilt
key={`${memberId}-${key}`}
className="inline-flex items-center gap-x-1 rounded-full bg-custom-background-90 px-1 capitalize"
>
<Avatar user={member} />
<Avatar name={member?.display_name} src={member?.avatar} />
<span>{member?.display_name}</span>
<span
className="cursor-pointer"
1 change: 0 additions & 1 deletion web/components/core/filters/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export * from "./date-filter-modal";
export * from "./date-filter-select";
export * from "./filters-list";
export * from "./workspace-filters-list";
Loading

0 comments on commit 490e032

Please sign in to comment.