Skip to content

Commit

Permalink
wip of new navigation with single source of truth
Browse files Browse the repository at this point in the history
  • Loading branch information
jthrilly committed Dec 7, 2023
1 parent ded387d commit 653805b
Show file tree
Hide file tree
Showing 14 changed files with 121 additions and 462 deletions.
6 changes: 4 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
{
"css.customData": ["./.vscode/css-data.json"],
"css.customData": [
"./.vscode/css-data.json"
],
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true,
"WillLuke.nextjs.addTypesOnSave": true,
"WillLuke.nextjs.hasPrompted": true
}
}
14 changes: 14 additions & 0 deletions app/(interview)/interview/_components/InterviewShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,27 @@ import ProtocolScreen from '~/lib/interviewer/containers/ProtocolScreen';
import { store } from '~/lib/interviewer/store';
import UserBanner from './UserBanner';
import { useSession } from '~/providers/SessionProvider';
import { useEffect } from 'react';
import { parseAsInteger, useQueryState } from 'next-usequerystate';

// The job of interview shell is to receive the server-side session and protocol
// and create a redux store with that data.
// Eventually it will handle syncing this data back.
const InterviewShell = ({ serverProtocol, serverSession }) => {
const { session } = useSession();

useEffect(() => {
store.dispatch({
type: 'SET_SERVER_SESSION',
payload: serverSession,
});
}, [serverSession]);

const [stage, setStage] = useQueryState(
'stage',
parseAsInteger.withDefault(1),
);

return (
<Provider store={store}>
{session && <UserBanner />}
Expand Down
86 changes: 57 additions & 29 deletions lib/interviewer/components/Navigation.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,54 @@
import React, { useEffect } from 'react';
import React, { useCallback, useEffect, useMemo } from 'react';
import ProgressBar from '~/lib/ui/components/ProgressBar';
import useReadyForNextStage from '~/lib/interviewer/hooks/useReadyForNextStage';
import { ChevronDown, ChevronUp, SettingsIcon } from 'lucide-react';
import { cn } from '~/utils/shadcn';
import { useDispatch, useSelector } from 'react-redux';
import { type State, getNavigationInfo } from '../selectors/session';
import { getNavigationInfo } from '../selectors/session';
import { getSkipMap } from '../selectors/skip-logic';
import { parseAsInteger, useQueryState } from 'next-usequerystate';
import { actionCreators as sessionActions } from '../ducks/modules/session';
import useReadyForNextStage from '../hooks/useReadyForNextStage';

type SkipMap = Record<string, boolean>;

const useNavigation = () => {
const useNavigationHelpers = (
currentStage: number,
setCurrentStage: (stage: number) => void,
) => {
const dispatch = useDispatch();
const skipMap = useSelector(getSkipMap);

const [isReadyForNextStage] = useReadyForNextStage();
const skipMap: SkipMap = useSelector(getSkipMap);
const {
progress,
isFirstPrompt,
isLastPrompt,
isLastStage,
currentStageIndex,
currentPromptIndex,
} = useSelector((state: State) => getNavigationInfo(state));
const { isReady: isReadyForNextStage } = useReadyForNextStage();

const [currentStage, setCurrentStage] = useQueryState(
'stage',
parseAsInteger.withDefault(1),
);
const { progress } = useSelector(getNavigationInfo);

const isCurrentStageValid = useMemo(() => {
return skipMap[currentStage] === false;
}, [currentStage, skipMap]);

const getPreviousValidStage = useCallback(() => {
return Object.keys(skipMap)
.reverse()
.find(
(stage) => parseInt(stage) < currentStage && skipMap[stage] === false,
);
}, [currentStage, skipMap]);

const validateCurrentStage = useCallback(() => {
if (!isCurrentStageValid) {
const previousValidStage = getPreviousValidStage();

if (previousValidStage) {
setCurrentStage(parseInt(previousValidStage));
}
}
}, [isCurrentStageValid, getPreviousValidStage, setCurrentStage]);

// Ddetermine if we can navigate to a given stage based on the skip logic
const canNavigateToStage = (stage: number) => {
return skipMap[stage];
};
const canNavigateToStage = useCallback(
(stage: number) => {
return skipMap[stage];
},
[skipMap],
);

// Move to the next available stage in the interview based on the current stage and skip logic
const moveForward = () => {
Expand Down Expand Up @@ -66,11 +82,12 @@ const useNavigation = () => {

return {
progress,
isReadyForNextStage: true,
isReadyForNextStage,
canMoveForward: true,
canMoveBackward: true,
moveForward,
moveBackward,
validateCurrentStage,
};
};

Expand Down Expand Up @@ -103,14 +120,25 @@ const NavigationButton = ({
};

const Navigation = () => {
const [currentStage, setCurrentStage] = useQueryState(
'stage',
parseAsInteger.withDefault(1),
);

const {
validateCurrentStage,
moveBackward,
moveForward,
canMoveBackward,
canMoveForward,
progress,
isReadyForNextStage,
canMoveForward,
canMoveBackward,
moveForward,
moveBackward,
} = useNavigation();
} = useNavigationHelpers(currentStage, setCurrentStage);

// Check if the current stage is valid for us to be on.
useEffect(() => {
validateCurrentStage();
}, [validateCurrentStage]);

return (
<div
Expand Down
2 changes: 1 addition & 1 deletion lib/interviewer/containers/Interfaces/EgoForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const EgoForm = ({
const [showScrollStatus, setShowScrollStatus] = useFlipflop(true, 7000, false);
const [isOverflowing, setIsOverflowing] = useState(false);

const [, setIsReadyForNext] = useReadyForNextStage();
const { updateReady: setIsReadyForNext } = useReadyForNextStage();

const formState = useRef({
isFormDirty,
Expand Down
150 changes: 0 additions & 150 deletions lib/interviewer/containers/Search/Search.js

This file was deleted.

Loading

0 comments on commit 653805b

Please sign in to comment.