From 2506a0f31a28f945e89d272aedc54696bc6693cd Mon Sep 17 00:00:00 2001 From: Tom Caiger Date: Thu, 7 Nov 2024 14:51:53 +1300 Subject: [PATCH] mobile survey select page --- .../CountrySelector/CountrySelector.tsx | 3 +- .../src/features/CountrySelector/index.ts | 2 +- .../CountrySelector/useUserCountries.ts | 2 - .../src/features/GroupedSurveyList.tsx | 84 +-------- .../features/MobileSelectList/ListItem.tsx | 106 +++++++++++ .../MobileSelectList/MobileSelectList.tsx | 97 ++++++++++ .../src/features/MobileSelectList/index.ts | 6 + packages/datatrak-web/src/features/index.ts | 6 +- .../src/features/useGroupedSurveyList.tsx | 92 +++++++++ .../src/layout/BackgroundPageLayout.tsx | 4 + .../src/layout/Header/MobileHeaderLeft.tsx | 31 ++- .../src/layout/MainPageLayout.tsx | 23 ++- .../src/layout/StickyMobileHeader.tsx | 26 +-- .../src/views/SurveySelectPage.tsx | 176 ------------------ .../SurveySelectPage/DesktopTemplate.tsx | 99 ++++++++++ .../views/SurveySelectPage/MobileTemplate.tsx | 76 ++++++++ .../SurveySelectPage/SurveySelectPage.tsx | 119 ++++++++++++ .../src/views/SurveySelectPage/index.ts | 6 + packages/datatrak-web/src/views/index.ts | 2 +- .../src/components/SelectList/ListItem.tsx | 11 +- .../src/components/SelectList/SelectList.tsx | 31 ++- 21 files changed, 694 insertions(+), 308 deletions(-) create mode 100644 packages/datatrak-web/src/features/MobileSelectList/ListItem.tsx create mode 100644 packages/datatrak-web/src/features/MobileSelectList/MobileSelectList.tsx create mode 100644 packages/datatrak-web/src/features/MobileSelectList/index.ts create mode 100644 packages/datatrak-web/src/features/useGroupedSurveyList.tsx delete mode 100644 packages/datatrak-web/src/views/SurveySelectPage.tsx create mode 100644 packages/datatrak-web/src/views/SurveySelectPage/DesktopTemplate.tsx create mode 100644 packages/datatrak-web/src/views/SurveySelectPage/MobileTemplate.tsx create mode 100644 packages/datatrak-web/src/views/SurveySelectPage/SurveySelectPage.tsx create mode 100644 packages/datatrak-web/src/views/SurveySelectPage/index.ts diff --git a/packages/datatrak-web/src/features/CountrySelector/CountrySelector.tsx b/packages/datatrak-web/src/features/CountrySelector/CountrySelector.tsx index ee486e38b8..faba4ee0f1 100644 --- a/packages/datatrak-web/src/features/CountrySelector/CountrySelector.tsx +++ b/packages/datatrak-web/src/features/CountrySelector/CountrySelector.tsx @@ -37,7 +37,8 @@ const Pin = styled.img.attrs({ height: auto; margin-right: 0.5rem; `; -const CountrySelectWrapper = styled.div` + +export const CountrySelectWrapper = styled.div` display: flex; align-items: center; `; diff --git a/packages/datatrak-web/src/features/CountrySelector/index.ts b/packages/datatrak-web/src/features/CountrySelector/index.ts index 98527f6ffc..d3a75ea06b 100644 --- a/packages/datatrak-web/src/features/CountrySelector/index.ts +++ b/packages/datatrak-web/src/features/CountrySelector/index.ts @@ -4,4 +4,4 @@ */ export { useUserCountries } from './useUserCountries'; -export { CountrySelector } from './CountrySelector'; +export { CountrySelector, CountrySelectWrapper } from './CountrySelector'; diff --git a/packages/datatrak-web/src/features/CountrySelector/useUserCountries.ts b/packages/datatrak-web/src/features/CountrySelector/useUserCountries.ts index b158961a45..907f6a11ae 100644 --- a/packages/datatrak-web/src/features/CountrySelector/useUserCountries.ts +++ b/packages/datatrak-web/src/features/CountrySelector/useUserCountries.ts @@ -54,7 +54,5 @@ export const useUserCountries = (onError?: (error: any) => void) => { countries: alphabetisedCountries, selectedCountry, updateSelectedCountry: setSelectedCountry, - // if the user has a country code, and it doesn't match the selected country, then the country has been updated, which means we need to update the user - countryHasUpdated: selectedCountry?.code !== user.country?.code, }; }; diff --git a/packages/datatrak-web/src/features/GroupedSurveyList.tsx b/packages/datatrak-web/src/features/GroupedSurveyList.tsx index c32fd13e30..28bbbfe5ae 100644 --- a/packages/datatrak-web/src/features/GroupedSurveyList.tsx +++ b/packages/datatrak-web/src/features/GroupedSurveyList.tsx @@ -2,14 +2,13 @@ * Tupaia * Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd */ -import React, { ReactNode, useEffect } from 'react'; +import React from 'react'; import styled from 'styled-components'; import { FormHelperText, FormLabelProps } from '@material-ui/core'; import { Country } from '@tupaia/types'; import { SelectList } from '@tupaia/ui-components'; -import { SurveyFolderIcon, SurveyIcon } from '../components'; import { Survey } from '../types'; -import { useCurrentUserContext, useProjectSurveys } from '../api'; +import { useGroupedSurveyList } from './useGroupedSurveyList'; const ListWrapper = styled.div` max-height: 35rem; @@ -22,26 +21,6 @@ const ListWrapper = styled.div` } `; -type ListItemType = Record & { - children?: ListItemType[]; - content: string | ReactNode; - value: string; - selected?: boolean; - icon?: ReactNode; - tooltip?: string; - button?: boolean; - disabled?: boolean; - labelProps?: FormLabelProps & { - component?: React.ElementType; - }; -}; - -const sortAlphanumerically = (a: ListItemType, b: ListItemType) => { - return (a.content as string).trim()?.localeCompare((b.content as string).trim(), 'en', { - numeric: true, - }); -}; - interface GroupedSurveyListProps { setSelectedSurvey: (surveyCode: Survey['code'] | null) => void; selectedSurvey: Survey['code'] | null; @@ -61,60 +40,11 @@ export const GroupedSurveyList = ({ labelProps, error, }: GroupedSurveyListProps) => { - const user = useCurrentUserContext(); - const { data: surveys } = useProjectSurveys(user?.projectId, selectedCountry?.code); - const groupedSurveys = - surveys - ?.reduce((acc: ListItemType[], survey: Survey) => { - const { surveyGroupName, name, code } = survey; - const formattedSurvey = { - content: name, - value: code, - selected: selectedSurvey === code, - icon: , - }; - // if there is no surveyGroupName, add the survey to the list as a top level item - if (!surveyGroupName) { - return [...acc, formattedSurvey]; - } - const group = acc.find(({ content }) => content === surveyGroupName); - // if the surveyGroupName doesn't exist in the list, add it as a top level item - if (!group) { - return [ - ...acc, - { - content: surveyGroupName, - icon: , - value: surveyGroupName, - children: [formattedSurvey], - }, - ]; - } - // if the surveyGroupName exists in the list, add the survey to the children - return acc.map(item => { - if (item.content === surveyGroupName) { - return { - ...item, - // sort the folder items alphanumerically - children: [...(item.children || []), formattedSurvey].sort(sortAlphanumerically), - }; - } - return item; - }); - }, []) - ?.sort(sortAlphanumerically) ?? []; - - useEffect(() => { - // when the surveys change, check if the selected survey is still in the list. If not, clear the selection - if (selectedSurvey && !surveys?.find(survey => survey.code === selectedSurvey)) { - setSelectedSurvey(null); - } - }, [JSON.stringify(surveys)]); - - const onSelectSurvey = (listItem: ListItemType | null) => { - if (!listItem) return setSelectedSurvey(null); - setSelectedSurvey(listItem?.value as Survey['code']); - }; + const { groupedSurveys, onSelectSurvey } = useGroupedSurveyList({ + setSelectedSurvey, + selectedSurvey, + selectedCountry, + }); return ( theme.palette.primary.main}; + transform: rotate(180deg); +`; + +export const BaseListItem = styled(MuiListItem)` + border-radius: 10px; + background: white; + padding: 1rem; + margin-bottom: 10px; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + border: 1px solid transparent; + text-align: left; +`; + +const IconWrapper = styled.div` + padding-right: 0.5rem; + display: flex; + align-items: center; + width: 2rem; + font-size: 2rem; + svg { + color: ${({ theme }) => theme.palette.primary.main}; + height: auto; + } +`; + +/** + * Taken from [Material UI's example](https://v4.mui.com/components/dialogs/#full-screen-dialogs) to make the dialog slide up from the bottom + */ +const Transition = React.forwardRef(function Transition( + props: TransitionProps & { children?: React.ReactElement }, + ref: React.Ref, +) { + return ; +}); + +interface ListItemProps { + item: ListItemType; + onSelect: (item: any) => void; + children?: ReactNode; +} + +export const ListItem = ({ item, onSelect, children }: ListItemProps) => { + const [expanded, setExpanded] = useState(false); + const { content, icon } = item; + const isNested = !!item.children; + + const onClose = () => setExpanded(false); + + const handleOnClick = () => { + if (children) { + setExpanded(true); + } else { + onSelect(item); + } + }; + + return ( + <> + + {icon} + {content} + {isNested && } + + {children && ( + + + {children} + + )} + + ); +}; diff --git a/packages/datatrak-web/src/features/MobileSelectList/MobileSelectList.tsx b/packages/datatrak-web/src/features/MobileSelectList/MobileSelectList.tsx new file mode 100644 index 0000000000..54322cb4f2 --- /dev/null +++ b/packages/datatrak-web/src/features/MobileSelectList/MobileSelectList.tsx @@ -0,0 +1,97 @@ +/* + * Tupaia + * Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd + */ + +import React from 'react'; +import styled from 'styled-components'; +import { List as MuiList, Typography } from '@material-ui/core'; +import { ListItem } from './ListItem'; +import { CountrySelectWrapper } from '../CountrySelector'; +import { ListItemType } from '../useGroupedSurveyList'; + +const BaseList = styled(MuiList)` + padding: 20px 25px; + height: 100%; + background: ${({ theme }) => theme.palette.background.default}; + + ${CountrySelectWrapper} { + margin-bottom: 1rem; + + .MuiOutlinedInput-notchedOutline { + border: none; + } + + .MuiInputBase-root .MuiSvgIcon-root { + display: none; + } + } +`; + +const CategoryTitle = styled(Typography)` + margin: -0.5rem 0 0.8rem; + padding-top: 1rem; + border-top: 1px solid ${({ theme }) => theme.palette.divider}; +`; + +const NoResultsMessage = styled(Typography)` + padding: 0.8rem 0.5rem; + font-size: 0.875rem; + color: ${({ theme }) => theme.palette.text.secondary}; +`; + +interface SelectListProps { + items?: ListItemType[]; + onSelect: (item: ListItemType) => void; + CountrySelector: React.ReactNode; +} + +const List = ({ parentItem, items, onSelect, CountrySelector }) => { + const parentTitle = parentItem?.value; + return ( + <> + + {CountrySelector} + {parentTitle && {parentTitle}} + {items?.map(item => ( + + {item?.children && ( + + )} + + ))} + + + ); +}; + +export const MobileSelectList = ({ items = [], onSelect, CountrySelector }: SelectListProps) => { + return ( + <> + {items.length === 0 ? ( + No items to display + ) : ( + + {CountrySelector} + {items?.map(item => ( + + {item?.children && ( + + )} + + ))} + + )} + + ); +}; diff --git a/packages/datatrak-web/src/features/MobileSelectList/index.ts b/packages/datatrak-web/src/features/MobileSelectList/index.ts new file mode 100644 index 0000000000..25c753e7c4 --- /dev/null +++ b/packages/datatrak-web/src/features/MobileSelectList/index.ts @@ -0,0 +1,6 @@ +/* + * Tupaia + * Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd + */ + +export { MobileSelectList } from './MobileSelectList'; diff --git a/packages/datatrak-web/src/features/index.ts b/packages/datatrak-web/src/features/index.ts index 6a59969cf0..00f69a3929 100644 --- a/packages/datatrak-web/src/features/index.ts +++ b/packages/datatrak-web/src/features/index.ts @@ -9,18 +9,20 @@ export { SurveyReviewScreen, SurveyContext, SurveyLayout, - SurveyToolbar, useSurveyForm, getAllSurveyComponents, SurveySideMenu, useValidationResolver, SurveyResubmitSuccessScreen, + SurveyToolbar, } from './Survey'; export { RequestProjectAccess } from './RequestProjectAccess'; export { MobileAppPrompt } from './MobileAppPrompt'; export { Leaderboard } from './Leaderboard'; export { Reports } from './Reports'; export { TaskPageHeader, TasksTable, TaskDetails, CreateTaskModal, TaskActionsMenu } from './Tasks'; -export { useUserCountries, CountrySelector } from './CountrySelector'; +export { useUserCountries, CountrySelector, CountrySelectWrapper } from './CountrySelector'; export { GroupedSurveyList } from './GroupedSurveyList'; +export { useGroupedSurveyList } from './useGroupedSurveyList'; export { SurveyResponseModal } from './SurveyResponseModal'; +export { MobileSelectList } from './MobileSelectList'; diff --git a/packages/datatrak-web/src/features/useGroupedSurveyList.tsx b/packages/datatrak-web/src/features/useGroupedSurveyList.tsx new file mode 100644 index 0000000000..d455744756 --- /dev/null +++ b/packages/datatrak-web/src/features/useGroupedSurveyList.tsx @@ -0,0 +1,92 @@ +/* + * Tupaia + * Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd + */ + +import React, { ReactNode, useEffect } from 'react'; +import { FormLabelProps } from '@material-ui/core'; +import { useCurrentUserContext, useProjectSurveys } from '../api'; +import { Survey } from '../types'; +import { SurveyIcon, SurveyFolderIcon } from '../components'; + +export type ListItemType = Record & { + children?: ListItemType[]; + content: string | ReactNode; + value: string; + selected?: boolean; + icon?: ReactNode; + tooltip?: string; + button?: boolean; + disabled?: boolean; + labelProps?: FormLabelProps & { + component?: React.ElementType; + }; +}; + +const sortAlphanumerically = (a: ListItemType, b: ListItemType) => { + return (a.content as string).trim()?.localeCompare((b.content as string).trim(), 'en', { + numeric: true, + }); +}; + +export const useGroupedSurveyList = ({ setSelectedSurvey, selectedSurvey, selectedCountry }) => { + const user = useCurrentUserContext(); + const { data: surveys } = useProjectSurveys(user?.projectId, selectedCountry?.code); + const groupedSurveys = + surveys + ?.reduce((acc: ListItemType[], survey: Survey) => { + const { surveyGroupName, name, code } = survey; + const formattedSurvey = { + content: name, + value: code, + selected: selectedSurvey === code, + icon: , + }; + // if there is no surveyGroupName, add the survey to the list as a top level item + if (!surveyGroupName) { + return [...acc, formattedSurvey]; + } + const group = acc.find(({ content }) => content === surveyGroupName); + // if the surveyGroupName doesn't exist in the list, add it as a top level item + if (!group) { + return [ + ...acc, + { + content: surveyGroupName, + icon: , + value: surveyGroupName, + children: [formattedSurvey], + }, + ]; + } + // if the surveyGroupName exists in the list, add the survey to the children + return acc.map(item => { + if (item.content === surveyGroupName) { + return { + ...item, + // sort the folder items alphanumerically + children: [...(item.children || []), formattedSurvey].sort(sortAlphanumerically), + }; + } + return item; + }); + }, []) + ?.sort(sortAlphanumerically) ?? []; + + useEffect(() => { + // when the surveys change, check if the selected survey is still in the list. If not, clear the selection + if (selectedSurvey && !surveys?.find(survey => survey.code === selectedSurvey)) { + setSelectedSurvey(null); + } + }, [JSON.stringify(surveys)]); + + const onSelectSurvey = (listItem: ListItemType | null) => { + if (!listItem) return setSelectedSurvey(null); + setSelectedSurvey(listItem?.value as Survey['code']); + }; + + return { + groupedSurveys, + onSelectSurvey, + }; +}; diff --git a/packages/datatrak-web/src/layout/BackgroundPageLayout.tsx b/packages/datatrak-web/src/layout/BackgroundPageLayout.tsx index 245cdc6f23..1ac8009957 100644 --- a/packages/datatrak-web/src/layout/BackgroundPageLayout.tsx +++ b/packages/datatrak-web/src/layout/BackgroundPageLayout.tsx @@ -25,6 +25,10 @@ export const Background = styled.div<{ }}; display: flex; margin-top: ${props => (props.$hideBorder ? '-1px' : 0)}; + + ${({ theme }) => theme.breakpoints.down('md')} { + flex: 1; + } `; export const BackgroundPageLayout = ({ diff --git a/packages/datatrak-web/src/layout/Header/MobileHeaderLeft.tsx b/packages/datatrak-web/src/layout/Header/MobileHeaderLeft.tsx index 6b4c6d2409..bb8406f521 100644 --- a/packages/datatrak-web/src/layout/Header/MobileHeaderLeft.tsx +++ b/packages/datatrak-web/src/layout/Header/MobileHeaderLeft.tsx @@ -28,8 +28,12 @@ const Logo = styled(IconButton)<{ to: string; }>` padding: 0; - height: 1.5rem; + + img { + max-height: 33px; + } `; + const UserDetailsContainer = styled.div` margin-inline-start: 0.5rem; height: 100%; @@ -48,17 +52,28 @@ const UserName = styled(Typography)` export const MobileHeaderLeft = ({ onClickLogo }) => { const { isLoggedIn, projectId, fullName } = useCurrentUserContext(); + + if (isLoggedIn) { + return ( + + + Tupaia Datatrak logo + + {isLoggedIn && ( + + {fullName} + {projectId && } + + )} + + ); + } + return ( - Tupaia Datatrak logo + Tupaia Datatrak logo - {isLoggedIn && ( - - {fullName} - {projectId && } - - )} ); }; diff --git a/packages/datatrak-web/src/layout/MainPageLayout.tsx b/packages/datatrak-web/src/layout/MainPageLayout.tsx index 75f9880a54..047a85b725 100644 --- a/packages/datatrak-web/src/layout/MainPageLayout.tsx +++ b/packages/datatrak-web/src/layout/MainPageLayout.tsx @@ -4,11 +4,13 @@ */ import React from 'react'; -import { Outlet } from 'react-router'; +import { Outlet, matchPath, useLocation } from 'react-router-dom'; import styled from 'styled-components'; import { HEADER_HEIGHT } from '../constants'; import { Header } from '.'; import { MobileAppPrompt, SurveyResponseModal } from '../features'; +import { ROUTES } from '../constants'; +import { useIsMobile } from '../utils'; const PageWrapper = styled.div` display: flex; @@ -18,13 +20,30 @@ const PageWrapper = styled.div` + .notistack-SnackbarContainer { top: calc(1rem + ${HEADER_HEIGHT}); + + ${({ theme }) => theme.breakpoints.down('md')} { + bottom: 3.5rem; + } } `; +const useHeaderVisibility = () => { + const { pathname } = useLocation(); + // Always show header if not mobile + if (!useIsMobile()) { + return true; + } + // Some routes on mobile don't have header + const headerLessRoutePatterns = [`${ROUTES.SURVEY}/*`, ROUTES.SURVEY_SELECT]; + + return !headerLessRoutePatterns.some(pathPattern => matchPath(pathPattern, pathname)); +}; + export const MainPageLayout = () => { + const showHeader = useHeaderVisibility(); return ( -
+ {showHeader &&
} diff --git a/packages/datatrak-web/src/layout/StickyMobileHeader.tsx b/packages/datatrak-web/src/layout/StickyMobileHeader.tsx index 68c258e145..3375d55c6f 100644 --- a/packages/datatrak-web/src/layout/StickyMobileHeader.tsx +++ b/packages/datatrak-web/src/layout/StickyMobileHeader.tsx @@ -10,8 +10,8 @@ import { ArrowLeftIcon } from '../components'; import { HEADER_HEIGHT } from '../constants'; import { Close } from '@material-ui/icons'; -const Wrapper = styled.div` - position: fixed; +export const MobileHeaderWrapper = styled.div` + position: sticky; top: 0; left: 0; width: 100%; @@ -35,6 +35,7 @@ const BackIcon = styled(ArrowLeftIcon)` `; const Title = styled(Typography).attrs({ variant: 'h2' })` + text-align: center; font-weight: ${({ theme }) => theme.typography.fontWeightMedium}; font-size: 1rem; `; @@ -44,18 +45,21 @@ const ButtonContainer = styled.div` `; interface StickyMobileHeaderProps { - onBack: () => void; - title: string; + title: string | React.ReactNode; + onBack?: () => void; onClose?: () => void; } + export const StickyMobileHeader = ({ onBack, title, onClose }: StickyMobileHeaderProps) => { return ( - - - - + + {onBack && ( + + + + )} {title} {onClose && ( @@ -64,6 +68,6 @@ export const StickyMobileHeader = ({ onBack, title, onClose }: StickyMobileHeade )} - + ); }; diff --git a/packages/datatrak-web/src/views/SurveySelectPage.tsx b/packages/datatrak-web/src/views/SurveySelectPage.tsx deleted file mode 100644 index 1318d82875..0000000000 --- a/packages/datatrak-web/src/views/SurveySelectPage.tsx +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Tupaia - * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd - */ -import React, { useEffect, useState } from 'react'; -import { useNavigate } from 'react-router'; -import { useSearchParams } from 'react-router-dom'; -import styled from 'styled-components'; -import { DialogActions, Paper, Typography } from '@material-ui/core'; -import { SpinningLoader } from '@tupaia/ui-components'; -import { useEditUser } from '../api/mutations'; -import { Button } from '../components'; -import { useCurrentUserContext, useProjectSurveys } from '../api'; -import { HEADER_HEIGHT } from '../constants'; -import { CountrySelector, GroupedSurveyList, useUserCountries } from '../features'; -import { Survey } from '../types'; - -const Container = styled(Paper).attrs({ - variant: 'outlined', -})` - width: 48rem; - display: flex; - flex-direction: column; - justify-content: space-between; - &.MuiPaper-root { - max-height: 100%; - height: 35rem; - } - ${({ theme }) => theme.breakpoints.down('sm')} { - width: 100%; - border-radius: 0; - border-left: none; - border-right: none; - &.MuiPaper-root { - height: 100%; - } - // parent selector - targets the parent of this container - div:has(&) { - padding: 0; - align-items: flex-start; - height: calc(100vh - ${HEADER_HEIGHT}); - } - } -`; - -const LoadingContainer = styled.div` - display: flex; - justify-content: center; - align-items: center; - height: 100%; - min-height: 20rem; - flex: 1; -`; - -const HeaderWrapper = styled.div` - display: flex; - align-items: center; - justify-content: space-between; - flex-direction: column; - margin-bottom: 1rem; - > div { - width: 100%; - } - ${({ theme }) => theme.breakpoints.up('sm')} { - flex-direction: row; - > div { - width: auto; - } - } -`; - -const Subheader = styled(Typography).attrs({ - variant: 'h2', -})` - color: ${({ theme }) => theme.palette.text.secondary}; - font-size: 0.875rem; - line-height: 1.125; - font-weight: 400; - margin-top: 0.67rem; - ${({ theme }) => theme.breakpoints.down('sm')} { - margin-bottom: 0.5rem; - } -`; - -export const SurveySelectPage = () => { - const navigate = useNavigate(); - const [selectedSurvey, setSelectedSurvey] = useState(null); - const [urlSearchParams] = useSearchParams(); - const urlProjectId = urlSearchParams.get('projectId'); - const { - countries, - selectedCountry, - updateSelectedCountry, - countryHasUpdated, - isLoading: isLoadingCountries, - } = useUserCountries(); - const navigateToSurvey = () => { - navigate(`/survey/${selectedCountry?.code}/${selectedSurvey}`); - }; - const { mutateAsync: updateUser, isLoading: isUpdatingUser } = useEditUser(); - const user = useCurrentUserContext(); - - const { isLoading, data: surveys } = useProjectSurveys(user.projectId, selectedCountry?.code); - - const handleSelectSurvey = () => { - if (countryHasUpdated) { - // update user with new country. If the user goes 'back' and doesn't select a survey, and does not yet have a country selected, that's okay because it will be set whenever they next select a survey - updateUser( - { countryId: selectedCountry?.id }, - { - onSuccess: navigateToSurvey, - }, - ); - } else navigateToSurvey(); - }; - - useEffect(() => { - // when the surveys change, check if the selected survey is still in the list. If not, clear the selection - if (selectedSurvey && !surveys?.find(survey => survey.code === selectedSurvey)) { - setSelectedSurvey(null); - } - }, [JSON.stringify(surveys)]); - - useEffect(() => { - const updateUserProject = async () => { - if (urlProjectId && user.projectId !== urlProjectId) { - updateUser({ projectId: urlProjectId }); - } - }; - updateUserProject(); - }, [urlProjectId]); - - const showLoader = - isLoading || - isLoadingCountries || - isUpdatingUser || - (urlProjectId && urlProjectId !== user?.projectId); // in this case the user will be updating and all surveys etc will be reloaded, so showing a loader when this is the case means a more seamless experience - return ( - - -
- Select survey - Select a survey from the list below -
- -
- {showLoader ? ( - - - - ) : ( - - )} - - - - -
- ); -}; diff --git a/packages/datatrak-web/src/views/SurveySelectPage/DesktopTemplate.tsx b/packages/datatrak-web/src/views/SurveySelectPage/DesktopTemplate.tsx new file mode 100644 index 0000000000..835296f845 --- /dev/null +++ b/packages/datatrak-web/src/views/SurveySelectPage/DesktopTemplate.tsx @@ -0,0 +1,99 @@ +/* + * Tupaia + * Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd + */ + +import React from 'react'; +import { DialogActions, Paper, Typography } from '@material-ui/core'; +import { GroupedSurveyList } from '../../features'; +import { Button } from '../../components'; +import styled from 'styled-components'; +import { SpinningLoader } from '@tupaia/ui-components'; + +const Container = styled(Paper).attrs({ + variant: 'outlined', +})` + width: 48rem; + display: flex; + flex-direction: column; + justify-content: space-between; + &.MuiPaper-root { + max-height: 100%; + height: 35rem; + } + + div:has(&) { + padding: 0; + align-items: start; + height: calc(100vh); + } +`; + +const LoadingContainer = styled.div` + display: flex; + justify-content: center; + align-items: center; + height: 100%; + min-height: 20rem; + flex: 1; +`; + +const Loader = () => ( + + + +); + +const HeaderWrapper = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + flex-direction: row; + margin-bottom: 1rem; +`; + +const Subheader = styled(Typography).attrs({ + variant: 'h2', +})` + color: ${({ theme }) => theme.palette.text.secondary}; + font-size: 0.875rem; + line-height: 1.125; + font-weight: 400; + margin-top: 0.67rem; +`; + +export const DesktopTemplate = ({ + selectedCountry, + selectedSurvey, + setSelectedSurvey, + showLoader, + SubmitButton, + CountrySelector, +}) => { + return ( + + +
+ Select survey + Select a survey from the list below +
+ {CountrySelector} +
+ {showLoader ? ( + + ) : ( + + )} + + + {SubmitButton} + +
+ ); +}; diff --git a/packages/datatrak-web/src/views/SurveySelectPage/MobileTemplate.tsx b/packages/datatrak-web/src/views/SurveySelectPage/MobileTemplate.tsx new file mode 100644 index 0000000000..1a94781e93 --- /dev/null +++ b/packages/datatrak-web/src/views/SurveySelectPage/MobileTemplate.tsx @@ -0,0 +1,76 @@ +/* + * Tupaia + * Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd + */ + +import React from 'react'; +import { useNavigate } from 'react-router'; +import styled from 'styled-components'; +import { SpinningLoader } from '@tupaia/ui-components'; +import { useGroupedSurveyList, MobileSelectList } from '../../features'; +import { StickyMobileHeader } from '../../layout'; + +const MobileContainer = styled.div` + max-height: 100%; + background: ${({ theme }) => theme.palette.background.default}; + width: 100%; + height: 100%; + + // parent selector - targets the parents of this container + div:has(&) { + padding: 0; + height: calc(100vh); + } +`; + +const LoadingContainer = styled.div` + display: flex; + justify-content: center; + align-items: center; + height: 100%; + min-height: 20rem; + flex: 1; +`; + +const Loader = () => ( + + + +); + +export const MobileTemplate = ({ + selectedCountry, + setSelectedSurvey, + showLoader, + CountrySelector, + selectedSurvey, + handleSelectSurvey, +}) => { + const { groupedSurveys } = useGroupedSurveyList({ + setSelectedSurvey, + selectedSurvey, + selectedCountry, + }); + const navigate = useNavigate(); + const onClose = () => { + navigate('/'); + }; + const onNavigateToSurvey = survey => { + handleSelectSurvey(selectedCountry, survey.value); + }; + + if (showLoader) { + return ; + } + + return ( + + + + + ); +}; diff --git a/packages/datatrak-web/src/views/SurveySelectPage/SurveySelectPage.tsx b/packages/datatrak-web/src/views/SurveySelectPage/SurveySelectPage.tsx new file mode 100644 index 0000000000..f6c1a46e00 --- /dev/null +++ b/packages/datatrak-web/src/views/SurveySelectPage/SurveySelectPage.tsx @@ -0,0 +1,119 @@ +/* + * Tupaia + * Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd + */ +import React, { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router'; +import { useSearchParams } from 'react-router-dom'; +import { useEditUser } from '../../api/mutations'; +import { Button } from '../../components'; +import { useCurrentUserContext, useProjectSurveys } from '../../api'; +import { CountrySelector, useUserCountries } from '../../features'; +import { Survey } from '../../types'; +import { useIsMobile } from '../../utils'; +import { DesktopTemplate } from './DesktopTemplate'; +import { MobileTemplate } from './MobileTemplate'; + +const useNavigateToSurvey = () => { + const navigate = useNavigate(); + const user = useCurrentUserContext(); + const { mutate: updateUser } = useEditUser(); + + return (country, surveyCode) => { + if (country?.code === user.country?.code) { + return navigate(`/survey/${country?.code}/${surveyCode}`); + } + + // update user with new country. If the user goes 'back' and doesn't select a survey, and does not yet have a country selected, that's okay because it will be set whenever they next select a survey + updateUser( + { countryId: country?.id }, + { + onSuccess: () => { + navigate(`/survey/${country?.code}/${surveyCode}`); + }, + }, + ); + }; +}; + +export const SurveySelectPage = () => { + const [selectedSurvey, setSelectedSurvey] = useState(null); + const [urlSearchParams] = useSearchParams(); + const urlProjectId = urlSearchParams.get('projectId'); + const { + countries, + selectedCountry, + updateSelectedCountry, + isLoading: isLoadingCountries, + } = useUserCountries(); + const handleSelectSurvey = useNavigateToSurvey(); + const { mutate: updateUser, isLoading: isUpdatingUser } = useEditUser(); + const user = useCurrentUserContext(); + + const { isLoading, data: surveys } = useProjectSurveys(user.projectId, selectedCountry?.code); + + useEffect(() => { + // when the surveys change, check if the selected survey is still in the list. If not, clear the selection + if (selectedSurvey && !surveys?.find(survey => survey.code === selectedSurvey)) { + setSelectedSurvey(null); + } + }, [JSON.stringify(surveys)]); + + useEffect(() => { + const updateUserProject = async () => { + if (urlProjectId && user.projectId !== urlProjectId) { + updateUser({ projectId: urlProjectId }); + } + }; + updateUserProject(); + }, [urlProjectId]); + + const showLoader = + isLoading || + isLoadingCountries || + isUpdatingUser || + (urlProjectId && urlProjectId !== user?.projectId); // in this case the user will be updating and all surveys etc will be reloaded, so showing a loader when this is the case means a more seamless experience + + if (useIsMobile()) { + return ( + + } + /> + ); + } + return ( + + } + SubmitButton={ + + } + /> + ); +}; diff --git a/packages/datatrak-web/src/views/SurveySelectPage/index.ts b/packages/datatrak-web/src/views/SurveySelectPage/index.ts new file mode 100644 index 0000000000..fa83a5ed9e --- /dev/null +++ b/packages/datatrak-web/src/views/SurveySelectPage/index.ts @@ -0,0 +1,6 @@ +/* + * Tupaia + * Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd + */ + +export { SurveySelectPage } from './SurveySelectPage'; diff --git a/packages/datatrak-web/src/views/index.ts b/packages/datatrak-web/src/views/index.ts index 93026c4860..dbe8239e85 100644 --- a/packages/datatrak-web/src/views/index.ts +++ b/packages/datatrak-web/src/views/index.ts @@ -5,7 +5,7 @@ export { LandingPage } from './LandingPage'; export { SurveyPage } from './SurveyPage'; -export { SurveySelectPage } from './SurveySelectPage'; +export { SurveySelectPage } from './SurveySelectPage/SurveySelectPage.tsx'; export { LoginPage } from './LoginPage'; export { RegisterPage } from './RegisterPage'; export { VerifyEmailResendPage } from './VerifyEmailResendPage'; diff --git a/packages/ui-components/src/components/SelectList/ListItem.tsx b/packages/ui-components/src/components/SelectList/ListItem.tsx index 09e4046cde..ea80a2f9a7 100644 --- a/packages/ui-components/src/components/SelectList/ListItem.tsx +++ b/packages/ui-components/src/components/SelectList/ListItem.tsx @@ -33,15 +33,10 @@ export const BaseListItem = styled(MuiListItem)` &.Mui-selected:hover, &:focus, &.Mui-selected:focus { - background-color: ${({ theme }) => - theme.palette.type === 'light' - ? `${theme.palette.primary.main}33` - : 'rgba(96, 99, 104, 0.50)'}; + background: none; } } - .MuiSvgIcon-root { - font-size: 1rem; - } + &.Mui-disabled { opacity: 1; // still have the icon as the full opacity color: ${({ theme }) => theme.palette.text.disabled}; @@ -133,7 +128,7 @@ export const ListItem = ({ item, children, onSelect }: ListItemProps) => { {isNested && } - {selected && } + {selected && } {isNested && {children}} diff --git a/packages/ui-components/src/components/SelectList/SelectList.tsx b/packages/ui-components/src/components/SelectList/SelectList.tsx index 7f1ffb3500..c5dc5148c5 100644 --- a/packages/ui-components/src/components/SelectList/SelectList.tsx +++ b/packages/ui-components/src/components/SelectList/SelectList.tsx @@ -18,24 +18,9 @@ const Wrapper = styled.div` flex: 1; `; -const FullBorder = css` - border: 1px solid ${({ theme }) => theme.palette.divider}; - border-radius: 3px; - padding: 0 1rem; -`; - -const TopBorder = css` - border-top: 1px solid ${({ theme }) => theme.palette.divider}; - border-radius: 0; - padding: 0.5rem 0; -`; - -const ListWrapper = styled.div<{ - $variant: string; -}>` +const ListWrapper = styled.div` overflow-y: auto; max-height: 100%; - ${({ $variant }) => ($variant === 'fullPage' ? TopBorder : FullBorder)}; flex: 1; height: 100%; `; @@ -55,16 +40,23 @@ const Label = styled(FormLabel)<{ font-weight: 400; `; +const Subtitle = styled(Typography)` + font-size: 0.875rem; + color: ${({ theme }) => theme.palette.text.secondary}; + font-weight: 400; + margin: 0 0 0.5rem 0.9rem; +`; + interface SelectListProps { items?: ListItemType[]; onSelect: (item: ListItemType) => void; label?: string; ListItem?: React.ElementType; - variant?: 'fullPage' | 'inline'; labelProps?: FormLabelProps & { component?: React.ElementType; }; noResultsMessage?: string; + subTitle?: string; } export const SelectList = ({ @@ -72,9 +64,9 @@ export const SelectList = ({ onSelect, label, ListItem, - variant = 'inline', labelProps = {}, noResultsMessage = 'No items to display', + subTitle = '', }: SelectListProps) => { return ( @@ -83,7 +75,8 @@ export const SelectList = ({ {label} )} - + + {subTitle && {subTitle}} {items.length === 0 ? ( {noResultsMessage} ) : (