diff --git a/frontend-next-migration/src/preparedPages/MainPage/ui/page.module.scss b/frontend-next-migration/src/preparedPages/MainPage/ui/page.module.scss index 49ef1e9f2..4b7843544 100644 --- a/frontend-next-migration/src/preparedPages/MainPage/ui/page.module.scss +++ b/frontend-next-migration/src/preparedPages/MainPage/ui/page.module.scss @@ -95,13 +95,10 @@ .newsGrid { display: grid; gap: 30px; - max-width: 1000px; + max-width: 716px; margin: 0 auto; - grid-template-columns: repeat(auto-fill, minmax(450px, 1fr)); + grid-template-columns: minmax(0, 1fr); justify-content: center; - @media (max-width: 480px) { - grid-template-columns: 1fr; - } } .newsHeader { diff --git a/frontend-next-migration/src/preparedPages/NewsPages/ui/NewsElementPage/ui/NewsElementPage.module.scss b/frontend-next-migration/src/preparedPages/NewsPages/ui/NewsElementPage/ui/NewsElementPage.module.scss index 1c89b7e82..efe75939e 100644 --- a/frontend-next-migration/src/preparedPages/NewsPages/ui/NewsElementPage/ui/NewsElementPage.module.scss +++ b/frontend-next-migration/src/preparedPages/NewsPages/ui/NewsElementPage/ui/NewsElementPage.module.scss @@ -130,12 +130,13 @@ .newsGrid { display: grid; gap: 20px; - grid-template-columns: repeat(auto-fill, minmax(450px, 1fr)); + grid-template-columns: minmax(0, 716px); justify-content: center; - @media (max-width: 480px) { - grid-template-columns: 1fr; - } margin-bottom: 1.5em; + + @media (max-width: breakpoint(sm)) { + grid-template-columns: minmax(0, 1fr); + } } .readmoreText { diff --git a/frontend-next-migration/src/preparedPages/NewsPages/ui/NewsPage.module.scss b/frontend-next-migration/src/preparedPages/NewsPages/ui/NewsPage.module.scss index f1e104062..23c80f566 100644 --- a/frontend-next-migration/src/preparedPages/NewsPages/ui/NewsPage.module.scss +++ b/frontend-next-migration/src/preparedPages/NewsPages/ui/NewsPage.module.scss @@ -2,20 +2,52 @@ $touchSize: 1023px; .NewsPage { min-height: 80vh; } + +.newsContent { + width: min(1440px, 100%); + height: min(255px, 100%); + margin: 0 auto; + + @media (max-width: breakpoint(md)) { + width: min(768px, calc(100% - 28px)); + } + + @media (max-width: breakpoint(sm)) { + width: min(328px, calc(100% - 28px)); + height: min(516px, 100%); + } +} + .title { - font: var(--font-sw-xxl); - text-align: center; - padding: 0; - color: var(--primary-color); + margin: 0 0 12px; + + h1 { + font: var(--font-sw-5xl); + text-align: left; + letter-spacing: -4%; + width: 1440px; + height: 129px; + gap: var(--spacing-800); + padding: 0; + color: var(--primary-color); + margin: 0; + } @media (max-width: $touchSize) { - order: 0; - border-right: unset; + h1 { + text-align: left; + } } - @media (max-width: breakpoint(md)) { - order: 1; - margin-right: 30px; + @media (max-width: breakpoint(sm)) { + h1 { + gap: 10px; + width: auto; + height: auto; + margin-bottom: 8px; + font: var(--font-sw-xl); + text-align: center; + } } } @@ -35,10 +67,11 @@ $touchSize: 1023px; .newsGrid { display: grid; gap: 20px; - grid-template-columns: repeat(auto-fill, minmax(450px, 1fr)); - justify-content: center; - @media (max-width: 480px) { - grid-template-columns: 1fr; + grid-template-columns: minmax(0, 1fr); + justify-content: stretch; + + @media (max-width: breakpoint(sm)) { + grid-template-columns: minmax(0, 1fr); } } diff --git a/frontend-next-migration/src/preparedPages/NewsPages/ui/NewsPage.test.tsx b/frontend-next-migration/src/preparedPages/NewsPages/ui/NewsPage.test.tsx new file mode 100644 index 000000000..d48b0668b --- /dev/null +++ b/frontend-next-migration/src/preparedPages/NewsPages/ui/NewsPage.test.tsx @@ -0,0 +1,154 @@ +import { act, render, screen, waitFor } from '@testing-library/react'; +import NewsPage from './NewsPage'; +import { useGetNewsQuery } from '@/entities/NewsV2'; +import { useGetTotalNewsCountQuery } from '@/entities/NewsV2/Api/newsApi'; + +const mockIntersectionObservers: MockIntersectionObserver[] = []; + +jest.mock('next/navigation', () => ({ + useParams: jest.fn(() => ({ lng: 'en' })), + usePathname: jest.fn(() => '/en/news'), +})); + +jest.mock('@/shared/i18n', () => ({ + useClientTranslation: jest.fn(() => ({ + t: (key: string) => key, + })), +})); + +jest.mock('@/entities/NewsV2', () => ({ + useGetNewsQuery: jest.fn(), + formatNews: (news: Array, lngCode: string) => + news.map((item) => ({ + id: item.id, + date: item.date, + title: + item.translations.find((translation: any) => translation.languages_code === lngCode) + ?.title ?? '', + previewText: + item.translations.find((translation: any) => translation.languages_code === lngCode) + ?.preview_text ?? '', + titlePicture: item.titlePicture, + })), +})); + +jest.mock('@/entities/NewsV2/Api/newsApi', () => ({ + useGetTotalNewsCountQuery: jest.fn(), +})); + +jest.mock('@/widgets/NewsCard', () => ({ + NewsCard: ({ id, title, previewText }: { id: number; title: string; previewText?: string }) => ( + + {title} + {previewText} + + ), +})); + +jest.mock('@/shared/ui/SkeletonLoader/ui/SkeletonLoader', () => ({ + SkeletonLoaderForNewsPage: ({ numberOfCards }: { numberOfCards: number }) => ( +
{numberOfCards}
+ ), +})); + +class MockIntersectionObserver { + private readonly callback: (entries: IntersectionObserverEntry[]) => void; + + constructor(callback: (entries: IntersectionObserverEntry[]) => void) { + this.callback = callback; + mockIntersectionObservers.push(this); + } + + observe = jest.fn(); + disconnect = jest.fn(); + + triggerIntersection() { + this.callback([{ isIntersecting: true } as IntersectionObserverEntry]); + } +} + +describe('NewsPage', () => { + const mockUseGetNewsQuery = useGetNewsQuery as jest.Mock; + const mockUseGetTotalNewsCountQuery = useGetTotalNewsCountQuery as jest.Mock; + + const createNews = (id: number) => ({ + id, + date: `2026-08-${String(id).padStart(2, '0')}`, + titlePicture: id === 1 ? { id: 'image-1' } : null, + translations: [ + { + languages_code: 'en-US', + title: `News ${id}`, + preview_text: `Preview ${id}`, + }, + ], + }); + const firstPageNews = [createNews(1)]; + const secondPageNews = [createNews(2)]; + const finalPageNews = [1, 2, 3, 4, 5].map(createNews); + + beforeEach(() => { + jest.clearAllMocks(); + mockIntersectionObservers.length = 0; + Object.defineProperty(global, 'IntersectionObserver', { + configurable: true, + writable: true, + value: MockIntersectionObserver, + }); + jest.requireMock('next/navigation').usePathname.mockReturnValue('/en/news'); + mockUseGetNewsQuery.mockImplementation(({ page }: { page: number }) => ({ + data: page === 1 ? firstPageNews : secondPageNews, + })); + mockUseGetTotalNewsCountQuery.mockReturnValue({ data: 1 }); + }); + + it('renders the translated page title and fetched news cards', async () => { + render(); + + expect(screen.getByRole('heading', { level: 1, name: 'head-title' })).toBeInTheDocument(); + const newsLink = await screen.findByRole('link', { name: /News 1/ }); + expect(newsLink).toHaveTextContent('News 1'); + expect(newsLink).toHaveTextContent('Preview 1'); + expect(newsLink).toHaveAttribute('href', '/news/1'); + expect(mockUseGetNewsQuery).toHaveBeenCalledWith({ + limit: 6, + page: 1, + categorySlug: undefined, + }); + }); + + it('shows six skeleton cards while news is loading', () => { + mockUseGetNewsQuery.mockReturnValue({ data: undefined }); + + render(); + + expect(screen.getByTestId('news-skeleton')).toHaveTextContent('6'); + }); + + it('shows the no-more-news message after the final page', async () => { + mockUseGetNewsQuery.mockReturnValue({ data: finalPageNews }); + mockUseGetTotalNewsCountQuery.mockReturnValue({ data: 1 }); + + render(); + + expect(await screen.findByText('no-more-news')).toBeInTheDocument(); + }); + + it('loads the next page when the sentinel intersects', async () => { + mockUseGetTotalNewsCountQuery.mockReturnValue({ data: 7 }); + + render(); + await waitFor(() => expect(mockIntersectionObservers.length).toBeGreaterThan(0)); + + act(() => { + mockIntersectionObservers[0].triggerIntersection(); + }); + + expect(await screen.findByRole('link', { name: /News 2/ })).toBeInTheDocument(); + expect(mockUseGetNewsQuery).toHaveBeenLastCalledWith({ + limit: 6, + page: 2, + categorySlug: undefined, + }); + }); +}); diff --git a/frontend-next-migration/src/preparedPages/NewsPages/ui/NewsPage.tsx b/frontend-next-migration/src/preparedPages/NewsPages/ui/NewsPage.tsx index 1182de4e8..06652cc4c 100644 --- a/frontend-next-migration/src/preparedPages/NewsPages/ui/NewsPage.tsx +++ b/frontend-next-migration/src/preparedPages/NewsPages/ui/NewsPage.tsx @@ -95,26 +95,28 @@ const NewsPage = () => { alternate={true} searchVisible={false} /> -
- {groupedNews.map((news) => { - const imageSrc = news.titlePicture?.id - ? `${directusBaseUrl}/assets/${news.titlePicture.id}` - : undefined; - return ( - - ); - })} +
+
+ {groupedNews.map((news) => { + const imageSrc = news.titlePicture?.id + ? `${directusBaseUrl}/assets/${news.titlePicture.id}` + : undefined; + return ( + + ); + })} - {isLoading && } + {isLoading && } - {hasMoreNewsState && !isLoading &&
} + {hasMoreNewsState && !isLoading &&
} +
{renderNoMoreNews()} diff --git a/frontend-next-migration/src/shared/assets/images/altLogoold.png b/frontend-next-migration/src/shared/assets/images/altLogoold.png index cc4540b40..388432320 100644 Binary files a/frontend-next-migration/src/shared/assets/images/altLogoold.png and b/frontend-next-migration/src/shared/assets/images/altLogoold.png differ diff --git a/frontend-next-migration/src/widgets/NewsCard/ui/NewsCard.module.scss b/frontend-next-migration/src/widgets/NewsCard/ui/NewsCard.module.scss index 4ed4a1930..c57559eea 100644 --- a/frontend-next-migration/src/widgets/NewsCard/ui/NewsCard.module.scss +++ b/frontend-next-migration/src/widgets/NewsCard/ui/NewsCard.module.scss @@ -1,148 +1,206 @@ -.NewsCard { - display: grid; - grid-template-columns: 1fr; - align-items: center; - justify-content: center; - background-color: var(--base-card-background); - border: 3px solid var(--drop-shadows); - border-radius: var(--border-radius-figmadesktop); - filter: drop-shadow(8px 12px var(--drop-shadows)); - height: 150px; +.NewsCardLink { + display: flex; + justify-content: flex-start; width: 100%; - max-width: 780px; - margin: 0 auto; - overflow: hidden; - position: relative; - transition: border-color 0.3s ease-out; - - @media (max-width: breakpoint(xl)) { - max-width: 500px; - } + color: inherit; + text-decoration: none; - @media (max-width: breakpoint(md)) { - height: 130px; - width: 70%; + &:focus { + outline: none; } +} - @media (max-width: breakpoint(sm)) { - height: 120px; - width: 80%; - } +.NewsCard { + display: grid; + grid-template-columns: 342px minmax(0, 1fr); + align-items: stretch; + width: 100%; + max-width: 100%; + min-height: auto; + gap: 32px; + padding: 0; + padding-right: var(--spacing-600); + overflow: hidden; + background-color: var(--base-card-background); + border: var(--border-desktop); + border-radius: var(--border-radius-figmadesktop); + transition: + border-color 0.2s ease-out, + transform 0.2s ease-out; +} - @media (max-width: breakpoint(xs)), - (max-width: 320px) { - height: 110px; - width: 70%; - margin: auto 0; - } +.NewsCardLink:hover .NewsCard, +.NewsCardLink:focus-visible .NewsCard { + border-color: var(--primary-color); +} - @media (max-width: 320px) { - width: 60%; - } +.NewsCardLink:active .NewsCard { + transform: scale(0.99); } +.noImage { + grid-template-columns: 342px minmax(0, 1fr); + padding-right: var(--spacing-600); +} -.NewsCardLink:hover .NewsCard, .NewsCardLink:focus .NewsCard { - border-color: var(--primary-color); +.noImage .content { + grid-column: 2; + justify-content: center; + padding-right: 0; + padding-block: 24px 18px; } +.noImage .meta { + margin-top: 2px; +} .imageContainer { - position: absolute; - right: 0; - top: 0; + width: 342px; + min-width: 342px; + height: 255px; + min-height: 255px; overflow: hidden; - width: 72%; - height: auto; - flex-shrink: 0; - border-radius: 10px; - - @media (max-width: breakpoint(xs)) { - width: 90%; - } + background-color: #172936; } .image { + display: block; width: 100%; height: 100%; - object-fit: cover; - margin-left: 32%; - clip-path: polygon(0 0, 100% 0, 100% 100%, 30% 100%, 0 -15%); - transform: translateY(-27%); - - @media (max-width: breakpoint(xs)), - (max-width: 320px) { - margin-left: 35%; - } -} - -.NewsCardLink { - width: 100%; - display: flex; - justify-content: center; - - &:focus { - outline: none; - } + object-fit: contain; + left: -6px; } .content { display: flex; flex-direction: column; - justify-content: space-between; - height: 100%; - padding: 0.5rem 50% 0 0.5rem; - overflow: hidden; + justify-content: center; + gap: 13px; + color: var(--white); } .title { - font: var(--font-sw-l); - color: var(--primary-color); - white-space: nowrap; + display: -webkit-box; + margin: 0; overflow: hidden; - text-overflow: ellipsis; - - @media (max-width: breakpoint(sm)), - (max-width: breakpoint(xs)), - (max-width: 320px) { - padding-top: 10px; - } + color: var(--white); + font: var(--font-sw-xl); + letter-spacing: -4%; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + line-clamp: 2; + gap: 10px; } .text { - color: var(--white); - line-height: 1.5em !important; - font: var(--font-dm-s); - width: 255px; - margin-top: auto; display: -webkit-box; + max-width: 100%; + margin: 0; + overflow: hidden; + color: var(--white); + font: var(--font-dm-l); + letter-spacing: -4%; + -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; + gap: 10px; +} - @media (max-width: breakpoint(md)) { - line-height: 1rem !important; - font: var(--font-dm-s); +.meta { + display: flex; + flex-wrap: wrap; + align-items: center; + width: 209px; + height: 26px; + gap: 4px; + color: var(--primary-color); + font: var(--font-dm-l); + letter-spacing: -4%; +} + +.date, +.publisher { + color: inherit; + font: inherit; +} + +.publisher { + white-space: nowrap; +} + +@media (max-width: breakpoint(sm)) { + .NewsCard { + grid-template-columns: 1fr; + width: 360px; + gap: 24px; + padding: 0; + border-radius: var(--border-radius-figmadesktop); + border: 2px solid #000000; + box-shadow: 4px 8px 0px 0px var(--ShadowblackShadow); + } + + .imageContainer { + width: 330px; + min-width: 330px; + height: 280px; + min-height: 280px; + } + + .image { + width: 100%; + height: 100%; + top: auto; + left: auto; } - @media (max-width: breakpoint(sm)) { - font: var(--font-dm-s); - width: 200px; + .noImage { + grid-template-columns: 1fr; + min-height: 180px; + padding-right: 0; } - @media (max-width: 390px) { - display: none; + .noImage .content { + justify-content: flex-start; + padding: 14px 14px; + gap: 16px; + text-align: left; + } + + .content { + justify-content: flex-start; + padding: 14px 14px; + gap: 16px; + } + + .meta { + width: auto; + height: auto; + order: -1; + font: var(--font-dm-m); + margin-bottom: 4px; + } + + .title { + font: var(--font-sw-m); + -webkit-line-clamp: 2; + line-clamp: 2; + margin: 0; + } + + .text { + font: var(--font-dm-m); + -webkit-line-clamp: 2; + line-clamp: 2; + margin: 0; } } -.date { - color: var(--white) !important; - font: var(--font-dm-s); - margin-top: auto; - margin-bottom: 5px; +@media (max-width: breakpoint(xs)) { + .NewsCard { + min-height: 286px; + } - @media (max-width: breakpoint(xs)) { - font: var(--font-dm-xs); + .content { + padding: 12px 14px 14px; } -} \ No newline at end of file +} diff --git a/frontend-next-migration/src/widgets/NewsCard/ui/NewsCard.test.tsx b/frontend-next-migration/src/widgets/NewsCard/ui/NewsCard.test.tsx new file mode 100644 index 000000000..faf7b4ab6 --- /dev/null +++ b/frontend-next-migration/src/widgets/NewsCard/ui/NewsCard.test.tsx @@ -0,0 +1,59 @@ +import { render, screen } from '@testing-library/react'; +import NewsCard from './NewsCard'; + +describe('NewsCard', () => { + const defaultProps = { + id: 42, + title: 'A new update', + previewText: 'Details about the latest update.', + date: '2026-08-24', + }; + + it('renders the title, preview text, and date', () => { + render(); + + expect( + screen.getByRole('heading', { level: 2, name: defaultProps.title }), + ).toBeInTheDocument(); + expect(screen.getByText(defaultProps.previewText)).toBeInTheDocument(); + expect(screen.getByText(defaultProps.date)).toBeInTheDocument(); + }); + + it('links to the news item and renders its image', () => { + render( + , + ); + + expect(screen.getByRole('link')).toHaveAttribute('href', '/news/42'); + expect(screen.getByRole('img', { name: defaultProps.title })).toHaveAttribute( + 'src', + 'https://example.com/news-image.jpg', + ); + }); + + it('uses description instead of preview text when both are provided', () => { + render( + , + ); + + expect(screen.getByText('The full description.')).toBeInTheDocument(); + expect(screen.queryByText(defaultProps.previewText)).not.toBeInTheDocument(); + }); + + it('renders the publisher when provided', () => { + render( + , + ); + + expect(screen.getByText('Altzone')).toBeInTheDocument(); + }); +}); diff --git a/frontend-next-migration/src/widgets/NewsCard/ui/NewsCard.tsx b/frontend-next-migration/src/widgets/NewsCard/ui/NewsCard.tsx index 98d217c83..fa5043b44 100644 --- a/frontend-next-migration/src/widgets/NewsCard/ui/NewsCard.tsx +++ b/frontend-next-migration/src/widgets/NewsCard/ui/NewsCard.tsx @@ -7,39 +7,53 @@ import Link from 'next/link'; interface NewsCardProps { className?: string; title: string; - previewText: string; + previewText?: string; + description?: string; date: string; + publisher?: string; id: number; titlePicture?: string; } const NewsCard = (props: NewsCardProps) => { - const { title, date, id, previewText, titlePicture } = props; - const picture = titlePicture; + const { + className = '', + title, + date, + id, + previewText, + description, + publisher, + titlePicture, + } = props; + const picture = titlePicture || '/images/logos/CommonALT.png'; + const text = description ?? previewText ?? ''; + return ( -
+
+
+ {title} +

{title}

-

{previewText}

- {date} -
- {picture && ( - {title} - )} + {text &&

{text}

} +
+ {date} + {publisher && {publisher}}
-
+
); };