Skip to content

Commit

Permalink
Merge pull request #551 from bounswe/feature/FE-annotation
Browse files Browse the repository at this point in the history
Add annotation and filter posts by tags
  • Loading branch information
mahmutbugramert authored Dec 16, 2024
2 parents f1cb4d8 + 0d07b00 commit 911d8d6
Show file tree
Hide file tree
Showing 7 changed files with 354 additions and 80 deletions.
41 changes: 41 additions & 0 deletions frontend/src/components/community/AnnotationUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
export const renderContentWithAnnotations = (content, annotations) => {
let annotatedContent = [];
let currentIndex = 0;

annotations
.sort((a, b) => a.start - b.start) // Ensure ascending order
.forEach(({ start, end, value, username, creationDate }, idx) => {
console.log(`Processing annotation ${idx}:`, { start, end, value });

// Add text before annotation
if (currentIndex < start) {
annotatedContent.push(
<span key={`plain-${idx}`}>{content.slice(currentIndex, start)}</span>
);
}

// Add the annotated text itself
if (start < end && end <= content.length) {
annotatedContent.push(
<span
key={`annotation-${idx}`}
className="annotated-text"
title={`${value}\nBy: ${username}\nOn: ${creationDate}`}
>
{content.slice(start, end)}
</span>
);
}

currentIndex = Math.max(currentIndex, end);
});

// Add the remaining content after last annotation
if (currentIndex < content.length) {
annotatedContent.push(
<span key="remainder">{content.slice(currentIndex)}</span>
);
}

return annotatedContent;
};
66 changes: 61 additions & 5 deletions frontend/src/components/community/CommunityPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,39 +13,81 @@ const CommunityPage = () => {
const [searchActive, setSearchActive] = useState(false);
const [posts, setPosts] = useState([]);
const [users, setUsers] = useState({});
const [tags, setTags] = useState([]);
const [selectedTags, setSelectedTags] = useState([]);
const navigate = useNavigate();

useEffect(() => {
const fetchPosts = async () => {
const fetchData = async () => {
try {
const response = await apiClient.get("/posts");
// Fetch posts and users
const postsResponse = await apiClient.get("/posts");
const usersResponse = await apiClient.get("/users");
const usersById = usersResponse.data.reduce((acc, user) => {
acc[user.id] = user.username;
return acc;
}, {});
setUsers(usersById);

// Fetch available tags
const tagsResponse = await apiClient.get("/tags");
setTags(tagsResponse.data);

// Process posts
const transformedPosts = await Promise.all(
response.data.map(async (post) => transformPost(post))
postsResponse.data.map(async (post) => {
return {
"post-id": post.id,
user: usersById[post.author] || "Unknown",
title: post.title,
content: [{ type: "plain-text", "plain-text": post.content }],
comments: [],
likes: post.liked_by?.length || 0,
tags: post.tags || [],
"publication-date": new Date(post.created_at),
};
})

);
setPosts(transformedPosts);
} catch (error) {
console.error("Error fetching posts:", error);
console.error("Error fetching posts or tags:", error);
}
};

fetchPosts();
fetchData();
}, []);

const filteredPosts = posts
.filter((post) =>
post.title.toLowerCase().includes(searchTerm.toLowerCase())
)
.filter((post) => {
if (selectedTags.length === 0) return true; // No tags selected, show all
const postTagIds = post.tags.map((tag) => tag.id);
if (selectedTags.length === 1) {
// If one tag is selected, check if it's in the post's tags
return postTagIds.includes(Number(selectedTags[0]));
} else {
// If multiple tags are selected, check if they are present as per the "and" or "or" condition
const hasAllTags = selectedTags.every((tagId) =>
postTagIds.includes(Number(tagId))
);
return hasAllTags; // For "and" filter (all tags must match)
}
})
.sort((a, b) => {
return sortOrder === "asc" ? a.likes - b.likes : b.likes - a.likes;
});

const handleTagSelection = (tagId) => {
setSelectedTags((prevSelectedTags) =>
prevSelectedTags.includes(tagId)
? prevSelectedTags.filter((id) => id !== tagId)
: [...prevSelectedTags, tagId]
);
};

const handleSubmitPost = () => {
navigate("/community/create-post");
};
Expand Down Expand Up @@ -83,6 +125,20 @@ const CommunityPage = () => {
</select>
</div>

<div className="tags-selection">
<h3>Select Tags</h3>
{tags.map((tag) => (
<label key={tag.id} className="tag-checkbox">
<input
type="checkbox"
checked={selectedTags.includes(tag.id.toString())}
onChange={() => handleTagSelection(tag.id.toString())}
/>
{tag.name}
</label>
))}
</div>

<div className="post-cards">
{filteredPosts.map((post) => (
<PostCard key={post["post-id"]} post={post} />
Expand Down
1 change: 0 additions & 1 deletion frontend/src/components/community/CreatePostPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,6 @@ const CreatePostPage = () => {
>
Cancel
</button>
<button className="preview-button">Preview</button>
<button className="post-button" onClick={handlePost}>
Post
</button>
Expand Down
3 changes: 1 addition & 2 deletions frontend/src/components/community/PostCard.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ const getColorForTag = (tag) => {

const PostCard = ({ post }) => {
const navigate = useNavigate();

const navigateToPost = (postId) => {
navigate(`/post/${postId}`);
};
Expand All @@ -38,7 +37,7 @@ const PostCard = ({ post }) => {
<FaHeart className="icon like-icon" /> {post.likes}
</span>
<span className="comments-box">
<FaComment className="icon comment-icon" /> {post.comments.length}
<FaComment className="icon comment-icon" />
</span>
</div>
<button
Expand Down
102 changes: 96 additions & 6 deletions frontend/src/components/community/PostView.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { apiClient } from "../../service/apiClient";
import CircleAnimation from "../CircleAnimation";
import NotFound from "../notfound/NotFound";
import UserService from "../../service/userService";
import { renderContentWithAnnotations } from "./AnnotationUtils";
import {
FaNewspaper,
FaImage,
Expand All @@ -23,18 +24,84 @@ const PostView = () => {
const [commentText, setCommentText] = useState("");
const [tags, setTags] = useState([]);
const [isLikedByUser, setIsLikedByUser] = useState(false);
const [annotations, setAnnotations] = useState([]);
const [usernameCache, setUsernameCache] = useState(new Map());
const [annotationsVisible, setAnnotationsVisible] = useState(true);

const getUserName = async (userID) => {
if (usernameCache.has(userID)) {
return usernameCache.get(userID);
}

try {
const userData = await apiClient.get(`/users/${userID}`);
const userName = userData.data.username;
setUsernameCache((prevCache) => new Map(prevCache).set(userID, userName)); // Update the cache
return userName;
} catch (error) {
console.error("Error fetching user name:", error);
return "Unknown";
}
};

const fetchAnnotations = async () => {
try {
const response = await apiClient.get(
`/annotations/post-annotations/${postId}`
);
console.log("Fetched annotations from API:", response.data);

const annotationsData = await Promise.all(
response.data.map(async (annotation) => {
const username = await getUserName(annotation.user_id); // Fetch username
const creationDate = new Date(
annotation.created_at
).toLocaleDateString(); // Format creation date
return {
...annotation,
username,
creationDate, // Store formatted creation date
};
})
);
setAnnotations(annotationsData);
} catch (error) {
console.error("Error fetching annotations:", error);
}
};

const handleTextSelection = async () => {
if (annotationsVisible) return; // Block annotation interaction when annotations are enabled.

const selection = window.getSelection();
if (selection && selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
const cleanText = range.toString().trim();
const cleanStart = range.startOffset + range.toString().search(/\S/);
const cleanEnd = cleanStart + cleanText.length;

if (cleanText) {
const note = prompt("Enter a note for the selected text:");
if (note) {
const annotationPayload = {
post_id: parseInt(postId, 10),
start: cleanStart,
end: cleanEnd,
value: note,
};

try {
await apiClient.post("/annotations/", annotationPayload);
setAnnotations((prev) => [...prev, annotationPayload]);
alert("Annotation added successfully!");
} catch (error) {
console.error("Error adding annotation:", error);
}
}
}
}
};

const getColorForTag = (tag) => {
const tagName = typeof tag === "string" ? tag : tag.name;
const asciiValue = tagName.charCodeAt(0);
Expand All @@ -54,7 +121,7 @@ const PostView = () => {
const commentsData = commentsResponse.data;

const backendComments = await Promise.all(
commentsData.map(async (comment) => {
commentsData.reverse().map(async (comment) => {
const username = await getUserName(comment.user_id);
return {
"comment-id": comment.id,
Expand Down Expand Up @@ -111,12 +178,12 @@ const PostView = () => {
setPost((prevPost) => ({
...prevPost,
comments: [
...prevPost.comments,
{
"comment-id": Date.now(), // Temporary ID until refreshed
user: username,
comment: commentText.trim(),
},
...prevPost.comments, // Prepend the new comment
],
}));
setCommentText("");
Expand Down Expand Up @@ -144,6 +211,12 @@ const PostView = () => {
}
};

useEffect(() => {
if (postId) {
fetchAnnotations();
}
}, [postId]);

if (loading) {
return (
<p>
Expand All @@ -161,9 +234,6 @@ const PostView = () => {
<div className="post-author">
<p>By: {post["user"]}</p>
<p>Published on: {post["publication-date"]}</p>
<button className="follow-button">
<FaUserPlus /> Follow Author
</button>
</div>
<div className="tags">
{tags.length > 0 ? (
Expand All @@ -184,6 +254,14 @@ const PostView = () => {
)}
</div>
<div className="post-content">
<button
style={{ color: "white" }}
className="toggle-annotations"
onClick={() => setAnnotationsVisible((prev) => !prev)}
>
{annotationsVisible ? "Disable Annotations" : "Enable Annotations"}
</button>

{post.content.map((content, index) => (
<div className="timeline-item" key={index}>
<span className="timeline-dot">
Expand All @@ -193,7 +271,19 @@ const PostView = () => {
{content.type === "graph" && <FaChartLine className="icon" />}
</span>
<div className="timeline-content">
{content.type === "plain-text" && <p>{content["plain-text"]}</p>}
{content.type === "plain-text" && (
<div
onMouseUp={!annotationsVisible ? handleTextSelection : null} // Disable text selection if annotationsVisible is true
className="annotated-content"
>
{annotationsVisible
? renderContentWithAnnotations(
content["plain-text"],
annotations
)
: content["plain-text"]}
</div>
)}
{content.type === "news" && (
<div className="news">
<a
Expand Down
Loading

0 comments on commit 911d8d6

Please sign in to comment.