Skip to content

Commit

Permalink
Merge pull request #89 from JimTheCat/CU-869768m7p_Connect-PostsMenu-…
Browse files Browse the repository at this point in the history
…with-backend_Patryk-Kosiski

feature: Connect posts fully with backend
  • Loading branch information
JimTheCat authored Dec 23, 2024
2 parents 2fe0204 + 77f4cf9 commit 13179db
Show file tree
Hide file tree
Showing 12 changed files with 184 additions and 79 deletions.
2 changes: 1 addition & 1 deletion frontend/src/Features/CreatePost/CreatePost.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const CreatePost = () => {
return;
}

api.post('/api/posts/create', null, {params: {content: contentToSave}}).then((response) => {
api.post('/api/posts', null, {params: {content: contentToSave}}).then((response) => {
if (response.status === 200) {
close();
}
Expand Down
78 changes: 54 additions & 24 deletions frontend/src/Features/MainPage/MainPage.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,64 @@
import {Box, Title} from "@mantine/core";
import {useEffect, useState} from "react";
import {PostDTO} from "./types/Post.tsx";
import api from "../shared/services/api.ts";
import {Box, Stack} from "@mantine/core";
import {InfiniteScroll} from "./components/InfiniteScroll";
import {Post} from "../shared/components/Cards/Post";
import {PostDTO} from "./types/Post.tsx";

export const MainPage = () => {
const [posts, setPosts] = useState<PostDTO[]>([]);
const [page, setPage] = useState(0);
const [hasMore, setHasMore] = useState(true);
const [loading, setLoading] = useState(false);

const loadMorePosts = async () => {
if (loading || !hasMore) return;

const [posts, setPosts] = useState<PostDTO[] | []>([]);
setLoading(true);
try {
const response = await api.get(`/api/posts`, {
params: {pageNo: page, pageSize: 3},
});

const data = response.data;

setPosts((prevPosts) => [...prevPosts, ...data.content]);
setHasMore(data.totalPages > page);
setPage((prevPage) => prevPage + 1);
} catch (error) {
console.error("Error loading posts:", error);
} finally {
setLoading(false);
}
};

useEffect(() => {
api.get<PostDTO[]>("/api/posts/get-all").then((response) => {
setPosts(response.data);
});
// Initial load
loadMorePosts();
}, []);

return (
<Box p={"md"}>
<Title order={1}>Main page</Title>

{/*Posts*/}
{posts.map((post) => (
<Post key={post.ownerLogin} ownerLogin={post.ownerLogin} contentHtml={post.content} createdAt={post.createdAt}
photosUrls={
// generate 100 random photos
Array.from({length: 100}, () => {
return "https://picsum.photos/seed/" + Math.random() + "/800/2200";
})
}
/>
))}
</Box>
)
}
return (
<Box p="lg">
<InfiniteScroll loadMore={loadMorePosts} hasMore={hasMore} loading={loading}>
<Stack align={"center"} gap="lg">
{posts.map((post) => (
<Post
key={post.id}
id={post.id}
author={post.author}
content={post.content}
createdAt={post.createdAt}
numberOfComments={post.numberOfComments}
photosUrls={
// generate 100 random photos
Array.from({length: 100}, () => {
return "https://picsum.photos/seed/" + Math.random() + "/800/2200";
})
}
/>
))}
</Stack>
</InfiniteScroll>
</Box>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import React, {useEffect, useRef} from "react";
import {Box, Loader, Text} from "@mantine/core";

interface InfiniteScrollProps {
loadMore: () => Promise<void>;
hasMore: boolean;
loading: boolean;
children: React.ReactNode;
}

export const InfiniteScroll: React.FC<InfiniteScrollProps> = ({
loadMore,
hasMore,
loading,
children,
}) => {
const sentinelRef = useRef<HTMLDivElement | null>(null);
const observer = useRef<IntersectionObserver | null>(null);

useEffect(() => {
// IntersectionObserver callback
const handleIntersection: IntersectionObserverCallback = (entries) => {
const [entry] = entries;
if (entry.isIntersecting && hasMore && !loading) {
loadMore();
}
};

if (sentinelRef.current) {
observer.current = new IntersectionObserver(handleIntersection, {
root: null,
rootMargin: "100px", // Load earlier when close to the bottom
threshold: 0.1, // Trigger when sentinel is at least 10% visible
});

observer.current.observe(sentinelRef.current);
}

return () => {
if (observer.current && sentinelRef.current) {
observer.current.unobserve(sentinelRef.current);
}
};
}, [hasMore, loading, loadMore]);

return (
<Box style={{position: "relative"}}>
{children}
{/* Sentinel element */}
<div ref={sentinelRef} style={{height: 1, visibility: "hidden"}}/>
{/* Loading indicator */}
{loading && (
<Box style={{textAlign: "center", margin: "20px 0"}}>
<Loader size="md"/>
</Box>
)}
{/* End of data */}
{!hasMore && !loading && (
<Text ta="center" c="dimmed" mt="lg">
No more posts ಥ_ಥ
</Text>
)}
</Box>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {InfiniteScroll} from './InfiniteScroll';
17 changes: 13 additions & 4 deletions frontend/src/Features/MainPage/types/Post.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
export type PostDTO = {
content: string;
ownerLogin: string;
createdAt: string;
}
id: string,
content: string,
createdAt: string,
numberOfComments: number,
author: {
id: string;
name: string;
surname: string;
login: string;
profilePicture: string | null;
},
photosUrls?: string[]
};
2 changes: 1 addition & 1 deletion frontend/src/Features/Root/components/Navbar/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const Navbar = () => {

useEffect(() => {
if (auth.user) {
api.get<BasicUserInfo>('/api/users/get-basic-user-info', {params: {login: auth.user.login}}).then((response) => {
api.get<BasicUserInfo>('/api/users/basic-user-info', {params: {login: auth.user.login}}).then((response) => {
setBasicUserInfo(response.data);
});
}
Expand Down
51 changes: 27 additions & 24 deletions frontend/src/Features/shared/components/Cards/Post/Post.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,29 @@
import {Avatar, BackgroundImage, Box, Button, Card, Divider, Group, SimpleGrid, Stack, Text} from "@mantine/core";
import {User} from "../../../types/User.tsx";
import {
Avatar,
BackgroundImage,
Badge,
Box,
Button,
Card,
Divider,
Group,
SimpleGrid,
Stack,
Text
} from "@mantine/core";
import {InnerHtmlHandler} from "../../InnerHtmlHandler";
import {DateFormatter} from "../../../utils/DateFormatter.tsx";
import {IconMessage, IconPaw, IconShare3} from "@tabler/icons-react";
import {useAuthStore} from "../../../services/authStore.ts";
import {useNavigate} from "react-router-dom";
import {ImageWithSkeleton} from "../../ImageWithSkeleton";
import {MenuPost} from "./components/MenuPost";
import {PostDTO} from "../../../../MainPage/types/Post.tsx";

type PostProps = {
ownerLogin: string;
contentHtml: string;
photosUrls?: string[];
createdAt: string;
}

export const Post = (props: PostProps) => {
export const Post = (props: PostDTO) => {

const auth = useAuthStore();
const isOwner = auth.user?.login === props.ownerLogin;
const isOwner = auth.user?.login === props.author.login;
const navigate = useNavigate();

/*Render this element each time when number of photos will change*/
Expand Down Expand Up @@ -92,7 +97,6 @@ export const Post = (props: PostProps) => {
>
<Text
size="xl"
w={700}
c="white"
ta="center"
style={{
Expand All @@ -118,29 +122,23 @@ export const Post = (props: PostProps) => {
);
};

const user = {
name: "John",
surname: "Doe",
login: "johndoe",
} as User;

return (
<Card w={"30vw"} radius={"md"} p={"lg"} my={'lg'}>
<Card w={"30vw"} radius={"md"} p={"lg"}>
<Stack>
<Group justify="space-between">
<Group onClick={() => navigate(`/profile/${auth.user?.tag}`)} style={{cursor: "pointer"}}>
<Group onClick={() => navigate(`/profile/@${props.author.login}`)} style={{cursor: "pointer"}}>
<Avatar src={null} size={"lg"} radius={180}/>
<Stack gap={0}>
<Text>{user.name} {user.surname}</Text>
<Text>{props.author.name} {props.author.surname}</Text>
<Text c="dimmed">{DateFormatter(props.createdAt)}</Text>
</Stack>
</Group>
{isOwner &&
<MenuPost content={props.contentHtml}/>
<MenuPost postId={props.id} content={props.content}/>
}
</Group>

<InnerHtmlHandler innerHtml={props.contentHtml}/>
<InnerHtmlHandler innerHtml={props.content}/>

{/*// Show photos if they exist*/}
{props.photosUrls && props.photosUrls.length > 0 && (
Expand All @@ -155,7 +153,12 @@ export const Post = (props: PostProps) => {
<Button variant={"subtle"} color={"gray"} leftSection={<IconPaw stroke={1.5}/>}>
Reaction
</Button>
<Button variant={"subtle"} color={"gray"} leftSection={<IconMessage stroke={1.5}/>}>
<Button
variant={"subtle"}
color={"gray"}
leftSection={<IconMessage stroke={1.5}/>}
rightSection={<Badge color={"gray"} circle>{props.numberOfComments}</Badge>}
>
Comment
</Button>
<Button variant={"subtle"} color={"gray"} leftSection={<IconShare3 stroke={1.5}/>}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {IconPencil} from "@tabler/icons-react";
import {Menu, rem} from "@mantine/core";
import {useRef, useState} from "react";
import {ModalRichContent} from "../../../../../consts";
import api from "../../../../../services/api.ts";

type EditPostProps = {
content?: string;
Expand All @@ -20,8 +21,7 @@ export const EditPost = (props: EditPostProps) => {
}

const handleEditPost = () => {
// TODO: Replace with actual edit logic
console.log('Edit post: ', contentRef.current);
api.put(`/api/posts/${props.postId}`, null, {params: {content: contentRef.current}});
};

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {EditPost} from "../EditPost";
import {RemovePost} from "../RemovePost";

type MenuPostProps = {
postId?: string;
content?: string;
}

Expand All @@ -17,8 +18,8 @@ export const MenuPost = (props: MenuPostProps) => {
</Menu.Target>
<Menu.Dropdown>
<Menu.Label>Manage</Menu.Label>
<EditPost content={props.content}/>
<RemovePost/>
<EditPost postId={props.postId} content={props.content}/>
<RemovePost postId={props.postId}/>
</Menu.Dropdown>
</Menu>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import {Menu, rem, Text} from "@mantine/core";
import {IconTrash} from "@tabler/icons-react";
import {ModificationModal} from "../../../../ModificationModal";
import api from "../../../../../services/api.ts";

type RemovePostProps = {
postId?: string;
}

export const RemovePost = (props: RemovePostProps) => {
const handleRemoval = () => {
// TODO: Replace with actual removal logic
console.log('Remove post: ', props.postId);
api.delete(`/api/posts/${props.postId}`);
};

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,11 @@ export const ImageWithSkeleton = (props: {
radius: number | MantineRadius,
style?: MantineStyleProp
}) => {

const [isImageLoaded, setIsImageLoaded] = useState(false);

return (
<>
{!isImageLoaded && <Skeleton height={400} radius={props.radius}/>}
{!isImageLoaded && <Skeleton style={props.style} radius={props.radius}/>}
<Image
src={props.src}
alt={props.alt}
Expand Down
Loading

0 comments on commit 13179db

Please sign in to comment.