diff --git a/frontend/src/component/executiveDashboard/ExecutiveDashboard.tsx b/frontend/src/component/executiveDashboard/ExecutiveDashboard.tsx index 398e11b7ea..0b6a19f1d1 100644 --- a/frontend/src/component/executiveDashboard/ExecutiveDashboard.tsx +++ b/frontend/src/component/executiveDashboard/ExecutiveDashboard.tsx @@ -3,6 +3,7 @@ import { PageHeader } from 'component/common/PageHeader/PageHeader'; import { VFC } from 'react'; import { UsersChart } from './UsersChart/UsersChart'; import { FlagsChart } from './FlagsChart/FlagsChart'; +import { UserStats } from './UserStats/UserStats'; const StyledGrid = styled(Box)(({ theme }) => ({ display: 'grid', @@ -26,7 +27,7 @@ export const ExecutiveDashboard: VFC = () => { {/* Dashboard */} - Stats + diff --git a/frontend/src/component/executiveDashboard/UserStats/UserStats.tsx b/frontend/src/component/executiveDashboard/UserStats/UserStats.tsx new file mode 100644 index 0000000000..f959996e83 --- /dev/null +++ b/frontend/src/component/executiveDashboard/UserStats/UserStats.tsx @@ -0,0 +1,223 @@ +import { ChevronRight } from '@mui/icons-material'; +import { Box, Typography, styled } from '@mui/material'; +import { Link } from 'react-router-dom'; + +const StyledContent = styled(Box)(({ theme }) => ({ + borderRadius: `${theme.shape.borderRadiusLarge}px`, + backgroundColor: theme.palette.background.paper, + maxWidth: 300, + padding: theme.spacing(3), +})); + +const StyledUserContainer = styled(Box)(({ theme }) => ({ + position: 'relative', +})); + +const StyledUserBox = styled(Box)(({ theme }) => ({ + borderRadius: `${theme.shape.borderRadiusExtraLarge}px`, + backgroundColor: theme.palette.primary.main, + maxWidth: 300, + padding: theme.spacing(2), + marginBottom: theme.spacing(3), + position: 'relative', + zIndex: 2, +})); + +const StyledCustomShadow = styled(Box)(({ theme }) => ({ + width: '220px', + height: '54px', + backgroundColor: 'rgba(108, 101, 229, 0.30)', + position: 'absolute', + margin: '0 auto', + top: '45px', + left: '15px', + borderRadius: `${theme.shape.borderRadiusExtraLarge}px`, + zIndex: 1, +})); + +const StyledUserDistributionContainer = styled(Box)(({ theme }) => ({ + marginBottom: theme.spacing(2.5), +})); + +const StyledUserCount = styled(Typography)(({ theme }) => ({ + color: theme.palette.primary.contrastText, + fontWeight: 'bold', + fontSize: theme.fontSizes.extraLargeHeader, + margin: 0, + padding: 0, +})); + +const StyledHeader = styled(Typography)(({ theme }) => ({ + marginBottom: theme.spacing(3), + fontSize: theme.fontSizes.bodySize, + fontWeight: 'bold', +})); + +const StyledDistInfoContainer = styled(Box)({ + display: 'flex', + flexDirection: 'column', + gap: '12px', +}); + +const StyledLinkContainer = styled(Box)(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + justifyContent: 'flex-end', + marginTop: theme.spacing(3), +})); + +const StyledLink = styled(Link)({ + fontWeight: 'bold', + textDecoration: 'none', + display: 'flex', + justifyContent: 'center', +}); + +export const UserStats = () => { + return ( + <> + + + Total users + + + 9999 + + + + + + + + + + + + + + + + View users + + + + + + ); +}; + +type UserType = 'active' | 'inactive'; + +interface StyledLinearProgressProps { + type: UserType; +} + +const StyledUserDistributionLine = styled(Box)( + ({ theme, type }) => ({ + borderRadius: theme.shape.borderRadius, + height: 16, + backgroundColor: + type === 'active' + ? theme.palette.success.border + : theme.palette.warning.border, + }), +); + +const UserDistribution = () => { + const getLineWidth = () => { + return [80, 20]; + }; + + const [activeWidth, inactiveWidth] = getLineWidth(); + + return ( + + + ({ + width: `${inactiveWidth}%`, + marginLeft: theme.spacing(0.5), + })} + /> + + ); +}; + +const StyledUserDistContainer = styled(Box)(({ theme }) => ({ + padding: `${theme.spacing(1.5)} ${theme.spacing(2)}`, + borderRadius: `${theme.shape.borderRadius}px`, + border: `1px solid ${theme.palette.divider}`, +})); + +const StyledUserDistIndicator = styled(Box)( + ({ theme, type }) => ({ + width: 8, + height: 8, + backgroundColor: + type === 'active' + ? theme.palette.success.border + : theme.palette.warning.border, + borderRadius: `2px`, + marginRight: theme.spacing(1), + marginTop: theme.spacing(0.8), + }), +); + +interface IUserDistributionInfoProps { + type: UserType; + count: string; + percentage: string; +} + +const StyledDistInfoInnerContainer = styled(Box)(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + width: '100%', +})); + +const StyledDistInfoTextContainer = styled(Box)(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', +})); + +const StyledCountTypography = styled(Typography)(({ theme }) => ({ + marginLeft: 'auto', + fontWeight: 'normal', +})); + +const UserDistributionInfo: React.FC = ({ + type, + count, + percentage, +}) => { + return ( + + + + + + + {type === 'active' ? 'Active' : 'Inactive'} users + + {percentage}% + + + {count} + + + + + ); +}; diff --git a/frontend/src/themes/dark-theme.ts b/frontend/src/themes/dark-theme.ts index e6535b2f3a..fd02437cee 100644 --- a/frontend/src/themes/dark-theme.ts +++ b/frontend/src/themes/dark-theme.ts @@ -56,6 +56,7 @@ const theme = { }, }, fontSizes: { + extraLargeHeader: '2.5rem', largeHeader: '2rem', mainHeader: '1.25rem', bodySize: '1rem', diff --git a/frontend/src/themes/theme.ts b/frontend/src/themes/theme.ts index 16a8df5858..c4a35df0c2 100644 --- a/frontend/src/themes/theme.ts +++ b/frontend/src/themes/theme.ts @@ -48,6 +48,7 @@ export const theme = { }, }, fontSizes: { + extraLargeHeader: '2.5rem', largeHeader: '2rem', mainHeader: '1.25rem', bodySize: '1rem', diff --git a/frontend/src/themes/themeTypes.ts b/frontend/src/themes/themeTypes.ts index 4d9ffd2456..db7412ea2c 100644 --- a/frontend/src/themes/themeTypes.ts +++ b/frontend/src/themes/themeTypes.ts @@ -7,6 +7,7 @@ declare module '@mui/material/styles' { * @deprecated */ fontSizes: { + extraLargeHeader: string; largeHeader: string; mainHeader: string; bodySize: string; @@ -121,13 +122,16 @@ declare module '@mui/material/styles' { **/ variants: string[]; } - + // biome-ignore lint/suspicious/noEmptyInterface: We need this to keep types from breaking interface Theme extends CustomTheme {} + // biome-ignore lint/suspicious/noEmptyInterface: We need this to keep types from breaking interface ThemeOptions extends CustomTheme {} + // biome-ignore lint/suspicious/noEmptyInterface: We need this to keep types from breaking interface Palette extends CustomPalette {} + // biome-ignore lint/suspicious/noEmptyInterface: We need this to keep types from breaking interface PaletteOptions extends CustomPalette {} - + // biome-ignore lint/suspicious/noEmptyInterface: We need this to keep types from breaking interface TypeBackground extends CustomTypeBackground {} /* Extend the background object from MUI */