-
Notifications
You must be signed in to change notification settings - Fork 0
Add very basic onboarding system #155
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
base: dev
Are you sure you want to change the base?
Changes from 4 commits
f4fd23a
f391d5b
86aa0b2
0c4e302
e83a798
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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; |
| 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"]} | ||
| </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; | ||
| 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" | ||
|
||
| ) { | ||
| router.push(props.onboardingText[onboardingStep]["route"]); | ||
| return { | ||
| switchingRoutes: true, | ||
| OnboardingComponent: <OnboardingComponent {...props} />, | ||
| }; | ||
| } | ||
|
|
||
| return { | ||
| switchingRoutes: false, | ||
| OnboardingComponent: <OnboardingComponent {...props} />, | ||
| }; | ||
| }; | ||
|
|
||
| export const OnboardingProvider = ({ | ||
|
||
| 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; | ||
| }; | ||
| 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; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,6 +12,7 @@ query getMyUser { | |
| profileJson | ||
| tagsJson | ||
| bio | ||
| showOnboarding | ||
| program { | ||
| programId | ||
| name | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.