diff --git a/backend/controllers/addBlogController.js b/backend/controllers/addBlogController.js index 232f2fcf..3497bde4 100644 --- a/backend/controllers/addBlogController.js +++ b/backend/controllers/addBlogController.js @@ -20,7 +20,7 @@ const upload = multer({ storage: storage }); // Function to save a new blog post export async function saveBlog(req, res) { try { - const { title, category, summary, excerpt, tags, publish, featuredImage } = req.body; + const { title, category, summary, excerpt, tags, publish, likes, featuredImage } = req.body; // If an image is uploaded, use its path let imagePath = req.file ? req.file.path : null; @@ -56,6 +56,7 @@ export async function saveBlog(req, res) { excerpt, tags, publish, + likes, featuredImage: imagePath // Use the determined image path }); @@ -104,4 +105,35 @@ export async function getBlog(req, res) { } } + + +export async function updateLikes(req, res) { + const { postId, liked } = req.body; // Destructure liked from the body + console.log(postId + " " + liked); + + try { + // Find the post by ID + const post = await BlogPost.findById(postId); + console.log(post); + + if (!post) { + return res.status(404).json({ message: "Post not found" }); + } + + // Update the likes count based on the "liked" value + post.likes = liked ? post.likes + 1 : Math.max(post.likes - 1, 0); // Update post.likes, not BlogPost.likes + + // Save the updated post + await post.save(); + + // Respond with the new likes count + return res.status(200).json({ likesCount: post.likes }); // Use post.likes + } catch (error) { + console.error("Error updating likes:", error); + return res.status(500).json({ message: "Failed to update likes" }); + } +} + + + export { upload }; \ No newline at end of file diff --git a/backend/models/addBlog.js b/backend/models/addBlog.js index 99c64c4d..334874e1 100644 --- a/backend/models/addBlog.js +++ b/backend/models/addBlog.js @@ -33,6 +33,10 @@ const blogPostSchema = new mongoose.Schema({ type: String, default: null }, + likes: { + type: Number, + default: 0 + }, createdAt: { type: Date, default: Date.now diff --git a/backend/routes/addBlogRoutes.js b/backend/routes/addBlogRoutes.js index 8524bd0a..52c549b9 100644 --- a/backend/routes/addBlogRoutes.js +++ b/backend/routes/addBlogRoutes.js @@ -1,9 +1,10 @@ import express from "express"; -import { getAllBlog, getBlog, saveBlog, upload } from "../controllers/addBlogController.js"; +import { getAllBlog, getBlog, saveBlog, upload, updateLikes } from "../controllers/addBlogController.js"; const router = express.Router(); router.post("/saveBlog", upload.single('featuredImage'), saveBlog); router.get("/getAllBlog", getAllBlog); +router.patch("/updateLikes", updateLikes); router.get("/getBlog/:id", getBlog); export default router; diff --git a/frontend/src/pages/AddBlog.js b/frontend/src/pages/AddBlog.js index d1098e8d..7741c509 100644 --- a/frontend/src/pages/AddBlog.js +++ b/frontend/src/pages/AddBlog.js @@ -95,6 +95,7 @@ export function renderAddBlog(container) { formData.append('excerpt', excerpt); formData.append('tags', tags); formData.append('publish', publish); + formData.append('likes', 0); if (featuredImage) { formData.append('featuredImage', featuredImage); // Ensure key matches backend } diff --git a/frontend/src/pages/Blogs.js b/frontend/src/pages/Blogs.js index 686047d3..86972301 100644 --- a/frontend/src/pages/Blogs.js +++ b/frontend/src/pages/Blogs.js @@ -86,10 +86,10 @@ function setupCategoryFilter(blogPosts) { } function renderBlogPosts(posts) { - return posts.map(post => renderBlogPost(post.id, post.title, post.excerpt, post.date, post.tags, post.featuredImage, post.publish)).join(''); + return posts.map(post => renderBlogPost(post._id, post.title, post.excerpt, post.createdAt, post.tags, post.featuredImage, post.publish, post.likes)).join(''); } -function renderBlogPost(id, title, excerpt, date, tags, imageUrl, publish) { +function renderBlogPost(id, title, excerpt, date, tags, imageUrl, publish, likes) { if (!publish) return ''; let imagePath = ""; if (imageUrl) { @@ -106,18 +106,91 @@ function renderBlogPost(id, title, excerpt, date, tags, imageUrl, publish) {
${tags} - + Read More
+
+ +
`; } +window.handleReaction = async function (postId) { + const reactionCountElement = document.getElementById(`reaction-count-${postId}`); + const heartIcon = document.getElementById(`heart-icon-${postId}`); + let currentCount = parseInt(reactionCountElement.innerText, 10); + + // Check if the heart is currently filled + const isFilled = heartIcon.getAttribute("fill") === "currentColor"; + + // Toggle the heart fill color and update count with animation + const newCount = isFilled ? currentCount - 1 : currentCount + 1; + heartIcon.classList.toggle("text-red-500", !isFilled); + heartIcon.setAttribute("fill", !isFilled ? "currentColor" : "none"); + reactionCountElement.innerText = newCount; + + // Add a quick scale animation for visual feedback + heartIcon.classList.add("scale"); + setTimeout(() => heartIcon.classList.remove("scale"), 150); + + // Send updated like count to the server + try { + const response = await fetch(`http://localhost:5000/api/addBlog/updateLikes`, { + method: "PATCH", // Change POST to PATCH + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ postId, liked: !isFilled }), // Ensure liked reflects the new state + }); + + if (!response.ok) { + throw new Error("Server error"); + } + + const data = await response.json(); + // Update the like count based on the server response + reactionCountElement.innerText = data.likesCount; // Ensure the server returns likesCount + } catch (error) { + console.error("Failed to update like on server:", error); + + // Revert the UI changes if the server update fails + heartIcon.classList.toggle("text-red-500", isFilled); + heartIcon.setAttribute("fill", isFilled ? "currentColor" : "none"); + reactionCountElement.innerText = currentCount; // Revert to previous count + } +}; + + + + + function renderSearchWidget() { return `
diff --git a/frontend/src/styles/output.css b/frontend/src/styles/output.css index 9e0a0e55..b889691d 100644 --- a/frontend/src/styles/output.css +++ b/frontend/src/styles/output.css @@ -694,6 +694,10 @@ video { margin-top: 2rem; } +.mt-\[6px\] { + margin-top: 6px; +} + .block { display: block; } @@ -1073,6 +1077,10 @@ video { background-color: rgb(254 249 195 / var(--tw-bg-opacity)); } +.fill-current { + fill: currentColor; +} + .object-cover { -o-object-fit: cover; object-fit: cover; @@ -1221,11 +1229,6 @@ video { line-height: 1.75rem; } -.text-xs { - font-size: 0.75rem; - line-height: 1rem; -} - .font-bold { font-weight: 700; } @@ -1242,10 +1245,6 @@ video { font-weight: 600; } -.font-normal { - font-weight: 400; -} - .leading-relaxed { line-height: 1.625; } @@ -1408,6 +1407,18 @@ video { transition-duration: 150ms; } +.transition-all { + transition-property: all; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.transition-transform { + transition-property: transform; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + .duration-150 { transition-duration: 150ms; } @@ -1416,6 +1427,10 @@ video { transition-duration: 300ms; } +.duration-200 { + transition-duration: 200ms; +} + .ease-in-out { transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); } @@ -1508,6 +1523,16 @@ body.dark-mode { color: rgb(255 255 255 / var(--tw-text-opacity)); } +.hover\:text-red-600:hover { + --tw-text-opacity: 1; + color: rgb(220 38 38 / var(--tw-text-opacity)); +} + +.hover\:text-red-500:hover { + --tw-text-opacity: 1; + color: rgb(239 68 68 / var(--tw-text-opacity)); +} + .hover\:underline:hover { text-decoration-line: underline; } @@ -1772,6 +1797,11 @@ body.dark-mode { color: rgb(255 255 255 / var(--tw-text-opacity)); } +.dark\:hover\:text-red-400:hover:is(.dark *) { + --tw-text-opacity: 1; + color: rgb(248 113 113 / var(--tw-text-opacity)); +} + .dark\:focus\:ring-gray-600:focus:is(.dark *) { --tw-ring-opacity: 1; --tw-ring-color: rgb(75 85 99 / var(--tw-ring-opacity));