diff --git a/src/main/web/src/scenes/Friends/UserFriend.css b/src/main/web/src/scenes/Friends/UserFriend.css index 253f10e6..f7365318 100644 --- a/src/main/web/src/scenes/Friends/UserFriend.css +++ b/src/main/web/src/scenes/Friends/UserFriend.css @@ -1,4 +1,4 @@ -.user-container { +.friend-user-container { font-size: var(--headline-font-size); align-items: center; text-align: center; @@ -10,24 +10,24 @@ color: white; } -.user-image { +.friend-user-image { width: 100px; height: 100px; border-radius: 50px; object-fit: cover; } -.custom-image { +.friend-custom-image { width: 100px; height: 100px; border-radius: 50px; object-fit: cover; } -.user-button { +.friend-user-button { text-decoration: none; color: inherit; } -.user-name{ +.friend-user-name{ padding-top: 10px; } diff --git a/src/main/web/src/scenes/Friends/UserFriend.tsx b/src/main/web/src/scenes/Friends/UserFriend.tsx index 40c00246..9ad67e63 100644 --- a/src/main/web/src/scenes/Friends/UserFriend.tsx +++ b/src/main/web/src/scenes/Friends/UserFriend.tsx @@ -14,14 +14,14 @@ export const UserFriend: React.FC = (props: UserFriendModel) => const truncatedUsername = username.length > 17 ? username.slice(0, 15) + "..." : username; return ( -
+
{image ? ( - {username} + {username} ) : ( - {"random"} + {"random"} )} -
{truncatedUsername}
+
{truncatedUsername}
); diff --git a/src/main/web/src/scenes/Search/components/SearchService.tsx b/src/main/web/src/scenes/Search/components/SearchService.tsx index a4c28208..01715de8 100644 --- a/src/main/web/src/scenes/Search/components/SearchService.tsx +++ b/src/main/web/src/scenes/Search/components/SearchService.tsx @@ -12,8 +12,6 @@ import {Post} from "../../Home/components/post/Post"; import {UserModel} from "./models/UserModel"; - - interface SearchedPostsProps { sortOption: string; query: string | null; @@ -45,8 +43,13 @@ export const SearchService: React.FC = ({sortOption, query, }); if (response.ok) { const data = await response.json(); - setSearchResults(data); - setNotFound(false); + if (data.length === 0) { + setNotFound(true); + } else { + setSearchResults(data); + setNotFound(false); + } + } else { setNotFound(true); } diff --git a/src/main/web/src/scenes/User/UserPost.tsx b/src/main/web/src/scenes/User/UserPost.tsx new file mode 100644 index 00000000..8683e926 --- /dev/null +++ b/src/main/web/src/scenes/User/UserPost.tsx @@ -0,0 +1,258 @@ +import React, {useCallback, useEffect, useRef, useState} from 'react'; +import '../../scenes/Home/components/post/Post.css'; +import {Share} from "../../organisms/share/Share"; +import {Link, useLocation} from "react-router-dom"; +import {Report} from "../../organisms/report/Report"; +import {PostMenu} from "../../organisms/post-menu/PostMenu"; +import {Tag} from "../../atoms/Tag"; +import {handleLike} from '../../services/LikeService'; +import {timeDifference} from "../../services/TimeService"; +import {PostModel} from "../Home/components/post/models/PostModel"; +import {Interaction} from "../../organisms/interaction/Interaction"; +import {shortenDescription} from "../../services/DescriptionService"; +import {useMediaQuery} from "@mui/system"; +import config from "../../config/config"; +import {getJWT, getUserId} from "../../services/AuthService"; +import {sendReportToBackend} from "../../services/ReportService"; + + +export const UserPost: React.FC = (props: PostModel) => { + const { + id, + title, + description, + tags, + likeAmount, + commentAmount, + timestamp, + postImage, + accountId, + username + } = props; + + const matches: boolean = useMediaQuery('(max-width: 412px)') + const formattedTime: string = timeDifference(new Date(timestamp)); + const [comments] = useState(commentAmount); + const [menuOpen, setMenuOpen] = useState(false); + const [shareWindowOpen, setShareWindowOpen] = useState(false); + const currentPageURL: string = window.location.href; + const location = useLocation(); + const [shortDescription, setShortDescription] = useState(''); + const searchParams: URLSearchParams = new URLSearchParams(window.location.search); + const userSelfid: string | null = searchParams.get('id'); + const userSelfId: number | null = getUserId(); + const jwt: string | null = getJWT(); + const headersWithJwt = { + ...config.headers, + 'Authorization': jwt ? `Bearer ${jwt}` : '' + }; + + const [reportOpen, setReportOpen] = useState(false); + const [reportReason, setReportReason] = useState(''); + const [reportDescription, setReportDescription] = useState(''); + + const handleReportClick = (): void => { + setReportOpen(!reportOpen); + }; + + const handleReportSubmit = (): void => { + sendReportToBackend(reportReason, reportDescription, id, accountId, "post"); + setReportOpen(!reportOpen); + setReportReason(''); + setReportDescription(''); + }; + + const [likes, setLikes] = useState(likeAmount); + const [userLiked, setUserLiked] = useState(false); + const [heartClass, setHeartClass] = useState('heart-empty'); + + useEffect((): void => { + const userLikedPost: string | null = localStorage.getItem(`post_liked_${id}`); + if (userLikedPost) { + setUserLiked(true); + setHeartClass('heart-filled'); + } + }, [location, id]); + + useEffect((): void => { + if (description){ + setShortDescription(shortenDescription(description, postImage ? 190 : matches ? 190 : 280)); + } + else { + setShortDescription(description); + } + }, [description, postImage, matches]); + + const handleMenuClick = (): void => { + setMenuOpen(!menuOpen); + setShareWindowOpen(false); + }; + + const handleShareClick = (): void => { + setShareWindowOpen(!shareWindowOpen); + }; + + const handleSaveClick = async (): Promise => { + try { + const response: Response = await fetch(config.apiUrl + "saved-post", { + method: 'POST', + credentials: 'include', + body: JSON.stringify({ + postId: id, + userId: userSelfId, + }), + headers: headersWithJwt + }); + if (response.ok) { + console.log('Post has been saved!'); + alert('Post has been saved!'); + } else { + console.error('Error saving the post: ', response.statusText); + } + } catch (err) { + console.error('Error saving the post: ', err); + alert('Error saving the post'); + } + }; + + const handleUnsaveClick = async (): Promise => { + try { + const response: Response = await fetch(config.apiUrl + `saved-post`, { + method: 'DELETE', + credentials: 'include', + body: JSON.stringify({ + postId: id, + userId: userSelfId, + }), + headers: headersWithJwt + }); + if (response.ok) { + alert('Post has been unsaved!'); + } else { + console.error('Error unsaving the post: ', response.statusText); + alert('Error unsaving the post'); + } + } catch (err) { + console.error('Error unsaving the post: ', err); + } + }; + + const imageRef = useRef(null); + const [imageWidth, setImageWidth] = useState(); + const [marginLeft, setMarginLeft] = useState(); + const [width, setWidth] = useState(); + const [marginTop, setMarginTop] = useState(); + + const handleClose = useCallback((): void => { + setReportOpen(false); + }, [setReportOpen]); + + useEffect((): () => void => { + const updateMargins = (): void => { + if (matches) { + setWidth(postImage ? '240px' : '260px'); + setMarginLeft(tags ? '110px' : '0'); + setMarginTop(postImage ? '140px' : '5px'); + } else { + setWidth(postImage ? '360px' : '600px'); + setMarginLeft(postImage ? (imageWidth ? `${imageWidth + 20}px` : '280px') : '10px'); + setMarginTop('0'); + } + }; + updateMargins(); + const imageElement = imageRef.current; + const handleResize = (): void => { + if (imageElement) { + const computedStyle: CSSStyleDeclaration = window.getComputedStyle(imageElement); + const width: number = parseInt(computedStyle.getPropertyValue('width')); + setImageWidth(width); + updateMargins(); + } + }; + if (imageElement) { + handleResize(); + window.addEventListener('resize', handleResize); + } + return (): void => { + window.removeEventListener('resize', handleResize); + }; + }, [matches, postImage, imageWidth, tags]); + + useEffect(() => { + const handleEsc = (event: KeyboardEvent): void => { + if (event.key === 'Escape' && reportOpen) { + handleClose(); + } + }; + document.addEventListener('keydown', handleEsc); + return (): void => { + document.removeEventListener('keydown', handleEsc); + }; + }, [reportOpen, handleClose]); + + return ( +
+
+ + {postImage && Post} + + Menu dots +
+ +

+ {title ? shortenDescription(title, 50): title }

+ +
+ {tags && tags.slice(0, 3).map((tag: string, index: number) => ( + + ))} +
+ +

+ {postImage && shortDescription ? shortenDescription(shortDescription, 150) : shortDescription} +

+ +
+ + {username} + +  · {formattedTime} +
+
+
+ handleLike(id, "post", likes, setLikes, setUserLiked, setHeartClass)} + id={id} + isHomepage={true} + /> +
+
+ + {menuOpen && ( +
+ +
+ )} + {shareWindowOpen && ( +
+ +
+ )} + {reportOpen && ( + + )} +
+ ); +}; \ No newline at end of file diff --git a/src/main/web/src/scenes/User/index.css b/src/main/web/src/scenes/User/index.css index 48e32699..0197df7e 100644 --- a/src/main/web/src/scenes/User/index.css +++ b/src/main/web/src/scenes/User/index.css @@ -58,8 +58,8 @@ } .follow-button { + cursor: pointer; font-size: var(--headline-font-size); - padding: 10px; color: white; align-items: center; @@ -67,4 +67,57 @@ background-color: var(--red); width: auto; } +.unfollow-button { + cursor: pointer; + font-size: var(--headline-font-size); + padding: 10px; + color: white; + align-items: center; + border-radius: 5px; + background-color: var(--lighter-grey); + width: auto; +} + +.user-not-found { + font-size: var(--headline-font-size); + padding: 10px; + color: white; + border-radius: 5px; +} + +.user-post-component { + font-size: var(--headline-font-size); + padding: 10px; + color: white; + border-radius: 5px; +} + +.user-posts { + width: 780px; + display: grid; + grid-template-columns: 1fr; + grid-gap: 10px; + position: relative; +} + +@media (max-width: 412px) { + .user-posts { + margin-left: 20px; + margin-top: -5px; + } +} + +.user-posts-header { + width: 780px; + padding: 20px; +} + +.follow-loading-animation { + cursor: pointer; + font-size: var(--headline-font-size); + padding: 10px; + align-items: center; + border-radius: 5px; + width: auto; +} \ No newline at end of file diff --git a/src/main/web/src/scenes/User/index.tsx b/src/main/web/src/scenes/User/index.tsx index 9af27c96..3a0fc8d0 100644 --- a/src/main/web/src/scenes/User/index.tsx +++ b/src/main/web/src/scenes/User/index.tsx @@ -5,87 +5,292 @@ import {Footer} from "../../organisms/footer/Footer"; import config from "../../config/config"; import {getJWT, getUserId} from "../../services/AuthService"; import {getDefaultOrRandomPicture} from "../../atoms/Pictures/PicturesComponent"; +import Lottie from "lottie-react"; +import animationData from "../../assets/loading.json"; +import {UserPost} from "./UserPost"; export const UserPage = () => { const searchParams: URLSearchParams = new URLSearchParams(window.location.search); const id: string | null = searchParams.get('id'); const senderId: number | null = getUserId(); - const [userId, setUserId] = useState(null); const [username, setUserName] = useState(null); - const [picture, setPicture] = useState<{id: number | null; name: string | null; imageData: string | null}>({id: null, name: null, imageData: null}); + const [picture, setPicture] = useState<{ + id: number | null; + name: string | null; + imageData: string | null; + }>({id: null, name: null, imageData: null}); + const [userPosts, setUserPosts] = useState([]); const [amountFollower, setAmountFollower] = useState(null); const [age, setAge] = useState(null); const [description, setDescription] = useState(null); const [course, setCourse] = useState(null); + const [loading, setLoading] = useState(true); + const [followLoading, setFollowLoading] = useState(true); + const [notFound, setNotFound] = useState(false); + const [follow, setFollow] = useState(false); + const [postLoading, setPostsLoading] = useState(true); + const [postsNotFound, setPostsNotFound] = useState(false); const jwt: string | null = getJWT(); const headersWithJwt = { ...config.headers, 'Authorization': jwt ? `Bearer ${jwt}` : '' }; - useEffect((): void => { - const fetchUserData = async (): Promise => { + useEffect(() => { + setFollowLoading(true); + setNotFound(false); + const fetchUserData = async () => { try { - const response: Response = await fetch(config.apiUrl +`user/user-information/${id}`, { + const response = await fetch(config.apiUrl + `user/user-information/${id}`, { headers: config.headers }); if (response.ok) { const data = await response.json(); setUserId(data.userId); setUserName(data.username); - if(data.picture !== null){ + if (data.picture !== null) { setPicture(data.picture); } setAmountFollower(data.amountFollower); setAge(data.age); setDescription(data.description); setCourse(data.course); - console.log("successful fetching of userdata") + console.log("Successful fetching of userdata"); + setNotFound(false); + setLoading(false); } else { - console.log(new Error("Failed to fetch Userdata")); + console.log(new Error("Failed to fetch userdata")); + setNotFound(true); + setLoading(false); } } catch (error) { - console.error("Error fetching Data:", error); + console.error("Error fetching data:", error); + setNotFound(true); + setLoading(false); } }; + fetchUserData(); - }, [id]); - const handleFollow = async () => { + const fetchUserPosts = async () => { + setPostsLoading(true); + setPostsNotFound(false); + + try { + const response = await fetch(config.apiUrl + `post/user-posts/${id}`, { + headers: headersWithJwt + }); + if (response.ok) { + const data = await response.json(); + if (data.length === 0) { + setPostsNotFound(true); + } else { + console.log("Successful fetching of user posts"); + setUserPosts(data); + setPostsLoading(false); + setPostsNotFound(false); + } + } else { + console.log(new Error("Failed to fetch user posts")); + setPostsLoading(false); + setPostsNotFound(true); + } + } catch (error) { + console.error("Error when retrieving the search data:", error); + setPostsLoading(false); + setPostsNotFound(true); + } + }; + + fetchUserPosts(); + + const checkIfFollowing = async () => { + try { + const response = await fetch(config.apiUrl + `friendship/is-following-user`, { + method: 'POST', + headers: headersWithJwt, + body: JSON.stringify({ + requesterId: senderId, + receiverId: id + }) + }); + + if (response.ok) { + const isFollowing = await response.json(); + setFollow(isFollowing); + setFollowLoading(false); + console.log("Successfully checked following status"); + } else { + setFollow(false); + setFollowLoading(false); + console.error("Failed to check following status"); + } + } catch (error) { + setFollow(false); + setFollowLoading(false); + console.error("Failed to check following status: ", error); + } + }; + checkIfFollowing(); + }, [id, senderId]); + + const handleFollow = async (following: boolean) => { if (userId === null) return; + if (!following) { + try { + const response = await fetch(config.apiUrl + `friendship/follow-user`, { + method: 'POST', + headers: headersWithJwt, + body: JSON.stringify({ + requesterId: senderId, + receiverId: id + }) + }); - try { - const response = await fetch(config.apiUrl +`friendship/follow-user`, { - method: 'POST', - headers: headersWithJwt, - body: JSON.stringify({ - "requesterId": senderId, - "receiverId": id - }) - }); - - if (response.ok) { - console.log("Successfully followed the user"); - - setAmountFollower(prev => (prev !== null ? prev + 1 : 1)); - } else { - console.error("Failed to follow the user"); + if (response.ok) { + console.log("Successfully followed the user"); + setAmountFollower(prev => (prev !== null ? prev + 1 : 1)); + setFollow(true); + } else { + console.error("Failed to follow the user"); + } + } catch (error) { + console.error("Failed to follow the user: ", error); + } + } else { + try { + const response = await fetch(config.apiUrl + `friendship/unfollow-user`, { + method: 'POST', + headers: headersWithJwt, + body: JSON.stringify({ + requesterId: senderId, + receiverId: id + }) + }); + + if (response.ok) { + console.log("Successfully unfollowed the user"); + setAmountFollower(prev => (prev !== null ? prev - 1 : 0)); + setFollow(false); + } else { + console.error("Failed to unfollow the user"); + } + } catch (error) { + console.error("Failed to unfollow the user: ", error); } - } catch (error) { - console.error("Error following the user:", error); } + }; + + const FollowButton = () => { + if (userId === senderId) { + return
; + } else if (followLoading) { + return ( +
+
+ +
+
+ ); + } else if (follow) { + return ( + + ); + } else { + return ( + + ); + } + }; + + const DisplayPosts = () => { + if (postsNotFound) { + return ( +
+

This User doesn´t have Posts

+
+ ); + } + if (postLoading) { + return ( +
+
+
+ +
+
+
+ ); + } else { + return ( +
+
Posts by {username}:
+
+
+ {userPosts.map(post => ( + + ))} +
+
+
+ ); + } + }; + + if (notFound) { + return ( +
+
+
+

This User doesn´t exist

+
+
+
+ ); + } + + if (loading) { + return ( +
+
+
+
+
+ +
+
+
+
+
+ ); } return (
-
+
{picture.id !== null && picture.imageData ? ( - {"user"} + user ) : ( - {"random-image"} + random-image )}
@@ -94,28 +299,30 @@ export const UserPage = () => {
Followers: {amountFollower}
-
- {course !== null ? ( + {course !== null && ( +
Course: {course} - ) : ( - Course: Not set - )} -
-
- {age !== null ? ( +
) + } + + {age !== null && ( +
Age: {age} - ): ( - Age: Not set - )} -
-
- {description !== null ? ( - Description: {description} - ): ( - Description: Not set - )} +
+ )} + {description !== null && ( +
+ {description} +
+ ) + } + + +
+
+
+
-
diff --git a/src/main/web/src/services/UserPageService.tsx b/src/main/web/src/services/UserPageService.tsx index 96288afb..3aaef0fe 100644 --- a/src/main/web/src/services/UserPageService.tsx +++ b/src/main/web/src/services/UserPageService.tsx @@ -2,8 +2,17 @@ import config from "../config/config"; export const getUserIdByAccountId = async (accountId:number) => { + try { + const response = await fetch(config.apiUrl +`account/get-user-id/${accountId}`) - const response = await fetch(config.apiUrl +`account/get-user-id/${accountId}`); - return (response.json()); + if (response.ok) { + return (response.json()); + } else { + console.log(new Error("Failed to fetch User Id")); + } + } catch (error) { + console.error("Error when retrieving the user Id,"+ error) + + } };