Skip to content

Commit

Permalink
feat(admin): Allow for admins to send mass newsletter consent emails (#…
Browse files Browse the repository at this point in the history
…1857)

* feat(admin): Add marketing sections
Will be used to send and manage marketing emails

* feat: Add Form to send newsletter consent email

* ui: Form improvements

* fix: Build issues

* feat: Add email subject to form
Will be used if subject is set dynamically to sendgrid
  • Loading branch information
sashko9807 authored Jun 23, 2024
1 parent f3a5aea commit b1b2036
Show file tree
Hide file tree
Showing 14 changed files with 290 additions and 2 deletions.
11 changes: 11 additions & 0 deletions public/locales/bg/marketing.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"admin": {
"marketing": "Маркетинг",
"sendConsentEmail": "Изпращане на емайл за съгласие",
"common": {
"templateId": "Идентификатор на Sendgrid шаблон",
"listId": "Идентифицатор на Sendgrid списък с контакти",
"subject": "Тема на емайл"
}
}
}
11 changes: 11 additions & 0 deletions public/locales/en/marketing.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"admin": {
"marketing": "Marketing",
"sendConsentEmail": "Send newsletter consent email",
"common": {
"templateId": "ID of Sendgrid template",
"listId": "ID of Sendgrid contact list",
"subject": "Email subject"
}
}
}
4 changes: 4 additions & 0 deletions src/common/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,10 @@ export const routes = {
company: {
create: '/admin/companies/create',
},
marketing: {
index: '/admin/marketing/',
newsLetterConsent: '/admin/marketing/newsletter-consent',
},
},
dev: {
openData: '/open-data',
Expand Down
6 changes: 4 additions & 2 deletions src/components/admin/cities/CreateForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Box, Button, Grid, Typography } from '@mui/material'

import { CityFormData, CityInput, CityResponse } from 'gql/cities'
import { routes } from 'common/routes'
import { ApiErrors, handleUniqueViolation } from 'service/apiErrors'
import { ApiErrors, Message, handleUniqueViolation } from 'service/apiErrors'
import { useCreateCity } from 'service/city'
import { AlertStore } from 'stores/AlertStore'
import GenericForm from 'components/common/form/GenericForm'
Expand Down Expand Up @@ -42,7 +42,9 @@ export default function EditForm() {
const error = e.response

if (error?.status === 409) {
const message = error.data.message.map((el) => handleUniqueViolation(el.constraints, t))
const message = (error.data.message as Message[]).map((el) =>
handleUniqueViolation(el.constraints, t),
)
return AlertStore.show(message.join('/n'), 'error')
}

Expand Down
117 changes: 117 additions & 0 deletions src/components/admin/marketing/EmailConsent/SendEmailConsentForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { Button, Grid, Typography } from '@mui/material'
import { useMutation } from '@tanstack/react-query'
import { AxiosError, AxiosResponse } from 'axios'
import { routes } from 'common/routes'
import FormDatePicker from 'components/common/form/FormDatePicker'
import FormTextField from 'components/common/form/FormTextField'
import GenericForm from 'components/common/form/GenericForm'
import SubmitButton from 'components/common/form/SubmitButton'
import { FormikHelpers } from 'formik'
import { NewsLetterConsentResponse, SendNewsLetterConsent } from 'gql/marketing'
import { useTranslation } from 'next-i18next'
import Link from 'next/link'
import { ApiError } from 'service/apiErrors'
import { useSendConsentEmail } from 'service/marketing'
import { AlertStore } from 'stores/AlertStore'
import * as yup from 'yup'

export default function SendConsentEmailForm() {
const { t } = useTranslation('marketing')

const initialValues: SendNewsLetterConsent = {
templateId: '',
listId: '',
subject: '',
dateThreshold: new Date().toISOString(),
}

const validationSchema: yup.SchemaOf<SendNewsLetterConsent> = yup.object().defined().shape({
templateId: yup.string().required(),
listId: yup.string().required(),
subject: yup.string().required(),
dateThreshold: yup.string().optional(),
})

const mutationFn = useSendConsentEmail()

const handleError = (e: AxiosError<ApiError>) => {
const error = e.response as AxiosResponse<ApiError>
AlertStore.show(error.data.message, 'error')
}

const mutation = useMutation<
AxiosResponse<NewsLetterConsentResponse>,
AxiosError<ApiError>,
SendNewsLetterConsent
>({
mutationFn,
onError: (error) => handleError(error),
onSuccess: (data) => {
const response = data.data
AlertStore.show(
t(`Съобщението беше изпратен успешно на ${response.contactCount} емайла.`),
'success',
)
},
})

async function onSubmit(
values: SendNewsLetterConsent,
formikHelpers: FormikHelpers<SendNewsLetterConsent>,
) {
const data: SendNewsLetterConsent = {
templateId: values.templateId,
listId: values.listId,
subject: values.subject,
dateThreshold: values.dateThreshold,
}
await mutation.mutateAsync(data)
if (mutation.isSuccess && !mutation.isLoading) {
formikHelpers.resetForm({ values: initialValues })
}
}

return (
<Grid container gap={2}>
<Grid item>
<Typography variant="h5" component="h2">
{t('admin.sendConsentEmail')}
</Typography>
</Grid>
<GenericForm
onSubmit={onSubmit}
initialValues={initialValues}
validationSchema={validationSchema}>
<Grid container item spacing={3} xs={12}>
<Grid item xs={12}>
<FormTextField type="text" label={t('admin.common.templateId')} name="templateId" />
</Grid>
<Grid item xs={12}>
<FormTextField type="text" label={t('admin.common.listId')} name="listId" />
</Grid>
<Grid item xs={12}>
<FormTextField type="text" label={t('admin.common.subject')} name="subject" />
</Grid>
<Grid
container
item
xs={12}
direction={'row'}
justifyContent={'space-between'}
alignItems={'center'}>
<Grid item xs={12} md={6}>
<Typography>Премахване от списък на потребители регистрирани след: </Typography>
</Grid>
<FormDatePicker name="dateThreshold" label="" />
</Grid>
<Grid item xs={12}>
<SubmitButton label="Изпрати" fullWidth loading={mutation.isLoading} />
<Link href={routes.admin.marketing.index} passHref>
<Button fullWidth>{t('Откажи')}</Button>
</Link>
</Grid>
</Grid>
</GenericForm>
</Grid>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import AdminContainer from 'components/common/navigation/AdminContainer'
import AdminLayout from 'components/common/navigation/AdminLayout'
import React from 'react'
import SendEmailConsentForm from './SendEmailConsentForm'
import { useTranslation } from 'next-i18next'
import { Container } from '@mui/material'

export default function SendEmailConsentPage() {
const { t } = useTranslation('marketing')
return (
<AdminLayout>
<AdminContainer title={t('admin.sendConsentEmail')}>
<Container maxWidth={'sm'} sx={{ py: 5 }}>
<SendEmailConsentForm />
</Container>
</AdminContainer>
</AdminLayout>
)
}
58 changes: 58 additions & 0 deletions src/components/admin/marketing/MarketingPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Box, Button, CardContent, Container, Grid, Typography } from '@mui/material'
import AdminContainer from 'components/common/navigation/AdminContainer'
import AdminLayout from 'components/common/navigation/AdminLayout'
import React from 'react'
import { useTranslation } from 'next-i18next'
import Link from 'next/link'
import { marketingCards } from './navigation/marketingCards'

const colors = ['#0179a8', '#346cb0', '#5f4b8b', '#b76ba3', '#a7c796', '#00a28a', '#3686a0']
export default function MarketingPage() {
const { t } = useTranslation('marketing')
return (
<AdminLayout>
<AdminContainer title={t('admin.marketing')}>
<Container maxWidth={false} sx={{ py: 5 }}>
<Grid container spacing={2} rowSpacing={4} px={4} pb={4} mb={2}>
{marketingCards.map(({ label, href, icon: Icon, disabled }, index) => (
<Grid xs={12} sm={6} md={4} lg={2.4} item key={index}>
<Button
disabled={disabled}
sx={{
height: 130,
maxWidth: 345,
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
borderRadius: 2,
boxShadow: '0px 2px 4px ' + `${colors[index % colors.length]}9A`,
color: colors[index % colors.length],
transition: 'transform 0.3s ease',
'&:hover': {
transform: 'translateY(-5px)',
boxShadow: '0px 4px 8px ' + `${colors[index % colors.length]}9A`,
backgroundColor: `${colors[index % colors.length]}1A`,
border: '1px solid ' + `${colors[index % colors.length]}9A`,
},
border: '1px solid ' + `${colors[index % colors.length]}7A`,
}}>
<Link href={href} style={{ textDecoration: 'none', color: 'inherit' }}>
<CardContent>
<Box textAlign="center">
<Icon fontSize="large" />
</Box>
<Typography variant="h6" component="h2" textAlign="center" fontWeight="bold">
{label}
</Typography>
</CardContent>
</Link>
</Button>
</Grid>
))}
</Grid>
</Container>
</AdminContainer>
</AdminLayout>
)
}
18 changes: 18 additions & 0 deletions src/components/admin/marketing/navigation/marketingCards.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { routes } from 'common/routes'
import ThumbUpAltIcon from '@mui/icons-material/ThumbUpAlt'
import SendIcon from '@mui/icons-material/Send'

export const marketingCards = [
{
label: 'Изпращане на емайл за съгласие',
icon: ThumbUpAltIcon,
href: routes.admin.marketing.newsLetterConsent,
disabled: false,
},
{
label: 'Изпращане на маркетинг емайл',
icon: SendIcon,
href: routes.admin.marketing.newsLetterConsent,
disabled: true,
},
]
6 changes: 6 additions & 0 deletions src/components/common/navigation/adminMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
DisplaySettings,
RequestQuote,
ArticleOutlined,
BroadcastOnPersonal,
} from '@mui/icons-material'
import VolunteerActivismOutlinedIcon from '@mui/icons-material/VolunteerActivismOutlined'
import LocationCityRoundedIcon from '@mui/icons-material/LocationCityRounded'
Expand Down Expand Up @@ -105,4 +106,9 @@ export const adminCards = [
icon: HandshakeIcon,
href: routes.admin.affiliates,
},
{
label: 'Маркетинг',
icon: BroadcastOnPersonal,
href: routes.admin.marketing.index,
},
]
13 changes: 13 additions & 0 deletions src/gql/marketing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export type SendMarketingEmail = {
templateId: string
listId: string
subject: string
}

export type SendNewsLetterConsent = SendMarketingEmail & {
dateThreshold?: string
}

export type NewsLetterConsentResponse = {
contactCount: number
}
6 changes: 6 additions & 0 deletions src/pages/admin/marketing/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import MarketingPage from 'components/admin/marketing/MarketingPage'
import { securedAdminProps } from 'middleware/auth/securedProps'

export const getServerSideProps = securedAdminProps(['common', 'auth', 'validation', 'marketing'])

export default MarketingPage
6 changes: 6 additions & 0 deletions src/pages/admin/marketing/newsletter-consent/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import SendEmailConsentPage from 'components/admin/marketing/EmailConsent/SendEmailConsentPage'
import { securedAdminProps } from 'middleware/auth/securedProps'

export const getServerSideProps = securedAdminProps(['common', 'auth', 'validation', 'marketing'])

export default SendEmailConsentPage
1 change: 1 addition & 0 deletions src/service/apiEndpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export const endpoints = {
notifications: {
sendConfirmationEmail: <Endpoint>{ url: '/notifications/send-confirm-email', method: 'POST' },
subscribePublicEmail: <Endpoint>{ url: '/notifications/public/subscribe', method: 'POST' },
sendNewsLetterConsentEmail: <Endpoint>{ url: '/notifications/send-newsletter-consent' },
unsubscribePublicEmail: <Endpoint>{ url: '/notifications/public/unsubscribe', method: 'POST' },
subscribeEmail: <Endpoint>{ url: '/notifications/subscribe', method: 'POST' },
unsubscribeEmail: <Endpoint>{ url: '/notifications/unsubscribe', method: 'POST' },
Expand Down
16 changes: 16 additions & 0 deletions src/service/marketing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { NewsLetterConsentResponse, SendNewsLetterConsent } from 'gql/marketing'
import { useSession } from 'next-auth/react'
import { authConfig } from './restRequests'
import { endpoints } from './apiEndpoints'
import { apiClient } from './apiClient'

export function useSendConsentEmail() {
const { data: session } = useSession()
return async (data: SendNewsLetterConsent) => {
return await apiClient.post<NewsLetterConsentResponse>(
endpoints.notifications.sendNewsLetterConsentEmail.url,
data,
authConfig(session?.accessToken),
)
}
}

0 comments on commit b1b2036

Please sign in to comment.