-
-
Notifications
You must be signed in to change notification settings - Fork 208
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(interview): add new feedback page
- Loading branch information
1 parent
5f32cfe
commit 95910c0
Showing
22 changed files
with
1,416 additions
and
25 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
133 changes: 118 additions & 15 deletions
133
client/src/modules/Interviews/pages/InterviewFeedback/getServerSideProps.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,42 +1,145 @@ | ||
import { CoursesInterviewsApi } from 'api'; | ||
import { | ||
CoursesTasksApi, | ||
CourseStatsApi, | ||
StudentDto, | ||
StudentsApi, | ||
CoursesInterviewsApi, | ||
InterviewFeedbackDto, | ||
} from 'api'; | ||
import { templates } from 'data/interviews'; | ||
import { notAuthorizedResponse, noAccessResponse } from 'modules/Course/data'; | ||
import { GetServerSideProps } from 'next'; | ||
import { getTasksTotalScore } from 'domain/course'; | ||
import { stageInterviewType } from 'domain/interview'; | ||
import { notAuthorizedResponse } from 'modules/Course/data'; | ||
import { GetServerSideProps, GetServerSidePropsContext } from 'next'; | ||
import { ParsedUrlQuery } from 'querystring'; | ||
import type { CourseOnlyPageProps } from 'services/models'; | ||
import { UserService } from 'services/user'; | ||
import { getApiConfiguration } from 'utils/axios'; | ||
import { getTokenFromContext } from 'utils/server'; | ||
|
||
export type PageProps = CourseOnlyPageProps & { | ||
export type StageFeedbackProps = CourseOnlyPageProps & { | ||
interviewId: number; | ||
student: StudentDto; | ||
courseSummary: { | ||
totalScore: number; | ||
studentsCount: number; | ||
}; | ||
interviewFeedback: InterviewFeedbackDto; | ||
type: typeof stageInterviewType; | ||
}; | ||
|
||
export type FeedbackProps = CourseOnlyPageProps & { | ||
interviewTaskId: number; | ||
type: keyof typeof templates; | ||
githubId: string; | ||
}; | ||
|
||
export type PageProps = FeedbackProps | StageFeedbackProps; | ||
|
||
export const getServerSideProps: GetServerSideProps<PageProps> = async ctx => { | ||
try { | ||
const alias = ctx.query.course as string; | ||
const type = ctx.params?.type as string; | ||
const githubId = ctx.query.githubId as string; | ||
const type = ctx.params?.type as PageProps['type']; | ||
|
||
const token = getTokenFromContext(ctx); | ||
const courses = await new UserService(token).getCourses(); | ||
const course = courses.find(course => course.alias === alias) ?? null; | ||
|
||
if (course == null) { | ||
if (!course || !type) { | ||
return notAuthorizedResponse; | ||
} | ||
|
||
const response = await new CoursesInterviewsApi(getApiConfiguration(token)).getInterviews(course.id, false); | ||
const interview = | ||
response.data.find(interview => (interview.attributes as { template?: string })?.template === type) ?? null; | ||
const pageProps = await (type === stageInterviewType | ||
? getStageInterviewData({ ctx, courseId: course.id, token }) | ||
: getInterviewData({ ctx, courseId: course.id, token })); | ||
|
||
const props: PageProps = { | ||
...pageProps, | ||
course, | ||
}; | ||
|
||
if (interview == null) { | ||
return notAuthorizedResponse; | ||
} | ||
return { | ||
props: { course, interviewTaskId: interview.id, type, githubId }, | ||
props: props, | ||
}; | ||
} catch (e) { | ||
return noAccessResponse; | ||
return notAuthorizedResponse; | ||
} | ||
}; | ||
|
||
/** | ||
* Gets stage interview data | ||
*/ | ||
async function getStageInterviewData({ | ||
ctx, | ||
token, | ||
courseId, | ||
}: { | ||
ctx: GetServerSidePropsContext<ParsedUrlQuery>; | ||
token: string | undefined; | ||
courseId: number; | ||
}): Promise<Omit<StageFeedbackProps, keyof CourseOnlyPageProps>> { | ||
const studentId = ctx.query.studentId as string | undefined; | ||
|
||
if (!studentId || !ctx.query.interviewId) { | ||
throw new Error('No studentId or interviewId'); | ||
} | ||
const interviewId = Number(ctx.query.interviewId); | ||
const axiosConfig = getApiConfiguration(token); | ||
|
||
const [ | ||
{ data: student }, | ||
{ data: tasks }, | ||
{ | ||
data: { studentsActiveCount }, | ||
}, | ||
{ data: interviewFeedback }, | ||
] = await Promise.all([ | ||
new StudentsApi(axiosConfig).getStudent(Number(studentId)), | ||
new CoursesTasksApi(axiosConfig).getCourseTasks(courseId), | ||
new CourseStatsApi(axiosConfig).getCourseStats(courseId), | ||
new CoursesInterviewsApi(axiosConfig).getInterviewFeedback(courseId, interviewId, stageInterviewType), | ||
]); | ||
if (!student) { | ||
throw new Error('Student not found'); | ||
} | ||
|
||
return { | ||
interviewId, | ||
student, | ||
courseSummary: { | ||
totalScore: getTasksTotalScore(tasks), | ||
studentsCount: studentsActiveCount, | ||
}, | ||
interviewFeedback, | ||
type: stageInterviewType, | ||
}; | ||
} | ||
|
||
/** | ||
* Gets regular interview data | ||
*/ | ||
async function getInterviewData({ | ||
ctx, | ||
token, | ||
courseId, | ||
}: { | ||
ctx: GetServerSidePropsContext<ParsedUrlQuery>; | ||
token: string | undefined; | ||
courseId: number; | ||
}): Promise<Omit<FeedbackProps, keyof CourseOnlyPageProps>> { | ||
const githubId = ctx.query.githubId as string; | ||
const type = ctx.params?.type as FeedbackProps['type']; | ||
const response = await new CoursesInterviewsApi(getApiConfiguration(token)).getInterviews(courseId, false); | ||
const interview = | ||
response.data.find(interview => (interview.attributes as { template?: string })?.template === type) ?? null; | ||
|
||
if (interview == null) { | ||
throw new Error('Interview not found'); | ||
} | ||
|
||
return { | ||
interviewTaskId: interview.id, | ||
type, | ||
githubId, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
44 changes: 44 additions & 0 deletions
44
client/src/modules/Interviews/pages/feedback/CustomQuestion.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import { useRef } from 'react'; | ||
import { Button, Card, Col, Input, InputRef, Row, Space } from 'antd'; | ||
|
||
type Props = { | ||
cancel: () => void; | ||
save(question: string): void; | ||
}; | ||
|
||
export function CustomQuestion({ cancel, save }: Props) { | ||
const addRef = useRef<InputRef>(null); | ||
|
||
function saveQuestion() { | ||
const value = addRef.current?.input?.value.trim(); | ||
if (!value) { | ||
return; | ||
} | ||
|
||
save(value); | ||
} | ||
|
||
return ( | ||
<Card bodyStyle={{ padding: '12px 24px' }} style={{ marginBottom: 16 }}> | ||
<Input | ||
placeholder="Enter your question" | ||
ref={addRef} | ||
autoFocus | ||
onPressEnter={e => { | ||
// prevent form submit | ||
e.preventDefault(); | ||
saveQuestion(); | ||
}} | ||
/> | ||
<Row style={{ marginTop: 15 }}> | ||
<Col flex={1} /> | ||
<Space> | ||
<Button onClick={cancel}>Cancel</Button> | ||
<Button type="primary" onClick={saveQuestion}> | ||
Save | ||
</Button> | ||
</Space> | ||
</Row> | ||
</Card> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import { Divider, Layout } from 'antd'; | ||
import dynamic from 'next/dynamic'; | ||
import { Header } from 'components/Header'; | ||
import { SessionContext } from 'modules/Course/contexts'; | ||
import { useContext } from 'react'; | ||
import { StageFeedbackProps } from '../InterviewFeedback/getServerSideProps'; | ||
|
||
import { Steps } from './Steps'; | ||
import { StudentInfo } from './StudentInfo'; | ||
import { SubHeader } from './SubHeader'; | ||
import { StepContextProvider } from './StepContext'; | ||
import { StepsContent } from './StepsContent'; | ||
import { featureToggles } from 'services/features'; | ||
|
||
const LegacyTechScreening = dynamic(() => import('pages/course/mentor/interview-technical-screening'), { | ||
loading: () => <p>Loading...</p>, | ||
}); | ||
|
||
export function Feedback(props: StageFeedbackProps) { | ||
const session = useContext(SessionContext); | ||
const { student, courseSummary, interviewFeedback, course, interviewId, type } = props; | ||
|
||
const shouldFallbackToLegacy = !featureToggles.feedback || interviewFeedback.version === 0; | ||
|
||
// if the feedback exists and doesn't have a version, it means it was created before the feedback feature was released | ||
// fallback to previous form. Once we migrate old data to new format(Artsiom A.), we may remove this fallback | ||
if (shouldFallbackToLegacy) { | ||
return <LegacyTechScreening {...props} session={session} />; | ||
} | ||
|
||
return ( | ||
<Layout style={{ background: 'transparent', minHeight: '100vh' }}> | ||
<Header title="Technical screening" username={session.githubId} /> | ||
<SubHeader isCompleted={interviewFeedback.isCompleted ?? false} /> | ||
<StepContextProvider | ||
interviewFeedback={interviewFeedback} | ||
course={course} | ||
interviewId={interviewId} | ||
type={type} | ||
interviewMaxScore={interviewFeedback.maxScore} | ||
> | ||
<Layout style={{ background: 'transparent' }}> | ||
<Layout.Content> | ||
<StepsContent /> | ||
</Layout.Content> | ||
<Layout.Sider | ||
reverseArrow | ||
theme="light" | ||
width={400} | ||
style={{ borderLeft: '1px solid rgba(240, 242, 245)' }} | ||
breakpoint="md" | ||
collapsedWidth={0} | ||
> | ||
<StudentInfo student={student} courseSummary={courseSummary} /> | ||
<Divider style={{ margin: 0 }} /> | ||
<Steps /> | ||
</Layout.Sider> | ||
</Layout> | ||
</StepContextProvider> | ||
</Layout> | ||
); | ||
} |
Oops, something went wrong.