Skip to content

Commit

Permalink
add email campaign statistics
Browse files Browse the repository at this point in the history
  • Loading branch information
raphaelblum committed Mar 19, 2024
1 parent ff67dec commit 7d55041
Show file tree
Hide file tree
Showing 5 changed files with 281 additions and 1 deletion.
7 changes: 6 additions & 1 deletion packages/admin/src/emailCampaigns/EmailCampaignsGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
useDataGridRemote,
usePersistentColumnState,
} from "@comet/admin";
import { Add as AddIcon, Edit } from "@comet/admin-icons";
import { Add as AddIcon, Edit, Statistics } from "@comet/admin-icons";
import { BlockInterface } from "@comet/blocks-admin";
import { ContentScopeInterface } from "@comet/cms-admin";
import { Button, IconButton } from "@mui/material";
Expand Down Expand Up @@ -202,6 +202,11 @@ export function EmailCampaignsGrid({
}
refetchQueries={[emailCampaignsQuery]}
/>
{row.sendingState === "SENT" && (
<IconButton component={StackLink} pageName="statistics" payload={row.id}>
<Statistics color="primary" />
</IconButton>
)}
</>
);
},
Expand Down
2 changes: 2 additions & 0 deletions packages/admin/src/emailCampaigns/EmailCampaignsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useIntl } from "react-intl";

import { EmailCampaignsGrid } from "./EmailCampaignsGrid";
import { EmailCampaignForm } from "./form/EmailCampaignForm";
import { EmailCampaignStatistics } from "./statistics/EmailCampaignStatistics";

interface CreateEmailCampaignsPageOptions {
scopeParts: string[];
Expand All @@ -29,6 +30,7 @@ export function createEmailCampaignsPage({ scopeParts, EmailCampaignContentBlock
<StackPage name="grid">
<EmailCampaignsGrid scope={scope} EmailCampaignContentBlock={EmailCampaignContentBlock} />
</StackPage>
<StackPage name="statistics">{(selectedId) => <EmailCampaignStatistics id={selectedId} />}</StackPage>
<StackPage
name="edit"
title={intl.formatMessage({ id: "cometBrevoModule.emailCampaigns.editEmailCampaign", defaultMessage: "Edit email campaign" })}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { gql } from "@apollo/client";

export const emailCampaignStatistics = gql`
query EmailCampaignStatistics($id: ID!) {
emailCampaignStatistics(id: $id) {
uniqueClicks
unsubscriptions
delivered
sent
softBounces
hardBounces
uniqueViews
}
}
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { useQuery } from "@apollo/client";
import { MainContent, Toolbar, ToolbarFillSpace, ToolbarItem, useStackApi } from "@comet/admin";
import { Add as AddIcon, ArrowLeft } from "@comet/admin-icons";
import { useContentScope, useContentScopeConfig } from "@comet/cms-admin";
import { Button, Grid, IconButton } from "@mui/material";
import React from "react";
import { FormattedMessage } from "react-intl";

import { emailCampaignStatistics } from "./EmailCampaignStatistics.gql";
import { GQLEmailCampaignStatisticsQuery, GQLEmailCampaignStatisticsQueryVariables } from "./EmailCampaignStatistics.gql.generated";
import { PercentageCard } from "./PercentageCard";

interface Props {
id: string;
}

export const EmailCampaignStatistics = ({ id }: Props): React.ReactElement => {
const { scope } = useContentScope();
const stackApi = useStackApi();
useContentScopeConfig({ redirectPathAfterChange: "/newsletter/dashboard" });

const { data: campaignStatistics } = useQuery<GQLEmailCampaignStatisticsQuery, GQLEmailCampaignStatisticsQueryVariables>(
emailCampaignStatistics,
{
variables: { id },
fetchPolicy: "network-only",
},
);

return (
<>
<Toolbar>
<ToolbarItem>
<IconButton onClick={stackApi?.goBack} size="large">
<ArrowLeft />
</IconButton>
</ToolbarItem>
<ToolbarFillSpace />
<ToolbarItem>
<Button
href={`/${scope.domain}/${scope.company}/newsletter/campaigns/add/add`}
variant="contained"
startIcon={<AddIcon />}
color="primary"
component="a"
>
<FormattedMessage id="newsletter.addNewCampaign" defaultMessage="Add new campaign" />
</Button>
</ToolbarItem>
</Toolbar>
<MainContent>
<Grid container spacing={4}>
<Grid item xs={12} md={6}>
<PercentageCard
title={<FormattedMessage id="newsletter.dashboard.overallDelivery" defaultMessage="Overall delivery" />}
currentNumber={campaignStatistics?.emailCampaignStatistics?.delivered}
targetNumber={campaignStatistics?.emailCampaignStatistics?.sent}
/>
</Grid>
<Grid item xs={12} md={6}>
<PercentageCard
title={<FormattedMessage id="newsletter.dashboard.overallFailedDelivery" defaultMessage="Overall failed delivery" />}
currentNumber={
campaignStatistics?.emailCampaignStatistics
? campaignStatistics.emailCampaignStatistics?.sent - campaignStatistics.emailCampaignStatistics?.delivered
: undefined
}
targetNumber={campaignStatistics?.emailCampaignStatistics?.sent}
/>
</Grid>
<Grid item xs={12} sm={6} lg={3}>
<PercentageCard
title={<FormattedMessage id="newsletter.dashboard.overallClicked" defaultMessage="Overall clicked" />}
variant="circle"
currentNumber={campaignStatistics?.emailCampaignStatistics?.uniqueViews}
targetNumber={campaignStatistics?.emailCampaignStatistics?.sent}
/>
</Grid>
<Grid item xs={12} sm={6} lg={3}>
<PercentageCard
title={<FormattedMessage id="newsletter.dashboard.overallClicked" defaultMessage="Overall clicked" />}
variant="circle"
currentNumber={campaignStatistics?.emailCampaignStatistics?.uniqueClicks}
targetNumber={campaignStatistics?.emailCampaignStatistics?.sent}
/>
</Grid>
<Grid item xs={12} sm={6} lg={3}>
<PercentageCard
title={<FormattedMessage id="newsletter.dashboard.overallBounce" defaultMessage="Overall bounce" />}
currentNumber={
campaignStatistics?.emailCampaignStatistics
? campaignStatistics.emailCampaignStatistics.softBounces + campaignStatistics.emailCampaignStatistics.hardBounces
: undefined
}
targetNumber={campaignStatistics?.emailCampaignStatistics?.sent}
variant="circle"
/>
</Grid>
<Grid item xs={12} sm={6} lg={3}>
<PercentageCard
title={<FormattedMessage id="newsletter.dashboard.unsubscriptions" defaultMessage="Unsubscriptions" />}
currentNumber={campaignStatistics?.emailCampaignStatistics?.unsubscriptions}
targetNumber={campaignStatistics?.emailCampaignStatistics?.sent}
variant="circle"
/>
</Grid>
</Grid>
</MainContent>
</>
);
};
147 changes: 147 additions & 0 deletions packages/admin/src/emailCampaigns/statistics/PercentageCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { Paper, Skeleton as MuiSkeleton, Typography } from "@mui/material";
import { styled } from "@mui/material/styles";
import * as React from "react";
import { FormattedNumber } from "react-intl";

interface Props {
title: React.ReactNode;
currentNumber?: number;
targetNumber?: number;
variant?: "normal" | "circle";
}

export const PercentageCard: React.FC<Props> = ({ title, currentNumber, targetNumber, variant = "normal" }) => {
const percentage = currentNumber === undefined || targetNumber === undefined || targetNumber <= 0 ? null : (currentNumber / targetNumber) * 100;
const renderSkeleton = currentNumber === undefined || targetNumber === undefined;

const values = (
<>
<Typography variant="h1">
{renderSkeleton ? (
<Skeleton variant="text" width={80} height={64} />
) : (
<>
{percentage === null ? (
"–"
) : (
<>
<FormattedNumber value={percentage} style="decimal" minimumSignificantDigits={3} maximumSignificantDigits={3} />%
</>
)}
</>
)}
</Typography>
<Typography variant="body2">
{renderSkeleton ? (
<Skeleton variant="text" width={100} height={20} />
) : (
<>
<FormattedNumber value={currentNumber} /> / <FormattedNumber value={targetNumber} />
</>
)}
</Typography>
</>
);

return (
<Paper>
<Content>
<Typography variant="h6" mb={2}>
{title}
</Typography>
{variant === "normal" && values}
{variant === "circle" && (
<CircleValueContainer>
<CircleContainer>
<Circle percentage={percentage ? Math.min(percentage, 100) : 0} />
</CircleContainer>
<CircleValue>{values}</CircleValue>
</CircleValueContainer>
)}
</Content>
</Paper>
);
};

const Content = styled("div")`
padding: 60px 30px;
text-align: center;
`;

const CircleValueContainer = styled("div")`
position: relative;
max-width: 250px;
margin-left: auto;
margin-right: auto;
`;

const CircleContainer = styled("div")`
position: relative;
padding-bottom: 100%;
`;

interface CircleProps {
percentage: number;
}

const Circle = styled("div")<CircleProps>`
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1;
display: flex;
border: 5px solid ${({ theme }) => theme.palette.grey[100]};
border-radius: 50%;
:before {
content: "";
display: block;
position: absolute;
z-index: 2;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: white;
border-radius: 50%;
}
:after {
content: "";
display: block;
position: absolute;
z-index: 1;
top: -5px;
right: -5px;
bottom: -5px;
left: -5px;
border-radius: 50%;
background: ${({ theme, percentage }) => {
if (!percentage) {
return theme.palette.grey[100];
}
const rotation = (18 / 5) * percentage - 90;
return `
linear-gradient(${rotation}deg, ${theme.palette.grey[100]} 50%, transparent 0) 0 / min(100%, (50 - ${percentage}) * 100%),
linear-gradient(${rotation}deg, transparent 50%, ${theme.palette.primary.main} 0) 0 / min(100%, (${percentage} - 50) * 100%),
linear-gradient(to right, ${theme.palette.grey[100]} 50%, ${theme.palette.primary.main} 0)`;
}};
}
`;

const CircleValue = styled("div")`
position: absolute;
z-index: 2;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
`;

const Skeleton = styled(MuiSkeleton)`
margin-left: auto;
margin-right: auto;
`;

0 comments on commit 7d55041

Please sign in to comment.