Skip to content

Commit

Permalink
Merge pull request #43 from complexdatacollective/feature/end-session…
Browse files Browse the repository at this point in the history
…-functionality

Feature/end session functionality
  • Loading branch information
jthrilly authored Jan 9, 2024
2 parents 0086d42 + 1311a5e commit 1fd9ee8
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 28 deletions.
75 changes: 75 additions & 0 deletions app/(interview)/interview/_components/FinishInterviewModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { type Dispatch, type SetStateAction } from 'react';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '~/components/ui/AlertDialog';
import { useRouter } from 'next/navigation';
import { api } from '~/trpc/client';
import { usePathname } from 'next/navigation';
import { clientRevalidateTag } from '~/utils/clientRevalidate';

type FinishInterviewModalProps = {
open: boolean;
setOpen: Dispatch<SetStateAction<boolean>>;
};

const FinishInterviewModal = ({ open, setOpen }: FinishInterviewModalProps) => {
const router = useRouter();
const pathname = usePathname();

const interviewId = pathname.split('/').pop();
const { mutateAsync: finishInterview } = api.interview.finish.useMutation({
onError(error) {
throw new Error(error.message);
},
async onSuccess() {
await clientRevalidateTag('interview.get.byId');

router.push('/interview/finished');
},
});
const handleFinishInterview = async () => {
if (!interviewId) {
throw new Error('No interview id found');
}
await finishInterview({ id: interviewId });
};
return (
<AlertDialog open={open}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle className="text-base">
Are you sure you want finish the interview?
</AlertDialogTitle>
<AlertDialogDescription className="text-sm">
Your responses cannot be changed after you finish the interview.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel
onClick={() => {
setOpen(false);
}}
>
Cancel
</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
await handleFinishInterview();
}}
>
Finish Interview
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};

export default FinishInterviewModal;
5 changes: 5 additions & 0 deletions app/(interview)/interview/_components/InterviewShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
import { getActiveSession } from '~/lib/interviewer/selectors/session';
import { store } from '~/lib/interviewer/store';
import { api } from '~/trpc/client';
import { useRouter } from 'next/navigation';

// The job of ServerSync is to listen to actions in the redux store, and to sync
// data with the server.
Expand Down Expand Up @@ -68,6 +69,7 @@ const ServerSync = ({ interviewId }: { interviewId: string }) => {
// Eventually it will handle syncing this data back.
const InterviewShell = ({ interviewID }: { interviewID: string }) => {
const [currentStage, setCurrentStage] = useQueryState('stage');
const router = useRouter();

const { isLoading } = api.interview.get.byId.useQuery(
{ id: interviewID },
Expand All @@ -79,6 +81,9 @@ const InterviewShell = ({ interviewID }: { interviewID: string }) => {
if (!data) {
return;
}
if (data.finishTime) {
router.push('/interview/finished');
}

const { protocol, ...serverSession } = data;

Expand Down
15 changes: 15 additions & 0 deletions app/(interview)/interview/finished/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { BadgeCheck } from 'lucide-react';

export default function InterviewCompleted() {
return (
<div className="flex h-screen flex-col items-center justify-center bg-[var(--nc-background)]">
<BadgeCheck className="mb-4 h-12 w-12 text-[var(--color-sea-green)]" />
<h1 className="text-3xl font-extrabold text-white">
Thank you for participating!
</h1>
<p className="text-lg text-white">
Your interview has been successfully completed.
</p>
</div>
);
}
27 changes: 17 additions & 10 deletions lib/interviewer/containers/Interfaces/FinishSession.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { useDispatch } from 'react-redux';
import Button from '~/lib/ui/components/Button';
import FinishInterviewModal from '~/app/(interview)/interview/_components/FinishInterviewModal';

const FinishSession = ({ endSession }) => {
const FinishSession = () => {
const dispatch = useDispatch();
const handleFinishSession = () => {
// eslint-disable-next-line no-console
console.log(
'handleFinishSession /lib/interviewer/containers/Interfaces/FinishSession.js',
);
// endSession(false, true);
};
const [openFinishInterviewModal, setOpenFinishInterviewModal] =
useState(false);

useEffect(() => {
dispatch({ type: 'PLAY_SOUND', sound: 'finishSession' });
}, []);
}, [dispatch]);

return (
<div className="interface finish-session-interface">
<FinishInterviewModal
open={openFinishInterviewModal}
setOpen={setOpenFinishInterviewModal}
/>
<div className="finish-session-interface__frame">
<h1 className="finish-session-interface__title type--title-1">
Finish Interview
Expand All @@ -27,6 +28,12 @@ const FinishSession = ({ endSession }) => {
the information you have entered, you may finish the interview now.
</p>
</div>

<div className="finish-session-interface__section finish-session-interface__section--buttons">
<Button onClick={() => setOpenFinishInterviewModal(true)}>
Finish
</Button>
</div>
</div>
</div>
);
Expand Down
28 changes: 10 additions & 18 deletions lib/ui/components/Button.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import cx from 'classnames';

const renderButtonIcon = ({ icon, iconPosition }) => {
const iconClassNames = cx({
button__icon: true,
'button__icon': true,
'button__icon--right': iconPosition === 'right',
});

Expand All @@ -15,10 +15,7 @@ const renderButtonIcon = ({ icon, iconPosition }) => {
const Icon = require('./Icon').default;
iconElement = <Icon name={icon} className={iconClassNames} />;
} else {
iconElement = React.cloneElement(
icon,
{ className: iconClassNames },
);
iconElement = React.cloneElement(icon, { className: iconClassNames });
}
}
return iconElement;
Expand All @@ -40,7 +37,7 @@ class Button extends PureComponent {
} = this.props;

const buttonClassNames = cx({
button: true,
'button': true,
[`button--${color}`]: !!color,
[`button--${size}`]: !!size,
'button--has-icon': !!icon,
Expand All @@ -52,37 +49,32 @@ class Button extends PureComponent {
// eslint-disable-next-line react/button-has-type
type={type}
className={buttonClassNames}
onClick={onClick?.()}
onClick={onClick}
disabled={disabled}
// eslint-disable-next-line react/jsx-props-no-spreading
{...rest}
>
{renderButtonIcon({ icon, iconPosition })}
{(content || children) && <span className="button__content">{children || content}</span>}
{(content || children) && (
<span className="button__content">{children || content}</span>
)}
</button>
);
}
}

Button.propTypes = {
content: PropTypes.oneOfType([
PropTypes.string,
PropTypes.element,
]),
content: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
children: PropTypes.node,
icon: PropTypes.oneOfType([
PropTypes.string,
PropTypes.element,
PropTypes.object,
]),
iconPosition: PropTypes.oneOf([
'left', 'right',
]),
iconPosition: PropTypes.oneOf(['left', 'right']),
size: PropTypes.string,
color: PropTypes.string,
type: PropTypes.oneOf([
'button', 'submit', 'reset',
]),
type: PropTypes.oneOf(['button', 'submit', 'reset']),
onClick: PropTypes.func,
disabled: PropTypes.bool,
};
Expand Down
22 changes: 22 additions & 0 deletions server/routers/interview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,28 @@ export const interviewRouter = router({
return interview;
}),
}),
finish: protectedProcedure
.input(
z.object({
id: z.string(),
}),
)
.mutation(async ({ input: { id } }) => {
try {
const updatedInterview = await prisma.interview.update({
where: {
id,
},
data: {
finishTime: new Date(),
},
});

return { error: null, interview: updatedInterview };
} catch (error) {
return { error: 'Failed to update interview', interview: null };
}
}),
delete: protectedProcedure
.input(
z.array(
Expand Down

0 comments on commit 1fd9ee8

Please sign in to comment.