Skip to content

Commit

Permalink
TC#73 Enhance and optimize the news page (#144)
Browse files Browse the repository at this point in the history
* TC#73: Added loader in news one page and some optimize images in carousel

* TC#73: Added skeleton loader to news page

* TC#73: Fixed bug with reload after any crud operations

* TC#73: Refactored styles skeleton

* TC#73: Removed line
  • Loading branch information
AlikhanKubatbekov authored Dec 21, 2024
1 parent 98f31ed commit 6e23c40
Show file tree
Hide file tree
Showing 10 changed files with 139 additions and 58 deletions.
13 changes: 2 additions & 11 deletions frontend/src/actions/news.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,8 @@ import { News, NewsResponse } from '@/shared/types/news.types';
import { Filters } from '@/shared/types/root.types';

export const fetchNews = async ({ data }: { data: Filters }) => {
let query = '';
try {
if (data?.query) {
query = toQueryParams(data.query);
}
const { data: news } = await axiosApi.get<NewsResponse>(`/news/${query}`);
return news;
} catch (error) {
console.error('Error fetching news: ', error);
throw error;
}
const query = data?.query ? toQueryParams(data.query) : '';
return (await axiosApi.get<NewsResponse>(`/news/${query}`)).data;
};

export const fetchOneNews = async (id: string) => {
Expand Down
86 changes: 47 additions & 39 deletions frontend/src/app/(root)/news/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { useDialogState } from '@/app/(root)/news/hooks/use-dialog-state';
import { useOneNews } from '@/app/(root)/news/hooks/use-one-news';
import { Container, ImageModal, NewsCard } from '@/shared/components/shared';
import { Container, ImageModal, Loader, NewsCard } from '@/shared/components/shared';
import { API_URL } from '@/shared/constants';
import { cn } from '@/shared/lib';
import DOMPurify from 'dompurify';
Expand All @@ -24,49 +24,57 @@ export default function Page() {
};

return (
<Container>
<div className={cn(styles.titleBlock)}>
<h1 className={cn(styles.title, 'dark:text-white')}>{oneNews?.title}</h1>
<h2 className={cn(styles.subtitle)}>{oneNews?.subtitle}</h2>
</div>
<>
{!oneNews ? (
<Container className={'d-flex'}>
<Loader className={'mx-auto'}/>
</Container>
) : (
<Container>
<div className={cn(styles.titleBlock)}>
<h1 className={cn(styles.title, 'dark:text-white')}>{oneNews?.title}</h1>
<h2 className={cn(styles.subtitle)}>{oneNews?.subtitle}</h2>
</div>

<section className={oneNews && oneNews.images.length > 0 ? cn(carouselStyles.embla) : 'hidden'}>
<div className={cn(carouselStyles.emblaViewport)} ref={emblaRef}>
<div className={cn(carouselStyles.emblaContainer)}>
{oneNews?.images?.map((imageUrl, index) => (
<div className={cn(carouselStyles.emblaSlide)} key={index} onClick={() => handleImageClick(index)}>
<Image
src={API_URL + '/' + imageUrl}
alt={`Slide ${index + 1}`}
className={cn(carouselStyles.emblaSlideImage)}
width={400}
height={200}
/>
<section className={oneNews && oneNews.images.length > 0 ? cn(carouselStyles.embla) : 'hidden'}>
<div className={cn(carouselStyles.emblaViewport)} ref={emblaRef}>
<div className={cn(carouselStyles.emblaContainer)}>
{oneNews?.images?.map((imageUrl, index) => (
<div className={cn(carouselStyles.emblaSlide)} key={index} onClick={() => handleImageClick(index)}>
<Image
src={API_URL + '/' + imageUrl}
alt={`Slide ${index + 1}`}
className={cn(carouselStyles.emblaSlideImage)}
width={400}
height={200}
/>
</div>
))}
</div>
))}
</div>
</div>
</section>
</div>
</section>

{oneNews && oneNews.content && (
<section
className={cn(styles.content, 'dark:text-white')}
dangerouslySetInnerHTML={{ __html: sanitize(oneNews.content) }}
/>
)}
{oneNews && oneNews.content && (
<section
className={cn(styles.content, 'dark:text-white')}
dangerouslySetInnerHTML={{ __html: sanitize(oneNews.content) }}
/>
)}

<section>
<h3 className={cn(styles.sectionTitle)}>Другие новости</h3>
<div className={cn(styles.cardContainer)}>
{news.map((newsItem) => (
<NewsCard key={newsItem._id} news={newsItem} />
))}
</div>
</section>
<section>
<h3 className={cn(styles.sectionTitle)}>Другие новости</h3>
<div className={cn(styles.cardContainer)}>
{news.map((newsItem) => (
<NewsCard key={newsItem._id} news={newsItem} />
))}
</div>
</section>

{oneNews && oneNews.images.length > 0 && (
<ImageModal images={oneNews.images} open={open} onClose={toggleOpen} initialIndex={initialIndex} />
{oneNews && oneNews.images.length > 0 && (
<ImageModal images={oneNews.images} open={open} onClose={toggleOpen} initialIndex={initialIndex} />
)}
</Container>
)}
</Container>
</>
);
}
2 changes: 1 addition & 1 deletion frontend/src/app/(root)/news/hooks/render-news.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import styles from '@/app/(root)/news/news.module.css';
import { CustomPagination, DatePicker, NewsCard } from '@/shared/components/shared';
import { cn } from '@/shared/lib';
import { News } from '@/shared/types/news.types';
import styles from '@/app/(root)/news/news.module.css';

interface Props {
news: News[];
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/app/(root)/news/hooks/use-one-news.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
'use client';

import { fetchNewsByLimit, fetchOneNews } from '@/actions/news';
import { News } from '@/shared/types/news.types';
import useEmblaCarousel from 'embla-carousel-react';
Expand All @@ -7,7 +9,7 @@ import { useEffect, useState } from 'react';

export const useOneNews = () => {
const { id } = useParams<{ id: string }>();
const [oneNews, setOneNews] = useState<News>();
const [oneNews, setOneNews] = useState<News | null>(null);
const [news, setNews] = useState<News[]>([]);
const [emblaRef] = useEmblaCarousel({ dragFree: true, loop: true });
const [initialIndex, setInitialIndex] = useState<number>(0);
Expand All @@ -24,6 +26,7 @@ export const useOneNews = () => {

void fetchData();
}, [id]);

return {
emblaRef,
oneNews,
Expand Down
36 changes: 36 additions & 0 deletions frontend/src/app/(root)/news/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Container, DatePicker, NewsTitle } from '@/shared/components/shared';
import { Skeleton } from '@/shared/components/ui';
import { cn } from '@/shared/lib';
import styles from '@/app/(root)/news/news.module.css';

import React from 'react';

export default function Loading() {
const arr = new Array(12).fill(null);

return (
<Container>
<NewsTitle />
<DatePicker />

<div className={cn(styles.newsContainer)}>
{arr.map((_, i) => (
<Skeleton key={i} className={cn(styles.skeletonCard)}>
<Skeleton>
<Skeleton className={cn(styles.skeletonCardImageBlock)}>
<Skeleton className={cn(styles.skeletonImage)} />
</Skeleton>
<Skeleton className={cn(styles.skeletonCardTextBlock)}>
<div className={cn(styles.skeletonCardTextContent)}>
<Skeleton className={cn(styles.skeletonCardText, 'w-[95%]')} />
<Skeleton className={cn(styles.skeletonCardText, 'w-[90%]')} />
<Skeleton className={cn(styles.skeletonCreatedAt)} />
</div>
</Skeleton>
</Skeleton>
</Skeleton>
))}
</div>
</Container>
);
}
32 changes: 32 additions & 0 deletions frontend/src/app/(root)/news/news.module.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,35 @@
.newsContainer {
@apply grid gap-5 justify-center items-center sm:grid-cols-2 lmd:grid-cols-3 xl:grid-cols-4 mb-6;
}

.skeletonCard {
@apply w-full min-w-[280px] rounded-lg;
}

.skeletonCardContent {
@apply w-full;
}

.skeletonCardImageBlock {
@apply p-0;
}

.skeletonImage {
@apply h-[300px] object-cover mb-5 min-w-full rounded-md;
}

.skeletonCardTextBlock {
@apply flex flex-wrap mt-auto items-start p-6 pt-0;
}

.skeletonCardTextContent {
@apply me-auto w-full pb-12;
}

.skeletonCardText {
@apply dark:text-white h-3 mb-2;
}

.skeletonCreatedAt {
@apply dark:text-white h-4 w-[30%];
}
12 changes: 8 additions & 4 deletions frontend/src/app/(root)/news/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { fetchNews } from '@/actions/news';
import { renderNewsContent } from '@/app/(root)/news/hooks/render-news';
import Loading from '@/app/(root)/news/loading';
import { Container, NewsTitle } from '@/shared/components/shared';
import { deleteEmptyQueryStrings } from '@/shared/lib';
import { NewsResponse } from '@/shared/types/news.types';
import type { Metadata } from 'next';
import { Suspense } from 'react';

export const metadata: Metadata = {
title: 'Свежие новости — Главные события мира тенниса в Кыргызстане',
Expand Down Expand Up @@ -39,10 +41,12 @@ const NewsPage = async ({ searchParams }: Props) => {
const news = newsResponse.data;

return (
<Container>
<NewsTitle />
{renderNewsContent({ news: news, pages: newsResponse.pages })}
</Container>
<Suspense fallback={<Loading />}>
<Container>
<NewsTitle />
{renderNewsContent({ news: news, pages: newsResponse.pages })}
</Container>
</Suspense>
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { XIcon } from 'lucide-react';
import React, { FormEvent, useEffect } from 'react';

import styles from './news-form.module.css';
import { useRouter } from 'next/navigation';

interface Props {
open: boolean;
Expand All @@ -45,6 +46,7 @@ export const NewsForm: React.FC<Props> = ({ open, setOpen, newsId, isEdit = fals
} = useNewsForm();
const dispatch = useAppDispatch();
const data = useAppSelector(selectOneNews);
const router = useRouter();

useEffect(() => {
if (newsId) {
Expand All @@ -70,9 +72,11 @@ export const NewsForm: React.FC<Props> = ({ open, setOpen, newsId, isEdit = fals
if (isEdit && newsId) {
await dispatch(updateNews({ newsId, newsMutation: news })).unwrap();
toast.success('Новость успешно обновлена!');
router.refresh();
} else {
await dispatch(createNews(news)).unwrap();
toast.success('Новость успешно добавлена!');
router.refresh();
}

setOpen(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,19 @@
import { useAppDispatch, useAppSelector } from '@/shared/hooks/hooks';
import { selectRemoveNewsLoading } from '@/shared/lib/features/news/news-slice';
import { removeNews } from '@/shared/lib/features/news/news-thunks';
import { useRouter } from 'next/navigation';

export const useNewsCard = (id: string) => {
const dispatch = useAppDispatch();
const newsRemoving = useAppSelector(selectRemoveNewsLoading);
const router = useRouter();

const handleRemove = async () => {
try {
const { toast } = await import('sonner');
await dispatch(removeNews(id)).unwrap();
toast.success('Новость успешно удалена!');
router.refresh();
} catch (e) {
const { toast } = await import('sonner');
console.error(e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ export const ImageModal: React.FC<Props> = ({ images, open, onClose, initialInde
src={API_URL + '/' + img}
alt={`Image ${i}`}
className={cn(styles.image)}
width={1920}
height={1080}
width={1400}
height={720}
/>
</CarouselItem>
))}
Expand Down

0 comments on commit 6e23c40

Please sign in to comment.