From 2e0ba5f49ebf55809ed2b9d44d99b1d4b85c2b88 Mon Sep 17 00:00:00 2001 From: Bogsana Date: Tue, 25 Jul 2023 21:29:06 +0300 Subject: [PATCH 1/7] Add subscribe boxes --- public/locales/bg/campaigns.json | 2 + public/locales/bg/validation.json | 2 + .../client/auth/register/RegisterForm.tsx | 5 + .../client/campaigns/CampaignDetails.tsx | 37 +++- .../campaigns/CampaignSubscribeModal.tsx | 172 ++++++++++++++++++ .../common/form/AcceptNewsletterField.tsx | 17 ++ src/gql/campaigns.ts | 10 + src/gql/person.d.ts | 1 + src/service/apiEndpoints.ts | 2 + src/service/campaign.ts | 19 +- 10 files changed, 263 insertions(+), 4 deletions(-) create mode 100644 src/components/client/campaigns/CampaignSubscribeModal.tsx create mode 100644 src/components/common/form/AcceptNewsletterField.tsx diff --git a/public/locales/bg/campaigns.json b/public/locales/bg/campaigns.json index 6dba84567..6a0b49b17 100644 --- a/public/locales/bg/campaigns.json +++ b/public/locales/bg/campaigns.json @@ -54,6 +54,8 @@ "save": "Запази", "submit": "Изпрати", "apply": "Кандидатствайте", + "subscribe": "Абониране за известия", + "subscribe-campaign": "Абониране за известия по тази кампания", "support": "Дарете", "support-cause-today": "Подкрепете кауза днес!", "support-now": "Подкрепете сега", diff --git a/public/locales/bg/validation.json b/public/locales/bg/validation.json index 984176311..3018432bb 100644 --- a/public/locales/bg/validation.json +++ b/public/locales/bg/validation.json @@ -15,9 +15,11 @@ "terms-of-service": "Моля, приемете политиката за защита на личните данни", "agree-terms": "Съгласявам се с Общите условия", "agree-with": "Съгласявам се с", + "agree-with-newsletter": "Съгласявам се да получавам известия ", "informed-agree-with": "Запознат съм и се съгласявам с", "terms-and-conditions": "общите условия", "gdpr": "политиката за защита на личните данни", + "newsletter": "Моля дайте своето съгласие", "legal-entity": "Юридическо лице", "unique-field-violation": "Полето `{1}` със тази стойност вече съществува в платформата", "payment-reference": "Невалиден формат на кода за плащане", diff --git a/src/components/client/auth/register/RegisterForm.tsx b/src/components/client/auth/register/RegisterForm.tsx index 408784000..02f54e986 100644 --- a/src/components/client/auth/register/RegisterForm.tsx +++ b/src/components/client/auth/register/RegisterForm.tsx @@ -16,6 +16,7 @@ import PasswordField from 'components/common/form/PasswordField' import AcceptPrivacyPolicyField from 'components/common/form/AcceptPrivacyPolicyField' import AcceptTermsField from 'components/common/form/AcceptTermsField' import EmailField from 'components/common/form/EmailField' +import AcceptNewsLetterField from 'components/common/form/AcceptNewsletterField' export type RegisterFormData = { firstName: string @@ -25,6 +26,7 @@ export type RegisterFormData = { confirmPassword: string terms: boolean gdpr: boolean + newsletter?: boolean } const validationSchema: yup.SchemaOf = yup @@ -38,6 +40,7 @@ const validationSchema: yup.SchemaOf = yup confirmPassword: confirmPassword.required('validation:password-match'), terms: yup.bool().required().oneOf([true], 'validation:terms-of-use'), gdpr: yup.bool().required().oneOf([true], 'validation:terms-of-service'), + newsletter: yup.bool().required().oneOf([true, false]), }) const defaults: RegisterFormData = { @@ -48,6 +51,7 @@ const defaults: RegisterFormData = { confirmPassword: '', terms: false, gdpr: false, + newsletter: false, } export type RegisterFormProps = { initialValues?: RegisterFormData } @@ -130,6 +134,7 @@ export default function RegisterForm({ initialValues = defaults }: RegisterFormP + diff --git a/src/components/client/campaigns/CampaignDetails.tsx b/src/components/client/campaigns/CampaignDetails.tsx index fef9bbe32..c11d0b105 100644 --- a/src/components/client/campaigns/CampaignDetails.tsx +++ b/src/components/client/campaigns/CampaignDetails.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useState } from 'react' import { useTranslation } from 'next-i18next' import dynamic from 'next/dynamic' @@ -7,7 +7,7 @@ import { CampaignResponse } from 'gql/campaigns' import 'react-quill/dist/quill.bubble.css' -import { Divider, Grid, Tooltip, Typography } from '@mui/material' +import { Button, Divider, Grid, Tooltip, Typography } from '@mui/material' import SecurityIcon from '@mui/icons-material/Security' import { styled } from '@mui/material/styles' @@ -21,11 +21,12 @@ import { campaignSliderUrls } from 'common/util/campaignImageUrls' import CampaignPublicExpensesGrid from './CampaignPublicExpensesGrid' import EditIcon from '@mui/icons-material/Edit' import { useCampaignApprovedExpensesList } from 'common/hooks/expenses' -import { Assessment } from '@mui/icons-material' +import { Assessment, Email } from '@mui/icons-material' import { routes } from 'common/routes' import { useCanEditCampaign } from 'common/hooks/campaigns' import { moneyPublic } from 'common/util/money' import ReceiptLongIcon from '@mui/icons-material/ReceiptLong' +import RenderSubscribeModal from './CampaignSubscribeModal' const ReactQuill = dynamic(() => import('react-quill'), { ssr: false }) const CampaignNewsSection = dynamic(() => import('./CampaignNewsSection'), { ssr: false }) @@ -34,6 +35,7 @@ const PREFIX = 'CampaignDetails' const classes = { banner: `${PREFIX}-banner`, + subscribeBtn: `${PREFIX}-subscribe`, campaignTitle: `${PREFIX}-campaignTitle`, linkButton: `${PREFIX}-linkButton`, securityIcon: `${PREFIX}-securityIcon`, @@ -89,6 +91,25 @@ const StyledGrid = styled(Grid)(({ theme }) => ({ width: theme.spacing(2.25), height: theme.spacing(2.75), }, + + [`& .${classes.subscribeBtn}`]: { + fontSize: theme.typography.pxToRem(16), + lineHeight: theme.spacing(3), + letterSpacing: theme.spacing(0.05), + color: theme.palette.common.black, + background: `${theme.palette.secondary.main}`, + padding: theme.spacing(1.5), + paddingLeft: theme.spacing(2), + paddingRight: theme.spacing(2.5), + width: '50%', + + '&:hover': { + background: theme.palette.primary.main, + }, + '& svg': { + color: '#333232 ', + }, + }, })) type Props = { @@ -101,6 +122,7 @@ export default function CampaignDetails({ campaign }: Props) { const canEditCampaign = useCanEditCampaign(campaign.slug) const { data: expensesList } = useCampaignApprovedExpensesList(campaign.slug) const totalExpenses = expensesList?.reduce((acc, expense) => acc + expense.amount, 0) + const [subscribeIsOpen, setSubscribeOpen] = useState(false) return ( @@ -111,6 +133,15 @@ export default function CampaignDetails({ campaign }: Props) { campaign={campaign} showExpensesLink={(expensesList && expensesList?.length > 0) || canEditCampaign} /> + {subscribeIsOpen && } + + + diff --git a/src/components/client/campaigns/CampaignSubscribeModal.tsx b/src/components/client/campaigns/CampaignSubscribeModal.tsx new file mode 100644 index 000000000..a7abe9782 --- /dev/null +++ b/src/components/client/campaigns/CampaignSubscribeModal.tsx @@ -0,0 +1,172 @@ +import * as yup from 'yup' +import { email } from 'common/form/validation' +import { useTranslation } from 'react-i18next' +import { useState } from 'react' +import { AxiosError, AxiosResponse } from 'axios' +import { ApiError } from 'next/dist/server/api-utils' +import { AlertStore } from 'stores/AlertStore' +import { useSubscribeToCampaign } from 'service/campaign' +import { CampaignResponse, CampaignSubscribeInput, CampaignSubscribeResponse } from 'gql/campaigns' +import { useMutation } from '@tanstack/react-query' +import { Dialog, DialogContent, DialogTitle, Grid } from '@mui/material' +import CloseModalButton from 'components/common/CloseModalButton' +import GenericForm from 'components/common/form/GenericForm' +import { styled } from '@mui/material/styles' +import SubmitButton from 'components/common/form/SubmitButton' +import EmailField from 'components/common/form/EmailField' +import AcceptNewsLetterField from 'components/common/form/AcceptNewsletterField' +import { useSession } from 'next-auth/react' +import { getCurrentPerson } from 'common/util/useCurrentPerson' + +const PREFIX = 'CampaignSubscribeModal' + +const classes = { + subscribeBtn: `${PREFIX}-subscribe`, +} + +const StyledGrid = styled(Grid)(({ theme }) => ({ + [`& .${classes.subscribeBtn}`]: { + fontSize: theme.typography.pxToRem(16), + background: `${theme.palette.secondary.main}`, + + '&:hover': { + background: theme.palette.primary.main, + }, + '& svg': { + color: '#ab2f26', + }, + }, +})) + +interface ModalProps { + campaign: CampaignResponse + setOpen: React.Dispatch> +} + +export type SubscribeToNotificationsInput = { + email: string + consent: boolean +} + +const validationSchema: yup.SchemaOf = yup + .object() + .defined() + .shape({ + email: email.required(), + consent: yup.bool().required().oneOf([true], 'validation:newsletter'), + }) + +export default function RenderSubscribeModal({ campaign, setOpen }: ModalProps) { + const { t } = useTranslation() + const { status } = useSession() + + const [loading, setLoading] = useState(false) + + const handleError = (e: AxiosError) => { + const error = e.response?.data?.message + AlertStore.show(error ? error : t('common:alerts.error'), 'error') + } + + const mutation = useMutation< + AxiosResponse, + AxiosError, + CampaignSubscribeInput + >({ + mutationFn: useSubscribeToCampaign(campaign.id), + onError: (error) => handleError(error), + onSuccess: (response) => { + AlertStore.show(t('common:alerts.message-sent'), 'success') + + handleClose() + }, + }) + + const handleClose = () => { + setOpen(false) + } + + async function onSubmit(values: { email: string; consent: boolean }) { + setLoading(true) + try { + await mutation.mutateAsync(values) + } finally { + setLoading(false) + } + } + + function AuthenticatedForm() { + const { data: user } = getCurrentPerson() + + return ( + + + {/* Show consent checkbox if user has not provided it previously */} + {!user?.user?.newsletter && ( + + + + )} + + + + + + ) + } + + function NonAuthenticatedForm() { + return ( + + + + + + + + + + + + + + ) + } + + return ( + + + + + + + {t('campaigns:cta.subscribe-campaign')} + + + {status === 'authenticated' ? : } + + + + ) +} diff --git a/src/components/common/form/AcceptNewsletterField.tsx b/src/components/common/form/AcceptNewsletterField.tsx new file mode 100644 index 000000000..31c3408e0 --- /dev/null +++ b/src/components/common/form/AcceptNewsletterField.tsx @@ -0,0 +1,17 @@ +import { useTranslation } from 'next-i18next' +import { Typography } from '@mui/material' +import CheckboxField from 'components/common/form/CheckboxField' + +export type AcceptNewsLetterFieldProps = { + name: string +} + +export default function AcceptNewsLetterField({ name }: AcceptNewsLetterFieldProps) { + const { t } = useTranslation() + return ( + {t('validation:agree-with-newsletter')}} + /> + ) +} diff --git a/src/gql/campaigns.ts b/src/gql/campaigns.ts index 840575267..d58202c3e 100644 --- a/src/gql/campaigns.ts +++ b/src/gql/campaigns.ts @@ -192,3 +192,13 @@ export type CampaignDonationHistoryResponse = { items: CampaignDonation[] total: number } + +export type CampaignSubscribeInput = { + email: string + consent: boolean +} + +export type CampaignSubscribeResponse = { + email: string + subscribed: boolean +} diff --git a/src/gql/person.d.ts b/src/gql/person.d.ts index 365c510ed..482821f21 100644 --- a/src/gql/person.d.ts +++ b/src/gql/person.d.ts @@ -48,6 +48,7 @@ export type Person = { personalNumber: string | null keycloakId: string | null stripeCustomerId: string | null + newsletter: boolean | null } export type UpdatePerson = Partial< diff --git a/src/service/apiEndpoints.ts b/src/service/apiEndpoints.ts index a1c387aa6..867bb1876 100644 --- a/src/service/apiEndpoints.ts +++ b/src/service/apiEndpoints.ts @@ -31,6 +31,8 @@ export const endpoints = { viewCampaign: (slug: string) => { url: `/campaign/${slug}`, method: 'GET' }, viewCampaignById: (id: string) => { url: `/campaign/byId/${id}`, method: 'GET' }, editCampaign: (id: string) => { url: `/campaign/${id}`, method: 'PUT' }, + subscribeToCampaign: (id: string) => + { url: `/campaign/${id}/subscribe`, method: 'POST' }, deleteCampaign: (id: string) => { url: `/campaign/${id}`, method: 'DELETE' }, uploadFile: (campaignId: string) => { url: `/campaign-file/${campaignId}`, method: 'POST' }, diff --git a/src/service/campaign.ts b/src/service/campaign.ts index b9bdf1ca0..2014c9e4b 100644 --- a/src/service/campaign.ts +++ b/src/service/campaign.ts @@ -5,7 +5,14 @@ import { apiClient } from 'service/apiClient' import { authConfig } from 'service/restRequests' import { endpoints } from 'service/apiEndpoints' import { UploadCampaignFiles } from 'components/common/campaign-file/roles' -import { CampaignResponse, CampaignInput, CampaignUploadImage, CampaignFile } from 'gql/campaigns' +import { + CampaignResponse, + CampaignInput, + CampaignUploadImage, + CampaignFile, + CampaignSubscribeInput, + CampaignSubscribeResponse, +} from 'gql/campaigns' import { Session } from 'next-auth' export const useCreateCampaign = () => { @@ -82,3 +89,13 @@ export const deleteCampaignFile = (id: string) => { ) } } + +export function useSubscribeToCampaign(id: string) { + const { data: session } = useSession() + return async (data: CampaignSubscribeInput) => { + return await apiClient.post< + CampaignSubscribeResponse, + AxiosResponse + >(endpoints.campaign.subscribeToCampaign(id).url, data, authConfig(session?.accessToken)) + } +} From 19e7990416108968317dc504c7fe82644529136c Mon Sep 17 00:00:00 2001 From: Bogsana Date: Fri, 28 Jul 2023 17:23:28 +0300 Subject: [PATCH 2/7] Added footer-subcsribe, subscribe confirmation route --- public/locales/bg/campaigns.json | 7 +- public/locales/bg/common.json | 4 +- public/locales/bg/notifications.json | 8 + .../client/campaigns/CampaignDetails.tsx | 6 +- .../client/layout/Footer/Footer.styled.tsx | 23 +++ .../client/layout/Footer/LogoSocialIcons.tsx | 2 + .../client/layout/Footer/SubscribeBtn.tsx | 17 ++ .../CampaignSubscribeModal.tsx | 31 ++- .../notifications/GeneralSubscribeModal.tsx | 142 ++++++++++++++ .../client/notifications/SubscriptionPage.tsx | 182 ++++++++++++++++++ src/components/common/brand/PodkrepiLogo.tsx | 3 +- src/gql/notification.ts | 19 ++ src/pages/notifications/subscribe/index.tsx | 19 ++ src/service/apiEndpoints.ts | 4 + src/service/campaign.ts | 19 +- src/service/notification.ts | 40 ++++ 16 files changed, 494 insertions(+), 32 deletions(-) create mode 100644 public/locales/bg/notifications.json create mode 100644 src/components/client/layout/Footer/SubscribeBtn.tsx rename src/components/client/{campaigns => notifications}/CampaignSubscribeModal.tsx (82%) create mode 100644 src/components/client/notifications/GeneralSubscribeModal.tsx create mode 100644 src/components/client/notifications/SubscriptionPage.tsx create mode 100644 src/gql/notification.ts create mode 100644 src/pages/notifications/subscribe/index.tsx create mode 100644 src/service/notification.ts diff --git a/public/locales/bg/campaigns.json b/public/locales/bg/campaigns.json index 6a0b49b17..6248a0f83 100644 --- a/public/locales/bg/campaigns.json +++ b/public/locales/bg/campaigns.json @@ -55,7 +55,6 @@ "submit": "Изпрати", "apply": "Кандидатствайте", "subscribe": "Абониране за известия", - "subscribe-campaign": "Абониране за известия по тази кампания", "support": "Дарете", "support-cause-today": "Подкрепете кауза днес!", "support-now": "Подкрепете сега", @@ -76,6 +75,12 @@ "download": "Изтеглете", "allow-donation-on-complete": "Разрешете дарения след достигане на сумата" }, + "subscribe": { + "confirm-sent": "Благодарим ви! На посочения e-mail адрес беше изпратено съобщение за потвърждение на вашето абониране.", + "confirm-subscribe": "Благодарим ви! Абонирахте се успешно.", + "subscribe-title": "Абониране за известия и новини от Podkrepi.bg", + "subscribe-campaign-title": "Абониране за известия по тази кампания" + }, "campaign": { "subheading": "Вашата подкрепа променя света и има значение. Всички подкрепящи чрез Подкрепи.бг са наши партньори в подпомагането на кампании за общността. Като щедър дарител Вие ставате важен партньор в подпомагането на кампания за нечие здраве или за успеха на кауза, която ви е близка до сърцето.", "subheading-bold": "Дори и най-малката помощ може да бъде двигател на голяма промяна.", diff --git a/public/locales/bg/common.json b/public/locales/bg/common.json index 000e21d02..835c32a12 100644 --- a/public/locales/bg/common.json +++ b/public/locales/bg/common.json @@ -58,6 +58,7 @@ "link": "Линк", "components": { "footer": { + "subscribe": "Абониране за известия", "donatе": "Дарете", "about-us": "За нас", "resources": "Ресурси", @@ -94,7 +95,8 @@ "personId": "Личност", "campaignId": "Кампания", "sourceCampaignId": "От кампания", - "targetCampaignId": "Към кампания" + "targetCampaignId": "Към кампания", + "email": "Имейл" }, "cta": { "read-more": "Прочетете още", diff --git a/public/locales/bg/notifications.json b/public/locales/bg/notifications.json new file mode 100644 index 000000000..1d800a42d --- /dev/null +++ b/public/locales/bg/notifications.json @@ -0,0 +1,8 @@ +{ + "subscribe": { + "thank-you-msg": "Абонирането за получаване на известия e успешно! Благодарим ❤️", + "subscription-fail": "Възникна проблем при потвърджаването на абонамента за известия. 🙄", + "cta": "Към сайта", + "cta-retry": "Опитай пак" + } +} diff --git a/src/components/client/campaigns/CampaignDetails.tsx b/src/components/client/campaigns/CampaignDetails.tsx index c11d0b105..1cda190cd 100644 --- a/src/components/client/campaigns/CampaignDetails.tsx +++ b/src/components/client/campaigns/CampaignDetails.tsx @@ -26,7 +26,7 @@ import { routes } from 'common/routes' import { useCanEditCampaign } from 'common/hooks/campaigns' import { moneyPublic } from 'common/util/money' import ReceiptLongIcon from '@mui/icons-material/ReceiptLong' -import RenderSubscribeModal from './CampaignSubscribeModal' +import RenderCampaignSubscribeModal from '../notifications/CampaignSubscribeModal' const ReactQuill = dynamic(() => import('react-quill'), { ssr: false }) const CampaignNewsSection = dynamic(() => import('./CampaignNewsSection'), { ssr: false }) @@ -133,7 +133,9 @@ export default function CampaignDetails({ campaign }: Props) { campaign={campaign} showExpensesLink={(expensesList && expensesList?.length > 0) || canEditCampaign} /> - {subscribeIsOpen && } + {subscribeIsOpen && ( + + )} + + ) +} diff --git a/src/components/client/campaigns/CampaignSubscribeModal.tsx b/src/components/client/notifications/CampaignSubscribeModal.tsx similarity index 82% rename from src/components/client/campaigns/CampaignSubscribeModal.tsx rename to src/components/client/notifications/CampaignSubscribeModal.tsx index a7abe9782..8673c5dbb 100644 --- a/src/components/client/campaigns/CampaignSubscribeModal.tsx +++ b/src/components/client/notifications/CampaignSubscribeModal.tsx @@ -5,7 +5,7 @@ import { useState } from 'react' import { AxiosError, AxiosResponse } from 'axios' import { ApiError } from 'next/dist/server/api-utils' import { AlertStore } from 'stores/AlertStore' -import { useSubscribeToCampaign } from 'service/campaign' +import { useSubscribeToCampaign } from 'service/notification' import { CampaignResponse, CampaignSubscribeInput, CampaignSubscribeResponse } from 'gql/campaigns' import { useMutation } from '@tanstack/react-query' import { Dialog, DialogContent, DialogTitle, Grid } from '@mui/material' @@ -17,6 +17,7 @@ import EmailField from 'components/common/form/EmailField' import AcceptNewsLetterField from 'components/common/form/AcceptNewsletterField' import { useSession } from 'next-auth/react' import { getCurrentPerson } from 'common/util/useCurrentPerson' +import React from 'react' const PREFIX = 'CampaignSubscribeModal' @@ -56,11 +57,12 @@ const validationSchema: yup.SchemaOf = yup consent: yup.bool().required().oneOf([true], 'validation:newsletter'), }) -export default function RenderSubscribeModal({ campaign, setOpen }: ModalProps) { +export default function RenderCampaignSubscribeModal({ campaign, setOpen }: ModalProps) { const { t } = useTranslation() const { status } = useSession() const [loading, setLoading] = useState(false) + const [isSuccess, setIsSuccess] = useState(false) const handleError = (e: AxiosError) => { const error = e.response?.data?.message @@ -77,7 +79,7 @@ export default function RenderSubscribeModal({ campaign, setOpen }: ModalProps) onSuccess: (response) => { AlertStore.show(t('common:alerts.message-sent'), 'success') - handleClose() + setIsSuccess(true) }, }) @@ -160,12 +162,23 @@ export default function RenderSubscribeModal({ campaign, setOpen }: ModalProps) - - {t('campaigns:cta.subscribe-campaign')} - - - {status === 'authenticated' ? : } - + {!isSuccess ? ( + + + {t('campaigns:subscribe.subscribe-campaign-title')} + + + {status === 'authenticated' ? : } + + + ) : ( + + {status === 'authenticated' + ? t('campaigns:subscribe.confirm-subscribe') + : t('campaigns:subscribe.confirm-sent')} + + )} ) diff --git a/src/components/client/notifications/GeneralSubscribeModal.tsx b/src/components/client/notifications/GeneralSubscribeModal.tsx new file mode 100644 index 000000000..ddcdf4e26 --- /dev/null +++ b/src/components/client/notifications/GeneralSubscribeModal.tsx @@ -0,0 +1,142 @@ +import * as yup from 'yup' +import { email } from 'common/form/validation' +import { useTranslation } from 'react-i18next' +import { useState } from 'react' +import { AxiosError, AxiosResponse } from 'axios' +import { ApiError } from 'next/dist/server/api-utils' +import { AlertStore } from 'stores/AlertStore' +import { useMutation } from '@tanstack/react-query' +import { Dialog, DialogContent, DialogTitle, Grid } from '@mui/material' +import CloseModalButton from 'components/common/CloseModalButton' +import GenericForm from 'components/common/form/GenericForm' +import { styled } from '@mui/material/styles' +import SubmitButton from 'components/common/form/SubmitButton' +import EmailField from 'components/common/form/EmailField' +import { useSendConfirmationEmail } from 'service/notification' +import { SendConfirmationEmailResponse, SendConfirmationEmailInput } from 'gql/notification' +import React from 'react' + +const PREFIX = 'SubscribeModal' + +const classes = { + subscribeBtn: `${PREFIX}-subscribe`, +} + +const StyledGrid = styled(Grid)(({ theme }) => ({ + [`& .${classes.subscribeBtn}`]: { + fontSize: theme.typography.pxToRem(16), + background: `${theme.palette.secondary.main}`, + + '&:hover': { + background: theme.palette.primary.main, + }, + '& svg': { + color: '#ab2f26', + }, + }, +})) + +interface ModalProps { + setOpen: React.Dispatch> +} + +export type SubscribeToNotificationsInput = { + email: string +} + +const validationSchema: yup.SchemaOf = yup.object().defined().shape({ + email: email.required(), +}) + +export default function RenderSubscribeModal({ setOpen }: ModalProps) { + const { t } = useTranslation() + + const [loading, setLoading] = useState(false) + const [isSuccess, setIsSuccess] = useState(false) + + const handleError = (e: AxiosError) => { + const error = e.response?.data?.message + AlertStore.show(error ? error : t('common:alerts.error'), 'error') + } + + const mutation = useMutation< + AxiosResponse, + AxiosError, + SendConfirmationEmailInput + >({ + mutationFn: useSendConfirmationEmail(), + onError: (error) => handleError(error), + onSuccess: (response, data) => { + AlertStore.show(t('common:alerts.message-sent'), 'success') + + setIsSuccess(true) + }, + }) + + const handleClose = () => { + setOpen(false) + } + + async function onSubmit(values: { email: string }) { + setLoading(true) + try { + await mutation.mutateAsync(values) + } finally { + setLoading(false) + } + } + + function SubscribeForm() { + return ( + + + + + + + + + + + ) + } + + return ( + + + + + + {!isSuccess ? ( + + + {t('campaigns:subscribe.subscribe-title')} + + + + + + ) : ( + + {t('campaigns:subscribe.confirm-sent').split('{{email}}')} + + )} + + + ) +} diff --git a/src/components/client/notifications/SubscriptionPage.tsx b/src/components/client/notifications/SubscriptionPage.tsx new file mode 100644 index 000000000..afafe036b --- /dev/null +++ b/src/components/client/notifications/SubscriptionPage.tsx @@ -0,0 +1,182 @@ +import { useTranslation } from 'react-i18next' +import Layout from '../layout/Layout' +import PodkrepiLogo from 'components/common/brand/PodkrepiLogo' +import { useRouter } from 'next/router' +import { Button, DialogContent, Grid } from '@mui/material' +import { styled } from '@mui/material/styles' +import LinkButton from 'components/common/LinkButton' +import React, { useEffect, useState } from 'react' +import { AlertStore } from 'stores/AlertStore' +import { useMutation } from '@tanstack/react-query' +import { AxiosError, AxiosResponse } from 'axios' +import { SubscribePublicEmailInput, SubscribePublicEmailResponse } from 'gql/notification' +import { ApiError } from 'next/dist/server/api-utils' +import { useSubscribePublicEmail } from 'service/notification' + +type Props = { + hash: string | null + email: string | null + consent: string | null + campaign: string | null +} + +type Payload = { + hash: string + email: string + consent: boolean + campaignId?: string | null +} + +const PREFIX = 'notification-subscribe' + +const classes = { + siteBtn: `${PREFIX}-siteBtn`, + loader: `${PREFIX}-loader`, +} + +const StyledGrid = styled(Grid)(({ theme }) => ({ + [`& .${classes.loader}`]: { + animation: 'pulsate 1s infinite', + + '@keyframes pulsate': { + ' 0%': { + transform: 'scale(1)', + }, + '50%': { + transform: 'scale(1.14)', + }, + '100% ': { + transform: ' scale(1)', + }, + }, + }, + + [`& .${classes.siteBtn}`]: { + fontSize: theme.typography.pxToRem(18), + lineHeight: theme.spacing(3), + letterSpacing: theme.spacing(0.05), + color: theme.palette.common.black, + background: `${theme.palette.secondary.main}`, + padding: theme.spacing(1.5), + paddingLeft: theme.spacing(2), + paddingRight: theme.spacing(2.5), + width: theme.spacing(60), + + '&:hover': { + background: theme.palette.primary.main, + }, + '& svg': { + color: '#333232 ', + }, + }, +})) + +export default function SubscriptionPage(data: Props) { + const { t } = useTranslation() + const { locale } = useRouter() + const router = useRouter() + + const [loading, setLoading] = useState(true) + const [isSuccess, setIsSuccess] = useState(false) + + useEffect(() => { + if (!data.consent || !data.email || !data.hash) { + router.push('/') + return + } + const payload: Payload = { + consent: data.consent === 'true' || false, + hash: data.hash, + email: data.email, + } + if (data.campaign) payload.campaignId = data.campaign + + callSubscribeApiRoute(payload).catch(() => console.log()) + }, []) + + const handleError = (e: AxiosError) => { + const error = e.response?.data?.message + console.log(error) + setLoading(false) + setIsSuccess(false) + } + + const mutation = useMutation< + AxiosResponse, + AxiosError, + SubscribePublicEmailInput + >({ + mutationFn: useSubscribePublicEmail(), + onError: (error) => handleError(error), + onSuccess: (response, data) => { + AlertStore.show(t('common:alerts.message-sent'), 'success') + setIsSuccess(true) + setLoading(false) + }, + }) + + async function callSubscribeApiRoute(values: { + hash: string + email: string + consent: boolean + campaignId?: string | null + }) { + setLoading(true) + try { + await mutation.mutateAsync(values) + } finally { + setLoading(false) + } + } + + // Show loader + if (loading) + return ( + + + + + + ) + + return ( + + + + + + {isSuccess ? ( + + + + {t('notifications:subscribe.thank-you-msg')} + + + + + {t('notifications:subscribe.cta')} + + {' '} + + ) : ( + + + + + {t('notifications:subscribe.subscription-fail')} + + + + + {' '} + + + )} + + + ) +} diff --git a/src/components/common/brand/PodkrepiLogo.tsx b/src/components/common/brand/PodkrepiLogo.tsx index 697358cc1..43c5524e5 100644 --- a/src/components/common/brand/PodkrepiLogo.tsx +++ b/src/components/common/brand/PodkrepiLogo.tsx @@ -21,13 +21,14 @@ const Root = styled('svg')(({ theme }) => ({ type ParkhandsLogoProps = { variant?: 'fixed' | 'adaptive' - size?: 'small' | 'large' + size?: 'small' | 'large' | 'giant' locale?: string className?: string } const sizes = { small: [118, 24], large: [236, 48], + giant: [472, 96], } export default function PodkrepiLogo({ variant = 'fixed', diff --git a/src/gql/notification.ts b/src/gql/notification.ts new file mode 100644 index 000000000..31058db6c --- /dev/null +++ b/src/gql/notification.ts @@ -0,0 +1,19 @@ +export type SendConfirmationEmailInput = { + email: string +} + +export type SendConfirmationEmailResponse = { + message: string +} + +export type SubscribePublicEmailInput = { + email: string + consent: boolean + hash: string + campaignId?: string | null +} + +export type SubscribePublicEmailResponse = { + email: string + subscribed: boolean +} diff --git a/src/pages/notifications/subscribe/index.tsx b/src/pages/notifications/subscribe/index.tsx new file mode 100644 index 000000000..5ad9684d9 --- /dev/null +++ b/src/pages/notifications/subscribe/index.tsx @@ -0,0 +1,19 @@ +import SubscriptionPage from 'components/client/notifications/SubscriptionPage' +import { GetServerSideProps } from 'next' +import { serverSideTranslations } from 'next-i18next/serverSideTranslations' + +export const getServerSideProps: GetServerSideProps = async ({ locale, query, res }) => { + const { hash, email, consent, campaign } = query + + return { + props: { + ...(await serverSideTranslations(locale ?? 'bg', ['common', 'campaigns', `notifications`])), + hash: hash || null, + email: email || null, + consent: consent || null, + campaign: campaign || null, + }, + } +} + +export default SubscriptionPage diff --git a/src/service/apiEndpoints.ts b/src/service/apiEndpoints.ts index 867bb1876..a1a3a345a 100644 --- a/src/service/apiEndpoints.ts +++ b/src/service/apiEndpoints.ts @@ -47,6 +47,10 @@ export const endpoints = { listNewsForCampaign: (page: number, slug: string) => { url: `/campaign/${slug}/news?page=${page}` }, }, + notifications: { + sendConfirmationEmail: { url: '/notifications/send-confirm-email', method: 'POST' }, + subscribePublicEmail: { url: '/notifications/public/subscribe', method: 'POST' }, + }, campaignNews: { createNewsArticle: { url: '/campaign-news', method: 'POST' }, listAdminNews: { url: '/campaign-news/list-all', method: 'GET' }, diff --git a/src/service/campaign.ts b/src/service/campaign.ts index 2014c9e4b..b9bdf1ca0 100644 --- a/src/service/campaign.ts +++ b/src/service/campaign.ts @@ -5,14 +5,7 @@ import { apiClient } from 'service/apiClient' import { authConfig } from 'service/restRequests' import { endpoints } from 'service/apiEndpoints' import { UploadCampaignFiles } from 'components/common/campaign-file/roles' -import { - CampaignResponse, - CampaignInput, - CampaignUploadImage, - CampaignFile, - CampaignSubscribeInput, - CampaignSubscribeResponse, -} from 'gql/campaigns' +import { CampaignResponse, CampaignInput, CampaignUploadImage, CampaignFile } from 'gql/campaigns' import { Session } from 'next-auth' export const useCreateCampaign = () => { @@ -89,13 +82,3 @@ export const deleteCampaignFile = (id: string) => { ) } } - -export function useSubscribeToCampaign(id: string) { - const { data: session } = useSession() - return async (data: CampaignSubscribeInput) => { - return await apiClient.post< - CampaignSubscribeResponse, - AxiosResponse - >(endpoints.campaign.subscribeToCampaign(id).url, data, authConfig(session?.accessToken)) - } -} diff --git a/src/service/notification.ts b/src/service/notification.ts new file mode 100644 index 000000000..a748b7894 --- /dev/null +++ b/src/service/notification.ts @@ -0,0 +1,40 @@ +import { AxiosResponse } from 'axios' +import { CampaignSubscribeInput, CampaignSubscribeResponse } from 'gql/campaigns' +import { useSession } from 'next-auth/react' +import { apiClient } from './apiClient' +import { endpoints } from './apiEndpoints' +import { authConfig } from './restRequests' +import { + SendConfirmationEmailInput, + SendConfirmationEmailResponse, + SubscribePublicEmailInput, + SubscribePublicEmailResponse, +} from 'gql/notification' + +export function useSubscribeToCampaign(id: string) { + const { data: session } = useSession() + return async (data: CampaignSubscribeInput) => { + return await apiClient.post< + CampaignSubscribeResponse, + AxiosResponse + >(endpoints.campaign.subscribeToCampaign(id).url, data, authConfig(session?.accessToken)) + } +} + +export function useSendConfirmationEmail() { + return async (data: SendConfirmationEmailInput) => { + return await apiClient.post< + SendConfirmationEmailResponse, + AxiosResponse + >(endpoints.notifications.sendConfirmationEmail.url, data) + } +} + +export function useSubscribePublicEmail() { + return async (data: SubscribePublicEmailInput) => { + return await apiClient.post< + SubscribePublicEmailResponse, + AxiosResponse + >(endpoints.notifications.subscribePublicEmail.url, data) + } +} From ddf5bf0d1ad87f028f0f68e99f353001e2092198 Mon Sep 17 00:00:00 2001 From: Bogsana Date: Fri, 4 Aug 2023 08:48:38 +0300 Subject: [PATCH 3/7] addd notifications tab to logged user page --- public/locales/bg/notifications.json | 8 +- public/locales/bg/profile.json | 27 +++ src/common/hooks/notification.ts | 15 ++ src/common/routes.ts | 1 + .../profile/MyCampaignNotificationsTable.tsx | 146 +++++++++++++++ .../MyNotificationsCampaignConfirmModal.tsx | 131 ++++++++++++++ .../profile/MyNotificationsConfirmModal.tsx | 153 ++++++++++++++++ .../auth/profile/MyNotificationsTab.tsx | 149 +++++++++++++++ .../client/auth/profile/ProfilePage.tsx | 8 + src/components/client/auth/profile/tabs.tsx | 7 + .../notifications/GeneralSubscribeModal.tsx | 2 +- .../notifications/UNsubscriptionsPage.tsx | 170 ++++++++++++++++++ src/gql/notification.ts | 36 +++- src/pages/notifications/subscribe/index.tsx | 2 +- src/pages/notifications/unsubscribe/index.tsx | 17 ++ src/service/apiEndpoints.ts | 7 + src/service/notification.ts | 37 ++++ 17 files changed, 912 insertions(+), 4 deletions(-) create mode 100644 src/common/hooks/notification.ts create mode 100644 src/components/client/auth/profile/MyCampaignNotificationsTable.tsx create mode 100644 src/components/client/auth/profile/MyNotificationsCampaignConfirmModal.tsx create mode 100644 src/components/client/auth/profile/MyNotificationsConfirmModal.tsx create mode 100644 src/components/client/auth/profile/MyNotificationsTab.tsx create mode 100644 src/components/client/notifications/UNsubscriptionsPage.tsx create mode 100644 src/pages/notifications/unsubscribe/index.tsx diff --git a/public/locales/bg/notifications.json b/public/locales/bg/notifications.json index 1d800a42d..b65e5c755 100644 --- a/public/locales/bg/notifications.json +++ b/public/locales/bg/notifications.json @@ -1,7 +1,13 @@ { "subscribe": { "thank-you-msg": "Абонирането за получаване на известия e успешно! Благодарим ❤️", - "subscription-fail": "Възникна проблем при потвърджаването на абонамента за известия. 🙄", + "subscription-fail": "Възникна проблем при потвърджаването на абонамента за известия 🙄", + "cta": "Към сайта", + "cta-retry": "Опитай пак" + }, + "unsubscribe": { + "thank-you-msg": "Успешно деактивирахте абонамента си за известия!", + "subscription-fail": "Възникна проблем при деактивирането на абонамента за известия 🙄", "cta": "Към сайта", "cta-retry": "Опитай пак" } diff --git a/public/locales/bg/profile.json b/public/locales/bg/profile.json index f5ff14d9b..a03fc02a7 100644 --- a/public/locales/bg/profile.json +++ b/public/locales/bg/profile.json @@ -56,6 +56,33 @@ "noCampaigns": "Вие не сте в роля организатор, координатор или бенефицент към нито една кампания", "donatedTo": "Кампании, в които съм дарил" }, + "myNotifications": { + "index": "Моите известия", + "status-title": "Статус на абонамента за известия", + "status-msg": "Абонаментът ви за получаване на известия e", + "modal": { + "title-subscribe": "Активиране на получаването на известия от Podkrepi.bg?", + "title-unsubscribe": "Отписване от получаването на известия от Podkrepi.bg?", + "campaign-title-unsubscribe": "Отписване от получаването на известия по тази кампания?", + "cta": "Потвърждавам", + "subscribe-msg": "Успешно активирахте абонамента си за получаването на известия!", + "unsubscribe-msg": "Успешно деактивирахте абонамента си за получаването на известия!", + "campaign-unsubscribe-msg": "Успешно се отписахте от получаването на известия по кампанията" + }, + "status": { + "active": "Активен", + "inactive": "Деактивиран" + }, + "cta": { + "activate": "Активирай", + "deactivate": "Деактивирай" + }, + "campaign": { + "index": "Нотификации по кампании", + "noSubscriptions": "Към момента не сте се записали за получаване на известия по конкретни кампании", + "cta": "Отписване" + } + }, "donationsContract": "Договор дарение", "certificates": "Сертификати", "birthdateModal": { diff --git a/src/common/hooks/notification.ts b/src/common/hooks/notification.ts new file mode 100644 index 000000000..8fcf11435 --- /dev/null +++ b/src/common/hooks/notification.ts @@ -0,0 +1,15 @@ +import { useQuery } from '@tanstack/react-query' +import { GetCampaignNotificationSubscriptionsResponse } from 'gql/notification' +import { useSession } from 'next-auth/react' +import { endpoints } from 'service/apiEndpoints' +import { authQueryFnFactory } from 'service/restRequests' + +export function useUserCampaignNotificationSubscriptions() { + const { data: session } = useSession() + return useQuery( + [endpoints.notifications.getCampaignNotificationSubscriptions.url], + { + queryFn: authQueryFnFactory(session?.accessToken), + }, + ) +} diff --git a/src/common/routes.ts b/src/common/routes.ts index f5edcaccd..0eeb2150e 100644 --- a/src/common/routes.ts +++ b/src/common/routes.ts @@ -115,6 +115,7 @@ export const routes = { contractDonation: '/profile/contract-donation', myCampaigns: '/profile/my-campaigns', recurringDonations: '/profile/recurring-donations', + myNotifications: '/profile/my-notifications', }, register: '/register', aboutProject: '/about-project', diff --git a/src/components/client/auth/profile/MyCampaignNotificationsTable.tsx b/src/components/client/auth/profile/MyCampaignNotificationsTable.tsx new file mode 100644 index 000000000..b98c3a7d0 --- /dev/null +++ b/src/components/client/auth/profile/MyCampaignNotificationsTable.tsx @@ -0,0 +1,146 @@ +import { useTranslation } from 'next-i18next' +import { DataGrid, GridColDef, GridRenderCellParams } from '@mui/x-data-grid' +import { Button, Box } from '@mui/material' + +import Link from 'components/common/Link' +import { useUserCampaignNotificationSubscriptions } from 'common/hooks/notification' + +import { useState } from 'react' +import { styled } from '@mui/material/styles' +import RenderCampaignNotificationsConfirmModal from './MyNotificationsCampaignConfirmModal' +import ContentTypography from 'components/client/faq/contents/ContentTypography' +import { getRelativeDate } from 'common/util/date' + +const PREFIX = 'MyNotificationsTab' + +const classes = { + h3: `${PREFIX}-h3`, + thinFont: `${PREFIX}-thinFont`, + smallText: `${PREFIX}-smallText`, + boxTitle: `${PREFIX}-boxTitle`, + statusBoxRow: `${PREFIX}-statusBoxRow`, + notificationsBox: `${PREFIX}-notificationBox`, + statusBtn: `${PREFIX}-statusBtn`, + statusActive: `${PREFIX}-statusActive`, + statusInactive: `${PREFIX}-statusInactive`, +} + +const StyledGrid = styled('div')(({ theme }) => ({ + [`& .${classes.statusBtn}`]: { + fontSize: theme.typography.pxToRem(16), + lineHeight: theme.spacing(3), + letterSpacing: theme.spacing(0.05), + color: theme.palette.common.black, + background: `${theme.palette.secondary.main}`, + padding: theme.spacing(1), + paddingLeft: theme.spacing(2), + paddingRight: theme.spacing(2.5), + width: theme.spacing(20), + + '&:hover': { + background: theme.palette.primary.main, + }, + '& svg': { + color: '#333232 ', + }, + }, +})) + +export default function MyCampaignNotificationsTable() { + const { t, i18n } = useTranslation() + const { data = [] } = useUserCampaignNotificationSubscriptions() + + const [confirmModalId, setConfirmModalId] = useState('') + + const commonProps: Partial = { + align: 'left', + width: 100, + headerAlign: 'left', + } + + const columns: GridColDef[] = [ + { + field: 'campaign.id', + headerName: t('campaigns:actions'), + align: 'left', + width: 180, + headerAlign: 'left', + renderCell: (cellValues: GridRenderCellParams) => { + return ( + + + + ) + }, + }, + { + field: 'campaign.title', + headerName: t('campaigns:title'), + ...commonProps, + align: 'left', + width: 450, + renderCell: (cellValues: GridRenderCellParams) => ( + + {cellValues.row.campaign.title} + + ), + }, + { + field: 'campaign.state', + headerName: t('campaigns:status'), + ...commonProps, + align: 'left', + width: 120, + renderCell: (cellValues: GridRenderCellParams) => ( + {cellValues.row.campaign?.state} + ), + }, + { + field: 'endDate', + headerName: t('campaigns:endDate'), + ...commonProps, + align: 'left', + width: 250, + renderCell: (cellValues: GridRenderCellParams) => ( + + {getRelativeDate(cellValues.row.campaign?.endDate, i18n.language)} + + ), + }, + ] + return ( + <> + {confirmModalId && ( + + )} + {data.length !== 0 ? ( + + ) : ( + {t('profile:myNotifications.campaign.noSubscriptions')} + )} + + ) +} diff --git a/src/components/client/auth/profile/MyNotificationsCampaignConfirmModal.tsx b/src/components/client/auth/profile/MyNotificationsCampaignConfirmModal.tsx new file mode 100644 index 000000000..e888fd178 --- /dev/null +++ b/src/components/client/auth/profile/MyNotificationsCampaignConfirmModal.tsx @@ -0,0 +1,131 @@ +import { useMutation } from '@tanstack/react-query' +import { useQueryClient } from '@tanstack/react-query' +import { endpoints } from 'service/apiEndpoints' +import { styled } from '@mui/material/styles' +import { Dialog, DialogContent, DialogTitle, Grid } from '@mui/material' +import { AxiosError, AxiosResponse } from 'axios' +import { UNsubscribeEmailResponse, UNsubscribeEmailInput } from 'gql/notification' +import { ApiError } from 'next/dist/server/api-utils' +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useUNsubscribeEmail } from 'service/notification' +import { AlertStore } from 'stores/AlertStore' +import CloseModalButton from 'components/common/CloseModalButton' +import GenericForm from 'components/common/form/GenericForm' +import SubmitButton from 'components/common/form/SubmitButton' +import React from 'react' + +const PREFIX = 'ProfileCampaignNotificationsModal' + +const classes = { + actionBtn: `${PREFIX}-campaign-subscriptions`, +} + +const StyledGrid = styled(Grid)(({ theme }) => ({ + [`& .${classes.actionBtn}`]: { + fontSize: theme.typography.pxToRem(16), + background: `${theme.palette.secondary.main}`, + + '&:hover': { + background: theme.palette.primary.main, + }, + '& svg': { + color: '#ab2f26', + }, + }, +})) + +interface ModalProps { + campaignId: string + setOpen: React.Dispatch> +} + +export default function RenderCampaignNotificationsConfirmModal({ + campaignId, + setOpen, +}: ModalProps) { + const { t } = useTranslation() + + const [loading, setLoading] = useState(false) + const [isSuccess, setIsSuccess] = useState(false) + + // Get QueryClient from the context + const queryClient = useQueryClient() + + const handleError = (e: AxiosError) => { + const error = e.response?.data?.message + AlertStore.show(error ? error : t('common:alerts.error'), 'error') + } + + const unSubscribeMutation = useMutation< + AxiosResponse, + AxiosError, + UNsubscribeEmailInput + >({ + mutationFn: useUNsubscribeEmail(), + onError: (error) => handleError(error), + onSuccess: (response) => { + AlertStore.show(t('common:alerts.message-sent'), 'success') + // Update data + queryClient.invalidateQueries({ + queryKey: [endpoints.notifications.getCampaignNotificationSubscriptions.url], + }) + setIsSuccess(true) + }, + }) + + const handleClose = () => { + setOpen('') + } + + async function onSubmit() { + setLoading(true) + try { + await unSubscribeMutation.mutateAsync({ campaignId }) + } finally { + setLoading(false) + } + } + + return ( + + + + + + {!isSuccess ? ( + + + {t('profile:myNotifications.modal.campaign-title-unsubscribe')} + + + + + + + + + + + + ) : ( + + {t('profile:myNotifications.modal.campaign-unsubscribe-msg')} + + )} + + + ) +} diff --git a/src/components/client/auth/profile/MyNotificationsConfirmModal.tsx b/src/components/client/auth/profile/MyNotificationsConfirmModal.tsx new file mode 100644 index 000000000..618913430 --- /dev/null +++ b/src/components/client/auth/profile/MyNotificationsConfirmModal.tsx @@ -0,0 +1,153 @@ +import { Dialog, DialogContent, Grid, DialogTitle } from '@mui/material' +import { useMutation } from '@tanstack/react-query' +import { AxiosError, AxiosResponse } from 'axios' +import CloseModalButton from 'components/common/CloseModalButton' +import { + SubscribeEmailResponse, + SubscribeEmailInput, + UNsubscribeEmailInput, + UNsubscribeEmailResponse, +} from 'gql/notification' +import { ApiError } from 'next/dist/server/api-utils' +import React from 'react' +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useSubscribeEmail, useUNsubscribeEmail } from 'service/notification' +import { AlertStore } from 'stores/AlertStore' +import { styled } from '@mui/material/styles' +import GenericForm from 'components/common/form/GenericForm' +import SubmitButton from 'components/common/form/SubmitButton' +import { useQueryClient } from '@tanstack/react-query' +import { endpoints } from 'service/apiEndpoints' + +const PREFIX = 'ProfileNotificationsModal' + +const classes = { + actionBtn: `${PREFIX}-subscriptions`, +} + +const StyledGrid = styled(Grid)(({ theme }) => ({ + [`& .${classes.actionBtn}`]: { + fontSize: theme.typography.pxToRem(16), + background: `${theme.palette.secondary.main}`, + + '&:hover': { + background: theme.palette.primary.main, + }, + '& svg': { + color: '#ab2f26', + }, + }, +})) + +interface ModalProps { + type: 'subscribe' | 'unsubscribe' + setOpen: React.Dispatch> +} + +export default function RenderNotificationsConfirmModal({ type, setOpen }: ModalProps) { + const { t } = useTranslation() + + const [loading, setLoading] = useState(false) + const [isSuccess, setIsSuccess] = useState(false) + + // Get QueryClient from the context + const queryClient = useQueryClient() + + const handleError = (e: AxiosError) => { + const error = e.response?.data?.message + AlertStore.show(error ? error : t('common:alerts.error'), 'error') + } + + const subscribeMutation = useMutation< + AxiosResponse, + AxiosError, + SubscribeEmailInput + >({ + mutationFn: useSubscribeEmail(), + onError: (error) => handleError(error), + onSuccess: (response) => { + AlertStore.show(t('common:alerts.message-sent'), 'success') + // Update data + queryClient.invalidateQueries({ queryKey: [endpoints.account.me.url] }) + setIsSuccess(true) + }, + }) + const unSubscribeMutation = useMutation< + AxiosResponse, + AxiosError, + UNsubscribeEmailInput + >({ + mutationFn: useUNsubscribeEmail(), + onError: (error) => handleError(error), + onSuccess: (response) => { + AlertStore.show(t('common:alerts.message-sent'), 'success') + // Update data + queryClient.invalidateQueries({ queryKey: [endpoints.account.me.url] }) + setIsSuccess(true) + }, + }) + + const handleClose = () => { + setOpen(false) + } + + async function onSubmit() { + setLoading(true) + try { + if (type === 'subscribe') { + await subscribeMutation.mutateAsync({ consent: true }) + } else { + await unSubscribeMutation.mutateAsync({}) + } + } finally { + setLoading(false) + } + } + + return ( + + + + + + {!isSuccess ? ( + + + {type === 'subscribe' + ? t('profile:myNotifications.modal.title-subscribe') + : t('profile:myNotifications.modal.title-unsubscribe')} + + + + + + + + + + + + ) : ( + + {type === 'subscribe' + ? t('profile:myNotifications.modal.unsubscribe-msg') + : t('profile:myNotifications.modal.subscribe-msg')} + + )} + + + ) +} diff --git a/src/components/client/auth/profile/MyNotificationsTab.tsx b/src/components/client/auth/profile/MyNotificationsTab.tsx new file mode 100644 index 000000000..ae50073a8 --- /dev/null +++ b/src/components/client/auth/profile/MyNotificationsTab.tsx @@ -0,0 +1,149 @@ +import { useTranslation } from 'react-i18next' +import { styled } from '@mui/material/styles' +import { Box, Button, Card, Typography } from '@mui/material' +import { getCurrentPerson } from 'common/util/useCurrentPerson' +import { useRouter } from 'next/router' +import { ProfileTabs } from './tabs' +import ProfileTab from './ProfileTab' +import MyCampaignNotificationsTable from './MyCampaignNotificationsTable' +import { useState } from 'react' +import RenderNotificationsConfirmModal from './MyNotificationsConfirmModal' + +const PREFIX = 'MyNotificationsTab' + +const classes = { + h3: `${PREFIX}-h3`, + thinFont: `${PREFIX}-thinFont`, + smallText: `${PREFIX}-smallText`, + boxTitle: `${PREFIX}-boxTitle`, + statusBoxRow: `${PREFIX}-statusBoxRow`, + notificationsBox: `${PREFIX}-notificationBox`, + statusBtn: `${PREFIX}-statusBtn`, + statusActive: `${PREFIX}-statusActive`, + statusInactive: `${PREFIX}-statusInactive`, +} + +const Root = styled('div')(({ theme }) => ({ + [`& .${classes.h3}`]: { + fontStyle: 'normal', + fontWeight: '500', + fontSize: '25px', + lineHeight: '116.7%', + margin: '0', + }, + [`& .${classes.thinFont}`]: { + fontStyle: 'normal', + fontWeight: 400, + fontSize: '24px', + lineHeight: '123.5%', + letterSpacing: '0.25px', + // color: '#000000', + margin: 0, + }, + [`& .${classes.smallText}`]: { + fontStyle: 'normal', + fontWeight: '500', + fontSize: '15px', + lineHeight: '160%', + letterSpacing: '0.15px', + }, + [`& .${classes.boxTitle}`]: { + backgroundColor: 'white', + padding: theme.spacing(3, 7), + paddingBottom: theme.spacing(3), + marginTop: theme.spacing(3), + boxShadow: theme.shadows[3], + }, + [`& .${classes.notificationsBox}`]: { + padding: theme.spacing(5), + boxShadow: theme.shadows[3], + marginTop: theme.spacing(0.5), + }, + [`& .${classes.statusBoxRow}`]: { + display: 'flex', + justifyContent: 'space-around', + alignItems: 'center', + }, + [`& .${classes.statusActive}`]: { + color: 'green', + }, + [`& .${classes.statusInactive}`]: { + color: '#880808', + }, + [`& .${classes.statusBtn}`]: { + fontSize: theme.typography.pxToRem(18), + lineHeight: theme.spacing(3), + letterSpacing: theme.spacing(0.05), + color: theme.palette.common.black, + background: `${theme.palette.secondary.main}`, + padding: theme.spacing(1.5), + paddingLeft: theme.spacing(2), + paddingRight: theme.spacing(2.5), + width: theme.spacing(30), + + '&:hover': { + background: theme.palette.primary.main, + }, + '& svg': { + color: '#333232 ', + }, + }, +})) + +export default function MyNotificationsTab() { + const { t } = useTranslation() + const router = useRouter() + const { data: user } = getCurrentPerson(!!router.query?.register) + const [showConfirmModal, setShowConfirmModal] = useState(false) + + return ( + + {showConfirmModal && ( + + )} + + {t('profile:myNotifications.status-title')} + + + + + {t('profile:myNotifications.status-msg')} + + + {user?.user.newsletter + ? t('profile:myNotifications.status.active') + : t('profile:myNotifications.status.inactive')} + + + + + {user?.user.newsletter && ( + <> + + + {t('profile:myNotifications.campaign.index')} + + + + + + + )} + + ) +} diff --git a/src/components/client/auth/profile/ProfilePage.tsx b/src/components/client/auth/profile/ProfilePage.tsx index 264c0ce1e..5528a9197 100644 --- a/src/components/client/auth/profile/ProfilePage.tsx +++ b/src/components/client/auth/profile/ProfilePage.tsx @@ -119,6 +119,14 @@ export default function ProfilePage() { onClick={() => router.push(routes.profile.recurringDonations)} icon={matches ? : undefined} /> + router.push(routes.profile.myNotifications)} + icon={matches ? : undefined} + /> {/* Currently we don't generate donation contract, when such document is generated we can either combine it with the certificate or unhide the contracts section. */} {/* ({ mutationFn: useSendConfirmationEmail(), onError: (error) => handleError(error), - onSuccess: (response, data) => { + onSuccess: (response) => { AlertStore.show(t('common:alerts.message-sent'), 'success') setIsSuccess(true) diff --git a/src/components/client/notifications/UNsubscriptionsPage.tsx b/src/components/client/notifications/UNsubscriptionsPage.tsx new file mode 100644 index 000000000..4baa7c576 --- /dev/null +++ b/src/components/client/notifications/UNsubscriptionsPage.tsx @@ -0,0 +1,170 @@ +import { useTranslation } from 'react-i18next' +import Layout from '../layout/Layout' +import PodkrepiLogo from 'components/common/brand/PodkrepiLogo' +import { useRouter } from 'next/router' +import { Button, DialogContent, Grid } from '@mui/material' +import { styled } from '@mui/material/styles' +import LinkButton from 'components/common/LinkButton' +import React, { useEffect, useState } from 'react' +import { AlertStore } from 'stores/AlertStore' +import { useMutation } from '@tanstack/react-query' +import { AxiosError, AxiosResponse } from 'axios' +import { UNsubscribePublicEmailInput, UNsubscribePublicEmailResponse } from 'gql/notification' +import { ApiError } from 'next/dist/server/api-utils' +import { useUNSubscribePublicEmail } from 'service/notification' + +type Props = { + email: string | null + campaign: string | null +} + +type Payload = { + email: string + campaignId?: string | null +} + +const PREFIX = 'notification-subscribe' + +const classes = { + siteBtn: `${PREFIX}-siteBtn`, + loader: `${PREFIX}-loader`, +} + +const StyledGrid = styled(Grid)(({ theme }) => ({ + [`& .${classes.loader}`]: { + animation: 'pulsate 1s infinite', + + '@keyframes pulsate': { + ' 0%': { + transform: 'scale(1)', + }, + '50%': { + transform: 'scale(1.14)', + }, + '100% ': { + transform: ' scale(1)', + }, + }, + }, + + [`& .${classes.siteBtn}`]: { + fontSize: theme.typography.pxToRem(18), + lineHeight: theme.spacing(3), + letterSpacing: theme.spacing(0.05), + color: theme.palette.common.black, + background: `${theme.palette.secondary.main}`, + padding: theme.spacing(1.5), + paddingLeft: theme.spacing(2), + paddingRight: theme.spacing(2.5), + width: theme.spacing(60), + + '&:hover': { + background: theme.palette.primary.main, + }, + '& svg': { + color: '#333232 ', + }, + }, +})) + +export default function UNsubscriptionPage(data: Props) { + const { t } = useTranslation() + const { locale } = useRouter() + const router = useRouter() + + const [loading, setLoading] = useState(true) + const [isSuccess, setIsSuccess] = useState(false) + + useEffect(() => { + if (!data.email) { + router.push('/') + return + } + const payload: Payload = { + email: data.email, + } + if (data.campaign) payload.campaignId = data.campaign + + callUNsubscribeApiRoute(payload).catch(() => console.log()) + }, []) + + const handleError = (e: AxiosError) => { + const error = e.response?.data?.message + setLoading(false) + setIsSuccess(false) + } + + const mutation = useMutation< + AxiosResponse, + AxiosError, + UNsubscribePublicEmailInput + >({ + mutationFn: useUNSubscribePublicEmail(), + onError: (error) => handleError(error), + onSuccess: (response, data) => { + AlertStore.show(t('common:alerts.message-sent'), 'success') + setIsSuccess(true) + setLoading(false) + }, + }) + + async function callUNsubscribeApiRoute(values: { email: string; campaignId?: string | null }) { + setLoading(true) + try { + await mutation.mutateAsync(values) + } finally { + setLoading(false) + } + } + + // Show loader + if (loading) + return ( + + + + + + ) + + return ( + + + + + + {isSuccess ? ( + + + + {t('notifications:unsubscribe.thank-you-msg')} + + + + + {t('notifications:unsubscribe.cta')} + + {' '} + + ) : ( + + + + + {t('notifications:unsubscribe.subscription-fail')} + + + + + {' '} + + + )} + + + ) +} diff --git a/src/gql/notification.ts b/src/gql/notification.ts index 31058db6c..5ce1842e7 100644 --- a/src/gql/notification.ts +++ b/src/gql/notification.ts @@ -1,3 +1,11 @@ +import { Campaign } from './recurring-donation' + +export type NotificationList = { + id: string + campaignId: string + campaign: Campaign +} + export type SendConfirmationEmailInput = { email: string } @@ -14,6 +22,32 @@ export type SubscribePublicEmailInput = { } export type SubscribePublicEmailResponse = { + message: string +} + +export type UNsubscribePublicEmailInput = { email: string - subscribed: boolean + campaignId?: string | null +} + +export type UNsubscribePublicEmailResponse = { + message: string +} + +export type SubscribeEmailInput = { + consent: boolean +} + +export type SubscribeEmailResponse = { + message: string } + +export type UNsubscribeEmailInput = { + campaignId?: string +} + +export type UNsubscribeEmailResponse = { + message: string +} + +export type GetCampaignNotificationSubscriptionsResponse = NotificationList[] diff --git a/src/pages/notifications/subscribe/index.tsx b/src/pages/notifications/subscribe/index.tsx index 5ad9684d9..493d6d3e6 100644 --- a/src/pages/notifications/subscribe/index.tsx +++ b/src/pages/notifications/subscribe/index.tsx @@ -2,7 +2,7 @@ import SubscriptionPage from 'components/client/notifications/SubscriptionPage' import { GetServerSideProps } from 'next' import { serverSideTranslations } from 'next-i18next/serverSideTranslations' -export const getServerSideProps: GetServerSideProps = async ({ locale, query, res }) => { +export const getServerSideProps: GetServerSideProps = async ({ locale, query }) => { const { hash, email, consent, campaign } = query return { diff --git a/src/pages/notifications/unsubscribe/index.tsx b/src/pages/notifications/unsubscribe/index.tsx new file mode 100644 index 000000000..b72996068 --- /dev/null +++ b/src/pages/notifications/unsubscribe/index.tsx @@ -0,0 +1,17 @@ +import UNsubscriptionPage from 'components/client/notifications/UNsubscriptionsPage' +import { GetServerSideProps } from 'next' +import { serverSideTranslations } from 'next-i18next/serverSideTranslations' + +export const getServerSideProps: GetServerSideProps = async ({ locale, query }) => { + const { email, campaign } = query + + return { + props: { + ...(await serverSideTranslations(locale ?? 'bg', ['common', 'campaigns', `notifications`])), + email: email || null, + campaign: campaign || null, + }, + } +} + +export default UNsubscriptionPage diff --git a/src/service/apiEndpoints.ts b/src/service/apiEndpoints.ts index a1a3a345a..59b7ece30 100644 --- a/src/service/apiEndpoints.ts +++ b/src/service/apiEndpoints.ts @@ -50,6 +50,13 @@ export const endpoints = { notifications: { sendConfirmationEmail: { url: '/notifications/send-confirm-email', method: 'POST' }, subscribePublicEmail: { url: '/notifications/public/subscribe', method: 'POST' }, + unsubscribePublicEmail: { url: '/notifications/public/unsubscribe', method: 'POST' }, + subscribeEmail: { url: '/notifications/subscribe', method: 'POST' }, + unsubscribeEmail: { url: '/notifications/unsubscribe', method: 'POST' }, + getCampaignNotificationSubscriptions: { + url: '/notifications/campaign-notifications', + method: 'GET', + }, }, campaignNews: { createNewsArticle: { url: '/campaign-news', method: 'POST' }, diff --git a/src/service/notification.ts b/src/service/notification.ts index a748b7894..5a33269fc 100644 --- a/src/service/notification.ts +++ b/src/service/notification.ts @@ -7,8 +7,14 @@ import { authConfig } from './restRequests' import { SendConfirmationEmailInput, SendConfirmationEmailResponse, + SubscribeEmailInput, + SubscribeEmailResponse, SubscribePublicEmailInput, SubscribePublicEmailResponse, + UNsubscribeEmailInput, + UNsubscribeEmailResponse, + UNsubscribePublicEmailInput, + UNsubscribePublicEmailResponse, } from 'gql/notification' export function useSubscribeToCampaign(id: string) { @@ -38,3 +44,34 @@ export function useSubscribePublicEmail() { >(endpoints.notifications.subscribePublicEmail.url, data) } } + +export function useUNSubscribePublicEmail() { + return async (data: UNsubscribePublicEmailInput) => { + return await apiClient.post< + UNsubscribePublicEmailResponse, + AxiosResponse + >(endpoints.notifications.unsubscribePublicEmail.url, data) + } +} + +export function useSubscribeEmail() { + const { data: session } = useSession() + return async (data: SubscribeEmailInput) => { + return await apiClient.post>( + endpoints.notifications.subscribeEmail.url, + data, + authConfig(session?.accessToken), + ) + } +} + +export function useUNsubscribeEmail() { + const { data: session } = useSession() + return async (data: UNsubscribeEmailInput) => { + return await apiClient.post>( + endpoints.notifications.unsubscribeEmail.url, + data, + authConfig(session?.accessToken), + ) + } +} From 2abbd78a5664ab42764b19ae6ab6e24dd827b504 Mon Sep 17 00:00:00 2001 From: Bogsana Date: Sun, 6 Aug 2023 13:19:17 +0300 Subject: [PATCH 4/7] add campaign news notify check-box --- public/locales/bg/campaigns.json | 1 + .../client/campaign-news/secured/EditForm.tsx | 13 +++++++++++++ src/gql/campaign-news.ts | 1 + 3 files changed, 15 insertions(+) diff --git a/public/locales/bg/campaigns.json b/public/locales/bg/campaigns.json index 6248a0f83..18aa49c90 100644 --- a/public/locales/bg/campaigns.json +++ b/public/locales/bg/campaigns.json @@ -86,6 +86,7 @@ "subheading-bold": "Дори и най-малката помощ може да бъде двигател на голяма промяна.", "subheading-bold-secondary": "Заедно подкрепяме и насърчаваме дарителската и доброволческата култура в България!", "title": "Име на кампанията", + "notify": "Изпрати известие до всички абонирани", "slug": { "name": "Кратко наименование на кампанията", "warning": "Редактирането на това поле ще измени линка към кампанията и ще направи текущия невалиден", diff --git a/src/components/client/campaign-news/secured/EditForm.tsx b/src/components/client/campaign-news/secured/EditForm.tsx index e518ff776..1f1cac29c 100644 --- a/src/components/client/campaign-news/secured/EditForm.tsx +++ b/src/components/client/campaign-news/secured/EditForm.tsx @@ -50,6 +50,7 @@ import { useEditNewsArticle, useUploadCampaignNewsFiles } from 'service/campaign import UploadedCampaignFile from 'components/admin/campaign-news/UploadedCampaignFile' import CampaignDropdownSelector from 'components/admin/campaign-news/CampaignDropdownSelector' +import CheckboxField from 'components/common/form/CheckboxField' const validationSchema: yup.SchemaOf = yup .object() @@ -62,6 +63,7 @@ const validationSchema: yup.SchemaOf = yup sourceLink: yup.string().optional(), state: yup.mixed().oneOf(Object.values(ArticleStatus)).required(), description: yup.string().required(), + notify: yup.bool().required(), }) type Props = { @@ -87,6 +89,7 @@ export default function EditForm({ article, campaignId = '', isAdmin = true }: P sourceLink: article?.sourceLink || '', state: article?.state, description: article?.description, + notify: false, } const handleError = (e: AxiosError) => { @@ -140,6 +143,7 @@ export default function EditForm({ article, campaignId = '', isAdmin = true }: P sourceLink: values.sourceLink, state: values.state, description: values.description, + notify: values.notify, }) if (files.length > 0) { @@ -254,6 +258,15 @@ export default function EditForm({ article, campaignId = '', isAdmin = true }: P {isAdmin && } + + {isAdmin && ( + {t('campaigns:campaign.notify')} + }> + )} + {t('campaigns:campaign.description')} diff --git a/src/gql/campaign-news.ts b/src/gql/campaign-news.ts index b2bca6e25..8fa206173 100644 --- a/src/gql/campaign-news.ts +++ b/src/gql/campaign-news.ts @@ -76,4 +76,5 @@ export type CampaignNewsInput = { sourceLink: string | undefined description: string state: ArticleStatus + notify: boolean } From bfb906b330175d40c0b9c4f982188f910d659939 Mon Sep 17 00:00:00 2001 From: Bogsana Date: Tue, 8 Aug 2023 00:08:23 +0300 Subject: [PATCH 5/7] Refactor button to link --- .../client/campaigns/CampaignDetails.tsx | 32 ----------------- .../client/campaigns/InlineDonation.tsx | 35 +++++++++++++++++-- .../client/layout/Footer/Footer.styled.tsx | 28 ++++++--------- .../client/layout/Footer/SubscribeBtn.tsx | 14 +++++--- 4 files changed, 52 insertions(+), 57 deletions(-) diff --git a/src/components/client/campaigns/CampaignDetails.tsx b/src/components/client/campaigns/CampaignDetails.tsx index 1cda190cd..6144a63e1 100644 --- a/src/components/client/campaigns/CampaignDetails.tsx +++ b/src/components/client/campaigns/CampaignDetails.tsx @@ -35,7 +35,6 @@ const PREFIX = 'CampaignDetails' const classes = { banner: `${PREFIX}-banner`, - subscribeBtn: `${PREFIX}-subscribe`, campaignTitle: `${PREFIX}-campaignTitle`, linkButton: `${PREFIX}-linkButton`, securityIcon: `${PREFIX}-securityIcon`, @@ -91,25 +90,6 @@ const StyledGrid = styled(Grid)(({ theme }) => ({ width: theme.spacing(2.25), height: theme.spacing(2.75), }, - - [`& .${classes.subscribeBtn}`]: { - fontSize: theme.typography.pxToRem(16), - lineHeight: theme.spacing(3), - letterSpacing: theme.spacing(0.05), - color: theme.palette.common.black, - background: `${theme.palette.secondary.main}`, - padding: theme.spacing(1.5), - paddingLeft: theme.spacing(2), - paddingRight: theme.spacing(2.5), - width: '50%', - - '&:hover': { - background: theme.palette.primary.main, - }, - '& svg': { - color: '#333232 ', - }, - }, })) type Props = { @@ -122,7 +102,6 @@ export default function CampaignDetails({ campaign }: Props) { const canEditCampaign = useCanEditCampaign(campaign.slug) const { data: expensesList } = useCampaignApprovedExpensesList(campaign.slug) const totalExpenses = expensesList?.reduce((acc, expense) => acc + expense.amount, 0) - const [subscribeIsOpen, setSubscribeOpen] = useState(false) return ( @@ -133,17 +112,6 @@ export default function CampaignDetails({ campaign }: Props) { campaign={campaign} showExpensesLink={(expensesList && expensesList?.length > 0) || canEditCampaign} /> - {subscribeIsOpen && ( - - )} - - - diff --git a/src/components/client/campaigns/InlineDonation.tsx b/src/components/client/campaigns/InlineDonation.tsx index 898f341f7..ba19f3d70 100644 --- a/src/components/client/campaigns/InlineDonation.tsx +++ b/src/components/client/campaigns/InlineDonation.tsx @@ -5,8 +5,8 @@ import { useRouter } from 'next/router' import { CampaignResponse } from 'gql/campaigns' -import { Button, CircularProgress, Grid, IconButton, Menu, Typography } from '@mui/material' -import { AddLinkOutlined, Favorite } from '@mui/icons-material' +import { Button, CircularProgress, Grid, Icon, IconButton, Menu, Typography } from '@mui/material' +import { AddLinkOutlined, Favorite, Mail, MarkEmailUnread } from '@mui/icons-material' import { lighten } from '@mui/material/styles' import { styled } from '@mui/material/styles' import ExpandLessIcon from '@mui/icons-material/ExpandLess' @@ -29,6 +29,7 @@ import CustomListItem from 'components/common/navigation/CustomListItem' import { socialMedia } from './helpers/socialMedia' import { CampaignState } from './helpers/campaign.enums' import { AlertStore } from 'stores/AlertStore' +import RenderCampaignSubscribeModal from '../notifications/CampaignSubscribeModal' const PREFIX = 'InlineDonation' @@ -51,6 +52,7 @@ const classes = { campaignInfoKey: `${PREFIX}-campaignInfoKey`, campaignInfoValue: `${PREFIX}-campaignInfoValue`, pagination: `${PREFIX}-pagination`, + subscribeLink: `${PREFIX}-subscribe`, } const StyledGrid = styled(Grid)(({ theme }) => ({ @@ -208,6 +210,19 @@ const StyledGrid = styled(Grid)(({ theme }) => ({ fontSize: theme.typography.pxToRem(15), }, }, + + [`& .${classes.subscribeLink}`]: { + fontWeight: 500, + fontSize: theme.typography.pxToRem(16.5), + textAlign: 'center', + + '&:hover': { + 'text-decoration': 'underline', + transform: 'scale(1.01)', + cursor: 'pointer', + transition: 'all 0.3s ease', + }, + }, })) type Props = { @@ -218,6 +233,7 @@ export default function InlineDonation({ campaign }: Props) { const { t } = useTranslation('campaigns') const { asPath } = useRouter() const [status, copyUrl] = useCopyToClipboard(baseUrl + asPath, 1000) + const [subscribeIsOpen, setSubscribeOpen] = useState(false) const active = status === 'copied' ? 'inherit' : 'primary' const [page, setPage] = useState(0) const pageSize = 5 @@ -319,6 +335,21 @@ export default function InlineDonation({ campaign }: Props) { + {subscribeIsOpen && ( + + )} + + setSubscribeOpen(true)} className={classes.subscribeLink}> + {t('campaigns:cta.subscribe')} + + setSubscribeOpen(true)} cursor="pointer"> + {detailsShown && (donationHistoryError ? ( 'Error fetching donation history' diff --git a/src/components/client/layout/Footer/Footer.styled.tsx b/src/components/client/layout/Footer/Footer.styled.tsx index a18f86e56..a42f27216 100644 --- a/src/components/client/layout/Footer/Footer.styled.tsx +++ b/src/components/client/layout/Footer/Footer.styled.tsx @@ -61,25 +61,17 @@ export const SocialIconsWrapper = styled(Grid)(() => ({ marginTop: theme.spacing(2), })) -export const SubscribeBtnWrapper = styled(Grid)(({ theme }) => ({ - [`& button`]: { +export const SubscribeLinkWrapper = styled(Grid)(({ theme }) => ({ + [`& p`]: { fontWeight: 500, - fontSize: theme.typography.pxToRem(16), - background: `${theme.palette.secondary.main}`, - color: theme.palette.common.black, - padding: theme.spacing(0.5), - paddingLeft: theme.spacing(1.2), - paddingRight: theme.spacing(1.2), + fontSize: theme.typography.pxToRem(17), + marginRight: theme.spacing(0.7), + }, - '&:hover': { - background: theme.palette.secondary.main, - opacity: 0.9, - color: theme.palette.common.black, - transform: 'scale(1.05)', - transition: 'all 0.3s ease', - }, - '& svg': { - color: '#ab2f26', - }, + '&:hover': { + 'text-decoration': 'underline', + transform: 'scale(1.03)', + cursor: 'pointer', + transition: 'all 0.3s ease', }, })) diff --git a/src/components/client/layout/Footer/SubscribeBtn.tsx b/src/components/client/layout/Footer/SubscribeBtn.tsx index 92e0981b4..e0bd05f7e 100644 --- a/src/components/client/layout/Footer/SubscribeBtn.tsx +++ b/src/components/client/layout/Footer/SubscribeBtn.tsx @@ -1,17 +1,21 @@ -import { Button } from '@mui/material' +import { , Typography } from '@mui/material' import { useTranslation } from 'react-i18next' -import { SubscribeBtnWrapper } from './Footer.styled' +import { SubscribeLinkWrapper } from './Footer.styled' import { useState } from 'react' import RenderSubscribeModal from 'components/client/notifications/GeneralSubscribeModal' +import { MarkEmailUnread } from '@mui/icons-material' export const SubscribeBtn = () => { const { t } = useTranslation() const [subscribeIsOpen, setSubscribeOpen] = useState(false) return ( - + {subscribeIsOpen && } - - + setSubscribeOpen(true)}> + {t('components.footer.subscribe')} + + setSubscribeOpen(true)} cursor="pointer"> + ) } From 73462e1933f54679ef871f72aac7daee64fc33ac Mon Sep 17 00:00:00 2001 From: Bogsana Date: Wed, 9 Aug 2023 08:58:12 +0300 Subject: [PATCH 6/7] fix broken import --- src/components/client/layout/Footer/SubscribeBtn.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/client/layout/Footer/SubscribeBtn.tsx b/src/components/client/layout/Footer/SubscribeBtn.tsx index e0bd05f7e..641dfa6b4 100644 --- a/src/components/client/layout/Footer/SubscribeBtn.tsx +++ b/src/components/client/layout/Footer/SubscribeBtn.tsx @@ -1,4 +1,4 @@ -import { , Typography } from '@mui/material' +import { Typography } from '@mui/material' import { useTranslation } from 'react-i18next' import { SubscribeLinkWrapper } from './Footer.styled' import { useState } from 'react' From 43e330e297c339a29459fae75b50bb51c27bbb7d Mon Sep 17 00:00:00 2001 From: Bogsana Date: Wed, 9 Aug 2023 11:05:28 +0300 Subject: [PATCH 7/7] final fixes --- src/components/client/auth/profile/MyNotificationsTab.tsx | 5 ++--- src/components/client/campaign-news/secured/CreateForm.tsx | 3 +++ src/components/client/campaign-news/secured/EditForm.tsx | 5 ++--- src/components/client/campaigns/InlineDonation.tsx | 2 +- src/components/client/layout/Footer/SubscribeBtn.tsx | 2 +- src/gql/campaign-news.ts | 1 + 6 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/components/client/auth/profile/MyNotificationsTab.tsx b/src/components/client/auth/profile/MyNotificationsTab.tsx index ae50073a8..e7595b747 100644 --- a/src/components/client/auth/profile/MyNotificationsTab.tsx +++ b/src/components/client/auth/profile/MyNotificationsTab.tsx @@ -101,9 +101,8 @@ export default function MyNotificationsTab() { {showConfirmModal && ( + type={user?.user.newsletter ? 'unsubscribe' : 'subscribe'} + /> )} {t('profile:myNotifications.status-title')} diff --git a/src/components/client/campaign-news/secured/CreateForm.tsx b/src/components/client/campaign-news/secured/CreateForm.tsx index e15341692..13290a3f6 100644 --- a/src/components/client/campaign-news/secured/CreateForm.tsx +++ b/src/components/client/campaign-news/secured/CreateForm.tsx @@ -56,6 +56,7 @@ const validationSchema: yup.SchemaOf = yup description: yup.string().required(), terms: yup.bool().required().oneOf([true], 'validation:terms-of-use'), gdpr: yup.bool().required().oneOf([true], 'validation:terms-of-service'), + notify: yup.bool().required(), }) export type CampaignFormProps = { @@ -80,6 +81,7 @@ export default function CreateForm({ campaignId = '', isAdmin = true }: Campaign description: '', terms: false, gdpr: false, + notify: false, } const handleError = (e: AxiosError) => { @@ -124,6 +126,7 @@ export default function CreateForm({ campaignId = '', isAdmin = true }: Campaign sourceLink: values.sourceLink, description: values.description, state: ArticleStatus.draft, + notify: values.notify, }) if (files.length > 0) { await fileUploadMutation.mutateAsync({ diff --git a/src/components/client/campaign-news/secured/EditForm.tsx b/src/components/client/campaign-news/secured/EditForm.tsx index 1f1cac29c..d7f948910 100644 --- a/src/components/client/campaign-news/secured/EditForm.tsx +++ b/src/components/client/campaign-news/secured/EditForm.tsx @@ -262,9 +262,8 @@ export default function EditForm({ article, campaignId = '', isAdmin = true }: P {isAdmin && ( {t('campaigns:campaign.notify')} - }> + label={{t('campaigns:campaign.notify')}} + /> )} diff --git a/src/components/client/campaigns/InlineDonation.tsx b/src/components/client/campaigns/InlineDonation.tsx index ba19f3d70..b5970c5f4 100644 --- a/src/components/client/campaigns/InlineDonation.tsx +++ b/src/components/client/campaigns/InlineDonation.tsx @@ -348,7 +348,7 @@ export default function InlineDonation({ campaign }: Props) { setSubscribeOpen(true)} className={classes.subscribeLink}> {t('campaigns:cta.subscribe')} - setSubscribeOpen(true)} cursor="pointer"> + setSubscribeOpen(true)} cursor="pointer" /> {detailsShown && (donationHistoryError ? ( diff --git a/src/components/client/layout/Footer/SubscribeBtn.tsx b/src/components/client/layout/Footer/SubscribeBtn.tsx index 641dfa6b4..65bee5a63 100644 --- a/src/components/client/layout/Footer/SubscribeBtn.tsx +++ b/src/components/client/layout/Footer/SubscribeBtn.tsx @@ -15,7 +15,7 @@ export const SubscribeBtn = () => { setSubscribeOpen(true)}> {t('components.footer.subscribe')} - setSubscribeOpen(true)} cursor="pointer"> + setSubscribeOpen(true)} cursor="pointer" /> ) } diff --git a/src/gql/campaign-news.ts b/src/gql/campaign-news.ts index 8fa206173..15209a37d 100644 --- a/src/gql/campaign-news.ts +++ b/src/gql/campaign-news.ts @@ -66,6 +66,7 @@ export type CampaignNewsAdminCreateFormData = { description: string terms: boolean gdpr: boolean + notify: boolean } export type CampaignNewsInput = {