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

feat(admin): Allow for admins to send mass newsletter consent emails #1857

Merged
merged 5 commits into from
Jun 23, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
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 @@ -248,6 +248,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),
)
}
}
Loading