Modified setupOptions data structure to be more straightforward, consolidated step logic

This commit is contained in:
Alec LaLonde
2021-02-07 21:00:29 -07:00
parent bf1a93418e
commit c6d669e725
12 changed files with 236 additions and 165 deletions

View File

@@ -7,20 +7,21 @@ import { useSetupFlow } from 'contexts/SetupContext';
import React from 'react';
export const SetupHeader: React.FC = () => {
const { step, screen, onNextPress, onBackPress, options } = useSetupFlow();
const { stepIndex, onNextPress, onBackPress, options } = useSetupFlow();
const {sectionIndex} = options.steps[stepIndex];
return (
<Grid templateColumns="0.5fr 1fr 1fr 1fr 0.5fr" gap="1rem" w="100%">
<FlexContainer justify="flex-end" onClick={onBackPress} cursor="pointer">
<Image src={BackImage} h="1rem" alt="Back" />
</FlexContainer>
{options.map((option, id) => (
<StepProgress
{options.sections.map((option, id) => (
<SectionProgress
key={option.label}
title={option.title}
step={id}
isActive={step === id}
isDone={step > id}
screen={screen}
isActive={sectionIndex === id}
isDone={sectionIndex > id}
/>
))}
<FlexContainer justify="flex-end" onClick={onNextPress} cursor="pointer">
@@ -34,22 +35,16 @@ interface StepProps {
title: { [any: string]: string | undefined };
isDone: boolean;
isActive: boolean;
step: number;
screen: number;
}
export const StepProgress: React.FC<StepProps> = ({
export const SectionProgress: React.FC<StepProps> = ({
title,
isDone,
isActive,
step,
screen,
}) => {
const { options } = useSetupFlow();
const { options, stepIndex } = useSetupFlow();
const progress = isDone
? 100
: Math.floor(((screen + 1) * 100.0) / options[step].screens.length);
const progress = isDone ? 100 : options.progressWithinSection(stepIndex);
return (
<FlexContainer pos="relative">
<ResponsiveText

View File

@@ -4,14 +4,11 @@ import { useSetupFlow } from 'contexts/SetupContext';
import React from 'react';
export const SetupProfile: React.FC = ({children}) => {
const {
step,
numTotalSteps
} = useSetupFlow();
const {options, stepIndex} = useSetupFlow();
return (
<PageContainer>
{(step + 1) % numTotalSteps !== 0 && <SetupHeader />}
{options.numSteps - 1 > stepIndex && <SetupHeader />}
<FlexContainer flex={1} pt={24}>
{children}
</FlexContainer>

View File

@@ -1,99 +1,73 @@
import { useRouter } from 'next/router';
import React, { useCallback, useContext, useEffect, useState } from 'react';
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { SetupOptions } from 'utils/setupOptions';
type SetupOption = {
label: string;
title: {
[any: string]: string | undefined;
};
screens: Array<{
label: string;
component?: React.ReactNode;
slug?: string;
}>;
};
const urlPrefix = `/profile/setup/`;
type SetupContextType = {
options: Array<SetupOption>;
step: number;
screen: number;
options: SetupOptions;
stepIndex: number;
onNextPress: () => void;
onBackPress: () => void;
nextButtonLabel: string;
numTotalSteps: number;
};
export const SetupContext = React.createContext<SetupContextType>({
options: [],
step: 0,
screen: 0,
options: new SetupOptions(),
stepIndex: 0,
onNextPress: () => undefined,
onBackPress: () => undefined,
nextButtonLabel: 'Next Step',
numTotalSteps: 0,
});
type Props = {
options: Array<SetupOption>;
};
export const SetupContextProvider: React.FC = ({children}) => {
const options = useMemo(() => {
return new SetupOptions();
}, []);
export const SetupContextProvider: React.FC<Props> = ({
children,
options
}) => {
const router = useRouter();
const [step, setStep] = useState<number>(0);
const [screen, setScreen] = useState<number>(0);
const numTotalSteps = options.length;
const pageMatches = router.pathname.match(`${urlPrefix}(.+)`);
const slug = pageMatches != null && pageMatches.length > 1 ? pageMatches[1] : null;
const stepIndex = options.stepIndexMatchingSlug(slug);
const currentStep = options.steps[stepIndex];
const [nextButtonLabel, setNextButtonLabel] = useState('Next Step');
useEffect(() => {
const numScreens = options[step].screens.length;
if (step >= numTotalSteps - 1) {
setNextButtonLabel(options[(step + 1) % numTotalSteps].label);
}
if (screen + 1 >= numScreens) {
setNextButtonLabel(`Next: ${options[(step + 1) % numTotalSteps].label}`);
if (options.isLastStep(stepIndex)) {
setNextButtonLabel(currentStep.label);
} else {
setNextButtonLabel(
`Next: ${options[step].screens[(screen + 1) % numScreens].label}`,
);
const nextStep = options.steps[stepIndex + 1];
let nextStepLabel = nextStep.label;
if (options.isFinalStepOfSection(stepIndex)) {
nextStepLabel = options.sections[nextStep.sectionIndex].label;
}
setNextButtonLabel(`Next: ${nextStepLabel}`);
}
}, [options, step, screen, setNextButtonLabel, numTotalSteps]);
}, [options, stepIndex, setNextButtonLabel, currentStep]);
const onNextPress = useCallback(() => {
const numScreens = options[step].screens.length;
if (step >= numTotalSteps - 1 && screen >= numScreens - 1) return;
if (screen + 1 >= numScreens) {
setStep((step + 1) % numTotalSteps);
setScreen(0);
} else {
setScreen((screen + 1) % numScreens);
if (!options.isLastStep(stepIndex)) {
const nextStep = options.steps[stepIndex + 1];
router.push(`${urlPrefix}${nextStep.slug}`);
}
router.push(`/profile/setup/${options[step].screens[(screen + 1) % numScreens].slug}`);
}, [router, options, step, screen, setStep, setScreen, numTotalSteps]);
}, [router, options, stepIndex]);
const onBackPress = useCallback(() => {
if (step <= 0 && screen <= 0) {
if (stepIndex <= 0) {
router.push('/');
return;
}
const numScreens = options[step].screens.length;
if (screen <= 0) {
setStep((step - 1) % numTotalSteps);
setScreen(options[(step - 1) % numTotalSteps].screens.length - 1);
} else {
setScreen((screen - 1) % numScreens);
const previousStep = options.steps[stepIndex - 1];
router.push(`${urlPrefix}${previousStep.slug}`);
}
}, [router, options, step, screen, setStep, setScreen, numTotalSteps]);
}, [router, options, stepIndex]);
return (
<SetupContext.Provider
value={{
options,
step,
screen,
numTotalSteps,
stepIndex,
onNextPress,
onBackPress,
nextButtonLabel,

View File

@@ -2,10 +2,20 @@ import { SetupAvailability } from 'components/Setup/SetupAvailability';
import { SetupProfile } from 'components/Setup/SetupProfile';
import { SetupContextProvider } from 'contexts/SetupContext';
import { useUser } from 'lib/hooks';
import { InferGetStaticPropsType } from 'next';
import React, { useState } from 'react';
import { options as setupOptions } from 'utils/setupOptions';
const AvailabilitySetup: React.FC = () => {
export const getStaticProps = async () => {
return {
props: {
hideAppDrawer: true
}
};
};
export type DefaultSetupProps = InferGetStaticPropsType<typeof getStaticProps>;
const AvailabilitySetup: React.FC<DefaultSetupProps> = () => {
const [availability, setAvailability] = useState<string>('');
const { user } = useUser({ redirectTo: '/' });
@@ -18,7 +28,7 @@ const AvailabilitySetup: React.FC = () => {
}
return (
<SetupContextProvider options={setupOptions}>
<SetupContextProvider>
<SetupProfile>
<SetupAvailability availability={availability} setAvailability={setAvailability} />
</SetupProfile>

View File

@@ -0,0 +1,26 @@
import { SetupDone } from 'components/Setup/SetupDone';
import { SetupProfile } from 'components/Setup/SetupProfile';
import { SetupContextProvider } from 'contexts/SetupContext';
import { InferGetStaticPropsType } from 'next';
import React from 'react';
export const getStaticProps = async () => {
return {
props: {
hideAppDrawer: true
}
};
};
export type DefaultSetupProps = InferGetStaticPropsType<typeof getStaticProps>;
const SetupComplete: React.FC<DefaultSetupProps> = () => {
return (
<SetupContextProvider>
<SetupProfile>
<SetupDone />
</SetupProfile>
</SetupContextProvider>
);
};
export default SetupComplete;

View File

@@ -4,10 +4,20 @@ import { SetupContextProvider } from 'contexts/SetupContext';
import { getMemberships } from 'graphql/getMemberships';
import { Membership } from 'graphql/types';
import { useWeb3 } from 'lib/hooks';
import { InferGetStaticPropsType } from 'next';
import React, { useState } from 'react';
import { options as setupOptions } from 'utils/setupOptions';
const MembershipsSetup: React.FC = () => {
export const getStaticProps = async () => {
return {
props: {
hideAppDrawer: true
}
};
};
export type DefaultSetupProps = InferGetStaticPropsType<typeof getStaticProps>;
const MembershipsSetup: React.FC<DefaultSetupProps> = () => {
const [memberships, setMemberships] = useState<
Array<Membership> | null | undefined
@@ -19,7 +29,7 @@ const MembershipsSetup: React.FC = () => {
});
return (
<SetupContextProvider options={setupOptions}>
<SetupContextProvider>
<SetupProfile>
<SetupMemberships memberships={memberships} setMemberships={setMemberships} />
</SetupProfile>

View File

@@ -6,14 +6,14 @@ import { PersonalityType, PersonalityTypes } from 'graphql/types';
import { useUser } from 'lib/hooks';
import { InferGetStaticPropsType } from 'next';
import React, { useState } from 'react';
import { options as setupOptions } from 'utils/setupOptions';
export const getStaticProps = async () => {
const personalityTypeChoices = await getPersonalityTypes();
return {
props: {
personalityTypeChoices
personalityTypeChoices,
hideAppDrawer: true
}
};
};
@@ -34,7 +34,7 @@ const PersonalityTypeSetup: React.FC<Props> = (props) => {
}
return (
<SetupContextProvider options={setupOptions}>
<SetupContextProvider>
<SetupProfile>
<SetupPersonalityType
personalityTypeChoices={personalityTypeChoices}

View File

@@ -6,15 +6,14 @@ import { getPlayerTypes } from 'graphql/getPlayerTypes';
import { useUser } from 'lib/hooks';
import { InferGetStaticPropsType } from 'next';
import React, { useState } from 'react';
import { options as setupOptions } from 'utils/setupOptions';
export const getStaticProps = async () => {
const playerTypeChoices = await getPlayerTypes();
return {
props: {
hideAppDrawer: true,
playerTypeChoices
playerTypeChoices,
hideAppDrawer: true
}
};
};
@@ -34,7 +33,7 @@ const PlayerTypeSetup: React.FC<Props> = (props) => {
}
return (
<SetupContextProvider options={setupOptions}>
<SetupContextProvider>
<SetupProfile>
<SetupPlayerType
playerTypeChoices={playerTypeChoices}

View File

@@ -5,7 +5,6 @@ import { getSkills } from 'graphql/getSkills';
import { useUser } from 'lib/hooks';
import { InferGetStaticPropsType } from 'next';
import React, { useState } from 'react';
import { options as setupOptions } from 'utils/setupOptions';
import { parseSkills, SkillOption } from 'utils/skillHelpers';
export const getStaticProps = async () => {
@@ -14,7 +13,8 @@ export const getStaticProps = async () => {
return {
props: {
skillChoices
skillChoices,
hideAppDrawer: true
},
};
};
@@ -44,7 +44,7 @@ const SkillsSetup: React.FC<Props> = (props) => {
}
return (
<SetupContextProvider options={setupOptions}>
<SetupContextProvider>
<SetupProfile>
<SetupSkills
skillChoices={skillChoices}

View File

@@ -2,10 +2,20 @@ import { SetupProfile } from 'components/Setup/SetupProfile';
import { SetupTimeZone } from 'components/Setup/SetupTimeZone';
import { SetupContextProvider } from 'contexts/SetupContext';
import { useUser } from 'lib/hooks';
import { InferGetStaticPropsType } from 'next';
import React, { useState } from 'react';
import { options as setupOptions } from 'utils/setupOptions';
const TimeZoneSetup: React.FC = () => {
export const getStaticProps = async () => {
return {
props: {
hideAppDrawer: true
}
};
};
export type DefaultSetupProps = InferGetStaticPropsType<typeof getStaticProps>;
const TimeZoneSetup: React.FC<DefaultSetupProps> = () => {
const [timeZone, setTimeZone] = useState<string>('');
const { user } = useUser({ redirectTo: '/' });
@@ -18,7 +28,7 @@ const TimeZoneSetup: React.FC = () => {
}
return (
<SetupContextProvider options={setupOptions}>
<SetupContextProvider>
<SetupProfile>
<SetupTimeZone timeZone={timeZone} setTimeZone={setTimeZone} />
</SetupProfile>

View File

@@ -2,11 +2,20 @@ import { SetupProfile } from 'components/Setup/SetupProfile';
import { SetupUsername } from 'components/Setup/SetupUsername';
import { SetupContextProvider } from 'contexts/SetupContext';
import { useUser, useWeb3 } from 'lib/hooks';
import { InferGetStaticPropsType } from 'next';
import React, { useState } from 'react';
import { options as setupOptions } from 'utils/setupOptions';
const UsernameSetup: React.FC = () => {
export const getStaticProps = async () => {
return {
props: {
hideAppDrawer: true
}
};
};
export type DefaultSetupProps = InferGetStaticPropsType<typeof getStaticProps>;
const UsernameSetup: React.FC<DefaultSetupProps> = () => {
const [username, setUsername] = useState<string>('');
const { address } = useWeb3();
const { user } = useUser({ redirectTo: '/' });
@@ -23,7 +32,7 @@ const UsernameSetup: React.FC = () => {
}
return (
<SetupContextProvider options={setupOptions}>
<SetupContextProvider>
<SetupProfile>
<SetupUsername username={username} setUsername={setUsername} />
</SetupProfile>

View File

@@ -1,63 +1,104 @@
import { SetupDone } from 'components/Setup/SetupDone';
import React from 'react';
export const options = [
{
label: 'About You',
title: { base: 'About You', sm: '1. About You' },
screens: [
{
label: 'Username',
slug: 'username'
export type SetupStep = {
label: string;
slug?: string;
sectionIndex: number;
}
export type SetupSection = {
label: string;
title: {
[any: string]: string | undefined;
};
};
export class SetupOptions {
sections: SetupSection[] = [
{
label: 'About You',
title: { base: 'About You', sm: '1. About You' }
}, {
label: 'Profile',
title: {
base: 'Profile',
sm: '2. Profile',
lg: '2. Professional Profile',
}
}, {
label: 'Start Playing',
title: {
base: 'Play',
sm: '3. Play',
md: '3. Start Playing',
},
{
label: 'Personality Type',
slug: 'personalityType'
},
{
label: 'Player Type',
slug: 'playerType',
},
],
},
{
label: 'Profile',
title: {
base: 'Profile',
sm: '2. Profile',
lg: '2. Professional Profile',
},
screens: [
{
label: 'Skills',
slug: 'skills'
},
{
label: 'Availability',
slug: 'availability',
},
{
label: 'Time Zone',
slug: 'timeZone'
},
{
label: 'Memberships',
slug: 'memberships',
},
],
},
{
label: 'Start Playing',
title: {
base: 'Play',
sm: '3. Play',
md: '3. Start Playing',
},
screens: [
{
label: 'Done',
component: <SetupDone />,
},
],
},
];
}
];
steps: SetupStep[] = [
{
label: 'Username',
slug: 'username',
sectionIndex: 0
}, {
label: 'Personality Type',
slug: 'personalityType',
sectionIndex: 0
}, {
label: 'Player Type',
slug: 'playerType',
sectionIndex: 0
}, {
label: 'Skills',
slug: 'skills',
sectionIndex: 1
}, {
label: 'Availability',
slug: 'availability',
sectionIndex: 1
}, {
label: 'Time Zone',
slug: 'timeZone',
sectionIndex: 1
}, {
label: 'Memberships',
slug: 'memberships',
sectionIndex: 1
}, {
label: 'Start Playing',
slug: 'complete',
sectionIndex: 2
}
]
stepIndexMatchingSlug(slug: string | null): number {
return this.steps.findIndex(step => step.slug === slug);
};
get numSteps(): number {
return this.steps.length;
}
isLastStep(stepIndex: number): boolean {
return stepIndex >= this.numSteps - 1;
}
isFinalStepOfSection(stepIndex: number): boolean {
if (this.isLastStep(stepIndex)) return true;
return this.steps[stepIndex].sectionIndex !== this.steps[stepIndex + 1].sectionIndex;
}
progressWithinSection(stepIndex: number): number {
const stepSectionIndex = this.steps[stepIndex].sectionIndex;
let stepsCompletedInSection = 0;
const stepsInSection = this.steps.reduce((count:number, step:SetupStep, index:number) => {
if (stepIndex === index) {
stepsCompletedInSection = count;
}
if (step.sectionIndex === stepSectionIndex) {
return count + 1;
}
return count;
}, 0);
return Math.floor((stepsCompletedInSection + 1) * 100.0) / stepsInSection;
}
}