+
{label}
- {dropdownInner && (
+ {children && (
)}
- {dropdownInner && (
+ {children && (
- {dropdownInner}
+ {children}
)}
diff --git a/src/core/const/index.ts b/src/core/const/index.ts
index abd1b986c..09f5f1474 100644
--- a/src/core/const/index.ts
+++ b/src/core/const/index.ts
@@ -2,6 +2,10 @@ export const ANCHORS = {
ABOUT_COMMUNITY: 'about-community',
ABOUT_SCHOOL: 'about-school',
MENTORS_WANTED: 'mentors-wanted',
+ UPCOMING_COURSES: 'upcoming-courses',
+ EVENTS: 'events',
+ MERCH: 'merch',
+ CONTRIBUTE: 'contribute',
};
export const COURSE_STALE_AFTER_DAYS = 14;
diff --git a/src/shared/__tests__/visual/main.spec.ts b/src/shared/__tests__/visual/main.spec.ts
index b7e545fcb..a98a197ac 100644
--- a/src/shared/__tests__/visual/main.spec.ts
+++ b/src/shared/__tests__/visual/main.spec.ts
@@ -27,3 +27,15 @@ test('Main page mobile', async ({ page }) => {
await page.getByTestId('burger').click();
await expect(mobileMenu).not.toBeInViewport();
});
+
+test('Main page desktop menu', async ({ page }) => {
+ await page.goto(ROUTES.HOME);
+
+ const elements = page.getByTestId('menu-item');
+ const elementsCount = await elements.count();
+
+ for (let i = 0; i < elementsCount; i++) {
+ await elements.nth(i).hover();
+ await takeScreenshot(page, `Main page desktop - menu open ${i + 1}`);
+ }
+});
diff --git a/src/widgets/mobile-view/ui/mobile-view.tsx b/src/widgets/mobile-view/ui/mobile-view.tsx
index 6c578c409..3ae5807a1 100644
--- a/src/widgets/mobile-view/ui/mobile-view.tsx
+++ b/src/widgets/mobile-view/ui/mobile-view.tsx
@@ -4,6 +4,7 @@ import { ROUTES } from '@/core/const';
import { Course } from '@/entities/course';
import { Logo } from '@/shared/ui/logo';
import { SchoolMenu } from '@/widgets/school-menu';
+import { communityMenuStaticLinks, mentorshipCourses, schoolMenuStaticLinks } from 'data';
import styles from './mobile-view.module.scss';
@@ -18,9 +19,10 @@ const Divider = ({ color }: DividerProps) =>
void;
};
-export const MobileView = ({ type, courses }: MobileViewProps) => {
+export const MobileView = ({ type, courses, onClose }: MobileViewProps) => {
const color = type === 'header' ? 'dark' : 'light';
const logoView = type === 'header' ? null : 'with-border';
@@ -30,35 +32,80 @@ export const MobileView = ({ type, courses }: MobileViewProps) => {
-
+
RS School
-
+
+ {schoolMenuStaticLinks.map((link, i) => (
+
+ ))}
+
-
+
Courses
-
+
+ {courses.map((course) => (
+
+ ))}
+
-
+
Community
-
+
+ {communityMenuStaticLinks.map((link, i) => (
+
+ ))}
+
-
+
Mentorship
-
+
+ {mentorshipCourses.map((course) => (
+
+ ))}
+
);
};
diff --git a/src/widgets/school-menu/index.ts b/src/widgets/school-menu/index.ts
index d88ff088d..5a81493ba 100644
--- a/src/widgets/school-menu/index.ts
+++ b/src/widgets/school-menu/index.ts
@@ -1 +1 @@
-export { SchoolMenu } from './ui/school-menu';
+export { SchoolMenu } from './ui/school-menu/school-menu';
diff --git a/src/widgets/school-menu/school-menu.test.tsx b/src/widgets/school-menu/school-menu.test.tsx
deleted file mode 100644
index 867c9570d..000000000
--- a/src/widgets/school-menu/school-menu.test.tsx
+++ /dev/null
@@ -1,83 +0,0 @@
-import { screen } from '@testing-library/react';
-import { SchoolMenu } from './ui/school-menu';
-import { Course } from '@/entities/course';
-import { MOCKED_IMAGE_PATH, mockedCourses } from '@/shared/__tests__/constants';
-import { renderWithRouter } from '@/shared/__tests__/utils';
-import { COURSE_TITLES } from 'data';
-
-describe('SchoolMenu', () => {
- const aws = mockedCourses.find(
- (course) => course.title === COURSE_TITLES.AWS_FUNDAMENTALS,
- ) as Course;
- const react = mockedCourses.find((course) => course.title === COURSE_TITLES.REACT) as Course;
-
- it('renders without crashing and displays "rs school" heading', () => {
- renderWithRouter(
);
-
- const headingElement = screen.getByRole('heading', { name: /rs school/i });
-
- expect(headingElement).toBeInTheDocument();
- });
-
- it('displays correct links and descriptions with "rs school" props', () => {
- const { container } = renderWithRouter(
-
,
- );
-
- expect(screen.getAllByRole('link')).toHaveLength(2);
-
- const links = screen.getAllByRole('link');
-
- links.forEach((link) => {
- expect(link).toBeInTheDocument();
- });
-
- const descriptions = container.getElementsByTagName('small');
-
- for (const description of descriptions) {
- expect(description).toBeInTheDocument();
- }
- });
-
- it('renders without crashing and displays "all courses" heading', () => {
- renderWithRouter(
);
-
- const headingElement = screen.getByRole('heading', { name: /all courses/i });
-
- expect(headingElement).toBeInTheDocument();
- });
-
- it('renders [mentorshipId] correct when "all courses" heading is passed', () => {
- renderWithRouter(
);
-
- const imageAWS = screen.getByRole('img', { name: aws.title });
-
- expect(imageAWS).toHaveAttribute('src', MOCKED_IMAGE_PATH.src);
- const imageReact = screen.getByRole('img', { name: react.title });
-
- expect(imageReact).toHaveAttribute('src', MOCKED_IMAGE_PATH.src);
- });
-
- it('renders correct link description when date is passed', () => {
- const { container } = renderWithRouter(
-
,
- );
-
- const descriptions = container.getElementsByClassName('description');
-
- expect(descriptions).toHaveLength(6);
- expect(descriptions[0]).toHaveTextContent(/tbd/i);
- expect(descriptions[3]).toHaveTextContent(/tbd/i);
- });
-
- it('renders correct link for "AWS Fundamentals" and "React JS [mentorshipId]"', () => {
- renderWithRouter(
);
-
- const links = screen.getAllByRole('link');
- const linkReact = links.at(3);
- const linkAWS = links.at(-1);
-
- expect(linkAWS).toHaveAttribute('href', aws.detailsUrl);
- expect(linkReact).toHaveAttribute('href', react.detailsUrl);
- });
-});
diff --git a/src/widgets/school-menu/types.ts b/src/widgets/school-menu/types.ts
new file mode 100644
index 000000000..f00162b20
--- /dev/null
+++ b/src/widgets/school-menu/types.ts
@@ -0,0 +1 @@
+export type Color = 'dark' | 'light';
diff --git a/src/widgets/school-menu/ui/school-item/school-item.module.scss b/src/widgets/school-menu/ui/school-item/school-item.module.scss
new file mode 100644
index 000000000..fdfca1147
--- /dev/null
+++ b/src/widgets/school-menu/ui/school-item/school-item.module.scss
@@ -0,0 +1,60 @@
+.school-item {
+ display: flex;
+ gap: 5px;
+ column-gap: 15px;
+ align-items: center;
+
+ .title {
+ font-weight: $font-weight-medium;
+ line-height: 20px;
+ text-align: start;
+
+ &.dark {
+ color: $color-gray-600;
+ }
+
+ &.light {
+ color: $color-gray-200;
+ }
+ }
+
+ &:hover {
+ .title {
+ &.dark {
+ color: $color-black;
+ }
+
+ &.light {
+ color: $color-gray-400;
+ }
+ }
+ }
+
+ &.with-icon {
+ display: flex;
+ flex-direction: row;
+ gap: 15px;
+ align-items: center;
+ justify-content: flex-start;
+
+ .details {
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
+ align-items: flex-start;
+ justify-content: flex-start;
+ }
+ }
+
+ .description-wrapper {
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
+ align-items: flex-start;
+
+ .description {
+ font-size: 12px;
+ color: $color-gray-500;
+ }
+ }
+}
diff --git a/src/widgets/school-menu/ui/school-item/school-item.tsx b/src/widgets/school-menu/ui/school-item/school-item.tsx
index 4189d6220..cb63fff0b 100644
--- a/src/widgets/school-menu/ui/school-item/school-item.tsx
+++ b/src/widgets/school-menu/ui/school-item/school-item.tsx
@@ -1,63 +1,49 @@
-import Image from 'next/image';
+/* eslint-disable @stylistic/jsx-closing-bracket-location */
+import { HTMLProps } from 'react';
+import classNames from 'classnames/bind';
+import Image, { StaticImageData } from 'next/image';
import Link from 'next/link';
-import { GenericItemProps } from '../school-list/school-list';
-import type { Course } from '@/entities/course';
-import { DateStart } from '@/shared/ui/date-start';
-import { MentorshipCourse } from 'data';
+import { Color } from '@/widgets/school-menu/types';
-interface SchoolItemProps {
- item: MentorshipCourse | Course | GenericItemProps;
- color: 'dark' | 'light';
-}
+import styles from './school-item.module.scss';
-export const SchoolItem = ({ item, color }: SchoolItemProps) => {
- const courseDate = 'startDate' in item && item.startDate;
- const registrationEndDate = 'registrationEndDate' in item && item.registrationEndDate;
- const descriptionText = 'description' in item ? item.description : courseDate;
+const cx = classNames.bind(styles);
- const descriptionContent = (
- <>
-
{item.title}
- {courseDate && registrationEndDate
- ? (
-
-
- )
- : (
-
{descriptionText}
- )}
- >
- );
-
- const descriptionBlock =
- 'description' in item
- ? (
- descriptionContent
- )
- : (
-
{descriptionContent}
- );
+type SchoolItemProps = HTMLProps
& {
+ title: string;
+ url: string;
+ description?: string;
+ icon?: StaticImageData;
+ color?: Color;
+};
+export const SchoolItem = ({
+ icon,
+ description,
+ title,
+ color = 'dark',
+ url,
+ ...props
+}: SchoolItemProps) => {
return (
-
-
- {'iconSmall' in item && (
-
- )}
- {descriptionBlock}
+
+
+ {icon && }
+
+ {title}
+ {description && (
+
+ {description}
+
+ )}
+
);
diff --git a/src/widgets/school-menu/ui/school-list/school-list.tsx b/src/widgets/school-menu/ui/school-list/school-list.tsx
deleted file mode 100644
index d14bcaf8f..000000000
--- a/src/widgets/school-menu/ui/school-list/school-list.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import { SchoolItem } from '../school-item/school-item';
-import type { Course } from '@/entities/course';
-import { MentorshipCourse } from 'data';
-
-export interface GenericItemProps {
- title: string;
- detailsUrl: string;
- description: string;
-}
-
-interface SchoolListProps {
- list: MentorshipCourse[] | Course[] | GenericItemProps[];
- color: 'dark' | 'light';
-}
-
-export const SchoolList = ({ list, color }: SchoolListProps) => {
- const className =
- !!list && !!list[0] && 'description' in list[0]
- ? 'school-list'
- : 'school-list school-list_width';
-
- return (
-
- {list?.map((item) => )}
-
- );
-};
diff --git a/src/widgets/school-menu/ui/school-menu.scss b/src/widgets/school-menu/ui/school-menu.scss
deleted file mode 100644
index e2fd0012f..000000000
--- a/src/widgets/school-menu/ui/school-menu.scss
+++ /dev/null
@@ -1,98 +0,0 @@
-.school-menu {
- display: flex;
- flex-direction: column;
- gap: 16px;
- align-items: baseline;
- justify-content: flex-start;
-
- color: $color-gray-100;
-
- & .heading {
- margin: 0;
- font-size: 12px;
- font-weight: $font-weight-medium;
- text-transform: uppercase;
-
- &.dark {
- color: $color-black;
- }
-
- &.light {
- color: $color-gray-400;
- }
- }
-
- .school-list {
- display: flex;
- flex-flow: column wrap;
- gap: 19px;
- column-gap: 40px;
- align-items: baseline;
-
- max-height: 280px;
-
- list-style-type: none;
-
- &_width {
- width: 512px;
-
- @media (width <= 795px) {
- width: auto;
- }
- }
-
- & .school-item {
- display: flex;
- flex-direction: column;
- gap: 5px;
- align-items: baseline;
- justify-content: flex-start;
-
- &.with-icon {
- display: flex;
- flex-direction: row;
- gap: 15px;
- align-items: center;
- justify-content: flex-start;
-
- .details {
- display: flex;
- flex-direction: column;
- gap: 5px;
- align-items: flex-start;
- justify-content: flex-start;
- }
- }
-
- span {
- @extend %transition-all;
-
- font-weight: $font-weight-medium;
- line-height: 20px;
- text-align: start;
-
- &.dark {
- color: $color-gray-600;
- }
-
- &.light {
- color: $color-gray-200;
- }
-
- &:hover {
- color: $color-gray-400;
- }
- }
-
- .description {
- font-size: 12px;
- color: $color-gray-500;
- }
- }
-
- @include media-mobile-landscape {
- column-gap: 10px;
- max-height: 600px;
- }
- }
-}
diff --git a/src/widgets/school-menu/ui/school-menu.tsx b/src/widgets/school-menu/ui/school-menu.tsx
deleted file mode 100644
index 171e048b0..000000000
--- a/src/widgets/school-menu/ui/school-menu.tsx
+++ /dev/null
@@ -1,84 +0,0 @@
-import { GenericItemProps, SchoolList } from './school-list/school-list';
-import { ANCHORS } from '@/core/const';
-import type { Course } from '@/entities/course';
-import { MentorshipCourse, MentorshipDefaultRouteKeys, mentorshipCourses } from 'data';
-
-import './school-menu.scss';
-
-const schoolMenuStaticLinks = [
- {
- title: 'About RS School',
- detailsUrl: `/#${ANCHORS.ABOUT_SCHOOL}`,
- description: 'Free online education',
- },
- {
- title: 'Upcoming courses',
- detailsUrl: '/#upcoming-courses',
- description: 'Schedule your study',
- },
-];
-
-const communityMenuStaticLinks = [
- {
- title: 'About',
- detailsUrl: `/community/#${ANCHORS.ABOUT_COMMUNITY}`,
- description: 'Who we are',
- },
- {
- title: 'Events',
- detailsUrl: '/community/#events',
- description: 'Meet us at events',
- },
- {
- title: 'Merch',
- detailsUrl: '/community/#merch',
- description: 'Sloths for your daily life',
- },
- {
- title: 'Contribute',
- detailsUrl: '/community/#contribute',
- description: 'Assist us and improve yourself',
- },
-];
-
-interface SchoolMenuProps {
- heading: 'rs school' | 'all courses' | 'community' | MentorshipDefaultRouteKeys;
- courses: Course[];
- hasTitle?: boolean;
- color?: 'dark' | 'light';
-}
-
-function getMenuItems(
- heading: SchoolMenuProps['heading'],
- courses: Course[],
- mentorshipCourses: MentorshipCourse[],
-): GenericItemProps[] | Course[] | MentorshipCourse[] {
- switch (heading) {
- case 'all courses':
- return courses;
- case 'rs school':
- return schoolMenuStaticLinks;
- case 'community':
- return communityMenuStaticLinks;
- case 'mentorship':
- return mentorshipCourses;
- default:
- return [];
- }
-}
-
-export const SchoolMenu = ({
- heading,
- courses,
- hasTitle = true,
- color = 'light',
-}: SchoolMenuProps) => {
- const menuItems = getMenuItems(heading, courses, mentorshipCourses);
-
- return (
-
- {hasTitle &&
{heading}
}
-
-
- );
-};
diff --git a/src/widgets/school-menu/ui/school-menu/school-menu.module.scss b/src/widgets/school-menu/ui/school-menu/school-menu.module.scss
new file mode 100644
index 000000000..1bd26433e
--- /dev/null
+++ b/src/widgets/school-menu/ui/school-menu/school-menu.module.scss
@@ -0,0 +1,44 @@
+.school-menu {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ color: $color-gray-100;
+
+ .heading {
+ margin: 0;
+ font-size: 12px;
+ font-weight: $font-weight-medium;
+ text-transform: uppercase;
+
+ &.dark {
+ color: $color-black;
+ }
+
+ &.light {
+ color: $color-gray-400;
+ }
+ }
+
+ .school-list {
+ display: flex;
+ flex-flow: column wrap;
+ gap: 19px 40px;
+
+ max-height: 280px;
+
+ list-style-type: none;
+
+ &:has(:nth-child(5)) {
+ width: 512px;
+
+ @include media-tablet {
+ width: unset;
+ }
+ }
+
+ @include media-mobile-landscape {
+ column-gap: 10px;
+ max-height: 600px;
+ }
+ }
+}
diff --git a/src/widgets/school-menu/ui/school-menu/school-menu.test.tsx b/src/widgets/school-menu/ui/school-menu/school-menu.test.tsx
new file mode 100644
index 000000000..c760222f1
--- /dev/null
+++ b/src/widgets/school-menu/ui/school-menu/school-menu.test.tsx
@@ -0,0 +1,134 @@
+import { screen } from '@testing-library/react';
+import { SchoolMenu } from '../school-menu/school-menu';
+import { Course } from '@/entities/course';
+import { mockedCourses } from '@/shared/__tests__/constants';
+import { renderWithRouter } from '@/shared/__tests__/utils';
+import { COURSE_TITLES, schoolMenuStaticLinks } from 'data';
+
+describe('SchoolMenu', () => {
+ const aws = mockedCourses.find(
+ (course) => course.title === COURSE_TITLES.AWS_FUNDAMENTALS,
+ ) as Course;
+ const react = mockedCourses.find((course) => course.title === COURSE_TITLES.REACT) as Course;
+
+ it('renders without crashing and displays "rs school" heading', () => {
+ renderWithRouter(
+
+ {schoolMenuStaticLinks.map((link) => (
+
+ ))}
+ ,
+ );
+
+ const headingElement = screen.getByRole('heading', { name: /rs school/i });
+
+ expect(headingElement).toBeInTheDocument();
+ });
+
+ it('displays correct links and descriptions with "rs school" props', () => {
+ const { container } = renderWithRouter(
+
+ {schoolMenuStaticLinks.map((link) => (
+
+ ))}
+ ,
+ );
+
+ expect(screen.getAllByRole('link')).toHaveLength(2);
+
+ expect(container.getElementsByTagName('small')).toHaveLength(2);
+ });
+
+ it('renders without crashing and displays "all courses" heading', () => {
+ renderWithRouter(
+
+ {mockedCourses.map((course) => (
+
+ ))}
+ ,
+ );
+
+ const headingElement = screen.getByRole('heading', { name: /all courses/i });
+
+ expect(headingElement).toBeInTheDocument();
+ });
+
+ it('renders [mentorshipId] correct when "all courses" heading is passed', () => {
+ renderWithRouter(
+
+ {mockedCourses.map((course) => (
+
+ ))}
+ ,
+ );
+
+ const images = screen.getAllByTestId('school-item-icon');
+
+ expect(images).toHaveLength(6);
+ images.forEach((img) => expect(img).toHaveAttribute('aria-hidden', 'true'));
+ });
+
+ it('renders correct link description when date is passed', () => {
+ renderWithRouter(
+
+ {mockedCourses.map((course) => (
+
+ ))}
+ ,
+ );
+
+ const descriptions = screen.getAllByTestId('school-item-description');
+
+ expect(descriptions).toHaveLength(6);
+ expect(descriptions[0]).toHaveTextContent(/Jun 24, 2024/i);
+ expect(descriptions[3]).toHaveTextContent(/Jul 1, 2024/i);
+ });
+
+ it('renders correct link for "AWS Fundamentals" and "React JS [mentorshipId]"', () => {
+ renderWithRouter(
+
+ {mockedCourses.map((course) => (
+
+ ))}
+ ,
+ );
+
+ const links = screen.getAllByRole('link');
+ const linkReact = links.at(3);
+ const linkAWS = links.at(-1);
+
+ expect(linkAWS).toHaveAttribute('href', aws.detailsUrl);
+ expect(linkReact).toHaveAttribute('href', react.detailsUrl);
+ });
+});
diff --git a/src/widgets/school-menu/ui/school-menu/school-menu.tsx b/src/widgets/school-menu/ui/school-menu/school-menu.tsx
new file mode 100644
index 000000000..5bfedefcc
--- /dev/null
+++ b/src/widgets/school-menu/ui/school-menu/school-menu.tsx
@@ -0,0 +1,25 @@
+import { HTMLProps, PropsWithChildren } from 'react';
+import classNames from 'classnames/bind';
+import { Color } from '@/widgets/school-menu/types';
+import { SchoolItem } from '@/widgets/school-menu/ui/school-item/school-item';
+
+import styles from './school-menu.module.scss';
+
+const cx = classNames.bind(styles);
+
+type SchoolMenuProps = PropsWithChildren &
+ HTMLProps & {
+ heading?: string;
+ color?: Color;
+ };
+
+export const SchoolMenu = ({ heading, color = 'light', children, className }: SchoolMenuProps) => {
+ return (
+
+ {heading &&
{heading}
}
+
+
+ );
+};
+
+SchoolMenu.Item = SchoolItem;