Skip to content

Commit d25f421

Browse files
authored
feat: church invite team select screen (#7420)
1 parent b5d5f01 commit d25f421

File tree

6 files changed

+471
-15
lines changed

6 files changed

+471
-15
lines changed

apps/journeys-admin/pages/templates/[journeyId]/customize.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useRouter } from 'next/router'
2-
import { withUser, withUserTokenSSR } from 'next-firebase-auth'
2+
import { AuthAction, withUser, withUserTokenSSR } from 'next-firebase-auth'
33
import { useTranslation } from 'next-i18next'
44
import { NextSeo } from 'next-seo'
55

@@ -28,7 +28,7 @@ function CustomizePage() {
2828
<JourneyProvider
2929
value={{
3030
journey: data?.journey,
31-
variant: 'admin'
31+
variant: 'default'
3232
}}
3333
>
3434
<MultiStepForm />
@@ -89,4 +89,7 @@ export const getServerSideProps = withUserTokenSSR()(async ({
8989
}
9090
})
9191

92-
export default withUser()(CustomizePage)
92+
export default withUser({
93+
// TODO: remove this after anon user is implemented
94+
whenUnauthedBeforeInit: AuthAction.REDIRECT_TO_LOGIN
95+
})(CustomizePage)
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { ReactElement } from 'react'
2+
3+
import FormControl from '@mui/material/FormControl'
4+
import MenuItem from '@mui/material/MenuItem'
5+
import Select from '@mui/material/Select'
6+
import Typography from '@mui/material/Typography'
7+
import sortBy from 'lodash/sortBy'
8+
import { useFormikContext } from 'formik'
9+
10+
import { useTeam } from '@core/journeys/ui/TeamProvider'
11+
12+
interface FormValues {
13+
teamSelect: string
14+
}
15+
16+
export function JourneyCustomizeTeamSelect(): ReactElement {
17+
const { values, handleChange } = useFormikContext<FormValues>()
18+
const { query, setActiveTeam } = useTeam()
19+
const teams = query?.data?.teams ?? []
20+
21+
return (
22+
<FormControl sx={{ alignSelf: 'center' }}>
23+
<Select
24+
name="teamSelect"
25+
value={values.teamSelect}
26+
onChange={(e) => {
27+
handleChange(e)
28+
const selected = teams.find(
29+
(t) => t.id === (e.target as HTMLInputElement).value
30+
)
31+
setActiveTeam(selected ?? null)
32+
}}
33+
displayEmpty
34+
inputProps={{ 'aria-label': 'Team' }}
35+
renderValue={(selected) => {
36+
const team = teams.find((t) => t.id === (selected as string))
37+
const label = team?.title ?? team?.publicTitle ?? ''
38+
return (
39+
<Typography
40+
variant="h6"
41+
noWrap
42+
sx={{
43+
maxWidth: '100%',
44+
overflow: 'hidden',
45+
textOverflow: 'ellipsis'
46+
}}
47+
>
48+
{label}
49+
</Typography>
50+
)
51+
}}
52+
sx={{
53+
width: 300,
54+
borderRadius: 99,
55+
bgcolor: 'secondary.light',
56+
color: 'text.primary',
57+
'& .MuiOutlinedInput-notchedOutline': { border: 'none' },
58+
'& .MuiSelect-select': {
59+
py: 2,
60+
px: 6,
61+
display: 'flex',
62+
alignItems: 'center',
63+
overflow: 'hidden',
64+
textOverflow: 'ellipsis',
65+
whiteSpace: 'nowrap'
66+
}
67+
}}
68+
>
69+
{sortBy(teams, 'title').map((team) => (
70+
<MenuItem
71+
key={team.id}
72+
value={team.id}
73+
sx={{
74+
display: 'block',
75+
whiteSpace: 'normal',
76+
wordWrap: 'break-word'
77+
}}
78+
>
79+
<Typography variant="h6">
80+
{team.title ?? team.publicTitle}
81+
</Typography>
82+
</MenuItem>
83+
))}
84+
</Select>
85+
</FormControl>
86+
)
87+
}
Lines changed: 102 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,24 @@
11
import Button from '@mui/material/Button'
22
import Stack from '@mui/material/Stack'
33
import Typography from '@mui/material/Typography'
4-
import { ReactElement } from 'react'
4+
import { ReactElement, useState } from 'react'
55

66
import ArrowRightIcon from '@core/shared/ui/icons/ArrowRight'
7+
import Box from '@mui/material/Box'
8+
9+
import { useTranslation } from 'next-i18next'
10+
import { LanguageScreenCardPreview } from './LanguageScreenCardPreview'
11+
import { useUser } from 'next-firebase-auth'
12+
import { useTeam } from '@core/journeys/ui/TeamProvider'
13+
import FormControl from '@mui/material/FormControl'
14+
import { Formik, FormikValues } from 'formik'
15+
import { boolean, object, string } from 'yup'
16+
import sortBy from 'lodash/sortBy'
17+
import { JourneyCustomizeTeamSelect } from './JourneyCustomizeTeamSelect'
18+
import { useJourneyDuplicateMutation } from '@core/journeys/ui/useJourneyDuplicateMutation'
19+
import { useJourney } from '@core/journeys/ui/JourneyProvider'
20+
import { useRouter } from 'next/router'
21+
import { useSnackbar } from 'notistack'
722

823
interface LanguageScreenProps {
924
handleNext: () => void
@@ -12,22 +27,97 @@ interface LanguageScreenProps {
1227
export function LanguageScreen({
1328
handleNext
1429
}: LanguageScreenProps): ReactElement {
30+
const { t } = useTranslation('journeys-ui')
31+
const user = useUser()
32+
const router = useRouter()
33+
const [loading, setLoading] = useState(false)
34+
const { enqueueSnackbar } = useSnackbar()
35+
36+
const { journey } = useJourney()
37+
//If the user is not authenticated, useUser will return a User instance with a null id https://github.com/gladly-team/next-firebase-auth?tab=readme-ov-file#useuser
38+
const isSignedIn = user?.id != null
39+
const { query } = useTeam()
40+
const teams = query?.data?.teams ?? []
41+
42+
const validationSchema = object({
43+
teamSelect: string().required()
44+
})
45+
46+
const initialValues = {
47+
teamSelect: query?.data?.getJourneyProfile?.lastActiveTeamId ?? ''
48+
}
49+
50+
const [journeyDuplicate] = useJourneyDuplicateMutation()
51+
52+
async function handleSubmit(values: FormikValues) {
53+
setLoading(true)
54+
if (journey == null) {
55+
setLoading(false)
56+
return
57+
}
58+
if (isSignedIn) {
59+
const { teamSelect: teamId } = values
60+
const { data: duplicateData } = await journeyDuplicate({
61+
variables: { id: journey.id, teamId }
62+
})
63+
if (duplicateData?.journeyDuplicate == null) {
64+
enqueueSnackbar(
65+
t(
66+
'Failed to duplicate journey to team, please refresh the page and try again'
67+
),
68+
{
69+
variant: 'error'
70+
}
71+
)
72+
setLoading(false)
73+
74+
return
75+
}
76+
await router.push(
77+
`/templates/${duplicateData.journeyDuplicate.id}/customize`,
78+
undefined,
79+
{ shallow: true }
80+
)
81+
handleNext()
82+
setLoading(false)
83+
}
84+
}
85+
1586
return (
16-
<Stack>
87+
<Stack justifyContent="center" alignItems="center" gap={4}>
1788
<Typography variant="h4" component="h1" gutterBottom>
18-
Language Selection
89+
{t('Lets get started!')}
1990
</Typography>
20-
<Typography variant="body1" color="text.secondary">
21-
Choose your preferred language for the journey template.
91+
<LanguageScreenCardPreview />
92+
93+
<Typography variant="body1" color="text.secondary" align="center">
94+
{t('Select a team')}
2295
</Typography>
23-
<Button
24-
variant="contained"
25-
color="secondary"
26-
onClick={handleNext}
27-
sx={{ width: '300px', alignSelf: 'center', mt: 4 }}
96+
<Formik
97+
initialValues={initialValues}
98+
validationSchema={validationSchema}
99+
enableReinitialize
100+
onSubmit={handleSubmit}
28101
>
29-
<ArrowRightIcon />
30-
</Button>
102+
{({ handleSubmit }) => (
103+
<FormControl sx={{ alignSelf: 'center' }}>
104+
{isSignedIn && <JourneyCustomizeTeamSelect />}
105+
<Button
106+
disabled={loading}
107+
variant="contained"
108+
color="secondary"
109+
onClick={() => handleSubmit()}
110+
sx={{
111+
width: { xs: '100%', sm: 300 },
112+
alignSelf: 'center',
113+
mt: 4
114+
}}
115+
>
116+
<ArrowRightIcon />
117+
</Button>
118+
</FormControl>
119+
)}
120+
</Formik>
31121
</Stack>
32122
)
33123
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import { ReactElement, useMemo } from 'react'
2+
3+
import Box from '@mui/material/Box'
4+
5+
import { BlockRenderer } from '@core/journeys/ui/BlockRenderer'
6+
import { CardWrapper } from '@core/journeys/ui/CardWrapper'
7+
import { FramePortal } from '@core/journeys/ui/FramePortal'
8+
import { useJourney } from '@core/journeys/ui/JourneyProvider'
9+
import { TreeBlock } from '@core/journeys/ui/block'
10+
import { getStepTheme } from '@core/journeys/ui/getStepTheme'
11+
import { getJourneyRTL } from '@core/journeys/ui/rtl'
12+
import { transformer } from '@core/journeys/ui/transformer'
13+
import { VideoWrapper } from '@core/journeys/ui/VideoWrapper'
14+
import { ThemeProvider } from '@core/shared/ui/ThemeProvider/ThemeProvider'
15+
16+
import {
17+
BlockFields_CardBlock as CardBlock,
18+
BlockFields_StepBlock as StepBlock
19+
} from '../../../../../../__generated__/BlockFields'
20+
import {
21+
ThemeMode,
22+
ThemeName
23+
} from 'libs/journeys/ui/__generated__/globalTypes'
24+
import { StepFields } from '@core/journeys/ui/Step/__generated__/StepFields'
25+
import { useJourneyDuplicateMutation } from '@core/journeys/ui/useJourneyDuplicateMutation'
26+
27+
export function LanguageScreenCardPreview(): ReactElement {
28+
const { journey } = useJourney()
29+
const { rtl, locale } = getJourneyRTL(journey)
30+
31+
const steps = useMemo(
32+
() =>
33+
journey != null
34+
? (transformer(journey.blocks ?? []) as Array<TreeBlock<StepBlock>>)
35+
: undefined,
36+
[journey]
37+
)
38+
39+
const firstStep = useMemo(
40+
() =>
41+
(steps?.find(
42+
(block) => block.__typename === 'StepBlock' && block.parentOrder === 0
43+
) as TreeBlock<StepFields> | undefined) ?? undefined,
44+
[steps]
45+
)
46+
47+
const cardBlock = useMemo(
48+
() =>
49+
(firstStep?.children.find(
50+
(child) => child.__typename === 'CardBlock'
51+
) as unknown as TreeBlock<CardBlock> | undefined) ?? undefined,
52+
[firstStep]
53+
)
54+
55+
const theme = useMemo(
56+
() => (firstStep != null ? getStepTheme(firstStep, journey) : null),
57+
[firstStep, journey]
58+
)
59+
60+
const journeyTheme = journey?.journeyTheme
61+
const fontFamilies = useMemo(() => {
62+
if (journeyTheme == null) return
63+
64+
return {
65+
headerFont: journeyTheme?.headerFont ?? '',
66+
bodyFont: journeyTheme?.bodyFont ?? '',
67+
labelFont: journeyTheme?.labelFont ?? ''
68+
}
69+
}, [journeyTheme])
70+
71+
const wrappers = useMemo(
72+
() => ({
73+
VideoWrapper,
74+
CardWrapper
75+
}),
76+
[]
77+
)
78+
79+
return (
80+
<Box>
81+
<Box
82+
sx={{
83+
position: 'relative',
84+
width: { xs: 194, sm: 267 },
85+
height: { xs: 295, sm: 404 },
86+
backgroundColor: 'background.default',
87+
borderRadius: 3,
88+
mx: 'auto'
89+
}}
90+
>
91+
<Box
92+
sx={{
93+
transform: { xs: 'scale(0.4)', sm: 'scale(0.6)' },
94+
transformOrigin: 'top left'
95+
}}
96+
>
97+
<Box
98+
sx={{
99+
position: 'absolute',
100+
display: 'block',
101+
width: { xs: 485, sm: 445 },
102+
height: { xs: 738, sm: 673 },
103+
zIndex: 2
104+
}}
105+
/>
106+
<FramePortal
107+
sx={{
108+
width: { xs: 485, sm: 445 },
109+
height: { xs: 738, sm: 673 }
110+
}}
111+
dir={rtl ? 'rtl' : 'ltr'}
112+
>
113+
<ThemeProvider
114+
themeName={
115+
cardBlock?.themeName ?? theme?.themeName ?? ThemeName.base
116+
}
117+
themeMode={
118+
cardBlock?.themeMode ?? theme?.themeMode ?? ThemeMode.dark
119+
}
120+
fontFamilies={fontFamilies}
121+
rtl={rtl}
122+
locale={locale}
123+
>
124+
<Box
125+
sx={{
126+
height: '100%',
127+
borderRadius: 4
128+
}}
129+
>
130+
<BlockRenderer block={firstStep} wrappers={wrappers} />
131+
</Box>
132+
</ThemeProvider>
133+
</FramePortal>
134+
</Box>
135+
</Box>
136+
</Box>
137+
)
138+
}

0 commit comments

Comments
 (0)