From 8a3b85b6b20022db90f4da66daa1572610f20b9d Mon Sep 17 00:00:00 2001 From: Alican Erdurmaz Date: Thu, 31 Aug 2023 15:54:19 +0300 Subject: [PATCH] feat(app-crm): add custom avatar group (#4889) * feat(app-crm): add custom avatar group * chore(app-crm): code structure * fix(app-crm): avatar align * fix(app-crm): avatar align * fix(app-crm): use 6px instead of 4px --- .../src/components/company/card-view.tsx | 68 ++++----- .../src/components/company/table-view.tsx | 32 ++-- .../src/components/custom-avatar-group.tsx | 141 ++++++++++++++++++ 3 files changed, 185 insertions(+), 56 deletions(-) create mode 100644 examples/app-crm/src/components/custom-avatar-group.tsx diff --git a/examples/app-crm/src/components/company/card-view.tsx b/examples/app-crm/src/components/company/card-view.tsx index 81c435761d18..f5a84b65d1b5 100644 --- a/examples/app-crm/src/components/company/card-view.tsx +++ b/examples/app-crm/src/components/company/card-view.tsx @@ -1,6 +1,5 @@ import { FC } from "react"; import { - Avatar, Button, Card, Col, @@ -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; @@ -36,6 +36,15 @@ export const CompaniesCardView: FC = ({ companies, pagination }) => { <> {companies.map((company) => { + const relatedContactAvatars = company.contacts?.nodes?.map( + (contact) => { + return { + name: contact.name, + src: contact.avatarUrl as string | undefined, + }; + }, + ); + return ( = ({ companies, pagination }) => { height: "60px", display: "flex", justifyContent: "space-between", - alignItems: "center", + alignItems: "flex-start", padding: "0 16px", }} > - Related contacts - - {company.contacts?.nodes?.map( - (contact) => { - return ( - - - - ); - }, - )} - - - + + +
Sales owner = ({ companies, pagination }) => { } /> - +
, ]} > diff --git a/examples/app-crm/src/components/company/table-view.tsx b/examples/app-crm/src/components/company/table-view.tsx index 5f566cb510ae..1acde44bc1af 100644 --- a/examples/app-crm/src/components/company/table-view.tsx +++ b/examples/app-crm/src/components/company/table-view.tsx @@ -3,16 +3,16 @@ 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; @@ -20,11 +20,7 @@ type Props = { sorters: CrudSorting; }; -export const CompaniesTableView: FC = ({ - tableProps, - filters, - sorters, -}) => { +export const CompaniesTableView: FC = ({ tableProps, filters }) => { const { selectProps: selectPropsUsers } = useSelect({ resource: "users", optionLabel: "name", @@ -165,23 +161,15 @@ export const CompaniesTableView: FC = ({ )} render={(_, record: Company) => { const value = record.contacts; + const avatars = value?.nodes?.map((contact) => { + return { + name: contact.name, + src: contact.avatarUrl as string | undefined, + }; + }); return ( - - {value?.nodes?.map((contact) => { - return ( - - - - ); - })} - + ); }} /> diff --git a/examples/app-crm/src/components/custom-avatar-group.tsx b/examples/app-crm/src/components/custom-avatar-group.tsx new file mode 100644 index 000000000000..78bdd18e49a4 --- /dev/null +++ b/examples/app-crm/src/components/custom-avatar-group.tsx @@ -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 = ({ + 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 ( +
+ {visibleAvatars.map((avatar, index) => { + const transform = shouldOverlap + ? `translateX(-${index * 8}px)` + : undefined; + + return ( + + + + ); + })} + + {hasRemainingAvatars && ( + + {remainingAvatars.map((avatar, index) => { + return ( + + + + {avatar.name} + + + ); + })} + + } + > + + +{remainingAvatars.length} + + + )} +
+ ); +};