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

Add email campaign statistics #39

Merged
merged 4 commits into from
Mar 19, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
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>
)}
jamesricky marked this conversation as resolved.
Show resolved Hide resolved
</>
);
},
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,108 @@
import { useQuery } from "@apollo/client";
import { MainContent, StackLink, Toolbar, ToolbarActions, ToolbarBackButton, ToolbarFillSpace } from "@comet/admin";
import { Add as AddIcon } from "@comet/admin-icons";
import { useContentScopeConfig } from "@comet/cms-admin";
import { Button, Grid } 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 => {
useContentScopeConfig({ redirectPathAfterChange: "/newsletter/email-campaigns" });

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

return (
<>
<Toolbar>
<ToolbarBackButton />
<ToolbarFillSpace />
<ToolbarActions>
<Button startIcon={<AddIcon />} component={StackLink} pageName="add" payload="add" variant="contained" color="primary">
<FormattedMessage id="cometBrevoModule.emailCampaign.newEmailCampaign" defaultMessage="New email campaign" />
</Button>
</ToolbarActions>
</Toolbar>
<MainContent>
<Grid container spacing={4}>
<Grid item xs={12} md={6}>
<PercentageCard
title={
<FormattedMessage id="cometBrevoModule.emailCampaignStatistics.overallDelivery" defaultMessage="Overall delivery" />
}
currentNumber={campaignStatistics?.emailCampaignStatistics?.delivered}
targetNumber={campaignStatistics?.emailCampaignStatistics?.sent}
/>
</Grid>
<Grid item xs={12} md={6}>
<PercentageCard
title={
<FormattedMessage
id="cometBrevoModule.emailCampaignStatistics.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="cometBrevoModule.emailCampaignStatistics.uniqueViews" defaultMessage="Unique views" />}
variant="circle"
currentNumber={campaignStatistics?.emailCampaignStatistics?.uniqueViews}
targetNumber={campaignStatistics?.emailCampaignStatistics?.sent}
/>
</Grid>
<Grid item xs={12} sm={6} lg={3}>
<PercentageCard
title={<FormattedMessage id="cometBrevoModule.emailCampaignStatistics.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="cometBrevoModule.emailCampaignStatistics.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="cometBrevoModule.emailCampaignStatistics.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} />%
</>
jamesricky marked this conversation as resolved.
Show resolved Hide resolved
)}
</>
)}
</Typography>
<Typography variant="body2">
{renderSkeleton ? (
<Skeleton variant="text" width={100} height={20} />
) : (
<>
<FormattedNumber value={currentNumber} /> / <FormattedNumber value={targetNumber} />
</>
jamesricky marked this conversation as resolved.
Show resolved Hide resolved
)}
</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;
`;
Loading