diff --git a/dev-data/about-course.data.tsx b/dev-data/about-course.data.tsx
deleted file mode 100644
index 653129bdd..000000000
--- a/dev-data/about-course.data.tsx
+++ /dev/null
@@ -1,295 +0,0 @@
-import awardIcon from '@/shared/assets/icons/award-icon.webp';
-import giftIcon from '@/shared/assets/icons/gift.webp';
-import noteIcon from '@/shared/assets/icons/note-icon.webp';
-import paperIcon from '@/shared/assets/icons/paper-icon.webp';
-import personIcon from '@/shared/assets/icons/person-icon.webp';
-import planetIcon from '@/shared/assets/icons/planet.webp';
-import {
- REGISTRATION_WILL_OPEN_SOON,
- REGISTRATION_WILL_OPEN_SOON_RU,
- ROUTES,
-} from '@/shared/constants';
-import { LinkCustom } from '@/shared/ui/link-custom';
-import { List } from '@/shared/ui/list';
-import type { AboutCourseInfo } from 'data';
-import { COURSE_TITLES, CourseNamesChannels, DISCORD_LINKS } from 'data';
-
-type ContentMap = {
- [key in CourseNamesChannels]: AboutCourseInfo[];
-};
-
-const enIntro = {
- title: 'About the course',
- linkLabel: 'Become a student',
- noLinkLabel: REGISTRATION_WILL_OPEN_SOON,
- paragraph: null,
-};
-const ruIntro = {
- title: 'О курсе',
- linkLabel: 'Cтать студентом',
- noLinkLabel: REGISTRATION_WILL_OPEN_SOON_RU,
- paragraph: null,
-};
-const preSchoolIntro = {
- title: 'JS/Frontend-разработка. Подготовительный этап',
- linkLabel: 'Стать студентом',
- noLinkLabel: REGISTRATION_WILL_OPEN_SOON_RU,
- paragraph:
- 'Подготовительный этап поможет тем, кто мало знаком или совсем не знаком с программированием и хотел бы впоследствии учиться на основном курсе «JavaScript/Front-end».',
-};
-
-export const introLocalizedContent = {
- [COURSE_TITLES.JS_PRESCHOOL_RU]: preSchoolIntro,
- [COURSE_TITLES.JS_EN]: enIntro,
- [COURSE_TITLES.JS_RU]: ruIntro,
- [COURSE_TITLES.REACT]: enIntro,
- [COURSE_TITLES.ANGULAR]: enIntro,
- [COURSE_TITLES.NODE]: enIntro,
- [COURSE_TITLES.AWS_FUNDAMENTALS]: enIntro,
- [COURSE_TITLES.AWS_CLOUD_DEVELOPER]: enIntro,
- [COURSE_TITLES.AWS_DEVOPS]: enIntro,
- [COURSE_TITLES.AWS_AI]: ruIntro,
-};
-
-const listData = {
- javaScriptEN: [
- [
- {
- id: 1,
- text: 'The Mentors and trainers of our school are front-end and javascript developers from different companies/countries. ',
- title: 'How to become a mentor?',
- link: `/${ROUTES.MENTORSHIP}/${ROUTES.JS}`,
- },
- ],
- ],
- javaScriptRU: [
- [
- {
- id: 1,
- text: 'Наставники и тренеры нашей школы - это фронтенд и разработчики JavaScript из разных компаний и стран. ',
- title: 'Как стать наставником?',
- link: `/${ROUTES.MENTORSHIP}/${ROUTES.JS_RU}`,
- },
- ],
- ],
- reactEn: [
- [
- {
- id: 1,
- text: 'School ',
- title: 'documentation',
- link: 'https://docs.rs.school',
- },
- ],
- 'All materials are publicly available on the YouTube channel and GitHub',
- ],
- reactRu: [
- [
- {
- id: 1,
- text: '',
- title: 'Документация школы',
- link: 'https://docs.rs.school',
- },
- ],
- 'Все материалы находятся в открытом доступе на YouTube и GitHub.Также предлагаем ознакомиться с конспектом первого этапа обучения.',
- ],
-};
-
-const angularNodejsAwsFundamentals: (course: string) => AboutCourseInfo[] = () => [
- {
- id: 1,
- title: 'For JS/FE graduates',
- info: 'This course is exclusively available for students who have successfully completed JS/FE Stage 2. The RS School continues working by the principle of "Pay it forward". Members of our community share their knowledge and check students\' tasks for free. And we hope that our students will continue this work as our mentors in the future.',
- icon: personIcon,
- },
- {
- id: 2,
- title: 'Materials',
- info: 'All materials are publicly available on the YouTube channel and GitHub',
- icon: paperIcon,
- },
- {
- id: 3,
- title: 'Schedule',
- info: 'Twice a week in the evenings. Duration: 9 weeks. Types of training: webinars.',
- icon: noteIcon,
- },
- {
- id: 4,
- title: 'Certificate',
- info: 'After successful completion of the course, students will receive an electronic certificate.',
- icon: awardIcon,
- },
-];
-
-const awsCloudDeveloper: AboutCourseInfo[] = angularNodejsAwsFundamentals('aws cloud dev').map(
- (item) => {
- if (item.id === 3) {
- return {
- ...item,
- info: 'Duration: 10 weeks.',
- };
- }
- return item;
- },
-);
-
-const javaScriptEN: () => AboutCourseInfo[] = () => {
- return [
- {
- id: 1,
- title: 'For everyone',
- info: 'Everyone can study at RS School, regardless of age, professional employment, or place of residence. However, you should have sufficient base knowledge before the program begins.',
- icon: personIcon,
- },
- {
- id: 2,
- title: 'Worldwide mentors and trainers',
- info:
,
- icon: planetIcon,
- },
- {
- id: 3,
- title: 'Free education',
- info: 'Feel the desire to share your experience and knowledge',
- icon: giftIcon,
- },
- {
- id: 4,
- title: 'Certificate',
- info: 'After successful completion of the course, students will receive an electronic certificate.',
- icon: awardIcon,
- },
- ];
-};
-const javaScriptRU: () => AboutCourseInfo[] = () => {
- return [
- {
- id: 1,
- title: 'Для всех',
- info: 'Каждый может учиться в RS School, независимо от возраста, профессиональной занятости или места жительства. Однако вам следует иметь достаточные базовые знания перед началом программы.',
- icon: personIcon,
- },
- {
- id: 2,
- title: 'Наставники и тренеры со всего мира',
- info: 'Онлайн встречи каждую неделю. Продолжительность курса: 4 недели.',
- icon: planetIcon,
- },
- {
- id: 3,
- title: 'Бесплатное образование',
- info: 'Наши курсы абсолютно бесплатны и доступны всем желающим.',
- icon: giftIcon,
- },
- {
- id: 4,
- title: 'Сертификат',
- info: 'Электронный сертификат об успешном окончании курса выдается всем, кто пройдет два этапа обучения.',
- icon: awardIcon,
- },
- ];
-};
-
-const javaScriptPreSchoolRU: () => AboutCourseInfo[] = () => {
- return [
- {
- id: 1,
- title: 'Для всех',
- info: 'Каждый может учиться в RS School, независимо от возраста, профессиональной занятости или места жительства. Однако вам следует иметь достаточные базовые знания перед началом программы.',
- icon: personIcon,
- },
- {
- id: 2,
- title: 'Время обучения',
- info: 'Длительность обучения: 10 недель. Формат обучения: самообучение, групповое обучение, общение в Discord, задания проверяют в процессе кросс-чек и автоматически.',
- icon: noteIcon,
- },
- {
- id: 3,
- title: 'Бесплатное образование',
- info: 'В RS School работает принцип "Pay it forward". Мы бесплатно делимся с учащимися своими знаниями сейчас, надеясь, что в будущем они вернутся к нам в качестве менторов и точно так же передадут свои знания следующему поколению студентов.',
- icon: giftIcon,
- },
- ];
-};
-
-const reactEn: AboutCourseInfo[] = javaScriptEN().map((item) => {
- if (item.id === 2) {
- return {
- ...item,
- title: 'Materials',
- info:
,
- icon: paperIcon,
- };
- }
- if (item.id === 5) {
- return {
- ...item,
- info: (
-
- Throughout the course, we mostly use
- {' '}
-
- Discord chat
-
- .
-
- ),
- };
- }
- return item;
-});
-
-const awsDevops: AboutCourseInfo[] = [
- ...reactEn,
- {
- id: 5,
- title: 'Duration',
- info: '12 weeks',
- icon: noteIcon,
- },
-];
-
-const awsAi: () => AboutCourseInfo[] = () => {
- return [
- {
- id: 1,
- title: 'Для всех',
- info: 'Каждый может учиться в RS School, независимо от возраста, профессиональной занятости или места жительства. Однако вам следует иметь достаточные базовые знания перед началом программы.',
- icon: personIcon,
- },
- {
- id: 2,
- title: 'Расписание',
- info: 'Онлайн встречи каждую неделю. Продолжительность курса 4 недели.',
- icon: planetIcon,
- },
- {
- id: 3,
- title: 'Бесплатное образование',
- info: 'Почувствуйте желание поделиться своим опытом и знаниями',
- icon: giftIcon,
- },
- {
- id: 4,
- title: 'Сертификат',
- info: 'Электронный сертификат об успешном окончании курса выдается всем, кто пройдет два этапа обучения.',
- icon: awardIcon,
- },
- ];
-};
-
-export const contentMapAbout: ContentMap = {
- [COURSE_TITLES.JS_RU]: javaScriptRU(),
- [COURSE_TITLES.JS_EN]: javaScriptEN(),
- [COURSE_TITLES.JS_PRESCHOOL_RU]: javaScriptPreSchoolRU(),
- [COURSE_TITLES.REACT]: reactEn,
- [COURSE_TITLES.ANGULAR]: angularNodejsAwsFundamentals('angular'),
- [COURSE_TITLES.NODE]: angularNodejsAwsFundamentals('node.js'),
- [COURSE_TITLES.AWS_FUNDAMENTALS]: angularNodejsAwsFundamentals('aws fundamentals'),
- [COURSE_TITLES.AWS_CLOUD_DEVELOPER]: awsCloudDeveloper,
- [COURSE_TITLES.AWS_DEVOPS]: awsDevops,
- [COURSE_TITLES.AWS_AI]: awsAi(),
-};
diff --git a/dev-data/about-video.data.ts b/dev-data/about-video.data.ts
deleted file mode 100644
index bea6b1a2c..000000000
--- a/dev-data/about-video.data.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export const videoTitleLocalized = {
- en: { title: 'RS School video' },
- ru: { title: 'Видео RS School' },
-};
diff --git a/dev-data/course-titles.data.ts b/dev-data/course-titles.data.ts
index 9ad7ee84f..3ec2cf020 100644
--- a/dev-data/course-titles.data.ts
+++ b/dev-data/course-titles.data.ts
@@ -1,5 +1,3 @@
-import { Course } from '@/widgets/required/types';
-
export const COURSE_TITLES = {
JS_PRESCHOOL_RU: 'JS / Front-end Pre-school RU',
JS_EN: 'JS / Front-end EN',
@@ -13,15 +11,8 @@ export const COURSE_TITLES = {
AWS_AI: 'AWS AI',
} as const;
-export const AWS_FUNDAMENTALS_BADGE = `${COURSE_TITLES.AWS_FUNDAMENTALS} badge` as const;
-export type AwsFundamentalsBadge = typeof AWS_FUNDAMENTALS_BADGE;
export type CourseNames = typeof COURSE_TITLES;
export type CourseNamesKeys = CourseNames[keyof CourseNames];
-export type CoursesWithRequirementsNames = Exclude;
-export type CourseMap = {
- [courseName in CoursesWithRequirementsNames]: Course;
-};
-export type TrainingProgramType = CourseNamesKeys | AwsFundamentalsBadge;
export const DISCORD_LINKS = {
[COURSE_TITLES.JS_PRESCHOOL_RU]: 'https://discord.com/invite/gFnRh8Sudg',
diff --git a/dev-data/courses-data.types.ts b/dev-data/courses-data.types.ts
deleted file mode 100644
index 0711436d1..000000000
--- a/dev-data/courses-data.types.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { ReactNode } from 'react';
-import { StaticImageData } from 'next/image';
-
-import type { MentorActivity } from './mentorship-data.types';
-import type { Course } from '@/entities/course';
-
-export type DataMap = {
- mentorship: MentorActivity[];
- courses: Course[];
-};
-
-export type AboutCourseInfo = {
- id: number;
- title: string;
- info: string | ReactNode;
- icon: StaticImageData;
-};
diff --git a/dev-data/faq.data.ts b/dev-data/faq.data.ts
deleted file mode 100644
index b7c96af5b..000000000
--- a/dev-data/faq.data.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-import { FaqData } from '@/widgets/faq';
-
-export const preschoolFaqData: FaqData = [
- {
- question: 'Где можно задать вопрос?',
- answer: [
- {
- id: 0,
- text: 'Вопросы можно задать в Discord ',
- title: 'чате',
- link: 'https://discord.gg/2Ww3TCBvz4',
- },
- ],
- },
- {
- question: 'Где происходит общение?',
- answer: [
- {
- id: 1,
- text: 'В Discord ',
- title: 'чате',
- link: 'https://discord.gg/2Ww3TCBvz4',
- },
- ],
- },
- {
- question: 'Имеет ли значения город проживания? Можно ли пройти курс полностью онлайн?',
- answer: 'Город проживания значения не имеет. Все этапы обучения можно пройти онлайн.',
- },
- {
- question: 'Можно ли изучать учебные модули и делать проекты заранее? ',
- answer:
- 'Конечно! После прохождения всех модулей подготовительного этапа вы можете начать прохождение stage#1. ',
- },
- {
- question:
- 'Нужна ли регистрация на основной курс если я зарегистрирован на подготовительный этап?',
- answer: [
- {
- id: 2,
- text: 'Да, нужна. Ссылка на регистрацию ',
- title: 'тут',
- link: 'https://rs.school/courses/javascript-ru',
- },
- ],
- },
- {
- question: 'Можно ли пропускать вебинары?',
- answer:
- 'Да, можно. Записи вебинаров можно будет найти на нашем канале - YouTube. Видео удобнее смотреть на скорости 1.25 или выше.',
- },
- {
- question: 'Обязательно ли смотреть вебинары школы?',
- answer:
- 'Нет. Ссылки на рекомендуемую для изучения теорию находится в модулях. После самостоятельного изучения материалов модуля вы можете посмотреть вебинар, чтобы закрепить информацию или задать вопросы тренеру.',
- },
- {
- question: 'Кто проверяет задания?',
- answer:
- 'Практические задания проверяются в ходе кросс-чек. Алгоритмические задания, задачи из Codewars, а также задания "CV#1. Markdown & Git", "CV#2. HTML, CSS & Git Basics" проверяются автоматически. Ваши решения необходимо сабмитнуть в RS APP до дедлайна.',
- },
-];
diff --git a/dev-data/index.ts b/dev-data/index.ts
index 48f2d4cb8..e6b285f46 100644
--- a/dev-data/index.ts
+++ b/dev-data/index.ts
@@ -1,4 +1,3 @@
-export type { AboutCourseInfo, DataMap } from './courses-data.types';
export type {
CourseTitle,
ImageLink,
@@ -9,9 +8,9 @@ export type {
MentorshipDetailsType,
MentorshipRoute,
} from './mentorship-data.types';
-export type { MentorshipCourseTitles, MentorshipLinks } from './mentorship-data.types';
+export type { LinkList, StageCardProps, StudyPathProps } from './study-path-data.types';
-export type { StageCardProps, StudyPathPage, StudyPathProps } from './study-path-data.types';
+export type { MentorshipCourseTitles, MentorshipLinks } from './mentorship-data.types';
export {
ANNOUNCEMENT_TELEGRAM_LINK,
@@ -23,25 +22,19 @@ export {
RS_DOCS_EN_LINK,
RS_DOCS_TELEGRAM_CHATS_LINK,
} from './communication.data';
+
+export { type Benefit } from './benefit-mentorship.data';
export {
- AWS_FUNDAMENTALS_BADGE,
COURSE_TITLES,
type CourseNames,
type CourseNamesKeys,
- type CoursesWithRequirementsNames,
DISCORD_LINKS,
- type TrainingProgramType,
} from './course-titles.data';
-export { type Benefit } from './benefit-mentorship.data';
export { aboutMentorsData } from './about-mentors.data';
export { benefitMentorshipHome, benefitMentorshipMentors } from './benefit-mentorship.data';
-export { communicationText } from './widget-communication.data';
export { communityGroups } from './community-media.data';
export { communityMenuStaticLinks, schoolMenuStaticLinks } from './school-menu-links';
-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';
export { courseStatus, heroCourseLocalized } from './hero-course.data';
export { donateOptions } from './donate-options.data';
export { events } from './events.data';
@@ -55,10 +48,7 @@ export { mentorsWantedData } from './mentors-wanted.data';
export { mentorshipCourses, mentorshipCoursesDefault } from './mentorship.data';
export { merchData } from './merch.data';
export { picturesSocialMediaLinks } from './pictures.data';
-export { preschoolFaqData } from './faq.data';
export { principleCards } from './principle-cards.data';
-export { requirementsData } from './requirements.data';
export { rsInNumbers } from './rs-in-numbers.data';
export { sliderPhotos } from './slider-photos.data';
export { studyPath } from './study-path.data';
-export { videoTitleLocalized } from './about-video.data';
diff --git a/dev-data/required.data.ts b/dev-data/required.data.ts
deleted file mode 100644
index ba1c413a3..000000000
--- a/dev-data/required.data.ts
+++ /dev/null
@@ -1,352 +0,0 @@
-import { COURSE_TITLES, CourseMap } from './course-titles.data';
-
-export const courseDataMap: CourseMap = {
- [COURSE_TITLES.JS_EN]: {
- title: 'What you should know before starting',
- knowBefore: {
- title: 'Required before the start',
- description: [
- 'Basic knowledge of HTML, CSS, Javascript is highly recommended before starting the course.',
- 'Basic computer science theory (data structures, algorithms, maths) is recommended before starting the course.',
- 'Experience with using any IDE.',
- 'English language level: Intermediate (B1) and up.',
- 'Register through this page and join the official discord channel for the training participants.',
- ],
- },
- willLearn: [
- {
- title: 'What to do if you lack base knowledge?',
- description: [
- 'In this case, you will have to spend enough time on self-preparation. We recommend:',
- [
- {
- id: 0,
- text: 'Take a course in ',
- title: 'Computer Science.',
- link: 'https://rkhaslarov.github.io/computer-science-introduction/#introduction',
- },
- ],
- [
- {
- id: 1,
- text: 'Read a good ',
- title: 'Javascript tutorial.',
- link: 'https://javascript.info/',
- },
- ],
- [
- {
- id: 2,
- text: 'Use the ',
- title: 'Codewars platform',
- link: 'https://www.codewars.com/kata/search/javascript',
- },
- {
- id: 3,
- text: ' to solve practical tasks. You can start with ',
- title: 'simpler ones.',
- link: 'https://www.codewars.com/kata/search/javascript?q=&r%5B%5D=-8&beta=false',
- },
- ],
- 'Take free online courses:',
- [
- {
- id: 4,
- text: '',
- title: 'Learn the Command Line ',
- link: 'https://www.codecademy.com/learn/learn-the-command-line',
- },
- ],
- [
- {
- id: 5,
- text: '',
- title: 'Learn Git ',
- link: 'https://www.codecademy.com/learn/learn-git',
- },
- ],
- [
- {
- id: 6,
- text: '',
- title: 'Algorithms. ',
- link: 'https://www.coursera.org/learn/algorithms-part1',
- },
- ],
- 'Believe in your strength!',
- ],
- },
- ],
- },
- [COURSE_TITLES.JS_RU]: {
- title: 'Что нужно знать до начала',
- knowBefore: {
- title: 'Требуется до начала',
- description: [
- 'Рекомендуется иметь базовые знания HTML, CSS, JavaScript перед началом курса.',
- 'Рекомендуется иметь базовые знания компьютерных наук (структуры данных, алгоритмы, математика) перед началом курса.',
- 'Опыт использования любой среды разработки.',
- 'Уровень владения английским языком: Средний (B1) и выше.',
- 'Зарегистрируйтесь на этой странице и присоединитесь к официальному каналу Discord для участников обучения.',
- ],
- },
- willLearn: [
- {
- title: 'Что делать, если у вас нет базовых знаний?',
- description: [
- 'В этом случае вам придется потратить достаточно времени на самоподготовку. Мы рекомендуем:',
- [
- {
- id: 0,
- text: 'Пройти курс по ',
- title: 'Computer Science.',
- link: 'https://rkhaslarov.github.io/computer-science-introduction/#introduction',
- },
- ],
- [
- {
- id: 1,
- text: 'Прочитать хороший ',
- title: 'учебник по JavaScript.',
- link: 'https://learn.javascript.ru/',
- },
- ],
- [
- {
- id: 2,
- text: 'Использовать платформу ',
- title: 'Codewars',
- link: 'https://www.codewars.com/kata/search/javascript',
- },
- {
- id: 3,
- text: ' для решения практических задач. Можно начать с ',
- title: 'более простых.',
- link: 'https://www.codewars.com/kata/search/javascript?q=&r%5B%5D=-8&beta=false',
- },
- ],
- 'Пройти бесплатные онлайн-курсы:',
- [
- {
- id: 4,
- text: '',
- title: 'Изучить command line ',
- link: 'https://www.codecademy.com/learn/learn-the-command-line',
- },
- ],
- [
- {
- id: 5,
- text: '',
- title: 'Изучить Git ',
- link: 'https://www.codecademy.com/learn/learn-git',
- },
- ],
- [
- {
- id: 6,
- text: '',
- title: 'Алгоритмы. ',
- link: 'https://www.coursera.org/learn/algorithms-part1',
- },
- ],
- 'Верьте в свои силы!',
- ],
- },
- ],
- },
- [COURSE_TITLES.JS_PRESCHOOL_RU]: {
- title: 'Что следует сделать до старта курса',
- willLearn: [
- {
- title: '',
- description: [
- [
- {
- id: 0,
- text: 'Зарегистрироваться на платформе ',
- title: 'RS School',
- link: 'https://app.rs.school/registry/student?course=js-fe-preschool-2024q2',
- },
- {
- id: 1,
- text: '. После регистрации вы сможете найти себя в ',
- title: 'Score',
- link: 'https://app.rs.school/course/score?course=js-fe-preschool-2024q2',
- },
- {
- id: 2,
- text: '.',
- title: '',
- link: '',
- },
- ],
- [
- {
- id: 3,
- text: 'Прочитать документацию о ',
- title: 'школе',
- link: 'https://docs.rs.school/#/',
- },
-
- {
- id: 4,
- text: '.',
- title: '',
- link: '',
- },
- ],
- [
- {
- id: 5,
- text: 'Присоединиться в ',
- title: 'Discord',
- link: 'https://discord.gg/gFnRh8Sudg',
- },
- {
- id: 6,
- text: ' чат курса и указать в нике свой GitHub аккаунт. Инструкция ',
- title: 'тут',
- link: 'https://docs.rs.school/#/rs-school-chats',
- },
-
- {
- id: 7,
- text: '.',
- title: '',
- link: '',
- },
- ],
- ],
- },
- {
- title: 'Запомнить правила хорошего тона RS School:',
- description: [
- [
- {
- id: 8,
- text: ' Если вам помогли, не забудьте написать спасибо. Желательно использовать специальный канал ',
- title: 'RS School',
- link: 'https://app.rs.school/gratitude)',
- },
-
- {
- id: 9,
- text: '.',
- title: '',
- link: '',
- },
- ],
- [
- {
- id: 10,
- text: 'Если вам помогли с каким-то вопросом и вы видите, что у других студентов возникли подобные сложности, то желательно не проходить мимо и помочь в свою очередь.',
- title: '',
- link: '',
- },
- ],
-
- [
- {
- id: 11,
- text: 'Если у вас какие-либо проблемы с выполнением заданий или платформой школы (RS App) - не следует писать в личные сообщения администраторам или модераторам.',
- title: '',
- link: '',
- },
- ],
- ],
- },
- ],
- },
- [COURSE_TITLES.AWS_FUNDAMENTALS]: {
- title: 'What you should know before starting',
- knowBefore: {
- title: 'Required before the start',
- description: [
- 'Beginners welcome!',
- 'No AWS Cloud experience is necessary.',
- 'We will use the AWS Free Tier',
- 'No IT prerequisites required',
- ],
- },
- willLearn: [
- {
- title: 'What you will learn',
- description: [
- 'Networking Fundamentals',
- 'Cloud Technical Fundamentals',
- 'AWS Cloud Essentials',
- 'Basic AWS Services (EC2, ELB, ASG, RDS, ElastiCache, S3)',
- ],
- },
- ],
- },
- [COURSE_TITLES.AWS_CLOUD_DEVELOPER]: {
- title: 'What you should know before starting',
- knowBefore: {
- title: 'Required before the start',
- description: [
- 'You should be comfortable with at least one programming language (such as Python, JavaScript, Java, or C#) and have a good understanding of basic web development concepts, including HTML, CSS, and JavaScript.',
- 'English language level: Intermediate (B1) and up.',
- 'Being able to spend at least 10 hours per week studying.',
- ],
- },
- willLearn: [],
- },
- [COURSE_TITLES.NODE]: {
- title: 'What you should know before starting',
- knowBefore: {
- title: 'Required before the start',
- description: ['Solid knowledge of JavaScript, including ES6, is required for this course.'],
- },
- willLearn: [],
- },
- [COURSE_TITLES.ANGULAR]: {
- title: 'What you should know before starting',
- knowBefore: {
- title: 'Required before the start',
- description: [
- 'JavaScript, TypeScript Basics, CSS3, HTML5, NPM',
- 'Git, GitHub (clone, add, commit, push, pull, merge, rebase, work with Pull Request)',
- 'Chrome DevTools',
- 'Figma',
- 'Understanding the concept of REST API',
- 'Successful completion of JS/FE Stage 2',
- ],
- },
- willLearn: [],
- },
- [COURSE_TITLES.AWS_DEVOPS]: {
- title: 'What is required for training?',
- knowBefore: {
- title: 'Required before the start',
- description: [
- 'English proficiency level from B1 (Intermediate) and higher',
- 'Solid knowledge of Git',
- 'Good understanding of hypervisors and networking',
- 'Solid knowledge of OS (Linux/Windows)',
- 'Experience in scripting languages: PowerShell, Bash, or Python',
- 'Expertise and practice with Docker',
- ],
- },
- willLearn: [
- {
- title: 'Nice to have:',
- description: ['Knowledge and experience with any cloud platforms (AWS, GCP, Azure)'],
- },
- ],
- },
- [COURSE_TITLES.AWS_AI]: {
- title: 'Что требуется для обучения?',
- knowBefore: {
- title: 'Необходимо до начала',
- description: [
- 'Уровень владения английским языком от B1 (Intermediate) и выше',
- 'Опыт работы с языками: JS или Python',
- 'Свободные 4-8 часа в неделю',
- 'Базовые знания и опыт работы с облачной платформой AWS будут полезны для обучения',
- ],
- },
- willLearn: [],
- },
-};
diff --git a/dev-data/requirements.data.ts b/dev-data/requirements.data.ts
deleted file mode 100644
index beaf9ee27..000000000
--- a/dev-data/requirements.data.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { LINKS } from '@/shared/constants';
-
-export const requirementsData = {
- button: {
- text: 'Register as a mentor',
- link: LINKS.BECOME_MENTOR,
- },
- headerRequirements: 'Requirements for mentors',
- headerTask: 'Mentor responsibilities',
- requirements: [
- 'Desire to help students. '
- + "If you've been working with JS/TS in production for more than 6 months, then that's great",
- 'Desire to mentor 2 to 6 students online or in person',
- 'Ability to spend 3 to 5 hours per week',
- ],
- tasks: ['Conducting an interview', 'Code review tasks', "Answers to students' questions"],
-};
diff --git a/dev-data/study-path-data.types.ts b/dev-data/study-path-data.types.ts
index 1afcc2e3a..43a8cbce6 100644
--- a/dev-data/study-path-data.types.ts
+++ b/dev-data/study-path-data.types.ts
@@ -1,11 +1,15 @@
import { StaticImageData } from 'next/image';
-import type { LinkList } from '@/widgets/required/types';
-
-export type StudyPathPage = {
- page: 'courses' | 'jsEn' | 'jsRu' | 'angular' | 'awsDev';
+type ItemWithLink = {
+ id: number;
+ text: string;
+ title: string;
+ link: string;
+ external?: boolean;
};
+export type LinkList = ItemWithLink[];
+
export type StudyPathProps = {
sectionTitle: string;
sectionIntro: string;
diff --git a/dev-data/study-path.data.ts b/dev-data/study-path.data.ts
index bd375fa30..3d699bffe 100644
--- a/dev-data/study-path.data.ts
+++ b/dev-data/study-path.data.ts
@@ -1,953 +1,218 @@
-import type { StudyPathPage, StudyPathProps } from './study-path-data.types';
+import { StudyPathProps } from './study-path-data.types';
import AWSIcon from '@/shared/assets/icons/aws-black.svg';
import HTMLIcon from '@/shared/assets/icons/html5.svg';
import JSIcon from '@/shared/assets/icons/javascript.svg';
import NodeJSIcon from '@/shared/assets/icons/node-js.svg';
import ReactAngIcon from '@/shared/assets/icons/react-angular.svg';
-import feJsStage1 from '@/shared/assets/stages/stage-1.webp';
-import feJsStage2 from '@/shared/assets/stages/stage-2.webp';
-import feJsStage3 from '@/shared/assets/stages/stage-3.webp';
import { ROUTES } from '@/shared/constants';
-export const studyPath: Record = {
- courses: {
- sectionTitle: 'Choose what you want to learn',
- sectionIntro: 'A full-stack developer is someone who has expertise in both frontend (what users see) and backend (server and database) development. This dual skill set enables them to supervise and implement projects from start to finish. Businesses today prioritize hiring full-stack developers because they can efficiently bridge various technological aspects, resulting in faster product development.',
- stages: [
- {
- id: 1,
- title: 'Pre-school (RU)',
- intro: 'For those brand new to coding, this is your starting point. Get acquainted with the basics and build a strong foundation.',
- modules: [
- {
- id: 1,
- text: '',
- links: [
- {
- id: 1,
- text: '',
- title: 'Pre-school (RU)',
- link: `/${ROUTES.COURSES}/${ROUTES.JS_PRESCHOOL_RU}`,
- external: false,
- },
- ],
- marked: false,
- },
- ],
- image: {
- src: HTMLIcon,
- alt: 'pre-school logo',
- className: 'stage-logo',
- },
- },
- {
- id: 2,
- title: 'JS/TS/FE Fundamentals',
- intro: 'Dive deep into the world of JavaScript, TypeScript, and Frontend development. Understand the core concepts and set yourself up for success.',
- modules: [
- {
- id: 1,
- text: '',
- links: [
- {
- id: 1,
- text: '',
- title: 'JS/TS/FE Fundamentals (RU)',
- link: `/${ROUTES.COURSES}/${ROUTES.JS_RU}`,
- external: false,
- },
- {
- id: 2,
- text: '',
- title: 'JS/TS/FE Fundamentals (EN)',
- link: `/${ROUTES.COURSES}/${ROUTES.JS}`,
- external: false,
- },
- ],
- marked: false,
- },
- ],
- image: {
- src: JSIcon,
- alt: 'JS logo',
- className: 'stage-logo',
- },
- },
- {
- id: 3,
- title: 'React or Angular',
- intro: "Choose your framework and become proficient. Whether you're Team React or Team Angular, we ensure you become an expert",
- modules: [
- {
- id: 1,
- text: '',
- links: [
- {
- id: 1,
- text: '',
- title: 'React',
- link: `/${ROUTES.COURSES}/${ROUTES.REACT}`,
- external: false,
- },
- {
- id: 2,
- text: '',
- title: 'Angular',
- link: `/${ROUTES.COURSES}/${ROUTES.ANGULAR}`,
- external: false,
- },
- ],
- marked: false,
- },
- ],
- image: {
- src: ReactAngIcon,
- alt: 'react and angular logo',
- className: 'stage-logo',
- },
- },
- {
- id: 4,
- title: 'NodeJS',
- intro: "Grasp the power of backend development. With Nodejs, you'll learn to build robust and scalable applications",
- modules: [
- {
- id: 1,
- text: '',
- links: [
- {
- id: 1,
- text: '',
- title: 'NodeJS',
- link: `/${ROUTES.COURSES}/${ROUTES.NODE_JS}`,
- external: false,
- },
- ],
- marked: false,
- },
- ],
- image: {
- src: NodeJSIcon,
- alt: 'Node JS logo',
- className: 'stage-logo',
- },
- },
- {
- id: 5,
- title: 'AWS Fundamentals',
- intro: 'Delve into the cloud with Amazon Web Services. Understand the essentials and ensure your apps are hosted seamlessly.',
- modules: [
- {
- id: 1,
- text: '',
- links: [
- {
- id: 1,
- text: '',
- title: 'AWS Fundamentals',
- link: `/${ROUTES.COURSES}/${ROUTES.AWS_FUNDAMENTALS}`,
- external: false,
- },
- ],
- marked: false,
- },
- ],
- image: {
- src: AWSIcon,
- alt: 'aws logo',
- className: 'stage-logo',
- },
- },
- {
- id: 6,
- title: 'AWS Developer',
- intro: 'Go beyond the basics. Become an AWS pro and unlock the potential of cloud development.',
- modules: [
- {
- id: 1,
- text: '',
- links: [
- {
- id: 1,
- text: '',
- title: 'AWS Developer',
- link: `/${ROUTES.COURSES}/${ROUTES.AWS_DEVELOPER}`,
- external: false,
- },
- ],
- marked: false,
- },
- ],
- image: {
- src: AWSIcon,
- alt: 'aws logo',
- className: 'stage-logo',
- },
- },
- {
- id: 7,
- title: 'AWS DevOps',
- intro: 'If you are looking for an entry point to kickstart your IT career as a DevOps engineer, then this AWS course challenge is what you need.',
- modules: [
- {
- id: 1,
- text: '',
- links: [
- {
- id: 1,
- text: '',
- title: 'AWS DevOps',
- link: `/${ROUTES.COURSES}/${ROUTES.AWS_DEVOPS}`,
- external: false,
- },
- ],
- marked: false,
- },
- ],
- image: {
- src: AWSIcon,
- alt: 'aws logo',
- className: 'stage-logo',
- },
- },
- ],
- },
- jsEn: {
- sectionTitle: 'Choose what you want to learn',
- sectionIntro: 'A full-stack developer is someone who has expertise in both frontend (what users see) and backend (server and database) development. This dual skill set enables them to supervise and implement projects from start to finish. Businesses today prioritize hiring full-stack developers because they can efficiently bridge various technological aspects, resulting in faster product development.',
- stages: [
- {
- id: 1,
- title: 'Stage 1',
- intro: 'Everyone registered is automatically eligible for this stage. The first stage lasts 15 weeks. This stage includes practical assignments and tests. Evaluation is either automatic or in the form of cross-checking between students.',
- modules: [
- {
- id: 1,
- text: 'Topics covered: Git, HTML, CSS, Javascript basics',
- links: [],
- marked: false,
- },
- ],
- image: {
- src: feJsStage1,
- alt: 'working students',
- className: 'stage-image',
- },
- },
- {
- id: 2,
- title: 'Stage 2',
- intro: 'To pass to the second stage, you must successfully complete the tasks and tests from the first stage without missing the deadlines, and pass a mock technical interview with one of our mentors.The second stage lasts 20 weeks. You will be assigned a personal mentor who will answer your questions from now on. This stage includes practical exercises and tests which will be reviewed and evaluated by your mentor.',
- modules: [
- {
- id: 1,
- text: 'Topics covered: Advanced Javascript, Security, Testing, Agile, Networking, Web development tools',
- links: [],
- marked: false,
- },
- ],
- image: {
- src: feJsStage2,
- alt: 'working students',
- className: 'stage-image',
- },
- },
- {
- id: 3,
- title: 'Stage 3',
- intro: 'Learning either React or Angular Framework (the choice belongs to the student). To enroll, you need to successfully complete two stages of training. Format: mentoring, self-study, webinars, and communication on Discord. Practical sessions are reviewed and evaluated by mentors, as well as through cross-checking methods. Throughout the training, mock interviews are conducted with different mentors.',
- modules: [
- {
- id: 1,
- text: 'Choose a Framework: React or Angular.',
- links: [],
- marked: true,
- },
- {
- id: 2,
- text: 'Collaborative development of a final project.',
- links: [],
- marked: true,
- },
- {
- id: 3,
- text: 'Framework-based interviews.',
- links: [],
- marked: true,
- },
- ],
- image: {
- src: feJsStage3,
- alt: 'student works',
- className: 'stage-image',
- },
- },
- ],
- },
- jsRu: {
- sectionTitle: 'Выберите, чему вы хотите научиться',
- sectionIntro: 'Full-stack разработчик — это человек, обладающий опытом как в области внешнего интерфейса (то, что видят пользователи), так и в области внутреннего интерфейса (сервера и базы данных). Этот двойной набор навыков позволяет им контролировать и реализовывать проекты от начала до конца. Сегодня компании отдают приоритет найму разработчиков полного стека, потому что они могут эффективно объединить различные технологические аспекты, что приводит к более быстрой разработке продукта.',
- stages: [
- {
- id: 1,
- title: 'Этап 1',
- intro: 'Все зарегистрированные автоматически имеют право на прохождение этого этапа. Первый этап продолжается 15 недель. На этом этапе включаются практические задания и тесты. Оценка может быть как автоматической, так и в форме перекрестной проверки между учащимися.',
- modules: [
- {
- id: 1,
- text: 'Темы: Git, HTML, CSS, Основы Javascript',
- links: [],
- marked: false,
- },
- ],
- image: {
- src: feJsStage1,
- alt: 'студенты за работой',
- className: 'stage-image',
- },
- },
- {
- id: 2,
- title: 'Этап 2',
- intro: 'Чтобы перейти на второй этап, вам необходимо успешно выполнить задания и тесты первого этапа без пропуска сроков и пройти пробное техническое интервью с одним из наших менторов. Второй этап длится 20 недель. Вам будет назначен личный ментор, который будет отвечать на ваши вопросы. Этот этап включает в себя практические задания и тесты, которые будут проверяться и оцениваться вашим ментором, а также перекрестной проверкой других студентов. Помимо этого, проводятся пробные интервью с другими менторами.',
- modules: [
- {
- id: 1,
- text: 'Темы: Advanced Javascript, Security, Testing, Agile, Networking, Web development tools',
- links: [],
- marked: false,
- },
- ],
- image: {
- src: feJsStage2,
- alt: 'студенты за работой',
- className: 'stage-image',
- },
- },
- {
- id: 3,
- title: 'Этап 3',
- intro: 'Обучение применению React или Angular (выбор за студентом). Чтобы записаться на курс, необходимо успешно пройти первые два этапа обучения. Формат: менторство, самостоятельные занятия, вебинары и общение в Discord. Практические занятия разбираются и оцениваются наставниками, а также перекрестной проверкой другими студентами. На протяжении всего обучения проводятся пробные интервью с другими менторами.',
- modules: [
- {
- id: 1,
- text: 'Выберите фреймворк: React или Angular.',
- links: [],
- marked: true,
- },
- {
- id: 2,
- text: 'Коллективная разработка финального проекта.',
- links: [],
- marked: true,
- },
- {
- id: 3,
- text: 'Фреймворк-зависимые интервью.',
- links: [],
- marked: true,
- },
- ],
- image: {
- src: feJsStage3,
- alt: 'студент изучает материал',
- className: 'stage-image',
- },
- },
- ],
- },
- angular: {
- sectionTitle: 'Course Curriculum',
- sectionIntro: 'This program will have theory and practice on the following topic:',
- stages: [
- {
- id: 1,
- title: 'Week #1',
- intro: '',
- modules: [
- {
- id: 1,
- text: 'Module "Angular Intro. TypeScript."',
- links: [],
- marked: true,
- },
- {
- id: 2,
- text: 'Module "Angular. Components"',
- links: [],
- marked: true,
- },
- {
- id: 3,
- text: 'Module "Angular. Directives & Pipes"',
- links: [],
- marked: true,
- },
- ],
- image: {
- src: null,
- alt: '',
- className: '',
- },
- },
- {
- id: 2,
- title: 'Week #2',
- intro: '',
- modules: [
- {
- id: 1,
- text: 'Module "Angular Intro. Task Review."',
- links: [],
- marked: true,
- },
- {
- id: 2,
- text: 'Angular. Modules & Services, Dependency Injection',
- links: [],
- marked: true,
- },
- {
- id: 3,
- text: 'Module "Angular. Directives & Pipes"',
- links: [],
- marked: true,
- },
- {
- id: 4,
- text: 'Module "Angular. Routing"',
- links: [],
- marked: true,
- },
- {
- id: 5,
- text: 'Workshop',
- links: [],
- marked: true,
- },
- ],
- image: {
- src: null,
- alt: '',
- className: '',
- },
- },
- {
- id: 3,
- title: 'Week #3',
- intro: '',
- modules: [
- {
- id: 1,
- text: '"Angular. Components, Directives, Pipes" task review',
- links: [],
- marked: true,
- },
- {
- id: 2,
- text: 'Module "RxJS & Observables"',
- links: [],
- marked: true,
- },
- {
- id: 3,
- text: 'Module "Angular. HTTP"',
- links: [],
- marked: true,
- },
- {
- id: 4,
- text: 'Module "Angular. Forms"',
- links: [],
- marked: true,
- },
- {
- id: 5,
- text: 'Workshop',
- links: [],
- marked: true,
- },
- ],
- image: {
- src: null,
- alt: '',
- className: '',
- },
- },
- {
- id: 4,
- title: 'Week #4',
- intro: '',
- modules: [
- {
- id: 1,
- text: '"Angular. Modules, Services, Routing" task review',
- links: [],
- marked: true,
- },
- {
- id: 2,
- text: 'Module "Angular. Redux & NgRx"',
- links: [],
- marked: true,
- },
- {
- id: 3,
- text: 'Module "Angular. Unit Test"',
- links: [],
- marked: true,
- },
- {
- id: 4,
- text: 'Workshop',
- links: [],
- marked: true,
- },
- ],
- image: {
- src: null,
- alt: '',
- className: '',
- },
- },
- {
- id: 5,
- title: 'Week #5-8',
- intro: '',
- modules: [
- {
- id: 1,
- text: '"Angular. RxJS & HTTPClient & NgRx & Forms", task review',
- links: [],
- marked: true,
- },
- {
- id: 2,
- text: 'Final Angular test',
- links: [],
- marked: true,
- },
- {
- id: 3,
- text: 'Workshop',
- links: [],
- marked: true,
- },
- {
- id: 4,
- text: '"Project Management Application" final task',
- links: [],
- marked: true,
- },
- ],
- image: {
- src: null,
- alt: '',
- className: '',
- },
- },
- {
- id: 6,
- title: 'Week #9',
- intro: '',
- modules: [
- {
- id: 1,
- text: 'Cross-checking the "Project management application" final task',
- links: [],
- marked: true,
- },
- ],
- image: {
- src: null,
- alt: '',
- className: '',
- },
- },
- ],
- },
- awsDev: {
- sectionTitle: 'Course Curriculum',
- sectionIntro: 'This program will have theory and practice on the following topic:',
- stages: [
- {
- id: 1,
- title: 'Module 1. Cloud Introduction',
- intro: '',
- modules: [
- {
- id: 1,
- text: 'Fundamental theory about cloud computing',
- links: [],
- marked: true,
- },
- {
- id: 2,
- text: 'Cloud service models, cloud deployment models, infrastructure-as-code',
- links: [],
- marked: true,
- },
- {
- id: 3,
- text: 'Monolith vs microservices vs serverless',
- links: [],
- marked: true,
- },
- {
- id: 4,
- text: 'AWS intro, registration, Cloud Watch, IAM Repository structure',
- links: [],
- marked: true,
- },
- ],
- image: {
- src: null,
- alt: '',
- className: '',
- },
- },
- {
- id: 2,
- title: 'Module 2. Serving SPA',
- intro: '',
- modules: [
- {
- id: 1,
- text: 'AWS Simple Storage Service overview',
- links: [],
- marked: true,
- },
- {
- id: 2,
- text: 'Services & tools overview',
- links: [],
- marked: true,
- },
- {
- id: 3,
- text: 'AWS CloudFront overview',
- links: [],
- marked: true,
- },
- {
- id: 4,
- text: 'Basic overview of deployment process to CloudFront and S3',
- links: [],
- marked: true,
- },
- {
- id: 5,
- text: 'AWS CLI overview',
- links: [],
- marked: true,
- },
- ],
- image: {
- src: null,
- alt: '',
- className: '',
- },
- },
- {
- id: 3,
- title: 'Module 3. Serverless API',
- intro: '',
- modules: [
- {
- id: 1,
- text: 'AWS Lambda overview',
- links: [],
- marked: true,
- },
- {
- id: 2,
- text: 'Introduction to collecting logs with AWS CloudWatch',
- links: [],
- marked: true,
- },
- {
- id: 3,
- text: 'Lambda advanced features and configuration',
- links: [],
- marked: true,
- },
- ],
- image: {
- src: null,
- alt: '',
- className: '',
- },
- },
- {
- id: 4,
- title: 'Module 4. Integration with NoSQL Database',
- intro: '',
- modules: [
- {
- id: 1,
- text: 'Easy way to store data in cloud',
- links: [],
- marked: true,
- },
- {
- id: 2,
- text: 'AWS DynamoDB and how to use it',
- links: [],
- marked: true,
- },
- ],
- image: {
- src: null,
- alt: '',
- className: '',
- },
- },
- {
- id: 5,
- title: 'Module 5. Integration with S3',
- intro: '',
- modules: [
- {
- id: 1,
- text: 'AWS S3 in-depth introduction',
- links: [],
- marked: true,
- },
- {
- id: 2,
- text: 'S3 storage classes and their use cases',
- links: [],
- marked: true,
- },
- {
- id: 3,
- text: 'S3 access control & encryption',
- links: [],
- marked: true,
- },
- {
- id: 4,
- text: 'S3 versioning, lifecycle management & events',
- links: [],
- marked: true,
- },
- {
- id: 5,
- text: 'Integration with S3 and Lambda overview',
- links: [],
- marked: true,
- },
- ],
- image: {
- src: null,
- alt: '',
- className: '',
- },
- },
- {
- id: 6,
- title: 'Module 6. Async Microservices Communication',
- intro: '',
- modules: [
- {
- id: 1,
- text: 'Async messaging overview',
- links: [],
- marked: true,
- },
- {
- id: 2,
- text: 'AWS SQS overview',
- links: [],
- marked: true,
- },
- {
- id: 3,
- text: 'AWS SNS overview',
- links: [],
- marked: true,
- },
- {
- id: 4,
- text: 'Integration with SQS, SNS, and Lambda overview',
- links: [],
- marked: true,
- },
- ],
- image: {
- src: null,
- alt: '',
- className: '',
- },
- },
- {
- id: 7,
- title: 'Module 7. Authorization',
- intro: '',
- modules: [
- {
- id: 1,
- text: 'Authentication & authorization overview',
- links: [],
- marked: true,
- },
- {
- id: 2,
- text: 'Lambda authorizer & API Gateway',
- links: [],
- marked: true,
- },
- {
- id: 3,
- text: 'AWS Cognito overview',
- links: [],
- marked: true,
- },
- {
- id: 4,
- text: 'Cognito user pool',
- links: [],
- marked: true,
- },
- {
- id: 5,
- text: 'Cognito identity pool',
- links: [],
- marked: true,
- },
- ],
- image: {
- src: null,
- alt: '',
- className: '',
- },
- },
- {
- id: 8,
- title: 'Module 8. Integration with SQL Database',
- intro: '',
- modules: [
- {
- id: 1,
- text: 'Relational databases theory',
- links: [],
- marked: true,
- },
- {
- id: 2,
- text: 'SQL overview',
- links: [],
- marked: true,
- },
- {
- id: 3,
- text: 'Overview of AWS database offering',
- links: [],
- marked: true,
- },
- {
- id: 4,
- text: 'AWS RDS and its engines',
- links: [],
- marked: true,
- },
- {
- id: 5,
- text: 'Serverless functions & AWS RDS',
- links: [],
- marked: true,
- },
- ],
- image: {
- src: null,
- alt: '',
- className: '',
- },
- },
- {
- id: 9,
- title: 'Module 9. Containerization',
- intro: '',
- modules: [
- {
- id: 1,
- text: 'Docker overview',
- links: [],
- marked: true,
- },
- {
- id: 2,
- text: 'Dockerfiles & images',
- links: [],
- marked: true,
- },
- {
- id: 3,
- text: 'Containers & VMs',
- links: [],
- marked: true,
- },
- {
- id: 4,
- text: 'Docker build optimizations',
- links: [],
- marked: true,
- },
- {
- id: 5,
- text: 'AWS Elastic Beanstalk overview',
- links: [],
- marked: true,
- },
- {
- id: 6,
- text: 'AWS EB CLI',
- links: [],
- marked: true,
- },
- ],
- image: {
- src: null,
- alt: '',
- className: '',
- },
- },
- {
- id: 10,
- title: 'Module 10. Backend for Frontend',
- intro: '',
- modules: [
- {
- id: 1,
- text: 'Backend for frontend overview',
- links: [],
- marked: true,
- },
- {
- id: 2,
- text: 'BFF as pattern',
- links: [],
- marked: true,
- },
- {
- id: 3,
- text: 'API Gateway as BFF',
- links: [],
- marked: true,
- },
- {
- id: 4,
- text: 'AWS Elastic Beanstalk configuration',
- links: [],
- marked: true,
- },
- ],
- image: {
- src: null,
- alt: '',
- className: '',
- },
- },
- ],
- },
+export const studyPath: StudyPathProps = {
+ sectionTitle: 'Choose what you want to learn',
+ sectionIntro:
+ 'A full-stack developer is someone who has expertise in both frontend (what users see) and backend (server and database) development. This dual skill set enables them to supervise and implement projects from start to finish. Businesses today prioritize hiring full-stack developers because they can efficiently bridge various technological aspects, resulting in faster product development.',
+ stages: [
+ {
+ id: 1,
+ title: 'Pre-school (RU)',
+ intro:
+ 'For those brand new to coding, this is your starting point. Get acquainted with the basics and build a strong foundation.',
+ modules: [
+ {
+ id: 1,
+ text: '',
+ links: [
+ {
+ id: 1,
+ text: '',
+ title: 'Pre-school (RU)',
+ link: `/${ROUTES.COURSES}/${ROUTES.JS_PRESCHOOL_RU}`,
+ external: false,
+ },
+ ],
+ marked: false,
+ },
+ ],
+ image: {
+ src: HTMLIcon,
+ alt: 'pre-school logo',
+ className: 'stage-logo',
+ },
+ },
+ {
+ id: 2,
+ title: 'JS/TS/FE Fundamentals',
+ intro:
+ 'Dive deep into the world of JavaScript, TypeScript, and Frontend development. Understand the core concepts and set yourself up for success.',
+ modules: [
+ {
+ id: 1,
+ text: '',
+ links: [
+ {
+ id: 1,
+ text: '',
+ title: 'JS/TS/FE Fundamentals (RU)',
+ link: `/${ROUTES.COURSES}/${ROUTES.JS_RU}`,
+ external: false,
+ },
+ {
+ id: 2,
+ text: '',
+ title: 'JS/TS/FE Fundamentals (EN)',
+ link: `/${ROUTES.COURSES}/${ROUTES.JS}`,
+ external: false,
+ },
+ ],
+ marked: false,
+ },
+ ],
+ image: {
+ src: JSIcon,
+ alt: 'JS logo',
+ className: 'stage-logo',
+ },
+ },
+ {
+ id: 3,
+ title: 'React or Angular',
+ intro:
+ "Choose your framework and become proficient. Whether you're Team React or Team Angular, we ensure you become an expert",
+ modules: [
+ {
+ id: 1,
+ text: '',
+ links: [
+ {
+ id: 1,
+ text: '',
+ title: 'React',
+ link: `/${ROUTES.COURSES}/${ROUTES.REACT}`,
+ external: false,
+ },
+ {
+ id: 2,
+ text: '',
+ title: 'Angular',
+ link: `/${ROUTES.COURSES}/${ROUTES.ANGULAR}`,
+ external: false,
+ },
+ ],
+ marked: false,
+ },
+ ],
+ image: {
+ src: ReactAngIcon,
+ alt: 'react and angular logo',
+ className: 'stage-logo',
+ },
+ },
+ {
+ id: 4,
+ title: 'NodeJS',
+ intro:
+ "Grasp the power of backend development. With Nodejs, you'll learn to build robust and scalable applications",
+ modules: [
+ {
+ id: 1,
+ text: '',
+ links: [
+ {
+ id: 1,
+ text: '',
+ title: 'NodeJS',
+ link: `/${ROUTES.COURSES}/${ROUTES.NODE_JS}`,
+ external: false,
+ },
+ ],
+ marked: false,
+ },
+ ],
+ image: {
+ src: NodeJSIcon,
+ alt: 'Node JS logo',
+ className: 'stage-logo',
+ },
+ },
+ {
+ id: 5,
+ title: 'AWS Fundamentals',
+ intro:
+ 'Delve into the cloud with Amazon Web Services. Understand the essentials and ensure your apps are hosted seamlessly.',
+ modules: [
+ {
+ id: 1,
+ text: '',
+ links: [
+ {
+ id: 1,
+ text: '',
+ title: 'AWS Fundamentals',
+ link: `/${ROUTES.COURSES}/${ROUTES.AWS_FUNDAMENTALS}`,
+ external: false,
+ },
+ ],
+ marked: false,
+ },
+ ],
+ image: {
+ src: AWSIcon,
+ alt: 'aws logo',
+ className: 'stage-logo',
+ },
+ },
+ {
+ id: 6,
+ title: 'AWS Developer',
+ intro:
+ 'Go beyond the basics. Become an AWS pro and unlock the potential of cloud development.',
+ modules: [
+ {
+ id: 1,
+ text: '',
+ links: [
+ {
+ id: 1,
+ text: '',
+ title: 'AWS Developer',
+ link: `/${ROUTES.COURSES}/${ROUTES.AWS_DEVELOPER}`,
+ external: false,
+ },
+ ],
+ marked: false,
+ },
+ ],
+ image: {
+ src: AWSIcon,
+ alt: 'aws logo',
+ className: 'stage-logo',
+ },
+ },
+ {
+ id: 7,
+ title: 'AWS DevOps',
+ intro:
+ 'If you are looking for an entry point to kickstart your IT career as a DevOps engineer, then this AWS course challenge is what you need.',
+ modules: [
+ {
+ id: 1,
+ text: '',
+ links: [
+ {
+ id: 1,
+ text: '',
+ title: 'AWS DevOps',
+ link: `/${ROUTES.COURSES}/${ROUTES.AWS_DEVOPS}`,
+ external: false,
+ },
+ ],
+ marked: false,
+ },
+ ],
+ image: {
+ src: AWSIcon,
+ alt: 'aws logo',
+ className: 'stage-logo',
+ },
+ },
+ ],
};
diff --git a/dev-data/training-program.data.tsx b/dev-data/training-program.data.tsx
deleted file mode 100644
index 51e291772..000000000
--- a/dev-data/training-program.data.tsx
+++ /dev/null
@@ -1,329 +0,0 @@
-// TODO separate data and markup
-import { JSX } from 'react';
-import { StaticImageData } from 'next/image';
-
-import {
- AWS_FUNDAMENTALS_BADGE,
- AwsFundamentalsBadge,
- COURSE_TITLES,
- CourseNamesKeys,
-} from './course-titles.data';
-import awsPractitionerBadge from '@/shared/assets/aws-cloud-pract-badge.webp';
-import angularImg from '@/shared/assets/rs-slope-angular.webp';
-import awsDevImg from '@/shared/assets/rs-slope-aws-dev.webp';
-import awsFundamentalsImg from '@/shared/assets/rs-slope-aws-fundamentals.webp';
-import jsImg from '@/shared/assets/rs-slope-js.webp';
-import nodejsImg from '@/shared/assets/rs-slope-nodejs.webp';
-import reactEnImg from '@/shared/assets/rs-slope-react-en.webp';
-import { REGISTRATION_WILL_OPEN_SOON, REGISTRATION_WILL_OPEN_SOON_RU } from '@/shared/constants';
-import { LinkCustom } from '@/shared/ui/link-custom';
-import { List } from '@/shared/ui/list';
-import { Paragraph } from '@/shared/ui/paragraph';
-import { Subtitle } from '@/shared/ui/subtitle';
-
-interface CourseInfo {
- title: string;
- content: JSX.Element[];
- image: StaticImageData;
-}
-
-type ContentMap = {
- [key in CourseNamesKeys | AwsFundamentalsBadge]: CourseInfo;
-};
-
-export const contentMap: ContentMap = {
- [COURSE_TITLES.AWS_CLOUD_DEVELOPER]: {
- title: 'Training Program',
- content: [
- // TODO delete keys
-
- This course is a step-by-step journey to become an AWS Certified Developer ‒ Associate
- through this course. You will gain practical experience working with various AWS services
- and technologies via over 10 hands-on tasks. During the course, you'll dive deep into
- AWS, from cloud computing basics to advanced integrations and deployment strategies, through
- nine carefully designed modules.
- ,
-
- Be well-prepared to pass the "AWS Certified Developer - Associate" certification
- and confidently apply your skills in real-world projects by the end of the course.
- ,
- Course highlights:,
-
,
- ],
- image: awsDevImg,
- },
- [COURSE_TITLES.AWS_FUNDAMENTALS]: {
- title: 'Training Program',
- content: [
-
- The AWS Certified Cloud Practitioner certification is a great entry-level certification for
- AWS. It's great at assessing how well you understand AWS, its services, and its
- ecosystem.
- ,
-
- The course consists of weekly assignments that you can complete at your own pace, followed
- by a test that will help you evaluate your understanding of the materials. We expect that
- you will need to dedicate 5‒10 hours per week to complete the assignments. The total
- duration of the course is 5 weeks.
- ,
-
- During the course, you will have access to online sessions led by AWS User Groups and RS
- School Mentors, where you can ask questions, discuss the materials, and get feedback.
- ,
- ],
- image: awsFundamentalsImg,
- },
- [COURSE_TITLES.NODE]: {
- title: 'Course Topics',
- content: [
-
- This course is designed for JavaScript / Front-End developers who want to get acquainted
- with Node.js and the server side of web application development.
- ,
-
- The course consists of weekly assignments that you can complete at your own pace, followed
- by a test that will help you evaluate your understanding of the materials
- ,
-
,
- ],
- image: nodejsImg,
- },
- [COURSE_TITLES.ANGULAR]: {
- title: 'Training Program',
- content: [
-
- This course is designed for individuals with a solid foundation in JavaScript, TypeScript,
- and front-end development. The course is exclusively available for graduates who have
- successfully completed JS/FE Stage 2.
- ,
-
- The course lasts 11 weeks, requiring approximately 20-40 hours of study per week.
- ,
-
-
- {`All webinars are recorded and available on our `}
-
- Youtube
-
- . Theoretical materials are provided as recorded lectures from previous courses.
- ,
- ],
- image: angularImg,
- },
- [COURSE_TITLES.JS_EN]: {
- title: 'Training Program',
- content: [
-
- The program consists of 3 stages. There may be requirements for advancing to each higher
- stage, which will be described below. This specific run of the program will take the form of
- self-study. This means that you will have access to pre-recorded webinars, recommended
- materials, and weekly live Q&A sessions with our mentors/coordinators to answer any
- questions you might have.
- ,
-
- You will also have the ability to communicate with other students and help each other solve
- any problems you might face. We will provide you with a list of topics that should be
- covered for each stage with recommended deadlines, but you will have the freedom to choose
- when you want to watch the lectures and complete the tasks.
- ,
-
- BE AWARE
- {` that practical tasks’ deadlines are not suggestions, and should be
- respected.`}
- ,
- ],
- image: jsImg,
- },
- [COURSE_TITLES.JS_RU]: {
- title: 'Программа обучения',
- content: [
-
- Программа состоит из 3 этапов. На каждом последующем этапе могут быть установлены требования
- для перехода на следующий уровень, которые будут описаны ниже. В данном запуске программы
- будет предусмотрено самостоятельное обучение. Это означает, что у вас будет доступ к
- предзаписанным вебинарам, рекомендуемым материалам и еженедельным онлайн сессиям вопросов и
- ответов с нашими наставниками/координаторами для ответа на любые вопросы, которые у вас
- могут возникнуть.
- ,
-
- У вас также будет возможность общаться с другими студентами и помогать друг другу решать
- любые проблемы, с которыми вы можете столкнуться. Мы предоставим вам список тем, которые
- должны быть рассмотрены на каждом этапе с рекомендуемыми сроками, но у вас будет свобода
- выбора времени для просмотра лекций и выполнения заданий.
- ,
-
- ОБРАТИТЕ ВНИМАНИЕ
- {`, что сроки выполнения практических заданий не являются
- рекомендацией и должны быть соблюдены.`}
- ,
- ],
- image: jsImg,
- },
- [COURSE_TITLES.JS_PRESCHOOL_RU]: {
- title: 'Программа обучения',
- content: [
- ,
- ,
- ,
- ],
- image: jsImg,
- },
- [COURSE_TITLES.REACT]: {
- title: 'Target audience',
- content: [
-
- We are looking for students with strong CoreJS/TS/Frontend skills.
- ,
- Requirements:,
-
,
- ],
- image: reactEnImg,
- },
- [AWS_FUNDAMENTALS_BADGE]: {
- title: 'AWS DIGITAL BADGE',
- content: [
-
- Upon completing the course and passing the AWS Cloud Quest: Cloud Practitioner, you will
- obtain an AWS digital badge. This badge will recognize your achievement and demonstrate your
- knowledge of AWS fundamentals to potential employers or clients. By the end of the course,
- you will have gained a solid foundation in AWS fundamentals and be prepared to pass the AWS
- Cloud Practitioner certification.
- ,
- ],
- image: awsPractitionerBadge,
- },
- [COURSE_TITLES.AWS_DEVOPS]: {
- title: 'Details',
- content: [
-
- If you are looking for an entry point to kickstart your IT career as a DevOps engineer, then
- this AWS course challenge is what you need.
- ,
-
- Showcase your level of expertise and join this expert-led program to:
- ,
-
,
- What do we offer?,
-
,
- ],
- image: awsDevImg,
- },
- [COURSE_TITLES.AWS_AI]: {
- title: 'О курсе',
- content: [
-
- AWS AI Practitioner - это бесплатный курс в RS School, направленный на обучение наших
- студентов основам искусственного интеллекта и машинного обучения (AI/ML), а также подготовку
- к сертификации AWS Certified AI Practitioner
- ,
- Программа включает:,
-
,
- ],
- image: awsDevImg,
- },
-};
-
-export const trainingProgramLink = {
- en: {
- linkLabel: 'Register',
- noLinkLabel: REGISTRATION_WILL_OPEN_SOON,
- },
- ru: {
- linkLabel: 'Зарегистрироваться',
- noLinkLabel: REGISTRATION_WILL_OPEN_SOON_RU,
- },
-};
diff --git a/dev-data/widget-communication.data.ts b/dev-data/widget-communication.data.ts
deleted file mode 100644
index 7d88a40f8..000000000
--- a/dev-data/widget-communication.data.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-export const communicationText = {
- en: {
- title: 'Communication',
- subTitle: 'Discord is the main communication channel in RS School',
- subTitleJs: 'RS School uses two main communication channels:',
- firstParagraphFirstHalf: 'Here is link for the ',
- discordParagraphTextJs: ': Join the Discord server to see the latest news and chat with students.',
- discordLink: 'course Discord server',
- discordLinkJs: 'Discord',
- firstParagraphSecondHalf: ', where you can see latest news and chat with students.',
- telegramParagraphTextJs: ': You can also join the official Telegram channel for updates and discussions.',
- discordNote: 'Attention! In some countries, access to Discord requires the use of a VPN. If you are having trouble connecting, please try using a reliable VPN service.',
- secondParagraphFirstHalf: 'There are channels in ',
- telegramLink: 'Telegram',
- secondParagraphSecondHalf:
- ' for discussing events related to your location. For example, offline lectures or just informal chats among students from the same location.',
- thirdParagraphFirstHalf: 'Please read the information about communication in RS School in the ',
- rsDocsLink: 'RS Docs',
- thirdParagraphSecondHalf: ', where you can find rules, descriptions of channels, FAQ.',
- },
- ru: {
- title: 'Общение',
- subTitle: 'Дискорд — основной способ общения в RS School',
- subTitleJs: '',
- firstParagraphFirstHalf: 'Вот ссылка на ',
- discordParagraphTextJs: '',
- discordLink: 'Дискорд сервер курса',
- discordLinkJs: '',
- firstParagraphSecondHalf:
- ', где вы можете посмотреть последние новости, задать вопросы и общаться со студентами.',
- telegramParagraphTextJs: '',
- discordNote: 'Внимание! В некоторых странах для доступа к Discord требуется использование VPN. Если у вас возникают трудности с подключением, попробуйте использовать надёжный VPN-сервис.',
- secondParagraphFirstHalf: 'Также есть каналы в ',
- telegramLink: 'Телеграм',
- secondParagraphSecondHalf:
- ' для обсуждения мероприятий, относящихся к вашему городу. Например, офлайн лекции или просто для общения студентов из одной локации.',
- thirdParagraphFirstHalf: 'Обязательно прочитайте информацию об общении в RS School в ',
- rsDocsLink: 'RS Docs',
- thirdParagraphSecondHalf: ', где вы можете найти правила, описание каналов, FAQ.',
- },
-};
diff --git a/eslint.config.js b/eslint.config.js
index aa02daca7..5c87d41bb 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -264,7 +264,10 @@ export default tseslint.config(
'@stylistic/quote-props': ['error', 'consistent'],
'unicorn/filename-case': [
'error',
- { cases: { kebabCase: true } },
+ {
+ cases: { kebabCase: true },
+ ignore: [/^Type[A-Z][a-zA-Z0-9]*\.ts$/],
+ },
],
},
},
diff --git a/package-lock.json b/package-lock.json
index 9b86aafda..f3202524b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,9 +8,11 @@
"name": "rs-site",
"version": "0.0.0",
"dependencies": {
+ "@contentful/rich-text-react-renderer": "^16.0.1",
"@next/third-parties": "^15.2.4",
"class-variance-authority": "^0.7.1",
"classnames": "^2.5.1",
+ "contentful-resolve-response": "^1.9.3",
"dayjs": "^1.11.13",
"github-markdown-css": "^5.8.1",
"http-status": "^2.1.0",
@@ -43,6 +45,7 @@
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
"@testing-library/user-event": "^14.6.1",
+ "@types/contentful-resolve-response": "^0.1.33",
"@types/gapi.youtube": "^3.0.40",
"@types/jest": "^29.5.14",
"@types/node": "^22.15.3",
@@ -479,6 +482,46 @@
"json-pointer": "^0.6.2"
}
},
+ "node_modules/@contentful/rich-text-react-renderer": {
+ "version": "16.0.1",
+ "resolved": "https://registry.npmjs.org/@contentful/rich-text-react-renderer/-/rich-text-react-renderer-16.0.1.tgz",
+ "integrity": "sha512-7wZZBMgwbq5Udp2KebKCJoh9K+EPGlgRkudhXSp+OxtAIdBC6JUz3Oi9kXXKYKLeSg7iTBpkO1dd0/xFjHHKbg==",
+ "license": "MIT",
+ "dependencies": {
+ "@contentful/rich-text-types": "^17.0.0"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.6 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^16.8.6 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@contentful/rich-text-react-renderer/node_modules/@contentful/rich-text-types": {
+ "version": "17.0.0",
+ "resolved": "https://registry.npmjs.org/@contentful/rich-text-types/-/rich-text-types-17.0.0.tgz",
+ "integrity": "sha512-x50t6sILzFHBdFpAo0foJRnH8fHWyidheWhAv3uwt9aOnNqTh893gxyoc3Q0RVEZxXfHpTi+O9gmakHcdlRdTA==",
+ "license": "MIT",
+ "dependencies": {
+ "is-plain-obj": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@contentful/rich-text-react-renderer/node_modules/is-plain-obj": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz",
+ "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/@contentful/rich-text-types": {
"version": "16.8.5",
"resolved": "https://registry.npmjs.org/@contentful/rich-text-types/-/rich-text-types-16.8.5.tgz",
@@ -3580,6 +3623,13 @@
"@types/node": "*"
}
},
+ "node_modules/@types/contentful-resolve-response": {
+ "version": "0.1.33",
+ "resolved": "https://registry.npmjs.org/@types/contentful-resolve-response/-/contentful-resolve-response-0.1.33.tgz",
+ "integrity": "sha512-o22sQ2VaRWN2/zGSAw3f+RvJkiBXvV+7mRNVpjAO1rUbs1301u7WAJ7JRqJQZ/ikPPSTjZLhBn0UnHWcCeKwbg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/debug": {
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
@@ -5868,10 +5918,9 @@
}
},
"node_modules/contentful-resolve-response": {
- "version": "1.9.2",
- "resolved": "https://registry.npmjs.org/contentful-resolve-response/-/contentful-resolve-response-1.9.2.tgz",
- "integrity": "sha512-VTY1hZGh29yspBeveJ62cuDf+cw9Iq/NcKWBdNHjTq8hvgxz+E19+Pej3LW/02o98+D0XcKfcPYXIpm4lHY+eg==",
- "dev": true,
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/contentful-resolve-response/-/contentful-resolve-response-1.9.3.tgz",
+ "integrity": "sha512-1mPTz7bJLZsLdWAOEyjoVKys/eTXa7o3aV3EYLH7sqqF8V1fnRHlJqlDoxGZFsPxQPBYag+V89B1E/HYE0ENnQ==",
"license": "MIT",
"dependencies": {
"fast-copy": "^2.1.7"
@@ -5884,7 +5933,6 @@
"version": "2.1.7",
"resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-2.1.7.tgz",
"integrity": "sha512-ozrGwyuCTAy7YgFCua8rmqmytECYk/JYAMXcswOcm0qvGoE3tPb7ivBeIHTOK2DiapBhDZgacIhzhQIKU5TCfA==",
- "dev": true,
"license": "MIT"
},
"node_modules/contentful-sdk-core": {
diff --git a/package.json b/package.json
index e1c59c1ff..d65fc122d 100644
--- a/package.json
+++ b/package.json
@@ -19,15 +19,17 @@
"precommit": "npx lint-staged",
"prepare": "husky",
"postbuild": "pagefind --force-language ru --site .next/server/app/docs/ru --output-path build/_next/static/pagefind/ru && pagefind --force-language en --site .next/server/app/docs/en --output-path build/_next/static/pagefind/en",
- "contentful:prepare": "dotenv -e .env -- bash -c 'cf-content-types-generator -X -r -d -s $CONTENTFUL_SPACE_ID -t $CONTENTFUL_MANAGEMENT_TOKEN -o src/shared/types/contentful' && npx eslint src/shared/types/contentful/*.ts --fix"
+ "contentful:prepare": "dotenv -e .env -- bash -c 'cf-content-types-generator -X -r -d -s $CONTENTFUL_SPACE_ID -t $CONTENTFUL_MANAGEMENT_TOKEN -o src/shared/types/contentful' && npx eslint src/shared/types/contentful/*.ts --fix && prettier --write src/shared/types/contentful/**/*.ts"
},
"engines": {
"node": ">=20.x"
},
"dependencies": {
+ "@contentful/rich-text-react-renderer": "^16.0.1",
"@next/third-parties": "^15.2.4",
"class-variance-authority": "^0.7.1",
"classnames": "^2.5.1",
+ "contentful-resolve-response": "^1.9.3",
"dayjs": "^1.11.13",
"github-markdown-css": "^5.8.1",
"http-status": "^2.1.0",
@@ -60,6 +62,7 @@
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
"@testing-library/user-event": "^14.6.1",
+ "@types/contentful-resolve-response": "^0.1.33",
"@types/gapi.youtube": "^3.0.40",
"@types/jest": "^29.5.14",
"@types/node": "^22.15.3",
diff --git a/src/app/courses/[slug]/page.tsx b/src/app/courses/[slug]/page.tsx
new file mode 100644
index 000000000..ee23515a0
--- /dev/null
+++ b/src/app/courses/[slug]/page.tsx
@@ -0,0 +1,35 @@
+import { Metadata } from 'next';
+
+import { resolveCoursePageLocale } from '@/entities/course/helpers/resolve-course-page-locale';
+import { Course } from '@/views/course/course';
+import { coursePageStore } from '@/views/course/model/store';
+
+type Params = {
+ slug: string;
+};
+
+type CourseRouteParams = {
+ params: Promise;
+};
+
+export async function generateMetadata({ params }: CourseRouteParams): Promise {
+ const { slug } = await params;
+ const locale = resolveCoursePageLocale(slug);
+
+ const pageTitle = await coursePageStore.loadCoursePageTitle(slug, locale);
+ const title = `${pageTitle} · The Rolling Scopes School`;
+
+ return { title };
+}
+
+export async function generateStaticParams() {
+ return await coursePageStore.loadCoursePages();
+}
+
+export default async function CourseRoute({ params }: CourseRouteParams) {
+ const { slug } = await params;
+ const locale = resolveCoursePageLocale(slug);
+ const { courseName, sections, courseId } = await coursePageStore.loadCoursePage(slug, locale);
+
+ return ;
+}
diff --git a/src/app/courses/angular/page.tsx b/src/app/courses/angular/page.tsx
deleted file mode 100644
index 4b0b9d1fd..000000000
--- a/src/app/courses/angular/page.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import { Metadata } from 'next';
-
-import { getCourseTitle } from '@/shared/helpers/get-course-title';
-import { Angular } from '@/views/angular';
-import { COURSE_TITLES } from 'data';
-
-const courseName = COURSE_TITLES.ANGULAR;
-
-export async function generateMetadata(): Promise {
- return { title: await getCourseTitle(courseName) };
-}
-
-export default async function AngularRoute() {
- return ;
-}
diff --git a/src/app/courses/aws-ai/page.tsx b/src/app/courses/aws-ai/page.tsx
deleted file mode 100644
index 3551200b4..000000000
--- a/src/app/courses/aws-ai/page.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import { Metadata } from 'next';
-
-import { getCourseTitle } from '@/shared/helpers/get-course-title';
-import { AwsAI } from '@/views/aws-ai';
-import { COURSE_TITLES } from 'data';
-
-const courseName = COURSE_TITLES.AWS_AI;
-
-export async function generateMetadata(): Promise {
- return { title: await getCourseTitle(courseName) };
-}
-
-export default async function AwsDeveloperRoute() {
- return ;
-}
diff --git a/src/app/courses/aws-cloud-developer/page.tsx b/src/app/courses/aws-cloud-developer/page.tsx
deleted file mode 100644
index 5eadb392c..000000000
--- a/src/app/courses/aws-cloud-developer/page.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import { Metadata } from 'next';
-
-import { getCourseTitle } from '@/shared/helpers/get-course-title';
-import { AwsDeveloper } from '@/views/aws-developer';
-import { COURSE_TITLES } from 'data';
-
-const courseName = COURSE_TITLES.AWS_CLOUD_DEVELOPER;
-
-export async function generateMetadata(): Promise {
- return { title: await getCourseTitle(courseName) };
-}
-
-export default async function AwsDeveloperRoute() {
- return ;
-}
diff --git a/src/app/courses/aws-devops/page.tsx b/src/app/courses/aws-devops/page.tsx
deleted file mode 100644
index c2e90de07..000000000
--- a/src/app/courses/aws-devops/page.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import { Metadata } from 'next';
-
-import { getCourseTitle } from '@/shared/helpers/get-course-title';
-import { AwsDevOps } from '@/views/aws-devops';
-import { COURSE_TITLES } from 'data';
-
-const courseName = COURSE_TITLES.AWS_DEVOPS;
-
-export async function generateMetadata(): Promise {
- return { title: await getCourseTitle(courseName) };
-}
-
-export default async function AwsDeveloperRoute() {
- return ;
-}
diff --git a/src/app/courses/aws-fundamentals/page.tsx b/src/app/courses/aws-fundamentals/page.tsx
deleted file mode 100644
index 08401eef2..000000000
--- a/src/app/courses/aws-fundamentals/page.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import { Metadata } from 'next';
-
-import { getCourseTitle } from '@/shared/helpers/get-course-title';
-import { AwsFundamentals } from '@/views/aws-fundamentals';
-import { COURSE_TITLES } from 'data';
-
-const courseName = COURSE_TITLES.AWS_FUNDAMENTALS;
-
-export async function generateMetadata(): Promise {
- return { title: await getCourseTitle(courseName) };
-}
-
-export default async function AwsFundRoute() {
- return ;
-}
diff --git a/src/app/courses/javascript-preschool-ru/page.tsx b/src/app/courses/javascript-preschool-ru/page.tsx
deleted file mode 100644
index c8db69c23..000000000
--- a/src/app/courses/javascript-preschool-ru/page.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import { Metadata } from 'next';
-
-import { getCourseTitle } from '@/shared/helpers/get-course-title';
-import { JavaScriptPreSchoolRu } from '@/views/javascript-preschool-ru';
-import { COURSE_TITLES } from 'data';
-
-const courseName = COURSE_TITLES.JS_PRESCHOOL_RU;
-
-export async function generateMetadata(): Promise {
- return { title: await getCourseTitle(courseName) };
-}
-
-export default async function JsPreRoute() {
- return ;
-}
diff --git a/src/app/courses/javascript-ru/page.tsx b/src/app/courses/javascript-ru/page.tsx
deleted file mode 100644
index 04b0ab2a8..000000000
--- a/src/app/courses/javascript-ru/page.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import { Metadata } from 'next';
-
-import { getCourseTitle } from '@/shared/helpers/get-course-title';
-import { JavaScriptRu } from '@/views/javascript-ru';
-import { COURSE_TITLES } from 'data';
-
-const courseName = COURSE_TITLES.JS_RU;
-
-export async function generateMetadata(): Promise {
- return { title: await getCourseTitle(courseName) };
-}
-
-export default async function JsRuRoute() {
- return ;
-}
diff --git a/src/app/courses/javascript/page.tsx b/src/app/courses/javascript/page.tsx
deleted file mode 100644
index 9b7b8c64e..000000000
--- a/src/app/courses/javascript/page.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import { Metadata } from 'next';
-
-import { getCourseTitle } from '@/shared/helpers/get-course-title';
-import { JavaScriptEn } from '@/views/javascript-en';
-import { COURSE_TITLES } from 'data';
-
-const courseName = COURSE_TITLES.JS_EN;
-
-export async function generateMetadata(): Promise {
- return { title: await getCourseTitle(courseName) };
-}
-
-export default async function JsEnRoute() {
- return ;
-}
diff --git a/src/app/courses/nodejs/page.tsx b/src/app/courses/nodejs/page.tsx
deleted file mode 100644
index e8a88f7bf..000000000
--- a/src/app/courses/nodejs/page.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import { Metadata } from 'next';
-
-import { getCourseTitle } from '@/shared/helpers/get-course-title';
-import { Nodejs } from '@/views/nodejs';
-import { COURSE_TITLES } from 'data';
-
-const courseName = COURSE_TITLES.NODE;
-
-export async function generateMetadata(): Promise {
- return { title: await getCourseTitle(courseName) };
-}
-
-export default async function NodeRoute() {
- return ;
-}
diff --git a/src/app/courses/reactjs/page.tsx b/src/app/courses/reactjs/page.tsx
deleted file mode 100644
index 79f9215e1..000000000
--- a/src/app/courses/reactjs/page.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import { Metadata } from 'next';
-
-import { getCourseTitle } from '@/shared/helpers/get-course-title';
-import { React } from '@/views/react';
-import { COURSE_TITLES } from 'data';
-
-const courseName = COURSE_TITLES.REACT;
-
-export async function generateMetadata(): Promise {
- return { title: await getCourseTitle(courseName) };
-}
-
-export default async function ReactRoute() {
- return ;
-}
diff --git a/src/core/api/app-api.ts b/src/core/api/app-api.ts
index 014f8bbc6..cb446fd80 100644
--- a/src/core/api/app-api.ts
+++ b/src/core/api/app-api.ts
@@ -3,6 +3,7 @@ import { MentorApi } from '@/entities/mentor/api/mentor-api';
import { TrainerApi } from '@/entities/trainer/api/trainer-api';
import { ApiBaseClass } from '@/shared/api/api-base-class';
import { ApiServices } from '@/shared/types';
+import { CoursePageApi } from '@/views/course/api/course-page-api';
export class Api {
public readonly services: ApiServices;
@@ -10,6 +11,7 @@ export class Api {
public readonly trainer: TrainerApi;
public readonly course: CourseApi;
public readonly mentor: MentorApi;
+ public readonly coursePage: CoursePageApi;
constructor(
private readonly baseURI: string,
@@ -23,5 +25,6 @@ export class Api {
this.trainer = new TrainerApi(this.services);
this.course = new CourseApi(this.services);
this.mentor = new MentorApi(this.services);
+ this.coursePage = new CoursePageApi(this.services);
}
}
diff --git a/src/entities/course/api/course-api.ts b/src/entities/course/api/course-api.ts
index d572d6b93..a8f927cea 100644
--- a/src/entities/course/api/course-api.ts
+++ b/src/entities/course/api/course-api.ts
@@ -15,6 +15,17 @@ export class CourseApi {
});
}
+ public queryCourse(id: string) {
+ return this.services.rest.get('/entries', {
+ params: {
+ 'content_type': API_CONTENT_TYPE_DICTIONARY.COURSE,
+ 'include': API_MAX_INCLUDE_DEPTH,
+ 'order': 'fields.order',
+ 'sys.id': id,
+ },
+ });
+ }
+
public queryCoursesSchedule() {
return this.services.rest.get('/app/courses.json');
}
diff --git a/src/entities/course/helpers/resolve-course-page-locale.ts b/src/entities/course/helpers/resolve-course-page-locale.ts
new file mode 100644
index 000000000..3c3d6727c
--- /dev/null
+++ b/src/entities/course/helpers/resolve-course-page-locale.ts
@@ -0,0 +1,13 @@
+import { ApiResourceLocale } from '@/shared/types';
+
+/**
+ * Resolves the locale of a course page based on the given slug.
+ *
+ * @param {string} slug - The slug of the course page used to determine the locale.
+ * @return {string} The resolved locale, either 'ru' or 'en-US'.
+ */
+export function resolveCoursePageLocale(slug: string): ApiResourceLocale {
+ const isRuLocale = slug.endsWith('ru');
+
+ return isRuLocale ? 'ru' : 'en-US';
+}
diff --git a/src/entities/course/index.ts b/src/entities/course/index.ts
index 150aafa45..88d6fc691 100644
--- a/src/entities/course/index.ts
+++ b/src/entities/course/index.ts
@@ -1,4 +1,4 @@
-export type { Course, CourseItemData, CourseStatus } from './types';
+export type { ApiCoursesIds, Course, CourseItemData, CourseStatus } from './types';
export { CourseCard } from './ui/course-card/course-card';
export { CourseItem } from './ui/course-item/course-item';
export { courseStore } from './model/store';
diff --git a/src/entities/course/model/store.ts b/src/entities/course/model/store.ts
index c906a7cd1..5c0d79adb 100644
--- a/src/entities/course/model/store.ts
+++ b/src/entities/course/model/store.ts
@@ -1,5 +1,6 @@
import { syncApiCoursesSchedule } from '@/entities/course/helpers/sync-api-courses-schedule';
import { transformCourses } from '@/entities/course/helpers/transform-courses';
+import { ApiCoursesIds } from '@/entities/course/types';
import { api } from '@/shared/api/api';
class CourseStore {
@@ -18,6 +19,28 @@ class CourseStore {
throw new Error('Something went wrong fetching courses!');
}
+ public async loadCourse(id: ApiCoursesIds) {
+ // TODO: seems to be not efficient to fetch schedule every time. Maybe cache?
+
+ const [courseRes, courseSchedule] = await Promise.all([
+ api.course.queryCourse(id),
+ this.loadCoursesSchedule(),
+ ]);
+
+ if (courseRes.isSuccess) {
+ const transformedCourseData = transformCourses(courseRes.result);
+ const course = syncApiCoursesSchedule(courseSchedule, transformedCourseData).at(0);
+
+ if (!course) {
+ throw new Error('Course not found!');
+ }
+
+ return course;
+ }
+
+ throw new Error('Something went wrong fetching course!');
+ }
+
public loadCoursesSchedule = async () => {
const res = await api.course.queryCoursesSchedule();
diff --git a/src/entities/course/types.ts b/src/entities/course/types.ts
index 4d0021b62..0d7d7bc4d 100644
--- a/src/entities/course/types.ts
+++ b/src/entities/course/types.ts
@@ -2,7 +2,12 @@ import { StaticImageData } from 'next/image';
import { API_COURSES_IDS_DICTIONARY } from '@/entities/course/constants';
import { COURSE_LINKS } from '@/shared/constants';
-import { ApiResourceLocale, Language, TypeCourseSkeleton } from '@/shared/types';
+import {
+ ApiResourceLocale,
+ Language,
+ TypeCourseSkeleton,
+ TypeHomePageSkeleton,
+} from '@/shared/types';
import type { EntryCollection } from 'contentful';
import { CourseNamesKeys } from 'data';
@@ -37,6 +42,12 @@ export type CoursesResponse = EntryCollection<
ApiResourceLocale
>;
+export type CoursePageResponse = EntryCollection<
+ TypeHomePageSkeleton,
+ 'WITHOUT_UNRESOLVABLE_LINKS',
+ ApiResourceLocale
+>;
+
export type Course = {
id: string;
title: string;
diff --git a/src/entities/trainer/model/store.ts b/src/entities/trainer/model/store.ts
index 8b1303cad..52d1a8af8 100644
--- a/src/entities/trainer/model/store.ts
+++ b/src/entities/trainer/model/store.ts
@@ -1,16 +1,11 @@
-import { API_COURSES_IDS_DICTIONARY } from '@/entities/course/@x/trainer';
import { transformTrainers } from '@/entities/trainer/helpers/transform-trainers';
import { api } from '@/shared/api/api';
-import { API_LOCALE_DICTIONARY } from '@/shared/constants';
-import { Language } from '@/shared/types';
-import { CourseNamesKeys } from 'data';
+import { ApiResourceLocale } from '@/shared/types';
class TrainerStore {
- public loadTrainers = async (course: CourseNamesKeys, language: Language = 'en') => {
+ public loadTrainers = async (courseId: string, locale: ApiResourceLocale = 'en-US') => {
try {
- const courseId = API_COURSES_IDS_DICTIONARY[course];
- const locale = API_LOCALE_DICTIONARY[language];
-
+ // TODO: move trainers to course page section?
const res = await api.trainer.queryTrainers(courseId, locale);
if (res.isSuccess) {
diff --git a/src/shared/__tests__/constants.ts b/src/shared/__tests__/constants.ts
index ad07249d3..5a7d75045 100644
--- a/src/shared/__tests__/constants.ts
+++ b/src/shared/__tests__/constants.ts
@@ -6,7 +6,6 @@ import { MentorFeedback } from '@/entities/mentor';
import type { Trainer } from '@/entities/trainer';
import nodejsImg1 from '@/shared/assets/mentors/m-shylau.webp';
import { COURSE_LINKS, ROUTES } from '@/shared/constants';
-import { FaqDataItem, FaqDataItemWithLink } from '@/widgets/faq/types';
import { COURSE_TITLES } from 'data';
export const MOCKED_IMAGE_PATH: StaticImageData = {
@@ -223,8 +222,6 @@ export const MOCKED_MENTORS_FEEDBACK = {
photo: nodejsImg1,
};
-export const MOCKED_ONE_MENTORS_FEEDBACK: MentorFeedback[] = [MOCKED_MENTORS_FEEDBACK];
-
export const MOCKED_SEVERAL_MENTORS_FEEDBACK: MentorFeedback[] = Array.from(
{ length: 8 },
() => MOCKED_MENTORS_FEEDBACK,
@@ -247,50 +244,3 @@ export const MOCKED_VIDEOS: Video[] = [
thumbnail: 'thumb3.jpg',
},
];
-
-export const MOCKED_FAQ: FaqDataItem[] = [
- {
- question: 'What is The Rolling Scopes?',
- answer:
- 'The Rolling Scopes is an independent international community of developers, mainly focusing on JavaScript, Front-end, iOS, and Android.',
- },
- {
- question: 'When was The Rolling Scopes organized?',
- answer: 'The Rolling Scopes was organized in 2013.',
- },
- {
- question: 'Are The Rolling Scopes events well known?',
- answer:
- 'Yes, many developers worldwide know about and participate in their events and activities.',
- },
- {
- question: 'What is the RS School JavaScript/Front-end course?',
- answer:
- 'It is a free Front-end/JavaScript course conducted by The Rolling Scopes Community since 2013.',
- },
-];
-
-export const MOCKED_FAQ_WITH_LINKS: FaqDataItemWithLink[] = [
- {
- question: 'Where can I ask a question?',
- answer: [
- {
- id: 0,
- text: 'You can ask questions in the Discord ',
- title: 'chat',
- link: 'https://discord.gg/2Ww3TCBvz4',
- },
- ],
- },
- {
- question: 'Where does communication take place?',
- answer: [
- {
- id: 1,
- text: 'In the Discord ',
- title: 'chat',
- link: 'https://discord.gg/2Ww3TCBvz4',
- },
- ],
- },
-];
diff --git a/src/shared/constants.ts b/src/shared/constants.tsx
similarity index 63%
rename from src/shared/constants.ts
rename to src/shared/constants.tsx
index 2ab7436e9..a3b75120a 100644
--- a/src/shared/constants.ts
+++ b/src/shared/constants.tsx
@@ -1,6 +1,16 @@
+import { ReactNode } from 'react';
+import { BLOCKS, Block, INLINES, Inline } from '@contentful/rich-text-types';
+import Image from 'next/image';
+
+import { isExternalUri } from '@/shared/helpers/is-external-uri';
+import { prepareAssetImage } from '@/shared/helpers/prepare-asset-image';
import { ApiResourceLocale, Language } from '@/shared/types/';
+import { LinkCustom } from '@/shared/ui/link-custom';
+import { ContentList } from '@/shared/ui/list/content-list';
+import { ListItem } from '@/shared/ui/list/list-item';
+import { Paragraph } from '@/shared/ui/paragraph';
+import { Subtitle } from '@/shared/ui/subtitle';
-export const RS_INTRO_URL = 'https://www.youtube.com/embed/n4unZLVpnaU';
export const RS_FOUNDATION_YEAR = '2013';
export const RS_EMAIL = 'rolling.scopes@gmail.com';
export const TO_BE_DETERMINED = 'TBD';
@@ -13,6 +23,7 @@ export const YOUTUBE_API_MAX_RESULTS_PER_PAGE = 50;
* https://www.contentful.com/developers/docs/references/content-preview-api/#/reference/links
*/
export const API_MAX_INCLUDE_DEPTH = 10;
+export const API_OMIT_LINKED_ITEMS_INCLUDE_DEPTH = 0;
export const LABELS = {
START_DATE: 'Course starts on:',
@@ -63,6 +74,7 @@ export const API_LOCALE_DICTIONARY: Record = {
export const API_CONTENT_TYPE_DICTIONARY = {
TRAINER: 'contributor',
COURSE: 'course',
+ COURSE_PAGE: 'homePage',
} as const;
export const ANCHORS = {
ABOUT_COMMUNITY: 'about-community',
@@ -109,3 +121,41 @@ export const ROUTES = {
} as const;
export const SWR_CACHE_KEY = { MENTORS_PLAYLIST: 'MENTORS_PLAYLIST' };
+
+export const RICH_TEXT_OPTIONS = {
+ renderNode: {
+ [BLOCKS.PARAGRAPH]: (_node: Block | Inline, children: ReactNode) => {
+ if (!children) {
+ return null;
+ }
+
+ // hack to prevent rendering an empty trailing paragraph
+ const isEmptyNode = children.toString().trim() === '';
+
+ return isEmptyNode ? '' : {children};
+ },
+ [BLOCKS.HEADING_3]: (_node: Block | Inline, children: ReactNode) => (
+ {children}
+ ),
+ [BLOCKS.UL_LIST]: (_node: Block | Inline, children: ReactNode) => (
+ {children}
+ ),
+ [BLOCKS.OL_LIST]: (_node: Block | Inline, children: ReactNode) => (
+ {children}
+ ),
+ [BLOCKS.LIST_ITEM]: (_node: Block | Inline, children: ReactNode) => (
+ {children}
+ ),
+ [INLINES.HYPERLINK]: (node: Inline | Block, children: ReactNode) => (
+
+ {children}
+
+ ),
+ [BLOCKS.EMBEDDED_ASSET]: (node: Inline | Block) => {
+ const src = prepareAssetImage(node.data.target.fields.file);
+ const alt = node.data.target.fields.title;
+
+ return ;
+ },
+ },
+};
diff --git a/src/shared/helpers/get-course-title.ts b/src/shared/helpers/get-course-title.ts
deleted file mode 100644
index 552f04e19..000000000
--- a/src/shared/helpers/get-course-title.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { selectCourse } from '../hooks/use-course-by-title/utils/select-course';
-import { Course } from '@/entities/course';
-
-export async function getCourseTitle(courseName: Course['title']) {
- const course = await selectCourse(courseName);
-
- return `${course.title} · The Rolling Scopes School`;
-}
diff --git a/src/shared/helpers/is-external-uri.ts b/src/shared/helpers/is-external-uri.ts
new file mode 100644
index 000000000..3c4a5df10
--- /dev/null
+++ b/src/shared/helpers/is-external-uri.ts
@@ -0,0 +1,9 @@
+/**
+ * Determines if a given link is an external link.
+ *
+ * @param {string} link - The URL or path to analyze.
+ * @return {boolean} Returns true if the link starts with "http", indicating it is an external link; otherwise, false.
+ */
+export function isExternalUri(link: string) {
+ return link.startsWith('http');
+}
diff --git a/src/shared/helpers/is-training-program.ts b/src/shared/helpers/is-training-program.ts
deleted file mode 100644
index 05cc6e937..000000000
--- a/src/shared/helpers/is-training-program.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { AWS_FUNDAMENTALS_BADGE, COURSE_TITLES, TrainingProgramType } from 'data';
-
-export function isTrainingProgramType(value: string): value is TrainingProgramType {
- const trainingProgramValues: TrainingProgramType[] = [
- ...Object.values(COURSE_TITLES),
- AWS_FUNDAMENTALS_BADGE,
- ];
-
- return trainingProgramValues.includes(value as TrainingProgramType);
-}
diff --git a/src/shared/helpers/prepare-asset-image.ts b/src/shared/helpers/prepare-asset-image.ts
new file mode 100644
index 000000000..4c84ae84a
--- /dev/null
+++ b/src/shared/helpers/prepare-asset-image.ts
@@ -0,0 +1,20 @@
+import { StaticImageData } from 'next/image';
+
+import { prepareHttpsUrl } from '@/shared/helpers/prepare-https-url';
+import type { AssetFile } from 'contentful';
+
+export function prepareAssetImage(asset: AssetFile | undefined): StaticImageData {
+ if (!asset) {
+ throw new Error('Assets is not defined.');
+ }
+
+ const src = prepareHttpsUrl(asset.url);
+ const width = asset.details.image?.width ?? 0;
+ const height = asset.details.image?.height ?? 0;
+
+ return {
+ src,
+ width,
+ height,
+ };
+}
diff --git a/src/shared/helpers/prepare-contentful-response.ts b/src/shared/helpers/prepare-contentful-response.ts
new file mode 100644
index 000000000..ef5c764b0
--- /dev/null
+++ b/src/shared/helpers/prepare-contentful-response.ts
@@ -0,0 +1,11 @@
+import resolveResponse from 'contentful-resolve-response';
+
+/**
+ * Transforms the given content into a typed response by resolving it.
+ *
+ * @param {unknown} content - The content to be processed and resolved.
+ * @return {TContent} The resolved content cast to the specified type.
+ */
+export function prepareContentfulResponse(content: unknown): TContent {
+ return resolveResponse(content);
+}
diff --git a/src/shared/helpers/rich-text-renderer.ts b/src/shared/helpers/rich-text-renderer.ts
new file mode 100644
index 000000000..5d7a48c9e
--- /dev/null
+++ b/src/shared/helpers/rich-text-renderer.ts
@@ -0,0 +1,13 @@
+import { documentToReactComponents } from '@contentful/rich-text-react-renderer';
+
+import { RICH_TEXT_OPTIONS } from '@/shared/constants';
+
+type RichTextDocument = Parameters['0'];
+type RichTextOptions = Parameters['1'];
+
+export function richTextRenderer(
+ document: RichTextDocument,
+ options: RichTextOptions = RICH_TEXT_OPTIONS,
+) {
+ return documentToReactComponents(document, options);
+}
diff --git a/src/shared/types/contentful/TypeAboutCourse.ts b/src/shared/types/contentful/TypeAboutCourse.ts
new file mode 100644
index 000000000..a260e948f
--- /dev/null
+++ b/src/shared/types/contentful/TypeAboutCourse.ts
@@ -0,0 +1,81 @@
+import type { TypeAboutCourseItemSkeleton } from './TypeAboutCourseItem';
+import type {
+ ChainModifiers,
+ Entry,
+ EntryFieldTypes,
+ EntrySkeletonType,
+ LocaleCode,
+} from 'contentful';
+
+/**
+ * Fields type definition for content type 'TypeAboutCourse'
+ * @name TypeAboutCourseFields
+ * @type {TypeAboutCourseFields}
+ * @memberof TypeAboutCourse
+ */
+export interface TypeAboutCourseFields {
+ /**
+ * Field type definition for field 'title' (title)
+ * @name title
+ * @localized false
+ */
+ title: EntryFieldTypes.Symbol;
+ /**
+ * Field type definition for field 'subTitle' (subTitle)
+ * @name subTitle
+ * @localized false
+ */
+ subTitle?: EntryFieldTypes.RichText;
+ /**
+ * Field type definition for field 'gridItems' (gridItems)
+ * @name gridItems
+ * @localized false
+ */
+ gridItems: EntryFieldTypes.Array>;
+ /**
+ * Field type definition for field 'registrationLinkText' (registrationLinkText)
+ * @name registrationLinkText
+ * @localized false
+ */
+ registrationLinkText: EntryFieldTypes.Symbol;
+ /**
+ * Field type definition for field 'registrationClosedLinkText' (registrationClosedLinkText)
+ * @name registrationClosedLinkText
+ * @localized false
+ */
+ registrationClosedLinkText: EntryFieldTypes.Symbol;
+}
+
+/**
+ * Entry skeleton type definition for content type 'aboutCourse' (About Course)
+ * @name TypeAboutCourseSkeleton
+ * @type {TypeAboutCourseSkeleton}
+ * @author 1gdRTUbGl7AN0NHL83pCVK
+ * @since 2025-05-16T19:51:09.481Z
+ * @version 11
+ */
+export type TypeAboutCourseSkeleton = EntrySkeletonType;
+/**
+ * Entry type definition for content type 'aboutCourse' (About Course)
+ * @name TypeAboutCourse
+ * @type {TypeAboutCourse}
+ * @author 1gdRTUbGl7AN0NHL83pCVK
+ * @since 2025-05-16T19:51:09.481Z
+ * @version 11
+ */
+export type TypeAboutCourse<
+ Modifiers extends ChainModifiers,
+ Locales extends LocaleCode = LocaleCode,
+> = Entry;
+export type TypeAboutCourseWithoutLinkResolutionResponse =
+ TypeAboutCourse<'WITHOUT_LINK_RESOLUTION'>;
+export type TypeAboutCourseWithoutUnresolvableLinksResponse =
+ TypeAboutCourse<'WITHOUT_UNRESOLVABLE_LINKS'>;
+export type TypeAboutCourseWithAllLocalesResponse =
+ TypeAboutCourse<'WITH_ALL_LOCALES', Locales>;
+export type TypeAboutCourseWithAllLocalesAndWithoutLinkResolutionResponse<
+ Locales extends LocaleCode = LocaleCode,
+> = TypeAboutCourse<'WITHOUT_LINK_RESOLUTION' | 'WITH_ALL_LOCALES', Locales>;
+export type TypeAboutCourseWithAllLocalesAndWithoutUnresolvableLinksResponse<
+ Locales extends LocaleCode = LocaleCode,
+> = TypeAboutCourse<'WITHOUT_UNRESOLVABLE_LINKS' | 'WITH_ALL_LOCALES', Locales>;
diff --git a/src/shared/types/contentful/TypeAboutCourseItem.ts b/src/shared/types/contentful/TypeAboutCourseItem.ts
new file mode 100644
index 000000000..47a884060
--- /dev/null
+++ b/src/shared/types/contentful/TypeAboutCourseItem.ts
@@ -0,0 +1,71 @@
+import type {
+ ChainModifiers,
+ Entry,
+ EntryFieldTypes,
+ EntrySkeletonType,
+ LocaleCode,
+} from 'contentful';
+
+/**
+ * Fields type definition for content type 'TypeAboutCourseItem'
+ * @name TypeAboutCourseItemFields
+ * @type {TypeAboutCourseItemFields}
+ * @memberof TypeAboutCourseItem
+ */
+export interface TypeAboutCourseItemFields {
+ /**
+ * Field type definition for field 'heading' (heading)
+ * @name heading
+ * @localized true
+ */
+ heading: EntryFieldTypes.Symbol;
+ /**
+ * Field type definition for field 'content' (content)
+ * @name content
+ * @localized true
+ */
+ content: EntryFieldTypes.RichText;
+ /**
+ * Field type definition for field 'icon' (icon)
+ * @name icon
+ * @localized false
+ */
+ icon: EntryFieldTypes.AssetLink;
+}
+
+/**
+ * Entry skeleton type definition for content type 'aboutCourseItem' (About Course Item)
+ * @name TypeAboutCourseItemSkeleton
+ * @type {TypeAboutCourseItemSkeleton}
+ * @author 1gdRTUbGl7AN0NHL83pCVK
+ * @since 2025-05-16T19:52:39.219Z
+ * @version 11
+ */
+export type TypeAboutCourseItemSkeleton = EntrySkeletonType<
+ TypeAboutCourseItemFields,
+ 'aboutCourseItem'
+>;
+/**
+ * Entry type definition for content type 'aboutCourseItem' (About Course Item)
+ * @name TypeAboutCourseItem
+ * @type {TypeAboutCourseItem}
+ * @author 1gdRTUbGl7AN0NHL83pCVK
+ * @since 2025-05-16T19:52:39.219Z
+ * @version 11
+ */
+export type TypeAboutCourseItem<
+ Modifiers extends ChainModifiers,
+ Locales extends LocaleCode = LocaleCode,
+> = Entry;
+export type TypeAboutCourseItemWithoutLinkResolutionResponse =
+ TypeAboutCourseItem<'WITHOUT_LINK_RESOLUTION'>;
+export type TypeAboutCourseItemWithoutUnresolvableLinksResponse =
+ TypeAboutCourseItem<'WITHOUT_UNRESOLVABLE_LINKS'>;
+export type TypeAboutCourseItemWithAllLocalesResponse =
+ TypeAboutCourseItem<'WITH_ALL_LOCALES', Locales>;
+export type TypeAboutCourseItemWithAllLocalesAndWithoutLinkResolutionResponse<
+ Locales extends LocaleCode = LocaleCode,
+> = TypeAboutCourseItem<'WITHOUT_LINK_RESOLUTION' | 'WITH_ALL_LOCALES', Locales>;
+export type TypeAboutCourseItemWithAllLocalesAndWithoutUnresolvableLinksResponse<
+ Locales extends LocaleCode = LocaleCode,
+> = TypeAboutCourseItem<'WITHOUT_UNRESOLVABLE_LINKS' | 'WITH_ALL_LOCALES', Locales>;
diff --git a/src/shared/types/contentful/type-contributor.ts b/src/shared/types/contentful/TypeContributor.ts
similarity index 97%
rename from src/shared/types/contentful/type-contributor.ts
rename to src/shared/types/contentful/TypeContributor.ts
index a852b35db..99d78c182 100644
--- a/src/shared/types/contentful/type-contributor.ts
+++ b/src/shared/types/contentful/TypeContributor.ts
@@ -1,4 +1,4 @@
-import type { TypeCourseSkeleton } from './type-course';
+import type { TypeCourseSkeleton } from './TypeCourse';
import type {
ChainModifiers,
Entry,
diff --git a/src/shared/types/contentful/type-course.ts b/src/shared/types/contentful/TypeCourse.ts
similarity index 88%
rename from src/shared/types/contentful/type-course.ts
rename to src/shared/types/contentful/TypeCourse.ts
index cfaeca011..7e3f0678e 100644
--- a/src/shared/types/contentful/type-course.ts
+++ b/src/shared/types/contentful/TypeCourse.ts
@@ -83,6 +83,19 @@ export interface TypeCourseFields {
* @summary Should contain valid Hex color value.
*/
accentColor: EntryFieldTypes.Symbol;
+ /**
+ * Field type definition for field 'order' (order)
+ * @name order
+ * @localized false
+ * @summary The order in which courses are shown by default. Course with order "1" appears at the top.
+ */
+ order: EntryFieldTypes.Integer;
+ /**
+ * Field type definition for field 'scheduleUrl' (scheduleUrl)
+ * @name scheduleUrl
+ * @localized false
+ */
+ scheduleUrl?: EntryFieldTypes.Array;
}
/**
@@ -91,7 +104,7 @@ export interface TypeCourseFields {
* @type {TypeCourseSkeleton}
* @author 5yCs5AqlcAan6ySHEWFdJn
* @since 2022-02-09T19:40:33.011Z
- * @version 7
+ * @version 25
*/
export type TypeCourseSkeleton = EntrySkeletonType;
/**
@@ -100,7 +113,7 @@ export type TypeCourseSkeleton = EntrySkeletonType;
* @type {TypeCourse}
* @author 5yCs5AqlcAan6ySHEWFdJn
* @since 2022-02-09T19:40:33.011Z
- * @version 7
+ * @version 25
*/
export type TypeCourse<
Modifiers extends ChainModifiers,
diff --git a/src/shared/types/contentful/TypeHeroSection.ts b/src/shared/types/contentful/TypeHeroSection.ts
new file mode 100644
index 000000000..9355fcf04
--- /dev/null
+++ b/src/shared/types/contentful/TypeHeroSection.ts
@@ -0,0 +1,62 @@
+import type {
+ ChainModifiers,
+ Entry,
+ EntryFieldTypes,
+ EntrySkeletonType,
+ LocaleCode,
+} from 'contentful';
+
+/**
+ * Fields type definition for content type 'TypeHeroSection'
+ * @name TypeHeroSectionFields
+ * @type {TypeHeroSectionFields}
+ * @memberof TypeHeroSection
+ */
+export interface TypeHeroSectionFields {
+ /**
+ * Field type definition for field 'heading' (heading)
+ * @name heading
+ * @localized false
+ */
+ heading: EntryFieldTypes.Symbol;
+ /**
+ * Field type definition for field 'image' (image)
+ * @name image
+ * @localized false
+ */
+ image: EntryFieldTypes.AssetLink;
+}
+
+/**
+ * Entry skeleton type definition for content type 'heroSection' (Hero section)
+ * @name TypeHeroSectionSkeleton
+ * @type {TypeHeroSectionSkeleton}
+ * @author 1gdRTUbGl7AN0NHL83pCVK
+ * @since 2025-05-12T19:41:11.842Z
+ * @version 1
+ */
+export type TypeHeroSectionSkeleton = EntrySkeletonType;
+/**
+ * Entry type definition for content type 'heroSection' (Hero section)
+ * @name TypeHeroSection
+ * @type {TypeHeroSection}
+ * @author 1gdRTUbGl7AN0NHL83pCVK
+ * @since 2025-05-12T19:41:11.842Z
+ * @version 1
+ */
+export type TypeHeroSection<
+ Modifiers extends ChainModifiers,
+ Locales extends LocaleCode = LocaleCode,
+> = Entry;
+export type TypeHeroSectionWithoutLinkResolutionResponse =
+ TypeHeroSection<'WITHOUT_LINK_RESOLUTION'>;
+export type TypeHeroSectionWithoutUnresolvableLinksResponse =
+ TypeHeroSection<'WITHOUT_UNRESOLVABLE_LINKS'>;
+export type TypeHeroSectionWithAllLocalesResponse =
+ TypeHeroSection<'WITH_ALL_LOCALES', Locales>;
+export type TypeHeroSectionWithAllLocalesAndWithoutLinkResolutionResponse<
+ Locales extends LocaleCode = LocaleCode,
+> = TypeHeroSection<'WITHOUT_LINK_RESOLUTION' | 'WITH_ALL_LOCALES', Locales>;
+export type TypeHeroSectionWithAllLocalesAndWithoutUnresolvableLinksResponse<
+ Locales extends LocaleCode = LocaleCode,
+> = TypeHeroSection<'WITHOUT_UNRESOLVABLE_LINKS' | 'WITH_ALL_LOCALES', Locales>;
diff --git a/src/shared/types/contentful/type-home-page.ts b/src/shared/types/contentful/TypeHomePage.ts
similarity index 55%
rename from src/shared/types/contentful/type-home-page.ts
rename to src/shared/types/contentful/TypeHomePage.ts
index dd377a193..6b2b4373c 100644
--- a/src/shared/types/contentful/type-home-page.ts
+++ b/src/shared/types/contentful/TypeHomePage.ts
@@ -1,3 +1,8 @@
+import type { TypeAboutCourseSkeleton } from './TypeAboutCourse';
+import type { TypeCourseSkeleton } from './TypeCourse';
+import type { TypeLearningPathStagesSkeleton } from './TypeLearningPathStages';
+import type { TypeMediaTextBlockSkeleton } from './TypeMediaTextBlock';
+import type { TypeVideoBlockSkeleton } from './TypeVideoBlock';
import type {
ChainModifiers,
Entry,
@@ -14,47 +19,56 @@ import type {
*/
export interface TypeHomePageFields {
/**
- * Field type definition for field 'subtitle' (Subtitle)
- * @name Subtitle
+ * Field type definition for field 'title' (title)
+ * @name title
* @localized false
+ * @summary The title will be used in metadata title (shown in a browser's title bar)
*/
- subtitle?: EntryFieldTypes.Symbol;
+ title: EntryFieldTypes.Symbol;
/**
- * Field type definition for field 'mainTitle' (MainTitle)
- * @name MainTitle
+ * Field type definition for field 'slug' (slug)
+ * @name slug
* @localized false
+ * @summary The slug that will be used in the url bar
*/
- mainTitle?: EntryFieldTypes.Symbol;
+ slug: EntryFieldTypes.Symbol;
/**
- * Field type definition for field 'widgetTitle' (WidgetTitle)
- * @name WidgetTitle
- * @localized false
+ * Field type definition for field 'sections' (sections)
+ * @name sections
+ * @localized true
*/
- widgetTitle?: EntryFieldTypes.Symbol;
+ sections?: EntryFieldTypes.Array<
+ EntryFieldTypes.EntryLink<
+ | TypeAboutCourseSkeleton
+ | TypeLearningPathStagesSkeleton
+ | TypeMediaTextBlockSkeleton
+ | TypeVideoBlockSkeleton
+ >
+ >;
/**
- * Field type definition for field 'image' (Image)
- * @name Image
- * @localized false
+ * Field type definition for field 'course' (course)
+ * @name course
+ * @localized true
*/
- image?: EntryFieldTypes.AssetLink;
+ course: EntryFieldTypes.EntryLink;
}
/**
- * Entry skeleton type definition for content type 'homePage' (Home Page)
+ * Entry skeleton type definition for content type 'homePage' (Course Page)
* @name TypeHomePageSkeleton
* @type {TypeHomePageSkeleton}
* @author 7eBAEG99Zg1EDoAM5bOSWX
* @since 2025-03-27T06:29:32.332Z
- * @version 1
+ * @version 65
*/
export type TypeHomePageSkeleton = EntrySkeletonType;
/**
- * Entry type definition for content type 'homePage' (Home Page)
+ * Entry type definition for content type 'homePage' (Course Page)
* @name TypeHomePage
* @type {TypeHomePage}
* @author 7eBAEG99Zg1EDoAM5bOSWX
* @since 2025-03-27T06:29:32.332Z
- * @version 1
+ * @version 65
*/
export type TypeHomePage<
Modifiers extends ChainModifiers,
diff --git a/src/shared/types/contentful/TypeLearningPathStageItem.ts b/src/shared/types/contentful/TypeLearningPathStageItem.ts
new file mode 100644
index 000000000..ad9812880
--- /dev/null
+++ b/src/shared/types/contentful/TypeLearningPathStageItem.ts
@@ -0,0 +1,72 @@
+import type {
+ ChainModifiers,
+ Entry,
+ EntryFieldTypes,
+ EntrySkeletonType,
+ LocaleCode,
+} from 'contentful';
+
+/**
+ * Fields type definition for content type 'TypeLearningPathStageItem'
+ * @name TypeLearningPathStageItemFields
+ * @type {TypeLearningPathStageItemFields}
+ * @memberof TypeLearningPathStageItem
+ */
+export interface TypeLearningPathStageItemFields {
+ /**
+ * Field type definition for field 'title' (title)
+ * @name title
+ * @localized true
+ */
+ title: EntryFieldTypes.Symbol;
+ /**
+ * Field type definition for field 'content' (content)
+ * @name content
+ * @localized true
+ */
+ content: EntryFieldTypes.RichText;
+ /**
+ * Field type definition for field 'image' (image)
+ * @name image
+ * @localized true
+ */
+ image?: EntryFieldTypes.AssetLink;
+}
+
+/**
+ * Entry skeleton type definition for content type 'learningPathStageItem' (Learning Path Stage Item)
+ * @name TypeLearningPathStageItemSkeleton
+ * @type {TypeLearningPathStageItemSkeleton}
+ * @author 1gdRTUbGl7AN0NHL83pCVK
+ * @since 2025-05-18T20:24:48.422Z
+ * @version 1
+ */
+export type TypeLearningPathStageItemSkeleton = EntrySkeletonType<
+ TypeLearningPathStageItemFields,
+ 'learningPathStageItem'
+>;
+/**
+ * Entry type definition for content type 'learningPathStageItem' (Learning Path Stage Item)
+ * @name TypeLearningPathStageItem
+ * @type {TypeLearningPathStageItem}
+ * @author 1gdRTUbGl7AN0NHL83pCVK
+ * @since 2025-05-18T20:24:48.422Z
+ * @version 1
+ */
+export type TypeLearningPathStageItem<
+ Modifiers extends ChainModifiers,
+ Locales extends LocaleCode = LocaleCode,
+> = Entry;
+export type TypeLearningPathStageItemWithoutLinkResolutionResponse =
+ TypeLearningPathStageItem<'WITHOUT_LINK_RESOLUTION'>;
+export type TypeLearningPathStageItemWithoutUnresolvableLinksResponse =
+ TypeLearningPathStageItem<'WITHOUT_UNRESOLVABLE_LINKS'>;
+export type TypeLearningPathStageItemWithAllLocalesResponse<
+ Locales extends LocaleCode = LocaleCode,
+> = TypeLearningPathStageItem<'WITH_ALL_LOCALES', Locales>;
+export type TypeLearningPathStageItemWithAllLocalesAndWithoutLinkResolutionResponse<
+ Locales extends LocaleCode = LocaleCode,
+> = TypeLearningPathStageItem<'WITHOUT_LINK_RESOLUTION' | 'WITH_ALL_LOCALES', Locales>;
+export type TypeLearningPathStageItemWithAllLocalesAndWithoutUnresolvableLinksResponse<
+ Locales extends LocaleCode = LocaleCode,
+> = TypeLearningPathStageItem<'WITHOUT_UNRESOLVABLE_LINKS' | 'WITH_ALL_LOCALES', Locales>;
diff --git a/src/shared/types/contentful/TypeLearningPathStages.ts b/src/shared/types/contentful/TypeLearningPathStages.ts
new file mode 100644
index 000000000..f6803e721
--- /dev/null
+++ b/src/shared/types/contentful/TypeLearningPathStages.ts
@@ -0,0 +1,72 @@
+import type { TypeLearningPathStageItemSkeleton } from './TypeLearningPathStageItem';
+import type {
+ ChainModifiers,
+ Entry,
+ EntryFieldTypes,
+ EntrySkeletonType,
+ LocaleCode,
+} from 'contentful';
+
+/**
+ * Fields type definition for content type 'TypeLearningPathStages'
+ * @name TypeLearningPathStagesFields
+ * @type {TypeLearningPathStagesFields}
+ * @memberof TypeLearningPathStages
+ */
+export interface TypeLearningPathStagesFields {
+ /**
+ * Field type definition for field 'title' (title)
+ * @name title
+ * @localized true
+ */
+ title: EntryFieldTypes.Symbol;
+ /**
+ * Field type definition for field 'description' (description)
+ * @name description
+ * @localized true
+ */
+ description?: EntryFieldTypes.RichText;
+ /**
+ * Field type definition for field 'stages' (stages)
+ * @name stages
+ * @localized true
+ */
+ stages: EntryFieldTypes.Array>;
+}
+
+/**
+ * Entry skeleton type definition for content type 'learningPathStages' (Learning Path Stages)
+ * @name TypeLearningPathStagesSkeleton
+ * @type {TypeLearningPathStagesSkeleton}
+ * @author 1gdRTUbGl7AN0NHL83pCVK
+ * @since 2025-05-18T20:20:28.432Z
+ * @version 3
+ */
+export type TypeLearningPathStagesSkeleton = EntrySkeletonType<
+ TypeLearningPathStagesFields,
+ 'learningPathStages'
+>;
+/**
+ * Entry type definition for content type 'learningPathStages' (Learning Path Stages)
+ * @name TypeLearningPathStages
+ * @type {TypeLearningPathStages}
+ * @author 1gdRTUbGl7AN0NHL83pCVK
+ * @since 2025-05-18T20:20:28.432Z
+ * @version 3
+ */
+export type TypeLearningPathStages<
+ Modifiers extends ChainModifiers,
+ Locales extends LocaleCode = LocaleCode,
+> = Entry;
+export type TypeLearningPathStagesWithoutLinkResolutionResponse =
+ TypeLearningPathStages<'WITHOUT_LINK_RESOLUTION'>;
+export type TypeLearningPathStagesWithoutUnresolvableLinksResponse =
+ TypeLearningPathStages<'WITHOUT_UNRESOLVABLE_LINKS'>;
+export type TypeLearningPathStagesWithAllLocalesResponse =
+ TypeLearningPathStages<'WITH_ALL_LOCALES', Locales>;
+export type TypeLearningPathStagesWithAllLocalesAndWithoutLinkResolutionResponse<
+ Locales extends LocaleCode = LocaleCode,
+> = TypeLearningPathStages<'WITHOUT_LINK_RESOLUTION' | 'WITH_ALL_LOCALES', Locales>;
+export type TypeLearningPathStagesWithAllLocalesAndWithoutUnresolvableLinksResponse<
+ Locales extends LocaleCode = LocaleCode,
+> = TypeLearningPathStages<'WITHOUT_UNRESOLVABLE_LINKS' | 'WITH_ALL_LOCALES', Locales>;
diff --git a/src/shared/types/contentful/TypeMediaTextBlock.ts b/src/shared/types/contentful/TypeMediaTextBlock.ts
new file mode 100644
index 000000000..9ce3d4ec4
--- /dev/null
+++ b/src/shared/types/contentful/TypeMediaTextBlock.ts
@@ -0,0 +1,103 @@
+import type {
+ ChainModifiers,
+ Entry,
+ EntryFieldTypes,
+ EntrySkeletonType,
+ LocaleCode,
+} from 'contentful';
+
+/**
+ * Fields type definition for content type 'TypeMediaTextBlock'
+ * @name TypeMediaTextBlockFields
+ * @type {TypeMediaTextBlockFields}
+ * @memberof TypeMediaTextBlock
+ */
+export interface TypeMediaTextBlockFields {
+ /**
+ * Field type definition for field 'tag' (tag)
+ * @name tag
+ * @localized true
+ * @summary This tag field is used internally only in contenful to unique identify identical content. THIS FIELD WILL NOT BE SHOWN ON THE WEBSITE
+ */
+ tag?: EntryFieldTypes.Symbol;
+ /**
+ * Field type definition for field 'title' (title)
+ * @name title
+ * @localized true
+ */
+ title: EntryFieldTypes.Symbol;
+ /**
+ * Field type definition for field 'contentLeft' (contentLeft)
+ * @name contentLeft
+ * @localized true
+ */
+ contentLeft?: EntryFieldTypes.RichText;
+ /**
+ * Field type definition for field 'contentRight' (contentRight)
+ * @name contentRight
+ * @localized true
+ */
+ contentRight?: EntryFieldTypes.RichText;
+ /**
+ * Field type definition for field 'linkUrl' (linkUrl)
+ * @name linkUrl
+ * @localized true
+ * @summary If no link is provided the course related link to the registration page will be used as a default value
+ */
+ linkUrl?: EntryFieldTypes.Symbol;
+ /**
+ * Field type definition for field 'linkLabel' (linkLabel)
+ * @name linkLabel
+ * @localized true
+ */
+ linkLabel?: EntryFieldTypes.Symbol;
+ /**
+ * Field type definition for field 'disabledLinkLabel' (disabledLinkLabel)
+ * @name disabledLinkLabel
+ * @localized true
+ */
+ disabledLinkLabel?: EntryFieldTypes.Symbol;
+ /**
+ * Field type definition for field 'backgroundColor' (backgroundColor)
+ * @name backgroundColor
+ * @localized true
+ */
+ backgroundColor?: EntryFieldTypes.Symbol;
+}
+
+/**
+ * Entry skeleton type definition for content type 'mediaTextBlock' (Media Text Block)
+ * @name TypeMediaTextBlockSkeleton
+ * @type {TypeMediaTextBlockSkeleton}
+ * @author 1gdRTUbGl7AN0NHL83pCVK
+ * @since 2025-05-18T14:21:33.812Z
+ * @version 41
+ */
+export type TypeMediaTextBlockSkeleton = EntrySkeletonType<
+ TypeMediaTextBlockFields,
+ 'mediaTextBlock'
+>;
+/**
+ * Entry type definition for content type 'mediaTextBlock' (Media Text Block)
+ * @name TypeMediaTextBlock
+ * @type {TypeMediaTextBlock}
+ * @author 1gdRTUbGl7AN0NHL83pCVK
+ * @since 2025-05-18T14:21:33.812Z
+ * @version 41
+ */
+export type TypeMediaTextBlock<
+ Modifiers extends ChainModifiers,
+ Locales extends LocaleCode = LocaleCode,
+> = Entry;
+export type TypeMediaTextBlockWithoutLinkResolutionResponse =
+ TypeMediaTextBlock<'WITHOUT_LINK_RESOLUTION'>;
+export type TypeMediaTextBlockWithoutUnresolvableLinksResponse =
+ TypeMediaTextBlock<'WITHOUT_UNRESOLVABLE_LINKS'>;
+export type TypeMediaTextBlockWithAllLocalesResponse =
+ TypeMediaTextBlock<'WITH_ALL_LOCALES', Locales>;
+export type TypeMediaTextBlockWithAllLocalesAndWithoutLinkResolutionResponse<
+ Locales extends LocaleCode = LocaleCode,
+> = TypeMediaTextBlock<'WITHOUT_LINK_RESOLUTION' | 'WITH_ALL_LOCALES', Locales>;
+export type TypeMediaTextBlockWithAllLocalesAndWithoutUnresolvableLinksResponse<
+ Locales extends LocaleCode = LocaleCode,
+> = TypeMediaTextBlock<'WITHOUT_UNRESOLVABLE_LINKS' | 'WITH_ALL_LOCALES', Locales>;
diff --git a/src/shared/types/contentful/TypeVideoBlock.ts b/src/shared/types/contentful/TypeVideoBlock.ts
new file mode 100644
index 000000000..80b14db06
--- /dev/null
+++ b/src/shared/types/contentful/TypeVideoBlock.ts
@@ -0,0 +1,67 @@
+import type {
+ ChainModifiers,
+ Entry,
+ EntryFieldTypes,
+ EntrySkeletonType,
+ LocaleCode,
+} from 'contentful';
+
+/**
+ * Fields type definition for content type 'TypeVideoBlock'
+ * @name TypeVideoBlockFields
+ * @type {TypeVideoBlockFields}
+ * @memberof TypeVideoBlock
+ */
+export interface TypeVideoBlockFields {
+ /**
+ * Field type definition for field 'title' (title)
+ * @name title
+ * @localized true
+ */
+ title: EntryFieldTypes.Symbol;
+ /**
+ * Field type definition for field 'url' (url)
+ * @name url
+ * @localized true
+ */
+ url: EntryFieldTypes.Symbol;
+ /**
+ * Field type definition for field 'videoTitle' (videoTitle)
+ * @name videoTitle
+ * @localized true
+ */
+ videoTitle: EntryFieldTypes.Symbol;
+}
+
+/**
+ * Entry skeleton type definition for content type 'videoBlock' (Video Block)
+ * @name TypeVideoBlockSkeleton
+ * @type {TypeVideoBlockSkeleton}
+ * @author 1gdRTUbGl7AN0NHL83pCVK
+ * @since 2025-05-18T20:17:03.834Z
+ * @version 5
+ */
+export type TypeVideoBlockSkeleton = EntrySkeletonType;
+/**
+ * Entry type definition for content type 'videoBlock' (Video Block)
+ * @name TypeVideoBlock
+ * @type {TypeVideoBlock}
+ * @author 1gdRTUbGl7AN0NHL83pCVK
+ * @since 2025-05-18T20:17:03.834Z
+ * @version 5
+ */
+export type TypeVideoBlock<
+ Modifiers extends ChainModifiers,
+ Locales extends LocaleCode = LocaleCode,
+> = Entry;
+export type TypeVideoBlockWithoutLinkResolutionResponse = TypeVideoBlock<'WITHOUT_LINK_RESOLUTION'>;
+export type TypeVideoBlockWithoutUnresolvableLinksResponse =
+ TypeVideoBlock<'WITHOUT_UNRESOLVABLE_LINKS'>;
+export type TypeVideoBlockWithAllLocalesResponse =
+ TypeVideoBlock<'WITH_ALL_LOCALES', Locales>;
+export type TypeVideoBlockWithAllLocalesAndWithoutLinkResolutionResponse<
+ Locales extends LocaleCode = LocaleCode,
+> = TypeVideoBlock<'WITHOUT_LINK_RESOLUTION' | 'WITH_ALL_LOCALES', Locales>;
+export type TypeVideoBlockWithAllLocalesAndWithoutUnresolvableLinksResponse<
+ Locales extends LocaleCode = LocaleCode,
+> = TypeVideoBlock<'WITHOUT_UNRESOLVABLE_LINKS' | 'WITH_ALL_LOCALES', Locales>;
diff --git a/src/shared/types/contentful/index.ts b/src/shared/types/contentful/index.ts
index 5407274be..79af0742e 100644
--- a/src/shared/types/contentful/index.ts
+++ b/src/shared/types/contentful/index.ts
@@ -1,3 +1,23 @@
+export type {
+ TypeAboutCourse,
+ TypeAboutCourseFields,
+ TypeAboutCourseSkeleton,
+ TypeAboutCourseWithAllLocalesAndWithoutLinkResolutionResponse,
+ TypeAboutCourseWithAllLocalesAndWithoutUnresolvableLinksResponse,
+ TypeAboutCourseWithAllLocalesResponse,
+ TypeAboutCourseWithoutLinkResolutionResponse,
+ TypeAboutCourseWithoutUnresolvableLinksResponse,
+} from './TypeAboutCourse';
+export type {
+ TypeAboutCourseItem,
+ TypeAboutCourseItemFields,
+ TypeAboutCourseItemSkeleton,
+ TypeAboutCourseItemWithAllLocalesAndWithoutLinkResolutionResponse,
+ TypeAboutCourseItemWithAllLocalesAndWithoutUnresolvableLinksResponse,
+ TypeAboutCourseItemWithAllLocalesResponse,
+ TypeAboutCourseItemWithoutLinkResolutionResponse,
+ TypeAboutCourseItemWithoutUnresolvableLinksResponse,
+} from './TypeAboutCourseItem';
export type {
TypeContributor,
TypeContributorFields,
@@ -7,8 +27,7 @@ export type {
TypeContributorWithAllLocalesResponse,
TypeContributorWithoutLinkResolutionResponse,
TypeContributorWithoutUnresolvableLinksResponse,
-} from './type-contributor';
-
+} from './TypeContributor';
export type {
TypeCourse,
TypeCourseFields,
@@ -18,8 +37,17 @@ export type {
TypeCourseWithAllLocalesResponse,
TypeCourseWithoutLinkResolutionResponse,
TypeCourseWithoutUnresolvableLinksResponse,
-} from './type-course';
-
+} from './TypeCourse';
+export type {
+ TypeHeroSection,
+ TypeHeroSectionFields,
+ TypeHeroSectionSkeleton,
+ TypeHeroSectionWithAllLocalesAndWithoutLinkResolutionResponse,
+ TypeHeroSectionWithAllLocalesAndWithoutUnresolvableLinksResponse,
+ TypeHeroSectionWithAllLocalesResponse,
+ TypeHeroSectionWithoutLinkResolutionResponse,
+ TypeHeroSectionWithoutUnresolvableLinksResponse,
+} from './TypeHeroSection';
export type {
TypeHomePage,
TypeHomePageFields,
@@ -29,4 +57,44 @@ export type {
TypeHomePageWithAllLocalesResponse,
TypeHomePageWithoutLinkResolutionResponse,
TypeHomePageWithoutUnresolvableLinksResponse,
-} from './type-home-page';
+} from './TypeHomePage';
+export type {
+ TypeLearningPathStageItem,
+ TypeLearningPathStageItemFields,
+ TypeLearningPathStageItemSkeleton,
+ TypeLearningPathStageItemWithAllLocalesAndWithoutLinkResolutionResponse,
+ TypeLearningPathStageItemWithAllLocalesAndWithoutUnresolvableLinksResponse,
+ TypeLearningPathStageItemWithAllLocalesResponse,
+ TypeLearningPathStageItemWithoutLinkResolutionResponse,
+ TypeLearningPathStageItemWithoutUnresolvableLinksResponse,
+} from './TypeLearningPathStageItem';
+export type {
+ TypeLearningPathStages,
+ TypeLearningPathStagesFields,
+ TypeLearningPathStagesSkeleton,
+ TypeLearningPathStagesWithAllLocalesAndWithoutLinkResolutionResponse,
+ TypeLearningPathStagesWithAllLocalesAndWithoutUnresolvableLinksResponse,
+ TypeLearningPathStagesWithAllLocalesResponse,
+ TypeLearningPathStagesWithoutLinkResolutionResponse,
+ TypeLearningPathStagesWithoutUnresolvableLinksResponse,
+} from './TypeLearningPathStages';
+export type {
+ TypeMediaTextBlock,
+ TypeMediaTextBlockFields,
+ TypeMediaTextBlockSkeleton,
+ TypeMediaTextBlockWithAllLocalesAndWithoutLinkResolutionResponse,
+ TypeMediaTextBlockWithAllLocalesAndWithoutUnresolvableLinksResponse,
+ TypeMediaTextBlockWithAllLocalesResponse,
+ TypeMediaTextBlockWithoutLinkResolutionResponse,
+ TypeMediaTextBlockWithoutUnresolvableLinksResponse,
+} from './TypeMediaTextBlock';
+export type {
+ TypeVideoBlock,
+ TypeVideoBlockFields,
+ TypeVideoBlockSkeleton,
+ TypeVideoBlockWithAllLocalesAndWithoutLinkResolutionResponse,
+ TypeVideoBlockWithAllLocalesAndWithoutUnresolvableLinksResponse,
+ TypeVideoBlockWithAllLocalesResponse,
+ TypeVideoBlockWithoutLinkResolutionResponse,
+ TypeVideoBlockWithoutUnresolvableLinksResponse,
+} from './TypeVideoBlock';
diff --git a/src/shared/types/types.ts b/src/shared/types/types.ts
index 4b4a2ec69..cf91a30c8 100644
--- a/src/shared/types/types.ts
+++ b/src/shared/types/types.ts
@@ -2,7 +2,8 @@ import { HttpStatus } from 'http-status';
import { ApiBaseClass } from '@/shared/api/api-base-class';
import { HTTP_METHOD } from '@/shared/constants';
-import { LinkList } from '@/widgets/required/types';
+import type { BaseEntry } from 'contentful';
+import { LinkList } from 'data';
export type ListData = (string | LinkList)[] | [];
export type ListType = 'marked' | 'unmarked';
@@ -83,3 +84,6 @@ export type ApiResponseError = {
message: string;
requestId: string;
};
+
+export type ExtractSectionName =
+ TContentType['sys']['contentType']['sys']['id'];
diff --git a/src/shared/ui/list/content-list.tsx b/src/shared/ui/list/content-list.tsx
new file mode 100644
index 000000000..3fa7522e8
--- /dev/null
+++ b/src/shared/ui/list/content-list.tsx
@@ -0,0 +1,52 @@
+import { HTMLAttributes, PropsWithChildren } from 'react';
+import { type VariantProps, cva } from 'class-variance-authority';
+import classNames from 'classnames/bind';
+
+import styles from './list.module.scss';
+
+type ListProps = Pick, 'className'> &
+ VariantProps &
+ PropsWithChildren & {
+ ordered?: boolean;
+ };
+
+export const cx = classNames.bind(styles);
+
+const listVariants = cva(cx('list'), {
+ variants: {
+ size: {
+ compact: cx('compact'),
+ medium: cx('medium'),
+ },
+ type: {
+ marked: cx('marked'),
+ unmarked: cx(''),
+ },
+ },
+ defaultVariants: {
+ size: 'medium',
+ type: 'marked',
+ },
+});
+
+export const ContentList = ({ className = '', size, type, children, ordered }: ListProps) => {
+ const classNameList = listVariants({
+ size,
+ type,
+ className,
+ });
+
+ if (ordered) {
+ return (
+
+ {children}
+
+ );
+ }
+
+ return (
+
+ );
+};
diff --git a/src/shared/ui/list/list-item.module.scss b/src/shared/ui/list/list-item.module.scss
new file mode 100644
index 000000000..ffd3ad72f
--- /dev/null
+++ b/src/shared/ui/list/list-item.module.scss
@@ -0,0 +1,6 @@
+.list-item {
+ p {
+ display: inline;
+ padding: 0 !important;
+ }
+}
diff --git a/src/shared/ui/list/list-item.tsx b/src/shared/ui/list/list-item.tsx
new file mode 100644
index 000000000..a22ada5b2
--- /dev/null
+++ b/src/shared/ui/list/list-item.tsx
@@ -0,0 +1,14 @@
+import React, { PropsWithChildren } from 'react';
+import classNames from 'classnames/bind';
+
+import styles from './list-item.module.scss';
+
+export const cx = classNames.bind(styles);
+
+export const ListItem = ({ children }: PropsWithChildren) => {
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/shared/ui/text-with-link/text-with-link.tsx b/src/shared/ui/text-with-link/text-with-link.tsx
index 6045ff4d5..835f2a528 100644
--- a/src/shared/ui/text-with-link/text-with-link.tsx
+++ b/src/shared/ui/text-with-link/text-with-link.tsx
@@ -1,7 +1,7 @@
import { Fragment } from 'react/jsx-runtime';
-import { LinkCustom } from '../link-custom';
-import { LinkList } from '@/widgets/required/types';
+import { LinkCustom } from '@/shared/ui/link-custom';
+import { LinkList } from 'data';
interface TextWithLinkProps {
data: LinkList;
diff --git a/src/views/angular.tsx b/src/views/angular.tsx
deleted file mode 100644
index d828ad864..000000000
--- a/src/views/angular.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import { trainerStore } from '@/entities/trainer';
-import { ROUTES } from '@/shared/constants';
-import { AboutCourse } from '@/widgets/about-course';
-import { AngularTopics } from '@/widgets/angular-topics';
-import { Breadcrumbs } from '@/widgets/breadcrumbs';
-import { Certification } from '@/widgets/certification';
-import { Communication } from '@/widgets/communication';
-import { HeroCourse } from '@/widgets/hero-course';
-import { MentorsWantedCourse } from '@/widgets/mentors-wanted-course';
-import { Required } from '@/widgets/required';
-import { StudyPath } from '@/widgets/study-path';
-import { Trainers } from '@/widgets/trainers';
-import { TrainingProgram } from '@/widgets/training-program';
-import { CourseNames } from 'data';
-
-type AngularProps = {
- courseName: CourseNames['ANGULAR'];
-};
-
-export const Angular = async ({ courseName }: AngularProps) => {
- const trainers = await trainerStore.loadTrainers(courseName);
-
- return (
- <>
-
-
-
-
-
-
-
-
-
-
- {trainers && }
- >
- );
-};
diff --git a/src/views/aws-ai.tsx b/src/views/aws-ai.tsx
deleted file mode 100644
index c340fc599..000000000
--- a/src/views/aws-ai.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import { trainerStore } from '@/entities/trainer';
-import { getCourseLanguage } from '@/shared/helpers/get-course-language';
-import { AboutCourse } from '@/widgets/about-course';
-import { Breadcrumbs } from '@/widgets/breadcrumbs';
-import { Certification } from '@/widgets/certification';
-import { Communication } from '@/widgets/communication';
-import { HeroCourse } from '@/widgets/hero-course';
-import { Required } from '@/widgets/required';
-import { Trainers } from '@/widgets/trainers';
-import { TrainingProgram } from '@/widgets/training-program';
-import { CourseNames } from 'data';
-
-type AwsAIProps = {
- courseName: CourseNames['AWS_AI'];
-};
-
-export const AwsAI = async ({ courseName }: AwsAIProps) => {
- const language = await getCourseLanguage(courseName);
- const trainers = await trainerStore.loadTrainers(courseName, language);
-
- return (
- <>
-
-
-
-
-
-
-
- {trainers && }
- >
- );
-};
diff --git a/src/views/aws-developer.tsx b/src/views/aws-developer.tsx
deleted file mode 100644
index 7fafd6ca9..000000000
--- a/src/views/aws-developer.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import { trainerStore } from '@/entities/trainer';
-import { AboutCourse } from '@/widgets/about-course';
-import { Breadcrumbs } from '@/widgets/breadcrumbs';
-import { Certification } from '@/widgets/certification';
-import { Communication } from '@/widgets/communication';
-import { HeroCourse } from '@/widgets/hero-course';
-import { Required } from '@/widgets/required';
-import { StudyPath } from '@/widgets/study-path';
-import { Trainers } from '@/widgets/trainers';
-import { TrainingProgram } from '@/widgets/training-program';
-import { CourseNames } from 'data';
-
-type AwsDeveloperProps = {
- courseName: CourseNames['AWS_CLOUD_DEVELOPER'];
-};
-
-export const AwsDeveloper = async ({ courseName }: AwsDeveloperProps) => {
- const trainers = await trainerStore.loadTrainers(courseName);
-
- return (
- <>
-
-
-
-
-
-
-
-
- {trainers && }
- >
- );
-};
diff --git a/src/views/aws-devops.tsx b/src/views/aws-devops.tsx
deleted file mode 100644
index 02d193ed2..000000000
--- a/src/views/aws-devops.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import { trainerStore } from '@/entities/trainer';
-import { AboutCourse } from '@/widgets/about-course';
-import { Breadcrumbs } from '@/widgets/breadcrumbs';
-import { Certification } from '@/widgets/certification';
-import { Communication } from '@/widgets/communication';
-import { HeroCourse } from '@/widgets/hero-course';
-import { Required } from '@/widgets/required';
-import { Trainers } from '@/widgets/trainers';
-import { TrainingProgram } from '@/widgets/training-program';
-import { CourseNames } from 'data';
-
-type AwsDevOpsProps = {
- courseName: CourseNames['AWS_DEVOPS'];
-};
-
-export const AwsDevOps = async ({ courseName }: AwsDevOpsProps) => {
- const trainers = await trainerStore.loadTrainers(courseName);
-
- return (
- <>
-
-
-
-
-
-
-
- {trainers && }
- >
- );
-};
diff --git a/src/views/aws-fundamentals.tsx b/src/views/aws-fundamentals.tsx
deleted file mode 100644
index ecadcb39b..000000000
--- a/src/views/aws-fundamentals.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import { trainerStore } from '@/entities/trainer';
-import { AboutCourse } from '@/widgets/about-course';
-import { Breadcrumbs } from '@/widgets/breadcrumbs';
-import { Certification } from '@/widgets/certification';
-import { Communication } from '@/widgets/communication';
-import { HeroCourse } from '@/widgets/hero-course';
-import { Required } from '@/widgets/required';
-import { Trainers } from '@/widgets/trainers';
-import { TrainingProgram } from '@/widgets/training-program';
-import { CourseNames } from 'data';
-
-type AwsFundamentalsProps = {
- courseName: CourseNames['AWS_FUNDAMENTALS'];
-};
-export const AwsFundamentals = async ({ courseName }: AwsFundamentalsProps) => {
- const trainers = await trainerStore.loadTrainers(courseName);
-
- return (
- <>
-
-
-
-
-
-
-
-
- {trainers && }
- >
- );
-};
diff --git a/src/views/course/api/course-page-api.ts b/src/views/course/api/course-page-api.ts
new file mode 100644
index 000000000..7a3b7df7c
--- /dev/null
+++ b/src/views/course/api/course-page-api.ts
@@ -0,0 +1,43 @@
+import { CoursePageResponse } from '@/entities/course/types';
+import {
+ API_CONTENT_TYPE_DICTIONARY,
+ API_MAX_INCLUDE_DEPTH,
+ API_OMIT_LINKED_ITEMS_INCLUDE_DEPTH,
+} from '@/shared/constants';
+import { ApiResourceLocale, ApiServices } from '@/shared/types';
+
+export class CoursePageApi {
+ constructor(private readonly services: ApiServices) {}
+
+ public queryCoursePage(slug: string, locale: ApiResourceLocale = 'en-US') {
+ return this.services.rest.get('/entries', {
+ params: {
+ 'content_type': API_CONTENT_TYPE_DICTIONARY.COURSE_PAGE,
+ 'include': API_MAX_INCLUDE_DEPTH,
+ 'fields.slug': slug,
+ locale,
+ },
+ });
+ }
+
+ public queryCoursePageTitle(slug: string, locale: ApiResourceLocale = 'en-US') {
+ return this.services.rest.get('/entries', {
+ params: {
+ 'content_type': API_CONTENT_TYPE_DICTIONARY.COURSE_PAGE,
+ 'include': API_OMIT_LINKED_ITEMS_INCLUDE_DEPTH,
+ 'select': 'fields.title',
+ 'fields.slug': slug,
+ locale,
+ },
+ });
+ }
+
+ public queryCoursePages() {
+ return this.services.rest.get('/entries', {
+ params: {
+ content_type: API_CONTENT_TYPE_DICTIONARY.COURSE_PAGE,
+ include: API_OMIT_LINKED_ITEMS_INCLUDE_DEPTH,
+ },
+ });
+ }
+}
diff --git a/src/views/course/constants.ts b/src/views/course/constants.ts
new file mode 100644
index 000000000..626a04cea
--- /dev/null
+++ b/src/views/course/constants.ts
@@ -0,0 +1,6 @@
+export const SECTION_TYPE = {
+ ABOUT_COURSE: 'aboutCourse',
+ MEDIA_TEXT_BLOCK: 'mediaTextBlock',
+ LEARNING_PATH_STAGES: 'learningPathStages',
+ VIDEO_BLOCK: 'videoBlock',
+} as const;
diff --git a/src/views/course/course.tsx b/src/views/course/course.tsx
new file mode 100644
index 000000000..5cca49a17
--- /dev/null
+++ b/src/views/course/course.tsx
@@ -0,0 +1,34 @@
+import { courseStore } from '@/entities/course';
+import { ApiCoursesIds } from '@/entities/course/types';
+import { trainerStore } from '@/entities/trainer';
+import { ApiResourceLocale } from '@/shared/types';
+import { SectionResolver } from '@/views/course/section-resolver';
+import { Section } from '@/views/course/types';
+import { Breadcrumbs } from '@/widgets/breadcrumbs';
+import { HeroCourse } from '@/widgets/hero-course';
+import { Trainers } from '@/widgets/trainers';
+
+type CourseProps = {
+ id: ApiCoursesIds;
+ sections: Section[];
+ name: string;
+ locale: ApiResourceLocale;
+};
+
+export const Course = async ({ id, name, sections, locale }: CourseProps) => {
+ const [trainers, course] = await Promise.all([
+ trainerStore.loadTrainers(id, locale),
+ courseStore.loadCourse(id),
+ ]);
+
+ return (
+ <>
+
+
+ {sections.map((section) => (
+
+ ))}
+ {trainers && }
+ >
+ );
+};
diff --git a/src/views/course/helpers/transform-course-pages.ts b/src/views/course/helpers/transform-course-pages.ts
new file mode 100644
index 000000000..95ae2d526
--- /dev/null
+++ b/src/views/course/helpers/transform-course-pages.ts
@@ -0,0 +1,25 @@
+import { ApiCoursesIds, CoursePageResponse } from '@/entities/course/types';
+
+type CoursePage = {
+ slug: string;
+ courseId: ApiCoursesIds;
+};
+
+export function transformCoursePages(coursesResponse: CoursePageResponse): CoursePage[] {
+ return coursesResponse.items.map((courses) => {
+ const {
+ fields: { slug, course },
+ } = courses;
+
+ const courseId = course?.sys?.id;
+
+ if (!courseId) {
+ throw new Error('Course id is not defined.');
+ }
+
+ return {
+ slug,
+ courseId,
+ };
+ });
+}
diff --git a/src/views/course/helpers/transform-course-sections.ts b/src/views/course/helpers/transform-course-sections.ts
new file mode 100644
index 000000000..983fcdfe7
--- /dev/null
+++ b/src/views/course/helpers/transform-course-sections.ts
@@ -0,0 +1,41 @@
+import { ApiCoursePageResponseSections, Section } from '@/views/course/types';
+import { isAboutCourseSection, transformAboutCourseSection } from '@/widgets/about-course';
+import {
+ isLearningPathStagesSection,
+ transformLearningPathStages,
+} from '@/widgets/learning-path-stages';
+import {
+ isMediaTextBlockSection,
+ transformMediaTextBlockSection,
+} from '@/widgets/media-text-block';
+import { isVideoBlockSection, transformVideoBlockSection } from '@/widgets/video-block';
+
+export function transformCourseSections(sections: ApiCoursePageResponseSections): Section[] {
+ if (!sections) {
+ throw new Error('Unable to determine list of sections.');
+ }
+
+ return sections.map((section) => {
+ if (!section) {
+ throw new Error('Unable to determine section.');
+ }
+
+ if (isAboutCourseSection(section)) {
+ return transformAboutCourseSection(section);
+ }
+
+ if (isMediaTextBlockSection(section)) {
+ return transformMediaTextBlockSection(section);
+ }
+
+ if (isLearningPathStagesSection(section)) {
+ return transformLearningPathStages(section);
+ }
+
+ if (isVideoBlockSection(section)) {
+ return transformVideoBlockSection(section);
+ }
+
+ throw new Error('Unable to determine section type.');
+ });
+}
diff --git a/src/views/course/model/store.ts b/src/views/course/model/store.ts
new file mode 100644
index 000000000..88375365d
--- /dev/null
+++ b/src/views/course/model/store.ts
@@ -0,0 +1,61 @@
+import { CoursePageResponse } from '@/entities/course/types';
+import { api } from '@/shared/api/api';
+import { prepareContentfulResponse } from '@/shared/helpers/prepare-contentful-response';
+import { ApiResourceLocale } from '@/shared/types';
+import { transformCoursePages } from '@/views/course/helpers/transform-course-pages';
+import { transformCourseSections } from '@/views/course/helpers/transform-course-sections';
+
+class CoursePageStore {
+ public loadCoursePage = async (slug: string, locale: ApiResourceLocale = 'en-US') => {
+ const res = await api.coursePage.queryCoursePage(slug, locale);
+
+ if (res.isSuccess) {
+ const preparedData = prepareContentfulResponse(res.result);
+
+ const { title = '', sections: coursePageSections, course } = preparedData.at(0)?.fields ?? {};
+ const courseId = course?.sys?.id;
+ const sections = transformCourseSections(coursePageSections);
+
+ if (!courseId) {
+ throw new Error('Course id is not defined.');
+ }
+
+ return {
+ courseId,
+ courseName: title,
+ sections,
+ };
+ }
+
+ throw new Error('Something went wrong fetching course page!');
+ };
+
+ public loadCoursePages = async () => {
+ const res = await api.coursePage.queryCoursePages();
+
+ if (res.isSuccess) {
+ return transformCoursePages(res.result);
+ }
+
+ throw new Error('Something went wrong fetching all course pages!');
+ };
+
+ public loadCoursePageTitle = async (slug: string, locale: ApiResourceLocale = 'en-US') => {
+ const res = await api.coursePage.queryCoursePageTitle(slug, locale);
+
+ if (res.isSuccess) {
+ const preparedData = prepareContentfulResponse(res.result);
+ const title = preparedData.at(0)?.fields?.title;
+
+ if (!title) {
+ throw new Error('Course page title is not defined.');
+ }
+
+ return title;
+ }
+
+ throw new Error('Something went wrong fetching course page title!');
+ };
+}
+
+export const coursePageStore = new CoursePageStore();
diff --git a/src/views/course/section-resolver.tsx b/src/views/course/section-resolver.tsx
new file mode 100644
index 000000000..652ea0074
--- /dev/null
+++ b/src/views/course/section-resolver.tsx
@@ -0,0 +1,68 @@
+import { SECTION_TYPE } from '@/views/course/constants';
+import { Section } from '@/views/course/types';
+import { AboutCourseSection } from '@/widgets/about-course';
+import { LearningPathStageItem, LearningPathStages } from '@/widgets/learning-path-stages';
+import { MediaTextBlock } from '@/widgets/media-text-block';
+import { VideoBlock } from '@/widgets/video-block';
+
+type SectionResolverProps = {
+ section: Section;
+ courseEnrollUrl: string | null;
+};
+
+export const SectionResolver = async ({ courseEnrollUrl, section }: SectionResolverProps) => {
+ const sectionName = section.name;
+
+ switch (sectionName) {
+ case SECTION_TYPE.ABOUT_COURSE:
+ return (
+
+ );
+
+ case SECTION_TYPE.MEDIA_TEXT_BLOCK:
+ return (
+
+ );
+
+ case SECTION_TYPE.LEARNING_PATH_STAGES:
+ return (
+
+ {section.data.stages.map((stage, index) => (
+
+ ))}
+
+ );
+
+ case SECTION_TYPE.VIDEO_BLOCK:
+ return (
+
+ );
+
+ default:
+ throw new Error(`No component found for section type: ${sectionName}`);
+ }
+};
diff --git a/src/views/course/types.ts b/src/views/course/types.ts
new file mode 100644
index 000000000..cf3267306
--- /dev/null
+++ b/src/views/course/types.ts
@@ -0,0 +1,42 @@
+import { CoursePageResponse } from '@/entities/course/types';
+import {
+ TypeAboutCourseWithAllLocalesAndWithoutLinkResolutionResponse,
+ TypeLearningPathStagesWithAllLocalesAndWithoutLinkResolutionResponse,
+ TypeMediaTextBlockWithAllLocalesAndWithoutLinkResolutionResponse,
+ TypeVideoBlockWithAllLocalesAndWithoutLinkResolutionResponse,
+} from '@/shared/types/contentful';
+import { ExtractSectionName } from '@/shared/types/types';
+import { AboutCourseSectionData } from '@/widgets/about-course';
+import { LearningPathStagesSectionData } from '@/widgets/learning-path-stages';
+import { MediaTextBlockSectionData } from '@/widgets/media-text-block';
+import { VideoBlockSectionData } from '@/widgets/video-block';
+
+export type SectionName =
+ | ExtractSectionName
+ | ExtractSectionName
+ | ExtractSectionName
+ | ExtractSectionName;
+
+export type Section =
+ | {
+ id: string;
+ name: Extract;
+ data: AboutCourseSectionData;
+ }
+ | {
+ id: string;
+ name: Extract;
+ data: MediaTextBlockSectionData;
+ }
+ | {
+ id: string;
+ name: Extract;
+ data: LearningPathStagesSectionData;
+ }
+ | {
+ id: string;
+ name: Extract;
+ data: VideoBlockSectionData;
+ };
+
+export type ApiCoursePageResponseSections = CoursePageResponse['items'][0]['fields']['sections'];
diff --git a/src/views/courses.tsx b/src/views/courses.tsx
index caa08567b..55d508057 100644
--- a/src/views/courses.tsx
+++ b/src/views/courses.tsx
@@ -11,7 +11,7 @@ export const Courses = () => {
-
+
>
);
diff --git a/src/views/javascript-en.tsx b/src/views/javascript-en.tsx
deleted file mode 100644
index 7366f0e33..000000000
--- a/src/views/javascript-en.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import { trainerStore } from '@/entities/trainer';
-import { ROUTES } from '@/shared/constants';
-import { getCourseLanguage } from '@/shared/helpers/get-course-language';
-import { AboutCourse } from '@/widgets/about-course';
-import { AboutVideo } from '@/widgets/about-video';
-import { Breadcrumbs } from '@/widgets/breadcrumbs';
-import { Certification } from '@/widgets/certification';
-import { Communication } from '@/widgets/communication';
-import { HeroCourse } from '@/widgets/hero-course';
-import { MentorsWanted } from '@/widgets/mentors-wanted';
-import { Required } from '@/widgets/required';
-import { StudyPath } from '@/widgets/study-path';
-import { Trainers } from '@/widgets/trainers';
-import { TrainingProgram } from '@/widgets/training-program';
-import { CourseNames } from 'data';
-
-type JavaScriptEnProps = {
- courseName: CourseNames['JS_EN'];
-};
-export const JavaScriptEn = async ({ courseName }: JavaScriptEnProps) => {
- const language = await getCourseLanguage(courseName);
- const trainers = await trainerStore.loadTrainers(courseName, language);
-
- return (
- <>
-
-
-
-
-
-
-
-
-
-
- {trainers && }
- >
- );
-};
diff --git a/src/views/javascript-preschool-ru.tsx b/src/views/javascript-preschool-ru.tsx
deleted file mode 100644
index 399cf0300..000000000
--- a/src/views/javascript-preschool-ru.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import { trainerStore } from '@/entities/trainer';
-import { getCourseLanguage } from '@/shared/helpers/get-course-language';
-import { AboutCourse } from '@/widgets/about-course';
-import { Breadcrumbs } from '@/widgets/breadcrumbs';
-import { Communication } from '@/widgets/communication';
-import { Faq } from '@/widgets/faq';
-import { HeroCourse } from '@/widgets/hero-course';
-import { Required } from '@/widgets/required';
-import { Trainers } from '@/widgets/trainers';
-import { TrainingProgram } from '@/widgets/training-program';
-import { CourseNames, preschoolFaqData } from 'data';
-
-type JavaScriptPreSchoolRuProps = {
- courseName: CourseNames['JS_PRESCHOOL_RU'];
-};
-
-export const JavaScriptPreSchoolRu = async ({ courseName }: JavaScriptPreSchoolRuProps) => {
- const language = await getCourseLanguage(courseName);
- const trainers = await trainerStore.loadTrainers(courseName, language);
-
- return (
- <>
-
-
-
-
-
-
-
- {trainers && }
- >
- );
-};
diff --git a/src/views/javascript-ru.tsx b/src/views/javascript-ru.tsx
deleted file mode 100644
index 5206c666a..000000000
--- a/src/views/javascript-ru.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-import { trainerStore } from '@/entities/trainer';
-import { ROUTES } from '@/shared/constants';
-import { getCourseLanguage } from '@/shared/helpers/get-course-language';
-import { AboutCourse } from '@/widgets/about-course';
-import { AboutVideo } from '@/widgets/about-video';
-import { Breadcrumbs } from '@/widgets/breadcrumbs';
-import { Certification } from '@/widgets/certification';
-import { Communication } from '@/widgets/communication';
-import { HeroCourse } from '@/widgets/hero-course';
-import { MentorsWanted } from '@/widgets/mentors-wanted';
-import { Required } from '@/widgets/required';
-import { StudyPath } from '@/widgets/study-path';
-import { Trainers } from '@/widgets/trainers';
-import { TrainingProgram } from '@/widgets/training-program';
-import { CourseNames } from 'data';
-
-type JavaScriptRuProps = {
- courseName: CourseNames['JS_RU'];
-};
-
-export const JavaScriptRu = async ({ courseName }: JavaScriptRuProps) => {
- const language = await getCourseLanguage(courseName);
- const trainers = await trainerStore.loadTrainers(courseName, language);
-
- return (
- <>
-
-
-
-
-
-
-
-
-
-
- {trainers && }
- >
- );
-};
diff --git a/src/views/nodejs.tsx b/src/views/nodejs.tsx
deleted file mode 100644
index 550fe0d7f..000000000
--- a/src/views/nodejs.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import { trainerStore } from '@/entities/trainer';
-import { AboutCourse } from '@/widgets/about-course';
-import { Breadcrumbs } from '@/widgets/breadcrumbs';
-import { Certification } from '@/widgets/certification';
-import { Communication } from '@/widgets/communication';
-import { HeroCourse } from '@/widgets/hero-course';
-import { Required } from '@/widgets/required';
-import { Trainers } from '@/widgets/trainers';
-import { TrainingProgram } from '@/widgets/training-program';
-import { CourseNames } from 'data';
-
-type NodejsProps = {
- courseName: CourseNames['NODE'];
-};
-
-export const Nodejs = async ({ courseName }: NodejsProps) => {
- const trainers = await trainerStore.loadTrainers(courseName);
-
- return (
- <>
-
-
-
-
-
-
-
- {trainers && }
- >
- );
-};
diff --git a/src/views/react.tsx b/src/views/react.tsx
deleted file mode 100644
index 82b70a4de..000000000
--- a/src/views/react.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import { trainerStore } from '@/entities/trainer';
-import { AboutCourse } from '@/widgets/about-course';
-import { Breadcrumbs } from '@/widgets/breadcrumbs';
-import { Certification } from '@/widgets/certification';
-import { Communication } from '@/widgets/communication';
-import { HeroCourse } from '@/widgets/hero-course';
-import { Trainers } from '@/widgets/trainers';
-import { TrainingProgram } from '@/widgets/training-program';
-import { CourseNames } from 'data';
-
-type ReactProps = {
- courseName: CourseNames['REACT'];
-};
-
-export const React = async ({ courseName }: ReactProps) => {
- const trainers = await trainerStore.loadTrainers(courseName);
-
- return (
- <>
-
-
-
-
-
-
- {trainers && }
- >
- );
-};
diff --git a/src/widgets/about-course/helpers/is-about-course-section.ts b/src/widgets/about-course/helpers/is-about-course-section.ts
new file mode 100644
index 000000000..fc8d8f6b1
--- /dev/null
+++ b/src/widgets/about-course/helpers/is-about-course-section.ts
@@ -0,0 +1,14 @@
+import { TypeAboutCourseWithoutUnresolvableLinksResponse } from '@/shared/types/contentful';
+import type { BaseEntry } from 'contentful';
+
+/**
+ * Determines if the provided section is of the content type 'aboutCourse'.
+ *
+ * @param section - The section to be checked, constrained to extend BaseEntry.
+ * @return Returns true if the section is of the type 'aboutCourse', otherwise false.
+ */
+export function isAboutCourseSection(
+ section: TSection,
+): section is Extract {
+ return section.sys.contentType.sys.id === 'aboutCourse';
+}
diff --git a/src/widgets/about-course/helpers/transform-about-course-section.ts b/src/widgets/about-course/helpers/transform-about-course-section.ts
new file mode 100644
index 000000000..5b77a349b
--- /dev/null
+++ b/src/widgets/about-course/helpers/transform-about-course-section.ts
@@ -0,0 +1,28 @@
+import { richTextRenderer } from '@/shared/helpers/rich-text-renderer';
+import { TypeAboutCourseWithoutUnresolvableLinksResponse } from '@/shared/types/contentful';
+import { Section } from '@/views/course/types';
+import { transformGridItems } from '@/widgets/about-course/helpers/transform-grid-items';
+
+export function transformAboutCourseSection(
+ section: TypeAboutCourseWithoutUnresolvableLinksResponse,
+): Section {
+ const id = section.sys.id;
+ const name = section.sys.contentType.sys.id;
+ const title = section.fields.title;
+ const subTitle = section.fields.subTitle ? richTextRenderer(section.fields.subTitle) : undefined;
+ const gridItems = transformGridItems(section.fields.gridItems);
+ const registrationLinkText = section.fields.registrationLinkText;
+ const registrationClosedLinkText = section.fields.registrationClosedLinkText;
+
+ return {
+ id,
+ name,
+ data: {
+ title,
+ subTitle,
+ gridItems,
+ registrationLinkText,
+ registrationClosedLinkText,
+ },
+ };
+}
diff --git a/src/widgets/about-course/helpers/transform-grid-items.ts b/src/widgets/about-course/helpers/transform-grid-items.ts
new file mode 100644
index 000000000..81e10fff0
--- /dev/null
+++ b/src/widgets/about-course/helpers/transform-grid-items.ts
@@ -0,0 +1,25 @@
+import { prepareAssetImage } from '@/shared/helpers/prepare-asset-image';
+import { richTextRenderer } from '@/shared/helpers/rich-text-renderer';
+import { TypeAboutCourseItemWithoutUnresolvableLinksResponse } from '@/shared/types/contentful';
+import { GridItem } from '@/widgets/about-course/types';
+
+export function transformGridItems(
+ items: (TypeAboutCourseItemWithoutUnresolvableLinksResponse | undefined)[],
+): GridItem[] {
+ return items.map((item): GridItem => {
+ if (!item) {
+ throw new Error('Grid item is not defined.');
+ }
+
+ const heading = item.fields.heading;
+ const content = richTextRenderer(item.fields.content);
+ const icon = prepareAssetImage(item.fields.icon?.fields.file);
+
+ return {
+ id: heading,
+ heading,
+ content,
+ icon,
+ };
+ });
+}
diff --git a/src/widgets/about-course/index.ts b/src/widgets/about-course/index.ts
index 93a00950f..f54dee969 100644
--- a/src/widgets/about-course/index.ts
+++ b/src/widgets/about-course/index.ts
@@ -1 +1,4 @@
-export { AboutCourse } from './ui/about-course/about-course';
+export type { AboutCourseSectionData } from './types';
+export { AboutCourseSection } from '@/widgets/about-course/ui/about-course-section/about-course-section';
+export { isAboutCourseSection } from './helpers/is-about-course-section';
+export { transformAboutCourseSection } from './helpers/transform-about-course-section';
diff --git a/src/widgets/about-course/types.ts b/src/widgets/about-course/types.ts
new file mode 100644
index 000000000..f65823bfa
--- /dev/null
+++ b/src/widgets/about-course/types.ts
@@ -0,0 +1,17 @@
+import { ReactNode } from 'react';
+import { StaticImageData } from 'next/image';
+
+export type GridItem = {
+ id: string;
+ heading: string;
+ content: ReactNode;
+ icon: StaticImageData;
+};
+
+export type AboutCourseSectionData = {
+ title: string;
+ subTitle?: ReactNode;
+ gridItems: GridItem[];
+ registrationLinkText?: string;
+ registrationClosedLinkText?: string;
+};
diff --git a/src/widgets/about-course/ui/about-course-grid/about-course-grid.module.scss b/src/widgets/about-course/ui/about-course-grid/about-course-grid.module.scss
deleted file mode 100644
index a356554c7..000000000
--- a/src/widgets/about-course/ui/about-course-grid/about-course-grid.module.scss
+++ /dev/null
@@ -1,66 +0,0 @@
-.about-course-grid {
- display: grid;
- grid-template-areas:
- 'a a b'
- 'c d e';
- grid-template-columns: repeat(3, 1fr);
- gap: 32px 30px;
-
- margin-bottom: 16px;
-
- .grid-item {
- display: flex;
- flex-direction: column;
- gap: 18px;
-
- width: 100%;
- padding: 24px;
- border: 1px solid hsla(from $color-yellow h s l/ $opacity-8);
- border-radius: 12px;
-
- background-color: $color-yellow-50;
-
- .item-title {
- display: flex;
- gap: 16px;
- align-items: center;
-
- .grid-icon {
- width: auto;
- max-width: 50px;
- height: auto;
- max-height: 40px;
- }
- }
-
- &:nth-child(1) {
- grid-area: a;
- }
-
- &:nth-child(2) {
- grid-area: b;
- }
-
- &:nth-child(3) {
- grid-area: c;
- }
-
- &:nth-child(4) {
- grid-area: d;
- }
-
- &:nth-child(5) {
- grid-area: e;
- }
- }
-
- @include media-laptop {
- display: flex;
- flex-direction: column;
- gap: 24px;
- }
-
- @include media-tablet {
- gap: 16px;
- }
-}
diff --git a/src/widgets/about-course/ui/about-course-grid/about-course-grid.test.tsx b/src/widgets/about-course/ui/about-course-grid/about-course-grid.test.tsx
deleted file mode 100644
index b4b01fc78..000000000
--- a/src/widgets/about-course/ui/about-course-grid/about-course-grid.test.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import { render, screen } from '@testing-library/react';
-import { describe, expect, it } from 'vitest';
-
-import { AboutCourseGrid } from './about-course-grid';
-import { MOCKED_IMAGE_PATH } from '@/shared/__tests__/constants';
-
-const mockedData = [
- {
- id: 1,
- title: 'Title 1',
- info: 'Info 1',
- icon: MOCKED_IMAGE_PATH,
- alt: '',
- },
- {
- id: 2,
- title: 'Title 2',
- info: 'Info 2',
- icon: MOCKED_IMAGE_PATH,
- alt: '',
- },
-];
-
-describe('AboutCourseGrid component', () => {
- let itemElements: HTMLElement[];
-
- beforeEach(() => {
- render();
- itemElements = screen.getAllByTestId('about-course-grid-item');
- });
-
- it('renders correct number of items', () => {
- expect(itemElements).toHaveLength(mockedData.length);
- });
-
- it.each([[mockedData[0]], [mockedData[1]]])(
- 'should render title, info and image with its attributes of %o grid-item',
- (o) => {
- const title = screen.getByText(o.title);
- const info = screen.getByText(o.info);
- const images = screen.getAllByTestId('grid-icon');
-
- expect(title).toBeVisible();
- expect(info).toBeVisible();
-
- images.forEach((image) => {
- expect(image).toBeVisible();
- expect(image).toHaveAttribute('src', o.icon.src);
- expect(image).toHaveAttribute('alt', o.alt);
- });
- },
- );
-});
diff --git a/src/widgets/about-course/ui/about-course-grid/about-course-grid.tsx b/src/widgets/about-course/ui/about-course-grid/about-course-grid.tsx
deleted file mode 100644
index 36cc5119d..000000000
--- a/src/widgets/about-course/ui/about-course-grid/about-course-grid.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import classNames from 'classnames/bind';
-import Image from 'next/image';
-
-import { Paragraph } from '@/shared/ui/paragraph';
-import { Subtitle } from '@/shared/ui/subtitle';
-import type { AboutCourseInfo } from 'data';
-
-import styles from './about-course-grid.module.scss';
-
-export const cx = classNames.bind(styles);
-
-type AboutCourseGridProps = {
- items: AboutCourseInfo[];
-};
-
-export const AboutCourseGrid = ({ items }: AboutCourseGridProps) => {
- return (
-
- {items.map(({ id, title, info, icon }) => (
-
-
- {typeof info === 'string' ? {info} : info}
-
- ))}
-
- );
-};
diff --git a/src/widgets/about-course/ui/about-course-section/about-course-section.module.scss b/src/widgets/about-course/ui/about-course-section/about-course-section.module.scss
new file mode 100644
index 000000000..6eebfde5f
--- /dev/null
+++ b/src/widgets/about-course/ui/about-course-section/about-course-section.module.scss
@@ -0,0 +1,26 @@
+.about-course {
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+
+ .about-course-grid {
+ display: grid;
+ grid-template-areas:
+ 'a a b'
+ 'c d e';
+ grid-template-columns: repeat(3, 1fr);
+ gap: 32px 30px;
+
+ margin-bottom: 16px;
+
+ @include media-laptop {
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+ }
+
+ @include media-tablet {
+ gap: 16px;
+ }
+ }
+}
diff --git a/src/widgets/about-course/ui/about-course-section/about-course-section.tsx b/src/widgets/about-course/ui/about-course-section/about-course-section.tsx
new file mode 100644
index 000000000..7b2a06d3a
--- /dev/null
+++ b/src/widgets/about-course/ui/about-course-section/about-course-section.tsx
@@ -0,0 +1,50 @@
+import classNames from 'classnames/bind';
+
+import { LinkCustom } from '@/shared/ui/link-custom';
+import { Paragraph } from '@/shared/ui/paragraph';
+import { WidgetTitle } from '@/shared/ui/widget-title';
+import { AboutCourseSectionData } from '@/widgets/about-course';
+import { GridItem } from '@/widgets/about-course/ui/grid-item/grid-item';
+
+import styles from './about-course-section.module.scss';
+
+export const cx = classNames.bind(styles);
+
+type AboutCourseSectionProps = AboutCourseSectionData & {
+ enrollUrl: string | null;
+};
+
+export const AboutCourseSection = async ({
+ enrollUrl,
+ title,
+ subTitle,
+ gridItems,
+ registrationClosedLinkText,
+ registrationLinkText,
+}: AboutCourseSectionProps) => {
+ const linkText = enrollUrl ? registrationLinkText : registrationClosedLinkText;
+ const enrollHref = enrollUrl ?? '';
+
+ return (
+
+
+
{title}
+ {subTitle &&
{subTitle}}
+
+ {gridItems.map(({ id, heading, content, icon }) => (
+
+ ))}
+
+
+ {linkText}
+
+
+
+ );
+};
diff --git a/src/widgets/about-course/ui/about-course/about-course.module.scss b/src/widgets/about-course/ui/about-course/about-course.module.scss
deleted file mode 100644
index 104b7f88b..000000000
--- a/src/widgets/about-course/ui/about-course/about-course.module.scss
+++ /dev/null
@@ -1,5 +0,0 @@
-.about-course {
- display: flex;
- flex-direction: column;
- gap: 24px;
-}
diff --git a/src/widgets/about-course/ui/about-course/about-course.test.tsx b/src/widgets/about-course/ui/about-course/about-course.test.tsx
deleted file mode 100644
index fdbb6b99d..000000000
--- a/src/widgets/about-course/ui/about-course/about-course.test.tsx
+++ /dev/null
@@ -1,90 +0,0 @@
-import { screen } from '@testing-library/react';
-import { beforeEach } from 'vitest';
-
-import { AboutCourse } from './about-course';
-import { mockedCourses } from '@/shared/__tests__/constants';
-import { renderWithRouter } from '@/shared/__tests__/utils';
-import { REGISTRATION_WILL_OPEN_SOON, REGISTRATION_WILL_OPEN_SOON_RU } from '@/shared/constants';
-import { COURSE_TITLES } from 'data';
-
-const mockReactCourse = mockedCourses.find((course) => course.title === COURSE_TITLES.REACT);
-const mockCourseWithoutLink = mockedCourses.find(
- (course) => course.title === COURSE_TITLES.AWS_DEVOPS,
-)!;
-
-describe('AboutCourse component', () => {
- describe('render with "react" props', () => {
- beforeEach(async () => {
- const widget = await AboutCourse({ courseName: COURSE_TITLES.REACT });
-
- renderWithRouter(widget);
- });
-
- it('renders correct content for component', async () => {
- const title = await screen.findByTestId('widget-title');
- const aboutCourseGrid = await screen.findByTestId('about-course-grid');
-
- expect(title).toBeVisible();
- expect(title.textContent).toBe('About the course');
-
- expect(aboutCourseGrid).toBeVisible();
- expect(aboutCourseGrid.children).toHaveLength(4);
- });
-
- it('renders "Become a student" button with correct href when courseName is "react"', async () => {
- const button = await screen.findByRole('link', { name: /Become a student/i });
-
- expect(button).toHaveAttribute('href', `/${mockReactCourse?.enroll}`);
- });
- });
-
- describe('render 5 grid-items with "aws-devops" props', () => {
- it('render 5 grid-items with "aws-devops" props', async () => {
- const widget = await AboutCourse({ courseName: COURSE_TITLES.AWS_DEVOPS });
-
- renderWithRouter(widget);
- const aboutCourseGrid = await screen.findByTestId('about-course-grid');
-
- expect(aboutCourseGrid).toBeVisible();
- expect(aboutCourseGrid.children).toHaveLength(5);
- });
- });
-
- describe('render "Paragraph" with "js / front-end pre-school ru" props', () => {
- it("renders 'Paragraph' and its' content", async () => {
- const widget = await AboutCourse({ courseName: COURSE_TITLES.JS_PRESCHOOL_RU });
-
- renderWithRouter(widget);
-
- const paragraph = await screen.findByText(/Подготовительный этап поможет тем/i);
-
- expect(paragraph).toBeVisible();
- });
- });
-
- describe('Render widget with empty link', () => {
- it('renders registration will open soon with correct label and href', async () => {
- const widget = await AboutCourse({ courseName: mockCourseWithoutLink.title });
-
- renderWithRouter(widget);
-
- const buttonElement = screen.getByText(REGISTRATION_WILL_OPEN_SOON);
-
- expect(buttonElement).toBeVisible();
- expect(buttonElement).toHaveAttribute('href', '/');
- expect(buttonElement).toHaveTextContent(REGISTRATION_WILL_OPEN_SOON);
- });
-
- it('renders registration will open soon in russian with correct label and href', async () => {
- const widget = await AboutCourse({ courseName: COURSE_TITLES.JS_RU });
-
- renderWithRouter(widget);
-
- const buttonElement = screen.getByText(REGISTRATION_WILL_OPEN_SOON_RU);
-
- expect(buttonElement).toBeVisible();
- expect(buttonElement).toHaveAttribute('href', '/');
- expect(buttonElement).toHaveTextContent(REGISTRATION_WILL_OPEN_SOON_RU);
- });
- });
-});
diff --git a/src/widgets/about-course/ui/about-course/about-course.tsx b/src/widgets/about-course/ui/about-course/about-course.tsx
deleted file mode 100644
index 9b7f6a6c5..000000000
--- a/src/widgets/about-course/ui/about-course/about-course.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-import classNames from 'classnames/bind';
-
-import { AboutCourseGrid } from '../about-course-grid/about-course-grid';
-import { Course } from '@/entities/course';
-import { selectCourse } from '@/shared/hooks/use-course-by-title/utils/select-course';
-import { LinkCustom } from '@/shared/ui/link-custom';
-import { Paragraph } from '@/shared/ui/paragraph';
-import { WidgetTitle } from '@/shared/ui/widget-title';
-import { CourseNamesKeys, contentMapAbout, introLocalizedContent } from 'data';
-
-import styles from './about-course.module.scss';
-
-export const cx = classNames.bind(styles);
-
-type AboutCourseProps = {
- courseName: Course['title'];
-};
-
-export const AboutCourse = async ({ courseName }: AboutCourseProps) => {
- // FIXME: refactor types to not rely on course names, since now they can be changed in contentful
-
- const course = await selectCourse(courseName);
- const courseInfoList = contentMapAbout[courseName as CourseNamesKeys];
- const registrationLinkText = course.enroll
- ? introLocalizedContent[courseName as CourseNamesKeys]?.linkLabel
- : introLocalizedContent[courseName as CourseNamesKeys]?.noLinkLabel;
- const enrollHref = course.enroll ?? '';
-
- return (
-
-
-
{introLocalizedContent[courseName as CourseNamesKeys].title}
- {introLocalizedContent[courseName as CourseNamesKeys]?.paragraph && (
-
{introLocalizedContent[courseName as CourseNamesKeys].paragraph}
- )}
-
-
- {registrationLinkText}
-
-
-
- );
-};
diff --git a/src/widgets/about-course/ui/grid-item/grid-item.module.scss b/src/widgets/about-course/ui/grid-item/grid-item.module.scss
new file mode 100644
index 000000000..ab2ecdd17
--- /dev/null
+++ b/src/widgets/about-course/ui/grid-item/grid-item.module.scss
@@ -0,0 +1,45 @@
+.grid-item {
+ display: flex;
+ flex-direction: column;
+ gap: 18px;
+
+ width: 100%;
+ padding: 24px;
+ border: 1px solid hsla(from $color-yellow h s l/ $opacity-8);
+ border-radius: 12px;
+
+ background-color: $color-yellow-50;
+
+ .item-title {
+ display: flex;
+ gap: 16px;
+ align-items: center;
+
+ .grid-icon {
+ width: auto;
+ max-width: 50px;
+ height: auto;
+ max-height: 40px;
+ }
+ }
+
+ &:nth-child(1) {
+ grid-area: a;
+ }
+
+ &:nth-child(2) {
+ grid-area: b;
+ }
+
+ &:nth-child(3) {
+ grid-area: c;
+ }
+
+ &:nth-child(4) {
+ grid-area: d;
+ }
+
+ &:nth-child(5) {
+ grid-area: e;
+ }
+}
diff --git a/src/widgets/about-course/ui/grid-item/grid-item.tsx b/src/widgets/about-course/ui/grid-item/grid-item.tsx
new file mode 100644
index 000000000..367182912
--- /dev/null
+++ b/src/widgets/about-course/ui/grid-item/grid-item.tsx
@@ -0,0 +1,28 @@
+import classNames from 'classnames/bind';
+import Image from 'next/image';
+
+import { Subtitle } from '@/shared/ui/subtitle';
+import { GridItem as TGridItem } from '@/widgets/about-course/types';
+
+import styles from './grid-item.module.scss';
+
+export const cx = classNames.bind(styles);
+
+export const GridItem = ({ heading, content, icon }: TGridItem) => {
+ return (
+
+
+ {content}
+
+ );
+};
diff --git a/src/widgets/about-video/index.ts b/src/widgets/about-video/index.ts
deleted file mode 100644
index b3991d44f..000000000
--- a/src/widgets/about-video/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { AboutVideo } from './ui/about-video';
diff --git a/src/widgets/about-video/ui/about-video.test.tsx b/src/widgets/about-video/ui/about-video.test.tsx
deleted file mode 100644
index c24e146d7..000000000
--- a/src/widgets/about-video/ui/about-video.test.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import { render, screen } from '@testing-library/react';
-
-import { AboutVideo } from './about-video';
-import { RS_INTRO_URL } from '@/shared/constants';
-import { videoTitleLocalized } from 'data';
-
-describe('AboutVideo component', () => {
- it('renders without crashing with default title', () => {
- render();
- const aboutVideo = screen.getByTestId('about-video');
- const title = screen.getByTestId('widget-title');
-
- expect(aboutVideo).toBeVisible();
- expect(title).toBeVisible();
- expect(title).toHaveTextContent(videoTitleLocalized['en'].title);
- });
-
- it('displays the RU title correctly', () => {
- render();
- const title = screen.getByTestId('widget-title');
-
- expect(title).toHaveTextContent(videoTitleLocalized['ru'].title);
- });
-
- it('renders the YouTube embed', () => {
- render();
- const video = screen.getByTitle('Introduction to The Rolling Scopes School Online Courses');
-
- expect(video).toHaveAttribute('src', RS_INTRO_URL);
- });
-});
diff --git a/src/widgets/angular-topics/constants.ts b/src/widgets/angular-topics/constants.ts
deleted file mode 100644
index cdf664707..000000000
--- a/src/widgets/angular-topics/constants.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-export const topicsList = [
- 'TypeScript',
- 'Components',
- 'Directives & Pipes',
- 'Modules & Services, Dependency Injection',
- 'Routing',
- 'RxJS & Observables',
- 'HTTP',
- 'Forms',
- 'Redux & NgRx',
- 'Unit Testing',
-];
diff --git a/src/widgets/angular-topics/index.ts b/src/widgets/angular-topics/index.ts
deleted file mode 100644
index b995276a1..000000000
--- a/src/widgets/angular-topics/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { AngularTopics } from './ui/angular-topics';
diff --git a/src/widgets/angular-topics/ui/angular-topics.module.scss b/src/widgets/angular-topics/ui/angular-topics.module.scss
deleted file mode 100644
index 862f37250..000000000
--- a/src/widgets/angular-topics/ui/angular-topics.module.scss
+++ /dev/null
@@ -1,7 +0,0 @@
-.angular-topics {
- .angular-topics-content {
- display: flex;
- flex-direction: column;
- gap: 20px;
- }
-}
diff --git a/src/widgets/angular-topics/ui/angular-topics.test.tsx b/src/widgets/angular-topics/ui/angular-topics.test.tsx
deleted file mode 100644
index e83fbe552..000000000
--- a/src/widgets/angular-topics/ui/angular-topics.test.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import { render, screen } from '@testing-library/react';
-import { describe, expect, it } from 'vitest';
-
-import { AngularTopics } from './angular-topics';
-import { topicsList } from '../constants';
-
-describe('AngularTopics component', () => {
- it('renders without crashing', () => {
- render();
- const angularTopics = screen.getByTestId('angular-topics');
-
- expect(angularTopics).toBeVisible();
- });
-
- it('displays the title with correct text', () => {
- render();
- const title = screen.getByTestId('widget-title');
-
- expect(title).toBeVisible();
- expect(title).toHaveTextContent('Topics Covered:');
- });
-
- it('displays the topics list with correct text', () => {
- render();
- const list = screen.getByTestId('list');
-
- expect(list).toBeVisible();
- expect(list).toHaveTextContent(topicsList.join(''));
- });
-});
diff --git a/src/widgets/angular-topics/ui/angular-topics.tsx b/src/widgets/angular-topics/ui/angular-topics.tsx
deleted file mode 100644
index 7ff24fd24..000000000
--- a/src/widgets/angular-topics/ui/angular-topics.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import classNames from 'classnames/bind';
-
-import { topicsList } from '../constants';
-import { List } from '@/shared/ui/list';
-import { WidgetTitle } from '@/shared/ui/widget-title';
-
-import styles from './angular-topics.module.scss';
-
-const cx = classNames.bind(styles);
-
-export const AngularTopics = () => {
- return (
-
-
- Topics Covered:
-
-
-
- );
-};
diff --git a/src/widgets/certification/index.tsx b/src/widgets/certification/index.tsx
deleted file mode 100644
index 3536ff69c..000000000
--- a/src/widgets/certification/index.tsx
+++ /dev/null
@@ -1 +0,0 @@
-export { Certification } from './ui/certification';
diff --git a/src/widgets/certification/ui/certification.module.scss b/src/widgets/certification/ui/certification.module.scss
deleted file mode 100644
index 41607667e..000000000
--- a/src/widgets/certification/ui/certification.module.scss
+++ /dev/null
@@ -1,35 +0,0 @@
-.certification {
- &.info-wrapper {
- display: flex;
- flex-direction: column;
- gap: 26px;
- align-items: flex-start;
- justify-content: flex-start;
-
- font-size: 20px;
-
- @include media-tablet-large {
- flex-direction: column;
- gap: 40px;
- align-items: flex-start;
- font-size: 18px;
-
- .paragraph-wrapper {
- display: flex;
- flex-direction: column;
- gap: 10px;
- align-items: flex-start;
- justify-content: flex-start;
- }
- }
- }
-
- p {
- padding-top: 5px;
- font-size: 18px;
-
- @include media-tablet-large {
- font-size: 16px;
- }
- }
-}
diff --git a/src/widgets/certification/ui/certification.tsx b/src/widgets/certification/ui/certification.tsx
deleted file mode 100644
index b2ceb6b30..000000000
--- a/src/widgets/certification/ui/certification.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-import classNames from 'classnames/bind';
-
-import { Paragraph } from '@/shared/ui/paragraph';
-import { WidgetTitle } from '@/shared/ui/widget-title';
-import { COURSE_TITLES, CourseNamesKeys } from 'data';
-
-import styles from './certification.module.scss';
-
-type RequiredProps = {
- courseName: CourseNamesKeys;
-};
-
-const localizedContent = {
- default: {
- title: 'Certification',
- firstParagraph:
- "To earn a course certificate, you must complete all assignments, finish the final project, and achieve at least 70% of the top student's score in the course. The certificate is a recognition of your hard work and dedication.",
- secondParagraph: '',
- },
- [COURSE_TITLES.JS_RU]: {
- title: 'Сертификат',
- firstParagraph:
- 'Чтобы получить сертификат о прохождении курса вам необходимо набрать 70% от результата TOP-1 студента. Сертификат является признанием вашего усердного труда и преданности делу.',
- secondParagraph: '',
- },
- [COURSE_TITLES.AWS_AI]: {
- title: 'Сертификат',
- firstParagraph:
- 'Чтобы получить сертификат о прохождении курса вам необходимо набрать 70% от результата TOP-1 студента. Сертификат является признанием вашего усердного труда и преданности делу.',
- secondParagraph: '',
- },
-};
-
-type LocalizedContentKey = keyof typeof localizedContent;
-
-const cx = classNames.bind(styles);
-
-export const Certification = ({ courseName }: RequiredProps) => {
- const { title, firstParagraph, secondParagraph } =
- courseName in localizedContent
- ? localizedContent[courseName as LocalizedContentKey]
- : localizedContent.default;
-
- return (
-
-
- {title}
-
-
{firstParagraph}
- {secondParagraph &&
{secondParagraph}}
-
-
-
- );
-};
diff --git a/src/widgets/communication/index.tsx b/src/widgets/communication/index.tsx
deleted file mode 100644
index 4d549f5a9..000000000
--- a/src/widgets/communication/index.tsx
+++ /dev/null
@@ -1 +0,0 @@
-export { Communication } from './ui/communication';
diff --git a/src/widgets/communication/ui/communication.module.scss b/src/widgets/communication/ui/communication.module.scss
deleted file mode 100644
index 789f5f7f4..000000000
--- a/src/widgets/communication/ui/communication.module.scss
+++ /dev/null
@@ -1,43 +0,0 @@
-.communication-content {
- display: flex;
- flex-direction: column;
-
- .communication-wrapper {
- display: flex;
- flex-direction: row;
- gap: 100px;
- align-items: center;
-
- @include media-tablet-large {
- flex-direction: column;
- gap: 40px;
- align-items: flex-start;
- }
- }
-
- .communication-subtitle,
- .communication-paragraph {
- padding-bottom: 5px;
- }
-
- .discord-logo-wrapper {
- flex-shrink: 0;
- align-self: center;
-
- width: 250px;
- padding: 30px;
- border-radius: 30px;
-
- background-color: hsl(234.9deg 85.6% 64.7%);
-
- img {
- width: 100%;
- height: auto;
- }
-
- @include media-tablet {
- width: 150px;
- padding: 15px;
- }
- }
-}
diff --git a/src/widgets/communication/ui/communication.test.tsx b/src/widgets/communication/ui/communication.test.tsx
deleted file mode 100644
index 3c71c9cec..000000000
--- a/src/widgets/communication/ui/communication.test.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-import { cleanup, screen } from '@testing-library/react';
-
-import { Communication } from './communication';
-import { mockedCourses } from '@/shared/__tests__/constants';
-import { renderWithRouter } from '@/shared/__tests__/utils';
-import { COURSE_TITLES, CourseNamesKeys, DISCORD_LINKS, communicationText } from 'data';
-
-const mockLangVariants = [
- {
- course: COURSE_TITLES.ANGULAR,
- texts: communicationText.en,
- },
- {
- course: COURSE_TITLES.JS_PRESCHOOL_RU,
- texts: communicationText.ru,
- },
-];
-
-const mockCourseVariants = mockedCourses.map((course) => {
- return {
- course,
- link: DISCORD_LINKS[course.title as CourseNamesKeys],
- };
-});
-
-describe('Communication section', () => {
- it.each(mockLangVariants)(
- 'should render component correctly with $course.title language prop',
- async ({ course, texts }) => {
- const { title, subTitle, firstParagraphFirstHalf, discordLink } = texts;
- const widget = await Communication({ courseName: course });
-
- renderWithRouter(widget);
- const titleElement = screen.getByText(title);
- const subtitleElement = screen.getByText(subTitle);
- const firstParagraphElement = screen.getByText(`${firstParagraphFirstHalf}`, { exact: false });
- const linkElement = screen.getByText(discordLink);
-
- expect(titleElement).toBeVisible();
- expect(subtitleElement).toBeVisible();
- expect(firstParagraphElement).toBeVisible();
- expect(linkElement).toBeVisible();
- expect(linkElement.getAttribute('href')).toMatch(DISCORD_LINKS[course]);
- cleanup();
- },
- );
-
- it.each(mockCourseVariants)('should render correct link of $course.title', async (variant) => {
- const widget = await Communication({ courseName: variant.course.title });
-
- renderWithRouter(widget);
- const linkElement = screen.getByTestId('discord-link');
-
- expect(linkElement).toBeVisible();
- expect(linkElement.getAttribute('href')).toBe(variant.link);
- cleanup();
- });
-});
diff --git a/src/widgets/communication/ui/communication.tsx b/src/widgets/communication/ui/communication.tsx
deleted file mode 100644
index aef8370b0..000000000
--- a/src/widgets/communication/ui/communication.tsx
+++ /dev/null
@@ -1,112 +0,0 @@
-import classNames from 'classnames/bind';
-import Image from 'next/image';
-
-import { Course } from '@/entities/course';
-import discordLogo from '@/shared/assets/svg/discord-logo.svg';
-import { selectCourse } from '@/shared/hooks/use-course-by-title/utils/select-course';
-import { LinkCustom } from '@/shared/ui/link-custom';
-import { Paragraph } from '@/shared/ui/paragraph';
-import { Subtitle } from '@/shared/ui/subtitle';
-import { WidgetTitle } from '@/shared/ui/widget-title';
-import {
- COURSE_TITLES,
- CourseNamesKeys,
- DISCORD_LINKS,
- JS_EN_TELEGRAM_CHAT_LINK,
- RS_DOCS_COMMUNICATION_LINK,
- RS_DOCS_EN_LINK,
- RS_DOCS_TELEGRAM_CHATS_LINK,
- communicationText,
-} from 'data';
-
-import styles from './communication.module.scss';
-
-const cx = classNames.bind(styles);
-
-type CommunicationProps = {
- courseName: Course['title'];
-};
-
-export const Communication = async ({ courseName }: CommunicationProps) => {
- const course = await selectCourse(courseName);
- const { language } = course;
- const {
- title,
- subTitle,
- subTitleJs,
- firstParagraphFirstHalf,
- discordParagraphTextJs,
- discordLink,
- discordLinkJs,
- firstParagraphSecondHalf,
- telegramParagraphTextJs,
- secondParagraphFirstHalf,
- telegramLink,
- secondParagraphSecondHalf,
- thirdParagraphFirstHalf,
- rsDocsLink,
- thirdParagraphSecondHalf,
- discordNote,
- } = communicationText[language];
-
- const isJsEnCourse = courseName === COURSE_TITLES.JS_EN;
- const courseSubTitle = isJsEnCourse ? subTitleJs : subTitle;
- const paragraphClassName = isJsEnCourse ? cx('communication-paragraph') : undefined;
- const courseDiscordLink = isJsEnCourse ? discordLinkJs : discordLink;
- const discordFirstHalfText = !isJsEnCourse ? firstParagraphFirstHalf : null;
- const discordSecondHalfText = isJsEnCourse ? discordParagraphTextJs : firstParagraphSecondHalf;
- const rsDocsHref = isJsEnCourse ? RS_DOCS_EN_LINK : RS_DOCS_COMMUNICATION_LINK;
-
- return (
-
-
- {title}
-
-
-
-
-
-
{courseSubTitle}
-
- {discordFirstHalfText}
-
- {courseDiscordLink}
-
- {discordSecondHalfText}
-
- {isJsEnCourse && (
-
-
- {telegramLink}
-
- {telegramParagraphTextJs}
-
- )}
-
- ⚠️
- {discordNote}
-
-
- {secondParagraphFirstHalf}
-
- {telegramLink}
-
- {secondParagraphSecondHalf}
-
-
- {thirdParagraphFirstHalf}
-
- {rsDocsLink}
-
- {thirdParagraphSecondHalf}
-
-
-
-
-
- );
-};
diff --git a/src/widgets/course-main/course-main.module.scss b/src/widgets/course-main/course-main.module.scss
deleted file mode 100644
index 0785e9d10..000000000
--- a/src/widgets/course-main/course-main.module.scss
+++ /dev/null
@@ -1,39 +0,0 @@
-.container {
- min-height: 494px;
- background: $color-yellow;
-
- .content {
- display: flex;
- gap: 40px;
- align-items: center;
- justify-content: flex-start;
-
- padding: 100px;
-
- text-align: left;
-
- .icon {
- width: auto;
- min-width: 150px;
- max-width: 200px;
- height: auto;
- margin-top: -36px;
-
- @include media-tablet {
- display: none;
- }
- }
-
- @include media-laptop {
- padding: 100px 40px;
- }
- }
-
- @include media-tablet-large {
- min-height: 546px;
- }
-
- @include media-tablet {
- min-height: 494px;
- }
-}
diff --git a/src/widgets/faq/index.tsx b/src/widgets/faq/index.tsx
deleted file mode 100644
index b44620c1f..000000000
--- a/src/widgets/faq/index.tsx
+++ /dev/null
@@ -1,2 +0,0 @@
-export type { FaqData } from './types';
-export { Faq } from './ui/faq';
diff --git a/src/widgets/faq/types.ts b/src/widgets/faq/types.ts
deleted file mode 100644
index 1a7c3c1a4..000000000
--- a/src/widgets/faq/types.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { LinkList } from '@/widgets/required/types';
-
-export type FaqDataItem = {
- question: string;
- answer: string;
-};
-
-export type FaqDataItemWithLink = {
- question: string;
- answer: LinkList;
-};
-
-export type FaqData = (FaqDataItem | FaqDataItemWithLink)[];
diff --git a/src/widgets/faq/ui/faq.module.scss b/src/widgets/faq/ui/faq.module.scss
deleted file mode 100644
index 6b4db3a04..000000000
--- a/src/widgets/faq/ui/faq.module.scss
+++ /dev/null
@@ -1,38 +0,0 @@
-.faq {
- .list {
- padding: 0;
- list-style: none;
- }
-
- .info-wrapper {
- display: flex;
- flex-direction: column;
- gap: 26px;
-
- font-size: 20px;
- line-height: 28px;
- letter-spacing: 0;
-
- .list {
- display: flex;
- flex-direction: column;
- gap: 20px;
-
- .question {
- display: block;
- margin-bottom: 12px;
- font-weight: $font-weight-bold;
- }
-
- @include media-tablet {
- font-size: 18px;
- font-weight: $font-weight-regular;
- line-height: 28px;
- }
- }
- }
-
- @include media-tablet-large {
- font-size: 16px;
- }
-}
diff --git a/src/widgets/faq/ui/faq.test.tsx b/src/widgets/faq/ui/faq.test.tsx
deleted file mode 100644
index 4071628b1..000000000
--- a/src/widgets/faq/ui/faq.test.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import { render, screen } from '@testing-library/react';
-import { describe, expect, it } from 'vitest';
-
-import { Faq } from './faq';
-import { MOCKED_FAQ, MOCKED_FAQ_WITH_LINKS } from '@/shared/__tests__/constants';
-
-const widgetTitle = 'FAQ';
-
-describe('FAQ component', () => {
- it('renders widget without crashing and display correct content', () => {
- render();
-
- const widget = screen.getByTestId('faq');
- const title = screen.getByTestId('widget-title');
- const faqList = screen.getByTestId('faq-list');
- const faqQuestions = screen.getAllByTestId('faq-question');
-
- expect(widget).toBeVisible();
- expect(title).toBeVisible();
- expect(faqList).toBeVisible();
- expect(title).toHaveTextContent(widgetTitle);
- expect(faqQuestions.length).toBe(MOCKED_FAQ.length);
-
- faqQuestions.forEach((question) => expect(question).toBeVisible());
- faqQuestions.forEach((faqQuestion) => {
- const question = MOCKED_FAQ.find(
- (item, index) => `${index + 1}. ${item.question}` === faqQuestion.textContent,
- );
-
- expect(question).toBeDefined();
- });
- });
-
- it('renders correctly answers with links', () => {
- render();
-
- const faqQuestions = screen.getAllByTestId('faq-question');
- const faqAnswersLinks = [...screen.getAllByRole('link')];
- const mockedAnswersLinks = MOCKED_FAQ_WITH_LINKS.flatMap(({ answer }) => answer);
-
- expect(faqQuestions.length).toBe(MOCKED_FAQ_WITH_LINKS.length);
- expect(faqAnswersLinks.length).toBe(mockedAnswersLinks.length);
-
- mockedAnswersLinks.forEach(({ title: mockedTitle, link: mockedLink }) => {
- const faqAnswerLink = faqAnswersLinks.find(
- (link) => link.getAttribute('href') === mockedLink,
- );
-
- expect(faqAnswerLink).toBeDefined();
- expect(faqAnswerLink).toHaveTextContent(mockedTitle);
- });
- });
-});
diff --git a/src/widgets/faq/ui/faq.tsx b/src/widgets/faq/ui/faq.tsx
deleted file mode 100644
index d0d0f28f3..000000000
--- a/src/widgets/faq/ui/faq.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import classNames from 'classnames/bind';
-
-import { FaqData } from '../types';
-import { Subtitle } from '@/shared/ui/subtitle';
-import { TextWithLink } from '@/shared/ui/text-with-link';
-import { WidgetTitle } from '@/shared/ui/widget-title';
-
-import styles from './faq.module.scss';
-
-const cx = classNames.bind(styles);
-
-type FaqProps = {
- faqData: FaqData;
-};
-
-export const Faq = ({ faqData }: FaqProps) => {
- return (
-
-
-
FAQ
-
- {faqData.map(({ question, answer }, index) => (
- -
-
- {`${index + 1}. ${question}`}
-
- {typeof answer === 'string' ? {answer} : }
-
- ))}
-
-
-
- );
-};
diff --git a/src/widgets/learning-path-stages/helpers/is-learning-path-stages-section.ts b/src/widgets/learning-path-stages/helpers/is-learning-path-stages-section.ts
new file mode 100644
index 000000000..9188df69a
--- /dev/null
+++ b/src/widgets/learning-path-stages/helpers/is-learning-path-stages-section.ts
@@ -0,0 +1,14 @@
+import { TypeLearningPathStagesWithoutUnresolvableLinksResponse } from '@/shared/types/contentful';
+import type { BaseEntry } from 'contentful';
+
+/**
+ * Checks if the given section is a "Learning Path Stages" section by validating the content type ID.
+ *
+ * @param {TSection} section - The section to be checked, which extends the BaseEntry.
+ * @return {boolean} - Returns true if the section is of type `TypeMediaTextBlockWithoutUnresolvableLinksResponse`, otherwise false.
+ */
+export function isLearningPathStagesSection(
+ section: TSection,
+): section is Extract {
+ return section?.sys?.contentType?.sys?.id === 'learningPathStages';
+}
diff --git a/src/widgets/learning-path-stages/helpers/transform-learning-path-stage-item.ts b/src/widgets/learning-path-stages/helpers/transform-learning-path-stage-item.ts
new file mode 100644
index 000000000..d250c4595
--- /dev/null
+++ b/src/widgets/learning-path-stages/helpers/transform-learning-path-stage-item.ts
@@ -0,0 +1,28 @@
+import { prepareAssetImage } from '@/shared/helpers/prepare-asset-image';
+import { richTextRenderer } from '@/shared/helpers/rich-text-renderer';
+import {
+ TypeLearningPathStageItemWithoutUnresolvableLinksResponse,
+} from '@/shared/types/contentful';
+import { LearningPathStageItem } from '@/widgets/learning-path-stages/types';
+
+export function transformLearningPathStageItem(
+ item: TypeLearningPathStageItemWithoutUnresolvableLinksResponse | undefined,
+): LearningPathStageItem {
+ if (!item) {
+ throw new Error('Learning path stage item is not defined.');
+ }
+
+ const id = item.sys.id;
+ const title = item.fields.title;
+ const image = item.fields.image?.fields?.file
+ ? prepareAssetImage(item.fields.image?.fields?.file)
+ : undefined;
+ const content = richTextRenderer(item.fields.content);
+
+ return {
+ id,
+ title,
+ image,
+ content,
+ };
+}
diff --git a/src/widgets/learning-path-stages/helpers/transform-learning-path-stages.ts b/src/widgets/learning-path-stages/helpers/transform-learning-path-stages.ts
new file mode 100644
index 000000000..207d7a301
--- /dev/null
+++ b/src/widgets/learning-path-stages/helpers/transform-learning-path-stages.ts
@@ -0,0 +1,26 @@
+import { richTextRenderer } from '@/shared/helpers/rich-text-renderer';
+import { TypeLearningPathStagesWithoutUnresolvableLinksResponse } from '@/shared/types/contentful';
+import { Section } from '@/views/course/types';
+import { transformLearningPathStageItem } from '@/widgets/learning-path-stages';
+
+export function transformLearningPathStages(
+ section: TypeLearningPathStagesWithoutUnresolvableLinksResponse,
+): Section {
+ const id = section.sys.id;
+ const name = section.sys.contentType.sys.id;
+ const title = section.fields.title;
+ const description = section.fields.description
+ ? richTextRenderer(section.fields.description)
+ : section.fields.description;
+ const stages = section.fields.stages.map(transformLearningPathStageItem);
+
+ return {
+ id,
+ name,
+ data: {
+ title,
+ description,
+ stages,
+ },
+ };
+}
diff --git a/src/widgets/learning-path-stages/index.ts b/src/widgets/learning-path-stages/index.ts
new file mode 100644
index 000000000..677fb70e3
--- /dev/null
+++ b/src/widgets/learning-path-stages/index.ts
@@ -0,0 +1,6 @@
+export type { LearningPathStagesSectionData } from './types';
+export { LearningPathStageItem } from './ui/learning-path-stage-item/learning-path-stage-item';
+export { LearningPathStages } from './ui/learning-path-stages/learning-path-stages';
+export { isLearningPathStagesSection } from './helpers/is-learning-path-stages-section';
+export { transformLearningPathStageItem } from './helpers/transform-learning-path-stage-item';
+export { transformLearningPathStages } from './helpers/transform-learning-path-stages';
diff --git a/src/widgets/learning-path-stages/types.ts b/src/widgets/learning-path-stages/types.ts
new file mode 100644
index 000000000..7fc60d3d8
--- /dev/null
+++ b/src/widgets/learning-path-stages/types.ts
@@ -0,0 +1,15 @@
+import { ReactNode } from 'react';
+import { StaticImageData } from 'next/image';
+
+export type LearningPathStageItem = {
+ id: string;
+ title: string;
+ content: ReactNode;
+ image?: StaticImageData;
+};
+
+export type LearningPathStagesSectionData = {
+ title: string;
+ description?: ReactNode;
+ stages: LearningPathStageItem[];
+};
diff --git a/src/widgets/learning-path-stages/ui/learning-path-stage-item/learning-path-stage-item.module.scss b/src/widgets/learning-path-stages/ui/learning-path-stage-item/learning-path-stage-item.module.scss
new file mode 100644
index 000000000..58cebdf2d
--- /dev/null
+++ b/src/widgets/learning-path-stages/ui/learning-path-stage-item/learning-path-stage-item.module.scss
@@ -0,0 +1,108 @@
+.learning-path-stage-item {
+ display: flex;
+ gap: 40px;
+
+ .stage-number {
+ $number-size: 80px;
+ $number-size-tablet: 40px;
+ $number-gap: 10px;
+
+ display: flex;
+ flex-direction: column;
+ gap: $number-gap;
+ align-items: center;
+
+ .step {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ width: $number-size;
+ height: $number-size;
+ border: 1px solid $color-yellow-100;
+ border-radius: 50%;
+
+ font-size: 24px;
+ color: $color-gray-600;
+
+ background-color: $color-yellow-50;
+
+ @include media-tablet {
+ width: $number-size-tablet;
+ height: $number-size-tablet;
+ font-size: 16px;
+ }
+ }
+
+ .decor-line {
+ width: 2px;
+ height: calc(100% - $number-size - $number-gap);
+ background-color: $color-gray-400;
+
+ @include media-tablet {
+ height: calc(100% - $number-size-tablet - $number-gap);
+ }
+ }
+ }
+
+ .stage-info {
+ display: flex;
+ flex: 1 1 40%;
+ flex-direction: column;
+ gap: 8px;
+
+ max-width: 600px;
+
+ .stage-intro:last-of-type {
+ padding-bottom: 0;
+ }
+
+ .stage-list {
+ a {
+ padding-right: 24px;
+
+ &:last-of-type {
+ padding: 0;
+ }
+
+ @include media-tablet-large {
+ display: block;
+ padding-right: 0;
+ padding-bottom: 8px;
+ }
+ }
+ }
+
+ @include media-tablet-large {
+ max-width: 100%;
+ }
+ }
+
+ .stage-picture {
+ overflow: hidden;
+ padding-right: 60px;
+ object-fit: contain;
+
+ &.stage-logo {
+ max-width: 143px;
+ max-height: 102px;
+ }
+
+ &.stage-image {
+ width: 290px;
+ height: 192px;
+ }
+
+ @include media-tablet {
+ display: none;
+ }
+ }
+
+ &:last-child > .stage-number .decor-line {
+ display: none;
+ }
+
+ @include media-tablet {
+ gap: 16px;
+ }
+}
diff --git a/src/widgets/learning-path-stages/ui/learning-path-stage-item/learning-path-stage-item.tsx b/src/widgets/learning-path-stages/ui/learning-path-stage-item/learning-path-stage-item.tsx
new file mode 100644
index 000000000..f28b6f826
--- /dev/null
+++ b/src/widgets/learning-path-stages/ui/learning-path-stage-item/learning-path-stage-item.tsx
@@ -0,0 +1,44 @@
+import classNames from 'classnames/bind';
+import Image from 'next/image';
+
+import { Subtitle } from '@/shared/ui/subtitle';
+import {
+ LearningPathStageItem as TLearningPathStageItem,
+} from '@/widgets/learning-path-stages/types';
+
+import styles from './learning-path-stage-item.module.scss';
+
+type LearningPathStagesProps = Omit & {
+ index: number;
+};
+
+const cx = classNames.bind(styles);
+
+export const LearningPathStageItem = ({
+ title,
+ content,
+ image,
+ index,
+}: LearningPathStagesProps) => {
+ const step = index + 1;
+
+ return (
+
+
+
+
+ {title}
+ {content}
+
+
+ {image && (
+
+ )}
+
+ );
+};
diff --git a/src/widgets/learning-path-stages/ui/learning-path-stages/learning-path-stages.module.scss b/src/widgets/learning-path-stages/ui/learning-path-stages/learning-path-stages.module.scss
new file mode 100644
index 000000000..d54d986e4
--- /dev/null
+++ b/src/widgets/learning-path-stages/ui/learning-path-stages/learning-path-stages.module.scss
@@ -0,0 +1,12 @@
+.learning-path-stages {
+ .stages {
+ display: flex;
+ flex-direction: column;
+ gap: 32px;
+ padding-top: 40px;
+
+ @include media-tablet-large {
+ padding-top: 24px;
+ }
+ }
+}
diff --git a/src/widgets/learning-path-stages/ui/learning-path-stages/learning-path-stages.tsx b/src/widgets/learning-path-stages/ui/learning-path-stages/learning-path-stages.tsx
new file mode 100644
index 000000000..916698f56
--- /dev/null
+++ b/src/widgets/learning-path-stages/ui/learning-path-stages/learning-path-stages.tsx
@@ -0,0 +1,25 @@
+import { PropsWithChildren } from 'react';
+import classNames from 'classnames/bind';
+
+import { WidgetTitle } from '@/shared/ui/widget-title';
+import { LearningPathStagesSectionData } from '@/widgets/learning-path-stages/types';
+
+import styles from './learning-path-stages.module.scss';
+
+const cx = classNames.bind(styles);
+
+type LearningPathStagesProps = Omit & PropsWithChildren;
+
+export const LearningPathStages = ({ title, description, children }: LearningPathStagesProps) => {
+ return (
+
+
+
+ {title}
+
+ {description}
+ {children}
+
+
+ );
+};
diff --git a/src/widgets/media-text-block/helpers/is-media-text-block-section.ts b/src/widgets/media-text-block/helpers/is-media-text-block-section.ts
new file mode 100644
index 000000000..32932ab61
--- /dev/null
+++ b/src/widgets/media-text-block/helpers/is-media-text-block-section.ts
@@ -0,0 +1,14 @@
+import { TypeMediaTextBlockWithoutUnresolvableLinksResponse } from '@/shared/types/contentful';
+import type { BaseEntry } from 'contentful';
+
+/**
+ * Determines whether a given section is of type 'mediaTextBlock'.
+ *
+ * @param section - The section to check, which extends the BaseEntry type.
+ * @return Returns true if the section is of type 'mediaTextBlock', otherwise false.
+ */
+export function isMediaTextBlockSection(
+ section: TSection,
+): section is Extract {
+ return section?.sys?.contentType?.sys?.id === 'mediaTextBlock';
+}
diff --git a/src/widgets/media-text-block/helpers/transform-media-text-block-section.ts b/src/widgets/media-text-block/helpers/transform-media-text-block-section.ts
new file mode 100644
index 000000000..15055b154
--- /dev/null
+++ b/src/widgets/media-text-block/helpers/transform-media-text-block-section.ts
@@ -0,0 +1,35 @@
+import { richTextRenderer } from '@/shared/helpers/rich-text-renderer';
+import { TypeMediaTextBlockWithoutUnresolvableLinksResponse } from '@/shared/types/contentful';
+import { Section } from '@/views/course/types';
+
+export function transformMediaTextBlockSection(
+ section: TypeMediaTextBlockWithoutUnresolvableLinksResponse,
+): Section {
+ const id = section.sys.id;
+ const name = section.sys.contentType.sys.id;
+ const title = section.fields.title;
+ const contentLeft = section.fields.contentLeft
+ ? richTextRenderer(section.fields.contentLeft)
+ : section.fields.contentLeft;
+ const contentRight = section.fields.contentRight
+ ? richTextRenderer(section.fields.contentRight)
+ : section.fields.contentRight;
+ const linkUrl = section.fields.linkUrl;
+ const linkLabel = section.fields.linkLabel;
+ const disabledLinkLabel = section.fields.disabledLinkLabel;
+ const backgroundColor = section.fields.backgroundColor;
+
+ return {
+ id,
+ name,
+ data: {
+ title,
+ contentLeft,
+ contentRight,
+ linkUrl,
+ linkLabel,
+ disabledLinkLabel,
+ backgroundColor,
+ },
+ };
+}
diff --git a/src/widgets/media-text-block/index.ts b/src/widgets/media-text-block/index.ts
new file mode 100644
index 000000000..4dfabbf25
--- /dev/null
+++ b/src/widgets/media-text-block/index.ts
@@ -0,0 +1,4 @@
+export type { MediaTextBlockSectionData } from './types';
+export { MediaTextBlock } from './ui/media-text-block';
+export { isMediaTextBlockSection } from './helpers/is-media-text-block-section';
+export { transformMediaTextBlockSection } from './helpers/transform-media-text-block-section';
diff --git a/src/widgets/media-text-block/types.ts b/src/widgets/media-text-block/types.ts
new file mode 100644
index 000000000..e772e560c
--- /dev/null
+++ b/src/widgets/media-text-block/types.ts
@@ -0,0 +1,11 @@
+import { ReactNode } from 'react';
+
+export type MediaTextBlockSectionData = {
+ title: string;
+ contentLeft?: ReactNode;
+ contentRight?: ReactNode;
+ linkUrl?: string | null;
+ linkLabel?: string;
+ disabledLinkLabel?: string;
+ backgroundColor?: string;
+};
diff --git a/src/widgets/media-text-block/ui/media-text-block.module.scss b/src/widgets/media-text-block/ui/media-text-block.module.scss
new file mode 100644
index 000000000..b11a1a015
--- /dev/null
+++ b/src/widgets/media-text-block/ui/media-text-block.module.scss
@@ -0,0 +1,75 @@
+.media-text-block {
+ .title {
+ grid-column: 1 / 3;
+ grid-row: 1 / 2;
+
+ @include media-tablet-large {
+ grid-column: 1 / 2;
+ }
+ }
+
+ .inner {
+ display: grid;
+ grid-auto-flow: column;
+ grid-template-rows: max-content 1fr;
+ gap: 24px 100px;
+
+ @include media-tablet-large {
+ grid-auto-flow: row;
+ gap: 40px;
+ }
+ }
+
+ .content-wrapper-left {
+ display: flex;
+ grid-column: 1 / 2;
+ grid-row: 2 / 3;
+ flex-direction: column;
+ gap: 24px;
+ }
+
+ .content-wrapper-right {
+ @include media-tablet-large {
+ grid-column: 1 / 2 !important;
+ grid-row: 3 / 4 !important;
+ }
+ }
+
+ // if the content is NOT an image, place it under the title -> |title | |
+ // -> |text content|text content|
+ .content-wrapper-right:not(:has([data-id='asset-image'])) {
+ grid-row: 2 / 3;
+ }
+
+ // if the content is an image, place it on the same leve as title -> |title |image|
+ // -> |text content|image|
+ .content-wrapper-right:has([data-id='asset-image']) {
+ grid-column: 2 / 3;
+ grid-row: 1 / 3;
+ }
+
+ // if the content is an image, format the image accordingly
+ .content-wrapper-right:has([data-id='asset-image']),
+ .content-wrapper-left:has([data-id='asset-image']) {
+ display: flex;
+ align-self: center;
+ width: 100%;
+ max-width: 380px;
+
+ [data-id='asset-image'] {
+ width: 100%;
+ height: fit-content;
+ object-fit: contain;
+
+ @include media-tablet-large {
+ justify-self: center;
+ width: 320px;
+ }
+ }
+
+ @include media-tablet-large {
+ justify-content: center;
+ justify-self: center;
+ }
+ }
+}
diff --git a/src/widgets/media-text-block/ui/media-text-block.tsx b/src/widgets/media-text-block/ui/media-text-block.tsx
new file mode 100644
index 000000000..38d511c71
--- /dev/null
+++ b/src/widgets/media-text-block/ui/media-text-block.tsx
@@ -0,0 +1,56 @@
+import classNames from 'classnames/bind';
+
+import { MediaTextBlockSectionData } from '../types';
+import { isExternalUri } from '@/shared/helpers/is-external-uri';
+import { LinkCustom } from '@/shared/ui/link-custom';
+import { WidgetTitle } from '@/shared/ui/widget-title';
+
+import styles from './media-text-block.module.scss';
+
+type MediaTextBlockProps = MediaTextBlockSectionData;
+
+const cx = classNames.bind(styles);
+
+export const MediaTextBlock = async ({
+ title,
+ contentLeft,
+ contentRight,
+ linkUrl,
+ linkLabel,
+ disabledLinkLabel,
+ backgroundColor,
+}: MediaTextBlockProps) => {
+ const linkText = linkUrl ? linkLabel : disabledLinkLabel;
+ const href = linkUrl ?? '';
+ const isLinkDisabled = !linkUrl;
+ const isLinkShown = (href && Boolean(linkLabel)) || (!href && Boolean(disabledLinkLabel));
+
+ return (
+
+
+
+ {title}
+
+
+
+ {contentLeft &&
{contentLeft}
}
+
+ {isLinkShown && (
+
+ {linkText}
+
+ )}
+
+
+ {contentRight && (
+
{contentRight}
+ )}
+
+
+ );
+};
diff --git a/src/widgets/required/index.ts b/src/widgets/required/index.ts
deleted file mode 100644
index 6302cb59d..000000000
--- a/src/widgets/required/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { Required } from './ui/required/required';
diff --git a/src/widgets/required/types.ts b/src/widgets/required/types.ts
deleted file mode 100644
index d3c23fe9b..000000000
--- a/src/widgets/required/types.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-type ItemWithLink = {
- id: number;
- text: string;
- title: string;
- link: string;
- external?: boolean;
-};
-
-export type LinkList = ItemWithLink[];
-
-type Description = (string | LinkList)[];
-
-export type CourseModule = {
- title: string;
- description: Description;
-};
-
-export type Course = {
- title: string;
- knowBefore?: CourseModule;
- willLearn?: CourseModule[];
-};
diff --git a/src/widgets/required/ui/course-module/course-module.module.scss b/src/widgets/required/ui/course-module/course-module.module.scss
deleted file mode 100644
index 46dc5fe81..000000000
--- a/src/widgets/required/ui/course-module/course-module.module.scss
+++ /dev/null
@@ -1,5 +0,0 @@
-.course-module-element {
- display: flex;
- flex-direction: column;
- gap: 8px;
-}
diff --git a/src/widgets/required/ui/course-module/course-module.tsx b/src/widgets/required/ui/course-module/course-module.tsx
deleted file mode 100644
index 2dd6884d7..000000000
--- a/src/widgets/required/ui/course-module/course-module.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import classNames from 'classnames/bind';
-
-import { CourseModule } from '../../types';
-import { List } from '@/shared/ui/list';
-import { Subtitle } from '@/shared/ui/subtitle';
-
-import styles from './course-module.module.scss';
-
-export const cx = classNames.bind(styles);
-
-type CourseModuleElementProps = {
- courseModule: CourseModule;
-};
-
-export function CourseModuleElement({ courseModule }: CourseModuleElementProps) {
- const { title, description } = courseModule;
-
- return (
-
-
- {title}
-
-
-
- );
-}
diff --git a/src/widgets/required/ui/required/required.module.scss b/src/widgets/required/ui/required/required.module.scss
deleted file mode 100644
index 0b72380ce..000000000
--- a/src/widgets/required/ui/required/required.module.scss
+++ /dev/null
@@ -1,26 +0,0 @@
-.info-wrapper {
- display: flex;
- flex-direction: column;
-
- .course-module-columns-layout {
- display: flex;
- gap: 8rem;
-
- .course-module-elements-column {
- flex: 1;
-
- @include media-tablet {
- max-width: 100%;
- }
- }
-
- @include media-laptop {
- gap: 5rem;
- }
-
- @include media-laptop-medium {
- flex-direction: column;
- gap: 32px;
- }
- }
-}
diff --git a/src/widgets/required/ui/required/required.test.tsx b/src/widgets/required/ui/required/required.test.tsx
deleted file mode 100644
index 710fd113f..000000000
--- a/src/widgets/required/ui/required/required.test.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-import { render, screen } from '@testing-library/react';
-import { describe, expect, it } from 'vitest';
-
-import { Required } from './required';
-
-describe('Required', () => {
- it('renders the title and subtitle correctly', () => {
- render();
- const titleElement = screen.getByText('What you should know before starting');
- const subtitleElement = screen.getByText('What you should know before starting');
-
- expect(titleElement).toBeVisible();
- expect(subtitleElement).toBeVisible();
- });
-
- it('renders correct requirement with "nodejs" props', () => {
- render();
- const requirement = screen.getByText(
- /Solid knowledge of JavaScript, including ES6, is required for this course./i,
- );
-
- expect(requirement).toBeVisible();
- });
-
- it('renders correct requirements with "angular" props', () => {
- render();
- const requirements = [
- 'JavaScript, TypeScript Basics, CSS3, HTML5, NPM',
- 'Git, GitHub',
- 'Chrome DevTools',
- 'Figma',
- 'Understanding the concept of REST API',
- ];
-
- requirements.forEach((requirement) => {
- expect(screen.getByText(new RegExp(requirement, 'i'))).toBeInTheDocument();
- });
- });
-
- it('renders correct requirements with "awsDev" props', () => {
- render();
- const requirements = [
- 'You should be comfortable with at',
- 'English language level: Intermediate',
- 'Being able to spend at least 10 hours per week studying.',
- ];
-
- requirements.forEach((requirement) => {
- expect(screen.getByText(new RegExp(requirement, 'i'))).toBeInTheDocument();
- });
- });
-
- it('renders correct requirements with "awsFundamentals" props', () => {
- render();
- const requirements = [
- 'Beginners welcome!',
- 'No AWS Cloud experience is necessary.',
- 'We will use the AWS Free Tier',
- 'No IT prerequisites required',
- 'Networking Fundamentals',
- 'Cloud Technical Fundamentals',
- 'AWS Cloud Essentials',
- 'Basic AWS Services',
- ];
-
- requirements.forEach((requirement) => {
- expect(screen.getByText(new RegExp(requirement, 'i'))).toBeInTheDocument();
- });
- });
-});
diff --git a/src/widgets/required/ui/required/required.tsx b/src/widgets/required/ui/required/required.tsx
deleted file mode 100644
index f534b53f7..000000000
--- a/src/widgets/required/ui/required/required.tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-import classNames from 'classnames/bind';
-
-import { CourseModuleElement } from '../course-module/course-module';
-import { WidgetTitle } from '@/shared/ui/widget-title';
-import { CoursesWithRequirementsNames, courseDataMap } from 'data';
-
-import styles from './required.module.scss';
-
-export const cx = classNames.bind(styles);
-
-type RequiredProps = {
- courseName: CoursesWithRequirementsNames;
-};
-
-export const Required = ({ courseName }: RequiredProps) => {
- const requiredKnowledge = courseDataMap[courseName];
- const { knowBefore, willLearn, title } = requiredKnowledge;
-
- const isKnowBeforeExist = knowBefore && Boolean(knowBefore.description.length);
- const isWillLearnExist = willLearn && Boolean(willLearn.length);
-
- return (
-
-
-
- {title}
-
-
-
- {isKnowBeforeExist && (
-
-
-
- )}
- {isWillLearnExist && (
-
- {willLearn.map((willLearn, index) => (
-
- ))}
-
- )}
-
-
-
- );
-};
diff --git a/src/widgets/requirements/index.ts b/src/widgets/requirements/index.ts
deleted file mode 100644
index 43293ba85..000000000
--- a/src/widgets/requirements/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { Requirements } from './ui/requirements';
diff --git a/src/widgets/requirements/ui/requirements.module.scss b/src/widgets/requirements/ui/requirements.module.scss
deleted file mode 100644
index 0c92490bf..000000000
--- a/src/widgets/requirements/ui/requirements.module.scss
+++ /dev/null
@@ -1,26 +0,0 @@
-.requirements {
- display: flex;
- flex-direction: column;
- gap: 32px;
-}
-
-.title {
- margin-bottom: 16px;
-}
-
-.requirements-info {
- display: flex;
- gap: 32px;
-
- @include media-tablet-large {
- flex-direction: column;
- }
-}
-
-.requirements-list-wrapper {
- max-width: 600px;
-
- @include media-tablet-large {
- max-width: 100%;
- }
-}
diff --git a/src/widgets/requirements/ui/requirements.test.tsx b/src/widgets/requirements/ui/requirements.test.tsx
deleted file mode 100644
index febb09946..000000000
--- a/src/widgets/requirements/ui/requirements.test.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-import { screen } from '@testing-library/react';
-import { beforeEach, describe, expect, it } from 'vitest';
-
-import { Requirements } from './requirements';
-import { renderWithRouter } from '@/shared/__tests__/utils';
-
-const registerLink = 'https://app.rs.school/registry/mentor';
-const requirements = [
- "Desire to help students. If you've been working with JS/TS in production for more than 6 months, then that's great",
- 'Desire to mentor 2 to 6 students online or in person',
- 'Ability to spend 3 to 5 hours per week',
-];
-const responsibilities = [
- 'Conducting an interview',
- 'Code review tasks',
- "Answers to students' questions",
-];
-
-describe('Requirements', () => {
- beforeEach(() => {
- renderWithRouter();
- });
- it('renders check', () => {
- const requirementWidget = screen.getByTestId('requirements');
-
- expect(requirementWidget).toBeVisible();
-
- expect(screen.getByText('Requirements for mentors')).toBeVisible();
-
- requirements.forEach((s) => expect(screen.getByText(s)).toBeVisible());
-
- expect(screen.getByText('Mentor responsibilities')).toBeVisible();
-
- responsibilities.forEach((s) => expect(screen.getByText(s)).toBeVisible());
-
- const button = screen.getByRole('link', { name: /Register as a mentor/i });
-
- expect(button).toHaveAttribute('href', registerLink);
- });
-});
diff --git a/src/widgets/requirements/ui/requirements.tsx b/src/widgets/requirements/ui/requirements.tsx
deleted file mode 100644
index c2b041ef4..000000000
--- a/src/widgets/requirements/ui/requirements.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import classNames from 'classnames/bind';
-
-import { LinkCustom } from '@/shared/ui/link-custom';
-import { List } from '@/shared/ui/list';
-import { Subtitle } from '@/shared/ui/subtitle';
-import { requirementsData } from 'data';
-
-import styles from './requirements.module.scss';
-
-const cx = classNames.bind(styles);
-
-export const Requirements = () => {
- return (
-
-
-
-
- {requirementsData.headerRequirements}
-
-
-
- {requirementsData.headerTask}
-
-
-
-
- {requirementsData.button.text}
-
-
-
- );
-};
diff --git a/src/widgets/study-path/ui/stage-card/stage-card.tsx b/src/widgets/study-path/ui/stage-card/stage-card.tsx
index 4beaefbb0..5426754b5 100644
--- a/src/widgets/study-path/ui/stage-card/stage-card.tsx
+++ b/src/widgets/study-path/ui/stage-card/stage-card.tsx
@@ -4,20 +4,13 @@ import Image from 'next/image';
import { List } from '@/shared/ui/list';
import { Paragraph } from '@/shared/ui/paragraph';
import { Subtitle } from '@/shared/ui/subtitle';
-import type { LinkList } from '@/widgets/required/types';
-import type { StageCardProps } from 'data';
+import type { LinkList, StageCardProps } from 'data';
import styles from './stage-card.module.scss';
const cx = classNames.bind(styles);
-export const StageCard = ({
- id,
- title,
- intro,
- modules,
- image,
-}: StageCardProps) => {
+export const StageCard = ({ id, title, intro, modules, image }: StageCardProps) => {
// moduleContent is a list of module topics, these can be either links or strings.
const moduleContent: (string | LinkList)[] = modules.map((module) => {
if (module.text) {
@@ -36,7 +29,9 @@ export const StageCard = ({
return (
@@ -44,12 +39,24 @@ export const StageCard = ({
{title}
{intro}
- {!!moduleContent.length
- &&
}
+ {!!moduleContent.length && (
+
+ )}
- {image.src
- && }
+ {image.src && (
+
+ )}
);
};
diff --git a/src/widgets/study-path/ui/study-path/study-path.test.tsx b/src/widgets/study-path/ui/study-path/study-path.test.tsx
index d3ceb04e0..4d7f6f4a0 100644
--- a/src/widgets/study-path/ui/study-path/study-path.test.tsx
+++ b/src/widgets/study-path/ui/study-path/study-path.test.tsx
@@ -2,11 +2,9 @@ import { render, screen } from '@testing-library/react';
import { StudyPath } from './study-path';
-const stages = ['courses', 'jsEn', 'jsRu', 'angular', 'awsDev'] as const;
-
describe('StudyPath Component', () => {
- it.each([stages])('renders component with title, intro and stages correctly for %o', (page) => {
- const studyPath = render();
+ it('renders component with title, intro and stages correctly', () => {
+ const studyPath = render();
const sectionTitle = studyPath.getByRole('heading', { level: 2 });
const sectionIntro = studyPath.getAllByRole('paragraph')[0];
diff --git a/src/widgets/study-path/ui/study-path/study-path.tsx b/src/widgets/study-path/ui/study-path/study-path.tsx
index dafa1fd45..6a5adf449 100644
--- a/src/widgets/study-path/ui/study-path/study-path.tsx
+++ b/src/widgets/study-path/ui/study-path/study-path.tsx
@@ -3,10 +3,10 @@ import cn from 'classnames';
import { Stages } from '../stages/stages';
import { Paragraph } from '@/shared/ui/paragraph';
import { WidgetTitle } from '@/shared/ui/widget-title';
-import { type StudyPathPage, type StudyPathProps, studyPath } from 'data';
+import { studyPath } from 'data';
-export const StudyPath = ({ page }: StudyPathPage) => {
- const path: StudyPathProps = studyPath[page];
+export const StudyPath = () => {
+ const path = studyPath;
const { sectionTitle, sectionIntro, stages } = path;
if (!stages || !stages.length) {
diff --git a/src/widgets/trainers/ui/trainers.tsx b/src/widgets/trainers/ui/trainers.tsx
index 65fa860d9..a6bf90ac1 100644
--- a/src/widgets/trainers/ui/trainers.tsx
+++ b/src/widgets/trainers/ui/trainers.tsx
@@ -4,14 +4,13 @@ import { Trainer, TrainerCard } from '@/entities/trainer';
import { selectCourse } from '@/shared/hooks/use-course-by-title/utils/select-course';
import { WidgetTitle } from '@/shared/ui/widget-title';
import { trainersTitle } from '@/widgets/trainers/constants';
-import { CourseNamesKeys } from 'data';
import styles from './trainers.module.scss';
const cx = classNames.bind(styles);
type TrainersProps = {
- courseName: CourseNamesKeys;
+ courseName: string;
trainers: Trainer[];
};
diff --git a/src/widgets/training-program/index.ts b/src/widgets/training-program/index.ts
deleted file mode 100644
index f9d69819b..000000000
--- a/src/widgets/training-program/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { TrainingProgram } from './ui/training-program';
diff --git a/src/widgets/training-program/ui/training-program.module.scss b/src/widgets/training-program/ui/training-program.module.scss
deleted file mode 100644
index 1c13ccdda..000000000
--- a/src/widgets/training-program/ui/training-program.module.scss
+++ /dev/null
@@ -1,66 +0,0 @@
-.training-program {
- 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 {
- gap: 40px;
- align-items: flex-start;
- }
-}
diff --git a/src/widgets/training-program/ui/training-program.test.tsx b/src/widgets/training-program/ui/training-program.test.tsx
deleted file mode 100644
index 5a4b04cd1..000000000
--- a/src/widgets/training-program/ui/training-program.test.tsx
+++ /dev/null
@@ -1,116 +0,0 @@
-import { screen } from '@testing-library/react';
-import { beforeEach } from 'vitest';
-
-import { renderWithRouter } from '@/shared/__tests__/utils';
-import angularImg from '@/shared/assets/rs-slope-angular.webp';
-import awsDevImg from '@/shared/assets/rs-slope-aws-dev.webp';
-import { REGISTRATION_WILL_OPEN_SOON, REGISTRATION_WILL_OPEN_SOON_RU } from '@/shared/constants';
-import { TrainingProgram } from '@/widgets/training-program';
-import { COURSE_TITLES } from 'data';
-
-const mockedParagraphsAngular = [
- 'This course is designed for individuals with a solid foundation in JavaScript, TypeScript, and front-end development. The course is exclusively available for graduates who have successfully completed JS/FE Stage 2.',
- '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', () => {
- beforeEach(async () => {
- const widget = await TrainingProgram({ courseName: COURSE_TITLES.ANGULAR });
-
- renderWithRouter(widget);
- });
-
- it(`renders correct title "Training Program"`, () => {
- const title = screen.getByTestId('widget-title');
-
- expect(title).toBeVisible();
- });
-
- 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', '/enroll');
- });
-
- it('renders correct image with alt text', () => {
- const image = screen.getByTestId('image');
-
- expect(image).toHaveAttribute('alt', expect.stringContaining('Angular'));
- expect(image).toHaveAttribute('src', angularImg.src);
- });
- });
-
- describe('with "awsDev" props', () => {
- beforeEach(async () => {
- const widget = await TrainingProgram({ courseName: COURSE_TITLES.AWS_CLOUD_DEVELOPER });
-
- renderWithRouter(widget);
- });
-
- it('renders correct title', () => {
- const title = screen.getByTestId('widget-title');
-
- expect(title).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', '/enroll');
- });
-
- it('renders correct image with alt text', () => {
- const image = screen.getByTestId('image');
-
- expect(image).toHaveAttribute('alt', expect.stringContaining('AWS Cloud Developer'));
- expect(image).toHaveAttribute('src', awsDevImg.src);
- });
- });
-
- describe('Render widget with empty link', () => {
- it('renders registration will open soon with correct label and href', async () => {
- const widget = await TrainingProgram({ courseName: COURSE_TITLES.AWS_DEVOPS });
-
- renderWithRouter(widget);
-
- const buttonElement = screen.getByText(REGISTRATION_WILL_OPEN_SOON);
-
- expect(buttonElement).toBeVisible();
- expect(buttonElement).toHaveAttribute('href', '/');
- expect(buttonElement).toHaveTextContent(REGISTRATION_WILL_OPEN_SOON);
- });
-
- it('renders registration will open soon in russian with correct label and href', async () => {
- const widget = await TrainingProgram({ courseName: COURSE_TITLES.JS_RU });
-
- renderWithRouter(widget);
-
- const buttonElement = screen.getByText(REGISTRATION_WILL_OPEN_SOON_RU);
-
- expect(buttonElement).toBeVisible();
- expect(buttonElement).toHaveAttribute('href', '/');
- expect(buttonElement).toHaveTextContent(REGISTRATION_WILL_OPEN_SOON_RU);
- });
- });
-});
diff --git a/src/widgets/training-program/ui/training-program.tsx b/src/widgets/training-program/ui/training-program.tsx
deleted file mode 100644
index ace9eba67..000000000
--- a/src/widgets/training-program/ui/training-program.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-import { cloneElement } from 'react';
-import classNames from 'classnames/bind';
-import Image from 'next/image';
-
-import { isTrainingProgramType } from '@/shared/helpers/is-training-program';
-import { selectCourse } from '@/shared/hooks/use-course-by-title/utils/select-course';
-import { LinkCustom } from '@/shared/ui/link-custom';
-import { WidgetTitle } from '@/shared/ui/widget-title';
-import { CourseNamesKeys, contentMap, trainingProgramLink } from 'data';
-
-import styles from './training-program.module.scss';
-
-const cx = classNames.bind(styles);
-
-type TrainingProgramProps = {
- courseName: CourseNamesKeys;
- specify?: string;
-};
-
-export const TrainingProgram = async ({ courseName, specify = '' }: TrainingProgramProps) => {
- const course = await selectCourse(courseName);
- const { language } = course;
- const programName = specify ? `${courseName} ${specify}` : courseName;
- const contentName = isTrainingProgramType(programName) ? programName : courseName;
- const isCourseWithBadge = courseName.includes('badge');
-
- const { title, content, image } = contentMap[contentName];
- const registrationLinkText = course.enroll
- ? trainingProgramLink[language].linkLabel
- : trainingProgramLink[language].noLinkLabel;
- const enrollHref = course.enroll ?? '';
-
- return (
-
-
-
- {title}
-
- {content.map((component, index) => cloneElement(component, { key: index }))}
-
- {course && (
-
- {registrationLinkText}
-
- )}
-
-
-
-
-
- );
-};
diff --git a/src/widgets/video-block/helpers/is-video-block-section.ts b/src/widgets/video-block/helpers/is-video-block-section.ts
new file mode 100644
index 000000000..2a8ce6e97
--- /dev/null
+++ b/src/widgets/video-block/helpers/is-video-block-section.ts
@@ -0,0 +1,14 @@
+import { TypeVideoBlockWithoutUnresolvableLinksResponse } from '@/shared/types/contentful';
+import type { BaseEntry } from 'contentful';
+
+/**
+ * Determines if a given section is of the type "videoBlock".
+ *
+ * @param {TSection} section - The section to evaluate.
+ * @return {boolean} Returns true if the section is a "videoBlock" type, otherwise false.
+ */
+export function isVideoBlockSection(
+ section: TSection,
+): section is Extract {
+ return section?.sys?.contentType?.sys?.id === 'videoBlock';
+}
diff --git a/src/widgets/video-block/helpers/transform-video-block-section.ts b/src/widgets/video-block/helpers/transform-video-block-section.ts
new file mode 100644
index 000000000..4d075efca
--- /dev/null
+++ b/src/widgets/video-block/helpers/transform-video-block-section.ts
@@ -0,0 +1,22 @@
+import { TypeVideoBlockWithoutUnresolvableLinksResponse } from '@/shared/types/contentful';
+import { Section } from '@/views/course/types';
+
+export function transformVideoBlockSection(
+ section: TypeVideoBlockWithoutUnresolvableLinksResponse,
+): Section {
+ const id = section.sys.id;
+ const name = section.sys.contentType.sys.id;
+ const title = section.fields.title;
+ const url = section.fields.url;
+ const videoTitle = section.fields.videoTitle;
+
+ return {
+ id,
+ name,
+ data: {
+ title,
+ url,
+ videoTitle,
+ },
+ };
+}
diff --git a/src/widgets/video-block/index.ts b/src/widgets/video-block/index.ts
new file mode 100644
index 000000000..59becc768
--- /dev/null
+++ b/src/widgets/video-block/index.ts
@@ -0,0 +1,4 @@
+export type { VideoBlockSectionData } from './types';
+export { VideoBlock } from './ui/video-block';
+export { isVideoBlockSection } from './helpers/is-video-block-section';
+export { transformVideoBlockSection } from './helpers/transform-video-block-section';
diff --git a/src/widgets/video-block/types.ts b/src/widgets/video-block/types.ts
new file mode 100644
index 000000000..5aa97a532
--- /dev/null
+++ b/src/widgets/video-block/types.ts
@@ -0,0 +1,5 @@
+export type VideoBlockSectionData = {
+ title: string;
+ url: string;
+ videoTitle: string;
+};
diff --git a/src/widgets/about-video/ui/about-video.module.scss b/src/widgets/video-block/ui/video-block.module.scss
similarity index 100%
rename from src/widgets/about-video/ui/about-video.module.scss
rename to src/widgets/video-block/ui/video-block.module.scss
diff --git a/src/widgets/about-video/ui/about-video.tsx b/src/widgets/video-block/ui/video-block.tsx
similarity index 52%
rename from src/widgets/about-video/ui/about-video.tsx
rename to src/widgets/video-block/ui/video-block.tsx
index 6c349d12e..325344b21 100644
--- a/src/widgets/about-video/ui/about-video.tsx
+++ b/src/widgets/video-block/ui/video-block.tsx
@@ -1,28 +1,26 @@
import classNames from 'classnames/bind';
-import { RS_INTRO_URL } from '@/shared/constants';
-import { Language } from '@/shared/types';
import { WidgetTitle } from '@/shared/ui/widget-title';
-import { videoTitleLocalized } from 'data';
+import { VideoBlockSectionData } from '@/widgets/video-block/types';
-import styles from './about-video.module.scss';
+import styles from './video-block.module.scss';
const cx = classNames.bind(styles);
-type AboutVideoProps = { lang?: Language };
+type VideoBlockProps = VideoBlockSectionData;
-export const AboutVideo = ({ lang = 'en' }: AboutVideoProps) => {
+export const VideoBlock = ({ title, url, videoTitle }: VideoBlockProps) => {
return (
-
+
- {videoTitleLocalized[lang].title}
+ {title}