Skip to content

Commit

Permalink
working new navigation system
Browse files Browse the repository at this point in the history
  • Loading branch information
jthrilly committed Dec 7, 2023
1 parent 653805b commit f572b0c
Show file tree
Hide file tree
Showing 23 changed files with 548 additions and 418 deletions.
7 changes: 5 additions & 2 deletions app/(interview)/interview/[interviewId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { api } from '~/trpc/server';
import InterviewShell from '../_components/InterviewShell';
import NoSSRWrapper from '~/utils/NoSSRWrapper';
import type { Prisma } from '@prisma/client';

export const dynamic = 'force-dynamic';

export type ServerSession = Prisma.InterviewGetPayload<null>;

export default async function Page({
params,
}: {
Expand All @@ -22,14 +25,14 @@ export default async function Page({
return 'No interview found';
}

const { protocol, ...serverInterview } = interview;
const { protocol, ...serverSession } = interview;

return (
<div className="flex h-[100vh] flex-col bg-[var(--nc-background)] text-[var(--nc-text)]">
<NoSSRWrapper>
<InterviewShell
serverProtocol={protocol}
serverInterview={serverInterview}
serverSession={serverSession}
/>
</NoSSRWrapper>
</div>
Expand Down
40 changes: 27 additions & 13 deletions app/(interview)/interview/_components/InterviewShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,45 @@ import DialogManager from '~/lib/interviewer/components/DialogManager';
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 { useEffect, useState } from 'react';
import { parseAsInteger, useQueryState } from 'next-usequerystate';
import type { Protocol } from '@codaco/shared-consts';
import type { ServerSession } from '../[interviewId]/page';
import {
SET_SERVER_SESSION,
type SetServerSessionAction,
} from '~/lib/interviewer/ducks/modules/setServerSession';

// 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();
const InterviewShell = ({
serverProtocol,
serverSession,
}: {
serverProtocol: Protocol;
serverSession: ServerSession;
}) => {
const [loading, setLoading] = useState(true);

useEffect(() => {
store.dispatch({
type: 'SET_SERVER_SESSION',
payload: serverSession,
store.dispatch<SetServerSessionAction>({
type: SET_SERVER_SESSION,
payload: {
protocol: serverProtocol,
session: serverSession,
},
});
}, [serverSession]);
setLoading(false);
}, [serverSession, serverProtocol]);

const [stage, setStage] = useQueryState(
'stage',
parseAsInteger.withDefault(1),
);
if (loading) {
return 'Second loading stage...';
}

return (
<Provider store={store}>
{session && <UserBanner />}
<UserBanner />
<ProtocolScreen />
<DialogManager />
</Provider>
Expand Down
7 changes: 6 additions & 1 deletion app/(interview)/interview/_components/UserBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import { Button } from '~/components/ui/Button';
import { useSession } from '~/providers/SessionProvider';

export default function UserBanner() {
const { signOut } = useSession();
const { session, signOut } = useSession();

if (!session) {
return null;
}

return (
<div className="relative isolate flex items-center overflow-hidden bg-gray-200 px-6 text-primary">
<div
Expand Down
105 changes: 61 additions & 44 deletions lib/interviewer/components/Navigation.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useMemo } from 'react';
import React, { useCallback, useEffect } from 'react';
import ProgressBar from '~/lib/ui/components/ProgressBar';
import { ChevronDown, ChevronUp, SettingsIcon } from 'lucide-react';
import { cn } from '~/utils/shadcn';
Expand All @@ -18,76 +18,93 @@ const useNavigationHelpers = (

const { isReady: isReadyForNextStage } = useReadyForNextStage();

const { progress } = useSelector(getNavigationInfo);
const {
progress,
isLastPrompt,
isFirstPrompt,
isLastStage,
promptIndex,
canMoveBackward,
canMoveForward,
} = useSelector(getNavigationInfo);

const calculateNextStage = useCallback(() => {
const nextStage = Object.keys(skipMap).find(
(stage) =>
parseInt(stage) > currentStage && skipMap[parseInt(stage)] === false,
);

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

return parseInt(nextStage);
}, [currentStage, skipMap]);

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

if (!previousStage) {
return currentStage;
}

return parseInt(previousStage);
}, [currentStage, skipMap]);

const validateCurrentStage = useCallback(() => {
if (!isCurrentStageValid) {
const previousValidStage = getPreviousValidStage();
if (!skipMap[currentStage] === false) {
const previousValidStage = calculatePreviousStage();

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

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

// Move to the next available stage in the interview based on the current stage and skip logic
const moveForward = () => {
const nextAvailableStage = Object.keys(skipMap).find(
(stage) => parseInt(stage) > currentStage && skipMap[stage] === false,
);
const moveForward = useCallback(() => {
if (isLastPrompt) {
const nextStage = calculateNextStage();
setCurrentStage(nextStage);
return;
}

dispatch(sessionActions.updateStage({ stageIndex: nextAvailableStage }));
};
dispatch(sessionActions.updatePrompt(promptIndex + 1));
}, [
dispatch,
isLastPrompt,
promptIndex,
calculateNextStage,
setCurrentStage,
]);

// Move to the previous available stage in the interview based on the current stage and skip logic
const moveBackward = () => {
const previousAvailableStage = Object.keys(skipMap)
.reverse()
.find(
(stage) => parseInt(stage) < currentStage && skipMap[stage] === false,
);
if (isFirstPrompt) {
const previousStage = calculatePreviousStage();
setCurrentStage(previousStage);
return;
}

dispatch(
sessionActions.updateStage({ stageIndex: previousAvailableStage }),
);
dispatch(sessionActions.updatePrompt(promptIndex - 1));
};

// When the current stage changes, try to set the session currentStage to the new value using the
// `canNavigateToStage` method.
useEffect(() => {
if (canNavigateToStage(currentStage)) {
dispatch(sessionActions.updateStage({ stageIndex: currentStage }));
}
}, [currentStage, canNavigateToStage, dispatch]);
dispatch(sessionActions.updateStage(currentStage));
}, [currentStage, dispatch]);

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

Expand Down
2 changes: 1 addition & 1 deletion lib/interviewer/components/RealtimeCanvas/EdgeLayout.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const EdgeLayout = () => {
const svgNS = svg.current.namespaceURI;
const el = document.createElementNS(svgNS, 'line');
const color = get(edgeDefinitions, [edge.type, 'color'], 'edge-color-seq-1');
el.setAttributeNS(null, 'stroke', `var(--${color})`);
el.setAttributeNS(null, 'stroke', `var(--nc-${color})`);
return { edge, el, link: links[index] };
});

Expand Down
2 changes: 1 addition & 1 deletion lib/interviewer/containers/Interfaces/DyadCensus/Pair.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ const Pair = ({
<Node {...fromNode} />
<motion.div
className="dyad-census__edge"
style={{ backgroundColor: `var(--${edgeColor})` }}
style={{ backgroundColor: `var(--nc-${edgeColor})` }}
variants={edgeVariants}
initial="hideEdge"
animate={!hasEdge ? 'hideEdge' : 'showEdge'}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ const Pair = ({
<Node {...fromNode} />
<motion.div
className="dyad-census__edge"
style={{ backgroundColor: `var(--${edgeColor})` }}
style={{ backgroundColor: `var(--nc-${edgeColor})` }}
variants={edgeVariants}
initial="hideEdge"
animate={!hasEdge ? 'hideEdge' : 'showEdge'}
Expand Down
16 changes: 8 additions & 8 deletions lib/interviewer/containers/ProtocolScreen.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@ import { getCurrentStage } from '../selectors/session';
import { useSelector } from 'react-redux';
import Navigation from '../components/Navigation';

const ProtocolScreen = () => {
const currentStage = useSelector(getCurrentStage);
const registerBeforeNext = () => {
// console.log('TODO: implement registerBeforeNext lib/interviewer/containers/ProtocolScreen.js');
};

const registerBeforeNext = () => {
console.log('TODO: implement registerBeforeNext lib/interviewer/containers/ProtocolScreen.js');
};
const onComplete = () => {
// console.log('TODO: implement onComplete lib/interviewer/containers/ProtocolScreen.js');
};

const onComplete = () => {
console.log('TODO: implement onComplete lib/interviewer/containers/ProtocolScreen.js');
};
const ProtocolScreen = () => {
const currentStage = useSelector(getCurrentStage);

return (
<motion.div className='flex h-full w-screen flex-row' initial={{ opacity: 0 }} animate={{ opacity: 1 }}>
Expand Down
2 changes: 1 addition & 1 deletion lib/interviewer/containers/SlidesForm/SlideFormEdge.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class SlideFormEdge extends PureComponent {
<div className="swiper-slide">
<div className="slide-content">
<Node {...fromNode} />
<div className="fake-edge" style={{ backgroundColor: `var(--${edgeColor})` }} />
<div className="fake-edge" style={{ backgroundColor: `var(--nc-${edgeColor})` }} />
<Node {...toNode} />
<div className="alter-form__form-container alter-edge-form__form-container">
<Scroller onScroll={onScroll}>
Expand Down
10 changes: 10 additions & 0 deletions lib/interviewer/ducks/modules/activeSessionId.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { actionTypes as SessionsActionTypes, actionCreators as sessionActions } from './session';
import { actionTypes as installedProtocolsActionTypes } from './installedProtocols';
import { SET_SERVER_SESSION } from './setServerSession';

const { ADD_SESSION } = SessionsActionTypes;
const SET_SESSION = 'SET_SESSION';
Expand All @@ -9,6 +10,15 @@ const initialState = null;

export default function reducer(state = initialState, action = {}) {
switch (action.type) {
case SET_SERVER_SESSION: {
if (!action.payload.session) {
return state;
}

const { id } = action.payload.session;

return id;
}
case SET_SESSION:
case ADD_SESSION:
return action.sessionId;
Expand Down
42 changes: 16 additions & 26 deletions lib/interviewer/ducks/modules/installedProtocols.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,13 @@ import React from 'react';
import { omit, findKey, get } from 'lodash';
import { actionCreators as dialogActions } from './dialogs';
import { withErrorDialog } from './errors';
import { SET_SERVER_SESSION } from './setServerSession';

const IMPORT_PROTOCOL_COMPLETE = 'IMPORT_PROTOCOL_COMPLETE';
const IMPORT_PROTOCOL_FAILED = 'IMPORT_PROTOCOL_FAILED';
const DELETE_PROTOCOL = 'INSTALLED_PROTOCOLS/DELETE_PROTOCOL';

const initialState = {
'1': {
name: 'test protocol',
stages: [{
"id": "1151e210-7969-11ee-a112-2fa2ba6c2d8e",
"type": "Information",
"items": [
{
"id": "b21c9dca-fa6a-40a2-8652-ec542939f71c",
"size": "MEDIUM",
"type": "text",
"content": "We can render text.\n\n<br>\n\n\nThe text has **markdown**!\n\n<br>\n\n\n\n- Including a\n- list!\n\n"
},
{
"id": "ae526a7f-28d5-4c75-a5eb-c30a0b263bcd",
"size": "MEDIUM",
"type": "asset",
"content": "38dc9035-02b4-4906-b0c5-9849415960fe"
}
],
"label": "Information Interface",
"title": "Information Interface"
}],
codebook: {},
}
};
const initialState = {};

const protocolHasSessions = (state, protocolUID) => new Promise((resolve) => {
const hasNotExportedSession = !!findKey(
Expand Down Expand Up @@ -104,6 +80,20 @@ const deleteProtocolAction = (protocolUID) => (dispatch, getState) =>

export default function reducer(state = initialState, action = {}) {
switch (action.type) {
case SET_SERVER_SESSION: {
if (!action.payload.protocol) { return state; }

const { protocol } = action.payload;
const uid = protocol.id;

return {
...state,
[uid]: {
...omit(protocol, 'id'),
installationDate: Date.now(),
},
};
}
case DELETE_PROTOCOL:
return omit(state, [action.protocolUID]);
case IMPORT_PROTOCOL_COMPLETE: {
Expand Down
Loading

0 comments on commit f572b0c

Please sign in to comment.