-
-
Notifications
You must be signed in to change notification settings - Fork 206
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 (#2273)
* feat(interview): add new feedback page
- Loading branch information
1 parent
5f32cfe
commit f482e4e
Showing
22 changed files
with
1,430 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
140 changes: 125 additions & 15 deletions
140
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,152 @@ | ||
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>> { | ||
validateQueryParams(ctx, ['studentId', 'interviewId']); | ||
|
||
const studentId = Number(ctx.query.studentId); | ||
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, | ||
}; | ||
} | ||
|
||
function validateQueryParams(ctx: GetServerSidePropsContext<ParsedUrlQuery>, params: string[]) { | ||
for (const param of params) { | ||
if (!ctx.query[param]) { | ||
throw new Error(`Parameter ${param} is not defined`); | ||
} | ||
} | ||
} |
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> | ||
); | ||
} |
Oops, something went wrong.