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

@ -10,7 +10,9 @@
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0", "@emotion/styled": "^11.14.0",
"@mui/icons-material": "^7.1.2", "@mui/icons-material": "^7.1.2",
"@mui/joy": "^5.0.0-beta.52",
"@mui/material": "^7.1.1", "@mui/material": "^7.1.1",
"@mui/system": "^7.1.1",
"@testing-library/dom": "^10.4.0", "@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3", "@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.1.0", "@testing-library/react": "^16.1.0",

View file

@ -1,7 +1,4 @@
import React, { use } from 'react';
import logo from './logo.svg';
import './App.css'; import './App.css';
import { useState, useEffect } from 'react';
import LoginAndSignUpPage from './pages/LoginAndSignUpPage'; import LoginAndSignUpPage from './pages/LoginAndSignUpPage';
import PostCreation from './pages/PostCreation'; import PostCreation from './pages/PostCreation';
import "./App.css"; import "./App.css";
@ -9,13 +6,14 @@ import "./styles/colors.css";
import "./styles/fonts.css"; import "./styles/fonts.css";
import "./styles/sizes.css"; import "./styles/sizes.css";
import Footer from "./components/footer/Footer"; 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 Header from './components/header/Header';
import Profile from "./pages/Profile"; 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 { Auth } from "./api/Auth";
import { NotFound } from "./pages/404Page/NotFoundPage"; import { NotFound } from "./pages/404Page/NotFoundPage";
import ScrollToAnchor from "./components/ScrollToAnchor";
function App() { function App() {
@ -23,7 +21,6 @@ function App() {
<Auth> <Auth>
<Router> <Router>
<Header /> <Header />
<ScrollToAnchor />
<div className="App"> <div className="App">
<Routes> <Routes>
<Route path="*" element={<NotFound />} /> <Route path="*" element={<NotFound />} />
@ -39,6 +36,13 @@ function App() {
<Route path="/createpost" element={<PostCreation />}></Route> <Route path="/createpost" element={<PostCreation />}></Route>
<Route path="/profile" element={<Profile />}></Route> <Route path="/profile" element={<Profile />}></Route>
<Route path="/feed/:user" element={<UserFeedRoute />}></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> </Routes>
<Footer /> <Footer />
</div> </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 { Avatar, Box, Typography } from "@mui/material";
import api from "../api/axios"; import api from "../api/axios";
import { useState,useEffect } from "react"; import { useState, useEffect } from "react";
interface UserAvatarProps { interface UserAvatarProps {
username: string|null; username: string | null;
src?: string; src?: string;
size?: number | string; size?: number | string;
onClick?: () => void;
} }
export default function UserAvatar({ username, size = 40 }: UserAvatarProps) { export default function UserAvatar({
const[pb, setPb] = useState<string>(); username,
size = 40,
onClick,
}: UserAvatarProps) {
const [pb, setPb] = useState<string>();
useEffect(() => { useEffect(() => {
(async () => { (async () => {
try { try {
const res = await api.get(`/profile/getProfilePicture/${username}`) const res = await api.get(`/profile/getProfilePicture/${username}`);
setPb((res.data as { url: string }).url); setPb((res.data as { url: string }).url);
} catch (error) { } catch (error: any) {
if (error.status !== 404) {
console.log(error); console.log(error);
} }
}
})(); })();
}, [username]); }, [username]);
return ( 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 <Avatar
src={pb} src={pb}
alt={username || "avatar"} alt={username || "avatar"}
@ -39,7 +55,11 @@ export default function UserAvatar({ username, size = 40 }: UserAvatarProps) {
<Typography <Typography
component="span" component="span"
fontWeight={500} fontWeight={500}
sx={{ whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis"}} sx={{
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}
> >
{username} {username}
</Typography> </Typography>

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 CardContent from "@mui/material/CardContent";
import CardActions from "@mui/material/CardActions"; import CardActions from "@mui/material/CardActions";
import Collapse from "@mui/material/Collapse"; import Collapse from "@mui/material/Collapse";
import Avatar from "@mui/material/Avatar";
import IconButton, { IconButtonProps } from "@mui/material/IconButton"; import IconButton, { IconButtonProps } from "@mui/material/IconButton";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import { red } from "@mui/material/colors";
import FavoriteIcon from "@mui/icons-material/Favorite"; import FavoriteIcon from "@mui/icons-material/Favorite";
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 api from "../../api/axios";
import api from "../api/axios";
import { Url } from "url"; import { Url } from "url";
import { LogLevel } from "vite";
import "./post.css"; 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 { interface ExpandMoreProps extends IconButtonProps {
expand: boolean; expand: boolean;
} }
interface PostProps { interface PostProps {
postId: string; postId: string;
autoScroll?: boolean;
} }
interface PostResponse { interface PostResponse {
description: string; description: string;
@ -58,17 +56,17 @@ const ExpandMore = styled((props: ExpandMoreProps) => {
transform: expand ? "rotate(180deg)" : "rotate(0deg)", 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 [expanded, setExpanded] = React.useState(false);
const [post, setPost] = React.useState<PostResponse | null>(null); const [post, setPost] = React.useState<PostResponse | null>(null);
const [currentImage, setCurrentImage] = React.useState(0); const [currentImage, setCurrentImage] = React.useState(0);
const [like, setLike] = React.useState(false); const [like, setLike] = React.useState(false);
const navigate = useNavigate();
const postRef = useRef<HTMLDivElement>(null);
const scrollTimeoutsRef = useRef<number[]>([]);
React.useEffect(() => { // Using useCallback to memoize the function and avoid dependency issues
getPostbyID(); const getPostbyID = useCallback(async (): Promise<void> => {
}, [postId]);
async function getPostbyID(): Promise<void> {
try { try {
const response = await api.get<PostResponse>( const response = await api.get<PostResponse>(
`/posts/getPost/{postId}?postId=${postId}` `/posts/getPost/{postId}?postId=${postId}`
@ -79,7 +77,43 @@ export default function Post({ postId }: PostProps) {
} catch (error) { } catch (error) {
console.error("Failed to fetch post:", 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) { if (!post) {
return ( return (
@ -122,9 +156,19 @@ export default function Post({ postId }: PostProps) {
return ( return (
<StyledEngineProvider injectFirst> <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 <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 && ( {images.length > 0 && (
<div className="post-image-carousel"> <div className="post-image-carousel">
@ -187,20 +231,16 @@ export default function Post({ postId }: PostProps) {
</CardActions> </CardActions>
<Collapse in={expanded} timeout="auto" unmountOnExit> <Collapse in={expanded} timeout="auto" unmountOnExit>
<CardContent> <CardContent>
{post.tags && post.tags.length > 0 && (
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}> <Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
Tags: {post.tags.join(", ")} Tags: {post.tags.join(", ")}
</Typography> </Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}> )}
Following: {post.following ? "Ja" : "Nein"} <Typography variant="body2" color="text.secondary">
</Typography> Created at: {new Date(post.createdAt).toLocaleString()}
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
Status: {post.status}
</Typography> </Typography>
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
Erstellt am: {new Date(post.createdAt).toLocaleString()} Last updated: {new Date(post.updatedAt).toLocaleString()}
</Typography>
<Typography variant="body2" color="text.secondary">
Zuletzt aktualisiert: {new Date(post.updatedAt).toLocaleString()}
</Typography> </Typography>
</CardContent> </CardContent>
</Collapse> </Collapse>

View file

@ -5,7 +5,7 @@ import { useState } from "react";
import "./bio.css"; import "./bio.css";
import IconButton from "@mui/material/IconButton"; import IconButton from "@mui/material/IconButton";
import EditSquareIcon from "@mui/icons-material/EditSquare"; import EditSquareIcon from "@mui/icons-material/EditSquare";
import ButtonPrimary from "../ButtonRotkehlchen"; import ButtonPrimary from "../buttons/buttonRotkehlchen/ButtonRotkehlchen";
import api from "../../api/axios"; import api from "../../api/axios";
export default function BioTextField({ ownAccount, bioText, setBio } : { ownAccount: boolean, bioText: string | undefined, setBio: (bio: string) => void }) { 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 CloseIcon from "@mui/icons-material/Close";
import EditSquareIcon from "@mui/icons-material/EditSquare"; import EditSquareIcon from "@mui/icons-material/EditSquare";
import "./changeAvatarDialog.css"; import "./changeAvatarDialog.css";
import ButtonRotkehlchen from "../ButtonRotkehlchen"; import ButtonRotkehlchen from "../buttons/buttonRotkehlchen/ButtonRotkehlchen";
import Username from "./Username"; import Username from "./Username";
import "./username.css"; import "./username.css";
import api from "../../api/axios"; import api from "../../api/axios";
@ -55,17 +55,17 @@ export default function AvatarDialog({
console.log("Saving profile picture:", file); console.log("Saving profile picture:", file);
if (file) { if (file) {
formData.append("image", file); formData.append("image", file);
const response = await api.post( const response = await api.post<{ url: string }>(
"/profile/uploadProfilePicture", "/profile/uploadProfilePicture",
formData formData
); );
console.log("Profile picture saved:", response.data); console.log("Profile picture saved:", response.data);
setUserData((prevData) => (prevData ?{ setUserData((prevData) => (prevData ? {
...prevData, ...prevData,
profilePictureUrl: response.data.url profilePictureUrl: response.data.url
} : null)); } : null));
} }
setOpen(false); // Close the dialog after saving setOpen(false);
} catch (error) { } catch (error) {
console.error("Error saving profile picture:", 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 { UserProfile } from "../../types/UserProfile";
import { useNavigate } from "react-router-dom"; 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 }) { export default function StandardImageList({ user }: { user: UserProfile }) {
const navigate = useNavigate(); const navigate = useNavigate();
const [images, setImages] = useState< const [images, setImages] = useState<
@ -22,17 +32,15 @@ export default function StandardImageList({ user }: { user: UserProfile }) {
const fetchUserPosts = async () => { const fetchUserPosts = async () => {
try { 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 posts = response.data.posts;
const fetchedImages = await Promise.all( const fetchedImages = await Promise.all(
posts.map( posts.map(
async (post: { async (post: Post) => {
id: string;
description: string;
createdAt: string;
}) => {
try { try {
const response = await api.get( const response = await api.get<PostImagesResponse>(
`/posts/getPost/{postId}?postId=${post.id}` `/posts/getPost/{postId}?postId=${post.id}`
); );
if (response.data && response.data.images.length > 0) { if (response.data && response.data.images.length > 0) {
@ -42,6 +50,7 @@ export default function StandardImageList({ user }: { user: UserProfile }) {
id: post.id, id: post.id,
description: post.description || "", description: post.description || "",
createdAt: post.createdAt, createdAt: post.createdAt,
}; };
} }
} catch (error) { } catch (error) {
@ -51,7 +60,12 @@ export default function StandardImageList({ user }: { user: UserProfile }) {
) )
); );
console.log("Fetched images:", fetchedImages); 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) { } catch (error) {
console.error("Error fetching user posts:", error); console.error("Error fetching user posts:", error);
} }
@ -68,10 +82,7 @@ export default function StandardImageList({ user }: { user: UserProfile }) {
<img <img
src={item.imageUrl} src={item.imageUrl}
alt={item.description} alt={item.description}
onClick={ onClick={() => navigate(`/feed/${user.username}#${item.id}`)}
() => navigate("/feed")
// anchor to post that was clicked
}
loading="lazy" 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 React, { useEffect, useRef, useState } from "react";
import "./notFound.css"; import "./notFound.css";
import ButtonPrimary from "../../components/ButtonRotkehlchen"; import ButtonPrimary from "../../components/buttons/buttonRotkehlchen/ButtonRotkehlchen";
type Block = { type Block = {
x: number; x: number;

View file

@ -1,7 +1,7 @@
import "./loginAndSignUpPage.css"; import "./loginAndSignUpPage.css";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import api from "../api/axios"; 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 { useLocation, useNavigate } from "react-router-dom";
import { useAuth } from "../api/Auth"; import { useAuth } from "../api/Auth";
import { createTheme, useMediaQuery } from "@mui/material"; 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 Chip from '@mui/material/Chip';
import Autocomplete from '@mui/material/Autocomplete'; import Autocomplete from '@mui/material/Autocomplete';
import TextField from '@mui/material/TextField'; import TextField from '@mui/material/TextField';
import ButtonPrimary from "../components/ButtonRotkehlchen"; import ButtonPrimary from "../components/buttons/buttonRotkehlchen/ButtonRotkehlchen";
import api from "../api/axios"; import api from "../api/axios";
import { useAuth } from "../api/Auth"; import { useAuth } from "../api/Auth";

View file

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

View file

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

View file

@ -1,7 +1,5 @@
.feedContainer { .feedContainer {
min-height: 100vh; min-height: 100vh;
/* Subtract he */
} }
@ -9,7 +7,8 @@
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: nowrap;
flex-direction: column;
justify-content: center; justify-content: center;
width: 100%; width: 100%;
max-width: 800px; max-width: 800px;
@ -23,9 +22,19 @@
font-weight: bold; font-weight: bold;
color: #333; 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 view*/
/* Desktop responsive behavior */
@media (min-width: 768px) { @media (min-width: 768px) {
.feedContainer { .feedContainer {
display: grid; display: grid;
@ -44,7 +53,7 @@
width: 100%; width: 100%;
max-width: 800px; max-width: 800px;
margin: 0 auto; margin: 0 auto;
padding: 2rem 0; padding: 0 2rem 0;
} }
} }

View file

@ -99,7 +99,7 @@
justify-content: start; justify-content: start;
width: 50vw; width: 50vw;
height: 60vh; height: 60vh;
min-height: 400px; min-height: 60vh;
min-width: 500px; 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;
}

File diff suppressed because it is too large Load diff