/
d.vader
/
e2e
Обзор
Документация
Войти
/
d.vader
/
e2e
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
src/components/posts/PostsList.tsx
299 строк
8 KB
Boris Meshcheryakov
initial commit
01 окт 2025, 09:22
01 окт 2025, 09:22
3edac10
Код
Авторство
О чём код?
import React, { useState, useEffect } from "react"; import { Card, CardContent, CardMedia, Typography, Box, Grid, Rating, Chip, CircularProgress, Alert, Button, TextField, InputAdornment, IconButton, AppBar, Toolbar, } from "@mui/material"; import { Search as SearchIcon, Clear as ClearIcon } from "@mui/icons-material"; import { useNavigate } from "react-router-dom"; import { fetchPosts, searchPosts, Post, PostsResponse, } from "../../services/api"; export const PostsList: React.FC = () => { const [posts, setPosts] = useState<Post[]>([]); const [loading, setLoading] = useState(true); const [error, setError] = useState<string | null>(null); const [searchTerm, setSearchTerm] = useState(""); const [page, setPage] = useState(1); const [hasNextPage, setHasNextPage] = useState(false); const [hasPrevPage, setHasPrevPage] = useState(false); const navigate = useNavigate(); // Загрузка постов const loadPosts = async (pageNum: number = 1, search: string = "") => { try { setLoading(true); setError(null); let response: PostsResponse; if (search.trim()) { response = await searchPosts(search, pageNum, 12); } else { response = await fetchPosts(pageNum, 12); } setPosts(response.data); setHasNextPage(!!response.next); setHasPrevPage(!!response.previous); } catch (err) { setError(err instanceof Error ? err.message : "Ошибка загрузки данных"); } finally { setLoading(false); } }; // Первоначальная загрузка useEffect(() => { // setTimeout(()=>{ loadPosts(); // }, 3000) }, []); // Поиск const handleSearch = () => { setPage(1); loadPosts(1, searchTerm); }; // Очистка поиска const handleClearSearch = () => { setSearchTerm(""); setPage(1); loadPosts(1, ""); }; // Пагинация const handleNextPage = () => { const nextPage = page + 1; setPage(nextPage); loadPosts(nextPage, searchTerm); }; const handlePrevPage = () => { const prevPage = page - 1; setPage(prevPage); loadPosts(prevPage, searchTerm); }; // Переход к посту const handlePostClick = (postId: number) => { navigate(`/post/${postId}`); }; if (loading && posts.length === 0) { return ( <Box display="flex" justifyContent="center" alignItems="center" minHeight="400px" > <CircularProgress data-testid="loading-spinner" /> </Box> ); } if (error) { return ( <Alert severity="error" sx={{ margin: 2 }} data-testid="error-message"> {error} </Alert> ); } return ( <Box sx={{ flexGrow: 1 }}> {/* Простой Header */} <AppBar position="static"> <Toolbar> <Typography variant="h6" component="div" sx={{ flexGrow: 1 }}> Travel Blog </Typography> <Button color="inherit" onClick={() => navigate("/style")} sx={{ marginLeft: 2 }} > Стили </Button> </Toolbar> </AppBar> <Box sx={{ padding: 2 }}> {/* Поиск */} <Box sx={{ marginBottom: 3 }}> <TextField fullWidth placeholder="Поиск по названию или описанию..." value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} onKeyPress={(e) => e.key === "Enter" && handleSearch()} InputProps={{ startAdornment: ( <InputAdornment position="start"> <SearchIcon /> </InputAdornment> ), endAdornment: searchTerm && ( <InputAdornment position="end"> <IconButton onClick={handleClearSearch} edge="end"> <ClearIcon /> </IconButton> </InputAdornment> ), }} /> <Box sx={{ display: "flex", gap: 2, marginTop: 1 }}> <Button variant="contained" onClick={handleSearch}> Найти </Button> {searchTerm && ( <Button data-testId='clear-btn' variant="outlined" onClick={handleClearSearch}> Очистить </Button> )} </Box> </Box> <h1>Путешествия по миру</h1> <h6>Откройте для себя самые красивые места</h6> {/* Список постов */} <Grid container spacing={3} data-testid="posts-grid"> {posts.map((post) => ( <Grid sx={{ display: "flex", flexWrap: "wrap" }} key={post.id}> <Card data-testid="post-card" sx={{ cursor: "pointer", transition: "transform 0.2s", "&:hover": { transform: "translateY(-4px)", boxShadow: 4, }, maxWidth: '400px' }} onClick={() => handlePostClick(post.id)} > <CardMedia component="img" height="200" image={post.url} alt={post.title} data-testid="post-image" sx={{ objectFit: "cover" }} /> <CardContent> <Typography variant="h6" component="h2" data-testid="post-title" sx={{ marginBottom: 1, overflow: "hidden", textOverflow: "ellipsis", display: "-webkit-box", WebkitLineClamp: 2, WebkitBoxOrient: "vertical", }} > {post.title} </Typography> <Typography variant="body2" color="text.secondary" data-testid="post-body" sx={{ marginBottom: 2, overflow: "hidden", textOverflow: "ellipsis", display: "-webkit-box", WebkitLineClamp: 3, WebkitBoxOrient: "vertical", }} > {post.body} </Typography> <Box sx={{ display: "flex", alignItems: "center", gap: 1 }}> <Rating value={post.rate} max={10} size="small" readOnly data-testid="post-rating" /> <Chip label={`${post.rate}/10`} size="small" color="primary" data-testid="rating-chip" /> </Box> </CardContent> </Card> </Grid> ))} </Grid> {/* Пагинация */} {posts.length > 0 && ( <Box sx={{ display: "flex", justifyContent: "center", gap: 2, marginTop: 4, }} > <Button variant="outlined" onClick={handlePrevPage} disabled={!hasPrevPage || loading} > Предыдущая </Button> <Typography variant="body1" sx={{ alignSelf: "center" }}> Страница {page} </Typography> <Button variant="outlined" onClick={handleNextPage} disabled={!hasNextPage || loading} > Следующая </Button> </Box> )} {/* Индикатор загрузки для пагинации */} {loading && posts.length > 0 && ( <Box display="flex" justifyContent="center" sx={{ marginTop: 2 }}> <CircularProgress size={24} /> </Box> )} </Box> </Box> ); };