From d11f92e11a32e1d64d3cacf36faab8d42c61f11a Mon Sep 17 00:00:00 2001 From: MisbehavedNinjaRadiator <120029998+MisbehavedNinjaRadiator@users.noreply.github.com.> Date: Sun, 29 Jun 2025 18:06:46 +0200 Subject: [PATCH] Feed done. --- .../backend/src/controllers/postController.ts | 27 +++- code/frontend/src/components/Post.tsx | 124 ++++++++++++++---- code/frontend/src/components/feed/Feed.tsx | 32 ++++- code/frontend/src/components/feed/feed.css | 30 +++-- .../feed/welcomeMessage/welcomeMessage.css | 27 ++++ .../feed/welcomeMessage/welcomeMessage.tsx | 16 +++ code/frontend/src/components/post.css | 54 ++++++++ 7 files changed, 267 insertions(+), 43 deletions(-) create mode 100644 code/frontend/src/components/feed/welcomeMessage/welcomeMessage.css create mode 100644 code/frontend/src/components/feed/welcomeMessage/welcomeMessage.tsx create mode 100644 code/frontend/src/components/post.css diff --git a/code/backend/src/controllers/postController.ts b/code/backend/src/controllers/postController.ts index 2cdc0f1..3b88c48 100644 --- a/code/backend/src/controllers/postController.ts +++ b/code/backend/src/controllers/postController.ts @@ -13,7 +13,6 @@ export const uploadPost = async (req: Request, res: Response) => { const user: JwtPayload = req.user!; // Get the user from the request const { description, status, tags } = uploadPostSchema.parse(req.body); const BUCKET = "images"; // Name of the bucket where the images are stored - try { const uploads = await Promise.all( files.map(async (file) => { @@ -58,15 +57,16 @@ export const uploadPost = async (req: Request, res: Response) => { })), }, }, - }); - // create a new post in the database + }); // create a new post in the database res.status(StatusCodes.CREATED).json({ message: "Upload successful", post: post, }); } catch (err) { console.error(err); - res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ error: "Upload failed" }); + res + .status(StatusCodes.INTERNAL_SERVER_ERROR) + .json({ error: "Upload failed" }); } }; @@ -104,6 +104,20 @@ export const getPost = async (req: Request, res: Response) => { postId: postId, }, }); + let hasLiked = false; + if (user) { + const like = await prisma.like.findUnique({ + where: { + postId_userId: { + postId: postId, + userId: user.sub!, + }, + }, + }); + if (like) { + hasLiked = true; + } + } if (!postObject) { res.status(StatusCodes.NOT_FOUND).json({ error: "Post not found", @@ -162,6 +176,7 @@ export const getPost = async (req: Request, res: Response) => { updatedAt: postObject.updatedAt, images: images.filter((image) => image !== null), // filter out the null images following: isFollowing, + hasLiked, }); return; } catch (err: any) { @@ -262,8 +277,8 @@ export const like = async (req: Request, res: Response) => { }); if (alreadyLiked) { res.status(StatusCodes.CONFLICT).json({ - error: "Already following", - details: [{ message: "You are already following this User" }], + error: "Already liked", + details: [{ message: "You already liked this User" }], }); return; } diff --git a/code/frontend/src/components/Post.tsx b/code/frontend/src/components/Post.tsx index a7736f2..fa6239f 100644 --- a/code/frontend/src/components/Post.tsx +++ b/code/frontend/src/components/Post.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { styled } from '@mui/material/styles'; +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'; @@ -15,6 +15,10 @@ 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" + interface ExpandMoreProps extends IconButtonProps { expand: boolean; @@ -30,6 +34,7 @@ interface PostResponse { user: { id: string; name: string; + profilePicture: Url; }; createdAt: string; updatedAt: string; @@ -39,6 +44,7 @@ interface PostResponse { url: string; }[]; following: boolean; + hasLiked: boolean; // <-- add this } const ExpandMore = styled((props: ExpandMoreProps) => { @@ -55,17 +61,19 @@ const ExpandMore = styled((props: ExpandMoreProps) => { export default function Post({ postId }: PostProps) { const [expanded, setExpanded] = React.useState(false); const [post, setPost] = React.useState(null); + const [currentImage, setCurrentImage] = React.useState(0); + const [like, setLike] = React.useState(false); React.useEffect(() => { getPostbyID(); - // eslint-disable-next-line }, [postId]); async function getPostbyID(): Promise { try { const response = await api.get(`/posts/getPost/{postId}?postId=${postId}`); - //const response = await api.get(`http://localhost:3001/api/posts/getPost/{postId}?postId=${postId}`); setPost(response.data); + setLike(response.data.hasLiked); // <-- initialize like state + setCurrentImage(0); } catch (error) { console.error("Failed to fetch post:", error); } @@ -73,7 +81,7 @@ export default function Post({ postId }: PostProps) { if (!post) { return ( - + Loading... @@ -81,8 +89,42 @@ export default function Post({ postId }: PostProps) { ); } + const images = post.images || []; + const hasMultipleImages = images.length > 1; + + const handlePrev = (e: React.MouseEvent) => { + e.stopPropagation(); + setCurrentImage((prev) => (prev === 0 ? images.length - 1 : prev - 1)); + }; + + const handleNext = (e: React.MouseEvent) => { + e.stopPropagation(); + setCurrentImage((prev) => (prev === images.length - 1 ? 0 : prev + 1)); + }; + + const handleLike = async () => { + try { + if (!like) { + await api.post(`/posts/like/${postId}`); + setLike(true); + 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 + ); + } + } catch (error) { + console.error("Failed to update like:", error); + } + }; + return ( - + + @@ -95,39 +137,59 @@ export default function Post({ postId }: PostProps) { } title={post.user.name} - subheader={new Date(post.createdAt).toLocaleString()} /> - {post.images && post.images.length > 0 && ( - + {images.length > 0 && ( +
+ + {hasMultipleImages && ( + <> + + {"<"} + + + {">"} + +
+ {currentImage + 1} / {images.length} +
+ + )} +
)} {post.description} - - Status: {post.status} - - - Likes: {post.likes} - Tags: {post.tags.join(", ")} - - Following: {post.following ? "Ja" : "Nein"} - - - - - - + + + {post.likes} + + 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/feed/Feed.tsx b/code/frontend/src/components/feed/Feed.tsx index f94a3f9..80d9c06 100644 --- a/code/frontend/src/components/feed/Feed.tsx +++ b/code/frontend/src/components/feed/Feed.tsx @@ -3,6 +3,12 @@ import Post from "../Post"; import "./feed.css"; import api from "../../api/axios"; import { create } from "axios"; +import WelcomeMessage from "./welcomeMessage/welcomeMessage"; +import { useAuth } from "../../api/Auth"; +import ButtonRotkehlchen from "../ButtonRotkehlchen"; +import { useNavigate } from "react-router-dom"; + + interface PostListItem { id: string; @@ -17,6 +23,8 @@ function Feed() { const [nextCursor, setNextCursor] = useState(null); const feedRef = useRef(null); const PAGE_SIZE = 10; + const { user } = useAuth(); + const navigate = useNavigate(); const fetchPosts = async () => { if (loading || !hasMore) return; @@ -24,8 +32,11 @@ function Feed() { try { let 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; @@ -64,7 +75,24 @@ function Feed() { }, [loading, hasMore, nextCursor]); return ( -
+
+ {!user && ( +
+ + navigate("/register")} + /> + navigate("/login")} + /> +
+ )}
{posts.length === 0 && !loading &&
Keine Posts gefunden.
} {posts.map((post) => ( diff --git a/code/frontend/src/components/feed/feed.css b/code/frontend/src/components/feed/feed.css index 1f18d11..3279708 100644 --- a/code/frontend/src/components/feed/feed.css +++ b/code/frontend/src/components/feed/feed.css @@ -1,19 +1,20 @@ .feedContainer { - display: flex; - flex-direction: column; - min-height: calc(100vh - var(--Header-height)); - background-color: #f9f9f9; + min-height: 100vh; + /* Subtract he */ + + } .feedContent { flex: 1; overflow-y: auto; - padding: 1rem; display: flex; flex-wrap: wrap; justify-content: center; - gap: 1rem; - height: 80vh; + width: 100%; + max-width: 800px; + margin: 0 auto; + padding: 1rem 0; } .loading { width: 100%; @@ -26,8 +27,21 @@ /* Desktop responsive behavior */ @media (min-width: 768px) { + .feedContainer { + display: grid; + grid-template-columns: 40% 60%; + } + .loggedInfeedContainer { + + } + .welcome-for-logged-out { + position: sticky; + top: 2rem; + align-self: flex-start; + } .feedContent { - width: 400px; + width: 100%; + max-width: 800px; margin: 0 auto; padding: 2rem 0; } diff --git a/code/frontend/src/components/feed/welcomeMessage/welcomeMessage.css b/code/frontend/src/components/feed/welcomeMessage/welcomeMessage.css new file mode 100644 index 0000000..904469d --- /dev/null +++ b/code/frontend/src/components/feed/welcomeMessage/welcomeMessage.css @@ -0,0 +1,27 @@ +.welcome-title { + color: var(--Rotkehlchen-orange-default); + font-size: 4rem; + font-weight: bolder; + margin-bottom: 0; +} +.welcome-text { + text-align: center; + color: var( --Rotkehlchen-gray); + +} +.desktop-welcome-text{ + display: none; +} + +@media (min-width: 768px) { + .welcome-text{ + display: none; + } + .desktop-welcome-text { + display: block; + text-align: center; + color: var(--Rotkehlchen-gray); + font-size: 1.5rem; + margin-top: 1rem; + } +} \ No newline at end of file diff --git a/code/frontend/src/components/feed/welcomeMessage/welcomeMessage.tsx b/code/frontend/src/components/feed/welcomeMessage/welcomeMessage.tsx new file mode 100644 index 0000000..da0422d --- /dev/null +++ b/code/frontend/src/components/feed/welcomeMessage/welcomeMessage.tsx @@ -0,0 +1,16 @@ +import React from "react"; +import "./welcomeMessage.css"; + +export default function WelcomeMessage() { + return ( +
+

Welcome!

+

+ Become a part of our big, bird loving community! +

+

+ 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. +

+
+ ); + } \ No newline at end of file diff --git a/code/frontend/src/components/post.css b/code/frontend/src/components/post.css new file mode 100644 index 0000000..4c88520 --- /dev/null +++ b/code/frontend/src/components/post.css @@ -0,0 +1,54 @@ +.css-izap9d-MuiTypography-root { + font-family: "Inter" +} + +.post-image-carousel { + position: relative; + width: 100%; +} + +.post-image { + width: 100%; + height: auto; + object-fit: contain; + max-height: 700px; +} + +.post-carousel-arrow { + position: absolute; + top: 50%; + transform: translateY(-50%); + background: var(--Rotkehlchen-gray); +} + + +.post-carousel-arrow.left { + left: 8px; +} + + +.post-carousel-arrow.right { + right: 8px; +} + +.post-image-counter { + position: absolute; + bottom: 8px; + right: 16px; + background: rgba(0,0,0,0.5); + color: white; + border-radius: 8px; + padding: 2px 8px; + font-size: 0.9rem; +} + +.post-like-icon { + font-size: 44px; + +} + +.post-like-count { + margin-left: 8px; + font-family: "Inter", sans-serif; + color: grey; +} \ No newline at end of file