diff --git a/src/components/Onboarding/Onboarding.tsx b/src/components/Onboarding/Onboarding.tsx new file mode 100644 index 00000000..916c62ce --- /dev/null +++ b/src/components/Onboarding/Onboarding.tsx @@ -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 ( +
+
{currentPageChildren}
+ +
+ + + Welcome to your new program! + + + +
+
+ ); +}; + +export default OnboardingComponent; diff --git a/src/components/Onboarding/OnboardingContent.tsx b/src/components/Onboarding/OnboardingContent.tsx new file mode 100644 index 00000000..1b6f5c24 --- /dev/null +++ b/src/components/Onboarding/OnboardingContent.tsx @@ -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 ( +
+ + {currentStep}) {onboardingText[currentStep]["step"]} + +
+
+
+ +
+ + +
+ +
+
+ ); +}; + +export default OnboardingContent; diff --git a/src/components/RichTextEditing/PublishButton.tsx b/src/components/RichTextEditing/PublishButton.tsx index 88d49ef4..f3ae16b7 100644 --- a/src/components/RichTextEditing/PublishButton.tsx +++ b/src/components/RichTextEditing/PublishButton.tsx @@ -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); }); }; diff --git a/src/components/TabFooterMenu.tsx b/src/components/TabFooterMenu.tsx index 0be9c636..9ff15cd1 100644 --- a/src/components/TabFooterMenu.tsx +++ b/src/components/TabFooterMenu.tsx @@ -17,7 +17,7 @@ const TabFooterMenu = () => { const myData = data.getMyUser; return ( -
+
diff --git a/src/components/atomic/StepTracker.tsx b/src/components/atomic/StepTracker.tsx new file mode 100644 index 00000000..68ac7bc0 --- /dev/null +++ b/src/components/atomic/StepTracker.tsx @@ -0,0 +1,24 @@ +import { range } from "lodash"; +import React, { HTMLAttributes } from "react"; + +type StepTrackerProps = HTMLAttributes & { + steps: number; + currentStep: number; +}; + +const StepTracker = ({ steps, currentStep }: StepTrackerProps) => ( +
+ {range(1, steps + 1).map((i: number) => { + return ( +
+ ); + })} +
+); + +export default StepTracker; diff --git a/src/graphql/queries/getMyUser.graphql b/src/graphql/queries/getMyUser.graphql index 763f1709..38b63fea 100644 --- a/src/graphql/queries/getMyUser.graphql +++ b/src/graphql/queries/getMyUser.graphql @@ -12,6 +12,7 @@ query getMyUser { profileJson tagsJson bio + onboarded program { programId name diff --git a/src/layouts/ChooseTabLayout.tsx b/src/layouts/ChooseTabLayout.tsx index 53a004b7..73039a85 100644 --- a/src/layouts/ChooseTabLayout.tsx +++ b/src/layouts/ChooseTabLayout.tsx @@ -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 = ({ children, @@ -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; } } @@ -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 ( - {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) ? ( + + ) : ( + children + )} ); }; diff --git a/src/layouts/PageContainer.tsx b/src/layouts/PageContainer.tsx index bd16d7b3..4fcf0d4f 100644 --- a/src/layouts/PageContainer.tsx +++ b/src/layouts/PageContainer.tsx @@ -3,7 +3,7 @@ import { HTMLAttributes } from "react"; const PageContainer = ({ children }: HTMLAttributes) => { // TODO: Add responsiveness return ( -
+
{children}
); diff --git a/src/layouts/TabLayout/AdminTabLayout.tsx b/src/layouts/TabLayout/AdminTabLayout.tsx index f84de9f6..58498899 100644 --- a/src/layouts/TabLayout/AdminTabLayout.tsx +++ b/src/layouts/TabLayout/AdminTabLayout.tsx @@ -12,10 +12,11 @@ const { PageItem, Dropdown, Separator } = TabLayout; const AdminTabLayout: React.FC = ({ children, + onboarded, basePath, }) => { return ( - + = ({ children, + onboarded, basePath, }) => { return ( - + = ({ children, + onboarded, basePath, }) => { return ( - + = ({ }); return ( - +
{ diff --git a/src/layouts/TabLayout/NoProgramTabLayout.tsx b/src/layouts/TabLayout/NoProgramTabLayout.tsx index 28c49fe2..bca6961d 100644 --- a/src/layouts/TabLayout/NoProgramTabLayout.tsx +++ b/src/layouts/TabLayout/NoProgramTabLayout.tsx @@ -18,7 +18,7 @@ const NoProgramTabLayout: React.FC = ({ children }) => { } return ( - + ); diff --git a/src/layouts/TabLayout/TabLayout.tsx b/src/layouts/TabLayout/TabLayout.tsx index fa3b4a60..70fdc475 100644 --- a/src/layouts/TabLayout/TabLayout.tsx +++ b/src/layouts/TabLayout/TabLayout.tsx @@ -46,6 +46,7 @@ const Arrow: React.FC = ({ down }) => { export interface BaseTabLayoutProps { children: React.ReactNode; + onboarded: boolean; basePath: string; } @@ -64,6 +65,7 @@ export function joinPath(...args: string[]): string { } interface TabLayoutProps { + onboarded: boolean; currentPageChildren: ReactNode; footerChildren?: ReactNode; } @@ -75,16 +77,24 @@ const TabLayout: React.FC & { label: string; Icon: React.FC>; }>; -} = ({ children, currentPageChildren }) => { +} = ({ children, onboarded, currentPageChildren }) => { + const tabLayoutStyles = classNames({ + "flex flex-col flex-grow h-screen bg-white shadow-lg relative box-border": + true, + "pointer-events-none bg-black opacity-25": !onboarded, + }); + return (
-
+
{children}
-
{currentPageChildren}
+
+ {currentPageChildren} +
); }; diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index f84ad570..67490ef6 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -11,10 +11,10 @@ import { AppProps } from "next/app"; import { ReactElement } from "react"; import "tailwindcss/tailwind.css"; import AuthLoadingScreen from "../layouts/AuthLoadingScreen"; +import { AuthProvider } from "../utils/firebase/auth"; import { SnackbarProvider } from "../notifications/SnackbarContext"; import "../styles/globals.css"; import Page from "../types/Page"; -import { AuthProvider } from "../utils/firebase/auth"; import firebase from "../utils/firebase/firebase"; const uploadLink = createUploadLink({ uri: process.env.NEXT_PUBLIC_API_URL }); diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 4b640ceb..6f5e338c 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -337,7 +337,7 @@ const NoMentorshipHome: Page = () => { } return ( - +
diff --git a/src/pages/program/[slug]/[profileRoute]/edit-profile.tsx b/src/pages/program/[slug]/[profileRoute]/edit-profile.tsx index 6fc3ea39..a4f224cf 100644 --- a/src/pages/program/[slug]/[profileRoute]/edit-profile.tsx +++ b/src/pages/program/[slug]/[profileRoute]/edit-profile.tsx @@ -71,9 +71,7 @@ const EditProfilePage: Page = (_) => { setBio(currentProfile.bio || ""); setProfileJson(getResponsesFromJson(currentProfile.profileJson)); setSelectedTagIds(currentProfile.profileTags.map((t) => t.profileTagId)); - - return () => {}; - }, [currentProfile]); + }, []); if (!currentProgram || !currentProfile || !myUserData || !programTagsData) return
404
; diff --git a/src/pages/program/[slug]/[profileRoute]/mentees/edit-profile/index.tsx b/src/pages/program/[slug]/[profileRoute]/mentees/edit-profile/index.tsx index 88cfe7f1..b2007df5 100644 --- a/src/pages/program/[slug]/[profileRoute]/mentees/edit-profile/index.tsx +++ b/src/pages/program/[slug]/[profileRoute]/mentees/edit-profile/index.tsx @@ -26,7 +26,7 @@ const EditMenteeProfilePage: Page = (_) => { const [modified, setModified] = useState(false); const [isSavingProfile, setIsSavingProfile] = useState(false); const { setSnackbarMessage } = useSnackbar(); - isSavingProfile; // TODO: If is saving, set loading state of button to true. + // TODO: If is saving, set loading state of button to true. useEffect(() => { if (!currentProgram) return; @@ -69,7 +69,7 @@ const EditMenteeProfilePage: Page = (_) => {
); diff --git a/tailwind.config.js b/tailwind.config.js index 131a67ea..f677fb6a 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -34,6 +34,9 @@ module.exports = { bold: 700, }, extend: { + animation: { + "border-pulse": "border-pulse 1s ease-in-out infinite", + }, borderWidth: { 1.5: "1.5px", 3: "3px", @@ -48,10 +51,26 @@ module.exports = { "se-resize": "se-resize", "sw-resize": "sw-resize", }, + fill: { + none: "none", + }, height: { "3/4-screen": "75vh", "9/10": "90%", }, + keyframes: { + "border-pulse": { + "0%": { + "-webkit-box-shadow": "0 0 4px 3px rgba(0,0,0, 0.3)", + }, + "50%": { + "-webkit-box-shadow": "0 0 4px 6px rgba(0,0,0, 0.9)", + }, + "100%": { + "-webkit-box-shadow": "0 0 4px 3px rgba(0,0,0, 0.3)", + }, + }, + }, maxHeight: { "3/4": "75%", }, @@ -68,6 +87,9 @@ module.exports = { 300: "75rem", 400: "100rem", }, + transitionDuration: { + 3000: "3000ms", + }, transitionProperty: { background: "background-color", }, @@ -82,9 +104,6 @@ module.exports = { stroke: (theme) => ({ white: theme("colors.white"), }), - fill: { - none: "none", - }, }, }, variants: {