diff --git a/frontend-next-migration/src/preparedPages/NewsPages/ui/NewsElementPage/ui/NewsElementPage.tsx b/frontend-next-migration/src/preparedPages/NewsPages/ui/NewsElementPage/ui/NewsElementPage.tsx index a7e884914..94dabc3b3 100644 --- a/frontend-next-migration/src/preparedPages/NewsPages/ui/NewsElementPage/ui/NewsElementPage.tsx +++ b/frontend-next-migration/src/preparedPages/NewsPages/ui/NewsElementPage/ui/NewsElementPage.tsx @@ -19,6 +19,7 @@ import { linkify } from '@/shared/ui/v2/Chatbot/utils/linkify'; import { useClientTranslation } from '@/shared/i18n'; import { NewsCard } from '@/widgets/NewsCard'; import { ShareButton } from '@/shared/ui/v2/ShareButton'; +import { SkeletonLoaderForNewsElementPage } from '@/shared/ui/SkeletonLoader/ui/SkeletonLoader'; type HeroImageProps = { picture?: string; @@ -59,18 +60,65 @@ const NewsElementPage = () => { const lng = params.lng as string; const { t } = useClientTranslation('news'); - const { data: moreNews } = useGetNewsQuery({ limit: 2 }); - const { data, isLoading } = useGetNewsByIdQuery(id as string); + const { data: moreNews, isLoading: isMoreNewsLoading } = useGetNewsQuery({ limit: 2 }); + const { data, isLoading: isNewsLoading } = useGetNewsByIdQuery(id as string); const directusBaseUrl = envHelper.directusHost; const router = useRouter(); - const lngCode = lng === 'en' ? 'en-US' : lng === 'fi' ? 'fi-FI' : lng; + + const pageIsLoading = isNewsLoading || isMoreNewsLoading || !data || !moreNews; + + const getLngCode = (lng: string) => { + if (lng === 'en') return 'en-US'; + if (lng === 'fi') return 'fi-FI'; + return lng; + }; + + const lngCode = getLngCode(lng); const handleNextNews = (newsId: string) => { if (newsId) router.push(`/news/${newsId}`); }; - if (isLoading || !data) { - return
Loading...
; + if (pageIsLoading) { + return ( + +
+ + + +
+ +
+ +
+
+ ); } const post = formatNewsSingle(data, lngCode || 'fi-FI'); diff --git a/frontend-next-migration/src/preparedPages/NewsPages/ui/NewsPage.tsx b/frontend-next-migration/src/preparedPages/NewsPages/ui/NewsPage.tsx index 13ea82efa..1182de4e8 100644 --- a/frontend-next-migration/src/preparedPages/NewsPages/ui/NewsPage.tsx +++ b/frontend-next-migration/src/preparedPages/NewsPages/ui/NewsPage.tsx @@ -10,6 +10,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { News } from '@/entities/NewsV2/model/types/types'; import { useClientTranslation } from '@/shared/i18n'; import { PageTitle } from '@/shared/ui/PageTitle'; +import { SkeletonLoaderForNewsPage } from '@/shared/ui/SkeletonLoader/ui/SkeletonLoader'; const NewsPage = () => { // later use this to fetch data from the backend @@ -22,7 +23,6 @@ const NewsPage = () => { const currentSlug = typeof params.slug === 'string' ? params.slug : undefined; const lngCode = lng === 'en' ? 'en-US' : lng === 'fi' ? 'fi-FI' : lng; const directusBaseUrl = envHelper.directusHost; - const limit = 6; const [currentPage, setCurrentPage] = useState(1); const [allNews, setAllNews] = useState([]); @@ -111,9 +111,11 @@ const NewsPage = () => { /> ); })} - {hasMoreNewsState &&
} + + {isLoading && } + + {hasMoreNewsState && !isLoading &&
}
-
{isLoading && 'Loading...'}
{renderNoMoreNews()} diff --git a/frontend-next-migration/src/shared/ui/SkeletonLoader/ui/SkeletonLoader.module.scss b/frontend-next-migration/src/shared/ui/SkeletonLoader/ui/SkeletonLoader.module.scss index 75a5ad5bd..cd55762a7 100644 --- a/frontend-next-migration/src/shared/ui/SkeletonLoader/ui/SkeletonLoader.module.scss +++ b/frontend-next-migration/src/shared/ui/SkeletonLoader/ui/SkeletonLoader.module.scss @@ -151,3 +151,221 @@ width: 100%; margin-bottom: 8px; } + +/* Skeleton News */ +.newsSkeletonCard { + 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; + width: 100%; + max-width: 780px; + margin: 0 auto; + overflow: hidden; + position: relative; + + @media (max-width: breakpoint(xl)) { + max-width: 500px; + } + + @media (max-width: breakpoint(md)) { + height: 130px; + width: 70%; + } + + @media (max-width: breakpoint(sm)) { + height: 120px; + width: 80%; + } + + @media (max-width: breakpoint(xs)), + (max-width: 320px) { + height: 110px; + width: 70%; + margin: auto 0; + } + + @media (max-width: 320px) { + width: 60%; + } +} + +.newsSkeletonContent { + display: flex; + flex-direction: column; + justify-content: space-between; + height: 100%; + padding: 0.5rem 50% 0 0.5rem; + overflow: hidden; + position: relative; + z-index: 2; +} + +.newsSkeletonImage { + position: absolute; + right: 0; + top: 0; + bottom: -50px; + width: 70%; + height: 100%; + background: var(--skeleton-color, #e5e5e5); + clip-path: polygon(0 0, 100% 0, 100% 100%, 14% 100%, 0 -22%); + transform: translateX(39%); + animation: skeletonPulse 1.5s infinite ease-in-out; + @media (max-width: breakpoint(xs)) { + width: 90%; + transform: translateX(35%); + } +} + +.newsSkeletonTitle { + width: 85%; + height: 24px; + border-radius: 6px; + background: var(--skeleton-color, #e5e5e5); + animation: skeletonPulse 1.5s infinite ease-in-out; +} + +.newsSkeletonText { + width: 245px; + height: 14px; + margin-top: auto; + border-radius: 6px; + background: var(--skeleton-color, #e5e5e5); + animation: skeletonPulse 1.5s infinite ease-in-out; +} + +.newsSkeletonTextShort { + width: 245px; + height: 14px; + border-radius: 6px; + margin-top: 10px; + background: var(--skeleton-color, #e5e5e5); + animation: skeletonPulse 1.5s infinite ease-in-out; +} + +.newsSkeletonDate { + width: 80px; + height: 14px; + margin-top: auto; + margin-bottom: 5px; + border-radius: 6px; + background: var(--skeleton-color, #e5e5e5); + animation: skeletonPulse 1.5s infinite ease-in-out; +} + +@keyframes skeletonPulse { + 0% { + opacity: 1; + } + + 50% { + opacity: 0.45; + } + + 100% { + opacity: 1; + } +} +/* Skeleton News Element Page */ +.newsElementSkeleton { + width: 100%; + display: flex; + flex-direction: column; + row-gap: 1rem; + padding-bottom: 10rem; +} + +.newsElementSkeletonHero { + width: 100%; + height: 355px; + margin-bottom: 0.5rem; + border-radius: 4px; + background: var(--skeleton-color, #e5e5e5); + animation: skeletonPulse 1.5s infinite ease-in-out; + + @media (max-width: breakpoint(md)) { + height: 250px; + } + + @media (max-width: breakpoint(sm)) { + height: 190px; + } +} + +.newsElementSkeletonTitle { + width: 90%; + height: 48px; + margin: 0 auto 24px; + border-radius: 8px; + background: var(--skeleton-color, #e5e5e5); + animation: skeletonPulse 1.5s infinite ease-in-out; + + @media (max-width: breakpoint(sm)) { + width: 70%; + height: 40px; + } +} + +.newsElementSkeletonSubtitle { + width: 90%; + height: 30px; + margin: 0 auto 24px; + border-radius: 8px; + background: var(--skeleton-color, #e5e5e5); + animation: skeletonPulse 1.5s infinite ease-in-out; + + @media (max-width: breakpoint(sm)) { + width: 85%; + height: 26px; + } +} + +.newsElementSkeletonMeta { + display: flex; + justify-content: center; + margin-bottom: 1rem; + + div { + width: 100px; + height: 18px; + border-radius: 6px; + background: var(--skeleton-color, #e5e5e5); + animation: skeletonPulse 1.5s infinite ease-in-out; + } +} + +.newsElementSkeletonBody { + display: flex; + flex-direction: column; + align-items: center; + gap: 20px; + margin-bottom: 1rem; + + div { + width: 85%; + height: 20px; + border-radius: 6px; + background: var(--skeleton-color, #e5e5e5); + animation: skeletonPulse 1.5s infinite ease-in-out; + } + + .short { + width: 55%; + } + + @media (max-width: breakpoint(sm)) { + div { + width: 100%; + } + + .short { + width: 75%; + } + } +} diff --git a/frontend-next-migration/src/shared/ui/SkeletonLoader/ui/SkeletonLoader.tsx b/frontend-next-migration/src/shared/ui/SkeletonLoader/ui/SkeletonLoader.tsx index bb051acb8..7ac18276f 100644 --- a/frontend-next-migration/src/shared/ui/SkeletonLoader/ui/SkeletonLoader.tsx +++ b/frontend-next-migration/src/shared/ui/SkeletonLoader/ui/SkeletonLoader.tsx @@ -181,3 +181,70 @@ export const SkeletonLoaderWithHeader = ({ sections = 1, className = '' }: Skele
); }; + +/** + * Renders skeleton cards for the news listing page. + * Used while news data is loading to preserve layout and improve UX. + * + * @param {Object} props - Component props + * @param {number} [props.numberOfCards=12] - Number of skeleton cards to render + * @param {string} [props.className] - Optional additional CSS classes + * @returns {JSX.Element} List of skeleton news cards + */ +export const SkeletonLoaderForNewsPage = ({ + numberOfCards = 12, + className = '', +}: SkeletonLoaderProps) => { + const cards = Array(numberOfCards).fill(0); + + return ( + <> + {cards.map((_, index) => ( +
+
+
+
+
+
+
+ +
+
+ ))} + + ); +}; + +/** + * Renders a skeleton layout for a single news article page + * while the article content is loading. + */ + +export const SkeletonLoaderForNewsElementPage = ({ className = '' }: SkeletonLoaderProps) => { + return ( +
+
+ +
+
+ +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ ); +};