Skip to content

Commit

Permalink
fix: students interviews page (#2575)
Browse files Browse the repository at this point in the history
* refactor: redesign student interviews page

* fix: add no intervews notification

* refactor: remove redundant styles

* fix: technical screening registration issue

* refactor: remove redundant results logic

* fix: not started condition

* fix: replace svgs with cdn links

* fix: card details choice

* fix: add error handling from review suggestion

* fix: apply review suggestions

* refactor: remove redundant function

* refactor: separate student interview components

* fix: data typing

* fix: remove unused variable

---------
  • Loading branch information
stardustmeg authored Jan 20, 2025
1 parent d7a0c8b commit ef7348c
Show file tree
Hide file tree
Showing 12 changed files with 292 additions and 102 deletions.
29 changes: 28 additions & 1 deletion client/src/domain/interview.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { Tag } from 'antd';
import { Tag, Typography } from 'antd';
import { TaskDtoTypeEnum } from 'api';
import { StageInterviewFeedbackVerdict, InterviewDetails as CommonInterviewDetails } from 'common/models';
import { Decision } from 'data/interviews/technical-screening';
import dayjs from 'dayjs';
import between from 'dayjs/plugin/isBetween';
import { featureToggles } from 'services/features';
import CalendarOutlined from '@ant-design/icons/CalendarOutlined';
import { formatDate, formatShortDate } from 'services/formatter';

dayjs.extend(between);

export function friendlyStageInterviewVerdict(value: StageInterviewFeedbackVerdict) {
Expand Down Expand Up @@ -134,3 +137,27 @@ export function DecisionTag({ decision, status }: { decision?: Decision; status?
}
}
}

export function InterviewPeriod({
startDate,
endDate,
shortDate,
}: {
startDate: string;
endDate: string;
shortDate?: boolean;
}) {
const format = shortDate ? formatShortDate : formatDate;
return (
<div className="interview-period">
<Typography.Text type="secondary">
<CalendarOutlined style={{ marginRight: 8 }} />
{`${format(startDate)} - ${format(endDate)}`}
</Typography.Text>
</div>
);
}

export const isRegistrationNotStarted = (studentRegistrationStartDate: string): boolean => {
return !!studentRegistrationStartDate && new Date() < new Date(studentRegistrationStartDate);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import css from 'styled-jsx/css';

export const AlertDescription = ({ backgroundImage }: { backgroundImage?: string }) => {
return (
<>
<div className={iconGroup.className} style={{ backgroundImage }} />
{iconGroup.styles}
</>
);
};

const iconGroup = css.resolve`
div {
background-image: url(https://cdn.rs.school/sloths/cleaned/lazy.svg);
background-position: center;
background-size: contain;
background-repeat: no-repeat;
max-width: 270px;
height: 160px;
margin: 10px auto;
}
`;
31 changes: 31 additions & 0 deletions client/src/modules/Interview/Student/components/ExtraInfo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { CheckCircleOutlined } from '@ant-design/icons';
import { Button, Tag } from 'antd';
import { formatShortDate } from 'services/formatter';
import { isRegistrationNotStarted } from 'domain/interview';

export const ExtraInfo = ({
id,
registrationStart,
isRegistered,
onRegister,
}: {
id: number;
registrationStart: string;
isRegistered: boolean;
onRegister: (id: string) => void;
}) => {
const registrationNotStarted = isRegistrationNotStarted(registrationStart);

return registrationNotStarted ? (
<Tag color="orange">Registration starts on {formatShortDate(registrationStart)}</Tag>
) : (
<Button
onClick={() => onRegister(id.toString())}
icon={isRegistered ? <CheckCircleOutlined /> : null}
disabled={isRegistered}
type={isRegistered ? 'default' : 'primary'}
>
{isRegistered ? 'Registered' : 'Register'}
</Button>
);
};
71 changes: 71 additions & 0 deletions client/src/modules/Interview/Student/components/InterviewCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Col, Card, Button, Alert } from 'antd';
import { InfoCircleTwoTone } from '@ant-design/icons';
import { InterviewDto } from 'api';
import { InterviewDetails, InterviewPeriod, InterviewStatus, isRegistrationNotStarted } from 'domain/interview';
import { InterviewDescription } from './InterviewDescription';
import { getInterviewCardDetails } from '../data/getInterviewCardDetails';
import { AlertDescription } from './AlertDescription';
import { ExtraInfo } from './ExtraInfo';

const { Meta } = Card;

export const InterviewCard = ({
interview,
item,
isRegistered,
onRegister,
}: {
interview: InterviewDto;
item: InterviewDetails | null;
isRegistered: boolean;
onRegister: (id: string) => void;
}) => {
const { id, descriptionUrl, name, startDate, endDate, studentRegistrationStartDate: registrationStart } = interview;
const interviewPassed = item?.status === InterviewStatus.Completed;
const registrationNotStarted = isRegistrationNotStarted(registrationStart);
const { cardMessage, backgroundImage } = getInterviewCardDetails({
interviewPassed,
isRegistered,
registrationNotStarted,
registrationStart,
});

return (
<Col key={id} xs={24} lg={12}>
<Card
bodyStyle={{ paddingTop: 0 }}
hoverable
title={
<Button type="link" href={descriptionUrl} target="_blank" style={{ padding: 0, fontWeight: 500 }}>
{name}
</Button>
}
extra={<InterviewPeriod startDate={startDate} endDate={endDate} shortDate />}
>
<Meta
style={{ minHeight: 80, alignItems: 'center', textAlign: 'center' }}
description={
item ? (
<InterviewDescription {...item} />
) : (
<ExtraInfo
id={id}
registrationStart={registrationStart}
isRegistered={isRegistered}
onRegister={onRegister}
/>
)
}
/>
<Alert
message={<div style={{ minHeight: 50 }}>{cardMessage}</div>}
icon={<InfoCircleTwoTone />}
showIcon
type="info"
description={<AlertDescription backgroundImage={backgroundImage} />}
style={{ minHeight: 275 }}
/>
</Card>
</Col>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Descriptions } from 'antd';
import { InterviewDetails, getInterviewResult } from 'domain/interview';
import { GithubUserLink } from 'components/GithubUserLink';
import { Decision } from 'data/interviews/technical-screening';
import { StatusLabel } from './StatusLabel';

export const InterviewDescription = ({ interviewer, status, result }: InterviewDetails) => {
return (
<div style={{ padding: '8px 0' }}>
<Descriptions layout="vertical" size="small">
<Descriptions.Item label="Interviewer">
<GithubUserLink value={interviewer.githubId} />
</Descriptions.Item>
<Descriptions.Item label="Status">
<StatusLabel status={status} />
</Descriptions.Item>
<Descriptions.Item label="Result">
<b>{getInterviewResult(result as Decision) ?? '-'}</b>
</Descriptions.Item>
</Descriptions>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { InfoCircleTwoTone } from '@ant-design/icons';
import { Row, Col, Alert } from 'antd';
import { AlertDescription } from './AlertDescription';

export const NoInterviewsAlert = () => (
<Row justify="center">
<Col xs={24} lg={12}>
<Alert
type="info"
showIcon
icon={<InfoCircleTwoTone />}
message="There are no planned interviews."
description={<AlertDescription />}
/>
</Col>
</Row>
);
13 changes: 13 additions & 0 deletions client/src/modules/Interview/Student/components/StatusLabel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Tag } from 'antd';
import { InterviewStatus } from 'domain/interview';

export const StatusLabel = ({ status }: { status: InterviewStatus }) => {
const statusMap = {
[InterviewStatus.Completed]: { color: 'green', label: 'Completed' },
[InterviewStatus.Canceled]: { color: 'red', label: 'Canceled' },
[InterviewStatus.NotCompleted]: { color: 'orange', label: 'Not Completed' },
};
const { color, label } = statusMap[status] || statusMap[InterviewStatus.NotCompleted];

return <Tag color={color}>{label}</Tag>;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { formatShortDate } from 'services/formatter';

interface StudentInterviewDetails {
registrationNotStarted: boolean;
isRegistered: boolean;
interviewPassed: boolean;
registrationStart: string;
}

export const getInterviewCardDetails = ({
interviewPassed,
isRegistered,
registrationNotStarted,
registrationStart,
}: StudentInterviewDetails) => {
if (interviewPassed) {
return {
cardMessage: 'You have your interview result. Congratulations!',
backgroundImage: 'url(https://cdn.rs.school/sloths/cleaned/congratulations.svg)',
};
}

if (isRegistered) {
return {
cardMessage: 'You’re all set! Prepare for your upcoming interview.',
backgroundImage: 'url(https://cdn.rs.school/sloths/cleaned/its-a-good-job.svg)',
};
}

if (registrationNotStarted) {
return {
cardMessage: (
<div>
Remember to come back and register after{' '}
<span style={{ whiteSpace: 'nowrap' }}>{formatShortDate(registrationStart ?? '')}</span>!
</div>
),
backgroundImage: 'url(https://cdn.rs.school/sloths/cleaned/listening.svg)',
};
}

return {
cardMessage: 'Register and get ready for your exciting interview!',
backgroundImage: 'url(https://cdn.rs.school/sloths/cleaned/take-notes.svg)',
};
};
2 changes: 2 additions & 0 deletions client/src/modules/Interview/Student/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { NoInterviewsAlert } from './components/NoInterviewsAlert';
export { InterviewCard } from './components/InterviewCard';
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Button, Card } from 'antd';
import css from 'styled-jsx/css';
import { InterviewPeriod } from './InterviewPeriod';
import { InterviewPeriod } from 'domain/interview';
import { InterviewDetails } from './InterviewDetails';
import { InterviewDto } from 'api';
import { Course } from 'services/models';
Expand Down

This file was deleted.

Loading

0 comments on commit ef7348c

Please sign in to comment.