Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add very basic onboarding system #155

Draft
wants to merge 5 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions src/components/Onboarding/Onboarding.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React, { ReactNode } from "react";
import { Card, Text } from "../atomic";
import { useOnboarding } from "./OnboardingContext";

type OnboardingLayoutProps = {
currentPageChildren: ReactNode;
};

const OnboardingLayout = ({ currentPageChildren }: OnboardingLayoutProps) => {
const { switchingRoutes, OnboardingComponent } = useOnboarding();

if (switchingRoutes) return <></>;

return (
<div className="flex flex-col h-full w-full bg-tertiary">
<div className="h-3/4 w-full box-border">{currentPageChildren}</div>

<div className="h-1/4 w-full z-10 px-6">
<Card className="animate-border-pulse z-10 w-full p-4 space-y-4 box-border">
<Text h2 b>
Welcome to your new program!
</Text>
{OnboardingComponent}
</Card>
</div>
</div>
);
};

export default OnboardingLayout;
68 changes: 68 additions & 0 deletions src/components/Onboarding/OnboardingComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { useRouter } from "next/router";
import { Button, Text } from "../atomic";
import StepTracker from "../atomic/StepTracker";
import { OnboardingProps } from "./OnboardingContext";

const Onboarding = ({
onFinish,
currentStep,
setCurrentStep,
loading,
setLoading,
onboardingText,
MAX_STEPS,
}: OnboardingProps) => {
const router = useRouter();

return (
<div className="w-full flex flex-col items-center space-y-2 z-10">
<Text h3 className="w-full">
{currentStep}) {onboardingText[currentStep]["step"]}
innopoop marked this conversation as resolved.
Show resolved Hide resolved
</Text>
<div className="h-2" />
<div className="flex w-full justify-end items-center box-border">
<div className="w-full">
<StepTracker steps={MAX_STEPS} currentStep={currentStep} />
</div>
<Button
size="small"
variant="inverted"
disabled={currentStep === 1 || loading}
onClick={() => {
setLoading(true);
const prevStep = Math.max(currentStep - 1, 1);
router.push(onboardingText[prevStep]["route"]).then(() => {
setCurrentStep(prevStep);
setLoading(false);
});
}}
>
Back
</Button>
<div className="w-4" />
<Button
size="small"
disabled={loading}
onClick={() => {
if (currentStep !== MAX_STEPS) {
setLoading(true);
const nextStep = Math.min(currentStep + 1, MAX_STEPS);
router.push(onboardingText[nextStep]["route"]).then(() => {
setCurrentStep(nextStep);
setLoading(false);
});
} else {
setLoading(true);
onFinish();
setLoading(false);
}
}}
>
{currentStep !== MAX_STEPS ? "Next" : "Finish"}
</Button>
</div>
</div>
);
};

export default Onboarding;
197 changes: 197 additions & 0 deletions src/components/Onboarding/OnboardingContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import { useRouter } from "next/router";
import React, { createContext, useContext, useEffect, useState } from "react";
import {
UpdateProfileInput,
useUpdateProfileMutation,
} from "../../generated/graphql";
import {
AuthorizationLevel,
useAuthorizationLevel,
useCurrentProfile,
} from "../../hooks";
import OnboardingComponent from "./OnboardingComponent";
import LocalStorage from "../../utils/localstorage";

interface OnboardingText {
[key: number]: { step: string; route: string };
}

export interface OnboardingProps {
currentStep: number;
setCurrentStep: (num: number) => void;
loading: boolean;
setLoading: (bool: boolean) => void;
onboardingText: OnboardingText;
MAX_STEPS: number;
onFinish: () => void;
}

const authorizationLevelToMaxSteps = (authLevel: AuthorizationLevel) => {
switch (authLevel) {
case AuthorizationLevel.Admin:
return 5;
case AuthorizationLevel.Mentor:
return 2;
case AuthorizationLevel.Mentee:
return 2;
default:
return 0;
}
};

const AdminOnboardingText = (baseRoute: string) => ({
1: {
step: "Set up your program homepage",
route: baseRoute,
},
2: {
step: "Edit your mentor applications",
route: baseRoute + "applications/edit-mentor-app",
},
3: {
step: "Edit your mentee applications",
route: baseRoute + "applications/edit-mentee-app",
},
4: {
step: "Edit your mentor profile structure",
route: baseRoute + "mentors/edit-profile",
},
5: {
step: "Edit your mentee profile structure",
route: baseRoute + "mentees/edit-profile",
},
});

const MentorOnboardingText = (baseRoute: string) => ({
1: {
step: "Fill out your profile",
route: baseRoute + "edit-profile",
},
2: {
step: "Set your availability",
route: baseRoute + "availability",
},
});

const MenteeOnboardingText = (baseRoute: string) => ({
1: {
step: "Fill out your profile",
route: baseRoute + "edit-profile",
},
2: {
step: "Browse through available mentors",
route: baseRoute + "mentors",
},
});

const authLevelToText = (authLevel: AuthorizationLevel) => {
switch (authLevel) {
case AuthorizationLevel.Admin:
return AdminOnboardingText;
case AuthorizationLevel.Mentor:
return MentorOnboardingText;
default:
return MenteeOnboardingText;
}
};

interface OnboardingContextType {
switchingRoutes: boolean;
OnboardingComponent: JSX.Element;
}

const OnboardingContext = createContext<OnboardingContextType | undefined>(
undefined
);

const useOnboardingProvider = () => {
const currentProfile = useCurrentProfile();
const [updateProfile] = useUpdateProfileMutation({
refetchQueries: ["getMyUser"],
});

const authorizationLevel = useAuthorizationLevel();
const [loading, setLoading] = useState(false);
const [currentStep, setCurrentStep] = useState<number>(1);
const router = useRouter();

const MAX_STEPS = authorizationLevelToMaxSteps(authorizationLevel);
const baseRoute = `/program/${router.query.slug}/${router.query.profileRoute}/`;

const onFinish = () => {
const updateProfileInput: UpdateProfileInput = {
...currentProfile.currentProfile,
showOnboarding: false,
};
updateProfile({
variables: {
profileId: currentProfile.currentProfile!.profileId,
data: updateProfileInput,
},
})
.then(() => {
currentProfile.refetchCurrentProfile!();
LocalStorage.delete("Onboarding Step");
})
.catch((err) => console.error(err));
};

const props: OnboardingProps = {
currentStep,
setCurrentStep: (num: number) => {
LocalStorage.set("Onboarding Step", num);
setCurrentStep(num);
},
loading,
setLoading,
onboardingText: authLevelToText(authorizationLevel)(baseRoute),
MAX_STEPS,
onFinish,
};

const onboardingStep = LocalStorage.get("Onboarding Step");
useEffect(() => {
if (onboardingStep && typeof onboardingStep == "number") {
setCurrentStep(onboardingStep);
}
}, []);

if (
authorizationLevel !== AuthorizationLevel.Admin &&
router.asPath !== props.onboardingText[currentStep]["route"] &&
onboardingStep &&
typeof onboardingStep == "number"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this if condition mean?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So there are a couple of different routes that require different onboarding but if a user navigates outside of the onboarding page (manually), then we have a way to counteract it because we check to see whether the user is still going through onboarding and if they are, we navigate them back to the designated onboarding step. The if statement, I don't know why I put authorizationLevel !== AuthorizationLevel.Admin I think that was a mistake, but the other conditions check for if the current path matches the step that you are supposed to be on and then it checks if the localstorage variable exists and is a number.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've corrected it :' )

) {
router.push(props.onboardingText[onboardingStep]["route"]);
return {
switchingRoutes: true,
OnboardingComponent: <OnboardingComponent {...props} />,
};
}

return {
switchingRoutes: false,
OnboardingComponent: <OnboardingComponent {...props} />,
};
};

export const OnboardingProvider = ({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious as to why this is its own provider, am noob to it

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made it a provider because I wanted to share state among multiple pages, but I think it's also possible to make it a hook?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed it to a hook :' )

children,
}: {
children: React.ReactNode;
}) => {
const value = useOnboardingProvider();
return (
<OnboardingContext.Provider value={value}>
{children}
</OnboardingContext.Provider>
);
};

export const useOnboarding = () => {
const context = useContext(OnboardingContext);
if (context === undefined) {
throw new Error("useOnboarding() must be within OnboardingProvider");
}
return context;
};
2 changes: 1 addition & 1 deletion src/components/RichTextEditing/PublishButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const PublishButton = ({ programId, ...props }: PublishButtonProps) => {
setSnackbarMessage({ text: "Homepage saved!" });
})
.catch((err) => {
console.log("Fail: ", err);
console.error("Fail: ", err);
setLoading(false);
});
};
Expand Down
2 changes: 1 addition & 1 deletion src/components/TabFooterMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const TabFooterMenu = () => {
const myData = data.getMyUser;

return (
<div className="w-full p-2 bg-white flex-shrink-0 border-t border-tertiary">
<div className="w-full p-2 flex-shrink-0 border-t border-tertiary">
<DropdownMenu
button={
<div className="inline-flex items-center">
Expand Down
24 changes: 24 additions & 0 deletions src/components/atomic/StepTracker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { range } from "lodash";
import React, { HTMLAttributes } from "react";

type StepTrackerProps = HTMLAttributes<HTMLDivElement> & {
steps: number;
currentStep: number;
};

const StepTracker = ({ steps, currentStep }: StepTrackerProps) => (
<div className="flex space-x-4">
{range(1, steps + 1).map((i: number) => {
return (
<div
key={i}
className={`h-4 w-4 rounded-full ${
i == currentStep ? "bg-secondary" : "bg-inactive"
}`}
/>
);
})}
</div>
);

export default StepTracker;
1 change: 1 addition & 0 deletions src/graphql/queries/getMyUser.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ query getMyUser {
profileJson
tagsJson
bio
showOnboarding
program {
programId
name
Expand Down
2 changes: 1 addition & 1 deletion src/layouts/PageContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { HTMLAttributes } from "react";
const PageContainer = ({ children }: HTMLAttributes<HTMLDivElement>) => {
// TODO: Add responsiveness
return (
<div className="h-screen bg-tertiary flex flex-col items-center py-10 overflow-y-auto">
<div className="h-full bg-tertiary flex flex-col items-center py-10 overflow-y-auto">
<div className="w-5/6">{children}</div>
</div>
);
Expand Down
Loading