From 7d550418931f18fe68b62f864cfe59c34520abac Mon Sep 17 00:00:00 2001 From: Raphael Blum Date: Tue, 19 Mar 2024 10:22:37 +0100 Subject: [PATCH] add email campaign statistics --- .../src/emailCampaigns/EmailCampaignsGrid.tsx | 7 +- .../src/emailCampaigns/EmailCampaignsPage.tsx | 2 + .../statistics/EmailCampaignStatistics.gql.ts | 15 ++ .../statistics/EmailCampaignStatistics.tsx | 111 +++++++++++++ .../statistics/PercentageCard.tsx | 147 ++++++++++++++++++ 5 files changed, 281 insertions(+), 1 deletion(-) create mode 100644 packages/admin/src/emailCampaigns/statistics/EmailCampaignStatistics.gql.ts create mode 100644 packages/admin/src/emailCampaigns/statistics/EmailCampaignStatistics.tsx create mode 100644 packages/admin/src/emailCampaigns/statistics/PercentageCard.tsx diff --git a/packages/admin/src/emailCampaigns/EmailCampaignsGrid.tsx b/packages/admin/src/emailCampaigns/EmailCampaignsGrid.tsx index 46fa6d3c..2172fb40 100644 --- a/packages/admin/src/emailCampaigns/EmailCampaignsGrid.tsx +++ b/packages/admin/src/emailCampaigns/EmailCampaignsGrid.tsx @@ -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"; @@ -202,6 +202,11 @@ export function EmailCampaignsGrid({ } refetchQueries={[emailCampaignsQuery]} /> + {row.sendingState === "SENT" && ( + + + + )} ); }, diff --git a/packages/admin/src/emailCampaigns/EmailCampaignsPage.tsx b/packages/admin/src/emailCampaigns/EmailCampaignsPage.tsx index e31efdaf..f2779f9a 100644 --- a/packages/admin/src/emailCampaigns/EmailCampaignsPage.tsx +++ b/packages/admin/src/emailCampaigns/EmailCampaignsPage.tsx @@ -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[]; @@ -29,6 +30,7 @@ export function createEmailCampaignsPage({ scopeParts, EmailCampaignContentBlock + {(selectedId) => } { + const { scope } = useContentScope(); + const stackApi = useStackApi(); + useContentScopeConfig({ redirectPathAfterChange: "/newsletter/dashboard" }); + + const { data: campaignStatistics } = useQuery( + emailCampaignStatistics, + { + variables: { id }, + fetchPolicy: "network-only", + }, + ); + + return ( + <> + + + + + + + + + + + + + + + } + currentNumber={campaignStatistics?.emailCampaignStatistics?.delivered} + targetNumber={campaignStatistics?.emailCampaignStatistics?.sent} + /> + + + } + currentNumber={ + campaignStatistics?.emailCampaignStatistics + ? campaignStatistics.emailCampaignStatistics?.sent - campaignStatistics.emailCampaignStatistics?.delivered + : undefined + } + targetNumber={campaignStatistics?.emailCampaignStatistics?.sent} + /> + + + } + variant="circle" + currentNumber={campaignStatistics?.emailCampaignStatistics?.uniqueViews} + targetNumber={campaignStatistics?.emailCampaignStatistics?.sent} + /> + + + } + variant="circle" + currentNumber={campaignStatistics?.emailCampaignStatistics?.uniqueClicks} + targetNumber={campaignStatistics?.emailCampaignStatistics?.sent} + /> + + + } + currentNumber={ + campaignStatistics?.emailCampaignStatistics + ? campaignStatistics.emailCampaignStatistics.softBounces + campaignStatistics.emailCampaignStatistics.hardBounces + : undefined + } + targetNumber={campaignStatistics?.emailCampaignStatistics?.sent} + variant="circle" + /> + + + } + currentNumber={campaignStatistics?.emailCampaignStatistics?.unsubscriptions} + targetNumber={campaignStatistics?.emailCampaignStatistics?.sent} + variant="circle" + /> + + + + + ); +}; diff --git a/packages/admin/src/emailCampaigns/statistics/PercentageCard.tsx b/packages/admin/src/emailCampaigns/statistics/PercentageCard.tsx new file mode 100644 index 00000000..4a30fafc --- /dev/null +++ b/packages/admin/src/emailCampaigns/statistics/PercentageCard.tsx @@ -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 = ({ 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 = ( + <> + + {renderSkeleton ? ( + + ) : ( + <> + {percentage === null ? ( + "–" + ) : ( + <> + % + + )} + + )} + + + {renderSkeleton ? ( + + ) : ( + <> + / + + )} + + + ); + + return ( + + + + {title} + + {variant === "normal" && values} + {variant === "circle" && ( + + + + + {values} + + )} + + + ); +}; + +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")` + 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; +`;