From 24aed1178f246952c2c2dd3b60f3bf001108c8d3 Mon Sep 17 00:00:00 2001 From: Caleb Cox Date: Thu, 22 Aug 2024 16:36:17 -0500 Subject: [PATCH 1/4] Support customized privacy policy and terms of use URLs --- README.md | 2 ++ next.config.js | 2 ++ .../ProfileMenuPanel/ProfileMenuPanel.tsx | 24 ++++++------------- .../TopBar/Items/ProfileMenu/ProfileMenu.tsx | 21 +++++----------- src/components/Shared/Links/Links.test.tsx | 20 ++++++++++++++++ src/components/Shared/Links/Links.tsx | 22 +++++++++++++++++ 6 files changed, 59 insertions(+), 32 deletions(-) create mode 100644 src/components/Shared/Links/Links.test.tsx create mode 100644 src/components/Shared/Links/Links.tsx diff --git a/README.md b/README.md index ecdb55b9d..a7967b492 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,8 @@ Note: there is a test account you can use. Get this from another developer if yo - `HS_HOME_SUGGESTIONS` - Comma-separated IDs of the HelpScout articles to suggest on the dashboard page - `HS_REPORTS_SUGGESTIONS` - Comma-separated IDs of the HelpScout articles to suggest on the reports pages - `HS_TASKS_SUGGESTIONS` - Comma-separated IDs of the HelpScout articles to suggest on the tasks page +- `PRIVACY_POLICY_URL` - URL of the privacy policy +- `TERMS_OF_USE_URL` - URL of the terms of use #### Auth provider diff --git a/next.config.js b/next.config.js index cc1dacf82..86d0e4f0c 100644 --- a/next.config.js +++ b/next.config.js @@ -96,6 +96,8 @@ const config = { process.env.HS_SETTINGS_SERVICES_SUGGESTIONS, HS_SETUP_FIND_ORGANIZATION: process.env.HS_SETUP_FIND_ORGANIZATION, ALERT_MESSAGE: process.env.ALERT_MESSAGE, + PRIVACY_POLICY_URL: process.env.PRIVACY_POLICY_URL, + TERMS_OF_USE_URL: process.env.TERMS_OF_USE_URL, DD_ENV: process.env.DD_ENV ?? 'development', }, experimental: { diff --git a/src/components/Layouts/Primary/NavBar/NavTools/ProfileMenuPanel/ProfileMenuPanel.tsx b/src/components/Layouts/Primary/NavBar/NavTools/ProfileMenuPanel/ProfileMenuPanel.tsx index a856171c1..e3a2f1867 100644 --- a/src/components/Layouts/Primary/NavBar/NavTools/ProfileMenuPanel/ProfileMenuPanel.tsx +++ b/src/components/Layouts/Primary/NavBar/NavTools/ProfileMenuPanel/ProfileMenuPanel.tsx @@ -3,10 +3,14 @@ import React, { useState } from 'react'; import { useApolloClient } from '@apollo/client'; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import ChevronRight from '@mui/icons-material/ChevronRight'; -import { Box, Button, Drawer, List, Link as MuiLink } from '@mui/material'; +import { Box, Button, Drawer, List } from '@mui/material'; import { styled } from '@mui/material/styles'; import { signOut } from 'next-auth/react'; import { useTranslation } from 'react-i18next'; +import { + PrivacyPolicyLink, + TermsOfUseLink, +} from 'src/components/Shared/Links/Links'; import { NextLinkComposed } from 'src/components/common/Links/NextLinkComposed'; import { clearDataDogUser } from 'src/lib/dataDog'; import { useAccountListId } from '../../../../../../hooks/useAccountListId'; @@ -222,23 +226,9 @@ export const ProfileMenuPanel: React.FC = () => { {t('Sign Out')} - - {t('Privacy Policy')} - +   •   - - {t('Terms of Use')} - + diff --git a/src/components/Layouts/Primary/TopBar/Items/ProfileMenu/ProfileMenu.tsx b/src/components/Layouts/Primary/TopBar/Items/ProfileMenu/ProfileMenu.tsx index 8aee0ec50..478cf794a 100644 --- a/src/components/Layouts/Primary/TopBar/Items/ProfileMenu/ProfileMenu.tsx +++ b/src/components/Layouts/Primary/TopBar/Items/ProfileMenu/ProfileMenu.tsx @@ -15,13 +15,16 @@ import { ListItemText, Menu, MenuItem, - Link as MuiLink, Typography, } from '@mui/material'; import { styled } from '@mui/material/styles'; import { signOut } from 'next-auth/react'; import { useSnackbar } from 'notistack'; import { useTranslation } from 'react-i18next'; +import { + PrivacyPolicyLink, + TermsOfUseLink, +} from 'src/components/Shared/Links/Links'; import { AccountList } from 'src/graphql/types.generated'; import { useRequiredSession } from 'src/hooks/useRequiredSession'; import { clearDataDogUser } from 'src/lib/dataDog'; @@ -374,21 +377,9 @@ const ProfileMenu = (): ReactElement => { )} - - {t('Privacy Policy')} - +   •   - - {t('Terms of Use')} - + diff --git a/src/components/Shared/Links/Links.test.tsx b/src/components/Shared/Links/Links.test.tsx new file mode 100644 index 000000000..35356255a --- /dev/null +++ b/src/components/Shared/Links/Links.test.tsx @@ -0,0 +1,20 @@ +import { render } from '@testing-library/react'; +import { PrivacyPolicyLink, TermsOfUseLink } from './Links'; + +describe('PrivacyPolicyLink', () => { + it('uses the link from an environment variable', () => { + process.env.PRIVACY_POLICY_URL = 'privacy-policy.com'; + + const { getByRole } = render(); + expect(getByRole('link')).toHaveAttribute('href', 'privacy-policy.com'); + }); +}); + +describe('TermsOfUseLink', () => { + it('uses the link from an environment variable', () => { + process.env.TERMS_OF_USE_URL = 'terms-of-use.com'; + + const { getByRole } = render(); + expect(getByRole('link')).toHaveAttribute('href', 'terms-of-use.com'); + }); +}); diff --git a/src/components/Shared/Links/Links.tsx b/src/components/Shared/Links/Links.tsx new file mode 100644 index 000000000..f4a705db9 --- /dev/null +++ b/src/components/Shared/Links/Links.tsx @@ -0,0 +1,22 @@ +import { Link, LinkProps } from '@mui/material'; +import { useTranslation } from 'react-i18next'; + +export const PrivacyPolicyLink: React.FC = (props) => { + const { t } = useTranslation(); + + return ( + + {t('Privacy Policy')} + + ); +}; + +export const TermsOfUseLink: React.FC = (props) => { + const { t } = useTranslation(); + + return ( + + {t('Terms of Use')} + + ); +}; From 1efbbbfc2443d46dc9016485793344ae5c897f8d Mon Sep 17 00:00:00 2001 From: Caleb Cox Date: Thu, 22 Aug 2024 16:36:50 -0500 Subject: [PATCH 2/4] Create setup start page --- pages/setup/start.page.test.tsx | 39 +++++++++ pages/setup/start.page.tsx | 103 +++++++++++++++++++++++ src/components/Setup/PageHeader.tsx | 30 +++++++ src/components/Setup/styledComponents.ts | 17 ++++ src/components/User/GetUser.graphql | 1 + 5 files changed, 190 insertions(+) create mode 100644 pages/setup/start.page.test.tsx create mode 100644 pages/setup/start.page.tsx create mode 100644 src/components/Setup/PageHeader.tsx create mode 100644 src/components/Setup/styledComponents.ts diff --git a/pages/setup/start.page.test.tsx b/pages/setup/start.page.test.tsx new file mode 100644 index 000000000..57b1d9d7d --- /dev/null +++ b/pages/setup/start.page.test.tsx @@ -0,0 +1,39 @@ +import { render, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import TestRouter from '__tests__/util/TestRouter'; +import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; +import StartPage from './start.page'; + +const push = jest.fn(); +const router = { + push, +}; + +describe('Setup start page', () => { + it('autocomplete renders and button saves and advances to the next page', async () => { + Object.defineProperty(window, 'navigator', { + value: { ...window.navigator, language: 'fr-FR' }, + }); + + const mutationSpy = jest.fn(); + const { getByRole } = render( + + + + + , + ); + + const autocomplete = getByRole('combobox'); + expect(autocomplete).toHaveValue('French (français)'); + userEvent.click(autocomplete); + userEvent.click(getByRole('option', { name: 'German (Deutsch)' })); + userEvent.click(getByRole('button', { name: "Let's Begin" })); + await waitFor(() => + expect(mutationSpy).toHaveGraphqlOperation('UpdatePersonalPreferences', { + input: { attributes: { locale: 'de' } }, + }), + ); + expect(push).toHaveBeenCalledWith('/setup/connect'); + }); +}); diff --git a/pages/setup/start.page.tsx b/pages/setup/start.page.tsx new file mode 100644 index 000000000..5e6caef51 --- /dev/null +++ b/pages/setup/start.page.tsx @@ -0,0 +1,103 @@ +import Head from 'next/head'; +import { useRouter } from 'next/router'; +import React, { ReactElement, useState } from 'react'; +import { Autocomplete, TextField } from '@mui/material'; +import { useTranslation } from 'react-i18next'; +import { useUpdatePersonalPreferencesMutation } from 'src/components/Settings/preferences/accordions/UpdatePersonalPreferences.generated'; +import { PageHeader } from 'src/components/Setup/PageHeader'; +import { + LargeButton, + PageWrapper, +} from 'src/components/Setup/styledComponents'; +import { + PrivacyPolicyLink, + TermsOfUseLink, +} from 'src/components/Shared/Links/Links'; +import useGetAppSettings from 'src/hooks/useGetAppSettings'; +import { formatLanguage, languages } from 'src/lib/data/languages'; +import { loadSession } from '../api/utils/pagePropsHelpers'; + +// This is the first page of the tour, and it lets users choose their language. It is always shown. +const StartPage = (): ReactElement => { + const { t } = useTranslation(); + const { appName } = useGetAppSettings(); + const { push } = useRouter(); + const [savePreferences] = useUpdatePersonalPreferencesMutation(); + + const [locale, setLocale] = useState( + (typeof window === 'undefined' + ? null + : window.navigator.language.toLowerCase()) || 'en-us', + ); + + const handleSave = async () => { + await savePreferences({ + variables: { + input: { + attributes: { + locale, + }, + }, + }, + }); + push('/setup/connect'); + }; + + return ( + <> + + + {appName} | {t('Setup - Start')} + + + + +

+ {t( + `Developing a healthy team of ministry partners sets your ministry up to thrive. +{{appName}} is designed to help you do the right things, with the right people at the right time to be fully funded.`, + { appName }, + )} +

+

+ {t( + `To get started, we're going to walk with you through a few key steps to set you up for success in {{appName}}!`, + { appName }, + )} +

+

{t('It looks like you speak')}

+ { + setLocale(value); + }} + options={languages.map((language) => language.id) || []} + getOptionLabel={(locale) => formatLanguage(locale)} + fullWidth + renderInput={(params) => ( + + )} + /> +

+ {t('By Clicking "Let\'s Begin!" you have read and agree to the ')} + + {t(' and the ')} + +

+ + {t("Let's Begin")} + +
+ + ); +}; + +export const getServerSideProps = loadSession; + +export default StartPage; diff --git a/src/components/Setup/PageHeader.tsx b/src/components/Setup/PageHeader.tsx new file mode 100644 index 000000000..2ad4eba18 --- /dev/null +++ b/src/components/Setup/PageHeader.tsx @@ -0,0 +1,30 @@ +import { CampaignOutlined } from '@mui/icons-material'; +import { Box, Divider, Typography } from '@mui/material'; +import { styled } from '@mui/material/styles'; + +const StyledIcon = styled(CampaignOutlined)(({ theme }) => ({ + width: 'auto', + height: theme.spacing(8), +})); + +const Wrapper = styled(Box)(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + gap: theme.spacing(1), +})); + +const StyledTypography = styled(Typography)({ fontWeight: 'bold' }); + +interface PageHeaderProps { + title: string; +} + +export const PageHeader: React.FC = ({ title }) => ( + <> + + + {title} + + + +); diff --git a/src/components/Setup/styledComponents.ts b/src/components/Setup/styledComponents.ts new file mode 100644 index 000000000..69aad2e1a --- /dev/null +++ b/src/components/Setup/styledComponents.ts @@ -0,0 +1,17 @@ +import { Box, Button } from '@mui/material'; +import { styled } from '@mui/material/styles'; + +export const PageWrapper = styled(Box)(({ theme }) => ({ + marginInline: 'auto', + padding: theme.spacing(4), + display: 'flex', + flexDirection: 'column', + gap: '1rem', + alignItems: 'center', + textAlign: 'center', + maxWidth: theme.spacing(100), +})); + +export const LargeButton = styled(Button)(({ theme }) => ({ + fontSize: theme.spacing(4), +})); diff --git a/src/components/User/GetUser.graphql b/src/components/User/GetUser.graphql index 1e5f329d3..fd2f8e693 100644 --- a/src/components/User/GetUser.graphql +++ b/src/components/User/GetUser.graphql @@ -10,6 +10,7 @@ query GetUser { email } preferences { + id language: locale locale: localeDisplay } From 468a345c66bfd9c8e85551ee81fb56d4558c9322 Mon Sep 17 00:00:00 2001 From: Caleb Cox Date: Mon, 26 Aug 2024 11:54:05 -0500 Subject: [PATCH 3/4] Make setup pages a card on a blue background --- pages/setup/start.page.tsx | 12 ++--- src/components/Setup/PageHeader.tsx | 30 ----------- src/components/Setup/SetupPage.tsx | 65 ++++++++++++++++++++++++ src/components/Setup/styledComponents.ts | 13 +---- 4 files changed, 70 insertions(+), 50 deletions(-) delete mode 100644 src/components/Setup/PageHeader.tsx create mode 100644 src/components/Setup/SetupPage.tsx diff --git a/pages/setup/start.page.tsx b/pages/setup/start.page.tsx index 5e6caef51..92bbc5222 100644 --- a/pages/setup/start.page.tsx +++ b/pages/setup/start.page.tsx @@ -4,11 +4,8 @@ import React, { ReactElement, useState } from 'react'; import { Autocomplete, TextField } from '@mui/material'; import { useTranslation } from 'react-i18next'; import { useUpdatePersonalPreferencesMutation } from 'src/components/Settings/preferences/accordions/UpdatePersonalPreferences.generated'; -import { PageHeader } from 'src/components/Setup/PageHeader'; -import { - LargeButton, - PageWrapper, -} from 'src/components/Setup/styledComponents'; +import { SetupPage } from 'src/components/Setup/SetupPage'; +import { LargeButton } from 'src/components/Setup/styledComponents'; import { PrivacyPolicyLink, TermsOfUseLink, @@ -50,8 +47,7 @@ const StartPage = (): ReactElement => { {appName} | {t('Setup - Start')} - - +

{t( `Developing a healthy team of ministry partners sets your ministry up to thrive. @@ -93,7 +89,7 @@ const StartPage = (): ReactElement => { {t("Let's Begin")} - + ); }; diff --git a/src/components/Setup/PageHeader.tsx b/src/components/Setup/PageHeader.tsx deleted file mode 100644 index 2ad4eba18..000000000 --- a/src/components/Setup/PageHeader.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { CampaignOutlined } from '@mui/icons-material'; -import { Box, Divider, Typography } from '@mui/material'; -import { styled } from '@mui/material/styles'; - -const StyledIcon = styled(CampaignOutlined)(({ theme }) => ({ - width: 'auto', - height: theme.spacing(8), -})); - -const Wrapper = styled(Box)(({ theme }) => ({ - display: 'flex', - alignItems: 'center', - gap: theme.spacing(1), -})); - -const StyledTypography = styled(Typography)({ fontWeight: 'bold' }); - -interface PageHeaderProps { - title: string; -} - -export const PageHeader: React.FC = ({ title }) => ( - <> - - - {title} - - - -); diff --git a/src/components/Setup/SetupPage.tsx b/src/components/Setup/SetupPage.tsx new file mode 100644 index 000000000..ada166774 --- /dev/null +++ b/src/components/Setup/SetupPage.tsx @@ -0,0 +1,65 @@ +import React, { ReactNode } from 'react'; +import { CampaignOutlined } from '@mui/icons-material'; +import { Card, CardContent, Divider, Typography } from '@mui/material'; +import { styled } from '@mui/material/styles'; + +const PageBackground = styled('div')(({ theme }) => ({ + minWidth: '100vw', + // Compensate for varying toolbar height + minHeight: 'calc(100vh - var(--toolbar-height))', + '--toolbar-height': '64px', + '@media (min-width: 0px) and (orientation: landscape)': { + '--toolbar-height': '48px', + }, + '@media (min-width: 600px)': { + '--toolbar-height': '64px', + }, + backgroundColor: theme.palette.primary.main, + paddingTop: theme.spacing(8), +})); + +const PageCard = styled(Card)(({ theme }) => ({ + marginInline: 'auto', + maxWidth: theme.spacing(100), +})); + +const PageContent = styled(CardContent)(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(3), + alignItems: 'center', + textAlign: 'center', +})); + +const StyledIcon = styled(CampaignOutlined)(({ theme }) => ({ + width: 'auto', + height: theme.spacing(8), +})); + +const HeaderWrapper = styled('div')(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + gap: theme.spacing(1), +})); + +const HeaderTypography = styled(Typography)({ fontWeight: 'bold' }); + +interface SetupPageProps { + title: string; + children: ReactNode; +} + +export const SetupPage: React.FC = ({ title, children }) => ( + + + + + + {title} + + + {children} + + + +); diff --git a/src/components/Setup/styledComponents.ts b/src/components/Setup/styledComponents.ts index 69aad2e1a..91a8b77e4 100644 --- a/src/components/Setup/styledComponents.ts +++ b/src/components/Setup/styledComponents.ts @@ -1,17 +1,6 @@ -import { Box, Button } from '@mui/material'; +import { Button } from '@mui/material'; import { styled } from '@mui/material/styles'; -export const PageWrapper = styled(Box)(({ theme }) => ({ - marginInline: 'auto', - padding: theme.spacing(4), - display: 'flex', - flexDirection: 'column', - gap: '1rem', - alignItems: 'center', - textAlign: 'center', - maxWidth: theme.spacing(100), -})); - export const LargeButton = styled(Button)(({ theme }) => ({ fontSize: theme.spacing(4), })); From 970597b68bdce3327ff2b7ad435a2bd2eb8cea0d Mon Sep 17 00:00:00 2001 From: Caleb Cox Date: Tue, 27 Aug 2024 10:47:45 -0500 Subject: [PATCH 4/4] Tweak styles --- pages/setup/start.page.tsx | 2 +- src/components/Setup/SetupPage.tsx | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/pages/setup/start.page.tsx b/pages/setup/start.page.tsx index 92bbc5222..973f06904 100644 --- a/pages/setup/start.page.tsx +++ b/pages/setup/start.page.tsx @@ -86,7 +86,7 @@ const StartPage = (): ReactElement => { {t(' and the ')}

- + {t("Let's Begin")}
diff --git a/src/components/Setup/SetupPage.tsx b/src/components/Setup/SetupPage.tsx index ada166774..5535c059c 100644 --- a/src/components/Setup/SetupPage.tsx +++ b/src/components/Setup/SetupPage.tsx @@ -20,7 +20,7 @@ const PageBackground = styled('div')(({ theme }) => ({ const PageCard = styled(Card)(({ theme }) => ({ marginInline: 'auto', - maxWidth: theme.spacing(100), + maxWidth: theme.spacing(75), })); const PageContent = styled(CardContent)(({ theme }) => ({ @@ -34,6 +34,7 @@ const PageContent = styled(CardContent)(({ theme }) => ({ const StyledIcon = styled(CampaignOutlined)(({ theme }) => ({ width: 'auto', height: theme.spacing(8), + color: theme.palette.primary.main, })); const HeaderWrapper = styled('div')(({ theme }) => ({ @@ -42,7 +43,10 @@ const HeaderWrapper = styled('div')(({ theme }) => ({ gap: theme.spacing(1), })); -const HeaderTypography = styled(Typography)({ fontWeight: 'bold' }); +const HeaderTypography = styled(Typography)({ + fontSize: '2.75rem', + fontWeight: 'bold', +}); interface SetupPageProps { title: string;