From a9a1727692e96ffb26b25222fc392bc589263e17 Mon Sep 17 00:00:00 2001 From: Caden Buckhalt Date: Wed, 20 Dec 2023 14:19:25 -0800 Subject: [PATCH 1/9] add back in finish button it was accidentally removed in a59e3a5 --- lib/interviewer/containers/Interfaces/FinishSession.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/interviewer/containers/Interfaces/FinishSession.js b/lib/interviewer/containers/Interfaces/FinishSession.js index 2da637f1..bccc6a31 100644 --- a/lib/interviewer/containers/Interfaces/FinishSession.js +++ b/lib/interviewer/containers/Interfaces/FinishSession.js @@ -1,5 +1,6 @@ import { useEffect } from 'react'; import { useDispatch } from 'react-redux'; +import { Button } from '~/lib/ui/components'; const FinishSession = ({ endSession }) => { const dispatch = useDispatch(); @@ -13,7 +14,7 @@ const FinishSession = ({ endSession }) => { useEffect(() => { dispatch({ type: 'PLAY_SOUND', sound: 'finishSession' }); - }, []); + }, [dispatch]); return (
@@ -27,6 +28,10 @@ const FinishSession = ({ endSession }) => { the information you have entered, you may finish the interview now.

+ +
+ +
); From 2f0dff87a7b5ebf0625809c129da67e01b2bcd4d Mon Sep 17 00:00:00 2001 From: Caden Buckhalt Date: Wed, 20 Dec 2023 14:57:04 -0800 Subject: [PATCH 2/9] feature: confirmation dialog button onclick was immediately invoked and making this component not work. changes Button component's onClick to not be immediately invoked --- .../_components/FinishInterviewModal.tsx | 55 +++++++++++++++++++ .../containers/Interfaces/FinishSession.js | 24 ++++---- lib/ui/components/Button.js | 28 ++++------ 3 files changed, 78 insertions(+), 29 deletions(-) create mode 100644 app/(interview)/interview/_components/FinishInterviewModal.tsx diff --git a/app/(interview)/interview/_components/FinishInterviewModal.tsx b/app/(interview)/interview/_components/FinishInterviewModal.tsx new file mode 100644 index 00000000..eb330e45 --- /dev/null +++ b/app/(interview)/interview/_components/FinishInterviewModal.tsx @@ -0,0 +1,55 @@ +import { type Dispatch, type SetStateAction } from 'react'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '~/components/ui/AlertDialog'; + +type FinishInterviewModalProps = { + open: boolean; + setOpen: Dispatch>; +}; + +const FinishInterviewModal = ({ open, setOpen }: FinishInterviewModalProps) => { + const handleFinishInterview = () => { + // redirect to thank you for participating page + // mark session as finished by updating finishedAt + }; + return ( + + + + + Are you sure you want finish the interview? + + + Your responses cannot be changed after you finish the interview. + + + + { + setOpen(false); + }} + > + Cancel + + { + handleFinishInterview(); + }} + > + Finish Interview + + + + + ); +}; + +export default FinishInterviewModal; diff --git a/lib/interviewer/containers/Interfaces/FinishSession.js b/lib/interviewer/containers/Interfaces/FinishSession.js index bccc6a31..cbf5551b 100644 --- a/lib/interviewer/containers/Interfaces/FinishSession.js +++ b/lib/interviewer/containers/Interfaces/FinishSession.js @@ -1,16 +1,12 @@ -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { useDispatch } from 'react-redux'; -import { Button } from '~/lib/ui/components'; +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' }); @@ -18,6 +14,10 @@ const FinishSession = ({ endSession }) => { return (
+

Finish Interview @@ -30,7 +30,9 @@ const FinishSession = ({ endSession }) => {

- +
diff --git a/lib/ui/components/Button.js b/lib/ui/components/Button.js index 0ae40502..1709d420 100644 --- a/lib/ui/components/Button.js +++ b/lib/ui/components/Button.js @@ -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', }); @@ -15,10 +15,7 @@ const renderButtonIcon = ({ icon, iconPosition }) => { const Icon = require('./Icon').default; iconElement = ; } else { - iconElement = React.cloneElement( - icon, - { className: iconClassNames }, - ); + iconElement = React.cloneElement(icon, { className: iconClassNames }); } } return iconElement; @@ -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, @@ -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) && {children || content}} + {(content || children) && ( + {children || content} + )} ); } } 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, }; From dbc3019ac676d8def4935c5160c8ad5eea3f7bfc Mon Sep 17 00:00:00 2001 From: Caden Buckhalt Date: Wed, 20 Dec 2023 15:01:24 -0800 Subject: [PATCH 3/9] wip thanks for participating page --- .../interview/_components/FinishInterviewModal.tsx | 3 +++ app/(interview)/interview/finished/page.tsx | 7 +++++++ 2 files changed, 10 insertions(+) create mode 100644 app/(interview)/interview/finished/page.tsx diff --git a/app/(interview)/interview/_components/FinishInterviewModal.tsx b/app/(interview)/interview/_components/FinishInterviewModal.tsx index eb330e45..09071d4d 100644 --- a/app/(interview)/interview/_components/FinishInterviewModal.tsx +++ b/app/(interview)/interview/_components/FinishInterviewModal.tsx @@ -9,6 +9,7 @@ import { AlertDialogHeader, AlertDialogTitle, } from '~/components/ui/AlertDialog'; +import { useRouter } from 'next/navigation'; type FinishInterviewModalProps = { open: boolean; @@ -16,8 +17,10 @@ type FinishInterviewModalProps = { }; const FinishInterviewModal = ({ open, setOpen }: FinishInterviewModalProps) => { + const router = useRouter(); const handleFinishInterview = () => { // redirect to thank you for participating page + router.push('/interview/finished'); // mark session as finished by updating finishedAt }; return ( diff --git a/app/(interview)/interview/finished/page.tsx b/app/(interview)/interview/finished/page.tsx new file mode 100644 index 00000000..23120dbe --- /dev/null +++ b/app/(interview)/interview/finished/page.tsx @@ -0,0 +1,7 @@ +export default function Page() { + return ( +
+

Thank you for participating!

+
+ ); +} From 595e7102c0cdc4af2b800c078c2fe6bb4e8e4a15 Mon Sep 17 00:00:00 2001 From: Caden Buckhalt Date: Thu, 21 Dec 2023 11:16:06 -0800 Subject: [PATCH 4/9] update finishedAt on finish. check for finished on interview route to redirect --- .../interview/[interviewId]/page.tsx | 18 ++++++++++- .../_components/FinishInterviewModal.tsx | 31 +++++++++++++++---- server/routers/interview.ts | 22 +++++++++++++ 3 files changed, 64 insertions(+), 7 deletions(-) diff --git a/app/(interview)/interview/[interviewId]/page.tsx b/app/(interview)/interview/[interviewId]/page.tsx index 4992c965..5c5fb6d2 100644 --- a/app/(interview)/interview/[interviewId]/page.tsx +++ b/app/(interview)/interview/[interviewId]/page.tsx @@ -1,13 +1,29 @@ import NoSSRWrapper from '~/utils/NoSSRWrapper'; import InterviewShell from '../_components/InterviewShell'; +import { api } from '~/trpc/server'; +import { redirect } from 'next/navigation'; -export default function Page({ params }: { params: { interviewId: string } }) { +export default async function Page({ + params, +}: { + params: { interviewId: string }; +}) { const { interviewId } = params; if (!interviewId) { return 'No interview id found'; } + // check if interview is finished + const interview = await api.interview.get.byId.query({ id: interviewId }); + if (!interview) { + return 'Interview not found'; + } + + if (interview.finishTime) { + redirect('/interview/finished'); + } + return (
diff --git a/app/(interview)/interview/_components/FinishInterviewModal.tsx b/app/(interview)/interview/_components/FinishInterviewModal.tsx index 09071d4d..69463ddf 100644 --- a/app/(interview)/interview/_components/FinishInterviewModal.tsx +++ b/app/(interview)/interview/_components/FinishInterviewModal.tsx @@ -10,6 +10,9 @@ import { 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; @@ -18,10 +21,26 @@ type FinishInterviewModalProps = { const FinishInterviewModal = ({ open, setOpen }: FinishInterviewModalProps) => { const router = useRouter(); - const handleFinishInterview = () => { - // redirect to thank you for participating page - router.push('/interview/finished'); - // mark session as finished by updating finishedAt + const pathname = usePathname(); + const utils = api.useUtils(); + + 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'); + await utils.interview.get.invalidate(); + await utils.interview.get.byId.refetch(); + router.push('/interview/finished'); + }, + }); + const handleFinishInterview = async () => { + if (!interviewId) { + throw new Error('No interview id found'); + } + await finishInterview({ id: interviewId }); }; return ( @@ -43,8 +62,8 @@ const FinishInterviewModal = ({ open, setOpen }: FinishInterviewModalProps) => { Cancel { - handleFinishInterview(); + onClick={async () => { + await handleFinishInterview(); }} > Finish Interview diff --git a/server/routers/interview.ts b/server/routers/interview.ts index a6957474..a7c6560a 100644 --- a/server/routers/interview.ts +++ b/server/routers/interview.ts @@ -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( From 8788fbd02ec9b65bcd35016f494c6bf8648e8424 Mon Sep 17 00:00:00 2001 From: Caden Buckhalt Date: Thu, 21 Dec 2023 11:28:15 -0800 Subject: [PATCH 5/9] style: finished interview page --- app/(interview)/interview/finished/page.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/app/(interview)/interview/finished/page.tsx b/app/(interview)/interview/finished/page.tsx index 23120dbe..3b51cb8c 100644 --- a/app/(interview)/interview/finished/page.tsx +++ b/app/(interview)/interview/finished/page.tsx @@ -1,7 +1,15 @@ -export default function Page() { +import { BadgeCheck } from 'lucide-react'; + +export default function InterviewCompleted() { return ( -
-

Thank you for participating!

+
+ +

+ Thank you for participating! +

+

+ Your interview has been successfully completed. +

); } From 79ae1a91b04c3ddf162f5fd5b06a9f28787df6fd Mon Sep 17 00:00:00 2001 From: Caden Buckhalt Date: Thu, 21 Dec 2023 12:10:45 -0800 Subject: [PATCH 6/9] style: nc bg and icon colors --- app/(interview)/interview/finished/page.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/(interview)/interview/finished/page.tsx b/app/(interview)/interview/finished/page.tsx index 3b51cb8c..b62669eb 100644 --- a/app/(interview)/interview/finished/page.tsx +++ b/app/(interview)/interview/finished/page.tsx @@ -2,12 +2,12 @@ import { BadgeCheck } from 'lucide-react'; export default function InterviewCompleted() { return ( -
- -

+
+ +

Thank you for participating!

-

+

Your interview has been successfully completed.

From 0b7675e8722de5cca999e3f572d157fc4bcaba97 Mon Sep 17 00:00:00 2001 From: Caden Buckhalt Date: Thu, 21 Dec 2023 12:11:11 -0800 Subject: [PATCH 7/9] style: nc icon color --- app/(interview)/interview/finished/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/(interview)/interview/finished/page.tsx b/app/(interview)/interview/finished/page.tsx index b62669eb..962d367d 100644 --- a/app/(interview)/interview/finished/page.tsx +++ b/app/(interview)/interview/finished/page.tsx @@ -3,7 +3,7 @@ import { BadgeCheck } from 'lucide-react'; export default function InterviewCompleted() { return (
- +

Thank you for participating!

From e909c257629cded6ed7109fcedc3fb2421fc7461 Mon Sep 17 00:00:00 2001 From: Caden Buckhalt Date: Thu, 21 Dec 2023 13:12:23 -0800 Subject: [PATCH 8/9] fix: stale finishTime move check for finishTime to interviewshell bc it is client component and bc its already fetching interview --- .../interview/[interviewId]/page.tsx | 18 +----------------- .../interview/_components/InterviewShell.tsx | 5 +++++ 2 files changed, 6 insertions(+), 17 deletions(-) diff --git a/app/(interview)/interview/[interviewId]/page.tsx b/app/(interview)/interview/[interviewId]/page.tsx index 5c5fb6d2..4992c965 100644 --- a/app/(interview)/interview/[interviewId]/page.tsx +++ b/app/(interview)/interview/[interviewId]/page.tsx @@ -1,29 +1,13 @@ import NoSSRWrapper from '~/utils/NoSSRWrapper'; import InterviewShell from '../_components/InterviewShell'; -import { api } from '~/trpc/server'; -import { redirect } from 'next/navigation'; -export default async function Page({ - params, -}: { - params: { interviewId: string }; -}) { +export default function Page({ params }: { params: { interviewId: string } }) { const { interviewId } = params; if (!interviewId) { return 'No interview id found'; } - // check if interview is finished - const interview = await api.interview.get.byId.query({ id: interviewId }); - if (!interview) { - return 'Interview not found'; - } - - if (interview.finishTime) { - redirect('/interview/finished'); - } - return (
diff --git a/app/(interview)/interview/_components/InterviewShell.tsx b/app/(interview)/interview/_components/InterviewShell.tsx index dc8a197f..40364bb1 100644 --- a/app/(interview)/interview/_components/InterviewShell.tsx +++ b/app/(interview)/interview/_components/InterviewShell.tsx @@ -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. @@ -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 }, @@ -79,6 +81,9 @@ const InterviewShell = ({ interviewID }: { interviewID: string }) => { if (!data) { return; } + if (data.finishTime) { + router.push('/interview/finished'); + } const { protocol, ...serverSession } = data; From 1311a5e8c72503220911f0475dd7c5fc7cf528ab Mon Sep 17 00:00:00 2001 From: Caden Buckhalt Date: Thu, 21 Dec 2023 13:14:18 -0800 Subject: [PATCH 9/9] remove: unneeded invalidate/refetch --- .../interview/_components/FinishInterviewModal.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/(interview)/interview/_components/FinishInterviewModal.tsx b/app/(interview)/interview/_components/FinishInterviewModal.tsx index 69463ddf..0d89a95e 100644 --- a/app/(interview)/interview/_components/FinishInterviewModal.tsx +++ b/app/(interview)/interview/_components/FinishInterviewModal.tsx @@ -22,7 +22,6 @@ type FinishInterviewModalProps = { const FinishInterviewModal = ({ open, setOpen }: FinishInterviewModalProps) => { const router = useRouter(); const pathname = usePathname(); - const utils = api.useUtils(); const interviewId = pathname.split('/').pop(); const { mutateAsync: finishInterview } = api.interview.finish.useMutation({ @@ -31,8 +30,7 @@ const FinishInterviewModal = ({ open, setOpen }: FinishInterviewModalProps) => { }, async onSuccess() { await clientRevalidateTag('interview.get.byId'); - await utils.interview.get.invalidate(); - await utils.interview.get.byId.refetch(); + router.push('/interview/finished'); }, });