Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(app-crm): add custom avatar group #4889

Merged
merged 5 commits into from
Aug 31, 2023
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
68 changes: 34 additions & 34 deletions examples/app-crm/src/components/company/card-view.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { FC } from "react";
import {
Avatar,
Button,
Card,
Col,
Expand All @@ -16,6 +15,7 @@ import { Text, CustomAvatar } from "..";
import { currencyNumber } from "../../utilities";
import { Company } from "../../interfaces/graphql";
import { useDelete, useNavigation } from "@refinedev/core";
import { CustomAvatarGroup } from "../custom-avatar-group";

type Props = {
loading?: boolean;
Expand All @@ -36,6 +36,15 @@ export const CompaniesCardView: FC<Props> = ({ companies, pagination }) => {
<>
<Row wrap gutter={[32, 32]}>
{companies.map((company) => {
const relatedContactAvatars = company.contacts?.nodes?.map(
(contact) => {
return {
name: contact.name,
src: contact.avatarUrl as string | undefined,
};
},
);

return (
<Col
key={company.id}
Expand All @@ -56,45 +65,36 @@ export const CompaniesCardView: FC<Props> = ({ companies, pagination }) => {
height: "60px",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
alignItems: "flex-start",
padding: "0 16px",
}}
>
<Space
direction="vertical"
align="start"
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
gap: "6px",
}}
>
<Text size="xs">
Related contacts
</Text>
<Avatar.Group
maxCount={3}
size="small"
>
{company.contacts?.nodes?.map(
(contact) => {
return (
<Tooltip
title={
contact.name
}
key={contact.id}
>
<CustomAvatar
name={
contact.name
}
src={
contact.avatarUrl
}
/>
</Tooltip>
);
},
)}
</Avatar.Group>
</Space>
<Space direction="vertical" align="end">
<CustomAvatarGroup
size={"small"}
overlap
gap="4px"
avatars={relatedContactAvatars}
/>
</div>
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "flex-end",
gap: "6px",
}}
>
<Text size="xs">Sales owner</Text>
<Tooltip
title={company.salesOwner?.name}
Expand All @@ -110,7 +110,7 @@ export const CompaniesCardView: FC<Props> = ({ companies, pagination }) => {
}
/>
</Tooltip>
</Space>
</div>
</div>,
]}
>
Expand Down
32 changes: 10 additions & 22 deletions examples/app-crm/src/components/company/table-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,24 @@ import {
DeleteButton,
EditButton,
FilterDropdown,
getDefaultSortOrder,
useSelect,
} from "@refinedev/antd";
import { CrudFilters, CrudSorting, getDefaultFilter } from "@refinedev/core";
import { Avatar, Input, Select, Space, Table, TableProps, Tooltip } from "antd";
import { Input, Select, Space, Table, TableProps } from "antd";

import { Text, CustomAvatar } from "..";
import { currencyNumber } from "../../utilities";
import { Company } from "../../interfaces/graphql";
import { EyeOutlined, SearchOutlined } from "@ant-design/icons";
import { CustomAvatarGroup } from "../custom-avatar-group";

type Props = {
tableProps: TableProps<Company>;
filters: CrudFilters;
sorters: CrudSorting;
};

export const CompaniesTableView: FC<Props> = ({
tableProps,
filters,
sorters,
}) => {
export const CompaniesTableView: FC<Props> = ({ tableProps, filters }) => {
const { selectProps: selectPropsUsers } = useSelect({
resource: "users",
optionLabel: "name",
Expand Down Expand Up @@ -165,23 +161,15 @@ export const CompaniesTableView: FC<Props> = ({
)}
render={(_, record: Company) => {
const value = record.contacts;
const avatars = value?.nodes?.map((contact) => {
return {
name: contact.name,
src: contact.avatarUrl as string | undefined,
};
});

return (
<Avatar.Group maxCount={3} size="small">
{value?.nodes?.map((contact) => {
return (
<Tooltip
title={contact.name}
key={contact.id}
>
<CustomAvatar
name={contact.name}
src={contact.avatarUrl}
/>
</Tooltip>
);
})}
</Avatar.Group>
<CustomAvatarGroup avatars={avatars} size={"small"} />
);
}}
/>
Expand Down
141 changes: 141 additions & 0 deletions examples/app-crm/src/components/custom-avatar-group.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { FC } from "react";
import { CustomAvatar } from "./custom-avatar";
import { AvatarProps, Space, Tooltip } from "antd";
import { Text } from "./text";

type Props = {
avatars: {
name?: string;
src?: string;
}[];
size?: AvatarProps["size"];
maxCount?: number;
containerStyle?: React.CSSProperties;
avatarStyle?: AvatarProps["style"];
gap?: string;
overlap?: boolean;
};

export const CustomAvatarGroup: FC<Props> = ({
avatars,
size,
overlap,
maxCount = 3,
gap = "8px",
containerStyle,
avatarStyle,
}) => {
const visibleAvatars = avatars.slice(0, maxCount);
const remainingAvatars = avatars.slice(maxCount);
const hasRemainingAvatars = remainingAvatars.length > 0;
const shouldOverlap = overlap && avatars.length > 3;

const getImageSize = (size: AvatarProps["size"] | number) => {
if (typeof size === "number") {
return shouldOverlap ? `${size + 4}px` : `${size}px`;
}

switch (size) {
case "large":
return shouldOverlap ? "44px" : "40px";
case "small":
return shouldOverlap ? "28px" : "24px";
default:
return shouldOverlap ? "36px" : "32px";
}
};

return (
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "flex-start",
gap: shouldOverlap ? "0" : gap,
...containerStyle,
}}
>
{visibleAvatars.map((avatar, index) => {
const transform = shouldOverlap
? `translateX(-${index * 8}px)`
: undefined;

return (
<Tooltip title={avatar.name} key={index}>
<CustomAvatar
style={{
cursor: "pointer",
transform,
zIndex: index,
border: shouldOverlap
? "2px solid #fff"
: "none",
width: getImageSize(size),
height: getImageSize(size),
...avatarStyle,
}}
name={avatar?.name}
src={avatar?.src}
size={size}
/>
</Tooltip>
);
})}

{hasRemainingAvatars && (
<Tooltip
destroyTooltipOnHide
title={
<Space direction="vertical">
{remainingAvatars.map((avatar, index) => {
return (
<Space key={index}>
<CustomAvatar
name={avatar.name}
src={avatar.src}
size="small"
/>
<Text
style={{
color: "#fff",
}}
key={avatar.name}
>
{avatar.name}
</Text>
</Space>
);
})}
</Space>
}
>
<Text
className="tertiary"
style={{
userSelect: "none",
cursor: "pointer",
fontSize: "10px",
lineHeight: "22px",
letterSpacing: "0.5px",
fontWeight: 600,
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "50%",
width: getImageSize(size),
height: getImageSize(size),
transform: shouldOverlap
? `translateX(-${visibleAvatars.length * 8}px)`
: undefined,
zIndex: shouldOverlap ? visibleAvatars.length : 1,
backgroundColor: "#D9D9D9",
border: overlap ? "2px solid #fff" : "none",
}}
>
+{remainingAvatars.length}
</Text>
</Tooltip>
)}
</div>
);
};
Loading