All files / pages TopPostsTanStack.tsx

0% Statements 0/34
0% Branches 0/23
0% Functions 0/10
0% Lines 0/33

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126                                                                                                                                                                                                                                                           
import { useSearchParams } from 'react-router-dom';
import { FormattedMessage } from 'react-intl';
import { Box, Typography, CircularProgress, Alert } from '@mui/material';
import { useQuery } from '@tanstack/react-query';
import { useSelector } from 'react-redux';
import { selectViewMode } from '@/store/reducers/settings/settings-selectors';
import { ViewMode } from '@/enums/settings';
import PostListItem from '@/components/posts/PostListItem/PostListItem';
import PostGridItem from '@/components/posts/PostGridItem/PostGridItem';
import Pagination from '@/components/posts/Pagination/Pagination';
import { getPageFromUrl } from '@/helpers/urlHelper';
import { calculateStartRank } from '@/helpers/paginationHelper';
import { getPostsPerPage } from '@/helpers/configHelper';
import { HNItem, HNItemId } from '@/types/hackernews';
 
const POSTS_PER_PAGE = getPostsPerPage();
const API_BASE_URL = 'https://hacker-news.firebaseio.com/v0';
 
async function fetchTopPostIds(): Promise<HNItemId[]> {
  const response = await fetch(`${API_BASE_URL}/topstories.json`);
  if (!response.ok) {
    throw new Error(`Failed to fetch top post IDs: ${response.status} ${response.statusText}`);
  }
  return response.json();
}
 
async function fetchItem(id: HNItemId): Promise<HNItem | null> {
  const response = await fetch(`${API_BASE_URL}/item/${id}.json`);
  if (!response.ok) {
    throw new Error(`Failed to fetch item ${id}: ${response.status} ${response.statusText}`);
  }
  return response.json();
}
 
async function fetchTopPosts(page: number): Promise<{ posts: HNItem[]; totalPages: number }> {
  const allPostIds = await fetchTopPostIds();
  const totalPages = Math.ceil(allPostIds.length / POSTS_PER_PAGE);
  const startIndex = (page - 1) * POSTS_PER_PAGE;
  const endIndex = startIndex + POSTS_PER_PAGE;
  const pagePostIds = allPostIds.slice(startIndex, endIndex);
 
  const posts = await Promise.all(
    pagePostIds.map(async (id) => {
      const item = await fetchItem(id);
      return item;
    })
  );
 
  return { posts: posts.filter((item): item is HNItem => item !== null), totalPages };
}
 
function TopPostsTanStack() {
  const [searchParams, setSearchParams] = useSearchParams();
  const viewMode = useSelector(selectViewMode);
  const pageFromUrl = getPageFromUrl(searchParams);
 
  const { data, isLoading, error } = useQuery({
    queryKey: ['topPosts', pageFromUrl],
    queryFn: () => fetchTopPosts(pageFromUrl),
    staleTime: 60000,
    retry: 2,
  });
 
  const handlePageChange = (page: number) => {
    setSearchParams({ page: page.toString() });
    window.scrollTo({ top: 0, behavior: 'smooth' });
  };
 
  const startRank = calculateStartRank(pageFromUrl, POSTS_PER_PAGE);
  const posts = data?.posts || [];
  const totalPages = data?.totalPages || 0;
 
  return (
    <Box sx={{ maxWidth: '1200px', margin: '0 auto', p: 3 }}>
      <Typography variant="h4" component="h1" gutterBottom>
        <FormattedMessage id="topPosts.title" />
      </Typography>
 
      {isLoading && (
        <Box sx={{ display: 'flex', justifyContent: 'center', my: 4 }}>
          <CircularProgress />
        </Box>
      )}
 
      {error && (
        <Alert severity="error" sx={{ mb: 2 }}>
          {error instanceof Error ? error.message : 'An error occurred'}
        </Alert>
      )}
 
      {!isLoading && !error && posts.length === 0 && (
        <Typography variant="body1" color="text.secondary">
          No posts found.
        </Typography>
      )}
 
      {!isLoading && posts.length > 0 && (
        <>
          {viewMode === ViewMode.List ? (
            <>
              {posts.map((post, index) => (
                <PostListItem key={post.id} item={post} rank={startRank + index} />
              ))}
            </>
          ) : (
            <Box
              sx={{
                display: 'grid',
                gridTemplateColumns: { xs: '1fr', sm: 'repeat(2, 1fr)', md: 'repeat(3, 1fr)' },
                gap: 2,
              }}
            >
              {posts.map((post, index) => (
                <PostGridItem key={post.id} item={post} rank={startRank + index} />
              ))}
            </Box>
          )}
          <Pagination currentPage={pageFromUrl} totalPages={totalPages} onPageChange={handlePageChange} />
        </>
      )}
    </Box>
  );
}
 
export default TopPostsTanStack;