diff --git a/dev-data/index.ts b/dev-data/index.ts index e9cfd3b86..b12e9fd0c 100644 --- a/dev-data/index.ts +++ b/dev-data/index.ts @@ -34,7 +34,7 @@ export { benefitMentorshipHome, benefitMentorshipMentors } from './benefit-mento export { communicationText } from './widget-communication.data'; export { communityGroups } from './community-media.data'; export { communityMenuStaticLinks, schoolMenuStaticLinks } from './school-menu-links'; -export { contentMap } from './training-program.data'; +export { contentMap, trainingProgramLink } from './training-program.data'; export { contentMapAbout, introLocalizedContent } from './about-course.data'; export { contributeOptions } from './contribute-options.data'; export { courseDataMap } from './required.data'; diff --git a/dev-data/training-program.data.tsx b/dev-data/training-program.data.tsx index abdc6f631..e7eabe671 100644 --- a/dev-data/training-program.data.tsx +++ b/dev-data/training-program.data.tsx @@ -299,3 +299,8 @@ export const contentMap: ContentMap = { image: awsDevImg, }, }; + +export const trainingProgramLink = { + en: { linkLabel: 'Register' }, + ru: { linkLabel: 'Зарегистрироваться' }, +}; diff --git a/src/widgets/training-program/ui/training-program.module.scss b/src/widgets/training-program/ui/training-program.module.scss new file mode 100644 index 000000000..d1e913f21 --- /dev/null +++ b/src/widgets/training-program/ui/training-program.module.scss @@ -0,0 +1,68 @@ +.training-program { + flex-direction: row; + gap: 100px; + + .left { + display: flex; + flex: 1 1 55%; + flex-direction: column; + gap: 26px; + + & span { + font-weight: $font-weight-bold; + } + + div { + p { + padding-bottom: 0; + font-weight: $font-weight-bold; + } + } + + & > .button { + margin-top: 24px; + } + + @include media-laptop-medium { + max-width: 100%; + } + } + + .image { + flex: 1 1 30%; + max-width: 30%; + height: auto; + + &.badge { + max-width: 276px; + height: auto; + + @include media-tablet-large { + max-width: 220px; + } + + @include media-tablet { + max-width: 175px; + } + + @include media-mobile { + max-width: 135px; + } + } + + @include media-tablet-large { + align-self: center; + + width: 320px; + max-width: 60%; + height: auto; + max-height: 511px; + } + } + + @include media-tablet-large { + flex-direction: column; + gap: 40px; + align-items: flex-start; + } +} diff --git a/src/widgets/training-program/ui/training-program.scss b/src/widgets/training-program/ui/training-program.scss deleted file mode 100644 index 32ab64c70..000000000 --- a/src/widgets/training-program/ui/training-program.scss +++ /dev/null @@ -1,94 +0,0 @@ -.training-program { - &.content { - padding: 80px 120px; - - &.column-2 { - display: flex; - flex-direction: row; - gap: 100px; - align-items: center; - justify-content: flex-start; - - .left { - display: flex; - flex: 1 1 55%; - flex-direction: column; - gap: 26px; - - max-width: 55%; - - & span { - font-weight: $font-weight-bold; - } - - div { - padding-bottom: 18px; - - p { - padding-bottom: 0; - font-weight: $font-weight-bold; - } - } - - & > .button { - margin-top: 24px; - } - - @include media-laptop-medium { - max-width: 100%; - } - } - - .right { - flex: 1 1 30%; - max-width: 30%; - - &.badge { - max-width: 276px; - height: auto; - - @include media-tablet-large { - max-width: 220px; - } - - @include media-tablet { - max-width: 175px; - } - - @include media-mobile { - max-width: 135px; - } - } - - img { - width: 100%; - height: auto; - object-fit: contain; - } - - @include media-tablet-large { - align-self: center; - - width: 320px; - max-width: 60%; - height: auto; - max-height: 511px; - } - } - - @include media-tablet-large { - flex-direction: column; - gap: 40px; - align-items: flex-start; - } - } - - @include media-laptop { - padding: 75px 40px 40px; - } - - @include media-tablet { - padding: 16px; - } - } -} diff --git a/src/widgets/training-program/ui/training-program.test.tsx b/src/widgets/training-program/ui/training-program.test.tsx index 41b5816eb..f6ad8560d 100644 --- a/src/widgets/training-program/ui/training-program.test.tsx +++ b/src/widgets/training-program/ui/training-program.test.tsx @@ -1,52 +1,27 @@ import { screen } from '@testing-library/react'; import { beforeEach } from 'vitest'; -import { ROUTES } from '@/core/const'; import { Course } from '@/entities/course'; -import { MOCKED_IMAGE_PATH } from '@/shared/__tests__/constants'; +import { mockedCourses } from '@/shared/__tests__/constants'; import { renderWithRouter } from '@/shared/__tests__/utils'; -import { COURSE_ALIASES, TO_BE_DETERMINED } from '@/shared/constants'; +import angularImg from '@/shared/assets/rs-slope-angular.webp'; +import awsDevImg from '@/shared/assets/rs-slope-aws-dev.webp'; import { TrainingProgram } from '@/widgets/training-program'; -import { COURSE_TITLES } from 'data'; - -const mockedCourseAngular: Course = { - id: '1', - title: COURSE_TITLES.ANGULAR, - subTitle: null, - alias: COURSE_ALIASES.ANGULAR, - startDate: '16 Oct, 2023', - registrationEndDate: '16 Oct, 2024', - iconSmall: MOCKED_IMAGE_PATH, - iconSrc: MOCKED_IMAGE_PATH, - secondaryIcon: MOCKED_IMAGE_PATH, - language: ['en'], - mode: 'online', - detailsUrl: `/${ROUTES.COURSES}/${ROUTES.ANGULAR}`, - enroll: 'https://wearecommunity.io/events/rs-angular-2023q4', - backgroundStyle: { - backgroundColor: '#F4F1FA', - accentColor: '#F4AFA7', - }, -}; - -const mockedCourseAws: Course = { - id: '8', - language: ['en'], - iconSmall: MOCKED_IMAGE_PATH, - iconSrc: MOCKED_IMAGE_PATH, - secondaryIcon: MOCKED_IMAGE_PATH, - mode: 'online', - startDate: '', - registrationEndDate: TO_BE_DETERMINED, - title: COURSE_TITLES.AWS_CLOUD_DEVELOPER, - subTitle: null, - alias: COURSE_ALIASES.AWS_CLOUD_DEVELOPER, - detailsUrl: `/${ROUTES.COURSES}/${ROUTES.AWS_DEVELOPER}`, - enroll: 'https://wearecommunity.io/events/aws-cloud-dev-rs2023q4', - backgroundStyle: { - backgroundColor: '#F4F1FA', - accentColor: '#F4AFA7', - }, -}; + +const mockedCourseAngular: Course = mockedCourses[4]; +const mockedCourseAws: Course = mockedCourses[5]; +const mockedParagraphsAngular = [ + 'This course is designed for individuals with a solid foundation in JavaScript, TypeScript, and front-end development. Familiarity with RS School processes and RS Stage #2 certification is a plus.', + 'The course lasts 11 weeks, requiring approximately 20-40 hours of study per week.', + 'All webinars are recorded and available on our', + '. Theoretical materials are provided as recorded lectures from previous courses.', +] as const; +const mockedParagraphsAws = [ + 'This course is a step-by-step journey to become an AWS Certified Developer ‒ Associate', + 'Be well-prepared to pass the "AWS Certified Developer - Associate"', + 'Course highlights', + 'using AWS S3 and CloudFront', + 'Implement backend-for-frontend using API Gateway', +] as const; describe('TrainingProgram', () => { describe('with "angular" props', () => { @@ -55,34 +30,29 @@ describe('TrainingProgram', () => { }); it(`renders correct title "Training Program"`, () => { - const title = screen.getByText(/Training program/i); + const title = screen.getByTestId('widget-title'); expect(title).toBeVisible(); }); - it('renders correct paragraphs', () => { - const paragraphs = [ - 'This course is designed for individuals with a solid foundation in JavaScript, TypeScript, and front-end development. Familiarity with RS School processes and RS Stage #2 certification is a plus.', - 'The course lasts 11 weeks, requiring approximately 20-40 hours of study per week.', - 'All webinars are recorded and available on our', - '. Theoretical materials are provided as recorded lectures from previous courses.', - ]; - - paragraphs.forEach((p) => { + it.each(mockedParagraphsAngular)( + 'should render Angular course "%s" paragraph correctly', + (p) => { expect(screen.getByText(new RegExp(p, 'i'))).toBeInTheDocument(); - }); - }); + }, + ); it('renders Button with correct url', () => { const button = screen.getByRole('link', { name: /register/i }); - expect(button).toHaveAttribute('href', 'https://wearecommunity.io/events/rs-angular-2023q4'); + expect(button).toHaveAttribute('href', '/enroll'); }); it('renders correct image with alt text', () => { - const image = screen.getByRole('img', { name: COURSE_TITLES.ANGULAR }); + const image = screen.getByTestId('image'); expect(image).toHaveAttribute('alt', expect.stringContaining('Angular')); + expect(image).toHaveAttribute('src', angularImg.src); }); }); @@ -94,38 +64,26 @@ describe('TrainingProgram', () => { }); it('renders correct title', () => { - const title = screen.getByText(/Training program/i); + const title = screen.getByTestId('widget-title'); expect(title).toBeInTheDocument(); }); - it('renders correct paragraphs', () => { - const paragraphs = [ - 'This course is a step-by-step journey to become an AWS Certified Developer ‒ Associate', - 'Be well-prepared to pass the "AWS Certified Developer - Associate"', - 'Course highlights', - 'using AWS S3 and CloudFront', - 'Implement backend-for-frontend using API Gateway', - ]; - - paragraphs.forEach((p) => { - expect(screen.getByText(new RegExp(p, 'i'))).toBeInTheDocument(); - }); + it.each(mockedParagraphsAws)('should render AWS course "%s" paragraph correctly', (p) => { + expect(screen.getByText(new RegExp(p, 'i'))).toBeInTheDocument(); }); it('renders Button with correct url', () => { const button = screen.getByRole('link', { name: /register/i }); - expect(button).toHaveAttribute( - 'href', - 'https://wearecommunity.io/events/aws-cloud-dev-rs2023q4', - ); + expect(button).toHaveAttribute('href', '/enroll'); }); it('renders correct image with alt text', () => { - const image = screen.getByRole('img', { name: 'AWS Cloud Developer' }); + const image = screen.getByTestId('image'); - expect(image).toHaveAttribute('alt', expect.stringContaining('AWS Cloud Developer')); + expect(image).toHaveAttribute('alt', expect.stringContaining('AWS Fundamentals')); + expect(image).toHaveAttribute('src', awsDevImg.src); }); }); }); diff --git a/src/widgets/training-program/ui/training-program.tsx b/src/widgets/training-program/ui/training-program.tsx index c815a6e6d..e56416a33 100644 --- a/src/widgets/training-program/ui/training-program.tsx +++ b/src/widgets/training-program/ui/training-program.tsx @@ -1,45 +1,47 @@ import { cloneElement } from 'react'; +import classNames from 'classnames/bind'; import Image from 'next/image'; import type { Course } from '@/entities/course'; +import { Language } from '@/shared/types'; import { LinkCustom } from '@/shared/ui/link-custom'; import { WidgetTitle } from '@/shared/ui/widget-title'; -import { TrainingProgramType, contentMap } from 'data'; +import { TrainingProgramType, contentMap, trainingProgramLink } from 'data'; -import './training-program.scss'; +import styles from './training-program.module.scss'; + +const cx = classNames.bind(styles); type TrainingProgramProps = { courseName: TrainingProgramType; - lang?: 'ru' | 'en'; + lang?: Language; course: Course; }; -const localizedContent = { - en: { linkLabel: 'Register' }, - ru: { linkLabel: 'Зарегистрироваться' }, -}; - export const TrainingProgram = ({ courseName, lang = 'en', course }: TrainingProgramProps) => { const { title, content, image } = contentMap[courseName]; - - // TODO remove 'cloneElement' on 37 line due 'Using cloneElement is uncommon and can lead to fragile code' https://react.dev/reference/react/cloneElement + const isCourseWithBadge = courseName.includes('badge'); return ( -
-
-
+
+
+
{title} {content.map((component, index) => cloneElement(component, { key: index }))} {course && ( - {localizedContent[lang].linkLabel} + {trainingProgramLink[lang].linkLabel} )} -
-
- {course?.title} -
+ + + {course?.title}
);