Feed funktioniert, infinite scroll funktioniert nicht mehr

This commit is contained in:
MisbehavedNinjaRadiator 2025-06-28 16:44:48 +02:00 committed by MisbehavedNinjaRadiator
parent c1d8e6b483
commit ea2d36de1c
3 changed files with 116 additions and 66 deletions

View file

@ -14,57 +14,79 @@ import FavoriteIcon from '@mui/icons-material/Favorite';
import ShareIcon from '@mui/icons-material/Share'; import ShareIcon from '@mui/icons-material/Share';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import MoreVertIcon from '@mui/icons-material/MoreVert'; import MoreVertIcon from '@mui/icons-material/MoreVert';
import api from "../api/axios";
interface ExpandMoreProps extends IconButtonProps { interface ExpandMoreProps extends IconButtonProps {
expand: boolean; expand: boolean;
} }
interface PostProps { interface PostProps {
postId: number; postId: string;
} }
interface PostResponse {
description: string;
status: string;
likes: number;
tags: string[];
user: {
id: string;
name: string;
};
createdAt: string;
updatedAt: string;
images: {
originalName: string;
mimetype: string;
url: string;
}[];
following: boolean;
}
const ExpandMore = styled((props: ExpandMoreProps) => { const ExpandMore = styled((props: ExpandMoreProps) => {
const { expand, ...other } = props; const { expand, ...other } = props;
return <IconButton {...other} />; return <IconButton {...other} />;
})(({ theme }) => ({ })<ExpandMoreProps>(({ theme, expand }) => ({
marginLeft: 'auto', marginLeft: 'auto',
transition: theme.transitions.create('transform', { transition: theme.transitions.create('transform', {
duration: theme.transitions.duration.shortest, duration: theme.transitions.duration.shortest,
}), }),
variants: [ transform: expand ? 'rotate(180deg)' : 'rotate(0deg)',
{
props: ({ expand }) => !expand,
style: {
transform: 'rotate(0deg)',
},
},
{
props: ({ expand }) => !!expand,
style: {
transform: 'rotate(180deg)',
},
},
],
})); }));
export default function Post({postId}: PostProps) { export default function Post({ postId }: PostProps) {
const [expanded, setExpanded] = React.useState(false); const [expanded, setExpanded] = React.useState(false);
const content = "Fetch content here"; const [post, setPost] = React.useState<PostResponse | null>(null);
const expandedContent = "Fetch expanded here"
const title = "Fetch heading here";
const createdAt = "Fetch created at here";
const user = "Fetch user here";
const media = "Fetch media here (path)";
const handleExpandClick = () => { React.useEffect(() => {
setExpanded(!expanded); getPostbyID();
}; // eslint-disable-next-line
}, [postId]);
async function getPostbyID(): Promise<void> {
try {
const response = await api.get<PostResponse>(`/posts/getPost/{postId}?postId=${postId}`);
//const response = await api.get<PostResponse>(`http://localhost:3001/api/posts/getPost/{postId}?postId=${postId}`);
setPost(response.data);
} catch (error) {
console.error("Failed to fetch post:", error);
}
}
if (!post) {
return (
<Card sx={{ maxWidth: 345, margin: 2 }}>
<CardContent>
<Typography>Loading...</Typography>
</CardContent>
</Card>
);
}
return ( return (
<Card sx={{ maxWidth: 345 }}> <Card sx={{ maxWidth: 345, margin: 2 }}>
<CardHeader <CardHeader
avatar={ avatar={
<Avatar sx={{ bgcolor: red[500] }} aria-label="recipe"> <Avatar sx={{ bgcolor: red[500] }} aria-label="user">
{user ? user.charAt(0).toUpperCase() : 'U'} //Todo: when fetching change to user.name or sth {post.user.name.charAt(0).toUpperCase()}
</Avatar> </Avatar>
} }
action={ action={
@ -72,17 +94,32 @@ export default function Post({postId}: PostProps) {
<MoreVertIcon /> <MoreVertIcon />
</IconButton> </IconButton>
} }
title={title} title={post.user.name}
subheader= {createdAt} subheader={new Date(post.createdAt).toLocaleString()}
/> />
{post.images && post.images.length > 0 && (
<CardMedia <CardMedia
component="img" component="img"
height="194" height="194"
image= {media} image={post.images[0].url}
alt={post.images[0].originalName}
/> />
)}
<CardContent> <CardContent>
<Typography variant="body2" sx={{ color: 'text.secondary' }}> <Typography variant="body1" sx={{ fontWeight: 600 }}>
{content} {post.description}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
Status: {post.status}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
Likes: {post.likes}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
Tags: {post.tags.join(", ")}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
Following: {post.following ? "Ja" : "Nein"}
</Typography> </Typography>
</CardContent> </CardContent>
<CardActions disableSpacing> <CardActions disableSpacing>
@ -94,7 +131,7 @@ export default function Post({postId}: PostProps) {
</IconButton> </IconButton>
<ExpandMore <ExpandMore
expand={expanded} expand={expanded}
onClick={handleExpandClick} onClick={() => setExpanded(!expanded)}
aria-expanded={expanded} aria-expanded={expanded}
aria-label="show more" aria-label="show more"
> >
@ -103,7 +140,12 @@ export default function Post({postId}: PostProps) {
</CardActions> </CardActions>
<Collapse in={expanded} timeout="auto" unmountOnExit> <Collapse in={expanded} timeout="auto" unmountOnExit>
<CardContent> <CardContent>
<Typography sx={{ marginBottom: 2 }}>{expandedContent}</Typography> <Typography variant="body2" color="text.secondary">
Erstellt am: {new Date(post.createdAt).toLocaleString()}
</Typography>
<Typography variant="body2" color="text.secondary">
Zuletzt aktualisiert: {new Date(post.updatedAt).toLocaleString()}
</Typography>
</CardContent> </CardContent>
</Collapse> </Collapse>
</Card> </Card>

View file

@ -1,34 +1,45 @@
import React, { useState, useEffect, useRef } from "react"; import React, { useState, useEffect, useRef } from "react";
import Post from "../Post"; import Post from "../Post";
import "./feed.css"; import "./feed.css";
import api from "../../api/axios";
interface PostListItem {
id: string;
createdAt: string;
description: string;
}
function Feed() { function Feed() {
const [posts, setPosts] = useState<number[]>([]); const [posts, setPosts] = useState<PostListItem[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [hasMore, setHasMore] = useState(true); const [hasMore, setHasMore] = useState(true);
const [nextCursor, setNextCursor] = useState<string | null>(null);
const feedRef = useRef<HTMLDivElement | null>(null); const feedRef = useRef<HTMLDivElement | null>(null);
const PAGE_SIZE = 10; const PAGE_SIZE = 10;
// Dummy fetch function that simulates loading posts by ID const fetchPosts = async () => {
const fetchPosts = () => { if (loading || !hasMore) return;
if (loading) return;
setLoading(true); setLoading(true);
try {
const params: any = { limit: PAGE_SIZE };
if (nextCursor) params.createdAt = nextCursor;
interface FeedResponse {
posts: PostListItem[];
setTimeout(() => { nextCursor: string | null;
setPosts((prev) => {
const newPosts = [];
for (let i = 0; i < PAGE_SIZE; i++) {
newPosts.push(prev.length + i + 1);
} }
return [...prev, ...newPosts]; const response = await api.get<FeedResponse>("/feed?limit=10");
}); //const response = await api.get<FeedResponse>("http://localhost:3001/api/feed?limit=10");
console.log("Feed response:", response.data);
const { posts: newPosts, nextCursor: newCursor } = response.data;
setPosts((prev) => [...prev, ...newPosts]);
setNextCursor(newCursor);
setHasMore(!!newCursor && newPosts.length > 0);
} catch (error) {
console.error("Error fetching posts:", error);
} finally {
setLoading(false); setLoading(false);
// Stop after 50 posts, just as an example
if (posts.length + PAGE_SIZE >= 50) {
setHasMore(false);
} }
}, 800);
}; };
useEffect(() => { useEffect(() => {
@ -39,24 +50,23 @@ function Feed() {
const onScroll = () => { const onScroll = () => {
const feed = feedRef.current; const feed = feedRef.current;
if (!feed || loading || !hasMore) return; if (!feed || loading || !hasMore) return;
if (feed.scrollTop + feed.clientHeight >= feed.scrollHeight - 100) { if (feed.scrollTop + feed.clientHeight >= feed.scrollHeight - 100) {
fetchPosts(); fetchPosts();
} }
}; };
const feed = feedRef.current; const feed = feedRef.current;
feed?.addEventListener("scroll", onScroll); feed?.addEventListener("scroll", onScroll);
return () => { return () => {
feed?.removeEventListener("scroll", onScroll); feed?.removeEventListener("scroll", onScroll);
}; };
}, [loading, hasMore]); }, [loading, hasMore, nextCursor]);
return ( return (
<div className="feedContainer"> <div className="feedContainer">
<main className="feedContent" ref={feedRef}> <main className="feedContent" ref={feedRef}>
{posts.map((postId) => ( {posts.length === 0 && !loading && <div>Keine Posts gefunden.</div>}
<Post key={postId} postId={postId} /> {posts.map((post) => (
<Post key={post.id} postId={post.id} />
))} ))}
{loading && <div className="loading">Loading more posts...</div>} {loading && <div className="loading">Loading more posts...</div>}
{!hasMore && <div>No more posts</div>} {!hasMore && <div>No more posts</div>}

View file

@ -1,9 +1,7 @@
.feedContainer { .feedContainer {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100vh; min-height: calc(100vh - var(--Header-height));
overflow: hidden;
background-color: #f9f9f9; background-color: #f9f9f9;
} }