From 39d374dd0515e4cfe00bc5797996a8b12b783015 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 14 Dec 2024 16:39:32 +0200 Subject: [PATCH 1/5] chore: bump nanoid from 3.3.7 to 3.3.8 in the npm_and_yarn group (#688) Bumps the npm_and_yarn group with 1 update: [nanoid](https://github.com/ai/nanoid). Updates `nanoid` from 3.3.7 to 3.3.8 - [Release notes](https://github.com/ai/nanoid/releases) - [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md) - [Commits](https://github.com/ai/nanoid/compare/3.3.7...3.3.8) --- updated-dependencies: - dependency-name: nanoid dependency-type: indirect dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index d600c7807..0c9ed3f81 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8449,16 +8449,15 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, From 6802e36214de0724dbfd18e5e02e4f1b44488c5c Mon Sep 17 00:00:00 2001 From: Kristina <93883470+KristiBo@users.noreply.github.com> Date: Sun, 15 Dec 2024 20:53:44 +0300 Subject: [PATCH 2/5] 567-refactor: Widget support (#684) * refactor: 567 - change styles to modules * refactor: 567 - change class names and replace tag selectors * refactor: 567 - add semantic tags * refactor: 567 - add descriptive alt attribute * refactor: 567 - move test file to the ui folder * refactor: 567 - update tests * refactor: 567 - change styles * refactor: 567 - change data-testid --- src/widgets/support/support.test.tsx | 39 ---------------- .../ui/{support.scss => support.module.scss} | 25 ++++------- src/widgets/support/ui/support.test.tsx | 44 +++++++++++++++++++ src/widgets/support/ui/support.tsx | 30 +++++++++---- 4 files changed, 74 insertions(+), 64 deletions(-) delete mode 100644 src/widgets/support/support.test.tsx rename src/widgets/support/ui/{support.scss => support.module.scss} (67%) create mode 100644 src/widgets/support/ui/support.test.tsx diff --git a/src/widgets/support/support.test.tsx b/src/widgets/support/support.test.tsx deleted file mode 100644 index f5774d37d..000000000 --- a/src/widgets/support/support.test.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { screen } from '@testing-library/react'; -import { beforeEach, describe, expect, it } from 'vitest'; -import { Support } from './ui/support'; -import { renderWithRouter } from '@/shared/__tests__/utils'; - -describe('Support', () => { - beforeEach(() => { - renderWithRouter(); - }); - - it('renders the title correctly', () => { - const titleElement = screen.getByText('Support Us'); - - expect(titleElement).toBeVisible(); - }); - - it('renders the subtitle correctly', () => { - const subtitleElement1 = screen.getByText( - 'Your donations help us cover hosting, domains, licenses, and advertising for courses and events. Every donation, big or small, helps!', - ); - const subtitleElement2 = screen.getByText('Thank you for your support!'); - - expect(subtitleElement1).toBeVisible(); - expect(subtitleElement2).toBeVisible(); - }); - - it('renders a button which has correct text and href', () => { - const buttonElement = screen.getByRole('link', { name: /donate now/i }); - - expect(buttonElement).toBeVisible(); - expect(buttonElement).toHaveAttribute('href', 'https://opencollective.com/rsschool'); - }); - - it('renders the image with correct alt text', () => { - const imageElement = screen.getByAltText('support-us'); - - expect(imageElement).toBeInTheDocument(); - }); -}); diff --git a/src/widgets/support/ui/support.scss b/src/widgets/support/ui/support.module.scss similarity index 67% rename from src/widgets/support/ui/support.scss rename to src/widgets/support/ui/support.module.scss index 6d6100e10..8e5d0c2e9 100644 --- a/src/widgets/support/ui/support.scss +++ b/src/widgets/support/ui/support.module.scss @@ -1,27 +1,20 @@ -.support { - &.container { - background-color: $color-gray-100; - } +.support-container { + background-color: $color-gray-100; - &.content { + .support-content { display: flex; - flex-direction: row; gap: 50px; - align-items: flex-start; + align-items: center; justify-content: space-between; - & .info { + .support-info { width: 640px; font-weight: 500; color: $color-black; text-align: left; - a { + .support-link { margin-top: 24px; - - @include media-tablet { - margin-bottom: 24px; - } } @include media-laptop { @@ -29,7 +22,7 @@ } } - & .picture { + .sloth-mascot { width: 348px; height: 288px; @@ -40,7 +33,6 @@ @include media-laptop { width: 300px; height: auto; - margin-top: 16px; object-fit: contain; } @@ -51,8 +43,7 @@ @include media-tablet-large { flex-direction: column; - gap: 0; - align-items: center; + gap: 40px; } } } diff --git a/src/widgets/support/ui/support.test.tsx b/src/widgets/support/ui/support.test.tsx new file mode 100644 index 000000000..a2bc59d60 --- /dev/null +++ b/src/widgets/support/ui/support.test.tsx @@ -0,0 +1,44 @@ +import { screen } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; +import { Support } from './support'; +import { renderWithRouter } from '@/shared/__tests__/utils'; +import supportImg from '@/shared/assets/support.webp'; + +describe('Support Component', () => { + const mockedData = { + titleText: 'Support Us', + firstParagraphText: + /Your donations help us cover hosting, domains, licenses, and advertising for courses and events./i, + secondParagraphText: 'Thank you for your support!', + href: 'https://opencollective.com/rsschool', + image: supportImg, + alt: 'A sloth mascot with a piggy bank in his hands', + }; + + const { titleText, firstParagraphText, secondParagraphText, href, image, alt } = mockedData; + + it('renders the component content correctly', () => { + renderWithRouter(); + const supportSection = screen.getByTestId('support-section'); + const title = screen.getByTestId('widget-title'); + const paragraphs = screen.getAllByTestId('paragraph'); + const link = screen.getByTestId('link-support'); + const slothImage = screen.getByTestId('sloth-mascot'); + + expect(supportSection).toBeVisible(); + expect(title).toBeVisible(); + paragraphs.forEach((paragraph) => { + expect(paragraph).toBeVisible(); + }); + expect(link).toBeVisible(); + expect(slothImage).toBeVisible(); + + expect(title).toHaveTextContent(titleText); + expect(paragraphs[0]).toHaveTextContent(firstParagraphText); + expect(paragraphs[1]).toHaveTextContent(secondParagraphText); + + expect(link).toHaveAttribute('href', href); + expect(slothImage).toHaveAttribute('src', image.src); + expect(slothImage).toHaveAttribute('alt', alt); + }); +}); diff --git a/src/widgets/support/ui/support.tsx b/src/widgets/support/ui/support.tsx index f0bd03c0c..4e7e13299 100644 --- a/src/widgets/support/ui/support.tsx +++ b/src/widgets/support/ui/support.tsx @@ -1,3 +1,4 @@ +import classNames from 'classnames/bind'; import Image from 'next/image'; import { LINKS } from '@/core/const'; import image from '@/shared/assets/support.webp'; @@ -5,12 +6,14 @@ import { LinkCustom } from '@/shared/ui/link-custom'; import { Paragraph } from '@/shared/ui/paragraph'; import { WidgetTitle } from '@/shared/ui/widget-title'; -import './support.scss'; +import styles from './support.module.scss'; + +const cx = classNames.bind(styles); export const Support = () => ( -
-
-
+
+
+
Support Us @@ -19,11 +22,22 @@ export const Support = () => ( events. Every donation, big or small, helps! Thank you for your support! - + Donate now -
- support-us + + A sloth mascot with a piggy bank in his hands
-
+ ); From 7306559a6dd1edb1f530a6e0bbe243c95c0508cb Mon Sep 17 00:00:00 2001 From: Bogdan Date: Mon, 16 Dec 2024 10:20:29 +0200 Subject: [PATCH 3/5] 563-refactor: Widget school menu (#659) * refactor: 634 - resolve sass deprecation warning * refactor: 563 - move school menu to dedicated folder * refactor: 563 - break down styles to their own files * refactor: 563 - school menu scss * refactor: 563 - move all anchor links to constant * refactor: 563 - move get menu items to separate file * refactor: 563 - move links to dev-data * refactor: 563 - create color type * refactor: 563 - merge school list with school menu component * refactor: 563 - menu as compound component * fix: 563 - test issues * refactor: 563 - remove type re-export * refactor: 563 - replace interface with type * refactor: 563 - replace tag selectors with classes * fix: 563 - stylelint issue * fix: 563 - incorrect course dates * fix: 563 - css property alignment * fix: 563 - remove important * refactor: 563 - remove redundant test case * feat: add desktop menu playwright test * feat: update desktop menu test * fix: desktop menu test issue * fix: menu incorrect width on safari issue * refactor: remove redundant style * fix: overflow issue * fix: mobile menu height issue * fix: 563 - to close menu on nav item click * fix: 563 - minor issues * fix: 563 - to add aria-hidden to menu icons --- dev-data/index.ts | 1 + dev-data/school-menu-links.ts | 37 +++++ .../components/footer/desktop-view.tsx | 26 +++- .../components/header/header.module.scss | 4 +- .../base-layout/components/header/header.tsx | 112 ++++++++------- .../components/header/nav-item/nav-item.tsx | 24 ++-- src/core/const/index.ts | 4 + src/shared/__tests__/visual/main.spec.ts | 12 ++ src/widgets/mobile-view/ui/mobile-view.tsx | 65 +++++++-- src/widgets/school-menu/index.ts | 2 +- src/widgets/school-menu/school-menu.test.tsx | 83 ----------- src/widgets/school-menu/types.ts | 1 + .../ui/school-item/school-item.module.scss | 60 ++++++++ .../ui/school-item/school-item.tsx | 94 ++++++------ .../ui/school-list/school-list.tsx | 27 ---- src/widgets/school-menu/ui/school-menu.scss | 98 ------------- src/widgets/school-menu/ui/school-menu.tsx | 84 ----------- .../ui/school-menu/school-menu.module.scss | 44 ++++++ .../ui/school-menu/school-menu.test.tsx | 134 ++++++++++++++++++ .../ui/school-menu/school-menu.tsx | 25 ++++ 20 files changed, 512 insertions(+), 425 deletions(-) create mode 100644 dev-data/school-menu-links.ts delete mode 100644 src/widgets/school-menu/school-menu.test.tsx create mode 100644 src/widgets/school-menu/types.ts create mode 100644 src/widgets/school-menu/ui/school-item/school-item.module.scss delete mode 100644 src/widgets/school-menu/ui/school-list/school-list.tsx delete mode 100644 src/widgets/school-menu/ui/school-menu.scss delete mode 100644 src/widgets/school-menu/ui/school-menu.tsx create mode 100644 src/widgets/school-menu/ui/school-menu/school-menu.module.scss create mode 100644 src/widgets/school-menu/ui/school-menu/school-menu.test.tsx create mode 100644 src/widgets/school-menu/ui/school-menu/school-menu.tsx diff --git a/dev-data/index.ts b/dev-data/index.ts index 8e3fb3bcd..e9cfd3b86 100644 --- a/dev-data/index.ts +++ b/dev-data/index.ts @@ -33,6 +33,7 @@ export { awsFundamentals } from './awsFundamentals.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 } from './training-program.data'; export { contentMapAbout, introLocalizedContent } from './about-course.data'; export { contributeOptions } from './contribute-options.data'; diff --git a/dev-data/school-menu-links.ts b/dev-data/school-menu-links.ts new file mode 100644 index 000000000..7f8801f97 --- /dev/null +++ b/dev-data/school-menu-links.ts @@ -0,0 +1,37 @@ +import { ANCHORS, ROUTES } from '@/core/const'; + +export const schoolMenuStaticLinks = [ + { + title: 'About RS School', + detailsUrl: `/#${ANCHORS.ABOUT_SCHOOL}`, + description: 'Free online education', + }, + { + title: 'Upcoming courses', + detailsUrl: `/#${ANCHORS.UPCOMING_COURSES}`, + description: 'Schedule your study', + }, +]; + +export const communityMenuStaticLinks = [ + { + title: 'About', + detailsUrl: `/${ROUTES.COMMUNITY}/#${ANCHORS.ABOUT_COMMUNITY}`, + description: 'Who we are', + }, + { + title: 'Events', + detailsUrl: `/${ROUTES.COMMUNITY}/#${ANCHORS.EVENTS}`, + description: 'Meet us at events', + }, + { + title: 'Merch', + detailsUrl: `/${ROUTES.COMMUNITY}/#${ANCHORS.MERCH}`, + description: 'Sloths for your daily life', + }, + { + title: 'Contribute', + detailsUrl: `/${ROUTES.COMMUNITY}/#${ANCHORS.CONTRIBUTE}`, + description: 'Assist us and improve yourself', + }, +]; diff --git a/src/core/base-layout/components/footer/desktop-view.tsx b/src/core/base-layout/components/footer/desktop-view.tsx index 4dd2b94c2..38b7cea05 100644 --- a/src/core/base-layout/components/footer/desktop-view.tsx +++ b/src/core/base-layout/components/footer/desktop-view.tsx @@ -1,6 +1,7 @@ import { AboutList } from './about-list'; import { getCourses } from '@/entities/course/api/course-api'; import { SchoolMenu } from '@/widgets/school-menu'; +import { schoolMenuStaticLinks } from 'data'; export const DesktopView = async () => { const courses = await getCourses(); @@ -9,11 +10,32 @@ export const DesktopView = async () => {
- + + {schoolMenuStaticLinks.map((link, i) => ( + + ))} +
- + + {courses.map((course) => ( + + ))} +
); diff --git a/src/core/base-layout/components/header/header.module.scss b/src/core/base-layout/components/header/header.module.scss index 0033401ca..d0d608f8c 100644 --- a/src/core/base-layout/components/header/header.module.scss +++ b/src/core/base-layout/components/header/header.module.scss @@ -92,8 +92,8 @@ width: 100%; height: min-content; - min-height: 100vh; - max-height: 100vh; + min-height: 100dvh; + max-height: 100dvh; margin-top: 0; padding: 4px 24px 28px 16px; diff --git a/src/core/base-layout/components/header/header.tsx b/src/core/base-layout/components/header/header.tsx index da8e42f20..6ce4881e0 100644 --- a/src/core/base-layout/components/header/header.tsx +++ b/src/core/base-layout/components/header/header.tsx @@ -10,6 +10,7 @@ import { Course } from '@/entities/course'; import { Logo } from '@/shared/ui/logo'; import { MobileView } from '@/widgets/mobile-view'; import { SchoolMenu } from '@/widgets/school-menu'; +import { communityMenuStaticLinks, mentorshipCourses, schoolMenuStaticLinks } from 'data'; import styles from './header.module.scss'; @@ -19,34 +20,9 @@ type HeaderProps = { courses: Course[]; }; -const navLinks = [ - { - label: 'RS School', - href: ROUTES.HOME, - heading: 'rs school', - }, - { - label: 'Courses', - href: `/${ROUTES.COURSES}`, - heading: 'all courses', - }, - { - label: 'Community', - href: `/${ROUTES.COMMUNITY}`, - heading: 'community', - }, - { - label: 'Mentorship', - href: `/${ROUTES.MENTORSHIP}`, - heading: 'mentorship', - }, -] as const; - export const Header = ({ courses }: HeaderProps) => { const [isMenuOpen, setMenuOpen] = useState(false); const [color, setColor] = useState('gray'); - const [hash, setHash] = useState(''); - const [key, setKey] = useState(''); const pathname = usePathname(); // const headerAccentColor = pathname.includes(ROUTES.MENTORSHIP) ? 'blue' : 'gray'; @@ -60,6 +36,10 @@ export const Header = ({ courses }: HeaderProps) => { setMenuOpen((prev) => !prev); }; + const handleMenuClose = () => { + setMenuOpen(false); + }; + useEffect(() => { const listenScrollEvent = () => { const scrollY = window.scrollY; @@ -81,18 +61,8 @@ export const Header = ({ courses }: HeaderProps) => { }, [headerAccentColor]); useEffect(() => { - if (typeof window !== 'undefined') { - setHash(window.location.hash); - setKey(window.location.href); - } - }, [pathname]); - - useEffect(() => { - if (location.pathname) { - setMenuOpen(false); - setColor(headerAccentColor); - } - }, [key, hash, pathname, headerAccentColor]); + setColor(headerAccentColor); + }, [pathname, headerAccentColor]); return ( diff --git a/src/core/base-layout/components/header/nav-item/nav-item.tsx b/src/core/base-layout/components/header/nav-item/nav-item.tsx index 599e5019a..c6de06c85 100644 --- a/src/core/base-layout/components/header/nav-item/nav-item.tsx +++ b/src/core/base-layout/components/header/nav-item/nav-item.tsx @@ -1,7 +1,7 @@ import { FocusEvent, KeyboardEvent, - ReactNode, + PropsWithChildren, useEffect, useRef, useState, @@ -16,13 +16,12 @@ import styles from './nav-item.module.scss'; const cx = classNames.bind(styles); -type NavItemProps = { +type NavItemProps = PropsWithChildren & { label: string; href: string; - dropdownInner?: ReactNode; }; -export const NavItem = ({ label, href, dropdownInner }: NavItemProps) => { +export const NavItem = ({ label, href, children }: NavItemProps) => { const [isDropdownOpen, setDropdownOpen] = useState(false); const dropdownToggleRef = useRef(null); @@ -58,20 +57,25 @@ export const NavItem = ({ label, href, dropdownInner }: NavItemProps) => { }, [pathname]); return ( -
+
{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 && ( - {item.title} - )} - {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}

    } +
      {children}
    +
    + ); +}; + +SchoolMenu.Item = SchoolItem; From 07ccaf58370f15d0f8c6231979b36caa9908d0bb Mon Sep 17 00:00:00 2001 From: Kristina <93883470+KristiBo@users.noreply.github.com> Date: Mon, 16 Dec 2024 13:09:59 +0300 Subject: [PATCH 4/5] feat: 691 - update registration link (#692) --- dev-data/courses.data.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-data/courses.data.ts b/dev-data/courses.data.ts index 98f7d4536..c693f7f2d 100644 --- a/dev-data/courses.data.ts +++ b/dev-data/courses.data.ts @@ -132,7 +132,7 @@ export const courses: Course[] = [ language: ['en'], mode: 'online', detailsUrl: `/${ROUTES.COURSES}/${ROUTES.NODE_JS}`, - enroll: 'https://wearecommunity.io/events/nodejs-2024q3', + enroll: 'https://wearecommunity.io/events/nodejs-2025q2', backgroundStyle: { backgroundColor: '#F0F9F4', accentColor: '#AEDF36', From c01dbbd91a8b592a39aef8c4b65a5babef61b2c5 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Mon, 16 Dec 2024 13:57:28 +0200 Subject: [PATCH 5/5] 564-refactor: Widget speakers (#668) * refactor: 564 - change css to modules * fix: 564 - adaptive issues * refactor: 564 - remove unused styles * refactor: 564 - get rid of divs * fix: 564 - test issue * refactor: 564 - move test to ui folder * refactor: 564 - add more meaningful alt attribute * refactor: 564 - encapsulate rs email in shard constant * refactor: 564 - replace EmailIcon with plain Image component * refactor: 564 - rename speakers wanted image * refactor: change email to link * refactor: 564 - merge test cases * fix: 564 - style issue --- src/shared/constants.ts | 1 + src/shared/icons/email.tsx | 6 -- src/shared/icons/index.tsx | 1 - src/widgets/speakers/ui/speakers.module.scss | 51 +++++++++++++ src/widgets/speakers/ui/speakers.scss | 76 ------------------- .../speakers/{ => ui}/speakers.test.tsx | 28 ++----- src/widgets/speakers/ui/speakers.tsx | 39 ++++++---- 7 files changed, 85 insertions(+), 117 deletions(-) delete mode 100644 src/shared/icons/email.tsx create mode 100644 src/widgets/speakers/ui/speakers.module.scss delete mode 100644 src/widgets/speakers/ui/speakers.scss rename src/widgets/speakers/{ => ui}/speakers.test.tsx (60%) diff --git a/src/shared/constants.ts b/src/shared/constants.ts index f5a5e944e..0ea235e86 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -1,5 +1,6 @@ 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'; export const PAGE_NAMES = { diff --git a/src/shared/icons/email.tsx b/src/shared/icons/email.tsx deleted file mode 100644 index 068ad9f67..000000000 --- a/src/shared/icons/email.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import Image from 'next/image'; -import email from '@/shared/assets/svg/email.svg'; - -export const EmailIcon = () => { - return email icon; -}; diff --git a/src/shared/icons/index.tsx b/src/shared/icons/index.tsx index f5e4138af..72f974a18 100644 --- a/src/shared/icons/index.tsx +++ b/src/shared/icons/index.tsx @@ -2,7 +2,6 @@ export { AngularIcon } from './angular-icon'; export { ArrowIcon } from './arrow-icon'; export { AwsLogo } from './aws'; export { DiscordIcon } from './discord-icon'; -export { EmailIcon } from './email'; export { EpamLogo } from './epam'; export { FacebookIcon } from './facebook'; export { GithubLogo } from './github'; diff --git a/src/widgets/speakers/ui/speakers.module.scss b/src/widgets/speakers/ui/speakers.module.scss new file mode 100644 index 000000000..ac586d2f9 --- /dev/null +++ b/src/widgets/speakers/ui/speakers.module.scss @@ -0,0 +1,51 @@ +.speakers { + display: flex; + gap: 50px; + justify-content: space-between; + background-color: $color-gray-100; + + .info { + width: 640px; + + .name { + margin-top: 16px; + } + + .email-wrapper { + display: flex; + gap: 8px; + + margin-top: 8px; + + font-size: 18px; + font-style: normal; + line-height: 24px; + + @include media-tablet { + font-size: 16px; + line-height: 20px; + } + } + + @include media-laptop { + width: 100%; + } + } + + .picture { + width: 294px; + height: 300px; + + @include media-laptop { + width: 235px; + height: 240px; + padding: 16px 0 0 0; + } + } + + @include media-tablet { + flex-direction: column; + gap: 0; + align-items: center; + } +} diff --git a/src/widgets/speakers/ui/speakers.scss b/src/widgets/speakers/ui/speakers.scss deleted file mode 100644 index c5afc8595..000000000 --- a/src/widgets/speakers/ui/speakers.scss +++ /dev/null @@ -1,76 +0,0 @@ -.speakers { - &.container { - background-color: $color-gray-100; - } - - &.content { - display: flex; - flex-direction: row; - gap: 50px; - align-items: flex-start; - justify-content: space-between; - - & .info { - width: 640px; - font-weight: 500; - color: $color-black; - text-align: left; - - & .name { - margin-top: 16px; - font-size: 28px; - line-height: 36px; - - @include media-tablet { - font-size: 24px; - line-height: 32px; - } - } - - & .email { - display: flex; - flex-direction: row; - align-items: flex-start; - - margin-top: 8px; - - font-size: 18px; - line-height: 24px; - - & span { - margin-left: 8px; - } - - @include media-tablet { - font-size: 16px; - line-height: 20px; - } - } - - @include media-laptop { - width: 100%; - } - } - - & .picture { - width: 294px; - height: 300px; - - background-repeat: no-repeat; - background-position: center; - background-size: contain; - - @include media-laptop { - width: 235px; - height: 240px; - margin-top: 16px; - } - } - - @include media-tablet { - flex-direction: column; - gap: 0; - align-items: center; - } - } -} diff --git a/src/widgets/speakers/speakers.test.tsx b/src/widgets/speakers/ui/speakers.test.tsx similarity index 60% rename from src/widgets/speakers/speakers.test.tsx rename to src/widgets/speakers/ui/speakers.test.tsx index 2ed3e4d9d..ec5163afd 100644 --- a/src/widgets/speakers/speakers.test.tsx +++ b/src/widgets/speakers/ui/speakers.test.tsx @@ -1,40 +1,28 @@ import { render, screen } from '@testing-library/react'; import { beforeEach, describe, expect, it } from 'vitest'; -import { Speakers } from './ui/speakers'; +import speakersWanted from '@/shared/assets/speakers-wanted.webp'; +import { Speakers } from '@/widgets/speakers'; describe('Speakers', () => { beforeEach(() => { render(); }); - it('renders the title correctly', () => { + it('renders the content correctly', () => { const titleElement = screen.getByText('Speakers wanted'); - - expect(titleElement).toBeVisible(); - }); - - it('renders both paragraphs correctly', () => { const paragraphs = screen.getAllByTestId('paragraph'); + const nameElement = screen.getByTestId('subtitle'); + const emailElement = screen.getByText('rolling.scopes@gmail.com'); + const imageElement = screen.getByTestId('sloth-image'); + expect(titleElement).toBeVisible(); expect(paragraphs.length).toBe(2); - }); - - it('renders the name correctly', () => { - const nameElement = screen.getByTestId('contact-name'); expect(nameElement).toBeInTheDocument(); expect(nameElement).toBeVisible(); - }); - - it('renders the email correctly', () => { - const emailElement = screen.getByText('rolling.scopes@gmail.com'); - expect(emailElement).toBeVisible(); - }); - - it('renders the image correctly', () => { - const imageElement = screen.getByAltText('speakers-wanted'); expect(imageElement).toBeInTheDocument(); + expect(imageElement).toHaveAttribute('src', speakersWanted.src); }); }); diff --git a/src/widgets/speakers/ui/speakers.tsx b/src/widgets/speakers/ui/speakers.tsx index 2a7450957..1ce63a1f6 100644 --- a/src/widgets/speakers/ui/speakers.tsx +++ b/src/widgets/speakers/ui/speakers.tsx @@ -1,15 +1,21 @@ +import classNames from 'classnames/bind'; import Image from 'next/image'; -import image from '@/shared/assets/speakers-wanted.webp'; -import { EmailIcon } from '@/shared/icons'; +import speakersWanted from '@/shared/assets/speakers-wanted.webp'; +import email from '@/shared/assets/svg/email.svg'; +import { RS_EMAIL } from '@/shared/constants'; +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 './speakers.scss'; +import styles from './speakers.module.scss'; + +const cx = classNames.bind(styles); export const Speakers = () => ( -
    -
    -
    +
    +
    +
    Speakers wanted @@ -21,15 +27,20 @@ export const Speakers = () => ( So don't hesitate to drop a short synopsis to RS Head -
    + Dzmitry Varabei -
    -
    - - rolling.scopes@gmail.com -
    -
    - speakers-wanted + +
    + + {RS_EMAIL} +
    + + Cartoon sloth wearing a yellow shirt, gesturing with a speech bubble
    );