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 all 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
37 changes: 37 additions & 0 deletions src/components/Onboarding/Onboarding.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import classNames from "classnames";
import React, { ReactNode, useState } from "react";
import { Card, Text } from "../atomic";
import OnboardingContent from "./OnboardingContent";

type OnboardingProps = {
currentPageChildren: ReactNode;
};

const OnboardingComponent = ({ currentPageChildren }: OnboardingProps) => {
const [switchingRoutes, setSwitchingRoutes] = useState(false);

const onboardingStyles = classNames({
"flex flex-col h-full w-full bg-tertiary": true,
hidden: switchingRoutes,
});

return (
<div className={onboardingStyles}>
<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>
<OnboardingContent
switchingRoutes={switchingRoutes}
setSwitchingRoutes={setSwitchingRoutes}
/>
</Card>
</div>
</div>
);
};

export default OnboardingComponent;
198 changes: 198 additions & 0 deletions src/components/Onboarding/OnboardingContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import Link from "next/link";
import { useRouter } from "next/router";
import React from "react";
import {
UpdateProfileInput,
useUpdateProfileMutation,
} from "../../generated/graphql";
import {
AuthorizationLevel,
useAuthorizationLevel,
useCurrentProfile,
} from "../../hooks";
import LocalStorage from "../../utils/localstorage";
import { Button, Text } from "../atomic";
import StepTracker from "../atomic/StepTracker";

const ONBOARDING_STEP = "Onboarding Step";

type OnboardingText = (baseRoute: string) => {
[key: number]: { step: string; route: string };
};

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: OnboardingText = (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: OnboardingText = (baseRoute: string) => ({
1: {
step: "Fill out your profile",
route: baseRoute + "/edit-profile",
},
2: {
step: "Set your availability",
route: baseRoute + "/availability",
},
});

const MenteeOnboardingText: OnboardingText = (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;
}
};

type OnboardingProps = {
switchingRoutes: boolean;
setSwitchingRoutes: (bool: boolean) => void;
};

const OnboardingContent = ({
switchingRoutes,
setSwitchingRoutes,
}: OnboardingProps) => {
const currentProfile = useCurrentProfile();
const [updateProfile] = useUpdateProfileMutation({
refetchQueries: ["getMyUser"],
});

const authorizationLevel = useAuthorizationLevel();
const router = useRouter();

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

const onFinish = () => {
const updateProfileInput: UpdateProfileInput = {
onboarded: true,
};
updateProfile({
variables: {
profileId: currentProfile.currentProfile!.profileId,
data: updateProfileInput,
},
})
.then(() => {
currentProfile.refetchCurrentProfile!();
LocalStorage.delete(ONBOARDING_STEP);
})
.catch((err) => console.error(err));
};

const storedStep = LocalStorage.get(ONBOARDING_STEP);
const currentStep =
storedStep && typeof storedStep == "number" ? storedStep : 1;

//If the user tries to navigate to another route or if they land on a page that isn't the first step
//Return them to the actual page of the current step
if (
router.asPath !== onboardingText[currentStep]["route"] &&
!switchingRoutes
) {
//Hide content if switching routes
setSwitchingRoutes(true);
router
.push(onboardingText[currentStep]["route"])
.then(() => setSwitchingRoutes(false));
}

const prevStep = Math.max(currentStep - 1, 1);
const nextStep = Math.min(currentStep + 1, MAX_STEPS);

//Use Links to switch between tabs so that you don't have to wait for router.push
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}
onClick={() => {
LocalStorage.set(ONBOARDING_STEP, prevStep);
}}
>
{currentStep !== 1 ? (
<Link href={onboardingText[prevStep]["route"]}>Back</Link>
) : (
"Back"
)}
</Button>
<div className="w-4" />
<Button
size="small"
onClick={() => {
if (currentStep !== MAX_STEPS) {
LocalStorage.set(ONBOARDING_STEP, nextStep);
} else {
onFinish();
}
}}
>
{currentStep !== MAX_STEPS ? (
<Link href={onboardingText[nextStep]["route"]}>Next</Link>
) : (
"Finish"
)}
</Button>
</div>
</div>
);
};

export default OnboardingContent;
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
onboarded
program {
programId
name
Expand Down
31 changes: 25 additions & 6 deletions src/layouts/ChooseTabLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { useRouter } from "next/router";
import React, { Fragment } from "react";
import { AuthorizationLevel, useAuthorizationLevel } from "../hooks";
import {
AuthorizationLevel,
useAuthorizationLevel,
useCurrentProfile,
} from "../hooks";
import { parseParam } from "../utils";
import { MAP_PROFILETYPE_TO_ROUTE } from "../utils/constants";
import { AdminTabLayout, MenteeTabLayout, MentorTabLayout } from "./TabLayout";
import NoMatchingProfileLayout from "./TabLayout/NoMatchingProfileTabLayout";
import { BaseTabLayoutProps } from "./TabLayout/TabLayout";
import OnboardingComponent from "../components/Onboarding/Onboarding";

const NotInProgramTabLayout: React.FC<BaseTabLayoutProps> = ({
children,
Expand All @@ -25,10 +30,11 @@ function getTabLayout(
return MenteeTabLayout;
case AuthorizationLevel.Admin:
return AdminTabLayout;
case AuthorizationLevel.NoMatchingProfile:
return NoMatchingProfileLayout;
default:
case AuthorizationLevel.Unauthenticated:
case AuthorizationLevel.Unverified:
return NotInProgramTabLayout;
default:
return NoMatchingProfileLayout;
}
}

Expand All @@ -49,16 +55,29 @@ interface ChooseTabLayoutProps {
const ChooseTabLayout = ({ children }: ChooseTabLayoutProps) => {
const router = useRouter();
const slug = parseParam(router.query.slug);
const currentProfile = useCurrentProfile();

const authorizationLevel = useAuthorizationLevel();

const TabLayout = getTabLayout(authorizationLevel);

return (
<TabLayout
onboarded={
currentProfile.currentProfile?.onboarded !== undefined
? currentProfile.currentProfile.onboarded
: true
}
basePath={`/program/${slug}/${getAuthRoute(authorizationLevel)}`}
>
{children}
{/* If the user is in a program right now as a mentor, mentee, or admin,
Check for onboarding */}
{currentProfile.currentProfile?.onboarded !== undefined &&
!currentProfile.currentProfile.onboarded &&
getAuthRoute(authorizationLevel) ? (
<OnboardingComponent currentPageChildren={children} />
) : (
children
)}
</TabLayout>
);
};
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
3 changes: 2 additions & 1 deletion src/layouts/TabLayout/AdminTabLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ const { PageItem, Dropdown, Separator } = TabLayout;

const AdminTabLayout: React.FC<BaseTabLayoutProps> = ({
children,
onboarded,
basePath,
}) => {
return (
<TabLayout currentPageChildren={children}>
<TabLayout onboarded={onboarded} currentPageChildren={children}>
<PageItem label="Homepage" Icon={Home} path={joinPath(basePath)} />
<PageItem
label="Settings"
Expand Down
3 changes: 2 additions & 1 deletion src/layouts/TabLayout/MenteeTabLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ const { PageItem } = TabLayout;

const MenteeTabLayout: React.FC<BaseTabLayoutProps> = ({
children,
onboarded,
basePath,
}) => {
return (
<TabLayout currentPageChildren={children}>
<TabLayout onboarded={onboarded} currentPageChildren={children}>
<PageItem label="Homepage" Icon={Home} path={joinPath(basePath)} />
<PageItem
label="View Mentors"
Expand Down
Loading