From 5f9e3a24f7cf3d9a764f7f8fb9cc586367f89608 Mon Sep 17 00:00:00 2001 From: MisbehavedNinjaRadiator <120029998+MisbehavedNinjaRadiator@users.noreply.github.com.> Date: Mon, 30 Jun 2025 16:35:30 +0200 Subject: [PATCH] Added user feed, Post Design improvement, Anchor --- code/frontend/src/App.tsx | 10 +- code/frontend/src/components/Post.tsx | 235 +++++++++--------- .../src/components/ScrollToAnchor.tsx | 26 ++ code/frontend/src/components/UserAvatar.tsx | 3 +- code/frontend/src/components/feed/Feed.tsx | 66 +++-- code/frontend/src/components/feed/feed.css | 7 +- .../frontend/src/components/header/Header.tsx | 142 ++++++++++- 7 files changed, 326 insertions(+), 163 deletions(-) create mode 100644 code/frontend/src/components/ScrollToAnchor.tsx diff --git a/code/frontend/src/App.tsx b/code/frontend/src/App.tsx index f59a86a..38ff752 100644 --- a/code/frontend/src/App.tsx +++ b/code/frontend/src/App.tsx @@ -1,4 +1,3 @@ - import React, { use } from 'react'; import logo from './logo.svg'; import './App.css'; @@ -10,18 +9,21 @@ import "./styles/colors.css"; import "./styles/fonts.css"; import "./styles/sizes.css"; import Footer from "./components/footer/Footer"; -import Header from "./components/Header"; +import Feed, {UserFeedRoute} from "./components/feed/Feed"; +import Header from './components/header/Header'; import Profile from "./pages/Profile"; import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; import { Auth } from "./api/Auth"; import { NotFound } from "./pages/404Page/NotFoundPage"; -import Feed from './components/feed/Feed'; +import ScrollToAnchor from "./components/ScrollToAnchor"; + function App() { return (
+
} /> @@ -35,6 +37,8 @@ function App() { > }> }> + }> + }>
diff --git a/code/frontend/src/components/Post.tsx b/code/frontend/src/components/Post.tsx index fa6239f..b1c4fe0 100644 --- a/code/frontend/src/components/Post.tsx +++ b/code/frontend/src/components/Post.tsx @@ -1,24 +1,24 @@ -import * as React from 'react'; -import { styled, StyledEngineProvider } 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'; +import * as React from "react"; +import { styled, StyledEngineProvider } 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"; import api from "../api/axios"; -import { Url } from 'url'; -import { LogLevel } from 'vite'; -import "./post.css" - +import { Url } from "url"; +import { LogLevel } from "vite"; +import "./post.css"; +import UserAvatar from "./UserAvatar"; interface ExpandMoreProps extends IconButtonProps { expand: boolean; @@ -44,18 +44,18 @@ interface PostResponse { url: string; }[]; following: boolean; - hasLiked: boolean; // <-- add this + hasLiked: boolean; } const ExpandMore = styled((props: ExpandMoreProps) => { const { expand, ...other } = props; return ; })(({ theme, expand }) => ({ - marginLeft: 'auto', - transition: theme.transitions.create('transform', { + marginLeft: "auto", + transition: theme.transitions.create("transform", { duration: theme.transitions.duration.shortest, }), - transform: expand ? 'rotate(180deg)' : 'rotate(0deg)', + transform: expand ? "rotate(180deg)" : "rotate(0deg)", })); export default function Post({ postId }: PostProps) { @@ -70,9 +70,11 @@ export default function Post({ postId }: PostProps) { async function getPostbyID(): Promise { try { - const response = await api.get(`/posts/getPost/{postId}?postId=${postId}`); + const response = await api.get( + `/posts/getPost/{postId}?postId=${postId}` + ); setPost(response.data); - setLike(response.data.hasLiked); // <-- initialize like state + setLike(response.data.hasLiked); setCurrentImage(0); } catch (error) { console.error("Failed to fetch post:", error); @@ -81,7 +83,7 @@ export default function Post({ postId }: PostProps) { if (!post) { return ( - + Loading... @@ -107,15 +109,11 @@ export default function Post({ postId }: PostProps) { if (!like) { await api.post(`/posts/like/${postId}`); setLike(true); - setPost((prev) => - prev ? { ...prev, likes: prev.likes + 1 } : prev - ); + setPost((prev) => (prev ? { ...prev, likes: prev.likes + 1 } : prev)); } else { await api.delete(`/posts/removeLike/${postId}`); setLike(false); - setPost((prev) => - prev ? { ...prev, likes: prev.likes - 1 } : prev - ); + setPost((prev) => (prev ? { ...prev, likes: prev.likes - 1 } : prev)); } } catch (error) { console.error("Failed to update like:", error); @@ -124,100 +122,89 @@ export default function Post({ postId }: PostProps) { return ( - - - {post.user.name.charAt(0).toUpperCase()} - - } - action={ - - - - } - title={post.user.name} - /> - {images.length > 0 && ( -
- - {hasMultipleImages && ( - <> - - {"<"} - - - {">"} - -
- {currentImage + 1} / {images.length} -
- - )} -
- )} - - - {post.description} - - - Tags: {post.tags.join(", ")} - - - - - - {post.likes} - - setExpanded(!expanded)} - aria-expanded={expanded} - aria-label="show more" - > - - - - + + } + /> + {images.length > 0 && ( +
+ + {hasMultipleImages && ( + <> + + {"<"} + + + {">"} + +
+ {currentImage + 1} / {images.length} +
+ + )} +
+ )} - - Following: {post.following ? "Ja" : "Nein"} - - - Status: {post.status} - - - Erstellt am: {new Date(post.createdAt).toLocaleString()} + + {post.description} - - Zuletzt aktualisiert: {new Date(post.updatedAt).toLocaleString()} - - -
-
+ + + + {post.likes} + + setExpanded(!expanded)} + aria-expanded={expanded} + aria-label="show more" + > + + + + + + + Tags: {post.tags.join(", ")} + + + Following: {post.following ? "Ja" : "Nein"} + + + Status: {post.status} + + + Erstellt am: {new Date(post.createdAt).toLocaleString()} + + + Zuletzt aktualisiert: {new Date(post.updatedAt).toLocaleString()} + + + +
); } diff --git a/code/frontend/src/components/ScrollToAnchor.tsx b/code/frontend/src/components/ScrollToAnchor.tsx new file mode 100644 index 0000000..4e23c39 --- /dev/null +++ b/code/frontend/src/components/ScrollToAnchor.tsx @@ -0,0 +1,26 @@ +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; \ No newline at end of file diff --git a/code/frontend/src/components/UserAvatar.tsx b/code/frontend/src/components/UserAvatar.tsx index 621062d..04fd0ba 100644 --- a/code/frontend/src/components/UserAvatar.tsx +++ b/code/frontend/src/components/UserAvatar.tsx @@ -26,6 +26,7 @@ export default function UserAvatar({ username, size = 40 }: UserAvatarProps) { - username[0]; + {username ? username[0].toUpperCase() : ""} ([]); const [loading, setLoading] = useState(false); const [hasMore, setHasMore] = useState(true); @@ -30,23 +32,27 @@ function Feed() { if (loading || !hasMore) return; setLoading(true); try { - let url = `/feed?limit=${PAGE_SIZE}`; - if (nextCursor) { - url = `/feed?createdAt=${encodeURIComponent( - nextCursor - )}&limit=${PAGE_SIZE}`; + let url: string; + if (username) { + url = `/posts/getUserPosts/${encodeURIComponent(username)}`; + const response = await api.get<{ posts: PostListItem[] }>(url); + setPosts(response.data.posts); + setHasMore(false); + } else { + url = `/feed?limit=${PAGE_SIZE}`; + if (nextCursor) { + url = `/feed?createdAt=${encodeURIComponent(nextCursor)}&limit=${PAGE_SIZE}`; + } + interface FeedResponse { + posts: PostListItem[]; + nextCursor: string | null; + } + const response = await api.get(url); + const { posts: newPosts, nextCursor: newCursor } = response.data; + setPosts((prev) => [...prev, ...newPosts]); + setNextCursor(newCursor); + setHasMore(!!newCursor && newPosts.length > 0); } - - interface FeedResponse { - posts: PostListItem[]; - nextCursor: string | null; - } - const response = await api.get(url); - 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 { @@ -59,6 +65,8 @@ function Feed() { }, []); useEffect(() => { + if (username) return; + const onScroll = () => { if (loading || !hasMore) return; if ( @@ -72,7 +80,14 @@ function Feed() { return () => { window.removeEventListener("scroll", onScroll); }; - }, [loading, hasMore, nextCursor]); + }, [loading, hasMore, nextCursor, username]); + + useEffect(() => { + setPosts([]); + setNextCursor(null); + setHasMore(true); + fetchPosts(); + }, [username]); return (
@@ -96,7 +111,9 @@ function Feed() {
{posts.length === 0 && !loading &&
Keine Posts gefunden.
} {posts.map((post) => ( - +
+ +
))} {loading &&
Loading more posts...
} {!hasMore &&
No more posts
} @@ -105,4 +122,9 @@ function Feed() { ); } +export function UserFeedRoute() { + const { user } = useParams<{ user: string }>(); + return ; +} + export default Feed; diff --git a/code/frontend/src/components/feed/feed.css b/code/frontend/src/components/feed/feed.css index 3279708..da56cdc 100644 --- a/code/frontend/src/components/feed/feed.css +++ b/code/frontend/src/components/feed/feed.css @@ -31,12 +31,13 @@ display: grid; grid-template-columns: 40% 60%; } - .loggedInfeedContainer { - } .welcome-for-logged-out { position: sticky; - top: 2rem; + top: 5rem; + left: 5rem; + right: 1rem; + align-self: center; align-self: flex-start; } .feedContent { diff --git a/code/frontend/src/components/header/Header.tsx b/code/frontend/src/components/header/Header.tsx index 77468cc..23ec6e2 100644 --- a/code/frontend/src/components/header/Header.tsx +++ b/code/frontend/src/components/header/Header.tsx @@ -1,17 +1,139 @@ import "./header.css"; - +import React, { useState } from "react"; +import { + List, + ListItem, + ListItemButton, + ListItemIcon, + ListItemText, + SwipeableDrawer, +} from "@mui/material"; +import Box from "@mui/material/Box"; +import AddAPhotoIcon from "@mui/icons-material/AddAPhoto"; +import DynamicFeedIcon from "@mui/icons-material/DynamicFeed"; +import PersonIcon from "@mui/icons-material/Person"; +import InfoIcon from "@mui/icons-material/Info"; +import LogoutIcon from "@mui/icons-material/Logout"; +import ExitToAppIcon from "@mui/icons-material/ExitToApp"; +import FollowTheSignsIcon from "@mui/icons-material/FollowTheSigns"; +import { useNavigate } from "react-router-dom"; +import { useAuth } from "../../api/Auth"; function Header() { + interface ListItemAttributes { + text: string; + icon: React.ElementType; + onClick: () => void; + onlyShowWhen: "loggedIn" | "loggedOut" | "always"; + } + const navigate = useNavigate(); + const [isOpen, setIsOpen] = useState(false); + const toggleMenu = () => { + setIsOpen(!isOpen); + }; + const { logout, user } = useAuth(); + const ListItems: ListItemAttributes[] = [ + { + text: "Feed", + icon: DynamicFeedIcon, + onClick: () => navigate("/feed", { replace: true }), + onlyShowWhen: "always", + }, + { + text: "Create Post", + icon: AddAPhotoIcon, + onClick: () => navigate("/createpost", { replace: true }), + onlyShowWhen: "loggedIn", + }, + { + text: "Profile", + icon: PersonIcon, + onClick: () => navigate("/profile", { replace: true }), + onlyShowWhen: "loggedIn", + }, + { + text: "About", + icon: InfoIcon, + onClick: () => navigate("/about", { replace: true }), + onlyShowWhen: "always", + }, + { + text: "Log Out", + icon: LogoutIcon, + onClick: logout, + onlyShowWhen: "loggedIn", + }, + { + text: "Log In", + icon: ExitToAppIcon, + onClick: () => navigate("/login", { replace: true }), + onlyShowWhen: "loggedOut", + }, + { + text: "Sign Up", + icon: FollowTheSignsIcon, + onClick: () => navigate("/register", { replace: true }), + onlyShowWhen: "loggedOut", + }, + ]; - return ( -
-
featherIcon
-

- Feather Feed -

-
menuIcon
-
- ); + const DrawerList = ( + setIsOpen(false)}> + + {ListItems.map((ListItemObject, index) => + ListItemObject.onlyShowWhen == "always" || + (ListItemObject.onlyShowWhen == "loggedIn" && user) || + (ListItemObject.onlyShowWhen == "loggedOut" && !user) ? ( + + + + {React.createElement(ListItemObject.icon)} + + + + + ) : null + )} + + + ); + + return ( + <> +
+ featherIcon +

Feather Feed

+ menu +
+ setIsOpen(false)} + onOpen={() => setIsOpen(true)} + > + {DrawerList} + + + ); } export default Header;