Feed and user Feed done

This commit is contained in:
MisbehavedNinjaRadiator 2025-06-30 18:50:04 +02:00 committed by MisbehavedNinjaRadiator
parent 5f9e3a24f7
commit 133cec2fb4
29 changed files with 331 additions and 10621 deletions

View file

@ -1,7 +1,4 @@
import React, { use } from 'react';
import logo from './logo.svg';
import './App.css';
import { useState, useEffect } from 'react';
import LoginAndSignUpPage from './pages/LoginAndSignUpPage';
import PostCreation from './pages/PostCreation';
import "./App.css";
@ -9,13 +6,14 @@ import "./styles/colors.css";
import "./styles/fonts.css";
import "./styles/sizes.css";
import Footer from "./components/footer/Footer";
import Feed, {UserFeedRoute} from "./components/feed/Feed";
import Feed, {UserFeedRoute} from "./pages/feed/Feed";
import Header from './components/header/Header';
import Profile from "./pages/Profile";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import { BrowserRouter as Router, Routes, Route, Navigate } from "react-router-dom";
import { Auth } from "./api/Auth";
import { NotFound } from "./pages/404Page/NotFoundPage";
import ScrollToAnchor from "./components/ScrollToAnchor";
function App() {
@ -23,7 +21,6 @@ function App() {
<Auth>
<Router>
<Header />
<ScrollToAnchor />
<div className="App">
<Routes>
<Route path="*" element={<NotFound />} />
@ -39,6 +36,13 @@ function App() {
<Route path="/createpost" element={<PostCreation />}></Route>
<Route path="/profile" element={<Profile />}></Route>
<Route path="/feed/:user" element={<UserFeedRoute />}></Route>
<Route path="/" element={<Navigate to="/feed" replace />} />
<Route path="/feed" element={<Feed />} />
<Route path="/feed/:user" element={<UserFeedRoute />} />
<Route path="*" element={<NotFound />} />
<Route path="/login" element={<LoginAndSignUpPage signupProp={false} />} />
<Route path="/register" element={<LoginAndSignUpPage signupProp={true} />} />
<Route path="/profile/:username" element={<Profile />} />
</Routes>
<Footer />
</div>

View file

@ -1,129 +0,0 @@
import * as React from 'react';
import { styled } from '@mui/material/styles';
import Card from '@mui/material/Card';
import CardHeader from '@mui/material/CardHeader';
import CardMedia from '@mui/material/CardMedia';
import CardContent from '@mui/material/CardContent';
import CardActions from '@mui/material/CardActions';
import Collapse from '@mui/material/Collapse';
import Avatar from '@mui/material/Avatar';
import IconButton, { IconButtonProps } from '@mui/material/IconButton';
import Typography from '@mui/material/Typography';
import { red } from '@mui/material/colors';
import FavoriteIcon from '@mui/icons-material/Favorite';
import ShareIcon from '@mui/icons-material/Share';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import MoreVertIcon from '@mui/icons-material/MoreVert';
interface ExpandMoreProps extends IconButtonProps {
expand: boolean;
}
const ExpandMore = styled((props: ExpandMoreProps) => {
const { expand, ...other } = props;
return <IconButton {...other} />;
})(({ theme }) => ({
marginLeft: 'auto',
transition: theme.transitions.create('transform', {
duration: theme.transitions.duration.shortest,
}),
variants: [
{
props: ({ expand }) => !expand,
style: {
transform: 'rotate(0deg)',
},
},
{
props: ({ expand }) => !!expand,
style: {
transform: 'rotate(180deg)',
},
},
],
}));
export default function Post() {
const [expanded, setExpanded] = React.useState(false);
const handleExpandClick = () => {
setExpanded(!expanded);
};
return (
<Card sx={{ maxWidth: 345 }}>
<CardHeader
avatar={
<Avatar sx={{ bgcolor: red[500] }} aria-label="recipe">
R
</Avatar>
}
action={
<IconButton aria-label="settings">
<MoreVertIcon />
</IconButton>
}
title="Shrimp and Chorizo Paella"
subheader="September 14, 2016"
/>
<CardMedia
component="img"
height="194"
image="/static/images/cards/paella.jpg"
alt="Paella dish"
/>
<CardContent>
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
This impressive paella is a perfect party dish and a fun meal to cook
together with your guests. Add 1 cup of frozen peas along with the mussels,
if you like.
</Typography>
</CardContent>
<CardActions disableSpacing>
<IconButton aria-label="add to favorites">
<FavoriteIcon />
</IconButton>
<IconButton aria-label="share">
<ShareIcon />
</IconButton>
<ExpandMore
expand={expanded}
onClick={handleExpandClick}
aria-expanded={expanded}
aria-label="show more"
>
<ExpandMoreIcon />
</ExpandMore>
</CardActions>
<Collapse in={expanded} timeout="auto" unmountOnExit>
<CardContent>
<Typography sx={{ marginBottom: 2 }}>Method:</Typography>
<Typography sx={{ marginBottom: 2 }}>
Heat 1/2 cup of the broth in a pot until simmering, add saffron and set
aside for 10 minutes.
</Typography>
<Typography sx={{ marginBottom: 2 }}>
Heat oil in a (14- to 16-inch) paella pan or a large, deep skillet over
medium-high heat. Add chicken, shrimp and chorizo, and cook, stirring
occasionally until lightly browned, 6 to 8 minutes. Transfer shrimp to a
large plate and set aside, leaving chicken and chorizo in the pan. Add
pimentón, bay leaves, garlic, tomatoes, onion, salt and pepper, and cook,
stirring often until thickened and fragrant, about 10 minutes. Add
saffron broth and remaining 4 1/2 cups chicken broth; bring to a boil.
</Typography>
<Typography sx={{ marginBottom: 2 }}>
Add rice and stir very gently to distribute. Top with artichokes and
peppers, and cook without stirring, until most of the liquid is absorbed,
15 to 18 minutes. Reduce heat to medium-low, add reserved shrimp and
mussels, tucking them down into the rice, and cook again without
stirring, until mussels have opened and rice is just tender, 5 to 7
minutes more. (Discard any mussels that don&apos;t open.)
</Typography>
<Typography>
Set aside off of the heat to let rest for 10 minutes, and then serve.
</Typography>
</CardContent>
</Collapse>
</Card>
);
}

View file

@ -1,26 +0,0 @@
import { useEffect, useRef } from 'react';
import { useLocation } from 'react-router-dom';
// use like this: onClick={() => navigate(`/feed/${username}#${post.id}`)}
function ScrollToAnchor() {
const location = useLocation();
const lastHash = useRef('');
useEffect(() => {
if (location.hash) {
lastHash.current = location.hash.slice(1);
}
if (lastHash.current && document.getElementById(lastHash.current)) {
setTimeout(() => {
document
.getElementById(lastHash.current)
?.scrollIntoView({ behavior: 'smooth', block: 'start' });
lastHash.current = '';
}, 100);
}
}, [location]);
return null;
}
export default ScrollToAnchor;

View file

@ -1,29 +1,45 @@
import { Avatar, Box, Typography } from "@mui/material";
import api from "../api/axios";
import { useState,useEffect } from "react";
import { useState, useEffect } from "react";
interface UserAvatarProps {
username: string|null;
username: string | null;
src?: string;
size?: number | string;
onClick?: () => void;
}
export default function UserAvatar({ username, size = 40 }: UserAvatarProps) {
const[pb, setPb] = useState<string>();
export default function UserAvatar({
username,
size = 40,
onClick,
}: UserAvatarProps) {
const [pb, setPb] = useState<string>();
useEffect(() => {
useEffect(() => {
(async () => {
try {
const res = await api.get(`/profile/getProfilePicture/${username}`)
setPb((res.data as { url: string }).url);
} catch (error) {
console.log(error);
const res = await api.get(`/profile/getProfilePicture/${username}`);
setPb((res.data as { url: string }).url);
} catch (error: any) {
if (error.status !== 404) {
console.log(error);
}
}
})();
}, [username]);
return (
<Box sx={{ display: "flex", alignItems: "center", gap: 1, maxWidth: "600px"}}>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: 1,
maxWidth: "600px",
cursor: onClick ? "pointer" : "default",
}}
onClick={onClick}
>
<Avatar
src={pb}
alt={username || "avatar"}
@ -39,10 +55,14 @@ export default function UserAvatar({ username, size = 40 }: UserAvatarProps) {
<Typography
component="span"
fontWeight={500}
sx={{ whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis"}}
sx={{
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
{username}
</Typography>
</Box>
);
}
}

View file

@ -0,0 +1,15 @@
import { useNavigate } from "react-router-dom";
import ButtonRotkehlchen from "./buttonRotkehlchen/ButtonRotkehlchen";
export default function LogInButton() {
const navigate = useNavigate();
return (
<ButtonRotkehlchen
style={"primary"}
label={"Login"}
type={"button"}
onClick={() => navigate("/login")}
/>
);
}

View file

@ -0,0 +1,15 @@
import { useNavigate } from "react-router-dom";
import ButtonRotkehlchen from "./buttonRotkehlchen/ButtonRotkehlchen";
export default function LogInButton() {
const navigate = useNavigate();
return (
<ButtonRotkehlchen
style={"secondary"}
label={"Sign Up"}
type={"button"}
onClick={() => navigate("/login")}
/>
);
}

View file

@ -1,16 +0,0 @@
import React from "react";
import "./welcomeMessage.css";
export default function WelcomeMessage() {
return (
<div className="welcome-message">
<h1 className="welcome-title">Welcome!</h1>
<p className="welcome-text">
Become a part of our big, bird loving community!
</p>
<p className="desktop-welcome-text">
Exchange pictures and information about birds and be part of our big bird loving community! Pellentesque vulputate a enim ac feugiat. Donec dictum magna sit amet arcu commodo, quis vehicula nunc commodo. Pellentesque varius congue varius.
</p>
</div>
);
}

View file

@ -0,0 +1,26 @@
import { useNavigate } from "react-router-dom";
import "./naggingFooter.css";
import LogInButton from "../buttons/LogInButton";
import SignUpButton from "../buttons/SignUpButton";
export default function NaggingFooter() {
const navigate = useNavigate();
return (
<div className="footer-container">
<img
className="header-icon header-icon-feather"
src="/assets/icons/BirdIconO.ico"
alt="featherIcon"
onClick={() => navigate("/")}
/>
<p className="header-title" onClick={() => navigate("/")}>
Feather Feed
</p>
<div className="footer-spacer" />
<div className="signup-button">
<SignUpButton />
</div>
<LogInButton />
</div>
);
}

View file

@ -0,0 +1,61 @@
.footer-container {
position: fixed;
left: 0;
bottom: 0;
width: 100vw;
min-height: 32px;
max-height: 15vh;
background-color: var(--Rotkehlchen-gray);
z-index: 2000;
display: flex;
flex-wrap: nowrap;
min-width: 0;
align-items: center;
justify-content: center;
padding: 0;
margin: 0;
}
@media (min-width: 768px) {
.footer-container {
display: none;
}
}
.header-title {
white-space: nowrap;
font-size: 1.5rem;
font-weight: bold;
margin-left: 0.3rem;
margin-right: 0;
flex-shrink: 0;
}
.footer-spacer {
flex: 1 1 2rem;
min-width: 0.5rem;
max-width: 2.5rem;
}
.signup-button,
.footer-container button,
.header-icon-feather {
flex-shrink: 0;
}
.signup-button {
white-space: nowrap;
font-size: 0.95rem;
margin-right: 0.2rem;
padding-right: 0;
}
.footer-container button {
margin: 0 !important;
padding: 8px;
margin-right: 6px !important;
}
.header-icon-feather {
width: 30px;
margin: 0px !important;
margin-left: 6px !important;
}

View file

@ -6,25 +6,23 @@ import CardMedia from "@mui/material/CardMedia";
import CardContent from "@mui/material/CardContent";
import CardActions from "@mui/material/CardActions";
import Collapse from "@mui/material/Collapse";
import Avatar from "@mui/material/Avatar";
import IconButton, { IconButtonProps } from "@mui/material/IconButton";
import Typography from "@mui/material/Typography";
import { red } from "@mui/material/colors";
import FavoriteIcon from "@mui/icons-material/Favorite";
import ShareIcon from "@mui/icons-material/Share";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import MoreVertIcon from "@mui/icons-material/MoreVert";
import api from "../api/axios";
import api from "../../api/axios";
import { Url } from "url";
import { LogLevel } from "vite";
import "./post.css";
import UserAvatar from "./UserAvatar";
import UserAvatar from "../UserAvatar";
import { useNavigate } from "react-router-dom";
import { useEffect, useRef, useCallback } from "react";
interface ExpandMoreProps extends IconButtonProps {
expand: boolean;
}
interface PostProps {
postId: string;
autoScroll?: boolean;
}
interface PostResponse {
description: string;
@ -58,17 +56,17 @@ const ExpandMore = styled((props: ExpandMoreProps) => {
transform: expand ? "rotate(180deg)" : "rotate(0deg)",
}));
export default function Post({ postId }: PostProps) {
export default function Post({ postId, autoScroll }: PostProps) {
const [expanded, setExpanded] = React.useState(false);
const [post, setPost] = React.useState<PostResponse | null>(null);
const [currentImage, setCurrentImage] = React.useState(0);
const [like, setLike] = React.useState(false);
const navigate = useNavigate();
const postRef = useRef<HTMLDivElement>(null);
const scrollTimeoutsRef = useRef<number[]>([]);
React.useEffect(() => {
getPostbyID();
}, [postId]);
async function getPostbyID(): Promise<void> {
// Using useCallback to memoize the function and avoid dependency issues
const getPostbyID = useCallback(async (): Promise<void> => {
try {
const response = await api.get<PostResponse>(
`/posts/getPost/{postId}?postId=${postId}`
@ -79,7 +77,43 @@ export default function Post({ postId }: PostProps) {
} catch (error) {
console.error("Failed to fetch post:", error);
}
}
}, [postId]);
// Auto-scroll effect with cleanup for timeouts
useEffect(() => {
if (autoScroll && post) {
console.log("Preparing to scroll to post:", postId);
// Clear any existing timeouts first
scrollTimeoutsRef.current.forEach(clearTimeout);
scrollTimeoutsRef.current = [];
// Using forEach instead of map since we don't need to return values
[50, 100, 300, 500, 1000, 1500].forEach((delay) => {
const timeoutId = window.setTimeout(() => {
if (postRef.current) {
console.log(`Scrolling attempt at ${delay}ms`);
postRef.current.scrollIntoView({
behavior: "auto",
block: "start",
});
}
}, delay);
scrollTimeoutsRef.current.push(timeoutId);
});
}
// Clean up timeouts when component unmounts or dependencies change
return () => {
scrollTimeoutsRef.current.forEach(clearTimeout);
scrollTimeoutsRef.current = [];
};
}, [autoScroll, post, postId]);
// Fetch post data when postId changes
useEffect(() => {
getPostbyID();
}, [getPostbyID]); // Now getPostbyID is a dependency
if (!post) {
return (
@ -122,9 +156,19 @@ export default function Post({ postId }: PostProps) {
return (
<StyledEngineProvider injectFirst>
<Card className="body-l" sx={{ maxWidth: 600, width: "100%", margin: 2 }}>
<Card
ref={postRef}
className="body-l"
sx={{ maxWidth: 600, width: "100%", margin: 2 }}
>
<CardHeader
avatar={<UserAvatar username={post.user.name} size={60} />}
avatar={
<UserAvatar
username={post.user.name}
size={60}
onClick={() => navigate(`/profile/${post.user.name}`)}
/>
}
/>
{images.length > 0 && (
<div className="post-image-carousel">
@ -187,20 +231,16 @@ export default function Post({ postId }: PostProps) {
</CardActions>
<Collapse in={expanded} timeout="auto" unmountOnExit>
<CardContent>
<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 variant="body2" color="text.secondary" sx={{ mb: 1 }}>
Status: {post.status}
{post.tags && post.tags.length > 0 && (
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
Tags: {post.tags.join(", ")}
</Typography>
)}
<Typography variant="body2" color="text.secondary">
Created at: {new Date(post.createdAt).toLocaleString()}
</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()}
Last updated: {new Date(post.updatedAt).toLocaleString()}
</Typography>
</CardContent>
</Collapse>

View file

@ -5,7 +5,7 @@ import { useState } from "react";
import "./bio.css";
import IconButton from "@mui/material/IconButton";
import EditSquareIcon from "@mui/icons-material/EditSquare";
import ButtonPrimary from "../ButtonRotkehlchen";
import ButtonPrimary from "../buttons/buttonRotkehlchen/ButtonRotkehlchen";
import api from "../../api/axios";
export default function BioTextField({ ownAccount, bioText, setBio } : { ownAccount: boolean, bioText: string | undefined, setBio: (bio: string) => void }) {

View file

@ -14,7 +14,7 @@ import {
import CloseIcon from "@mui/icons-material/Close";
import EditSquareIcon from "@mui/icons-material/EditSquare";
import "./changeAvatarDialog.css";
import ButtonRotkehlchen from "../ButtonRotkehlchen";
import ButtonRotkehlchen from "../buttons/buttonRotkehlchen/ButtonRotkehlchen";
import Username from "./Username";
import "./username.css";
import api from "../../api/axios";
@ -55,17 +55,17 @@ export default function AvatarDialog({
console.log("Saving profile picture:", file);
if (file) {
formData.append("image", file);
const response = await api.post(
const response = await api.post<{ url: string }>(
"/profile/uploadProfilePicture",
formData
);
console.log("Profile picture saved:", response.data);
setUserData((prevData) => (prevData ?{
setUserData((prevData) => (prevData ? {
...prevData,
profilePictureUrl: response.data.url
} : null));
}
setOpen(false); // Close the dialog after saving
setOpen(false);
} catch (error) {
console.error("Error saving profile picture:", error);
}

View file

@ -7,6 +7,16 @@ import { useEffect, useState } from "react";
import { UserProfile } from "../../types/UserProfile";
import { useNavigate } from "react-router-dom";
type Post = {
id: string;
description: string;
createdAt: string;
};
type PostImagesResponse = {
images: { url: string }[];
};
export default function StandardImageList({ user }: { user: UserProfile }) {
const navigate = useNavigate();
const [images, setImages] = useState<
@ -22,17 +32,15 @@ export default function StandardImageList({ user }: { user: UserProfile }) {
const fetchUserPosts = async () => {
try {
const response = await api.get(`/posts/getUserPosts/${user.username}`);
const response = await api.get<{ posts: Post[] }>(
`/posts/getUserPosts/${user.username}`
);
const posts = response.data.posts;
const fetchedImages = await Promise.all(
posts.map(
async (post: {
id: string;
description: string;
createdAt: string;
}) => {
async (post: Post) => {
try {
const response = await api.get(
const response = await api.get<PostImagesResponse>(
`/posts/getPost/{postId}?postId=${post.id}`
);
if (response.data && response.data.images.length > 0) {
@ -42,6 +50,7 @@ export default function StandardImageList({ user }: { user: UserProfile }) {
id: post.id,
description: post.description || "",
createdAt: post.createdAt,
};
}
} catch (error) {
@ -51,7 +60,12 @@ export default function StandardImageList({ user }: { user: UserProfile }) {
)
);
console.log("Fetched images:", fetchedImages);
setImages(fetchedImages.filter((image) => image !== undefined));
setImages(
fetchedImages.filter(
(image): image is { imageUrl: string; id: string; description: string; createdAt: string } =>
image !== undefined
)
);
} catch (error) {
console.error("Error fetching user posts:", error);
}
@ -68,10 +82,7 @@ export default function StandardImageList({ user }: { user: UserProfile }) {
<img
src={item.imageUrl}
alt={item.description}
onClick={
() => navigate("/feed")
// anchor to post that was clicked
}
onClick={() => navigate(`/feed/${user.username}#${item.id}`)}
loading="lazy"
/>
) : (

View file

@ -0,0 +1,15 @@
import "./welcomeMessage.css";
export default function WelcomeMessage() {
return (
<div className="welcome-message">
<h1 className="welcome-title">Welcome!</h1>
<p className="welcome-text">Join our bird-loving community!</p>
<p className="desktop-welcome-text">
Exchange pictures, share your experiences, and discover interesting
facts about birds from all over the world. Celebrate
the beauty and diversity of birds with us!
</p>
</div>
);
}

View file

@ -1,6 +1,6 @@
import React, { useEffect, useRef, useState } from "react";
import "./notFound.css";
import ButtonPrimary from "../../components/ButtonRotkehlchen";
import ButtonPrimary from "../../components/buttons/buttonRotkehlchen/ButtonRotkehlchen";
type Block = {
x: number;

View file

@ -1,7 +1,7 @@
import "./loginAndSignUpPage.css";
import { useEffect, useState } from "react";
import api from "../api/axios";
import ButtonRotkehlchen from "../components/ButtonRotkehlchen";
import ButtonRotkehlchen from "../components/buttons/buttonRotkehlchen/ButtonRotkehlchen";
import { useLocation, useNavigate } from "react-router-dom";
import { useAuth } from "../api/Auth";
import { createTheme, useMediaQuery } from "@mui/material";

View file

@ -5,7 +5,7 @@ import { useNavigate } from "react-router-dom";
import Chip from '@mui/material/Chip';
import Autocomplete from '@mui/material/Autocomplete';
import TextField from '@mui/material/TextField';
import ButtonPrimary from "../components/ButtonRotkehlchen";
import ButtonPrimary from "../components/buttons/buttonRotkehlchen/ButtonRotkehlchen";
import api from "../api/axios";
import { useAuth } from "../api/Auth";

View file

@ -3,7 +3,7 @@ import QuiltedImageList from "../components/profile/QuiltedImageList";
import { StyledEngineProvider, Divider } from "@mui/material";
import ChangeAvatarDialog from "../components/profile/ChangeAvatarDialog";
import Bio from "../components/profile/Bio";
import RotkehlchenButton from "../components/ButtonRotkehlchen";
import RotkehlchenButton from "../components/buttons/buttonRotkehlchen/ButtonRotkehlchen";
import api, { redirectToLogin } from "../api/axios";
import { useAuth } from "../api/Auth";
import { useNavigate, useParams } from "react-router-dom";
@ -28,11 +28,11 @@ function Profile() {
const userProfile = async () => {
try {
const response = await api.get(`/profile/${username}`);
const response = await api.get<{ data: UserProfile }>(`/profile/${username}`);
setUserData(response.data.data);
return;
} catch (error) {
navigate("/"); /* replace to 404 page */
navigate("/notfound");
console.error("Error fetching user profile:", error);
return null;
}

View file

@ -1,12 +1,13 @@
import React, { useState, useEffect, useRef } from "react";
import Post from "../Post";
import Post from "../../components/post/Post";
import "./feed.css";
import api from "../../api/axios";
import { create } from "axios";
import WelcomeMessage from "./welcomeMessage/welcomeMessage";
import WelcomeMessage from "../../components/welcomeMessage/welcomeMessage";
import { useAuth } from "../../api/Auth";
import ButtonRotkehlchen from "../ButtonRotkehlchen";
import { useNavigate, useParams } from "react-router-dom";
import LogInButton from "../../components/buttons/LogInButton";
import SignUpButton from "../../components/buttons/SignUpButton";
import NaggingFooter from "../../components/naggingFooter/NaggingFooter";
import { useLocation, useParams } from "react-router-dom";
interface PostListItem {
id: string;
@ -26,7 +27,19 @@ function Feed({ username }: FeedProps) {
const feedRef = useRef<HTMLDivElement | null>(null);
const PAGE_SIZE = 10;
const { user } = useAuth();
const navigate = useNavigate();
const scrollTargetRef = useRef<string | null>(null);
const location = useLocation();
useEffect(() => {
// Remove the # character from the hash
const hashValue = location.hash.replace("#", "");
console.log(hashValue);
console.log("Hash value:", hashValue);
if (hashValue) {
scrollTargetRef.current = hashValue;
}
}, [location]);
const fetchPosts = async () => {
if (loading || !hasMore) return;
@ -37,11 +50,13 @@ function Feed({ username }: FeedProps) {
url = `/posts/getUserPosts/${encodeURIComponent(username)}`;
const response = await api.get<{ posts: PostListItem[] }>(url);
setPosts(response.data.posts);
setHasMore(false);
setHasMore(false);
} else {
url = `/feed?limit=${PAGE_SIZE}`;
if (nextCursor) {
url = `/feed?createdAt=${encodeURIComponent(nextCursor)}&limit=${PAGE_SIZE}`;
url = `/feed?createdAt=${encodeURIComponent(
nextCursor
)}&limit=${PAGE_SIZE}`;
}
interface FeedResponse {
posts: PostListItem[];
@ -49,7 +64,8 @@ function Feed({ username }: FeedProps) {
}
const response = await api.get<FeedResponse>(url);
const { posts: newPosts, nextCursor: newCursor } = response.data;
setPosts((prev) => [...prev, ...newPosts]);
const tempPost: PostListItem[] = [...posts, ...newPosts];
setPosts(tempPost);
setNextCursor(newCursor);
setHasMore(!!newCursor && newPosts.length > 0);
}
@ -59,11 +75,6 @@ function Feed({ username }: FeedProps) {
setLoading(false);
}
};
useEffect(() => {
fetchPosts();
}, []);
useEffect(() => {
if (username) return;
@ -94,30 +105,25 @@ function Feed({ username }: FeedProps) {
{!user && (
<div className="welcome-for-logged-out">
<WelcomeMessage />
<ButtonRotkehlchen
style={"secondary"}
label={"Sign Up"}
type={"button"}
onClick={() => navigate("/register")}
/>
<ButtonRotkehlchen
style={"primary"}
label={"Login"}
type={"button"}
onClick={() => navigate("/login")}
/>
<SignUpButton />
<LogInButton />
</div>
)}
<main className="feedContent" ref={feedRef}>
{posts.length === 0 && !loading && <div>Keine Posts gefunden.</div>}
{posts.map((post) => (
<div id={post.id} key={post.id}>
<Post postId={post.id} />
<div id={post.id} key={post.id} className="feed-post-container">
<Post
postId={post.id}
autoScroll={post.id === scrollTargetRef.current}
/>
</div>
))}
{loading && <div className="loading">Loading more posts...</div>}
{!hasMore && <div>No more posts</div>}
{!hasMore && <div className="no-more-posts-message">No more posts</div>}
</main>
{!user && <NaggingFooter />}
</div>
);
}

View file

@ -1,7 +1,5 @@
.feedContainer {
min-height: 100vh;
/* Subtract he */
min-height: 100vh;
}
@ -9,7 +7,8 @@
flex: 1;
overflow-y: auto;
display: flex;
flex-wrap: wrap;
flex-wrap: nowrap;
flex-direction: column;
justify-content: center;
width: 100%;
max-width: 800px;
@ -23,9 +22,19 @@
font-weight: bold;
color: #333;
}
.feed-post-container {
width: 100%;
max-width: 600px;
margin: 0.1rem auto;
box-sizing: border-box;
display: flex;
justify-content: center;
}
.no-more-posts-message {
color: white;
}
/* Desktop responsive behavior */
/* Desktop view*/
@media (min-width: 768px) {
.feedContainer {
display: grid;
@ -44,7 +53,7 @@
width: 100%;
max-width: 800px;
margin: 0 auto;
padding: 2rem 0;
padding: 0 2rem 0;
}
}

View file

@ -99,7 +99,7 @@
justify-content: start;
width: 50vw;
height: 60vh;
min-height: 400px;
min-height: 60vh;
min-width: 500px;
}

View file

@ -1,30 +0,0 @@
import React, { useMemo } from "react";
import "./testPost.css";
interface TestPostProps {
postId: number;
}
const getRandomColor = () => {
const letters = "0123456789ABCDEF";
let color = "#";
for (let i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
};
const TestPost: React.FC<TestPostProps> = ({ postId }) => {
const bgColor = useMemo(() => getRandomColor(), []);
return (
<div
className="testPostCard"
style={{ backgroundColor: bgColor }}
>
<span className="testPostNumber">{postId}</span>
</div>
);
};
export default TestPost;

View file

@ -1,17 +0,0 @@
.testPostCard {
width: 100%;
height: 180px;
border-radius: 10px;
display: flex;
justify-content: center;
align-items: center;
font-size: 3rem;
font-weight: bold;
color: white;
user-select: none;
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.2);
}
.testPostNumber {
pointer-events: none;
}