From f4c9f0811449adbd9e80f629e9bd946b00fd252e Mon Sep 17 00:00:00 2001 From: zuies Date: Wed, 28 Jun 2023 03:54:45 +0400 Subject: [PATCH 01/15] add active member breakdown --- package-lock.json | 85 +++ package.json | 2 + src/components/layouts/Sidebar.tsx | 5 +- src/components/layouts/xs/SidebarXs.tsx | 3 +- .../statistics/ActiveMembersComposition.tsx | 4 + .../memberBreakdowns/CustomDialogDetail.tsx | 152 ++++ .../memberBreakdowns/CustomPagination.tsx | 36 + .../memberBreakdowns/CustomPopover.tsx | 36 + .../memberBreakdowns/CustomTable.tsx | 686 ++++++++++++++++++ .../activeMembers/ActiveMemberBreakdown.tsx | 129 ++++ src/configs/index.ts | 1 + src/store/slices/breakdownsSlice.ts | 74 ++ src/store/types/IBreakdown.ts | 14 + src/store/useStore.ts | 2 + src/utils/interfaces.ts | 9 + 15 files changed, 1234 insertions(+), 4 deletions(-) create mode 100644 src/components/pages/statistics/memberBreakdowns/CustomDialogDetail.tsx create mode 100644 src/components/pages/statistics/memberBreakdowns/CustomPagination.tsx create mode 100644 src/components/pages/statistics/memberBreakdowns/CustomPopover.tsx create mode 100644 src/components/pages/statistics/memberBreakdowns/CustomTable.tsx create mode 100644 src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.tsx create mode 100644 src/store/slices/breakdownsSlice.ts create mode 100644 src/store/types/IBreakdown.ts diff --git a/package-lock.json b/package-lock.json index d01d8b3c..7591afde 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,6 +38,7 @@ "moment-timezone": "^0.5.38", "next": "13.0.2", "prettier": "^2.8.2", + "query-string": "^8.1.0", "react": "18.2.0", "react-dom": "18.2.0", "react-helmet-async": "^1.3.0", @@ -54,6 +55,7 @@ "devDependencies": { "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^14.0.0", + "@types/lodash": "^4.14.195", "autoprefixer": "^10.4.13", "babel-jest": "^29.5.0", "identity-obj-proxy": "^3.0.0", @@ -3113,6 +3115,12 @@ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" }, + "node_modules/@types/lodash": { + "version": "4.14.195", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.195.tgz", + "integrity": "sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg==", + "dev": true + }, "node_modules/@types/node": { "version": "18.11.9", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", @@ -4450,6 +4458,14 @@ "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", "dev": true }, + "node_modules/decode-uri-component": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.4.1.tgz", + "integrity": "sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==", + "engines": { + "node": ">=14.16" + } + }, "node_modules/dedent": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", @@ -5561,6 +5577,17 @@ "node": ">=8" } }, + "node_modules/filter-obj": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-5.1.0.tgz", + "integrity": "sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", @@ -9612,6 +9639,22 @@ } ] }, + "node_modules/query-string": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-8.1.0.tgz", + "integrity": "sha512-BFQeWxJOZxZGix7y+SByG3F36dA0AbTy9o6pSmKFcFz7DAj0re9Frkty3saBn3nHo3D0oZJ/+rx3r8H8r8Jbpw==", + "dependencies": { + "decode-uri-component": "^0.4.1", + "filter-obj": "^5.1.0", + "split-on-first": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", @@ -10179,6 +10222,17 @@ "resolved": "https://registry.npmjs.org/spacetime/-/spacetime-7.4.1.tgz", "integrity": "sha512-khQpvLNMhHFzfFJslMfunqqsjOxdmoDDYX5Wh4qYb8N6f8vBPI6HvbT7Wb2wcMa+oP1Xh1HpEcwfPFrj8UfvHQ==" }, + "node_modules/split-on-first": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-3.0.0.tgz", + "integrity": "sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -13255,6 +13309,12 @@ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" }, + "@types/lodash": { + "version": "4.14.195", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.195.tgz", + "integrity": "sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg==", + "dev": true + }, "@types/node": { "version": "18.11.9", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", @@ -14228,6 +14288,11 @@ "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", "dev": true }, + "decode-uri-component": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.4.1.tgz", + "integrity": "sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==" + }, "dedent": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", @@ -15065,6 +15130,11 @@ "to-regex-range": "^5.0.1" } }, + "filter-obj": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-5.1.0.tgz", + "integrity": "sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==" + }, "find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", @@ -17948,6 +18018,16 @@ "integrity": "sha512-6Yg0ekpKICSjPswYOuC5sku/TSWaRYlA0qsXqJgM/d/4pLPHPuTxK7Nbf7jFKzAeedUhR8C7K9Uv63FBsSo8xQ==", "dev": true }, + "query-string": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-8.1.0.tgz", + "integrity": "sha512-BFQeWxJOZxZGix7y+SByG3F36dA0AbTy9o6pSmKFcFz7DAj0re9Frkty3saBn3nHo3D0oZJ/+rx3r8H8r8Jbpw==", + "requires": { + "decode-uri-component": "^0.4.1", + "filter-obj": "^5.1.0", + "split-on-first": "^3.0.0" + } + }, "querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", @@ -18349,6 +18429,11 @@ "resolved": "https://registry.npmjs.org/spacetime/-/spacetime-7.4.1.tgz", "integrity": "sha512-khQpvLNMhHFzfFJslMfunqqsjOxdmoDDYX5Wh4qYb8N6f8vBPI6HvbT7Wb2wcMa+oP1Xh1HpEcwfPFrj8UfvHQ==" }, + "split-on-first": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-3.0.0.tgz", + "integrity": "sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==" + }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", diff --git a/package.json b/package.json index 618ac80d..a1d212eb 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "moment-timezone": "^0.5.38", "next": "13.0.2", "prettier": "^2.8.2", + "query-string": "^8.1.0", "react": "18.2.0", "react-dom": "18.2.0", "react-helmet-async": "^1.3.0", @@ -58,6 +59,7 @@ "devDependencies": { "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^14.0.0", + "@types/lodash": "^4.14.195", "autoprefixer": "^10.4.13", "babel-jest": "^29.5.0", "identity-obj-proxy": "^3.0.0", diff --git a/src/components/layouts/Sidebar.tsx b/src/components/layouts/Sidebar.tsx index d21eb4c2..5eb1018b 100644 --- a/src/components/layouts/Sidebar.tsx +++ b/src/components/layouts/Sidebar.tsx @@ -7,8 +7,7 @@ type items = { icon: any; }; -import polygon from '../../assets/svg/polygon.svg'; - +import { conf } from '../../configs/index'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; // import the icons you need @@ -122,7 +121,7 @@ const Sidebar = () => {
{guildId && guildInfoByDiscord.icon ? ( {guildInfoByDiscord.name { const { guildInfoByDiscord } = useAppStore(); @@ -112,7 +113,7 @@ const Sidebar = () => {
{guildId && guildInfoByDiscord.icon ? ( {guildInfoByDiscord.name([]); @@ -208,6 +210,8 @@ export default function ActiveMembersComposition({
+ +

diff --git a/src/components/pages/statistics/memberBreakdowns/CustomDialogDetail.tsx b/src/components/pages/statistics/memberBreakdowns/CustomDialogDetail.tsx new file mode 100644 index 00000000..0d71aa7f --- /dev/null +++ b/src/components/pages/statistics/memberBreakdowns/CustomDialogDetail.tsx @@ -0,0 +1,152 @@ +import React from 'react'; +import { Dialog, DialogProps } from '@mui/material'; +import { IoClose } from 'react-icons/io5'; +import { conf } from '../../../../configs'; +import { Avatar } from '@mui/material'; + +interface CustomDialogDetailProps extends DialogProps { + open: boolean; + rowDetail: any; + onClose: () => void; +} + +const options = [ + { name: 'All active', value: 'all_active', color: '#3AAE2B' }, + { name: 'Newly active', value: 'all_new_active', color: '#FF9022' }, + { name: 'Consistently active', value: 'all_consistent', color: '#804EE1' }, + { name: 'Vital member', value: 'all_vital', color: '#313671' }, + { name: 'Became disengaged', value: 'all_new_disengaged', color: '#EB3E56' }, + { name: 'Others', value: 'others', color: '#AAAAAA' }, +]; + +const CustomDialogDetail: React.FC = ({ + open, + rowDetail, + onClose, + ...props +}) => { + const handleClose = () => { + onClose(); + }; + + return ( + +
+ +
+
+ + + {rowDetail?.username} + +
+
+

Roles:

+
+ {rowDetail?.roles.map((role: any) => ( +
+ + + {role.name} + +
+ ))} +
+
+
+

Activity composition:

+
+ {rowDetail && rowDetail?.activityComposition.length > 0 ? ( + rowDetail?.activityComposition.map((composition: any) => { + const matchedOption = options.find( + (option) => option.value === composition + ); + const backgroundColor = matchedOption + ? matchedOption.color + : '#96A5A6'; + + return ( +
+ + + {composition} + +
+ ); + }) + ) : ( +
+ + + other{' '} + +
+ )} +
+
+
+
+
+ ); +}; + +export default CustomDialogDetail; diff --git a/src/components/pages/statistics/memberBreakdowns/CustomPagination.tsx b/src/components/pages/statistics/memberBreakdowns/CustomPagination.tsx new file mode 100644 index 00000000..64a72856 --- /dev/null +++ b/src/components/pages/statistics/memberBreakdowns/CustomPagination.tsx @@ -0,0 +1,36 @@ +import { useState } from 'react'; +import { Pagination, PaginationItem } from '@mui/material'; + +interface PaginationProps { + totalItems: number; + itemsPerPage: number; + currentPage: number; + onChangePage: (page: number) => void; +} + +const CustomPagination: React.FC = ({ + totalItems, + itemsPerPage, + currentPage, + onChangePage, +}) => { + const totalPages = Math.ceil(totalItems / itemsPerPage); + + const handleChangePage = (page: number) => { + if (page !== currentPage) { + onChangePage(page); + } + }; + + return ( + handleChangePage(page)} + renderItem={(item) => } + /> + ); +}; + +export default CustomPagination; diff --git a/src/components/pages/statistics/memberBreakdowns/CustomPopover.tsx b/src/components/pages/statistics/memberBreakdowns/CustomPopover.tsx new file mode 100644 index 00000000..0f930487 --- /dev/null +++ b/src/components/pages/statistics/memberBreakdowns/CustomPopover.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { Popover, List, ListItem, ListItemText } from '@mui/material'; + +interface CustomPopoverProps { + open: boolean; + anchorEl: HTMLButtonElement | null; + onClose: () => void; + children: React.ReactNode; +} + +const CustomPopover: React.FC = ({ + open, + anchorEl, + onClose, + children, +}) => { + return ( + + {children} + + ); +}; + +export default CustomPopover; diff --git a/src/components/pages/statistics/memberBreakdowns/CustomTable.tsx b/src/components/pages/statistics/memberBreakdowns/CustomTable.tsx new file mode 100644 index 00000000..b599d7d7 --- /dev/null +++ b/src/components/pages/statistics/memberBreakdowns/CustomTable.tsx @@ -0,0 +1,686 @@ +import { + Avatar, + Button, + Checkbox, + FormControlLabel, + List, + ListItem, + ListItemText, + Popover, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + TextField, +} from '@mui/material'; +import { useEffect, useState } from 'react'; +import { + MdOutlineKeyboardArrowDown, + MdOutlineKeyboardArrowUp, + MdSearch, +} from 'react-icons/md'; +import moment from 'moment'; + +import { Column, Row } from '../../../../utils/interfaces'; +import { IUser } from '../../../../utils/types'; +import { conf } from '../../../../configs'; +import Loading from '../../../global/Loading'; +import useAppStore from '../../../../store/useStore'; +import { StorageService } from '../../../../services/StorageService'; +import CustomDialogDetail from './CustomDialogDetail'; + +interface CustomTableProps { + data: Row[]; + columns: Column[]; + isLoading: boolean; + handleRoleSelectionChange: (selectedRoles: string[]) => void; + handleActivityOptionSelectionChange: (selectedRoles: string[]) => void; + handleJoinedAtChange: (joinedAt: string) => void; + handleUsernameChange: (userName: string) => void; +} + +const options = [ + { name: 'All active', value: 'all_active', color: '#3AAE2B' }, + { name: 'Newly active', value: 'all_new_active', color: '#FF9022' }, + { name: 'Consistently active', value: 'all_consistent', color: '#804EE1' }, + { name: 'Vital member', value: 'all_vital', color: '#313671' }, + { name: 'Became disengaged', value: 'all_new_disengaged', color: '#EB3E56' }, + { name: 'Others', value: 'others', color: '#AAAAAA' }, +]; + +const CustomTable: React.FC = ({ + data, + columns, + isLoading, + handleRoleSelectionChange, + handleActivityOptionSelectionChange, + handleJoinedAtChange, + handleUsernameChange, +}) => { + const { getRoles, roles } = useAppStore(); + useEffect(() => { + const user = StorageService.readLocalStorage('user'); + + if (!user) { + return; + } + + const { guild } = user; + getRoles(guild.guildId); + }, []); + + const [anchorElRoles, setAnchorElRoles] = useState( + null + ); + const [anchorElActivity, setAnchorElActivity] = + useState(null); + const [selectedRoles, setSelectedRoles] = useState([]); + useEffect(() => { + setSelectedRoles(roles.map((role: { id: any }) => role.id)); + }, [roles]); + const [selectAllRoles, setSelectAllRoles] = useState(true); + const [selectedActivityOptions, setSelectedActivityOptions] = useState< + string[] + >(options.map((option) => option.value)); + const [selectAllActivityOptions, setSelectAllActivityOptions] = + useState(true); + + const handleOpenRolesPopup = (event: React.MouseEvent) => { + setAnchorElRoles(event.currentTarget); + }; + + const handleOpenActivityPopup = ( + event: React.MouseEvent + ) => { + setAnchorElActivity(event.currentTarget); + }; + + const handleClosePopup = () => { + setAnchorElRoles(null); + setAnchorElActivity(null); + setAnchorElJoinedAt(null); + }; + + const isRolesPopupOpen = Boolean(anchorElRoles); + const isActivityPopupOpen = Boolean(anchorElActivity); + + const handleSelectAllRoles = (event: React.ChangeEvent) => { + if (event.target.checked) { + const allRoleNames = roles.map((role: any) => role.id); + setSelectedRoles(allRoleNames); + } else { + setSelectedRoles([]); + } + setSelectAllRoles(event.target.checked); + }; + + const handleSelectRole = (event: React.ChangeEvent) => { + const roleName = event.target.value; + const updatedSelectedRoles = selectedRoles.includes(roleName) + ? selectedRoles.filter((role) => role !== roleName) + : [...selectedRoles, roleName]; + + setSelectedRoles(updatedSelectedRoles); + setSelectAllRoles(updatedSelectedRoles.length === roles.length); + }; + + useEffect(() => { + handleRoleSelectionChange(selectedRoles); + }, [selectedRoles, handleRoleSelectionChange]); + + useEffect(() => { + handleActivityOptionSelectionChange(selectedActivityOptions); + }, [selectedActivityOptions, handleActivityOptionSelectionChange]); + + const handleSelectAllActivityOptions = ( + event: React.ChangeEvent + ) => { + if (event.target.checked) { + setSelectedActivityOptions(options.map((option) => option.value)); + } else { + setSelectedActivityOptions([]); + } + setSelectAllActivityOptions(event.target.checked); + }; + + const handleSelectActivityOption = ( + event: React.ChangeEvent + ) => { + const optionValue = event.target.value; + const updatedSelectedOptions = selectedActivityOptions.includes(optionValue) + ? selectedActivityOptions.filter((option) => option !== optionValue) + : [...selectedActivityOptions, optionValue]; + + setSelectedActivityOptions(updatedSelectedOptions); + setSelectAllActivityOptions( + updatedSelectedOptions.length === options.length + ); + }; + + const formatDate = (date: string) => { + return moment(date).format('DD MMM YYYY'); + }; + + const [anchorElJoinedAt, setAnchorElJoinedAt] = + useState(null); + const [selectedSortOption, setSelectedSortOption] = useState(''); + + const handleOpenJoinedAtPopup = ( + event: React.MouseEvent + ) => { + setAnchorElJoinedAt(event.currentTarget); + }; + + const isJoinedAtPopupOpen = Boolean(anchorElJoinedAt); + + const handleSortOptionClick = (optionValue: string) => { + setSelectedSortOption(optionValue); + handleJoinedAtChange(optionValue); + handleClosePopup(); + }; + + const [searchText, setSearchText] = useState(''); + + const handleSearchChange = (event: React.ChangeEvent) => { + const { value } = event.target; + setSearchText(value); + }; + + const handleUsernameBlur = (event: React.FocusEvent) => { + const { value } = event.target; + setSearchText(value); + handleUsernameChange(value); + }; + + const [open, setOpen] = useState(false); + const [rowDetail, setRowDetail] = useState(); + const handleShowDetails = (rowData: Row) => { + setOpen(true); + setRowDetail(rowData); + }; + + const handleClose = () => { + setOpen(false); + }; + + return ( + <> + + + + {columns.map((column) => ( + + {column.id === 'roles' ? ( + <> + + +
+ + } + label={'All Roles'} + /> +

Show members with tags:

+ {roles.map((role: any) => ( + + + } + label={ +
+ +
{role.name}
+
+ } + /> +
+ ))} +
+
+ + ) : column.id === 'activityComposition' ? ( + <> + + +
+ + } + label={
All
} + /> + {options.map((option: any) => ( + + + } + label={ +
+ +
{option.name}
+
+ } + /> +
+ ))} +
+
+ + ) : column.id === 'joinedAt' ? ( + <> + + + + handleSortOptionClick('desc')} + selected={selectedSortOption === 'desc'} + > + + + handleSortOptionClick('asc')} + selected={selectedSortOption === 'asc'} + > + + + + + + ) : column.id === 'username' ? ( +
+ Name + , + sx: { backgroundColor: '#F5F5F5', padding: '0 0.4rem' }, + }} + value={searchText} + onChange={handleSearchChange} + onBlur={handleUsernameBlur} + sx={{ marginLeft: '8px' }} + /> +
+ ) : ( + column.label + )} +
+ ))} +
+
+ + {isLoading ? ( + + +
+ +
+
+
+ ) : ( + <> + {data && data.length > 0 ? ( + data.map((row, index) => ( + + {columns.map((column) => ( + + {column.id === 'username' ? ( +
+ + + {row[column.id]} + +
+ ) : column.id === 'roles' ? ( +
+ {row.roles.length > 0 ? ( + <> + {row.roles.slice(0, 4).map((role: any) => ( +
+ + + {role.name} + +
+ ))} + {row.roles.length > 4 && ( +
handleShowDetails(row)} + > + + +{row.roles.length - 4} + +
+ )} + + ) : ( +
+ + + None + +
+ )} +
+ ) : column.id === 'activityComposition' ? ( +
+ {row.activityComposition && + row.activityComposition.length > 0 ? ( + <> + {row.activityComposition + .slice(0, 1) + .map((composition: any) => { + const matchedOption = options.find( + (option) => option.value === composition + ); + const backgroundColor = matchedOption + ? matchedOption.color + : '#96A5A6'; + + return ( +
+ + + {composition} + +
+ ); + })} + {row.activityComposition.length > 4 && ( +
handleShowDetails(row)} + > + + +{row.activityComposition.length - 4} + +
+ )} + + ) : ( +
+ + + other{' '} + +
+ )} +
+ ) : column.id === 'joinedAt' ? ( + formatDate(row.joinedAt) + ) : ( + row[column.id] + )} +
+ ))} +
+ )) + ) : ( + + + No data available + + + )} + + )} +
+
+ + + ); +}; + +export default CustomTable; diff --git a/src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.tsx b/src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.tsx new file mode 100644 index 00000000..11e49baa --- /dev/null +++ b/src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.tsx @@ -0,0 +1,129 @@ +import { useState, useEffect } from 'react'; +import { StorageService } from '../../../../../services/StorageService'; +import useAppStore from '../../../../../store/useStore'; +import { IUser } from '../../../../../utils/types'; +import CustomTable from '../CustomTable'; +import { Column } from '../../../../../utils/interfaces'; +import CustomPagination from '../CustomPagination'; +import CustomButton from '../../../../global/CustomButton'; +import clsx from 'clsx'; + +const columns: Column[] = [ + { id: 'username', label: 'Name' }, + { id: 'roles', label: 'Roles' }, + { id: 'activityComposition', label: 'Activity composition' }, + { id: 'joinedAt', label: 'DAO member since' }, +]; + +export default function ActiveMemberBreakdown() { + const { getActiveMemberCompositionTable, isActiveMembersBreakdownLoading } = + useAppStore(); + const [isExpanded, toggleExpanded] = useState(false); + const [page, setPage] = useState(1); + const [roles, setRoles] = useState([]); + const [activityComposition, setActivityComposition] = useState([]); + const [username, setUsername] = useState(''); + const [sortBy, setSortBy] = useState('joinedAt:desc'); + const [fetchedData, setFetchedData] = useState({ + results: [], + totalResults: 0, + totalPages: 0, + }); + const user = StorageService.readLocalStorage('user'); + const guild = user?.guild; + + const handlePageChange = (selectedPage: number) => { + setPage(selectedPage); + }; + + useEffect(() => { + if (!guild) { + return; + } + + const fetchData = async () => { + const res = await getActiveMemberCompositionTable( + guild.guildId, + activityComposition, + roles, + username, + sortBy, + page + ); + setFetchedData(res); + }; + + fetchData(); + }, [page, roles, activityComposition, username, sortBy]); + + useEffect(() => { + setPage(1); + }, [activityComposition, roles, username, sortBy]); + + const handleRoleSelectionChange = (selectedRoles: string[]) => { + setRoles(selectedRoles); + }; + + const handleActivityOptionSelectionChange = (selectedOptions: string[]) => { + setActivityComposition(selectedOptions); + }; + + const handleJoinedAtChange = (joinedAt: string) => { + setSortBy(joinedAt); + }; + + const handleUsernameChange = (value: string) => { + setUsername(value); + }; + + return ( + <> +
+

+ Members breakdown +

+
+ {isExpanded && ( +
+ )} +
+ + {fetchedData.totalResults > 0 && ( +
+ +
+ )} +
+
+
+
+ toggleExpanded(!isExpanded)} + /> +
+ + ); +} diff --git a/src/configs/index.ts b/src/configs/index.ts index 523460d0..65054cb1 100644 --- a/src/configs/index.ts +++ b/src/configs/index.ts @@ -10,4 +10,5 @@ export const conf = { SENTRY_DSN: process.env.NEXT_PUBLIC_SENTRY_DSN, SENTRY_ENV: process.env.NEXT_PUBLIC_SENTRY_ENV, SENTRY_TOKEN: process.env.SENTRY_TOKEN, + DISCORD_CDN: process.env.NEXT_PUBLIC_DISCORD_CDN, }; diff --git a/src/store/slices/breakdownsSlice.ts b/src/store/slices/breakdownsSlice.ts new file mode 100644 index 00000000..51f6b1b4 --- /dev/null +++ b/src/store/slices/breakdownsSlice.ts @@ -0,0 +1,74 @@ +import { StateCreator } from 'zustand'; +import { axiosInstance } from '../../axiosInstance'; +import IBreakdown from '../types/IBreakdown'; + +const createBreakdownsSlice: StateCreator = (set, get) => ({ + isActiveMembersBreakdownLoading: false, + isRolesLoading: false, + roles: [], + getActiveMemberCompositionTable: async ( + guild_id: string, + activityComposition: string[], + roles: string[], + username?: string, + sortBy?: string, + page?: number + ) => { + try { + set(() => ({ isActiveMembersBreakdownLoading: true })); + + const requestData = { + activityComposition: activityComposition || [], + roles: roles || [], + username: username || undefined, + }; + + const params = new URLSearchParams(); + if (page) { + params.append('page', page.toString()); + } + if (sortBy) { + params.append('sortBy', `joinedAt:${sortBy}`); + } + + requestData.activityComposition.forEach((value) => { + params.append('activityComposition', value); + }); + + requestData.roles.forEach((value) => { + params.append('roles', value); + }); + + if (username) { + params.append('username', username); + } + + const url = `/member-activity/${guild_id}/active-members-composition-table?${params.toString()}`; + + const { data } = await axiosInstance.post(url); + + set(() => ({ isActiveMembersBreakdownLoading: false })); + return data; + } catch (error) { + set(() => ({ isActiveMembersBreakdownLoading: false })); + // Handle the error + } + }, + getRoles: async (guild_id: string) => { + try { + set(() => ({ isRolesLoading: true })); + + const { data } = await axiosInstance.get( + `/guilds/discord-api/${guild_id}/roles` + ); + + set(() => ({ roles: data, isRolesLoading: false })); + return data; + } catch (error) { + set(() => ({ isRolesLoading: false })); + // Handle the error + } + }, +}); + +export default createBreakdownsSlice; diff --git a/src/store/types/IBreakdown.ts b/src/store/types/IBreakdown.ts new file mode 100644 index 00000000..3b01187e --- /dev/null +++ b/src/store/types/IBreakdown.ts @@ -0,0 +1,14 @@ +export default interface IHeatmap { + isActiveMembersBreakdownLoading: boolean; + isRolesLoading: boolean; + roles: any[]; + getActiveMemberCompositionTable: ( + guild_id: string, + activityComposition: string[], + roles: string[], + username?: string, + sortBy: string, + page?: number // Add page as an optional parameter + ) => any; + getRoles: (guild_id: string) => any; +} diff --git a/src/store/useStore.ts b/src/store/useStore.ts index 1e53d5ae..076ce7ae 100644 --- a/src/store/useStore.ts +++ b/src/store/useStore.ts @@ -2,11 +2,13 @@ import { create } from 'zustand'; import createAuthSlice from './slices/authSlice'; import createChartSlice from './slices/chartSlice'; import createSettingSlice from './slices/settingSlice'; +import createBreakdownsSlice from './slices/breakdownsSlice'; const useAppStore = create()((...a) => ({ ...createAuthSlice(...a), ...createChartSlice(...a), ...createSettingSlice(...a), + ...createBreakdownsSlice(...a), })); export default useAppStore; diff --git a/src/utils/interfaces.ts b/src/utils/interfaces.ts index fe7b675d..52d9d83e 100644 --- a/src/utils/interfaces.ts +++ b/src/utils/interfaces.ts @@ -13,3 +13,12 @@ export interface SeriesData { name: string; data: number[]; } + +export interface Column { + id: string; + label: string; +} + +export interface Row { + [key: string]: any; +} From dd609a5246458c6227096c498077d75658cfdf17 Mon Sep 17 00:00:00 2001 From: zuies Date: Wed, 28 Jun 2023 03:55:48 +0400 Subject: [PATCH 02/15] remove useless packages --- package-lock.json | 85 ----------------------------------------------- package.json | 2 -- 2 files changed, 87 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7591afde..d01d8b3c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,7 +38,6 @@ "moment-timezone": "^0.5.38", "next": "13.0.2", "prettier": "^2.8.2", - "query-string": "^8.1.0", "react": "18.2.0", "react-dom": "18.2.0", "react-helmet-async": "^1.3.0", @@ -55,7 +54,6 @@ "devDependencies": { "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^14.0.0", - "@types/lodash": "^4.14.195", "autoprefixer": "^10.4.13", "babel-jest": "^29.5.0", "identity-obj-proxy": "^3.0.0", @@ -3115,12 +3113,6 @@ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" }, - "node_modules/@types/lodash": { - "version": "4.14.195", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.195.tgz", - "integrity": "sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg==", - "dev": true - }, "node_modules/@types/node": { "version": "18.11.9", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", @@ -4458,14 +4450,6 @@ "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", "dev": true }, - "node_modules/decode-uri-component": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.4.1.tgz", - "integrity": "sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==", - "engines": { - "node": ">=14.16" - } - }, "node_modules/dedent": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", @@ -5577,17 +5561,6 @@ "node": ">=8" } }, - "node_modules/filter-obj": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-5.1.0.tgz", - "integrity": "sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", @@ -9639,22 +9612,6 @@ } ] }, - "node_modules/query-string": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-8.1.0.tgz", - "integrity": "sha512-BFQeWxJOZxZGix7y+SByG3F36dA0AbTy9o6pSmKFcFz7DAj0re9Frkty3saBn3nHo3D0oZJ/+rx3r8H8r8Jbpw==", - "dependencies": { - "decode-uri-component": "^0.4.1", - "filter-obj": "^5.1.0", - "split-on-first": "^3.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", @@ -10222,17 +10179,6 @@ "resolved": "https://registry.npmjs.org/spacetime/-/spacetime-7.4.1.tgz", "integrity": "sha512-khQpvLNMhHFzfFJslMfunqqsjOxdmoDDYX5Wh4qYb8N6f8vBPI6HvbT7Wb2wcMa+oP1Xh1HpEcwfPFrj8UfvHQ==" }, - "node_modules/split-on-first": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-3.0.0.tgz", - "integrity": "sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -13309,12 +13255,6 @@ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" }, - "@types/lodash": { - "version": "4.14.195", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.195.tgz", - "integrity": "sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg==", - "dev": true - }, "@types/node": { "version": "18.11.9", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", @@ -14288,11 +14228,6 @@ "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", "dev": true }, - "decode-uri-component": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.4.1.tgz", - "integrity": "sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==" - }, "dedent": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", @@ -15130,11 +15065,6 @@ "to-regex-range": "^5.0.1" } }, - "filter-obj": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-5.1.0.tgz", - "integrity": "sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==" - }, "find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", @@ -18018,16 +17948,6 @@ "integrity": "sha512-6Yg0ekpKICSjPswYOuC5sku/TSWaRYlA0qsXqJgM/d/4pLPHPuTxK7Nbf7jFKzAeedUhR8C7K9Uv63FBsSo8xQ==", "dev": true }, - "query-string": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-8.1.0.tgz", - "integrity": "sha512-BFQeWxJOZxZGix7y+SByG3F36dA0AbTy9o6pSmKFcFz7DAj0re9Frkty3saBn3nHo3D0oZJ/+rx3r8H8r8Jbpw==", - "requires": { - "decode-uri-component": "^0.4.1", - "filter-obj": "^5.1.0", - "split-on-first": "^3.0.0" - } - }, "querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", @@ -18429,11 +18349,6 @@ "resolved": "https://registry.npmjs.org/spacetime/-/spacetime-7.4.1.tgz", "integrity": "sha512-khQpvLNMhHFzfFJslMfunqqsjOxdmoDDYX5Wh4qYb8N6f8vBPI6HvbT7Wb2wcMa+oP1Xh1HpEcwfPFrj8UfvHQ==" }, - "split-on-first": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-3.0.0.tgz", - "integrity": "sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==" - }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", diff --git a/package.json b/package.json index a1d212eb..618ac80d 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,6 @@ "moment-timezone": "^0.5.38", "next": "13.0.2", "prettier": "^2.8.2", - "query-string": "^8.1.0", "react": "18.2.0", "react-dom": "18.2.0", "react-helmet-async": "^1.3.0", @@ -59,7 +58,6 @@ "devDependencies": { "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^14.0.0", - "@types/lodash": "^4.14.195", "autoprefixer": "^10.4.13", "babel-jest": "^29.5.0", "identity-obj-proxy": "^3.0.0", From 84b8e845358b297d8082c4152a7ac8d4564bd5eb Mon Sep 17 00:00:00 2001 From: zuies Date: Wed, 28 Jun 2023 03:58:44 +0400 Subject: [PATCH 03/15] fix issue on ci --- src/store/types/IBreakdown.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/store/types/IBreakdown.ts b/src/store/types/IBreakdown.ts index 3b01187e..c5123c48 100644 --- a/src/store/types/IBreakdown.ts +++ b/src/store/types/IBreakdown.ts @@ -7,7 +7,7 @@ export default interface IHeatmap { activityComposition: string[], roles: string[], username?: string, - sortBy: string, + sortBy?: string, page?: number // Add page as an optional parameter ) => any; getRoles: (guild_id: string) => any; From 4d0cae1055ee7fc8848d5f46d2021ba7ea8f29ef Mon Sep 17 00:00:00 2001 From: zuies Date: Wed, 28 Jun 2023 04:04:03 +0400 Subject: [PATCH 04/15] add interfaces and assign to code --- .../memberBreakdowns/CustomTable.tsx | 74 ++++++++++--------- .../activeMembers/ActiveMemberBreakdown.tsx | 8 +- src/utils/interfaces.ts | 6 ++ 3 files changed, 54 insertions(+), 34 deletions(-) diff --git a/src/components/pages/statistics/memberBreakdowns/CustomTable.tsx b/src/components/pages/statistics/memberBreakdowns/CustomTable.tsx index b599d7d7..e1ab5b03 100644 --- a/src/components/pages/statistics/memberBreakdowns/CustomTable.tsx +++ b/src/components/pages/statistics/memberBreakdowns/CustomTable.tsx @@ -22,7 +22,7 @@ import { } from 'react-icons/md'; import moment from 'moment'; -import { Column, Row } from '../../../../utils/interfaces'; +import { Column, IRoles, Row } from '../../../../utils/interfaces'; import { IUser } from '../../../../utils/types'; import { conf } from '../../../../configs'; import Loading from '../../../global/Loading'; @@ -77,7 +77,7 @@ const CustomTable: React.FC = ({ useState(null); const [selectedRoles, setSelectedRoles] = useState([]); useEffect(() => { - setSelectedRoles(roles.map((role: { id: any }) => role.id)); + setSelectedRoles(roles.map((role: IRoles) => role.id)); }, [roles]); const [selectAllRoles, setSelectAllRoles] = useState(true); const [selectedActivityOptions, setSelectedActivityOptions] = useState< @@ -107,7 +107,7 @@ const CustomTable: React.FC = ({ const handleSelectAllRoles = (event: React.ChangeEvent) => { if (event.target.checked) { - const allRoleNames = roles.map((role: any) => role.id); + const allRoleNames = roles.map((role: IRoles) => role.id); setSelectedRoles(allRoleNames); } else { setSelectedRoles([]); @@ -262,7 +262,7 @@ const CustomTable: React.FC = ({ label={'All Roles'} />

Show members with tags:

- {roles.map((role: any) => ( + {roles.map((role: IRoles) => ( = ({ } label={
All
} /> - {options.map((option: any) => ( - - - } - label={ -
- ( + + -
{option.name}
-
- } - /> -
- ))} + } + label={ +
+ +
+ {option.name} +
+
+ } + /> +
+ ) + )}

@@ -489,7 +497,7 @@ const CustomTable: React.FC = ({
{row.roles.length > 0 ? ( <> - {row.roles.slice(0, 4).map((role: any) => ( + {row.roles.slice(0, 4).map((role: IRoles) => (
= ({ <> {row.activityComposition .slice(0, 1) - .map((composition: any) => { + .map((composition: string) => { const matchedOption = options.find( (option) => option.value === composition ); diff --git a/src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.tsx b/src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.tsx index 11e49baa..117ab710 100644 --- a/src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.tsx +++ b/src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.tsx @@ -24,7 +24,13 @@ export default function ActiveMemberBreakdown() { const [activityComposition, setActivityComposition] = useState([]); const [username, setUsername] = useState(''); const [sortBy, setSortBy] = useState('joinedAt:desc'); - const [fetchedData, setFetchedData] = useState({ + const [fetchedData, setFetchedData] = useState<{ + limit?: string | number; + page?: string | number; + results: any[]; + totalPages: string | number; + totalResults: string | number; + }>({ results: [], totalResults: 0, totalPages: 0, diff --git a/src/utils/interfaces.ts b/src/utils/interfaces.ts index 52d9d83e..4fd6d965 100644 --- a/src/utils/interfaces.ts +++ b/src/utils/interfaces.ts @@ -22,3 +22,9 @@ export interface Column { export interface Row { [key: string]: any; } + +export interface IRoles { + id: string; + color: number | string; + name: string; +} From b95854eeb87f1de814e322ba034db1a325f0b089 Mon Sep 17 00:00:00 2001 From: zuies Date: Wed, 28 Jun 2023 04:06:26 +0400 Subject: [PATCH 05/15] change type --- .../memberBreakdowns/activeMembers/ActiveMemberBreakdown.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.tsx b/src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.tsx index 117ab710..995cd505 100644 --- a/src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.tsx +++ b/src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.tsx @@ -28,8 +28,8 @@ export default function ActiveMemberBreakdown() { limit?: string | number; page?: string | number; results: any[]; - totalPages: string | number; - totalResults: string | number; + totalPages: number; + totalResults: number; }>({ results: [], totalResults: 0, From a5599b3e683a7fc6408ca3710de3910d8b6482b0 Mon Sep 17 00:00:00 2001 From: zuies Date: Wed, 28 Jun 2023 04:10:59 +0400 Subject: [PATCH 06/15] change isExpanded statements --- .../activeMembers/ActiveMemberBreakdown.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.tsx b/src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.tsx index 995cd505..558e8e21 100644 --- a/src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.tsx +++ b/src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.tsx @@ -89,12 +89,15 @@ export default function ActiveMemberBreakdown() { Members breakdown
- {isExpanded && ( + {!isExpanded && (
)} -
+
Date: Wed, 28 Jun 2023 13:09:30 +0400 Subject: [PATCH 07/15] update some classes and change sortBy default value --- .../statistics/memberBreakdowns/CustomTable.tsx | 15 ++++++++++----- .../activeMembers/ActiveMemberBreakdown.tsx | 8 ++++---- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/components/pages/statistics/memberBreakdowns/CustomTable.tsx b/src/components/pages/statistics/memberBreakdowns/CustomTable.tsx index e1ab5b03..7fe605d0 100644 --- a/src/components/pages/statistics/memberBreakdowns/CustomTable.tsx +++ b/src/components/pages/statistics/memberBreakdowns/CustomTable.tsx @@ -474,14 +474,19 @@ const CustomTable: React.FC = ({ : 'border-1 border-solid border-gray-700' }`} > - {columns.map((column) => ( + {columns.map((column, columnIndex) => ( {column.id === 'username' ? (
@@ -572,7 +577,7 @@ const CustomTable: React.FC = ({ row.activityComposition.length > 0 ? ( <> {row.activityComposition - .slice(0, 1) + .slice(0, 4) .map((composition: string) => { const matchedOption = options.find( (option) => option.value === composition @@ -641,7 +646,7 @@ const CustomTable: React.FC = ({ flexShrink: 0, }} /> - other{' '} + other
)} diff --git a/src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.tsx b/src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.tsx index 558e8e21..02b9b317 100644 --- a/src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.tsx +++ b/src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.tsx @@ -23,7 +23,7 @@ export default function ActiveMemberBreakdown() { const [roles, setRoles] = useState([]); const [activityComposition, setActivityComposition] = useState([]); const [username, setUsername] = useState(''); - const [sortBy, setSortBy] = useState('joinedAt:desc'); + const [sortBy, setSortBy] = useState('desc'); const [fetchedData, setFetchedData] = useState<{ limit?: string | number; page?: string | number; @@ -99,7 +99,7 @@ export default function ActiveMemberBreakdown() { )}
- {fetchedData.totalResults > 0 && ( + {fetchedData?.totalResults > 0 && (
toggleExpanded(!isExpanded)} />
From 0aad49b5e31c7df2082268fedbe07e1eb7d7b1e2 Mon Sep 17 00:00:00 2001 From: zuies Date: Wed, 28 Jun 2023 15:10:28 +0400 Subject: [PATCH 08/15] add margin --- .../memberBreakdowns/activeMembers/ActiveMemberBreakdown.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.tsx b/src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.tsx index 02b9b317..11bf18ce 100644 --- a/src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.tsx +++ b/src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.tsx @@ -85,7 +85,7 @@ export default function ActiveMemberBreakdown() { return ( <>
-

+

Members breakdown

Date: Wed, 28 Jun 2023 19:29:22 +0400 Subject: [PATCH 09/15] update style in empty state --- src/components/pages/statistics/memberBreakdowns/CustomTable.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/pages/statistics/memberBreakdowns/CustomTable.tsx b/src/components/pages/statistics/memberBreakdowns/CustomTable.tsx index 7fe605d0..ca0e9999 100644 --- a/src/components/pages/statistics/memberBreakdowns/CustomTable.tsx +++ b/src/components/pages/statistics/memberBreakdowns/CustomTable.tsx @@ -668,6 +668,7 @@ const CustomTable: React.FC = ({ style={{ border: 'none', }} + className="bg-gray-100 py-8 rounded-md font-semibold" > No data available From f9bdf19c87c4ed1f08583fa7a560e6651aafce92 Mon Sep 17 00:00:00 2001 From: zuies Date: Wed, 28 Jun 2023 19:30:59 +0400 Subject: [PATCH 10/15] hide button while there is no record to show in the table --- .../activeMembers/ActiveMemberBreakdown.tsx | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.tsx b/src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.tsx index 11bf18ce..94d627c6 100644 --- a/src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.tsx +++ b/src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.tsx @@ -84,7 +84,7 @@ export default function ActiveMemberBreakdown() { return ( <> -
+

Members breakdown

@@ -124,15 +124,17 @@ export default function ActiveMemberBreakdown() {
-
- toggleExpanded(!isExpanded)} - /> -
+ {fetchedData?.results.length > 0 ?? ( +
+ toggleExpanded(!isExpanded)} + /> +
+ )} ); } From d6ec66356416d6c96aaba5aed9ab2bd3f30edc1d Mon Sep 17 00:00:00 2001 From: zuies Date: Thu, 29 Jun 2023 14:13:40 +0400 Subject: [PATCH 11/15] add style --- .../activeMembers/ActiveMemberBreakdown.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.tsx b/src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.tsx index 94d627c6..435c9087 100644 --- a/src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.tsx +++ b/src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.tsx @@ -97,7 +97,13 @@ export default function ActiveMemberBreakdown() { {!isExpanded && (
)} -
+
0 + ? 'pointer-events-none' + : '' + )} + > Date: Fri, 30 Jun 2023 13:24:19 +0400 Subject: [PATCH 12/15] update table --- .../memberBreakdowns/CustomDialogDetail.tsx | 10 +++--- .../memberBreakdowns/CustomTable.tsx | 34 +++++++++++-------- .../activeMembers/ActiveMemberBreakdown.tsx | 27 ++++++++------- src/store/slices/breakdownsSlice.ts | 7 ++-- src/utils/interfaces.ts | 2 +- 5 files changed, 41 insertions(+), 39 deletions(-) diff --git a/src/components/pages/statistics/memberBreakdowns/CustomDialogDetail.tsx b/src/components/pages/statistics/memberBreakdowns/CustomDialogDetail.tsx index 0d71aa7f..c1ac95bd 100644 --- a/src/components/pages/statistics/memberBreakdowns/CustomDialogDetail.tsx +++ b/src/components/pages/statistics/memberBreakdowns/CustomDialogDetail.tsx @@ -49,7 +49,7 @@ const CustomDialogDetail: React.FC = ({

Roles:

-
+
{rowDetail?.roles.map((role: any) => (
= ({

Activity composition:

-
+
{rowDetail && rowDetail?.activityComposition.length > 0 ? ( rowDetail?.activityComposition.map((composition: any) => { const matchedOption = options.find( - (option) => option.value === composition + (option) => option.name === composition ); const backgroundColor = matchedOption ? matchedOption.color @@ -98,10 +98,10 @@ const CustomDialogDetail: React.FC = ({ return (
= ({ useState(null); const [selectedRoles, setSelectedRoles] = useState([]); useEffect(() => { - setSelectedRoles(roles.map((role: IRoles) => role.id)); + setSelectedRoles(roles.map((role: IRoles) => role.roleId)); }, [roles]); const [selectAllRoles, setSelectAllRoles] = useState(true); const [selectedActivityOptions, setSelectedActivityOptions] = useState< @@ -107,7 +107,7 @@ const CustomTable: React.FC = ({ const handleSelectAllRoles = (event: React.ChangeEvent) => { if (event.target.checked) { - const allRoleNames = roles.map((role: IRoles) => role.id); + const allRoleNames = roles.map((role: IRoles) => role.roleId); setSelectedRoles(allRoleNames); } else { setSelectedRoles([]); @@ -268,9 +268,9 @@ const CustomTable: React.FC = ({ control={ } label={ @@ -434,7 +434,11 @@ const CustomTable: React.FC = ({ InputProps={{ disableUnderline: true, startAdornment: , - sx: { backgroundColor: '#F5F5F5', padding: '0 0.4rem' }, + sx: { + backgroundColor: '#F5F5F5', + padding: '0 0.4rem', + borderRadius: '4px', + }, }} value={searchText} onChange={handleSearchChange} @@ -482,7 +486,7 @@ const CustomTable: React.FC = ({ border: index % 2 === 0 ? 'none' : '1px solid #F5F5F5', // Apply border style conditionally for even rows }} - className={`px-4 py-4 first:rounded-l-md first:border-r-0 last:rounded-r-md last:border-l-0 ${ + className={`px-1 first:px-3 py-4 first:rounded-l-md first:border-r-0 last:rounded-r-md last:border-l-0 ${ columnIndex === 1 || columnIndex === 2 ? 'border-l-0 border-r-0' : '' // Add top and bottom border to even rows @@ -502,9 +506,9 @@ const CustomTable: React.FC = ({
{row.roles.length > 0 ? ( <> - {row.roles.slice(0, 4).map((role: IRoles) => ( + {row.roles.slice(0, 2).map((role: IRoles) => (
@@ -550,7 +554,7 @@ const CustomTable: React.FC = ({ backgroundColor: '#AAAAAA', }} > - +{row.roles.length - 4} + +{row.roles.length - 2}
)} @@ -572,15 +576,15 @@ const CustomTable: React.FC = ({ )}
) : column.id === 'activityComposition' ? ( -
+
{row.activityComposition && row.activityComposition.length > 0 ? ( <> {row.activityComposition - .slice(0, 4) + .slice(0, 2) .map((composition: string) => { const matchedOption = options.find( - (option) => option.value === composition + (option) => option.name === composition ); const backgroundColor = matchedOption ? matchedOption.color @@ -589,7 +593,7 @@ const CustomTable: React.FC = ({ return (
= ({ onClick={() => handleShowDetails(row)} > - +{row.activityComposition.length - 4} + +{row.activityComposition.length - 2}
)} diff --git a/src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.tsx b/src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.tsx index 435c9087..14f883b4 100644 --- a/src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.tsx +++ b/src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.tsx @@ -115,22 +115,21 @@ export default function ActiveMemberBreakdown() { handleUsernameChange={handleUsernameChange} isLoading={isActiveMembersBreakdownLoading} /> - {fetchedData?.totalResults > 0 && ( -
- -
- )} + +
+ +
- {fetchedData?.results.length > 0 ?? ( + {fetchedData && fetchedData?.totalResults > 10 ? (
toggleExpanded(!isExpanded)} />
+ ) : ( + '' )} ); diff --git a/src/store/slices/breakdownsSlice.ts b/src/store/slices/breakdownsSlice.ts index 51f6b1b4..51a3a988 100644 --- a/src/store/slices/breakdownsSlice.ts +++ b/src/store/slices/breakdownsSlice.ts @@ -45,7 +45,7 @@ const createBreakdownsSlice: StateCreator = (set, get) => ({ const url = `/member-activity/${guild_id}/active-members-composition-table?${params.toString()}`; - const { data } = await axiosInstance.post(url); + const { data } = await axiosInstance.get(url); set(() => ({ isActiveMembersBreakdownLoading: false })); return data; @@ -58,15 +58,12 @@ const createBreakdownsSlice: StateCreator = (set, get) => ({ try { set(() => ({ isRolesLoading: true })); - const { data } = await axiosInstance.get( - `/guilds/discord-api/${guild_id}/roles` - ); + const { data } = await axiosInstance.get(`/guilds/${guild_id}/roles`); set(() => ({ roles: data, isRolesLoading: false })); return data; } catch (error) { set(() => ({ isRolesLoading: false })); - // Handle the error } }, }); diff --git a/src/utils/interfaces.ts b/src/utils/interfaces.ts index 4fd6d965..053cf99d 100644 --- a/src/utils/interfaces.ts +++ b/src/utils/interfaces.ts @@ -24,7 +24,7 @@ export interface Row { } export interface IRoles { - id: string; + roleId: string; color: number | string; name: string; } From 60c1bed3c6cf5beb82c074f48529cb8b9210cd2e Mon Sep 17 00:00:00 2001 From: zuies Date: Fri, 30 Jun 2023 13:48:26 +0400 Subject: [PATCH 13/15] change some styles --- .../memberBreakdowns/CustomTable.tsx | 6 ++--- .../activeMembers/ActiveMemberBreakdown.tsx | 23 +++++++++---------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/components/pages/statistics/memberBreakdowns/CustomTable.tsx b/src/components/pages/statistics/memberBreakdowns/CustomTable.tsx index 7b3f2615..9aac6a4e 100644 --- a/src/components/pages/statistics/memberBreakdowns/CustomTable.tsx +++ b/src/components/pages/statistics/memberBreakdowns/CustomTable.tsx @@ -503,7 +503,7 @@ const CustomTable: React.FC = ({
) : column.id === 'roles' ? ( -
+
{row.roles.length > 0 ? ( <> {row.roles.slice(0, 2).map((role: IRoles) => ( @@ -593,10 +593,10 @@ const CustomTable: React.FC = ({ return (
-
+

Members breakdown

@@ -115,20 +115,19 @@ export default function ActiveMemberBreakdown() { handleUsernameChange={handleUsernameChange} isLoading={isActiveMembersBreakdownLoading} /> - -
- -
+
+ +
{fetchedData && fetchedData?.totalResults > 10 ? (
Date: Fri, 30 Jun 2023 17:00:00 +0400 Subject: [PATCH 14/15] add unit test for components --- .../CustomDialogDetail.spec.tsx | 85 +++++++++++++++++++ .../memberBreakdowns/CustomDialogDetail.tsx | 15 ++-- .../CustomPagination.spec.tsx | 53 ++++++++++++ .../memberBreakdowns/CustomPopover.tsx | 36 -------- .../memberBreakdowns/CustomTable.tsx | 42 +++++---- .../activeMembers/ActiveMemberBreakdown.tsx | 15 +++- src/utils/interfaces.ts | 6 ++ 7 files changed, 184 insertions(+), 68 deletions(-) create mode 100644 src/components/pages/statistics/memberBreakdowns/CustomDialogDetail.spec.tsx create mode 100644 src/components/pages/statistics/memberBreakdowns/CustomPagination.spec.tsx delete mode 100644 src/components/pages/statistics/memberBreakdowns/CustomPopover.tsx diff --git a/src/components/pages/statistics/memberBreakdowns/CustomDialogDetail.spec.tsx b/src/components/pages/statistics/memberBreakdowns/CustomDialogDetail.spec.tsx new file mode 100644 index 00000000..ef075803 --- /dev/null +++ b/src/components/pages/statistics/memberBreakdowns/CustomDialogDetail.spec.tsx @@ -0,0 +1,85 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import CustomDialogDetail from './CustomDialogDetail'; +import { activityCompositionOptions } from '../../../../utils/interfaces'; + +const mockRowDetail = { + discordId: '123', + avatar: 'avatar.png', + username: 'John Doe', + roles: [ + { id: 1, name: 'Role 1', color: '#ff0000' }, + { id: 2, name: 'Role 2', color: '#00ff00' }, + ], + activityComposition: ['Composition 1', 'Composition 2'], +}; + +const mockOptions: activityCompositionOptions[] = [ + { name: 'Option 1', value: 'option1', color: '#0000ff' }, + { name: 'Option 2', value: 'option2', color: '#ffff00' }, +]; + +test('renders the dialog with all states', () => { + // Render the dialog with open state + render( + + ); + + // Check if the close button is rendered + const closeButton = screen.getByTestId('close-modal-icon'); + expect(closeButton).toBeInTheDocument(); + + // Check if the user avatar is rendered + const avatar = screen.getByAltText('User Avatar'); + expect(avatar).toBeInTheDocument(); + + // Check if the username is rendered + const username = screen.getByText('John Doe'); + expect(username).toBeInTheDocument(); + + // Check if the roles are rendered + const roleElements = screen.getAllByTestId('role'); + expect(roleElements.length).toBe(2); + + // Check if the activity compositions are rendered + const compositionElements = screen.getAllByTestId('activity-composition'); + expect(compositionElements.length).toBe(2); + + // Render the dialog with closed state + render( + + ); + + // Check if the dialog is not rendered + const dialog = screen.queryByRole('MuiDialog-paper'); + expect(dialog).toBeNull(); +}); + +test('calls the onClose callback when the close button is clicked', () => { + const onClose = jest.fn(); + render( + + ); + + // Click the close button + const closeButton = screen.getByTestId('close-modal-icon'); + fireEvent.click(closeButton); + + // Check if the onClose callback is called + expect(onClose).toHaveBeenCalled(); +}); diff --git a/src/components/pages/statistics/memberBreakdowns/CustomDialogDetail.tsx b/src/components/pages/statistics/memberBreakdowns/CustomDialogDetail.tsx index c1ac95bd..45bd533d 100644 --- a/src/components/pages/statistics/memberBreakdowns/CustomDialogDetail.tsx +++ b/src/components/pages/statistics/memberBreakdowns/CustomDialogDetail.tsx @@ -3,26 +3,20 @@ import { Dialog, DialogProps } from '@mui/material'; import { IoClose } from 'react-icons/io5'; import { conf } from '../../../../configs'; import { Avatar } from '@mui/material'; +import { activityCompositionOptions } from '../../../../utils/interfaces'; interface CustomDialogDetailProps extends DialogProps { open: boolean; rowDetail: any; + options: activityCompositionOptions[]; onClose: () => void; } -const options = [ - { name: 'All active', value: 'all_active', color: '#3AAE2B' }, - { name: 'Newly active', value: 'all_new_active', color: '#FF9022' }, - { name: 'Consistently active', value: 'all_consistent', color: '#804EE1' }, - { name: 'Vital member', value: 'all_vital', color: '#313671' }, - { name: 'Became disengaged', value: 'all_new_disengaged', color: '#EB3E56' }, - { name: 'Others', value: 'others', color: '#AAAAAA' }, -]; - const CustomDialogDetail: React.FC = ({ open, rowDetail, onClose, + options, ...props }) => { const handleClose = () => { @@ -33,6 +27,7 @@ const CustomDialogDetail: React.FC = ({
= ({ key={role.id} className="flex flex-row flex-wrap" style={{ whiteSpace: 'nowrap' }} + data-testid="role" > = ({
{ + const mockTotalItems = 100; + const mockItemsPerPage = 10; + const mockCurrentPage = 1; + const mockOnChangePage = jest.fn(); + + it('renders the component', () => { + render( + + ); + + expect(screen.getByRole('navigation')).toBeInTheDocument(); + expect(screen.getByText('1')).toBeInTheDocument(); + expect(screen.getByText('2')).toBeInTheDocument(); + }); + + it('triggers onChangePage when a page is clicked', () => { + render( + + ); + + fireEvent.click(screen.getByText('2')); + expect(mockOnChangePage).toHaveBeenCalledWith(2); + }); + + it('does not trigger onChangePage when the current page is clicked', () => { + render( + + ); + + fireEvent.click(screen.getByText('1')); + expect(mockOnChangePage).toHaveBeenCalled(); + }); +}); diff --git a/src/components/pages/statistics/memberBreakdowns/CustomPopover.tsx b/src/components/pages/statistics/memberBreakdowns/CustomPopover.tsx deleted file mode 100644 index 0f930487..00000000 --- a/src/components/pages/statistics/memberBreakdowns/CustomPopover.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react'; -import { Popover, List, ListItem, ListItemText } from '@mui/material'; - -interface CustomPopoverProps { - open: boolean; - anchorEl: HTMLButtonElement | null; - onClose: () => void; - children: React.ReactNode; -} - -const CustomPopover: React.FC = ({ - open, - anchorEl, - onClose, - children, -}) => { - return ( - - {children} - - ); -}; - -export default CustomPopover; diff --git a/src/components/pages/statistics/memberBreakdowns/CustomTable.tsx b/src/components/pages/statistics/memberBreakdowns/CustomTable.tsx index 9aac6a4e..3f3cca8c 100644 --- a/src/components/pages/statistics/memberBreakdowns/CustomTable.tsx +++ b/src/components/pages/statistics/memberBreakdowns/CustomTable.tsx @@ -22,7 +22,12 @@ import { } from 'react-icons/md'; import moment from 'moment'; -import { Column, IRoles, Row } from '../../../../utils/interfaces'; +import { + Column, + IRoles, + Row, + activityCompositionOptions, +} from '../../../../utils/interfaces'; import { IUser } from '../../../../utils/types'; import { conf } from '../../../../configs'; import Loading from '../../../global/Loading'; @@ -34,21 +39,13 @@ interface CustomTableProps { data: Row[]; columns: Column[]; isLoading: boolean; + activityCompositionOptions: activityCompositionOptions[]; handleRoleSelectionChange: (selectedRoles: string[]) => void; handleActivityOptionSelectionChange: (selectedRoles: string[]) => void; handleJoinedAtChange: (joinedAt: string) => void; handleUsernameChange: (userName: string) => void; } -const options = [ - { name: 'All active', value: 'all_active', color: '#3AAE2B' }, - { name: 'Newly active', value: 'all_new_active', color: '#FF9022' }, - { name: 'Consistently active', value: 'all_consistent', color: '#804EE1' }, - { name: 'Vital member', value: 'all_vital', color: '#313671' }, - { name: 'Became disengaged', value: 'all_new_disengaged', color: '#EB3E56' }, - { name: 'Others', value: 'others', color: '#AAAAAA' }, -]; - const CustomTable: React.FC = ({ data, columns, @@ -57,6 +54,7 @@ const CustomTable: React.FC = ({ handleActivityOptionSelectionChange, handleJoinedAtChange, handleUsernameChange, + activityCompositionOptions, }) => { const { getRoles, roles } = useAppStore(); useEffect(() => { @@ -82,7 +80,7 @@ const CustomTable: React.FC = ({ const [selectAllRoles, setSelectAllRoles] = useState(true); const [selectedActivityOptions, setSelectedActivityOptions] = useState< string[] - >(options.map((option) => option.value)); + >(activityCompositionOptions.map((option) => option.value)); const [selectAllActivityOptions, setSelectAllActivityOptions] = useState(true); @@ -137,7 +135,9 @@ const CustomTable: React.FC = ({ event: React.ChangeEvent ) => { if (event.target.checked) { - setSelectedActivityOptions(options.map((option) => option.value)); + setSelectedActivityOptions( + activityCompositionOptions.map((option) => option.value) + ); } else { setSelectedActivityOptions([]); } @@ -154,7 +154,7 @@ const CustomTable: React.FC = ({ setSelectedActivityOptions(updatedSelectedOptions); setSelectAllActivityOptions( - updatedSelectedOptions.length === options.length + updatedSelectedOptions.length === activityCompositionOptions.length ); }; @@ -338,12 +338,8 @@ const CustomTable: React.FC = ({ } label={
All
} /> - {options.map( - (option: { - name: string; - value: string; - color: string; - }) => ( + {activityCompositionOptions.map( + (option: activityCompositionOptions) => ( = ({ {row.activityComposition .slice(0, 2) .map((composition: string) => { - const matchedOption = options.find( - (option) => option.name === composition - ); + const matchedOption = + activityCompositionOptions.find( + (option) => option.name === composition + ); const backgroundColor = matchedOption ? matchedOption.color : '#96A5A6'; @@ -696,6 +693,7 @@ const CustomTable: React.FC = ({ open={open} onClose={handleClose} rowDetail={rowDetail} + options={activityCompositionOptions} /> ); diff --git a/src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.tsx b/src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.tsx index f6f32791..df0e3a8f 100644 --- a/src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.tsx +++ b/src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.tsx @@ -3,7 +3,10 @@ import { StorageService } from '../../../../../services/StorageService'; import useAppStore from '../../../../../store/useStore'; import { IUser } from '../../../../../utils/types'; import CustomTable from '../CustomTable'; -import { Column } from '../../../../../utils/interfaces'; +import { + Column, + activityCompositionOptions, +} from '../../../../../utils/interfaces'; import CustomPagination from '../CustomPagination'; import CustomButton from '../../../../global/CustomButton'; import clsx from 'clsx'; @@ -15,6 +18,15 @@ const columns: Column[] = [ { id: 'joinedAt', label: 'DAO member since' }, ]; +const options: activityCompositionOptions[] = [ + { name: 'All active', value: 'all_active', color: '#3AAE2B' }, + { name: 'Newly active', value: 'all_new_active', color: '#FF9022' }, + { name: 'Consistently active', value: 'all_consistent', color: '#804EE1' }, + { name: 'Vital member', value: 'all_vital', color: '#313671' }, + { name: 'Became disengaged', value: 'all_new_disengaged', color: '#EB3E56' }, + { name: 'Others', value: 'others', color: '#AAAAAA' }, +]; + export default function ActiveMemberBreakdown() { const { getActiveMemberCompositionTable, isActiveMembersBreakdownLoading } = useAppStore(); @@ -114,6 +126,7 @@ export default function ActiveMemberBreakdown() { handleJoinedAtChange={handleJoinedAtChange} handleUsernameChange={handleUsernameChange} isLoading={isActiveMembersBreakdownLoading} + activityCompositionOptions={options} />
diff --git a/src/utils/interfaces.ts b/src/utils/interfaces.ts index 053cf99d..58b9d6aab 100644 --- a/src/utils/interfaces.ts +++ b/src/utils/interfaces.ts @@ -28,3 +28,9 @@ export interface IRoles { color: number | string; name: string; } + +export interface activityCompositionOptions { + name: string; + value: string; + color: string; +} From 16c63880542f7491cac0cacde59b9118c7c71587 Mon Sep 17 00:00:00 2001 From: zuies Date: Fri, 30 Jun 2023 17:11:03 +0400 Subject: [PATCH 15/15] add unit test for main component --- .../activeMembers/ActiveMemberBreakdown.spec.tsx | 12 ++++++++++++ .../activeMembers/ActiveMemberBreakdown.tsx | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.spec.tsx diff --git a/src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.spec.tsx b/src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.spec.tsx new file mode 100644 index 00000000..1d21b973 --- /dev/null +++ b/src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.spec.tsx @@ -0,0 +1,12 @@ +import { render, screen } from '@testing-library/react'; +import ActiveMemberBreakdown from './ActiveMemberBreakdown'; + +describe('ActiveMemberBreakdown', () => { + it('renders the component', () => { + render(); + + // Assert the component is rendered + const component = screen.getByText('Members breakdown'); + expect(component).toBeInTheDocument(); + }); +}); diff --git a/src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.tsx b/src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.tsx index df0e3a8f..934fade2 100644 --- a/src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.tsx +++ b/src/components/pages/statistics/memberBreakdowns/activeMembers/ActiveMemberBreakdown.tsx @@ -131,7 +131,7 @@ export default function ActiveMemberBreakdown() {
-
+