Added user feed, Post Design improvement, Anchor

This commit is contained in:
MisbehavedNinjaRadiator 2025-06-30 16:35:30 +02:00 committed by MisbehavedNinjaRadiator
parent d11f92e11a
commit 5f9e3a24f7
7 changed files with 326 additions and 163 deletions

View file

@ -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 (
<Auth>
<Router>
<Header />
<ScrollToAnchor />
<div className="App">
<Routes>
<Route path="*" element={<NotFound />} />
@ -35,6 +37,8 @@ function App() {
></Route>
<Route path="/profile/:username" element={<Profile />}></Route>
<Route path="/createpost" element={<PostCreation />}></Route>
<Route path="/profile" element={<Profile />}></Route>
<Route path="/feed/:user" element={<UserFeedRoute />}></Route>
</Routes>
<Footer />
</div>

View file

@ -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 <IconButton {...other} />;
})<ExpandMoreProps>(({ 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<void> {
try {
const response = await api.get<PostResponse>(`/posts/getPost/{postId}?postId=${postId}`);
const response = await api.get<PostResponse>(
`/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 (
<Card sx={{ maxWidth: 400, width: '100%', margin: 2 }}>
<Card sx={{ maxWidth: 400, width: "100%", margin: 2 }}>
<CardContent>
<Typography>Loading...</Typography>
</CardContent>
@ -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,19 +122,9 @@ export default function Post({ postId }: PostProps) {
return (
<StyledEngineProvider injectFirst>
<Card className="body-l" sx={{ maxWidth: 600, width: '100%', margin: 2 }}>
<Card className="body-l" sx={{ maxWidth: 600, width: "100%", margin: 2 }}>
<CardHeader
avatar={
<Avatar sx={{ bgcolor: red[500] }} aria-label="user">
{post.user.name.charAt(0).toUpperCase()}
</Avatar>
}
action={
<IconButton aria-label="settings">
<MoreVertIcon />
</IconButton>
}
title={post.user.name}
avatar={<UserAvatar username={post.user.name} size={60} />}
/>
{images.length > 0 && (
<div className="post-image-carousel">
@ -175,9 +163,6 @@ export default function Post({ postId }: PostProps) {
<Typography variant="body1" sx={{ fontWeight: 600 }}>
{post.description}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
Tags: {post.tags.join(", ")}
</Typography>
</CardContent>
<CardActions disableSpacing>
<IconButton aria-label="like" onClick={handleLike}>
@ -186,7 +171,7 @@ export default function Post({ postId }: PostProps) {
sx={{
color: like ? "#d32f2f" : "#fff",
stroke: !like ? "grey" : "none",
strokeWidth: !like ? 2 : 0
strokeWidth: !like ? 2 : 0,
}}
/>
<span className="post-like-count">{post.likes}</span>
@ -202,6 +187,9 @@ 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>
@ -214,7 +202,6 @@ export default function Post({ postId }: PostProps) {
<Typography variant="body2" color="text.secondary">
Zuletzt aktualisiert: {new Date(post.updatedAt).toLocaleString()}
</Typography>
</CardContent>
</Collapse>
</Card>

View file

@ -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;

View file

@ -26,6 +26,7 @@ export default function UserAvatar({ username, size = 40 }: UserAvatarProps) {
<Box sx={{ display: "flex", alignItems: "center", gap: 1, maxWidth: "600px"}}>
<Avatar
src={pb}
alt={username || "avatar"}
sx={{
width: size,
height: size,
@ -33,7 +34,7 @@ export default function UserAvatar({ username, size = 40 }: UserAvatarProps) {
fontWeight: 500,
}}
>
username[0];
{username ? username[0].toUpperCase() : ""}
</Avatar>
<Typography
component="span"

View file

@ -6,9 +6,7 @@ import { create } from "axios";
import WelcomeMessage from "./welcomeMessage/welcomeMessage";
import { useAuth } from "../../api/Auth";
import ButtonRotkehlchen from "../ButtonRotkehlchen";
import { useNavigate } from "react-router-dom";
import { useNavigate, useParams } from "react-router-dom";
interface PostListItem {
id: string;
@ -16,7 +14,11 @@ interface PostListItem {
description: string;
}
function Feed() {
interface FeedProps {
username?: string;
}
function Feed({ username }: FeedProps) {
const [posts, setPosts] = useState<PostListItem[]>([]);
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}`;
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}`;
url = `/feed?createdAt=${encodeURIComponent(nextCursor)}&limit=${PAGE_SIZE}`;
}
interface FeedResponse {
posts: PostListItem[];
nextCursor: string | null;
}
const response = await api.get<FeedResponse>(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 (
<div className={user ? "loggedInfeedContainer" : "feedContainer"}>
@ -96,7 +111,9 @@ function Feed() {
<main className="feedContent" ref={feedRef}>
{posts.length === 0 && !loading && <div>Keine Posts gefunden.</div>}
{posts.map((post) => (
<Post key={post.id} postId={post.id} />
<div id={post.id} key={post.id}>
<Post postId={post.id} />
</div>
))}
{loading && <div className="loading">Loading more posts...</div>}
{!hasMore && <div>No more posts</div>}
@ -105,4 +122,9 @@ function Feed() {
);
}
export function UserFeedRoute() {
const { user } = useParams<{ user: string }>();
return <Feed username={user} />;
}
export default Feed;

View file

@ -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 {

View file

@ -1,16 +1,138 @@
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",
},
];
const DrawerList = (
<Box role="menu" onClick={() => setIsOpen(false)}>
<List className="drawer-list">
{ListItems.map((ListItemObject, index) =>
ListItemObject.onlyShowWhen == "always" ||
(ListItemObject.onlyShowWhen == "loggedIn" && user) ||
(ListItemObject.onlyShowWhen == "loggedOut" && !user) ? (
<ListItem
className="drawer-list-item"
key={ListItemObject.text}
disablePadding
>
<ListItemButton
className="drawer-list-item-button"
onClick={ListItemObject.onClick}
>
<ListItemIcon className="drawer-list-item">
{React.createElement(ListItemObject.icon)}
</ListItemIcon>
<ListItemText
className="drawer-list-item"
primary={ListItemObject.text}
/>
</ListItemButton>
</ListItem>
) : null
)}
</List>
</Box>
);
return (
<header className="base-header">
<div className="base-header-icon"> <img src='/assets/icons/feather_black.svg' alt="featherIcon" /> </div>
<p className="header-title small-title">
Feather Feed
</p>
<div className="base-header-icon"> <img src="/assets/icons/three_menu_stripes_black.svg" alt="menuIcon" /> </div>
<>
<header className="base-header blue-background">
<img
className="header-icon header-icon-feather"
src="/assets/icons/BirdIconO.ico"
alt="featherIcon"
/>
<p className="header-title small-title">Feather Feed</p>
<img
className="header-icon header-icon-menu"
src="/assets/icons/menu_orange.svg"
alt="menu"
onClick={toggleMenu}
/>
</header>
<SwipeableDrawer
anchor={"right"}
open={isOpen}
onClose={() => setIsOpen(false)}
onOpen={() => setIsOpen(true)}
>
{DrawerList}
</SwipeableDrawer>
</>
);
}